Copiez et collez cet exemple de script et modifiez-le si nécessaire pour votre environnement :
<# . SYNOPSIS Agrège le démarrage sécurisé status données JSON de plusieurs appareils dans des rapports de synthèse.
. DESCRIPTION Lit le démarrage sécurisé collecté status fichiers JSON et génère : - Tableau de bord HTML avec graphiques et filtrage - Résumé par ConfidenceLevel - Analyse de compartiment d’appareils unique pour la stratégie de test Soutient: - Fichiers par ordinateur : HOSTNAME_latest.json (recommandé) - Fichier JSON unique Déduplique automatiquement par HostName, en conservant collectionTime le plus récent. Par défaut, inclut uniquement les appareils avec une confiance « Action Req » ou « Haute » pour se concentrer sur les compartiments actionnables. Utilisez -IncludeAllConfidenceLevels pour remplacer.
. PARAMETER InputPath Chemin d’accès au ou aux fichiers JSON : - Dossier : lit tous les fichiers *_latest.json (ou *.json si aucun fichier _latest) - Fichier : lit un seul fichier JSON
. PARAMETER OutputPath Chemin des rapports générés (par défaut : .\SecureBootReports)
. EXEMPLE # Agréger à partir d’un dossier de fichiers par ordinateur (recommandé) .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » Nombre de lectures : \\contoso\SecureBootLogs$\*_latest.json
. EXEMPLE # Emplacement de sortie personnalisé .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » -OutputPath « C :\Reports\SecureBoot »
. EXEMPLE # Inclure uniquement Action Req et Haute confiance (comportement par défaut) .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » # Exclut : Observation, Suspendu, Non pris en charge
. EXEMPLE # Inclure tous les niveaux de confiance (filtre de remplacement) .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » -IncludeAllConfidenceLevels
. EXEMPLE # Filtre de niveau de confiance personnalisé .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » -IncludeConfidenceLevels @(« Action Req », « High », « Observation »)
. EXEMPLE # ENTERPRISE SCALE : mode incrémentiel : traiter uniquement les fichiers modifiés (exécutions suivantes rapides) .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » -IncrementalMode # Première exécution : Chargement complet ~ 2 heures pour 500 000 appareils Nombre d’exécutions suivantes : secondes si aucune modification, minutes pour les deltas
. EXEMPLE # Ignorer le code HTML si rien n’a changé (plus rapide pour la surveillance) .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » -IncrementalMode -SkipReportIfUnchanged # Si aucun fichier n’a été modifié depuis la dernière exécution : ~5 secondes
. EXEMPLE Mode # Résumé uniquement : ignorer les tables d’appareils volumineuses (1 à 2 minutes ou plus de 20 minutes) .\Aggregate-SecureBootData.ps1 -InputPath « \\contoso\SecureBootLogs$ » -SummaryOnly # Génère des csv, mais ignore le tableau de bord HTML avec des tables d’appareils complètes
. NOTES Couplez avec Detect-SecureBootCertUpdateStatus.ps1 pour le déploiement d’entreprise.Consultez GPO-DEPLOYMENT-GUIDE.md pour le guide de déploiement complet. Le comportement par défaut exclut les appareils Observation, Suspendus et Non pris en charge pour concentrer la création de rapports sur les compartiments d’appareils actionnables uniquement.#>
param( [Parameter(Mandatory = $true)] [string]$InputPath, [Parameter(Mandatory = $false)] [string]$OutputPath = .\SecureBootReports", [Parameter(Mandatory = $false)] [string]$ScanHistoryPath = .\SecureBootReports\ScanHistory.json", [Parameter(Mandatory = $false)] [string]$RolloutStatePath, # Chemin d’accès à RolloutState.json pour identifier les appareils InProgress [Parameter(Mandatory = $false)] [string]$RolloutSummaryPath, # Chemin d’accès à SecureBootRolloutSummary.json à partir d’Orchestrator (contient des données de projection) [Parameter(Mandatory = $false)] [string[]]$IncludeConfidenceLevels = @(« Action required », « High Confidence »), # incluez uniquement ces niveaux de confiance (par défaut : compartiments actionnables uniquement) [Parameter(Mandatory = $false)] [switch]$IncludeAllConfidenceLevels, # Remplacer le filtre pour inclure tous les niveaux de confiance [Parameter(Mandatory = $false)] [switch]$SkipHistoryTracking, [Parameter(Mandatory = $false)] [switch]$IncrementalMode, # Activer le traitement delta : charger uniquement les fichiers modifiés depuis la dernière exécution [Parameter(Mandatory = $false)] [string]$CachePath, # Chemin d’accès au répertoire du cache (par défaut : OutputPath\.cache) [Parameter(Mandatory = $false)] [int]$ParallelThreads = 8, # Nombre de threads parallèles pour le chargement de fichiers (PS7+) [Parameter(Mandatory = $false)] [switch]$ForceFullRefresh, # Forcer le rechargement complet, même en mode incrémentiel [Parameter(Mandatory = $false)] [switch]$SkipReportIfUnchanged, # Ignorer la génération HTML/CSV si aucun fichier n’a été modifié (uniquement les statistiques de sortie) [Parameter(Mandatory = $false)] [switch]$SummaryOnly, # Générer des statistiques récapitulatives uniquement (pas de tables d’appareils volumineuses) - beaucoup plus rapidement [Parameter(Mandatory = $false)] [switch]$StreamingMode # Mode mémoire efficace : traiter les segments, écrire des csv de manière incrémentielle, conserver uniquement les résumés en mémoire )
# Élévation automatique vers PowerShell 7 si disponible (6 fois plus rapide pour les jeux de données volumineux) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ($pwshPath) { Write-Host « PowerShell $($PSVersionTable.PSVersion) détecté - re-lancement avec PowerShell 7 pour un traitement plus rapide... - ForegroundColor Yellow # Reconstruire la liste d’arguments à partir de paramètres liés $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key dans $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] if ($val -is [switch]) { if ($val. IsPresent) { $relaunchArgs += « -$key » } } elseif ($val -is [array]) { $relaunchArgs += « -$key » $relaunchArgs += ($val -join ',') } else { $relaunchArgs += « -$key » $relaunchArgs += « $val » } } & $pwshPath @relaunchArgs $LASTEXITCODE de sortie } }
$ErrorActionPreference = « Continue » $timestamp = Get-Date -Format « aaaaMMdd-HHmmss » $scanTime = Get-Date -Format « aaaa-MM-dd HH :mm :ss » $DownloadUrl = « https://aka.ms/getsecureboot » $DownloadSubPage = « Exemples de déploiement et de surveillance »
# Remarque : Ce script n’a aucune dépendance sur les autres scripts. # Pour l’ensemble d’outils complet, téléchargez à partir de : $DownloadUrl -> $DownloadSubPage
Configuration de #region Write-Host « = » * 60 -ForegroundColor Cyan Write-Host « Secure Boot Data Aggregation » -ForegroundColor Cyan Write-Host « = » * 60 -ForegroundColor Cyan
# Créer un répertoire de sortie if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }
# Charger des données : prend en charge les formats CSV (hérité) et JSON (natif) Write-Host « 'nLoading data from : $InputPath » -ForegroundColor Yellow
Fonction d’assistance # pour normaliser l’objet d’appareil (gérer les différences de nom de champ) function Normalize-DeviceRecord { param($device) # Gérer le nom d’hôte et le nom d’hôte (JSON utilise le nom d’hôte, CSV utilise le nom d’hôte) if ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Nom d’hôte -Force } # Handle Confidence vs ConfidenceLevel (JSON utilise Confidence, CSV utilise ConfidenceLevel) # ConfidenceLevel est le nom officiel du champ - mapper confiance à celui-ci if ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Confiance -Force } # Suivre les status de mise à jour via Event1808Count OU UEFICA2023Status="Updated » # Cela permet de suivre le nombre d’appareils dans chaque compartiment de confiance qui ont été mis à jour $event 1808 = 0 if ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false if ($device. PSObject.Properties['UEFICA2023Status'] - et $device. UEFICA2023Status -eq « Updated ») { $uefiCaUpdated = $true } if ($event 1808 -gt 0 -or $uefiCaUpdated) { # Marquer comme mis à jour pour la logique de tableau de bord/de déploiement, mais NE PAS remplacer ConfidenceLevel $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } else { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force Classification # ConfidenceLevel : # - « Haute confiance », « En observation... », « Temporairement suspendu... », « Non pris en charge... » = utiliser tel tel qu’il est # - Tout le reste (null, empty, « UpdateType :... », « Unknown », « N/A ») = tombe à Action Requise dans les compteurs # Aucune normalisation n’est nécessaire : la branche Else du compteur de streaming le gère } # Gérer OEMManufacturerName vs WMI_Manufacturer (JSON utilise OEM*, hérité utilise WMI_*) if ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force } # Gérer OEMModelNumber par rapport à WMI_Model if ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force } # Gérer FirmwareVersion vs BIOSDescription if ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } $device de retour }
#region traitement incrémentiel/gestion du cache Nombre de chemins d’installation du cache if (-not $CachePath) { $CachePath = Join-Path $OutputPath .cache" } $manifestPath = Join-Path $CachePath « FileManifest.json » $deviceCachePath = Join-Path $CachePath « DeviceCache.json »
Nombre de fonctions de gestion du cache function Get-FileManifest { param([string]$Path) if (Test-Path $Path) { try { $json = Get-Content $Path -Raw | ConvertFrom-Json # Convertir PSObject en table de hachage (compatible PS5.1 - PS7 a -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Name] = $_. Valeur } $ht de retour } catch { return @{} } } return @{} }
function Save-FileManifest { param([hashtable]$Manifest, [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $Manifest | ConvertTo-Json -Profondeur 3 -Compresser | Set-Content $Path -Force }
function Get-DeviceCache { param([string]$Path) if (Test-Path $Path) { try { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host « Loaded device cache : $($cacheData.Count) devices » -ForegroundColor DarkGray $cacheData de retour } catch { Write-Host « Cache endommagé, régénéré » -ForegroundColor Yellow return @() } } return @() }
function Save-DeviceCache { param($Devices, [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } # Convertir en tableau et enregistrer $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Profondeur 10 -Compresser | Set-Content $Path -Force Write-Host « Cache d’appareil enregistré : appareils $($deviceArray.Count) » -ForegroundColor DarkGray }
function Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [table de hachage]$Manifest ) $changed = [System.Collections.ArrayList] ::new() $unchanged = [System.Collections.ArrayList] ::new() $newManifest = @{} # Générer une recherche non sensible à la casse à partir du manifeste (normaliser en minuscules) $manifestLookup = @{} foreach ($mk dans $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file dans $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalize path to lowercase $lwt = $file. LastWriteTimeUtc.ToString(« o ») $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Taille = $file. Longueur } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] if ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Taille -eq $file. Longueur) { [void]$unchanged. Add($file) Continuer } } [void]$changed. Add($file) } return @{ Modifié = $changed Inchangé = $unchanged NewManifest = $newManifest } }
Chargement de fichiers parallèles ultra-rapides à l’aide d’un traitement par lots function Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Compter # Utiliser des lots d’environ 1 000 fichiers chacun pour un meilleur contrôle de la mémoire $batchSize = [math] ::Min(1000, [math] ::Ceiling($totalFiles / [math] ::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]] ::new()
for ($i = 0 ; $i -lt $totalFiles ; $i += $batchSize) { $end = [math] ::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Add($batch) } Write-Host " ($($batches. Count) lots de ~$batchSize fichiers chacun)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]] ::new() # Vérifier si PowerShell 7+ parallel est disponible $canParallel = $PSVersionTable.PSVersion.Major -ge 7 if ($canParallel -and $Threads -gt 1) { # PS7+ : Traiter les lots en parallèle $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]] ::new() foreach ($file dans $batchFiles) { try { $content = [System.IO.File] ::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } catch { } } $batchResults.ToArray() } foreach ($batch dans $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } else { # PS5.1 de secours : traitement séquentiel (toujours rapide pour <10 000 fichiers) foreach ($file en $Files) { try { $content = [System.IO.File] ::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } catch { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Fichier JSON unique if ($InputPath -like « *.json ») { $jsonContent = Get-Content -Chemin $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host « Enregistrements $($allDevices.Count) chargés à partir du fichier » } else { Write-Error « Seul le format JSON est pris en charge. Le fichier doit avoir .json extension. » sortie 1 } } elseif (Test-Path $InputPath -PathType Container) { # Dossier - JSON uniquement $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter « *.json » -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Préférer *_latest.json fichiers s’ils existent (mode par ordinateur) $latestJson = $jsonFiles | Where-Object { $_. Nom -like « *_latest.json » } if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count if ($totalFiles -eq 0) { Write-Error « Aucun fichier JSON trouvé dans : $InputPath » sortie 1 } Write-Host « Fichiers JSON trouvés $totalFiles » -ForegroundColor Gray Fonction d’assistance # pour faire correspondre les niveaux de confiance (gère les formulaires courts et complets) # Défini à l’avance pour que streamingMode et les chemins d’accès normaux puissent l’utiliser function Test-ConfidenceLevel { param([string]$Value, [string]$Match) if ([string] ::IsNullOrEmpty($Value)) { return $false } switch ($Match) { « HighConfidence » { return $Value -eq « High Confidence » } « UnderObservation » { return $Value -like « Under Observation* » } « ActionRequired » { return ($Value -like « *Action Required* » -or $Value -eq « Action Required ») } « TemporarilyPaused » { return $Value -like « Temporairement suspendu* » } « NotSupported » { return ($Value -like « Not Supported* » -or $Value -eq « Not Supported ») } default { return $false } } } mode de diffusion en continu #region - Traitement à l’efficacité de la mémoire pour les jeux de données volumineux # Toujours utiliser StreamingMode pour un traitement efficace en mémoire et un tableau de bord nouveau style if (-not $StreamingMode) { Write-Host « Activation automatique de StreamingMode (nouveau tableau de bord) » -ForegroundColor Yellow $StreamingMode = $true if (-not $IncrementalMode) { $IncrementalMode = $true } } # Lorsque -StreamingMode est activé, traitez les fichiers en blocs en conservant uniquement les compteurs en mémoire.# Les données au niveau de l’appareil sont écrites dans des fichiers JSON par bloc pour le chargement à la demande dans le tableau de bord.Utilisation de la mémoire : ~1,5 Go, quelle que soit la taille du jeu de données (contre 10 à 20 Go sans diffusion en continu).if ($StreamingMode) { Write-Host « MODE STREAMING activé - traitement à l’efficacité de la mémoire » -ForegroundColor Vert $streamSw = [System.Diagnostics.Stopwatch] ::StartNew() # VÉRIFICATION INCRÉMENTIELLE : si aucun fichier n’a été modifié depuis la dernière exécution, ignorez entièrement le traitement if ($IncrementalMode -and -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath .cache " $stManifestPath = Join-Path $stManifestDir « StreamingManifest.json » if (Test-Path $stManifestPath) { Write-Host « Vérification des modifications depuis la dernière exécution du streaming... » -ForegroundColor Cyan $stOldManifest = Get-FileManifest -Chemin d'$stManifestPath if ($stOldManifest.Count -gt 0) { $stChanged = $false # Case activée rapide : même nombre de fichiers ? if ($stOldManifest.Count -eq $totalFiles) { # Vérifiez les 100 fichiers NEWEST (triés par LastWriteTime décroissant) # Si un fichier a changé, il aura l’horodatage le plus récent et apparaîtra en premier $sampleSize = [math] ::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc - Décroissant | Select-Object -Premier $sampleSize foreach ($sf dans $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Pause } # Comparer les horodatages - mis en cache peut être DateTime ou chaîne après un aller-retour JSON $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc try { # Si le cache a déjà la valeur DateTime (ConvertFrom-Json convertit automatiquement), utilisez directement if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } else { $cachedDT = [DateTimeOffset] ::P arse(« $cachedLWT »). UtcDateTime } if ([math] ::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Pause } } catch { $stChanged = $true Pause } } } else { $stChanged = $true } if (-not $stChanged) { # Vérifier s’il existe des fichiers de sortie $stSummaryExists = Get-ChildItem (Join-Path $OutputPath « SecureBoot_Summary_*.csv ») -EA SilentlyContinue | Select-Object -First 1 $stDashExists = Get-ChildItem (Join-Path $OutputPath « SecureBoot_Dashboard_*.html ») -EA SilentlyContinue | Select-Object -First 1 if ($stSummaryExists -and $stDashExists) { Write-Host « Aucune modification détectée (fichiers $totalFiles inchangés) - Ignorer le traitement » -ForegroundColor Vert Write-Host « Last dashboard : $($stDashExists.FullName) » -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-CSV Write-Host " Appareils : $($cachedStats.TotalDevices) | Mise à jour : $($cachedStats.Updated) | Erreurs : $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host « Completed in $([math] ::Round($streamSw.Elapsed.TotalSeconds, 1))s (aucun traitement nécessaire) » -ForegroundColor Green $cachedStats de retour } } else { # DELTA PATCH : Rechercher exactement les fichiers modifiés Write-Host « Modifications détectées - Identification des fichiers modifiés... » -ForegroundColor Yellow $changedFiles = [System.Collections.ArrayList] ::new() $newFiles = [System.Collections.ArrayList] ::new() foreach ($jf dans $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } else { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc try { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset] ::P arse(« $cachedLWT »). UtcDateTime } if ([math] ::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } catch { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [math] ::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Modifié : $($changedFiles.Count) | Nouveau : $($newFiles.Count) | Total : $totalChanged ($changePct %) " -ForegroundColor Yellow if ($totalChanged -gt 0 -and $changePct -lt 10) { # MODE DE CORRECTIF DELTA : <10 % modifié, corriger les données existantes Write-Host « Mode de correctif delta ($changePct % < 10 %) - Mise à jour corrective des fichiers $totalChanged... - ForegroundColor Vert $dataDir = Join-Path $OutputPath « data » Nombre de chargements d’enregistrements d’appareil modifiés/nouveaux $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df dans $allDeltaFiles) { try { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData if ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } catch { } } Write-Host « Enregistrements d’appareil modifiés $($deltaDevices.Count) chargés » -ForegroundColor Gray # Pour chaque catégorie JSON : supprimer les anciennes entrées pour les noms d’hôte modifiés, ajouter de nouvelles entrées $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 dans $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat dans $categoryFiles) { $catPath = Join-Path $dataDir « $cat.json » if (Test-Path $catPath) { try { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Supprimer les anciennes entrées pour les noms d’hôte modifiés $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Reclassifier chaque appareil modifié en catégories # (sera ajouté ci-dessous après la classification) $catData | ConvertTo-Json -Profondeur 5 | Set-Content $catPath -Encodage UTF8 } catch { } } } # Classifier chaque appareil modifié et ajouter aux fichiers de catégorie appropriés foreach ($dev dans $deltaDevices.Values) { $slim = [ordonné]@{ 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 } } $isUpd = $dev. IsUpdated -eq $true $conf = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { « » } $hasErr = (-not [string] ::IsNullOrEmpty($dev. UEFICA2023Error) et $dev. UEFICA2023Error -ne « 0 » -and $dev. UEFICA2023Error -ne « ») $tskDis = ($dev. SecureBootTaskEnabled -eq $false -or $dev. SecureBootTaskStatus -eq 'Disabled' -or $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -et "$($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 -ou $dev. MissingKEK -eq $true) $hKI = ((-not [string] ::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -ou (-not [string] ::IsNullOrEmpty($dev. KnownIssueId))) $rStat = if ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { « » } # Ajouter aux fichiers de catégorie correspondants $targets = @() if ($isUpd) { $targets += « updated_devices » } if ($hasErr) { $targets += « errors » } if ($hKI) { $targets += « known_issues » } if ($mKEK) { $targets += « missing_kek » } if (-not $isUpd -and $sbOn) { $targets += « not_updated » } if ($tskDis) { $targets += « task_disabled » } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += « temp_failures » } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += « perm_failures » } if (-not $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += « action_required » } if (-not $sbOn) { $targets += « secureboot_off » } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq « InProgress ») { $targets += « rollout_inprogress » } foreach ($tgt dans $targets) { $tgtPath = Join-Path $dataDir « $tgt.json » if (Test-Path $tgtPath) { $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json $existing = @($existing) + @([PSCustomObject]$slim) $existing | ConvertTo-Json -Profondeur 5 | Set-Content $tgtPath -Encodage UTF8 } } } # Régénérer des csv à partir de JSONs corrigés Write-Host « Régénération des csv à partir de données corrigées... » -ForegroundColor Gray $newTimestamp = Get-Date -Format « aaaaMMdd-HHmmss » foreach ($cat dans $categoryFiles) { $catJsonPath = Join-Path $dataDir « $cat.json » $catCsvPath = Join-Path $OutputPath « SecureBoot_${cat}_$newTimestamp.csv » if (test-path $catJsonPath) { try { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8 } } catch { } } } # Recompter les statistiques des fichiers JSON corrigés Write-Host « Recalcul d’un résumé à partir de données corrigées... » -ForegroundColor Gray $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString(« aaaa-MM-jj HH :mm :ss ») } $pTotal = 0 ; $pUpdated = 0 ; $pErrors = 0 ; $pKI = 0 ; $pKEK = 0 $pTaskDis = 0 ; $pTempFail = 0 ; $pPermFail = 0 ; $pActionReq = 0 ; $pSBOff = 0 ; $pRIP = 0 foreach ($cat dans $categoryFiles) { $catPath = Join-Path $dataDir « $cat.json » $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } catch { } } switch ($cat) { « updated_devices » { $pUpdated = $cnt } « errors » { $pErrors = $cnt } « known_issues » { $pKI = $cnt } « missing_kek » { $pKEK = $cnt } « not_updated » { } # calculé « 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). Compter $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host « Correctif delta terminé : $totalChanged appareils mis à jour » -ForegroundColor Vert Write-Host " Total : $pTotal | Mise à jour : $pUpdated | NotUpdated : $pNotUpdated | Erreurs : $pErrors" -ForegroundColor White # Mettre à jour le manifeste $stManifestDir = Join-Path $OutputPath .cache" $stNewManifest = @{} foreach ($jf dans $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString(« o ») ; Taille = $jf. Longueur } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Completed in $([math] ::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices) » -ForegroundColor Green # Effectuer un retraitement de streaming complet pour régénérer le tableau de bord HTML # Les fichiers de données sont déjà corrigés, ce qui garantit que le tableau de bord reste à jour Write-Host « Régénération du tableau de bord à partir de données corrigées... » -ForegroundColor Yellow } else { Write-Host « $changePct % files changed (>= 10 %) - full streaming reprocess required » -ForegroundColor Yellow } } } } } # Créer un sous-répertoire de données pour les fichiers JSON d’appareil à la demande $dataDir = Join-Path $OutputPath « data » if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Déduplication via HashSet (O(1) par recherche, ~50 Mo pour 600 000 noms d’hôte) $seenHostnames = [System.Collections.Generic.HashSet[string]] ::new([System.StringComparer] ::OrdinalIgnoreCase) # Compteurs résumés légers (remplace $allDevices + $uniqueDevices en mémoire) $c = @{ Total = 0 ; SBEnabled = 0 ; SBOff = 0 Mise à jour = 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 } # Suivi des compartiments pour AtRisk/SafeList (jeux légers) $stFailedBuckets = [System.Collections.Generic.HashSet[string]] ::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]] ::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{} ; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} Nombre de fichiers de données d’appareil en mode Batch : accumulation par bloc, vidage aux limites des blocs $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 dans $stDeviceFiles) { $dfPath = Join-Path $dataDir « $dfName.json » [System.IO.File] ::WriteAllText($dfPath, « ['n », [System.Text.Encoding] ::UTF8) $stDeviceFilePaths[$dfName] = $dfPath ; $stDeviceFileCounts[$dfName] = 0 } # Enregistrement d’appareil mince pour la sortie JSON (uniquement les champs essentiels, ~200 octets vs ~2 Ko complet) function Get-SlimDevice { param($Dev) return [ordered]@{ HostName = $Dev.HostName WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { « » } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { « » } } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { « » } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { « » } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } 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 } } } # Vider le lot dans un fichier JSON (mode ajout) function 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 dans $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) } # BOUCLE DE STREAMING PRINCIPALE $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 } $stTotalChunks = [math] ::Ceiling($totalFiles / $stChunkSize) $stPeakMemMB = 0 if ($stTotalChunks -gt 1) { Write-Host « Traitement des fichiers $totalFiles dans $stTotalChunks blocs de $stChunkSize (diffusion en continu, threads $ParallelThreads) » -ForegroundColor Cyan } else { Write-Host « Traitement des fichiers $totalFiles (streaming, $ParallelThreads threads) » -ForegroundColor Cyan } for ($ci = 0 ; $ci -lt $stTotalChunks ; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [math] ::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] if ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) files)) : " -NoNewline -ForegroundColor Gray } else { Write-Host " Chargement des fichiers $($cFiles.Count) : " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch] ::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads Nombre de listes de lots par bloc $cBatches = @{} foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]] ::new() } $cNew = 0 ; $cDupe = 0 foreach ($raw dans $rawDevices) { if (-not $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Hostname if (-not $hostname) { continue } if ($seenHostnames.Contains($hostname)) { $cDupe++ ; continue } [void]$seenHostnames.Add($hostname) $cNew++ ; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled) » -ne « False ») if ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++ ; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = if ($device. PSObject.Properties['ConfidenceLevel'] - et $device. ConfidenceLevel) { "$($device. ConfidenceLevel) » } else { « » } $hasErr = (-not [string] ::IsNullOrEmpty($device. UEFICA2023Error) -and "$($device. UEFICA2023Error) » -ne « 0 » -and « $($device. UEFICA2023Error) » -ne « ») $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus) » -eq 'Disabled' -or « $($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = if ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId) » } 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 -ou $device. MissingKEK -eq $true -or "$($device. MissingKEK) » -eq « True ») $hKI = ((-not [string] ::IsNullOrEmpty($device. SkipReasonKnownIssue)) -ou (-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 { « Unknown » } $bid = if (-not [string] ::IsNullOrEmpty($bid)) { $bid } else { « » } Indicateur # Mise à jour de pré-calcul En attente (stratégie/WinCS appliquée, status pas encore mise à jour, SB ON, tâche non désactivée) $uefiStatus = if ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status) » } else { « » } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy) " -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] - et $device. WinCSKeyApplied -eq $true) $statusPending = ([string] ::IsNullOrEmpty($uefiStatus) -or $uefiStatus -eq 'NotStarted' -or $uefiStatus -eq 'InProgress') $isUpdatePending = ((($hasPolicy -or $hasWinCS) -and $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis) if ($isUpd) { $c.Updated++ ; [void]$stSuccessBuckets.Add($bid) ; $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Suivre les appareils mis à jour qui doivent être redémarrés (UEFICA2023Status=Mis à jour, mais Event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++ ; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF : hors de portée, ne classifiez pas par confiance } else { if ($isUpdatePending) { } # Compté séparément dans Mise à jour en attente — mutuellement exclusif pour le graphique en secteurs elseif (Test-ConfidenceLevel $conf « HighConfidence ») { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf « UnderObservation ») { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf « TemporarilyPaused ») { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf « NotSupported ») { $c.NotSupported++ } else { $c.ActionReq++ } if ([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 } else { $device. KnownIssueId } if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 } ; $stKnownIssueCounts[$ki]++ } if ($mKEK) { $c.WithMissingKEK++ ; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++ ; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++ ; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq « InProgress ») { $c.RolloutInProgress++ ; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne « InProgress ») { $c.NotYetInitiated++ } if ($rStat -eq « InProgress » -and $e 1808 -eq 0) { $c.InProgress++ } # Mise à jour en attente : stratégie ou WinCS appliquée, status en attente, SB ON, tâche non désactivée if ($isUpdatePending) { $c.UpdatePending++ ; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } Nombre d’appareils sous observation (distincts de l’action requise) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Action requise : non mis à jour, SB ON, ne correspondant pas à d’autres catégories de confiance, pas Mise à jour en attente si (-pas $isUpd -et $sbOn -and -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarilyPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0 ; Mise à jour =0 ; UpdatePending=0 ; HighConf=0 ; UnderObs=0 ; ActionReq=0 ; TempPaused=0 ; NotSupported=0 ; SBOff=0 ; WithErrors=0 } } $stMfrCounts[$mfr]. Total++ if ($isUpd) { $stMfrCounts[$mfr]. Mis à jour++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. UpdatePending++ } elseif (Test-ConfidenceLevel $conf « HighConfidence ») { $stMfrCounts[$mfr]. HighConf++ } elseif (Test-ConfidenceLevel $conf « UnderObservation ») { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf « TemporarilyPaused ») { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf « NotSupported ») { $stMfrCounts[$mfr]. NotSupported++ } else { $stMfrCounts[$mfr]. ActionReq++ } if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Suivre tous les appareils par compartiment (y compris BucketId vide) $bucketKey = if ($bid -and $bid -ne « ») { $bid } else { « (empty) » } } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0 ; Mise à jour =0 ; Manufacturer=$mfr ; Model=" » ; BIOS=" » } if ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Modèle = $device. WMI_Model } if ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ if ($isUpd) { $stAllBuckets[$bucketKey]. Mis à jour++ } } # Vider les lots sur le disque foreach ($df en $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null ; $cBatches = $null ; [System.GC] ::Collect() $cSw.Stop() $cTime = [Math] ::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 Mo, 0) if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " + $cNew nouveau, $cDupe dupes, ${cTime}s | Mem : ${cMem}MB$cEta" -ForegroundColor Vert } Nombre de tableaux JSON finalisés foreach ($dfName dans $stDeviceFiles) { [System.IO.File] ::AppendAllText($stDeviceFilePaths[$dfName], « 'n] », [System.Text.Encoding] ::UTF8) Write-Host " $dfName.json : $($stDeviceFileCounts[$dfName]) devices » -ForegroundColor DarkGray } Nombre de statistiques dérivées du calcul $stAtRisk = 0 ; $stSafeList = 0 foreach ($bid dans $stAllBuckets.Keys) { $b = $stAllBuckets[$bid] ; $nu = $b.Count - $b.Updated if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [math] ::Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = count from not_updated batch (appareils avec SB ON et non mis à jour) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [ordonné]@{ ReportGeneratedAt = (Get-Date). ToString(« aaaa-MM-jj HH :mm :ss ») TotalDevices = $c.Total ; SecureBootEnabled = $c.SBEnabled ; SecureBootOFF = $c.SBOff Updated = $c.Updated ; HighConfidence = $c.HighConf ; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq ; TemporarilyPaused = $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) { [math] ::Round($stNotUptodate/$c.Total*100,1) } else { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math] ::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count ; PeakMemoryMB = $stPeakMemMB ; ProcessingMode = « Streaming » } Nombre de csv d’écriture [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath « SecureBoot_Summary_$timestamp.csv ») -NoTypeInformation -Encoding UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Décroissant | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Clé; Count=$_. Value.Total ; Updated=$_. Value.Updated ; HighConfidence=$_. Value.HighConf ; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (Join-Path $OutputPath « SecureBoot_ByManufacturer_$timestamp.csv ») -NoTypeInformation -Encoding UTF8 $stErrorCodeCounts.GetEnumerator() | valeur Sort-Object -Décroissante | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Clé; Count=$_. Valeur; SampleDevices=($stErrorCodeSamples[$_. Clé] -join « , « ) } } | Export-Csv -Path (Join-Path $OutputPath « SecureBoot_ErrorCodes_$timestamp.csv ») -NoTypeInformation -Encoding UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Décroissant | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Clé; Count=$_. Value.Count ; Updated=$_. Value.Updated ; NotUpdated=$_. Value.Count-$_. Value.Updated ; Manufacturer=$_. Value.Manufacturer } } | Export-Csv -Path (Join-Path $OutputPath « SecureBoot_UniqueBuckets_$timestamp.csv ») -NoTypeInformation -Encoding UTF8 # Générer des csv compatibles avec orchestrateur (noms de fichiers attendus pour Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir « not_updated.json » if (Test-Path $notUpdatedJsonPath) { try { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json if ($nuData.Count -gt 0) { Csv # NotUptodate : l’orchestrateur recherche *NotUptodate*.csv $nuData | Export-Csv -Path (Join-Path $OutputPath « SecureBoot_NotUptodate_$timestamp.csv ») -NoTypeInformation -Encoding UTF8 Write-Host « Orchestrator CSV : SecureBoot_NotUptodate_$timestamp.csv (appareils $($nuData.Count) ) » -ForegroundColor Gray } } catch { } } # Écrire des données JSON pour le tableau de bord $stats | ConvertTo-Json -Profondeur 3 | Set-Content (Join-Path $dataDir « summary.json ») - Encodage UTF8 # SUIVI HISTORIQUE : Enregistrer le point de données pour le graphique de tendance # Utilisez un emplacement de cache stable pour que les données de tendance persistent dans les dossiers d’agrégation horodatés. # Si OutputPath ressemble à « ...\Aggregation_yyyyMMdd_HHmmss », le cache est placé dans le dossier parent.# Sinon, le cache passe à l’intérieur de OutputPath lui-même.$parentDir = Split-Path $OutputPath -Parent $leafName = Split-Path $OutputPath -Leaf if ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current') { # Dossier horodaté créé par Orchestrator : utilisez le parent pour le cache stable $historyPath = Join-Path $parentDir .cache\trend_history.json" } else { $historyPath = Join-Path $OutputPath .cache\trend_history.json" } $historyDir = Split-Path $historyPath -Parent if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() if (Test-Path $historyPath) { try { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } catch { $historyData = @() } } # Également case activée dans OutputPath\.cache\ (emplacement hérité des versions antérieures) # Fusionner tous les points de données qui ne se trouvent pas déjà dans l’historique principal 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) { try { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Date }) foreach ($entry dans $innerData) { if ($entry. Date et $entry. Date -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host « Points de données $($innerData.Count) fusionnés à partir du cache interne » -ForegroundColor DarkGray } } catch { } } }
# BOOTSTRAP : si l’historique des tendances est vide/partiellement alloué, reconstruire à partir de données historiques if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host « Historique des tendances de démarrage à partir de données historiques... » -ForegroundColor Yellow $dailyData = @{} # Source 1 : Csv de résumé dans le dossier actif (Aggregation_Current conserve tous les csv résumés) $localSummaries = Get-ChildItem $OutputPath -Filter « SecureBoot_Summary_*.csv » -EA SilentlyContinue | nom de la Sort-Object foreach ($summCsv dans $localSummaries) { try { $summ = Import-Csv $summCsv.FullName | Select-Object -First 1 if ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -et $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString(« aaaa-MM-jj ») $updated = if ($summ. Mise à jour) { [int]$summ. Mis à jour } else { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Date = $dateStr ; Total = [int]$summ. TotalDevices ; Mis à jour = $updated ; NotUpdated = $notUpd NeedsReboot = 0 ; Erreurs = 0 ; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } catch { } } # Source 2 : Anciens dossiers Aggregation_* horodatés (hérités, s’ils existent toujours) $aggFolders = Get-ChildItem $parentDir -Directory -Filter « Aggregation_* » -EA SilentlyContinue | Where-Object { $_. Name -match '^Aggregation_\d{8}' } | nom de la Sort-Object foreach ($folder dans $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter « SecureBoot_Summary_*.csv » -EA SilentlyContinue | Select-Object -First 1 if ($summCsv) { try { $summ = Import-Csv $summCsv.FullName | Select-Object -First 1 if ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Name -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = if ($summ. Mise à jour) { [int]$summ. Mis à jour } else { 0 } $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Date = $dateStr ; Total = [int]$summ. TotalDevices ; Mis à jour = $updated ; NotUpdated = $notUpd NeedsReboot = 0 ; Erreurs = 0 ; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } catch { } } } Source 3 : RolloutState.json WaveHistory (a des horodatages par onde à partir du jour 1) # Cela fournit des points de données de base, même s’il n’existe aucun ancien dossier d’agrégation $rolloutStatePaths = @( (Join-Path $parentDir « RolloutState\RolloutState.json »), (Join-Path $OutputPath « RolloutState\RolloutState.json ») ) foreach ($rsPath dans $rolloutStatePaths) { if (Test-Path $rsPath) { try { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Utiliser les dates de début des vagues comme points de données de tendance # Calculer les appareils cumulés ciblés à chaque vague $cumulativeTargeted = 0 foreach ($wave dans $rsData.WaveHistory) { if ($wave. StartedAt et $wave. DeviceCount) { $waveDate = ([datetime]$wave. StartedAt). ToString(« aaaa-MM-jj ») $cumulativeTargeted += [int]$wave. DeviceCount if (-not $dailyData.ContainsKey($waveDate)) { # Approximatif : à l’heure de début de la vague, seuls les appareils des vagues précédentes ont été mis à jour $dailyData[$waveDate] = [PSCustomObject]@{ Date = $waveDate ; Total = $c.Total ; Mise à jour = [math] ::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NotUpdated = $c.Total - [math] ::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0 ; Erreurs = 0 ; ActionRequired = 0 } } } } } } catch { } break # Utiliser d’abord trouvé } }
if ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | clé Sort-Object | ForEach-Object { $_. Valeur }) Write-Host « Bootstrapped $($historyData.Count) data points from historical summaries » -ForegroundColor Green } }
# Ajouter un point de données actuel (dédupliquer par jour - conserver la dernière date par jour) $todayKey = (Get-Date). ToString(« aaaa-MM-jj ») $existingToday = $historyData | Where-Object { "$($_. Date) » -like « $todayKey* » } if ($existingToday) { # Remplacer l’entrée du jour $historyData = @($historyData | Where-Object { "$($_. Date) » -notlike « $todayKey* » }) } $historyData += [PSCustomObject]@{ Date = $todayKey Total = $c.Total Updated = $c.Updated NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Errors = $c.WithErrors ActionRequired = $c.ActionReq } # Supprimer les points de données incorrects (0 total) et conserver les 90 derniers $historyData = @($historyData | Where-Object { [int]$_. Total -gt 0 }) # Aucune limite : les données de tendance sont d’environ 100 octets/entrée, une année entière = ~36 Ko $historyData | ConvertTo-Json -Profondeur 3 | Set-Content $historyPath -Encodage UTF8 Write-Host " Trend history : $($historyData.Count) data points » -ForegroundColor DarkGray # Générer des données de graphique de tendance pour HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)' » }) -join « , » $trendUpdated = ($historyData | ForEach-Object { $_. Mise à jour }) -join « , » $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join « , » $trendTotal = ($historyData | ForEach-Object { $_. Total }) -join « , » # Projection : étendre la courbe de tendance à l’aide d’un doublement exponentiel (2,4,8,16...) # Dérive la taille des vagues et la période d’observation à partir de données d’historique des tendances réelles.Nombre - Taille de la vague = augmentation la plus importante sur une seule période observée dans l’historique (la vague la plus récente déployée) Nombre - Jours d’observation = jours calendaires moyens entre les points de données de tendance (fréquence d’exécution) # Double ensuite la taille de l’onde chaque période, correspondant à la stratégie de croissance 2x de l’orchestrateur.$projLabels = « » ; $projUpdated = « » ; $projNotUpdated = « » ; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Updated $remaining = $stNotUptodate # Uniquement les appareils SB-ON non mis à jour (à l’exclusion de SecureBoot OFF) $projDates = @() ; $projValues = @() ; $projNotUpdValues = @() $projDate = Get-Date
# Dériver la taille des ondes et la période d’observation à partir de l’historique des tendances $increments = @() $dayGaps = @() for ($hi = 1 ; $hi -lt $historyData.Count ; $hi++) { $inc = $historyData[$hi]. Mise à jour : $historyData[$hi-1]. Actualisé if ($inc -gt 0) { $increments += $inc } try { $d 1 = [datetime] ::P arse($historyData[$hi-1]. Date) $d 2 = [datetime] ::P arse($historyData[$hi]. Date) $gap = ($d 2 - $d 1). TotalDays if ($gap -gt 0) { $dayGaps += $gap } } catch {} } # Taille de l’onde = incrément positif le plus récent (onde actuelle), secours à la moyenne, minimum 2 $waveSize = if ($increments. Count -gt 0) { [math] ::Max(2, $increments[-1]) } else { 2 } # Période d’observation = écart moyen entre les points de données (jours calendaires par vague), minimum 1 $waveDays = if ($dayGaps.Count -gt 0) { [math] ::Max(1, [math] ::Round(($dayGaps | Measure-Object -Average). Moyenne, 0)) } else { 1 }
Write-Host " Projection : waveSize=$waveSize (à partir du dernier incrément), waveDays=$waveDays (avg gap from history) » -ForegroundColor DarkGray
$dayCounter = 0 Nombre de projets jusqu’à ce que tous les appareils soient mis à jour ou 365 jours maximum for ($pi = 1 ; $pi -le 365 ; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # À chaque limite de période d’observation, déployez une onde puis doublez if ($dayCounter -ge $waveDays) { $devicesThisWave = [math] ::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate ; $remaining = 0 } # Taille d’onde double pour la période suivante (stratégie d’orchestrateur 2x) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += « '$($projDate.ToString(« aaaa-MM-dd »)' » $projValues += $lastUpdated $projNotUpdValues += [math] ::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 « Projection : besoin d’au moins 2 points de données de tendance pour dériver le minutage des vagues » -ForegroundColor DarkGray } # Générer des chaînes de données de graphique combinées pour la chaîne here-string $allChartLabels = if ($hasProjection) { « $trendLabels,$projLabels » } else { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } else { « » } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { « » } $histCount = ($historyData | Measure-Object). Compter $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Décroissant | ForEach-Object { @{ name=$_. Clé; total=$_. Value.Total ; updated=$_. Value.Updated ; highConf=$_. Value.HighConf ; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Profondeur 3 | Set-Content (Join-Path $dataDir « manufacturers.json ») - Encodage UTF8 # Convertir des fichiers de données JSON au format CSV pour les téléchargements Excel lisibles par l’utilisateur Write-Host « Conversion des données d’appareil en CSV pour le téléchargement d’Excel... » -ForegroundColor Gray foreach ($dfName dans $stDeviceFiles) { $jsonFile = Join-Path $dataDir « $dfName.json » $csvFile = Join-Path $OutputPath « SecureBoot_${dfName}_$timestamp.csv » if (Test-Path $jsonFile) { try { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json if ($jsonData.Count -gt 0) { # Inclure des colonnes supplémentaires pour 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) rows -> CSV » -ForegroundColor DarkGray } } catch { Write-Host " $dfName - ignoré » -ForegroundColor DarkYellow } } } # Générer un tableau de bord HTML autonome $htmlPath = Join-Path $OutputPath « SecureBoot_Dashboard_$timestamp.html » Write-Host « Génération d’un tableau de bord HTML autonome... » -ForegroundColor Yellow # PROJECTION DE VITESSE : calculer à partir de l’historique d’analyse ou du résumé précédent $stDeadline = [datetime]"2026-06-24 » # KEK cert expiry $stDaysToDeadline = [math] ::Max(0, ($stDeadline - (Get-Date)). Jours) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = « N/A » $stWorkingDays = 0 $stCalendarDays = 0 # Essayez d’abord l’historique des tendances (léger, déjà géré par l’agrégateur — remplace les ScanHistory.json gonflés) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Mise à jour de -ge 0 }) 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, [Math] ::Min(10, $curr. Date.Length))) $daysDiff = ($currDate - $prevDate). TotalDays if ($daysDiff -gt 0) { $updDiff = [int]$curr. Mise à jour : [int]$prev. Actualisé if ($updDiff -gt 0) { $stDevicesPerDay = [math] ::Round($updDiff / $daysDiff, 0) $stVelocitySource = « TrendHistory » } } } } # Essayer le résumé du déploiement de l’orchestrateur (a une vitesse précalcalée) if ($stVelocitySource -eq « N/A » -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { try { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [math] ::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = « Orchestrator » if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } catch { } } # Secours : essayez le csv récapitulative précédent (recherche dans le dossier actif ET dossiers d’agrégation parent/frère) if ($stVelocitySource -eq « N/A ») { $searchPaths = @( (Join-Path $OutputPath « SecureBoot_Summary_*.csv ») ) # Rechercher également des dossiers d’agrégation frères (l’orchestrateur crée un dossier à chaque exécution) $parentPath = Split-Path $OutputPath -Parent 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 - Décroissant | Select-Object -First 1 if ($prevSummary) { try { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-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 = [math] ::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = « PreviousReport » } } } catch { } } } # Secours : calculer la vitesse à partir de l’étendue complète de l’historique des tendances (premier et dernier point de données) if ($stVelocitySource -eq « N/A » -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Mise à jour de -ge 0 }) if ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime] ::P arse($first. Date.Substring(0, [Math] :Min(10, $first. Date.Length))) $lastDate = [datetime] ::P arse($last. Date.Substring(0, [Math] ::Min(10, $last. Date.Length))) $daysDiff = ($lastDate - $firstDate). TotalDays if ($daysDiff -gt 0) { $updDiff = [int]$last. Mise à jour : [int]$first. Actualisé if ($updDiff -gt 0) { $stDevicesPerDay = [math] ::Round($updDiff / $daysDiff, 1) $stVelocitySource = « TrendHistory » } } } } # Calculer la projection à l’aide d’un doublement exponentiel (cohérent avec le graphique de tendance) # Réutiliser les données de projection déjà calculées pour le graphique, le cas échéant if ($hasProjection -and $projDates.Count -gt 0) { # Utiliser la dernière date projetée (lorsque tous les appareils sont mis à jour) $lastProjDateStr = $projDates[-1] -replace « ' », « » $stProjectedDate = ([datetime] ::P arse($lastProjDateStr)). ToString(« MMM dd, aaaa ») $stCalendarDays = ([datetime] ::P arse($lastProjDateStr) - (Get-Date)). Jours $stWorkingDays = 0 $d = Get-Date for ($i = 0 ; $i -lt $stCalendarDays ; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -and $stNotUptodate -gt 0) { # Secours : projection linéaire si aucune donnée exponentielle n’est disponible $daysNeeded = [math] ::Ceiling($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString(« MMM dd, aaaa ») $stWorkingDays = 0 ; $stCalendarDays = $daysNeeded $d = Get-Date for ($i = 0 ; $i -lt $daysNeeded ; $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } Nombre de fichiers HTML de vitesse de génération $velocityHtml = if ($stDevicesPerDay -gt 0) { « <div><forte>🚀 ; Devices/Day :</strong> $($stDevicesPerDay.ToString('N0')) (source : $stVelocitySource)</div>" + « <div><forte>📅 ; Achèvement projeté :</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><forte>🕐 ; Jours ouvrés :</strong> $stWorkingDays | <jours>Calendar forts :</strong> $stCalendarDays</div>" + "<div style='font-size :.8em ; color :#888'>Échéance : 24 juin 2026 (expiration du certificat KEK) | Jours restants : $stDaysToDeadline</div> » } else { "<div style='padding :8px ; background :#fff3cd ; border-radius :4px ; border-left :3px solid #ffc107'>" + « <forte>📅 ; Achèvement projeté : </strong> Données insuffisantes pour le calcul de la vitesse. " + « Exécuter l’agrégation au moins deux fois avec des modifications de données pour établir un taux.<br/> » + « <échéance>forte : </strong> 24 juin 2026 (expiration du certificat KEK) | <jours>forts restants : </strong> $stDaysToDeadline</div> » } Compte à rebours de l’expiration du certificat $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24 » $certUefiExpiry = [datetime]"27-06-2026 » $certPcaExpiry = [datetime]"2026-10-19 » $daysToKek = [math] ::Max(0, ($certKekExpiry - $certToday). Jours) $daysToUefi = [math] ::Max(0, ($certUefiExpiry - $certToday). Jours) $daysToPca = [math] ::Max(0, ($certPcaExpiry - $certToday). Jours) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Helper : Lire les enregistrements à partir de JSON, résumé du compartiment de génération + N premières lignes d’appareil $maxInlineRows = 200 function Build-InlineTable { param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = « ») $bucketSummary = « » $deviceRows = « » $totalCount = 0 if (Test-Path $JsonPath) { try { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Compter # RÉSUMÉ DU COMPARTIMENT : Regrouper par BucketId, afficher les nombres par compartiment avec Mise à jour à partir des statistiques globales du compartiment if ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | nombre de Sort-Object -Décroissant $bucketSummary = "><2 h3 style='font-size :.95em ; color :#333 ; margin :10px 0 5px'><3 By Hardware Bucket ($($buckets. Count) buckets)><4 /h3> » $bucketSummary += "><6 div style='max-height :300px ; overflow-y :auto ; margin-bottom :15px'><table><thead><tr><th><5 BucketID><6 /th><th style='text-align :right'>Total</th><th style='text-align :right ; color :#28a745'>mise à jour</th><th style='text-align :right ; color :#dc3545'>Not Updated</th><th><1 Manufacturer><2 /th></tr></thead><tbody>" foreach ($b dans $buckets) { $bid = if ($b.Name) { $b.Name } else { « (empty) » } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Obtenir le nombre de mises à jour à partir des statistiques globales du compartiment (tous les appareils de ce compartiment sur l’ensemble du jeu de données) $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 ; color :#28a745 ; font-weight :bold'>$bUpdatedGlobal><2 /td><td style='text-align :right ; color :#dc3545 ; font-weight :bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += « </tbody></table></div> » } # DÉTAILS DE L’APPAREIL : N premières lignes en tant que liste plate $slice = $data | Select-Object -Premier $MaxRows foreach ($d dans $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match « High ») { '<span class="badge badge-success">High Conf><2 /span>' } elseif ($conf -match « Not Sup ») { '<span class="badge badge-danger">Non pris en charge><6 /span>' } elseif ($conf -match « Under ») { '<span class="badge badge-info">Under Obs><0 /span>' } elseif ($conf -match « Paused ») { '<span class="badge badge-warning">Paused><4 /span>' } else { '<span class="badge badge-warning">Action Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="badge badge-success"><01 Updated</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge badge-danger"><05 Error</span>' } else { '><08 span class="badge badge-warning"><09 En attente><0 /span>' } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 // td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size :.75em'><39 $($d.BucketId)><40 /td></tr><3 'n' } } catch { } } if ($totalCount -eq 0) { return "><44 div style='padding :20px ; color :#888 ; font-style :italic'><45 Aucun appareil de cette catégorie.><46 /div> » } $showing = [math] ::Min($MaxRows, $totalCount) $header = "><48 div style='margin :5px 0 ; font-size :.85em ; color :#666'><49 Total : $($totalCount.ToString(« N0 »)) devices » if ($CsvFileName) { $header += " | ><50 un href='$CsvFileName' style='color :#1a237e ; font-weight :bold'>📄 ; Télécharger le fichier CSV complet pour Excel><3 /a>" } $header += « ><55 /div> » $deviceHeader = "><57 h3 style='font-size :.95em ; color :#333 ; margin :10px 0 5px'><58 Détails de l’appareil (montrant la première $showing)><59 /h3>" $deviceTable = "><61 div style='max-height :500px ; overflow-y :auto'><table><thead><tr><th><0 HostName><1 /th><th><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th><th><6 Status><7 /th><th><0 Error><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div> » return « $header$bucketSummary$deviceHeader$deviceTable » } # Générer des tables inline à partir des fichiers JSON déjà sur le disque, en les liant à des csv $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 » # Table personnalisée pour la mise à jour en attente : inclut les colonnes UEFICA2023Status et UEFICA2023Error $tblUpdatePending = « » $upJsonPath = Join-Path $dataDir « update_pending.json » if (test-Path $upJsonPath) { try { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Count if ($upCount -gt 0) { $upHeader = "<div style='margin :5px 0 ; font-size :.85em ; color :#666'>Total : $($upCount.ToString(« N0 »)) appareils | <un href='SecureBoot_update_pending_$timestamp.csv' style='color :#1a237e ; font-weight :bold'>📄 ; Télécharger le fichier CSV complet pour Excel><4 /a></div> » $upRows = « » $upSlice = $upData | Select-Object -Premier $maxInlineRows foreach ($d dans $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 badge-success">Oui><8 /span>' } else { '-' } $upRows += « <tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size :.75em'>$($d.BucketId)</td></tr><65 'n » } $upShowing = [math] ::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size :.95em ; color :#333 ; margin :10px 0 5px'>Détails de l’appareil (montrant la première $upShowing)</h3>" $upTable = "<div style='max-height :500px ; overflow-y :auto'><table><thead><tr><th><9 HostName><0 /th><th><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA 2023Error><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 ; color :#888 ; font-style :italic'>Aucun appareil de cette catégorie.</div> » } } catch { $tblUpdatePending = "<div style='padding :20px ; color :#888 ; font-style :italic'>Aucun appareil de cette catégorie.</div> » } } else { $tblUpdatePending = "<div style='padding :20px ; color :#888 ; font-style :italic'>Aucun appareil de cette catégorie.</div> » } Compte à rebours de l’expiration du certificat $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24 » $certUefiExpiry = [datetime]"27-06-2026 » $certPcaExpiry = [datetime]"2026-10-19 » $daysToKek = [math] ::Max(0, ($certKekExpiry - $certToday). Jours) $daysToUefi = [math] ::Max(0, ($certUefiExpiry - $certToday). Jours) $daysToPca = [math] ::Max(0, ($certPcaExpiry - $certToday). Jours) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Générer des données de graphique de fabricant inline (10 premiers par nombre d’appareils) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Décroissant | Select-Object -10 premiers $mfrChartTitle = if ($stMfrCounts.Count -le 10) { « By Manufacturer » } else { « Top 10 Manufacturers » } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Clé)' » }) -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 « , » # Table du fabricant de build $mfrTableRows = « » $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Décroissant | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Key)</td><td>$($_. Value.Total.ToString(« N0 »))</td><td>$($_. Value.Updated.ToString(« N0 »))</td><td>$($_. Value.HighConf.ToString(« N0 »))><0 /td><td>$($_. Value.ActionReq.ToString(« N0 »))><4 /td></tr>'n » } $htmlContent = @" < ! DOCTYPE html> <html lang="en"> ><3 de tête de < <meta charset="UTF-8"> <meta name="viewport » content="width=device-width, initial-scale=1.0"> <titre><9 tableau de bord de l’état du certificat de démarrage sécurisé><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 de style < *{box-sizing :border-box ; margin :0 ; remplissage :0} body{font-family :'Segoe UI',Tahoma,sans-serif ; background :#f0f2f5 ; color :#333} .header{background :linear-gradient(135deg,#1a237e,#0d47a1) ; color :#fff ; remplissage :20px 30px} .header h1{font-size :1.6em ; margin-bottom :5px} .header .meta{font-size :.85em ; opacity :.9} .container{max-width :1400px ; margin :0 auto ; remplissage :20px} .cards{display :grid ; grid-template-columns :repeat(auto-fill,minmax(170px,1fr)) ; gap :12px ; margin :20px 0} . carte{background :#fff ; border-radius :10px ; remplissage :15px ; box-shadow :0 2px 8px rgba(0,0,0,08) ; border-left :4px solid #ccc ;transition :transform .2s} . carte :hover{transform :translateY(-2px) ; box-shadow :0 4px 15px rgba(0,0,0,.12)} . carte .value{font-size :1.8em ; font-weight :700} . carte .label{font-size :.8em ; color :#666 ; margin-top :4px} . carte .pct{font-size :.75em ; color :#888} .section{background :#fff ; border-radius :10px ; remplissage :20px ; margin :15px 0 ; box-shadow :0 2px 8px rgba(0,0,0,08)} .section h2{font-size :1.2em ; color :#1a237e ; margin-bottom :10px ; cursor :pointer ; user-select :none} .section h2 :hover{text-decoration :underline} .section-body{display :none} .section-body.open{display :block} .charts{display :grid ; grid-template-columns :1fr 1fr ; gap :20px ; margin :20px 0} .chart-box{background :#fff ; border-radius :10px ; remplissage :20px ; box-shadow :0 2px 8px rgba(0,0,0,08)} table{width :100 %; border-collapse :collapse ; font-size :.85em} th{background :#e8eaf6 ; remplissage :8px 10px ; text-align :left ; position :collante ; top :0 ; z-index :1} td{padding :6px 10px ; border-bottom :1px solid #eee} tr :hover{background :#f5f5f5} .badge{display :inline-block ; remplissage :2px 8px ;border-radius :10px ; font-size :.75em ; font-weight :700} .badge-success{background :#d4edda ; color :#155724} .badge-danger{background :#f8d7da ; color :#721c24} .badge-warning{background :#fff3cd ; color :#856404} .badge-info{background :#d1ecf1 ; color :#0c5460} .top-link{float :right ; font-size :.8em ; color :#1a237e ; text-decoration :none} .footer{text-align :center ; remplissage :20px ; color :#999 ; font-size :.8em} a{color :#1a237e} ><9 /style < <>/head >du corps < <div class="header"> <tableau de bord d’état du certificat de démarrage sécurisé h1></h1> <div class="meta">Généré : $($stats. ReportGeneratedAt) | Nombre total d’appareils : $($c.Total.ToString(« N0 »)) | Compartiments uniques : $($stAllBuckets.Count)</div><3 <><5 /div <div class="container">
<!-- les cartes d’indicateur de performance clé (KPI Cards) - cliquables, liées à des sections --> <div class="cards"> <a class="carte » href="#s-nu » onclick="openSection('d-nu') » style="border-left-color :#dc3545 ; text-decoration :none ; position :relative"><div style="position :absolute ; top :8px ; right :8px ; background :#dc3545 ; color :#fff ; remplissage :1px 6px ; border-radius :8px ; font-size :.65em ; font-weight :700">PRIMARY</div><div class="value » style="color :#dc3545">$($stNotUptodate.ToString(« ( » N0 »))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate) % - ACTION REQUISE><0 /div></a><3 <a class="carte » href="#s-upd » onclick="openSection('d-upd') » style="border-left-color :#28a745 ; text-decoration :none ; position :relative"><div style="position :absolute ; top :8px ; right :8px ; background :#28a745 ; color :#fff ; remplissage :1px 6px ; border-radius :8px ; font-size :.65em ; font-weight :700">PRIMARY><8 /div><div class="value » style="color :#28a745">$($c.Updated.ToString(« ) N0 »))</div><div class="label">mise à jour><6 /div><div class="pct">$($stats. PercentCertUpdated) %</div></a><3 <a class="carte » href="#s-sboff » onclick="openSection('d-sboff') » style="border-left-color :#6c757d ; text-decoration :none ; position :relative"><div style="position :absolute ; top :8px ; right :8px ; background :#6c757d ; color :#fff ; remplissage :1px 6px ; border-radius :8px ; font-size :.65em ; font-weight :700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString(« N0 »))><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[math] ::Round(($c.SBOff/$c.Total)*100,1)}else{0}) % - Out of Scope><0 /div></a><3 <a class="carte » 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">nécessite un redémarrage><2 /div><classe div ="pct">$(if($c.Total -gt 0){[math] ::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0}) % - en attente de redémarrage><6 /div></a><9 <a class="carte » 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, en attente de mise à jour><2 /div></a><5 <a class="carte » href="#s-rip » onclick="openSection('d-rip') » style="border-left-color :#17a2b8 ; text-decoration :none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Rollout In Progress><4 /div><div class="pct">$(if($c.Total -gt 0){[math] ::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{0}) %</div></a><11 <a class="carte » 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) % :><24 /div></a><27 <a class="carte » 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="carte » 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 Obligatoire><2 /div><div class="pct">$($stats. PercentActionRequired) % : doit tester><6 /div></a><9 <a class="carte » 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) % : similaire à échec><2 /div></a><5 <a class="carte » 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><classe div ="pct">$(if($c.Total -gt 0){[math] ::Round(($c.TaskDisabled/$c.Total)*100,1)}else{0}) % - Bloqué><8 /div></a><91 <a class="carte » 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. Suspendu</div><div class="pct">$(if($c.Total -gt 0){[math] ::Round(($c.TempPaused/$c.Total)*100,1)}else{0}) %</div></a> <a class="carte » 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">Problèmes connus><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="carte » 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">KEK manquant</div><div class="pct">$(if($c.Total -gt 0){[math] ::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0}) %</div></a> <a class="carte » 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) % : erreurs UEFI</div></a> ><6 a class="carte » 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. Échecs</div><div class="pct">$(if($c.Total -gt 0){[math] ::Round(($c.TempFailures/$c.Total)*100,1)}else{0}) %</div></a> <a class="carte » 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">Non pris en charge><6 /div><classe div ="pct">$(if($c.Total -gt 0){[math] ::Round(($c.PermFailures/$c.Total)*100,1)}else{0}) %</div></a><3 </div>
<!-- vitesse de déploiement & expiration du certificat --> <div id="s-velocity » style="display :grid ; grid-template-columns :1fr 1fr ; gap :20px ; margin :15px 0"> <div class="section » style="margin :0"> <h2>📅 ; Vitesse de déploiement<> /h2 <div class="section-body open"> <div style="font-size :2.5em ; font-weight :700 ; color :#28a745">$($c.Updated.ToString(« N0 »))</div> <div style="color :#666">appareils mis à jour à partir de $($c.Total.ToString(« N0 »))</div> <div style="margin :10px 0 ; background :#e8eaf6 ; height :20px ; border-radius :10px ; overflow :hidden"><div style="background :#28a745 ; height :100 %; width :$($stats. PercentCertUpdated) %; border-radius :10px"></div></div> <div style="font-size :.8em ; color :#888">$($stats. PercentCertUpdated) % complete</div> <div style="margin-top :10px ; remplissage :10px ; background :#f8f9fa ; border-radius :8px ; font-size :.85em"> <appareils div><strong>Remaining :</strong> $($stNotUptodate.ToString(« N0 »)) ont besoin d’action</div> <div><des appareils>blocage forts :</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) (erreurs + permanent + tâche désactivée)</div> <div><fort>Sécurisé à déployer :</strong> appareils $($stSafeList.ToString(« N0 »)) (même compartiment que réussite)</div> $velocityHtml <>/div <>/div <>/div <div class="section » style="margin :0 ; border-left :4px solid #dc3545"> <h2 style="color :#dc3545">⚠ Compte à rebours d’expiration du certificat</h2> <div class="section-body open"> <div style="display :flex ; gap :15px ; margin-top :10px"> <div style="text-align :center ; remplissage :15px ; border-radius :8px ; min-width :120px ; background :linear-gradient(135deg,#fff5f5,#ffe0e0) ; border :2px solid #dc3545 ; flex :1"> <div style="font-size :.65em ; color :#721c24 ; text-transform :uppercase ; font-weight :bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size :.85em ; font-weight :bold ; color :#dc3545 ; margin :3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek » style="font-size :2.5em ; font-weight :700 ; color :#dc3545 ; line-height :1"><9 $daysToKek</div> ><2 div style="font-size :.8em ; color :#721c24"><3 days (24 juin 2026)><4 /div> ><6>/div ><8 div style="text-align :center ; remplissage :15px ; border-radius :8px ; min-width :120px ; background :linear-gradient(135deg,#fffef5,#fff3cd) ; border :2px solid #ffc107 ; flex :1"><9 <div style="font-size :.65em ; color :#856404 ; text-transform :uppercase ; font-weight :bold">UEFI CA 2011</div> <div id="daysUefi » style="font-size :2.2em ; font-weight :700 ; color :#856404 ; line-height :1 ; margin :5px 0">$daysToUefi</div> <div style="font-size :.8em ; color :#856404">jours (27 juin 2026)</div> <>/div <div style="text-align :center ; remplissage :15px ; border-radius :8px ; min-width :120px ; background :linear-gradient(135deg,#f0f8ff,#d4edff) ; border :2px solid #0078d4 ; flex :1"> <div style="font-size :.65em ; color :#0078d4 ; text-transform :uppercase ; font-weight :bold">Windows PCA</div> <div id="daysPca » style="font-size :2.2em ; font-weight :700 ; color :#0078d4 ; line-height :1 ; margin :5px 0">$daysToPca><2 /div><3 <div style="font-size :.8em ; color :#0078d4">days (Oct 19, 2026)</div><7 <><9 /div <><1 /div <div style="margin-top :15px ; remplissage :10px ; background :#f8d7da ; border-radius :8px ; font-size :.85em ; border-left :4px solid #dc3545"> <strong>⚠ CRITICAL :</strong> Tous les appareils doivent être mis à jour avant l’expiration du certificat. Les appareils non mis à jour à l’échéance ne peuvent pas appliquer les mises à jour de sécurité futures pour le Gestionnaire de démarrage et le démarrage sécurisé après l’expiration.<>/div <>/div <>/div <> /div
graphiques<!-- --> <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) { « graphique de tendance historique <!-- --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 ; Mettre à jour la progression dans le temps <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 ; color :#888 ; margin-top :5px'>Lignes solides = actual data$(if ($historyData.Count -ge 2) { " | Trait en pointillés = projeté (doublement exponentiel : 2→4→8→16... appareils par onde) » } else { " | Réexécutez l’agrégation demain pour voir les lignes de tendance et la projection " })</div> <>/div </div> » })
téléchargements CSV<!-- --> <div class="section"> <h2 onclick="toggle('dl-csv')">📥 ; Télécharger les données complètes (CSV pour Excel) <class="top-link » href="#">Top</a></h2><2 <div id="dl-csv » class="section-body open » style="display :flex ; flex-wrap :wrap ; gap :5px"> <href="SecureBoot_not_updated_$timestamp.csv » style="display :inline-block ; background :#dc3545 ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Non mis à jour ($($stNotUptodate.ToString(« N0 »))))</a><8 <href="SecureBoot_errors_$timestamp.csv » style="display :inline-block ; background :#dc3545 ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Errors ($($c.WithErrors.ToString(« N0 »))))</a> <a href="SecureBoot_action_required_$timestamp.csv » style="display :inline-block ; background :#fd7e14 ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Action requise ($($c.ActionReq.ToString(« N0 »))))</a> <un href="SecureBoot_known_issues_$timestamp.csv » style="display :inline-block ; background :#dc3545 ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Problèmes connus ($($c.WithKnownIssues.ToString(« N0 »)))) </a> <un href="SecureBoot_task_disabled_$timestamp.csv » style="display :inline-block ; background :#dc3545 ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Task Disabled ($($c.TaskDisabled.ToString(« N0 »)))</a> <href="SecureBoot_updated_devices_$timestamp.csv » style="display :inline-block ; background :#28a745 ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Updated ($($c.Updated.ToString(« N0 »))))</a> <un href="SecureBoot_Summary_$timestamp.csv » style="display :inline-block ; background :#6c757d ; color :#fff ; remplissage :6px 14px ; border-radius :5px ; text-decoration :none ; font-size :.8em">Résumé</a> <div style="width :100 %; font-size :.75em ; color :#888 ; margin-top :5px">fichiers CSV ouverts dans Excel. Disponible lorsqu’il est hébergé sur le serveur web.</div> <>/div <> /div
<!-- Répartition des fabricants --> <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"> <tableau><><tr><th><1 Manufacturer><2 /th><th><5 Total><6 /th><th><9 Updated><0><9 /th><th><3 High Confidence><4 /th><th><7 Action Requise><8 /th></tr></thead><3 <tbody><5 $mfrTableRows><6 /tbody></table><9 <><1 /div <> /div
<!-- Sections de l’appareil (200 premiers téléchargements inclus + CSV) --> <div class="section » id="s-err"> <h2 onclick="toggle('d-err')">🔴 ; Appareils avec erreurs ($($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">🔴 ; Problèmes connus ($($c.WithKnownIssues.ToString(« N0 »))) <a class="top-link » href="#">↑ Top</a></h2> <div id="d-ki » class="section-body">$tblKI</div> <>/div <div class="section » id="s-kek"> <h2 onclick="toggle('d-kek')">🟠 ; KeK manquant - Événement 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">🟠 ; Action Obligatoire ($($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">🔵 ; Sous Observation ($($c.UnderObs.ToString(« N0 »))) <a class="top-link » href="#">↑ Top</a></h2> <div id="d-uo » class="section-body">$tblUnderObs</div> <>/div <div class="section » id="s-nu"> <h2 onclick="toggle('d-nu') » style="color :#dc3545">🔴 ; Not Updated ($($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">🔴 ; Task Disabled ($($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 <><7 /div <div class="section » id="s-tf"> <h2 onclick="toggle('d-tf') » style="color :#dc3545">🔴 ; Échecs temporaires ($($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">🔴 ; Échecs permanents / Non pris en charge ($($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, En attente de mise à jour <a class="top-link » href="#">↑ Top</a></h2> <div id="d-upd-pend » class="section-body"><p style="color :#666 ; margin-bottom :10px">appareils où la clé AvailableUpdatesPolicy ou WinCS est appliquée, mais UEFICA2023Status est toujours NotStarted, InProgress ou null.</p>$tblUpdatePending</div> <>/div <div class="section » id="s-rip"> <h2 onclick="toggle('d-rip') » style="color :#17a2b8">🔵 ; Lancement en cours ($($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 »))) - Hors portée <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">🟢 ; Appareils mis à jour ($($c.Updated.ToString(« N0 »))) <un 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">🔄 ; Mise à jour : nécessite un redémarrage ($($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">Tableau de bord de déploiement des certificats de démarrage sécurisé | $($stats généré. ReportGeneratedAt) | StreamingMode | Mémoire maximale : ${stPeakMemMB} Mo</div> </div><!-- /container -->
>de script< function toggle(id){var e=document.getElementById(id) ; e.classList.toggle('open')} fonction 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. Suspendu','Non pris en charge','SecureBoot OFF','With Errors'],datasets :[{data :[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$(() $c.WithErrors)],backgroundColor :['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24,'#adb5bd','#dc3545']}},options :{responsive :true,plugins :{legend :{position :'right',labels :{font :{size :11}}}}}}) ; new Chart(document.getElementById('mfrChart'),{type :'bar',data :{labels :[$mfrLabels],datasets :[{label :'Updated',data :[$mfrUpdated],backgroundColor :'#28a745'},{label :'Update Pending',data :[$mfrUpdatePending],backgroundColor :'#6f42c1'},{label :'High Confidence',data :[$mfrHighConf],backgroundColor :'#20c997'},{label :'Under Observation',data :[$mfrUnderObs],backgroundColor :'#17a2b8'},{label :'Action obligatoire',data :[$mfrActionReq],backgroundColor :'#fd7e14'},{ label :'Temp. Paused',data :[$mfrTempPaused],backgroundColor :'#6c757d'},{label :'Not Supported',data :[$mfrNotSupported],backgroundColor :'#721c24'},{label :'SecureBoot OFF',data :[$mfrSBOff],backgroundColor :'#adb5bd'},{label :'With Errors',data :[$mfrWithErrors],backgroundColor :'#dc3545'}]},options :{responsive :true,scales :{x :{stacked :true},y :{stacked :true}},plugins :{legend :{position :'top'}}}}}}) ; Graphique de tendances historiques if (document.getElementById('trendChart')) { var allLabels = [$allChartLabels] ; var actualUpdated = [$trendUpdated] ; var actualNotUpdated = [$trendNotUpdated] ; var actualTotal = [$trendTotal] ; var projData = [$projDataJS] ; var projNotUpdData = [$projNotUpdJS] ; var histLen = actualUpdated.length ; var projLen = projData.length ; var paddedUpdated = actualUpdated.concat(Array(projLen).fill(null)) ; var paddedNotUpdated = actualNotUpdated.concat(Array(projLen).fill(null)) ; var paddedTotal = actualTotal.concat(Array(projLen).fill(null)) ; var projLine = Array(histLen).fill(null) ; var projNotUpdLine = Array(histLen).fill(null) ; if (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1] ; projLine = projLine.concat(projData) ; projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1] ; projNotUpdLine = projNotUpdLine.concat(projNotUpdData) ; } var datasets = [ {label :'Updated',data :paddedUpdated,borderColor :'#28a745',backgroundColor :'rgba(40,167,69,0.1)',fill :true,tension :0.3,borderWidth :2}, {label :'Not Updated',data :paddedNotUpdated,borderColor :'#dc3545',backgroundColor :'rgba(220,53,69,0.1)',fill :true,tension :0.3,borderWidth :2}, {label :'Total',data :paddedTotal,borderColor :'#6c757d',borderDash :[5,5],fill :false,tension :0,pointRadius :0,borderWidth :1} ] ; if (projLen > 0) { datasets.push({label :'Projected Updated (2x doublement)',data :projLine,borderColor :'#28a745',borderDash :[8,4],borderWidth :3,fill :false,tension :0.3,pointRadius :3,pointStyle :'triangle'}) ; datasets.push({label :'Projected Not Updated',data :projNotUpdLine,borderColor :'#dc3545',borderDash :[8,4],borderWidth :3,fill :false,tension :0.3,pointRadius :3,pointStyle :'triangle'}) ; } new Chart(document.getElementById('trendChart'),{type :'line',data :{labels :allLabels,datasets :datasets},options :{responsive :true,scales :{y :{beginAtZero :true,title :{display :true,text :'Devices'}},x :{title :{display :true,text :'Date'}}},plugins :{legend :{position :'top'},title :{display :true,text :'Secure Boot Update Progress Over Time'}}}}}) ; } Compte à rebours dynamique (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)) # Conservez toujours une copie « Dernière » stable afin que les administrateurs n’ont pas besoin de suivre les horodatages $latestPath = Join-Path $OutputPath « SecureBoot_Dashboard_Latest.html » Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # Enregistrer le manifeste de fichier pour le mode incrémentiel (détection rapide sans modification lors de la prochaine exécution) if ($IncrementalMode -ou $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 « Enregistrement du manifeste de fichier en mode incrémentiel... » -ForegroundColor Gray foreach ($jf dans $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString(« o ») Taille = $jf. Longueur } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host « Manifeste enregistré pour les fichiers $($stNewManifest.Count) » -ForegroundColor DarkGray } # NETTOYAGE DE RÉTENTION # Dossier réutilisable Orchestrator (Aggregation_Current) : conserver uniquement l’exécution la plus récente (1) # Administration exécutions manuelles / autres dossiers : conserver les 7 dernières exécutions # Résumé Des csv ne sont JAMAIS supprimés : ils sont minuscules (environ 1 Ko) et constituent la source de sauvegarde de l’historique des tendances $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } else { 7 } Nombre de préfixes de fichier sécurisés pour propre (instantanés éphémères par exécution) $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_' ) # Rechercher tous les horodatages uniques à partir de fichiers pouvant être nettoyés uniquement $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. Nom; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Count -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { si ($_. Name -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Unique -Décroissant) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object -Ignorer $retentionCount $removedFiles = 0 ; $freedBytes = 0 foreach ($oldTs dans $oldTimestamps) { foreach ($prefix dans $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter « ${prefix}${oldTs}* » -EA SilentlyContinue foreach ($f dans $oldFiles) { $freedBytes += $f.Length Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [math] ::Round($freedBytes / 1 Mo, 1) Write-Host « Nettoyage de rétention : suppression des fichiers $removedFiles des anciennes exécutions $($oldTimestamps.Count), libération de ${freedMB} Mo (conservation des dernières $retentionCount + tous les csv Summary/NotUptodate) » -ForegroundColor DarkGray } Write-Host « 'n$(« = » * 60) » -ForegroundColor Cyan Write-Host « STREAMING AGGREGATION COMPLETE » -ForegroundColor Vert Write-Host (« = » * 60) -ForegroundColor Cyan Write-Host " Total Devices : $($c.Total.ToString(« N0 »)) » -ForegroundColor White Write-Host " NOT UPDATED : $($stNotUptodate.ToString(« N0 »)) ($($stats. PercentNotUptodate) %) » -ForegroundColor $(if ($stNotUptodate -gt 0) { « Yellow » } else { « Green » }) Write-Host " Mis à jour : $($c.Updated.ToString(« N0 »)) ($($stats. PercentCertUpdated) %) " -ForegroundColor Vert Write-Host " With Errors : $($c.WithErrors.ToString(« N0 »)) » -ForegroundColor $(if ($c.WithErrors -gt 0) { « Red » } else { « Green » }) Write-Host « Peak Memory : ${stPeakMemMB} MB » -ForegroundColor Cyan Write-Host " Time : $([math] ::Round($stTotal/60,1)) min » -ForegroundColor White Write-Host « Tableau de bord : $htmlPath » -ForegroundColor White return [PSCustomObject]$stats } mode de diffusion en continu #endregion } else { Write-Error « Chemin d’entrée introuvable : $InputPath » sortie 1 }