Skopiuj i wklej ten przykładowy skrypt i zmodyfikuj go zgodnie z potrzebami środowiska:
<# . STRESZCZENIE Agreguje dane JSON stanu bezpiecznego rozruchu z wielu urządzeń w raportach podsumowujących.
. OPIS Odczytuje zebrane pliki JSON stanu bezpiecznego rozruchu i generuje: - Pulpit nawigacyjny HTML z wykresami i filtrowaniem - Podsumowanie według ufnościPoziom - Unikatowa analiza zasobnika urządzenia na potrzeby strategii testowania Obsługuje: - Pliki na komputerze: HOSTNAME_latest.json (zalecane) - Pojedynczy plik JSON Automatycznie deduplikuje według hostname, zachowując najnowsze CollectionTime. Domyślnie obejmuje tylko urządzenia z ufnością "Req akcji" lub "Wysoka" , aby skupić się na zasobnikach z akcjami. Użyj polecenia -IncludeAllConfidenceLevels, aby zastąpić.
. PARAMETR InputPath Ścieżka do plików JSON: - Folder: odczytuje wszystkie *pliki _latest.json (lub *.json, jeśli nie ma _latest plików) - Plik: odczytuje pojedynczy plik JSON
. PARAMETER OutputPath Ścieżka wygenerowanych raportów (domyślna: .\SecureBootReports)
. PRZYKŁAD # Agreguj z folderu plików na komputerze (zalecane) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Odczytuje: \\contoso\SecureBootLogs$\*_latest.json
. PRZYKŁAD # Niestandardowa lokalizacja wyjściowa .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. PRZYKŁAD # Uwzględnij tylko req akcji i wysoką pewność siebie (zachowanie domyślne) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Wyklucza: Obserwacja, Wstrzymane, Nieobsługiwane
. PRZYKŁAD # Uwzględnij wszystkie poziomy ufności (zastępowanie filtru) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. PRZYKŁAD # Niestandardowy filtr poziomu ufności .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. PRZYKŁAD # SKALA PRZEDSIĘBIORSTWA: Tryb przyrostowy — tylko proces zmienił pliki (szybkie kolejne biegi) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Pierwsze uruchomienie: Pełne ładowanie ~2 godziny dla urządzeń 500K # Kolejne biegi: Sekundy, jeśli nie ma zmian, minuty dla różnic
. PRZYKŁAD # Pomiń html, jeśli nic się nie zmieniło (najszybszy do monitorowania) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Jeśli od ostatniego uruchomienia nie zmieniły się żadne pliki: ~5 sekund
. PRZYKŁAD # Tryb tylko do podsumowania — pomijanie dużych tabel urządzeń (1–2 minuty i ponad 20 minut) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Generuje pliki CSV, ale pomija pulpit nawigacyjny HTML z pełnymi tabelami urządzeń
. NOTATKI Paruj z Detect-SecureBootCertUpdateStatus.ps1 do wdrożenia w przedsiębiorstwie.Zobacz GPO-DEPLOYMENT-GUIDE.md, aby uzyskać pełny przewodnik po wdrażaniu. Zachowanie domyślne nie obejmuje urządzeń obserwacji, wstrzymanych i nieobsługiwanych , aby skupić raportowanie tylko na zasobnikach urządzeń z akcjami.#>
param( [Parameter(Mandatory = $true)] [ciąg]$InputPath, [Parameter(Mandatory = $false)] [string]$OutputPath = ".\SecureBootReports", [Parameter(Mandatory = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Mandatory = $false)] [string]$RolloutStatePath, # Path to RolloutState.json to identify InProgress devices [Parameter(Mandatory = $false)] [string]$RolloutSummaryPath, # Path to SecureBootRolloutSummary.json from Orchestrator (contains projection data) [Parameter(Mandatory = $false)] [string[]]$IncludeConfidenceLevels = @("Wymagane działanie", "Wysoka pewność siebie") # Uwzględniaj tylko te poziomy ufności (domyślnie: tylko zasobniki z możliwością akcji) [Parameter(Mandatory = $false)] [switch]$IncludeAllConfidenceLevels, # Zastąp filtr, aby uwzględnić wszystkie poziomy ufności [Parameter(Mandatory = $false)] [switch]$SkipHistoryTracking, [Parameter(Mandatory = $false)] [switch]$IncrementalMode, # Enable delta processing - only load changed files since last run [Parameter(Mandatory = $false)] [string]$CachePath, # Path to cache directory (default: OutputPath\.cache) [Parameter(Mandatory = $false)] [int]$ParallelThreads = 8, # Liczba wątków równoległych do ładowania plików (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, # Generate summary stats only (no large device tables) — znacznie szybciej [Parameter(Mandatory = $false)] [switch]$StreamingMode # Tryb wydajny pod względem pamięci: fragmenty procesu, przyrostowe zapisywanie plików CSV, zachowywanie tylko podsumowań w pamięci )
# Automatyczne podniesienie uprawnień do programu PowerShell 7, jeśli jest dostępny (6 razy szybciej w przypadku dużych zestawów danych) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ($pwshPath) { Write-Host wykryto "Program PowerShell $($PSVersionTable.PSVersion) — ponowne uruchomienie za pomocą programu PowerShell 7 w celu szybszego przetwarzania..." -Kolor pierwszego planu Żółty # Odbuduj listę argumentów z parametrów powiązanych $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key w $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] if ($val -is [switch]) { if ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [tablica]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } inaczej { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs $LASTEXITCODE wyjścia } }
$ErrorActionPreference = "Kontynuuj" $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "rrrr-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Próbki wdrażania i monitorowania"
# Uwaga: Ten skrypt nie jest zależny od innych skryptów. # W przypadku pełnego narzędzia pobierz z: $DownloadUrl -> $DownloadSubPage
konfiguracja #region Write-Host "=" * 60 -Pierwszy planColor Cyan Write-Host "Secure Boot Data Aggregation" -ForegroundColor Cyan Write-Host "=" * 60 -Pierwszy planColor Cyan
# Utwórz katalog danych wyjściowych if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }
# Załaduj dane — obsługuje formaty CSV (starsze) i JSON (natywne) Write-Host "'nLoading data from: $InputPath" -ForegroundColor Yellow
Funkcja # Pomocnik w celu znormalizowania obiektu urządzenia (obsługa różnic nazw pól) funkcja Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON używa Hostname, CSV używa HostName) if ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force } # Handle Confidence vs ConfidenceLevel (JSON używa ufności, csv używa ufnościPoziom) # UfnośćPoziom to oficjalna nazwa pola — zaufanie do mapy if ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Pewność siebie -Siła } # Śledź stan aktualizacji za pośrednictwem event1808Count OR UEFICA2023Status="Updated" # Umożliwia to śledzenie liczby urządzeń w każdym zasobniku ufności, które zostały zaktualizowane $event 1808 = 0 if ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false if ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Aktualizacja") { $uefiCaUpdated = $true } if ($event 1808 -gt 0 -lub $uefiCaUpdated) { # Oznacz jako zaktualizowane dla logiki pulpitu nawigacyjnego/wdrażania — ale nie zastępuj funkcji ufnościPoziom $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } inaczej { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # Ufność Klasyfikacja poziomów: # - "Wysoka pewność siebie", "W obszarze obserwacji...", "Tymczasowo wstrzymane...", "Nieobsługiwane..." = użyj jako-jest # — wszystko inne (null, puste, "UpdateType:...", "Nieznane", "N/D") = spada do działania wymaganego w licznikach # Nie jest wymagana normalizacja — pozostałe gałęzie licznika przesyłania strumieniowego obsługują tę funkcję } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON używa OEM*, starsze używa WMI_*) if ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force } # Handle OEMModelNumber vs WMI_Model if ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber — siła } # Handle FirmwareVersion vs BIOSDescription if ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion - Force } $device zwrotu }
#region przyrostowe przetwarzanie / zarządzanie pamięcią podręczną # Skonfiguruj ścieżki pamięci podręcznej jeżeli (nie $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Funkcje zarządzania pamięcią podręczną funkcja Get-FileManifest { param([ciąg]$Path) if (Test-Path $Path) { wypróbuj { $json = Get-Content $Path -Raw | ConvertFrom-Json # Konwertuj PSObject na hashtable (kompatybilny z PS5.1 — PS7 ma -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Nazwa] = $_. Wartość } $ht zwrotu } złapać { zwróć wartość @{} } } zwróć wartość @{} }
funkcja Save-FileManifest { param([hashtable]$Manifest; [string]$Path) $dir = Split-Path $Path -Rodzic if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $Manifest | ConvertTo-Json -Głębokość 3 -Kompresowanie | Set-Content $Path -Siła }
funkcja Get-DeviceCache { param([ciąg]$Path) if (Test-Path $Path) { wypróbuj { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host " Loaded device cache: $($cacheData.Count) devices" -ForegroundColor DarkGray $cacheData zwrotu } złapać { Write-Host " Pamięć podręczna uszkodzona, odbuduje" -Pierwszy planColor Yellow zwróć wartość @() } } zwróć wartość @() }
funkcja Save-DeviceCache { param($Devices;[ciąg]$Path) $dir = Split-Path $Path -Rodzic if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } # Konwertuj na tablicę i zapisz $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Głębokość 10 -Kompresowanie | Set-Content $Path -Siła Write-Host " Saved device cache: $($deviceArray.Count) devices" -ForegroundColor DarkGray }
funkcja Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Konstruuj niewrażliwe na wielkość liter odnośniki z manifestu (normalizuj do małych liter) $manifestLookup = @{} foreach ($mk w $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file w $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalizuj ścieżkę do małych liter $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Rozmiar = $file. Długość } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] if ($cached. LastWriteTimeUtc -eq $lwt -i $cached. Rozmiar -eq $file. Długość) { [nieważna]$unchanged. Add($file) Kontynuować } } [nieważna]$changed. Add($file) } zwróć wartość @{ Zmieniono = $changed Bez zmian = $unchanged NewManifest = $newManifest } }
# Bardzo szybkie ładowanie plików równoległych przy użyciu przetwarzania wsadowego funkcja Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Liczba # Użyj partii z ~1000 plików każdy dla lepszej kontroli pamięci $batchSize = [matematyka]::Min(1000; [matematyka]::Sufit($totalFiles / [matematyka]::Maksimum(1; $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
for ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [matematyka]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Add($batch) } Write-Host " ($($batches. Liczba) partii ~$batchSize plików każdy)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # Sprawdź, czy program PowerShell 7+ parallel jest dostępny $canParallel = $PSVersionTable.PSVersion.Major -ge 7 if ($canParallel -and $Threads -gt 1) { # PS7+: Partie procesów równolegle $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file w $batchFiles) { wypróbuj { $content = [System.IO.File]::ReadAllText($file. Imię i nazwisko) | ConvertFrom-Json $batchResults.Add($content) } złapać { } } $batchResults.ToArray() } foreach ($batch w $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } inaczej { #PS5.1 fallback: Sequential processing (still fast for <10K files) foreach ($file w $Files) { wypróbuj { $content = [System.IO.File]::ReadAllText($file. Imię i nazwisko) | ConvertFrom-Json $flatResults.Add($content) } złapać { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Pojedynczy plik JSON if ($InputPath -like "*.json") { $jsonContent = Get-Content -Ścieżka $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "Loaded $($allDevices.Count) records from file" (Załadowano rekordy $($allDevices.Count) z pliku" } inaczej { Write-Error "Obsługiwany jest tylko format JSON. Plik musi mieć rozszerzenie .json". wyjdź 1 } } elseif (Test-Path $InputPath -PathType Container) { # Folder — tylko JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|Stan wdrożenia|Plan wdrożenia" }) # Preferuj *_latest.json pliki, jeśli istnieją (w trybie na komputerze) $latestJson = $jsonFiles | Where-Object { $_. Name -like "*_latest.json" } if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Liczba if ($totalFiles -eq 0) { Write-Error "Brak plików JSON znalezionych w: $InputPath" wyjdź 1 } Write-Host "Found $totalFiles JSON files" -ForegroundColor Gray Funkcja # Pomocnik odpowiadająca poziomom ufności (obsługuje zarówno krótkie, jak i pełne formularze) # Zdefiniowane wcześniej, dzięki czemu zarówno StreamingMode, jak i normalne ścieżki mogą z niego korzystać funkcja Test-ConfidenceLevel { param([ciąg]$Value;[ciąg]$Match) if ([string]::IsNullOrEmpty($Value)) { return $false } przełącznik ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Wymagane działanie*" -lub $Value -eq "Wymagane działanie") } "TemporarilyPaused" { return $Value -like "Tymczasowo wstrzymane*" } "Nieobsługiwane" { return ($Value -like "Nieobsługiwane*" -lub $Value -eq "Nieobsługiwane") } default { return $false } } } #region TRYB PRZESYŁANIA STRUMIENIOWEGO — wydajne przetwarzanie pamięci dla dużych zestawów danych # Zawsze używaj trybu przesyłania strumieniowego do wydajnego przetwarzania pamięci i nowego stylu pulpitu nawigacyjnego if (-nie $StreamingMode) { Write-Host "Automatyczne włączanie trybu przesyłania strumieniowego (pulpit nawigacyjny nowego stylu)" —Kolor pierwszego planu Żółty $StreamingMode = $true if (-nie $IncrementalMode) { $IncrementalMode = $true } } # Gdy opcja -StreamingMode jest włączona, pliki przetwarzane w fragmentach przechowują tylko liczniki w pamięci.# Dane na poziomie urządzenia są zapisywane w plikach JSON na fragment w celu ładowania na żądanie na pulpicie nawigacyjnym.# Użycie pamięci: ~1,5 GB niezależnie od rozmiaru zestawu danych (vs 10-20 GB bez przesyłania strumieniowego).if ($StreamingMode) { Write-Host "TRYB PRZESYŁANIA STRUMIENIOWEGO włączony - wydajne przetwarzanie pamięci" -ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # PRZYROSTOWE SPRAWDŹ: Jeśli od ostatniego uruchomienia nie zmieniły się żadne pliki, pomiń przetwarzanie całkowicie if ($IncrementalMode -and -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" if (Test-Path $stManifestPath) { Write-Host "Sprawdzanie zmian od ostatniego uruchomienia strumieniowego..." -ForegroundColor Cyan $stOldManifest = $stManifestPath ścieżki Get-FileManifest if ($stOldManifest.Count -gt 0) { $stChanged = $false # Szybkie sprawdzanie: ta sama liczba plików? if ($stOldManifest.Count -eq $totalFiles) { # Sprawdź 100 NAJNOWSZYCH plików (posortowanych według opcji LastWriteTime malejąco) # Jeśli jakikolwiek plik został zmieniony, będzie miał najnowszą sygnaturę czasowa i pojawi się jako pierwszy $sampleSize = [matematyka]::Min(100; $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc — malejąco | Select-Object — pierwsza $sampleSize foreach ($sf w $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Przerwy } # Porównaj sygnatury czasowe — buforowane mogą być datetime lub ciąg po łapce JSON $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc wypróbuj { # Jeśli pamięć podręczna jest już datetime (ConvertFrom-Json auto-konwertuje), użyj bezpośrednio if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } inaczej { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). Czas UtcDateTime } if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Przerwy } } złapać { $stChanged = $true Przerwy } } } inaczej { $stChanged = $true } jeżeli (nie $stChanged) { # Sprawdź, czy istnieją pliki wyjściowe $stSummaryExists = Get-ChildItem ($OutputPath ścieżki sprzężenia "SecureBoot_Summary_*.csv") — EA SilentlyContinue | Select-Object -Pierwsza 1 $stDashExists = Get-ChildItem ($OutputPath ścieżki sprzężenia "SecureBoot_Dashboard_*.html") — EA SilentlyContinue | Select-Object —Pierwsza 1 if ($stSummaryExists -and $stDashExists) { Write-Host " Nie wykryto żadnych zmian ($totalFiles plików bez zmian) — pomijanie przetwarzania" -ForegroundColor Green Write-Host " Ostatni pulpit nawigacyjny: $($stDashExists.FullName)" -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv Write-Host " Urządzenia: $($cachedStats.TotalDevices) | Zaktualizowano: $($cachedStats.Updated) | Błędy: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host " Completed in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (bez przetwarzania)" -ForegroundColor Green $cachedStats zwrotu } } inaczej { # DELTA PATCH: Znajdź dokładnie pliki, które zostały zmienione Write-Host " Wykryte zmiany — identyfikowanie zmienionych plików..." -ForegroundColor Yellow $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf w $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } inaczej { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc wypróbuj { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } inaczej { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } złapać { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [matematyka]::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Zmieniono: $($changedFiles.Count) | Nowość: $($newFiles.Count) | Suma: $totalChanged ($changePct%)" -Pierwszy planColor Yellow if ($totalChanged -gt 0 -i $changePct -lt 10) { # DELTA PATCH MODE: <10% zmienione, popraw istniejące dane Write-Host " Delta patch mode ($changePct% < 10%) - patching $totalChanged files..." -ForegroundColor Green $dataDir = "dane" Join-Path $OutputPath # Load changed/new device records $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df w $allDeltaFiles) { wypróbuj { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData if ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } złapać { } } Write-Host " Loaded $($deltaDevices.Count) changed device records" -ForegroundColor Gray # Dla każdej kategorii JSON: usuwanie starych wpisów dla zmienionych nazw hostów, dodawanie nowych wpisów $categoryFiles = @("błędy", "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 w $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat w $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" if (Test-Path $catPath) { wypróbuj { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Usuwanie starych wpisów zmienionych nazw hostów $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Ponowne klasyfikowanie każdego zmienionego urządzenia w kategorie # (zostanie dodany poniżej po klasyfikacji) $catData | ConvertTo-Json -Głębokość 5 | Set-Content $catPath kodowania UTF8 } złapać { } } } # Klasyfikowanie każdego zmienionego urządzenia i dołączanie do odpowiednich plików kategorii foreach ($dev w $deltaDevices.Wartości) { $slim = [zamówione]@{ HostName = $dev. Nazwa hosta WMI_Manufacturer = jeżeli ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } inaczej { "" } WMI_Model = jeżeli ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } inaczej { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } inaczej { "" } UfnośćPoziom = jeżeli ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } inaczej { "" } IsUpdated = $dev. IsUpdated UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } inaczej { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } inaczej { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } inaczej { $null } SkipReasonKnownIssue = jeżeli ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } inaczej { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = jeżeli ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } inaczej { "" } $hasErr = (-nie [ciąg]::IsNullOrEmpty($dev. UEFICA2023Error) - i $dev. UEFICA2023Error -ne "0" -i $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -lub $dev. SecureBootTaskStatus -eq 'Disabled' -lub $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = jeżeli ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } inaczej { 0 } $e 1808 = jeżeli ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Zdarzenie1808Count } inaczej { 0 } $e 1803 = jeżeli ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Zdarzenie1803Count } inaczej { 0 } $mKEK = ($e 1803 -gt 0 -lub $dev. MissingKEK -eq $true) $hKI = ((-nie [ciąg]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -lub (-nie [ciąg]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = jeżeli ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } inaczej { "" } # Dołącz do pasujących plików kategorii $targets = @() if ($isUpd) { $targets += "updated_devices" } if ($hasErr) { $targets += "errors" } if ($hKI) { $targets += "known_issues" } if ($mKEK) { $targets += "missing_kek" } if (-not $isUpd -and $sbOn) { $targets += "not_updated" } if ($tskDis) { $targets += "task_disabled" } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused')) { $targets += "temp_failures" } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" } if (-not $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" } if (-nie $sbOn) { $targets += "secureboot_off" } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } foreach ($tgt w $targets) { $tgtPath = Join-Path $dataDir "$tgt.json" if (Test-Path $tgtPath) { $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Głębokość 5 | Set-Content $tgtPath kodowania UTF8 } } } # Regeneruj pliki CSV z poprawionych nazw JSON Write-Host " Regenerowanie csv z poprawionych danych..." -ForegroundColor Gray $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat w $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (Test-Path $catJsonPath) { wypróbuj { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Kodowanie UTF8 } } złapać { } } } # Przelicz statystyki z poprawionych plików JSON Write-Host " Ponowne obliczanie podsumowania na podstawie poprawionych danych..." -Pierwszy planColor Gray $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") } $pTotal = 0; $pUpdated = 0; $pErrors = 0; $pKI = 0; $pKEK = 0 $pTaskDis = 0; $pTempFail = 0; $pPermFail = 0; $pActionReq = 0; $pSBOff = 0; $pRIP = 0 foreach ($cat w $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | KonwertujFrom-Json). Policz } złapać { } } przełącznik ($cat) { "updated_devices" { $pUpdated = $cnt } "errors" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # obliczane "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 | KonwertujFrom-Json). Liczba $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Delta patch complete: $totalChanged devices updated" -ForegroundColor Green Write-Host " Suma: $pTotal | Zaktualizowano: $pUpdated | NotUpdated: $pNotUpdated | Błędy: $pErrors" -ForegroundColor White # Aktualizacja manifestu $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf w $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Rozmiar = $jf. Długość } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Completed in $([math]:Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green # Przejdź do pełnego przesyłania strumieniowego, aby ponownie wygenerować pulpit nawigacyjny HTML # Pliki danych są już poprawione, dzięki czemu pulpit nawigacyjny pozostaje aktualny Write-Host "Regenerowanie pulpitu nawigacyjnego z poprawionych danych..." -Kolor pierwszego planu Żółty } inaczej { Write-Host " $changePct% zmieniono pliki (>= 10%) — wymagane jest pełne ponowne przetwarzanie strumieniowe" -Kolor pierwszego planu Żółty } } } } } # Tworzenie podkatalogu danych dla plików JSON urządzenia na żądanie $dataDir = Join-Path $OutputPath "dane" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Deduplikacja za pośrednictwem zestawu skrótów (O(1) na odnośnik, ~50 MB dla nazw hostów 600K) $seenHostnames = [System.Collections.Generic.HashSet[string]]:new([System.StringComparer]::OrdinalIgnoreCase) # Lekkie liczniki podsumowań (zastępuje $allDevices + $uniqueDevices w pamięci) $c = @{ Suma = 0; SBEnabled = 0; SBOff = 0 Zaktualizowano = 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 Oczekiwanie na aktualizację = 0 } # Śledzenie zasobnika dla atrisk/listy bezpiecznych elementów (zestawy lekkie) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Pliki danych urządzenia trybu wsadowego: gromadzą się na fragment, opróżnianie granic fragmentów $stDeviceFiles = @("błędy", "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 = @{} foreach ($dfName w $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 bajtów vs ~2KB full) funkcja Get-SlimDevice { param($Dev) return [zamówione]@{ HostName = $Dev.HostName WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } inaczej { "" } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } inaczej { "" } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } inaczej { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" } KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } inaczej { $null } SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } inaczej { $null } UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } inaczej { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } inaczej { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } inaczej { $null } } } # Opróżnij partię do pliku JSON (tryb dołączania) funkcja Flush-DeviceBatch { param([ciąg]$StreamName; [System.Collections.Generic.List[obiekt]]$Batch) if ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev w $Batch) { if ($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) } # MAIN STREAMING LOOP $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } inaczej { 10000 } $stTotalChunks = [matematyka]::Sufit($totalFiles / $stChunkSize) $stPeakMemMB = 0 if ($stTotalChunks -gt 1) { Write-Host "Przetwarzanie plików $totalFiles w $stTotalChunks fragmentach $stChunkSize (przesyłanie strumieniowe, $ParallelThreads wątki):" -Pierwszy planColor Cyan } inaczej { Write-Host "Przetwarzanie plików $totalFiles (przesyłanie strumieniowe, $ParallelThreads wątki):" -ForegroundColor Cyan } for ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [matematyka]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] if ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) files): " -NoNewline -ForegroundColor Gray } inaczej { Write-Host " Ładowanie plików $($cFiles.Count): " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = $ParallelThreads Load-FilesParallel -Files $cFiles -wątków # Listy partii na fragmenty $cBatches = @{} foreach ($df w $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 foreach ($raw w $rawDevices) { if (-nie $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Nazwa hosta jeśli (nie $hostname) { continue } if ($seenHostnames.Contains($hostname)) { $cDupe++; continue } [void]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False") jeśli ($sbOn) { $c.SBEnabled++ } inaczej { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = jeżeli ($device. PSObject.Properties['ConfidenceLevel'] -and $device. UfnośćPoziom) { "$($device. ConfidenceLevel)" } inaczej { "" } $hasErr = (-nie [ciąg]::IsNullOrEmpty($device. UEFICA2023Error) -i "$($device. UEFICA2023Error)" -ne "0" -i "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -lub "$($device. SecureBootTaskStatus)" -eq 'Disabled' -lub "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = jeżeli ($device. PSObject.Properties['BucketId'] -i $device. BucketId) { "$($device. BucketId)" } inaczej { "" } $e 1808 = jeżeli ($device. PSObject.Properties['Event1808Count']) { [int]$device. Zdarzenie1808Count } inaczej { 0 } $e 1801 = jeżeli ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } inaczej { 0 } $e 1803 = jeżeli ($device. PSObject.Properties['Event1803Count']) { [int]$device. Zdarzenie1803Count } inaczej { 0 } $mKEK = ($e 1803 -gt 0 -lub $device. MissingKEK -eq $true -lub "$($device. MissingKEK)" -eq "True") $hKI = ((-nie [ciąg]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -lub (-nie [ciąg]::IsNullOrEmpty($device. KnownIssueId))) $rStat = jeżeli ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } inaczej { "" } $mfr = jeżeli ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } inaczej { "Nieznany" } $bid = jeżeli (-nie [ciąg]::IsNullOrEmpty($bid)) { $bid } inaczej { "" } # Flaga oczekująca na aktualizację wstępnie obliczona (zastosowano zasady/WinCS, stan jeszcze nie zaktualizowano, SB ON, zadanie nie jest wyłączone) $uefiStatus = jeżeli ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } inaczej { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] - i $device. WinCSKeyApplied -eq $true) $statusPending = ([ciąg]::IsNullOrEmpty($uefiStatus) -lub $uefiStatus -eq 'NotStarted' -lub $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -lub $hasWinCS) -i $statusPending -i -nie $isUpd -i $sbOn -i -nie $tskDis) if ($isUpd) { $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Śledź zaktualizowane urządzenia wymagające ponownego uruchomienia (UEFICA2023Status=Updated but Event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-nie $sbOn) { # SecureBoot OFF — poza zakresem, nie klasyfikuj według ufności } inaczej { if ($isUpdatePending) { } # Liczone oddzielnie w oczekującej aktualizacji — wzajemnie wyklucza się dla wykresu kołowego 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++ } inaczej { $c.ActionReq++ } if ([ciąg]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } if ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } if ($tskNF) { $c.TaskNotFound++ } if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ } if ($hasErr) { $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["błędy"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Error if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ if ($stErrorCodeSamples[$ec]. Liczba -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } if ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = jeżeli (-nie [ciąg]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } inaczej { $device. KnownIssueId } if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -lub ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ } if ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ } # Oczekiwanie na aktualizację: zastosowano zasady lub WinCS, stan oczekujący, SB ON, zadanie nie jest wyłączone if ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # W obszarze Urządzenia do obserwacji (niezależne od wymaganego działania) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Działanie Wymagane: nie zaktualizowane, SB WŁ., niezgodne z innymi kategoriami ufności, a nie Oczekujące aktualizacje jeżeli (-nie $isUpd -i $sbOn -i -nie $isUpdatePending -i -nie (Test-ConfidenceLevel $conf 'HighConfidence') -and -nie (Test-ConfidenceLevel $conf 'UnderObservation') -and -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] = @{ Total=0; Zaktualizowano=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Suma++ if ($isUpd) { $stMfrCounts[$mfr]. Zaktualizowano++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. Oczekiwanie na aktualizację++ } elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. NotSupported++ } inaczej { $stMfrCounts[$mfr]. ActionReq++ } if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Śledź wszystkie urządzenia według zasobnika (w tym pusty identyfikator zasobnika) $bucketKey = if ($bid -and $bid -ne "") { $bid } inaczej { "(puste)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Zaktualizowano=0; Producent=$mfr; Model=""; BIOS="" } if ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } if ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Liczba++ if ($isUpd) { $stAllBuckets[$bucketKey]. Zaktualizowano++ } } # Opróżnij partie na dysk foreach ($df w $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Matematyka]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = jeżeli ($cRem -gt 0) { " | ETA: ~$([Matematyka]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } inaczej { "" } $cMem = [matematyka]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew nowe, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # Finalizowanie tablic JSON foreach ($dfName w $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) devices" -ForegroundColor DarkGray } # Oblicz statystyki pochodne $stAtRisk = 0; $stSafeList = 0 foreach ($bid w $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count — $b.Updated if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [matematyka]::Maksimum(0; $stAtRisk — $c.WithErrors) # NotUptodate = count from not_updated batch (devices with SB ON and not updated) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [zamówione]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Zaktualizowano = $c.Zaktualizowano; 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 Oczekujące aktualizacje = $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) } inaczej { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } inaczej { 0 } PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } inaczej { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } inaczej { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } inaczej { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]:Round(($c.ActionReq/$c.Total)*100,1) } inaczej { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } inaczej { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } inaczej { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Pisanie plików CSV [PSCustomObject]$stats | Export-Csv -Path (join-path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Kodowanie UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Malejąco | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Klucz; Liczba=$_. Wartość.Suma; Zaktualizowano=$_. Wartość.Zaktualizowano; HighConfidence=$_. Wartość.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (join-path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stErrorCodeCounts.GetEnumerator() | Sort-Object wartość malejąca | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Klucz; Liczba=$_. Wartość; SampleDevices=($stErrorCodeSamples[$_. Klucz] -join ", ") } } | Export-Csv -Path (join-path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Wartość.Liczba } -Malejąco | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Klucz; Liczba=$_. Wartość.Liczba; Zaktualizowano=$_. Wartość.Zaktualizowano; NotUpdated=$_. Wartość.Liczba-$_. Wartość.Zaktualizowano; Producent=$_. Value.Manufacturer } } | Export-Csv -Path (join-path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Kodowanie UTF8 # Wygeneruj pliki CSV zgodne z aranżacjami (oczekiwane nazwy plików dla Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (Test-Path $notUpdatedJsonPath) { wypróbuj { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json if ($nuData.Count -gt 0) { # NotUptodate CSV — aranżacja lub wyszukiwanie *NotUptodate*.csv $nuData | Export-Csv -Path (join-path $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Kodowanie UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) devices)" -ForegroundColor Gray } } złapać { } } # Zapisywanie danych JSON na pulpicie nawigacyjnym $stats | ConvertTo-Json -Głębokość 3 | Set-Content (ścieżka sprzężenia $dataDir "summary.json") — kodowanie UTF8 # ŚLEDZENIE HISTORYCZNE: Zapisywanie punktu danych dla wykresu trendu # Użyj stabilnej lokalizacji pamięci podręcznej, aby dane trendu utrzymywały się w folderach agregacji sygnatur czasowych. # Jeśli program OutputPath wygląda tak: "...\Aggregation_yyyyMMdd_HHmmss", pamięć podręczna przechodzi do folderu nadrzędnego.# W przeciwnym razie pamięć podręczna przechodzi do samego programu OutputPath.$parentDir = Split-Path $OutputPath -Rodzic $leafName = Split-Path $OutputPath -Liść if ($leafName -match '^Aggregation_\d{8}' -lub $leafName -eq 'Aggregation_Current') { #Orchestrator-created timestamped folder — użyj elementu nadrzędnego do stabilnej pamięci podręcznej $historyPath = Join-Path $parentDir ".cache\trend_history.json" } inaczej { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Rodzic if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() if (Test-Path $historyPath) { wypróbuj { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } catch { $historyData = @() } } # Sprawdź również wewnątrz OutputPath\.cache\ (starsza lokalizacja ze starszych wersji) # Scal wszystkie punkty danych, których nie ma jeszcze w historii podstawowej if ($leafName -eq 'Aggregation_Current' -lub $leafName -match '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { wypróbuj { $innerData = @($innerHistoryPath zawartości get-raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Data }) foreach ($entry w $innerData) { if ($entry. Data -i $entry. Date -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host " Merged $($innerData.Count) data points from inner cache" -ForegroundColor DarkGray } } złapać { } } }
# BOOTSTRAP: Jeśli historia trendu jest pusta/rozrzedzona, przekonstruuj dane historyczne if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -lub $leafName -eq 'Aggregation_Current')) { Write-Host "Bootstrapping trend history from historical data..." -ForegroundColor Yellow $dailyData = @{} # Źródło 1: CsV podsumowania wewnątrz bieżącego folderu (Aggregation_Current zachowuje wszystkie csv podsumowania) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | nazwa Sort-Object foreach ($summCsv w $localSummaries) { wypróbuj { $summ = Import-Csv $summCsv.FullName | Select-Object -Pierwszy 1 if ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -i $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = jeżeli ($summ. Zaktualizowano) { [int]$summ. Zaktualizowano } inaczej { 0 } $notUpd = jeżeli ($summ. NotUptodate) { [int]$summ. NotUptodate } inaczej { [int]$summ. TotalDevices — $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Data = $dateStr; Suma = [int]$summ. TotalDevices; Zaktualizowano = $updated; NotUpdated = $notUpd NeedsReboot = 0; Błędy = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } inaczej { 0 } } } } złapać { } } # Źródło 2: Stare sygnatury czasowe Aggregation_* folderów (starsze, jeśli nadal istnieją) $aggFolders = Get-ChildItem $parentDir -Katalog -Filtr "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Name -match '^Aggregation_\d{8}' } | nazwa Sort-Object foreach ($folder w $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -Pierwszy 1 if ($summCsv) { wypróbuj { $summ = Import-Csv $summCsv.FullName | Select-Object -Pierwsza 1 if ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Name -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = jeżeli ($summ. Zaktualizowano) { [int]$summ. Zaktualizowano } inaczej { 0 } $notUpd = jeżeli ($summ. NotUptodate) { [int]$summ. NotUptodate } inaczej { [int]$summ. TotalDevices — $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Data = $dateStr; Suma = [int]$summ. TotalDevices; Zaktualizowano = $updated; NotUpdated = $notUpd NeedsReboot = 0; Błędy = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } inaczej { 0 } } } } złapać { } } } # Źródło 3: RolloutState.json WaveHistory (ma sygnatury czasowe na fali od dnia 1) # Zapewnia punkty danych planu bazowego nawet wtedy, gdy nie istnieją stare foldery agregacji $rolloutStatePaths = @( (Ścieżka sprzężenia $parentDir "RolloutState\RolloutState.json"), (Ścieżka sprzężenia $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath w $rolloutStatePaths) { if (Test-Path $rsPath) { wypróbuj { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Używanie dat rozpoczęcia fali jako punktów danych trendu # Obliczanie urządzeń skumulowanych kierowanych na poszczególne etapy $cumulativeTargeted = 0 foreach ($wave w $rsData.WaveHistory) { if ($wave. StartedAt -i $wave. DeviceCount) { $waveDate = ([datetime]$wave. StartedAt). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. DeviceCount if (-not $dailyData.ContainsKey($waveDate)) { # Przybliżone: w czasie rozpoczęcia fali zaktualizowano tylko urządzenia z poprzednich fal $dailyData[$waveDate] = [PSCustomObject]@{ Data = $waveDate; Suma = $c.Suma; Zaktualizowano = [matematyka]::Maksimum(0; $cumulativeTargeted — [int]$wave. DeviceCount) NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0; Błędy = 0; ActionRequired = 0 } } } } } } złapać { } break # Use first found } }
if ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | klawisz Sort-Object | ForEach-Object { $_. Wartość }) Write-Host " Bootstrapped $($historyData.Count) data points from historical summaries" -ForegroundColor Green } }
# Dodaj bieżący punkt danych (deduplikacja według dnia — zachowaj najnowsze dane na dzień) $todayKey = (Get-Date). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" } if ($existingToday) { # Zamień dzisiejszy wpis $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Data = $todayKey Suma = $c.Suma Zaktualizowano = $c.Zaktualizowano NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Errors = $c.WithErrors ActionRequired = $c.ActionReq } # Usuń nieprawidłowe punkty danych (łącznie 0) i zachowaj ostatnie 90 $historyData = @($historyData | Where-Object { [int]$_. Suma -gt 0 }) # Bez ograniczeń — dane trendu to ~100 bajtów/wpis, cały rok = ~36 KB $historyData | ConvertTo-Json -Głębokość 3 | Set-Content $historyPath kodowania UTF8 Write-Host " Historia trendów: $($historyData.Count) punkty danych" -ForegroundColor DarkGray # Konstruuj dane wykresu trendu dla języka HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Zaktualizowano }) -join "" $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Łączna liczba }) -sprzężenie "" # Projekcja: rozszerzanie linii trendu przy użyciu wykładniczego podwojenia (2,4,8,16...) # Pobiera rozmiar fali i okres obserwacji z rzeczywistych danych historii trendów.# - Rozmiar fali = największy wzrost pojedynczego okresu widoczny w historii (najnowsza wdrożona fala) # - Dni obserwacji = średnie dni kalendarzowe między punktami danych trendu (jak często uruchamiamy) # Następnie podwaja rozmiar fali każdego okresu, dopasowując strategię 2x wzrostu aranżatora.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Zaktualizowano $remaining = $stNotUptodate # Tylko nie zaktualizowane urządzenia SB-ON (z wyłączeniem funkcji SecureBoot OFF) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Wyczerpuj rozmiar fali i okres obserwacji z historii trendu $increments = @() $dayGaps = @() for ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Zaktualizowano — $historyData[$hi-1]. Aktualizacja if ($inc -gt 0) { $increments += $inc } wypróbuj { $d 1 = [datetime]::P arse($historyData[$hi-1]. Data) $d 2 = [datetime]::P arse($historyData[$hi]. Data) $gap = ($d 2 – $d 1). TotalDays if ($gap -gt 0) { $dayGaps += $gap } } złapać {} } # Rozmiar fali = najnowszy przyrost dodatni (bieżąca fala), powrót do średniej, minimum 2 $waveSize = jeżeli ($increments. Liczba -gt 0) { [matematyka]::Maksimum(2; $increments[-1]) } inaczej { 2 } # Okres obserwacji = średnia przerwa między punktami danych (dni kalendarzowe na falę), minimum 1 $waveDays = if ($dayGaps.Count -gt 0) { [matematyka]:Maksimum(1; [matematyka]:round(($dayGaps | Measure-Object -średnia). Średnia, 0)) } inaczej { 1 }
Write-Host " Projekcja: waveSize=$waveSize (od ostatniego przyrostu), waveDays=$waveDays (avg gap from history)" -ForegroundColor DarkGray
$dayCounter = 0 # Wyświetlaj na innym ekranie do momentu aktualizacji wszystkich urządzeń lub maksymalnie 365 dni for ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # Na każdej granicy okresu obserwacji wdróż falę, a następnie dwukrotnie if ($dayCounter -ge $waveDays) { $devicesThisWave = [matematyka]::Min($waveSize;$remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Podwójny rozmiar fali na następny okres (strategia aranżacji lub 2x) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd"))'" $projValues += $lastUpdated $projNotUpdValues += [matematyka]::Maksimum(0; $remaining) if ($remaining -le 0) { break } } $projLabels = $projDates -join "" $projUpdated = $projValues -join "" $projNotUpdated = $projNotUpdValues -join "" $hasProjection = $projDates.Liczba -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host " Projekcja: potrzebujesz co najmniej 2 punktów danych trendu, aby uzyskać chronometraż fali" -Pierwszy planColor DarkGray } # Konstruuj połączone ciągi danych wykresu dla tego ciągu $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } inaczej { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } inaczej { "" } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } inaczej { "" } $histCount = ($historyData | Obiekt-miara). Liczba $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Malejąco | ForEach-Object { @{ name=$_. Klucz; suma=$_. Wartość.Suma; zaktualizowano=$_. Wartość.Zaktualizowano; highConf=$_. Wartość.HighConf; akcjaReq=$_. Value.ActionReq } } | ConvertTo-Json -Głębokość 3 | Set-Content ($dataDir ścieżka sprzężenia "manufacturers.json") — kodowanie UTF8 # Konwertowanie plików danych JSON na plik CSV na potrzeby czytelnych dla człowieka plików do pobrania w programie Excel Write-Host "Konwertowanie danych urządzenia na plik CSV dla programu Excel do pobrania..." —Pierwszy planColor Gray foreach ($dfName w $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPathtimestamp.csv "SecureBoot_${dfName}_$" if (Test-Path $jsonFile) { wypróbuj { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json if ($jsonData.Count -gt 0) { # Uwzględnij dodatkowe kolumny dla update_pending CSV $selectProps = if ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } inaczej { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv -Path $csvFile -NoTypeInformation -Kodowanie UTF8 Write-Host " $dfName -> wiersze $($jsonData.Count) -> CSV" -ForegroundColor DarkGray } } catch { Write-Host " $dfName - skipped" -ForegroundColor DarkYellow } } } # Wygeneruj samodzielny pulpit nawigacyjny HTML $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Generowanie samodzielnego pulpitu nawigacyjnego HTML..." — Kolor pierwszego planu Żółty # VELOCITY PROJECTION: Obliczanie na podstawie historii skanowania lub poprzedniego podsumowania $stDeadline = [datetime]"2026-06-24" # KEK cert expiry $stDaysToDeadline = [matematyka]::Maksimum(0; ($stDeadline — (Data_uzyskania)). Dni) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "Nie dotyczy" $stWorkingDays = 0 $stCalendarDays = 0 # Najpierw wypróbuj historię trendów (uproszczona, już obsługiwana przez agregatora — zastępuje nadęte ScanHistory.json) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Suma -gt 0 -i [int]$_. Zaktualizowano -ge 0 }) if ($validHistory.Count -ge 2) { $prev = $validHistory[-2]; $curr = $validHistory[-1] $prevDate = [datetime]::P arse($prev. Date.Substring(0, [Matematyka]::Min(10, $prev. Data.Długość))) $currDate = [datetime]::P arse($curr. Date.Substring(0, [Matematyka]::Min(10, $curr. Data.Długość))) $daysDiff = ($currDate - $prevDate). TotalDays if ($daysDiff -gt 0) { $updDiff = [int]$curr. Zaktualizowano — [int]$prev. Aktualizacja if ($updDiff -gt 0) { $stDevicesPerDay = [matematyka]::Round($updDiff / $daysDiff; 0) $stVelocitySource = "TrendHistory" } } } } # Spróbuj podsumować wdrożenie aranżacji lub (ma prędkość wstępnie obliczoną) if ($stVelocitySource -eq "N/D" -i $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { wypróbuj { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [matematyka]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = "Aranżator" if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } złapać { } } # Rezerwowy: spróbuj poprzedniego podsumowania CSV (wyszukaj bieżący folder ORAZ foldery agregacji elementu nadrzędnego/równorzędnego) if ($stVelocitySource -eq "N/D") { $searchPaths = @( (Ścieżka sprzężenia $OutputPath "SecureBoot_Summary_*.csv") ) # Wyszukaj również foldery agregacji rodzeństwa (aranżacja lub tworzenie nowego folderu po każdym uruchomieniu) $parentPath = Split-Path $OutputPath -Rodzic if ($parentPath) { $searchPaths += ($parentPath ścieżka sprzężenia "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += ($parentPath ścieżka sprzężenia "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descending | Select-Object -Pierwszy 1 if ($prevSummary) { wypróbuj { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Data_uzyskania) — $prevDate). TotalDays jeżeli ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Zaktualizowano — $prevUpdated if ($updDelta -gt 0) { $stDevicesPerDay = [matematyka]::Round($updDelta / $daysSinceLast; 0) $stVelocitySource = "PreviousReport" } } } złapać { } } } # Rezerwowy: obliczanie prędkości z pełnego zakresu historii trendu (pierwszy a ostatni punkt danych) if ($stVelocitySource -eq "N/D" -i $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Suma -gt 0 -i [int]$_. Zaktualizowano -ge 0 }) if ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime]::P arse($first. Date.Substring(0, [Matematyka]::Min(10, $first. Data.Długość))) $lastDate = [datetime]::P arse($last. Date.Substring(0, [Matematyka]::Min(10, $last. Data.Długość))) $daysDiff = ($lastDate - $firstDate). TotalDays if ($daysDiff -gt 0) { $updDiff = [int]$last. Zaktualizowano — [int]$first. Aktualizacja if ($updDiff -gt 0) { $stDevicesPerDay = [matematyka]::Round($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # Obliczanie projekcji przy użyciu wykładniczego podwojenia (zgodne z wykresem trendu) # Ponowne używanie danych projekcji już obliczonych dla wykresu, jeśli są dostępne if ($hasProjection -and $projDates.Count -gt 0) { # Użyj ostatniej przewidywanej daty (po zaktualizowaniu wszystkich urządzeń) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("Dd MMM, yyyy") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Dni $stWorkingDays = 0 $d = Data uzyskania for ($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 -i $stNotUptodate -gt 0) { # Rezerwowy: projekcja liniowa, jeśli nie ma dostępnych danych wykładniczych $daysNeeded = [matematyka]::Sufit($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("Dd MMM, yyyy") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Data uzyskania for ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Prędkość kompilacji HTML $velocityHtml = if ($stDevicesPerDay -gt 0) { "<div><strong>🚀 Urządzenia/dzień:</strong> $($stDevicesPerDay.ToString('N0')) (źródło: $stVelocitySource)</div>" + "<div><strong>📅 Przewidywane zakończenie:</strong> $stProjectedDate" + $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>⚠ PAST DEADLINE</span>" } else { " <span style='color:#28a745'>✓ Before deadline</span>" }) + "</div>" + "<div><strong>🕐 Dni robocze:</strong> $stWorkingDays | <silne>Calendar Dni:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Deadline: 24 czerwca 2026 r. (wygaśnięcie certyfikatu KEK) | Pozostałe dni: $stDaysToDeadline</div>" } inaczej { "<div style='padding:8px; tło:#fff3cd; promień obramowania:4px; border-left:3px solid #ffc107'>" + "<silne>📅 Zakończenie projektu:</strong> Za mało danych do obliczania prędkości. " + "Uruchom agregację co najmniej dwa razy ze zmianami danych, aby ustalić wartość rate.<br/>" + "<strong>Deadline:</strong> 24 czerwca 2026 r. (wygaśnięcie certyfikatu KEK) | <>dni pozostałe:</strong> $stDaysToDeadline</div>" } # Cert expiry countdown $certToday = Data uzyskania $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematyka]::Maksimum(0; ($certKekExpiry — $certToday). Dni) $daysToUefi = [matematyka]::Maksimum(0; ($certUefiExpiry — $certToday). Dni) $daysToPca = [matematyka]::Maksimum(0; ($certPcaExpiry — $certToday). Dni) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } inaczej { '#28a745' } # Pomocnik: Odczytywanie rekordów z JSON, podsumowanie zasobnika kompilacji + pierwsze wiersze urządzenia N $maxInlineRows = 200 funkcja Build-InlineTable { param([ciąg]$JsonPath;[int]$MaxRows = 200; [ciąg]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (Test-Path $JsonPath) { wypróbuj { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Liczba # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats if ($totalCount -gt 0) { $buckets = $data | Identyfikator zasobnika Group-Object | Sort-Object liczba -malejąco $bucketSummary = "><2 h3 style='font-size:.95em; kolor:#333; margines:10 pikseli 0 5 pikseli ><3 według zasobnika sprzętowego ($($buckets. Liczba) zasobników)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; przepełnienie-y:auto; dolny margines:tabela ><15 pikseli><><tr><><5 Identyfikator zasobnika><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; kolor:#28a745 >Zaktualizowano</th><th style='text-align:right; color:#dc3545'>Not Updated</th><th><1 Manufacturer><2 /th></tr></thead><tbody>" foreach ($b w $buckets) { $bid = if ($b.Name) { $b.Name } inaczej { "(puste)" } $mfr = ($b.Group | Select-Object -Pierwszy 1). WMI_Manufacturer # Pobierz zaktualizowaną statystykę z globalnych statystyk zasobnika (wszystkie urządzenia w tym zasobniku w całym zestawie danych) $lookupKey = $bid $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } inaczej { $null } $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } inaczej { 0 } $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } inaczej { $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; kolor:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; kolor:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # SZCZEGÓŁY URZĄDZENIA: Pierwsze wiersze N jako płaska lista $slice = $data | Select-Object — pierwszy $MaxRows foreach ($d w $slice) { $conf = $d.UfnośćPoziom $confBadge = if ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge-danger">Nieobsługiwana><6 /span>' } elseif ($conf -match "Under") { '<span class="badge-info">Under Obs><0 /span>' } elseif ($conf -match "Paused") { '<span class="badge-warning">Wstrzymane><4 /span>' } inaczej { '<span class="badge-warning">Action Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="badge-success"><01 Zaktualizowano</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge badge-danger"><05 Error</span>' } inaczej { '><08 span class="badge-warning"><09 Oczekiwanie na><0 /span>' } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20><9 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 /td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n" } } złapać { } } if ($totalCount -eq 0) { zwróć "><44 div style='padding:20px; kolor:#888; font-style:kursywa'><45 Brak urządzeń w tej kategorii.><46 /div>" } $showing = [matematyka]::Min($MaxRows;$totalCount) $header = "><48 div style='margin:5px 0; rozmiar czcionki:.85em; color:#666'><49 Suma: urządzenia $($totalCount.ToString("N0")) if ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; grubość czcionki:>📄 Pobierz pełny plik CSV dla programu Excel><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; kolor:#333; margin:10px 0 5px ><58 Szczegóły urządzenia (wyświetlanie pierwszego $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><0 HostName><1 /th><th><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th><th><6 Status><7 /th><th><0 Error><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>" zwróć "$header$bucketSummary$deviceHeader$deviceTable" } # Tworzenie wbudowanych tabel z plików JSON już na dysku, łączących się z plikami CSV $tblErrors = Build-InlineTable -JsonPath ($dataDir ścieżka sprzężenia "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath ($dataDir ścieżka sprzężenia "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath ($dataDir ścieżka sprzężenia "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath ($dataDir ścieżki sprzężenia "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath ($dataDir ścieżka sprzężenia "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath ($dataDir ścieżka sprzężenia "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # Tabela niestandardowa dla oczekującej aktualizacji — zawiera kolumny UEFICA2023Status i UEFICA2023Error $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (Test-Path $upJsonPath) { wypróbuj { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Liczba if ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; rozmiar czcionki:.85em; color:#666'>Suma: urządzenia $($upCount.ToString("N0")) | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>📄 Pobierz pełny plik CSV dla programu Excel><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object — pierwszy $maxInlineRows foreach ($d w $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } inaczej { '<span style="color:#999">null><0 /span>' } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } inaczej { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } inaczej { '-' } $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge-success">Tak><8 /span>' } inaczej { '-' } $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 = [matematyka]::Min($maxInlineRows; $upCount) $upDevHeader = "<h3 style='font-size:.95em; kolor:#333; margin:10px 0 5px'>Szczegóły urządzenia (wyświetlanie pierwszego $upShowing)</h3>" $upTable = "<div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><9 HostName><0 /th><th><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFI ZASADY><6 /th><th><9</th><>klucz wincs</th><>BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } inaczej { $tblUpdatePending = "<div style='padding:20px; kolor:#888; styl czcionki:kursywa >Brak urządzeń w tej kategorii.</div>" } } złapać { $tblUpdatePending = "<div style='padding:20px; kolor:#888; font-style:kursywa'>Brak urządzeń w tej kategorii.</div>" } } inaczej { $tblUpdatePending = "<div style='padding:20px; kolor:#888; font-style:kursywa'>Brak urządzeń w tej kategorii.</div>" } # Cert expiry countdown $certToday = Data uzyskania $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematyka]::Maksimum(0; ($certKekExpiry — $certToday). Dni) $daysToUefi = [matematyka]::Maksimum(0; ($certUefiExpiry — $certToday). Dni) $daysToPca = [matematyka]::Maksimum(0; ($certPcaExpiry — $certToday). Dni) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } inaczej { '#28a745' } # Build manufacturer chart data inline (Top 10 by device count) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Malejąco | Select-Object -Pierwsza 10 $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "Według producenta" } inaczej { "10 najlepszych producentów" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Key)'" }) -join "," $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join "," $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join "," $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) -join "," $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join "," $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join "," $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) -join "," $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join "," $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join "," $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join "," Tabela producenta kompilacji # $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Malejąco | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Klucz)</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 głowy < <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <tytuł><9 pulpit nawigacyjny stanu certyfikatu bezpiecznego rozruchu><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 stylu < *{box-sizing:border-box; margines:0; dopełnienie:0} body{font-family:'Segoe UI',Tahoma,sans-serif; tło:#f0f2f5; kolor:#333} .header{background:gradient liniowy(135deg;#1a237e;#0d47a1); kolor:#fff; dopełnienie:20 pikseli 30 pikseli} .header h1{font-size:1.6em; margines-dół:5 pikseli} .header .meta{font-size:.85em; nieprzezroczystość:.9} .container{max-width:1400px; margines:0 automatycznie; dopełnienie:20 pikseli} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); gap:12px; margines:20 pikseli 0} .card{background:#fff; promień obramowania:10 pikseli; dopełnienie:15 pikseli; cień-ramka:0 2 piksele 8 pikseli rgba(0;0\0;,08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); cień-skrzynka:0 4 pikseli 15 pikseli rgba(0\0\0\.12)} .card .value{font-size:1.8em; grubość czcionki:700} .card .label{font-size:.8em; kolor:#666; margines-top:4px} .card .pct{font-size:.75em; kolor:#888} .section{background:#fff; promień obramowania:10 pikseli; dopełnienie:20 pikseli; margines:15 pikseli 0; box-shadow:0 2px 8px rgba(0;0\0\.08)} .section h2{font-size:1.2em; kolor:#1a237e; margines-dół:10 pikseli; kursor:wskaźnik; wybór użytkownika:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; gap:20px; margines:20 pikseli 0} .chart-box{background:#fff; promień obramowania:10 pikseli; dopełnienie:20 pikseli; box-shadow:0 2px 8px rgba(0;0\0\.08)} tabela{width:100%; border-collapse:collapse; rozmiar czcionki:.85em} th{background:#e8eaf6; dopełnienie:8 pikseli 10 pikseli; wyrównanie tekstu:do lewej; pozycja:lepka; góra:0; indeks z:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; dopełnienie:2 piksele 8 pikseli;promień obramowania:10 pikseli; rozmiar czcionki:.75em; grubość czcionki:700} .badge-success{background:#d4edda; color:#155724} .badge-danger{background:#f8d7da; kolor:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; kolor:#0c5460} .top-link{float:right; rozmiar czcionki:.8em; kolor:#1a237e; text-decoration:none} .footer{text-align:center; dopełnienie:20 pikseli; kolor:#999; rozmiar czcionki:.8em} a{color:#1a237e} </style><9 </head> <> ciała <div class="header"> <h1>pulpit nawigacyjny stanu certyfikatu bezpiecznego rozruchu</h1> <div class="meta">Wygenerowane: $($stats. ReportGeneratedAt) | Łączna liczba urządzeń: $($c.Total.ToString("N0")) | Unikatowe zasobniki: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">
<!-- karty wskaźników KPI — klikalne, połączone z sekcjami — > <div class="cards"> <class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; pozycja:relative"><div style="position:absolute; góra:8 pikseli; prawy:8 pikseli; tło:#dc3545; kolor:#fff; dopełnienie:1 pikseli 6 pikseli; promień obramowania:8 pikseli; rozmiar czcionki:.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)% — WYMAGA DZIAŁANIA><0 /div></a><3 <class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; pozycja:relative"><div style="position:absolute; góra:8 pikseli; prawy:8 pikseli; tło:#28a745; kolor:#fff; dopełnienie:1 pikseli 6 pikseli; promień obramowania:8 pikseli; rozmiar czcionki:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString("N0")</div><div class="label">Zaktualizowano><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3 <class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; pozycja:relative"><div style="position:absolute; góra:8 pikseli; prawy:8 pikseli; tło:#6c757d; kolor:#fff; dopełnienie:1 pikseli 6 pikseli; promień obramowania:8 pikseli; rozmiar czcionki:.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})% — poza zakresem><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">Needs Reboot><2 /div><div class=""pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% — oczekiwanie na ponowne uruchomienie komputera><6 /div></a><9 <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">UpdatePending</div<><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Zasady/WinCS zastosowane, oczekiwanie na aktualizację><2 /div></a><5 <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">Rollout In Progress><4 /div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.RolloutInProgress/$c.Total)*100;1)}else{0})%</div></a><11 <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">High Confidence><20 /div><div class="pct">$($stats). PercentHighConfidence)% — bezpieczne do wdrożenia><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){[matematyka]::Round(($c.UnderObs/$c.Total)*100;1)}inaczej{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">Action Required><2 /div><div class="pct">$($stats). PercentActionRequired)% — musi zostać przetestowany><6 /div></a><9 <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">At Risk><68 /div><div class="pct">$($stats. PercentAtRisk)% — podobny do nieudanego><2 /div></a><5 <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){[matematyka]::Round(($c.TaskDisabled/$c.Total)*100,1)}inaczej{0})% - Zablokowane><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. Wstrzymano</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></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">Known Issues><6 /div><div class="pct">$(if($c.Total -gt 0){[matematyka]::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">Missing KEK</div><div class="pct">$(if($c.Total -gt 0){[matematyka]::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0})%</div></a> <class="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)% — błędy UEFI</div></a> ><6 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. Błędy</div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></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=""pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
<!-- Data wygaśnięcia certyfikatu & wdrażania — > <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margines:15 pikseli 0"> <div class="section" style="margin:0"> <h2>📅>prędkości wdrażania</h2 <div class="section-body open"> <div style="font-size:2.5em; czcionka-waga:700; color:#28a745">$($c.Updated.ToString("N0"))</div> <div style="color:#666">urządzenia zaktualizowane z $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; tło:#e8eaf6; wysokość:20 pikseli; promień obramowania:10 pikseli; przepełnienie:hidden"><div style="background:#28a745; wysokość:100%; szerokość:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; kolor:#888">$($stats. Wykonano percentcertUpdated)% ukończenia</div> <div style="margin-top:10px; dopełnienie:10 pikseli; tło:#f8f9fa; promień obramowania:8 pikseli; rozmiar czcionki:.85em"> <div><silne>Pozostałe:</strong> $($stNotUptodate.ToString("N0")) urządzenia wymagają działania</div> <div><strong>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) devices (errors + permanent + task disabled)</div> <div><strong>Safe to deploy:</strong> $($stSafeList.ToString("N0")) urządzenia (ten sam zasobnik co udany)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; border-left:4px solid #dc3545"> <h2 style="color:#dc3545">⚠ Countdown Certificate Expiry Countdown</h2> <div class="section-body open"> <div style="display:flex; gap:15px; margines-góra:10 pikseli"> <div style="text-align:center; dopełnienie:15 pikseli; promień obramowania:8 pikseli; minimalna szerokość:120 pikseli; tło:gradient liniowy(135deg;#fff5f5;#ffe0e0); obramowanie:2 piksele #dc3545 ciągły; flex:1"> <div style="font-size:.65em; kolor:#721c24; przekształcanie tekstu:wielkie litery; font-weight:bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size:.85em; grubość czcionki:pogrubienie; kolor:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; czcionka-waga:700; kolor:#dc3545; wysokość linii:1"><9 $daysToKek</div> ><2 div style="font-size:.8em; color:#721c24"><3 dni (24 czerwca 2026 r.)><4 /div> ><6 /div> ><8 div style="text-align:center; dopełnienie:15 pikseli; promień obramowania:8 pikseli; minimalna szerokość:120 pikseli; tło:gradient liniowy(135deg;#fffef5;#fff3cd); obramowanie:2 piksele #ffc107 ciągły; flex:1"><9 <div style="font-size:.65em; kolor:#856404; przekształcanie tekstu:wielkie litery; font-weight:bold">UEFI CA 2011</div> <div id="daysUefi" style="font-size:2.2em; czcionka-waga:700; kolor:#856404; wysokość linii:1; margin:5px 0">$daysToUefi</div> <div style="font-size:.8em; color:#856404">dni (27 czerwca 2026 r.)</div> </div> <div style="text-align:center; dopełnienie:15 pikseli; promień obramowania:8 pikseli; minimalna szerokość:120 pikseli; tło:gradient liniowy(135deg;#f0f8ff;#d4edff); obramowanie:2 piksele #0078d4 ciągłe; flex:1"> <div style="font-size:.65em; kolor:#0078d4; przekształcanie tekstu:wielkie litery; font-weight:bold">Windows PCA</div> <div id="daysPca" style="font-size:2.2em; czcionka-waga:700; kolor:#0078d4; wysokość linii:1; margines:5 pikseli 0">$daysToPca><2 /div><3 <div style="font-size:.8em; color:#0078d4">dni (19 października 2026)</div><7 </div><9 </div><1 <div style="margin-top:15px; dopełnienie:10 pikseli; tło:#f8d7da; promień obramowania:8 pikseli; rozmiar czcionki:.85em; border-left:4px solid #dc3545"> <silne>⚠ KRYTYCZNE:</strong> Wszystkie urządzenia muszą zostać zaktualizowane przed wygaśnięciem certyfikatu. Urządzenia, które nie zostały zaktualizowane w terminie ostatecznym, nie mogą zastosować przyszłych aktualizacji zabezpieczeń menedżera rozruchu i bezpiecznego rozruchu po wygaśnięciu.</div> </div> </div> </div>
wykresy<!-- —> <div class="charts"> <div class="chart-box"><h3><stanu wdrożenia /h3><canvas id="deployChart" height="200"></canvas></div><5 <div class="chart-box"><h3><9 $mfrChartTitle</h3><canvas id="mfrChart" height="200"></canvas></div> </div>
$(if ($historyData.Count -ge 1) { "<!-- Historical Trend Chart --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Aktualizowanie postępu w czasie <class='top-link' href='#'>↑ Top</a></h2> <div id='d-trend' class='section-body open'> <canvas id='trendChart' height='120'></canvas> <div style='font-size:.75em; kolor:#888; margines-top:5px'>Linie ciągłe = rzeczywiste dane$(jeśli ($historyData.Count -ge 2) { " | Linia kreskowana = rzutowana (podwojenie wykładnicze: 2→4→8→16... urządzeń na falę)" } inaczej { " | Uruchom agregację ponownie jutro, aby zobaczyć linie trendu i projekcję" })</div> </div> </div>" })
<!-- plików DO POBRANIA CSV — > <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Pobierz pełne dane (CSV dla programu Excel) <a class="top-link" href="#">Top</a></h2><2 <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap:5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; tło:#dc3545; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; rozmiar czcionki:.8em">Nie zaktualizowano ($($stNotUptodate.ToString("N0")))</a><8 <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; tło:#dc3545; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a> <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; tło:#fd7e14; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; rozmiar czcionki:.8em">Wymagane działanie ($($c.ActionReq.ToString("N0")))</a> <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; tło:#dc3545; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; font-size:.8em">Znane problemy ($($c.WithKnownIssues.ToString("N0")))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; tło:#dc3545; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; rozmiar czcionki:.8em">Wyłączono zadanie ($($c.TaskDisabled.ToString("N0")))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; tło:#28a745; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; rozmiar czcionki:.8em">Zaktualizowano ($($c.Updated.ToString("N0")))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; tło:#6c757d; kolor:#fff; dopełnienie:6 pikseli 14 pikseli; promień obramowania:5 pikseli; text-decoration:none; font-size:.8em">Summary</a> <div style="width:100%; rozmiar czcionki:.75em; kolor:#888; margines-top:5px">pliki CSV otwarte w programie Excel. Dostępne w przypadku hostowanych na serwerze sieci Web.</div> </div> </div>
Awaria producenta<!-- — > <div class="section"> <h2 onclick="toggle('mfr')">Według producenta <a class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <tabela><><><><1><2><><5><6><6><9><><9><0 /th><><3><4 ufność /><><7 Akcja Wymagana><8 /></tr></><3 <tbody><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- sekcji urządzenia (pierwszych 200 w tekście + csv do pobrania) -- > <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Urządzenia z błędami ($($c.WithErrors.ToString("N0"))) <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">🔴 Znane problemy ($($c.WithKnownIssues.ToString("N0"))) <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')">🟠 Brak KEK — zdarzenie 1803 ($($c.WithMissingKEK.ToString("N0"))) <class="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">🟠 Wymagane działanie ($($c.ActionReq.ToString("N0"))) <class="top-link" href="#">↑ ><4 najwyższego poziomu /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">🔵 W obszarze Obserwacja ($($c.UnderObs.ToString("N0"))) <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">🔴 Nie zaktualizowano ($($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">🔴 Zadanie wyłączone ($($c.TaskDisabled.ToString("N0"))) >↑ 5 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">🔴 Błędy tymczasowe ($($c.TempFailures.ToString("N0"))) <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">🔴 Permanent Failures / Not Supported ($($c.PermFailures.ToString("N0"))) <a class="top-link" href="#">↑ Top</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">⏳ Oczekująca aktualizacja ($($c.UpdatePending.ToString("N0"))) - Zasady/WinCS Zastosowane, Oczekiwanie na aktualizację <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margines-dół:10px">Urządzenia, na których zastosowano klucz AvailableUpdatesPolicy lub WinCS, ale uefica2023Status jest nadal nierozpoczynany, InProgress lub null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Wdrożenie w toku ($($c.RolloutInProgress.ToString("N0"))) <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"))) - Poza zakresem <class="top-link" href="#">↑<najwyższego</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">🟢 Zaktualizowane urządzenia ($($c.Updated.ToString("N0"))) <class="top-link" href="#">↑ Najlepsze</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">🔄 Zaktualizowano — wymaga ponownego uruchomienia ($($c.NeedsReboot.ToString("N0"))) <class="top-link" href="#">↑ Top</a></h2> <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>
<div class="footer">pulpit nawigacyjny wdrażania certyfikatu bezpiecznego rozruchu | Wygenerowano $($stats. ReportGeneratedAt) | StreamingMode | Szczytowa pamięć: ${stPeakMemMB} MB</div> </div><!-- /container -->
<skrypt> przełącznik funkcji(id){var e=document.getElementById(id); e.classList.toggle('open')} funkcja 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. Paused','Not Supported','SecureBoot OFF','With Errors'],datasets:[{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'''#adb5bd'''#dc3545']}]};options:{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'},{ etykieta:'Temp. Paused',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd''},{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}}); Wykres trendu historycznego 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 datasets = [ {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} ]; if (projLen > 0) { datasets.push({label:'Projected Updated (2x podwojenie)',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,scales:{beginAtZero:true,title:{display:true, text:'Devices'}},x:{title:{display:true,text:'Date'}}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}} } Dynamiczne odliczanie (funkcja(){var t=new Date(),k=nowa data('2026-06-24');u=nowa data('2026-06-27');p=nowa data('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> </> ciała </html> "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent; [System.Text.UTF8Encoding]::new($false)) # Zawsze przechowuj stabilną kopię "Najnowsza", aby administratorzy nie musieli śledzić sygnatur czasowych $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # Zapisywanie manifestu pliku w trybie przyrostowym (szybkie wykrywanie bez zmian w następnym uruchomieniu) if ($IncrementalMode -lub $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 "Zapisywanie manifestu pliku w trybie przyrostowym..." -ForegroundColor Gray foreach ($jf w $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Rozmiar = $jf. Długość } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Saved manifest for $($stNewManifest.Count) files" -ForegroundColor DarkGray } # OCZYSZCZANIE PRZECHOWYWANIA # Aranżacji lub folderu wielokrotnego użytku (Aggregation_Current): zachowaj tylko najnowsze uruchomienie (1) # Administracja ręczne uruchamia / inne foldery: zachowaj ostatnie 7 działa # Summary CSV ARE NEVER deleted - they're tiny (~1 KB) and are the backup source for trend history $outputLeaf = Split-Path $OutputPath -Liść $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } inaczej { 7 } # Bezpieczne prefiksy plików do oczyszczenia (migawki okresowe na bieg) $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_" ) # Znajdź wszystkie unikatowe sygnatury czasowe tylko z plików do czyszczenia $cleanableFiles = Get-ChildItem $OutputPath -Plik -EA SilentlyContinue | Where-Object { $f = $_. Nazwa; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Liczba -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { if ($_. Name -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Unikatowy -Malejąco) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object — pomiń $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs w $oldTimestamps) { foreach ($prefix w $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f w $oldFiles) { $freedBytes += $f.Długość Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [matematyka]::Round($freedBytes / 1MB, 1) Write-Host "Oczyszczanie przechowywania: usunięto pliki $removedFiles ze starych wersji $($oldTimestamps.Count), uwolniono plik ${freedMB} MB (zachowując ostatni $retentionCount + wszystkie csvy podsumowania/notUptodate)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Cyan Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -Pierwszy planColor Cyan Write-Host " Total Devices: $($c.Total.ToString("N0"))" -ForegroundColor White Write-Host " NIE ZAKTUALIZOWANO: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -Pierwszy planColor $(jeśli ($stNotUptodate -gt 0) { "Żółty" } inaczej { "Zielony" }) Write-Host " Zaktualizowano: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -Pierwszy planKolor zielony Write-Host " With Errors: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Peak Memory: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Time: $([math]::Round($stTotal/60;1)) min" -ForegroundColor White Write-Host " Dashboard: $htmlPath" -ForegroundColor White return [PSCustomObject]$stats } #endregion TRYB PRZESYŁANIA STRUMIENIOWEGO } inaczej { Write-Error "Nie można odnaleźć ścieżki wprowadzania: $InputPath" wyjdź 1 }