Copiați și lipiți acest script eșantion și modificați după cum este necesar pentru mediul dvs.:
<# . SINOPSIS Rezuma datele JSON ale stării de bootare securizată de pe mai multe dispozitive în rapoarte rezumative.
. . DESCRIEREA / Citește fișierele JSON de stare secure boot colectate și generează: - Tablou de bord HTML cu diagrame și filtrare - Rezumat după Nivelul de încredere - Analiza unică a bucketului dispozitivului pentru strategia de testare Acceptă: - Fișiere pe fiecare computer: HOSTNAME_latest.json (recomandat) - Fișier JSON unic Se anulează automat asocierea de către HostName, păstrând cea mai recentă CollectionTime. În mod implicit, include doar dispozitivele cu încredere "Action Req" sau "High" pentru a vă concentra pe recipientele acționabile. Utilizați -IncludeAllConfidenceLevels pentru înlocuire.
. . CALE INTRARE PARAMETRU Cale către fișiere JSON: - Folder: Citește toate fișierele *_latest.json (sau *.json dacă nu există fișiere _latest) - Fișier: citește un singur fișier JSON
. . Parameter OutputPath Calea pentru rapoartele generate (implicit: .\SecureBootReports)
. . EXEMPLU # Agregați din folderul fișierelor per computer (recomandat) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Citește: \\contoso\SecureBootLogs$\*_latest.json
. . EXEMPLU # Locație de ieșire particularizată .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. . EXEMPLU # Includeți doar acțiunile de încredere maximă (comportament implicit) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Excluderi: observare, în pauză, neacceptat
. . EXEMPLU # Includeți toate nivelurile de încredere (înlocuiți filtrul) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. . EXEMPLU # Filtru particularizat de nivel de încredere .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. . EXEMPLU # ENTERPRISE SCALE: Modul incremental - doar fișiere modificate de proces (rulări ulterioare rapide) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Prima rulare: Încărcare completă ~ 2 ore pentru dispozitive 500K # Următoarele rulează: Secunde dacă nu există modificări, minute pentru delta
. . EXEMPLU # Skip HTML if nothing changed (fastest for monitoring) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Dacă nu s-au modificat fișiere de la ultima rulare: ~5 secunde
. . EXEMPLU # Modul doar rezumat - săriți peste tabelele de dispozitive mari (1-2 minute versus 20+ minute) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Generează CSV-uri, dar ignoră tabloul de bord HTML cu tabele de dispozitive complete
. . NOTE Asociați-vă cu Detect-SecureBootCertUpdateStatus.ps1 pentru implementarea la nivel de întreprindere.Consultați GPO-DEPLOYMENT-GUIDE.md pentru ghidul complet de implementare. Comportamentul implicit exclude dispozitivele de observație, în pauză și neacceptate pentru a concentra raportarea doar asupra bucketurilor de dispozitiv acționabile.#>
param( [Parametru(Obligatoriu = $true)] [șir]$InputPath, [Parametru(Obligatoriu = $false)] [string]$OutputPath = ".\SecureBootReports", [Parametru(Obligatoriu = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parametru(Obligatoriu = $false)] [string]$RolloutStatePath, # Path to RolloutState.json to identify InProgress devices [Parametru(Obligatoriu = $false)] [string]$RolloutSummaryPath, # Path to SecureBootRolloutSummary.json from Orchestrator (conține date de proiecție) [Parametru(Obligatoriu = $false)] [string[]]$IncludeConfidenceLevels = @("Acțiune necesară", "Nivel înalt de încredere"), # Includeți numai aceste niveluri de încredere (implicit: numai bucketuri care pot fi acționate) [Parametru(Obligatoriu = $false)] [switch]$IncludeAllConfidenceLevels, # Înlocuire filtru pentru a include toate nivelurile de încredere [Parametru(Obligatoriu = $false)] [comutator]$SkipHistoryTracking, [Parametru(Obligatoriu = $false)] [switch]$IncrementalMode, # Enable delta processing - only load changed files since last run [Parametru(Obligatoriu = $false)] [string]$CachePath, # Path to cache directory (implicit: OutputPath\.cache) [Parametru(Obligatoriu = $false)] [int]$ParallelThreads = 8, # Numărul de fire paralele pentru încărcarea fișierelor (PS7+) [Parametru(Obligatoriu = $false)] [switch]$ForceFullRefresh, # Impuneți reîncărcarea completă chiar și în modul incremental [Parametru(Obligatoriu = $false)] [switch]$SkipReportIfUnchanged, # Ignorare generație HTML/CSV dacă nu s-au modificat fișiere (doar statistici de ieșire) [Parametru(Obligatoriu = $false)] [switch]$SummaryOnly, # Generați doar statistici rezumat (nu există tabele mari de dispozitive) - mult mai rapid [Parametru(Obligatoriu = $false)] [switch]$StreamingMode # Modul eficient din punct de vedere al memoriei: segmente de proces, scriere CSV-uri incremental, păstrarea doar a rezumatelor în memorie )
# Elevare automată la PowerShell 7 dacă este disponibilă (de 6 ori mai rapidă pentru seturile de date mari) dacă ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object - Extindere SursăProprietate dacă ($pwshPath) { Write-Host a detectat "PowerShell $($PSVersionTable.PSVersion) - re-lansarea cu PowerShell 7 pentru procesare mai rapidă..." -Culoare prim plan galbenă # Rebuild argument list from bound parameters $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) pentru a căuta ($key în $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] dacă ($val -este [comutator]) { dacă ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [matrice]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } altfel, { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs ieșire din $LASTEXITCODE } }
$ErrorActionPreference = "Continuare" $timestamp = Get-Date - Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Eșantioane de implementare și monitorizare"
# Notă: Acest script nu are dependențe de alte scripturi. # Pentru set de instrumente complet, descărcați de la: $DownloadUrl -> $DownloadSubPage
instalare #region Write-Host "=" * 60 -Prim planColor Cyan Write-Host "Agregare date de bootare securizată" -Prim-planColor Cyan Write-Host "=" * 60 -Prim planColor Cyan
# Creare director de rezultate dacă (-nu (Test-Cale $OutputPath)) { New-Item -ItemType Directory -Cale $OutputPath -Force | Nul în exterior }
# Încărcare date - acceptă formate CSV (moștenite) și JSON (native) Write-Host "'nÎncărcarea datelor din: $InputPath" -Culoare prim plan galben
# Funcția Helper pentru normalizarea obiectului dispozitivului (handle field name differences) funcție Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON utilizează Hostname, CSV uses HostName) dacă ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force } # Handle Confidence vs ConfidenceLevel (JSON utilizează Confidence, CSV uses ConfidenceLevel) # ConfidenceLevel este numele oficial al câmpului - hartă Încredere în el dacă ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName "ConfidenceLevel" -NotePropertyValue $device. Încredere - Forțare } # Urmăriți starea actualizării prin Event1808Count SAU UEFICA2023Status="Updated" # Acest lucru permite urmărirea numărului de dispozitive din fiecare bucket de încredere care au fost actualizate $event 1808 = 0 dacă ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Eveniment1808Count } $uefiCaUpdated = $false dacă ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Actualizat") { $uefiCaUpdated = $true } dacă ($event 1808 -gt 0 -sau $uefiCaUpdated) { # Marcați ca actualizat pentru logica tablou de bord/implementare - dar NU înlocuiți Nivelul de încredere $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } altfel, { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # ConfidenceLevel clasificare: # - "High Confidence", "Under observation...", "Temporar paused...", "Not Supported..." = use as-is # - Orice altceva (nul, gol, "UpdateType:...", "Necunoscut", "N/A") = se încadrează în Acțiune necesară în contoare # Nu este necesară normalizarea - ramura contorului de redare în flux altceva se ocupă de ea } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON utilizează OEM*, moștenit utilizează WMI_*) dacă ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName "WMI_Manufacturer" -NotePropertyValue $device. OEMManufacturerName -Force } # Handle OEMModelNumber vs WMI_Model dacă ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName "WMI_Model" -NotePropertyValue $device. OEMModelNumber -Force } # Handle FirmwareVersion vs BIOSDescription dacă ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } $device de returnare }
#region procesare incrementală / Gestionare cache # Configurare căi cache dacă (-nu $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Funcții de gestionare a memoriei cache funcție Get-FileManifest { param([șir]$Path) if (Test-Cale $Path) { încercați { $json = Get-Content $Path -Brut | ConvertFrom-Json # Convert PSObject to hashtable (compatibil PS5.1 - PS7 has -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Nume] = $_. Valoare } $ht de returnare } captură { returnează @{} } } returnează @{} }
funcție Save-FileManifest { param([hashtable]$Manifest, [șir]$Path) $dir = Split-Path $Path -Părinte if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Cale $dir -Force | Nul în exterior } $Manifest | ConvertTo-Json -Adâncime 3 -Comprimare | Set-Content $Path - Forțare }
funcție Get-DeviceCache { param([șir]$Path) if (Test-Cale $Path) { încercați { $cacheData = Get-Content $Path -Brut | ConvertFrom-Json Write-Host " Memorie cache de dispozitiv încărcată: dispozitive $($cacheData.Count) " -Prim-planColor DarkGray $cacheData de returnare } captură { Write-Host " Cache deteriorat, se va reconstrui" -Prim-planColor galben returnare @() } } returnare @() }
funcție Save-DeviceCache { param($Devices, [șir]$Path) $dir = Split-Path $Path -Părinte if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Cale $dir -Force | Nul în exterior } # Conversie în matrice și salvare $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Adâncime 10 -Comprimare | Set-Content $Path - Forțare Write-Host " Memorie cache de dispozitiv salvată: dispozitive $($deviceArray.Count) " -Prim-planColor DarkGray }
funcție Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Generați căutarea fără sensibilitate la litere mari și mici din manifest (normalizare la litere mici) $manifestLookup = @{} pentru a căuta ($mk în $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } pentru fiecare ($file din $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalizați calea la litere mici $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Dimensiune = $file. Lungime } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] dacă ($cached. LastWriteTimeUtc -eq $lwt -și $cached. Dimensiune -eq $file. Lungime) { [nul]$unchanged. Adăugare($file) Continua } } [nul]$changed. Adăugare($file) } returnează @{ Modificat = $changed Neschimbat = $unchanged NewManifest = $newManifest } }
# Ultra-rapid de încărcare a fișierelor paralele utilizând procesarea pe loturi funcție Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = Files de lei. Conta # Utilizați seturi de ~1000 de fișiere fiecare pentru un control mai bun al memoriei $batchSize = [matematică]::Min(1000, [matematică]:Ceiling($totalFiles / [matematică]:Max(1, $Threads))) $batches = [System.Collections.Generic.List[obiect]]:new()
pentru ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [matematică]::Min($i + $batchSize, $totalFiles) $batch = Files lei[$i.) ($end-1)] $batches. Adăugare($batch) } Write-Host " ($($batches. Count) loturi de ~$batchSize fiecare)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[obiect]]:new() # Verificați dacă PowerShell 7+ parallel este disponibil $canParallel = $PSVersionTable.PSVersion.Major -ge 7 dacă ($canParallel -și $Threads -gt 1) { # PS7+: Procesarea loturilor în paralel $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[obiect]]:new() pentru a căuta ($file în $batchFiles) { încercați { $content = [System.IO.File]::ReadAllText($file. NumeComplet) | ConvertFrom-Json $batchResults.Add($content) } captură { } } $batchResults.ToArray() } pentru a căuta ($batch în $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } altfel, { # PS5.1 rezervă: Sequential de prelucrare (încă mai rapid pentru <fișiere 10K) foreach ($file in $Files) { încercați { $content = [System.IO.File]::ReadAllText($file. NumeComplet) | ConvertFrom-Json $flatResults.Add($content) } captură { } } } returnează $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Single JSON file dacă ($InputPath -like "*.json") { $jsonContent = Get-Content -Cale $InputPath -Brut | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "S-au încărcat înregistrări $($allDevices.Count) din fișier" } altfel, { Write-Error "Se acceptă numai formatul JSON. Fișierul trebuie să aibă extensia .json." ieșire 1 } } elseif (Cale-test $InputPath -Container PathType) { # Folder - numai JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Preferați *_latest.json fișiere dacă există (modul per computer) $latestJson = $jsonFiles | Where-Object { $_. Nume - cum ar fi "*_latest.json" } dacă ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count dacă ($totalFiles -eq 0) { Write-Error "Nu s-au găsit fișiere JSON în: $InputPath" ieșire 1 } Write-Host "S-au găsit fișiere JSON $totalFiles" -Prim planColor Gri # Funcția Helper pentru a corespunde nivelurilor de încredere (gestionează atât forme scurte, cât și complete) # Definit timpuriu, astfel încât atât StreamingMode, cât și căile normale să îl poată utiliza funcție Test-ConfidenceLevel { param([șir]$Value, [șir]$Match) if ([string]::IsNullOrEmpty($Value)) { returnează $false } comutator ($Match) { "HighConfidence" { returnează $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Acțiune necesară*" -sau $Value -eq "Acțiune necesară") } "TemporarPaused" { returnează $Value -cum ar fi "Temporar în pauză*" } "Neacceptat" { return ($Value -like "Neacceptat*" -sau $Value -eq "Neacceptat") } { return $false } implicit } } #region MODUL DE REDARE ÎN FLUX - procesare eficientă a memoriei pentru seturi mari de date # Utilizați întotdeauna StreamingMode pentru procesarea eficientă a memoriei și tabloul de bord în stil nou dacă (-nu $StreamingMode) { Write-Host "Activare automată StreamingMode (tablou de bord în stil nou)" -Prim planColor galben $StreamingMode = $true dacă (-nu $IncrementalMode) { $IncrementalMode = $true } } # Atunci când este activat StreamingMode, procesați fișierele în bucăți menținând doar contoare în memorie.# Datele la nivel de dispozitiv sunt scrise în fișiere JSON per bloc pentru încărcarea la cerere în tabloul de bord.# Utilizare memorie: ~1,5 GB indiferent de dimensiunea setului de date (versus 10-20 GB fără redare în flux).dacă ($StreamingMode) { Write-Host "STREAMING MODE enabled - memory-efficient processing" -ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTAL CHECK: Dacă nu s-au modificat fișiere de la ultima rulare, omiteți procesarea în întregime dacă ($IncrementalMode -și -nu $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" if (Test-Cale $stManifestPath) { Write-Host "Se verifică modificările de la ultima rulare în flux..." -Prim-planColor Cyan $stOldManifest = Get-FileManifest - $stManifestPath cale dacă ($stOldManifest.Count -gt 0) { $stChanged = $false # Verificare rapidă: același număr de fișiere? dacă ($stOldManifest.Count -eq $totalFiles) { # Verificați cele mai noi 100 de fișiere (sortate după LastWriteTime descendent) # Dacă un fișier s-a modificat, acesta va avea cea mai recentă marcă de timp și va apărea primul $sampleSize = [matematică]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Descendent | Select-Object - primul $sampleSize foreach ($sf în $sampleFiles) { $sfKey = $sf. NumeComplet.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Pauză } # Compare timestamps - memorate în cache pot fi DateTime sau șir după JSON roundtrip $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc încercați { # Dacă memorate în cache este deja DateTime (ConvertFrom-Json auto-converts), utilizați direct dacă ($cachedLWT -este [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } altfel, { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematică]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Pauză } } captură { $stChanged = $true Pauză } } } altfel, { $stChanged = $true } dacă (-nu $stChanged) { # Verificați dacă există fișiere de ieșire $stSummaryExists = Get-ChildItem ($OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -Primul 1 $stDashExists = Get-ChildItem ($OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -Primul 1 dacă ($stSummaryExists și $stDashExists) { Write-Host " Nu s-au detectat modificări ($totalFiles fișiere neschimbate) - se ignoră procesarea" -Prim-planColor verde Write-Host " Ultimul tablou de bord: $($stDashExists.FullName)" -Prim-planColor Alb $cachedStats = Get-Content $stSummaryExists.NumeComplet | ConvertFrom-Csv Write-Host " Dispozitive: $($cachedStats.TotalDevices) | Actualizat: $($cachedStats.Updated) | Erori: $($cachedStats.WithErrors)" -Prim-planColor gri Write-Host " Terminat în $([matematică]::Round($streamSw.Elapsed.TotalSeconds, 1))s (nu este necesară nicio procesare)" -Prim-planColor Verde $cachedStats de returnare } } altfel, { # DELTA PATCH: Aflați exact ce fișiere s-au modificat Write-Host " Modificări detectate - se identifică fișierele modificate..." -Prim-planColor galben $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() pentru a căuta ($jf în $jsonFiles) { $jfKey = $jf. NumeComplet.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [nul]$newFiles.Add($jf) } altfel, { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc încercați { $cachedDT = dacă ($cachedLWT -este [DateTime]) { $cachedLWT.ToUniversalTime() } altfel{ [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematică]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [nud]$changedFiles.Add($jf) } } captură { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [matematică]:Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Modificat: $($changedFiles.Count) | Nou: $($newFiles.Count) | Total: $totalChanged ($changePct%)" -Culoare prim plan galben dacă ($totalChanged -gt 0 și $changePct -lt 10) { # DELTA PATCH MODE: <10% modificat, patch-uri de date existente Write-Host " Modul de corecție Delta ($changePct% < 10%) - se corectează fișierele $totalChanged..." -Prim planColor Verde $dataDir = Join-Path $OutputPath "date" # Încărcați înregistrările modificate/noi de dispozitiv $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) pentru a căuta ($df în $allDeltaFiles) { încercați { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData dacă ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } captură { } } Write-Host " S-au încărcat înregistrările de dispozitiv $($deltaDevices.Count) modificate" -Prim-planColor gri # Pentru fiecare categorie JSON: eliminați intrările vechi pentru numele de gazdă modificate, adăugați intrări noi $categoryFiles = @("erori", "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 în $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } pentru fiecare ($cat din $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" if (Test-Path $catPath) { încercați { $catData = Get-Content $catPath -Brut | ConvertFrom-Json # Eliminați intrările vechi pentru numele de gazdă modificate $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. Nume Gazdă) }) # Re clasificați fiecare dispozitiv modificat în categorii # (vor fi adăugate mai jos după clasificare) $catData | ConvertTo-Json -Adâncime 5 | Set-Content $catPath -codificare UTF8 } captură { } } } # Clasificați fiecare dispozitiv modificat și adăugați la fișierele categoriei potrivite foreach ($dev în $deltaDevices.Values) { $slim = [ordonat]@{ HostName = $dev. Hostname WMI_Manufacturer = dacă ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } altceva { "" } WMI_Model = dacă ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } altceva { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } altfel { "" } ConfidenceLevel = dacă ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. IsUpdated UEFICA2023Eroare = dacă ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } altceva { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } altceva { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } else { $null } SkipReasonKnownIssue = dacă ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = dacă ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-nu [șir]::IsNullOrEmpty($dev. UEFICA2023Error) -și $dev. UEFICA2023Error -ne "0" -și $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -sau $dev. SecureBootTaskStatus -eq "Dezactivat" -sau $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -și "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = dacă ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Eveniment1801Count } altceva { 0 } $e 1808 = dacă ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Eveniment1808Count } altceva { 0 } $e 1803 = dacă ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Eveniment1803Count } altfel { 0 } $mKEK = ($e 1803 -gt 0 -sau $dev. MissingKEK -eq $true) $hKI = ((nu [șir]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -sau (-nu [șir]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = dacă ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" } # Adăugare la fișierele categorie care se potrivesc $targets = @() dacă ($isUpd) { $targets += "updated_devices" } dacă ($hasErr) { $targets += "erori" } dacă ($hKI) { $targets += "known_issues" } dacă ($mKEK) { $targets += "missing_kek" } dacă (-nu $isUpd -și $sbOn) { $targets += "not_updated" } dacă ($tskDis) { $targets += "task_disabled" } dacă (-nu $isUpd -și ($tskDis -sau (Test-ConfidenceLevel $conf 'TemporarPaused')) { $targets += "temp_failures" } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" } dacă (-nu $isUpd -și (Test-ConfidenceLevel $conf "ActionRequired")) { $targets += "action_required" } dacă (-nu $sbOn) { $targets += "secureboot_off" } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } pentru a căuta ($tgt în $targets) { $tgtPath = Join-Path $dataDir "$tgt.json" if (Test-Cale $tgtPath) { $existing = Get-Content $tgtPath -Brut | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Adâncime 5 | Set-Content $tgtPath -codificare UTF8 } } } # Regenerare CSV-uri din JSON-uri corectate Write-Host " Regenerarea CSV-urilor din datele corectate..." -Prim-planColor gri $newTimestamp = Get-Date - Format "yyyyMMdd-HHmmss" pentru a căuta ($cat în $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (Test-Path $catJsonPath) { încercați { $catJsonData = Get-Content $catJsonPath -Brut | ConvertFrom-Json dacă ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -codificare UTF8 } } captură { } } } # Recount stats from the patched JSON files Write-Host " Recalculare rezumat din datele corectate..." -Prim planColor gri $patchedStats = [ordonat]@{ 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 în $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } captură { } } comutator ($cat) { "updated_devices" { $pUpdated = $cnt } "erori" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # calculat "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). Conta $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Corecție delta terminată: $totalChanged dispozitive actualizate" -Prim planColor Verde Write-Host " Total: $pTotal | Actualizat: $pUpdated | Neacceptat: $pNotUpdated | Erori: $pErrors" - Culoare alb prim plan # Actualizare manifest $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf în $jsonFiles) { $stNewManifest[$jf. NumeComplet.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o";); Dimensiune = $jf. Lungime } } $stManifestPath cale Save-FileManifest -Manifest $stNewManifest Write-Host " Terminat în $([matematică]::Round($streamSw.Elapsed.TotalSeconds, 1))s (corecție delta - dispozitive $totalChanged)" -Prim-planColor verde # Fall through to full streaming reprocess to regenerate HTML dashboard # Fișierele de date sunt corectate deja, astfel încât acest lucru asigură că tabloul de bord rămâne la zi Write-Host " Se regenerează tabloul de bord din datele corectate..." -Culoare prim plan galben } altfel, { Write-Host " $changePct% fișiere modificate (>= 10%) - sunt necesare reprocesări complete de redare în flux" -Culoare prim plan galben } } } } } # Creați subdirector de date pentru fișierele JSON ale dispozitivului la cerere $dataDir = Join-Path $OutputPath "date" dacă (-nu (test-cale $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Deduplication prin HashSet (O(1) per căutare, ~ 50 MB pentru 600K hostnames) $seenHostnames = [System.Collections.Generic.HashSet[string]]:new([System.StringComparer]::OrdinalIgnoreCase) # Contoare de rezumat ușoare (înlocuiește $allDevices + $uniqueDevices în memorie) $c = @{ Total = 0; SBEnabled = 0; SBOff = 0 Actualizat = 0; HighConf = 0; Suboburi = 0; ActionReq = 0; TempPaused = 0; Neacceptat = 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 Actualizare în așteptare = 0 } # Urmărire bucket pentru AtRisk/SafeList (seturi ușoare) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]:new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]:new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Batch-mode device data files: acumulează pe bucată, goli la limitele blocului $stDeviceFiles = @("erori", "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 în $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim device record for JSON output (only essential fields, ~200 bytes vs ~2KB full) funcție Get-SlimDevice { param($Dev) returnare [comandat]@{ HostName = $Dev.HostName WMI_Manufacturer = dacă ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } altceva { "" } WMI_Model = dacă ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" } BucketId = dacă ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = dacă ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null } SecureBootTaskStatus = dacă ($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 } } } # Goliți lotul la fișierul JSON (modul de adăugare) funcție Flush-DeviceBatch { param([șir]$StreamName, [System.Collections.Generic.List[obiect]]$Batch) dacă ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() pentru fiecare ($fDev din $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) } # BUCLĂ DE REDARE ÎN FLUX PRINCIPALĂ $stChunkSize = dacă ($totalFiles -le 10000) { $totalFiles } altfel { 10000 } $stTotalChunks = [matematică]::Ceiling($totalFiles / $stChunkSize) $stPeakMemMB = 0 dacă ($stTotalChunks -gt 1) { Write-Host "Se procesează fișierele $totalFiles în bucăți $stTotalChunks de $stChunkSize (redare în flux, $ParallelThreads fire):" -Prim-planColor Cyan } altfel, { Write-Host "Se procesează fișierele $totalFiles (redare în flux, $ParallelThreads fire):" -Prim-planColor Cyan } pentru ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [matematică]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.) $cEnd] dacă ($stTotalChunks -gt 1) { Write-Host " Bloc $($ci + 1)/$stTotalChunks fișiere ($($cFiles.Count): " -NoNewline -Prim-planColor gri } altfel, { Write-Host " Se încarcă fișierele $($cFiles.Count): " -NoNewline -Prim-planColor gri } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Fire $ParallelThreads # Liste pe fiecare bucată de lot $cBatches = @{} foreach ($df în $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]:new() } $cNew = 0; $cDupe = 0 pentru a căuta ($raw în $rawDevices) { dacă (-nu $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Hostname dacă (-nu $hostname) { continue } dacă ($seenHostnames.Contains($hostname)) { $cDupe++; continue } [nul]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False") dacă ($sbOn) { $c.SBEnabled++ } altceva { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = dacă ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } altcineva { "" } $hasErr = (-nu [șir]::IsNullOrEmpty($device. UEFICA2023Error) -și "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -sau "$($device. SecureBootTaskStatus)" -eq "NotFound") $tskNF = ("$($device. SecureBootTaskStatus)" -eq "NotFound") $bid = dacă ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } altfel { "" } $e 1808 = dacă ($device. PSObject.Properties['Event1808Count']) { [int]$device. Eveniment1808Count } altceva { 0 } $e 1801 = dacă ($device. PSObject.Properties['Event1801Count']) { [int]$device. Eveniment1801Count } altceva { 0 } $e 1803 = dacă ($device. PSObject.Properties['Event1803Count']) { [int]$device. Eveniment1803Count } altfel { 0 } $mKEK = ($e 1803 -gt 0 -sau $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True") $hKI = ((nu [șir]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -sau (-nu [șir]::IsNullOrEmpty($device. KnownIssueId))) $rStat = dacă ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" } $mfr = dacă ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } altceva { "Necunoscut" } $bid = dacă (-nu [șir]::IsNullOrEmpty($bid)) { $bid } altfel{ "" } # Semnalizare în așteptare actualizare pre-calculare (politică/WinCS aplicată, stare neactivată încă, SB ON, activitate neactivată) $uefiStatus = dacă ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } altcineva { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy și "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied -eq $true) $statusPending = ([șir]::IsNullOrEmpty($uefiStatus) -sau $uefiStatus -eq 'NotStarted' -sau $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -sau $hasWinCS) - și $statusPending -și -not $isUpd -și $sbOn -și -not $tskDis) dacă ($isUpd) { $c.Updated++; [nul]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Urmăriți dispozitivele actualizate care necesită repornire (UEFICA2023Status=Actualizat, dar Evenimentul1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-nu $sbOn) { # SecureBoot OFF - în afara domeniului de aplicare, nu clasifica de încredere } altfel, { if ($isUpdatePending) { } # Counted separat in Update Pending — mutual exclusive for pie chart elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarPaused") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "Neacceptat") { $c.NotSupported++ } altfel, { $c.ActionReq++ } if ([string]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } if ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } if ($tskNF) { $c.TaskNotFound++ } if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ } dacă ($hasErr) { $c.WithErrors++; [nul]$stFailedBuckets.Add($bid); $cBatches["erori"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Eroare if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } dacă ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = dacă (-nu [șir]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } altceva, { $device. KnownIssueId } dacă (-nu $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarPaused')) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ } if ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ } # Actualizare în așteptare: politică sau WinCS aplicat, stare în așteptare, SB ACTIVAT, activitate ne dezactivată dacă ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } dacă (-nu $isUpd -și $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # Sub Dispozitive de observație (separat de Acțiune necesară) dacă (-nu $isUpd -și (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Acțiune obligatoriu: ne-actualizat, SB ACTIVAT, care nu se potrivește cu alte categorii de încredere, nu Actualizare în așteptare dacă (-nu $isUpd -și $sbOn -și -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } dacă (-nu $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Actualizat=0; Actualizare în așteptare=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; CuErrors=0 } } $stMfrCounts[$mfr]. Total++ dacă ($isUpd) { $stMfrCounts[$mfr]. Actualizat++ } elseif (-nu $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. Actualizare în așteptare++ } elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ } elseif (Test-ConfidenceLevel $conf "SubObservare") { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarPaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "Neacceptat") { $stMfrCounts[$mfr]. NotSupported++ } altfel, { $stMfrCounts[$mfr]. ActionReq++ } dacă ($hasErr) { $stMfrCounts[$mfr]. CuErrors++ } # Urmăriți toate dispozitivele în funcție de bucket (inclusiv BucketId gol) $bucketKey = dacă ($bid -și $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Actualizat=0; Manufacturer=$mfr; Model=""; BIOS="" } dacă ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } dacă ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ dacă ($isUpd) { $stAllBuckets[$bucketKey]. Actualizat++ } } # Goliți loturile pe disc foreach ($df în $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Matematică]:Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = dacă ($cRem -gt 0) { " | ETA: ~$([Matematică]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" } $cMem = [matematică]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) dacă ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew forme noi, $cDupe, ${cTime}s | Mem: ${cMem}MB$cEta" -Prim planColor Verde } # Finalizare matrice JSON foreach ($dfName în $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: dispozitive $($stDeviceFileCounts[$dfName]) " -Prim-planColor DarkGray } # Statistici derivate $stAtRisk = 0; $stSafeList = 0 pentru a căuta ($bid în $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Actualizat dacă ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [matematică]:Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = numărul din not_updated lot (dispozitive cu SB ACTIVAT și neactualizate) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [ordonat]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Actualizat = $c.Updated; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; TemporarPaused = $c.TempPaused; NotSupported = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificateUpdated = $c.Updated; NotUptodate = $stNotUptodate; CompletUpdated = $c.Updated Actualizări în așteptare = $stNotUptodate; UpdatesComplete = $c.Updated WithErrors = $c.WithErrors; InProgress = $c.InProgress; NotYetInitiated = $c.NotYetInitiated RolloutInProgress = $c.RolloutInProgress; WithKnownIssues = $c.WithKnownIssues WithMissingKEK = $c.WithMissingKEK; TemporaryFailures = $c.TempFailures; PermanentFailures = $c.PermFailures NeedsReboot = $c.NeedsReboot; Actualizare în așteptare = $c.UpdatePending AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList PercentWithErrors = dacă ($c.Total -gt 0) { [matematică]:Round(($c.WithErrors/$c.Total)*100,2) } altfel { 0 } PercentAtRisk = dacă ($c.Total -gt 0) { [matematică]:Round(($stAtRisk/$c.Total)*100,2) } altceva { 0 } PercentSafeList = dacă ($c.Total -gt 0) { [matematică]:Round(($stSafeList/$c.Total)*100,2) } altceva { 0 } PercentHighConfidence = dacă ($c.Total -gt 0) { [matematică]:Round(($c.HighConf/$c.Total)*100,1) } altfel { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]:Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = dacă ($c.Total -gt 0) { [matematică]:Round(($c.ActionReq/$c.Total)*100,1) } altceva { 0 } PercentNotUptodate = dacă ($c.Total -gt 0) { [matematică]:Round($stNotUptodate/$c.Total*100;1) } altceva { 0 } PercentFullyUpdated = dacă ($c.Total -gt 0) { [matematică]:Round(($c.Updated/$c.Total)*100,1) } altceva { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Redare în flux" } # Scrie CSVs [PSCustomObject]$stats | Export-Csv -path ($OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -codificare UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Cheie; Contor=$_. Valoare.Total; Actualizat=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (cale de asociere $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -codificare UTF8 $stErrorCodeCounts.GetEnumerator() | valoare Sort-Object - descendentă | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Cheie; Contor=$_. Valoarea; SampleDevices=($stErrorCodeSamples[$_. Cheie] -unire ", ") } } | Export-Csv -Path ($OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -codificare UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Descendent | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Cheie; Contor=$_. Value.Count; Actualizat=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Producător=$_. Value.Manufacturer } } | Export-Csv -Path ($OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -codificare UTF8 # Generați CSV-uri compatibile cu orchestratorii (nume de fișiere așteptate pentru Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (Test-Cale $notUpdatedJsonPath) { încercați { $nuData = Get-Content $notUpdatedJsonPath -Brut | ConvertFrom-Json dacă ($nuData.Count -gt 0) { # NotUptodate CSV - orchestrator căutări pentru *NotUptodate*.csv $nuData | Export-Csv -Path (cale de asociere $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -codificare UTF8 Write-Host "Orchestrator CSV: dispozitive SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count)" -Prim planColor Gray } } captură { } } # Scrieți date JSON pentru tabloul de bord $stats | ConvertTo-Json -Adâncime 3 | Set-Content ($dataDir de asociere "summary.json") - codificare UTF8 # ISTORIC TRACKING: Save data point for trend chart # Utilizați o locație de cache stabilă, astfel încât datele tendințelor să persiste în folderele de agregare temporală. # Dacă OutputPath arată ca "...\Aggregation_yyyyMMdd_HHmmss", memoria cache ajunge în folderul părinte.# În caz contrar, memoria cache intră chiar în OutputPath.$parentDir = Split-Path $OutputPath -Părinte $leafName = frunză Split-Path $OutputPath dacă ($leafName -match '^Aggregation_\d{8}' -sau $leafName -eq 'Aggregation_Current') { # Orchestrator creat timestamped folder - utilizarea părinte pentru memoria cache stabilă $historyPath = Join-Path $parentDir ".cache\trend_history.json" } altfel, { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Părinte if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() if (Test-Cale $historyPath) { încercați { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } captură { $historyData = @() } } # De asemenea, verificați în Interiorul OutputPath\.cache\ (locație moștenită din versiuni mai vechi) # Îmbinați punctele de date care nu se află deja în istoricul principal dacă ($leafName -eq 'Aggregation_Current' sau $leafName -match '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { încercați { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Data }) foreach ($entry în $innerData) { dacă ($entry. Data și $entry. Data -notin $existingDates) { $historyData += $entry } } dacă ($innerData.Count -gt 0) { Write-Host " Puncte de date $($innerData.Count) îmbinate din memoria cache internă" -Prim-planColor DarkGray } } captură { } } }
# BOOTSTRAP: Dacă istoricul tendințelor este gol/rare, reconstruiți din datele istorice dacă ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -sau $leafName -eq 'Aggregation_Current')) { Write-Host " Bootstrapping trend history from historical data..." -ForegroundColor Yellow $dailyData = @{} # Sursa 1: Rezumat CSV-uri în interiorul folderului curent (Aggregation_Current păstrează toate CSV-uri rezumat) $localSummaries = Get-ChildItem $OutputPath -Filtru "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Nume Sort-Object pentru a căuta ($summCsv în $localSummaries) { încercați { $summ = Import-Csv $summCsv.NumeComplet | Select-Object -Primul 1 dacă ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -și $summ. ReportGeneratedAt) { $dateStr = ([dată-oră]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = dacă ($summ. Actualizat) { [int]$summ. S-a actualizat } altcineva { 0 } $notUpd = dacă ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Data = $dateStr; Total = [int]$summ. Dispozitive Total; Actualizat = $updated; Neacceptat = $notUpd NeedsReboot = 0; Erori = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } altceva { 0 } } } } captură { } } # Sursa 2: Timestamped vechi Aggregation_ * foldere (legacy, în cazul în care încă mai există) $aggFolders = Get-ChildItem $parentDir -Director - Filtru "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Nume -potrivire '^Aggregation_\d{8}' } | Nume Sort-Object pentru a căuta ($folder în $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filtru "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -Primul 1 dacă ($summCsv) { încercați { $summ = Import-Csv $summCsv.NumeComplet | Select-Object -Primul 1 dacă ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Nume -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = dacă ($summ. Actualizat) { [int]$summ. S-a actualizat } altcineva { 0 } $notUpd = dacă ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Data = $dateStr; Total = [int]$summ. Dispozitive Total; Actualizat = $updated; NotUpdated = $notUpd NeedsReboot = 0; Erori = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } altceva { 0 } } } } captură { } } } # Sursa 3: RolloutState.json WaveHistory (are pe val timestamps din ziua 1) # Acest lucru oferă puncte de date de referință chiar și atunci când nu există foldere de agregare vechi $rolloutStatePaths = @( (Join-Path $parentDir "RolloutState\RolloutState.json"), (Cale de asociere $OutputPath "RolloutState\RolloutState.json") ) pentru a căuta ($rsPath în $rolloutStatePaths) { if (Test-Path $rsPath) { încercați { $rsData = Get-Content $rsPath -Brut | ConvertFrom-Json if ($rsData.WaveHistory) { # Utilizați datele de început ale valului ca puncte de date de tendință # Calculați dispozitivele cumulative orientate spre fiecare val $cumulativeTargeted = 0 foreach ($wave în $rsData.WaveHistory) { dacă ($wave. StartedAt -și $wave. DeviceCount) { $waveDate = ([dată-oră]$wave. StartedAt). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. Cont dispozitiv if (-not $dailyData.ContainsKey($waveDate)) { # Aproximative: la ora de începere a valului, numai dispozitivele din undele anterioare au fost actualizate $dailyData[$waveDate] = [PSCustomObject]@{ Data = $waveDate; Total = $c.Total; Actualizat = [matematic]:Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NotUpdated = $c.Total - [matematică]:Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0; Erori = 0; AcțiuneObligatoriu = 0 } } } } } } captură { } break # Utilizați primul găsit } }
dacă ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object cheie | ForEach-Object { $_. Valoare }) Write-Host punctele de date "Bootstrapped $($historyData.Count) din rezumatele istorice" -ForegroundColor Green } }
# Adăugați punctul de date curent (deduplicați după zi - păstrați cele mai recente date pe zi) $todayKey = (Get-Date). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Dată)" - cum ar fi "$todayKey*" } dacă ($existingToday) { # Înlocuiți intrarea de astăzi $historyData = @($historyData | Where-Object { "$($_. Dată)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Dată = $todayKey Total = $c.Total Actualizat = $c.Updated NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Erori = $c.WithErrors ActionRequired = $c.ActionReq } # Eliminați punctele de date eronate (0 în total) și păstrați ultimele 90 $historyData = @($historyData | Where-Object { [int]$_. Total -gt 0 }) # Fără capac - datele tendinței sunt ~100 byți/intrare, un an întreg = ~36 KB $historyData | ConvertTo-Json -Adâncime 3 | Set-Content $historyPath -codificare UTF8 Write-Host " Istoric tendințe: puncte de date $($historyData.Count) " -Prim planColor ÎntunecatGray # Generare date diagramă de tendință pentru HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Dată)'" }) -asociere "," $trendUpdated = ($historyData | ForEach-Object { $_. Actualizat }) -join "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Total }) -asociere "," # Proiectare: extinde linia de tendință utilizând dublarea exponențială (2,4,8,16...) # Derivă dimensiunea valului și perioada de observație din datele reale ale istoricului tendințelor.# - Wave dimensiunea = cea mai mare creștere o singură perioadă văzută în istorie (cel mai recent val implementat) # - Zile de observare = medie zile calendaristice între punctele de date de tendință (cât de des rulăm) # Apoi dublează dimensiunea undelor în fiecare perioadă, potrivind strategia de creștere 2x a orchestratorului.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false dacă ($historyData.Count -ge 2) { $lastUpdated = $c.Actualizat $remaining = $stNotUptodate # Numai dispozitivele ne actualizate SB-ON (exclude SecureBoot OFF) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Derivați dimensiunea valului și perioada de observație din istoricul tendințelor $increments = @() $dayGaps = @() pentru ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Actualizat - $historyData[$hi-1]. Actualizat dacă ($inc -gt 0) { $increments += $inc } încercați { $d 1 = [dată-oră]::P arse($historyData[$hi-1]. Data) $d 2 = [dată-oră]::P arse($historyData[$hi]. Data) $gap = ($d 2 - $d 1). TotalDays dacă ($gap -gt 0) { $dayGaps += $gap } } captură {} } # Dimensiune val = cea mai recentă incrementare pozitivă (val curent), revenire la medie, minim 2 $waveSize = dacă ($increments. Count -gt 0) { [matematică]:Max(2, $increments[-1]) } altfel , { 2 } # Perioada de observație = diferența medie dintre punctele de date (zile calendaristice pe undă), minim 1 $waveDays = dacă ($dayGaps.Count -gt 0) { [matematică]::Max(1, [matematică]:Round(($dayGaps | Measure-Object -Average). Medie, 0)) } altfel , { 1 }
Write-Host " Proiecție: waveSize=$waveSize (de la ultima incrementare), waveDays=$waveDays (spațiu liber mediu din istorie)" -Prim-planColor DarkGray
$dayCounter = 0 # Project până când toate dispozitivele sunt actualizate sau maximum 365 de zile pentru ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # La fiecare limită a perioadei de observație, implementați un val apoi dublu dacă ($dayCounter -ge $waveDays) { $devicesThisWave = [matematică]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Dimensiune undă dublă pentru perioada următoare (orchestrator strategie 2x) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd")")'" $projValues += $lastUpdated $projNotUpdValues += [matematică]:Max(0, $remaining) dacă ($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 " Proiecție: trebuie cel puțin 2 puncte de date de tendință pentru a deriva temporizarea valului" -Prim-planColor DarkGray } # Construirea șirurilor de date de diagramă combinate pentru șirul de aici $allChartLabels = dacă ($hasProjection) { "$trendLabels,$projLabels" } altceva { $trendLabels } $projDataJS = dacă ($hasProjection) { $projUpdated } altfel { "" } $projNotUpdJS = dacă ($hasProjection) { $projNotUpdated } altfel { "" } $histCount = ($historyData | Measure-Object). Conta $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | ForEach-Object { @{ name=$_. Cheie; total=$_. Valoare.Total; actualizat=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Adâncime 3 | Set-Content ($dataDir "manufacturers.json de asociere") - codificare UTF8 # Convertiți fișierele de date JSON în CSV pentru descărcări Excel care pot fi citite de oameni Write-Host "Conversia datelor dispozitivului în CSV pentru descărcare Excel..." -Prim planColor Gri pentru a căuta ($dfName în $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" if (Test-Path $jsonFile) { încercați { $jsonData = Get-Content $jsonFile -Brut | ConvertFrom-Json dacă ($jsonData.Count -gt 0) { # Includeți coloane suplimentare pentru update_pending CSV $selectProps = dacă ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } altfel, { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv -Path $csvFile -NoTypeInformation -codificare UTF8 Write-Host " $dfName -> rânduri $($jsonData.Count) -> CSV" -Prim-planColor DarkGray } } captură { Write-Host " $dfName - ignorat" -Prim-planColor DarkYellow } } } # Generați tabloul de bord HTML auto-conținut $htmlPath = Join-Path $OutputPath "timestamp.html SecureBoot_Dashboard_$ Write-Host "Se generează tabloul de bord HTML auto-conținut..." -Prim-planColor galben # VELOCITY PROJECTION: Calculați din istoricul de scanare sau rezumatul anterior $stDeadline = [datetime]"2026-06-24" # KEK cert expiră $stDaysToDeadline = [matematică]:Max(0; ($stDeadline - (Get-Date)). Zile) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "N/A" $stWorkingDays = 0 $stCalendarDays = 0 # Încercați mai întâi istoricul tendințelor (ușor, întreținut deja de agregator - înlocuiește ScanHistory.json umflate) dacă ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Actualizat -ge 0 }) dacă ($validHistory.Count -ge 2) { $prev = $validHistory[-2]; $curr = $validHistory[-1] $prevDate = [dată-oră]::P arse($prev. Date.Substring(0, [Matematică]:Min(10, $prev. Date.Length))) $currDate = [dată-oră]::P arse($curr. Date.Substring(0, [Matematică]:Min(10, $curr. Date.Length))) $daysDiff = ($currDate - $prevDate). TotalDays dacă ($daysDiff -gt 0) { $updDiff = [int]$curr. Actualizat - [int]$prev. Actualizat dacă ($updDiff -gt 0) { $stDevicesPerDay = [matematică]:Round($updDiff / $daysDiff, 0) $stVelocitySource = "TrendHistory" } } } } # Încercați rezumatul lansării orchestratorului (are o viteză pre-calculată) if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { încercați { $rolloutSummary = Get-Content $RolloutSummaryPath -Brut | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [matematică]::Round([dublu]$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 } } } captură { } } # Rezervă: încercați rezumatul anterior CSV (căutare folderul curent ȘI folderele de agregare părinte/frate) dacă ($stVelocitySource -eq "N/A") { $searchPaths = @( (Cale de asociere $OutputPath "SecureBoot_Summary_*.csv") ) # De asemenea, căutare foldere de agregare frate (orchestrator creează folder nou la fiecare rulare) $parentPath = Split-Path $OutputPath -Părinte dacă ($parentPath) { $searchPaths += ($parentPath cale de asociere "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += ($parentPath "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descendent | Select-Object -Primul 1 dacă ($prevSummary) { încercați { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) - $prevDate). TotalDays dacă ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Actualizat - $prevUpdated dacă ($updDelta -gt 0) { $stDevicesPerDay = [matematică]:Round($updDelta / $daysSinceLast, 0) $stVelocitySource = "PreviousReport" } } } captură { } } } # Rezervă: calcula viteza de la span istoricul de tendință complet (primul vs cel mai recent punct de date) if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Actualizat -ge 0 }) if ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [dată-oră]::P arse($first. Date.Substring(0, [Matematică]::Min(10, $first. Date.Length))) $lastDate = [dată-oră]::P arse($last. Date.Substring(0, [Matematică]:Min(10, $last. Date.Length))) $daysDiff = ($lastDate - $firstDate). TotalDays dacă ($daysDiff -gt 0) { $updDiff = [int]$last. Actualizat - [int]$first. Actualizat dacă ($updDiff -gt 0) { $stDevicesPerDay = [matematică]:Round($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # Calculați proiecția utilizând dublarea exponențială (în concordanță cu diagrama de tendințe) # Reutilizați datele de proiecție calculate deja pentru diagramă, dacă sunt disponibile dacă ($hasProjection -și $projDates.Count -gt 0) { # Utilizați ultima dată proiectată (când toate dispozitivele sunt actualizate) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([dată-oră]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy") $stCalendarDays = ([dată-oră]::P arse($lastProjDateStr) - (Get-Date)). Zile $stWorkingDays = 0 $d = Get-Date pentru ($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) { # Rezervă: proiectarea liniară dacă nu există date exponențiale disponibile $daysNeeded = [matematică]::Ceiling($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Get-Date pentru ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Build velocity HTML $velocityHtml = dacă ($stDevicesPerDay -gt 0) { "<div><>🚀 Dispozitive/Zi:</strong> $($stDevicesPerDay.ToString('N0')) (sursă: $stVelocitySource)</div>" + "<div><>📅 puternic; Finalizare proiectată:</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><>🕐 puternic; Zile lucrătoare:</strong> $stWorkingDays | <zile>Calendar puternice:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Termen limită: 24 iunie 2026 (expiră certificatul KEK) | Zile rămase: $stDaysToDeadline</div>" } altfel, { "<div style='padding:8px; background:#fff3cd; rază bordură:4px; border-left:3px solid #ffc107'>" + "<>📅 puternic; Finalizare proiectată:</strong> Date insuficiente pentru calculul vitezei. " + "Rulați agregarea cel puțin de două ori cu modificările de date pentru a stabili o rată.<br/>" + "<termen>puternic:</strong> 24 iunie 2026 (expiră certificatul KEK) | <zile>puternice rămase:</strong> $stDaysToDeadline</div>" } # Numărătoarea inversă de expirare a certificatului $certToday = Get-Date $certKekExpiry = [dată-oră]"2026-06-24" $certUefiExpiry = [dată-oră]"2026-06-27" $certPcaExpiry = [dată-oră]"2026-10-19" $daysToKek = [matematică]:Max(0; ($certKekExpiry - $certToday). Zile) $daysToUefi = [matematică]:Max(0; ($certUefiExpiry - $certToday). Zile) $daysToPca = [matematică]:Max(0; ($certPcaExpiry - $certToday). Zile) $certUrgency = dacă ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } altceva { '#28a745' } # Ajutor: Citiți înregistrările din JSON, compilarea rezumatului bucketului + primele N rânduri de dispozitiv $maxInlineRows = 200 funcție Build-InlineTable { param([șir]$JsonPath, [int]$MaxRows = 200, [șir]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (Test-Cale $JsonPath) { încercați { $data = Get-Content $JsonPath -Brut | ConvertFrom-Json $totalCount = $data. Conta # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats dacă ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | contor Sort-Object -descendent $bucketSummary = "><2 h3 style='font-size:.95em; culoare:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. Bucketuri count)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:15px'><table><thead><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'><actualizat /th><th style='text-align:right; color:#dc3545'></th><><1 Manufacturer><2 /th></tr></thead><întruchiparea>" foreach ($b în $buckets) { $bid = dacă ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Grup | Select-Object -Primul 1). WMI_Manufacturer # Obțineți numărul actualizat de statistici globale de bucket (toate dispozitivele din acest bucket din întregul set de date) $lookupKey = $bid $globalBucket = dacă ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } altfel, { $null } $bUpdatedGlobal = dacă ($globalBucket) { $globalBucket.Updated } altceva { 0 } $bTotalGlobal = dacă ($globalBucket) { $globalBucket.Count } altceva { $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; culoare:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; culoare:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # DEVICE DETAIL: First N rows as flat list $slice = $data | Select-Object - Primul $MaxRows pentru a căuta ($d în $slice) { $conf = $d.ConfidenceLevel $confBadge = dacă ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge-danger">Not Supported><6 /span>' } elseif ($conf -match "Under") { '<span class="badge-info">Under Obs><0 /span>' } elseif ($conf -match "Paused") { "<span class="badge-warning">Paused><4 /span>' } else { '<span class="badge badge-warning">Action Req><8 /span>' } $statusBadge = dacă ($d.IsUpdated) { '><00 span class="badge-success"><01 actualizat</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge badge-danger"><05 Eroare</span>' } else { '><08 span class="badge badge-warning"><09><0 în așteptare /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{'-'})$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n" } } captură { } } dacă ($totalCount -eq 0) { returnează "><44 div style='padding:20px; culoare:#888; font-style:italic'><45 Niciun dispozitiv din această categorie.><46 /div>" } $showing = [matematică]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Total: dispozitive $($totalCount.ToString("N0")") dacă ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>📄 Descărcați fișierul CSV complet pentru Excel><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; culoare:#333; margin:10px 0 5px'><58 Device Details (showing first $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; overflow-y:auto'><tabel><thead><tr><><0 HostName><1 /th><><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th><th><6 stare><7 .th><><0 eroare><1 /th><><4 BucketId><5 /th></tr></thead><întruchiparea><2 $deviceRows><3 /tbody></table></div>" returnează "$header$bucketSummary$deviceHeader$deviceTable" } # Construiți tabele în linie din fișierele JSON aflate deja pe disc, conectându-vă la CSV-uri $tblErrors = Build-InlineTable -JsonPath ($dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath ($dataDir "known_issues.json de asociere") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath ($dataDir "missing_kek.json pentru unire") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath ($dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath ($dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath ($dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath ($dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath ($dataDir "updated_devices.json de asociere") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath ($dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath ($dataDir de asociere "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath ($dataDir "needs_reboot.json de unire") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath ($dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath ($dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # Tabel particularizat pentru actualizare în așteptare - include coloanele UEFICA2023Status și UEFICA2023Error $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (Test-Cale $upJsonPath) { încercați { $upData = Get-Content $upJsonPath -Brut | ConvertFrom-Json $upCount = $upData.Count dacă ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Total: dispozitive $($upCount.ToString("N0")) | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>📄 Descărcați fișierul CSV complet pentru Excel><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object - Primul $maxInlineRows foreach ($d în $upSlice) { $uefiSt = dacă ($d.UEFICA2023Status) { $d.UEFICA2023Status } altfel, { "<span style="color:#999">null><0 /span>' } $uefiErr = dacă ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' } $policyVal = dacă ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = dacă ($d.WinCSKeyApplied) { '<span class="badge badge-success">Yes><8 /span>' } else { '-' } $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n" } $upShowing = [matematică]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; culoare:#333; margin:10px 0 5px'>Device Details (showing first $upShowing)</h3>" $upTable = "<div style='max-height:500px; overflow-y:auto'><tabel><thead><tr><><9 HostName><0 /th><><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2 023Eroare><6 /th><><9</th><><><BucketId>BucketId</th></tr></thead><body><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } altfel, { $tblUpdatePending = "<div style='padding:20px; culoare:#888; font-style:italic'>Niciun dispozitiv din această categorie.</div>" } } captură { $tblUpdatePending = "<div style='padding:20px; culoare:#888; font-style:italic'>Niciun dispozitiv din această categorie.</div>" } } altfel, { $tblUpdatePending = "<div style='padding:20px; culoare:#888; font-style:italic'>Niciun dispozitiv din această categorie.</div>" } # Numărătoarea inversă de expirare a certificatului $certToday = Get-Date $certKekExpiry = [dată-oră]"2026-06-24" $certUefiExpiry = [dată-oră]"2026-06-27" $certPcaExpiry = [dată-oră]"2026-10-19" $daysToKek = [matematică]:Max(0; ($certKekExpiry - $certToday). Zile) $daysToUefi = [matematică]:Max(0; ($certUefiExpiry - $certToday). Zile) $daysToPca = [matematică]:Max(0; ($certPcaExpiry - $certToday). Zile) $certUrgency = dacă ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } altceva { '#28a745' } # Build manufacturer chart data inline (Top 10 by device count) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | Select-Object -Primul 10 $mfrChartTitle = dacă ($stMfrCounts.Count -le 10) { "De producător" } altceva { "Primii 10 producători" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Cheie)'" }) -asociere "," $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join "," $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join "," $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) - asociere "," $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join "," $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join "," $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) - unire "," $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join "," $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join "," $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join "," # Build manufacturer table $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descendent | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Key)</td><td>$($_. Value.Total.ToString("N0"))</td><td>$($_. Value.Updated.ToString("N0"))</td><td>$($_. Value.HighConf.ToString("N0"))><0 /td><td>$($_. Value.ActionReq.ToString("N0"))><4 /td></tr>'n" } $htmlContent = @" <!> HTML DOCTYPE <html lang="en"> ><3 cap < <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> titlul <><9 tabloul de bord cu starea certificatului de bootare securizat><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 stil < *{box-sizing:border-box; margine:0; spațiere:0} body{font-family:'Segoe UI',Tahoma,sans-serif; background:#f0f2f5; culoare:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); culoare:#fff; padding:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; opacitate:.9} .container{max-width:1400px; margin:0 auto; spațiere:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); spațiu liber:12px; margin:20px 0} .card{background:#fff; rază bordură:10px; spațiere:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; font-weight:700} .card .label{font-size:.8em; culoare:#666; margin-top:4px} .card .pct{font-size:.75em; culoare:#888} .section{background:#fff; rază bordură:10px; spațiere:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; culoare:#1a237e; margin-bottom:10px; cursor:pointer; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; spațiu liber:20px; margin:20px 0} .chart-box{background:#fff; rază bordură:10px; spațiere:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabel{lățime:100%; border-collapse:collapse; font-size:.85em} th{background:#e8eaf6; padding:8px 10px; aliniere text:la stânga; poziție:adeziv; primele:0; index z:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; spațiere:2px 8px;rază-bordură:10px; font-size:.75em; font-weight:700} .badge-success{background:#d4edda; culoare:#155724} .badge-danger{background:#f8d7da; culoare:#721c24} .badge-warning{background:#fff3cd; culoare:#856404} .badge-info{background:#d1ecf1; culoare:#0c5460} .top-link{float:right; font-size:.8em; culoare:#1a237e; text-decoration:none} .footer{text-align:center; spațiere:20px; culoare:#999; font-size:.8em} a{color:#1a237e} ><9 </style ></cap >corp < <div class="header"> <h1>tabloul de bord cu starea certificatului de bootare securizat</h1> <div class="meta">generat: $($stats. ReportGeneratedAt) | Total dispozitive: $($c.Total.ToString("N0")) | Bucketuri unice: $($stAllBuckets.Count)</div><3 ><5 </div <div class="container">
Fișe KPI<!-- - se poate face clic, se poate face clic pe secțiuni --> <div class="cards"> <o clasă="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; top:8px; dreapta:8px; background:#dc3545; culoare:#fff; padding:1px 6px; rază bordură:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString(") N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% - NEEDS ACTION><0 /div></a><3 <o clasă="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; top:8px; dreapta:8px; background:#28a745; culoare:#fff; padding:1px 6px; rază bordură:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString(") N0"))</div><div class="label">Actualizat><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3 <o clasă="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; top:8px; dreapta:8px; background:#6c757d; culoare:#fff; padding:1px 6px; rază bordură:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[math]:Round(($c.SBOff/$c.Total)*100,1)}else{0})% - Out of Scope><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">necesită repornirea><2 /div><clasa div="pct">$(if($c.Total -gt 0){[matematică]:Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% - așteaptă repornirea><6 /div></a><9 <o clasă="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})% - Politica/WinCS aplicată, se așteaptă actualizarea><2 /div></a><5 <o clasă="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Rollout In Progress><4 /div><div class="pct"" >$(if($c.Total -gt 0){[matematică]:Round(($c.RolloutInProgress/$c.Total)*100;1)}else{0})%</div></a><11 <o clasă="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)% - sigur pentru lansarea><24 /div></a><27 <o clasă="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><1div class="pct"><9 $(if($c.Total -gt 0){[matematică]: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)% - trebuie să testați><6 /div></a><9 <o clasă="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)% - Similar cu nereușit><2 /div></a><5 <o clasă="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){[matematică]:Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})% - Blocat><8 /div></a><91 <o clasă="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. În pauză</div><div class="pct">$(if($c.Total -gt 0){[matematică]:Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a> <o clasă="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Known Issues><6 /div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3 <o clasă="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> <o clasă="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)% - erori UEFI</div></a> ><6 o clasă="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. Erori</div><div class="pct">$(if($c.Total -gt 0){[math]:Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a> <o clasă="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){[matematică]:Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
<!-- de expirare a vitezei de implementare & certificat --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; spațiu liber:20px; margin:15px 0"> <div class="section" style="margin:0"> <h2>📅 Viteza de implementare</h2> <div class="section-body open"> <div style="font-size:2.5em; font-weight:700; color:#28a745">$($c.Updated.ToString("N0"))</div> <div style="color:#666">dispozitive actualizate din $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; background:#e8eaf6; înălțime:20px; rază bordură:10px; overflow:hidden"><div style="background:#28a745; înălțime:100%; lățime:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% complet</div> <div style="margin-top:10px; spațiere:10px; background:#f8f9fa; rază bordură:8px; font-size:.85em"> <div><dispozitive puternice>Rămase:</strong> $($stNotUptodate.ToString("N0")) necesită acțiuni</div> <div><dispozitive puternice>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) (erori + activități permanente + dezactivate)</div> <div><dispozitive puternice>Sigur de implementat:</strong> $($stSafeList.ToString("N0")) dispozitive (același bucket ca reușit)</div> $velocityHtml ></div ></div ></div <div class="section" style="margin:0; border-left:4px solid #dc3545"> <h2 style="color:#dc3545">⚠ numărătoarea inversă a expirării certificatului</h2> <div class="section-body open"> <div style="display:flex; spațiu liber:15px; margin-top:10px"> <div style="text-align:center; spațiere:15px; rază bordură:8px; lățime minimă:120px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); border:2px solid #dc3545; flex:1"> <div style="font-size:.65em; culoare:#721c24; transformare text:majuscule; font-weight:bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size:.85em; font-weight:bold; culoare:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; font-weight:700; culoare:#dc3545; line-height:1"><9 $daysToKek</div> ><2 div style="font-size:.8em; color:#721c24"><3 zile (24 iunie 2026)><4 /div> ><6 /div> ><8 div style="text-align:center; spațiere:15px; rază bordură:8px; lățime minimă:120px; background:linear-gradient(135deg,#fffef5,#fff3cd); border:2px solid #ffc107; flex:1"><9 <div style="font-size:.65em; culoare:#856404; transformare text:majuscule; font-weight:bold">UEFI CA 2011</div> <div id="daysUefi" style="font-size:2.2em; font-weight:700; culoare:#856404; înălțime linie:1; margin:5px 0">$daysToUefi</div> <div style="font-size:.8em; color:#856404">zile (27 iunie 2026)</div> ></div <div style="text-align:center; spațiere:15px; rază bordură:8px; lățime minimă:120px; background:linear-gradient(135deg,#f0f8ff,#d4edff); border:2px solid #0078d4; flex:1"> <div style="font-size:.65em; culoare:#0078d4; transformare text:majuscule; font-weight:bold">Windows PCA</div> <div id="daysPca" style="font-size:2.2em; font-weight:700; culoare:#0078d4; înălțime linie:1; margin:5px 0">$daysToPca><2 /div><3 <div style="font-size:.8em; color:#0078d4">zile (19 octombrie 2026)</div><7 ><9 </div ><1 </div <div style="margin-top:15px; spațiere:10px; background:#f8d7da; rază bordură:8px; font-size:.85em; border-left:4px solid #dc3545"> <>⚠ CRITIC:</strong> Toate dispozitivele trebuie actualizate înainte de expirarea certificatului. Dispozitivele care nu au fost actualizate până la termenul limită nu pot aplica actualizări de securitate viitoare pentru Manager de bootare și Bootare sigură după expirare.></div ></div ></div > </div
diagrame<!-- --> <div class="charts"> <div class="chart-box"><h3>Deployment Status</h3><canvas id="deployChart" height="200"></canvas></div><5 <div class="chart-box"><h3><9 $mfrChartTitle</h3><canvas id="mfrChart" height="200"></canvas></div> > </div
$(dacă ($historyData.Count -ge 1) { "<!-- Diagramă tendințe istorice --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Actualizați progresul în timp <o clasă='top-link' href='#'>↑ Primele</a></h2> <div id='d-trend' class='section-body open'> id pânză <='trendChart' height='120'></canvas> <div style='font-size:.75em; culoare:#888; margin-top:5px'>Linii solide = date reale$(if ($historyData.Count -ge 2) { " | Linie întreruptă = proiectat (dublare exponențială: 2→4→8→16... dispozitive pe undă)" } altceva { " | Rulați din nou agregarea mâine pentru a vedea liniile de tendință și proiecția" })</div> ></div </div>" })
Descărcări<!-- CSV - > <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Descărcați Date complete (CSV pentru Excel) <a class="top-link" href="#">Top</a></h2><2 <div id="dl-csv" class="section-body open" style="display:flex; încadrare flexibilă:încadrare; gap:5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Nedate ($($stNotUptodate.ToString("N0")))</a><8 <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a> <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; background:#fd7e14; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Acțiune necesară ($($c.ActionReq.ToString("N0"))</a> <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Probleme cunoscute ($($c.WithKnownIssues.ToString("N0"))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; background:#dc3545; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Task Disabled ($($c.TaskDisabled.ToString("N0")))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; background:#28a745; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">actualizat ($($c.Updated.ToString("N0")))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; background:#6c757d; culoare:#fff; padding:6px 14px; rază bordură:5px; text-decoration:none; font-size:.8em">Summary</a> <div style="width:100%; font-size:.75em; culoare:#888; margin-top:5px">fișiere CSV deschise în Excel. Disponibil atunci când este găzduit pe web server.</div> ></div > </div
Defalcarea producătorului<!-- - > <div class="section"> <h2 onclick="toggle('mfr')">de producător <a class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <tabel><><><><1 producătorul><2><><5><6 total><><9 actualizate><9><0><><3><4><><7 acțiune necesară><8 /th></tr></thead><3 <întruchiparea><5 $mfrTableRows><6 /tbody></table><9 ><1 </div </div>
<!-- secțiunile dispozitivului (primele 200 de descărcări în linie + CSV) - > <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Dispozitivele cu erori ($($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">🔴 Probleme cunoscute ($($c.WithKnownIssues.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-ki" class="section-body">$tblKI</div> ></div <div class="section" id="s-kek"> <h2 onclick="toggle('d-kek')">🟠 KEK lipsă - evenimentul 1803 ($($c.WithMissingKEK.ToString("N0"))) <o clasă="top-link" href="#">↑<de sus /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">🟠 Acțiune necesară ($($c.ActionReq.ToString("N0"))) <o clasă="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">🔵 Sub Observație ($($c.UnderObs.ToString("N0"))) <o clasă="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">🔴 Nedate ($($stNotUptodate.ToString("N0"))) <o clasă="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">🔴 Activitate dezactivată ($($c.TaskDisabled.ToString("N0"))) >↑ 5 o clasă="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">🔴 Erori temporare ($($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">🔴 Erori permanente / Neacceptat ($($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">⏳ Actualizare în așteptare ($($c.UpdatePending.ToString("N0"))) - Politică/WinCS aplicată, Se așteaptă Actualizarea <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Dispozitivele la care se aplică cheia AvailableUpdatesPolicy sau WinCS, dar UEFICA2023Status este încă NotStarted, InProgress sau null.</p>$tblUpdatePending</div> ></div <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Lansare în curs ($($c.RolloutInProgress.ToString("N0"))) <o clasă="top-link" href="#">↑<de sus /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"))) - În afara domeniului <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">🟢 Dispozitive actualizate ($($c.Updated.ToString("N0"))) <o clasă="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">🔄 Actualizat - necesită repornire ($($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">tabloul de bord de implementare a certificatului de bootare securizat | $($stats) generat. ReportGeneratedAt) | StreamingMode | Memorie vârf: ${stPeakMemMB} MB</div> </div><!-- /container -->
>scriptului< comutare funcție(id){var e=document.getElementById(id); e.classList.toggle('open')} function openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} new Chart(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Action Required','Temp. Paused','Not Supported','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#6c757d', '#721c24','#adb5bd','#dc3545']}]},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); new Chart(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ etichetă:'Temp. Paused',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}}); Diagramă tendințe istorice 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); } seturi de date var = [ {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40.167,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220\53\69\0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; if (projLen > 0) { datasets.push({label:'Projected Updated (dublare 2x)',date: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 :adevărat,text:'Devices'}},x:{title:{display:true,text:'Date'}}},pluginuri:{legendă:{position:'top'},title:{display:true,text:'Secure Boot Update Progress over Time'}}}} ); } Numărătoarea inversă dinamică (function(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19'); var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0,Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0,Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0,Math.ceil((p-t)/864e5))}))(); ></script ></corp > </html "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Păstrați întotdeauna o copie stabilă "Cea mai recentă", astfel încât administratorii nu au nevoie pentru a urmări timestamps $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath - Forțare $stTotal = $streamSw.Elapsed.TotalSeconds # Salvați manifestul fișierului pentru modul incremental (detectare rapidă fără modificări la următoarea rulare) dacă ($IncrementalMode -sau $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 "Se salvează manifestul fișierului pentru modul incremental..." -Prim-planColor gri foreach ($jf în $jsonFiles) { $stNewManifest[$jf. NumeComplet.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Dimensiune = $jf. Lungime } } Save-FileManifest $stNewManifest -Cale manifest $stManifestPath Write-Host " Manifest salvat pentru fișiere $($stNewManifest.Count) " -ForegroundColor DarkGray } # CURĂȚARE RETENȚIE # Orchestrator folder reutilizabil (Aggregation_Current): păstrați doar cea mai recentă rula (1) # Admin rulează manual / alte foldere: să păstreze ultimele 7 rulează # REZUMAT CSV-uri nu sunt șterse - acestea sunt mici (~ 1 KB) și sunt sursa de backup pentru istoricul tendințelor $outputLeaf = Split-Path $OutputPath - Frunză $retentionCount = dacă ($outputLeaf -eq 'Aggregation_Current') { 1 } altfel { 7 } # Prefixe de fișier sigure pentru a curăța (instantanee efemere per-run) $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_" ) # Găsiți toate marcajele de timp unice doar din fișierele care se pot curăța $cleanableFiles = Get-ChildItem $OutputPath -Fișier -EA SilentlyContinue | Where-Object { $f = $_. Nume; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Count -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { dacă ($_. Nume -potrivire '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Unic -descendent) dacă ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object - Ignorare $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs în $oldTimestamps) { pentru a căuta ($prefix în $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath - Fișier - Filtru "${prefix}${oldTs}*" -EA SilentlyContinue pentru a căuta ($f în $oldFiles) { $freedBytes += $f.Lungime Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [matematică]:Round($freedBytes / 1 MB, 1) Write-Host "Curățare reținere: s-au eliminat fișierele $removedFiles din rulările vechi $($oldTimestamps.Count), s-au eliberat ${freedMB} MB (se păstrează ultima $retentionCount + toate CSV-urile Summary/NotUptodate)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -Prim planColor Cyan Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -Prim-planColor Cyan Write-Host " Total dispozitive: $($c.Total.ToString("N0"))" -Prim planColor Alb Write-Host " NEDATE: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -Prim-planColor $(dacă ($stNotUptodate -gt 0) { "Galben" } altceva { "Verde" }) Write-Host " Actualizat: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -Prim-planColor verde Write-Host " Cu erori: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Memorie vârf: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Oră: $([matematică]:Round($stTotal/60,1)) min" -Culoare prim plan alb Write-Host " Tablou de bord: $htmlPath" - Culoare prim plan alb return [PSCustomObject]$stats } #endregion MODUL DE REDARE ÎN FLUX } altfel, { Write-Error "Calea de intrare nu s-a găsit: $InputPath" ieșire 1 }