複製貼上這個範例腳本,並根據你的環境進行修改:
<# .劇情簡介 將多個裝置的安全開機狀態 JSON 資料彙整成摘要報告。
。描述 讀取、收集安全開機狀態 JSON 檔案並產生: - 具備圖表與篩選功能的 HTML 儀表板 - ConfidenceLevel 摘要 - 用於測試策略 的獨特裝置桶分析 支援: - 每台機器檔案:HOSTNAME_latest.json (推薦) - 單一 JSON 檔案 自動依 HostName 進行重複解碼,並保留最新的 CollectTime。 預設情況下,只包含「動作需求」或「高」信心度的裝置 專注於可執行的目標。 使用 -IncludeAllConfidenceLevels 來覆蓋。
。參數輸入路徑 JSON 檔案的路徑 (s) : - 資料夾:若沒有_latest檔案,則讀取所有 *_latest.json 檔案 (或 *.json) - 檔案:讀取單一 JSON 檔案
。參數輸出路徑 產生報告的路徑預設 (:.\SecureBootReports)
。範例 # 從每台機器檔案資料夾中彙整 (推薦) .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” # 讀著:\\contoso\SecureBootLogs$\*_latest.json
。範例 # 自訂輸出位置 .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” -OutputPath “C:\Reports\SecureBoot”
。範例 # 只包含行動要求和高信心 (預設行為) .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” # 排除:觀察、暫停、無支持
。範例 # 包含所有信心等級 (覆蓋過濾器) .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” -IncludeAllConfidenceLevels
。範例 # 自訂信心等級過濾器 .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” -IncludeConfidenceLevels @ (“Action Req”, “High”, “Observation”)
。範例 # 企業規模:增量模式 - 僅處理變更檔案 (快速後續執行) .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” -IncrementalMode # 首次運行:滿載 ~2小時,50萬裝置 # 後續運行:若無變化則為秒數,若為數值,為 delta 為分鐘
。範例 # 跳過 HTML 如果沒變 (監控最快) .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” -IncrementalMode -SkipReportIfUnchanged # 若自上次運行以來檔案未更改:~5秒
。範例 # 僅摘要模式 - 跳過大型裝置表 1-2 分鐘 (20+分鐘) .\Aggregate-SecureBootData.ps1 -InputPath “\\contoso\SecureBootLogs$” -SummaryOnly # 產生 CSV,但跳過包含完整裝置資料表的 HTML 儀表板
。註釋 搭配 Detect-SecureBootCertUpdateStatus.ps1 進行企業部署。完整部署指南請參見 GPO-DEPLOYMENT-GUIDE.md。 預設行為排除觀察、暫停及不支援裝置 專注於可操作的裝置桶報告。#>
參數 ( [參數 (強制 = $true) ] [string]$InputPath, [參數 (強制 = $false) ] [string]$OutputPath = “.\SecureBootReports”, [參數 (強制 = $false) ] [字串]$ScanHistoryPath = “.\SecureBootReports\ScanHistory.json”, [參數 (強制 = $false) ] [string]$RolloutStatePath, # Path to RolloutState.json 以識別 InProgress 裝置 [參數 (強制 = $false) ] [string]$RolloutSummaryPath, # Orchestrator (的 Path to SecureBootRolloutSummary.json 包含投影資料) [參數 (強制 = $false) ] [string[]]$IncludeConfidenceLevels = @ (「需要行動」、「高信心度」) 、# 僅預設包含這些信心等級:僅 (可操作的桶) [參數 (強制 = $false) ] [開關]$IncludeAllConfidenceLevels,# 覆蓋過濾器以包含所有信心等級 [參數 (強制 = $false) ] [開關]$SkipHistoryTracking, [參數 (強制 = $false) ] [switch]$IncrementalMode, # 啟用 delta processing - 只載入自上次執行 以來變更的檔案 [參數 (強制 = $false) ] [string]$CachePath, # 前往快取目錄的路徑 (預設:OutputPath\.cache) [參數 (強制 = $false) ] [int]$ParallelThreads = 8, # PS7+ (檔案載入的平行執行緒數量) [參數 (強制 = $false) ] [開關]$ForceFullRefresh, # 即使在增量模式下 強制全裝填 [參數 (強制 = $false) ] [switch]$SkipReportIfUnchanged,# 如果檔案沒變 (只輸出統計,跳過 HTML/CSV 產生) [參數 (強制 = $false) ] [switch]$SummaryOnly, # 只產生摘要統計 (不要) 大型裝置資料表——速度 快很多 [參數 (強制 = $false) ] [開關]$StreamingMode # 記憶體效率模式:處理區塊,逐步寫入 CSV,僅保留摘要於記憶體中 )
# 如果有的話自動升級到 PowerShell 7 (對於大型資料集) 如果 ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction 靜默繼續 |Select-Object -ExpandProperty 來源 如果 ($pwshPath) { Write-Host 「偵測到 PowerShell $ ($PSVersionTable.PSVersion) - 為了加快處理速度,使用 PowerShell 7 重新啟動...」 - ForegroundColor Yellow # 從綁定參數重建參數列表 $relaunchArgs = @ ('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key in $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] 若 ($val -是 [開關]) { 如果 ($val。IsPresent) { $relaunchArgs += “-$key” } } elseif ($val -is [陣列]) { $relaunchArgs += 「-$key」 $relaunchArgs += ($val -加入 ',) } 否則 { $relaunchArgs += 「-$key」 $relaunchArgs += 「$val」 } } & $pwshPath @relaunchArgs 退出$LASTEXITCODE } }
$ErrorActionPreference = 「繼續」 $timestamp = Get-Date -格式「yyyyMMdd-HHmmss」 $scanTime = Get-Date -格式「yyyy-mm-dd HH:mm:ss」 $DownloadUrl = 「https://aka.ms/getsecureboot」 $DownloadSubPage = 「部署與監控樣本」
# 註:此文字不依賴其他文字。 # 完整工具組請下載:$DownloadUrl -> $DownloadSubPage
#region 設定 Write-Host “=” * 60 -前景色 青色 Write-Host 「安全啟動資料聚合」-ForegroundColor 青色 Write-Host “=” * 60 -前景色 青色
# 建立輸出目錄 若 (-not (測試路徑 $OutputPath) ) { New-Item -ItemType Directory -Path $OutputPath -Force |Out-Null }
# 載入資料 - 支援 CSV (舊式) 與 JSON (原生) 格式 Write-Host 「'nLoading 資料來源:$InputPath」-前景色為黃色
# 輔助功能用於標準化裝置物件 (處理欄位名稱差異) 函數 Normalize-DeviceRecord { 參數 ($device) # 處理主機名稱與主機名稱 (JSON 使用主機名稱,CSV 使用 HostName) 如果 ($device。PSObject.Properties['Hostname'] -而不是 $device。PSObject.Properties['HostName']) { $device |Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device.主機名稱 -Force } # 處理信心與信心等級 (JSON 使用信心,CSV 使用 信心等級) # ConfidenceLevel 是官方欄位名稱 - 映射 Confidence 到 如果 ($device。PSObject.Properties['信心']-而不是$device。PSObject.Properties['ConfidenceLevel']) { $device |Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device.信心 - 力 } # 透過 Event1808Count 或 UEFICA2023Status=“已更新” 追蹤更新狀態 # 這讓人們能追蹤每個信心桶中更新了多少裝置 $event 1808 = 0 如果 ($device。PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device。事件1808伯爵 } $uefiCaUpdated = $false 如果 ($device。PSObject.Properties['UEFICA2023Status'] -以及$device。UEFICA2023Status -eq “Updated”) { $uefiCaUpdated = $true } 若 ($event 1808 -gt 0 -或 $uefiCaUpdated) { # 標記為儀表板/推出邏輯的更新,但不要覆蓋 ConfidenceLevel $device |Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } 否則 { $device |Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # 信心水準分類: # - 「高度自信」、「觀察中...」、「暫時暫停...」、「不支援...」= 原樣使用 # - 其他所有 (空、空、「更新類型:...」、「未知」、「N/A」) = 在計數器中屬於需要行動 # 不需要正規化 — 串流計數器的 else 分支會處理 } # 處理 OEMManufacturerName 與 WMI_Manufacturer (JSON 使用 OEM*,舊版使用 WMI_*) 如果 ($device。PSObject.Properties[''OEMManufacturerName'] -而不是 $device。PSObject.Properties['WMI_Manufacturer']) { $device |Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device.OEMManufacturerName -Force } # 處理OEMModelNumber對WMI_Model 如果 ($device。PSObject.Properties['OEMModelNumber'] -與 -not $device。PSObject.Properties['WMI_Model']) { $device |Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device.OEMModelNumber -Force } # 處理韌體版本與 BIOS 版本 如果 ($device。PSObject.Properties['FirmwareVersion'] -而不是 $device。PSObject.Properties['BIOSDescription']) { $device |Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device.韌體版本 -Force } 返回$device }
#region 增量處理/快取管理 # 設置快取路徑 如果 (-not $CachePath) { $CachePath = Join-Path $OutputPath 「.cache」 } $manifestPath = Join-Path $CachePath「FileManifest.json」 $deviceCachePath = Join-Path $CachePath「DeviceCache.json」
# 快取管理功能 函數 Get-FileManifest { 參數 ([字串]$Path) 若 (測試路徑$Path) { 試試看 { $json = Get-Content $Path -原始碼 |ConvertFrom-Json # 將 PSObject 轉成雜湊表 (相容 PS5.1 - PS7 有 -AsHashtable) $ht = @{} $json。PSObject.Properties |ForEach-Object { $ht[$_.姓名] = $_。價值 } 回歸$ht } 抓 { 回轉 @{} } } 回轉 @{} }
函數 Save-FileManifest { 參數 ([雜湊表]$Manifest、[字串]$Path) $dir = Split-Path $Path -家長 若 (-非 (測試路徑$dir) ) { New-Item -ItemType Directory -Path $dir -Force |Out-Null } $Manifest |ConvertTo-Json -深度3 -壓縮 |Set-Content $Path -原力 }
函數 Get-DeviceCache { 參數 ([字串]$Path) 若 (測試路徑$Path) { 試試看 { $cacheData = Get-Content $Path -原始碼 |ConvertFrom-Json Write-Host “載入裝置快取:$ ($cacheData.Count) devices” -ForegroundColor DarkGray 返回$cacheData } 抓 { Write-Host「快取損壞,將重建」-前景黃色 回覆 @ () } } 回覆 @ () }
函數 Save-DeviceCache { 參數 ($Devices,[字串]$Path) $dir = Split-Path $Path -家長 如果 (-不 (測試路徑$dir) ) { New-Item -ItemType Directory -Path $dir -Force |Out-Null } # 轉成陣列並儲存 $deviceArray = @ ($Devices) $deviceArray |ConvertTo-Json -深度10 -壓縮 |Set-Content $Path -原力 Write-Host 「儲存裝置快取:$ ($deviceArray。計數) 裝置」-前景色深灰色 }
函數 Get-ChangedFiles { 參數 ( [System.IO.FileInfo[]]$AllFiles, [雜湊表]$Manifest ) $changed = [System.Collections.ArrayList]::新 () $unchanged = [System.Collections.ArrayList]::新 () $newManifest = @{} # 從 manifest (normalize 建立大小寫不區分查詢,再歸一化為小寫) $manifestLookup = @{} foreach ($mk 在 $Manifest.Keys) { $manifestLookup[$mk。ToLower不變量 () ] = $Manifest[$mk] } $AllFiles) 中每個 ($file { $key = $file。FullName.ToLowerInvariant () # 將路徑正規化為小寫 $lwt = $file。LastWriteTimeUtc.ToString (“o”) $newManifest[$key] = @{ LastWriteTimeUtc = $lwt 大小 = $file。長度 } 如果 ($manifestLookup.ContainsKey ($key) ) { $cached = $manifestLookup[$key] 如果 ($cached。LastWriteTimeUTC -eq $lwt -和$cached。尺寸 -eq $file。長度) { [虛無]$unchanged。加入 ($file) 繼續 } } [虛空]$changed。加入 ($file) } 返回 @{ 變動 = $changed 不變 = $unchanged NewManifest = $newManifest } }
# 使用批次處理進行超高速平行檔案載入 函數 Load-FilesParallel { Param ( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = Files美元。伯爵 # 使用每批 ~1000 個檔案以更好控制記憶體 $batchSize = [數學]::最小 (1000,[數學]::天花板 ($totalFiles / [數學]::最大 (1,$Threads) ) ) $batches = [System.Collections.Generic.List[物件]]::新 ()
當 ($i = 0;$i -lt $totalFiles;$i += $batchSize) { $end = [數學]::最小 ($i + $batchSize,$totalFiles) $batch = $Files[$i.. ($end-1) ] $batches。加個 ($batch) } Write-Host 「 ($ ($batches。」數) 批次的 ~$batchSize 檔案,每批) “ -無新行 -前景色彩 深灰色 $flatResults = [System.Collections.Generic.List[object]]::新 () # 檢查是否有 PowerShell 7+ 平行支援 $canParallel = $PSVersionTable.PSVersion.大調 -ge 7 若 ($canParallel -和 $Threads -gt 1) { # PS7+:平行處理批次 $results = $batches |ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new () $batchFiles) 中的每個 ($file { 試試看 { $content = [System.IO.File]::ReadAllText ($file。全名) |ConvertFrom-Json $batchResults.加 ($content) } 接住 { } } $batchResults.ToArray () } $results) 中每個 ($batch { 若 ($batch) { foreach 在 $batch) ($item { $flatResults.Add ($item) } } } } 否則 { # PS5.1 備用:連續處理 (10K 檔案 <仍然快速) 每人 ($file 以$Files) { 試試看 { $content = [System.IO.File]::ReadAllText ($file。全名) |ConvertFrom-Json $flatResults.加碼 ($content) } 接住 { } } } 返回 $flatResults.ToArray () } #endregion
$allDevices = @ () 若 (測試路徑 $InputPath -路徑類型葉節點) { # 單一 JSON 檔案 如果 ($InputPath -類「*.json」) { $jsonContent = Get-Content -路徑 $InputPath -原始 |ConvertFrom-Json $allDevices = @ ($jsonContent) |ForEach-Object { Normalize-DeviceRecord $_ } Write-Host 「已載入 $ ($allDevices.Count) 檔案中的紀錄」 } 否則 { Write-Error「只支援 JSON 格式。 檔案必須有.json延期。」 出口1 } } elseif (Test-Path $InputPath -PathType 容器) { # 資料夾 - 僅限 JSON $jsonFiles = @ (Get-ChildItem -路徑 $InputPath -Filter “*.json” -遞迴 -ErrorAction 靜默繼續 | Where-Object { $_。名稱 -notmatch「ScanHistory|RolloutState|RolloutPlan“ }) # 若每台機器模式 (存在檔案,則偏好 *_latest.json 檔案) $latestJson = $jsonFiles |Where-Object { $_。名字 -類「*_latest.json」 } 若 ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.伯爵 若 ($totalFiles -eq 0) { Write-Error 「在:$InputPath 中找不到 JSON 檔案」 出口1 } Write-Host 「找到$totalFiles JSON 檔案」-ForegroundColor Gray # 輔助功能以匹配信心等級 (同時處理短文和完整) # 早期定義,讓串流模式和一般路徑都能使用 函數 Test-ConfidenceLevel { 參數 ([string]$Value、[string]$Match) 如果 ([string]::IsNullOrEmpty ($Value) ) { return $false } 切換 ($Match) { 「高自信」 { 返回 $Value -eq 「高自信」} 「觀察不足」 { 返回 $Value -類「觀察之下*」 } “ActionRequired” { 回傳 ($Value -類 “*Action Required*” -或 $Value -eq “Action Required”) } 「暫時暫停」 { 回放 $Value -類「暫時暫停*」 } 「NotSupported」{ 回傳 ($Value -類「Not Supported*」 -或 $Value -eq 「Not Supported」) } 預設 { 返回 $false } } } #region 串流模式 - 對大型資料集進行記憶體效率高的處理 # 務必使用 StreamingMode 以提升記憶體效率與新式儀表板 如果 (-不$StreamingMode) { Write-Host「自動啟用串流模式 (新式儀表板) 」-前景色黃 $StreamingMode = $true 若 (-not $IncrementalMode) { $IncrementalMode = $true } } # 啟用 -StreamingMode 時,會將檔案分段處理,記憶體中只保留計數器。# 裝置層級資料會依區塊寫入 JSON 檔案,供隨需在儀表板載入。# 記憶體使用量:~1.5 GB,不論資料集大小 (對比沒有串流) 時 10-20 GB。如果 ($StreamingMode) { Write-Host 「啟用串流模式 - 記憶體效率高處理」-前景綠色 $streamSw = [System.Diagnostics.Stopwatch]::StartNew () # 增量檢查:若自上次執行以來檔案未變更,請完全跳過處理 如果 ($IncrementalMode -且 -不是 $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath「.cache」 $stManifestPath = Join-Path $stManifestDir「StreamingManifest.json」 若 (測試路徑$stManifestPath) { Write-Host「檢查自上次串流以來的變化......」-ForegroundColor 青色 $stOldManifest = Get-FileManifest -路徑$stManifestPath 如果 ($stOldManifest.Count -gt 0) { $stChanged = $false # 快速檢查:檔案數量相同嗎? 若 ($stOldManifest.Count -eq $totalFiles) { # 查看 100 個最新檔案 (依 LastWriteTime 排序) # 若有任何檔案變更,將使用最新的時間戳記並優先顯示 $sampleSize = [數學]::最小 (100,$totalFiles) $sampleFiles = $jsonFiles |Sort-Object LastWriteTimeUTC -下降 |Select-Object -第一$sampleSize ($sf$sampleFiles) { $sfKey = $sf。FullName.ToLower不變 () 如果 (-not $stOldManifest.ContainsKey ($sfKey) ) { $stChanged = $true 休息 } # 比較時間戳記 - 快取可能是 DateTime 或 JSON 往返後的字串 $cachedLWT = $stOldManifest[$sfKey]。最後寫作時間UTC $fileDT = $sf。最後寫作時間UTC 試試看 { # 如果快取已經是 DateTime (ConvertFrom-Json 會自動轉換) ,請直接使用 若 ($cachedLWT -是 [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime () } 否則 { $cachedDT = [DateTimeOffset]::P arse (“$cachedLWT”) 。UtcDateTime } 如果 ([數學]::腹肌 ( ($cachedDT - $fileDT) 。TotalSeconds) -gt 1) { $stChanged = $true 休息 } } 抓 { $stChanged = $true 休息 } } } 否則 { $stChanged = $true } 如果 (-not $stChanged) { # 檢查輸出檔是否存在 $stSummaryExists = Get-ChildItem (join-Path $OutputPath 「SecureBoot_Summary_*.csv」) -EA 靜默繼續 |Select-Object -第一 $stDashExists = Get-ChildItem (join-Path $OutputPath 「SecureBoot_Dashboard_*.html」) -EA 靜默繼續 |Select-Object -第一 若 ($stSummaryExists -且 $stDashExists) { Write-Host「未偵測到變更 ($totalFiles 檔案未更改) - 跳過處理」 - 前景綠色 Write-Host 「最後儀表板:$ ($stDashExists.全名) “ -前景白色 $cachedStats = Get-Content $stSummaryExists.全名 |ConvertFrom-Csv Write-Host 「裝置:$ ($cachedStats.TotalDevices) |更新時間:$ ($cachedStats。更新) |錯誤:$ ($cachedStats.WithErrors) “ -前景灰色 Write-Host 「完成時間為 $ ([數學]::輪數 ($streamSw。經過。總秒數,1) ) 秒 (無需處理) 」 -前景綠色 返回$cachedStats } } 否則 { # DELTA PATCH:找出哪些檔案被更改 Write-Host「偵測到變更 - 識別變更檔案......」 - 前景色 黃色 $changedFiles = [System.Collections.ArrayList]::新 () $newFiles = [System.Collections.ArrayList]::新 () foreach ($jf in $jsonFiles) { $jfKey = $jf。全名:Lower不變 () 如果 (-not $stOldManifest.ContainsKey ($jfKey) ) { [虛無]$newFiles。加入 ($jf) } 否則 { $cachedLWT = $stOldManifest[$jfKey]。最後寫作時間UTC $fileDT = $jf。最後寫作時間UTC 試試看 { $cachedDT = 如果 ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime () } 否則 { [DateTimeOffset]::P arse (“$cachedLWT”) 。UtcDateTime } 如果 ([數學]::Abs ( ($cachedDT - $fileDT) 。TotalSeconds) -gt 1) { [void]$changedFiles.Add ($jf) } } 捕捉 { [void]$changedFiles.Add ($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [數學]::輪數 ( ($totalChanged / $totalFiles) * 100,1) Write-Host “ 變更:$ ($changedFiles.Count) |新作:$ ($newFiles.Count) |總計:$totalChanged ($changePct%) “ -前景色 黃色 若 ($totalChanged -gt 0 -且 $changePct -lt 10) { # DELTA 補丁模式:<10% 變更,修補現有資料 Write-Host「Delta 補丁模式 ($changePct% < 10%) - 補丁$totalChanged檔案...」 -ForegroundColor Green $dataDir = Join-Path $OutputPath「資料」 # 載入變更/新裝置記錄 $deltaDevices = @{} $allDeltaFiles = @ ($changedFiles) + @ ($newFiles) ($df$allDeltaFiles) { 試試看 { $devData = Get-Content $df。全名 -Raw |ConvertFrom-Json $dev = Normalize-DeviceRecord $devData 如果 ($dev。主機名稱) { $deltaDevices[$dev。主機名稱] = $dev } } 接住 { } } Write-Host「已載入$ ($deltaDevices。計數) 更改裝置紀錄」-前景灰色 # 對每個類別 JSON:移除變更主機名稱的舊條目,新增條目 $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 在 $deltaDevices.Keys) { [void]$changedHostnames.Add ($hn) } 每個 ($cat 在$categoryFiles) { $catPath = Join-Path $dataDir「$cat.json」 若 (測試路徑$catPath) { 試試看 { $catData = Get-Content $catPath -原始碼 |ConvertFrom-Json # 移除更換主機名稱的舊條目 $catData = @ ($catData |Where-Object { -not $changedHostnames。包含 ($_。主機名稱) }) # 將每個更換過的裝置重新分類成類別 # (將在分類後加入) $catData |ConvertTo-Json -深度 5 |Set-Content $catPath -UTF8 編碼 } 接住 { } } } # 分類每個變更的裝置,並附加到正確的分類檔案 對於每個 ($dev 在 $deltaDevices.Values) { $slim = [有序]@{ 主機名稱 = $dev。主機名稱 WMI_Manufacturer = 如果 ($dev。PSObject.Properties['WMI_Manufacturer']) { $dev.WMI_Manufacturer } 否則 { “” } WMI_Model = 如果 ($dev。PSObject.Properties['WMI_Model']) { $dev.WMI_Model } 否則 { “” } BucketId = 如果 ($dev。PSObject.Properties['BucketId']) { $dev.BucketId } else { “” } 信心水準 = 如果 ($dev。PSObject.Properties['ConfidenceLevel']) { $dev.ConfidenceLevel } 否則 { “” } 已更新 = $dev。已更新 UEFICA2023Error = 如果 ($dev。PSObject.Properties['UEFICA2023Error']) { $dev.UEFICA2023Error } else { $null } SecureBootTaskStatus = 如果 ($dev。PSObject.Properties['SecureBootTaskStatus']) { $dev.SecureBootTaskStatus } else { “” } KnownIssueID = 如果 ($dev。PSObject.Properties['KnownIssueId']) { $dev.KnownIssueId } else { $null } SkipReasonKnownIssue = 如果 ($dev。PSObject.Properties['SkipReasonKnownIssue']) { $dev.SkipReasonKnownIssue } 否則 { $null } } $isUpd = $dev。IsUpdated -eq $true $conf = 如果 ($dev。PSObject.Properties['ConfidenceLevel']) { $dev.ConfidenceLevel } 否則 { “” } $hasErr = (-not [string]::IsNullOrEmpty ($dev。UEFICA2023 錯誤) -和 $dev。UEFICA2023Error -ne “0” -和 $dev。UEFICA2023Error -ne “”) $tskDis = ($dev。SecureBootTaskEnabled -eq $false -或 $dev。SecureBootTaskStatus -eq 'Disabled' -或 $dev。SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev。SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev。SecureBootEnabled -ne $false -和「$ ($dev。SecureBootEnabled) “ -ne ”False“) $e 1801 = 如果 ($dev。PSObject.Properties['Event1801Count']) { [int]$dev.Event1801Count } else { 0 } $e 1808 = 如果 ($dev。PSObject.Properties['Event1808Count']) { [int]$dev.Event1808Count } else { 0 } $e 1803 = 如果 ($dev。PSObject.Properties['Event1803Count']) { [int]$dev.Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -或$dev。MissingKEK -eq $true) $hKI = ( (-not [string]::IsNullOrEmpty ($dev。SkipReasonKnownIssue) ) -or (-not [字串]::IsNullOrEmpty ($dev。已知問題) ) ) $rStat = 如果 ($dev。PSObject.Properties['RolloutStatus']) { $dev.RolloutStatus } 否則 { “” } # 附加到匹配的分類檔案 $targets = @ () 若 ($isUpd) { $targets += 「updated_devices」} 若 ($hasErr) { $targets += 「錯誤」} 若 ($hKI) { $targets += 「known_issues」 } 若 ($mKEK) { $targets += 「missing_kek」} 若 (-not $isUpd -且 $sbOn) { $targets += “not_updated” } 若 ($tskDis) { $targets += 「task_disabled」 } 若 (-not $isUpd -且 ($tskDis -或 (測試信心水準$conf「暫時暫停」) ) ) { $targets += 「temp_failures」 } 若 (-not $isUpd -且 ( (測試信心水準$conf「不支持」) -或 ($tskNF -且 $hasErr) ) ) { $targets += “perm_failures” } 若 (-not $isUpd -且 (測試信心水準$conf「ActionRequired」) ) { $targets += “action_required” } 若 (-not $sbOn) { $targets += 「secureboot_off」} 若 ($e 1801 -gt 0 -與 $e 1808 -eq 0 -且 -not $hasErr -且 $rStat -eq 「InProgress」) { $targets += “rollout_inprogress” } $targets) 中每個 ($tgt { $tgtPath = Join-Path $dataDir「$tgt.json」 若 (測試路徑$tgtPath) { $existing = Get-Content $tgtPath -原始碼 |ConvertFrom-Json $existing = @ ($existing) + @ ([PSCustomObject]$slim) $existing |ConvertTo-Json -深度 5 |Set-Content $tgtPath -編碼 UTF8 } } } # 從修補過的 JSON 重新生成 CSV Write-Host「從補丁資料重新生成 CSV......」-ForegroundColor Gray $newTimestamp = Get-Date -格式「yyyyMMdd-HHmmss」 ($cat$categoryFiles) { $catJsonPath = Join-Path $dataDir「$cat.json」 $catCsvPath = Join-Path $OutputPath “SecureBoot_${cat}_$newTimestamp.csv” 若 (測試路徑 $catJsonPath) { 試試看 { $catJsonData = Get-Content $catJsonPath -原始碼 |ConvertFrom-Json 如果 ($catJsonData.Count -gt 0) { $catJsonData |Export-Csv -路徑$catCsvPath -NoTypeInformation -編碼 UTF8 } } 接住 { } } } # 從修補過的 JSON 檔案中重新計算統計數據 Write-Host「從補丁資料重新計算摘要......」-ForegroundColor Gray $patchedStats = [ordered]@{ ReportGeneratedAt = (取得日期) 。ToString (“yyyy-mm-dd HH:mm:ss”) } $pTotal = 0;$pUpdated = 0;$pErrors = 0;$pKI = 0;$pKEK = 0 $pTaskDis = 0;$pTempFail = 0;$pPermFail = 0;$pActionReq = 0;$pSBOff = 0;$pRIP = 0 $categoryFiles) 中每個 ($cat { $catPath = Join-Path $dataDir「$cat.json」 $cnt = 0 若 (測試路徑 $catPath) { 嘗試 { $cnt = (取得內容 $catPath -Raw |ConvertFrom-Json) .Count } catch { } } 切換 ($cat) { 「updated_devices」 { $pUpdated = $cnt } 「錯誤」 { $pErrors = $cnt } 「known_issues」 { $pKI = $cnt } 「missing_kek」 { $pKEK = $cnt } 「not_updated」{ } # 計算中 「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」) -原始碼 |ConvertFrom-Json) .伯爵 $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host「Delta 補丁完成:$totalChanged裝置已更新」-ForegroundColor Green Write-Host 「總計:$pTotal |更新日期:$pUpdated |未更新:$pNotUpdated |錯誤:$pErrors“ -前景白色 # 更新清單 $stManifestDir = Join-Path $OutputPath「.cache」 $stNewManifest = @{} ($jf$jsonFiles) { $stNewManifest[$jf。FullName.ToLowerInvariant () ] = @{ LastWriteTimeUtc = $jf。LastWriteTimeUtc.ToString (“o”) ;尺寸 = $jf。長度 } } Save-FileManifest -顯現$stNewManifest -路徑$stManifestPath Write-Host 「完成時間為 $ ([數學]:::輪數 ($streamSw。經過。總秒數,1) ) 秒 (delta patch - $totalChanged 裝置) “ -前景綠色 # 切換到完整的串流重處理以重新生成 HTML 儀表板 # 資料檔案已經修補過,確保儀表板保持最新 Write-Host「從補丁資料重新生成儀表板......」-ForegroundColor Yellow } 否則 { Write-Host「$changePct% 檔案變更 (>= 10%) - 需要完整串流重新處理」 - ForegroundColor Yellow } } } } } # 建立按需裝置 JSON 檔案的資料子目錄 $dataDir = Join-Path $OutputPath「資料」 如果 (-not (測試路徑 $dataDir) ) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # 透過 HashSet 進行重複刪除 (O (每次查詢 1) ,600K 主機名稱 ~50MB) $seenHostnames = [System.Collections.Generic.HashSet[string]]::new ([System.StringComparer]::OrdinalIgnoreCase) # 輕量級摘要計數器 (取代記憶體中的 $allDevices + $uniqueDevices) $c = @{ 總分 = 0;SBEnabled = 0;SBOff = 0 更新 = 0;HighConf = 0;UnderObs = 0;行動需求 = 0;暫停時間 = 0;NotSupported = 0;NoConfData = 0 TaskDisabled = 0;未找到任務 = 0;TaskDisabledNotUpdated = 0 失誤數 = 0;進行中 = 0;尚未入職 = 0;RolloutInProgress(進行中)= 0 已知問題 = 0;WithMissingKEK = 0;溫度失效 = 0;PermFailures = 0;需求重啟 = 0 更新待處理 = 0 } # AtRisk/SafeList (輕量級套裝的桶追蹤) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new () $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new () $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{};$stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # 批次模式裝置資料檔案:逐區塊累積,並在區塊邊界處刷新 $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 = @{} 每個 ($dfName 在 $stDeviceFiles) { $dfPath = Join-Path $dataDir「$dfName.json」 [System.IO.File]::WriteAllText ($dfPath, “['n”, [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath;$stDeviceFileCounts[$dfName] = 0 } # JSON輸出 (僅包含必要欄位的纖薄裝置記錄,~200位元組對比~2KB完整) 函數 Get-SlimDevice { 參數 ($Dev) 回傳 [ordered]@{ 主機名稱 = $Dev.主機名稱 WMI_Manufacturer = 如果 ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { “” } WMI_Model = 如果 ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } 否則 { “” } BucketId = 如果 ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { “” } ConfidenceLevel = 如果 ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } 否則 { “” } 已更新 = $Dev.已更新 UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus} 否則 { “” } KnownIssueID = 如果 ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId} 否則 { $null } SkipReasonKnownIssue = 如果 ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } 否則 { $null } UEFICA2023Status = 如果 ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null } AvailableUpdatesPolicy = 如果 ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } 否則 { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } else { $null } } } # 將批次排入 JSON 檔案 (附加模式) 函數 Flush-DeviceBatch { param ([string]$StreamName, [System.Collections.Generic.List[object]$Batch) 如果 ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::新 () $Batch) 中每個 ($fDev { 如果 ($stDeviceFileCounts[$StreamName] -gt 0) { [void]$fSb.Append (“,'n”) } [虛空]$fSb。附加 ( ($fDev |ConvertTo-Json -壓縮) ) $stDeviceFileCounts[$StreamName]++ } [System.IO.File]::AppendAllText ($fPath、$fSb.ToString () 、[System.Text.Encoding]::UTF8) } # 主串流循環 $stChunkSize = 如果 ($totalFiles -le 10000) { $totalFiles } 否則 { 10000 } $stTotalChunks = [數學]::天花板 ($totalFiles / $stChunkSize) $stPeakMemMB = 0 若 ($stTotalChunks -gt 1) { Write-Host 「處理$totalFiles檔案$stTotalChunks$stChunkSize (串流,$ParallelThreads執行緒) :」 -ForegroundColor 青色 } 否則 { Write-Host 「串流 (處理$totalFiles檔案,$ParallelThreads 線程) :」 -ForegroundColor 青色 } 當 ($ci = 0;$ci -$stTotalChunks;$ci++) { $cStart = $ci * $stChunkSize $cEnd = [數學]::最小 ($cStart + $stChunkSize,$totalFiles) - 1 $cFiles = $jsonFiles[$cStart..$cEnd] 如果 ($stTotalChunks -gt 1) { Write-Host 「區塊 $ ($ci + 1) /$stTotalChunks ($ ($cFiles.計數) 檔案) : “ -無新行 -前景灰色 } 否則 { Write-Host 「載入中 $ ($cFiles.Count) 檔案: “ -無新行 -前景顏色灰色 } $cSw = [System.Diagnostics.Stopwatch]::StartNew () $rawDevices = Load-FilesParallel -Files $cFiles -線程$ParallelThreads # 每區塊批次列表 $cBatches = @{} 在 $stDeviceFiles) 中每個 ($df { $cBatches[$df] = [System.Collections.Generic.List[object]]::new () } $cNew = 0;$cDupe = 0 $rawDevices) 中每個 ($raw { 如果 (-不$raw) {繼續 } $device = Normalize-DeviceRecord $raw $hostname = $device。主機名稱 如果 (-not $hostname) { 繼續 } 如果 ($seenHostnames.Contains ($hostname) ) { $cDupe++; 繼續 } [虛無]$seenHostnames。加入 ($hostname) $cNew++;$c.Total++ $sbOn = ($device。SecureBootEnabled -ne $false -和「$ ($device。SecureBootEnabled) “ -ne ”False“) 如果 ($sbOn) { $c.SBEnabled++ } 否則 { $c.SBOff++; $cBatches[“secureboot_off”]。新增 ( (Get-SlimDevice $device) ) } $isUpd = $device。IsUpdated -eq $true $conf = 如果 ($device。PSObject.Properties['ConfidenceLevel'])和$device。ConfidenceLevel) { “$ ($device.ConfidenceLevel) “ } 否則 { ”“ } $hasErr = (-not [字串]::IsNullOrEmpty ($device。UEFICA2023Error) -以及「$ ($device。UEFICA2023Error) “ -ne ”0“ -和 ”$ ($device.UEFICA2023Error) “ -ne ”“) $tskDis = ($device。SecureBootTaskEnabled -eq $false -或「$ ($device。SecureBootTaskStatus) “ -eq 'Disabled' -或「$ ($device.SecureBootTaskStatus) “ -eq 'NotFound') $tskNF = (“$ ($device。SecureBootTaskStatus) “ -eq 'NotFound') $bid = 如果 ($device。PSObject.Properties['BucketId'] -以及$device。BucketId) { “$ ($device.BucketId) “ } 否則 { ”“ } $e 1808 = 如果 ($device。PSObject.Properties['Event1808Count']) { [int]$device.Event1808Count } else { 0 } $e 1801 = 如果 ($device。PSObject.Properties['Event1801Count']) { [int]$device.Event1801Count } else { 0 } $e 1803 = 如果 ($device。PSObject.Properties['Event1803Count']) { [int]$device.Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -或$device。MissingKEK -eq $true -或「$ ($device。MissingKEK) “ -eq ”True“) $hKI = ( (-not [string]::IsNullOrEmpty ($device。SkipReasonKnownIssue) ) -或 (-not [string]::IsNullOrEmpty ($device。已知問題ID) ) ) $rStat = 如果 ($device。PSObject.Properties['RolloutStatus']) { $device.RolloutStatus } 否則 { “” } $mfr = 如果 ($device。PSObject.Properties['WMI_Manufacturer'] -和 -not [string]::IsNullOrEmpty ($device。WMI_Manufacturer) ) { $device。WMI_Manufacturer } else {「未知」} $bid = 如果 (-not [string]::IsNullOrEmpty ($bid) ) { $bid } 否則 { “” } # 預計算更新 待處理標誌 (政策/WinCS 已套用,狀態尚未更新,SB 開啟,任務未被停用) $uefiStatus = 如果 ($device。PSObject.Properties['UEFICA2023Status']) { “$ ($device.UEFICA2023Status) “ } 否則 { ”“ } $hasPolicy = ($device。PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device.AvailableUpdatesPolicy -以及「$ ($device。AvailableUpdatesPolicy) “ -ne '') $hasWinCS = ($device。PSObject.Properties['WinCSKeyApplied'] -以及$device。WinCSKeyApplied -eq $true) $statusPending = ([字串]::IsNullOrEmpty ($uefiStatus) -或 $uefiStatus -eq 'NotStarted' -或 $uefiStatus -eq 'InProgress') $isUpdatePending = ( ($hasPolicy -或 $hasWinCS) -且 $statusPending -且 -not $isUpd -且 $sbOn -且 -not $tskDis) 如果 ($isUpd) { $c.Updated++;[void]$stSuccessBuckets.Add ($bid) ;$cBatches[「updated_devices」)]。新增 ( (Get-SlimDevice $device) ) # 追蹤需要重啟的更新裝置 (UEFICA2023狀態=已更新但事件1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches[“needs_reboot”]。新增 ( (Get-SlimDevice $device) ) } } elseif (-not $sbOn) { # SecureBoot OFF — 超出範圍,請勿依信心分類 } 否則 { 若 ($isUpdatePending) { } # 在「更新待定」中分別計數 — 圓餅圖中互斥 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++ } } 如果 ($tskDis) { $c.TaskDisabled++; $cBatches[“task_disabled”]。新增 ( (Get-SlimDevice $device) ) } 如果 ($tskNF) { $c.TaskNotFound++ } 如果 (-not $isUpd -和 $tskDis) { $c.TaskDisabledNotUpdated++ } 如果 ($hasErr) { $c.WithErrors++;[無效]$stFailedBuckets。加 ($bid) ;$cBatches[「錯誤」]。新增 ( (Get-SlimDevice $device) ) $ec = $device。UEFICA2023 錯誤 若 (-not $stErrorCodeCounts.ContainsKey ($ec) ) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @ () } $stErrorCodeCounts[$ec]++ 如果 ($stErrorCodeSamples[$ec]。計數 -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } 如果 ($hKI) { $c.已知問題++;$cBatches[「known_issues」)。新增 ( (Get-SlimDevice $device) ) $ki = 如果 (-not [string]::IsNullOrEmpty ($device。SkipReasonKnownIssue) ) { $device。SkipReasonKnownIssue } 否則 { $device.已知問題ID } 若 (-not $stKnownIssueCounts.ContainsKey ($ki) ) { $stKnownIssueCounts[$ki] = 0 };$stKnownIssueCounts[$ki]++ } 如果 ($mKEK) { $c.WithMissingKEK++; $cBatches[“missing_kek”]。新增 ( (Get-SlimDevice $device) ) } 若 (-not $isUpd -且 ($tskDis -或 (測試信心水準$conf「暫時暫停」) ) ) {$c.TempFailures++; $cBatches[“temp_failures”]。新增 ( (Get-SlimDevice $device) ) } 若 (-not $isUpd -且 ( (測試信心水準$conf「NotSupported」) -或 ($tskNF -,$hasErr) ) ) { $c.PermFailures++; $cBatches[“perm_failures”]。新增 ( (Get-SlimDevice $device) ) } 若 ($e 1801 -gt 0 -和 $e 1808 -eq 0 -and -not $hasErr -且$rStat -eq “InProgress”) { $c.RolloutInProgress++; $cBatches[“rollout_inprogress”]。新增 ( (Get-SlimDevice $device) ) } 如果 ($e 1801 -gt 0 -和 $e 1808 -eq 0 -和 -not $hasErr -且 $rStat -ne 「InProgress」) { $c.NotYetInitiated++ } 若 ($rStat -eq 「InProgress」 -及 $e 1808 -eq 0) { $c.InProgress++ } # 更新待處理:政策或 WinCS 已套用,狀態待定,SB 開啟,任務未被停用 若 ($isUpdatePending) { $c.更新Pending++;$cBatches[「update_pending」]。新增 ( (Get-SlimDevice $device) ) } 若 (-not $isUpd -,$sbOn) { $cBatches[“not_updated”]。新增 ( (Get-SlimDevice $device) ) } # 觀察裝置 (與行動要求分開) 若 (-not $isUpd - (檢定信心水準$conf「觀察不足」) ) { $cBatches[“under_observation”]。新增 ( (Get-SlimDevice $device) ) } # 需要行動:未更新、SB 開啟、與其他信心類別不符、非待更新 如果 (-不$isUpd -和 $sbOn -不$isUpdatePending -且 -不 (測試信心水準$conf「高信心」) -且 -不是 (測試信心水準$conf「觀察不足」) - (測試信心水準$conf「暫時暫停」) -且 -不是 (測試信心水準$conf「不支持」) ) { $cBatches[「action_required」)。新增 ( (Get-SlimDevice $device) ) } 若 (-not $stMfrCounts.ContainsKey ($mfr) ) { $stMfrCounts[$mfr] = @{ Total=0;更新=0;更新待處理=0;HighConf=0;UnderObs=0;ActionReq=0;暫停=0;NotSupported=0;SBOff=0;WithErrors=0 } } $stMfrCounts[$mfr]。Total++ 如果 ($isUpd) { $stMfrCounts[$mfr]。Updated++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]。SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]。更新待定++ } 否則 (測試信心水準$conf「高信心」) { $stMfrCounts[$mfr]。HighConf++ } 否則 (測試信心水準$conf「觀察不足」) { $stMfrCounts[$mfr]。UnderObs++ } elseif (測試信心水準$conf「暫時暫停」) { $stMfrCounts[$mfr]。暫停++ } elseif (測試信心水準$conf「NotSupported」) { $stMfrCounts[$mfr]。不支援++ } 否則 { $stMfrCounts[$mfr]。ActionReq++ } 如果 ($hasErr) { $stMfrCounts[$mfr]。WithErrors++ } # 以桶 (追蹤所有裝置,包括空的 BucketId) $bucketKey = 如果 ($bid -且 $bid -ne “) { $bid } 否則 { ” (空) “ } 如果 (-not $stAllBuckets.ContainsKey ($bucketKey) ) { $stAllBuckets[$bucketKey] = @{ 計數=0;更新=0;製造商=$mfr;Model=“”;BIOS=“” } 如果 ($device。PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey].模型 = $device。WMI_Model } 如果 ($device。PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey].BIOS = $device。BIOSDescription } } $stAllBuckets[$bucketKey]。計數++ 如果 ($isUpd) { $stAllBuckets[$bucketKey]。Updated++ } } # 將批次沖洗到磁碟 foreach ($df in $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null;$cBatches = $null;[System.GC]::收集 () $cSw。停 () $cTime = [數學]::第 ($cSw 輪。經過。總秒數,1) $cRem = $stTotalChunks - $ci - 1 $cEta = 若 ($cRem -gt 0) { “ |預計時間:~$ ([數學]:: ($cRem * $cSw.經過.總秒數 / 60,1) ) 分鐘“ } 否則 { ”“ } $cMem = [數學]::輪 ([System.GC]::GetTotalMemory ($false) / 1MB,0) 若 ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host “ +$cNew 新,$cDupe 個複製人,${cTime}s |Mem: ${cMem}MB$cEta“ -前景綠色 } # 完成 JSON 陣列 ($dfName$stDeviceFiles) { [System.IO.File]::AppendAllText ($stDeviceFilePaths[$dfName], “'n]”, [System.Text.Encoding]::UTF8) Write-Host 「$dfName.json:$ ($stDeviceFileCounts[$dfName]) 裝置」 -前景色 深灰色 } # 計算衍生統計 $stAtRisk = 0;$stSafeList = 0 foreach ($bid in $stAllBuckets.Keys) { $b = $stAllBuckets[$bid];$nu = $b.Count - $b.更新 若 ($stFailedBuckets.Contains ($bid) ) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains ($bid) ) { $stSafeList += $nu } } $stAtRisk = [數學]::最大 (0, $stAtRisk - $c.WithErrors) # NotUptodate = 從not_updated批次 (SB 開啟且未更新的裝置中計數) $stNotUptodate = $stDeviceFileCounts[“not_updated”] $stats = [有序]@{ ReportGeneratedAt = (取得日期) 。ToString (「yyyy-mm-dd HH:mm:ss」) TotalDevices = $c.Total;SecureBootEnabled = $c.SBEnabled;SecureBootOFF = $c.SBOff 更新 = $c.更新;高信心 = $c.高信心;觀察不足 = $c.觀察不足 ActionRequired = $c.ActionReq;暫時暫停 = $c.暫時暫停;不支援 = $c.不支援 NoConfidenceData = $c.NoConfData;TaskDisabled = $c.TaskDisabled;未找到任務 = $c.未找到 TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated 證書已更新 = $c.更新;NotUptodate = $stNotUptodate;完全更新 = $c.更新 更新待處理 = $stNotUptodate;更新完整 = $c.更新 WithErrors = $c.WithErrors;進度中 = $c.進度;尚未開始 = $c.尚未開始 RolloutInProgress = $c.RolloutInProgress;已知問題 = $c。已知問題 WithMissingKEK = $c.WithMissingKEK;TemporaryFailures = $c.TempFailures;永久失敗次數 = $c.PermFailures NeedsReboot = $c.NeedsReboot;更新待定 = $c.更新待定 AtRiskDevices = $stAtRisk;SafeListDevices = $stSafeList PercentWithErrors = 如果 ($c.Total -gt 0) { [math]::round ( ($c.WithErrors/$c.Total) *100,2) } 否則 { 0 } PercentAtRisk = 若 ($c.Total -gt 0) { [數學]::round ( ($stAtRisk/$c.Total) *100,2) } 否則 { 0 } PercentSafeList = 如果 ($c.總計 -gt 0) {[數學]::round ( ($stSafeList/$c.Total) *100,2) } 否則 { 0 } PercentHighConfidence = 若 ($c.Total -gt 0) { [math]::Round ( ($c.HighConf/$c.Total) *100,1) } 否則 { 0 } PercentCertUpdated = 如果 ($c.Total -gt 0) { [math]::round ( ($c.Updated/$c.Total) *100,1) } 否則 { 0 } PercentActionRequired = 如果 ($c.Total -gt 0) { [math]::Round ( ($c.ActionReq/$c.Total) *100,1) } 否則 { 0 } PercentNotUptodate = 如果 ($c.Total -gt 0) { [數學]::Round ($stNotUptodate/$c.Total*100,1) } 否則 { 0 } PercentFullyUpdated = 如果 ($c.Total -gt 0) { [math]::Round ( ($c.Updated/$c.Total) *100,1) } 否則 { 0 } UniqueBuckets = $stAllBuckets.Count;峰值記憶體MB = $stPeakMemMB;ProcessingMode = 「串流」 } # 寫CSVs(CSV) [PSCustomObject]$stats |Export-Csv -路徑 (連接路徑 $OutputPath 「SecureBoot_Summary_$timestamp.csv」) -NoTypeInformation -編碼 UTF8 $stMfrCounts.GetEnumerator () |Sort-Object { $_。Value.Total } -降序 |ForEach-Object { [PSCustomObject]@{ Manufacturer=$_.關鍵;計數=$_。價值。總數;更新=$_。價值。更新;高自信=$_。價值。高會議;ActionRequired=$_。價值。行動需求 } } |Export-Csv -路徑 (連接路徑 $OutputPath 「SecureBoot_ByManufacturer_$timestamp.csv」) -noTypeInformation -編碼 UTF8 $stErrorCodeCounts.GetEnumerator () |Sort-Object 值 -降序 |ForEach-Object { [PSCustomObject]@{ ErrorCode=$_.關鍵;計數=$_。價值;SampleDevices= ($stErrorCodeSamples[$_.Key] -join “, ”) } } |Export-Csv -路徑 (加入路徑 $OutputPath 「SecureBoot_ErrorCodes_$timestamp.csv」) -noTypeInformation -編碼 UTF8 $stAllBuckets.GetEnumerator () |Sort-Object { $_。Value.Count } -降 |ForEach-Object { [PSCustomObject]@{ BucketId=$_.關鍵;計數=$_。價值。數量;更新=$_。價值。更新;未更新=$_。價值。計數-$_。價值。更新;製造商=$_。價值。製造商 } } |Export-Csv -路徑 (連接路徑 $OutputPath 「SecureBoot_UniqueBuckets_$timestamp.csv」) -noTypeInformation -編碼 UTF8 # 產生與編排器相容的 CSV 檔名 (預期的檔名 Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir「not_updated.json」 若 (測試路徑$notUpdatedJsonPath) { 試試看 { $nuData = Get-Content $notUpdatedJsonPath -原始碼 |ConvertFrom-Json 如果 ($nuData.Count -gt 0) { # NotUptodate CSV - 調解器搜尋 *NotUptodate*.csv $nuData |Export-Csv -路徑 (加入路徑$OutputPath「SecureBoot_NotUptodate_$timestamp.csv」) -noTypeInformation -編碼 UTF8 Write-Host 「編曲器 CSV:SecureBoot_NotUptodate_timestamp.csv ($ ($nuData。計數) 裝置) 」-前景灰色 } } 接住 { } } # 為儀表板撰寫 JSON 資料 $stats |ConvertTo-Json -深度3 |Set-Content (連接路徑$dataDir「summary.json」) -編碼 UTF8 # 歷史追蹤:將數據點儲存至趨勢圖 # 使用穩定的快取位置,讓趨勢資料能在有時間戳的聚合資料夾中持續存在。 # 如果 OutputPath 看起來像「...\Aggregation_yyyyMMdd_HHmmss」,快取會放在父資料夾。# 否則,快取會進入 OutputPath 本身。$parentDir = Split-Path $OutputPath -家長 $leafName = Split-Path $OutputPath -葉片 如果 ($leafName -匹配 '^Aggregation_\d{8}' -或 $leafName -EQ 'Aggregation_Current') { # Orchestrator 建立的時間戳資料夾 — 使用父檔以穩定快取 $historyPath = Join-Path $parentDir「.cache\trend_history.json」 } 否則 { $historyPath = Join-Path $OutputPath 「.cache\trend_history.json」 } $historyDir = Split-Path $historyPath -家長 如果 (-not (測試路徑 $historyDir) ) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @ () 若 (測試路徑$historyPath) { 試著 { $historyData = @ (Get-Content $historyPath -Raw |ConvertFrom-Json) } catch { $historyData = @ () } } # 也要檢查 OutputPath\.cache\ 裡面 (舊版本的舊版位置) # 合併所有尚未在主要歷史中的資料點 如果 ($leafName -eq 'Aggregation_Current' -或 $leafName -匹配 '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath 「.cache\trend_history.json」 若 ( (測試路徑$innerHistoryPath) -且$innerHistoryPath -ne $historyPath) { 試試看 { $innerData = @ (Get-Content $innerHistoryPath -Raw |ConvertFrom-Json) $existingDates = @ ($historyData |ForEach-Object { $_。日期}) foreach ($entry in $innerData) { 如果 ($entry。日期和$entry。Date -notin $existingDates) { $historyData += $entry } } 如果 ($innerData.Count -gt 0) { Write-Host 「合併 $ ($innerData.Count) 內部快取資料點」-ForegroundColor DarkGray } } 接住 { } } }
# BOOTSTRAP:如果趨勢歷史是空白或稀疏的,請從歷史資料 重建 如果 ($historyData.Count -lt 2 -和 ($leafName -匹配 '^Aggregation_\d{8}' -或 $leafName -eq 'Aggregation_Current') ) { Write-Host「從歷史資料自助趨勢歷史......」-ForegroundColor Yellow $dailyData = @{} # 來源 1:目前資料夾中的摘要 CSV 檔 (Aggregation_Current 保留所有摘要 CSV) $localSummaries = Get-ChildItem $OutputPath -過濾器「SecureBoot_Summary_*.csv」 -EA 靜默繼續 |Sort-Object 名稱 在$localSummaries) 中每個 ($summCsv { 試試看 { $summ = Import-Csv $summCsv。全名 |Select-Object -第一位 如果 ($summ。TotalDevices -and [int]$summ。TotalDevices -gt 0 -和$summ。報導生成於) { $dateStr = ([約會時間]$summ。報導生成於) 。ToString (「yyyy-mm-dd」) $updated = 如果 ($summ。更新於) { [int]$summ。更新 } 否則 { 0 } $notUpd = 如果 ($summ。NotUptodate) { [int]$summ。NotUptodate } 否則 { [int]$summ。TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ 日期 = $dateStr;總計 = [int]$summ。TotalDevices;更新 = $updated;未更新 = $notUpd 需求重啟 = 0;誤差 = 0;ActionRequired = 如果有 ($summ。ActionRequired) { [int]$summ。ActionRequired } 否則 { 0 } } } } 接住 { } } # 來源 2:舊有時間戳記的 Aggregation_* 資料夾 (遺產,如果它們還存在的話) $aggFolders = Get-ChildItem $parentDir -Directory -Filter “Aggregation_*” -EA 靜默繼續 | Where-Object { $_。姓名匹配 '^Aggregation_\d{8}' } | Sort-Object 名稱 foreach 在 $aggFolders) 中 ($folder { $summCsv = Get-ChildItem $folder。全名 -過濾器「SecureBoot_Summary_*.csv」-EA 靜默繼續 |Select-Object -第一 如果 ($summCsv) { 試試看 { $summ = Import-Csv $summCsv.全名 |Select-Object -第一 如果 ($summ。TotalDevices -and [int]$summ。TotalDevices -gt 0) { $dateStr = $folder。名稱 -替換 '^Aggregation_ (\d{4}) (\d{2}) (\d{2}) _.*', '$1-$2-$3' $updated = 如果 ($summ。更新於) { [int]$summ。更新 } 否則 { 0 } $notUpd = 如果 ($summ。NotUptodate) { [int]$summ。NotUptodate } 否則 { [int]$summ。TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ 日期 = $dateStr;總計 = [int]$summ。TotalDevices;更新 = $updated;未更新 = $notUpd 需求重啟 = 0;誤差 = 0;ActionRequired = 若 ($summ。ActionRequired) { [int]$summ。ActionRequired } 否則 { 0 } } } } 接住 { } } } # 來源 3:RolloutState.json WaveHistory (從第一天起有每波段的時間戳) # 即使沒有舊的聚合資料夾,也能提供基線資料點 $rolloutStatePaths = @ ( (「RolloutState\RolloutState.json」$parentDir Join-Path ) , (加入路徑$OutputPath「RolloutState\RolloutState.json」) ) ($rsPath$rolloutStatePaths) { 若 (測試路徑$rsPath) { 試試看 { $rsData = Get-Content $rsPath -原始碼 |ConvertFrom-Json 如果 ($rsData.WaveHistory) { # 使用波浪起始日期作為趨勢數據點 # 計算針對每一波的累積裝置 $cumulativeTargeted = 0 ($wave$rsData.WaveHistory) { 如果 ($wave。開始了 -和$wave。DeviceCount) { $waveDate = ([日期時間]$wave。從) 開始。ToString (「yyyy-MM-dd」) $cumulativeTargeted += [int]$wave。裝置計數 如果 (-not $dailyData.ContainsKey ($waveDate) ) { # 近似值:波浪開始時,僅更新前一波的裝置 $dailyData[$waveDate] = [PSCustomObject]@{ 日期 = $waveDate;總分 = $c。總分;更新 = [數學]::最大 (0, $cumulativeTargeted - [int]$wave。裝置計數) 未更新 = $c.總數 - [數學]::最大 (0, $cumulativeTargeted - [int]$wave。裝置計數) 需求重啟 = 0;誤差 = 0;所需行動 = 0 } } } } } } 接住 { } break# 先找到 } }
如果 ($dailyData.Count -gt 0) { $historyData = @ ($dailyData.GetEnumerator () |Sort-Object 說明 |ForEach-Object { $_。價值}) Write-Host「自助 $ ($historyData。從歷史摘要中計數) 資料點」-ForegroundColor Green } }
# 新增當前資料點 (每日重複解體 - 保持每日最新) $todayKey = (Get-Date) 。ToString (「yyyy-mm-dd」) $existingToday = $historyData |Where-Object { 「$ ($_。Date) “ -類似「$todayKey*」} 如果 ($existingToday) { # 替換今天的條目 $historyData = @ ($historyData |Where-Object { 「$ ($_。日期) “ -不像 ”$todayKey*“ }) } $historyData += [PSCustomObject]@{ 日期 = $todayKey 總計 = $c。總計 更新 = $c。更新 未更新 = $stNotUptodate 需求重啟 = $c.需要重啟 錯誤數 = $c.WithErrors ActionRequired = $c.ActionReq } # 移除錯誤資料點 (總) 0,保留最後 90 個 $historyData = @ ($historyData |Where-Object { [int]$_。總計 -gt 0 }) # 無上限 — 趨勢資料為 ~100 位元組/條目,整整一年 = ~36 KB $historyData |ConvertTo-Json -深度3 |Set-Content $historyPath -編碼 UTF8 Write-Host 「趨勢歷史:$ ($historyData。計數) 資料點」-前景色彩 深灰色 # 建立 HTML 趨勢圖表資料 $trendLabels = ($historyData |ForEach-Object { “'$ ($_。日期) '“ }) -加入 ”, $trendUpdated = ($historyData |ForEach-Object { $_。更新 }) -加入“,” $trendNotUpdated = ($historyData |ForEach-Object { $_。NotUpdated }) -加入“,” $trendTotal = ($historyData |ForEach-Object { $_。Total }) -join “, # 預測:利用指數倍增延伸趨勢線 (2、4、8、16......) # 從實際趨勢歷史資料推導波浪大小與觀察週期。# - 浪長 = (最近部署波次歷史上最大單期增長) # - 觀測天數 = 趨勢數據點之間的平均日曆天數 (我們執行) 頻率 # 則每個週期將波浪大小加倍,與協調者的 2 倍成長策略相符。$projLabels = “”;$projUpdated = “”;$projNotUpdated = “”;$hasProjection = $false 若 ($historyData.Count -ge 2) { $lastUpdated = $c。更新 $remaining = $stNotUptodate # 僅 SB-ON 未更新裝置 (排除 SecureBoot OFF) $projDates = @ () ;$projValues = @ () ;$projNotUpdValues = @ () $projDate = Get-Date
# 從趨勢歷史推導波浪大小與觀察週期 $increments = @ () $dayGaps = @ () 當 ($hi = 1 時;$hi -中尉$historyData伯爵;$hi++) { $inc = $historyData[$hi]。更新日期 - $historyData[$hi-1]。更新 若 ($inc -gt 0) { $increments += $inc } 試試看 { $d 1 = [約會時間]::P屁股 ($historyData[$hi-1]。日期) $d 2 = [約會時間]::P屁股 ($historyData[$hi]。日期) $gap = ($d 2 - $d 1) 。總日數 若 ($gap -gt 0) { $dayGaps += $gap } } 接住 {} } # 浪型 = 目前浪 () 最近一次正增量,回落至平均值,最低 2 $waveSize = 如果 ($increments。Count -gt 0) { [數學]::最大 (2,$increments[-1]) } 否則 { 2 } # 觀察期間 = 每個波) 資料點 (日曆日的平均間隔,最少 1 $waveDays = 如果 ($dayGaps。計數 -gt 0) { [數學]::最大 (1, [數學]::round ( ($dayGaps |Measure-Object -平均的) 。平均,0) ) } 否則 { 1 }
Write-Host “投影:waveSize=$waveSize (,從最後一個) 增量開始,waveDays=$waveDays (歷史平均間隙) ” -ForegroundColor DarkGray
$dayCounter = 0 # 投影直到所有裝置更新或最多365天 當 ($pi = 1 時;$pi -le 365;$pi++) { $projDate = $projDate.AddDays (1) $dayCounter++ # 在每個觀察期邊界,先部署一波,然後加倍 若 ($dayCounter -ge $waveDays) { $devicesThisWave = [數學]::最小 ($waveSize,$remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave 如果 ($lastUpdated -gt ($c.Updated + $stNotUptodate) ) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # 下一階段雙倍波浪 (策划者2倍策略) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += “'$ ($projDate.ToString (”yyyy-MM-dd“) ) '” $projValues += $lastUpdated $projNotUpdValues += [數學]::最大 (0, $remaining) 若 ($remaining -le 0) { break } } $projLabels = $projDates -加入「」, $projUpdated = $projValues -加入「」, $projNotUpdated = $projNotUpdValues -加入「」, $hasProjection = $projDates.計數 -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host「投影:至少需要兩個趨勢資料點才能推導波浪時序」-ForegroundColor DarkGray } # 建立 here-string 的合併圖表資料字串 $allChartLabels = 若 ($hasProjection) { “$trendLabels,$projLabels” } 否則 { $trendLabels } $projDataJS = 若 ($hasProjection) { $projUpdated } 否則 { “” } $projNotUpdJS = 若 ($hasProjection) { $projNotUpdated } 否則 { “” } $histCount = ($historyData |測量-物件) 。伯爵 $stMfrCounts.GetEnumerator () |Sort-Object { $_。Value.Total } -降序 |ForEach-Object { @{ 姓名=$_。關鍵;總計=$_。價值。總數;更新=$_。價值。更新;highConf=$_。價值。高會議;actionReq=$_。價值。行動需求 } } |ConvertTo-Json -深度3 |Set-Content (連接路徑$dataDir「manufacturers.json」) 編碼 UTF8 # 將 JSON 資料檔轉換為 CSV 以便人類閱讀 Excel 下載。 Write-Host 「將裝置資料轉換為 Excel 下載用的 CSV...」-ForegroundColor Gray ($dfName$stDeviceFiles) { $jsonFile = Join-Path $dataDir「$dfName.json」 $csvFile = Join-Path $OutputPath “SecureBoot_${dfName}_$timestamp.csv” 若 (測試路徑$jsonFile) { 試試看 { $jsonData = Get-Content $jsonFile -原始碼 |ConvertFrom-Json 如果 ($jsonData.Count -gt 0) { # 為 CSV 加入額外欄位update_pending $selectProps = 若 ($dfName -eq “update_pending”) { @ ('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } 否則 { @ ('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData |Select-Object $selectProps | Export-Csv -路徑$csvFile -NoTypeInformation -編碼 UTF8 Write-Host 「$dfName -> $ ($jsonData.數) 行 -> CSV」 -前景顏色 深灰色 } } 捕捉 { Write-Host “ $dfName - 跳過” -前景顏色 深黃色 } } } # 產生自包含的 HTML 儀表板 $htmlPath = Join-Path $OutputPath「SecureBoot_Dashboard_$timestamp.html」 Write-Host「產生自包含的 HTML 儀表板......」-ForegroundColor Yellow # 速度預測:根據掃描歷史或先前摘要計算 $stDeadline = [datetime]“2026-06-24” # KEK 證書到期 $stDaysToDeadline = [數學]::最大 (0, ($stDeadline - (取得日期) ) 。天) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = 「無資料」 $stWorkingDays = 0 $stCalendarDays = 0 # 先試試趨勢歷史, (輕量級,已由聚合器維護——取代冗長的ScanHistory.json) 如果 ($historyData.計數 -ge 2) { $validHistory = @ ($historyData |Where-Object { [int]$_。總計 -gt 0 -和 [int]$_。更新 -ge 0 }) 如果 ($validHistory.計數 -ge 2) { $prev = $validHistory[-2];$curr = $validHistory[-1] $prevDate = [約會時間]::P屁股 ($prev。Date.Substring (0, [數學]::Min (10, $prev。日期。長度) ) ) $currDate = [約會時間]::P屁股 ($curr。Date.Substring (0, [數學]::Min (10, $curr。日期。長度) ) ) $daysDiff = ($currDate - $prevDate) 。總日數 若 ($daysDiff -gt 0) { $updDiff = [int]$curr。更新 - [int]$prev。更新 若 ($updDiff -gt 0) { $stDevicesPerDay = [數學]::輪數 ($updDiff / $daysDiff,0) $stVelocitySource = 「趨勢歷史」 } } } } # 試試編排器推出摘要 (已有預先計算的速度) 若 ($stVelocitySource -eq “N/A” -且$RolloutSummaryPath -, (測試路徑$RolloutSummaryPath) ) { 試試看 { $rolloutSummary = Get-Content $RolloutSummaryPath -原始碼 |ConvertFrom-Json 如果 ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [數學]::每日輪數 ([雙倍]$rolloutSummary.裝置,1) $stVelocitySource = 「配器者」 如果 ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.預計完工日期 } 若 ($rolloutSummary.工作天數剩餘) { $stWorkingDays = [int]$rolloutSummary.工作日數} 若 ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } 接住 { } } # 備用:嘗試先前的摘要 CSV (搜尋目前資料夾及父/兄弟姊妹的聚合資料夾) 若 ($stVelocitySource -eq “N/A”) { $searchPaths = @ ( (連接路徑$OutputPath「SecureBoot_Summary_*.csv」) ) # 也搜尋兄弟姊妹的聚合資料夾 (編排器每次執行都會建立新資料夾) $parentPath = Split-Path $OutputPath -家長 如果 ($parentPath) { $searchPaths += (連接路徑$parentPath「Aggregation_*\SecureBoot_Summary_*.csv」) $searchPaths += (連接路徑$parentPath「SecureBoot_Summary_*.csv」) } $prevSummary = $searchPaths |ForEach-Object { Get-ChildItem $_ -EA 靜默繼續 } |Sort-Object 最後寫作時間 -下降 |Select-Object -第一 若 ($prevSummary) { 試試看 { $prevStats = Get-Content $prevSummary.全名 |ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ( (約會時間) - $prevDate) 。總日數 若 ($daysSinceLast -gt 0.01) { $prevUpdated = [int]$prevStats。更新 $updDelta = $c。更新 - $prevUpdated 若 ($updDelta -gt 0) { $stDevicesPerDay = [數學]::四輪 ($updDelta / $daysSinceLast,0) $stVelocitySource = 「前一報告」 } } } 接住 { } } } # 備用:從完整趨勢歷史跨度 (計算速度,從第一點與最新數據點) 若 ($stVelocitySource -eq “N/A” -與 $historyData。計數 -ge 2) { $validHistory = @ ($historyData |Where-Object { [int]$_。總計 -gt 0 -和 [int]$_。更新 -ge 0 }) 如果 ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [約會時間]::P屁股 ($first。Date.Substring (0, [數學]::Min (10, $first.日期。長度) ) ) $lastDate = [約會時間]::P屁股 ($last。Date.Substring (0, [Math]::Min (10, $last。日期。長度) ) ) $daysDiff = ($lastDate - $firstDate) 。總日數 若 ($daysDiff -gt 0) { $updDiff = [int]$last。更新 - [int]$first。更新 若 ($updDiff -gt 0) { $stDevicesPerDay = [數學]::輪數 ($updDiff / $daysDiff,1) $stVelocitySource = 「趨勢歷史」 } } } } # 利用指數倍增計算預測 (與趨勢圖一致) # 若有,請重用已計算出的投影資料 若 ($hasProjection -和 $projDates。計數 -gt 0) { # 使用所有裝置更新後 (最後預計日期) $lastProjDateStr = $projDates[-1] -將「'', “」替換 $stProjectedDate = ([日期時間]::P屁股 ($lastProjDateStr) ) 。ToString (「嗯嗯,嗯,yyyy」) $stCalendarDays = ([約會時間]::P屁股 ($lastProjDateStr) - (約會) ) 。日子 $stWorkingDays = 0 $d = 取得日期 當 ($i = 0;$i -lt $stCalendarDays;$i++) { $d = $d.AddDays (1) 如果 ($d.DayOfWeek -ne 「星期六」 -和 $d.DayOfWeek -ne 「星期日」) { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -and $stNotUptodate -gt 0) { # 備用:若無指數數據則使用線性投影 $daysNeeded = [數學]::天花板 ($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (約會) 。AddDays ($daysNeeded) 。ToString (「嗯嗯dd,yyyy」) $stWorkingDays = 0;$stCalendarDays = $daysNeeded $d = 取得日期 當 ($i = 0;$i -lt $daysNeeded;$i++) { $d = $d.AddDays (1) 如果 ($d.DayOfWeek -ne 「星期六」 -以及 $d.DayOfWeek -ne 「星期日」) { $stWorkingDays++ } } } # 建立速度 HTML $velocityHtml = 若 ($stDevicesPerDay -gt 0) { 「<師><強>🚀裝置/日:</strong> $ ($stDevicesPerDay.ToString ('N0') ) (來源:$stVelocitySource) </div>“ + 「<div><強力>📅預計完成度:</強> $stProjectedDate“ + $ (如果 ($stProjectedDate -和 [datetime]::P arse ($stProjectedDate) -gt $stDeadline) { “ <span style='color:#dc3545;font-weight:bold'>⚠ 逾期截止日</span>“ } 否則 { ” <span style='color:#28a745'>✓ 截止日前</span>“ }) + 「</div>」+ 「<師><強>🕐工作天數:</強> $stWorkingDays |<強>Calendar 天數:</強> $stCalendarDays</div>“ + “<div style='font-size:.8em;顏色:#888'>截止日期:2026年6月24日 (KEK證書到期) |剩餘天數:$stDaysToDeadline</div>」 } 否則 { “<div style='padding:8px;背景:#fff3cd;邊界半徑:4px;邊框左邊:3px 實心 #ffc107'>“ + 「<強力>📅預計完成度:</強力> 速度計算資料不足。 “ + 「至少執行兩次彙整並更改資料以建立速率。<br/>」+ 「<強>截止日期:</強力> 2026年6月24日 (KEK 證書到期) |剩餘 <強>天數:</強力> $stDaysToDeadline</div>” } # 證書倒數 $certToday = 取得日期 $certKekExpiry = [日期時間]“2026-06-24” $certUefiExpiry = [日期時間]“2026-06-27” $certPcaExpiry = [日期時間]“2026-10-19” $daysToKek = [數學]::最大 (0, ($certKekExpiry - $certToday) 。天) $daysToUefi = [數學]::最大 (0, ($certUefiExpiry - $certToday) 。天) $daysToPca = [數學]::最大 (0, ($certPcaExpiry - $certToday) 。天) $certUrgency = 若 ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } 否則 { '#28a745' } # 助手:從 JSON 讀取記錄,建立桶摘要 + 前 N 個裝置列 $maxInlineRows = 200 函數 Build-InlineTable { 參數 ([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = “”) $bucketSummary = “” $deviceRows = “” $totalCount = 0 若 (測試路徑$JsonPath) { 試試看 { $data = Get-Content $JsonPath -原始碼 |ConvertFrom-Json $totalCount = $data。伯爵 # BUCKET 摘要:依 BucketID 群組,顯示每個桶的計數,並從全域桶統計更新 若 ($totalCount -gt 0) { $buckets = $data |Group-Object BucketID |Sort-Object 計數 -降格 $bucketSummary = “><2 h3 style='font-size:.95em;顏色:#333;邊距:10px 0 5px'><3 依硬體桶分類 ($ ($buckets。數) 桶) ><4 /h3>」 $bucketSummary += “><6 div style='max-height:300px;overflow-y:auto;margin-bottom:15px'><table><thead><tr><th th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right;color:#28a745' (3 更新為</th><th style='text-align:right;顏色:#dc3545's (7 未更新</th><><1 製造商><2 /th></tr></thead><tbody>” $buckets) 中每個 ($b { $bid = 如果 ($b.Name) { $b.Name } 否則 { “ (空) ” } $mfr = ($b.群 |Select-Object -前一) 。WMI_Manufacturer # 從全球桶統計中更新的計數 (該桶中所有裝置,涵蓋整個資料集) $lookupKey = $bid $globalBucket = 如果 ($stAllBuckets.ContainsKey ($lookupKey) ) { $stAllBuckets[$lookupKey] } 否則 { $null } $bUpdatedGlobal = 如果 ($globalBucket) { $globalBucket.Updated } 否則 { 0 } $bTotalGlobal = 如果 ($globalBucket) { $globalBucket.Count } 否則 { $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;顏色:#28a745;font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right;顏色:#dc3545;字體粗重:粗體 >$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n” } $bucketSummary += “</tbody></table></div>” } # 裝置細節:前 N 排為平面列表 $slice = $data |Select-Object -第一$MaxRows ($d$slice) { $conf = $d.信心水準 $confBadge = 若 ($conf -匹配「High」) { '<span class =“badge badge-success”>High Conf><2 /span>' } elseif ($conf -match “Not Sup”) { '<span class=“badge badge-danger”>不支援><6 /span>' } elseif ($conf -match “Under”) { '<span class=“badge badge-info”>Under Obs><0 /span>' } elseif ($conf -match “Paused”) { '<span class =“badge badge-warning”>暫停><4 /span>' } 否則 { '<span class=“badge badge-warning”>行動需求><8 /span>' } $statusBadge = 如果 ($d.IsUpdated) {'><00 span class=“badge badge-success”><01 更新</span>' } elseif ($d.UEFICA2023Error) { '><04 span class=“badge badge-danger”><05 Error</span>' } 否則 { '><08 span class=“badge badge-warning”><09 待處理><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” } } 接住 { } } 若 ($totalCount -eq 0) { 回傳 “><44 div style='padding:20px;顏色:#888;font-style:italic'><45 此類別無裝置。><46 /div>” } $showing = [數學]::最小 ($MaxRows,$totalCount) $header = “><48 div style='margin:5px 0;字體大小:.85em;顏色:#666'><49 總計:$ ($totalCount.ToString (「N0」) ) 裝置」 若 ($CsvFileName) { $header += “ |><50 a href='$CsvFileName' style='color:#1a237e;font-weight:bold'>📄下載完整 Excel CSV><3 /a>“ } $header += 「><55 /div>」 $deviceHeader = “><57 h3 style='font-size:.95em;顏色:#333;margin:10px 0 5px'><58 裝置細節 (顯示首$showing) ><59 /h3>” $deviceTable = “><61 div style='max-height:500px;overflow-y:auto' (5 table><thead><tr><><0 HostName><1 /th><th><4 製造商><5 /th><><8 Model><9 /th><th><2 信心><3 /th><><6 狀態><><1><7 /th><><4><0 Bucket ID><5 /th /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>” return “$header$bucketSummary$deviceHeader$deviceTable” } # 從磁碟上的 JSON 檔案建立內嵌表格,並連結到 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” # 自訂待定更新表 — 包含 UEFICA2023Status 與 UEFICA2023Error 欄位 $tblUpdatePending = “” $upJsonPath = Join-Path $dataDir「update_pending.json」 若 (測試路徑$upJsonPath) { 試試看 { $upData = Get-Content $upJsonPath -原始碼 |ConvertFrom-Json $upCount = $upData.Count 若 ($upCount -gt 0) { $upHeader = “<div style='margin:5px 0;字體大小:.85em;顏色:#666'>總計:$ ($upCount.ToString (“N0”) ) 裝置 |<a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e;font-weight:bold'>📄下載 Excel 的完整 CSV><4 /a></div>” $upRows = “” $upSlice = $upData |Select-Object -第一$maxInlineRows foreach ($d in $upSlice) { $uefiSt = 如果 ($d.UEFICA2023Status) { $d.UEFICA2023Status } 否則 { '<span style=“color:#999”>null><0 /span>' } $uefiErr = 如果 ($d.UEFICA2023Error) { “<span style='color:#dc3545'>$ ($d.UEFICA2023Error) </span>” } 否則 { '-' } $policyVal = 如果 ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } 否則 { '-' } $wincsVal = 如果 ($d.WinCSKeyApplied) { '<span class=“badge badge-success”>Yes><8 /span>' } 否則 { '-' } $upRows += “<tr><td><3 $ ($d.HostName) </td><td><7 $ ($d.WMI_Manufacturer) </td><td><1 $ ($d.WMI_Model) </td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$ ($d.BucketId) </td></tr><65 'n” } $upShowing = [數學]::最小 ($maxInlineRows,$upCount) $upDevHeader = “<h3 style='font-size:.95em;顏色:#333;margin:10px 0 5px'>裝置細節 (顯示首$upShowing) </h3>” $upTable = “<div style='max-height:500px;溢出-Y: auto'><s table><thead><tr><th><9 HostName><0 /th><><3 製造商><4 /th><><7 Model><8 /th><th><1 UEFICA2023 狀態><2 /th><th><5 UEFICA2023 錯誤><6 /th><th><9 Policy</th><th>WinCS Key</th><>BucketID</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>” $tblUpdatePending = “$upHeader$upDevHeader$upTable” } 否則 { $tblUpdatePending = “ (22 div style='padding:20px;顏色:#888;font-style:italic' (23 此類別中無裝置。 (24 /div>” } } 抓 { $tblUpdatePending = “ (26 div style='padding:20px;顏色:#888;font-style:italic' (27 此類別中無裝置。 (28 /div>” } } 否則 { $tblUpdatePending = “ (30 div style='padding:20px;顏色:#888;font-style:italic' (31 此類別無裝置。 (32 /div>” } # 證書倒數 $certToday = 取得日期 $certKekExpiry = [日期時間]“2026-06-24” $certUefiExpiry = [日期時間]「2026-06-27」 $certPcaExpiry = [日期時間]「2026-10-19」 $daysToKek = [數學]::最大 (0, ($certKekExpiry - $certToday) 。天) $daysToUefi = [數學]::最大 (0, ($certUefiExpiry - $certToday) 。天) $daysToPca = [數學]::最大 (0, ($certPcaExpiry - $certToday) 。天) $certUrgency = 若 ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } 否則 { '#28a745' } # 內建製造商圖表數據 (依裝置數量排名前十) $mfrSorted = $stMfrCounts.GetEnumerator () |Sort-Object { $_。Value.Total } -降序 |Select-Object -前10名 $mfrChartTitle = 如果 ($stMfrCounts。計數 -le 10) { 「按製造商分類」 } 否則 { 「十大製造商」 } $mfrLabels = ($mfrSorted |ForEach-Object { “'$ ($_。按鍵) '“ }) -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 “, # 建造製造商表 $mfrTableRows = “” $stMfrCounts.GetEnumerator () |Sort-Object { $_。Value.Total } -降序 |ForEach-Object { $mfrTableRows += “ (34 tr><td><7 $ ($_.按鍵) (38 /td><td>$ ($_。Value.Total.ToString (“N0”) ) (42 /td><td>$ ($_.Value.Updated.ToString (“N0”) ) (46 /td><td>$ ($_.Value.HighConf.ToString (“N0”) ) (50 /td><td (3 $ ($_.Value.ActionReq.ToString (“N0”) ) (54 /td></tr (7 'n” } $htmlContent = @” <!DOCTYPE html (9 (0 html lang=“en” (1 (2 頭><3 (4 meta charset=“UTF-8” (5 (6 meta name=“viewport” 內容=“width=device-width, initial-scale=1.0” (7 (8 標題><9 安全啟動憑證狀態儀表板><0 /title><1 (72 script src=“https://cdn.jsdelivr.net/npm/chart.js”></script><5 (76 風格><7 *{box-sizing:border-box;margin:0;填充:0} body{font-family:'Segoe UI', Tahoma, sans-serif;背景:#f0f2f5;顏色:#333} .header{background:linear-gradient (135deg,#1a237e,#0d47a1) ;顏色:#fff;填充:20px 30px} .header h1{font-size:1.6em;margin-bottom:5px} .header .meta{font-size:.85em;不透明度:0.9} .container{max-width:1400px;利潤:0 自動;填充:20px} .cards{display:grid;grid-template-columns:重複 (auto-fill,minmax (170px,1fr) ) ;空隙:12像素;邊距:20px 0} .card{background:#fff;邊界半徑:10px;填充:15像素;盒子陰影:0 2px 8px RGBA (0,0,0,0,.08) ;border-left:4px solid #ccc;transition:transform .2s} .card:hover{transform:translateY (-2px) ;box-shadow:0 4px 15px RGBA (0,0,0,.12) } .card .value{font-size:1.8em;font-weight:700} .card .label{font-size:.8em;顏色:#666;margin-top:4px} .card .pct{font-size:.75em;顏色:#888} .section{background:#fff;邊界半徑:10px;填充:20像素;邊距:15px 0;box-shadow:0 2px 8px RGBA (0,0,0,.08) } .section h2{font-size:1.2em;顏色:#1a237e;邊緣底:10px;游標:指標;使用者選擇:無} .section h2:懸停{text-decoration:底線} .section-body{display:none} .section-body.open{display:block} .charts{display:grid;grid-template-columns:1fr 1fr;空隙:20像素;邊距:20px 0} .chart-box{background:#fff;邊界半徑:10px;填充:20像素;box-shadow:0 2px 8px RGBA (0,0,0,.08) } 表格{寬度:100%;邊界崩潰:崩潰;字體大小:.85em} th{background:#e8eaf6;填充:8px 10px;text-align:left;位置:黏性;top:0;Z指數:1} TD{填充:6px 10px;border-bottom:1px 實心 #eee} TR:hover{background:#f5f5f5} .badge{display:inline-block;填充:2px 8px;邊框半徑:10px;字體大小:.75em;font-weight:700} .badge-success{background:#d4edda;顏色:#155724} .badge-danger{background:#f8d7da;顏色:#721c24} .badge-warning{background:#fff3cd;顏色:#856404} .badge-info{background:#d1ecf1;顏色:#0c5460} .top-link{float:right;font-size:.8em;顏色:#1a237e;text-decoration:none} .footer{text-align:center;填充:20像素;顏色:#999;font-size:.8em} a{color:#1a237e} (78 /style><9 (80 /head> (82 身體> (84 div class=“header” (85 (86 h1>Secure Boot Certificate Status Dashboard</h1> (90 div class=“meta” (91 產生:$ ($stats。報導生成於) |總裝置數:$ ($c.Total.ToString (“N0”) ) |獨特桶數:$ ($stAllBuckets.Count) (92 /div><3 (94 /div><5 (96 div class=“container”>
<!-- KPI 卡 - 可點擊,連結至各區塊 --> <div class=“cards”> <a class=“card” href=“#s-nu” onclick=“openSection ('d-nu') ” style=“border-left-color:#dc3545;文字裝飾:無;position:relative“><div style=”position:absolute;頂層:8px;右:8px;背景:#dc3545;顏色:#fff;填充:1px 6px;邊界半徑:8px;字體大小:.65em;font-weight:700“>主要</div><div class=”value“ style=”color:#dc3545“>$ ($stNotUptodate.ToString (”N0“) ) </div><div class=”label“>未更新><6 /div><div class=”pct“>$ ($stats。PercentNotUptodate) % - 需要行動><0 /div></a><3 <a class=“card” href=“#s-upd” onclick=“openSection ('d-upd') ” style=“border-left-color:#28a745;文字裝飾:無;position:relative“><div style=”position:absolute;頂層:8px;右:8px;背景:#28a745;顏色:#fff;填充:1px 6px;邊界半徑:8px;字體大小:.65em;font-weight:700“>主要><8 /div><div class=”value“ style=”color:#28a745“>$ ($c.Updated.ToString (”N0“) ) </div><div class=”label“>更新><6 /div><div class=”pct“>$ ($stats。PercentCertUpdated) %</div></a><3 <a class=“card” href=“#s-sboff” onclick=“openSection ('d-sboff') ” style=“border-left-color:#6c757d;文字裝飾:無;position:relative“><div style=”position:absolute;頂層:8px;右:8px;背景:#6c757d;顏色:#fff;填充:1px 6px;邊界半徑:8px;字體大小:.65em;font-weight:700“>PRIMARY><8 /div><div class=”value“><1 $ ($c.SBOff.ToString (”N0“) ) ><2 /div><div class=”label“><5 SecureBoot OFF (6 /div><div class=”pct“><9 $ (if ($c.Total -gt 0) {[math]::round ( ($c.SBOff/$c.Total) *100,1) }else{0}) % - 超出範圍><0 /div></a><3 <a class=“card” href=“#s-nrb” onclick=“openSection ('d-nrb') ” style=“border-left-color:#ffc107;text-decoration:none“><div class=”value“ style=”color:#ffc107“>$ ($c.NeedsReboot.ToString (”N0“) ) </div><div class=”label“>需要重啟><2 /div><div class=”pct“>$ (if ($c.Total -gt 0) {[math]::round ( ($c.NeedsReboot/$c.Total) *100,1) }else{0}) % - 等待重啟><6 /div></a><9 <a class=“card” href=“#s-upd-pend” onclick=“openSection ('d-upd-pend') ” style=“border-left-color:#6f42c1;text-decoration:none“><div class=”value“ style=”color:#6f42c1“>$ ($c.UpdatePending.ToString (”N0“) ) </div><div class=”label“>Update Pending</div><div class=”pct“>$ (if ($c.Total -gt 0) {[math]::Round ( ($c.UpdatePending/$c.Total) *100,1) }else{0}) % - Policy/WinCS 已套用, 等待更新><2 /div></a><5 <a class=“card” href=“#s-rip” onclick=“openSection ('d-rip') ” style=“border-left-color:#17a2b8;text-decoration:none“><div class=”value“>$ ($c.RolloutInProgress) </div><div class=”label“>Rollout In Progress><4 /div><div class=”pct“>$ (if ($c.Total -gt 0) {[math]::Round ( ($c.RolloutInProgress/$c.Total) *100,1) }else{0}) %</div></a (1 <a class=“card” href=“#s-nu” onclick=“openSection ('d-nu') ” style=“border-left-color:#28a745;text-decoration:none“><div class=”value“ style=”color:#28a745“>$ ($c.HighConf.ToString (”N0“) ) </div><div class=”label“>高信心><20 /div><div class=”pct“>$ ($stats.PercentHighConfidence) % - 可用於推展><24 /div></a><27 <a class=“card” href=“#s-uo” onclick=“openSection ('d-uo') ” style=“border-left-color:#17a2b8;text-decoration:none“><div class=”value“ style=”color:#ffc107“ ><1 $ ($c.UnderObs.ToString (”N0“) ) ><2 /div><div class=”label“><5 Under Observation><36 /div (0 div class=”pct“><9 $ (if ($c.Total -gt 0) {[math]::round ( ($c.UnderObs/$c.Total) *100,1) }else{0}) %</div (1 /a><3 <a class=“card” href=“#s-ar” onclick=“openSection ('d-ar') ” style=“border-left-color:#fd7e14;text-decoration:none“><div class=”value“ style=”color:#fd7e14“>$ ($c.ActionReq.ToString (”N0“) ) </div (4 div class=”label“>Action Required><2 /div (5 div class=”pct“>$ ($stats.PercentActionRequired) % - 必須測試><6 /div (7 /a><9 <a class=“card” href=“#s-err” onclick=“openSection ('d-err') ” style=“border-left-color:#dc3545;text-decoration:none“><div class=”value“ style=”color:#dc3545“>$ ($stAtRisk.ToString (”N0“) ) </div><div class=”label“>風險><68 /div><div class=”pct“>$ ($stats.PercentAtRisk) % - 類似失敗><2 /div></a><5 <a class=“card” href=“#s-td” onclick=“openSection ('d-td') ” style=“border-left-color:#dc3545;text-decoration:none“><div class=”value“ style=”color:#dc3545“>$ ($c.TaskDisabled.ToString (”N0“) ) </div><div class=”label“>任務被禁用><4 /div><div class=”pct“>$ (如果 ($c.Total -gt 0) {[math]::round ( ($c.TaskDisabled/$c.Total) *100,1) }else{0}) % -><8 /div></a (1 <a class=“card” href=“#s-tf” onclick=“openSection ('d-tf') ” style=“border-left-color:#fd7e14;text-decoration:none“><div class=”value“ style=”color:#fd7e14“>$ ($c.TempPaused.ToString (”N0“) ) </div><div class=”label“>Temp. 暫停</div><div class=“pct”>$ (如果 ($c.Total -gt 0) {[math]::round ( ($c.TempPaused/$c.Total) *100,1) }else{0}) %</div></a> <a class=“card” href=“#s-ki” onclick=“openSection ('d-ki') ” style=“border-left-color:#dc3545;text-decoration:none“><div class=”value“ style=”color:#dc3545“>$ ($c.WithKnownIssues.ToString (”N0“) ) </div><div class=”label“>已知問題><6 /div><div class=”pct“>$ (if ($c.Total -gt 0) {[math]::round ( ($c.WithKnownIssues/$c.Total) *100,1) }else{0}) %</div></a><3 <a class=“card” href=“#s-kek” onclick=“openSection ('d-kek') ” style=“border-left-color:#fd7e14;text-decoration:none“><div class=”value“ style=”color:#fd7e14“ >$ ($c.WithMissingKEK.ToString (”N0“) ) </div><div class=”label“>缺少 KEK (2 /div><div class=”pct“>$ (if ($c.Total -gt 0) {[math]::Round ( ($c.WithMissingKEK/$c.Total) *100,1) }else{0}) %</div></a (9 <a class=“card” href=“#s-err” onclick=“openSection ('d-err') ” style=“border-left-color:#dc3545;text-decoration:none“><div class=”value“ style=”color:#dc3545“>$ ($c.WithErrors.ToString (”N0“) ) </div><div class=”label“>錯誤 (8 /div><div class=”pct“><1 $ ($stats。PercentWithErrors) % - UEFI 錯誤</div></a> ><6 a class=“card” href=“#s-tf” onclick=“openSection ('d-tf') ” style=“border-left-color:#dc3545;text-decoration:none“><div class=”value“ style=”color:#dc3545“><9 $ ($c.TempFailures.ToString (”N0“) ) </div (0 div class=”label“>Temp. 失敗次數</div><div class=“pct”>$ (如果 ($c.Total -gt 0) {[math]::round ( ($c.TempFailures/$c.Total) *100,1) }else{0}) %</div></a> <a class=“card” href=“#s-pf” onclick=“openSection ('d-pf') ” style=“border-left-color:#721c24;text-decoration:none“><div class=”value“ style=”color:#721c24“>$ ($c.PermFailures.ToString (”N0“) ) </div><div class=”label“>不支援><6 /div><div class=”pct“>$ (如果 ($c.Total -gt 0) {[math]::round ( ($c.PermFailures/$c.Total) *100,1) }else{0}) %</div></a><3 </div>
<!-- 部署速度 & 證書到期 --> <div id=“s-velocity” style=“display:grid;grid-template-columns:1fr 1fr;空隙:20像素;邊距:15px 0“> <div class=“section” style=“margin:0”> <h2>📅展開速度</h2> <div class=“section-body open”> <div style=“font-size:2.5em;字體粗細:700;color:#28a745“>$ ($c.Updated.ToString (”N0“) ) </div> <div style=“color:#666”>裝置已更新出 $ ($c.Total.ToString (“N0”) ) </div> <div style=“margin:10px 0;背景:#e8eaf6;高度:20像素;邊界半徑:10px;overflow:hidden“><div style=”background:#28a745;身高:100%;寬度:$ ($stats。PercentCertUpdated) %;border-radius:10px“></div></div> <div style=“font-size:.8em;顏色:#888“>$ ($stats。PercentCertUpdated) % 完成</div> <div style=“margin-top:10px;填充:10px;背景:#f8f9fa;邊界半徑:8px;字體大小:.85em“> <div><強>剩餘:</strong> $ ($stNotUptodate.ToString (“N0”) ) 裝置需要動作</div> <div><強>阻擋:</strong> $ ($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) 裝置 (錯誤 + 永久 + 任務已停用) </div (1 <div><強 (5 安全部署:</strong> $ ($stSafeList.ToString (“N0”) ) 裝置 (與成功) </div (9 $velocityHtml </div> </div> </div> <div class=“section” style=“margin:0;邊框左側:4像素實心 #dc3545」> <h2 style=“color:#dc3545”>⚠ 證書到期倒數計時</h2> <div class=“section-body open”> <div style=“display:flex;空隙:15px;margin-top:10px“> <div style=“text-align:center;填充:15像素;邊界半徑:8px;最小寬度:120px;背景:線性梯度 (135度,#fff5f5,#ffe0e0) ;邊框:2px 實心 #dc3545;彈性:1吋 > <div style=“font-size:.65em;顏色:#721c24;text-transform:大寫;font-weight:bold“>⚠ 第一個過期的</div> ><4 div style=“font-size:.85em;字體粗細:粗體;顏色:#dc3545;邊距:3px 0“><5 KEK CA 2011</div> ><8 div id=“daysKek” style=“font-size:2.5em;字體粗細:700;顏色:#dc3545;線高:1“><9 $daysToKek</div> ><2 div style=“font-size:.8em;顏色:#721c24“><3 天 (2026年6月24日) ><4 /div> ><6 /div> ><8 div style=“text-align:center;填充:15像素;邊界半徑:8px;最小寬度:120px;背景:線性漸 (135度,#fffef5,#fff3cd) ;邊框:2px 實心 #ffc107;彈性:1吋 ><9 <div style=“font-size:.65em;顏色:#856404;text-transform:大寫;font-weight:bold“>UEFI CA 2011</div> <div id=“daysUefi” style=“font-size:2.2em;字體粗細:700;顏色:#856404;線高:1;margin:5px 0“>$daysToUefi</div> <div style=“font-size:.8em;顏色:#856404“>天 (2026年6月27日) </div> </div> <div style=“text-align:center;填充:15像素;邊界半徑:8px;最小寬度:120px;背景:線性梯度 (135度,#f0f8ff,#d4edff) ;邊框:2px 實心 #0078d4;彈性:1吋 > <div style=“font-size:.65em;顏色:#0078d4;text-transform:大寫;font-weight:bold“>Windows PCA</div> <div id=“daysPca” style=“font-size:2.2em;字體粗細:700;顏色:#0078d4;線高:1;邊距:5px 0“>$daysToPca><2 /div><3 <div style=“font-size:.8em;顏色:#0078d4“>天 (2026年10月19日) </div><7 </div><9 </div><1 <div style=“margin-top:15px;填充:10px;背景:#f8d7da;邊界半徑:8px;字體大小:.85em;邊框左側:4像素實心 #dc3545 > <強>⚠ critical:</strong> 所有裝置必須在憑證到期前更新。 未在截止日前更新的裝置,在過期後無法套用未來的安全更新給 Boot Manager 和安全開機。</div> </div> </div> </div>
<!-- 排行榜 --> <div class =「charts」> <div class=“chart-box”><h3>部署狀態</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>
$ (如果 ($historyData。計數 -ge 1) { 「<!-- 歷史趨勢圖——> <div 類別=「section」> <h2 onclick='“toggle ('d-trend') '”>📈更新進度 <a class='top-link' href='#'>↑ Top</a></h2> <div id='d-trend' class='section-body open'> <canvas id='trendChart' 高度='120'></canvas> <div style='font-size:.75em;顏色:#888;margin-top:5px'>實線 = 實際數據$ (如果 ($historyData。計數 -ge 2) { “ |虛線 = 預測 (指數倍增:2→4→8→16...每波形裝置數) “ } 否則 { ” |明天再跑一次彙整,可以看到趨勢線和預測」}) </div> </div> </div>” })
<!-- CSV 下載次數 --> <div class=「section」> <h2 onclick=“toggle ('dl-csv') ”>📥下載完整資料 (CSV,用於 Excel) <a class=“top-link” href=“#”>Top</a></h2><2 <div id=“dl-csv” class=“section-body open” style=“display:flex;彈性包裹:包裹;Gap:5Px“> <a href=“SecureBoot_not_updated_$timestamp.csv” style=“display:inline-block;背景:#dc3545;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>未更新 ($ ($stNotUptodate.ToString (”N0“) ) ) </a><8 <a href=“SecureBoot_errors_$timestamp.csv” style=“display:inline-block;背景:#dc3545;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>錯誤 ($ ($c.WithErrors.ToString (”N0“) ) ) </a> <a href=“SecureBoot_action_required_$timestamp.csv” style=“display:inline-block;背景:#fd7e14;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>Action Needs ($ ($c.ActionReq.ToString (”N0“) ) ) </a> <a href=“SecureBoot_known_issues_$timestamp.csv” style=“display:inline-block;背景:#dc3545;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>已知問題 ($ ($c.WithKnownIssues.ToString (”N0“) ) ) </a> <a href=“SecureBoot_task_disabled_$timestamp.csv” style=“display:inline-block;背景:#dc3545;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>任務已停用 ($ ($c.TaskDisabled.ToString (”N0“) ) ) </a> <a href=“SecureBoot_updated_devices_$timestamp.csv” style=“display:inline-block;背景:#28a745;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>更新 ($ ($c.Updated.ToString (”N0“) ) ) </a> <a href=“SecureBoot_Summary_$timestamp.csv” style=“display:inline-block;背景:#6c757d;顏色:#fff;填充:6px 14px;邊界半徑:5px;文字裝飾:無;font-size:.8em“>摘要</a> <div style=“width:100%;字體大小:.75em;顏色:#888;margin-top:5px“>CSV 檔案在 Excel 中開啟。 當主機託管於網頁伺服器時可取得。</div> </div> </div>
<!-- 製造商分析 --> <div class=「section」> <h2 onclick=“toggle ('mfr') ”>按製造商分類 <a 類別=“top-link” href=“#”>頂部</a></h2><1 <div id=“mfr” class=“section-body open”> <表><thead><tr><><1 製造商><2><><5 總計><6><><9 更新為><0 /th><><3 高信心><4 /th><7><需要採取行動><8></th /tr></thead><3 <tbody><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- 裝置區段 (前 200 個 inline + CSV 下載) --> <div class=“section” id=“s-err”> <h2 onclick=“toggle ('d-err') ”>🔴有錯誤的裝置 ($ ($c.WithErrors.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂端</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”>🔴已知問題 ($ ($c.WithKnownIssues.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂尖</a></h2> <div id=“d-ki” class=“section-body”>$tblKI</div> </div> <div class=“section” id=“s-kek”> <h2 onclick=“toggle ('d-kek') ”>🟠失蹤的 KEK - 事件 1803 ($ ($c.WithMissingKEK.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂尖</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”>🟠所需行動 ($ ($c.ActionReq.ToString (“N0”) ) ) (1 a class=“top-link” href=“#”>↑ 頂端><4 /a></h2><7 (8 div id=“d-ar” 類別=“section-body” (9 $tblActionReq (0 /div (1 </div (3 <div class=“section” id=“s-uo”> <h2 onclick=“toggle ('d-uo') ” style=“color:#17a2b8”>🔵觀察 ($ ($c.UnderObs.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂端</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”>🔴未更新 ($ ($stNotUptodate.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂尖</a></h2> <div id=“d-nu” 類別=“section-body”>$tblNotUpd</div> </div> >↑ 0 div class=“section” id=“s-td”>↑ 1 >↑ 2 h2 onclick=“toggle ('d-td') ” style=“color:#dc3545”>🔴任務被停用 ($ ($c.TaskDisabled.ToString (“N0”) ) ) >↑ 5 a class=“top-link” href=“#”>↑ 頂端</a></h2><1 <div id=“d-td” class=“section-body”>$tblTaskDis><4 /div><5 </div><7 <div class=“section” id=“s-tf”> <h2 onclick=“toggle ('d-tf') ” style=“color:#dc3545”>🔴暫時性故障 ($ ($c.TempFailures.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂端</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”>🔴永久失敗 / 不支援 ($ ($c.PermFailures.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂尖</a></h2> <div id=“d-pf” class=“section-body”>$tblPerm</div> (2 /div> (4 div class=“section” id=“s-upd-pend” (5 (6 h2 onclick=“toggle ('d-upd-pend') ” style=“color:#6f42c1”>⏳ Update Pending ($ ($c.UpdatePending.ToString (“N0”) ) ) - Policy/WinCS 已套用,等待更新 (9 a class=“top-link” href=“#”>↑ Top</a></h2> <div id=“d-upd-pend” class=“section-body” (2 p style=“color:#666;margin-bottom:10px“>裝置 AvailableUpdatesPolicy 或 WinCS 鍵被套用,但 UEFICA2023Status 仍為 NotStarted、InProgress 或 null。</p (1 $tblUpdatePending (2 /div (3 </div (5 <div class=“section” id=“s-rip”> <h2 onclick=“toggle ('d-rip') ” style=“color:#17a2b8”>🔵Rollout 進行中 ($ ($c.RolloutInProgress.ToString (“N0”) ) ) <a class=“top-link” href=“#” (5 頂部</a></h2> <div id=“d-rip” 類別=“section-body”>$tblRolloutIP</div> </div> <div class=“section” id=“s-sboff”> <h2 onclick=“toggle ('d-sboff') ” style=“color:#6c757d” (7 SecureBoot OFF ($ ($c.SBOff.ToString (“N0”) ) ) - 超出範圍 <a class=“top-link” href=“#”>↑ 頂端</a (2 /h2> <div id=“d-sboff” 類別=“section-body”>$tblSBOff</div> </div> <div class=“section” id=“s-upd”> <h2 onclick=“toggle ('d-upd') ” style=“color:#28a745”>🟢更新裝置 ($ ($c.Updated.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ 頂尖</a (8 /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”>🔄更新 - 需要重啟 ($ ($c.NeedsReboot.ToString (“N0”) ) ) <a class=“top-link” href=“#”>↑ Top</a></h2> <div id=“d-nrb” class=“section-body”>$tblNeedsReboot</div> </div>
<div class=“footer”>Secure Boot Certificate Rollout Dashboard |產生了 ($stats 美元。報導生成於) |串流模式 |峰值記憶體:${stPeakMemMB} MB</div> </div><!-- /container -->
<劇本> function toggle (id) {var e=document.getElementById (id) ;e.classList.toggle ('open') } function openSection (id) {var e=document.getElementById (id id) ;如果 (e&&!e.classList.contains ('open') ) {e.classList.add ('open') }}} 新圖表 (document.getElementById ('deployChart') ,{type:'doughnut',data:{labels:[''更新','更新待處理','高度信心','觀察中','需行動','臨時工作'。 停頓了,」 不支援','SecureBoot OFF','有錯誤'],datasets:[{data:[$ ($c.Updated) ,$ ($c.UpdatePending) ,$ ($c.HighConf) ,$ ($c.UnderObs) ,$ ($c.ActionReq) ,$ ($c.暫時暫停) ,$ ($c.不支持) ,$ ($c.SBOff) ,$ ($c.WithErrors) ],backgroundColor:[''#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24','#adb5bd','#dc3545']}},選項:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}) ; 新圖表 (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 Needed',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'}}}) ; 歷史趨勢圖表 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 = 陣列 (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] = actualNotUpd[histLen-1]; projNotUpdLine = projNotUpdLine.concat (projNotUpdData) ; } VAR 資料集 = [ {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba (40,167,69,0.1) ',fill:true,tension:0.3,borderWidth:2}, {label:'未更新',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} ]; 如果 (projLen > 0) { datasets.push ({label:'投影更新 (2倍加倍) ',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}) ; datasets.push ({label:'投影未更新',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}) ; } 新圖表 (文件。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'}) ; } 動態倒數計時 (函式 () {var t=新日期 () ,k=新日期 ('2026-06-24') ,u=新日期 ('2026-06-27') ,p=新日期 ('2026-10-19') ;var dk=document.getElementById ('daysKek') ,du=document.getElementById ('daysUefi') ,dp=document.getElementById ('daysPca') ;若 (dk) dk.textContent=Math.max (0,Math.ceil ( (k-t) /864e5) ) ;如果 (du) du.textContent=Math.max (0,Math.ceil ( (u-t) /864e5) ) ;如果 (dp) dp.textContent=Math.max (0,Math.ceil ( (p-t) /864e5) ) }) () ; </劇本> </身體> </html> 「@ [System.IO.File]::WriteAllText ($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::新 ($false) ) # 務必保持穩定的「最新」副本,這樣管理員就不用追蹤時間戳記 $latestPath = Join-Path $OutputPath「SecureBoot_Dashboard_Latest.html」 Copy-Item $htmlPath $latestPath -原力 $stTotal = $streamSw.經過.總秒數 # 增量模式的存檔清單 (下一次執行時快速偵測無變更) 若 ($IncrementalMode -或$StreamingMode) { $stManifestDir = Join-Path $OutputPath「.cache」 如果 (-not (測試路徑 $stManifestDir) ) { New-Item -ItemType Directory -Path $stManifestDir -Force | Out-Null } $stManifestPath = Join-Path $stManifestDir「StreamingManifest.json」 $stNewManifest = @{} Write-Host「儲存檔案清單以支援增量模式......」-ForegroundColor Gray ($jf$jsonFiles) { $stNewManifest[$jf。FullName.ToLowerInvariant () ] = @{ LastWriteTimeUtc = $jf。LastWriteTimeUtc.ToString (“o”) 尺寸 = $jf。長度 } } Save-FileManifest -顯現$stNewManifest -路徑$stManifestPath Write-Host「已儲存清單,金額為 $ ($stNewManifest.Count) 檔案」-前景色 深灰色 } # 保留清理 # Orchestrator 可重用資料夾 (Aggregation_Current) :只保留最新執行 (1) # 管理員手冊執行 / 其他資料夾:保留最近7次執行 # 摘要 CSV 從不被刪除——它們非常小 (~1 KB) ,是趨勢歷史的備份來源 $outputLeaf = Split-Path $OutputPath -葉片 $retentionCount = 若 ($outputLeaf -eq 'Aggregation_Current') { 1 } 否則 { 7 } # 檔案前綴安全清理 (短暫的每次執行快照) $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_」 ) # 只從可清理的檔案中找出所有獨特時間戳 $cleanableFiles = Get-ChildItem $OutputPath -檔案 -EA 靜默繼續 | Where-Object { $f = $_。名稱; ($cleanupPrefixes |Where-Object { $f.以 ($_) }) 。計數 -gt 0 } $allTimestamps = @ ($cleanableFiles |ForEach-Object { 如果 ($_。姓名匹配 ' (\d{8}-\d{6}) ') { $Matches[1] } } |Sort-Object -唯一 -降) 如果 ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps |Select-Object -跳過$retentionCount $removedFiles = 0;$freedBytes = 0 foreach ($oldTs in $oldTimestamps) { 每個 ($prefix 在$cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter “${prefix}${oldTs}*” -EA SilentlyContinue foreach ($f in $oldFiles) { $freedBytes += $f.長度 Remove-Item $f.全名 -強制 -EA 靜默繼續 $removedFiles++ } } } $freedMB = [數學]::輪數 ($freedBytes / 1MB,1) Write-Host「保留清理:移除了 $ ($oldTimestamps.Count) 舊跑道中的 $removedFiles 檔案,釋放了 ${freedMB} MB (保留最後$retentionCount + 所有摘要/未更新的 CSV) 」 -ForegroundColor DarkGray } Write-Host 「'n$ (」=“ * 60) ” -前景 青色 Write-Host「串流聚合完成」-前景綠色 Write-Host (“=” * 60) -前景色彩 青色 Write-Host 「總裝置數:$ ($c.Total.ToString (「N0“) ) ” -前景白色 Write-Host 「未更新:$ ($stNotUptodate.ToString (“N0”) ) ($ ($stats。PercentNotUptodate) %) “ -ForegroundColor $ (如果 ($stNotUptodate -gt 0) { ”Yellow“ } 否則 { ”Green“ }) Write-Host “ 更新:$ ($c.Updated.ToString (”N0“) ) ($ ($stats。PercentCertUpdated) %) “ -前景綠色 Write-Host “帶錯誤:$ ($c.WithErrors.ToString (”N0“) ) ” -ForegroundColor $ (如果 ($c.WithErrors -gt 0) { “Red” } 否則 { “Green” }) Write-Host 「峰值記憶體:${stPeakMemMB} MB」 -前景色青色 Write-Host 「時間:$ ([數學]::輪次 ($stTotal/60,1) ) 分鐘」 -前景白色 Write-Host 「儀表板:$htmlPath」-前景白色 返回 [PSCustomObject]$stats } #endregion 串流模式 } 否則 { Write-Error「輸入路徑未找到:$InputPath」 出口1 }