Add Invoke-SecureBootRemediation.ps1
This commit is contained in:
@@ -0,0 +1,698 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detekce + interaktivní remediace Secure Boot certifikátů na jednom serveru (KB5062710 / KB5068202).
|
||||
|
||||
.DESCRIPTION
|
||||
Sloučení detekčního (Invoke-SecureBootAudit.ps1) a remediačního
|
||||
(Set-SecureBootCertificateUpdate.ps1) skriptu do jednoho lokálního průvodce:
|
||||
|
||||
1. DETEKCE — zjistí prostředí, stav Secure Boot, přítomnost expirujících 2011
|
||||
a nových 2023 certifikátů, stav v registrech a Event Logu.
|
||||
2. ROZHODNUTÍ — vyhodnotí, zda lze remediaci aplikovat. Pokud ano, ZEPTÁ SE uživatele.
|
||||
3. REMEDIACE — pouze po souhlasu. Kroky probíhají SEKVENČNĚ; každý čeká na dokončení
|
||||
předchozího (zápis registry se ověří read-backem, na servicing task se ČEKÁ).
|
||||
4. DALŠÍ KROKY — skript vypíše, co je třeba udělat dál.
|
||||
|
||||
Skript ZÁMĚRNĚ NERESTARTUJE server. Restart je nutný pro dokončení aktualizace,
|
||||
ale necháváme ho na plánovaném okně správce.
|
||||
|
||||
Skript je určen k LOKÁLNÍMU spuštění na serveru (jako Administrator) nebo uvnitř
|
||||
interaktivního Enter-PSSession. Pro hromadný/neinteraktivní audit flotily použijte
|
||||
původní Start-SecureBootFleetAudit.ps1 / Set-SecureBootCertificateUpdate.ps1.
|
||||
|
||||
.PARAMETER AssumeYes
|
||||
Přeskočí interaktivní dotaz a remediaci rovnou aplikuje (pokud je smysluplná).
|
||||
Vhodné pro neinteraktivní běh. Restart se stále NEPROVÁDÍ.
|
||||
|
||||
.PARAMETER Force
|
||||
Aplikuje remediaci i ve stavech, kdy ji skript jinak nedoporučuje
|
||||
(firmware nezpůsobilý, již úspěšně dokončeno, Hyper-V Event 1795).
|
||||
|
||||
.PARAMETER SkipScheduledTask
|
||||
Nastaví registry, ale nespustí servicing task. Task se spustí sám
|
||||
při příštím plánovaném běhu (cca každých 12 h).
|
||||
|
||||
.PARAMETER Detailed
|
||||
Vypíše rozšířený detekční rozpis (jednotlivé certifikáty, posledních N událostí).
|
||||
|
||||
.PARAMETER PassThru
|
||||
Vrátí výsledný objekt (detekce + případná remediace) do pipeline.
|
||||
|
||||
.PARAMETER LogPath
|
||||
Cesta k logu remediace. Default: log se vytvoří vedle skriptu pouze pokud se remediace
|
||||
skutečně provede. Zadáním cesty vynutíte logování od začátku.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-SecureBootRemediation.ps1
|
||||
Interaktivní průběh — detekce, dotaz, případná remediace.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-SecureBootRemediation.ps1 -WhatIf
|
||||
Pouze detekce + ukázka, co by remediace udělala. Žádné změny.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-SecureBootRemediation.ps1 -AssumeYes
|
||||
Neinteraktivně aplikuje remediaci, pokud je smysluplná (bez restartu).
|
||||
|
||||
.NOTES
|
||||
Reference: KB5062710, KB5068202, KB5085046, KB5085790.
|
||||
AvailableUpdates = 0x5944 (KEK + UEFI CA + Windows UEFI CA + Boot Manager) dle KB5068202.
|
||||
Vyžaduje Administrator pro čtení UEFI databází a zápis do registry.
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
|
||||
param(
|
||||
[switch]$AssumeYes,
|
||||
[switch]$Force,
|
||||
[switch]$SkipScheduledTask,
|
||||
[switch]$Detailed,
|
||||
[switch]$PassThru,
|
||||
[string]$LogPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
Set-StrictMode -Off
|
||||
|
||||
# ── Konzole: vynutit UTF-8 výstup, aby diakritika fungovala i ve Windows PowerShell 5.1 ──
|
||||
# Zdroják je uložen jako UTF-8 s BOM (PS 5.1 tak čte řetězce správně); tady sjednotíme i výstup.
|
||||
# Při přesměrovaném výstupu (Invoke-Command, pipe do souboru) může vyhodit chybu → try/catch.
|
||||
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 # KB5068202: KEK + UEFI CA + Windows UEFI CA + Boot Manager
|
||||
$TASK_PATH = '\Microsoft\Windows\PI\'
|
||||
$TASK_NAME = 'Secure-Boot-Update'
|
||||
$TASK_TIMEOUT_SEC = 180
|
||||
|
||||
#endregion
|
||||
|
||||
#region ── Log / výpis ────────────────────────────────────────────────────────
|
||||
|
||||
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
|
||||
$script:LogFile = if ($LogPath) { $LogPath } else {
|
||||
Join-Path $scriptDir ("SecureBootRemediation-{0}-{1}.log" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
}
|
||||
$script:LogActive = [bool]$LogPath # při explicitním -LogPath logujeme od začátku
|
||||
|
||||
function Add-LogLine {
|
||||
param([string]$Text)
|
||||
if (-not $script:LogActive) { return }
|
||||
try { ('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Text) |
|
||||
Out-File -FilePath $script:LogFile -Append -Encoding UTF8 -Force } catch { }
|
||||
}
|
||||
|
||||
function Write-Line {
|
||||
# Stručný výpis na konzoli + (volitelně) do logu.
|
||||
param([string]$Text = '', [string]$Color = 'Gray', [switch]$NoLog)
|
||||
if ($Color) { Write-Host $Text -ForegroundColor $Color } else { Write-Host $Text }
|
||||
if (-not $NoLog) { Add-LogLine $Text }
|
||||
}
|
||||
|
||||
function Write-Rule { Write-Host ('=' * 62) -ForegroundColor DarkCyan }
|
||||
|
||||
#endregion
|
||||
|
||||
#region ── Detekce (z Invoke-SecureBootAudit.ps1) ─────────────────────────────
|
||||
|
||||
function Get-EnvironmentType {
|
||||
try {
|
||||
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
||||
$mfr = [string]$cs.Manufacturer
|
||||
$model = [string]$cs.Model
|
||||
if ($mfr -like '*VMware*') { return 'VMware VM' }
|
||||
if ($mfr -like '*Microsoft*' -and $model -eq 'Virtual Machine') { return 'Hyper-V VM' }
|
||||
if ($model -like '*Virtual*' -or $mfr -like '*QEMU*' -or $mfr -like '*Xen*') { return 'Other VM' }
|
||||
return 'Physical'
|
||||
} catch { return 'Unknown' }
|
||||
}
|
||||
|
||||
function Get-HardwareInfo {
|
||||
$hw = [ordered]@{ Manufacturer='Unknown'; Model='Unknown'; SerialNumber='Unknown'
|
||||
FirmwareType='Unknown'; BiosVersion='Unknown'; BiosReleaseDate='Unknown' }
|
||||
try {
|
||||
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
|
||||
$hw.Manufacturer = [string]$cs.Manufacturer
|
||||
$hw.Model = [string]$cs.Model
|
||||
} catch { }
|
||||
try {
|
||||
$bios = Get-CimInstance Win32_BIOS -ErrorAction Stop
|
||||
$hw.BiosVersion = [string]$bios.SMBIOSBIOSVersion
|
||||
$hw.SerialNumber = [string]$bios.SerialNumber
|
||||
if ($bios.ReleaseDate) { $hw.BiosReleaseDate = ([datetime]$bios.ReleaseDate).ToString('yyyy-MM-dd') }
|
||||
} catch { }
|
||||
$fwTypeKey = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction SilentlyContinue
|
||||
if ($fwTypeKey) {
|
||||
$hw.FirmwareType = if ($fwTypeKey.PEFirmwareType -eq 2) { 'UEFI' } else { 'Legacy BIOS' }
|
||||
} elseif (Test-Path $REG_SECUREBOOT) { $hw.FirmwareType = 'UEFI' } else { $hw.FirmwareType = 'Legacy BIOS' }
|
||||
return $hw
|
||||
}
|
||||
|
||||
function Get-SecureBootState {
|
||||
$state = [ordered]@{ IsUEFI=$false; IsSupported=$false; IsEnabled=$false; ConfirmResult='Unknown'; Error=$null }
|
||||
$fwTypeKey = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction SilentlyContinue
|
||||
$state.IsUEFI = ($fwTypeKey -and $fwTypeKey.PEFirmwareType -eq 2) -or (Test-Path $REG_SECUREBOOT)
|
||||
if (-not $state.IsUEFI) { return $state }
|
||||
try {
|
||||
$result = Confirm-SecureBootUEFI -ErrorAction Stop
|
||||
$state.IsSupported = $true; $state.IsEnabled = [bool]$result; $state.ConfirmResult = $result.ToString()
|
||||
} catch {
|
||||
$msg = $_.Exception.Message
|
||||
if ($msg -like '*not supported*' -or $msg -like '*Cmdlet not supported*') {
|
||||
$state.IsSupported = $false; $state.ConfirmResult = 'NotSupported'
|
||||
} elseif ($msg -like '*disabled*') {
|
||||
$state.IsSupported = $true; $state.IsEnabled = $false; $state.ConfirmResult = 'Disabled'
|
||||
} else {
|
||||
$state.IsSupported = $true; $state.IsEnabled = $false; $state.ConfirmResult = 'Error'; $state.Error = $msg
|
||||
}
|
||||
}
|
||||
return $state
|
||||
}
|
||||
|
||||
function Parse-EFISignatureList {
|
||||
param([byte[]]$Bytes)
|
||||
$certs = @()
|
||||
if (-not $Bytes -or $Bytes.Length -lt 28) { return $certs }
|
||||
$X509_GUID = [byte[]](0xa1,0x59,0xc0,0xa5, 0xe4,0x94, 0xa7,0x4a, 0x87,0xb5,0xab,0x15,0x5c,0x2b,0xf0,0x72)
|
||||
$offset = 0
|
||||
while ($offset + 28 -le $Bytes.Length) {
|
||||
$sigTypeGUID = $Bytes[$offset..($offset + 15)]
|
||||
$sigListSize = [BitConverter]::ToUInt32($Bytes, $offset + 16)
|
||||
$sigHeaderSize = [BitConverter]::ToUInt32($Bytes, $offset + 20)
|
||||
$sigSize = [BitConverter]::ToUInt32($Bytes, $offset + 24)
|
||||
if ($sigListSize -lt 28 -or $sigListSize -gt ($Bytes.Length - $offset)) { break }
|
||||
$isX509 = $true
|
||||
for ($i = 0; $i -lt 16; $i++) { if ($sigTypeGUID[$i] -ne $X509_GUID[$i]) { $isX509 = $false; break } }
|
||||
if ($isX509 -and $sigSize -gt 16) {
|
||||
$sigOffset = $offset + 28 + $sigHeaderSize
|
||||
$listEnd = $offset + $sigListSize
|
||||
while ($sigOffset + $sigSize -le $listEnd) {
|
||||
$certOffset = $sigOffset + 16
|
||||
$certSize = [int]$sigSize - 16
|
||||
if ($certOffset + $certSize -le $Bytes.Length -and $certSize -gt 0) {
|
||||
$certBytes = $Bytes[$certOffset..($certOffset + $certSize - 1)]
|
||||
try { $certs += New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(, [byte[]]$certBytes) } catch { }
|
||||
}
|
||||
$sigOffset += $sigSize
|
||||
}
|
||||
}
|
||||
$offset += $sigListSize
|
||||
}
|
||||
return $certs
|
||||
}
|
||||
|
||||
function Convert-CertToInfo {
|
||||
param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert)
|
||||
return [ordered]@{ Subject=$Cert.Subject; Thumbprint=$Cert.Thumbprint
|
||||
NotBefore=$Cert.NotBefore.ToString('yyyy-MM-dd'); NotAfter=$Cert.NotAfter.ToString('yyyy-MM-dd') }
|
||||
}
|
||||
|
||||
function Get-CertificateStatus {
|
||||
$status = [ordered]@{
|
||||
KEK = [ordered]@{ Has2011=$false; Has2023=$false; Certs2011=@(); Certs2023=@(); Error=$null }
|
||||
DB = [ordered]@{ Has2011UEFI=$false; Has2011WindowsPCA=$false; Has2023UEFI=$false
|
||||
Has2023OptionROM=$false; Has2023WindowsUEFI=$false; Certs2011=@(); Certs2023=@(); Error=$null }
|
||||
AnyExpiring2011=$false; AllReplacements2023=$false; AllReplacements2023_VM=$false
|
||||
}
|
||||
try {
|
||||
$kekObj = Get-SecureBootUEFI -Name KEK -ErrorAction Stop
|
||||
$kekCerts = Parse-EFISignatureList -Bytes $kekObj.Bytes
|
||||
foreach ($cert in $kekCerts) {
|
||||
$subj = $cert.Subject; $info = Convert-CertToInfo $cert
|
||||
if ($subj -like '*KEK CA 2011*') { $status.KEK.Has2011 = $true; $status.KEK.Certs2011 += $info }
|
||||
if ($subj -like '*KEK 2K CA 2023*' -or ($subj -like '*KEK*CA 2023*')) { $status.KEK.Has2023 = $true; $status.KEK.Certs2023 += $info }
|
||||
}
|
||||
} catch { $status.KEK.Error = $_.Exception.Message }
|
||||
try {
|
||||
$dbObj = Get-SecureBootUEFI -Name db -ErrorAction Stop
|
||||
$dbCerts = Parse-EFISignatureList -Bytes $dbObj.Bytes
|
||||
foreach ($cert in $dbCerts) {
|
||||
$subj = $cert.Subject; $info = Convert-CertToInfo $cert
|
||||
if ($subj -like '*UEFI CA 2011*') { $status.DB.Has2011UEFI = $true; $status.DB.Certs2011 += $info }
|
||||
if ($subj -like '*Windows Production PCA 2011*' -or $subj -like '*Windows PCA 2011*') { $status.DB.Has2011WindowsPCA = $true; $status.DB.Certs2011 += $info }
|
||||
if ($subj -like '*UEFI CA 2023*' -and $subj -notlike '*Option ROM*' -and $subj -notlike '*Windows UEFI*') { $status.DB.Has2023UEFI = $true; $status.DB.Certs2023 += $info }
|
||||
if ($subj -like '*Option ROM UEFI CA 2023*') { $status.DB.Has2023OptionROM = $true; $status.DB.Certs2023 += $info }
|
||||
if ($subj -like '*Windows UEFI CA 2023*') { $status.DB.Has2023WindowsUEFI = $true; $status.DB.Certs2023 += $info }
|
||||
}
|
||||
} catch { $status.DB.Error = $_.Exception.Message }
|
||||
$status.AnyExpiring2011 = $status.KEK.Has2011 -or $status.DB.Has2011UEFI -or $status.DB.Has2011WindowsPCA
|
||||
$status.AllReplacements2023 = $status.KEK.Has2023 -and $status.DB.Has2023UEFI -and $status.DB.Has2023OptionROM -and $status.DB.Has2023WindowsUEFI
|
||||
$status.AllReplacements2023_VM = $status.KEK.Has2023 -and $status.DB.Has2023WindowsUEFI
|
||||
return $status
|
||||
}
|
||||
|
||||
function Get-RegistryStatus {
|
||||
$reg = [ordered]@{
|
||||
SecureBootEnabled=$null; AvailableUpdates=$null; HighConfidenceOptOut=$null
|
||||
ServicingKeyExists=$false; UEFICA2023Status=$null; UEFICA2023StatusText='KeyNotPresent'
|
||||
UEFICA2023Error=$null; WindowsUEFICA2023Capable=$null
|
||||
}
|
||||
$mainProps = Get-ItemProperty $REG_SECUREBOOT -ErrorAction SilentlyContinue
|
||||
if ($mainProps) {
|
||||
$reg.SecureBootEnabled = $mainProps.SecureBootEnabled
|
||||
$reg.AvailableUpdates = $mainProps.AvailableUpdates
|
||||
$reg.HighConfidenceOptOut = $mainProps.HighConfidenceOptOut
|
||||
}
|
||||
$svcProps = Get-ItemProperty $REG_SERVICING -ErrorAction SilentlyContinue
|
||||
if ($svcProps) {
|
||||
$reg.ServicingKeyExists = $true
|
||||
$reg.UEFICA2023Status = $svcProps.UEFICA2023Status
|
||||
$reg.UEFICA2023Error = $svcProps.UEFICA2023Error
|
||||
$reg.WindowsUEFICA2023Capable = $svcProps.WindowsUEFICA2023Capable
|
||||
$reg.UEFICA2023StatusText = switch ($reg.UEFICA2023Status) {
|
||||
0 { 'NotStarted' } 1 { 'InProgress' } 2 { 'Success' } 3 { 'Failed' }
|
||||
$null { 'KeyNotPresent' } default { "Unknown ($($reg.UEFICA2023Status))" }
|
||||
}
|
||||
}
|
||||
return $reg
|
||||
}
|
||||
|
||||
function Get-EventLogStatus {
|
||||
$evtStatus = [ordered]@{ LastEventId=$null; LastEventTime=$null; ConfidenceLevel='NoRelevantEvents'; RelevantEvents=@(); Error=$null }
|
||||
$relevantIds = @(1795,1796,1800,1801,1802,1803,1808)
|
||||
try {
|
||||
$events = Get-WinEvent -FilterHashtable @{ LogName='System'; Id=$relevantIds } -MaxEvents 20 -ErrorAction Stop
|
||||
if ($events) {
|
||||
$sorted = $events | Sort-Object TimeCreated -Descending
|
||||
foreach ($evt in ($sorted | Select-Object -First 8)) {
|
||||
$evtStatus.RelevantEvents += [ordered]@{
|
||||
EventId=$evt.Id; TimeCreated=$evt.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'); Level=$evt.LevelDisplayName }
|
||||
}
|
||||
$last = $sorted | Select-Object -First 1
|
||||
$evtStatus.LastEventId = $last.Id
|
||||
$evtStatus.LastEventTime = $last.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
|
||||
$evtStatus.ConfidenceLevel = switch ($last.Id) {
|
||||
1808 { 'HighConfidence-Success' } 1801 { 'HighConfidence-Failed' }
|
||||
1795 { 'Failed-HyperVKnownIssue' } 1802 { 'Pending' } 1803 { 'InProgress' } default { 'Informational' } }
|
||||
}
|
||||
} catch {
|
||||
if ($_.CategoryInfo.Reason -eq 'NoMatchingEventsException') { $evtStatus.ConfidenceLevel = 'NoRelevantEvents' }
|
||||
else { $evtStatus.Error = $_.Exception.Message; $evtStatus.ConfidenceLevel = 'EventLogError' }
|
||||
}
|
||||
return $evtStatus
|
||||
}
|
||||
|
||||
function Get-RemediationCategory {
|
||||
param($Result)
|
||||
$sb = $Result.SecureBoot; $cert = $Result.Certificates; $reg = $Result.Registry; $env = $Result.EnvironmentType
|
||||
if (-not $sb.IsUEFI -or -not $sb.IsSupported) {
|
||||
if ($env -like '*VM*') { return @{ Code='NO_SECUREBOOT_VM'; Emoji='[X]'; Label='Secure Boot nepodporováno (VM bez vTPM/UEFI)' } }
|
||||
return @{ Code='NO_SECUREBOOT'; Emoji='[X]'; Label='Secure Boot nepodporováno (Legacy BIOS)' }
|
||||
}
|
||||
if (-not $sb.IsEnabled) { return @{ Code='SECUREBOOT_DISABLED'; Emoji='[OFF]'; Label='Secure Boot vypnuto' } }
|
||||
$isVM = $env -like '*VM*'
|
||||
$certOK = if ($isVM) { $cert.AllReplacements2023_VM } else { $cert.AllReplacements2023 }
|
||||
$certOKFull = $cert.AllReplacements2023
|
||||
if ($certOK -and -not $cert.AnyExpiring2011) {
|
||||
$lbl = if ($isVM -and -not $certOKFull) { 'OK — má povinné 2023 certifikáty pro VM' } else { 'OK — má nové 2023 certifikáty' }
|
||||
return @{ Code='OK'; Emoji='[OK]'; Label=$lbl }
|
||||
}
|
||||
if ($certOK -and $cert.AnyExpiring2011) {
|
||||
$lbl = if ($isVM -and -not $certOKFull) { 'OK — přechodný stav, VM má povinné 2023 certifikáty' } else { 'OK — přechodný stav (2023 i 2011 certifikáty)' }
|
||||
return @{ Code='OK_TRANSITION'; Emoji='[OK]'; Label=$lbl }
|
||||
}
|
||||
if ($reg.UEFICA2023Status -eq 3 -or $Result.EventLog.LastEventId -eq 1795 -or $Result.EventLog.LastEventId -eq 1801) {
|
||||
return @{ Code='UPDATE_FAILED'; Emoji='[FAIL]'; Label='Selhání aktualizace certifikátů' }
|
||||
}
|
||||
if ($reg.UEFICA2023Status -eq 1 -or $reg.UEFICA2023Status -eq 2) {
|
||||
return @{ Code='UPDATE_PENDING'; Emoji='[WAIT]'; Label='Aktualizace připravena, čeká na restart' }
|
||||
}
|
||||
if ($null -ne $reg.WindowsUEFICA2023Capable -and $reg.WindowsUEFICA2023Capable -eq 0) {
|
||||
return @{ Code='FIRMWARE_UPDATE_NEEDED'; Emoji='[FW]'; Label='Čeká na firmware update (OEM)' }
|
||||
}
|
||||
return @{ Code='UPDATE_NEEDED'; Emoji='[!]'; Label='Nutná aktualizace certifikátů' }
|
||||
}
|
||||
|
||||
function Invoke-Detection {
|
||||
$auditStart = Get-Date
|
||||
$osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
|
||||
$result = [ordered]@{
|
||||
AuditTimestamp = $auditStart.ToString('yyyy-MM-dd HH:mm:ss')
|
||||
Hostname = $env:COMPUTERNAME
|
||||
OSCaption = if ($osInfo) { $osInfo.Caption } else { $null }
|
||||
OSBuild = if ($osInfo) { $osInfo.BuildNumber } else { $null }
|
||||
EnvironmentType = Get-EnvironmentType
|
||||
Hardware = Get-HardwareInfo
|
||||
SecureBoot = Get-SecureBootState
|
||||
Certificates = $null
|
||||
Registry = Get-RegistryStatus
|
||||
EventLog = Get-EventLogStatus
|
||||
Category = $null
|
||||
CategoryLabel = $null
|
||||
}
|
||||
if ($result.SecureBoot.IsUEFI -and $result.SecureBoot.IsSupported) {
|
||||
$result.Certificates = Get-CertificateStatus
|
||||
} else {
|
||||
$result.Certificates = [ordered]@{
|
||||
KEK = [ordered]@{ Has2011=$false; Has2023=$false; Certs2011=@(); Certs2023=@(); Error='Secure Boot nedostupný' }
|
||||
DB = [ordered]@{ Has2011UEFI=$false; Has2011WindowsPCA=$false; Has2023UEFI=$false
|
||||
Has2023OptionROM=$false; Has2023WindowsUEFI=$false; Certs2011=@(); Certs2023=@(); Error='Secure Boot nedostupný' }
|
||||
AnyExpiring2011=$false; AllReplacements2023=$false; AllReplacements2023_VM=$false }
|
||||
}
|
||||
$cat = Get-RemediationCategory -Result $result
|
||||
$result.Category = $cat.Code
|
||||
$result.CategoryLabel = "$($cat.Emoji) $($cat.Label)"
|
||||
return $result
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ── Stručný výpis detekce ──────────────────────────────────────────────
|
||||
|
||||
function Show-DetectionSummary {
|
||||
param($R)
|
||||
$sb = $R.SecureBoot; $c = $R.Certificates; $reg = $R.Registry; $evt = $R.EventLog; $hw = $R.Hardware
|
||||
|
||||
Write-Line ''
|
||||
Write-Line 'DETEKCE' Yellow
|
||||
Write-Line (" Server : {0} ({1}, build {2})" -f $R.Hostname, $R.OSCaption, $R.OSBuild)
|
||||
Write-Line (" Prostředí : {0} [{1} {2}]" -f $R.EnvironmentType, $hw.Manufacturer, $hw.Model)
|
||||
|
||||
$sbText = if (-not $sb.IsUEFI) { 'Legacy BIOS — nepodporováno' }
|
||||
elseif (-not $sb.IsSupported) { 'UEFI, ale Secure Boot nepodporováno' }
|
||||
elseif ($sb.IsEnabled) { 'zapnuto (UEFI)' } else { 'vypnuto (UEFI)' }
|
||||
Write-Line (" Secure Boot : {0}" -f $sbText)
|
||||
|
||||
if ($sb.IsUEFI -and $sb.IsSupported) {
|
||||
$l2011 = @()
|
||||
if ($c.KEK.Has2011) { $l2011 += 'KEK CA 2011' }
|
||||
if ($c.DB.Has2011UEFI) { $l2011 += 'UEFI CA 2011' }
|
||||
if ($c.DB.Has2011WindowsPCA) { $l2011 += 'Windows PCA 2011' }
|
||||
$l2023 = @()
|
||||
if ($c.KEK.Has2023) { $l2023 += 'KEK 2K CA 2023' }
|
||||
if ($c.DB.Has2023UEFI) { $l2023 += 'UEFI CA 2023' }
|
||||
if ($c.DB.Has2023OptionROM) { $l2023 += 'Option ROM 2023' }
|
||||
if ($c.DB.Has2023WindowsUEFI) { $l2023 += 'Windows UEFI CA 2023' }
|
||||
|
||||
$t2011 = if ($l2011.Count) { ('přítomny ({0}) — expirují 6–10/2026' -f ($l2011 -join ', ')) } else { 'nepřítomny' }
|
||||
$t2023 = if ($l2023.Count) { ('přítomny ({0})' -f ($l2023 -join ', ')) } else { 'CHYBÍ' }
|
||||
Write-Line (" Certifikáty 11 : {0}" -f $t2011)
|
||||
Write-Line (" Certifikáty 23 : {0}" -f $t2023)
|
||||
Write-Line (" Registry stav : UEFICA2023Status = {0} ({1})" -f $reg.UEFICA2023Status, $reg.UEFICA2023StatusText)
|
||||
if ($null -ne $reg.UEFICA2023Error) { Write-Line (" UEFICA2023Error = {0}" -f $reg.UEFICA2023Error) Red }
|
||||
if ($evt.LastEventId) { Write-Line (" Poslední event : EventID {0} ({1})" -f $evt.LastEventId, $evt.LastEventTime) }
|
||||
}
|
||||
|
||||
$catColor = switch -Wildcard ($R.Category) {
|
||||
'OK*' { 'Green' } 'UPDATE_NEEDED' { 'Yellow' } 'UPDATE_PENDING' { 'Cyan' }
|
||||
'UPDATE_FAILED' { 'Red' } 'FIRMWARE*' { 'Magenta' } default { 'Gray' } }
|
||||
Write-Line ''
|
||||
Write-Line (" VÝSLEDEK: {0}" -f $R.CategoryLabel) $catColor
|
||||
}
|
||||
|
||||
function Show-DetailedDetection {
|
||||
param($R)
|
||||
$c = $R.Certificates; $evt = $R.EventLog
|
||||
Write-Line ''
|
||||
Write-Line 'PODROBNOSTI' DarkYellow
|
||||
foreach ($grp in @(@{N='KEK';O=$c.KEK}, @{N='DB';O=$c.DB})) {
|
||||
$all = @($grp.O.Certs2011) + @($grp.O.Certs2023)
|
||||
foreach ($ci in $all) {
|
||||
Write-Line (" [{0}] {1}" -f $grp.N, $ci.Subject) DarkGray
|
||||
Write-Line (" platnost do {0} | thumbprint {1}" -f $ci.NotAfter, $ci.Thumbprint) DarkGray
|
||||
}
|
||||
}
|
||||
if ($evt.RelevantEvents.Count) {
|
||||
Write-Line ' Poslední relevantní události:' DarkGray
|
||||
foreach ($e in ($evt.RelevantEvents | Select-Object -First 5)) {
|
||||
$ec = if ($e.EventId -eq 1808) { 'Green' } elseif ($e.EventId -in @(1795,1801)) { 'Red' } else { 'DarkGray' }
|
||||
Write-Line (" [{0}] EventID {1} {2}" -f $e.TimeCreated, $e.EventId, $e.Level) $ec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ── Vyhodnocení remediace ──────────────────────────────────────────────
|
||||
|
||||
function Resolve-RemediationPlan {
|
||||
<# Vrátí plán: zda lze remediovat, zda se ptát, a jaké jsou další kroky. #>
|
||||
param($R, [bool]$ForceMode)
|
||||
$plan = [ordered]@{ Applicable=$false; AskUser=$false; Caution=$null; Reason=$null; NextSteps=@() }
|
||||
$evtId = $R.EventLog.LastEventId
|
||||
|
||||
switch -Wildcard ($R.Category) {
|
||||
'OK' {
|
||||
$plan.Reason = 'Server má potřebné 2023 certifikáty — žádná akce není nutná.'
|
||||
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Server je již OK — -Force vynutí opětovné nastavení.' }
|
||||
}
|
||||
'OK_TRANSITION' {
|
||||
$plan.Reason = 'Server má nové 2023 certifikáty (a dosud i staré 2011) — žádná akce, jen monitorovat.'
|
||||
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Přechodný stav je v pořádku — -Force vynutí opětovné nastavení.' }
|
||||
}
|
||||
'UPDATE_NEEDED' {
|
||||
$plan.Applicable=$true; $plan.AskUser=$true
|
||||
$plan.Reason='Lze aplikovat remediaci metodou registry (KB5068202).'
|
||||
}
|
||||
'UPDATE_PENDING' {
|
||||
$plan.Reason='Aktualizace je už připravená (UEFICA2023Status). Chybí pouze restart — což skript záměrně neprovádí.'
|
||||
$plan.NextSteps=@('Naplánujte RESTART serveru.', 'Po restartu ověřte EventID 1808 v System logu.')
|
||||
}
|
||||
'UPDATE_FAILED' {
|
||||
if ($evtId -eq 1795) {
|
||||
$plan.Reason='Selhání s EventID 1795 (známý problém Hyper-V). Příčina je na straně HOSTITELE, ne této VM.'
|
||||
$plan.NextSteps=@('Aktualizujte Windows Server na Hyper-V hostiteli (KB5085790, fix od 3/2026).','Poté spusťte tuto kontrolu na VM znovu.')
|
||||
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='EventID 1795 = problém hostitele. -Force pouze zopakuje pokus na VM.' }
|
||||
} else {
|
||||
$plan.Applicable=$true; $plan.AskUser=$true
|
||||
$plan.Reason='Předchozí pokus selhal (EventID 1801 / status Failed). Lze zopakovat metodou registry.'
|
||||
$plan.Caution='Před opakováním zkontrolujte UEFICA2023Error (viz detekce výše).'
|
||||
}
|
||||
}
|
||||
'FIRMWARE_UPDATE_NEEDED' {
|
||||
$plan.Reason='WindowsUEFICA2023Capable=0 — firmware nemusí nové certifikáty podporovat.'
|
||||
$plan.NextSteps=@('Aktualizujte firmware serveru u výrobce (Dell/HP/Lenovo/Supermicro).','Pokud update není dostupný, dokumentujte jako trvalou výjimku.')
|
||||
if ($ForceMode) { $plan.Applicable=$true; $plan.AskUser=$true; $plan.Caution='Firmware se jeví jako nezpůsobilý — -Force se pokusí i tak (může selhat).' }
|
||||
}
|
||||
'SECUREBOOT_DISABLED' {
|
||||
$plan.Reason='Secure Boot existuje, ale je vypnutý. Zapnutí je rozhodnutí mimo rozsah tohoto skriptu.'
|
||||
$plan.NextSteps=@('Rozhodněte o zapnutí Secure Boot v UEFI/nastavení VM (pozor na BitLocker PCR7).')
|
||||
}
|
||||
'NO_SECUREBOOT*' {
|
||||
$plan.Reason='Secure Boot není podporováno (Legacy BIOS nebo VM bez vTPM/UEFI).'
|
||||
$plan.NextSteps=@('Dokumentujte server jako výjimku — remediace certifikátů zde nedává smysl.')
|
||||
}
|
||||
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 remediaci (bez restartu)'
|
||||
$no = New-Object System.Management.Automation.Host.ChoiceDescription '&Ne', 'Neprovádět žádné změny'
|
||||
$choice = $Host.UI.PromptForChoice('', 'Vaše volba:', [System.Management.Automation.Host.ChoiceDescription[]]@($yes,$no), 1)
|
||||
return ($choice -eq 0)
|
||||
} catch {
|
||||
$ans = Read-Host 'Aplikovat? [a/N]'
|
||||
return ($ans -match '^(a|ano|y|yes)$')
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ── Remediace (sekvenčně, s čekáním; z Set-SecureBootCertificateUpdate.ps1) ──
|
||||
|
||||
function Invoke-Remediation {
|
||||
<# Sekvenční remediace. Každý krok čeká na dokončení předchozího. Bez restartu. #>
|
||||
$outcome = [ordered]@{ Status='Unknown'; Message=''; FinalStatusText='Unknown'; LogFile=$script:LogFile }
|
||||
|
||||
Write-Line ''
|
||||
Write-Line 'REMEDIACE' Yellow
|
||||
Add-LogLine 'REMEDIACE START'
|
||||
|
||||
# ── Krok 1/3 — registry (a ověření zápisu read-backem) ──
|
||||
Write-Host ' [1/3] Nastavuji registry klíče (AvailableUpdates = 0x5944) ... ' -NoNewline
|
||||
try {
|
||||
if (-not (Test-Path $REG_SECUREBOOT)) { New-Item -Path $REG_SECUREBOOT -Force -ErrorAction Stop | Out-Null }
|
||||
Set-ItemProperty -Path $REG_SECUREBOOT -Name 'AvailableUpdates' -Value $AVAILABLE_UPDATES_VALUE -Type DWord -Force -ErrorAction Stop
|
||||
|
||||
$optOut = Get-ItemProperty $REG_SECUREBOOT -Name 'HighConfidenceOptOut' -ErrorAction SilentlyContinue
|
||||
if ($optOut -and $optOut.HighConfidenceOptOut -ne 0) {
|
||||
Set-ItemProperty -Path $REG_SECUREBOOT -Name 'HighConfidenceOptOut' -Value 0 -Type DWord -Force -ErrorAction Stop
|
||||
}
|
||||
if (-not (Test-Path $REG_SERVICING)) { New-Item -Path $REG_SERVICING -Force -ErrorAction Stop | Out-Null }
|
||||
|
||||
# Read-back: další krok pustíme jen když je hodnota skutečně zapsaná
|
||||
$verify = (Get-ItemProperty $REG_SECUREBOOT -Name 'AvailableUpdates' -ErrorAction Stop).AvailableUpdates
|
||||
if ($verify -ne $AVAILABLE_UPDATES_VALUE) { throw "Ověření selhalo — AvailableUpdates = $verify (očekáváno $AVAILABLE_UPDATES_VALUE)" }
|
||||
|
||||
Write-Host 'hotovo' -ForegroundColor Green
|
||||
Add-LogLine ("Krok 1: AvailableUpdates=0x{0:X4} zapsáno a ověřeno; HighConfidenceOptOut=0" -f $AVAILABLE_UPDATES_VALUE)
|
||||
} catch {
|
||||
Write-Host 'CHYBA' -ForegroundColor Red
|
||||
Write-Line (" {0}" -f $_.Exception.Message) Red
|
||||
$outcome.Status='Error'; $outcome.Message="Zápis registry selhal: $($_.Exception.Message)"
|
||||
Add-LogLine "Krok 1 CHYBA: $($_.Exception.Message)"
|
||||
return $outcome
|
||||
}
|
||||
|
||||
# ── Krok 2/3 — servicing task a ČEKÁNÍ na dokončení ──
|
||||
if ($SkipScheduledTask) {
|
||||
Write-Line ' [2/3] Servicing task přeskočen (-SkipScheduledTask) — spustí se sám (cca á 12 h).' DarkGray
|
||||
Add-LogLine 'Krok 2: task přeskočen (-SkipScheduledTask)'
|
||||
} else {
|
||||
Write-Host ' [2/3] Spouštím servicing task a čekám na dokončení ' -NoNewline
|
||||
$task = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue
|
||||
if (-not $task) {
|
||||
Write-Host '— task nenalezen' -ForegroundColor Yellow
|
||||
Write-Line ' Task neexistuje; registry je nastavena, Windows ji zpracuje při příštím servisním běhu.' DarkGray
|
||||
Add-LogLine 'Krok 2: scheduled task nenalezen — registry zpracuje Windows samostatně'
|
||||
} else {
|
||||
try {
|
||||
Start-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop
|
||||
Start-Sleep -Seconds 2 # dát tasku čas přejít do stavu Running
|
||||
$elapsed = 0; $state = 'Running'
|
||||
do {
|
||||
Start-Sleep -Seconds 3; $elapsed += 3
|
||||
Write-Host '.' -NoNewline -ForegroundColor DarkGray
|
||||
$state = (Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue).State
|
||||
} while ($state -eq 'Running' -and $elapsed -lt $TASK_TIMEOUT_SEC)
|
||||
|
||||
$info = Get-ScheduledTaskInfo -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue
|
||||
if ($state -eq 'Running' -and $elapsed -ge $TASK_TIMEOUT_SEC) {
|
||||
Write-Host (' stále běží po {0}s' -f $elapsed) -ForegroundColor Yellow
|
||||
Write-Line ' Task neskončil v limitu — pokračuji. Stav ověřte později.' Yellow
|
||||
Add-LogLine "Krok 2: task po ${elapsed}s stále Running (timeout)"
|
||||
} else {
|
||||
$rc = if ($info) { '0x{0:X}' -f $info.LastTaskResult } else { 'n/a' }
|
||||
Write-Host (' hotovo ({0}s, stav: {1}, výsledek: {2})' -f $elapsed, $state, $rc) -ForegroundColor Green
|
||||
Add-LogLine "Krok 2: task dokončen za ${elapsed}s, stav=$state, LastTaskResult=$rc"
|
||||
}
|
||||
} catch {
|
||||
Write-Host '— nepodařilo se spustit' -ForegroundColor Yellow
|
||||
Write-Line (" {0}" -f $_.Exception.Message) DarkGray
|
||||
Write-Line ' Registry je nastavena; task se spustí sám při příštím běhu (cca á 12 h).' DarkGray
|
||||
Add-LogLine "Krok 2: spuštění tasku selhalo: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── Krok 3/3 — ověření výsledného stavu (až po dokončení tasku) ──
|
||||
Write-Host ' [3/3] Ověřuji výsledný stav ... ' -NoNewline
|
||||
Start-Sleep -Seconds 2
|
||||
$post = Get-RegistryStatus
|
||||
$outcome.FinalStatusText = $post.UEFICA2023StatusText
|
||||
Write-Host ('UEFICA2023Status = {0} ({1})' -f $post.UEFICA2023Status, $post.UEFICA2023StatusText) -ForegroundColor Cyan
|
||||
Add-LogLine ("Krok 3: UEFICA2023Status={0} ({1})" -f $post.UEFICA2023Status, $post.UEFICA2023StatusText)
|
||||
if ($null -ne $post.UEFICA2023Error) { Write-Line (" UEFICA2023Error = {0}" -f $post.UEFICA2023Error) Red }
|
||||
|
||||
$outcome.Status = 'Applied'
|
||||
$outcome.Message = 'Registry nastavena a ověřena, servicing task zpracován. Certifikáty se aplikují až při restartu.'
|
||||
Add-LogLine 'REMEDIACE KONEC: Applied'
|
||||
return $outcome
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ── Main ───────────────────────────────────────────────────────────────
|
||||
|
||||
$isWhatIf = [bool]$WhatIfPreference
|
||||
|
||||
Write-Host ''
|
||||
Write-Rule
|
||||
Write-Host ' SECURE BOOT — KONTROLA A REMEDIACE CERTIFIKÁTŮ' -ForegroundColor Cyan
|
||||
Write-Host (" {0} {1}" -f $env:COMPUTERNAME, (Get-Date -Format 'yyyy-MM-dd HH:mm')) -ForegroundColor DarkGray
|
||||
Write-Rule
|
||||
|
||||
# Admin check (čtení UEFI db a zápis registry vyžaduje elevaci)
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Line ''
|
||||
Write-Line ' ! Skript neběží jako Administrator — některé hodnoty nemusí být dostupné a remediaci nelze provést.' Yellow
|
||||
}
|
||||
|
||||
# ── 1) DETEKCE ──
|
||||
$result = Invoke-Detection
|
||||
Show-DetectionSummary -R $result
|
||||
if ($Detailed) { Show-DetailedDetection -R $result }
|
||||
|
||||
# ── 2) VYHODNOCENÍ ──
|
||||
$plan = Resolve-RemediationPlan -R $result -ForceMode:$Force.IsPresent
|
||||
|
||||
Write-Line ''
|
||||
Write-Line 'VYHODNOCENÍ' Yellow
|
||||
if ($plan.Reason) { Write-Line (" {0}" -f $plan.Reason) }
|
||||
|
||||
$remediation = $null
|
||||
|
||||
if (-not $plan.Applicable) {
|
||||
# Nelze / netřeba remediovat — vypsat jasné další kroky
|
||||
if ($plan.NextSteps.Count) {
|
||||
Write-Line ''
|
||||
Write-Line ' Další kroky:' White
|
||||
$i = 1; foreach ($s in $plan.NextSteps) { Write-Line (" {0}. {1}" -f $i, $s); $i++ }
|
||||
} else {
|
||||
Write-Line ' Není potřeba žádná akce.' Green
|
||||
}
|
||||
} else {
|
||||
if ($plan.Caution) { Write-Line (" Pozor: {0}" -f $plan.Caution) Yellow }
|
||||
|
||||
if (-not $isAdmin) {
|
||||
Write-Line ''
|
||||
Write-Line ' Remediaci nelze provést bez práv Administrator. Spusťte skript elevovaně.' Yellow
|
||||
} elseif ($isWhatIf) {
|
||||
Write-Line ''
|
||||
Write-Line ' -WhatIf — co by remediace udělala (žádné změny se neprovádějí):' Cyan
|
||||
Write-Line (" • nastavila by AvailableUpdates = 0x{0:X4} a HighConfidenceOptOut = 0" -f $AVAILABLE_UPDATES_VALUE)
|
||||
if (-not $SkipScheduledTask) { Write-Line (" • spustila by servicing task '{0}{1}' a počkala na jeho dokončení" -f $TASK_PATH, $TASK_NAME) }
|
||||
Write-Line ' • server by NErestartovala'
|
||||
} else {
|
||||
# ── 3) DOTAZ + REMEDIACE ──
|
||||
$q = 'Chcete nyní zahájit aktualizaci Secure Boot certifikátů?'
|
||||
$d = 'Nastaví se registry klíče (KB5068202) a spustí servicing task. Server NEBUDE restartován.'
|
||||
if (Get-UserConsent -Question $q -Detail $d) {
|
||||
$script:LogActive = $true # od teď logujeme do souboru
|
||||
Add-LogLine ("Detekce: {0} | kategorie={1}" -f $result.Hostname, $result.Category)
|
||||
$remediation = Invoke-Remediation
|
||||
} else {
|
||||
Write-Line ''
|
||||
Write-Line ' Remediace neprovedena (volba uživatele). Žádné změny.' Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ── 4) ZÁVĚR / DALŠÍ KROKY ──
|
||||
Write-Line ''
|
||||
Write-Rule
|
||||
if ($remediation -and $remediation.Status -eq 'Applied') {
|
||||
Write-Line 'HOTOVO — aktualizace je připravena.' Green
|
||||
Write-Line ''
|
||||
Write-Line 'Další kroky:' White
|
||||
Write-Line ' 1. Naplánujte RESTART serveru — skript jej záměrně neprovedl.'
|
||||
Write-Line ' 2. Pozor na BitLocker: je-li aktivní s PCR7, před restartem ověřte recovery key.'
|
||||
Write-Line ' 3. Po restartu ověřte úspěch — EventID 1808 v System logu:'
|
||||
Write-Line " Get-WinEvent -FilterHashtable @{LogName='System';Id=1808} -MaxEvents 3" DarkGray
|
||||
Write-Line ' 4. Volitelně spusťte tuto kontrolu znovu — očekávaný výsledek: OK.'
|
||||
Write-Line ''
|
||||
Write-Line ("Log: {0}" -f $script:LogFile) DarkGray
|
||||
} elseif ($remediation -and $remediation.Status -eq 'Error') {
|
||||
Write-Line ('CHYBA REMEDIACE — {0}' -f $remediation.Message) Red
|
||||
Write-Line ("Log: {0}" -f $script:LogFile) DarkGray
|
||||
} else {
|
||||
Write-Line 'KONEC — bez změn na serveru.' Cyan
|
||||
}
|
||||
Write-Rule
|
||||
Write-Host ''
|
||||
|
||||
if ($PassThru) {
|
||||
return [pscustomobject]@{ Detection = $result; Plan = $plan; Remediation = $remediation }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user