Kopieer en plak dit voorbeeldscript en wijzig deze indien nodig voor uw omgeving:
<# . SYNOPSIS Hiermee worden JSON-gegevens van de Status van Beveiligd opstarten van meerdere apparaten samengevoegd in overzichtsrapporten.
. BESCHRIJVING Hiermee leest u de verzamelde JSON-bestanden met de status beveiligd opstarten en genereert u: - HTML-dashboard met grafieken en filteren - Samenvatting door ConfidenceLevel - Unieke analyse van apparaat-buckets voor teststrategie Ondersteunt: - Bestanden per machine: HOSTNAME_latest.json (aanbevolen) - Eén JSON-bestand Ontdubbelt automatisch op HostName, met behoud van de meest recente CollectionTime. Standaard bevat alleen apparaten met 'Actie-req' of 'Hoog' betrouwbaarheid om te focussen op actie-buckets. Gebruik -IncludeAllConfidenceLevels om te overschrijven.
. PARAMETER InputPath Pad naar JSON-bestand(en): - Map: leest alle *_latest.json bestanden (of *.json als er geen _latest bestanden zijn) - Bestand: leest één JSON-bestand
. PARAMETER OutputPath Pad voor gegenereerde rapporten (standaard: .\SecureBootReports)
. VOORBEELD # Aggregeren uit map met bestanden per machine (aanbevolen) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Reads: \\contoso\SecureBootLogs$\*_latest.json
. VOORBEELD # Aangepaste uitvoerlocatie .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. VOORBEELD # Alleen actie-req en hoge betrouwbaarheid (standaardgedrag) opnemen .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Sluit uit: observatie, onderbroken, niet ondersteund
. VOORBEELD # Alle betrouwbaarheidsniveaus opnemen (onderdrukkingsfilter) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. VOORBEELD # Aangepast filter voor betrouwbaarheidsniveau .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. VOORBEELD # ENTERPRISE SCALE: Incrementele modus - alleen gewijzigde bestanden verwerken (snelle volgende uitvoeringen) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Eerste uitvoering: Volledige belasting ~2 uur voor 500.000 apparaten # Volgende uitvoeringen: seconden als er geen wijzigingen zijn, minuten voor delta's
. VOORBEELD # HTML overslaan als er niets is gewijzigd (snelste voor bewaking) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Als er geen bestanden zijn gewijzigd sinds de laatste uitvoering: ~5 seconden
. VOORBEELD #Alleen samenvattingsmodus: grote apparaattabellen overslaan (1-2 minuten versus 20+ minuten) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Genereert CSV's, maar slaat HTML-dashboard met volledige apparaattabellen over
. NOTITIES Koppelen met Detect-SecureBootCertUpdateStatus.ps1 voor bedrijfsimplementatie.Zie GPO-DEPLOYMENT-GUIDE.md voor de volledige implementatiehandleiding. Standaardgedrag sluit observatie, onderbroken en niet-ondersteunde apparaten uit om de rapportage alleen te richten op bruikbare apparaat-buckets.#>
param( [Parameter(Verplicht = $true)] [tekenreeks]$InputPath, [Parameter(Verplicht = $false)] [string]$OutputPath = ".\SecureBootReports", [Parameter(Verplicht = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Verplicht = $false)] [tekenreeks]$RolloutStatePath, # Pad naar RolloutState.json om InProgress-apparaten te identificeren [Parameter(Verplicht = $false)] [tekenreeks]$RolloutSummaryPath, # Pad naar SecureBootRolloutSummary.json van Orchestrator (bevat projectiegegevens) [Parameter(Verplicht = $false)] [tekenreeks[]]$IncludeConfidenceLevels = @("Actie vereist", "Hoge betrouwbaarheid"), # Neem alleen deze betrouwbaarheidsniveaus op (standaard: alleen actie-buckets) [Parameter(Verplicht = $false)] [switch]$IncludeAllConfidenceLevels, # Override filter om alle betrouwbaarheidsniveaus op te nemen [Parameter(Verplicht = $false)] [switch]$SkipHistoryTracking, [Parameter(Verplicht = $false)] [switch]$IncrementalMode, # Schakel deltaverwerking in - alleen gewijzigde bestanden laden sinds de laatste uitvoering [Parameter(Verplicht = $false)] [string]$CachePath, # Pad naar cachemap (standaard: OutputPath\.cache) [Parameter(Verplicht = $false)] [int]$ParallelThreads = 8, # Aantal parallelle threads voor het laden van bestanden (PS7+) [Parameter(Verplicht = $false)] [switch]$ForceFullRefresh, # Forceer volledig opnieuw laden, zelfs in incrementele modus [Parameter(Verplicht = $false)] [switch]$SkipReportIfUnchanged, # HTML/CSV-generatie overslaan als er geen bestanden zijn gewijzigd (alleen uitvoerstatistieken) [Parameter(Verplicht = $false)] [switch]$SummaryOnly, # Genereer alleen samenvattingsstatistieken (geen grote apparaattabellen) - veel sneller [Parameter(Verplicht = $false)] [switch]$StreamingMode # Modus Voor efficiënt geheugen: segmenten verwerken, CSV's incrementeel schrijven, alleen samenvattingen in het geheugen bewaren )
# Automatisch verhogen naar PowerShell 7 indien beschikbaar (6x sneller voor grote gegevenssets) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ($pwshPath) { Write-Host 'PowerShell $($PSVersionTable.PSVersion) gedetecteerd - opnieuw starten met PowerShell 7 voor snellere verwerking...' -ForegroundColor Yellow # Argumentenlijst opnieuw opbouwen van afhankelijke parameters $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key in $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] if ($val -is [switch]) { if ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [matrix]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } else { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs $LASTEXITCODE afsluiten } }
$ErrorActionPreference = Doorgaan $timestamp = Get-Date -Format "jjjjMMd-HHmmss" $scanTime = Get-Date -Format "jjjj-MM-dd uu:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Implementatie- en bewakingsvoorbeelden"
# Opmerking: dit script heeft geen afhankelijkheden van andere scripts. # Download voor de volledige toolset van: $DownloadUrl -> $DownloadSubPage
#region instellen Write-Host "=" * 60 -VoorgrondKleur Cyaan Write-Host "Secure Boot Data Aggregation" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan
# Uitvoermap maken if (-not (testpad $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }
# Gegevens laden - ondersteunt CSV-indelingen (verouderd) en JSON (systeemeigen) Write-Host "'nGegevens laden van: $InputPath" -ForegroundColor Yellow
# Helper-functie om het apparaatobject te normaliseren (veldnaamverschillen verwerken) functie Normalize-DeviceRecord { param($device) # Handle Hostname versus HostName (JSON gebruikt Hostname, CSV maakt gebruik van HostName) if ($device. PSObject.Properties['Hostname'] -en -niet $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostnaam -Forceren } # Handle Confidence vs ConfidenceLevel (JSON maakt gebruik van Betrouwbaarheid, CSV maakt gebruik van ConfidenceLevel) # ConfidenceLevel is de officiële veldnaam - wijs Betrouwbaarheid toe aan het if ($device. PSObject.Properties['Betrouwbaarheid'] -en -niet $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Betrouwbaarheid - Forceren } # Updatestatus bijhouden via Event1808Count OR UEFICA2023Status="Bijgewerkt" # Hiermee kunt u bijhouden hoeveel apparaten in elke betrouwbaarheids bucket zijn bijgewerkt $event 1808 = 0 if ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false if ($device. PSObject.Properties['UEFICA2023Status'] -en $device. UEFICA2023Status -eq "Bijgewerkt") { $uefiCaUpdated = $true } if ($event 1808 -gt 0 -of $uefiCaUpdated) { # Markeren als bijgewerkt voor dashboard-/implementatielogica- maar Niet overschrijven ConfidenceLevel $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } else { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # ConfidenceLevel-classificatie: # - "Hoge betrouwbaarheid", "Onder observatie...", "Tijdelijk onderbroken...", "Niet ondersteund..." = gebruiken als-is # - Al het andere (null, leeg, "UpdateType:...", "Onbekend", "N/B") = valt onder Actie Vereist in tellers # Er is geen normalisatie nodig: de else-vertakking van de streamingteller verwerkt dit } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON maakt gebruik van OEM*, verouderd gebruikt WMI_*) if ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force } # Handle OEMModelNumber versus WMI_Model if ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force } # Handle FirmwareVersion versus BIOSDescription if ($device. PSObject.Properties['FirmwareVersion'] -en -niet $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } $device retourneren }
#region incrementele verwerking/cachebeheer # Cachepaden instellen if (-not $CachePath) { $CachePath = Join-Path $OutputPath '.cache' } $manifestPath = Join-Path $CachePath 'FileManifest.json' $deviceCachePath = Join-Path $CachePath 'DeviceCache.json'
# Cachebeheerfuncties functie Get-FileManifest { param([string]$Path) if (testpad $Path) { probeer { $json = Get-Content $Path -Raw | ConvertFrom-Json # PSObject converteren naar hashtabel (ps5.1 compatibel - PS7 heeft -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Naam] = $_. Waarde } $ht retourneren } catch { @{} retourneren } } @{} retourneren }
functie Save-FileManifest { param([hashtable]$Manifest, [string]$Path) $dir = Split-Path $Path -Bovenliggend if (-not (testpad $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $Manifest | ConvertTo-Json -Diepte 3 -Comprimeren | Set-Content $Path -Forceren }
functie Get-DeviceCache { param([string]$Path) if (testpad $Path) { probeer { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host 'Geladen apparaatcache: $($cacheData.Count) apparaten' -ForegroundColor DarkGray $cacheData retourneren } catch { Write-Host 'Cache beschadigd, wordt opnieuw opgebouwd' -ForegroundColor Yellow retour @() } } retour @() }
functie Save-DeviceCache { param($Devices; [tekenreeks]$Path) $dir = Split-Path $Path -Bovenliggend if (-not (testpad $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } # Converteren naar matrix en opslaan $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Diepte 10 -Comprimeren | Set-Content $Path -Forceren Write-Host "Opgeslagen apparaatcache: $($deviceArray.Count) apparaten" -ForegroundColor DarkGray }
functie Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Niet-hoofdlettergevoelig opzoeken vanuit manifest (normaliseren naar kleine letters) $manifestLookup = @{} foreach ($mk in $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file in $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Pad naar kleine letters normaliseren $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Grootte = $file. Lengte } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] if ($cached. LastWriteTimeUtc -eq $lwt -en $cached. Size -eq $file. Lengte) { [void]$unchanged. Toevoegen($file) Blijven } } [void]$changed. Toevoegen($file) } @{ retourneren Gewijzigd = $changed Ongewijzigd = $unchanged NewManifest = $newManifest } }
# Ultrasnelle parallelle bestanden laden met behulp van batchverwerking functie Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Tellen # Gebruik batches van ~1000 bestanden elk voor een beter geheugenbeheer $batchSize = [wiskunde]::Min(1000; [wiskunde]::Plafond($totalFiles / [wiskunde]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
voor ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [wiskunde]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Toevoegen($batch) } Write-Host " ($($batches. Aantal) batches van ~$batchSize bestanden elk)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # Controleer of PowerShell 7+ parallel beschikbaar is $canParallel = $PSVersionTable.PSVersion.Major -ge 7 if ($canParallel -en $Threads -gt 1) { # PS7+: Batches parallel verwerken $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file in $batchFiles) { probeer { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } catch { } } $batchResults.ToArray() } foreach ($batch in $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } else { # PS5.1 fallback: sequentiële verwerking (nog steeds snel voor <10K-bestanden) foreach ($file in $Files) { probeer { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } catch { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Eén JSON-bestand if ($InputPath -like "*.json") { $jsonContent = Get-Content -Path $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host 'Geladen $($allDevices.Count)-records uit bestand' } else { Write-Error 'Alleen de JSON-indeling wordt ondersteund. Het bestand moet de extensie .json hebben.' afsluiten 1 } } elseif (Test-Path $InputPath -PathType-container) { # Map - alleen JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Geef de voorkeur aan *_latest.json bestanden als ze bestaan (modus per machine) $latestJson = $jsonFiles | Where-Object { $_. Naam -like "*_latest.json" } if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count if ($totalFiles -eq 0) { Write-Error 'Geen JSON-bestanden gevonden in: $InputPath' afsluiten 1 } Write-Host 'Gevonden $totalFiles JSON-bestanden' -ForegroundColor Gray # Helper-functie om de betrouwbaarheidsniveaus te vinden (verwerkt zowel korte als volledige formulieren) # Vroeg gedefinieerd, zodat zowel StreamingMode als normale paden deze kunnen gebruiken functie Test-ConfidenceLevel { param([string]$Value, [tekenreeks]$Match) if ([string]::IsNullOrEmpty($Value)) { return $false } schakelaar ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") } "Tijdelijkpaused" { return $Value -like "Tijdelijk onderbroken*" } "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") } standaard { return $false } } } #region STREAMINGMODUS - Geheugenefficiënte verwerking voor grote gegevenssets # Gebruik altijd StreamingMode voor geheugenefficiënte verwerking en dashboard in nieuwe stijl if (-not $StreamingMode) { Write-Host 'StreamingMode automatisch inschakelen (dashboard nieuwe stijl)' -ForegroundColor Yellow $StreamingMode = $true if (-not $IncrementalMode) { $IncrementalMode = $true } } # Wanneer -StreamingMode is ingeschakeld, verwerkt u bestanden in segmenten en houdt u alleen tellers in het geheugen.# Gegevens op apparaatniveau worden per segment naar JSON-bestanden geschreven voor het laden op aanvraag in het dashboard.# Geheugengebruik: ~1,5 GB, ongeacht de grootte van de gegevensset (versus 10-20 GB zonder streaming).if ($StreamingMode) { Write-Host 'STREAMING MODE enabled - memory-efficient processing' -ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTELE CONTROLE: Als er geen bestanden zijn gewijzigd sinds de laatste uitvoering, slaat u de verwerking volledig over if ($IncrementalMode -and -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath '.cache' $stManifestPath = Join-Path $stManifestDir 'StreamingManifest.json' if (testpad $stManifestPath) { Write-Host 'Controleren op wijzigingen sinds laatste streaming-uitvoering...' -ForegroundColor Cyan $stOldManifest = Get-FileManifest -Pad $stManifestPath if ($stOldManifest.Count -gt 0) { $stChanged = $false # Snelle controle: hetzelfde aantal bestanden? if ($stOldManifest.Count -eq $totalFiles) { # Controleer de 100 NIEUWSTE bestanden (gesorteerd op LastWriteTime aflopend) # Als een bestand is gewijzigd, heeft het de meest recente tijdstempel en wordt het eerst weergegeven $sampleSize = [wiskunde]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Aflopend | Select-Object - Eerste $sampleSize foreach ($sf in $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Breken } # Tijdstempels vergelijken - in de cache kan Datum/tijd of tekenreeks na JSON roundtrip worden opgeslagen $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc probeer { # Als de cache al DateTime is (ConvertFrom-Json automatisch converteert), gebruikt u rechtstreeks if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } else { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([wiskunde]:Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Breken } } catch { $stChanged = $true Breken } } } else { $stChanged = $true } if (-not $stChanged) { # Controleer of uitvoerbestanden bestaan $stSummaryExists = Get-ChildItem (Join-Path $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -Eerste 1 $stDashExists = Get-ChildItem (Join-Path $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -Eerste 1 if ($stSummaryExists -en $stDashExists) { Write-Host "Geen wijzigingen gedetecteerd ($totalFiles bestanden ongewijzigd) - verwerking overslaan" -VoorgrondKleur Groen Write-Host 'Laatste dashboard: $($stDashExists.FullName)' -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConverterenVan-CSV Write-Host " Apparaten: $($cachedStats.TotalDevices) | Bijgewerkt: $($cachedStats.Updated) | Fouten: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host " Voltooid in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (geen verwerking nodig)" -ForegroundColor Green $cachedStats retourneren } } else { # DELTA PATCH: Zoek precies welke bestanden zijn gewijzigd Write-Host "Wijzigingen gedetecteerd - gewijzigde bestanden identificeren..." -ForegroundColor Yellow $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf in $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } else { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc probeer { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([wiskunde]:Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } catch { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [wiskunde]::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host gewijzigd: $($changedFiles.Count) | Nieuw: $($newFiles.Count) | Totaal: $totalChanged ($changePct%)" -VoorgrondKleur Geel if ($totalChanged -gt 0 -en $changePct -lt 10) { # DELTA PATCH MODE: <10% gewijzigd, patch bestaande gegevens Write-Host " Delta patch mode ($changePct% < 10%) - patching $totalChanged bestanden..." -ForegroundColor Green $dataDir = Join-Path $OutputPath 'gegevens' # Gewijzigde/nieuwe apparaatrecords laden $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df in $allDeltaFiles) { probeer { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData if ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } catch { } } Write-Host 'Geladen $($deltaDevices.Count) gewijzigde apparaatrecords' -ForegroundColor Gray # Voor elke categorie JSON: oude vermeldingen verwijderen voor gewijzigde hostnamen, nieuwe vermeldingen toevoegen $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 in $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat in $categoryFiles) { $catPath = Join-Path $dataDir '$cat.json' if (testpad $catPath) { probeer { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Oude vermeldingen verwijderen voor gewijzigde hostnamen $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Elk gewijzigd apparaat opnieuw classificeren in categorieën # (wordt hieronder toegevoegd na classificatie) $catData | ConvertTo-Json -Diepte 5 | Set-Content $catPath -UTF8 coderen } catch { } } } # Elk gewijzigd apparaat classificeren en toevoegen aan de juiste categoriebestanden foreach ($dev in $deltaDevices.Values) { $slim = [besteld]@{ HostName = $dev. Hostname WMI_Manufacturer = if ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } anders { "" } WMI_Model = if ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } else { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } else { "" } ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. IsUpdated UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } anders { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } else { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } else { $null } SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) -en $dev. UEFICA2023Error -ne "0" -en $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -or $dev. SecureBootTaskStatus -eq 'Disabled' -of $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = if ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } else { 0 } $e 1808 = if ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } else { 0 } $e 1803 = if ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -of $dev. MissingKEK -eq $true) $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = if ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" } # Toevoegen aan overeenkomende categoriebestanden $targets = @() if ($isUpd) { $targets += "updated_devices" } if ($hasErr) { $targets += "errors" } if ($hKI) { $targets += "known_issues" } if ($mKEK) { $targets += "missing_kek" } if (-not $isUpd -and $sbOn) { $targets += "not_updated" } if ($tskDis) { $targets += "task_disabled" } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'Tijdelijkpaused'))) { $targets += "temp_failures" } if (-niet $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -of ($tskNF -and $hasErr))) { $targets += "perm_failures" } if (-not $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" } if (-not $sbOn) { $targets += "secureboot_off" } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } foreach ($tgt in $targets) { $tgtPath = Join-Path $dataDir '$tgt.json' if (testpad $tgtPath) { $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Diepte 5 | Set-Content $tgtPath -UTF8 coderen } } } # CSV's opnieuw genereren van gepatchte JSON's Write-Host "CSV's regenereren van gepatchte gegevens..." -ForegroundColor Gray $newTimestamp = Get-Date -Format "jjjjMMMd-HHmmss" foreach ($cat in $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (testpad $catJsonPath) { probeer { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8 } } catch { } } } # Statistieken van de gepatchte JSON-bestanden tellen Write-Host "Samenvatting herberekenen van gepatchte gegevens..." -ForegroundColor Gray $patchedStats = [order]@{ ReportGeneratedAt = (Get-Date). ToString("jjjj-MM-dd UU:mm:ss") } $pTotal = 0; $pUpdated = 0; $pErrors = 0; $pKI = 0; $pKEK = 0 $pTaskDis = 0; $pTempFail = 0; $pPermFail = 0; $pActionReq = 0; $pSBOff = 0; $pRIP = 0 foreach ($cat in $categoryFiles) { $catPath = Join-Path $dataDir '$cat.json' $cnt = 0 if (testpad $catPath) { probeer { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } catch { } } schakelaar ($cat) { "updated_devices" { $pUpdated = $cnt } "errors" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # berekend "task_disabled" { $pTaskDis = $cnt } "temp_failures" { $pTempFail = $cnt } "perm_failures" { $pPermFail = $cnt } "action_required" { $pActionReq = $cnt } "secureboot_off" { $pSBOff = $cnt } "rollout_inprogress" { $pRIP = $cnt } } } $pNotUpdated = (Get-Content (Join-Path $dataDir "not_updated.json") -Raw | ConvertFrom-Json). Tellen $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host "Delta-patch voltooid: $totalChanged apparaten bijgewerkt" -ForegroundColor Green Totaal van Write-Host: $pTotal | Bijgewerkt: $pUpdated | NotUpdated: $pNotUpdated | Fouten: $pErrors" -ForegroundColor White # Manifest bijwerken $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf in $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Grootte = $jf. Lengte } } Save-FileManifest -Manifest $stNewManifest -Pad $stManifestPath Write-Host " Voltooid in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (deltapatch - $totalChanged apparaten)" -ForegroundColor Green # Doorschakelen naar volledige streaming-herverwerking om HTML-dashboard opnieuw te genereren # De gegevensbestanden zijn al gepatcht, zodat het dashboard actueel blijft Write-Host 'Dashboard regenereren van gepatchte gegevens...' -ForegroundColor Yellow } else { Write-Host " $changePct% bestanden gewijzigd (>= 10%) - volledige streaming herverwerking vereist" -ForegroundColor Yellow } } } } } # Submap Gegevens maken voor JSON-bestanden op aanvraag $dataDir = Join-Path $OutputPath 'gegevens' if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Ontdubbeling via HashSet (O(1) per opzoekactie, ~50 MB voor 600.000 hostnamen) $seenHostnames = [System.Collections.Generic.HashSet[string]::new([System.StringComparer]::OrdinalIgnoreCase) # Lichtgewicht overzichtstellers (vervangt $allDevices + $uniqueDevices in het geheugen) $c = @{ Totaal = 0; SBEnabled = 0; SBOff = 0 Bijgewerkt = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; NotSupported = 0; NoConfData = 0 TaskDisabled = 0; TaskNotFound = 0; TaskDisabledNotUpdated = 0 WithErrors = 0; InProgress = 0; NotYetInitiated = 0; RolloutInProgress = 0 WithKnownIssues = 0; WithMissingKEK = 0; TempFailures = 0; PermFailures = 0; NeedsReboot = 0 UpdatePending = 0 } # Buckettracking voor AtRisk/SafeList (lichtgewicht sets) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Batch-modus apparaatgegevensbestanden: verzamelen per segment, leegmaken bij segmentgrenzen $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 in $stDeviceFiles) { $dfPath = Join-Path $dataDir '$dfName.json' [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim apparaatrecord voor JSON-uitvoer (alleen essentiële velden, ~200 bytes versus ~2KB vol) functie Get-SlimDevice { param($Dev) retourneren [besteld]@{ HostName = $Dev.HostName WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" } KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } else { $null } SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } else { $null } UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } else { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } else { $null } } } # Batch leegmaken naar JSON-bestand (toevoegmodus) functie Flush-DeviceBatch { param([string]$StreamName, [System.Collections.Generic.List[object]]$Batch) if ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev in $Batch) { if ($stDeviceFileCounts[$StreamName] -gt 0) { [void]$fSb.Append(",'n") } [void]$fSb.Append(($fDev | ConvertTo-Json -Compress)) $stDeviceFileCounts[$StreamName]++ } [System.IO.File]::AppendAllText($fPath, $fSb.ToString(), [System.Text.Encoding]::UTF8) } # MAIN STREAMING LOOP $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 } $stTotalChunks = [wiskunde]::Plafond($totalFiles / $stChunkSize) $stPeakMemMB = 0 if ($stTotalChunks -gt 1) { Write-Host "Verwerking van $totalFiles bestanden in $stTotalChunks segmenten van $stChunkSize (streaming, $ParallelThreads threads):" -ForegroundColor Cyan } else { Write-Host "Processing $totalFiles files (streaming, $ParallelThreads threads):" -ForegroundColor Cyan } voor ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [wiskunde]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] if ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count)-bestanden): " -NoNewline -ForegroundColor Gray } else { Write-Host " $($cFiles.Count)-bestanden laden: " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads # Batchlijsten per segment $cBatches = @{} foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 foreach ($raw in $rawDevices) { if (-not $raw) { doorgaan } $device = Normalize-DeviceRecord $raw $hostname = $device. Hostname if (-not $hostname) { doorgaan } if ($seenHostnames.Contains($hostname)) { $cDupe++; doorgaan } [void]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "False") if ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = if ($device. PSObject.Properties['ConfidenceLevel'] -en $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } else { "" } $hasErr = (-niet [tekenreeks]::IsNullOrEmpty($device. UEFICA2023Error) -en "$($device. UEFICA2023Error)" -ne "0" -en "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -or "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = if ($device. PSObject.Properties['BucketId'] -en $device. BucketId) { "$($device. BucketId)" } else { "" } $e 1808 = if ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } else { 0 } $e 1801 = if ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } else { 0 } $e 1803 = if ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -of $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True") $hKI = ((-not [tekenreeks]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($device. KnownIssueId))) $rStat = if ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" } $mfr = if ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } else { "Onbekend" } $bid = if (-not [string]::IsNullOrEmpty($bid)) { $bid } else { "" } # Pre-compute update in behandeling vlag (beleid/WinCS toegepast, status nog niet bijgewerkt, SB AAN, taak niet uitgeschakeld) $uefiStatus = if ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } else { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -en $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] - en $device. WinCSKeyApplied -eq $true) $statusPending = ([string]::IsNullOrEmpty($uefiStatus) -or $uefiStatus -eq 'NotStarted' -or $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -of $hasWinCS) -en $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis) if ($isUpd) { $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Bijgewerkte apparaten bijhouden die opnieuw moeten worden opgestart (UEFICA2023Status=Updated but Event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF: valt buiten het bereik, niet classificeren op betrouwbaarheid } else { if ($isUpdatePending) { } # afzonderlijk geteld in Bijwerken in behandeling— wederzijds exclusief voor cirkeldiagram elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "Tijdelijkpaused") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ } else { $c.ActionReq++ } if ([string]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } if ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } if ($tskNF) { $c.TaskNotFound++ } if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ } if ($hasErr) { $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["errors"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Error if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } if ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = if (-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } anders { $device. KnownIssueId } if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'Tijdelijkpaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ } if ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ } # Update in behandeling: beleid of WinCS toegepast, status in behandeling, SB AAN, taak niet uitgeschakeld if ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # Onder Observatieapparaten (los van actie vereist) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Actie Vereist: niet bijgewerkt, SB ON, komt niet overeen met andere betrouwbaarheidscategorieën, niet update in behandeling if (-not $isUpd -and $sbOn -and -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TijdelijkPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Bijgewerkt=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Totaal++ if ($isUpd) { $stMfrCounts[$mfr]. Bijgewerkt++ } 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 "Tijdelijkpaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. NotSupported++ } else { $stMfrCounts[$mfr]. ActionReq++ } if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Alle apparaten bijhouden per bucket (inclusief lege BucketId) $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Bijgewerkt=0; Manufacturer=$mfr; Model=""; BIOS="" } if ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } if ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ if ($isUpd) { $stAllBuckets[$bucketKey]. Bijgewerkt++ } } # Batches naar schijf spoelen foreach ($df in $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Wiskunde]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = if ($cRem -gt 0) { " | ETA: ~$([Math]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" } $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1 MB, 0) if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew nieuw, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # JSON-matrices voltooien foreach ($dfName in $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) devices" -ForegroundColor DarkGray } # Afgeleide statistieken voor rekenkracht $stAtRisk = 0; $stSafeList = 0 foreach ($bid in $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Updated if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [wiskunde]::Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = aantal van not_updated batch (apparaten met SB ON en niet bijgewerkt) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [besteld]@{ ReportGeneratedAt = (Get-Date). ToString("jjjj-MM-dd uu:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Bijgewerkt = $c.Bijgewerkt; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; Tijdelijkpaused = $c.TempPaused; NotSupported = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; FullyUpdated = $c.Updated UpdatesPending = $stNotUptodate; UpdatesComplete = $c.Updated WithErrors = $c.WithErrors; InProgress = $c.InProgress; NotYetInitiated = $c.NotYetInitiated RolloutInProgress = $c.RolloutInProgress; WithKnownIssues = $c.WithKnownIssues WithMissingKEK = $c.WithMissingKEK; TemporaryFailures = $c.TempFailures; PermanentFailures = $c.PermFailures NeedsReboot = $c.NeedsReboot; UpdatePending = $c.UpdatePending AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList PercentWithErrors = if ($c.Total -gt 0) { [math]::Round(($c.WithErrors/$c.Total)*100,2) } else { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } else { 0 } PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } else { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } else { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } else { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [wiskunde]::Round($stNotUptodate/$c.Total*100,1) } else { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # CSV's schrijven [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Aflopend | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Sleutel; Count=$_. Waarde.Totaal; Bijgewerkt=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stErrorCodeCounts.GetEnumerator() | Sort-Object -Aflopend | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Sleutel; Count=$_. Waarde; SampleDevices=($stErrorCodeSamples[$_. Sleutel] -join ", ") } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Aflopend | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Sleutel; Count=$_. Value.Count; Bijgewerkt=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Manufacturer=$_. Value.Manufacturer } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8 # Orchestrator-compatibele CSV's genereren (verwachte bestandsnamen voor Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir 'not_updated.json' if (testpad $notUpdatedJsonPath) { probeer { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json if ($nuData.Count -gt 0) { # NotUptodate CSV - orchestrator zoekt naar *NotUptodate*.csv $nuData | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Encoding UTF8 Write-Host 'Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count)' -ForegroundColor Gray } } catch { } } # JSON-gegevens schrijven voor dashboard $stats | ConvertTo-Json -Diepte 3 | Set-Content (Join-Path $dataDir "summary.json") -UTF8 coderen # HISTORISCH BIJHOUDEN: gegevenspunt opslaan voor trendgrafiek # Gebruik een stabiele cachelocatie zodat trendgegevens behouden blijven in aggregatiemappen met tijdstempels. # Als OutputPath eruitziet als '...\Aggregation_yyyyMMdd_HHmmss', gaat de cache naar de bovenliggende map.# Anders gaat de cache in OutputPath zelf.$parentDir = Split-Path $OutputPath -Bovenliggend $leafName = Split-Path $OutputPath -Leaf if ($leafName -match ^Aggregation_\d{8} -or $leafName -eq 'Aggregation_Current') { # Door Orchestrator gemaakte map met tijdstempel : gebruik bovenliggende map voor stabiele cache $historyPath = Join-Path $parentDir ".cache\trend_history.json" } else { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Bovenliggend if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() if (testpad $historyPath) { probeer { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } catch { $historyData = @() } } # Controleer ook in OutputPath\.cache\ (verouderde locatie van oudere versies) # Gegevenspunten samenvoegen die nog niet in de primaire geschiedenis zijn opgenomen if ($leafName -eq 'Aggregation_Current' -or $leafName -match ^Aggregation_\d{8}) { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { probeer { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Datum }) foreach ($entry in $innerData) { if ($entry. Datum -en $entry. Datum -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host 'Samengevoegde $($innerData.Count) gegevenspunten uit de binnencache' -ForegroundColor DarkGray } } catch { } } }
# BOOTSTRAP: Als de trendgeschiedenis leeg/sparse is, reconstrueren uit historische gegevens if ($historyData.Count -lt 2 -and ($leafName -match ^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host "Bootstrapping trend history from historical data..." -ForegroundColor Yellow $dailyData = @{} # Bron 1: Samenvatting CSV's in de huidige map (Aggregation_Current alle samenvattings-CSV's bewaart) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object naam foreach ($summCsv in $localSummaries) { probeer { $summ = Import-Csv $summCsv.FullName | Select-Object -Eerste 1 if ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -en $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("jjjj-MM-dd") $updated = if ($summ. Bijgewerkt) { [int]$summ. Bijgewerkt } else { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Datum = $dateStr; Totaal = [int]$summ. TotalDevices; Bijgewerkt = $updated; NotUpdated = $notUpd NeedsReboot = 0; Fouten = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } catch { } } # Bron 2: Oude mappen met tijdstempel Aggregation_* (verouderd, als ze nog bestaan) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Naam -overeenkomst ^Aggregation_\d{8}} | Sort-Object naam foreach ($folder in $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object - Eerste 1 if ($summCsv) { probeer { $summ = Import-Csv $summCsv.FullName | Select-Object - Eerste 1 if ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Naam -vervang ^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = if ($summ. Bijgewerkt) { [int]$summ. Bijgewerkt } else { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Datum = $dateStr; Totaal = [int]$summ. TotalDevices; Bijgewerkt = $updated; NotUpdated = $notUpd NeedsReboot = 0; Fouten = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } catch { } } } # Bron 3: RolloutState.json WaveHistory (heeft tijdstempels per golf vanaf dag 1) # Dit biedt basislijngegevenspunten, zelfs als er geen oude aggregatiemappen bestaan $rolloutStatePaths = @( (Join-Path $parentDir "RolloutState\RolloutState.json") (Join-Path $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath in $rolloutStatePaths) { if (testpad $rsPath) { probeer { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Begindatums voor golf gebruiken als trendgegevenspunten # Cumulatieve apparaten berekenen die op elke golf zijn gericht $cumulativeTargeted = 0 foreach ($wave in $rsData.WaveHistory) { if ($wave. StartedAt -en $wave. DeviceCount) { $waveDate = ([datetime]$wave. StartedAt). ToString("jjjj-MM-dd") $cumulativeTargeted += [int]$wave. DeviceCount if (-not $dailyData.ContainsKey($waveDate)) { # Bij benadering: bij het begintijd van de golf zijn alleen apparaten van eerdere golven bijgewerkt $dailyData[$waveDate] = [PSCustomObject]@{ Datum = $waveDate; Totaal = $c.Totaal; Bijgewerkt = [wiskunde]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NotUpdated = $c.Total - [wiskunde]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0; Fouten = 0; ActionRequired = 0 } } } } } } catch { } break # Gebruik eerst gevonden } }
if ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object Key | ForEach-Object { $_. Waarde }) Write-Host "Bootstrapped $($historyData.Count) gegevenspunten uit historische samenvattingen" -ForegroundColor Green } }
# Huidig gegevenspunt toevoegen (ontdubbelen per dag - laatste per dag behouden) $todayKey = (Get-Date). ToString("jjjj-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" } if ($existingToday) { # Vervang de invoer van vandaag $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Datum = $todayKey Totaal = $c.Totaal Bijgewerkt = $c.Bijgewerkt NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Errors = $c.WithErrors ActionRequired = $c.ActionReq } # Verwijder ongeldige gegevenspunten (totaal 0) en behoud de laatste 90 $historyData = @($historyData | Where-Object { [int]$_. Totaal -gt 0 }) # Geen limiet: trendgegevens zijn ~100 bytes/invoer, een volledig jaar = ~36 kB $historyData | ConvertTo-Json -Diepte 3 | Set-Content $historyPath -UTF8 coderen Write-Host "Trendgeschiedenis: $($historyData.Count) gegevenspunten" -ForegroundColor DarkGray # Trendgrafiekgegevens bouwen voor HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Bijgewerkt }) -join "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Totaal }) -join "," # Projectie: trendlijn uitbreiden met exponentieel verdubbelen (2,4,8,16...) # Leidt de golfgrootte en observatieperiode af aan de werkelijke trendgeschiedenisgegevens.# - Golfgrootte = grootste toename van één periode in de geschiedenis (de meest recente geïmplementeerde golf) # - Observatiedagen = gemiddelde kalenderdagen tussen trendgegevenspunten (hoe vaak we uitvoeren) # Vervolgens verdubbelt de golfgrootte elke periode, overeenkomend met de 2x groeistrategie van de orchestrator.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Bijgewerkt $remaining = $stNotUptodate # Alleen SB-ON-apparaten die niet zijn bijgewerkt (secureboot is uitgeschakeld) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# De golfgrootte en observatieperiode afleiden uit de trendgeschiedenis $increments = @() $dayGaps = @() voor ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Bijgewerkt - $historyData[$hi-1]. Bijgewerkt if ($inc -gt 0) { $increments += $inc } probeer { $d 1 = [datetime]::P arse($historyData[$hi-1]. Datum) $d 2 = [datetime]::P arse($historyData[$hi]. Datum) $gap = ($d 2 - $d 1). TotalDays if ($gap -gt 0) { $dayGaps += $gap } } catch {} } # Golfgrootte = meest recente positieve toename (huidige golf), terugval naar gemiddelde, minimaal 2 $waveSize = if ($increments. Aantal -gt 0) { [wiskunde]::Max(2, $increments[-1]) } anders { 2 } # Observatieperiode = gemiddelde kloof tussen gegevenspunten (kalenderdagen per golf), minimaal 1 $waveDays = if ($dayGaps.Count -gt 0) { [wiskunde]::Max(1; [wiskunde]::Round((($dayGaps | Measure-Object -Average). Gemiddelde, 0)) } else { 1 }
Write-Host "Projectie: waveSize=$waveSize (vanaf laatste stap), waveDays=$waveDays (gemiddelde hiaat uit geschiedenis)" -ForegroundColor DarkGray
$dayCounter = 0 # Projecteer totdat alle apparaten zijn bijgewerkt of maximaal 365 dagen voor ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # Implementeer bij elke grens van de observatieperiode een golf en vervolgens dubbel if ($dayCounter -ge $waveDays) { $devicesThisWave = [wiskunde]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Dubbele golfgrootte voor de volgende periode (orchestrator 2x-strategie) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("jjjj-MM-dd")'" $projValues += $lastUpdated $projNotUpdValues += [wiskunde]::Max(0, $remaining) if ($remaining -le 0) { break } } $projLabels = $projDates -join "," $projUpdated = $projValues -join "," $projNotUpdated = $projNotUpdValues -join "," $hasProjection = $projDates.Count -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host "Projectie: minimaal 2 trendgegevenspunten nodig om de timing van de golf af te leiden" -ForegroundColor DarkGray } # Gecombineerde grafiekgegevenstekenreeksen bouwen voor de here-tekenreeks $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } else { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } else { "" } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { "" } $histCount = ($historyData | Measure-Object). Tellen $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Aflopend | ForEach-Object { @{ name=$_. Sleutel; total=$_. Waarde.Totaal; updated=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Diepte 3 | Set-Content (Join-Path $dataDir "manufacturers.json") -UTF8 coderen # JSON-gegevensbestanden converteren naar CSV voor leesbare Excel-downloads Write-Host "Apparaatgegevens converteren naar CSV voor Excel downloaden..." -ForegroundColor Gray foreach ($dfName in $stDeviceFiles) { $jsonFile = Join-Path $dataDir '$dfName.json' $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" if (testpad $jsonFile) { probeer { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json if ($jsonData.Count -gt 0) { # Extra kolommen opnemen voor update_pending CSV $selectProps = if ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } else { @('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) rijen -> CSV' -ForegroundColor DarkGray } } catch { Write-Host " $dfName - overgeslagen" -ForegroundColor DarkYellow } } } # Zelfstandig HTML-dashboard genereren $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Self-contained HTML dashboard..." -ForegroundColor Yellow # SNELHEIDSPROJECTIE: berekenen op basis van scangeschiedenis of vorige samenvatting $stDeadline = [datetime]"2026-06-24" # Vervaldatum KEK-certificaat $stDaysToDeadline = [wiskunde]::Max(0, ($stDeadline - (Get-Date)). Dagen) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "N.v.v." $stWorkingDays = 0 $stCalendarDays = 0 # Probeer eerst de trendgeschiedenis (lichtgewicht, al onderhouden door aggregator — vervangt opgeblazen ScanHistory.json) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Totaal -gt 0 -en [int]$_. -ge 0 }) bijgewerkt if ($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; [Wiskunde]::Min(10, $curr. Date.Length))) $daysDiff = ($currDate - $prevDate). TotalDays if ($daysDiff -gt 0) { $updDiff = [int]$curr. Bijgewerkt - [int]$prev. Bijgewerkt if ($updDiff -gt 0) { $stDevicesPerDay = [wiskunde]::Round($updDiff / $daysDiff, 0) $stVelocitySource = "TrendHistory" } } } } # Samenvatting van orchestrator-implementatie proberen (heeft vooraf berekende snelheid) if ($stVelocitySource -eq "N/B" -and $RolloutSummaryPath -and (testpad $RolloutSummaryPath)) { probeer { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [wiskunde]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = "Orchestrator" if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } catch { } } # Terugval: probeer vorige samenvattings-CSV (zoek huidige map EN aggregatiemappen bovenliggend/zusje) if ($stVelocitySource -eq "N/B") { $searchPaths = @( (Join-Path $OutputPath 'SecureBoot_Summary_*.csv') ) # Zoek ook naar aggregatiemappen voor broers en zussen (orchestrator maakt elke uitvoering een nieuwe map) $parentPath = Split-Path $OutputPath -Bovenliggend if ($parentPath) { $searchPaths += (Join-Path $parentPath "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += (Join-Path $parentPath 'SecureBoot_Summary_*.csv') } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Descending | Select-Object -Eerste 1 if ($prevSummary) { probeer { $prevStats = Get-Content $prevSummary.FullName | ConverterenVan-CSV $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) - $prevDate). TotalDays if ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Updated - $prevUpdated if ($updDelta -gt 0) { $stDevicesPerDay = [wiskunde]::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = "PreviousReport" } } } catch { } } } # Terugval: de snelheid berekenen op basis van de volledige trendgeschiedenis (eerste versus meest recente gegevenspunt) if ($stVelocitySource -eq "N/B" -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Totaal -gt 0 -en [int]$_. -ge 0 }) bijgewerkt if ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime]::P arse($first. Date.Substring(0; [Wiskunde]::Min(10, $first. Date.Length))) $lastDate = [datetime]::P arse($last. Date.Substring(0; [Wiskunde]::Min(10, $last. Date.Length))) $daysDiff = ($lastDate - $firstDate). TotalDays if ($daysDiff -gt 0) { $updDiff = [int]$last. Bijgewerkt - [int]$first. Bijgewerkt if ($updDiff -gt 0) { $stDevicesPerDay = [wiskunde]::Round($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # Projectie berekenen met exponentieel verdubbelen (consistent met trendgrafiek) # Gebruik de projectiegegevens die al zijn berekend voor de grafiek, indien beschikbaar if ($hasProjection -and $projDates.Count -gt 0) { # Gebruik de laatste geplande datum (wanneer alle apparaten zijn bijgewerkt) $lastProjDateStr = $projDates[-1] -vervang "'", "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, jjjj") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Dagen $stWorkingDays = 0 $d = Get-Date voor ($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 -en $stNotUptodate -gt 0) { # Terugval: lineaire projectie als er geen exponentiële gegevens beschikbaar zijn $daysNeeded = [wiskunde]::Plafond($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, jjjj") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Get-Date voor ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } #Build velocity HTML $velocityHtml = if ($stDevicesPerDay -gt 0) { "<div><strong>🚀 Apparaten/Dag:</strong> $($stDevicesPerDay.ToString('N0')) (bron: $stVelocitySource)</div>" + "<div><strong>📅 Verwachte voltooiing: </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>🕐 Werkdagen:</strong> $stWorkingDays | <strong>Calendar Days:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Deadline: 24 juni 2026 (vervaldatum KEK-certificaat) | Resterende dagen: $stDaysToDeadline</div>" } else { "<div style='padding:8px; achtergrond:#fff3cd; rand-radius: 4px; rand-links: 3px solid #ffc107'>" + "<sterke>📅 Verwachte voltooiing:</strong> Onvoldoende gegevens voor snelheidsberekening. " + 'Aggregatie minstens twee keer uitvoeren met gegevenswijzigingen om een rate tot stand te brengen.<br/>' + "<sterke>Deadline:</strong> 24 juni 2026 (vervaldatum KEK-certificaat) | <resterende>dagen:</strong> $stDaysToDeadline</div>' } # Cert vervaldatum aftelling $certToday = Get-Date $certKekExpiry = [datum/tijd]"24-06-2026" $certUefiExpiry = [datum/tijd]"27-06-2026" $certPcaExpiry = [datum/tijd]"10-2026" $daysToKek = [wiskunde]::Max(0, ($certKekExpiry - $certToday). Dagen) $daysToUefi = [wiskunde]::Max(0, ($certUefiExpiry - $certToday). Dagen) $daysToPca = [wiskunde]::Max(0, ($certPcaExpiry - $certToday). Dagen) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Helper: Records lezen uit JSON, samenvatting van bucket maken + eerste N-apparaatrijen $maxInlineRows = 200 functie Build-InlineTable { param([tekenreeks]$JsonPath; [int]$MaxRows = 200, [tekenreeks]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (testpad $JsonPath) { probeer { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Tellen # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats if ($totalCount -gt 0) { $buckets = $data | BucketId Group-Object | Sort-Object aantal aflopend $bucketSummary = "><2 h3 style='font-size:.95em; kleur:#333; margin: 10px 0 5px'><3 By Hardware Bucket ($($buckets. Aantal) buckets)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:15px'><tabel><thead><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Bijgewerkt</th><th style='text-align:right; color:#dc3545'>Niet bijgewerkt</th><th><1 Fabrikant><2 /th></tr></thead><tbody>" foreach ($b in $buckets) { $bid = if ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Get Updated count from global bucket stats (alle apparaten in deze bucket in de hele gegevensset) $lookupKey = $bid $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null } $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } else { 0 } $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } else { $b.Count } $bNotUpdatedGlobal = $bTotalGlobal - $bUpdatedGlobal $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; font-weight:bold'>$bTotalGlobal><8 /td><td style='text-align:right; kleur:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; kleur:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # APPARAATDETAILS: Eerste N rijen als platte lijst $slice = $data | Select-Object - Eerste $MaxRows foreach ($d in $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge-danger">Niet ondersteund><6 /span>' } elseif ($conf -match "Under") { '<span class="badge-info">Onder Obs><0 /span>' } elseif ($conf -match "Onderbroken") { '<span class="badge-warning">Onderbroken><4 /span>' } else { '<span class="badge-warning">Action Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="badge-success"><01 Bijgewerkt</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge-danger"><05 Error</span>' } else { '><08 span class="badge-warning"><09 In behandeling><0 /span>' } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20><9 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 /td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n" } } catch { } } if ($totalCount -eq 0) { retourneert "><44 div style='padding:20px; kleur:#888; lettertypestijl:cursief'><45 Geen apparaten in deze categorie.><46 /div>' } $showing = [wiskunde]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; tekengrootte:.85em; color:#666'><49 Totaal: $($totalCount.ToString("N0")) apparaten" if ($CsvFileName) { $header += " | ><50 een href='$CsvFileName' style='color:#1a237e; font-weight:bold'>📄 Volledige CSV voor Excel downloaden><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; kleur:#333; marge: 10px 0 5px'><58 Apparaatdetails (met eerste $showing) ><59 /h3>" $deviceTable = "><61 div style='max-height:500px; tabel overflow-y:auto'><><thead><tr><th><0 HostName><1 /th><th><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Betrouwbaarheid><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>' retourneert "$header$bucketSummary$deviceHeader$deviceTable" } # Bouw inlinetabellen op basis van de JSON-bestanden die al op de schijf staan en koppelen aan CSV's $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" # Aangepaste tabel voor update in behandeling— bevat UEFICA2023Status en UEFICA2023Errorkolommen $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir 'update_pending.json' if (testpad $upJsonPath) { probeer { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Count if ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; tekengrootte:.85em; color:#666'>Totaal: $($upCount.ToString("N0")) | <een href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>📄 Volledige CSV voor Excel downloaden><4 /a></div>' $upRows = "" $upSlice = $upData | Select-Object - Eerste $maxInlineRows foreach ($d in $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge-success">Ja><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 = [wiskunde]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; kleur:#333; marge: 10px 0 5px'>Apparaatdetails (met eerste $upShowing) </h3>" $upTable = "<div style='max-height:500px; tabel overflow-y:auto'><><dead><tr><th><9 HostName><0 /th><th><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2023Error><6 /th><th><9 Policy</th><th>WinCS Key</th><th>BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } else { $tblUpdatePending = "<div style='padding:20px; kleur:#888; lettertypestijl:cursief'>Geen apparaten in deze categorie.</div>' } } catch { $tblUpdatePending = "<div style='padding:20px; kleur:#888; lettertypestijl:cursief'>Geen apparaten in deze categorie.</div>' } } else { $tblUpdatePending = "<div style='padding:20px; kleur:#888; lettertypestijl:cursief'>Geen apparaten in deze categorie.</div>' } # Cert vervaldatum aftelling $certToday = Get-Date $certKekExpiry = [datum/tijd]"24-06-2026" $certUefiExpiry = [datum/tijd]"27-06-2026" $certPcaExpiry = [datum/tijd]"10-2026" $daysToKek = [wiskunde]::Max(0, ($certKekExpiry - $certToday). Dagen) $daysToUefi = [wiskunde]::Max(0, ($certUefiExpiry - $certToday). Dagen) $daysToPca = [wiskunde]::Max(0, ($certPcaExpiry - $certToday). Dagen) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Bouw de grafiekgegevens van de fabrikant inline (Top 10 op aantal apparaten) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Aflopend | Select-Object - Eerste 10 $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "By Manufacturer" } else { "Top 10 Manufacturers" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Sleutel)'" }) -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 "," # Build manufacturer table $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Aflopend | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Sleutel)</td><td>$($_. Value.Total.ToString("N0")</td><td>$($_. Value.Updated.ToString("N0")</td><td>$($_. Value.HighConf.ToString("N0")><0 /td><td>$($_. Value.ActionReq.ToString("N0")><4 /td></tr>'n" } $htmlContent = @" <! DOCTYPE html-> <html lang="en"> <hoofd><3 <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <titel><9 Statusdashboard van beveiligd opstarten certificaat><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 <stijl><7 *{box-sizing:border-box; marge:0; opvulling:0} body{font-family:'Segoe UI',Tahoma,sans-serif; achtergrond:#f0f2f5; kleur:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); kleur:#fff; opvulling: 20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; ondoorzichtigheid:.9} .container{max-width:1400px; marge:0 automatisch; opvulling: 20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); gap: 12px; marge: 20px 0} .card{background:#fff; rand-radius: 10px; opvulling: 15px; box-shadow: 0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; font-weight:700} .card .label{font-size:.8em; kleur:#666; margin-top: 4px} .card .pct{font-size:.75em; kleur:#888} .section{background:#fff; rand-radius: 10px; opvulling: 20px; marge: 15px 0; box-shadow: 0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; kleur:#1a237e; margin-bottom: 10px; cursor:aanwijzer; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; rastersjabloon-kolommen:1fr 1fr; gap: 20px; marge: 20px 0} .chart-box{background:#fff; rand-radius: 10px; opvulling: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.08)} table{width:100%; rand samenvouwen:samenvouwen; tekengrootte:.85em} th{background:#e8eaf6; opvulling: 8px 10px; tekst uitlijnen:links; positie: plakkerig; boven:0; z-index:1} td{opvulling: 6px 10px; border-bottom: 1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; opvulling:2px 8px;border-radius:10px; tekengrootte:.75em; font-weight:700} .badge-success{background:#d4edda; kleur:#155724} .badge-danger{background:#f8d7da; kleur:#721c24} .badge-warning{background:#fff3cd; kleur:#856404} .badge-info{background:#d1ecf1; kleur:#0c5460} .top-link{float:right; tekengrootte:.8em; kleur:#1a237e; text-decoration:none} .footer{text-align:centreren; opvulling: 20px; kleur:#999; tekengrootte:.8em} a{color:#1a237e} </style><9 </head> <hoofdtekst> <div class="header"> <h1></h1 > <div class="meta">Generated: $($stats. ReportGeneratedAt) | Totaal aantal apparaten: $($c.Total.ToString("N0")) | Unieke buckets: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">
<!-- KPI-kaarten - klikbaar, gekoppeld aan secties --> <div class="cards"> <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; tekst-decoratie:geen; position:relative"><div style="position:absolute; top: 8px; rechts:8px; achtergrond:#dc3545; kleur:#fff; opvulling: 1px 6px; rand-radius: 8px; tekengrootte:.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)% - ACTIE vereist><0 /div></a><3 <a class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; tekst-decoratie:geen; position:relative"><div style="position:absolute; top: 8px; rechts:8px; achtergrond:#28a745; kleur:#fff; opvulling: 1px 6px; rand-radius: 8px; tekengrootte:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString ("N0"))</div><div class="label">Bijgewerkt><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; tekst-decoratie:geen; position:relative"><div style="position:absolute; top: 8px; rechts:8px; achtergrond:#6c757d; kleur:#fff; opvulling: 1px 6px; rand-radius: 8px; tekengrootte:.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})% - Buiten het bereik><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})% - in afwachting van opnieuw opstarten><6 /div></a><9 <a class="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Update Pending</div><>div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Policy/WinCS applied, in afwachting van update><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">Implementatie wordt uitgevoerd><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)% - Veilig voor implementatie><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)% - moet><6 /div testen></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)% - Vergelijkbaar met mislukte><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})% - Geblokkeerde><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. Onderbroken</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-fouten</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. Fouten</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">Not Supported><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
Verloop van<!---&-> <div id="s-velocity" style="display:grid; rastersjabloon-kolommen:1fr 1fr; gap: 20px; marge: 15px 0"> <div class="section" style="margin:0"> <h2>📅 Implementatiesnelheid</h2> <div class="section-body open"> <div style="font-size:2.5em; lettertype-gewicht: 700; color:#28a745">$($c.Updated.ToString("N0")</div> <div style="color:#666">apparaten bijgewerkt van $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; achtergrond:#e8eaf6; hoogte: 20px; rand-radius: 10px; overflow:hidden"><div style="background:#28a745; hoogte:100%; width:$($stats. PercentCertUpdated)%; rand-radius: 10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% voltooid</div> <div style="margin-top:10px; opvulling: 10px; achtergrond:#f8f9fa; rand-radius: 8px; tekengrootte:.85em"> <div><sterke>Resterend:</strong> $($stNotUptodate.ToString("N0")) apparaten hebben actie nodig</div> <div><strong>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) apparaten (errors + permanent + task disabled)</div> <div-apparaten><sterke>Veilig om te implementeren:</strong> $($stSafeList.ToString("N0")) (dezelfde bucket als geslaagd)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; rand-links: 4px solid #dc3545"> <h2 style="color:#dc3545">⚠ Certificate Expiry Countdown</h2> <div class="section-body open"> <div style="display:flex; gap: 15px; margin-top: 10px"> <div style="text-align:center; opvulling: 15px; rand-radius: 8px; min-breedte: 120px; achtergrond:lineaire kleurovergang(135deg,#fff5f5,#ffe0e0); rand: 2px solide #dc3545; flex:1"> <div style="font-size:.65em; kleur:#721c24; text-transform:hoofdletters; font-weight:bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size:.85em; lettertype-gewicht:vet; kleur:#dc3545; margin: 3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; lettertype-gewicht: 700; kleur:#dc3545; lijnhoogte: 1"><9 $daysToKek</div> ><2 div style="font-size:.8em; color:#721c24"><3 dagen (24 juni 2026)><4 /div> ><6 /div> ><8 div style="text-align:center; opvulling: 15px; rand-radius: 8px; min-breedte: 120px; achtergrond:lineaire kleurovergang(135deg,#fffef5,#fff3cd); rand: 2px solide #ffc107; flex:1"><9 <div style="font-size:.65em; kleur:#856404; text-transform:hoofdletters; font-weight:bold">UEFI CA 2011</div> <div id="daysUefi" style="font-size:2.2em; lettertype-gewicht: 700; kleur:#856404; lijnhoogte:1; marge: 5px 0">$daysToUefi</div> <div style="font-size:.8em; color:#856404">days (27 juni 2026)</div> </div> <div style="text-align:center; opvulling: 15px; rand-radius: 8px; min-breedte: 120px; achtergrond:lineaire kleurovergang(135deg,#f0f8ff,#d4edff); rand: 2px solide #0078d4; flex:1"> <div style="font-size:.65em; kleur:#0078d4; text-transform:hoofdletters; font-weight:bold">Windows PCA</div> <div id="daysPca" style="font-size:2.2em; lettertype-gewicht: 700; kleur:#0078d4; lijnhoogte:1; marge: 5px 0">$daysToPca><2 /div><3 <div style="font-size:.8em; color:#0078d4">dagen (19 oktober 2026)</div><7 </div><9 </div><1 <div style="margin-top:15px; opvulling: 10px; achtergrond:#f8d7da; rand-radius: 8px; tekengrootte:.85em; rand-links: 4px solide #dc3545"> <sterke>⚠ CRITICAL:</strong> Alle apparaten moeten worden bijgewerkt voordat het certificaat verloopt. Op apparaten die niet binnen de deadline zijn bijgewerkt, kunnen toekomstige beveiligingsupdates voor Opstartbeheer en Beveiligd opstarten na verloop van tijd niet worden toegepast.</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>
$(if ($historyData.Count -ge 1) { "<!-- historische trendgrafiek --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Voortgang in de loop van de tijd bijwerken <een class='top-link' href='#'>↑ Top</a></h2> <div id='d-trend' class='section-body open'> <canvas id='trendChart' height='120'></canvas> <div style='font-size:.75em; kleur:#888; margin-top:5px'>Vaste lijnen = werkelijke gegevens$(if ($historyData.Count -ge 2) { " | Onderbroken lijn = geprojecteerd (exponentieel verdubbelen: 2→4→8→16... apparaten per golf)" } else { " | Voer de aggregatie morgen opnieuw uit om trendlijnen en projectie te zien" })</div> </div> </div>" })
CSV-downloads<!-- --> <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Volledige gegevens (CSV voor Excel) downloaden <a class="top-link" href="#">Top</a></h2><2 <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap: 5px"> <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; achtergrond:#dc3545; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0"))</a><8 <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; achtergrond:#dc3545; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Errors ($($c.WithErrors.ToString("N0"))</a> <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; achtergrond:#fd7e14; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Action Required ($($c.ActionReq.ToString("N0"))</a> <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; achtergrond:#dc3545; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Known Issues ($($c.WithKnownIssues.ToString("N0"))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; achtergrond:#dc3545; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Task Disabled ($($c.TaskDisabled.ToString("N0"))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; achtergrond:#28a745; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Updated ($($c.Updated.ToString("N0"))</a> <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; achtergrond:#6c757d; kleur:#fff; opvulling: 6px 14px; rand-radius: 5px; tekst-decoratie:geen; font-size:.8em">Summary</a> <div style="width:100%; tekengrootte:.75em; kleur:#888; margin-top:5px">CSV-bestanden worden geopend in Excel. Beschikbaar wanneer gehost op webserver.</div> </div> </div>
<!-- Fabrikant uitsplitsing --> <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"> <tabel><de><><><1 Fabrikant><2 /><><5 Totaal><6 /><><9 Bijgewerkt><0><9 /th><th><3 high confidence><4 /th><th><7 actie Vereist><8 /th></tr></thead><3 <tbody><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- Apparaatsecties (eerste 200 inline + CSV-download) --> <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Apparaten met fouten ($($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">🔴 Bekende problemen ($($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')">🟠 Ontbrekende KEK - Gebeurtenis 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">🟠 Actie vereist ($($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">🔵 Onder Observatie ($($c.UnderObs.ToString("N0")) <een 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">🔴 Niet bijgewerkt ($($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">🔴 Taak uitgeschakeld ($($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">🔴 Tijdelijke fouten ($($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 fouten/niet ondersteund ($($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 Applied, In afwachting update <a class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Apparaten waarop de toets AvailableUpdatesPolicy of WinCS is toegepast, maar UEFICA2023Status is nog steeds NotStarted, InProgress of null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Implementatie wordt uitgevoerd ($($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"))) - Buiten bereik <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">🟢 Bijgewerkte apparaten ($($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">🔄 Bijgewerkt- Moet opnieuw worden opgestart ($($c.NeedsReboot.ToString("N0")) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>
<div class="footer">Secure Boot Certificate Rollout Dashboard | Gegenereerd $($stats. ReportGeneratedAt) | StreamingMode | Piekgeheugen: ${stPeakMemMB} MB</div> </div><!-- /container -->
<script> function toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} function openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} new Chart(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update pending','High Confidence','Under Observation','Action Required','Temp. Onderbroken','Niet ondersteund','SecureBoot OFF','With Errors'],gegevenssets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24','#adb5bd','#dc3545']}]},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); new Chart(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update in behandeling',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. Onderbroken',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'}}}}); Historische trendgrafiek 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 = Matrix(histLen).fill(null); var projNotUpdLine = Matrix(histLen).fill(null); if (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdData); } var datasets = [ {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; if (projLen > 0) { datasets.push({label:'Projected Updated (2x doublebling)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display:true,text:'Devices'}},x:{title:{display:true,text:'Date'}}},plugins:{legenda:{position:'top'},title:{display:true,text:'Voortgang bij het bijwerken van beveiligd opstarten in de loop van de tijd'}}}}); } Dynamisch aftellen (function(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19'); var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0,Math.ceil(((k-t)/864e5)); if(du)du.textContent=Math.max(0,Math.ceil(((u-t)/864e5)); if(dp)dp.textContent=Math.max(0,Math.ceil(((p-t)/864e5))})(); </script> </body> </html> "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Houd altijd een stabiele 'laatste' kopie, zodat beheerders geen tijdstempels hoeven bij te houden $latestPath = Join-Path $OutputPath 'SecureBoot_Dashboard_Latest.html' Copy-Item $htmlPath $latestPath -Forceren $stTotal = $streamSw.Elapsed.TotalSeconds # Bestandsmanifest opslaan voor incrementele modus (snelle detectie zonder wijzigingen bij de volgende uitvoering) if ($IncrementalMode -of $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 'Bestandsmanifest opslaan voor incrementele modus...' -ForegroundColor Gray foreach ($jf in $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Grootte = $jf. Lengte } } Save-FileManifest -Manifest $stNewManifest -Pad $stManifestPath Write-Host "Opgeslagen manifest voor $($stNewManifest.Count)-ForegroundColor DarkGray } # RETENTIE OPSCHONEN Herbruikbare map # Orchestrator (Aggregation_Current): alleen de meest recente uitvoering behouden (1) # Beheer handmatige uitvoeringen / andere mappen: houd de laatste 7 uitvoeringen # Samenvatting CSV's worden NOOIT verwijderd- ze zijn klein (~1 kB) en zijn de back-upbron voor trendgeschiedenis $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } else { 7 } # Bestandsvoorvoegsels veilig om op te schonen (kortstondige momentopnamen per uitvoering) $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_' ) # Alleen zoeken naar unieke tijdstempels van opschoonbare bestanden $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. Naam; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Aantal -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { if ($_. Naam -overeenkomst '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Uniek -Aflopend) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object $retentionCount overslaan $removedFiles = 0; $freedBytes = 0 foreach ($oldTs in $oldTimestamps) { foreach ($prefix in $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f in $oldFiles) { $freedBytes += $f.Length Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [wiskunde]::Round($freedBytes / 1 MB, 1) Write-Host "Retentieopruiming: $removedFiles bestanden verwijderd uit $($oldTimestamps.Count) oude uitvoeringen, ${freedMB} MB vrijgemaakt (het behouden van laatste $retentionCount + alle CV's Summary/NotUptodate)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Cyan Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -ForegroundColor Cyaan Write-Host " Totaal aantal apparaten: $($c.Total.ToString("N0"))" -ForegroundColor White Write-Host " NIET BIJGEWERKT: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Groen" }) Write-Host " Bijgewerkt: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -ForegroundColor Green Write-Host " Met fouten: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host "Peak Memory: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Tijd: $([math]::Round($stTotal/60,1)) min" -ForegroundColor White Write-Host Dashboard: $htmlPath -ForegroundColor White return [PSCustomObject]$stats } #endregion STREAMINGMODUS } else { Write-Error 'Invoerpad niet gevonden: $InputPath' afsluiten 1 }