Salin dan tempelkan contoh skrip ini dan ubah sesuai kebutuhan untuk lingkungan Anda:
<# . SINOPSIS Menggabungkan data JSON status Boot Aman dari beberapa perangkat ke dalam laporan ringkasan.
. DESKRIPSI Membaca file JSON status Boot Aman yang dikumpulkan dan menghasilkan: - Dasbor HTML dengan bagan dan pemfilteran - Ringkasan menurut ConfidenceLevel - Analisis bucket perangkat unik untuk strategi pengujian Mendukung: - File per mesin: HOSTNAME_latest.json (disarankan) - File JSON tunggal Secara otomatis deduplikat oleh HostName, mempertahankan CollectionTime terbaru. Secara default, hanya menyertakan perangkat dengan kepercayaan diri "Action Req" atau "High" untuk fokus pada wadah yang dapat ditindak lanjuti. Gunakan -IncludeAllConfidenceLevels untuk menimpa.
. PARAMETER InputPath Jalur ke file JSON: - Folder: Membaca semua *_latest.json file (atau *.json jika tidak ada file _latest) - File: Membaca satu file JSON
. PARAMETER OutputPath Jalur untuk laporan yang dihasilkan (default: .\SecureBootReports)
. CONTOH # Agregat dari folder file per mesin (disarankan) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Baca: \\contoso\SecureBootLogs$\*_latest.json
. CONTOH # Lokasi output kustom .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. CONTOH # Sertakan hanya Req Tindakan dan Kepercayaan diri tinggi (perilaku default) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Tidak termasuk: Observasi, Dijeda, Tidak Didukung
. CONTOH # Sertakan semua tingkat kepercayaan (mengesampingkan filter) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. CONTOH # Filter tingkat kepercayaan kustom .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. CONTOH # SKALA PERUSAHAAN: Mode penambahan - hanya proses file yang diubah (berjalan cepat selanjutnya) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Jalankan pertama: Muat penuh ~2 jam untuk perangkat 500K # Selanjutnya berjalan: Detik jika tidak ada perubahan, menit untuk delta
. CONTOH # Lewati HTML jika tidak ada yang berubah (tercepat untuk pemantauan) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Jika tidak ada file yang berubah sejak dijalankan terakhir: ~5 detik
. CONTOH # Mode ringkasan saja - lewati tabel perangkat besar (1-2 menit vs 20+ menit) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Menghasilkan CSV tetapi melewati dasbor HTML dengan tabel perangkat penuh
. CATATAN Pasangkan dengan Detect-SecureBootCertUpdateStatus.ps1 untuk penyebaran perusahaan.Lihat GPO-DEPLOYMENT-GUIDE.md untuk panduan penggunaan penuh. Perilaku default tidak termasuk perangkat Observasi, Dijeda, dan Tidak Didukung untuk memfokuskan pelaporan hanya pada wadah perangkat yang dapat ditindaklanjuti.#>
param( [Parameter(Wajib = $true)] [string]$InputPath, [Parameter(Wajib = $false)] [string]$OutputPath = ".\SecureBootReports", [Parameter(Wajib = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parameter(Wajib = $false)] [string]$RolloutStatePath, # Jalur ke RolloutState.json untuk mengidentifikasi perangkat InProgress [Parameter(Wajib = $false)] [string]$RolloutSummaryPath, # Jalur ke SecureBootRolloutSummary.json dari Orkestrator (berisi data proyeksi) [Parameter(Wajib = $false)] [string[]$IncludeConfidenceLevels = @("Tindakan Diperlukan", "Kepercayaan Diri Tinggi"), # Hanya menyertakan tingkat kepercayaan ini (default: wadah yang dapat ditindaklanjuti saja) [Parameter(Wajib = $false)] [switch]$IncludeAllConfidenceLevels, # Override filter untuk menyertakan semua tingkat kepercayaan [Parameter(Wajib = $false)] [sakelar]$SkipHistoryTracking, [Parameter(Wajib = $false)] [sakelar]$IncrementalMode, # Aktifkan pemrosesan delta - hanya muat file yang diubah sejak dijalankan terakhir [Parameter(Wajib = $false)] [string]$CachePath, # Jalur ke direktori cache (default: OutputPath\.cache) [Parameter(Wajib = $false)] [int]$ParallelThreads = 8, # Jumlah utas paralel untuk pemuatan file (PS7+) [Parameter(Wajib = $false)] [sakelar]$ForceFullRefresh, # Paksa muat ulang penuh bahkan dalam mode penambahan [Parameter(Wajib = $false)] [switch]$SkipReportIfUnchanged, # Lewati pembuatan HTML/CSV jika tidak ada file yang berubah (hanya statistik output) [Parameter(Wajib = $false)] [sakelar]$SummaryOnly, # Hasilkan statistik ringkasan saja (tidak ada tabel perangkat besar) - jauh lebih cepat [Parameter(Wajib = $false)] [switch]$StreamingMode # Mode efisien memori: potongan proses, menulis CSV secara bertahap, hanya menyimpan ringkasan dalam memori )
# Peningkatan otomatis ke PowerShell 7 jika tersedia (6x lebih cepat untuk kumpulan data besar) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ($pwshPath) { Write-Host "PowerShell $($PSVersionTable.PSVersion) terdeteksi - meluncurkan ulang dengan PowerShell 7 untuk pemrosesan yang lebih cepat..." -ForegroundColor Yellow # Susun ulang daftar argumen dari parameter terikat $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key di $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] if ($val -is [switch]) { jika ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [array]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } lain { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs keluar dari $LASTEXITCODE } }
$ErrorActionPreference = "Lanjutkan" $timestamp = Get-Date -Format "yyyyMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Sampel Penyebaran dan Pemantauan"
# Catatan: Skrip ini tidak memiliki dependensi pada skrip lain. # Untuk peralatan lengkap, unduh dari: $DownloadUrl -> $DownloadSubPage
Penyiapan #region Write-Host "=" * 60 -ForegroundColor Sian Write-Host "Agregasi Data Boot Aman" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Sian
# Buat direktori output if (-not (Test-Path $OutputPath)) { New-Item -Direktori ItemType -Path $OutputPath -Force | Out-Null }
# Memuat data - mendukung format CSV (warisan) dan JSON (asli) Write-Host "'n Memuat data dari: $InputPath" -ForegroundColor Yellow
# Fungsi helper untuk menormalkan objek perangkat (menangani perbedaan nama bidang) fungsi Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON menggunakan Hostname, CSV menggunakan HostName) jika ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Nama host -Force } # Handle Confidence vs ConfidenceLevel (JSON menggunakan Confidence, CSV uses ConfidenceLevel) # ConfidenceLevel adalah nama bidang resmi - map Confidence to it jika ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Kepercayaan diri -Force } # Lacak status pembaruan melalui Event1808Count OR UEFICA2023Status="Updated" # Ini memungkinkan pelacakan berapa banyak perangkat dalam setiap wadah kepercayaan telah diperbarui $event 1808 = 0 jika ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false jika ($device. PSObject.Properties['UEFICA2023Status'] -dan $device. UEFICA2023Status -eq "Diperbarui") { $uefiCaUpdated = $true } if ($event 1808 -gt 0 -or $uefiCaUpdated) { # Tandai sebagai diperbarui untuk dasbor/logika peluncuran - tapi JANGAN mengesampingkan ConfidenceLevel $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } lain { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # ConfidenceKlasifikasi tingkat tinggi: # - "Kepercayaan Diri Tinggi", "Di bawah Observasi...", "Dijeda Sementara...", "Tidak Didukung..." = gunakan sebagaimana adanya # - Segala sesuatu yang lain (null, kosong, "UpdateType:...", "Unknown", "N/A") = jatuh ke Tindakan Diperlukan dalam counter # Tidak diperlukan normalisasi — counter streaming cabang lain menanganinya } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON menggunakan OEM*, warisan menggunakan WMI_*) jika ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force } # Tangani OEMModelNumber vs WMI_Model jika ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force } # Tangani FirmwareVersion vs BIOSDescription jika ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } $device pengembalian }
#region Pemrosesan Penambahan / Manajemen Singgahan # Jalur cache penyiapan if (-not $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Fungsi manajemen singgahan fungsi Get-FileManifest { param([string]$Path) if (Test-Path $Path) { coba { $json = Get-Content $Path -Raw | ConvertFrom-Json # Konversi PSObject ke hashtable (kompatibel PS5.1 - PS7 memiliki -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Nama] = $_. Nilai } $ht pengembalian } tangkap { return @{} } } return @{} }
fungsi Save-FileManifest { param([hashtable]$Manifest, [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -Direktori ItemType -Path $dir -Force | Out-Null } $Manifest | ConvertTo-Json -Kedalaman 3 -Kompresi | Set-Content $Path -Force }
fungsi Get-DeviceCache { param([string]$Path) if (Test-Path $Path) { coba { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host " Cache perangkat yang dimuat: perangkat $($cacheData.Count) " -ForegroundColor DarkGray $cacheData pengembalian } tangkap { Write-Host " Cache rusak, akan dibangun ulang" -ForegroundColor Yellow return @() } } return @() }
fungsi Save-DeviceCache { param($Devices, [string]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -Direktori ItemType -Path $dir -Force | Out-Null } # Konversi ke array dan simpan $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Kedalaman 10 -Kompresi | Set-Content $Path -Force Write-Host " Cache perangkat yang disimpan: perangkat $($deviceArray.Count) " -ForegroundColor DarkGray }
fungsi Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Menyusun pencarian yang tidak peka huruf besar kecil dari manifes (dinormalkan ke huruf kecil) $manifestLookup = @{} foreach ($mk di $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file di $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalisasi jalur ke huruf kecil $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Ukuran = $file. Panjang } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] jika ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Ukuran -eq $file. Panjang) { [batal]$unchanged. Tambahkan($file) Lanjutkan } } [batal]$changed. Add($file) } return @{ Diubah = $changed Tidak berubah = $unchanged NewManifest = $newManifest } }
# Pemuatan file paralel ultra cepat menggunakan pemrosesan kumpulan fungsi Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = $Files. Menghitung # Gunakan kumpulan file ~1000 masing-masing untuk kontrol memori yang lebih baik $batchSize = [matematika]::Min(1000, [matematika]::Plafon($totalFiles / [matematika]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
for ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [matematika]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Add($batch) } Write-Host " ($($batches. Hitung) kumpulan ~$batchSize file masing-masing)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # Periksa apakah PowerShell 7+ paralel tersedia $canParallel = $PSVersionTable.PSVersion.Major -ge 7 if ($canParallel -and $Threads -gt 1) { # PS7+: Kumpulan proses secara paralel $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file di $batchFiles) { coba { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } tangkap { } } $batchResults.ToArray() } foreach ($batch dalam $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } lain { # PS5.1 mundur: Pemrosesan berurutan (masih cepat untuk file <10K) foreach ($file dalam $Files) { coba { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } tangkap { } } } return $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # File JSON tunggal if ($InputPath -like "*.json") { $jsonContent = Get-Content -Jalur $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "Memuat rekaman $($allDevices.Count) dari file" } lain { Write-Error "Hanya format JSON yang didukung. File harus memiliki ekstensi .json." keluar 1 } } elseif (Test-Path $InputPath -PathType Container) { # Folder - hanya JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Nama -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Pilih *_latest.json file jika ada (mode per-mesin) $latestJson = $jsonFiles | Where-Object { $_. Nama -like "*_latest.json" } if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count if ($totalFiles -eq 0) { Write-Error "Tidak ada file JSON yang ditemukan di: $InputPath" keluar 1 } Write-Host "Ditemukan $totalFiles file JSON" -ForegroundColor Gray # Fungsi helper untuk mencocokkan tingkat kepercayaan (menangani bentuk pendek dan penuh) # Didefinisikan lebih awal sehingga jalur StreamingMode dan normal dapat menggunakannya fungsi Test-ConfidenceLevel { param([string]$Value, [string]$Match) if ([string]:::IsNullOrEmpty($Value)) { return $false } sakelar ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Action Diperlukan*" -or $Value -eq "Action Diperlukan") } "TemporarilyPaused" { return $Value -like "Temporarily Paused*" } "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") } default { return $false } } } mode streaming #region - Pemrosesan memori efisien untuk kumpulan data besar # Selalu gunakan StreamingMode untuk pemrosesan yang efisien memori dan dasbor gaya baru if (-not $StreamingMode) { Write-Host "Auto-enabling StreamingMode (new-style dashboard)" -ForegroundColor Yellow $StreamingMode = $true if (-not $IncrementalMode) { $IncrementalMode = $true } } # Ketika -StreamingMode diaktifkan, proses file dalam bagian yang hanya menyimpan penghitung dalam memori.# Data tingkat perangkat ditulis ke file JSON per-chunk untuk pemuatan sesuai permintaan di dasbor.# Penggunaan memori: ~1,5 GB terlepas dari ukuran kumpulan data (vs 10-20 GB tanpa streaming).if ($StreamingMode) { Write-Host "MODE STREAMING diaktifkan - pemrosesan yang efisien memori" -ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTAL CHECK: Jika tidak ada file yang berubah sejak terakhir dijalankan, lewati pemrosesan seluruhnya if ($IncrementalMode -and -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" if (Test-Path $stManifestPath) { Write-Host "Memeriksa perubahan sejak streaming terakhir dijalankan..." -ForegroundColor Cyan $stOldManifest = Get-FileManifest -Path $stManifestPath if ($stOldManifest.Count -gt 0) { $stChanged = $false # Pemeriksaan cepat: jumlah file yang sama? if ($stOldManifest.Count -eq $totalFiles) { # Periksa 100 file TERBARU (diurutkan menurut LastWriteTime turun) # Jika ada file yang diubah, akan memiliki stempel waktu terbaru dan muncul terlebih dahulu $sampleSize = [matematika]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Turun | Select-Object -$sampleSize pertama foreach ($sf di $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Istirahat } # Bandingkan cap waktu - singgahan mungkin DateTime atau string setelah roundtrip JSON $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc coba { # Jika singgahan sudah DateTime (ConvertFrom-Json auto-converts), gunakan secara langsung if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } lain { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Istirahat } } tangkap { $stChanged = $true Istirahat } } } lain { $stChanged = $true } if (-not $stChanged) { # Periksa apakah ada file output $stSummaryExists = Get-ChildItem ($OutputPath Jalur Bergabung "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -1 Pertama $stDashExists = Get-ChildItem ($OutputPath Jalur Gabungan "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -1 Pertama if ($stSummaryExists -and $stDashExists) { Write-Host " Tidak ada perubahan yang terdeteksi ($totalFiles file tidak berubah) - melewati pemrosesan" -ForegroundColor Green Write-Host " Dasbor terakhir: $($stDashExists.FullName)" -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv Write-Host " Perangkat: $($cachedStats.TotalDevices) | Diperbarui: $($cachedStats.Diperbarui) | Kesalahan: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host " Selesai dalam $([matematika]:Round($streamSw.Elapsed.TotalSeconds, 1)s (tidak diperlukan pemrosesan)" -ForegroundColor Green $cachedStats pengembalian } } lain { # DELTA PATCH: Temukan persis file mana yang diubah Write-Host " Perubahan terdeteksi - mengidentifikasi file yang diubah..." -ForegroundColor Yellow $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf di $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } lain { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc coba { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([matematika]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } tangkap { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [matematika]::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Diubah: $($changedFiles.Count) | Baru: $($newFiles.Count) | Total: $totalChanged ($changePct%)" -ForegroundColor Yellow if ($totalChanged -gt 0 -and $changePct -lt 10) { # DELTA PATCH MODE: <10% berubah, patch data yang sudah ada Write-Host " Mode patch Delta ($changePct% < 10%) - patching file $totalChanged..." -ForegroundColor Green $dataDir = Join-Path $OutputPath "data" # Muat catatan perangkat yang diubah/baru $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df di $allDeltaFiles) { coba { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData jika ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } tangkap { } } Write-Host " Loaded $($deltaDevices.Count) mengubah catatan perangkat" -ForegroundColor Gray # Untuk setiap kategori JSON: hapus entri lama untuk nama host yang diubah, tambahkan entri baru $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 di $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } kudeta ($cat di $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" if (Test-Path $catPath) { coba { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Hapus entri lama untuk nama host yang diubah $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Klasifikasi ulang setiap perangkat yang diubah ke dalam kategori # (akan ditambahkan di bawah ini setelah klasifikasi) $catData | ConvertTo-Json -Kedalaman 5 | Set-Content $catPath -Pengodean UTF8 } tangkap { } } } # Klasifikasi setiap perangkat yang diubah dan tambahkan ke file kategori yang tepat foreach ($dev dalam $deltaDevices.Values) { $slim = [dipesan]@{ HostName = $dev. HostName WMI_Manufacturer = jika ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } lagi { "" } WMI_Model = jika ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } lagi { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } else { "" } ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. Diupdated 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 = jika ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) dan $dev. UEFICA2023Error -ne "0" -dan $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -atau $dev. SecureBootTaskStatus -eq 'Disabled' -or $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = jika ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } else { 0 } $e 1808 = jika ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } else { 0 } $e 1803 = jika ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -atau $dev. MissingKEK -eq $true) $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = jika ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } lagi { "" } # Tambahkan ke file kategori yang cocok $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 di $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 -Kedalaman 5 | Set-Content $tgtPath -Pengodean UTF8 } } } # Regenerasi CSV dari JSON yang ditambal Write-Host " Meregenerasi CSV dari data yang ditambal..." -ForegroundColor Gray $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat di $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (Test-Path $catJsonPath) { coba { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8 } } tangkap { } } } # Recount stats from the patched JSON files Write-Host " Menghitung ulang ringkasan dari data yang ditambal..." -Warna Latar Depan Abu-abu $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") } $pTotal = 0; $pUpdated = 0; $pErrors = 0; $pKI = 0; $pKEK = 0 $pTaskDis = 0; $pTempFail = 0; $pPermFail = 0; $pActionReq = 0; $pSBOff = 0; $pRIP = 0 foreach ($cat di $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } tangkapan { } } sakelar ($cat) { "updated_devices" { $pUpdated = $cnt } "kesalahan" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # terhitung "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). Menghitung $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Patch Delta lengkap: perangkat $totalChanged diperbarui" -ForegroundColor Green Write-Host " Total: $pTotal | Diperbarui: $pUpdated | NotUpdated: $pNotUpdated | Kesalahan: $pErrors" -ForegroundColor White # Perbarui manifes $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf di $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Ukuran = $jf. Panjang } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Selesai dalam $([matematika]:Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green # Jatuh melalui ke streaming penuh reprocess untuk meregenerasi dasbor HTML # File data sudah ditambal, jadi ini memastikan dasbor tetap terkini Write-Host " Meregenerasi dasbor dari data yang ditambal..." -ForegroundColor Yellow } lain { Write-Host " $changePct% file berubah (>= 10%) - diperlukan pengolah ulang streaming penuh" -ForegroundColor Yellow } } } } } # Buat subdirektori data untuk file JSON perangkat sesuai permintaan $dataDir = Join-Path $OutputPath "data" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Deduplikasi melalui HashSet (O(1) per pencarian, ~50MB untuk nama host 600K) $seenHostnames = [System.Collections.Generic.HashSet[string]]:new([System.StringComparer]::OrdinalIgnoreCase) # Penghitung ringkasan ringan (menggantikan $allDevices + $uniqueDevices dalam memori) $c = @{ Total = 0; SBEnabled = 0; SBOff = 0 Diperbarui = 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 } # Pelacakan wadah untuk AtRisk/SafeList (set ringan) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # File data perangkat mode batch: akumulasi per-potong, siram pada batas-batas bagian $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 di $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Catatan perangkat ramping untuk output JSON (hanya bidang penting, ~200 byte vs ~2KB penuh) fungsi 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 { "" } Diupdated = $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 } } } # Flush batch ke file JSON (mode tambahan) fungsi 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 di $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) } # LOOP STREAMING UTAMA $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 } $stTotalChunks = [matematika]::Plafon($totalFiles / $stChunkSize) $stPeakMemMB = 0 if ($stTotalChunks -gt 1) { Write-Host "Memproses file $totalFiles dalam bagian $stTotalChunks $stChunkSize (streaming, utas $ParallelThreads):" -ForegroundColor Cyan } lain { Write-Host "Memproses file $totalFiles (streaming, utas $ParallelThreads):" -ForegroundColor Sian } untuk ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [matematika]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] if ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) file): " -NoNewline -ForegroundColor Gray } lain { Write-Host " Memuat file $($cFiles.Count): " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads # Daftar kumpulan per-bongkahan $cBatches = @{} foreach ($df dalam $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]::new() } $cNew = 0; $cDupe = 0 foreach ($raw di $rawDevices) { if (-not $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. HostName if (-not $hostname) { continue } if ($seenHostnames.Contains($hostname)) { $cDupe++; lanjutkan } [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 = jika ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($device. UEFICA2023Error) -dan "$($device. UEFICA2023Error)" -ne "0" -dan "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -or "$($device. SecureBootTaskStatus)" -eq 'Disabled' -or "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = jika ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } else { "" } $e 1808 = jika ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } else { 0 } $e 1801 = jika ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } else { 0 } $e 1803 = jika ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } else { 0 } $mKEK = ($e 1803 -gt 0 -atau $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True") $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -or (-not [string]::IsNullOrEmpty($device. KnownIssueId))) $rStat = jika ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } lagi { "" } $mfr = jika ($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 { "" } # Bendera Tertunda Pembaruan Prakomputasi (kebijakan/WinCS diterapkan, status belum Diperbarui, SB AKTIF, tugas tidak dinonaktifkan) $uefiStatus = jika ($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'] -dan $device. WinCSKeyApplied -eq $true) $statusPending = ([string]::IsNullOrEmpty($uefiStatus) -atau $uefiStatus -eq 'NotStarted' -or $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -atau $hasWinCS) -dan $statusPending -dan -bukan $isUpd -dan $sbOn -dan -bukan $tskDis) if ($isUpd) { $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Lacak Perangkat yang diperbarui yang memerlukan boot ulang (UEFICA2023Status=Diperbarui tapi Event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF — di luar lingkup, jangan mengklasifikasikan dengan percaya diri } lainnya { if ($isUpdatePending) { } # Dihitung secara terpisah dalam Pembaruan Tertunda — sama eksklusifnya untuk bagan pai 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]++ jika ($stErrorCodeSamples[$ec]. Hitung -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++ } # Pembaruan Tertunda: kebijakan atau WinCS diterapkan, status tertunda, SB ON, tugas tidak dinonaktifkan if ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # Di bawah Perangkat observasi (terpisah dari Tindakan Diperlukan) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Tindakan Diperlukan: tidak diperbarui, SB AKTIF, tidak cocok dengan kategori kepercayaan lainnya, bukan Pembaruan Tertunda if (-not $isUpd -and $sbOn -and -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarilyPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Diperbarui=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Total++ jika ($isUpd) { $stMfrCounts[$mfr]. Diperbarui++ } 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++ } # Lacak semua perangkat menurut wadah (termasuk BucketId kosong) $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Diperbarui=0; Manufacturer=$mfr; Model=""; BIOS="" } jika ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model } jika ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ jika ($isUpd) { $stAllBuckets[$bucketKey]. Diperbarui++ } } # Flush kumpulan ke disk foreach ($df di $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Matematika]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = jika ($cRem -gt 0) { " | ETA: ~$([Matematika]:Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } else { "" } $cMem = [matematika]:Round([System.GC]::GetTotalMemory($false) / 1MB, 0) if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew baru, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # Menyelesaikan array JSON foreach ($dfName di $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: perangkat $($stDeviceFileCounts[$dfName]) -ForegroundColor DarkGray } # Statistik turunan komputasi $stAtRisk = 0; $stSafeList = 0 foreach ($bid di $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count - $b.Updated if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [matematika]::Maks(0, $stAtRisk - $c.WithErrors) # NotUptodate = hitung dari kumpulan not_updated (perangkat dengan SB AKTIF dan tidak diperbarui) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [dipesan]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Diperbarui = $c.Diperbarui; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; SementaraPaused = $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 } PersenHighConfidence = 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 } PersenFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Tulis CSV [PSCustomObject]$stats | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Kunci; Count=$_. Value.Total; Diperbarui=$_. Value.Updated; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stErrorCodeCounts.GetEnumerator() | Nilai Sort-Object -Turun | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Kunci; Count=$_. Nilai; SampleDevices=($stErrorCodeSamples[$_. Kunci] -join ", ") } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Turun | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Kunci; Count=$_. Value.Count; Diperbarui=$_. Value.Updated; NotUpdated=$_. Value.Count-$_. Value.Updated; Produsen=$_. Value.Manufacturer } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8 # Hasilkan CSV yang kompatibel dengan orkestrator (nama file yang diharapkan untuk Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (Test-Path $notUpdatedJsonPath) { coba { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json if ($nuData.Count -gt 0) { # NotUptodate CSV - orkestrator mencari *NotUptodate*.csv $nuData | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Encoding UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) perangkat)" -ForegroundColor Gray } } tangkap { } } # Tulis data JSON untuk dasbor $stats | ConvertTo-Json -Kedalaman 3 | Set-Content ($dataDir Jalur Bergabung "summary.json") -Pengodean UTF8 # HISTORIS PELACAKAN: Simpan titik data untuk bagan tren # Gunakan lokasi singgahan yang stabil sehingga data tren tetap ada di seluruh folder agregasi bertingkat waktu. # Jika OutputPath terlihat seperti "...\Aggregation_yyyyMMdd_HHmmss", cache akan masuk ke folder induk.# Jika tidak, cache masuk ke dalam OutputPath itu sendiri.$parentDir = Split-Path $OutputPath -Parent $leafName = Split-Path $OutputPath -Leaf if ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current') { # Orchestrator-create timestamped folder — use parent for stable cache $historyPath = Join-Path $parentDir ".cache\trend_history.json" } lain { $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) { coba { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } tangkapan { $historyData = @() } } # Periksa juga di dalam OutputPath\.cache\ (lokasi warisan dari versi yang lebih lama) # Gabungkan titik data apa pun yang belum ada dalam riwayat utama 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) { coba { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Tanggal }) foreach ($entry di $innerData) { jika ($entry. Tanggal dan $entry. Tanggal -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host " Titik data $($innerData.Count) gabungan dari cache dalam" -ForegroundColor DarkGray } } tangkap { } } }
# BOOTSTRAP: Jika riwayat tren kosong/jarang, rekonstruksi dari data riwayat if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host " Riwayat tren bootstrapping dari data historis..." -ForegroundColor Yellow $dailyData = @{} # Sumber 1: CSV ringkasan di dalam folder saat ini (Aggregation_Current menyimpan semua CSV Ringkasan) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Nama Sort-Object foreach ($summCsv di $localSummaries) { coba { $summ = Import-Csv $summCsv.FullName | Select-Object -1 Pertama jika ($summ. TotalDevices -dan [int]$summ. TotalDevices -gt 0 -dan $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = jika ($summ. Diperbarui) { [int]$summ. Diperbarui } lagi { 0 } $notUpd = jika ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Tanggal = $dateStr; Total = [int]$summ. TotalDevices; Diperbarui = $updated; NotUpdated = $notUpd NeedsReboot = 0; Kesalahan = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } tangkap { } } # Sumber 2: Folder lama Aggregation_* (warisan, jika masih ada) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Nama -match '^Aggregation_\d{8}' } | Nama Sort-Object foreach ($folder di $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -1 Pertama if ($summCsv) { coba { $summ = Import-Csv $summCsv.FullName | Select-Object -1 Pertama jika ($summ. TotalDevices -dan [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Nama -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = jika ($summ. Diperbarui) { [int]$summ. Diperbarui } lagi { 0 } $notUpd = jika ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Tanggal = $dateStr; Total = [int]$summ. TotalDevices; Diperbarui = $updated; NotUpdated = $notUpd NeedsReboot = 0; Kesalahan = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } tangkap { } } } # Sumber 3: RolloutState.json WaveHistory (memiliki stempel waktu per gelombang dari hari 1) # Ini menyediakan titik data garis dasar bahkan ketika tidak ada folder agregasi lama $rolloutStatePaths = @( (Join-Path $parentDir "RolloutState\RolloutState.json"), (Join-Path $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath di $rolloutStatePaths) { if (Test-Path $rsPath) { coba { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Gunakan tanggal mulai gelombang sebagai titik data tren # Hitung perangkat kumulatif yang ditargetkan pada setiap gelombang $cumulativeTargeted = 0 foreach ($wave di $rsData.WaveHistory) { jika ($wave. StartedAt -and $wave. DeviceCount) { $waveDate = ([datetime]$wave. StartedAt). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. DeviceCount if (-not $dailyData.ContainsKey($waveDate)) { # Perkiraan: pada waktu mulai gelombang, hanya perangkat dari gelombang sebelumnya yang diperbarui $dailyData[$waveDate] = [PSCustomObject]@{ Tanggal = $waveDate; Total = $c.Total; Diperbarui = [matematika]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0; Kesalahan = 0; ActionRequired = 0 } } } } } } tangkap { } break # Gunakan pertama kali ditemukan } }
if ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | Kunci Sort-Object | ForEach-Object { $_. Nilai }) Write-Host " Titik data $($historyData.Count) bootstrapped dari ringkasan historis" -ForegroundColor Green } }
# Tambahkan titik data saat ini (deduplikasi menurut hari - tetap terbaru per hari) $todayKey = (Get-Date). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" } if ($existingToday) { # Ganti entri hari ini $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Tanggal = $todayKey Total = $c.Total Diperbarui = $c.Diperbarui NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Kesalahan = $c.WithErrors ActionRequired = $c.ActionReq } # Hapus titik data yang buruk (total 0) dan pertahankan 90 terakhir $historyData = @($historyData | Where-Object { [int]$_. Total -gt 0 }) # Tanpa tutup — data tren ~100 byte/entry, setahun penuh = ~36 KB $historyData | ConvertTo-Json -Kedalaman 3 | Set-Content $historyPath -Pengodean UTF8 Write-Host " Riwayat tren: titik data $($historyData.Count) " -ForegroundColor DarkGray # Susun data bagan tren untuk HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Tanggal)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Diperbarui }) -join "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Total }) -join "," # Proyeksi: memperluas garis tren menggunakan penggambatan eksponensial (2,4,8,16...) # Memperoleh ukuran gelombang dan periode pengamatan dari data riwayat tren aktual.# - Ukuran gelombang = peningkatan periode tunggal terbesar terlihat dalam sejarah (gelombang terbaru yang digunakan) # - Hari observasi = rata-rata hari kalender antara titik data tren (seberapa sering kita menjalankan) # Lalu gandakan ukuran gelombang setiap periode, sesuai dengan strategi pertumbuhan 2x orkestrator.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Diperbarui $remaining = $stNotUptodate # Hanya perangkat yang tidak diperbarui SB-ON (tidak termasuk SecureBoot NONAKTIF) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Derive wave size and observation period from trend history $increments = @() $dayGaps = @() for ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Diperbarui - $historyData[$hi-1]. Diperbarui if ($inc -gt 0) { $increments += $inc } coba { $d 1 = [datetime]::P arse($historyData[$hi-1]. Tanggal) $d 2 = [datetime]::P arse($historyData[$hi]. Tanggal) $gap = ($d 2 - $d 1). TotalDays if ($gap -gt 0) { $dayGaps += $gap } } tangkap {} } # Ukuran gelombang = penambahan positif terbaru (gelombang saat ini), mundur ke rata-rata, minimum 2 $waveSize = jika ($increments. Hitung -gt 0) { [matematika]::Max(2, $increments[-1]) } lagi { 2 } # Periode observasi = kesenjangan rata-rata antara titik data (hari kalender per gelombang), minimal 1 $waveDays = if ($dayGaps.Count -gt 0) { [matematika]::Maks(1, [matematika]::Round(($dayGaps | Measure-Object -Average). Rata-rata, 0)) } lagi { 1 }
Write-Host " Proyeksi: waveSize=$waveSize (dari penambahan terakhir), waveDays=$waveDays (kesenjangan avg dari sejarah)" -ForegroundColor DarkGray
$dayCounter = 0 # Proyek hingga semua perangkat diperbarui atau maksimal 365 hari untuk ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # Pada setiap batas periode observasi, sebarkan gelombang kemudian ganda if ($dayCounter -ge $waveDays) { $devicesThisWave = [matematika]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Diperbarui + $stNotUptodate; $remaining = 0 } # Ukuran gelombang ganda untuk periode berikutnya (strategi orkestrator 2x) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd"))'" $projValues += $lastUpdated $projNotUpdValues += [matematika]:Maks(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 " Proyeksi: membutuhkan setidaknya 2 titik data tren untuk mendapatkan pengaturan waktu gelombang" -ForegroundColor DarkGray } # Susun string data bagan gabungan untuk string di sini $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } else { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } else { "" } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { "" } $histCount = ($historyData | Objek Pengukuran). Menghitung $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | ForEach-Object { @{ name=$_. Kunci; total=$_. Value.Total; diperbarui=$_. Value.Updated; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Kedalaman 3 | Set-Content ($dataDir Jalur Bergabung "manufacturers.json") -Pengodean UTF8 # Konversi file data JSON ke CSV untuk unduhan Excel yang dapat dibaca manusia Write-Host "Mengonversi data perangkat ke CSV untuk unduhan Excel..." -ForegroundColor Gray foreach ($dfName di $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" if (Test-Path $jsonFile) { coba { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json if ($jsonData.Count -gt 0) { # Sertakan kolom tambahan untuk CSV update_pending $selectProps = if ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } lain { @('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) baris -> CSV" -ForegroundColor DarkGray } } menangkap { Write-Host " $dfName - dilewati" -ForegroundColor DarkYellow } } } # Hasilkan dasbor HTML mandiri $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Menghasilkan dasbor HTML mandiri..." -ForegroundColor Yellow # VELOCITY PROJECTION: Menghitung dari riwayat pemindaian atau ringkasan sebelumnya $stDeadline = [datetime]"2026-06-24" # Sert KEK kedaluwarsa $stDaysToDeadline = [matematika]::Max(0, ($stDeadline - (Get-Date)). Hari) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "N/A" $stWorkingDays = 0 $stCalendarDays = 0 # Cobalah riwayat tren terlebih dahulu (ringan, sudah dipertahankan oleh agregator — menggantikan ScanHistory.json yang membengkak) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Diperbarui -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. Diperbarui - [int]$prev. Diperbarui if ($updDiff -gt 0) { $stDevicesPerDay = [matematika]::Round($updDiff / $daysDiff, 0) $stVelocitySource = "TrendHistory" } } } } # Coba ringkasan peluncuran orkestrator (memiliki kecepatan terkomputasi sebelumnya) if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { coba { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [matematika]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = "Orkestrator" if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } tangkap { } } # Fallback: coba CSV ringkasan sebelumnya (cari folder saat ini AND folder agregasi induk/saudara) if ($stVelocitySource -eq "N/A") { $searchPaths = @( ($OutputPath Jalur Bergabung "SecureBoot_Summary_*.csv") ) # Juga cari folder agregasi saudara (orkestrator membuat folder baru setiap jalankan) $parentPath = Split-Path $OutputPath -Parent if ($parentPath) { $searchPaths += ($parentPath Jalur Bergabung "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += ($parentPath Jalur Gabungan "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Turun | Select-Object -1 Pertama if ($prevSummary) { coba { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) - $prevDate). TotalDays if ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Diperbarui $updDelta = $c.Diperbarui - $prevUpdated if ($updDelta -gt 0) { $stDevicesPerDay = [matematika]::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = "PreviousReport" } } } tangkap { } } } # Fallback: menghitung kecepatan dari rentang riwayat tren penuh (titik data pertama vs terbaru) if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. Diperbarui -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. Diperbarui - [int]$first. Diperbarui if ($updDiff -gt 0) { $stDevicesPerDay = [matematika]::Round($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # Menghitung proyeksi menggunakan penggambatan eksponensial (konsisten dengan bagan tren) # Gunakan kembali data proyeksi yang sudah dihitung untuk bagan jika tersedia if ($hasProjection -and $projDates.Count -gt 0) { # Gunakan tanggal proyeksi terakhir (saat semua perangkat diperbarui) $lastProjDateStr = $projDates[-1] -replace "'", "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Hari $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) { # Fallback: proyeksi linear jika tidak ada data eksponensial yang tersedia $daysNeeded = [matematika]::Plafon($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy") $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++ } } } # Build velocity HTML $velocityHtml = jika ($stDevicesPerDay -gt 0) { "<><>& kuat#128640; Perangkat/Hari:</strong> $($stDevicesPerDay.ToString('N0')) (sumber: $stVelocitySource)</div>" + "<><>& kuat#128197; Penyelesaian Yang Diproyeksikan:</strong> $stProjectedDate" + $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:>⚠ PAST DEADLINE</span>" } else { " <span style='color:#28a745'>✓ Sebelum tenggat waktu</span>" }) + "</div>" + "<><>🕐 yang kuat; Hari Kerja:</strong> $stWorkingDays | <kuat>Calendar Hari:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Deadline: Jun 24, 2026 (Kedaluwarsa sertifikat KEK) | Hari tersisa: $stDaysToDeadline</div>" } lain { "<div style='padding:8px; latar belakang:#fff3cd; radius batas:4px; border-left:3px solid #ffc107'>" + "<>📅 yang kuat; Penyelesaian yang Diproyeksikan:</strong> Data yang tidak mencukupi untuk penghitungan kecepatan. " + "Jalankan agregasi setidaknya dua kali dengan perubahan data untuk menetapkan rate.<br/>" + "<tenggat waktu>yang kuat:</strong> 24 Jun 2026 (Sertifikat KEK kedaluwarsa) | <hari>yang kuat tersisa:</strong> $stDaysToDeadline</div>" } # Penghitungan mundur kedaluwarsa # Cert $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematika]::Maks(0, ($certKekExpiry - $certToday). Hari) $daysToUefi = [matematika]::Maks(0, ($certUefiExpiry - $certToday). Hari) $daysToPca = [matematika]::Maks(0, ($certPcaExpiry - $certToday). Hari) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Helper: Membaca rekaman dari JSON, ringkasan wadah build + baris perangkat N pertama $maxInlineRows = 200 fungsi Build-InlineTable { param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (Test-Path $JsonPath) { coba { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Menghitung # BUCKET SUMMARY: Mengelompokkan menurut BucketId, memperlihatkan hitungan per wadah dengan Diperbarui dari statistik bucket global if ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | Sort-Object Count -Descending $bucketSummary = "><2 h3 style='font-size:.95em; warna:#333; margin:10px 0 5px'><3 Menurut Bucket Perangkat Keras ($($buckets. Hitung) wadah)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:tabel ><15px><ad><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Diperbarui</th><th style='text-align:right; color:#dc3545'>Tidak Diperbarui</th><th><1 Produsen><2 /th></tr></thead><tbody>" kudeta ($b di $buckets) { $bid = if ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Group | Select-Object -1 Pertama). WMI_Manufacturer # Dapatkan hitungan yang diperbarui dari statistik bucket global (semua perangkat dalam wadah ini di seluruh kumpulan data) $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; warna:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; warna:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # DETAIL PERANGKAT: Baris N pertama sebagai daftar datar $slice = $data | Select-Object -$MaxRows pertama foreach ($d di $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match "High") { '<span class="lencana-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="lencana badge-danger">Tidak Didukung><6 /span>' } elseif ($conf -match "Under") { '<span class="badge badge-info">Under Obs><0 /span>' } elseif ($conf -match "Jeda") { '<span class="badge badge-warning">Jeda><4 /span>' } else { '<span class="lencana-warning">Tindakan Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="lencana-sukses"><01 Memperbarui</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="lencana-bahaya"><05 Kesalahan</span>' } else { '><08 span class="lencana-warning"><09 Tertunda><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><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n" } } tangkap { } } if ($totalCount -eq 0) { return "><44 div style='padding:20px; warna:#888; gaya font:><45 Miring Tidak ada perangkat dalam category.><46 /div>" } $showing = [matematika]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Total: perangkat $($totalCount.ToString("N0")" if ($CsvFileName) { $header += " | ><50 href='$CsvFileName' style='color:#1a237e; font-weight:bold'>📄 Unduh CSV Lengkap untuk Excel><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; warna:#333; margin:Detail Perangkat ><58 10px 0 5px (memperlihatkan $showing pertama)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; overflow-y:tabel ><otomatis><><tr><th><0 HostName><1 /th><th><4 Produsen><5 /th><th model><8><9 /th><th><2 Confidence><3 /th><th><6 Status><7 /th><kesalahan><0><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>" mengembalikan "$header$bucketSummary$deviceHeader$deviceTable" } # Susun tabel sebaris dari file JSON yang sudah ada di disk, menautkan ke 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 ($dataDir Join-Path "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 ($dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath ($dataDir Join-Path "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath ($dataDir Join-Path "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 ($dataDir Join-Path "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath ($dataDir Join-Path "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath ($dataDir Join-Path "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" # Tabel kustom untuk Pembaruan Tertunda — termasuk kolom UEFICA2023Status dan UEFICA2023Error $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (Test-Path $upJsonPath) { coba { $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")) perangkat | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; font-weight:bold'>📄 Unduh CSV Lengkap untuk Excel><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object -$maxInlineRows pertama foreach ($d di $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge-success">Ya><8 /span>' } else { '-' } $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n" } $upShowing = [matematika]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; warna:#333; margin:10px 0 5px'>Detail Perangkat (memperlihatkan $upShowing pertama)</h3>" $upTable = "<div style='max-height:500px; overflow-y:tabel ><otomatis><ad><tr><th><9 HostName><0 /th><th><3 Produsen><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2023Error><6 /th><th><9 Kebijakan</th><>Kunci WinCS</th><th><<</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } lain { $tblUpdatePending = "<div style='padding:20px; warna:#888; gaya font:>Miring Tidak ada perangkat dalam kategori ini.</div>" } } tangkap { $tblUpdatePending = "<div style='padding:20px; warna:#888; gaya font:>Miring Tidak ada perangkat dalam kategori ini.</div>" } } lain { $tblUpdatePending = "<div style='padding:20px; warna:#888; gaya font:>Miring Tidak ada perangkat dalam kategori ini.</div>" } # Penghitungan mundur kedaluwarsa # Cert $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematika]::Maks(0, ($certKekExpiry - $certToday). Hari) $daysToUefi = [matematika]::Maks(0, ($certUefiExpiry - $certToday). Hari) $daysToPca = [matematika]::Maks(0, ($certPcaExpiry - $certToday). Hari) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } else { '#28a745' } # Susun data bagan produsen sebaris (10 teratas menurut jumlah perangkat) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | Select-Object -10 Pertama $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "By Manufacturer" } else { "Top 10 Manufacturers" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Kunci)'" }) -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 "," # Susun tabel produsen $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Turun | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Key)</td><td>$($_. Value.Total.ToString("N0"))</td><td>$($_. Value.Updated.ToString("N0"))</td><td>$($_. Value.HighConf.ToString("N0"))><0 /td><td>$($_. Value.ActionReq.ToString("N0"))><4 /td></tr>'n" } $htmlContent = @" <!> html DOCTYPE <html lang="en"> ><3 kepala < <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> judul <><9 Dasbor Status Sertifikat Boot Aman><0 /judul><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 gaya < *{box-sizing:border-box; margin:0; pengikisan:0} body{font-family:'Segoe UI',Tahoma,sans-serif; latar belakang:#f0f2f5; warna:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); warna:#fff; pengisian:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; keburaman:.9} .container{max-width:1400px; margin:0 otomatis; pengisian:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); jarak:12px; margin:20px 0} .card{background:#fff; radius batas:10px; pengisian:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); border-left:4px solid #ccc;transisi:transformasi .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; warna:#666; margin-top:4px} .card .pct{font-size:.75em; warna:#888} .section{background:#fff; radius batas:10px; pengisian:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; warna:#1a237e; margin-bottom:10px; kursor:penunjuk; pilih-pengguna:tidak ada} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; jarak:20px; margin:20px 0} .chart-box{background:#fff; radius batas:10px; pengisian:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabel{width:100%; batas-ciutkan:ciutkan; font-size:.85em} th{background:#e8eaf6; pengisian:8px 10px; text-align:left; posisi:tempel; atas:0; z-index:1} td{padding:6px 10px; border-bottom:1px solid #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; pengisian:2px 8px;radius batas:10px; font-size:.75em; font-weight:700} .badge-success{background:#d4edda; color:#155724} .badge-danger{background:#f8d7da; warna:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; warna:#0c5460} .top-link{float:right; font-size:.8em; warna:#1a237e; text-decoration:none} .footer{text-align:center; pengisian:20px; warna:#999; font-size:.8em} a{color:#1a237e} </style><9 </head> > tubuh < <div class="header"> <h1>Dasbor Status Sertifikat Boot Aman</h1> <div class="meta">Generated: $($stats. ReportGeneratedAt) | Total Perangkat: $($c.Total.ToString("N0")) | Bucket unik: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">
Kartu KPI<!-- - dapat diklik, ditautkan ke bagian --> <div class="cards"> <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; posisi:relative"><div style="position:absolute; atas:8px; kanan:8px; latar belakang:#dc3545; warna:#fff; pengisian:1px 6px; radius batas:8px; font-size:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString(""value N0"))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% - PERLU TINDAKAN><0 /div></a><3 <a class="card" href="#s-upd" onclick="openSection('d-upd')"style="border-left-color:#28a745; text-decoration:none; posisi:relative"><div style="position:absolute; atas:8px; kanan:8px; latar belakang:#28a745; warna:#fff; pengisian:1px 6px; radius batas:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString(""color:#28a745">$($c.Updated.ToString(""N0"))</div><div class="label">Diperbarui><6 /div><div class="pct">$($stats. PersenCertUpdated)%</div></a><3 <a class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; position:relative"><div style="position:absolute; atas:8px; kanan:8px; latar belakang:#6c757d; warna:#fff; pengisian:1px 6px; radius batas:8px; font-size:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.SBOff/$c.Total)*100,1)}else{0})% - Out of Scope><0 /div></a><3 <a class="card" href="#s-nrb" onclick="openSection('d-nrb')" style="border-left-color:#ffc107; text-decoration:none"><div class="value" style="color:#ffc107">$($c.NeedsReboot.ToString("N0"))</div><div class="label">Needs Reboot><2 /div><div class=="pct">$(if($c.Total -gt 0){[matematika]:Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% - menunggu boot ulang><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 Tertunda</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Policy/WinCS diterapkan, menunggu pembaruan><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){[matematika]::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{0})%</div></a><11 <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0"))</div><div class="label">High Confidence><20 /div><div class="pct">$($stats. PersenHighConfidence)% - Aman untuk peluncuran><24 /div></a><27 <a class="card" href="#s-uo" onclick="openSection('d-uo')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value" style="color:#ffc107"><1 $($c.UnderObs.ToString("N0"))><2 /div><div class="label"><5 Under Observation><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0"))</div><div class="label">Action Diperlukan><2 /div><div class="pct">$($stats. PersenActionRequired)% - harus menguji><6 /div></a><9 <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($stAtRisk.ToString("N0"))</div><div class="label">At Risk><68 /div><div class="pct">$($stats. PercentAtRisk)% - Mirip dengan><2 /div></a><5 gagal <a class="card" href="#s-td" onclick="openSection('d-td')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.TaskDisabled.ToString("N0"))</div><div class="label">Task Disabled><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})% -><8 /div></a><91 <a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.TempPaused.ToString("N0"))</div><div class="label">Temp. Dijeda</div><div class="pct">$(if($c.Total -gt 0){[matematika]: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">Masalah Umum><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3 <a class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0"))</div><div class="label">Missing KEK</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithMissingKEK/$c.Total)*100,1)}else{0})%</div></a> <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithErrors.ToString("N0"))</div><div class="label">With Errors</div><div class="pct"><1 $($stats. PercentWithErrors)% - Kesalahan 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><div class="label">Temp. Kegagalan</div><div class="pct">$(if($c.Total -gt 0){[matematika]::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">Tidak Didukung><6 /div><div class="pct">$(if($c.Total -gt 0){[matematika]::Round(($c.PermFailures/$c.Total)*100,1)}else{0})%</div></a><3 </div>
Kecepatan Penggunaan<!-- & Cert Kedaluwarsa --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; jarak:20px; margin:15px 0"> <div class="section" style="margin:0"> <h2>📅 Kecepatan Penyebaran</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">perangkat yang diperbarui dari $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; latar belakang:#e8eaf6; tinggi:20px; radius batas:10px; overflow:hidden"><div style="background:#28a745; tinggi:100%; width:$($stats. PersenCertUpdated)%; radius batas:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PersenCertUpdated)% lengkap</div> <div style="margin-top:10px; pengisian:10px; latar belakang:#f8f9fa; radius batas:8px; font-size:.85em"> <><>kuat Sisa:</strong> $($stNotUptodate.ToString("N0")) perangkat memerlukan tindakan</div> <div><strong>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) perangkat (kesalahan + permanen + tugas nonaktif)</div> <div><strong>Safe to deploy:</strong> $($stSafeList.ToString("N0")) perangkat (bucket yang sama berhasil)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; border-left:4px solid #dc3545"> <h2 style="color:#dc3545">⚠ Certificate Expiry Countdown</h2> <div class="section-body open"> <div style="display:flex; jarak:15px; margin-top:10px"> <div style="text-align:center; pengisian:15px; radius batas:8px; lebar-min:120px; latar belakang:linear-gradient(135deg,#fff5f5,#ffe0e0); batas:#dc3545 solid 2px; flex:1"> <div style="font-size:.65em; warna:#721c24; text-transform:uppercase; font-weight:bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size:.85em; font-weight:bold; warna:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; font-weight:700; warna:#dc3545; tinggi-garis:1"><9 $daysToKek</div> ><2 div style="font-size:.8em; color:#721c24"><3 days (24 Jun 2026)><4 /div> ><6 /div> ><8 div style="text-align:center; pengisian:15px; radius batas:8px; lebar-min:120px; latar belakang:linear-gradient(135deg,#fffef5,#fff3cd); batas:#ffc107 solid 2px; flex:1"><9 <div style="font-size:.65em; warna:#856404; text-transform:uppercase; font-weight:bold">UEFI CA 2011</div> <div id="daysUefi" style="font-size:2.2em; font-weight:700; warna:#856404; tinggi garis:1; margin:5px 0">$daysToUefi</div> <div style="font-size:.8em; color:#856404">days (27 Jun 2026)</div> </div> <div style="text-align:center; pengisian:15px; radius batas:8px; lebar-min:120px; latar belakang:linear-gradient(135deg,#f0f8ff,#d4edff); batas:#0078d4 padat 2px; flex:1"> <div style="font-size:.65em; warna:#0078d4; text-transform:uppercase; font-weight:bold">Windows PCA</div> <div id="daysPca" style="font-size:2.2em; font-weight:700; warna:#0078d4; tinggi garis:1; margin:5px 0">$daysToPca><2 /div><3 <div style="font-size:.8em; color:#0078d4">days (19 Okt 2026)</div><7 </div><9 </div><1 <div style="margin-top:15px; pengisian:10px; latar belakang:#f8d7da; radius batas:8px; font-size:.85em; batas kiri:4px solid #dc3545"> <strong>⚠ CRITICAL:</strong> Semua perangkat harus diperbarui sebelum sertifikat kedaluwarsa. Perangkat yang tidak diperbarui oleh tenggat waktu tidak dapat menerapkan pembaruan Keamanan di masa mendatang untuk Boot Manager dan Boot Aman setelah kedaluwarsa.</div> </div> </div> </div>
Bagan<!-- --> <div class="charts"> <div class="chart-box"><h3>Status Penyebaran</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) { "<!-- Historis Bagan Tren --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Kemajuan Pembaruan Seiring Waktu <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; warna:#888; margin-top:5px'>Garis penuh = data aktual$(if ($historyData.Count -ge 2) { " | Garis putus-putus = diproyeksikan (penggambatan eksponensial: 2→4→8→16... perangkat per gelombang)" } lainnya { " | Jalankan agregasi lagi besok untuk melihat garis tren dan proyeksi" })</div> </div> </div>" })
unduhan CSV<!-- --> <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Unduh Data Lengkap (CSV untuk 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; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Not Updated ($($stNotUptodate.ToString("N0")))</a><8 <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a> <href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; latar belakang:#fd7e14; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Action Diperlukan ($($c.ActionReq.ToString("N0")))</a> <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Masalah Umum ($($c.WithKnownIssues.ToString("N0")))</a> <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; latar belakang:#dc3545; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Task Disabled ($($c.TaskDisabled.ToString("N0")))</a> <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; latar belakang:#28a745; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Updated ($($c.Updated.ToString("N0")))</a> <href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; latar belakang:#6c757d; warna:#fff; pengisian:6px 14px; radius batas:5px; text-decoration:none; font-size:.8em">Ringkasan</a> <div style="width:100%; font-size:.75em; warna:#888; margin-top:5px">file CSV terbuka di Excel. Tersedia ketika dihosting di server web.</div> </div> </div>
Uraian Produsen<!-- --> <div class="section"> <h2 onclick="toggle('mfr')">Oleh Produsen <class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> tabel <><ad><tr><th><1 Produsen><2 /th><th><5 Total><6 /th><th><9 Diperbarui><0 /th><th><3 Kepercayaan Diri Tinggi><4 /th><th><7 Tindakan diperlukan><8 /th></tr></thead><3 <><9><5 $mfrTableRows><6 /tbody></table </div><1 </div>
<!-- Bagian Perangkat (200 inline + unduhan CSV pertama) --> <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Perangkat dengan Kesalahan ($($c.WithErrors.ToString("N0"))) <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">🔴 Masalah yang Diketahui ($($c.WithKnownIssues.ToString("N0"))) <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 yang Hilang - Kejadian 1803 ($($c.WithMissingKEK.ToString("N0"))) <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">🟠 Tindakan Diperlukan ($($c.ActionReq.ToString("N0"))) <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">🔵 Di bawah Observasi ($($c.UnderObs.ToString("N0"))) <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 </div><7 <div class="section" id="s-tf"> <h2 onclick="toggle('d-tf')" style="color:#dc3545">🔴 Kegagalan Sementara ($($c.TempFailures.ToString("N0"))) <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">🔴 Kegagalan Permanen / Tidak Didukung ($($c.PermFailures.ToString("N0"))) <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"))) - Kebijakan/WinCS Diterapkan, Menunggu Pembaruan <class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Devices where AvailableUpdatesPolicy atau WinCS key is applied but UEFICA2023Status is still NotStarted, InProgress, or null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Rollout In Progress ($($c.RolloutInProgress.ToString("N0"))) <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"))) - Di Luar Lingkup <kelas="top-link" href="#">↑<Atas /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">🟢 Perangkat yang Diperbarui ($($c.Updated.ToString("N0"))) <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">🔄 Diperbarui - Perlu Boot Ulang ($($c.NeedsReboot.ToString("N0"))) <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 | Menghasilkan $($stats. ReportGeneratedAt) | StreamingMode | Memori Puncak: ${stPeakMemMB} MB</div> </div><!-- /container -->
> skrip< fungsi toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} fungsi openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} Bagan baru(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Update','Update Pending','High Confidence','Under Observation','Action Diperlukan','Temp. Dijeda','Tidak Didukung','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#6c757d','#721c24 #17a2b8','#6c757d',','#adb5bd','#dc3545']}]},options:{responsive:true,plugins:{legends:{position:'right',labels:{font:{size:11}}}}}}); Bagan baru(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'Kepercayaan Tinggi',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Diperlukan',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. Dijeda',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'}}}}); Bagan Tren Riwayat 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); } set data var = [ {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; if (projLen > 0) { datasets.push({label:'Projected Updated (2x doubling)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } Bagan baru(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'}}}}); } Hitungan mundur dinamis (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))})(); > /skrip < </body> </html> "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Selalu simpan salinan "Terbaru" yang stabil sehingga admin tidak perlu melacak stempel waktu $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # Simpan manifes file untuk mode tambahan (deteksi tanpa perubahan cepat pada jalankan berikutnya) if ($IncrementalMode -or $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 "Menyimpan manifes file untuk mode penambahan..." -ForegroundColor Gray foreach ($jf di $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Ukuran = $jf. Panjang } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Manifes tersimpan untuk file $($stNewManifest.Count) " -ForegroundColor DarkGray } # PEMBERSIHAN PENYIMPANAN # Folder orkestrator yang dapat digunakan kembali (Aggregation_Current): hanya jalankan terbaru (1) # Admin jalankan manual / folder lainnya: terus jalankan 7 kali terakhir # CSV Ringkasan TIDAK PERNAH dihapus — CSV kecil (~1 KB) dan merupakan sumber cadangan untuk riwayat tren $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } else { 7 } # Prefiks file aman untuk dibersihkan (snapshot ephemeral per-run) $cleanupPrefixes = @( 'SecureBoot_Dashboard_', 'SecureBoot_action_required_', 'SecureBoot_ByManufacturer_', 'SecureBoot_ErrorCodes_', 'SecureBoot_errors_', 'SecureBoot_known_issues_', 'SecureBoot_missing_kek_', 'SecureBoot_needs_reboot_', 'SecureBoot_not_updated_', 'SecureBoot_secureboot_off_', 'SecureBoot_task_disabled_', 'SecureBoot_temp_failures_', 'SecureBoot_perm_failures_', 'SecureBoot_under_observation_', 'SecureBoot_UniqueBuckets_', 'SecureBoot_update_pending_', 'SecureBoot_updated_devices_', 'SecureBoot_rollout_inprogress_', 'SecureBoot_NotUptodate_', 'SecureBoot_Kusto_' ) # Temukan semua stempel waktu unik dari file yang dapat dibersihkan saja $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. Nama; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Hitung -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { jika ($_. Nama -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Unique -Descending) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object -Lewati $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs di $oldTimestamps) { foreach ($prefix di $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f di $oldFiles) { $freedBytes += $f.Length Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [matematika]::Round($freedBytes / 1MB, 1) Write-Host "Pembersihan penyimpanan: menghapus file $removedFiles dari $($oldTimestamps.Count) yang berjalan lama, membebaskan ${freedMB} MB (menyimpan $retentionCount terakhir + semua Ringkasan/NotUptodate CSV)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Sian Write-Host "STREAMING AGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -ForegroundColor Sian Write-Host " Total Perangkat: $($c.Total.ToString("N0"))" -ForegroundColor White Write-Host " TIDAK DIPERBARUI: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" }) Write-Host " Diperbarui: $($c.Updated.ToString("N0")) ($($stats. PersenCertUpdated)%)" -ForegroundColor Green Write-Host " Dengan Kesalahan: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Memori Puncak: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Waktu: $([matematika]:Round($stTotal/60,1)) mnt" -ForegroundColor White Write-Host " Dasbor: $htmlPath" -ForegroundColor White return [PSCustomObject]$stats } mode streaming #endregion } lain { Write-Error "Jalur input tidak ditemukan: $InputPath" keluar 1 }