POMEMBNO Ta članek, ki vsebuje ta vzorčni skript, je bil ukinjen. Od izdaje posodobitev sistema Windows 12. maja 2026 in po tem se vzorčni skript nahaja v mapi %systemroot%\SecureBoot\ExampleRolloutScripts v vaši napravi.
Kopirajte in prilepite ta vzorčni skript ter ga po potrebi spremenite za svoje okolje:
<# . SINOPSIS Združi podatke JSON stanja varnega zagona iz več naprav v povzetke poročil.
. OPIS Bere zbrani stanje Varnega zagona JSON datoteke in ustvarja: – nadzorna plošča HTML z grafikoni in filtriranjem - Povzetek po ConfidenceLevel - Edinstvena analiza vedra naprave za strategijo preskušanja Podpira: – datoteke na računalnik: HOSTNAME_latest.json (priporočeno) - Single JSON file HostName samodejno deduplicates, ohranjanje najnovejše CollectionTime. Privzeto so vključene le naprave z zaupanjem »Action Req« ali »High« da se osredotočite na vedra, ki jih je mogoče izvesti. Za preglasitev uporabite -IncludeAllConfidenceLevels.
. PARAMETER InputPath Pot do datotek JSON: - Folder: Reads all *_latest.json files (or *.json if no _latest files) - File: Reads single JSON file
. PARAMETER OutputPath Pot za ustvarjena poročila (privzeto: .\SecureBootReports)
. PRIMER # Združevanje iz mape z datotekami na računalnik (priporočeno) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Bere: \\contoso\SecureBootLogs$\*_latest.json
. PRIMER # Izhodno mesto po meri .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. PRIMER # Vključi le Action Req in High confidence (privzeto vedenje) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Izključuje: opazovanje, začasno ustavljeno, ni podprto
. PRIMER # Vključi vse ravni zaupanja (preglasi filter) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. PRIMER # Filter ravni zaupanja po meri .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. PRIMER # ENTERPRISE SCALE: Postopni način – obdelajo le spremenjene datoteke (hiter nadaljnji postopek) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # First run: Full load ~2 hours for 500K devices # Naslednji teki: Število sekund, če ni sprememb, minut za delta
. PRIMER # Preskoči HTML, če se nič ne spremeni (najhitreje za nadzorovanje) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Če od zadnjega zagona ni bila spremenjena nobena datoteka: ~5 sekund
. PRIMER # Način samo za povzetek – preskočite velike tabele naprav (1–2 minuti v primerjavi z 20 minutami) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Ustvari datoteke CSV, vendar preskoči nadzorno ploščo HTML s polnimi tabelami naprave
. OPOMBE Seznanite se Detect-SecureBootCertUpdateStatus.ps1 uvedbo v podjetjih.Glejte GPO-DEPLOYMENT-GUIDE.md popoln vodnik za uvedbo. Privzeto vedenje izključuje opazovanje, začasno ustavljeno in naprave, ki niso podprte , da poročanje osredotočite le na vedra naprav, ki jih je mogoče ukrepati.#>
param ( [Parameter(Obvezno = $true)] [string]$InputPath, [Parameter(Obvezno = $false)] [string]$OutputPath = ".\SecureBootReports", [Parameter(Obvezno = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Obvezno = $false)] [string]$RolloutStatePath, # Path to RolloutState.json to identify InProgress devices [Parameter(Obvezno = $false)] [string]$RolloutSummaryPath, # Path to SecureBootRolloutSummary.json iz orchestrator (vsebuje podatke projekcije) [Parameter(Obvezno = $false)] [string[]]$IncludeConfidenceLevels = @("Zahtevano je dejanje", "Visoka raven zaupanja"), # Vključuje le te ravni zaupanja (privzeto: samo vedra, ki jih je mogoče izvesti) [Parameter(Obvezno = $false)] [switch]$IncludeAllConfidenceLevels, # Override filter to include all confidence levels [Parameter(Obvezno = $false)] [switch]$SkipHistoryTracking, [Parameter(Obvezno = $false)] [switch]$IncrementalMode, # Enable delta processing - only load changed files since last run [Parameter(Obvezno = $false)] [string]$CachePath, # Pot do predpomnjenja imenika (privzeto: OutputPath\.cache) [Parameter(Obvezno = $false)] [int]$ParallelThreads = 8, # Število vzporednih niti za nalaganje datotek (PS7+) [Parameter(Obvezno = $false)] [switch]$ForceFullRefresh, # Force full reload even in incremental mode [Parameter(Obvezno = $false)] [switch]$SkipReportIfUnchanged, # Skip HTML/CSV generation if no files changed (just output stats) [Parameter(Obvezno = $false)] [switch]$SummaryOnly, # Generate summary stats only (no large device tables) - much faster [Parameter(Obvezno = $false)] [switch]$StreamingMode # Način, ki učinkovito deluje s pomnilnikom: procesni koščki, postopno pisanje datotek CSV, obdrži samo povzetke v pomnilniku )
# Samo popravilo: Pri kopiranju iz člankov HTML s spletnega mesta cms odstranite nevidne znake Unicode.# V support.microsoft.com CMS vstavi presledke ničelne širine (U+200B), netržne presledke (U+00A0) in druge # nevidni znaki okrog oznak html znotraj tukaj– nizi, zaradi česar pride do napak pri razčlenjevanjem ogrodja PowerShell.if ($MyInvocation.MyCommand.Path) { $rawScript = [System.IO.File]::ReadAllText($MyInvocation.MyCommand.Path) if ($rawScript -match '[\u200B-\u200F\uFEFF]' -or $rawScript -match '\xA0') { Write-Host »OPOZORILO: Zaznani nevidni znaki Unicode (verjetno iz spletnega kopiranja in lepljenja) – skript za samodejno čiščenje ...-ForegroundColor Yellow $cleaned = $rawScript -replace '[\u200B-\u200F\uFEFF]', '' $cleaned = $cleaned -replace '\xA0', ' ' [System.IO.File]::WriteAllText($MyInvocation.MyCommand.Path, $cleaned, [System.Text.UTF8Encoding]::new($false)) Write-Host »Script cleaned successfully. Vnovični zagon ...-OspredjeBarva zelena & $MyInvocation.MyCommand.Path @PSBoundParameters exit $LASTEXITCODE } }
# Samodejno povzdignite na PowerShell 7, če je na voljo (6x hitreje za velike nabore podatkov) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ($pwshPath) { Write-Host zaznan ukaz »PowerShell $($PSVersionTable.PSVersion) – ponovno zaganjanje z ogrodjem PowerShell 7 za hitrejšo obdelavo ... – ospredjeBarva rumena # Obnovi seznam argumentov iz vezanih parametrov $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key v $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] if ($val -is [switch]) { če ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [array]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } drugo { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs exit $LASTEXITCODE } }
$ErrorActionPreference = »Nadaljuj« $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = »Vzorci uvajanja in spremljanja«
# Opomba: Ta skript nima odvisnosti od drugih skriptov.# Za celoten nabor orodij prenesite: $DownloadUrl -> $DownloadSubPage
#region namestitev Write-Host "=" * 60 -OspredjeBarva cijan Write-Host "Secure Boot Data Aggregation" -ForegroundColor Cyan Write-Host "=" * 60 -OspredjeBarva cijan
# Ustvari izhodni imenik if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Zunaj ničelne vrednosti }
# Nalaganje podatkov – podpira obliki zapisa CSV (podedovano) in JSON (izvorno) Write-Host »'nOnos podatkov iz: $InputPath« -ForegroundColor Yellow
# Helper function to normalize device object (handle field name differences) funkcija Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON uses Hostname, CSV uses HostName) če ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force (Ime gostitelja) } # Handle Confidence in ConfidenceLevel (JSON uses Confidence, CSV uses ConfidenceLevel) # ConfidenceLevel je uradno ime polja - zemljevid Zaupanje, da ga če ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Confidence -Force } # Track update status via Event1808Count OR UEFICA2023Status="Updated" # To omogoča sledenje števila naprav v vsakem vedru zaupanja, ki so bile posodobljene $event 1808 = 0 če ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false če ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Posodobljeno") { $uefiCaUpdated = $true } if ($event 1808-gt 0 -or $uefiCaUpdated) { # Označi kot posodobljeno za logiko nadzorne plošče/uveljavitve – vendar ne preglasi ravni ConfidenceLevel $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } drugo { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # Razvrstitev ConfidenceLevel: # - "High Confidence", "Under Observation...", "Temporarily Paused...", "Not Supported..."= use as-is # - Vse ostalo (null, prazno, "UpdateType:...", "Neznano", "N/A") = pade v dejanje, potrebno v števcih # Normalizacija ni potrebna – veja pretočnega števca jo obravnava } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON uporablja OEM*, legacy uses WMI_*) če ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName – Force } # Handle OEMModelNumber in WMI_Model če ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force } # Handle FirmwareVersion vs BIOSDescription če ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } return $device }
#region postopna obdelava/upravljanje predpomnilnika # Setup cache paths if (-not $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Funkcije za upravljanje predpomnilnika funkcija Get-FileManifest { param([niz]$Path) if (testna pot $Path) { poskusite { $json = Get-Content $Path -Raw | ConvertFrom-Json # Convert PSObject to hashtable (PS5.1 compatible - PS7 has -AsHashtable) $ht = @{} -$json. PSObject.Lastnosti | ForEach-Object { $ht[$_. Ime] = $_. Vrednost } return $ht } ulova { vrni @{} } } vrni @{} }
function Save-FileManifest { param([tabela z lojdr $Manifest, [niz]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Zunaj ničelne vrednosti } $Manifest | ConvertTo-Json -Depth 3 -Stisni | Set-Content $Path – vsili }
function Get-DeviceCache { param([niz]$Path) if (testna pot $Path) { poskusite { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host » Loaded device cache: $($cacheData.Count) devices« -ForegroundColor DarkGray return $cacheData } ulova { Write-Host "Cache corrupted, will rebuild" -ForegroundColor Yellow return @() } } return @() }
function Save-DeviceCache { param($Devices, [niz]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Zunaj ničelne vrednosti } # Pretvori v matriko in shrani $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Depth 10 -Stisni | Set-Content $Path – vsili Write-Host » Saved device cache: $($deviceArray.Count) devices« -ForegroundColor DarkGray }
function Get-ChangedFiles { param ( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Build case-insensitive lookup from manifest (normalize to lowercase) $manifestLookup = @{} foreach ($mk v $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file mestu $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalize path to male črke $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Velikost = $file. Dolžina } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] če ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Velikost –eq $file. Dolžina) { [void]$unchanged. Add($file) Nadaljuj } } [void]$changed. Add($file) } vrni @{ Spremenjeno = $changed Nespremenjeno = $unchanged NewManifest = $newManifest } }
# Ultra-fast parallel file loading using batched processing funkcija Load-FilesParallel { param ( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Count # Uporabite pakete ~1000 datotek za boljši nadzor pomnilnika $batchSize = [math]::Min(1000, [math]::Ceiling($totalFiles / [math]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
for ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [math]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Add($batch) } Write-Host " ($($batches. Count) batches of ~$batchSize files each)« -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # Preverite, ali je na voljo powershell 7+ parallel $canParallel = $PSVersionTable.PSVersion.Major -ge 7 če ($canParallel -in $Threads -gt 1) { # PS7+: Vzporedno obdelavo paketov $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file mestu $batchFiles) { poskusite { $content = [System.IO.File]::ReadAllText($file. Polnoime) | ConvertFrom-Json $batchResults.Add($content) } ulov { } } $batchResults.ToArray() } foreach ($batch mestu $results) { if ($batch) { foreach ($item v $batch) { $flatResults.Add($item) } } } } drugo { # PS5.1 fallback: Zaporedna obdelava (še vedno hitro za <10K datoteke) foreach ($file v $ Files) { poskusite { $content = [System.IO.File]::ReadAllText($file. Polnoime) | ConvertFrom-Json $flatResults.Add($content) } ulov { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (Testna pot $InputPath -PathType Leaf) { # Single JSON file if ($InputPath -like "*.json") { $jsonContent = Get-Content -Path $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host »Loaded $($allDevices.Count) records from file« (Naloženi zapisi $($allDevices.Count) iz datoteke« } drugo { Write-Error »Podprta je le oblika zapisa JSON. Datoteka mora imeti .json pripono.« izhod 1 } } elseif (Test-Path $InputPath -PathType Container) { # Mapa – samo JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Ime -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Prefer *_latest.json files if they exist (per-machine mode) $latestJson = $jsonFiles | Where-Object { $_. Name -like "*_latest.json" } if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count if ($totalFiles -eq 0) { Write-Error »Ni najdenih datotek JSON v: $InputPath« izhod 1 } Write-Host »Najdeno $totalFiles JSON datoteke« –ospredjeBarva siva # Helper function to match confidence levels (handles both short and full forms) # Določeno zgodaj, tako da jo lahko uporabljajo tako StreamingMode kot tudi običajne poti function Test-ConfidenceLevel { param([niz]$Value, [niz]$Match) if ([string]::IsNullOrEmpty($Value)) { return $false } stikalo ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value like "*Action Required*" -or $Value -eq "Action Required") } »Začasno zaustavljeno« { vrniti $Value kot »Začasno začasno ustavljeno*« } "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") } privzeto { return $false } } } #region STREAMING MODE - Memory-efficient processing for large datasets # Vedno uporabite StreamingMode za obdelavo z učinkovito pomnilnikom in nadzorno ploščo novega sloga če (ne $StreamingMode) { Write-Host »Auto-enabling StreamingMode (new-style dashboard)« (Samodejno omogočanje streamingMode (nadzorna plošča novega sloga)) -ForegroundColor Yellow $StreamingMode = $true if (-not $IncrementalMode) { $IncrementalMode = $true } } # Ko je omogočen StreamingMode, obdelajte datoteke v koščkih, ki v pomnilniku ohranjajo le števce.# Podatki na ravni naprave so zapisani v datoteke JSON na kos za nalaganje na zahtevo na nadzorni plošči.# Uporaba pomnilnika: ~1,5 GB ne glede na velikost nabora podatkov (v primerjavi z 10-20 GB brez pretakanja).if ($StreamingMode) { Write-Host »STREAMING MODE enabled - memory-efficient processing« -ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTAL CHECK: If no files changed since last run, skip processing entirely če ($IncrementalMode -in-not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" if (testna pot $stManifestPath) { Write-Host »Checking for changes since last streaming run...-ForegroundColor Cyan $stOldManifest = Get-FileManifest -Path $stManifestPath if ($stOldManifest.Count -gt 0) { $stChanged = $false # Hitro preverjanje: enako število datotek? if ($stOldManifest.Count -eq $totalFiles) { # Preverite 100 najnovejših datotek (razvrščenih po padajočem času LastWriteTime) # Če se je katera koli datoteka spremenila, bo imel najnovejši časovni žig in se bo najprej prikazal $sampleSize = [math]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Descending | Select-Object – prvi $sampleSize foreach ($sf mestu $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Odmor } # Compare timestamps - cached may be DateTime or string after JSON roundtrip $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc poskusite { # Če je predpomnjeno že DateTime (ConvertFrom-Json auto-converts), uporabite neposredno if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } drugo { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Odmor } } ulova { $stChanged = $true Odmor } } } drugo { $stChanged = $true } if (-not $stChanged) { # Preverite, ali izhodne datoteke obstajajo $stSummaryExists = Get-ChildItem (Join-Path $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object – prvi 1 $stDashExists = Get-ChildItem (Join-Path $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object – prvi 1 if ($stSummaryExists -and $stDashExists) { Write-Host »Zaznane niso nobene spremembe ($totalFiles nespremenjene) – preskakovanje obdelave« –OspredjeBarva zelena Write-Host » Zadnja nadzorna plošča: $($stDashExists.PolnoIme)« -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom- Csv Write-Host » Naprave: $($cachedStats.TotalDevices) | Posodobljeno: $($cachedStats.Posodobitev) | Napake: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host » Completed in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (no processing needed)« -ForegroundColor Green return $cachedStats } } drugo { # DELTA PATCH: Poiščite točno katere datoteke so se spremenile Write-Host »Zaznane spremembe – prepoznavanje spremenjenih datotek ...« – barva ospredjaBarva rumena $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf mestu $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } drugo { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc poskusite { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } ulov { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [math]::Round(($totalChanged/$totalFiles) * 100, 1) Write-Host » Spremenjeno: $($changedFiles.Count) | Novo: $($newFiles.Count) | Skupaj: $totalChanged ($changePct%)« -OspredjeBarva rumena if ($totalChanged -gt 0 -and $changePct -lt 10) { # DELTA PATCH MODE: <10% spremenjen, popravek obstoječih podatkov Write-Host " Delta patch mode ($changePct% < 10%) - patching $totalChanged files..." -ForegroundColor Green $dataDir = Join-Path $OutputPath "data" # Spremenjeno nalaganje/zapisi nove naprave $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df mestu $allDeltaFiles) { poskusite { $devData = Get-Content $df. FullName - Surova | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData če ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } ulov { } } Write-Host » Loaded $($deltaDevices.Count) changed device records« (Naloženi zapisi naprave so spremenjeni) – ospredjeBarva siva # Za vsako kategorijo JSON: odstranite stare vnose za spremenjena imena gostiteljev, dodajte nove vnose $categoryFiles = @("errors", "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 v $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat mestu $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" if (testna pot $catPath) { poskusite { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Odstranite stare vnose za spremenjena imena gostiteljev $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Znova razvrsti vsako spremenjeno napravo v kategorije # (se doda spodaj po razvrstitvi) $catData | ConvertTo-Json -Depth 5 | Set-Content $catPath -Encoding UTF8 } ulov { } } } # Razvrsti vsako spremenjeno napravo in pripni datotekam v pravi kategoriji foreach ($dev v $deltaDevices.Values) { $slim = [ordered]@{ HostName = $dev. Hostname WMI_Manufacturer = if ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } drugo { "" } WMI_Model = if ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } drugo { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } drugo { "" } ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. Funkcija »IsUpdated« UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } drugo { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } else { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } drugo { $null } SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } drugo { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) – in $dev. UEFICA2023Error -ne "0" -and $dev. UEFICA2023Napaka -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -or $dev. SecureBootTaskStatus -eq 'Disabled' -or $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = if ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } else { 0 } $e 1808 = if ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } else { 0 } $e 1803 = if ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -or $dev. MissingKEK -eq $true) $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = if ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" } # Append to matching category files $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 (-not $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 mestu $targets) { $tgtPath = Join-Path $dataDir "$tgt.json" if (testna pot $tgtPath) { $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Depth 5 | Set-Content $tgtPath -Encoding UTF8 } } } # Obnovi CSVs iz krpar JSONs Write-Host »Regenerating CSVs from patched data... -ForegroundColor Gray $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat mestu $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (preskusna pot $catJsonPath) { poskusite { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8 } } ulov { } } } # Recount stats from the patched JSON files Write-Host »Recalculating summary from patched data... -ForegroundColor 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 mestu $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } ulov { } } stikalo ($cat) { "updated_devices" { $pUpdated = $cnt } "errors" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # izračunano "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 | ConvertFrom-Json). Count $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host » Delta patch complete: $totalChanged devices updated« -ForegroundColor Green Write-Host » Skupaj: $pTotal | Posodobljeno: $pUpdated | NotUpdated: $pNotUpdated | Napake: $pErrors« -ForegroundColor White # Posodobi manifest $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf mestu $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Velikost = $jf. Dolžina } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host » Completed in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green # Fall through to full streaming reprocess to regenerate HTML dashboard # Podatkovne datoteke so že popravkov, tako da to zagotavlja, nadzorna plošča ostane posodobljena Write-Host »Regenerating dashboard from patched data...« -ForegroundColor Yellow } drugo { Write-Host » $changePct % spremenjene datoteke (>= 10 %) – zahtevana je polna pretočna reprocess« -ForegroundColor Yellow } } } } } # Create data subdirectory for on-demand device JSON files $dataDir = Join-Path $OutputPath »data« if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Deduplication via HashSet (O(1) per lookup, ~50MB for 600K hostnames) $seenHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Števci povzetkov z lahkotjo ($allDevices + $uniqueDevices v pomnilniku) $c = @{ Skupaj = 0; SBEnabled = 0; SBOff = 0 Posodobljeno = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; NotSupported = 0; NoConfData = 0 TaskDisabled = 0; TaskNotFound = 0; TaskDisabledNotUpdated = 0 WithErrors = 0; InProgress = 0; NotyetInitiated = 0; RolloutInProgress = 0 WithKnownIssues = 0; WithMissingKEK = 0; TempFailures = 0; PermFailures = 0; NeedsReboot = 0 UpdatePending = 0 } # Sledenje vedru za AtRisk/SafeList (lahki nabori) $stFailedBuckets = [System.Collections.Generic.HashSet[niz]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[niz]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Batch-mode device data files: accumulate per-chunk, flush at chunk boundaries $stDeviceFiles = @("errors", "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 mestu $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim device record for JSON output (only essential fields, ~200 bytes in ~2KB full) function Get-SlimDevice { param($Dev) return [ordered]@{ HostName = $Dev.HostName WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } drugo { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" } KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } else { $null } SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } drugo { $null } UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } drugo { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } else { $null } } } # Paket splakni v datoteko JSON (način prilaganja) funkcija Flush-DeviceBatch { param([niz]$StreamName, [System.Collections.Generic.List[object]]$Batch) if ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev mestu $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) } # GLAVNA PRETOČNA ZANKA $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } drugo { 10000 } $stTotalChunks = [matematika]::Ceiling($totalFiles/$stChunkSize) $stPeakMemMB = 0 če ($stTotalChunks -gt 1) { Write-Host »Processing $totalFiles files in $stTotalChunks chunks of $stChunkSize (streaming, $ParallelThreads threads):« -ForegroundColor Cyan } drugo { Write-Host »Processing $totalFiles files (streaming, $ParallelThreads threads):« -ForegroundColor Cyan } za ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [matematika]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] če ($stTotalChunks -gt 1) { Write-Host »Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count)): " -NoNewline -ForegroundColor Gray } drugo { Write-Host » Nalaganje datotek $($cFiles.Count): " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads # Na kose paketnih seznamov $cBatches = @{} foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 foreach ($raw mestu $rawDevices) { if (-not $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Hostname if (-not $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") if ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = if ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } drugo { "" } $hasErr = (-not [string]::IsNullOrEmpty($device. UEFICA2023Error) – in "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Napaka)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -or "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = if ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } drugo { "" } $e 1808 = if ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } else { 0 } $e 1801 = if ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } else { 0 } $e 1803 = if ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -or $device. Manjkajoči KEK -eq $true -ali "$($device. MissingKEK)" -eq "True") $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($device. KnownIssueId))) $rStat = if ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" } $mfr = if ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } drugo { "Neznano" } $bid = if (-not [string]::IsNullOrEmpty($bid)) { $bid } else { "" } # Zastavica pred izračunom čakajočih posodobitev (pravilnik/winCS je uporabljen, stanje še ni posodobljeno, SB VKLOPLJENO, opravilo ni onemogočeno) $uefiStatus = if ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } drugo { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy – in "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied -eq $true) $statusPending = ([niz]::IsNullOrEmpty($uefiStatus) -or $uefiStatus -eq 'NotStarted' -or $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -ali $hasWinCS) -and $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis) if ($isUpd) { $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices" ]. Add((Get-SlimDevice $device)) # Spremljajte posodobljene naprave, ki jih je treba znova zagnati (UEFICA2023Status=Updated, vendar event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF — zunaj obsega, ne razvrsti po zaupanju } druga oseba { if ($isUpdatePending) { } # Counted separateed in Update Pending — mutually exclusive for pie chart elseif (raven zaupanja v $conf »HighConfidence«) { $c.HighConf++ } elseif (raven zaupanja preskusa $conf »UnderObservation«) { $c.UnderObs++ } elseif (preskusno raven zaupanja $conf »Začasno zaustavljeno«) { $c.TempPaused++ } elseif (raven zaupanja preskusa $conf »Ni podprto«) { $c.NotSupported++ } else { $c.ActionReq++ } if ([string]::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["napake"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Napaka if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } if ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = if (-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } drugo { $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') -or ($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++ } # Posodobitev je v čakanju: pravilnik ali winCS je uporabljen, stanje je v čakanju, SB ON, opravilo ni onemogočeno if ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # V razdelku Opazovalne naprave (ločeno od zahtevanega dejanja) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Zahtevano je ukrepanje: ni posodobljeno, SB VKLOPLJENO, ne ujemanje z drugimi kategorijami zaupanja in ne »Posodobitev čaka« če (-not $isUpd -and $sbOn -and-not $isUpdatePending -and-not (Raven zaupanja preskusa $conf 'HighConfidence') -and-not (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; Posodobljeno=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Skupaj + + if ($isUpd) { $stMfrCounts[$mfr]. Posodobljeno + + } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. UpdatePending++ } elseif (raven zaupanja v $conf »HighConfidence«) { $stMfrCounts[$mfr]. HighConf++ } elseif (raven zaupanja v $conf »UnderObservation«) { $stMfrCounts[$mfr]. UnderObs++ } elseif (preskusno raven zaupanja $conf »Začasno zaustavljeno«) { $stMfrCounts[$mfr]. TempPaused++ } elseif (raven zaupanja preskusa $conf »Ni podprto«) { $stMfrCounts[$mfr]. NotSupported++ } else { $stMfrCounts[$mfr]. ActionReq++ } if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Sledite vsem napravam po vedru (vključno s praznim BucketId) $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Posodobljeno=0; Proizvajalec =$mfr; model=""; BIOS="" } če ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } če ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count + + if ($isUpd) { $stAllBuckets[$bucketKey]. Posodobljeno + + } } # Izpiranje paketov na disk foreach ($df v $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Matematika]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = if ($cRem -gt 0) { " | ETA: ~$([Matematika]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" } $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) if ($cMem-gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host » +$cNew, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # Dokončanje JSON matrik foreach ($dfName mestu $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host » $dfName.json: $($stDeviceFileCounts[$dfName]) devices« –ForegroundColor DarkGray } # Izračunana izpeljana statistika $stAtRisk = 0; $stSafeList = 0 foreach ($bid in $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count – $b.Posodobitev if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [math]::Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = število iz not_updated paket (naprave z vklopljeno funkcijo SB ON in niso posodobljene) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Skupaj; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Posodobljeno = $c.Posodobljeno; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; ZačasnoPaused = $c.TempPaused; NotSupported = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; FullyUpdated = $c.Updated UpdatesPending = $stNotUptodate; PosodobitveDokončanje = $c.Posodobljeno 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) } drugo { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } razen { 0 } PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } razen { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } drugo { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } drugo { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } else { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } Enolična vedra = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Pisanje CSVs [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Ključnih; Count=$_. Vrednost.Skupaj; Posodobljeno=$_. Vrednost.Posodobljeno; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stErrorCodeCounts.GetEnumerator() | Sort-Object Vrednost – padajoče | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Ključnih; Count=$_. Vrednost; SampleDevices=($stErrorCodeSamples[$_. Ključ] –pridružite se ", ") } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Descending | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Ključnih; Count=$_. Value.Count; Posodobljeno=$_. Vrednost.Posodobljeno; NotUpdated=$_. Value.Count-$_. Vrednost.Posodobljeno; Proizvajalec=$_. Value.Manufacturer } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8 # Generate orchestrator-compatible CSVs (expected filenames for Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (testna pot $notUpdatedJsonPath) { poskusite { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json if ($nuData.Count -gt 0) { # NotUptodate CSV - orchestrator searches for *NotUptodate*.csv $nuData | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Encoding UTF8 Write-Host »Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count)« -ForegroundColor Gray } } ulov { } } # Pisanje podatkov JSON za nadzorno ploščo $stats | ConvertTo-Json -Depth 3 | Set-Content (Join-Path $dataDir "summary.json") -Encoding UTF8 # HISTORICAL TRACKING: Save data point for trend chart # Uporabite stabilno lokacijo predpomnilnika, tako da se trendni podatki ohranijo v mapah s časovnim žigom združevanja. # Če je OutputPath videti kot »...\Aggregation_yyyyMMdd_HHmmss«, se predpomnilnik shrani v nadrejeno mapo.# Sicer predpomnilnik preide v sam OutputPath.$parentDir = Split-Path $OutputPath -Parent $leafName = Split-Path $OutputPath -Leaf if ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current') { # Orchestrator-created timestamped mapo - uporabite starše za stabilen predpomnilnik $historyPath = Join-Path $parentDir ".cache\trend_history.json" } drugo { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Parent if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() if (testna pot $historyPath) { poskusite { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } ulov { $historyData = @() } } # Preverite tudi znotraj OutputPath\.cache\ (podedovano mesto iz starejših različic) # Spojite vse podatkovne točke, ki še niso v primarni zgodovini if ($leafName -eq 'Aggregation_Current' -or $leafName -match '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Testna pot $innerHistoryPath) in $innerHistoryPath -ne $historyPath) { poskusite { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Datum }) foreach ($entry mestu $innerData) { če ($entry. Datum in $entry. Datum -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host » Spojene podatkovne točke $($innerData.Count) iz notranjega predpomnilnika« –ForegroundColor DarkGray } } ulov { } } }
# BOOTSTRAP: Če je zgodovina trendov prazna/redka, pouredite iz zgodovinskih podatkov if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host »Bootstrapping trend history from historical data... -ForegroundColor Yellow $dailyData = @{} # Source 1: Summary CSVs inside current folder (Aggregation_Current keeps all Summary CSVs) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object Ime foreach ($summCsv mestu $localSummaries) { poskusite { $summ = Import-Csv $summCsv.FullName | Select-Object – prvi 1 če ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -and $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = if ($summ. Posodobljeno) { [int]$summ. Posodobljeno } drugo { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices – $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Datum = $dateStr; Skupaj = [int]$summ. TotalDevices; Posodobljeno = $updated; NotUpdated = $notUpd NeedsReboot = 0; Napake = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } ulov { } } # Source 2: Old timestamped Aggregation_* mape (podedovano, če še vedno obstajajo) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Ime -match '^Aggregation_\d{8}' } | Sort-Object Name foreach ($folder mestu $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object – prvi 1 if ($summCsv) { poskusite { $summ = Import-Csv $summCsv.FullName | Select-Object – prvi 1 če ($summ. TotalDevices -and [int]$summ. SkupajDevices -gt 0) { $dateStr = $folder. Ime -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = if ($summ. Posodobljeno) { [int]$summ. Posodobljeno } drugo { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices – $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Datum = $dateStr; Skupaj = [int]$summ. TotalDevices; Posodobljeno = $updated; NotUpdated = $notUpd NeedsReboot = 0; Napake = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } ulov { } } } # Vir 3: RolloutState.json WaveHistory (ima na val timestamps od 1. dne) # To zagotavlja podatkovne točke osnovnega načrta, tudi če stare mape združevanja ne obstajajo $rolloutStatePaths = @( (Join-Path $parentDir "RolloutState\RolloutState.json"), (Join-Path $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath mestu $rolloutStatePaths) { if (testna pot $rsPath) { poskusite { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Za podatkovne točke trenda uporabite začetne datume za val # Izračun zbirnih naprav, ki so ciljane na posamezne vale $cumulativeTargeted = 0 foreach ($wave mestu $rsData.WaveHistory) { če ($wave. Zagnan na in $wave. DeviceCount) { $waveDate = ([datetime]$wave. ZagnanoNa). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. DeviceCount (Račun naprave) if (-not $dailyData.ContainsKey($waveDate)) { # Approximate: at wave start time, only devices from prior waves were updated $dailyData[$waveDate] = [PSCustomObject]@{ Datum = $waveDate; Skupaj = $c.Skupaj; Posodobljeno = [matematika]::Max(0, $cumulativeTargeted - [int]$wave. »DeviceCount«) NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. »DeviceCount«) NeedsReboot = 0; Napake = 0; ActionRequired = 0 } } } } } } ulov { } break # Uporabi prvo najdeno } }
if ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object Key | ForEach-Object { $_. Vrednost }) Write-Host »Bootstrapped $($historyData.Count) data points from historical summaries« -ForegroundColor Green } }
# Dodajte trenutno podatkovno točko (polepšajte podnevi – obdržite najnovejšo podatkovno točko na dan) $todayKey = (Get-Date). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" } if ($existingToday) { # Zamenjaj današnji vnos $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Datum = $todayKey Skupaj = $c.Skupaj Posodobljeno = $c.Posodobljeno NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Napake = $c.WithErrors ActionRequired = $c.ActionReq } # Odstranite slabe podatkovne točke (0 skupno) in obdržite zadnjih 90 $historyData = @($historyData | Where-Object { [int]$_. Skupaj -gt 0 }) # Brez velike črke – podatki o trendu so ~100 bajtov/vnos, celotno leto = ~36 KB $historyData | ConvertTo-Json -Depth 3 | Set-Content $historyPath -Encoding UTF8 Write-Host » Trend zgodovino: $($historyData.Count) data points« -ForegroundColor DarkGray # Build trend chart data for HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Posodobljeno }) – pridružite se »,« $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Skupaj }) – pridružite se "," # Projekcija: podaljšanje trendne črte z uporabo eksponentnega podvojitev (2,4,8,16 ...) # Pridobi velikost valov in obdobje opazovanja iz dejanskih podatkov o zgodovini trenda. # - Wave size = največje eno obdobje povečanje videl v zgodovini (najnovejši val uvedene) # – dnevi opazovanja = povprečni koledarski dnevi med podatkovnima točkama trenda (kako pogosto se izvajamo) # Nato podvaja val velikosti vsako obdobje, ki ustreza 2x strategija za rast orchestrator's.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Posodobitev $remaining = $stNotUptodate # Samo ne posodobljene naprave SB-ON (izključi SecureBoot IZKLOPLJENO) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Izpeljite velikost valov in opazovalno obdobje iz zgodovine trenda $increments = @() $dayGaps = @() za ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Posodobljeno – $historyData[$hi-1]. Posodobljene if ($inc -gt 0) { $increments += $inc } poskusite { $d 1 = [datetime]::P arse($historyData[$hi-1]. (Datum) $d 2 = [datetime]::P arse($historyData[$hi]. (Datum) $gap = ($d 2 - $d 1). TotalDays if ($gap -gt 0) { $dayGaps += $gap } } ulov {} } # Wave size = zadnji pozitivni prirastek (trenutni val), nazaj na povprečje, najmanj 2 $waveSize = if ($increments. Count -gt 0) { [matematika]::Max(2, $increments[-1]) } drugo { 2 } # Obdobje opazovanja = povprečna vrzel med podatkovnima točkama (koledarski dnevi na val), najmanj 1 $waveDays = if ($dayGaps.Count -gt 0) { [math]::Max(1, [math]::Round(($dayGaps | Measure-Object -Average). Povprečje, 0)) } drugo { 1 }
Write-Host » Projection: waveSize=$waveSize (from last increment), waveDays=$waveDays (avg gap from history)« -ForegroundColor DarkGray
$dayCounter = 0 # Projiciranje, dokler niso vse naprave posodobljene ali največ 365 dni za ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter + + # Na vsaki meja opazovalnega obdobja, uvedete val, nato pa dvojno če ($dayCounter -ge $waveDays) { $devicesThisWave = [math]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated-gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Double wave size for next period (orchestrator 2x strategy) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd"))'" $projValues += $lastUpdated $projNotUpdValues += [matematika]::Max(0, $remaining) if ($remaining-le 0) { break } } $projLabels = $projDates -join "," $projUpdated = $projValues -join "," $projNotUpdated = $projNotUpdValues -join "," $hasProjection = $projDates.Count -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host » Projekcija: potrebujete vsaj 2 trendni podatkovni točki za izpeljavo časa vala« –OspredjeBarva DarkGray } # Ustvarite nize podatkov kombiniranega grafikona za tukaj niz $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } drugo { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } else { "" } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { "" } $histCount = ($historyData | Measure-Object (Mera–predmet). Count $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | ForEach-Object { @{ name=$_. Ključnih; skupaj=$_. Vrednost.Skupaj; updated=$_. Vrednost.Posodobljeno; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Depth 3 | Set-Content (Join-Path $dataDir "manufacturers.json") -Encoding UTF8 # Pretvorite podatkovne datoteke JSON v CSV za prenose Excela, ki jih lahko preberejo ljudje Write-Host »Pretvorba podatkov naprave v CSV za Excel prenos ...« –ospredjeBarva siva foreach ($dfName mestu $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath »SecureBoot_${dfName}_$timestamp.csv« if (preskusna pot $jsonFile) { poskusite { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json if ($jsonData.Count -gt 0) { # Vključi dodatne stolpce za update_pending CSV $selectProps = if ($dfName -eq "update_pending") { @('Ime HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } drugo { @('Ime HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8 Write-Host » $dfName –> $($jsonData.Count) rows -> CSV« –ForegroundColor DarkGray } } ulov { Write-Host " $dfName - preskočen" -ForegroundColor DarkYellow } } } # Ustvari vgrajeno nadzorno ploščo HTML $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host »Generating self-contained HTML dashboard...« (Ustvarjanje samospomnjene nadzorne plošče HTML... – barva v ospredjuBarva rumena # VELOCITY PROJECTION: Calculate from scan history or previous summary $stDeadline = [datetime]"2026-06-24" # KEK cert expiry $stDaysToDeadline = [matematika]::Max(0, ($stDeadline - (Get-Date)). Število dni) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "N/A" $stWorkingDays = 0 $stCalendarDays = 0 # Najprej preskusite zgodovino trendov (lahek, ki ga že vzdržuje združevalnik – zamenja bloated ScanHistory.json) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Skupaj -gt 0 -and [int]$_. Posodobljeno – ge 0 }) if ($validHistory.Count -ge 2) { $prev = $validHistory[-2]; $curr = $validHistory[-1] $prevDate = [datetime]::P arse($prev. Date.Substring(0, [Matematika]::Min(10, $prev. Datum.Dolžina))) $currDate = [datetime]::P arse($curr. Date.Substring(0, [Matematika]::Min(10, $curr. Datum.Dolžina))) $daysDiff = ($currDate - $prevDate). TotalDays če ($daysDiff -gt 0) { $updDiff = [int]$curr. Posodobljeno – [int]$prev. Posodobljene if ($updDiff -gt 0) { $stDevicesPerDay = [math]::Round($updDiff/$daysDiff, 0) $stVelocitySource = "Trendhistory" } } } } # Try orchestrator rollout povzetek (ima vnaprej izračunano hitrost) if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { poskusite { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [math]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = »Orchestrator« if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } ulov { } } # Rezervni: preizkusite prejšnji povzetek CSV (iskanje trenutne mape IN nadrejene / sorodne mape združevanja) if ($stVelocitySource -eq "N/A") { $searchPaths = @( (Join-Path $OutputPath "SecureBoot_Summary_**.csv") ) # Prav tako iskanje bratovščina združevanja mape (orchestrator ustvarja novo mapo vsak teči) $parentPath = Split-Path $OutputPath -Parent if ($parentPath) { $searchPaths += (Ime poti $parentPath »Aggregation_*\SecureBoot_Summary_*.csv«) $searchPaths += (Ime poti $parentPath »SecureBoot_Summary_*.csv«) } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descending | Select-Object – prvi 1 if ($prevSummary) { poskusite { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom- Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) – $prevDate). TotalDays če ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Updated – posodobitev $prevUpdated če ($updDelta -gt 0) { $stDevicesPerDay = [math]::Round($updDelta/$daysSinceLast, 0) $stVelocitySource = "PreviousReport" } } } ulov { } } } # Rezervni: izračun hitrosti iz celotnega razpona zgodovine trendov (prvi v primerjavi z najnovejšo podatkovno točko) if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Skupaj -gt 0 -and [int]$_. Posodobljeno – ge 0 }) if ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime]::P arse($first. Date.Substring(0, [Matematika]::Min(10, $first. Datum.Dolžina))) $lastDate = [datetime]::P arse($last. Date.Substring(0, [Matematika]::Min(10, $last. Datum.Dolžina))) $daysDiff = ($lastDate - $firstDate). TotalDays če ($daysDiff -gt 0) { $updDiff = [int]$last. Posodobljeno – [int]$first. Posodobljene če ($updDiff -gt 0) { $stDevicesPerDay = [math]::Round($updDiff/$daysDiff, 1) $stVelocitySource = "Trendhistory" } } } } # Izračunajte projekcijo z eksponentno podvojitev (skladno s trendni grafikon) # Znova uporabite podatke projekcije, ki so že izračunani za grafikon, če so na voljo če ($hasProjection -in $projDates.Count -gt 0) { # Uporabi zadnji projektni datum (ko so posodobljene vse naprave) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("DD MMM, yyyy") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Dni $stWorkingDays = 0 $d = Get-Date 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 -and $stNotUptodate -gt 0) { # Rezervni: linearna projekcija, če ni na voljo eksponentnih podatkov $daysNeeded = [matematika]::Ceiling($stNotUptodate/$stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("DD MMM, yyyy") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Get-Date for ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'sobota' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Build hitrost HTML $velocityHtml = if ($stDevicesPerDay -gt 0) { »<div><strong>🚀 Naprave/dan:</strong> $($stDevicesPerDay.ToString('N0')) (vir: $stVelocitySource)</div>" + »<div><strong>📅 Projektno dokončanje:</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>🕐 Delovni dnevi:</strong> $stWorkingDays | <strong>Calendar Days:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Rok: 24. junij 2026 (KEK potrdilo o poteku) | Preostali dnevi: $stDaysToDeadline</div>« } drugo { "<div style='padding:8px; ozadje:#fff3cd; polmerom:4 slikovnih pik; border-left:3px solid #ffc107'>" + »<strong>📅 Projected Completion:</strong> Insufficient data for velocity calculation. " + »Zaženi združevanje vsaj dvakrat s spremembami podatkov, da se določi mera.<br/>« + "<močan>Rok:</strong> 24. junij 2026 (potrdilo KEK poteče) | <močni>preostali dnevi:</strong> $stDaysToDeadline</div>" } # Cert expiry countdown $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematika]::Max(0, ($certKekExpiry - $certToday). Število dni) $daysToUefi = [matematika]::Max(0, ($certUefiExpiry – $certToday). Število dni) $daysToPca = [matematika]::Max(0, ($certPcaExpiry – $certToday). Število dni) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Helper: Read records from JSON, build bucket summary + first N device rows $maxInlineRows = 200 funkcija Build-InlineTable { param([niz]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (testna pot $JsonPath) { poskusite { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Count # POVZETEK VEDRA: Združevanje po vedru, prikaz štetja na vedro s posodobljeno iz globalne statistike vedra if ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | Sort-Object Count –Descending $bucketSummary = "><2 h3 style='font-size:.95em; color:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. Count) vedra)><4 /h3>« $bucketSummary += "><6 div style='max-height:300px; prekoračitev-y:samodejno; margin-bottom:15px'><table><thead><tr><><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Updated</th><th style='text-align:right; color:#dc3545 ni >Posodobljeno</th><th><1 Manufacturer><2 /th></tr></thead><tbody>" foreach ($b mestu $buckets) { $bid = if ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Get Updated count from global bucket stats (all devices in this bucket across the entire dataset) $lookupKey = $bid $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null } $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } else { 0 } $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } else { $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; barva:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><style='text-align:right; barva:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # DEVICE DETAIL: First N rows as flat list $slice = $data | Select-Object – prvi $MaxRows foreach ($d mestu $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match "High") { '<span class="badge badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge badge-danger">Not Supported><6 /span>' } elseif ($conf -match "Under") { '<span class="badge badge-info">Under Obs><0 /span>' } elseif ($conf -match "Paused") { '<span class="badge badge-warning">Paused><4 /span>' } else { '<span class="badge badge-warning">Action Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="badge badge-success"><01 Updated</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge-danger"><05 Error</span>' } else { '><08 span class="badge badge-warning"><09 Pending><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" } } ulov { } } if ($totalCount -eq 0) { return "><44 div style='padding:20px; color:#888; font-style:italic'><45 No devices in this category.><46 /div>" } $showing = [math]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; velikost pisave:.85em; color:#666's ><49 Skupaj: $($totalCount.ToString("N0")) devices" if ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; debelina pisave:bold'>📄 Prenesite polno datoteko CSV za Excel><3 /a>« } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; color:#333; margin:10px 0 5px'><58 Device Details (showing first $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>" return "$header$bucketSummary$deviceHeader$deviceTable" } # Build inline tabele iz datoteke JSON že na disku, povezovanje s CSVs $tblErrors = Build-InlineTable -JsonPath (Join-Path $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath (Join-Path $dataDir "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath (Join-Path $dataDir "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath (Join-Path $dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath (Join-Path $dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath (Join-Path $dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath (Join-Path $dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath (Join-Path $dataDir "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath (Join-Path $dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath (Join-Path $dataDir "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath (Join-Path $dataDir "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath (Join-Path $dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath (Join-Path $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # Tabela po meri za čakajoče posodobitve – vključuje stolpca UEFICA2023Status in UEFICA2023Error $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (testna pot $upJsonPath) { poskusite { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Count če ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; velikost pisave:.85em; color:#666'>Skupaj: naprave $($upCount.ToString("N0")) | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; debelina pisave:bold'>📄 Prenos celotne datoteke CSV za Excel><4 /a></div>« $upRows = "" $upSlice = $upData | Select-Object -First $maxInlineRows foreach ($d mestu $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } drugo { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge badge-success">Yes><8 /span>' } else { '-' } $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n" } $upShowing = [math]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; color:#333; margin:10px 0 5px'>Device Details (showing first $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 UEFICA 2023Napaka><6 /th><th><9 Pravilnik</th><th>WinCS Key</th><th><th>Idedlja</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } drugo { $tblUpdatePending = "<div style='odmik:20px; color:#888; font-style:italic'>No devices in this category.</div>" } } ulova { $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>No devices in this category.</div>" } } drugo { $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>No devices in this category.</div>" } # Cert expiry countdown $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematika]::Max(0, ($certKekExpiry - $certToday). Število dni) $daysToUefi = [matematika]::Max(0, ($certUefiExpiry – $certToday). Število dni) $daysToPca = [matematika]::Max(0, ($certPcaExpiry – $certToday). Število dni) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Podatki grafikona izdelovalca graditve v vrstici (10 najvišjih po število naprav) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | Select-Object – prvih 10 $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "Po izdelovalcu" } drugo { "Prvih 10 proizvajalcev" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Ključ)'" }) -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 proizvajalca graditve $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | ForEach-Object { $mfrTableRows += "<><ni><7 $($_. Tipka)</td><za>$($_. 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" } # Fragmenti oznak HTML od blizu: razdeljeni v dele, tako da spletne platforme CMS ne # interpretira jih kot pravi HTML in okoli njih vstavi nevidne znake Unicode.$endScript = '</scr' + 'ipt>' $endStyle = '</sty' + 'le>' $endHead = '</he' + 'ad>' $endBody = '</bo' + 'dy>' $endHtml = '</ht' + 'ml>' $htmlContent = @" -Ne, < sem. DOCTYPE html> <html lang="en"> <glavo> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <naslov>nadzorna plošča za stanje potrdila varnega zagona</title> <script src="https://cdn.jsdelivr.net/npm/chart.js">$endScript <slog> *{box-sizing:border-box; rob:0; odmik:0} body{font-family:'Segoe UI',Tahoma,sans-serif; ozadje:#f0f2f5; color:#333} .header{background:linear-gradient(135deg, #1a237e, #0d47a1); barva:#fff; odmik:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; opacity:.9} .container{max-width:1400px; margin:0 auto; odmik:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); gap:12px; margin:20px 0} .card{background:#fff; polmerom:10 slikovnih pik; odmik:15px; box-shadow:0 2px 8px rgba(0,0,0,08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; debelina pisave: 700} .card .label{font-size:.8em; color:#666; margin-top:4px} .card .pct{font-size:.75em; color:#888} .section{background:#fff; polmerom:10 slikovnih pik; odmik:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; barva:#1a237e; margin-bottom:10px; kazalec:kazalec; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:20px 0} .chart-box{background:#fff; polmerom:10 slikovnih pik; odmik:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabela{width:100%; border-collapse:collapse; velikost pisave:.85em} th{background:#e8eaf6; odmik:8px 10px; poravnava besedila:levo; položaj:lepljivi; zgoraj:0; z-index:1} td{odmik:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; odmik:2px 8px;polmer obrobe:10 slikovnih pik; velikost pisave:.75em; debelina pisave: 700} .badge-success{background:#d4edda; color:#155724} .badge-danger{background:#f8d7da; color:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; color:#0c5460} .top-link{float:right; velikost pisave:.8em; barva:#1a237e; text-decoration:none} .footer{text-align:center; odmik:20px; color:#999; velikost pisave:.8em} a{color:#1a237e}$endStyle $endHead <telo> <div class="header"> <H1>nadzorna plošča za stanje potrdila varnega zagona</h1> <div class="meta">Ustvarjeno: $($stats. ReportGeneratedAt) | Skupaj naprav: $($c.Total.ToString("N0")) | Enolična vedra: $($stAllBuckets.Count)</div> </div> <div class="container">
<!-- kartice KPI – mogoče jih je klikniti, povezane z odseki – > <div class="cards"> <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; top:8px; desno:8px; ozadje:#dc3545; barva:#fff; odmik:1px 6px; polmerom:8 slikovnih pik; velikost pisave:.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)% – potrebno je ukrepanje><0 /div></a><3 <a class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; top:8px; desno:8px; ozadje:#28a745; barva:#fff; odmik:1px 6px; polmerom:8 slikovnih pik; velikost pisave:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString() "N0"))</div><div class="label">Updated><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3 <a class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; top:8px; desno:8px; ozadje:#6c757d; barva:#fff; odmik:1px 6px; polmerom:8 slikovnih pik; velikost pisave:.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})% – zunaj obsega><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">Potrebuje ponovni zagon><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% – čakajo na ponovni zagon><6 /div></a><9 <a class="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Update Pending</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Pravilnik/WinCS applied, čakajo na posodobitev><2 /div></a><5 <a class="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">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 <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0")))</div><div class="label">High Confidence><20 /div><div class="pct">$($stats. PercentHighConfidence)% – varno za><24 /div></a><27 <a class="card" href="#s-uo" onclick="openSection('d-uo')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value" style="color:#ffc107"><1 $($c.UnderObs.ToString("N0"))><2 /div><div class="label"><5 Under Observation><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0")))</div><div class="label">Action Required><2 /div><div class="pct">$($stats. PercentActionRequired)% – mora><6 /div></a><9 <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($stAtRisk.ToString("N0"))</div><div class="label">At Risk><68 /div><div class="pct">$($stats. PercentAtRisk)% – podobno neuspeli><2 /div></a><5 <a class="card" href="#s-td" onclick="openSection('d-td')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.TaskDisabled.ToString("N0"))</div><div class="label">Task Disabled><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})% – blokirano><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. Začasno ustavljeno</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Known Issues><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0"))</div><div class="label">Manjkajoči razred KEK</div><div="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0})%</div></a> <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)% – napake UEFI</div></a> ><6 a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545"><9 $($c.TempFailures.ToString("N0")))</div><div class="label">Temp. Napake</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0"))</div><div class="label">Ni podprto><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
<!-- Deployment Velocity & Cert Expiry --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:15px 0"> <div class="section" style="margin:0"> <h2>📅 Hitrost</h2> ><2 div class="section-body open"><3 ><4 div style="font-size:2.5em; debelina pisave:700; color:#28a745"><5 $($c.Updated.ToString("N0"))><6 /div> ><8 div style="color:#666"><9 naprave, posodobljene od $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; ozadje:#e8eaf6; višina:20px; polmerom:10 slikovnih pik; overflow:hidden"><div style="background:#28a745; višina:100 %; width:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% dokončanega</div> <div style="margin-top:10px; odmik:10px; ozadje:#f8f9fa; polmerom:8 slikovnih pik; font-size:.85em"> <div><močni>Remaining:</strong> $($stNotUptodate.ToString("N0")) naprave potrebujejo ukrepanje</div> <div><močan>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) naprave (napake + trajno + opravilo onemogočeno)</div> <div><strong>Varno za uvedbo:</strong> $($stSafeList.ToString("N0")) naprave (isto vedro kot uspešna)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; border-left:4px solid #dc3545"> <h2 style="color:#dc3545">⚠ Expiry Count<down in /h2 > <div class="section-body open"> <div style="display:flex; gap:15px; margin-top:10px"> <div style="text-align:center; odmik:15px; polmerom:8 slikovnih pik; min-width:120px; ozadje:linearni-preliv(135deg; #fff5f5; #ffe0e0); border:2px masivnega #dc3545; flex:1"> <div style="font-size:.65em; barva:#721c24; pretvorba besedila:velike črke; font-weight:bold">⚠ DA POTEČE</div> ><4 div style="font-size:.85em; debelina pisave:krepko; barva:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; debelina pisave:700; barva:#dc3545; line-height:1"><9 $daysToKek><0 /div><1 <div style="font-size:.8em; color:#721c24">dni (24. junij 2026)</div><5 </div><7 <div style="text-align:center; odmik:15px; polmerom:8 slikovnih pik; min-width:120px; ozadje:linearni preliv(135deg; #fffef5; #fff3cd); border:2px masivnega #ffc107; flex:1"> ><00 div style="font-size:.65em; barva:#856404; pretvorba besedila:velike črke; font-weight:bold"><01 UEFI CA 2011</div> ><04 div id="daysUefi" style="font-size:2.2em; debelina pisave:700; barva:#856404; višina črte:1; margin:5px 0"><05 $daysToUefi</div> ><08 div style="font-size:.8em; color:#856404"><09 dni (27. junij 2026)><10 /div> ><12 /div> ><14 div style="text-align:center; odmik:15px; polmerom:8 slikovnih pik; min-width:120px; ozadje:linearni-preliv(135deg; #f0f8ff; #d4edff); border:2px trdnih #0078d4; flex:1"><15 ><16 div style="font-size:.65em; barva:#0078d4; pretvorba besedila:velike črke; font-weight:bold"><17 Windows PCA</div> ><20 div id="daysPca" style="font-size:2.2em; debelina pisave:700; barva:#0078d4; višina črte:1; margin:5px 0"><21 $daysToPca><2 /div><3 ><24 div style="font-size:.8em; color:#0078d4"><25 dni (19. oktober 2026)><26 /div><7 ><28 /div><9 ><30 /div><1 ><32 div style="margin-top:15px; odmik:10px; ozadje:#f8d7da; polmerom:8 slikovnih pik; velikost pisave:.85em; border-left:4px solid #dc3545"><33 ><34 je>⚠ KRITIČNO:><37 /strong> Vse naprave je treba posodobiti pred iztekom potrdila. Naprave, ki do roka niso posodobljene, ne morejo uporabiti prihodnjih varnostnih posodobitev za upravitelja zagona in varni zagon po poteku.</div> </div> </div> </div>
<!-- Charts --> <div class="charts"> <div class="chart-box"><h3>Deployment Status</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) { »<!-- Zgodovinske Trend --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Napredek posodobitve v <a class='top-link' href='#'>↑ Top</a></h2> <div id='d-trend' class='section-body open'> <platno id='trendChart' height='120'></canvas> <div style='font-size:.75em; color:#888; margin-top:5px'>Solid lines = actual data$(if ($historyData.Count -ge 2) { " | Črtkana črta = projiciranje (eksponentno podvojitev: 2→4→8→16 ... naprave na val)" } drugo { " | Znova zaženite združevanje jutri, da si ogledate trendne črte in projekcije« })</div> </div> </div>" })
<!-- prenose datoteke CSV – > <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Prenesite celotne podatke (CSV za Excel) <class="top-link" href="#">Top</a></h2> <div id="dl-csv" class="section-body open" style="display:flex; upognite:zaviti; gap:5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; ozadje:#dc3545; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0")))</a> <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; ozadje:#dc3545; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a><2 <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; ozadje:#fd7e14; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Action Required ($($c.ActionReq.ToString("N0")))</a><6 <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; ozadje:#dc3545; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Known Issues ($($c.WithKnownIssues.ToString("N0")))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; ozadje:#dc3545; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Task Disabled ($($c.TaskDisabled.ToString("N0")))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; ozadje:#28a745; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Updated ($($c.Updated.ToString("N0")))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; ozadje:#6c757d; barva:#fff; odmik:6px 14px; polmerom:5 slikovnih pik; text-decoration:none; font-size:.8em">Summary</a> <div style="width:100%; velikost pisave:.75em; color:#888; margin-top:5px">CSV files open in Excel. Na voljo, če gostuje v spletnem strežniku.</div> </div> </div>
<!-- proizvajalca - --> <div class="section"> <h2 onclick="toggle('mfr')">By Manufacturer <a class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <tabelo><so><><><1 izdelovalec><2 /th><><5 Skupno><6 /><><9 Posodobljeno><9><0 /><><3 visoka raven zaupanja><4 /th><><7 Potrebno je ukrepanje><8 /th></tr></thead><3 <tbody><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- odsekov naprave (prvih 200 v vrstici + prenos datoteke CSV) – > <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Naprave z napakami ($($c.WithErrors.ToString("N0")))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-err" class="section-body">$tblErrors</div> </div> <div class="section" id="s-ki"> <h2 onclick="toggle('d-ki')" style="color:#dc3545">🔴 Znane težave ($($c.WithKnownIssues.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-ki" class="section-body">$tblKI</div> </div> <div class="section" id="s-kek"> <h2 onclick="toggle('d-kek')">🟠 Manjka KEK – dogodek 1803 ($($c.WithMissingKEK.ToString("N0"))) <a 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">🟠 Zahtevano je dejanje ($($c.ActionReq.ToString("N0")))) <a class="top-link" href="#">↑ Top><4 /a></h2><7 <div id="d-ar" class="section-body">$tblActionReq</div> </div> <div class="section" id="s-uo"> <h2 onclick="toggle('d-uo')" style="color:#17a2b8">🔵 V razdelku Opazovanje ($($c.UnderObs.ToString("N0")))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-uo" class="section-body">$tblUnderObs</div> </div> <div class="section" id="s-nu"> <h2 onclick="toggle('d-nu')" style="color:#dc3545">🔴 Ni posodobljeno ($($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">🔴 Opravilo je onemogočeno ($($c.TaskDisabled.ToString("N0")))) >↑ 5 a class="top-link" href="#">↑ Top</a></h2><1 <div id="d-td" class="section-body">$tblTaskDis><4 /div><5 </div><7 <div class="section" id="s-tf"> <h2 onclick="toggle('d-tf')" style="color:#dc3545">🔴 Začasne napake ($($c.TempFailures.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-tf" class="section-body">$tblTemp</div> </div> <div class="section" id="s-pf"> <h2 onclick="toggle('d-pf')" style="color:#721c24">🔴 Trajne napake/ni podprto ($($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">⏳ Update Pending ($($c.UpdatePending.ToString("N0"))) – pravilnik/uporabljen WinCS, Čakajo na posodobitev <class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Devices where AvailableUpdatesPolicy or WinCS key is applied but UEFICA2023Status is still NotStarted, InProgress, or null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Izdaja v teku ($($c.RolloutInProgress.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-rip" class="section-body">$tblRolloutIP</div> </div> <div class="section" id="s-sboff"> <h2 onclick="toggle('d-sboff')" style="color:#6c757d">⚫ SecureBoot OFF ($($c.SBOff.ToString("N0")))) - Out of Scope <a class="top-link" href="#">↑ Top</a></h2> <div id="d-sboff" class="section-body">$tblSBOff</div> </div> <div class="section" id="s-upd"> <h2 onclick="toggle('d-upd')" style="color:#28a745">🟢 Posodobljene naprave ($($c.Updated.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd" class="section-body">$tblUpdated</div> </div> <div class="section" id="s-nrb"> <h2 onclick="toggle('d-nrb')" style="color:#ffc107">🔄 Posodobljeno – treba je znova zagnati ($($c.NeedsReboot.ToString("N0")))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>
<div class="footer">nadzorno ploščo za povrnitev potrdila za varni zagon | Ustvarjeno je bilo $($stats. ReportGeneratedAt) | StreamingMode | Peak Memory: ${stPeakMemMB} MB</div> </div><!-- /container -->
<skript> function toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} function 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. Začasno ustavljeno','Ni podprto','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#6c757d','#721c24 #17a2b8', '#6c757d', '#6c757d', '#6c757d', '#6c757d','#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'},{ label:'Temp. Začasno ustavljeno',podatki:[$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'}}}}); Zgodovinski Trend grafikon 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 doubling)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display::true,text:'Devices'},x:{title:{display:true,text:'Date'}}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}); } Dinamično odštevanje (function(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19'); var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0,Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0;Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0,Math.ceil((p-t)/864e5))})();$endScript $endBody $endHtml "@ (v redu), [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Vedno imejte stabilno "Latest" kopijo, tako da skrbniki ne potrebujejo za sledenje timestamps $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath – vsili $stTotal = $streamSw.Elapsed.TotalSeconds # Save file manifest for incremental mode (quick no-change detection on next run) če ($IncrementalMode -ali $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 »Shranjevanje manifesta datoteke za postopni način ...« –OspredjeBarva siva foreach ($jf mestu $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Velikost = $jf. Dolžina } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host » Saved manifest for $($stNewManifest.Count) files« -ForegroundColor DarkGray } # ČIŠČENJE HRANJENJA # Orchestrator reusable folder (Aggregation_Current): keep only latest run (1) # Skrbnik ročno runs / druge mape: keep last 7 runs # Povzetek CSVs se NIKOLI izbrišejo - oni so drobni (~ 1 KB) in so backup vir za trend zgodovine $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } drugo { 7 } # Predpone datoteke so varne za čiščenje (kratkotrajni posnetki na zagon) $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_« ) # Find all unique timestamps from cleanable files only $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. Ime; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Count -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { if ($_. Ime -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object –Enolično – padajoče) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object – preskočite $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs mestu $oldTimestamps) { foreach ($prefix mestu $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f mestu $oldFiles) { $freedBytes += $f.Dolžina Remove-Item $f.FullName - Force -EA SilentlyContinue $removedFiles + + } } } $freedMB = [math]::Round($freedBytes / 1 MB, 1) Write-Host »Čiščenje hranjenja: odstranjene $removedFiles datotek iz starih voščilnic $($oldTimestamps.Count), sprostite ${freedMB} MB (obdržite zadnji $retentionCount + vse povzetek/NotUptodate CSVs)« -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Cyan Write-Host »STREAMING AGGREGATION COMPLETE« -ForegroundColor Green Write-Host ("=" * 60) -OspredjeBarva cijan Write-Host » Skupno število naprav: $($c.Total.ToString("N0"))" -ForegroundColor White Write-Host » NI POSODOBLJENO: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" }) Write-Host » Posodobljeno: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -OspredjeBarva zelena 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 PRETOČNI NAČIN } drugo { Write-Error »Vnosne poti ni mogoče najti: $InputPath« izhod 1 }