Копирайте и поставете този примерен скрипт и променете, както е необходимо за вашата среда:
< на 1000 000 00 . СИНОПСИС Обединява JSON данни за състоянието на защитеното стартиране от множество устройства в обобщени отчети.
Не, не, не ОПИСАНИЕ Чете събраните JSON файлове за състоянието на защитеното стартиране и генерира: - HTML табло с диаграми и филтриране - Резюме по confidenceLevel - Уникален анализ на набор от устройства за стратегия за тестване Поддържа: - Файлове на машина: HOSTNAME_latest.json (препоръчително) - Един JSON файл Автоматично премахва дублирания по HostName, запазвайки най-новата версия на CollectionTime. По подразбиране включва само устройства с "Повторение на действие" или "Висока" увереност , за да се съсредоточите върху набори с действия. Използвайте -IncludeAllConfidenceLevels, за да заместите.
Не, не, не PARAMETER InputPath Път до JSON файлове: - Папка: Прочита всички *_latest.json файлове (или *.json, ако няма _latest файлове) - Файл: Чете един JSON файл
Не, не, не PARAMETER OutputPath Път за генерирани отчети (по подразбиране: .\SecureBootReports)
Не, не, не ПРИМЕР # Агрегиране от папка с файлове на машина (препоръчително) .\Aggregate-SecureBootData.ps1 –InputPath "\\contoso\SecureBootLogs$" # Чете: \\contoso\SecureBootLogs$\*_latest.json
Не, не, не ПРИМЕР # Местоположение за изход по избор .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" - OutputPath "C:\Reports\SecureBoot"
Не, не, не ПРИМЕР # Включвай само "Отговори на действия" и "Висока достоверност" (поведение по подразбиране) .\Aggregate-SecureBootData.ps1 –InputPath "\\contoso\SecureBootLogs$" # Изключва: Наблюдение, в пауза, не се поддържа
Не, не, не ПРИМЕР # Включване на всички доверителни нива (заместване на филтъра) .\Aggregate-SecureBootData.ps1 –InputPath "\\contoso\SecureBootLogs$" – IncludeAllConfidenceLevels
Не, не, не ПРИМЕР # Потребителски филтър на доверително ниво .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
Не, не, не ПРИМЕР # ENTERPRISE SCALE: Постъпков режим - само обработка на променени файлове (бързи следващи изпълнения) .\Aggregate-SecureBootData.ps1 –InputPath "\\contoso\SecureBootLogs$" – IncrementalMode # Първо изпълнение: Пълно натоварване ~ 2 часа за 500K устройства # Следващите изпълнения: Секунди, ако няма промени, минути за делта
Не, не, не ПРИМЕР # Пропускане на HTML, ако нищо не се е променило (най-бързо за наблюдение) .\Aggregate-SecureBootData.ps1 –InputPath "\\contoso\SecureBootLogs$" - IncrementalMode -SkipReportIfUnchanged # Ако няма променени файлове след последното изпълнение: ~ 5 секунди
Не, не, не ПРИМЕР # Режим "Само резюме" – пропускане на таблици с големи устройства (1-2 минути спрямо 20 + минути) .\Aggregate-SecureBootData.ps1 –InputPath "\\contoso\SecureBootLogs$" – SummaryOnly # Генерира CSVs, но пропуска HTML табло с пълни таблици на устройството
Не, не, не БЕЛЕЖКИ Сдвоете с Detect-SecureBootCertUpdateStatus.ps1 за корпоративно разполагане.Вижте GPO-DEPLOYMENT-GUIDE.md за пълно ръководство за разполагане. Поведението по подразбиране изключва наблюдение, пауза и неподдържани устройства , за да фокусирате отчитането само върху набори от устройства, които могат да се предприемат действия.#>
парам( [Parameter(Mandatory = $true)] [низ]$InputPath, [Parameter(Mandatory = $false)] [string]$OutputPath = ".\SecureBootReports", [Parameter(Mandatory = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Mandatory = $false)] [string]$RolloutStatePath, # Път до RolloutState.json за идентифициране на устройства InProgress [Parameter(Mandatory = $false)] [string]$RolloutSummaryPath, # Път до SecureBootRolloutSummary.json от Orchestrator (съдържа данни за прожектиране) [Parameter(Mandatory = $false)] [string[]]$IncludeConfidenceLevels = @("Изисква се действие";"Висока достоверност"), # Включвай само тези доверителни нива (по подразбиране: само набори с действие) [Parameter(Mandatory = $false)] [switch]$IncludeAllConfidenceLevels, # Заместване на филтър, за да се включат всички доверителни нива [Parameter(Mandatory = $false)] [switch]$SkipHistoryTracking, [Parameter(Mandatory = $false)] [switch]$IncrementalMode, # Enable delta processing – зареждане само на променени файлове след последното изпълнение [Parameter(Mandatory = $false)] [string]$CachePath, # Път до директорията за кеширане (по подразбиране: OutputPath\.cache) [Parameter(Mandatory = $false)] [int]$ParallelThreads = 8, # Брой паралелни нишки за зареждане на файла (PS7+) [Parameter(Mandatory = $false)] [switch]$ForceFullRefresh, # Force full reload even in incremental mode [Parameter(Mandatory = $false)] [switch]$SkipReportIfUnchanged, # Skip HTML/CSV generation if no files changed (just output stats) [Parameter(Mandatory = $false)] [switch]$SummaryOnly, # Генерирайте само обобщени данни (без големи таблици с устройства) – много по-бързо [Parameter(Mandatory = $false)] [switch]$StreamingMode # Memory-efficient mode: process chunks, write CSVs incrementally, keep only summaries in memory )
# Автоматично повишаване до PowerShell 7, ако е налично (6x по-бързо за големи набори от данни) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh –ErrorAction SilentlyContinue | източник на Select-Object ExpandProperty ако ($pwshPath) { Write-Host открит PowerShell $($PSVersionTable.PSVersion) – повторно стартиране с PowerShell 7 за по-бърза обработка..." – Цвят на преден план – жълто # Повторно създаване на списък с аргументи от обвързани параметри $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) търсене на място ($key в $PSBoundParameters.keys) { $val = $PSBoundParameters[$key] ако ($val е [ключ]) { ако ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [масив]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } друго { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs излизане от $LASTEXITCODE } }
$ErrorActionPreference = "Продължи" $timestamp = Get-Date – формат "yyyyMMdd-HHmmss" $scanTime = Get-Date -Формат "ггггг-ММ-дд ЧЧ:мм:сс" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Примери за разполагане и мониторинг"
# Забележка: Този скрипт няма зависимости от други скриптове. # За пълния набор инструменти изтеглете от: $DownloadUrl -> $DownloadSubPage
инсталиране на #region Write-Host "=" * 60 -ForegroundColor Cyan Write-Host "Агрегиране на данни за защитено стартиране" – ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan
# Създаване на изходна директория if (-not (Test-Path $OutputPath)) { New-Item -ItemType директория -път $OutputPath -Force | Out-Null }
# Зареждане на данни – поддържа ФОРМАТИ CSV (наследени) и JSON (присъщи) Write-Host "'nЗареди данните от: $InputPath" – Цвят на преден план Жълто
# Помощна функция за нормализиране на обекта на устройството (разлики в името на полето за манипулатор) функция Normalize-DeviceRecord { парам($device) # Handle Hostname vs HostName (JSON използва Hostname, CSV използва HostName) ако ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName "HostName" – $device NotePropertyValue. Име на хост – принудително } # Handle Confidence vs ConfidenceLevel (JSON uses Confidence, CSV uses ConfidenceLevel) # ConfidenceLevel е официалното име на полето – карта Доверие към него ако ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName "ConfidenceLevel" - NotePropertyValue $device. Достоверност – сила } # Проследяване на състоянието на актуализацията чрез Event1808Count ИЛИ UEFICA2023Status="Updated" # Това позволява да се проследи колко устройства във всеки доверителен набор са актуализирани $event 1808 = 0 ако ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false ако ($device. PSObject.Properties['UEFICA2023Status'] – и $device. UEFICA2023Status -eq "Актуализирано") { $uefiCaUpdated = $true } ако ($event 1808 -gt 0 -или $uefiCaUpdated) { # Маркирай като актуализирано за логиката на таблото/внедряването, но не замествайте ConfidenceLevel $device | Add-Member -NotePropertyName "IsUpdated" - NotePropertyValue $true -Force } друго { $device | Add-Member -NotePropertyName "IsUpdated" - NotePropertyValue $false -Force # Класификация на ConfidenceLevel: # - "Висока достоверност", "Под наблюдение...", "Временно на пауза...", "Не се поддържа..." = използване както е # - Всичко друго (null, празно, "UpdateType:...", "Неизвестен", "N/A") = попада в изискваното действие в броячите # Не е необходимо нормализиране – клонът на брояча за поточно предаване го обработва } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON uses OEM*, legacy uses WMI_*) ако ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName "WMI_Manufacturer" – $device NotePropertyValue. OEMManufacturerName – force } # Handle OEMModelNumber vs WMI_Model ако ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName "WMI_Model" – $device NotePropertyValue. OEMModelNumber – принудително } # Handle FirmwareVersion vs BIOSDescription ако ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName "BIOSDescription" – $device NotePropertyValue. FirmwareVersion – принудително } връщане $device }
#region постъпково обработване/управление на кеша # Setup cache paths ако (-not $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Функции за управление на кеша функция Get-FileManifest { param([string]$Path) ако (тест-път $Path) { опитайте { $json = Get-Content $Path -Raw | Преобразуване от Json # Преобразуване на PSObject в таблица с хеширане (съвместима с PS5.1 – PS7 has -AsHashtable) $ht = @{} - $json. PSObject.Properties | ForEach-Object { $ht[$_. Name] = $_. Стойност } връщане $ht } улов { връщане на @{} } } връщане на @{} }
функция Save-FileManifest { param([hashtable]$Manifest; [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType директория -Път $dir -Force | Out-Null } $Manifest | ConvertTo-Json -Дълбочина 3 -Компресиране | Set-Content $Path -Force }
функция Get-DeviceCache { param([string]$Path) ако (тест-път $Path) { опитайте { $cacheData = Get-Content $Path -Raw | Преобразуване от Json Write-Host " Зареден кеш на устройство: устройства с $($cacheData.Count) " -ForegroundColor DarkGray връщане $cacheData } улов { Write-Host " Повреден кеш, ще се възстанови" – Цвят на преден план – жълт връщане на @() } } връщане на @() }
функция Save-DeviceCache { param($Devices; [низ]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType директория -Път $dir -Force | Out-Null } # Преобразуване в масив и записване $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Depth 10 -Компресиране | Set-Content $Path -Force Write-Host " Кеш за записани устройства: устройства с $($deviceArray.Count) " -ForegroundColor DarkGray }
функция Get-ChangedFiles { парам( [System.IO.FileInfo[]]$AllFiles, [таблица с хеширане]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Създайте нечувствително към малки букви търсене от манифест (нормализиране на малки букви) $manifestLookup = @{} foreach ($mk в $Manifest.Keys) { $manifestLookup$mk. ToLowerInvariant()] = $Manifest[$mk] } действия ($file в $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalize path to lowercase $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Размер = $file. Дължина } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] ако ($cached. LastWriteTimeUtc -eq $lwt и $cached. Размер -eq $file. Дължина) { [void]$unchanged. Add($file) Продължи } } [void]$changed. Add($file) } връщане на @{ Променено = $changed Непроменен = $unchanged NewManifest = $newManifest } }
# Ultra-fast parallel file loading using batched processing функция Load-FilesParallel { парам( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Брой # Използвайте партиди от ~ 1000 файла всеки за по-добро управление на паметта $batchSize = [math]::Min(1000; [math]::Ceiling($totalFiles / [math]::Max(1; $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
за ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [math]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches е. Add($batch) } Write-Host " ($($batches. Брой) партиди от ~$batchSize всеки файл)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # Проверете дали PowerShell 7+ паралелно е наличен $canParallel = $PSVersionTable.PSVersion.Major -ge 7 ако ($canParallel и $Threads -gt 1) { # PS7+: Процес на партиди паралелно $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() действия ($file в $batchFiles) { опитайте { $content = [System.IO.File]::ReadAllText($file. Пълно име) | Преобразуване от Json $batchResults.Add($content) } улов { } } $batchResults.ToArray() } действия ($batch в $results) { ако ($batch) { $item в $batch) { $flatResults.Add($item) } } } } друго { # PS5.1 резервна: Поредна обработка (все още бърза за файлове с <10 000) foreach ($file в $Files) { опитайте { $content = [System.IO.File]::ReadAllText($file. Пълно име) | Преобразуване от Json $flatResults.Add($content) } улов { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Един JSON файл ако ($InputPath -like "*.json") { $jsonContent = Get-Content -Път $InputPath -Raw | Преобразуване от Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "Заредени $($allDevices.Count) записи от файл" } друго { Write-Error "Поддържа се само JSON формат. Файлът трябва да има разширение .json". изход 1 } } elseif (тест-път $InputPath контейнер PathType) { # Folder – само JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Име -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Предпочитам *_latest.json файлове, ако съществуват (режим за всяка машина) $latestJson = $jsonFiles | Where-Object { $_. Име -като "*_latest.json" } ако ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count ако ($totalFiles -eq 0) { Write-Error "Не са намерени JSON файлове в: $InputPath" изход 1 } Write-Host "Намерени $totalFiles JSON файлове" - Сиво на преден план # Помощна функция, която да съответства на доверителните нива (обработва както кратки, така и пълни формуляри) # Дефиниран рано, така че както StreamingMode, така и нормалните пътища да могат да го използват функция Test-ConfidenceLevel { param([низ]$Value; [низ]$Match) if ([низ]::IsNullOrEmpty($Value)) { return $false } превключвател ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Изисква се действие*" или $Value -eq "Изисква се действие") } "TemporarilyPaused" { return $Value -like "Временно на пауза*" } "NotSupported" { return ($Value -like "Не се поддържа*" или $Value -eq "Не се поддържа") } по подразбиране { return $false } } } #region РЕЖИМ НА ПОТОЧНО ПРЕДАВАНЕ - Ефективна обработка на паметта за големи набори от данни # Винаги използвай StreamingMode за ефективна обработка на паметта и табло в нов стил ако (-not $StreamingMode) { Write-Host "Auto-enabling StreamingMode (new-style dashboard)" -ForegroundColor Yellow $StreamingMode = $true ако (-not $IncrementalMode) { $IncrementalMode = $true } } # Когато -StreamingMode е разрешено, обработвайте файловете на части, запазвайки само броячи в паметта.# Данните на ниво устройство се записват в JSON файлове на блок за зареждане при поискване в таблото.# Използване на паметта: ~1,5 ГБ независимо от размера на набора от данни (спрямо 10-20 ГБ без поточно предаване).ако ($StreamingMode) { Write-Host "РАЗРЕШЕН РЕЖИМ НА ПОТОЧНО ПРЕДАВАНЕ - обработка с ефективно използване на паметта" - ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTAL CHECK: Ако няма променени файлове след последното изпълнение, прескочете обработката изцяло ако ($IncrementalMode и -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" ако (тест-път $stManifestPath) { Write-Host "Проверка за промени след последното поточно изпълнение..." -ForegroundColor Cyan $stOldManifest = Get-FileManifest път $stManifestPath ако ($stOldManifest.Брой -gt 0) { $stChanged = $false # Бърза проверка: същия брой файлове? ако ($stOldManifest.Count -eq $totalFiles) { # Проверете 100 NEWEST файла (сортирани по LastWriteTime низходящо) # Ако някой файл е променен, той ще има най-новото времево клеймо и ще се появи първи $sampleSize = [математически]::Min(100; $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Descending | Select-Object – първи $sampleSize ( $sf в $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Почивка } # Compare timestamps - cached may be DateTime or string after JSON roundtrip $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc опитайте { # Ако кеширани вече е DateTime (ConvertFrom-Json автоматично преобразува), използвайте директно if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } друго { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT – $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Почивка } } улов { $stChanged = $true Почивка } } } друго { $stChanged = $true } ако (-not $stChanged) { # Проверете дали съществуват изходни файлове $stSummaryExists = Get-ChildItem (път към съединение $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -първи 1 $stDashExists = Get-ChildItem (път на съединение $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object - първи 1 ако ($stSummaryExists -и $stDashExists) { Write-Host " Няма открити промени ($totalFiles файлове непроменени) – пропускане на обработка" -ForegroundColor Green Write-Host " Фамилно табло: $($stDashExists.FullName)" –ForegroundColor White $cachedStats = Get-Content $stSummaryExists.ПълноИме | ConvertFrom-Csv Write-Host " Устройства: $($cachedStats.TotalDevices) | Актуализирано: $($cachedStats.Актуализирано) | Грешки: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host " Завършена в $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (не е необходима обработка)" – ForegroundColor Green връщане $cachedStats } } друго { # DELTA PATCH: Намерете точно кои файлове са променени Write-Host " Открити промени – идентифициране на променени файлове..." -Цвят на текста Жълто $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() действия ($jf в $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } друго { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc опитайте { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT – $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } улов { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [математически]::Round(($totalChanged /$totalFiles) * 100; 1) Write-Host " Променено: $($changedFiles.Count) | Ново: $($newFiles.Count) | Общо: $totalChanged ($changePct%)" -Преден планЦветно жълто ако ($totalChanged -gt 0 -и $changePct -lt 10) { # DELTA PATCH MODE: <10% променен, коригиране на съществуващи данни Write-Host " Режим на делта корекция ($changePct% < 10%) - корекция $totalChanged файлове..." -ForegroundColor Green $dataDir = Join-Path $OutputPath "data" # Зареждане на променени/нови записи на устройства $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) действия ($df в $allDeltaFiles) { опитайте { $devData = Get-Content $df. FullName -Raw | Преобразуване от Json $dev = Normalize-DeviceRecord $devData ако ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } улов { } } Write-Host " Заредени $($deltaDevices.Count) променени записи на устройството" - ForegroundColor Gray # За всяка категория JSON: премахване на стари записи за променени имена на хостове, добавяне на нови записи $categoryFiles = @("грешки"; "known_issues"; "missing_kek"; "not_updated"; "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required", "secureboot_off", "rollout_inprogress") $changedHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($hn в $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } ( $cat в $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" ако (тест-път $catPath) { опитайте { $catData = Get-Content $catPath -Raw | Преобразуване от Json # Премахване на стари записи за променени имена на хостове $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Прекласифициране на всяко променено устройство в категории # (ще бъде добавен по-долу след класификацията) $catData | ConvertTo-Json -Depth 5 | Set-Content $catPath – UTF8 за кодиране } улов { } } } # Класифицирайте всяко променено устройство и добавете към правилната категория файлове foreach ($dev в $deltaDevices.Values) { $slim = [поръчано]@{ HostName = $dev. Хост WMI_Manufacturer = ако ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } друго { "" } WMI_Model = ако ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } друго { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } друго { "" } ConfidenceLevel = ако ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } друго { "" } IsUpdated = $dev. IsUpdated UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Грешка } друго { $null } SecureBootTaskStatus = ако ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } друго { "" } KnownIssueId = ако ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } друго { $null } SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } друго { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } друго { "" } $hasErr = (-not [низ]::IsNullOrEmpty($dev. UEFICA2023Error) и $dev. UEFICA2023Грешка -ne "0" - и $dev. UEFICA2023Грешка -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false или $dev. SecureBootTaskStatus -eq "Забранено" – или $dev. SecureBootTaskStatus -eq "NotFound") $tskNF = ($dev. SecureBootTaskStatus -eq "NotFound") $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = ако ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } друго { 0 } $e 1808 = ако ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } друго { 0 } $e 1803 = ако ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } друго { 0 } $mKEK = ($e 1803 -gt 0 -или $dev. MissingKEK -eq $true) $hKI = ((-not [низ]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) или (-not [string]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = ако ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } друго { "" } # Добавяне към съответстващи файлове на категория $targets = @() ако ($isUpd) { $targets += "updated_devices" } ако ($hasErr) { $targets += "грешки" } ако ($hKI) { $targets += "known_issues" } ако ($mKEK) { $targets += "missing_kek" } ако (-not $isUpd -and $sbOn) { $targets += "not_updated" } ако ($tskDis) { $targets += "task_disabled" } if (-not $isUpd -and ($tskDis -или (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += "temp_failures" } if (-not $isUpd -and ((Test-ConfidenceLevel $conf "NotSupported") -или ($tskNF -and $hasErr))) { $targets += "perm_failures" } ако (-not $isUpd -и (Test-ConfidenceLevel $conf "ActionRequired")) { $targets += "action_required" } ако (-not $sbOn) { $targets += "secureboot_off" } ако ($e 1801 -gt 0 -и $e 1808 -eq 0 -and -not $hasErr -и $rStat -eq "InProgress") { $targets += "rollout_inprogress" } действия ($tgt в $targets) { $tgtPath = Join-Path $dataDir "$tgt.json" ако (тест-път $tgtPath) { $existing = Get-Content $tgtPath -Raw | Преобразуване от Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Depth 5 | Set-Content $tgtPath – кодиране UTF8 } } } # Генерирай CSVs от кръпка JSONs Write-Host " Генериращи CSVs от коригирани данни..." - Цвят на преден план – сиво $newTimestamp = Get-Date – формат "yyyyMMdd-HHmmss" ($cat в $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" ако (тест-път $catJsonPath) { опитайте { $catJsonData = Get-Content $catJsonPath -Raw | Преобразуване от Json ако ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv път $catCsvPath NoTypeInformation – кодиране UTF8 } } улов { } } } # Статистика на преброяване от коригираните JSON файлове Write-Host " Преизчисляване на резюме от коригирани данни..." -ForegroundColor Gray $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("г-ММ-дд ЧЧ:мм:сс") } $pTotal = 0; $pUpdated = 0; $pErrors = 0; $pKI = 0; $pKEK = 0 $pTaskDis = 0; $pTempFail = 0; $pPermFail = 0; $pActionReq = 0; $pSBOff = 0; $pRIP = 0 действия ($cat в $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | Преобразуване от Json). Брой } улов { } } превключвател ($cat) { "updated_devices" { $pUpdated = $cnt } "грешки" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # изчислени "task_disabled" { $pTaskDis = $cnt } "temp_failures" { $pTempFail = $cnt } "perm_failures" { $pPermFail = $cnt } "action_required" { $pActionReq = $cnt } "secureboot_off" { $pSBOff = $cnt } "rollout_inprogress" { $pRIP = $cnt } } } $pNotUpdated = (Get-Content (Join-Path $dataDir "not_updated.json") -Raw | Преобразуване от Json). Брой $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Delta patch complete: $totalChanged устройства актуализирани" -ForegroundColor Green Write-Host " Общо: $pTotal | Актуализирано: $pUpdated | NotUpdated: $pNotUpdated | Грешки: $pErrors" – цвят на текста – бял # Актуализация на манифест $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} ( $jf в $jsonFiles) { $stNewManifest$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Размер = $jf. Дължина } } Save-FileManifest – манифест $stNewManifest – път $stManifestPath Write-Host " Завършена в $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch – $totalChanged устройства)" -ForegroundColor Green # Преминаване към пълен репроцес за поточно предаване за повторно генериране на HTML табло # Файловете с данни вече са коригирани, така че това гарантира, че таблото ще остане актуално Write-Host " Повторно генериране на табло от коригирани данни..." -ForegroundColor Жълто } друго { Write-Host " $changePct% файла са променени (>= 10%) - изисква се пълен репроцесор за поточно предаване" - ForegroundColor Жълто } } } } } # Създаване на поддиректория на данни за JSON файлове на устройство при поискване $dataDir = Join-Path $OutputPath "data" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Дедубликация чрез HashSet (O(1) за справка, ~ 50 МБ за 600K имена на хостове) $seenHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Lightweight summary counters (replaces $allDevices + $uniqueDevices in memory) $c = @{ Общо = 0; SBEnabled = 0; SBOff = 0 Актуализирано = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; NotSupported = 0; NoConfData = 0 TaskDisabled = 0; TaskNotFound = 0; TaskDisabledNotUpdated = 0 WithErrors = 0; InProgress = 0; NotYetInitiated = 0; RolloutInProgress = 0 WithKnownIssues = 0; WithMissingKEK = 0; TempFailures = 0; PermFailures = 0; NeedsReboot = 0 UpdatePending = 0 } # Проследяване на набори за AtRisk/SafeList (lightweight sets) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Batch-mode device data files: accumulate per-chunk, flush at chunk boundaries $stDeviceFiles = @("грешки"; "known_issues"; "missing_kek"; "not_updated"; "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required", "secureboot_off", "rollout_inprogress", "under_observation", "needs_reboot", "update_pending") $stDeviceFilePaths = @{}; $stDeviceFileCounts = @{} търсене на място ($dfName в $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath; "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim device record for JSON output (only essential fields, ~200 bytes vs ~2KB full) функция Get-SlimDevice { парам($Dev) връщане [поръчано]@{ HostName = $Dev.HostName WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } друго { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } друго { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" } KnownIssueId = ако ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } друго { $null } SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } друго { $null } UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } else { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } друго { $null } } } # Flush batch to JSON file (append mode) функция Flush-DeviceBatch { param([string]$StreamName, [System.Collections.Generic.List[object]]$Batch) ако ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() действия ($fDev в $Batch) { ако ($stDeviceFileCounts[$StreamName] -gt 0) { [void]$fSb.Append(",'n") } [void]$fSb.Append(($fDev | ConvertTo-Json -Compress)) $stDeviceFileCounts[$StreamName]++ } [System.IO.File]::AppendAllText($fPath, $fSb.ToString(), [System.Text.encoding]::UTF8) } # ОСНОВЕН ЦИКЪЛ НА ПОТОЧНО ПРЕДАВАНЕ $stChunkSize = ако ($totalFiles -le 10000) { $totalFiles } друго { 10000 } $stTotalChunks = [math]::Ceiling($totalFiles / $stChunkSize) $stPeakMemMB = 0 ако ($stTotalChunks -gt 1) { Write-Host "Обработка на $totalFiles файлове в $stTotalChunks части от $stChunkSize (поточно предаване, $ParallelThreads нишки):" -ForegroundColor Cyan } друго { Write-Host "Обработка на $totalFiles файлове (поточно предаване, $ParallelThreads нишки):" -ForegroundColor Cyan } за ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [math]::Min($cStart + $stChunkSize, $totalFiles) – 1 $cFiles = $jsonFiles[$cStart.. $cEnd на 10000? ако ($stTotalChunks -gt 1) { Write-Host " Блок $($ci + 1)/$stTotalChunks ($($cFiles.Count) файлове): " -NoNewline -ForegroundColor Gray } друго { Write-Host " Зареждане на файлове на $($cFiles.Count): " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Нишки $ParallelThreads # Списъци с папки на блок $cBatches = @{} foreach ($df в $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 игра ($raw в $rawDevices) { ако (-не $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Хост ако (-not $hostname) { continue } ако ($seenHostnames.Contains($hostname)) { $cDupe++; продължи } [void]$seenHostnames.Add($hostname) $cNew++; $c.Общо++ $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False") ако ($sbOn) { $c.SBEnabled++ } друго { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = ако ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } else { "" } $hasErr = (-not [низ]::IsNullOrEmpty($device. UEFICA2023Error) и "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false или "$($device. SecureBootTaskStatus)" -eq "Disabled" -или "$($device. SecureBootTaskStatus)" -eq "NotFound") $tskNF = ("$($device. SecureBootTaskStatus)" -eq "NotFound") $bid = ако ($device. PSObject.Properties['BucketId'] - и $device. BucketId) { "$($device. BucketId)" } else { "" } $e 1808 = ако ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } друго { 0 } $e 1801 = ако ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } друго { 0 } $e 1803 = ако ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } друго { 0 } $mKEK = ($e 1803 -gt 0 - или $device. ЛипсваKEK -eq $true или "$($device. MissingKEK)" -eq "True") $hKI = ((-not [низ]::IsNullOrEmpty($device. SkipReasonKnownIssue)) или (-not [string]::IsNullOrEmpty($device. KnownIssueId))) $rStat = if ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } друго { "" } $mfr = if ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } друго { "Неизвестен" } $bid = if (-not [низ]::IsNullOrEmpty($bid)) { $bid } друго { "" } # Предварително изчислен флаг "Чакаща актуализация" (приложени правила/WinCS, състояние все още не е актуализирано, SB ON, задачата не е забранена) $uefiStatus = ако ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } друго { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] и $device. WinCSKeyApplied -eq $true) $statusPending = ([низ]::IsNullOrEmpty($uefiStatus) -или $uefiStatus -eq "NotStarted" или $uefiStatus -eq "InProgress") $isUpdatePending = (($hasPolicy -или $hasWinCS) -и $statusPending -and -not $isUpd -и $sbOn -и -not $tskDis) ако ($isUpd) { $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Проследяване на актуализирани устройства, които се нуждаят от рестартиране (UEFICA2023Status=Updated, но Event1808=0) ако ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot ИЗКЛ. – извън обхвата, не класифицирайте по увереност } друго { if ($isUpdatePending) { } # Преброява се отделно в Чакащо актуализиране – взаимно изключващо се за кръгова диаграма elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ } else { $c.ActionReq++ } if ([string]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } ако ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } ако ($tskNF) { $c.TaskNotFound++ } if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ } ако ($hasErr) { $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["грешки"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Грешка if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ ако ($stErrorCodeSamples[$ec]. Брой -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } ако ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = if (-not [низ]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } друго { $device. KnownIssueId } if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } ако ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } ако (-not $isUpd -и ($tskDis -или (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } ако (-not $isUpd -и ((Test-ConfidenceLevel $conf "NotSupported") -или ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } ако ($e 1801 -gt 0 -и $e 1808 -eq 0 -and -not $hasErr -и $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } ако ($e 1801 -gt 0 -и $e 1808 -eq 0 -and -not $hasErr -и $rStat -ne "InProgress") { $c.NotYetInitiated++ } ако ($rStat -eq "InProgress" - и $e 1808 -eq 0) { $c.InProgress++ } # Чакаща актуализация: приложени правила или WinCS, чакащо състояние, SB ON, задачата не е забранена ако ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } ако (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # Под Устройства за наблюдение (отделно от Изисква се действие) ако (-not $isUpd -and (Test-ConfidenceLevel $conf "UnderObservation")) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Изисква се действие: не актуализиран, SB ON, не съответства на други доверителни категории, а не чакаща актуализация if (-not $isUpd -и $sbOn -и -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf "HighConfidence") -and -not (Test-ConfidenceLevel $conf "UnderObservation") и -not (Test-ConfidenceLevel $conf "TemporarilyPaused") -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Общо=0; Актуализирано=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; Сгрешени =0 } } $stMfrCounts[$mfr]. Общо++ ако ($isUpd) { $stMfrCounts[$mfr]. Актуализирано++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. АктуализацияPending++ } elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. Високо ниво ++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. Неподдържан++ } else { $stMfrCounts[$mfr]. ActionReq++ } ако ($hasErr) { $stMfrCounts[$mfr]. Сгрешени ++ } # Проследяване на всички устройства по набор (включително empty BucketId) $bucketKey = if ($bid -и $bid -ne "") { $bid } друго { "(празно)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Актуализирано=0; Manufacturer=$mfr; Модел=""; BIOS="" } ако ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } ако ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Брой ++ ако ($isUpd) { $stAllBuckets[$bucketKey]. Актуализирано++ } } # Изпразване на партиди на диск foreach ($df в $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Математически]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = ако ($cRem -gt 0) { " | ETA: ~$([Математически]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" } $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) ако ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew нов, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" –ForegroundColor Green } # Финализиране на JSON масиви игра ($dfName в $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: устройства с $($stDeviceFileCounts[$dfName]) " -ForegroundColor DarkGray } # Изчисляване на получените статистики $stAtRisk = 0; $stSafeList = 0 foreach ($bid в $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count – $b.Актуализация if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [math]::Max(0, $stAtRisk – $c.WithErrors) # NotUptodate = брой от not_updated партида (устройства с SB ON и неактуирани) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [поръчано]@{ ReportGeneratedAt = (Get-Date). ToString("г-ММ-дд ЧЧ:мм:сс") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Актуализирано = $c.Updated; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; TemporarilyPaused = $c.TempPaused; NotSupported = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; FullyUpdated = $c.Updated UpdatesPending = $stNotUptodate; UpdatesComplete = $c.Updated WithErrors = $c.WithErrors; InProgress = $c.InProgress; NotYetInitiated = $c.NotYetInitiated RolloutInProgress = $c.RolloutInProgress; WithKnownIssues = $c.WithKnownIssues WithMissingKEK = $c.WithMissingKEK; TemporaryFailures = $c.TempFailures; PermanentFailures = $c.PermFailures NeedsReboot = $c.NeedsReboot; UpdatePending = $c.UpdatePending AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList PercentWithErrors = if ($c.Total -gt 0) { [math]::Round(($c.WithErrors/$c.Total)*100,2) } друго { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } друго { 0 } PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } друго { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } друго { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } друго { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Общо*100,1) } друго { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Писане на CSVs [PSCustomObject]$stats | Export-Csv път (път към съединение $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Стойност.Обща сума } - Низходящ | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Ключ; Брой=$_. Стойност.Общо; Актуализирано=$_. Стойност.Актуализирана; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv път (път към съединение $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stErrorCodeCounts.GetEnumerator() | стойност на Sort-Object – низходящо | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Ключ; Брой=$_. Стойност; SampleDevices=($stErrorCodeSamples[$_. Ключ] -join ", ") } } | Export-Csv път (път към съединение $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -UTF8 кодиране $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } - Низходящ | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Ключ; Брой=$_. Value.Count; Актуализирано=$_. Стойност.Актуализирана; NotUpdated=$_. Value.Count-$_. Стойност.Актуализирана; Производител=$_. Value.Manufacturer } } | Export-Csv път (път към съединение $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8 # Генерирайте CSVs, съвместими с orchestrator (очаквани имена на файлове за Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" ако (тест-път $notUpdatedJsonPath) { опитайте { $nuData = Get-Content $notUpdatedJsonPath -Raw | Преобразуване от Json ако ($nuData.Брой -gt 0) { # NotUptodate CSV – orchestrator търси *NotUptodate*.csv $nuData | Export-Csv път (път към съединение $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Encoding UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) устройства)" –ForegroundColor Gray } } улов { } } # Писане на JSON данни за табло $stats | ConvertTo-Json -Depth 3 | Set-Content (Join-Path $dataDir "summary.json") -Encoding UTF8 # HISTORICAL TRACKING: Save data point for trend chart # Използвайте стабилно местоположение на кеша, така че данните за тенденциите да се запазват в папките на агрегираното агрегиране с времеви клеймо. # Ако OutputPath изглежда като "...\Aggregation_yyyyMMdd_HHmmss", кешът отива в родителската папка.# В противен случай кешът влиза в самия OutputPath.$parentDir = Split-Path $OutputPath – родител $leafName = Split-Path $OutputPath -Leaf ако ($leafName -match '^Aggregation_\d{8}' -или $leafName -eq 'Aggregation_Current') { # Папка с клеймо с дата и час, създадена от Orchestrator – използвайте родителя за стабилен кеш $historyPath = Join-Path $parentDir ".cache\trend_history.json" } друго { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath родител if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() ако (тест-път $historyPath) { опитайте { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } улов { $historyData = @() } } # Също така проверете в OutputPath\.cache\ (наследено местоположение от по-стари версии) # Обединяване на всички точки от данни, които още не са в основната хронология ако ($leafName -eq "Aggregation_Current" -или $leafName -match "^Aggregation_\d{8}") { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { опитайте { $innerData = @(Get-Content $innerHistoryPath -Raw | Преобразуване от Json) $existingDates = @($historyData | ForEach-Object { $_. Дата }) ( $entry в $innerData) { ако ($entry. Дата и $entry. Дата –няма $existingDates) { $historyData += $entry } } ако ($innerData.Брой -gt 0) { Write-Host " Обединени точки от данни на $($innerData.Count) от вътрешния кеш" -ForegroundColor DarkGray } } улов { } } }
# BOOTSTRAP: Ако хронологията на тенденцията е празна / разредена, реконструирайте от хронологични данни if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -или $leafName -eq "Aggregation_Current")) { Write-Host " Bootstrapping trend history from historical data..." -ForegroundColor Yellow $dailyData = @{} # Източник 1: Резюме на CSVs в текущата папка (Aggregation_Current запазва всички Резюме CSVs) $localSummaries = Get-ChildItem $OutputPath -Филтър "SecureBoot_Summary_*.csv" -EA SilentlyContinue | име на Sort-Object ( $summCsv в $localSummaries) { опитайте { $summ = Import-Csv $summCsv.ПълноИме | Select-Object - първи 1 ако ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 – и $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("гггг-ММ-дд") $updated = ако ($summ. Актуализирано) { [int]$summ. Актуализирано } друго { 0 } $notUpd = ако ($summ. NotUptodate) { [int]$summ. NotUptodate } друго { [int]$summ. TotalDevices – $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Дата = $dateStr; Общо = [int]$summ. TotalDevices; Актуализирано = $updated; NotUpdated = $notUpd NeedsReboot = 0; Грешки = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } друго { 0 } } } } улов { } } # Източник 2: Стари времеви клеймо Aggregation_* папки (наследени, ако все още съществуват) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Име -match '^Aggregation_\d{8}' } | име на Sort-Object ( $folder в $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Филтър "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object - първи 1 ако ($summCsv) { опитайте { $summ = Import-Csv $summCsv.ПълноИме | Select-Object - първи 1 ако ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Име -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*", "$1-$2-$3" $updated = ако ($summ. Актуализирано) { [int]$summ. Актуализирано } друго { 0 } $notUpd = ако ($summ. NotUptodate) { [int]$summ. NotUptodate } друго { [int]$summ. TotalDevices – $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Дата = $dateStr; Общо = [int]$summ. TotalDevices; Актуализирано = $updated; NotUpdated = $notUpd NeedsReboot = 0; Грешки = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } друго { 0 } } } } улов { } } } # Източник 3: RolloutState.json WaveHistory (има времево клеймо за всяка вълна от ден 1) # Това предоставя базови точки от данни дори когато не съществуват стари папки за агрегиране $rolloutStatePaths = @( (Път на съединение $parentDir "RolloutState\RolloutState.json"), (Път на съединение $OutputPath "RolloutState\RolloutState.json") ) търсене на място ($rsPath в $rolloutStatePaths) { ако (тест-път $rsPath) { опитайте { $rsData = Get-Content $rsPath -Raw | Преобразуване от Json if ($rsData.WaveHistory) { # Използвайте датите за начало на вълната като точки от данни за тенденцията # Изчисляване на кумулативните устройства, насочени към всяка вълна $cumulativeTargeted = 0 foreach ($wave в $rsData.WaveHistory) { ако ($wave. Стартирана на -and $wave. DeviceCount) { $waveDate = ([datetime]$wave. Стартирана на). ToString("гггг-ММ-дд") $cumulativeTargeted += [int]$wave. DeviceCount if (-not $dailyData.ContainsKey($waveDate)) { # Приблизително: при начален час на вълната са актуализирани само устройствата от предишните вълни $dailyData[$waveDate] = [PSCustomObject]@{ Дата = $waveDate; Общо = $c.Общо; Актуализирано = [math]::Max(0, $cumulativeTargeted – [int]$wave. DeviceCount) NotUpdated = $c.Total – [math]::Max(0, $cumulativeTargeted – [int]$wave. DeviceCount) NeedsReboot = 0; Грешки = 0; ActionRequired = 0 } } } } } } улов { } break # Use first found } }
ако ($dailyData.Брой -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object ключ | ForEach-Object { $_. Стойност }) Write-Host " Стартирани $($historyData.Count) точки от данни от исторически резюмета" - ForegroundColor Green } }
# Добавяне на текуща точка от данни (deduplicate by day - keep latest per day) $todayKey = (Get-Date). ToString("гггг-ММ-дд") $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" } ако ($existingToday) { # Заместване на днешния запис $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Дата = $todayKey Общо = $c.Общо Актуализирано = $c.Актуализирано NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Грешки = $c.WithErrors ActionRequired = $c.ActionReq } # Премахване на неправилни точки от данни (общо 0) и запазване на последните 90 $historyData = @($historyData | Where-Object { [int]$_. Общо -0 }) # Без главни букви — данните за тенденцията са ~100 байта/запис, пълна година = ~36 КБ $historyData | ConvertTo-Json -Depth 3 | Set-Content $historyPath – кодиране UTF8 Write-Host " Хронология на тенденцията: $($historyData.Count) точки от данни" -ForegroundColor DarkGray # Build trend chart data for HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)"" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Актуализирано }) -join "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Общо }) -join "," # Прожектиране: разширяване на линията на тренда с помощта на експоненциално удвояване (2,4,8,16...) # Извлича размера на вълната и период на наблюдение от действителни данни за хронологията на тенденцията.# - Размер на вълната = най-голямото увеличение в еднократния период, наблюдавано в историята (най-скорошната внедрена вълна) # - Дни на наблюдение = средни календарни дни между точките от данни за тенденцията (колко често се изпълняват) # След това удвоя размера на вълната всеки период, като съпоставя 2x стратегията за растеж на оркестъра.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Актуализиран $remaining = $stNotUptodate # Само SB-ON не актуализирани устройства (изключва SecureBoot ИЗКЛ.) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Извличане на размера на вълната и период на наблюдение от хронологията на тенденцията $increments = @() $dayGaps = @() for ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Актуализирано – $historyData[$hi-1]. Актуализира ако ($inc -gt 0) { $increments += $inc } опитайте { $d 1 = [datetime]::P arse($historyData[$hi-1]. Дата) $d 2 = [datetime]::P arse($historyData[$hi]. Дата) $gap = ($d 2 – $d 1). Общо дни ако ($gap -gt 0) { $dayGaps += $gap } } улов {} } # Размер на вълната = най-скорошната положителна стъпка (текуща вълна), връщане към средна стойност, минимум 2 $waveSize = ако ($increments. Брой -gt 0) { [математически]::Max(2, $increments[-1]) } друго { 2 } # Период на наблюдение = средна междина между точките от данни (календарни дни на вълна), минимум 1 $waveDays = ако ($dayGaps.Брой -gt 0) { [math]::Max(1; [math]::Round(($dayGaps | Measure-Object –Average). Средно, 0)) } друго { 1 }
Write-Host " Прожектиране: waveSize=$waveSize (от последната стъпка), waveDays=$waveDays (средно разстояние от хронологията)" -ForegroundColor DarkGray
$dayCounter = 0 # Прожектиране, докато всички устройства не се актуализират или максимум 365 дни за ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ При всяка граница на периода на наблюдение разположете вълна, след което я ако ($dayCounter -ge $waveDays) { $devicesThisWave = [math]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Актуализирано + $stNotUptodate)) { $lastUpdated = $c.Актуализирано + $stNotUptodate; $remaining = 0 } # Размер на двойна вълна за следващия период (стратегия за orchestrator 2x) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd"))")")"" $projValues += $lastUpdated $projNotUpdValues += [математически]::Max(0, $remaining) ако ($remaining -le 0) { break } } $projLabels = $projDates -join "," $projUpdated = $projValues -join "," $projNotUpdated = $projNotUpdValues -join "," $hasProjection = $projDates.Count -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host " Прожектиране: трябват поне 2 точки от данни за тенденции, за да извлечете времената на вълните" -ForegroundColor DarkGray } # Създаване на комбинирани низове за данни на диаграма за тук-низа $allChartLabels = ако ($hasProjection) { "$trendLabels,$projLabels" } друго { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } друго { "" } $projNotUpdJS = ако ($hasProjection) { $projNotUpdated } други { "" } $histCount = ($historyData | Measure-Object). Брой $stMfrCounts.GetEnumerator() | Sort-Object { $_. Стойност.Обща сума } - Низходящ | ForEach-Object { @{ име=$_. Ключ; total=$_. Стойност.Общо; updated=$_. Стойност.Актуализирана; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Depth 3 | Set-Content (път към съединение $dataDir "manufacturers.json") – кодиране UTF8 # Конвертиране на JSON файлове с данни в CSV файл за четими от човек файлове за изтегляне на Excel Write-Host "Конвертиране на данни за устройството в CSV файл за изтегляне на Excel..." – Цвят на текста Сиво действия ($dfName в $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" ако (тест-път $jsonFile) { опитайте { $jsonData = Get-Content $jsonFile -Raw | Преобразуване от Json ако ($jsonData.Брой -gt 0) { # Включване на допълнителни колони за update_pending CSV $selectProps = ако ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } друго { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv път $csvFile -NoTypeInformation – кодиране UTF8 Write-Host " $dfName > $($jsonData.Count) редове – > CSV" – Цвят на текста DarkGray } } улов { Write-Host " $dfName – пропуснато" -ForegroundColor DarkYellow } } } # Генериране на самостоятелно HTML табло $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Генериране на самостоятелно HTML табло..." - ForegroundColor Жълто # VELOCITY PROJECTION: Изчисляване от хронологията на сканирането или предишното резюме $stDeadline = [datetime]"2026-06-24" # KEK cert expiry $stDaysToDeadline = [math]::Max(0, ($stDeadline – (Get-Date)). дни) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "Няма" $stWorkingDays = 0 $stCalendarDays = 0 # Изпробвайте първо хронологията на тренд (лека, вече поддържана от агрегатор — замества подут ScanHistory.json) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Общо -gt 0 -и [int]$_. Актуализирано -ge 0 }) if ($validHistory.Count -ge 2) { $prev = $validHistory[-2]; $curr = $validHistory[-1] $prevDate = [datetime]::P arse($prev. Date.Substring(0, [Математически]::Min(10, $prev. Дата.дължина))) $currDate = [datetime]::P arse($curr. Date.Substring(0, [Математически]::Min(10, $curr. Дата.дължина))) $daysDiff = ($currDate – $prevDate). Общо дни ако ($daysDiff -gt 0) { $updDiff = [int]$curr. Актуализирано – [int]$prev. Актуализира ако ($updDiff -gt 0) { $stDevicesPerDay = [math]::Round($updDiff / $daysDiff; 0) $stVelocitySource = "TrendHistory" } } } } # Try orchestrator rollout summary (has pre-computed velocity) ако ($stVelocitySource -eq "Няма" и $RolloutSummaryPath и (Тест-път $RolloutSummaryPath)) { опитайте { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | Преобразуване от Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [math]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = "Orchestrator" if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } улов { } } # Резервен: изпробвайте предишното резюме CSV (търсене в текущата папка И папки за агрегиране на родител/елемент от същото ниво) ако ($stVelocitySource -eq "Няма") { $searchPaths = @( (Път към съединение $OutputPath "SecureBoot_Summary_*.csv") ) # Също така търсене на папки с агрегиране на елемент от същото ниво (orchestrator създава нова папка при всяко изпълнение) $parentPath = Split-Path $OutputPath – родител ако ($parentPath) { $searchPaths += (път на съединение $parentPath "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += (път към съединение $parentPath "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descending | Select-Object -първи 1 ако ($prevSummary) { опитайте { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) – $prevDate). Общо дни ако ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Актуализация - $prevUpdated ако ($updDelta -gt 0) { $stDevicesPerDay = [math]::Round($updDelta / $daysSinceLast; 0) $stVelocitySource = "PreviousReport" } } } улов { } } } # Fallback: изчисляване на скорост от пълната хронология на тенденциите (първа спрямо най-новата точка от данни) if ($stVelocitySource -eq "N/A" и $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Общо -gt 0 -и [int]$_. Актуализирано -ge 0 }) if ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime]::P arse($first. Date.Substring(0, [Математически]::Min(10, $first. Дата.дължина))) $lastDate = [datetime]::P arse($last. Date.Substring(0, [Математически]::Min(10, $last. Дата.дължина))) $daysDiff = ($lastDate – $firstDate). Общо дни ако ($daysDiff -gt 0) { $updDiff = [int]$last. Актуализирано – [int]$first. Актуализира ако ($updDiff -gt 0) { $stDevicesPerDay = [математически]::Round($updDiff / $daysDiff; 1) $stVelocitySource = "TrendHistory" } } } } # Изчисляване на прожектиране с помощта на експоненциално удвояване (в съответствие с диаграмата на тенденцията) # Повторно използване на данните за прожектиране, които вече са изчислени за диаграмата, ако има такива ако ($hasProjection и $projDates.Брой -gt 0) { # Използвайте последната прогнозна дата (когато всички устройства са актуализирани) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([дата и час]::P arse($lastProjDateStr)). ToString("MMM дд, гггг") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) – (Get-Date)). Дни $stWorkingDays = 0 $d = Get-Date за ($i = 0; $i -lt $stCalendarDays; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne "saturday" -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -и $stNotUptodate -gt 0) { # Резервен: линейна прожектиране, ако няма налични експоненциални данни $daysNeeded = [math]::Ceiling($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM дд, гггг") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Получаване на дата за ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne "saturday" -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Build velocity HTML $velocityHtml = ако ($stDevicesPerDay -gt 0) { "<div><strong>🚀 Устройства/Ден:</strong> $($stDevicesPerDay.ToString('N0')) (източник: $stVelocitySource)</div>" + "<div><strong>📅 Прожектиране:</strong> $stProjectedDate" + $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>⚠ ПРОСРОЧЕН КРАЕН СРОК</span>" } else { " <span style='color:#28a745'>✓ Преди крайния срок</span>" }) + "</div>" + "<div><strong>🕐 Работни дни:</strong> $stWorkingDays | <силни>Calendar Дни:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Краен срок: 24 юни 2026 г. (изтичане на срока на KEK сертификата) | Оставащи дни: $stDaysToDeadline</div>" } друго { "<div style='padding:8px; фон:#fff3cd; гранична радиус:4px; border-left:3px solid #ffc107'>" + "<силни>📅 Прожектиране:</strong> Недостатъчни данни за изчисляване на скоростта. " " + "Изпълнете агрегиране поне два пъти с промени в данните, за да определите ставка.<br/>" + "<сигурна>Краен срок:</strong> 24 юни 2026 г. (изтичане на срока на сертификата KEK) | <на силните>дни:</strong> $stDaysToDeadline</div>" } # Обратно отброяване при изтичане на срока на сертификата $certToday = Получаване на дата $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [дата и час]"2026-06-27" $certPcaExpiry = [дата и час]"2026-10-19" $daysToKek = [math]::Max(0, ($certKekExpiry – $certToday). дни) $daysToUefi = [math]::Max(0, ($certUefiExpiry – $certToday). дни) $daysToPca = [math]::Max(0, ($certPcaExpiry – $certToday). дни) $certUrgency = ако ($daysToKek -lt 30) { "#dc3545" } elseif ($daysToKek -lt 90) { "#fd7e14" } друго { "#28a745" } # Helper: Read records from JSON, build bucket summary + first N device rows $maxInlineRows = 200 функция Build-InlineTable { param([string]$JsonPath; [int]$MaxRows = 200; [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 ако (тест-път $JsonPath) { опитайте { $data = Get-Content $JsonPath -Raw | Преобразуване от Json $totalCount = $data. Брой # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats ако ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | Sort-Object брой – низходящо $bucketSummary = "><2 h3 style='font-size:.95em; цвят:#333; margin:10px 0 5px'><3 По хардуерен набор ($($buckets. Count) набори)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:15px'><table><thead><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Актуализирано</th><th style='text-align:right; color:#dc3545'>Не е актуализирано</th><th><1 Manufacturer><2 /th></tr></thead><tbody>" игра ($b в $buckets) { $bid = ако ($b.Name) { $b.Name } друго { "(празно)" } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Получаване на актуализиран брой от глобални статистика на наборите (всички устройства в този набор от данни в целия набор от данни) $lookupKey = $bid $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null } $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } друго { 0 } $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } друго { $b.Count } $bNotUpdatedGlobal = $bTotalGlobal – $bUpdatedGlobal $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; font-weight:bold'>$bTotalGlobal><8 /td><td style='text-align:right; цвят: #28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; цвят:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n' } $bucketSummary += "</tbody></table></div>" } # DEVICE DETAIL: First N rows as flat list $slice = $data | Select-Object – първи $MaxRows действия ($d в $slice) { $conf = $d.ConfidenceLevel $confBadge = ако ($conf -match "High") { "<span class="badge badge-success">High Conf><2 /span>" } elseif ($conf -match "Not Sup") { "<span class="badge badge-danger">Not Supported><6 /span>" } elseif ($conf -match "Under") { "<span class="badge-info">Under Obs><0 /span>" } elseif ($conf -match "Paused") { "<span class="badge badge-warning">Поставени на пауза><4 /span>" } else { '<span class="badge-warning">Req><8 /span>" } $statusBadge = ако ($d.IsUpdated) { "><00 span class="badge badge-success"><01 Актуализирано</span>" } elseif ($d.UEFICA2023Грешка) { "><04 span class="badge badge-danger"><05 грешка</span>" } else { '><08 span class="badge-warning"><09 Чакащи><0 /span>" } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><16 ><20 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 /td td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 "n" } } улов { } } ако ($totalCount -eq 0) { return "><44 div style='padding:20px; цвят:#888; font-style:italic'><45 Няма устройства в тази категория.><46 /div>" } $showing = [math]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Общо: $($totalCount.ToString("N0")) устройства" ако ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>📄 Изтегляне на пълния CSV файл за Excel><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; цвят:#333; margin:10px 0 5px'><58 Подробности за устройството (показване на първия $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; таблица overflow-y:auto'><><thead><tr><><0 HostName><1 /th><><4 Manufacturer><5 /th><><8 Model><9 /><><2 Confidence><3 /th><th><><6 състоянието><7 /th><><1><><0><1><4 BucketId><5 /></tr></thead><><2 $deviceRows><3 /tbody></table></div>" return "$header$bucketSummary$deviceHeader$deviceTable" } # Създайте вградени таблици от JSON файловете, които вече са на диска, като свържете CSVs $tblErrors = Build-InlineTable -JsonPath ($dataDir "errors.json на съединение") -MaxRows $maxInlineRows CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath ($dataDir "known_issues.json на съединение") -MaxRows $maxInlineRows CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath (път на съединение $dataDir "missing_kek.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath ($dataDir "not_updated.json на съединение") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath ($dataDir "task_disabled.json на съединение") -MaxRows $maxInlineRows CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath (път на съединение $dataDir "temp_failures.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath (път към съединение $dataDir "perm_failures.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath (път към съединение $dataDir "updated_devices.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath (път към съединение $dataDir "action_required.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath (път към съединение $dataDir "under_observation.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath (път към съединение $dataDir "needs_reboot.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath (път към съединение $dataDir "secureboot_off.json") -MaxRows $maxInlineRows CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath (join-path $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # Custom table for Update Pending – includes UEFICA2023Status and UEFICA2023Error columns $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" ако (тест-път $upJsonPath) { опитайте { $upData = Get-Content $upJsonPath -Raw | Преобразуване от Json $upCount = $upData.Count ако ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Общо: устройства с $($upCount.ToString("N0")) | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>📄 Изтегляне на пълния CSV файл за Excel><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object – Първи $maxInlineRows ( $d в $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>" } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545">$($d.UEFICA2023Error)</span>" } else { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = if ($d.WinCSKeyApplied) { "<span class="badge badge-success">Yes><8 /span>" } else { '-' } $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n' } $upShowing = [математически]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; цвят:#333; margin:10px 0 5px'>Подробности за устройството (показване на първия $upShowing)</h3>" $upTable = "<div style='max-height:500px; overflow-y:auto'><table><thead><tr><><9 HostName><0 /th><><3 Manufacturer><4 /th><3 Manufacturer><4 /1><7 модел><8><><><1 UEFICA2023Status><2 /><><5 UEFICA2023Грешка><6 /th><правилата на><9</><>ключ<winCS /><><>><BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } друго { $tblUpdatePending = "<div style='padding:20px; цвят:#888; font-style:italic'>Няма устройства в тази категория.</div>" } } улов { $tblUpdatePending = "<div style='padding:20px; цвят:#888; font-style:italic'>Няма устройства в тази категория.</div>" } } друго { $tblUpdatePending = "<div style='padding:20px; цвят:#888; font-style:italic'>Няма устройства в тази категория.</div>" } # Обратно отброяване при изтичане на срока на сертификата $certToday = Get-Date $certKekExpiry = [дата и час]"2026-06-24" $certUefiExpiry = [дата и час]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [math]::Max(0, ($certKekExpiry – $certToday). дни) $daysToUefi = [math]::Max(0, ($certUefiExpiry – $certToday). дни) $daysToPca = [math]::Max(0, ($certPcaExpiry – $certToday). дни) $certUrgency = if ($daysToKek -lt 30) { "#dc3545" } elseif ($daysToKek -lt 90) { "#fd7e14" } друго { "#28a745" } # Build manufacturer chart data inline (Top 10 by device count) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Стойност.Обща сума } - Низходящ | Select-Object -Първи 10 $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "От производител" } друго { "Първите 10 производители" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Ключ)"" }) -join "," $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) - join "," $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) – присъединяване към "," $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) – съединение "," $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) – съединение "," $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) – присъединяване към "," $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) – съединение "," $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) – съединение "," $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) – съединение "," $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) – съединяване на "," # Build таблица на производителя $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Стойност.Обща сума } - Низходящ | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Ключ)</td><td>$($_. Value.Total.ToString("N0"))</td><td>$($_. Value.Updated.ToString("N0"))</td><td>$($_. Value.HighConf.ToString("N0"))><0 /td><td>$($_. Value.ActionReq.ToString("N0"))><4 /td></tr>"n" } $htmlContent = @" <! DOCTYPE html> <html lang="en"> <главата><3 <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> заглавие на <><9 таблото за състояние на сертификата за защитено стартиране><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 <стил><7 *{box-sizing:border-box; марж:0; допълване:0} body{font-family:'Segoe UI',Tahoma,sans-serif; фон:#f0f2f5; цвят:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); цвят:#fff; padding:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; непрозрачност:.9} .container{max-width:1400px; поле:0 автоматично; padding:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px;1fr)); междина:12px; margin:20px 0} .card{background:#fff; гранична радиус:10px; padding:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; font-weight:700} .card .label{font-size:.8em; цвят:#666; margin-top:4px} .card .pct{font-size:.75em; цвят:#888} .section{background:#fff; гранична радиус:10px; padding:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; цвят:#1a237e; margin-bottom:10px; cursor:pointer; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; междина:20px; margin:20px 0} .chart-box{background:#fff; гранична радиус:10px; padding:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} таблица{width:100%; border-collapse:collapse; font-size:.85em} th{background:#e8eaf6; padding:8px 10px; подравняване на текста:отляво; позиция:лепкава; връх:0; z-индекс:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; padding:2px 8px;border-radius:10px; font-size:.75em; font-weight:700} .badge-success{background:#d4edda; цвят:#155724} .badge-danger{background:#f8d7da; цвят:#721c24} .badge-warning{background:#fff3cd; цвят:#856404} .badge-info{background:#d1ecf1; цвят:#0c5460} .top-link{float:right; font-size:.8em; цвят:#1a237e; text-decoration:none} .footer{text-align:center; padding:20px; цвят:#999; font-size:.8em} a{color:#1a237e} </style><9 </head> <на тялото> <div class="header"> <h1>табло за състояние на сертификат за защитено стартиране</h1> <div class="meta">Генерирано: $($stats. ReportGeneratedAt) | Общо устройства: $($c.Total.ToString("N0")) | Уникални набори: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">
<!-- KPI карти – върху които може да се щраква, свързани със секции – > <div class="cards"> <клас="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; отгоре:8px; отдясно:8px; фон:#dc3545; цвят:#fff; padding:1px 6px; гранична радиус:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString("("N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% - NEEDS ACTION><0 /div></a><3 <клас="card" href="#s upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; отгоре:8px; отдясно:8px; фон:#28a745; цвят:#fff; padding:1px 6px; гранична радиус:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString("(">$($c.Updated.ToString("N0"))</div><div class="label">Актуализирано><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3 <a class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; отгоре:8px; отдясно:8px; фон:#6c757d; цвят:#fff; padding:1px 6px; гранична радиус:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.SBOff/$c.Total)*100;1)}else{0})% – Извън обхвата><0 /div></a><3 <a class="card" href="#s-nrb" onclick="openSection('d-nrb')" style="border-left-color:#ffc107; text-decoration:none"><div class="value" style="color:#ffc107">$($c.NeedsReboot.ToString("N0"))</div><div class="label">Нуждае се от рестартиране><2 /div><div class="label "pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% – очаква рестартиране><6 /div></a><9 <a class="card" href="#s upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Чакаща актуализация</div><<div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% – приложени правила/WinCS, очакване на актуализация><2 /div></a><5 <a class="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Внедряване в ход><4 /div><div class="pct". >$(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{0})%</div></a><11 <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0"))</div><div class="label">><20 с висока достоверност /div><div class="pct">$($stats. PercentHighConfidence)% – Безопасно за внедряване><24 /div></a><27 <a class="card" href="#s-uo" onclick="openSection('d-uo')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value" style="color:#ffc107"><1 $($c.UnderObs.ToString("N0"))><2 /div><div class="label"><5 Under Observation><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100;1)}else{0})%</div></a><3 <a class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0"))</div><div class="label">Изисква се действие><2 /div><div class="pct">$($stats. PercentActionRequired)% – трябва да тества><6 /div></a><9 <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($stAtRisk.ToString("N0"))</div><div class="label">В риск><68 /div><div class="pct">$($stats. PercentAtRisk)% – Подобно на неуспешното><2 /div></a><5 <a class="card" href="#s-td" onclick="openSection('d-td')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.TaskDisabled.ToString("N0"))</div><div class="label">Task Disabled><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100;1)}else{0})% – Блокирано><8 /div></a><91 <a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.TempPaused.ToString("N0"))</div><div class="label">Temp. В пауза</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Известни проблеми><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0"))</div><div class="label">Липсва KEK</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0})%</div></a> <клас="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithErrors.ToString("N0"))</div><div class="label">With Errors</div><div class="pct"><1 $($stats. PercentWithErrors)% – UEFI грешки</div></a> ><6 a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545"><9 $($c.TempFailures.ToString("N0"))</div><div class="label">Temp. Грешки</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100;1)}else{0})%</div></a> <a class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0"))</div><div class="label">Not Supported><6 /div><div class=div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
<!-- изтичане на скоростта на разполагане & cert – > <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; междина:20px; margin:15px 0"> <div class="section" style="margin:0"> <h2>📅 Deployment Velocity</h2> <div class="section-body open"> <div style="font-size:2.5em; font-weight:700; color:#28a745">$($c.Updated.ToString("N0"))</div> <div style="color:#666">устройства са актуализирани от $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; фон:#e8eaf6; височина: 20px; гранична радиус:10px; overflow:hidden"><div style="background:#28a745; височина: 100%; width:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; цвят:#888">$($stats. PercentCertUpdated)% завършено</div> <div style="margin-top:10px; padding:10px; фон:#f8f9fa; гранична радиус:8px; font-size:.85em"> <div><силен оставащ>:</strong> устройства с $($stNotUptodate.ToString("N0")) трябва да предприемат действие</div> <div><силни>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) устройства (грешки + дезактивирани постоянни + задачи)</div> <div><сигурна>Safe за разполагане:</strong> устройства с $($stSafeList.ToString("N0")) (същия набор като успешните)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; border-left:4px solid #dc3545"> <h2 style="color:#dc3545">⚠<</h2> <div class="section-body open"> <div style="display:flex; междина:15px; margin-top:10px"> <div style="text-align:center; padding:15px; гранична радиус:8px; минимална ширина:120px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); border:2px solid #dc3545; flex:1"> <div style="font-size:.65em; цвят:#721c24; преобразуване на текст:главни букви; font-weight:bold">⚠ ПЪРВО ДА ИЗТЕЧЕ</div> ><4 div style="font-size:.85em; font-weight:bold; цвят:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; font-weight:700; цвят:#dc3545; височина на линия:1"><9 $daysToKek</div> ><2 div style="font-size:.8em; color:#721c24"><3 дни (24 юни 2026 г.)><4 /div> ><6 /div> ><8 div style="text-align:center; padding:15px; гранична радиус:8px; минимална ширина:120px; background:linear-gradient(135deg,#fffef5,#fff3cd); border:2px solid #ffc107; flex:1"><9 <div style="font-size:.65em; цвят:#856404; преобразуване на текст:главни букви; font-weight:bold">UEFI CA 2011</div> <div id="daysUefi" style="font-size:2.2em; font-weight:700; цвят:#856404; височина на линия:1; margin:5px 0">$daysToUefi</div> <div style="font-size:.8em; color:#856404">дни (27 юни 2026 г.)</div> </div> <div style="text-align:center; padding:15px; гранична радиус:8px; минимална ширина:120px; background:linear-gradient(135deg,#f0f8ff,#d4edff); граница:2px плътна #0078d4; flex:1"> <div style="font-size:.65em; цвят:#0078d4; преобразуване на текст:главни букви; font-weight:bold">Windows PCA</div> <div id="daysPca" style="font-size:2.2em; font-weight:700; цвят:#0078d4; височина на линия:1; margin:5px 0">$daysToPca><2 /div><3 <div style="font-size:.8em; color:#0078d4">дни (19 октомври 2026 г.)</div><7 </div><9 </div><1 <div style="margin-top:15px; padding:10px; фон:#f8d7da; гранична радиус:8px; font-size:.85em; border-left:4px solid #dc3545"> <силен>⚠ CRITICAL:</strong> Всички устройства трябва да се актуализират преди изтичането на срока на сертификата. Устройства, които не са актуализирани до крайния срок, не могат да се приложат бъдещи актуализации на защитата за диспечера за зареждане и защитеното стартиране след изтичане на срока.</div> </div> </div> </div>
<!-- диаграми --> <div class="charts"> <div class="chart-box"><h3 състояние на разполагане></h3><ИД на платно="deployChart" height="200"></canvas></div><5 <div class="chart-box"><h3><9 $mfrChartTitle</h3><иД на платното="mfrChart" height="200"></canvas></div> </div>
$(if ($historyData.Count -ge 1) { "<!-- историческа диаграма на тенденцията - > <div class='section'> <h2 onclick='"toggle('d-trend')'>📈 Актуализиране на напредъка във времето <class='top-link' href='#'>↑ Топ</a></h2> <div id='d-trend' class='section-body open'> <ИД на платно='trendChart' height='120'></canvas> <div style='font-size:.75em; цвят:#888; margin-top:5px'>Solid lines = actual data$(if ($historyData.Count -ge 2) { " | Пунктирана линия = прожектиране (експоненциално удвояване: 2→4→8→16... устройства на вълна)" } друго { " | Изпълнете отново агрегирането утре, за да видите линиите на тренд и прожектирането" })</div> </div> </div>" })
<!-- на csv файлове за изтегляне – > <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Изтегляне на пълни данни (CSV за Excel) <a class="top-link" href="#">Top</a></h2><2 <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; междина:5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; фон:#dc3545; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em">не е актуализиран ($($stNotUptodate.ToString("N0")))</a><8 <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; фон:#dc3545; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em">Грешки ($($c.WithErrors.ToString("N0")))</a> <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; фон:#fd7e14; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em"изисква >действие ($($c.ActionReq.ToString("N0")))</a> <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; фон:#dc3545; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em">известни проблеми ($($c.WithKnownIssues.ToString("N0")))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; фон:#dc3545; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em">Задача е забранена ($($c.TaskDisabled.ToString("N0")))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; фон:#28a745; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em">Актуализирано ($($c.Updated.ToString("N0")))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; фон:#6c757d; цвят:#fff; padding:6px 14px; гранична радиус:5px; text-decoration:none; font-size:.8em">Резюме</a> <div style="width:100%; font-size:.75em; цвят:#888; margin-top:5px">CSV файловете се отварят в Excel. Налично, когато се хоства на web server.</div> </div> </div>
Разбивка на производителя на<!-- --> <div class="section"> <h2 onclick="toggle('mfr')">By Manufacturer <a class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <таблицата><><><><2><1 производителя /><><5><6><6><6><актуализира><0 на><9><><3><4 с висока достоверност /><><7 изисква><8 /></tr></thead><3 <><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- секции на устройство (първо вградено 200 + csv изтегляне) – > <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Устройства с грешки ($($c.WithErrors.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-err" class="section-body">$tblErrors</div> </div> <div class="section" id="s-ki"> <h2 onclick="toggle('d-ki')" style="color:#dc3545">🔴 Известни проблеми ($($c.WithKnownIssues.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-ki" class="section-body">$tblKI</div> </div> <div class="section" id="s-kek"> <h2 onclick="toggle('d-kek')">🟠 Липсва KEK – събитие 1803 ($($c.WithMissingKEK.ToString("N0"))) <клас="top-link" href="#">↑ Top</a></h2> >↑ 0 div id="d-kek" class="section-body">↑ 1 $tblKEK</div> >↑ 4 /div> >↑ 6 div class="section" id="s-ar">↑ 7 >↑ 8 h2 onclick="toggle('d-ar')" style="color:#fd7e14">🟠 Изисква се действие ($($c.ActionReq.ToString("N0"))) <a class="top-link" href="#">↑ Топ><4 /a></h2><7 <div id="d-ar" class="section-body">$tblActionReq</div> </div> <div class="section" id="s-uo"> <h2 onclick="toggle('d-uo')" style="color:#17a2b8">🔵 Под Observation ($($c.UnderObs.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-uo" class="section-body">$tblUnderObs</div> </div> <div class="section" id="s-nu"> <h2 onclick="toggle('d-nu')" style="color:#dc3545">🔴 Не е актуализиран ($($stNotUptodate.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-nu" class="section-body">$tblNotUpd</div> </div> >↑ 0 div class="section" id="s-td">↑ 1 >↑ 2 h2 onclick="toggle('d-td')" style="color:#dc3545">🔴 Задачата е забранена ($($c.TaskDisabled.ToString("N0"))) >↑ 5 a class="top-link" href="#">↑ Top</a></h2><1 <div id="d-td" class="section-body">$tblTaskDis><4 /div><5 </div><7 <div class="section" id="s-tf"> <h2 onclick="toggle('d-tf')" style="color:#dc3545">🔴 Временни грешки ($($c.TempFailures.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-tf" class="section-body">$tblTemp</div> </div> <div class="section" id="s-pf"> <h2 onclick="toggle('d-pf')" style="color:#721c24">🔴 Постоянни неуспешни или неподдържани ($($c.PermFailures.ToString("N0"))) <клас="top-link" href="#">↑ най-</a></h2> <div id="d-pf" class="section-body">$tblPerm</div> </div> <div class="section" id="s-upd-pend"> <h2 onclick="toggle('d-upd-pend')" style="color:#6f42c1">⏳ Чакаща актуализация ($($c.UpdatePending.ToString("N0"))) – Приложени правила/WinCS, Очаква се актуализацията <клас="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">устройства, където е приложен ключ AvailableUpdatesPolicy или WinCS, но UEFICA2023Status все още е NotStarted, InProgress или null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Внедряване в ход ($($c.RolloutInProgress.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-rip" class="section-body">$tblRolloutIP</div> </div> <div class="section" id="s-sboff"> <h2 onclick="toggle('d-sboff')" style="color:#6c757d">⚫ SecureBoot OFF ($($c.SBOff.ToString("N0"))) – Извън обхвата <клас="top-link" href="#">↑ Top</a></h2> <div id="d-sboff" class="section-body">$tblSBOff</div> </div> <div class="section" id="s-upd"> <h2 onclick="toggle('d-upd')" style="color:#28a745">🟢 Актуализирани устройства ($($c.Updated.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd" class="section-body">$tblUpdated</div> </div> <div class="section" id="s-nrb"> <h2 onclick="toggle('d-nrb')" style="color:#ffc107">🔄 Актуализирано – Нуждае се от рестартиране ($($c.NeedsReboot.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>
<div class="footer">табло за внедряване на сертификат за защитено стартиране | Генерирано $($stats. ReportGeneratedAt) | StreamingMode | Върхова памет: ${stPeakMemMB} МБ</div> </div><!-- /container -->
>на скрипт на< функция toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} функция openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} new Chart(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Action Required','Temp. В пауза","Не се поддържа","Защитено рестартиране ИЗКЛ.","С грешки"],набори от данни:[{data:[$($c.Updated),$($c.UpdatePending);$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused);$($c.NotSupported);$($c.SBOff ),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24 #6c757d',","#adb5bd","#dc3545"]}]},опции:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); new Chart(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. В пауза',данни:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},{{label етикет:"С грешки",данни:[$mfrWithErrors],backgroundColor:'#dc3545'}]},опции:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},плъгини:{legend:{position:'top'}}}); Хронологична диаграма на тенденциите if (document.getElementById('trendChart')) { var allLabels = [$allChartLabels]; var actualUpdated = [$trendUpdated]; var actualNotUpdated = [$trendNotUpdated]; var actualTotal = [$trendTotal]; var projData = [$projDataJS]; var projNotUpdData = [$projNotUpdJS]; var histLen = actualUpdated.length; var projLen = projData.length; var paddedUpdated = actualUpdated.concat(Array(projLen).fill(null)); var paddedNotUpdated = actualNotUpdated.concat(Array(projLen).fill(null)); var paddedTotal = actualTotal.concat(Array(projLen).fill(null)); var projLine = Array(histLen).fill(null); var projNotUpdLine = Array(histLen).fill(null); if (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdData); } var набори от данни = [ {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5;5],fill:false,tension:0,pointRadius:0,borderWidth:1} 1000 00 ако (projLen > 0) { datasets.push({label:'Projected Updated (2x doubling)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{{responsive:true display:true,text:'Devices'}},x:{title:{display:true,text:'Date'}}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}); } Динамично обратно броене (function(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27');p=new Date('2026-10-19'); var dk=document.getElementById('daysKek');du=document.getElementById('daysUefi');dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0;Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0;Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0;Math.ceil((p-t)/864e5))})(); </script> </> на тялото </html> "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Винаги запазвайте стабилно "Най-новото" копие, така че администраторите да не трябва да проследяват времевите клейма $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # Записване на файлов манифест за постъпков режим (бързо откриване без промяна при следващо изпълнение) ако ($IncrementalMode -или $StreamingMode) { $stManifestDir = Join-Path $OutputPath ".cache" if (-not (Test-Path $stManifestDir)) { New-Item -ItemType Directory -Path $stManifestDir -Force | Out-Null } $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" $stNewManifest = @{} Write-Host "Записване на файлов манифест за постъпков режим..." -ForegroundColor Gray място за игра ($jf в $jsonFiles) { $stNewManifest$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Размер = $jf. Дължина } } Save-FileManifest – манифест $stNewManifest – път $stManifestPath Write-Host " Записан манифест за файлове на $($stNewManifest.Count) " -ForegroundColor DarkGray } # ПОЧИСТВАНЕ НА ЗАДЪРЖАНЕ # Orchestrator повторно използваема папка (Aggregation_Current): запазване само на най-новото изпълнение (1) # Администрация ръчно изпълнение / други папки: запазване на последните 7 работи # Резюме CSVs никога не се изтриват – те са малки (~1 КБ) и са резервен източник за хронологията на тенденцията $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = ако ($outputLeaf -eq "Aggregation_Current") { 1 } друго { 7 } # Файловите префикси са безопасни за почистване (ефимерни снимки за всяко изпълнение) $cleanupPrefixes = @( "SecureBoot_Dashboard_", "SecureBoot_action_required_", "SecureBoot_ByManufacturer_", "SecureBoot_ErrorCodes_", "SecureBoot_errors_", "SecureBoot_known_issues_", "SecureBoot_missing_kek_", "SecureBoot_needs_reboot_", "SecureBoot_not_updated_", "SecureBoot_secureboot_off_", "SecureBoot_task_disabled_", "SecureBoot_temp_failures_", "SecureBoot_perm_failures_", "SecureBoot_under_observation_", "SecureBoot_UniqueBuckets_", "SecureBoot_update_pending_", "SecureBoot_updated_devices_", "SecureBoot_rollout_inprogress_", "SecureBoot_NotUptodate_", "SecureBoot_Kusto_" ) # Намерете всички уникални времеви клейма само от чисти файлове $cleanableFiles = Get-ChildItem $OutputPath -Файл -EA SilentlyContinue | Where-Object { $f = $_. Име; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Брой -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { ако ($_. Име -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object – уникален – низходящ) ако ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object – Пропускане на $retentionCount $removedFiles = 0; $freedBytes = 0 действия ($oldTs в $oldTimestamps) { $prefix в $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -Файл -Филтър "${prefix}${oldTs}*" -EA SilentlyContinue действия ($f в $oldFiles) { $freedBytes += $f.Дължина Remove-Item $f.FullName - Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [math]::Round($freedBytes / 1MB, 1) Write-Host "Почистване на задържане: премахнато $removedFiles файлове от стари изпълнявания на $($oldTimestamps.Count), освободени ${freedMB} МБ (запазване на последните $retentionCount + всички Резюме/NotUptodate CSVs)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Cyan Write-Host "ПОТОЧНО ПРЕДАВАНЕ НА АГРЕГИРАНЕ ЗАВЪРШЕНО" – Цвят на преден планЗелено Write-Host ("=" * 60) -ForegroundColor Cyan Write-Host " Общо устройства: $($c.Total.ToString("N0"))" -ForegroundColor White Write-Host " НЕ Е АКТУАЛИЗИРАНО: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(ако ($stNotUptodate -gt 0) { "Жълто" } друго { "Зелено" }) Write-Host " Актуализирано: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -ForegroundColor Green Write-Host " С грешки: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Върхова памет: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Време: $([математика]::Round($stTotal/60,1)) min" -ForegroundColor White Write-Host " Dashboard: $htmlPath" - ForegroundColor White return [PSCustomObject]$stats } #ENDREGION РЕЖИМ НА ПОТОЧНО ПРЕДАВАНЕ } друго { Write-Error "Входният път не е намерен: $InputPath" изход 1 }