VIKTIG Denne artikkelen som inneholder dette eksempelskriptet, er trukket tilbake. Fra og med Windows-oppdateringene som ble utgitt 12. mai 2026, er eksempelskriptet plassert i mappen %systemroot%\SecureBoot\ExampleRolloutScripts på enheten.
Kopier og lim inn dette eksempelskriptet og endre etter behov for miljøet:
<# . SYNOPSIS Samler JSON-data for sikker oppstartsstatus fra flere enheter til sammendragsrapporter.
. BESKRIVELSE Leser inn innsamlede JSON-filer for sikker oppstartsstatus og genererer: – HTML-instrumentbord med diagrammer og filtrering - Sammendrag av ConfidenceLevel – Unik enhetssamlingsanalyse for teststrategi Støtter: - Filer per maskin: HOSTNAME_latest.json (anbefales) – enkel JSON-fil Deduplicates automatisk av HostName, og beholder den nyeste CollectionTime. Som standard inkluderer bare enheter med «Handlingsreq» eller «Høy» konfidens for å fokusere på handlingssamlinger. Bruk -IncludeAllConfidenceLevels til å overstyre.
. PARAMETER InputPath Bane til JSON-fil(er): - Mappe: Leser alle *_latest.json filer (eller *.json hvis ingen _latest filer) - Fil: Leser enkelt JSON-fil
. PARAMETER OutputPath Bane for genererte rapporter (standard: .\SecureBootReports)
. EKSEMPEL # Aggreger fra mappe med filer per maskin (anbefales) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Leser: \\contoso\SecureBootLogs$\*_latest.json
. EKSEMPEL # Egendefinert utdataplassering .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. EKSEMPEL # Inkluder bare handling på nytt og høy visshet (standardvirkemåte) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Utelatelser: Observasjon, stanset midlertidig, støttes ikke
. EKSEMPEL # Inkluder alle konfidensnivåer (overstyringsfilter) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. EKSEMPEL # Egendefinert filter for konfidensnivå .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. EKSEMPEL # ENTERPRISE SCALE: Trinnvis modus – bare prosessendret filer (raske etterfølgende kjøringer) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Første kjøring: Full belastning ~ 2 timer for 500K-enheter # Etterfølgende kjøringer: Sekunder hvis ingen endringer, minutter for deltaer
. EKSEMPEL # Hopp over HTML hvis ingenting endres (raskest for overvåking) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Hvis ingen filer er endret siden forrige kjøring: ~5 sekunder
. EKSEMPEL # Modus for bare sammendrag – hopp over store enhetstabeller (1–2 minutter kontra 20 minutter) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Genererer CSV-er, men hopper over HTML-instrumentbord med fullstendige enhetstabeller
. NOTATER Par med Detect-SecureBootCertUpdateStatus.ps1 for virksomhetsdistribusjon.Se GPO-DEPLOYMENT-GUIDE.md for fullstendig distribusjonsveiledning. Standardvirkemåte utelukker enheter som er observasjon, midlertidig stanset og ikke støttet for å fokusere rapportering bare på handlingsbare enhetssamlinger.#>
param( [Parameter(Obligatorisk = $true)] [streng]$InputPath, [Parameter(Obligatorisk = $false)] [string]$OutputPath = ".\SecureBootReports", [Parameter(Obligatorisk = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Obligatorisk = $false)] [string]$RolloutStatePath, # Bane til RolloutState.json for å identifisere InProgress-enheter [Parameter(Obligatorisk = $false)] [string]$RolloutSummaryPath, # Bane til SecureBootRolloutSummary.json fra Orchestrator (inneholder projeksjonsdata) [Parameter(Obligatorisk = $false)] [string[]]$IncludeConfidenceLevels = @("Action Required", "High Confidence"), # Only include these confidence levels (default: actionable buckets only) [Parameter(Obligatorisk = $false)] [switch]$IncludeAllConfidenceLevels, # Overstyr filter for å inkludere alle konfidensnivåer [Parameter(Obligatorisk = $false)] [switch]$SkipHistoryTracking, [Parameter(Obligatorisk = $false)] [switch]$IncrementalMode, # Aktiver deltabehandling – last bare inn endrede filer siden forrige kjøring [Parameter(Obligatorisk = $false)] [string]$CachePath, # Bane til hurtigbufferkatalog (standard: OutputPath\.cache) [Parameter(Obligatorisk = $false)] [int]$ParallelThreads = 8, # Antall parallelle tråder for filinnlasting (PS7+) [Parameter(Obligatorisk = $false)] [switch]$ForceFullRefresh, # Tving full innlasting på nytt selv i trinnvis modus [Parameter(Obligatorisk = $false)] [switch]$SkipReportIfUnchanged, # Hopp over HTML/CSV-generering hvis ingen filer endres (bare utdatastatistikk) [Parameter(Obligatorisk = $false)] [switch]$SummaryOnly, # Generer bare sammendragsstatistikk (ingen store enhetstabeller) – mye raskere [Parameter(Obligatorisk = $false)] [switch]$StreamingMode # Minneeffektiv modus: prosesssegmenter, skriv CSV-er trinnvis, behold bare sammendrag i minnet )
# Selvreparasjon: Fjerne usynlige Unicode-tegn som settes inn av cms på nettet ved kopiering fra HTML-artikler.# Cms-support.microsoft.com setter inn mellomrom med tom bredde (U+200B), mellomrom uten brudd (U+00A0) og andre # usynlige tegn rundt html-koder i her-strenger, noe som forårsaker PowerShell-analysefeil.hvis ($MyInvocation.MyCommand.Path) { $rawScript = [System.IO.File]::ReadAllText($MyInvocation.MyCommand.Path) hvis ($rawScript -match '[\u200B-\u200F\uFEFF]' -eller $rawScript -match '\xA0') { Write-Host "ADVARSEL: Usynlige Unicode-tegn oppdaget (sannsynligvis fra nettkopi-lim inn) - auto-rengjøring skript ..." -Forgrunnsfarge Gul $cleaned = $rawScript -replace '[\u200B-\u200F\uFEFF]', '' $cleaned = $cleaned -replace '\xA0', ' ' [System.IO.File]::WriteAllText($MyInvocation.MyCommand.Path, $cleaned, [System.Text.UTF8Encoding]::new($false)) Write-Host «Skriptet ble renset. Re-launching..." -ForegroundColor Green & $MyInvocation.MyCommand.Path @PSBoundParameters avslutt $LASTEXITCODE } }
# Opphøyd automatisk til PowerShell 7 hvis tilgjengelig (6x raskere for store datasett) hvis ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object –ExpandProperty-kilde hvis ($pwshPath) { Write-Host "PowerShell $($PSVersionTable.PSVersion) oppdaget - re-lansering med PowerShell 7 for raskere behandling ..." -Forgrunnsfarge Gul # Gjenoppbygg argumentliste fra bundne parametere $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key i $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] hvis ($val -er [bryter]) { hvis ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [array]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } ellers { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs avslutt $LASTEXITCODE } }
$ErrorActionPreference = "Fortsett" $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = «Eksempler på distribusjon og overvåking»
# Obs! Dette skriptet er ikke avhengig av andre skript.# Last ned fra: $DownloadUrl -> $DownloadSubPage
#region installasjonsprogrammet Write-Host "=" * 60 -ForegroundColor Cyan Write-Host "Secure Boot Data Aggregation" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan
# Opprett utdatakatalog if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Ut-null }
# Innlastingsdata – støtter CSV-formater (eldre) og JSON-formater (opprinnelige) Write-Host "'nLaster data fra: $InputPath" -Forgrunnsfarge gul
# Hjelpefunksjon for å normalisere enhetsobjekt (håndtere forskjeller i feltnavn) funksjon Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON bruker Hostname, CSV bruker HostName) hvis ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Vertsnavn -Force } # Handle Confidence vs ConfidenceLevel (JSON bruker konfidens, CSV bruker ConfidenceLevel) # ConfidenceLevel er det offisielle feltnavnet - tilordne tillit til det hvis ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Konfidens -Force } # Spor oppdateringsstatus via Event1808Count OR UEFICA2023Status="Updated" # Dette gjør det mulig å spore hvor mange enheter i hver konfidenssamling som er oppdatert $event 1808 = 0 hvis ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false hvis ($device. PSObject.Properties['UEFICA2023Status'] - og $device. UEFICA2023Status -eq "Oppdatert") { $uefiCaUpdated = $true } hvis ($event 1808 -gt 0 -eller $uefiCaUpdated) { # Marker som oppdatert for instrumentbord/utrullingslogikk – men ikke overstyr ConfidenceLevel $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } ellers { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # Konfidensnivåklassifisering: # - "Høy visshet", "Under observasjon ...", "Midlertidig stanset ...", "Støttes ikke ..." = bruk som den er # - Alt annet (null, tom, "UpdateType:...", "Unknown", "I/T") = faller til Handling kreves i tellere # Ingen normalisering er nødvendig – strømmingstellerens andre gren håndterer den } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON bruker OEM*, eldre bruker WMI_*) hvis ($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 hvis ($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 hvis ($device. PSObject.Properties['FirmwareVersion'] - og -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } returner $device }
#region trinnvis behandling / hurtigbufferbehandling # Hurtigbufferbaner for installasjon hvis (-ikke $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Funksjoner for hurtigbufferadministrasjon funksjon Get-FileManifest { param([streng]$Path) hvis (testbane $Path) { prøv { $json = Get-Content $Path -Raw | ConvertFrom-Json # Konverter PSObject til hashtable (PS5.1-kompatibel - PS7 har -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Navn] = $_. Value } returner $ht } catch { returner @{} } } returner @{} }
funksjon Save-FileManifest { param([hashtable]$Manifest, [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Ut-null } $Manifest | ConvertTo-Json -Dybde 3 -Komprimer | Set-Content $Path -Force }
funksjon Get-DeviceCache { param([streng]$Path) hvis (testbane $Path) { prøv { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host " Lastet enhetsbuffer: $($cacheData.Count) enheter" -ForegroundColor DarkGray returner $cacheData } catch { Write-Host " Cache corrupted, will rebuild" -ForegroundColor Yellow returner @() } } returner @() }
funksjon Save-DeviceCache { param($Devices; [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Ut-null } # Konverter til matrise og lagre $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Dybde 10 -Komprimer | Set-Content $Path -Force Write-Host " Lagret enhetsbuffer: $($deviceArray.Count) enheter" -ForegroundColor DarkGray }
funksjon Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Bygg oppslag som ikke skilles mellom store og små bokstaver, fra manifest (normaliser til små bokstaver) $manifestLookup = @{} foreach ($mk i $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file i $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normaliser bane til små bokstaver $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Størrelse = $file. Lengde } hvis ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] hvis ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Størrelse -eq $file. Lengde) { [void]$unchanged. Add($file) Fortsette } } [void]$changed. Add($file) } returner @{ Endret = $changed Uendret = $unchanged NewManifest = $newManifest } }
# Ultra-rask parallell fillasting ved hjelp av satsvis behandling funksjon Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Telle # Bruk grupper med ~1000 filer hver for bedre minnekontroll $batchSize = [math]::Min(1000, [math]::Ceiling($totalFiles / [math]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
for ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [matematikk]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Add($batch) } Write-Host " ($($batches. Antall) grupper med ~$batchSize filer hver)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # Kontroller om PowerShell 7+ parallelt er tilgjengelig $canParallel = $PSVersionTable.PSVersion.Major -ge 7 hvis ($canParallel -og $Threads -gt 1) { # PS7+: Prosessgrupper parallelt $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file i $batchFiles) { prøv { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } catch { } } $batchResults.ToArray() } foreach ($batch i $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } ellers { # PS5.1-tilbakefall: Sekvensiell behandling (fremdeles rask for <10K-filer) foreach ($file i $Files) { prøv { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } catch { } } } returner $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Enkel JSON-fil hvis ($InputPath -like "*.json") { $jsonContent = Get-Content -Path $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host «Lastet inn $($allDevices.Count)-poster fra fil» } ellers { Write-Error «Bare JSON-format støttes. Filen må ha .json filtype». avslutt 1 } } elseif (Test-Path $InputPath -PathType Container) { # Mappe - bare JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Foretrekker *_latest.json filer hvis de finnes (per maskin-modus) $latestJson = $jsonFiles | Where-Object { $_. Navn som «*_latest.json» } hvis ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count hvis ($totalFiles -eq 0) { Write-Error «Finner ingen JSON-filer i: $InputPath» avslutt 1 } Write-Host "Fant $totalFiles JSON-filer" -Forgrunnsfarge grå # Helper-funksjon som samsvarer med konfidensnivåer (håndterer både korte og fullstendige skjemaer) # Definert tidlig, slik at både StreamingMode og vanlige baner kan bruke den funksjon Test-ConfidenceLevel { param([string]$Value, [string]$Match) hvis ([streng]::IsNullOrEmpty($Value)) { returner $false } bryter ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { returner $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") } "TemporarilyPaused" { returner $Value -like "Midlertidig midlertidig stanset*" } "Ikke støttet" { retur ($Value -like "Støttes ikke*" - eller $Value -eq "Støttes ikke") } standard { return $false } } } #region STRØMMINGSMODUS – minneeffektiv behandling for store datasett # Bruk alltid StreamingMode for minneeffektiv behandling og instrumentbord i ny stil hvis (-ikke $StreamingMode) { Write-Host «Automatisk aktivering av StreamingMode (nytt instrumentbord)» –Forgrunnsfarge gul $StreamingMode = $true hvis (-ikke $IncrementalMode) { $IncrementalMode = $true } } # Når -StreamingMode er aktivert, behandler du filer i biter og beholder bare tellere i minnet.# Data på enhetsnivå skrives til JSON-filer per del for behovsbetinget innlasting på instrumentbordet.# Minnebruk: ~1,5 GB uavhengig av datasettstørrelse (vs. 10–20 GB uten strømming).hvis ($StreamingMode) { Write-Host «STRØMMINGSMODUS aktivert – minneeffektiv behandling» – Forgrunnsfarge grønn $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # TRINNVIS KONTROLL: Hvis ingen filer er endret siden forrige kjøring, kan du hoppe over behandlingen fullstendig hvis ($IncrementalMode -og -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" hvis (testbane $stManifestPath) { Write-Host «Ser etter endringer siden forrige kjøring av strømming...» –Forgrunnsfarge Cyan $stOldManifest = Get-FileManifest -Path $stManifestPath hvis ($stOldManifest.Count -gt 0) { $stChanged = $false # Hurtigkontroll: samme filantall? hvis ($stOldManifest.Count -eq $totalFiles) { # Kontroller de 100 NYESTE filene (sortert etter LastWriteTime synkende) # Hvis en fil endres, vil den ha det nyeste tidsstempelet og vises først $sampleSize = [matematikk]::Min(100; $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Descending | Select-Object –Første $sampleSize foreach ($sf i $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Bryte } # Sammenlign tidsstempler - bufret kan være DateTime eller streng etter JSON-rundtur $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc prøv { # Hvis bufret allerede er DateTime (ConvertFrom-Json automatisk konverterer), bruk direkte hvis ($cachedLWT -er [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } ellers { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } hvis ([matematikk]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Bryte } } catch { $stChanged = $true Bryte } } } ellers { $stChanged = $true } hvis (-ikke $stChanged) { # Kontroller om det finnes utdatafiler $stSummaryExists = Get-ChildItem (join-path $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object –Første 1 $stDashExists = Get-ChildItem (koblingsbane $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object –Første 1 hvis ($stSummaryExists -og $stDashExists) { Write-Host " Ingen endringer oppdaget ($totalFiles filer uendret) - hopper over behandling" -ForegroundColor Green Write-Host " Siste instrumentbord: $($stDashExists.FullName)" -Forgrunnsfarge hvit $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv Write-Host " Enheter: $($cachedStats.TotalDevices) | Oppdatert: $($cachedStats.Updated) | Feil: $($cachedStats.WithErrors)" -Forgrunnsfarge grå Write-Host " Fullført i $([matematikk]::Round($streamSw.Elapsed.TotalSeconds, 1))s (ingen behandling nødvendig)" -ForegroundColor Green returner $cachedStats } } ellers { # DELTA PATCH: Finn nøyaktig hvilke filer som er endret Write-Host " Endringer oppdaget - identifiserer endrede filer ..." -Forgrunnsfarge Gul $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf i $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() hvis (-ikke $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } ellers { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc prøv { $cachedDT = hvis ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } hvis ([matematikk]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } catch { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [matematikk]::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Endret: $($changedFiles.Count) | Ny: $($newFiles.Count) | Totalt: $totalChanged ($changePct%)" -Forgrunnsfarge gul hvis ($totalChanged -gt 0 -og $changePct -lt 10) { # DELTA-OPPDATERINGSMODUS: <10 % endret, oppdater eksisterende data Write-Host " Delta-oppdateringsmodus ($changePct% < 10%) - oppdatering $totalChanged filer ..." -Forgrunnsfarge grønn $dataDir = Join-Path $OutputPath "data" # Last inn endrede/nye enhetsposter $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df i $allDeltaFiles) { prøv { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData hvis ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } catch { } } Write-Host " Loaded $($deltaDevices.Count) changed device records" -ForegroundColor Gray # For hver kategori-JSON: Fjern gamle oppføringer for endrede vertsnavn, legg til nye oppføringer $categoryFiles = @("errors", "known_issues", "missing_kek", "not_updated", "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required", "secureboot_off", "rollout_inprogress") $changedHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($hn i $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat i $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" hvis (testbane $catPath) { prøv { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Fjern gamle oppføringer for endrede vertsnavn $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Klassifiser hver endrede enhet i kategorier # (legges til nedenfor etter klassifisering) $catData | ConvertTo-Json -Dybde 5 | Set-Content $catPath -Koding UTF8 } catch { } } } # Klassifiser hver endret enhet og tilføy til de riktige kategorifilene foreach ($dev i $deltaDevices.Values) { $slim = [bestilt]@{ HostName = $dev. Vertsnavn WMI_Manufacturer = hvis ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } else { "" } WMI_Model = hvis ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } else { "" } BucketId = hvis ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } else { "" } ConfidenceLevel = hvis ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. IsUpdated UEFICA2023Error = hvis ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } else { $null } SecureBootTaskStatus = hvis ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } else { "" } KnownIssueId = hvis ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } else { $null } SkipReasonKnownIssue = hvis ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = hvis ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) - og $dev. UEFICA2023Error -ne "0" - og $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -eller $dev. SecureBootTaskStatus -eq 'Disabled' -eller $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = hvis ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } else { 0 } $e 1808 = hvis ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } else { 0 } $e 1803 = hvis ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -eller $dev. MissingKEK -eq $true) $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = hvis ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" } # Tilføy til samsvarende kategorifiler $targets = @() hvis ($isUpd) { $targets += "updated_devices" } hvis ($hasErr) { $targets += "errors" } hvis ($hKI) { $targets += "known_issues" } hvis ($mKEK) { $targets += "missing_kek" } if (-not $isUpd -and $sbOn) { $targets += "not_updated" } hvis ($tskDis) { $targets += "task_disabled" } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += "temp_failures" } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" } if (-not $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" } if (-not $sbOn) { $targets += "secureboot_off" } hvis ($e 1801 -gt 0 -og $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } foreach ($tgt i $targets) { $tgtPath = Join-Path $dataDir "$tgt.json" hvis (testbane $tgtPath) { $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Depth 5 | Set-Content $tgtPath -Koding UTF8 } } } # Generer CSV-er på nytt fra oppdaterte JSON-er Write-Host " Regenerating CSVs from patched data..." -ForegroundColor Gray $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat i $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" hvis (testbane $catJsonPath) { prøv { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json hvis ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8 } } catch { } } } # Recount stats from the patched JSON files Write-Host " Beregne sammendrag på nytt fra oppdaterte data ..." -Forgrunnsfarge grå $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") } $pTotal = 0; $pUpdated = 0; $pErrors = 0; $pKI = 0; $pKEK = 0 $pTaskDis = 0; $pTempFail = 0; $pPermFail = 0; $pActionReq = 0; $pSBOff = 0; $pRIP = 0 foreach ($cat i $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } catch { } bryter ($cat) { "updated_devices" { $pUpdated = $cnt } "errors" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # beregnet "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 = (Hent innhold (koblingsbane $dataDir "not_updated.json") -Raw | ConvertFrom-Json). Telle $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Delta-oppdatering fullført: $totalChanged enheter oppdatert" -ForegroundColor Green Write-Host " Total: $pTotal | Oppdatert: $pUpdated | NotUpdated: $pNotUpdated | Feil: $pErrors" -Forgrunnsfarge hvit # Oppdater manifest $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf i $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Størrelse = $jf. Lengde } } Save-FileManifest -Manifest $stNewManifest -Bane $stManifestPath Write-Host " Fullført i $([matematikk]::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green # Gå til full reprosessering av strømming for å generere HTML-instrumentbord på nytt # Datafilene er allerede oppdatert, så dette sikrer at instrumentbordet forblir oppdatert Write-Host " Generer instrumentbord på nytt fra oppdaterte data ..." -Forgrunnsfarge Gul } ellers { Write-Host " $changePct% filer endret (>= 10%) - full reprosessering av strømming kreves" -Forgrunnsfarge gul } } } } } # Opprett dataunderkatalog for on-demand-enhets-JSON-filer $dataDir = Join-Path $OutputPath "data" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Deduplication via HashSet (O(1) per oppslag, ~50 MB for 600 000 vertsnavn) $seenHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Lette sammendragstellere (erstatter $allDevices + $uniqueDevices i minnet) $c = @{ Totalt = 0; SBEnabled = 0; SBOff = 0 Oppdatert = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; Støttes ikke = 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 Oppdatering venter = 0 } # Samlingssporing for AtRisk/SafeList (lette sett) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Enhetsdatafiler i satsvis modus: akkumuler per del, tømming ved delgrenser $stDeviceFiles = @("errors", "known_issues", "missing_kek", "not_updated", "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required", "secureboot_off", "rollout_inprogress", "under_observation", "needs_reboot", "update_pending") $stDeviceFilePaths = @{}; $stDeviceFileCounts = @{} foreach ($dfName i $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim enhetspost for JSON-utdata (bare viktige felt, ~200 byte vs ~2 KB full) funksjon Get-SlimDevice { param($Dev) returner [bestilt]@{ HostName = $Dev.HostName WMI_Manufacturer = hvis ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" } WMI_Model = hvis ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" } BucketId = hvis ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = hvis ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null } SecureBootTaskStatus = hvis ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" } KnownIssueId = hvis ($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 = hvis ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } else { $null } WinCSKeyApplied = hvis ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } ellers { $null } } } # Tøm bunken til JSON-fil (tilføyingsmodus) funksjon Flush-DeviceBatch { param([string]$StreamName, [System.Collections.Generic.List[object]]$Batch) hvis ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev i $Batch) { hvis ($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) } # HOVEDSTRØMMINGSLØKKE $stChunkSize = hvis ($totalFiles -le 10000) { $totalFiles } annet { 10000 } $stTotalChunks = [matematikk]::Tak($totalFiles / $stChunkSize) $stPeakMemMB = 0 hvis ($stTotalChunks -gt 1) { Write-Host «Behandler $totalFiles filer i $stTotalChunks deler av $stChunkSize (strømming, $ParallelThreads tråder):» –Forgrunnsfarge Cyan } ellers { Write-Host "Behandler $totalFiles filer (strømming, $ParallelThreads tråder):" -Forgrunnsfarge Cyan } for ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [matematikk]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] hvis ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) files): " -NoNewline -ForegroundColor Gray } ellers { Write-Host " Laster inn $($cFiles.Count)-filer: " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Tråder $ParallelThreads # Bunkelister per del $cBatches = @{} foreach ($df i $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 foreach ($raw i $rawDevices) { hvis (-ikke $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Vertsnavn hvis (-ikke $hostname) { continue } hvis ($seenHostnames.Contains($hostname)) { $cDupe++; fortsett } [void]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False") hvis ($sbOn) { $c.SBEnabled++ } ellers { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = hvis ($device. PSObject.Properties['ConfidenceLevel'] - og $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($device. UEFICA2023Error) -and "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -eller "$($device. SecureBootTaskStatus)" -eq 'Disabled' -or "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = hvis ($device. PSObject.Properties['BucketId'] - og $device. BucketId) { "$($device. BucketId)" } else { "" } $e 1808 = hvis ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } else { 0 } $e 1801 = hvis ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } else { 0 } $e 1803 = hvis ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -eller $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True") $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($device. KnownIssueId))) $rStat = hvis ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" } $mfr = hvis ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } else { "Unknown" } $bid = hvis (-ikke [string]::IsNullOrEmpty($bid)) { $bid } else { "" } # Flagg for ventende oppdatering før beregning (policy/WinCS er aktivert, statusen er ikke oppdatert ennå, SB PÅ, oppgaven er ikke deaktivert) $uefiStatus = hvis ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } else { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] - og $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] - og $device. WinCSKeyApplied -eq $true) $statusPending = ([streng]::IsNullOrEmpty($uefiStatus) -eller $uefiStatus -eq 'NotStarted' -eller $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -or $hasWinCS) -and $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis) hvis ($isUpd) { $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Spor oppdaterte enheter som trenger omstart (UEFICA2023Status=Oppdatert, men Event1808=0) hvis ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF – utenfor omfanget, ikke klassifiser etter konfidens } else { hvis ($isUpdatePending) { } # Telles separat i ventende oppdatering – gjensidig utelukkende for sektordiagram elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ } else { $c.ActionReq++ } hvis ([streng]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } hvis ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } hvis ($tskNF) { $c.TaskNotFound++ } if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ } hvis ($hasErr) { $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["errors"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Error hvis (-ikke $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ hvis ($stErrorCodeSamples[$ec]. Antall -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } hvis ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = hvis (-ikke [streng]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } else { $device. KnownIssueId } hvis (-ikke $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } hvis ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } hvis ($e 1801 -gt 0 -og $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++ } hvis ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ } # Oppdatering venter: policy eller WinCS brukt, status venter, SB PÅ, oppgave ikke deaktivert hvis ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } hvis (-ikke $isUpd -og $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # Under Observasjonsenheter (atskilt fra handling kreves) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Handling kreves: ikke oppdatert, SB PÅ, samsvarer ikke med andre konfidenskategorier, ikke Oppdatering venter hvis (-ikke $isUpd -og $sbOn -og -not $isUpdatePending -and-not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarilyPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } hvis (-ikke $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Oppdatert=0; Oppdatering venter=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; Ikke støttet=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Total++ hvis ($isUpd) { $stMfrCounts[$mfr]. Oppdatert++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. UpdatePending++ } elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. NotSupported++ } else { $stMfrCounts[$mfr]. ActionReq++ } hvis ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Spor alle enheter etter samling (inkludert tom BucketId) $bucketKey = hvis ($bid -and $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Oppdatert=0; Manufacturer=$mfr; Model=""; BIOS="" } hvis ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Modell = $device. WMI_Model } hvis ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Antall++ hvis ($isUpd) { $stAllBuckets[$bucketKey]. Oppdatert++ } } # Tøm grupper til disk foreach ($df i $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Matematikk]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = hvis ($cRem -gt 0) { " | ETA: ~$([Math]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" } $cMem = [matematikk]::Round([System.GC]::GetTotalMemory($false) / 1 MB, 0) hvis ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew ny, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # Fullfør JSON-matriser foreach ($dfName i $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) enheter" -ForegroundColor DarkGray } # Databehandling avledet statistikk $stAtRisk = 0; $stSafeList = 0 foreach ($bid i $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Updated hvis ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [math]::Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = antall fra not_updated batch (enheter med SB ON og ikke oppdatert) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [bestilt]@{ ReportGeneratedAt = (Hent-dato). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Oppdatert = $c.Oppdatert; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; TemporarilyPaused = $c.TempPaused; Støttes ikke = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; Fullstendig oppdatert = $c.oppdatert Oppdateringer venter = $stNotUptodate; UpdatesComplete = $c.Updated WithErrors = $c.WithErrors; InProgress = $c.InProgress; NotYetInitiated = $c.NotYetInitiated RolloutInProgress = $c.RolloutInProgress; WithKnownIssues = $c.WithKnownIssues WithMissingKEK = $c.WithMissingKEK; TemporaryFailures = $c.TempFailures; PermanentFailures = $c.PermFailures NeedsReboot = $c.NeedsReboot; UpdatePending = $c.UpdatePending AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList PercentWithErrors = hvis ($c.Total -gt 0) { [math]::Round(($c.WithErrors/$c.Total)*100,2) } else { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } else { 0 } PercentSafeList = hvis ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } ellers { 0 } PercentHighConfidence = hvis ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } else { 0 } PercentCertUpdated = hvis ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = hvis ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } else { 0 } PercentNotUptodate = hvis ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } ellers { 0 } PercentFullyUpdated = hvis ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Skrive CSV-er [PSCustomObject]$stats | Export-Csv -Path (join-path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Koding UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Nøkkel; Antall=$_. Value.Total; Oppdatert=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (join-path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Koding UTF8 $stErrorCodeCounts.GetEnumerator() | Sort-Object Verdi -Synkende | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Nøkkel; Antall=$_. Verdi; SampleDevices=($stErrorCodeSamples[$_. Key] -join ", ") } } | Export-Csv -Path (join-path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Descending | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Nøkkel; Antall=$_. Value.Count; Oppdatert=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Produsent=$_. Value.Manufacturer } } | Export-Csv -Path (join-path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Koding UTF8 # Generer orchestratorkompatible CSV-er (forventede filnavn for Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" hvis (testbane $notUpdatedJsonPath) { prøv { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json hvis ($nuData.Count -gt 0) { # NotUptodate CSV - orchestrator søker etter *NotUptodate*.csv $nuData | Export-Csv -Path (join-path $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Koding UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) devices)" -ForegroundColor Gray } } catch { } } # Skrive JSON-data for instrumentbord $stats | ConvertTo-Json -Dybde 3 | Set-Content (koblingsbane $dataDir "summary.json") -Koding UTF8 # HISTORISK SPORING: Lagre datapunkt for trenddiagram # Bruk en stabil plassering av hurtigbufferen, slik at trenddata vedvarer på tvers av tidsstemplet aggregasjonsmapper. # Hvis OutputPath ser ut som ...\Aggregation_yyyyMMdd_HHmmss, legges hurtigbufferen i den overordnede mappen.# Hvis ikke, går hurtigbufferen i selve OutputPath.$parentDir = Split-Path $OutputPath -Parent $leafName = Split-Path $OutputPath -Leaf hvis ($leafName -matcher ^Aggregation_\d{8} - eller $leafName -eq Aggregation_Current) { # Orchestrator-opprettet tidsstemplet mappe – bruk overordnet for stabil hurtigbuffer $historyPath = Join-Path $parentDir ".cache\trend_history.json" } ellers { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Parent if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() hvis (testbane $historyPath) { try { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } catch { $historyData = @() } } # Kontroller også i OutputPath\.cache\ (eldre plassering fra eldre versjoner) # Slå sammen datapunkter som ikke allerede finnes i primærloggen hvis ($leafName -eq 'Aggregation_Current' -eller $leafName -match '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { prøv { $innerData = @(Hent innhold $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Dato }) foreach ($entry i $innerData) { hvis ($entry. Dato og $entry. Dato -notin $existingDates) { $historyData += $entry } } hvis ($innerData.Count -gt 0) { Write-Host " Merged $($innerData.Count) data points from inner cache" -ForegroundColor DarkGray } } catch { } } }
# BOOTSTRAP: Hvis trendhistorikken er tom/sparsom, rekonstruerer du fra historiske data hvis ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host " Bootstrapping trend history from historical data..." -ForegroundColor Yellow $dailyData = @{} # Kilde 1: Sammendrags-CSV-er i gjeldende mappe (Aggregation_Current beholder alle sammendrags-CSV-er) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object navn foreach ($summCsv i $localSummaries) { prøv { $summ = Import-Csv $summCsv.FullName | Select-Object –Første 1 hvis ($summ. TotalDevices -og [int]$summ. TotalDevices -gt 0 -og $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = hvis ($summ. Oppdatert) { [int]$summ. Oppdatert } ellers { 0 } $notUpd = hvis ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Dato = $dateStr; Total = [int]$summ. TotalDevices; Oppdatert = $updated; NotUpdated = $notUpd NeedsReboot = 0; Feil = 0; ActionRequired = hvis ($summ. ActionRequired) { [int]$summ. ActionRequired } ellers { 0 } } } } catch { } } # Kilde 2: Gamle tidsstemplete Aggregation_* mapper (eldre, hvis de fremdeles finnes) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Navn -match ^Aggregation_\d{8}} } | Sort-Object navn foreach ($folder i $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object –Første 1 hvis ($summCsv) { prøv { $summ = Import-Csv $summCsv.FullName | Select-Object -Første 1 hvis ($summ. TotalDevices -og [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Navn -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = hvis ($summ. Oppdatert) { [int]$summ. Oppdatert } ellers { 0 } $notUpd = hvis ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Dato = $dateStr; Total = [int]$summ. TotalDevices; Oppdatert = $updated; NotUpdated = $notUpd NeedsReboot = 0; Feil = 0; ActionRequired = hvis ($summ. ActionRequired) { [int]$summ. ActionRequired } ellers { 0 } } } } catch { } } } # Kilde 3: RolloutState.json WaveHistory (har tidsstempler per bølge fra dag 1) # Dette gir opprinnelige datapunkter selv når det ikke finnes noen gamle aggregasjonsmapper $rolloutStatePaths = @( (Join-Path $parentDir "RolloutState\RolloutState.json"), (Join-Path $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath i $rolloutStatePaths) { hvis (testbane $rsPath) { prøv { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json hvis ($rsData.WaveHistory) { # Bruk startdatoer for bølge som trenddatapunkter # Beregne kumulative enheter rettet mot hver bølge $cumulativeTargeted = 0 foreach ($wave i $rsData.WaveHistory) { hvis ($wave. StartedAt - og $wave. DeviceCount) { $waveDate = ([datetime]$wave. StartedAt). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. DeviceCount hvis (-ikke $dailyData.ContainsKey($waveDate)) { # Omtrentlig: ved bølgestart ble bare enheter fra tidligere bølger oppdatert $dailyData[$waveDate] = [PSCustomObject]@{ Dato = $waveDate; Total = $c.Total; Oppdatert = [matematikk]::Maks.(0; $cumulativeTargeted - [int]$wave. DeviceCount) NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0; Feil = 0; ActionRequired = 0 } } } } } } catch { } break # Bruk først funnet } }
hvis ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object Key | ForEach-Object { $_. Verdi }) Write-Host " Bootstrapped $($historyData.Count) data points from historical summaries" -ForegroundColor Green } }
# Legg til gjeldende datapunkt (deduplicate etter dag – behold siste per dag) $todayKey = (Hent-dato). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Dato)" -like "$todayKey*" } hvis ($existingToday) { # Erstatt dagens oppføring $historyData = @($historyData | Where-Object { "$($_. Dato)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Dato = $todayKey Total = $c.Total Oppdatert = $c.Oppdatert NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Feil = $c.WithErrors ActionRequired = $c.ActionReq } # Fjern ugyldige datapunkter (0 totalt) og behold de siste 90 $historyData = @($historyData | Where-Object { [int]$_. Totalt -gt 0 }) # Ingen grense – trenddata er ~100 byte/oppføring, et helt år = ~36 kB $historyData | ConvertTo-Json -Dybde 3 | Set-Content $historyPath -Koding UTF8 Write-Host " Trendlogg: $($historyData.Count) datapunkter" -ForegroundColor DarkGray # Bygg trenddiagramdata for HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Oppdatert }) -join "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Total }) -join "," # Projeksjon: utvide trendlinjen ved hjelp av eksponentiell dobling (2,4,8,16...) # Henter bølgestørrelse og observasjonsperiode fra faktiske trendloggdata.# - Bølgestørrelse = største enkeltperiodeøkning sett i historien (den siste bølgen distribuert) # - Observasjonsdager = gjennomsnittlige kalenderdager mellom trenddatapunkter (hvor ofte vi kjører) # Dobler deretter bølgestørrelsen hver periode, og samsvarer med orchestratorens vekststrategi på 2x.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false hvis ($historyData.Count -ge 2) { $lastUpdated = $c.Updated $remaining = $stNotUptodate # Bare SB-ON ikke oppdaterte enheter (utelukker SecureBoot OFF) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Utled bølgestørrelse og observasjonsperiode fra trendhistorikk $increments = @() $dayGaps = @() for ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Oppdatert – $historyData[$hi-1]. Oppdatert hvis ($inc -gt 0) { $increments += $inc } prøv { $d 1 = [datetime]::P arse($historyData[$hi-1]. Dato) $d 2 = [datetime]::P arse($historyData[$hi]. Dato) $gap = ($d 2 - $d 1). TotalDays hvis ($gap -gt 0) { $dayGaps += $gap } } catch {} } # Bølgestørrelse = siste positive økning (gjeldende bølge), tilbakefall til gjennomsnitt, minimum 2 $waveSize = hvis ($increments. Antall -gt 0) { [matematikk]::Maks.(2; $increments[-1]) } ellers { 2 } # Observasjonsperiode = gjennomsnittlig mellomrom mellom datapunkter (kalenderdager per bølge), minimum 1 $waveDays = hvis ($dayGaps.Count -gt 0) { [matematikk]::Maks(1; [matematikk]::Rund(($dayGaps | Measure-Object -Gjennomsnitt). Gjennomsnitt, 0)) } andre { 1 }
Write-Host " Projeksjon: waveSize = $waveSize (fra siste økning), waveDays = $waveDays (gjennomsnittlig gap fra historien)" -ForegroundColor DarkGray
$dayCounter = 0 # Project til alle enheter er oppdatert eller maksimalt 365 dager for ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # Ved hver observasjonsperiodegrense, distribuer en bølge og dobbelt hvis ($dayCounter -ge $waveDays) { $devicesThisWave = [matematikk]::Min($waveSize; $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave hvis ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Dobbel bølgestørrelse for neste periode (orchestrator 2x strategi) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd")'" $projValues += $lastUpdated $projNotUpdValues += [matematikk]::Maks(0; $remaining) hvis ($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 " Projeksjon: trenger minst 2 trenddatapunkter for å avlede bølgetidsberegning" -ForegroundColor DarkGray } # Bygg kombinerte diagramdatastrenger for her-strengen $allChartLabels = hvis ($hasProjection) { "$trendLabels,$projLabels" } ellers { $trendLabels } $projDataJS = hvis ($hasProjection) { $projUpdated } ellers { "" } $projNotUpdJS = hvis ($hasProjection) { $projNotUpdated } annet { "" } $histCount = ($historyData | Mål-objekt). Telle $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | ForEach-Object { @{ name=$_. Nøkkel; total=$_. Value.Total; oppdatert=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Dybde 3 | Set-Content (koblingsbane $dataDir "manufacturers.json") -Koding UTF8 # Konverter JSON-datafiler til CSV for menneskelig lesbare Excel-nedlastinger Write-Host "Konvertere enhetsdata til CSV for Excel-nedlasting..." -Forgrunnsfarge grå foreach ($dfName i $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" hvis (testbane $jsonFile) { prøv { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json hvis ($jsonData.Count -gt 0) { # Inkluder ekstra kolonner for update_pending CSV $selectProps = hvis ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } ellers { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8 Write-Host " $dfName -> $($jsonData.Count) rader -> CSV" -ForegroundColor DarkGray } } catch { Write-Host " $dfName - skipped" -ForegroundColor DarkYellow } } } # Generer selvstendig HTML-instrumentbord $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Genererer selvstendig HTML-instrumentbord..." -Forgrunnsfarge gul # VELOCITY PROJECTION: Beregne fra skannelogg eller forrige sammendrag $stDeadline = [datetime]"2026-06-24" # KEK cert expiry $stDaysToDeadline = [matematikk]::Maks(0; ($stDeadline - (Hent-dato)). Dager) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "I/T" $stWorkingDays = 0 $stCalendarDays = 0 # Prøv trendhistorie først (lett, allerede vedlikeholdt av aggregator - erstatter oppsvulmet ScanHistory.json) hvis ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Totalt -gt 0 -og [int]$_. Oppdatert -ge 0 }) hvis ($validHistory.Count -ge 2) { $prev = $validHistory[-2]; $curr = $validHistory[-1] $prevDate = [datetime]::P arse($prev. Date.Substring(0, [Math]::Min(10, $prev. Date.Length))) $currDate = [datetime]::P arse($curr. Date.Substring(0, [Math]::Min(10, $curr. Date.Length))) $daysDiff = ($currDate - $prevDate). TotalDays hvis ($daysDiff -gt 0) { $updDiff = [int]$curr. Oppdatert - [int]$prev. Oppdatert hvis ($updDiff -gt 0) { $stDevicesPerDay = [matematikk]::Round($updDiff / $daysDiff, 0) $stVelocitySource = "TrendHistory" } } } } # Try orchestrator rollout summary (has pre-computed velocity) hvis ($stVelocitySource -eq "I/T" -og $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { prøv { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [matematikk]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = "Orchestrator" hvis ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } catch { } } # Tilbakefall: prøv forrige sammendrags-CSV (søk i gjeldende mappe OG overordnede/sideordnede aggregasjonsmapper) hvis ($stVelocitySource -eq "I/T") { $searchPaths = @( (Koblingsbane $OutputPath «SecureBoot_Summary_*.csv») ) # Søk også etter sideordnede aggregasjonsmapper (orchestrator oppretter en ny mappe hver kjøring) $parentPath = Split-Path $OutputPath -Parent hvis ($parentPath) { $searchPaths += (koblingsbane $parentPath "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += (koblingsbane $parentPath «SecureBoot_Summary_*.csv») } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descending | Select-Object –Første 1 hvis ($prevSummary) { prøv { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Hent-dato) - $prevDate). TotalDays hvis ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Updated - $prevUpdated hvis ($updDelta -gt 0) { $stDevicesPerDay = [matematikk]::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = «PreviousReport» } } } catch { } } } # Tilbakefall: beregne hastighet fra full trend historie span (første vs nyeste datapunkt) hvis ($stVelocitySource -eq "I/T" - og $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Totalt -gt 0 -og [int]$_. Oppdatert -ge 0 }) hvis ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime]::P arse($first. Date.Substring(0, [Math]::Min(10, $first. Date.Length))) $lastDate = [datetime]::P arse($last. Date.Substring(0, [Math]::Min(10, $last. Date.Length))) $daysDiff = ($lastDate - $firstDate). TotalDays hvis ($daysDiff -gt 0) { $updDiff = [int]$last. Oppdatert - [int]$first. Oppdatert hvis ($updDiff -gt 0) { $stDevicesPerDay = [matematikk]::Round($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # Beregn projeksjon ved hjelp av eksponentiell dobling (i samsvar med trenddiagram) # Bruk projeksjonsdataene som allerede er beregnet for diagrammet, hvis tilgjengelig hvis ($hasProjection -og $projDates.Count -gt 0) { # Bruk den siste forventede datoen (når alle enheter oppdateres) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Dager $stWorkingDays = 0 $d = Hent-dato for ($i = 0; $i -lt $stCalendarDays; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -og $stNotUptodate -gt 0) { # Tilbakefall: lineær projeksjon hvis ingen eksponentielle data er tilgjengelige $daysNeeded = [matematikk]::Tak($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Hent-dato). AddDays($daysNeeded). ToString("MMM dd, yyyy") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Hent-dato for ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Bygghastighet HTML $velocityHtml = hvis ($stDevicesPerDay -gt 0) { "<div><strong>🚀 Enheter/dag:</strong> $($stDevicesPerDay.ToString('N0')) (kilde: $stVelocitySource)</div>» + "<div><strong>📅 Forventet fullføring:</strong> $stProjectedDate" + $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>⚠ PAST DEADLINE</span>" } else { " <span style='color:#28a745'>✓ Before deadline</span>" }) + "</div>" + "<div><strong>🕐 Arbeidsdager:</strong> $stWorkingDays | <sterk>Calendar Days:</strong> $stCalendarDays</div>» + "<div style='font-size:.8em; color:#888'>Deadline: Jun 24, 2026 (KEK certificate expiry) | Gjenstående dager: $stDaysToDeadline</div>» } ellers { "<div style='padding:8px; bakgrunn:#fff3cd; kantlinjeradius:4px; kantlinje-venstre:3px heltrukket #ffc107'>" + "<strong>📅 Forventet fullføring:</strong> Utilstrekkelige data for hastighetsberegning. " + "Kjør aggregasjon minst to ganger med dataendringer for å etablere en rate.<br/>" + "<sterk>Deadline:</ strong> 24 juni 2026 (KEK sertifikat utløp) | <sterk>dager igjen:</strong> $stDaysToDeadline</div>» } # Cert utløpsnedtelling $certToday = Hent-dato $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematikk]::Maks(0; ($certKekExpiry - $certToday). Dager) $daysToUefi = [matematikk]::Maks(0; ($certUefiExpiry - $certToday). Dager) $daysToPca = [matematikk]::Maks(0; ($certPcaExpiry - $certToday). Dager) $certUrgency = hvis ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Hjelper: Les poster fra JSON, bygg samlingssammendrag + første N enhetsrader $maxInlineRows = 200 funksjon Build-InlineTable { param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 hvis (testbane $JsonPath) { prøv { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Telle # BUCKET SUMMARY: Grupper etter BucketId, vis antall per samling med oppdatert fra global samlingsstatistikk hvis ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | Sort-Object -Synkende $bucketSummary = "><2 h3 style='font-size:.95em; farge:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. Antall) samlinger)><4 /h3>» $bucketSummary += "><6 div style='max-height:300px; overflyt-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'>Oppdatert</th><th style='text-align:right; color:#dc3545'>Ikke oppdatert</th><th><1 Manufacturer><2 /th></tr></thead><tbody>" foreach ($b i $buckets) { $bid = hvis ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Hent oppdatert antall fra global samlingsstatistikk (alle enheter i denne samlingen på tvers av hele datasettet) $lookupKey = $bid $globalBucket = hvis ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } ellers { $null } $bUpdatedGlobal = hvis ($globalBucket) { $globalBucket.Updated } ellers { 0 } $bTotalGlobal = hvis ($globalBucket) { $globalBucket.Count } else { $b.Count } $bNotUpdatedGlobal = $bTotalGlobal - $bUpdatedGlobal $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; font-weight:bold'>$bTotalGlobal><8 /td><td style='text-align:right; color:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; color:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n' } $bucketSummary += "</tbody></table></div>" } # ENHETSDETALJER: Første N rader som flat liste $slice = $data | Select-Object –Første $MaxRows foreach ($d i $slice) { $conf = $d.ConfidenceLevel $confBadge = hvis ($conf -match "High") { '<span class="badge badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge badge-danger">Støttes ikke><6 /span>' } elseif ($conf -match "Under") { '<span class="badge badge-info">Under Obs><0 /span>' } elseif ($conf -match "Paused") { '<span class="badge badge-warning">Paused><4 /span>' } else { '<span class="badge badge-warning">Action Req><8 /span>' } $statusBadge = hvis ($d.IsUpdated) { '><00 span class="badge badge-success"><01 Oppdatert</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge badge-danger"><05 Error</span>' } else { '><08 span class="badge badge-warning"><09 Venter på><0 /span>' } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 // td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n' } } catch { } } hvis ($totalCount -eq 0) { returner «><44 div style='padding:20px; farge:#888; font-style:italic'><45 Ingen enheter i denne kategorien.><46 /div>» } $showing = [matematikk]::Min($MaxRows; $totalCount) $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Totalt: $($totalCount.ToString("N0"))-enheter" hvis ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; font-weight:bold'>📄 Last ned fullstendig CSV for Excel><3 /a>» } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; farge:#333; margin:10px 0 5px'><58 Enhetsdetaljer (viser første $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; overflyt-y:auto'><tabell><><tr><th><0 HostName><1 /th><th><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th><th><6 Status><7 /th><th><0 Error><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>" returner «$header$bucketSummary$deviceHeader$deviceTable» } # Bygg innebygde tabeller fra JSON-filene som allerede er på disken, og koble til CSV-er $tblErrors = Build-InlineTable -JsonPath (Join-Path $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath (join-path $dataDir "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath (join-path $dataDir "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath (Join-Path $dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath (Join-Path $dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath (join-path $dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath (Join-Path $dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath (Join-Path $dataDir "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath (Join-Path $dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath (join-path $dataDir "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath (join-path $dataDir "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath (join-path $dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath (Join-Path $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # Egendefinert tabell for ventende oppdateringer – omfatter UEFICA2023Status- og UEFICA2023Error-kolonner $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" hvis (testbane $upJsonPath) { prøv { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Count hvis ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Totalt: $($upCount.ToString("N0"))-enheter | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; skrifttykkelse:bold'>📄 Last ned fullstendig CSV for Excel><4 /a></div>» $upRows = "" $upSlice = $upData | Select-Object –Første $maxInlineRows foreach ($d i $upSlice) { $uefiSt = hvis ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' } $uefiErr = hvis ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' } $policyVal = hvis ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = hvis ($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 = [matematikk]::Min($maxInlineRows; $upCount) $upDevHeader = "<h3 style='font-size:.95em; farge:#333; margin:10px 0 5px'>Enhetsdetaljer (viser første $upShowing)</h3>» $upTable = "<div style='max-height:500px; overflyt-y:auto'><tabellen><><tr><th><9 HostName><0 /th><th><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2 023Error><6 /th><th><9 Policy</th><th>WinCS Key</th><th></th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>» $tblUpdatePending = "$upHeader$upDevHeader$upTable" } ellers { $tblUpdatePending = "<div style='padding:20px; farge:#888; font-style:italic'>Ingen enheter i denne category.</div>» } } catch { $tblUpdatePending = "<div style='padding:20px; farge:#888; font-style:italic'>Ingen enheter i denne kategorien.</div>» } } ellers { $tblUpdatePending = "<div style='padding:20px; farge:#888; font-style:italic'>Ingen enheter i denne kategorien.</div>» } # Cert utløpsnedtelling $certToday = Hent-dato $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematikk]::Maks(0; ($certKekExpiry - $certToday). Dager) $daysToUefi = [matematikk]::Maks(0; ($certUefiExpiry - $certToday). Dager) $daysToPca = [matematikk]::Maks(0; ($certPcaExpiry - $certToday). Dager) $certUrgency = hvis ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Bygg produsentdiagramdata innebygd (topp 10 etter enhetsantall) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | Select-Object –Første 10 $mfrChartTitle = hvis ($stMfrCounts.Count -le 10) { "By Manufacturer" } else { "Top 10 Manufacturers" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Key)'" }) -join "," $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join "," $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join "," $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) -join "," $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join "," $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join "," $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) -join "," $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join "," $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join "," $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join "," # Bygg produsenttabell $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Descending | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Nøkkel)</td><td>$($_. Value.Total.ToString("N0"))</td><td>$($_. Value.Updated.ToString("N0"))</td><td>$($_. Value.HighConf.ToString("N0"))><0 /td><td>$($_. Value.ActionReq.ToString("N0"))><4 /td></tr>'n" } # HTML-lukkekodefragmenter: delt inn i deler slik at cms-plattformer ikke # tolker dem som ekte HTML og injiserer usynlige Unicode-tegn rundt dem.$endScript = '</scr' + 'ipt>' $endStyle = '</sty' + 'le>' $endHead = '</he' + 'ad>' $endBody = '</bo' + 'dy>' $endHtml = '</ht' + 'ml>' $htmlContent = @" <! DOCTYPE HTML-> <html lang="en"> <hodet> <metategn="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <tittel>instrumentbordet for status for sikker oppstartssertifikat</title> <script src="https://cdn.jsdelivr.net/npm/chart.js">$endScript <stil> *{box-sizing:border-box; marg:0; utfylling:0} body{font-family:'Segoe UI',Tahoma,sans-serif; bakgrunn:#f0f2f5; farge:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); color:#fff; utfylling:20px 30px} .header h1{font-size:1.6em; marg-bunn:5px} .header .meta{font-size:.85em; ugjennomsiktighet:.9} .container{max-width:1400px; marg:0 auto; utfylling:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); mellomrom:12px; margin:20px 0} .card{background:#fff; kantlinjeradius:10px; utfylling:15px; boks-skygge:0 2px 8px rgba(0,0,0,.08); kantlinje-venstre:4px heldekkende #ccc;overgang:transformer .2s} .card:hover{transform:translateY(-2px); boks-skygge:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; skrifttykkelse:700} .card .label{font-size:.8em; farge:#666; margin-top:4px} .card .pct{font-size:.75em; farge:#888} .section{background:#fff; kantlinjeradius:10px; utfylling:20px; marg:15px 0; boksskygge:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; color:#1a237e; marg-bunn:10px; markør:peker; bruker-velg:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; mellomrom:20px; margin:20px 0} .chart-box{background:#fff; kantlinjeradius:10px; utfylling:20px; boksskygge:0 2px 8px rgba(0,0,0,.08)} table{width:100%; kantlinje-skjul:skjul; skriftstørrelse:.85em} th{background:#e8eaf6; utfylling:8px 10px; tekstjustering:venstre; posisjon:klistrel; topp:0; z-indeks:1} td{padding:6px 10px; heltrukket kantlinje:1px heldekkende #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; utfylling:2px 8px;kantlinjeradius:10px; skriftstørrelse:.75em; skrifttykkelse:700} .badge-success{background:#d4edda; farge:#155724} .badge-danger{background:#f8d7da; color:#721c24} .badge-warning{background:#fff3cd; farge:#856404} .badge-info{background:#d1ecf1; color:#0c5460} .top-link{float:right; skriftstørrelse:.8em; color:#1a237e; tekstdekorasjon:none} .footer{text-align:center; utfylling:20px; farge:#999; skriftstørrelse:.8em} a{color:#1a237e}$endStyle $endHead <brødtekst> <div class="header"> <h1>instrumentbord for status for sikker oppstartssertifikat</h1> <div class="meta">Generert: $($stats. ReportGeneratedAt) | Totalt antall enheter: $($c.Total.ToString("N0")) | Unike samlinger: $($stAllBuckets.Count)</div> </div> <div class="container">
<!-- KPI-kort – klikkbare, koblet til inndelinger – > <div class="cards"> <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; top:8px; right:8px; bakgrunn:#dc3545; color:#fff; utfylling:1px 6px; kantlinjeradius:8px; skriftstørrelse:.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)% - TRENGER HANDLING><0 /div></a><3 <a class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; top:8px; right:8px; bakgrunn:#28a745; color:#fff; utfylling:1px 6px; kantlinjeradius:8px; skriftstørrelse:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString("N0"))</div><div class="label">Oppdatert><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3 <a class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; top:8px; right:8px; bakgrunn:#6c757d; color:#fff; utfylling:1px 6px; kantlinjeradius:8px; skriftstørrelse:.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})% - Utenfor omfanget><0 /div></a><3 <a class="card" href="#s-nrb" onclick="openSection('d-nrb')" style="border-left-color:#ffc107; text-decoration:none"><div class="value" style="color:#ffc107">$($c.NeedsReboot.ToString("N0"))</div><div class="label">Needs Reboot><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% – venter på omstart><6 /div></a><9 <a class="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Update Venter</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Policy/WinCS brukt, venter på oppdatering><2 /div></a><5 <a class="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Rollout In Progress><4 /div><div class="pct">Rollout In Progress><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{0})%</div></a><11 <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0"))</div><div class="label">High Confidence><20 /div><div class="pct">$($stats. PercentHighConfidence)% – trygt for utrulling><24 /div></a><27 <a class="card" href="#s-uo" onclick="openSection('d-uo')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value" style="color:#ffc107"><1 $($c.UnderObs.ToString("N0"))><2 /div><div class="label"><5 Under Observation><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0"))</div><div class="label">Action Required><2 /div><div class="pct">$($stats. PercentActionRequired)% – må teste><6 /div></a><9 <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($stAtRisk.ToString("N0"))</div><div class="label">At Risk><68 /div><div class="pct">$($stats. PercentAtRisk)% - Ligner på mislykkede><2 /div></a><5 <a class="card" href="#s-td" onclick="openSection('d-td')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.TaskDisabled.ToString("N0"))</div><div class="label">Task Disabled><4 /div><div class ="pct">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})% - Blocked><8 /div></a><91 <a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.TempPaused.ToString("N0"))</div><div class="label">Temp. Midlertidig stanset</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Known Issues><6 /div><div class ="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0"))</div><div class="label">Missing KEK</div><div class ="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithErrors.ToString("N0"))</div><div class="label">With Errors</div><div class="pct"><1 $($stats. PercentWithErrors)% - UEFI-feil</div></a> ><6 a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545"><9 $($c.TempFailures.ToString("N0"))</div><div class="label">Temp. Feil</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0"))</div><div class="label">Støttes ikke><6 /div><div class=""pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
<!-- distribusjonshastighet & Cert --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; mellomrom:20px; marg:15px 0"> <div class="section" style="margin:0"> <h2>📅 Distribusjonshastighet</h2> ><2 div class="section-body open"><3 ><4 div style="font-size:2.5em; skrifttykkelse:700; color:#28a745"><5 $($c.Updated.ToString("N0"))><6 /div> ><8 div style="color:#666"><9 devices updated out of $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; bakgrunn:#e8eaf6; høyde: 20px; kantlinjeradius:10px; overflyt:hidden"><div style="background:#28a745; høyde:100%; bredde:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% fullført</div> <div style="margin-top:10px; utfylling:10px; bakgrunn:#f8f9fa; kantlinjeradius:8px; font-size:.85em"> <div><sterke>remaining:</strong> $($stNotUptodate.ToString("N0")-enheter trenger handling</div> <div><sterk>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated)-enheter (feil + permanent + oppgave deaktivert)</div> <div><strong>Safe to deploy:</strong> $($stSafeList.ToString("N0"))-enheter (samme samling som vellykket)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; kantlinje-venstre:4px heldekkende #dc3545"> <h2 style="color:#dc3545">⚠ Certificate Expiry Countdown</h2> <div class="section-body open"> <div style="display:flex; mellomrom:15px; margin-top:10px"> <div style="text-align:center; utfylling:15px; kantlinjeradius:8px; min-bredde:120px; background:linear-gradient(135deg,#fff5f5,#ffe0e0); kantlinje:2px heldekkende #dc3545; flex:1"> <div style="font-size:.65em; color:#721c24; teksttransformering:store bokstaver; font-weight:bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size:.85em; skrifttykkelse:fet; color:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; skrifttykkelse:700; color:#dc3545; linjehøyde:1"><9 $daysToKek><0 /div><1 <div style="font-size:.8em; color:#721c24">dager (24. juni 2026)</div><5 </div><7 <div style="text-align:center; utfylling:15px; kantlinjeradius:8px; min-bredde:120px; background:linear-gradient(135deg,#fffef5,#fff3cd); kantlinje:2px heldekkende #ffc107; flex:1"> ><00 div style="font-size:.65em; farge:#856404; teksttransformering:store bokstaver; font-weight:bold"><01 UEFI CA 2011</div> ><04 div id="daysUefi" style="font-size:2.2em; skrifttykkelse:700; farge:#856404; linjehøyde:1; margin:5px 0"><05 $daysToUefi</div> ><08 div style="font-size:.8em; color:#856404"><09 days (Jun 27, 2026)><10 /div> ><12 /div> ><14 div style="text-align:center; utfylling:15px; kantlinjeradius:8px; min-bredde:120px; background:linear-gradient(135deg,#f0f8ff,#d4edff); kantlinje:2px heldekkende #0078d4; flex:1"><15 ><16 div style="font-size:.65em; color:#0078d4; teksttransformering:store bokstaver; font-weight:bold"><17 Windows PCA</div> ><20 div id="daysPca" style="font-size:2.2em; skrifttykkelse:700; color:#0078d4; linjehøyde:1; margin:5px 0"><21 $daysToPca><2 /div><3 ><24 div style="font-size:.8em; color:#0078d4"><25 dager (19. oktober 2026)><26 /div><7 ><28 /div><9 ><30 /div><1 ><32 div style="margin-top:15px; utfylling:10px; bakgrunn:#f8d7da; kantlinjeradius:8px; font-size:.85em; kantlinje-venstre:4px heldekkende #dc3545"><33 ><34 sterk>⚠ KRITISK:><37 /strong> Alle enheter må oppdateres før sertifikatet utløper. Enheter som ikke er oppdatert innen tidsfristen, kan ikke bruke fremtidige sikkerhetsoppdateringer for Oppstartsbehandling og Sikker oppstart etter utløp.</div> </div> </div> </div>
<!-- --> <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>
$(hvis ($historyData.Count -ge 1) { "<!-- historisk trenddiagram --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Oppdater fremdrift over tid <en klasse='top-link' href='#'>↑ Øverste</a></h2> <div id='d-trend' class='section-body open'> <lerrets-ID='trendDiagram' høyde='120'></canvas> <div style='font-size:.75em; farge:#888; margin-top:5px'>Heltrukket linje = faktiske data$(hvis ($historyData.Count -ge 2) { " | Stiplet linje = forventet (eksponentiell dobling: 2→4→8→16... enheter per bølge)" } annet { " | Kjør aggregasjon på nytt i morgen for å se trendlinjer og projeksjon" })</div> </div> </div>» })
<!-- CSV-nedlastinger > <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Last ned fullstendige data (CSV for Excel) <a class="top-link" href="#">Top</a></h2> <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; mellomrom:5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; bakgrunn:#dc3545; color:#fff; utfylling:6px 14px; kantlinjeradius:5px; text-decoration:none; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0"))</a> <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; bakgrunn:#dc3545; color:#fff; utfylling:6px 14px; kantlinjeradius:5px; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0"))</a><2 <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; bakgrunn:#fd7e14; color:#fff; utfylling:6px 14px; kantlinjeradius:5px; text-decoration:none; font-size:.8em">Handling kreves ($($c.ActionReq.ToString("N0")))</a><6 <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; bakgrunn:#dc3545; color:#fff; utfylling:6px 14px; kantlinjeradius:5px; text-decoration:none; font-size:.8em">Kjente problemer ($($c.WithKnownIssues.ToString("N0")))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; bakgrunn:#dc3545; color:#fff; utfylling:6px 14px; kantlinjeradius: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; bakgrunn:#28a745; color:#fff; utfylling:6px 14px; kantlinjeradius:5px; text-decoration:none; font-size:.8em">oppdatert ($($c.Updated.ToString("N0"))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; bakgrunn:#6c757d; color:#fff; utfylling:6px 14px; kantlinjeradius:5px; text-decoration:none; font-size:.8em">Summary</a> <div style="width:100%; skriftstørrelse:.75em; farge:#888; margin-top:5px">CSV-filer åpnes i Excel. Tilgjengelig når det driftes på web server.</div> </div> </div>
<!--–> <div class="section"> <h2 onclick="toggle('mfr')">By Manufacturer <a class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <tabell><><><.><1><2 /th><th><5 Total><6 /th><th><9 Updated><9><0 /th><th><3 High Confidence><4 /th><th><7 Action Required><8 /th></tr></thead><3 <><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- Enhetsinndelinger (første 200 innebygde + CSV-nedlasting) - > <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Enheter med feil ($($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">🔴 Kjente problemer ($($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')">🟠 Mangler KEK - Event 1803 ($($c.WithMissingKEK.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> >↑ 0 div id="d-kek" class="section-body">↑ 1 $tblKEK</div> >↑ 4 /div> >↑ 6 div class="section" id="s-ar">↑ 7 >↑ 8 h2 onclick="toggle('d-ar')" style="color:#fd7e14">🟠 Handling kreves ($($c.ActionReq.ToString("N0")) <a class="top-link" href="#">↑ Top><4 /a></h2><7 <div id="d-ar" class="section-body">$tblActionReq</div> </div> <div class="section" id="s-uo"> <h2 onclick="toggle('d-uo')" style="color:#17a2b8">🔵 Under Observasjon ($($c.UnderObs.ToString("N0")) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-uo" class="section-body">$tblUnderObs</div> </div> <div class="section" id="s-nu"> <h2 onclick="toggle('d-nu')" style="color:#dc3545">🔴 Ikke oppdatert ($($stNotUptodate.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-nu" class="section-body">$tblNotUpd</div> </div> >↑ 0 div class="section" id="s-td">↑ 1 >↑ 2 h2 onclick="toggle('d-td')" style="color:#dc3545">🔴 Aktivitet deaktivert ($($c.TaskDisabled.ToString("N0"))) >↑ 5 a class="top-link" href="#">↑ Top</a></h2><1 <div id="d-td" class="section-body">$tblTaskDis><4 /div><5 </div><7 <div class="section" id="s-tf"> <h2 onclick="toggle('d-tf')" style="color:#dc3545">🔴 Midlertidige feil ($($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">🔴 Permanente feil / støttes ikke ($($c.PermFailures.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-pf" class="section-body">$tblPerm</div> </div> <div class="section" id="s-upd-pend"> <h2 onclick="toggle('d-upd-pend')" style="color:#6f42c1">⏳ Update Pending ($($c.UpdatePending.ToString("N0"))) - Policy/WinCS brukt, Venter på oppdatering <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Devices where AvailableUpdatesPolicy or WinCS key is applied but UEFICA2023Status is still NotStarted, InProgress, or null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Utrulling pågår ($($c.RolloutInProgress.ToString("N0"))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-rip" class="section-body">$tblRolloutIP</div> </div> <div class="section" id="s-sboff"> <h2 onclick="toggle('d-sboff')" style="color:#6c757d">⚫ SecureBoot OFF ($($c.SBOff.ToString("N0"))) - Utenfor omfanget <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">🟢 Oppdaterte enheter ($($c.Updated.ToString("N0")) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd" class="section-body">$tblUpdated</div> </div> <div class="section" id="s-nrb"> <h2 onclick="toggle('d-nrb')" style="color:#ffc107">🔄 Oppdatert - Trenger omstart ($($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">instrumentbord for utrulling av sertifikat for sikker oppstart | Genererte $($stats. ReportGeneratedAt) | StreamingMode | Topp minne: ${stPeakMemMB} MB</div> </div><!-- /container -->
<skript> function toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} function openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} nytt diagram(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Handling kreves','Temp. Midlertidig stanset,'Støttes ikke','SecureBoot OFF','Med feil'],datasett:[{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','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24 #20c997','#adb5bd','#dc3545']}]},alternativer:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); nytt diagram(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. Midlertidig stanset,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'}}}); Historisk trenddiagram 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); hvis (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdData); } var-datasett = [ {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} ]; hvis (projLen > 0) { datasets.push({label:'Projected Updated (2x doubling)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } nytt diagram(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display:true,text:'Devices'}},x:{title:{display:true,text:'Date'}}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}); } Dynamisk nedtelling (function(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19'); var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0,Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0,Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0,Math.ceil((p-t)/864e5))})();$endScript $endBody $endHtml "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Behold alltid en stabil «Nyeste»-kopi, slik at administratorer ikke trenger å spore tidsstempler $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # Lagre filmanifest for trinnvis modus (rask gjenkjenning uten endring ved neste kjøring) hvis ($IncrementalMode -eller $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 "Lagrer filmanifest for trinnvis modus..." -Forgrunnsfarge grå foreach ($jf i $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Størrelse = $jf. Lengde } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Lagret manifest for $($stNewManifest.Count)-filer" -ForegroundColor DarkGray } # OPPBEVARINGSOPPRYDDING # Orchestrator gjenbrukbar mappe (Aggregation_Current): behold bare siste kjøring (1) # Admin manuelle kjøringer / andre mapper: behold de siste 7 kjøringene # CSV-er for sammendrag slettes ALDRI – de er små (~1 kB) og er sikkerhetskopikilden for trendloggen $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = hvis ($outputLeaf -eq 'Aggregation_Current') { 1 } ellers { 7 } # Filprefikser trygt å rydde opp i (flyktige øyeblikksbilder per kjøring) $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_" ) # Finn alle unike tidsstempler bare fra filer som kan rengjøres $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. Navn; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Antall -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { hvis ($_. Navn -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Unique -Descending) hvis ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object -Hopp over $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs i $oldTimestamps) { foreach ($prefix i $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f i $oldFiles) { $freedBytes += $f.Length Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [matematikk]::Round($freedBytes / 1 MB, 1) Write-Host "Oppbevaringsopprydding: fjernet $removedFiles filer fra $($oldTimestamps.Count) gamle kjøringer, frigjort ${freedMB} MB (holder siste $retentionCount + alle sammendrag / NotUptodate CSV)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -Forgrunnsfarge Cyan Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -ForegroundColor Cyan Write-Host " Totalt antall enheter: $($c.Total.ToString("N0")) -Forgrunnsfarge hvit Write-Host " IKKE OPPDATERT: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(hvis ($stNotUptodate -gt 0) { "Gul" } ellers { "Grønn" }) Write-Host " Oppdatert: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -ForegroundColor Green Write-Host " Med feil: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(hvis ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Peak Memory: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Tid: $([math]::Round($stTotal/60,1)) min" -ForegroundColor White Write-Host « Instrumentbord: $htmlPath» –Forgrunnsfarge hvit returner [PSCustomObject]$stats } #endregion STRØMMINGSMODUS } ellers { Write-Error "Finner ikke inndatabane: $InputPath" avslutt 1 }