VAЋNO Ovaj članak koji sadrži ovu probnu skriptu je povučen. Počevši od Ispravki za Windows objavljenih 12. maja 2026. i posle toga, uzorak skripte nalazi se u fascikli %systemroot%\SecureBoot\ExampleRolloutScripts na uređaju.
Kopirajte i nalepite ovu uzorak skripte i izmenite po potrebi za okruženje:
<# . SINOPSIS Prikuplja JSON podatke o statusu bezbednog pokretanja sa više uređaja u izveštaje sa rezimeom.
. OPIS Čita prikupljene JSON datoteke statusa bezbednog pokretanja i generiše: - HTML kontrolna tabla sa grafikonima i filtriranjem - rezime po nivou pouzdanosti - Jedinstvena analiza grupe uređaja za strategiju testiranja Podržava: - Datoteke po računaru: HOSTNAME_latest.json (preporučuje se) - Jedna JSON datoteka Automatski se dedupliraj od strane HostName, zadržavajući najnoviju KolekcijuTime. Podrazumevano uključuje samo uređaje sa pouzdanošću "Req radnje" ili "Najviši stepen pouzdanosti" da biste se fokusirali na operativne kontejnere. Koristite -IncludeAllConfidenceLevels da biste zamenili.
. PARAMETER InputPath Putanja do JSON datoteka: - Fascikla: Čita sve *_latest.json (ili *.json ako nema _latest datoteka) - Datoteka: Čita jednu JSON datoteku
. IZLAZNI PARAMETAR Putanja za generisane izveštaje (podrazumevano: .\SecureBootReports)
. PRIMER # Skupi iz fascikle datoteka po računaru (preporučuje se) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Čita: \\contoso\SecureBootLogs$\*_latest.json
. PRIMER # Prilagođena izlazna lokacija .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. PRIMER # Uključi samo req radnji i visok stepen pouzdanosti (podrazumevano ponašanje) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Isključuje: posmatranje, pauzirano, nije podržano
. PRIMER # Uključi sve nivoe pouzdanosti (filter zamene) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. PRIMER # Filter prilagođenog nivoa pouzdanosti .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. PRIMER # ENTERPRISE SCALE: Incremental mode - samo proces promenjenih datoteka (brzi naredni pokreću se) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Prvo pokretanje: potpuno učitavanje ~2 sata za uređaje koji imaju 500K # Naredna izvršavanja: Sekunde ako nema promena, minuti za deltas
. PRIMER # Preskoči HTML ako se ništa ne promeni (najbrže za nadgledanje) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Ako se nijedna datoteka nije promenila od poslednjeg izvršavanja: ~5 sekundi
. PRIMER # Režim samo rezimea – preskočite velike tabele uređaja (1-2 minuta u odnosu na 20+ minuta) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Generiše CSV datoteke, ali preskače HTML kontrolnu tablu sa celim tabelama uređaja
. BELEŠKE Uparite Detect-SecureBootCertUpdateStatus.ps1 za primenu preduzeća.Pogledajte GPO-DEPLOYMENT-GUIDE.md kompletni vodič za primenu. Podrazumevano ponašanje isključuje posmatranje, pauzirano i nije podržano uređaje da biste fokusirali izveštavanje samo na operativne kontejnere uređaja.#>
param( [Parameter(Obavezno = $true)] [niska]$InputPath, [Parameter(Obavezno = $false)] [niska]$OutputPath = ".\SecureBootReports", [Parameter(Obavezno = $false)] [niska]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Obavezno = $false)] [string]$RolloutStatePath, # putanja do RolloutState.json identifikovanje InProgress uređaja [Parameter(Obavezno = $false)] [niska]$RolloutSummaryPath, # putanja do SecureBootRolloutSummary.json iz usluge Orchestrator (sadrži podatke o projekciji) [Parameter(Obavezno = $false)] [string[]]$IncludeConfidenceLevels = @("Potrebna je radnja", "Visoka pouzdanost"), # Obuhvati samo ove nivoe pouzdanosti (podrazumevano: samo operativne kontejnere) [Parameter(Obavezno = $false)] [switch]$IncludeAllConfidenceLevels, # Zameni filter da biste uključili sve nivoe pouzdanosti [Parameter(Obavezno = $false)] [switch]$SkipHistoryTracking, [Parameter(Obavezno = $false)] [switch]$IncrementalMode, # Enable delta processing - učitaj samo promenjene datoteke od poslednjeg izvršavanja [Parameter(Obavezno = $false)] [niska]$CachePath, # Putanja do direktorijuma keša (podrazumevano: OutputPath\.cache) [Parameter(Obavezno = $false)] [int]$ParallelThreads = 8, # Broj paralelnih niti za učitavanje datoteke (PS7+) [Parameter(Obavezno = $false)] [switch]$ForceFullRefresh, # Nametni potpuno ponovno učitavanje čak i u inkrementalnom režimu [Parameter(Obavezno = $false)] [switch]$SkipReportIfUnchanged, # Preskoči HTML/CSV generisanje ako se nijedna datoteka nije promenila (samo statistika izlaza) [Parameter(Obavezno = $false)] [switch]$SummaryOnly, # Generiši samo statistiku rezimea (bez velikih tabela uređaja) – mnogo brže [Parameter(Obavezno = $false)] [switch]$StreamingMode # Memorijski efikasni režim: odlomci procesa, postepeno pisanje CSV datoteka, čuvanje samo rezimea u memoriji )
# Samopraravljanje: Skidajte nevidljive Unikod znakove koje je ubrizgao veb CMS prilikom kopiranja iz HTML članaka.# CMS support.microsoft.com ubrizgava razmake nulte širine (U+200B), razmake koji se ne prelamaju (U+00A0) i druge # nevidljivi znakovi oko HTML oznaka unutar niski, izazivajući greške PowerShell analizacije.if ($MyInvocation.MyCommand.Path) { $rawScript = [System.IO.File]::ReadAllText($MyInvocation.MyCommand.Path) if ($rawScript -match "[\u200B-\u200F\uFEFF]" -ili $rawScript -match "\xA0") { Write-Host "UPOZORENJE: otkriveni nevidljivi Unikod znakovi (verovatno iz veb kopiranja i lepljenja) - skripta za automatsko čišćenje..." -Boja prednjeg plana Žuta boja $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 "Skripta je uspešno očišćena. Ponovno pokretanje..." - Boja prednjeg plana zelene boje & $MyInvocation.MyCommand.Path @PSBoundParameters izađi $LASTEXITCODE } }
# Automatski podići na PowerShell 7 ako je dostupan (6x brže za velike skupove podataka) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty izvor ako ($pwshPath) { Write-Host "PowerShell $($PSVersionTable.PSVersion) je otkriven – ponovno pokretanje sa programom PowerShell 7 radi brže obrade..." -Boja prednjeg plana Žuta boja # Ponovo napravi listu argumenata iz povezanih parametara $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key in $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] ako ($val -je [prekidač]) { ako ($val. Predstavlja) { $relaunchArgs += "-$key" } } elseif ($val -is [array]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -pridruži se ',') } još { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs izađi $LASTEXITCODE } }
$ErrorActionPreference = "Nastavi" $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Uzorci primene i nadgledanja"
# Napomena: Ova skripta nema zavisnosti od drugih skripti.# Za kompletan skup alatki preuzmite: $DownloadUrl -> $DownloadSubPage
#region podešavanje Write-Host "=" * 60 -Boja prednjeg plana– cijan Write-Host "Agregacija podataka bezbednog pokretanja" - Boja prednjeg plana– cijan Write-Host "=" * 60 -Boja prednjeg plana– cijan
# Kreiraj izlazni direktorijum ako (-ne (probna putanja $OutputPath)) { New-Item -ItemType direktorijum -putanja $OutputPath -Force | Bez vrednosti }
# Učitavanje podataka - podržava CSV (zastarele) i JSON (izvorne) formate Write-Host "'n Učitavanje podataka iz: $InputPath" - Boja prednjeg plana žuta
#Helper function to normalize device object (handle field name differences) funkcije Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON koristi ime hosta, CSV koristi Ime HostName) ako ($device. PSObject.Properties['Hostname'] -a -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName "Ime HostName" -NotePropertyValue $device. Ime hosta -Force } # Handle Confidence vs ConfidenceLevel (JSON uses Confidence, CSV uses ConfidenceLevel) # ConfidenceLevel je zvanično ime polja – mapa Pouzdana u njega ako ($device. PSObject.Properties['Confidence'] -a -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Pouzdanost – sila } # Pratite status ažuriranja putem "Event1808Count OR UEFICA2023Status="Updated" # Ovo omogućava praćenje koliko uređaja je ažurirano u svakom kontejneru pouzdanosti $event 1808 = 0 ako ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false ako ($device. PSObject.Properties['UEFICA2023Status'] -i $device. UEFICA2023Status -eq "Ažurirano") { $uefiCaUpdated = $true } if ($event 1808 -gt 0 -ili $uefiCaUpdated) { # Mark as updated for dashboard/rollout logic - but DON't override ConfidenceLevel $device | Add-Member -NotePropertyName "IsUpdated" -NotePropertyValue $true -Force } još { $device | Add-Member -NotePropertyName "IsUpdated" -NotePropertyValue $false -Force # ConfidenceLevel klasifikacija: # - "High Confidence", "Under Observation...", "Privremeno pauzirano...", "Not Supported..." = use as-is # - Sve drugo (bez vrednosti, prazno, "UpdateType:...", "Nepoznato", "N/A") = spada u radnju koja je neophodna u brojačima # Nije potrebna normalizacija – grana drugog brojača protoka rukuje sa ovim } # Handle OEMManufacturerName nas WMI_Manufacturer (JSON koristi OEM*, zastarelo koristi WMI_*) ako ($device. PSObject.Properties['OEMManufacturerName'] -a -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName "WMI_Manufacturer" -NotePropertyValue $device. OEMManufacturerName -Force } # Rukovati OEMModelNumber naspram WMI_Model ako ($device. PSObject.Properties['OEMModelNumber'] -a -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName "WMI_Model" -NotePropertyValue $device. OEMModelNumber -Force } # Rukovati firmververzijom u odnosu na BIOSDescription ako ($device. PSObject.Properties['FirmwareVersion'] -a -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion - Force } vrati $device }
#region inkrementalne obrade/upravljanja kešom # Instalacione putanje keša ako (-ne $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Funkcije upravljanja kešom funkcije Get-FileManifest { param([niska]$Path) if (probna putanja $Path) { pokušajte { $json = Get-Content $Path -Raw | ConvertFrom-Json # Konvertuj PSObject u heštable (PS5.1 kompatibilno – PS7 ima -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Ime] = $_. Vrednost } povratni $ht } hvatanje { vrati @{} } } vrati @{} }
funkcije Save-FileManifest { param([hashtable]$Manifest, [string]$Path) $dir = Split-Path $Path -Nadređeni ako (-ne (probna putanja $dir)) { New-Item -ItemType direktorijum -Putanja $dir -Force | Bez vrednosti } $Manifest | ConvertTo-Json -Dubina 3 -Komprimovanje | Set-Content $Path - Sila }
funkcije Get-DeviceCache { param([niska]$Path) if (probna putanja $Path) { pokušajte { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host " Keš učitanog uređaja: $($cacheData.Count) uređaji" -PrednjigroundColor TamnaGray return $cacheData } hvatanje { Write-Host " Keš je oštećen, ponovo će se izgraditi" – boja prednjeg plana žuta return @() } } return @() }
funkcije Save-DeviceCache { param($Devices, [niska]$Path) $dir = Split-Path $Path -Nadređeni ako (-ne (probna putanja $dir)) { New-Item -ItemType direktorijum -Putanja $dir -Force | Bez vrednosti } # Konvertuj u niz i sačuvaj $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Dubina 10 -Komprimovanje | Set-Content $Path - Sila Write-Host " Sačuvani keš uređaja: $($deviceArray.Count) uređaji" -Prednji planColor Tamnagray }
funkcije Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Napravi pronalaženje koje ne razlikuje mala i velika slova iz manifesta (normalizuj u mala slova) $manifestLookup = @{} foreach ($mk in $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file in $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalizuj putanju do malih slova $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Veličina = $file. Dužina } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] ako ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Veličina -eq $file. Dužina) { Ne, $unchanged. Dodaj($file) Nastavite } } [void]$changed. Dodaj($file) } vrati @{ Promenjeno = $changed Nepromenjeno = $unchanged NewManifest = $newManifest } }
# Ultra-fast paralelno učitavanje datoteke pomoću grupne obrade funkcije Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Raиuna # Koristite pakete od ~1000 datoteka za bolju kontrolu memorije $batchSize = [matematika]::Min(1000, [matematika]::Ceiling($totalFiles / [math]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
za ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [matematika]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Dodaj($batch) } Write-Host " ($($batches. Count) paketi od ~$batchSize svaki)" -NoNewline -ForegroundColor Tamnagray $flatResults = [System.Collections.Generic.List[object]]::new() # Proverite da li je dostupna paralela PowerShell 7+ $canParallel = $PSVersionTable.PSVersion.Major -ge 7 if ($canParallel -and $Threads -gt 1) { # PS7+: Paralelno obrađivanje paketa $results = $batches | ForEach-Object - ograničenje $Threads - paralelno { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file in $batchFiles) { pokušajte { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } hvatanje { } } $batchResults.ToArray() } foreach ($batch in $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } } još { # Osnovni PS5.1: Sekvencijalna obrada (i dalje brza za <10K datoteka) foreach ($file $Files) { pokušajte { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } hvatanje { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (test-path $InputPath -PathType List) { # Jedna JSON datoteka ako ($InputPath -like "*.json") { $jsonContent = Get-Content -Putanja $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "Učitani $($allDevices.Count) zapisi iz datoteke" } još { Write-Error "Podržan je samo JSON format. Datoteka mora da ima .json". izlaz 1 } } elseif (Test-Path $InputPath -PathType Container) { # Folder - samo JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Ime –notmatch "ScanHistory|Status primene|RolloutPlan" }) # Više voli *_latest.json ako postoje (režim po računaru) $latestJson = $jsonFiles | Where-Object { $_. Ime - like "*_latest.json" } if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count if ($totalFiles -eq 0) { Write-Error "Nije pronađena nijedna JSON datoteka u: $InputPath" izlaz 1 } Write-Host "Pronađene $totalFiles JSON datoteke" - Boja prednjeg plana siva # Pomoćna funkcija koja se podudara sa nivoima pouzdanosti (rukuje kratkim i punim obrascima) # Definisano rano tako da i StreamingMode i normalne putanje mogu da ga koriste funkcije Test-ConfidenceLevel { param([niska]$Value, [niska]$Match) if ([string]::IsNullOrEmpty($Value)) { return $false } prebaci ($Match) { "HighConfidence" { vraća $Value -eq "Visoki stepen pouzdanosti" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") } "TemporarilyPaused" { return $Value -like "Temporarily Pauzused*" } "NotSupported" { return ($Value -like "Not Supported*" -ili $Value -eq "Not Supported") } podrazumevano { vrati $false } } } #region STREAMING MODE – Memorijski efikasna obrada za velike skupove podataka # Uvek koristite StreamingMode za memorijski efikasnu obradu i kontrolnu tablu novog stila ako (-ne $StreamingMode) { Write-Host "Automatsko omogućavanje streamingMode (kontrolna tabla novog stila)" -Boja prednjeg plana žuta $StreamingMode = $true if (-not $IncrementalMode) { $IncrementalMode = $true } } # Kada je omogućen StreamingMode, obrađuje datoteke u odlomcima zadržavajući samo brojače u memoriji.# Podaci na nivou uređaja su upisani u JSON datoteke po odlomku za učitavanje na zahtev na kontrolnoj tabli.# Iskorišćenost memorije: ~1,5 GB bez obzira na veličinu skupa podataka (naspram 10-20 GB bez protoka).ako ($StreamingMode) { Write-Host "STREAMING MODE omogućen – memorijski efikasna obrada" - Zelena boja prednjeg plana $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTAL CHECK: If no files changed since last run, skip processing entire if ($IncrementalMode -and -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" if (probna putanja $stManifestPath) { Write-Host "Provera promena od poslednjeg izvršavanja protoka..." - Boja prednjeg plana Cijan $stOldManifest = Get-FileManifest -Putanja $stManifestPath if ($stOldManifest.Count -gt 0) { $stChanged = $false # Brza provera: isti broj datoteka? if ($stOldManifest.Count -eq $totalFiles) { # Proverite 100 NAJNOVIJIH datoteka (sortirano po padajućem redosledu "LastWriteTime") # Ako se neka datoteka promeni, ona će imati najnoviju vremensku oznaku i prvo će se pojaviti $sampleSize = [matematika]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Descending | Select-Object - Prvi $sampleSize foreach ($sf in $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Slomiti } # Compare timestamps - keširano može biti DateTime ili string after JSON roundtrip $cachedLWT = $stOldManifest[$sfKey]. Vreme poslednjeg pisca $fileDT = $sf. Vreme poslednjeg pisca pokušajte { # Ako je keširano već DateTime (Konvertovanje Automatski konvertujeFrom-Json), koristite direktno if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } još { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Slomiti } } hvatanje { $stChanged = $true Slomiti } } } još { $stChanged = $true } ako (-ne $stChanged) { # Proveri da li izlazne datoteke postoje $stSummaryExists = Get-ChildItem (Pridruživanje putanji $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object - Prvih 1 $stDashExists = Get-ChildItem (Pridruživanje putanji $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object - Prvih 1 if ($stSummaryExists -and $stDashExists) { Write-Host " Nije otkrivena nijedna promena ($totalFiles nepromenjene datoteke) - preskače se obrada" - boja prednjeg plana – zelena boja prednjeg plana Write-Host " Poslednja kontrolna tabla: $($stDashExists.FullName)" -Boja prednjeg plana Bela $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv Write-Host " Uređaji: $($cachedStats.TotalDevices) | Ažurirano: $($cachedStats.Updated) | Greške: $($cachedStats.WithErrors)" -Boja prednjeg plana siva Write-Host " Dovršeno u $([matematika]::Round($streamSw.Elapsed.TotalSeconds, 1))s (nije potrebna obrada)" -Boja prednjeg plana – zelena boja povratna $cachedStats } } još { # DELTA ZAKRPA: Pronađite tačno koje su datoteke promenjene Write-Host " Otkrivene su promene – identifikovanje promenjenih datoteka..." - Boja prednjeg plana žuta $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf in $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } još { $cachedLWT = $stOldManifest[$jfKey]. Vreme poslednjeg pisca $fileDT = $jf. Vreme poslednjeg pisca pokušajte { $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) } } uhvatite { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [matematika]::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Promenjeno: $($changedFiles.Count) | Novo: $($newFiles.Count) | Ukupno: $totalChanged ($changePct%)" -Boja prednjeg plana žuta if ($totalChanged -gt 0 -i $changePct -lt 10) { # REŽIM DELTA ZAKRPA: <10% promenjeno, zakrpi postojeće podatke Write-Host " Režim Delta zakrpe ($changePct% < 10%) – zakrpa $totalChanged datoteka..." -Boja prednjeg plana – zelena boja $dataDir = Join-Path $OutputPath "podaci" # Učitavanje izmenjenih/novih zapisa uređaja $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df in $allDeltaFiles) { pokušajte { $devData = Get-Content $df. FullName - Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData ako ($dev. HostName) { $deltaDevices[$dev. Ime HostName] = $dev } } hvatanje { } } Write-Host " Učitani $($deltaDevices.Count) promenjeni zapisi uređaja" -Boja prednjeg plana Siva # Za svaku kategoriju JSON: uklonite stare stavke za promenjena imena hosta, dodajte nove stavke $categoryFiles = @("greške", "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 in $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat in $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" if (probna putanja $catPath) { pokušajte { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Ukloni stare stavke za promenjena imena hosta $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. Ime hosta) }) # Ponovo klasifikujte svaki promenjeni uređaj u kategorije # (biće dodato ispod posle klasifikacije) $catData | ConvertTo-Json - Dubina 5 | Set-Content $catPath – šifrovanje UTF8 } hvatanje { } } } # Klasifikujte svaki promenjeni uređaj i dodajte odgovarajuće datoteke kategorije foreach ($dev in $deltaDevices.Values) { $slim = [poručeno]@{ Ime HostName = $dev. Ime hosta WMI_Manufacturer = if ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } još { "" } WMI_Model = if ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } još { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } else { "" } ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. Ažurirano UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } još { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } else { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. PoznatiIssueId } još { $null } SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-ne [niska]::IsNullOrEmpty($dev. UEFICA2023Error) - i $dev. UEFICA2023Error -ne "0" -i $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -ili $dev. SecureBootTaskStatus -eq "Disabled" - ili $dev. SecureBootTaskStatus -eq "NotFound") $tskNF = ($dev. SecureBootTaskStatus -eq "NotFound") $sbOn = ($dev. SecureBootEnabled -ne $false -i "$($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 -ili $dev. MissingKEK -eq $true) $hKI = ((-not [niska]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -ili (-not [string]::IsNullOrEmpty($dev. PoznatoIsueId)) $rStat = if ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" } # Dodaj u datoteke kategorije koje se podudaraju $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") -ili ($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 -i $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } foreach ($tgt in $targets) { $tgtPath = Join-Path $dataDir "$tgt.json" if (probna putanja $tgtPath) { $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json - Dubina 5 | Set-Content $tgtPath – šifrovanje UTF8 } } } # Ponovo generiši CSV datoteke iz zakrpanih JSON-ova Write-Host " Ponovno generiše CSV datoteke iz zakrpanih podataka..." - Boja prednjeg plana siva $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat in $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (probna putanja $catJsonPath) { pokušajte { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -putanja $catCsvPath -NoTypeInformation -Šifrovanje UTF8 } } hvatanje { } } } # Ponovo nabroj statistiku u zakrvljenim JSON datotekama Write-Host " Ponovno izračunavanje rezimea iz zakrpljenih podataka..." - Boja prednjeg plana siva $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 in $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (test-path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Broj } hvatanja { } } prebaci ($cat) { "updated_devices" { $pUpdated = $cnt } "greške" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # je izračunat "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). Raиuna $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Delta zakrpa je dovršena: ažurirani $totalChanged uređaji" - Boja prednjeg plana – zelena Write-Host " Ukupno: $pTotal | Ažurirano: $pUpdated | NotUpdated: $pNotUpdated | Greške: $pErrors" - Boja prednjeg plana bela # Ažuriraj manifest $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf in $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Veličina = $jf. Dužina } } Save-FileManifest - Manifest $stNewManifest -Putanja $stManifestPath Write-Host " Dovršeno u $([matematika]::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta zakrpa - $totalChanged uređaji)" -Boja prednjeg plana – zelena boja prednjeg plana # Padne u potpuni proces protoka da bi ponovo generisali HTML kontrolnu tablu # Datoteke sa podacima su već zakrčene, tako da na ovaj način kontrolna tabla ostaje aktuelna Write-Host " Ponovno generiše kontrolnu tablu iz zakrpanih podataka..." - Boja prednjeg plana žuta } još { Write-Host " $changePct% promenjenih datoteka (>= 10%) – potreban je potpuni proces protoka" - boja prednjeg plana Boja žute boje } } } } } # Kreiranje poddirektorijuma podataka za JSON datoteke uređaja na zahtev $dataDir = Join-Path $OutputPath "podaci" if (-not (Test-Path $dataDir)) { New-Item -ItemType directory -Path $dataDir -Force | Out-Null } # Deduplication via HashSet (O(1) po pronalaženju, ~50 MB za imena hostova od 600 K) $seenHostnames = [System.Collections.Generic.HashSet[string]]:new([System.StringComparer]::OrdinalIgnoreCase) # Laki brojači rezimea ($allDevices + $uniqueDevices memoriji) $c = @{ Ukupno = 0; SBEnabled = 0; SBOff = 0 Ažurirano = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; Nije podržano = 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 Ažuriranje na čekanju = 0 } # Praćenje kontejnera za AtRisk/SafeList (laki skupovi) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Datoteke sa podacima za uređaj u grupnom režimu: akumuliranje po odlomku, praženje u granicama odlomka $stDeviceFiles = @("greške", "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 in $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim zapis uređaja za JSON izlaz (samo osnovna polja, ~200 bajtona naspram ~2KB punog) funkcije Get-SlimDevice { param($Dev) return [ordered]@{ Ime hosta = $Dev.Ime hosta 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 } else { $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 } else { $null } UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } else { $null } WinCSKeyApplied = if ($Dev.PSObject.properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } else { $null } } } # Poništi paket u JSON datoteku (režim dodavanja) funkcije Flush-DeviceBatch { param([niska]$StreamName, [System.Collections.Generic.List[object]]$Batch) if ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev in $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 PETLJA PROTOKA $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 } $stTotalChunks = [matematika]::Ceiling($totalFiles / $stChunkSize) $stPeakMemMB = 0 if ($stTotalChunks -gt 1) { Write-Host "Obrada $totalFiles datoteka u $stTotalChunks odlomcima $stChunkSize (striming, $ParallelThreads niti):" - Boja prednjeg plana – boja cijan } još { Write-Host "Obrada $totalFiles (strimovanje, $ParallelThreads niti):" -Prednji planColor Cyan } za ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [matematika]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd. if ($stTotalChunks -gt 1) { Write-Host " Odlomak $($ci + 1)/$stTotalChunks ($($cFiles.Count) datoteke): " -NoNewline -ForegroundColor Gray } još { Write-Host " Učitavanje $($cFiles.Count) datoteka: " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Niti $ParallelThreads # Paketne liste po odlomku $cBatches = @{} foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 foreach ($raw in $rawDevices) { if (-not $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Ime hosta if (-not $hostname) { continue } if ($seenHostnames.Contains($hostname)) { $cDupe++; nastavi } [void]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -i "$($device. SecureBootEnabled)" -ne "False") ako ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = if ($device. PSObject.Properties['ConfidenceLevel'] -i $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } još { "" } $hasErr = (-ne [niska]::IsNullOrEmpty($device. UEFICA2023Error) -i "$($device. UEFICA2023Error)" -ne "0" -i "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -ili "$($device. SecureBootTaskStatus)" -eq "Disabled" -ili "$($device. SecureBootTaskStatus)" -eq "NotFound") $tskNF = ("$($device. SecureBootTaskStatus)" -eq "NotFound") $bid = if ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } else { "" } $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 -ili $device. NedostajeKEK -eq $true -ili "$($device. MissingKEK)" -eq "True") $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -ili (-not [string]::IsNullOrEmpty($device. PoznatoIsueId)) $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 } još { "Nepoznato" } $bid = if (-not [string]::IsNullOrEmpty($bid)) { $bid } else { "" } # Zastavica "Ažuriranje na čekanju" (primenjena smernica/WinCS, status još nije ažuriran, SB UKLJUČENO, zadatak nije onemogućen) $uefiStatus = if ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } još { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy – i "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -i $device. WinCSKeyApplied -eq $true) $statusPending = ([niska]::IsNullOrEmpty($uefiStatus) -ili $uefiStatus -eq "NotStarted" -ili $uefiStatus -eq "InProgress") $isUpdatePending = (($hasPolicy -ili $hasWinCS) - i $statusPending -i -not $isUpd -$sbOn -a -$tskDis) ako ($isUpd) { $c.Ažurirano++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add(Get-SlimDevice $device)) # Prati ažurirane uređaje koji treba ponovo pokrenuti (UEFICA2023Status=Updated ali Event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF – izvan opsega, ne klasifikuj po pouzdanosti } else { if ($isUpdatePending) { } # Prebrojano zasebno u ažuriranju na čekanju – uzajamno isključivo za kružni grafikon elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ } else { $c.ActionReq++ } if ([niska]::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++ } ako ($hasErr) { $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["greške"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Greška if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ ako ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } ako ($hKI) { $c.WithKnownIsues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = if (-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } else { $device. PoznatiSueId } if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } ako (-ne $isUpd -i ($tskDis -ili (Test-ConfidenceLevel $conf "TemporarilyPaused")) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ((Test-ConfidenceLevel $conf "NotSupported") -ili ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -i $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 -i $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ } if ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ } # Ažuriranje na čekanju: primenjena smernica ili WinCS, status na čekanju, SB UKLJUČENO, zadatak nije onemogućen ako ($isUpdatePending) { $c.Ažuriraj na čekanju++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } ako (-ne $isUpd -i $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # U okviru uređaji za posmatranje (odvojeno od radnje obavezno) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Potrebna je radnja: nije ažurirano, SB UKLJUČENO, ne podudara se sa drugim kategorijama pouzdanosti, a ne sa ažuriranjem na čekanju if (-not $isUpd -and $sbOn $isUpdatePending -and -not -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -a -not (Test-ConfidenceLevel $conf "UnderObservation") -i -not (Test-ConfidenceLevel $conf "TemporarilyPaused") -a -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Ažurirano=0; Ažuriranje na čekanju=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Ukupno++ ako ($isUpd) { $stMfrCounts[$mfr]. Ažurirano++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. Ažuriranje na čekanju++ } elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "Nije podržano") { $stMfrCounts[$mfr]. Nije podržano++ } else { $stMfrCounts[$mfr]. ActionReq++ } ako ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Pratite sve uređaje po kontejneru (uključujući i prazan ID kontejnera) $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Broj=0; Ažurirano=0; Proizvođač=$mfr; Model=""; BIOS="" } ako ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } ako ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ ako ($isUpd) { $stAllBuckets[$bucketKey]. Ažurirano++ } } # Poništi pakete na disk foreach ($df in $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 = [matematika]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew, $cDupe, ${cTime}s | Mem: ${cMem}MB$cEta" -Prednji plan Boja zelene boje } # Dovršavanje JSON nizova foreach ($dfName in $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) uređaji" -Boja prednjeg plana Tamnagray } # Izračunavanje izvedene statistike $stAtRisk = 0; $stSafeList = 0 foreach ($bid in $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Updated if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [matematika]::Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = broj iz not_updated (uređaji sa SB UKLJUČENO i nisu ažurirani) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [poručeno]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Ažurirano = $c.Updated; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; TemporarilyPaused = $c.TempPaused; Nije podržano = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; Potpuno ažurirano = $c.Updated Ispravke na čekanju = $stNotUptodate; UpdatesComplete = $c.Updated WithErrors = $c.WithErrors; InProgress = $c.InProgress; NotYetInitiated = $c.NotYetInitiated RolloutInProgress = $c.RolloutInProgress; WithKnownIssues = $c.WithKnownIsues WithMissingKEK = $c.WithMissingKEK; TemporaryFailures = $c.TempFailures; PermanentFailures = $c.PermFailures NeedsReboot = $c.NeedsReboot; Ažuriranje je na čekanju = $c.UpdatePending AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList PercentWithErrors = if ($c.Total -gt 0) { [matematika]::Round(($c.WithErrors/$c.Total)*100,2) } else { 0 } PercentAtRisk = if ($c.Total -gt 0) { [matematika]::Round(($stAtRisk/$c.Total)*100,2) } else { 0 } PercentSafeList = if ($c.Total -gt 0) { [matematika]::Round(($stSafeList/$c.Total)*100,2) } else { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } else { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [matematika]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } else { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [matematika]::Round($stNotUptodate/$c.Total*100,1) } else { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [matematika]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Pisanje CSV datoteka [PSCustomObject]$stats | Export-Csv -Putanja (pridruži se $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Šifrovanje UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } - Opadajući redosled | ForEach-Object { [PSCustomObject]@{ Proizvođač=$_. Ključ; Broj=$_. Vrednost.Ukupna vrednost; Ažurirano=$_. Vrednost.Ažurirano; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Putanja (pridružena putanja $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Šifrovanje UTF8 $stErrorCodeCounts.GetEnumerator() | Sort-Object vrednost -Opadajući redosled | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Ključ; Broj=$_. Vrednost; SampleDevices=($stErrorCodeSamples[$_. Taster] - pridruži se ", ") } } | Export-Csv -Putanja (pridružena putanja $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Šifrovanje UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } - Opadajući redosled | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Ključ; Broj=$_. Vrednost.Prebrojavanje; Ažurirano=$_. Vrednost.Ažurirano; NotUpdated=$_. Value.Count-$_. Vrednost.Ažurirano; Proizvođač=$_. Value.Manufacturer } } | Export-Csv -Putanja (pridružena putanja $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Šifrovanje UTF8 # Generiši CSV datoteke kompatibilne sa orkestratorom (očekivana imena datoteka za Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (probna putanja $notUpdatedJsonPath) { pokušajte { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json if ($nuData.Count -gt 0) { # NotUptodate CSV - orchestrator traži *NotUptodate*.csv $nuData | Export-Csv -Putanja (pridružena putanja $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Šifrovanje UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) uređaji)" -Boja prednjeg plana Siva } } hvatanje { } } # Napišite JSON podatke za kontrolnu tablu $stats | ConvertTo-Json - Dubina 3 | Set-Content (Pridruži se $dataDir "summary.json") -Kodiranje UTF8 # ISTORIJSKO PRAĆENJE: Čuvanje tačke podataka za grafikon trenda # Koristite stabilnu lokaciju keša tako da se podaci o trendu i dalje pojavljuju u fasciklama za prikupljanje vremenski ograničenih oznaka. # Ako OutputPath izgleda kao "...\Aggregation_yyyyMMdd_HHmmss", keš ide u nadređenu fasciklu.# U suprotnom, keš ide unutar samog OutputPath.$parentDir = Split-Path $OutputPath -Nadređeni $leafName = Split-Path $OutputPath -List ako ($leafName -podudaranje "^Aggregation_\d{8}" -ili $leafName -eq "Aggregation_Current") { # Orchestrator-created timestamped folder — use parent for stable cache $historyPath = Join-Path $parentDir ".cache\trend_history.json" } još { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Nadređeni if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() if (probna putanja $historyPath) { isprobajte { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } hvatanje { $historyData = @() } } # Takođe proverite unutar OutputPath\.cache\ (zastarela lokacija iz starijih verzija) # Objedinite sve tačke podataka koje nisu već u primarnoj istoriji if ($leafName -eq 'Aggregation_Current' -ili $leafName -match "^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((test-path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { pokušajte { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Datum }) foreach ($entry in $innerData) { ako ($entry. Datum -i $entry. Date -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host " Objedinjene $($innerData.Count) tačke podataka iz unutrašnjeg keša" -Boja prednjeg plana Tamnagray } } hvatanje { } } }
# BOOTSTRAP: Ako je istorija trenda prazna/proređena, rekonstruišite iz istorijskih podataka if ($historyData.Count -lt 2 -and ($leafName -match "^Aggregation_\d{8}" -ili $leafName -eq "Aggregation_Current")) { Write-Host " Istorija trenda pokretanja iz istorijskih podataka..." - Boja prednjeg plana žuta $dailyData = @{} # Izvor 1: CSV-ove rezimea unutar trenutne fascikle (Aggregation_Current čuva sve CSV-ove rezimea) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object ime foreach ($summCsv in $localSummaries) { pokušajte { $summ = Import-Csv $summCsv.FullName | Select-Object - Prvih 1 ako ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -i $summ. ReportGeneratedAt) { $dateStr = ([datum/vreme]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = if ($summ. Ažurirano) { [int]$summ. Ažurirano } još { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Datum = $dateStr; Ukupno = [int]$summ. TotalDevices; Ažurirano = $updated; NotUpdated = $notUpd NeedsReboot = 0; Greške = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } hvatanje { } } # Izvor 2: Stare Aggregation_* fascikle (zastarelo, ako i dalje postoje) $aggFolders = Get-ChildItem $parentDir -Direktorijum -Filtriraj "Aggregation_*" -EA Tihokontinuiraj | Where-Object { $_. Ime -podudaranje "^Aggregation_\d{8}" } | Sort-Object ime foreach ($folder in $aggFolders) { $summCsv = Get-ChildItem $folder. FullName - Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object - Prvih 1 ako ($summCsv) { pokušajte { $summ = Import-Csv $summCsv.FullName | Select-Object - Prvih 1 ako ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Name -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = if ($summ. Ažurirano) { [int]$summ. Ažurirano } još { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Datum = $dateStr; Ukupno = [int]$summ. TotalDevices; Ažurirano = $updated; NotUpdated = $notUpd NeedsReboot = 0; Greške = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } hvatanje { } } } # Izvor 3: RolloutState.json WaveHistory (ima vremenske oznake po talasu od 1. dana) # Ovo obezbeđuje tačke podataka o osnovnoj liniji čak i kada ne postoje stare fascikle za agregaciju $rolloutStatePaths = @( (Putanja spajanja $parentDir "RolloutState\RolloutState.json"), (Pridruži se putanji $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath in $rolloutStatePaths) { if (probna putanja $rsPath) { pokušajte { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Koristite talasne datume početka kao tačke podataka trenda # Izračunavanje kumulativnih uređaja ciljanih na svakom talasu $cumulativeTargeted = 0 foreach ($wave in $rsData.WaveHistory) { ako ($wave. StartedAt -and $wave. Broj uređaja) { $waveDate = ([datum/vreme]$wave. StartedAt). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. Broj uređaja if (-not $dailyData.ContainsKey($waveDate)) { # Približno: u talasu vremena početka ažurirani su samo uređaji iz prethodnih talasa $dailyData[$waveDate] = [PSCustomObject]@{ Datum = $waveDate; Ukupno = $c.Ukupno; Ažurirano = [matematika]::Max(0, $cumulativeTargeted - [int]$wave. Broj uređaja) NotUpdated = $c.Total - [matematika]::Max(0, $cumulativeTargeted - [int]$wave. Broj uređaja) NeedsReboot = 0; Greške = 0; ActionRequired = 0 } } } } } } hvatanje { } # Koristi prvo pronađeno } }
if ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object | ForEach-Object { $_. Vrednost }) Write-Host " Bootstrapped $($historyData.Count) tačaka podataka iz istorijskih rezimea" -Boja prednjeg plana – zelena } }
# Dodajte trenutnu tačku podataka (seduplicate po danu – zadržite najnovije po danu) $todayKey = (Get-Date). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Date)" - like "$todayKey*" } ako ($existingToday) { # Zameni današnji unos $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Datum = $todayKey Ukupno = $c.Ukupno Ažurirano = $c.Ažurirano NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Greške = $c.WithErrors ActionRequired = $c.ActionReq } # Uklonite pogrešne tačke podataka (0 ukupno) i zadržite poslednjih 90 $historyData = @($historyData | Where-Object { [int]$_. Ukupno -gt 0 }) # Bez inicijala – podaci trenda su ~100 bajova/unosa, a čitava godina = ~36 kB $historyData | ConvertTo-Json -Dubina 3 | Set-Content $historyPath – šifrovanje UTF8 Write-Host " trend istorija: $($historyData.Count) tačke podataka" -Boja prednjeg plana Tamnagray # Napravite podatke grafikona trenda za HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) –join "," $trendUpdated = ($historyData | ForEach-Object { $_. Ažurirano }) – pridruži se "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) – pridruživanje "," $trendTotal = ($historyData | ForEach-Object { $_. Total }) - pridruži se "," # Projekcija: proširivanje linije trenda pomoću eksponencijalnog udubljanja (2,4,8,16...) # Izvodi veličinu talasa i period posmatranja iz stvarnih podataka o istoriji trenda. # - Veličina talasa = najveće povećanje pojedinačnog perioda viđeno u istoriji (primenjeni najnoviji talas) # - Dani posmatranja = prosečni kalendarski dani između tačaka podataka trenda (koliko često se pokrećemo) # Zatim duplira veličinu talasa za svaki period, podudarajući se sa strategijom rasta od 2x.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Updated $remaining = $stNotUptodate # Samo SB-ON uređaji koji nisu ažurirani (isključuje SecureBoot OFF) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Izvedena veličina talasa i period posmatranja iz istorije trendova $increments = @() $dayGaps = @() za ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Ažurirano - $historyData[$hi-1]. Ažurira if ($inc -gt 0) { $increments += $inc } pokušajte { $d 1 = [datetime]::P arse($historyData[$hi-1]. Datum) $d 2 = [datetime]::P arse($historyData[$hi]. Datum) $gap = ($d 2 – $d 1). Ukupni dani if ($gap -gt 0) { $dayGaps += $gap } } uhvatite {} } # Veličina talasa = najnoviji pozitivan inkrement (trenutni talas), povratna vrednost na prosek, minimalno 2 $waveSize = if ($increments. Broj -gt 0) { [matematika]:Max(2, $increments[-1]) } else { 2 } # Period posmatranja = prosečan razmak između tačaka podataka (kalendarski dani po talasu), minimalno 1 $waveDays = if ($dayGaps.Count -gt 0) { [matematika]::Max(1, [matematika]::Round(($dayGaps | Measure-Object -Prosek). Prosek, 0)) } else { 1 }
Write-Host " Projekcija: waveSize=$waveSize (od poslednjeg povećanja), waveDays=$waveDays (avg gap from history)" -ForegroundColor DarkGray
$dayCounter = 0 # Projektujte dok svi uređaji ne budu ažurirani ili maksimalno 365 dana za ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # Na granicu svakog perioda posmatranja primenite talas, a zatim dvaput if ($dayCounter -ge $waveDays) { $devicesThisWave = [matematika]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Veličina dvostrukog talasa za sledeći period (strategija orkestratora 2x) $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: potrebno je najmanje 2 tačke podataka trenda da bi se izvršilo vremensko podešavanje talasa" -Boja prednjeg plana TamnaGray } # Napravite niske podataka kombinovanog grafikona za nisku $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } else { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } else { "" } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { "" } $histCount = ($historyData | Objekat mere). Raиuna $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } - Opadajući redosled | ForEach-Object { @{ ime=$_. Ključ; total=$_. Vrednost.Ukupna vrednost; ažurirano=$_. Vrednost.Ažurirano; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json - Dubina 3 | Set-Content (pridruživanje putanji $dataDir "manufacturers.json") -Šifrovanje UTF8 # Konvertujte JSON datoteke sa podacima u CSV za Excel preuzimanja koja je moguće čitati Write-Host "Konvertovanje podataka o uređaju u preuzimanje CSV datoteke za Excel..." -Boja prednjeg plana siva foreach ($dfName in $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" if (probna putanja $jsonFile) { pokušajte { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json if ($jsonData.Count -gt 0) { # Uključi dodatne kolone za update_pending CSV $selectProps = if ($dfName -eq "update_pending") { @('Ime HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } još { @('Ime HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv -putanja $csvFile -NoTypeInformation -Šifrovanje UTF8 Write-Host " $dfName -> $($jsonData.Count) redove -> CSV" -Boja prednjeg plana Tamnagray } } hvatanje { Write-Host " $dfName - preskočeno" - Boja prednjeg plana Tamnozelena } } } # Generiši samouslužnu HTML kontrolnu tablu $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Generisanje samokontejnirane HTML kontrolne table..." -Boja prednjeg plana žuta # VELOCITY PROJEKCIJA: Izračunavanje iz istorije skeniranja ili prethodnog rezimea $stDeadline = [datetime]"2026-06-24" # KEK datum isteka certifikata $stDaysToDeadline = [matematika]::Max(0, ($stDeadline - (Get-Date)). Dani) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "N/A" $stWorkingDays = 0 $stCalendarDays = 0 # Prvo pokušajte istoriju trenda (laku kategoriju, koju već održava agregator – zamenjuje nagomilne ScanHistory.json) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Ukupno -gt 0 -i [int]$_. Ažurirano -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. Date.Length)) $currDate = [datetime]::P arse($curr. Date.Substring(0, [Matematika]:Min(10, $curr. Date.Length)) $daysDiff = ($currDate – $prevDate). Ukupni dani if ($daysDiff -gt 0) { $updDiff = [int]$curr. Ažurirano - [int]$prev. Ažurira if ($updDiff -gt 0) { $stDevicesPerDay = [matematika]::Round($updDiff / $daysDiff, 0) $stVelocitySource = "Istorija trenda" } } } } # Isprobajte rezime primene orkestratora (ima unapred izračunato vreme) if ($stVelocitySource -eq "N/A" -i $RolloutSummaryPath -i (Test-Path $RolloutSummaryPath)) { pokušajte { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [matematika]::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 } } } hvatanje { } } # Osnova: isprobajte prethodnu CSV datoteku rezimea (pretraži trenutnu fasciklu I nadređene/srodne fascikle za agregaciju) if ($stVelocitySource -eq "N/A") { $searchPaths = @( (Pridruži se putanji $OutputPath "SecureBoot_Summary_*.csv") ) # Takođe pretražite srodne agregatne fascikle (orchestrator kreira novu fasciklu za svako pokretanje) $parentPath = Split-Path $OutputPath -Nadređeni ako ($parentPath) { $searchPaths += (Pridruži se $parentPath "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += (Pridruži se $parentPath "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object vreme poslednjeg upisivanja -Opadajući | Select-Object -Prvih 1 ako ($prevSummary) { pokušajte { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) - $prevDate). Ukupni dani if ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Updated - $prevUpdated if ($updDelta -gt 0) { $stDevicesPerDay = [matematika]::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = "Prethodni izveštaj" } } } hvatanje { } } } # Vraćanje: izračunavanje brzine u punoj istoriji trenda (prva naspram najnovije tačke podataka) if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Ukupno -gt 0 -i [int]$_. Ažurirano -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. Date.Length)) $lastDate = [datetime]::P arse($last. Date.Substring(0, [Matematika]:Min(10, $last. Date.Length)) $daysDiff = ($lastDate – $firstDate). Ukupni dani if ($daysDiff -gt 0) { $updDiff = [int]$last. Ažurirano - [int]$first. Ažurira if ($updDiff -gt 0) { $stDevicesPerDay = [matematika]::Round($updDiff / $daysDiff, 1) $stVelocitySource = "Istorija trenda" } } } } # Izračunaj projekciju pomoću eksponencijalnog udubljanja (u skladu sa grafikonom trenda) # Ponovo koristite podatke projekcije koji su već izračunati za grafikon ako su dostupni if ($hasProjection -and $projDates.Count -gt 0) { # Koristite poslednji projektovani datum (kada se svi uređaji ažuriraju) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([datum i vreme]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy") $stCalendarDays = ([datum i vreme]::P arse($lastProjDateStr) - (Get-Date)). Dana $stWorkingDays = 0 $d = Get-Date za ($i = 0; $i -lt $stCalendarDays; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -i $stNotUptodate -gt 0) { # Povratno: linearna projekcija ako nema dostupnih eksponencijalnih podataka $daysNeeded = [matematika]::Ceiling($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Get-Date za ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Izrada brzog HTML-a $velocityHtml = if ($stDevicesPerDay -gt 0) { "<div><strong>🚀 Uređaji/dan:</strong> $($stDevicesPerDay.ToString('N0')) (izvor: $stVelocitySource)</div>" + "<div><strong>📅 Projektovano dovršavanje:</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><jak>🕐 Radni dani:</strong> $stWorkingDays | <su>Calendar dana:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Krajnji rok: 24. jun 2026. (rok važenja KEK certifikata) | Preostalo dana: $stDaysToDeadline</div>" } još { "<div style='padding:8px; background:#fff3cd; ivica-radijus:4px; border-left:3px solid #ffc107'>" + "<je>📅 Projektovano dovršavanje:</strong> nedovoljno podataka za izračunavanje velečasnosti. " + + "Pokrenite agregaciju najmanje dvaput sa promenama podataka da biste uspostavili stopu.<br/>" + "<je>rok:</strong> 24. jun 2026. || <je>preostalih dana:</strong> $stDaysToDeadline</div>" } # Odbrojavanje datuma isteka $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematika]:Max(0, ($certKekExpiry - $certToday). Dani) $daysToUefi = [matematika]:Max(0, ($certUefiExpiry - $certToday). Dani) $daysToPca = [matematika]::Max(0, ($certPcaExpiry - $certToday). Dani) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Helper: Čitanje zapisa iz usluge JSON, rezime izdanja kontejnera + prvi redovi N uređaja $maxInlineRows = 200 funkcije Build-InlineTable { param([niska]$JsonPath; [int]$MaxRows = 200, [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (probna putanja $JsonPath) { pokušajte { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Raиuna # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats if ($totalCount -gt 0) { $buckets = $data | Group-Object ID grupe | Sort-Object broj - opadajući redosled $bucketSummary = "><2 h3 style='font-size:,95em; boja:#333; margina:10px 0 5 px'><3 By Hardware Bucket ($($buckets. Broj) kontejnera)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:15px'><tabela><tead><tr><th><5 BucketID><6 /th><th style='text-align:right'>Ukupno</th><th style='text-align:right; color:#28a745'>Ažurirano</th><th style='text-align:right; color:#dc3545'>Nije ažurirano</th><th><1 proizvođač><2 /th></tr></thead><tbody>" foreach ($b in $buckets) { $bid = if ($b.Name) { $b.Name } else { "(prazno)" } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Preuzmi ažurirani broj od globalne statistike kontejnera (svi uređaji u ovoj kontejnera u celom skupu podataka) $lookupKey = $bid $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null } $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } else { 0 } $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } još { $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; boja:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; color:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # DETALJI O UREĐAJU: Prvi N redovi kao ravna lista $slice = $data | Select-Object - Prvi $MaxRows foreach ($d in $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match "High") { '<span class="bedge bedge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="bedge bedge-danger">Not Supported><6 /span>' } elseif ($conf -match "Under") { '<span class="bedge bedž info">Under Obs><0 /span>' } elseif ($conf -match "Pauzirano") { '<span class="bedge bedge-warning">Pauzirano><4 /span>' } else { '<span class="bedge-warning">Action Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="bedge bedge-success"><01 Ažurirano</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="bedge bedge-danger"><05 Error</span>' } else { '><08 span class="bedge bedž warning"><09 Pending><0 /span>' } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20 /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" } } hvatanje { } } if ($totalCount -eq 0) { return "><44 div style='padding:20px; color:#888; font-style:italic'><45 No devices in this category.><46 /div>" } $showing = [matematika]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; font-size:,85em; color:#666'><49 Total: $($totalCount.ToString("N0")) devices" if ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>📄 Preuzmite ceo CSV za Excel><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:,95em; boja:#333; margina:10px 0 5px ><58 detalji uređaja (prikazuje prvu $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500 px; overflow-y:auto'><table><thead><tr><th><0 HostName><1 /th><th><4 proizvođač><5 /th><th><8><9 th><8 th><th><2 Confidence><3 /th><thh><6 Status><7 /th><th><0 Greška><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>" return "$header$$bucketSummary$deviceHeader$deviceTable" } # Napravite umetnute tabele od JSON datoteka koje su već na disku, povezane sa CSV datotekama $tblErrors = Build-InlineTable -JsonPath (pridružena putanja $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath (pridružena putanja $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 (pridružena putanja $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" # Prilagođena tabela za ažuriranje na čekanju – uključuje kolone UEFICA2023Status i UEFICA2023Greška $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (probna putanja $upJsonPath) { pokušajte { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Count if ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; font-size:,85em; color:#666'>Total: $($upCount.ToString("N0")) uređaji | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>📄 Preuzmite ceo CSV za Excel><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object - Prvi $maxInlineRows foreach ($d in $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = if ($d.WinCSKeyApplied) { '<span class="bedge bedge-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 = [matematika]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:,95em; boja:#333; margina:10px 0 5px >detalji uređaja (prikazuje prvu $upShowing)</h3>" $upTable = "<div style='max-height:500 px; 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 UEFICA2 23Greška><6 /th><th><9 smernica</th><th>WinCS ključ</th><th>BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } još { $tblUpdatePending = "<div style='padding:20px; color:#888; stil fonta:>Nijedan uređaj u ovoj kategoriji.</div>" } } hvatanje { $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>No devices in this category.</div>" } } još { $tblUpdatePending = "<div style='padding:20px; color:#888; font-style:italic'>No devices in this category.</div>" } # Odbrojavanje datuma isteka $certToday = Get-Date $certKekExpiry = [datum i vreme]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematika]:Max(0, ($certKekExpiry - $certToday). Dani) $daysToUefi = [matematika]:Max(0, ($certUefiExpiry - $certToday). Dani) $daysToPca = [matematika]::Max(0, ($certPcaExpiry - $certToday). Dani) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Izgradite podatke u samom grafikonu proizvođača (prvih 10 po broju uređaja) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } - Opadajući redosled | Select-Object - Prvih 10 $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "Autor" } još { "Prvih 10 proizvođača" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Key)'" }) -join "," $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -pridruži se "," $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) - pridruži se "," $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) – spajanje "," $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join "," $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -pridruži se "," $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) –join "," $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join "," $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) – spajanje "," $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) – spajanje "," # Napravite tabelu proizvođača $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } - Opadajući redosled | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Ključ)</td><td>$($_. Value.Total.ToString("N0")))</td><td>$($_. Value.Updated.ToString("N0")))</td><td>$($_. Value.HighConf.ToString("N0"))><0 /td><td>$($_. Value.ActionReq.ToString("N0")))><4 /td></tr>'n" } # HTML fragmenti zatvorenih oznaka: razdeljeni na delove kako ih veb CMS platforme ne bi koristile # tumaиi ih kao pravi HTML i umeжe nevidljive Unikod znakove oko njih.$endScript = '</scr' + 'ipt>' $endStyle = '</sty' + 'le>' $endHead = '</he' + 'ad>' $endBody = '</bo' + 'dy>' $endHtml = '</ht' + 'ml>' $htmlContent = @" <! DOCTYPE html> <html lang="en"> <glavu> <meta znakova="UTF-8"> <metaime="viewport" content="width=device-width, initial-scale=1.0"> <naslov>za status certifikata za bezbedno pokretanje</title> <skripta src="https://cdn.jsdelivr.net/npm/chart.js">$endScript <stila> *{box-sizing:border-box; margina:0; popunjenost:0} body{font-family:'Segoe UI',Tahoma,sans-serif; background:#f0f2f5; boja:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); boja:#fff; padding:20px 30px} .header h1{font-size:1,6em; margin-bottom:5px} .header .meta{font-size:.85em; neprozirnost:.9} .container{max-width:1400px; margina:0 automatski; padding:20px} .cards{display:grid; koordinatna mreža-predložak-kolone:repeat(automatsko popunjavanje,minmax(170px,1fr)); razmak:12px; margina:20px 0} .card{background:#fff; ivica-radijus:10px; popunjenost:15 px; box-shadow:0 2px 8px rgba(0,0,0,,,08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1,8em; font-weight:700} .card .label{font-size:.8em; color:#666; margin-top:4px} .card .pct{font-size:.75em; color:#888} .section{background:#fff; ivica-radijus:10px; popunjenost:20 px; margina:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; color:#1a237e; margin-bottom:10px; kursor:pokazivač; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; koordinatna mreža-predložak-kolone:1fr 1fr; razmak:20 px; margina:20px 0} .chart-box{background:#fff; ivica-radijus:10px; popunjenost:20 px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabela{width:100%; skupi ivice:skupi; font-size:.85em} th{background:#e8eaf6; padding:8px 10px; poravnavanje teksta:levo; pozicija:lepljiv; prvih:0; z-indeks:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .bedž{display:inline-block; padding:2px 8px;border-radius:10px; font-size:.75em; font-weight:700} .bedž uspeh{background:#d4edda; color:#155724} .značka-opasnost{background:#f8d7da; color:#721c24} .bedž upozorenje{background:#fff3cd; color:#856404} .bedž-info{background:#d1ecf1; color:#0c5460} .top-link{float:right; font-size:.8em; color:#1a237e; text-decoration:none} .footer{text-align:center; popunjenost:20 px; color:#999; font-size:.8em} a{color:#1a237e}$endStyle $endHead <telo> <div class="header"> <h1>statusa certifikata za bezbedno pokretanje</h1> <div class="meta">Generisani: $($stats. ReportGeneratedAt) | Ukupan broj uređaja: $($c.Total.ToString("N0")) | Jedinstvene grupe: $($stAllBuckets.Count)</div> </div> <div class="container">
<!-- KPI kartice – može se kliknuti, povezane sa odeljcima -- > <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; right:8px; background:#dc3545; boja:#fff; padding:1px 6px; ivica-radijus:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString("N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% – POTREBNA JE RADNJA><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; right:8px; background:#28a745; boja:#fff; padding:1px 6px; ivica-radijus:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString() "N0"))</div><div class="label">Ažurirano><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; right:8px; background:#6c757d; boja:#fff; padding:1px 6px; ivica-radijus:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[matematika]::Round(($c.SBOff/$c.Total)*100,1)}else{0})% – Izvan opsega><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">Potrebno je ponovo pokretanje sistema><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% – čeka se ponovno pokretanje sistema><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})% - primenjeno smernice/WinCS, čekanje na><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">><4 $(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)% – Bezbedno za objavljivanje><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){[matematika]::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 da><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)% - Slično neuspešnim><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. Pauziran</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.WithKnownIsues/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0"))</div><div class="label">Missing KEK</div><div class="pct">$(if($c.Total -gt 0){[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)% – UEFI greške</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. Neuspeh</div><div class="pct">$(if($c.Total -gt 0){[matematika]::Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0"))</div><div class="label">Not Supported><6 /div><div class="pct">$(if($c.Total -gt 0){[matematika]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
<!-- za vreme primene & datum isteka certifikata --> <div id="s-velocity" style="display:grid; koordinatna mreža-predložak-kolone:1fr 1fr; razmak:20 px; margina:15px 0"> <div class="section" style="margin:0"> <h2>📅 Velociti primene</h2> ><2 div class="section-body open"><3 ><4 div style="font-size:2,5em; font-weight:700; color:#28a745"><5 $($c.Updated.ToString("N0"))><6 /div> ><8 div style="color:#666"><9 uređaji ažurirani za $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; background:#e8eaf6; visina:20 px; ivica-radijus:10px; overflow:hidden"><div style="background:#28a745; visina:100%; širina:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% je dovršeno</div> <div style="margin-top:10px; popunjenost:10 px; background:#f8f9fa; ivica-radijus:8px; font-size:,85em"> <div><strong>Preostalo:</strong> $($stNotUptodate.ToString("N0")) uređajima potrebna je</div> <div><strong>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) uređaji (greške + trajni + zadatak onemogućen)</div> <div><strong>Safe za primenu:</strong> $($stSafeList.ToString("N0")) uređaji (isti kontejner kao uspešan)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; border-left:4px solid #dc3545"> <h2 style="color:#dc3545">⚠ Certificate Expidown</h2> <div class="section-body open"> <div style="display:flex; razmak:15 px; margin-top:10px"> <div style="text-align:center; popunjenost:15 px; ivica-radijus:8px; min-širina:120 px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); border:2px solid #dc3545; flex:1"> <div style="font-size:.65em; boja:#721c24; transformacija teksta:velika slova; font-weight:bold">⚠ ISTEKLO</div> ><4 div style="font-size:.85em; font-weight:bold; color:#dc3545; margina:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2,5em; font-weight:700; color:#dc3545; line-height:1"><9 $daysToKek><0 /div><1 <div style="font-size:.8em; color:#721c24">dana (24. jun 2026.)</div><5 </div><7 <div style="text-align:center; popunjenost:15 px; ivica-radijus:8px; min-širina:120 px; background:linear-gradient(135deg,#fffef5,#fff3cd); border:2px solid #ffc107; flex:1"> ><00 div style="font-size:.65em; boja:#856404; transformacija teksta:velika slova; font-weight:bold"><01 UEFI CA 2011</div> ><04 div id="daysUefi" style="font-size:2,2em; font-weight:700; boja:#856404; visina reda:1; margina:5px 0"><05 $daysToUefi</div> ><08 div style="font-size:.8em; color:#856404"><09 dana (27. jun 2026.)><10 /div> ><12 /div> ><14 div style="text-align:center; popunjenost:15 px; ivica-radijus:8px; min-širina:120 px; background:linear-gradient(135deg,#f0f8ff,#d4edff); border:2px solid #0078d4; flex:1"><15 ><16 div style="font-size:.65em; boja:#0078d4; transformacija teksta:velika slova; font-weight:bold"><17 Windows PCA</div> ><20 div id="daysPca" style="font-size:2,2em; font-weight:700; boja:#0078d4; visina reda:1; margina:5px 0"><21 $daysToPca><2 /div><3 ><24 div style="font-size:.8em; color:#0078d4"><25 dana (19. okt 2026.)><26 /div><7 ><28 /div><9 ><30 /div><1 ><32 div style="margin-top:15 px; popunjenost:10 px; pozadina:#f8d7da; ivica-radijus:8px; font-size:,85em; border-left:4px solid #dc3545"><33 ><34 je>⚠ CRITICAL:><37 /strong> Svi uređaji moraju da se ažuriraju pre isteka certifikata. Uređaji koji nisu ažurirani krajnjim rokom ne mogu da primene buduće bezbednosne ispravke za upravljač pokretanjem i bezbedno pokretanje nakon isteka.</div> </div> </div> </div>
<!-- grafikona --> <div class="charts"> <div class="chart-box"><h3>Status primene</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) { "<!-- istorijski trend grafikon --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Ažuriranje toka tokom <a class='top-link' href='#'>↑ Top</a></h2> <div id='d-trend' class='section-body open'> <id='trendChart' height='120'></canvas> <div style='font-size:.75em; color:#888; margin-top:5px'>Pune linije = stvarni podaci$(ako ($historyData.Count -ge 2) { " | Isprekidana linija = projektovana (eksponencijalno dvostruko: 2→4→8→16... uređaji po talasu)" } još { " | Pokrenite agregaciju ponovo sutra da biste videli linije trenda i projekciju" })</div> </div> </div>" })
<!-- CSV preuzimanja --> <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Preuzmite kompletne podatke (CSV za Excel) <a class="top-link" href="#">Top</a></h2> <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap:5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; background:#dc3545; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">Nije ažurirano ($($stNotUptodate.ToString("N0"))</a> <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; background:#dc3545; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">Greške ($($c.WithErrors.ToString("N0"))))</a><2 <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; background:#fd7e14; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">Radnja obavezna ($($c.ActionReq.ToString("N0")))</a><6 <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; background:#dc3545; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">poznati problemi ($($c.WithKnownIssues.ToString("N0")))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; background:#dc3545; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">zadatak je onemogućen ($($c.TaskDisabled.ToString("N0"))))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; background:#28a745; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">Ažurirano ($($c.Updated.ToString("N0")))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; background:#6c757d; boja:#fff; padding:6px 14 px; ivica-preliv:5 px; text-decoration:none; font-size:.8em">rezime</a> <div style="width:100%; font-size:.75em; color:#888; margin-top:5px">CSV datoteke otvaraju u programu Excel. Dostupno kada se hostuje na veb serveru.</div> </div> </div>
<!-- analiza proizvođača --> <div class="section"> <h2 onclick="toggle('mfr')">Proizvođač <a class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <tabelu><tead><tr><th><1 proizvođač><2 /th><th><5 ukupan><6 /th><th><9 Updated><0><9 /th><th><3 visoki stepen pouzdanosti><4 /th><th><7 Potrebna je radnja><8 /th></tr></thead><3 <telo><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- odeljaka uređaja (prvih 200 umetnutih + CSV preuzimanja) -- > <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Uređaji sa greškama ($($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">🔴 Poznati problemi ($($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')">🟠 Nedostaje KEK – događaj 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">🟠 Potrebna je radnja ($($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">🔵 U okviru Posmatranje ($($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">🔴 Nije ažurirano ($($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">🔴 Zadatak je onemogućen ($($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">🔴 Privremena otkazivanja ($($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">🔴 Trajna otkazivanja / nije podržano ($($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"))) - primenjeno smernice/WinCS, Čeka se <ažuriranje klase="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">uređaji na kojima je primenjen taster AvailableUpdatesPolicy ili WinCS, ali je UEFICA2023Status i dalje NotStarted, InProgress ili null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Objavljivanje je u toku ($($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")) – izvan opsega <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">🟢 Ažurirani uređaji ($($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">🔄 Ažurirano - potrebno je ponovno pokretanje ($($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">Secure Boot Certificate Rollout Dashboard | Generisani $($stats. ReportGeneratedAt) | StreamingMode | Najveća memorija: ${stPeakMemMB} MB</div> </div><!-- /container -->
<skripte> preklopnik(id){var e=document.getElementById(id); e.classList.toggle('open')} funkcija 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. Pauzirano",'Nije podržano','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24 ','#adb5bd','#dc3545']}},opcije:{responsive:true,plugins:{legenda:{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:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'}.'},{label:'Visok stepen pouzdanosti',podaci:[$mfrHighConf],boja pozadine:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. Pauzirano",podaci:[$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'}}}); Istorijski 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čko odbrojavanje (funkcija(){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 "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Uvek čuvajte stabilnu kopiju "Najnovije" da administratori ne bi trebalo da prate vremenske oznake $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath - Sila $stTotal = $streamSw.Elapsed.TotalSeconds # Sačuvaj manifest datoteke za inkrementalni režim (brzo otkrivanje bez promene pri sledećem izvršavanju) ako ($IncrementalMode -ili $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 "Čuvanje manifesta datoteke za postepeno režim..." - Boja prednjeg plana siva foreach ($jf in $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Veličina = $jf. Dužina } } Save-FileManifest - Manifest $stNewManifest -Putanja $stManifestPath Write-Host " Sačuvani manifest za $($stNewManifest.Count) datoteke" - Boja prednjeg plana Tamnagray } # ČIŠĆENJE ZADRŽAVANJA # Orchestrator fascikla koja se može ponovo koristi (Aggregation_Current): zadržite samo najnovije pokretanje (1) # Administrator ručna pokretanja / druge fascikle: zadrži poslednjih 7 pokretanja # CSV-ove rezimea SE NIKADA ne brišu – sićušni su (~1 kB) i rezervni su izvor za istoriju trendova $outputLeaf = Split-Path $OutputPath -List $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } else { 7 } # Prefiksi datoteka bezbedni za čišćenje (snimci efemerala po pokretanju) $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_' ) # Pronađite sve jedinstvene vremenske oznake samo iz datoteka koje je moguće urediti $cleanableFiles = Get-ChildItem $OutputPath -Datoteka -EA SilentlyContinue | Where-Object { $f = $_. Ime; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Count -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { ako ($_. Name -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Jedinstveno -Opadajući redosled) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object - Preskoči $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs in $oldTimestamps) { foreach ($prefix in $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f in $oldFiles) { $freedBytes += $f.Dužina Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [matematika]::Round($freedBytes / 1MB, 1) Write-Host "Čišćenje zadržavanja: uklonjene $removedFiles datoteke iz starih pokretanja programa $($oldTimestamps.Count), oslobodilo se ${freedMB} MB (zadržavanje poslednjeg $retentionCount + svih rezimea/NotUptodate CSV datoteka)" - Boja prednjeg plana Tamnagray } Write-Host "'n$("=" * 60)" -Prednji planColor Cijan Write-Host "STRIMOVANJE AGREGACIJE DOVRŠENO" - Boja prednjeg plana – zelena Write-Host ("=" * 60) -Boja prednjeg plana– cijan Write-Host " Ukupan broj uređaja: $($c.Total.ToString("N0"))" -Boja prednjeg plana Bela Write-Host " NIJE AŽURIRANO: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -Prednji planColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" }) Write-Host " Ažurirano: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -Boja prednjeg plana – zelena boja prednjeg plana Write-Host " Sa greškama: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Najveća memorija: ${stPeakMemMB} MB" -Prednji planColor Cijan Write-Host " Vreme: $([matematika]::Round($stTotal/60,1)) min" -Boja prednjeg plana Bela Write-Host " Kontrolna tabla: $htmlPath" - Boja prednjeg plana belo return [PSCustomObject]$stats } #endregion PROTOKA } još { Write-Error "Putanja unosa nije pronađena: $InputPath" izlaz 1 }