TÄRKEÄÄ Tämä esimerkkikomentosarjan sisältävä artikkeli on poistettu käytöstä. 12. toukokuuta 2026 ja sen jälkeen julkaistuista Windows-päivityksistä alkaen mallikomentosarja sijaitsee laitteesi %systemroot%\SecureBoot\ExampleRolloutScripts-kansiossa .
Kopioi ja liitä tämä esimerkkikomentosarja ja muokkaa sitä tarpeen mukaan ympäristössäsi:
<# . SYNOPSIS Koostaa suojatun käynnistyksen tilan JSON-tiedot useista laitteista yhteenvetoraporteiksi.
. KUVAUS Lukee kerätyn suojatun käynnistyksen tilan JSON-tiedostot ja luo: – HTML-koontinäyttö, jossa on kaavioita ja suodatus - Yhteenveto luottamusvälin mukaan - Yksilöllinen laitesäilöanalyysi testausstrategiaa varten Tukee: - Laitekohtaiset tiedostot: HOSTNAME_latest.json (suositus) - Yksittäinen JSON-tiedosto Automaattisesti deduplicates by HostName, keeping latest CollectionTime. Oletusarvoisesti sisältää vain laitteet, joissa on "Toiminnon uudelleenk" tai "Suuri" luottamus , jos haluat keskittyä toimintokelpoisiin säilöihin. Käytä -IncludeAllConfidenceLevels-komentoa ohittamiseen.
. PARAMETER InputPath Polku JSON-tiedostoihin: - Kansio: Lukee kaikki *_latest.json tiedostot (tai *.json, jos _latest tiedostoja ei ole) - Tiedosto: Lukee yksittäisen JSON-tiedoston
. PARAMETER OutputPath Luotujen raporttien polku (oletus: .\SecureBootReports)
. ESIMERKKI # Kooste laitekohtaisten tiedostojen kansiosta (suositus) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Lukee: \\contoso\SecureBootLogs$\*_latest.json
. ESIMERKKI # Mukautettu tulostussijainti .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. ESIMERKKI # Sisällytä vain toiminnon kysymykset ja suuri luottamus (oletustoiminta) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Poissuljetut: Havainnointi, keskeytetty, ei tuettu
. ESIMERKKI # Sisällytä kaikki luottamustasot (ohita suodatin) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. ESIMERKKI # Mukautettu luottamusvälisuodatin .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. ESIMERKKI # ENTERPRISE SCALE: Incremental mode - only process changed files (fast subsequent runs) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # Ensimmäinen suoritus: Täysi lataus ~2 tuntia 500 000 laitteelle # Seuraavat juoksut: Sekunnit, jos muutoksia ei ole, minuutit delta-arvoille
. ESIMERKKI # Ohita HTML, jos mikään ei muutu (nopein seuranta) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # Jos tiedostoja ei ole muutettu edellisen suorituksen jälkeen: ~5 sekuntia
. ESIMERKKI # Vain yhteenveto -tila – ohita suuret laitetaulukot (1–2 minuuttia vs 20+ minuuttia) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # Luo CSV:t, mutta ohittaa HTML-koontinäytön, jossa on täydet laitetaulukot
. MUISTIINPANOJA Muodosta laitepari yrityksen käyttöönoton Detect-SecureBootCertUpdateStatus.ps1 kanssa.Tutustu GPO-DEPLOYMENT-GUIDE.md täydelliseen käyttöönotto-oppaaseen. Oletustoiminta sulkee pois havainnointi-, keskeytys- ja ei-tuetut laitteet kohdistaaksesi raportoinnin vain toimintokelpoisiin laitesäilöihin.#>
param( [Parametri(Pakollinen = $true)] [merkkijono]$InputPath, [Parametri(Pakollinen = $false)] [merkkijono]$OutputPath = ".\SecureBootReports", [Parametri(Pakollinen = $false)] [merkkijono]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json", [Parametri(Pakollinen = $false)] [string]$RolloutStatePath, # Polku RolloutState.json InProgress-laitteiden tunnistamiseksi [Parameter(Mandatory = $false)] [string]$RolloutSummaryPath, # Path to SecureBootRolloutSummary.json from Orchestrator (sisältää projektiotiedot) [Parametri(Pakollinen = $false)] [string[]]$IncludeConfidenceLevels = @("Toiminto pakollinen", "Suuri luottamus"), # Sisällytä vain nämä luottamustasot (oletus: vain toiminnalliset säilöt) [Parametri(Pakollinen = $false)] [switch]$IncludeAllConfidenceLevels, # Ohita suodatin, joka sisältää kaikki luottamusvälit [Parametri(Pakollinen = $false)] [switch]$SkipHistoryTracking, [Parametri(Pakollinen = $false)] [switch]$IncrementalMode, # Enable delta processing - load changed files only last run [Parameter(Mandatory = $false)] [string]$CachePath, # Polku välimuistihakemistoon (oletus: OutputPath\.cache) [Parametri(Pakollinen = $false)] [int]$ParallelThreads = 8, # Rinnakkaisten säikeiden määrä tiedostojen lataamista varten (PS7+) [Parametri(Pakollinen = $false)] [switch]$ForceFullRefresh, # Force full reload even incremental mode [Parametri(Pakollinen = $false)] [switch]$SkipReportIfUnchanged, # Skip HTML/CSV generation if no files changed (just output stats) [Parametri(Pakollinen = $false)] [switch]$SummaryOnly, # Luo vain yhteenvetotilastot (ei suuria laitetaulukoita) – paljon nopeammin [Parameter(Mandatory = $false)] [switch]$StreamingMode # Muistia säästävä tila: prosessilohkot, CSV-tiedostoihin kirjoittaminen asteittain, vain yhteenvetojen pitäminen muistissa )
# Itsekorjaus: Poista näkymättömät Unicode-merkit, jotka web CMS lisää kopioitaessa HTML-artikkeleista.# support.microsoft.com CMS lisää nollaleveät välilyönnit (U+200B), Non-Breaking Spaces (U+00A0) ja muita # näkymättömät merkit html-tunnisteiden ympärillä merkkijonoissa, mikä aiheuttaa PowerShellin jäsennysvirheitä.if ($MyInvocation.MyCommand.Path) { $rawScript = [System.IO.File]::ReadAllText($MyInvocation.MyCommand.Path) jos ($rawScript -match '[\u200B-\u200F\uFEFF]' -tai $rawScript -match '\xA0') { Write-Host "VAROITUS: Näkymättömiä Unicode-merkkejä havaittu (todennäköisesti verkkokopion liittämisen yhteydessä) - automaattinen puhdistuskomentosarja..." -EdustaVäri Keltainen $cleaned = $rawScript -replace '[\u200B-\u200F\uFEFF]', '' $cleaned = $cleaned -replace '\xA0', ' ' [System.IO.File]::WriteAllText($MyInvocation.MyCommand.Path, $cleaned, [System.Text.UTF8Encoding]::new($false)) Write-Host "Komentosarja tyhjennys onnistui. Uudelleenkäynnistäminen..." -EtualallaColor Green & $MyInvocation.MyCommand.Path @PSBoundParameters poistu $LASTEXITCODE } }
# Automaattinen korotus PowerShell 7:ään, jos se on käytettävissä (6 x nopeampi suurissa tietojoukoissa) if ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source jos ($pwshPath) { Write-Host "PowerShell $($PSVersionTable.PSVersion) havaittu - uudelleenkäynnistäminen PowerShell 7:n avulla nopeampaa käsittelyä varten..." -EdustaVäri Keltainen # Muodosta argumenttiluettelo uudelleen sidotuista parametreista $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Ohita', '-Tiedosto', $MyInvocation.MyCommand.Path) foreach ($key in $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] jos ($val -is [switch]) { jos ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [array]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } muu { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs poistu $LASTEXITCODE } }
$ErrorActionPreference = Jatka $timestamp = Get-Date -Format "yyyyMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = Käyttöönotto- ja valvontanäytteet
# Huomautus: Tämä komentosarja ei ole riippuvainen muista komentosaroista.# Lataa kaikki työkalut osoitteesta: $DownloadUrl -> $DownloadSubPage
#region asennus Write-Host "=" * 60 -ForegroundColor Cyan Write-Host "Secure Boot Data Aggregation" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan
# Luo kohdehakemisto if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }
# Lataa tiedot – tukee CSV(vanha) - ja JSON (alkuperäinen) -muotoja Write-Host "'nLoading data from: $InputPath" -ForegroundColor Yellow
# Helper-funktio laitteen objektin normalisointiin (kenttien nimierojen käsittely) funktio Normalize-DeviceRecord { param($device) # Handle Hostname vs HostName (JSON käyttää isäntänimeä, CSV käyttää HostNameia) jos ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force } # Handle Confidence vs ConfidenceLevel (JSON käyttää luottamusta, CSV käyttää ConfidenceLevel) # ConfidenceLevel on virallinen kentän nimi - kartta Luottamus siihen jos ($device. PSObject.Properties['Luottamusväli'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Luottamus -Voima } # Seuraa päivityksen tilaa tapahtuman1808Count OR UEFICA2023Status="Päivitetty" kautta # Näin voit seurata, kuinka monta laitetta kustakin luottamussäilöstä on päivitetty $event 1808 = 0 jos ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false jos ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Päivitetty") { $uefiCaUpdated = $true } jos ($event 1808 -gt 0 -tai $uefiCaUpdated) { # Merkitse päivitetyksi koontinäytön/käyttöönoton logiikan mukaan , mutta älä ohita Luottamusväliä $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } muu { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # Luottamusvälin luokitus: # - "High Confidence", "Under Observation...", "Temporarily Paused...", "Not Supported..." = use as-is # - Kaikki muu (tyhjäarvo, tyhjä, "UpdateType:...", "Tuntematon", "PUUTTUU") = kuuluu laskurissa pakollinen toiminto # Normalisointia ei tarvita – suoratoistolaskurin muuten haara käsittelee sen } # Handle OEMManufacturerName vs WMI_Manufacturer (JSON käyttää OEM*,vanha käyttää WMI_*) jos ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force } # Handle OEMModelNumber vs WMI_Model jos ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force } # Handle FirmwareVersion vs BIOSDescription jos ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } palauta $device }
#region lisäävä käsittely / välimuistin hallinta # Välimuistin polkujen määrittäminen jos (ei $CachePath) { $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# Välimuistin hallintatoiminnot funktio Get-FileManifest { param([string]$Path) if (Test-Path $Path) { kokeile { $json = Get-Content $Path -Raw | ConvertFrom-Json # Muunna PSObject hajautusarvoksi (PS5.1-yhteensopiva - PS7 on -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Nimi] = $_. Arvo } palauta $ht } saalis { palauta @{} } } palauta @{} }
funktio Save-FileManifest { param([hajautusarvo]$Manifest, [merkkijono]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $Manifest | ConvertTo-Json -Depth 3 -Compress | Set-Content $Path -Force }
funktio Get-DeviceCache { param([string]$Path) if (Test-Path $Path) { kokeile { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host " Ladattu laitevälimuisti: $($cacheData.Count) laitteet" -EtualallaColor DarkGray palauta $cacheData } saalis { Write-Host " Välimuisti vioittunut, luodaan uudelleen" -Edustaväri Keltainen palauta @() } } palauta @() }
funktio Save-DeviceCache { param($Devices; [merkkijono]$Path) $dir = Split-Path $Path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } # Muunna matriisiksi ja tallenna $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -Depth 10 -Compress | Set-Content $Path -Force Write-Host " Tallennetun laitteen välimuisti: $($deviceArray.Count) laitteet" -ForegroundColor DarkGray }
funktio Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles, [hajautusarvo]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # Muodosta kirjainkoon huomioon ottava haku luettelosta (normalisoi pieniksi kirjaimiksi) $manifestLookup = @{} foreach ($mk in $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file in $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalisoi polku pieniin kirjaimiin $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt Koko = $file. Pituus } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] jos ($cached. LastWriteTimeUtc -eq $lwt -and $cached. Koko -eq $file. Pituus) { $unchanged. Add($file) Jatkaa } } [void]$changed. Add($file) } palauta @{ Muutettu = $changed Muuttumaton = $unchanged NewManifest = $newManifest } }
# Erittäin nopea rinnakkaistiedostojen lataaminen eräkäsittelyn avulla funktio Load-FilesParallel { param( [System.IO.FileInfo[]]$Files, [int]$Threads = 8 )
$totalFiles = Files $. Laskea # Käytä ~1 000 tiedoston eriä muistin hallinnan parantamiseksi $batchSize = [math]::Min(1000, [math]::Ceiling($totalFiles / [math]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]::new()
kohteelle ($i = 0; $i -lt $totalFiles; $i += $batchSize) { $end = [matematiikka]::Min($i + $batchSize, $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. Add($batch) } Write-Host " ($($batches). Laske) ~$batchSize -tiedostojen erät)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]::new() # Tarkista, onko PowerShell 7+ parallel käytettävissä $canParallel = $PSVersionTable.PSVersion.Major -ge 7 jos ($canParallel -ja $Threads -gt 1) { # PS7+: Käsittele eriä rinnakkain $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]::new() foreach ($file in $batchFiles) { kokeile { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } saalis { } } $batchResults.ToArray() } foreach ($batch in $results) { jos ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } muu { # PS5.1 fallback: Peräkkäinen käsittely (edelleen nopea <10K-tiedostoille) ($file $Files) { kokeile { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } saalis { } } } palauta $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # Yksittäinen JSON-tiedosto jos ($InputPath -like "*.json") { $jsonContent = Get-Content -Path $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "Ladatut $($allDevices.Count) -tietueet tiedostosta" } muu { Write-Error "Vain JSON-muotoa tuetaan. Tiedostolla on oltava .json tunniste". exit 1 } } elseif (Test-Path $InputPath -PathType Container) { # Kansio – vain JSON $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Nimi -notmatch "ScanHistory|RolloutState|RolloutPlan" }) # Mieluummin *_latest.json tiedostoja, jos niitä on olemassa (konekohtainen tila) $latestJson = $jsonFiles | Where-Object { $_. Nimi -like "*_latest.json" } jos ($latestJson.Määrä -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count jos ($totalFiles -eq 0) { Write-Error "JSON-tiedostoja ei löytynyt: $InputPath" exit 1 } Write-Host "Found $totalFiles JSON files" -ForegroundColor Gray # Helper-funktio vastaa luottamustasoja (käsittelee sekä lyhyitä että täydellisiä lomakkeita) # Määritetty aikaisin, jotta sekä StreamingMode että normaalit polut voivat käyttää sitä funktio Test-ConfidenceLevel { param([string]$Value, [string]$Match) if ([string]::IsNullOrEmpty($Value)) { return $false } valitsin ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Observation*" } "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") } "Tilapäisesti keskeytetty" { return $Value -like "Temporarily Paused*" } "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") } oletusarvo { return $false } } } #region SUORATOISTOTILA – muistitehokas käsittely suurille tietojoukoille # Käytä StreamingModea aina muistin tehokkaaseen käsittelyyn ja uudentyyliseen koontinäyttöön jos (ei $StreamingMode) { Write-Host "StreamingModen automaattinen käyttöönotto (uuden tyylinen koontinäyttö)" -EtualallaColor Keltainen $StreamingMode = $true jos (ei $IncrementalMode) { $IncrementalMode = $true } } # Kun -StreamingMode on käytössä, käsittele tiedostoja paloina, jotka pitävät vain laskurit muistissa.# Laitetason tiedot kirjoitetaan JSON-tiedostoihin lohkokohtaista latausta varten koontinäytössä tarvittaessa.# Muistin käyttö: ~1,5 Gt tietojoukon koosta riippumatta (vs 10–20 Gt ilman suoratoistoa).jos ($StreamingMode) { Write-Host "SUORATOISTOTILA käytössä - muistitehokas käsittely" -EdustaVäri vihreä $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # ASTEITTAINEN TARKISTUS: Jos tiedostoja ei ole muutettu edellisen suorituksen jälkeen, ohita käsittely kokonaan jos ($IncrementalMode -ja -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".cache" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" if (Test-Path $stManifestPath) { Write-Host "Tarkistetaan muutoksia edellisen suoratoiston jälkeen..." -EtualallaColor Cyan $stOldManifest = Get-FileManifest -Path $stManifestPath jos ($stOldManifest.Määrä -gt 0) { $stChanged = $false # Pikatarkistus: sama tiedostomäärä? if ($stOldManifest.Count -eq $totalFiles) { # Tarkista 100 UUSIN-tiedostoa (lajiteltu LastWriteTime laskevan mukaan) # Jos jokin tiedosto on muuttunut, siinä on uusin aikaleima ja se näkyy ensimmäisenä $sampleSize = [matematiikka]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Laskeva | Select-Object -First $sampleSize foreach ($sf in $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() jos (ei $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true Tauko } # Vertaa aikaleimaa – välimuistissa voi olla DateTime tai merkkijono JSON roundtrip -funktion jälkeen $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc kokeile { # Jos välimuistissa on jo DateTime (ConvertFrom-Json auto-converts), käytä suoraan if ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } muu { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true Tauko } } saalis { $stChanged = $true Tauko } } } muu { $stChanged = $true } jos (ei $stChanged) { # Tarkista, onko tulostetiedostoja olemassa $stSummaryExists = Get-ChildItem (Liitospolku-$OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -Ensimmäinen 1 $stDashExists = Get-ChildItem (Liitospolku-$OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -Ensimmäinen 1 if ($stSummaryExists -and $stDashExists) { Write-Host " Ei havaittuja muutoksia ($totalFiles tiedostot muuttumattomina) - käsittelyn ohittaminen" -EdustaVäri vihreä Write-Host " Last dashboard: $($stDashExists.FullName)" -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv Write-Host " Laitteet: $($cachedStats.TotalDevices) | Päivitetty: $($cachedStats.Päivitetty) | Virheet: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host " Valmis $([math]:::Round($streamSw.Elapsed.TotalSeconds, 1))s (ei käsittelyä tarvita)" -EdustaVäri vihreä palauta $cachedStats } } muu { # DELTA PATCH: Etsi tarkalleen, mitkä tiedostot ovat muuttuneet Write-Host " Havaitut muutokset – muutettujen tiedostojen tunnistaminen..." -Edustaväri Keltainen $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf in $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } muu { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc kokeile { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } saalis { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [matematiikka]::Pyöristä(($totalChanged / $totalFiles) * 100, 1) Write-Host " Muutettu: $($changedFiles.Count) | Uusi: $($newFiles.Count) | Yhteensä: $totalChanged ($changePct%)" -Edustaväri Keltainen jos ($totalChanged -gt 0 -ja $changePct -lt 10) { # DELTA PATCH MODE: <10% muuttunut, korjaa olemassa olevat tiedot Write-Host " Delta-korjaustila ($changePct % < 10 %) – $totalChanged tiedostojen korjaus... -EtualallaColor Green $dataDir = Join-Path $OutputPath "tiedot" # Lataa muutetut/uudet laitetietueet $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df in $allDeltaFiles) { kokeile { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData jos ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev } } saalis { } } Write-Host " Ladattu $($deltaDevices.Count) muutti laitetietueita" -EtualallaColor Gray # Kullekin luokalle JSON: poista vanhat merkinnät muutetuista isäntänimistä, lisää uusia merkintöjä $categoryFiles = @("virheet", "known_issues", "missing_kek", "not_updated", "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required", "secureboot_off", "rollout_inprogress") $changedHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($hn in $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat in $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" if (Test-Path $catPath) { kokeile { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # Poista vanhat merkinnät muutetuista isäntänimistä $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) }) # Luokittele jokainen muutettu laite uudelleen luokiksi # (lisätään alla luokituksen jälkeen) $catData | ConvertTo-Json -Syvyys 5 | Set-Content $catPath -KOODaus UTF8 } saalis { } } } # Luokittele jokainen muutettu laite ja liitä oikeat luokkatiedostot foreach ($dev in $deltaDevices.Values) { $slim = [ordered]@{ HostName = $dev. Hostname WMI_Manufacturer = jos ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } muu { "" } WMI_Model = jos ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } muu { "" } BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } muu { "" } Luottamusväli = jos ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } muu { "" } IsUpdated = $dev. Vanhentunut UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } muu { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } muu { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } muu { $null } SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } muu { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = jos ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } muu { "" } $hasErr = (-not [merkkijono]::IsNullOrEmpty($dev. UEFICA2023Error) - ja $dev. UEFICA2023Error -ne "0" -ja $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false tai $dev. SecureBootTaskStatus -eq 'Disabled' tai $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -ja "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = jos ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } muu { 0 } $e 1808 = jos ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } muu { 0 } $e 1803 = jos ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } muu { 0 } $mKEK = ($e 1803 -gt 0 - tai $dev. MissingKEK -eq $true) $hKI = ((-not [merkkijono]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -tai (-not [merkkijono]::IsNullOrEmpty($dev. TunnettuIssueId))) $rStat = jos ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } muu { "" } # Liitä vastaaviin luokkatiedostoihin $targets = @() jos ($isUpd) { $targets += "updated_devices" } jos ($hasErr) { $targets += "virheet" } jos ($hKI) { $targets += "known_issues" } jos ($mKEK) { $targets += "missing_kek" } jos (ei $isUpd -ja $sbOn) { $targets += "not_updated" } jos ($tskDis) { $targets += "task_disabled" } jos (ei $isUpd -ja ($tskDis -tai (Test-ConfidenceLevel $conf 'Tilapäisestikäytetty')) { $targets += "temp_failures" } jos (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -tai ($tskNF -and $hasErr)) { $targets += "perm_failures" } jos (ei $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" } jos (ei $sbOn) { $targets += "secureboot_off" } jos ($e 1801 -gt 0 -ja $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } foreach ($tgt $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 -Syvyys 5 | Set-Content $tgtPath -KOODaus UTF8 } } } # Luo CSV:t uudelleen korjatuilta JSON-näiltä Write-Host " CSV:iden uudelleenyhteensyöttäminen korjatuista tiedoista..." -Edustaväri Harmaa $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat in $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (Test-Path $catJsonPath) { kokeile { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json if ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8 } } saalis { } } } # Uudelleenlasketut tilastot paikatuista JSON-tiedostoista Write-Host " Yhteenvedon laskeminen uudelleen korjatuista tiedoista..." -EdustaväriVärin harmaa $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 in $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Laske } saalis { } } valitsin ($cat) { "updated_devices" { $pUpdated = $cnt } "virheet" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # laskettu "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). Laskea $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host " Delta-korjaus valmis: $totalChanged laitteet päivitetty" -ForegroundColor Green Write-Host Yhteensä: $pTotal | Päivitetty: $pUpdated | Ei vanhentunut: $pNotUpdated | Virheet: $pErrors" -ForegroundColor White # Päivitä luettelo $stManifestDir = Join-Path $OutputPath ".cache" $stNewManifest = @{} foreach ($jf in $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Koko = $jf. Pituus } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " Valmis $([math]:::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green # Siirry täydelliseen suoratoiston uudelleenkäsittelyyn HTML-koontinäytön uudistamiseksi # Datatiedostot on jo korjattu, joten tämä varmistaa, että koontinäyttö pysyy ajan tasalla Write-Host " Raporttinäkymän uudistaminen korjatuista tiedoista..." -Edustaväri Keltainen } muu { Write-Host " $changePct% tiedostoja muutettu (>= 10 %) - täyden suoratoiston uudelleenkäsittely vaaditaan" -EdustaVäri Keltainen } } } } } # Luo datan alihakemisto tarvittaessa -laitteen JSON-tiedostoille $dataDir = Join-Path $OutputPath "tiedot" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } # Deduplication via HashSet (O(1) per haku, ~50 Mt 600K isäntänimille) $seenHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Kevyet yhteenvetolaskurit (korvaa $allDevices + $uniqueDevices muistissa) $c = @{ Yhteensä = 0; SBEnabled = 0; SBOff = 0 Päivitetty = 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 } # Säilön seuranta AtRiskille/SafeListille (kevyet sarjat) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # Erätilan laitteen datatiedostot: kerätään lohkoa kohti, tyhjennys lohkojen rajojen yli $stDeviceFiles = @("virheet", "known_issues", "missing_kek", "not_updated", "task_disabled", "temp_failures", "perm_failures", "updated_devices", "action_required", "secureboot_off", "rollout_inprogress", "under_observation", "needs_reboot", "update_pending") $stDeviceFilePaths = @{}; $stDeviceFileCounts = @{} foreach ($dfName in $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # Slim device record for JSON output (only essential fields, ~200 tavua vs ~2KB full) funktio Get-SlimDevice { param($Dev) return [ordered]@{ HostName = $Dev.HostName WMI_Manufacturer = jos ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } muu { "" } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } muu { "" } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } muu { "" } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } muu { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } muu { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } muu { "" } KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } muu { $null } SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } muu { $null } UEFICA2023Status = jos ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } muu { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } muu { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } muu { $null } } } # Tyhjennä erä JSON-tiedostoon (liittämistila) funktio Flush-DeviceBatch { param([string]$StreamName, [System.Collections.Generic.List[object]]$Batch) jos ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev in $Batch) { jos ($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) } # TÄRKEIN SUORATOISTOSILMUKKA $stChunkSize = jos ($totalFiles -le 10000) { $totalFiles } muu { 10000 } $stTotalChunks = [matematiikka]::Katto($totalFiles / $stChunkSize) $stPeakMemMB = 0 jos ($stTotalChunks -gt 1) { Write-Host "$totalFiles tiedostojen käsittely $stTotalChunks $stChunkSize (suoratoisto, $ParallelThreads säikeet):" -Edustavärisyan } muu { Write-Host "Käsitellään $totalFiles tiedostoja (suoratoisto, $ParallelThreads säikeet):" -EtualallaColor Cyan } kohteelle ($ci = 0; $ci -lt $stTotalChunks; $ci++) { $cStart = $ci * $stChunkSize $cEnd = [math]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] jos ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) -tiedostot): " -NoNewline -ForegroundColor Gray } muu { Write-Host " Ladataan $($cFiles.Count) tiedostoja: " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads # Lohkokohtaiset eräluettelot $cBatches = @{} foreach ($df in $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0; $cDupe = 0 foreach ($raw $rawDevices) { jos (ei $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. Hostname jos (ei $hostname) { continue } jos ($seenHostnames.Contains($hostname)) { $cDupe++; continue } [void]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -ja "$($device. SecureBootEnabled)" -ne "False") jos ($sbOn) { $c.SBEnabled++ } muuta { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = jos ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } muu { "" } $hasErr = (-not [merkkijono]::IsNullOrEmpty($device. UEFICA2023Error) -ja "$($device. UEFICA2023Error)" -ne "0" -ja "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -tai "$($device. SecureBootTaskStatus)" -eq 'Disabled' tai "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device). SecureBootTaskStatus)" -eq 'NotFound') $bid = jos ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } muu { "" } $e 1808 = jos ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } muu { 0 } $e 1801 = jos ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } muu { 0 } $e 1803 = jos ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } muu { 0 } $mKEK = ($e 1803 -gt 0 - tai $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True") $hKI = ((-not [merkkijono]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -tai (-not [merkkijono]::IsNullOrEmpty($device. TunnettuIssueId))) $rStat = jos ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } muu { "" } $mfr = jos ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } muu { "Tuntematon" } $bid = jos (-not [merkkijono]::IsNullOrEmpty($bid)) { $bid } muu { "" } # Pre-compute Update Pending flag (käytäntö/WinCS käytössä, tila ei ole vielä Päivitetty, SB ON, tehtävä ei ole poissa käytöstä) $uefiStatus = jos ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } muu { "" } $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy -and "$($device. AvailableUpdatesPolicy)" -ne '') $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied -eq $true) $statusPending = ([merkkijono]::IsNullOrEmpty($uefiStatus) -tai $uefiStatus -eq 'Ei käynnistynyt' -tai $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -tai $hasWinCS) -ja $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis) jos ($isUpd) { $c.Päivitetty++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # Seuraa päivitettyjä laitteita, jotka tarvitsevat uudelleenkäynnistyksen (UEFICA2023Status=Päivitetty, mutta Tapahtuma1808=0) jos ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (ei $sbOn) { # SecureBoot OFF – ei kuulu alueeseen, älä luokita luottamuksella } muu { if ($isUpdatePending) { } # Counted separateed in Update Pending — mutually exclusive for pie chart elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "Tilapäisesti") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ } muu { $c.ActionReq++ } if ([string]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } jos ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } if ($tskNF) { $c.TaskNotFound++ } jos (ei $isUpd -ja $tskDis) { $c.TaskDisabledNotUpdated++ } jos ($hasErr) { $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["virheet"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Virhe if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ jos ($stErrorCodeSamples[$ec]. Määrä -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } jos ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = if (-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } muu { $device. TunnettuIssueId } jos (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } jos ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } jos (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } jos (ei $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -tai ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } jos ($e 1801 -gt 0 -ja $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } jos ($e 1801 -gt 0 -ja $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ } jos ($rStat -eq "InProgress" -ja $e 1808 -eq 0) { $c.InProgress++ } # Päivitys odottaa: käytäntö tai WinCS käytössä, tila odottaa, SB ON, tehtävää ei ole poistettu käytöstä jos ($isUpdatePending) { $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } jos (ei $isUpd -ja $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # Kohdassa Tarkkailulaitteet (erillään toiminnosta Pakollinen) jos (ei $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # Toiminto Pakollinen: ei päivitetty, SB ON, ei vastaa muita luottamusluokat, ei Päivitä odottaa jos (ei $isUpd - ja $sbOn -ja -ei $isUpdatePending -ja -not (Testi-Luottamustaso $conf 'HighConfidence') -ja -not (Test-ConfidenceLevel $conf 'UnderObservation') -ja -not (Test-ConfidenceLevel $conf 'TilapäisestiKäytetty') -ja -not (Test-ConfidenceLevel $conf 'Ei tueta')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } jos (ei $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Päivitetty=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Yhteensä++ jos ($isUpd) { $stMfrCounts[$mfr]. Päivitetty++ } elseif (ei $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++ } muu { $stMfrCounts[$mfr]. ActionReq++ } jos ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # Seuraa kaikkia laitteita säilön mukaan (mukaan lukien tyhjä BucketId) $bucketKey = jos ($bid -ja $bid -ne "") { $bid } muu { "(tyhjä)" } jos (ei $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; Päivitetty=0; Manufacturer=$mfr; Model=""; BIOS="" } jos ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Malli = $device. WMI_Model } jos ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ jos ($isUpd) { $stAllBuckets[$bucketKey]. Päivitetty++ } } # Tyhjennä erät levylle foreach ($df in $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null; $cBatches = $null; [System.GC]::Collect() $cSw.Stop() $cTime = [Math]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = jos ($cRem -gt 0) { " | ETA: ~$([Math]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) min" } muu { "" } $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) jos ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew uusi, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # Viimeistele JSON-matriisit foreach ($dfName $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) laitteet" -ForegroundColor DarkGray } # Laske johdetut tilastot $stAtRisk = 0; $stSafeList = 0 foreach ($bid in $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]; $nu = $b.Count – $b.Updated jos ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [matematiikka]::Maks.0, $stAtRisk - $c.WithErrors) # NotUptodate = count from not_updated batch (devices with SB ON and not updated) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff Päivitetty = $c.Päivitetty; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; Tilapäisesti Poistettu = $c.TempPaused; NotSupported = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; Täysin päivitetty = $c.Päivitetty PäivityksetKäyttö = $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) } muu { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } muu { 0 } PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } muu { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } muu { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } muu { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } muu { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } muu { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } muu { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming" } # Kirjoita CSV:t [PSCustomObject]$stats | Export-Csv -Polku (liitospolku $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -KOODAUS UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Laskeva | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. Avain; Count=$_. Arvo.Summa; Päivitetty=$_. Arvo.Päivitetty; HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Polku (liitospolku $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -KOODAUS UTF8 $stErrorCodeCounts.GetEnumerator() | Sort-Object arvo -Laskeva | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. Avain; Count=$_. Arvo; SampleDevices=($stErrorCodeSamples[$_. Key] -join ", ") } } | Export-Csv -Polku (liitospolku $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -KOODAUS UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Laskeva | ForEach-Object { [PSCustomObject]@{ BucketId=$_. Avain; Count=$_. Arvo.määrä; Päivitetty=$_. Arvo.Päivitetty; NotUpdated=$_. Value.Count-$_. Arvo.Päivitetty; Manufacturer=$_. Value.Manufacturer } } | Export-Csv -Polku (liitospolku $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -KOODAUS UTF8 # Luo orkestraattoriyhteensopivia CSV-tiedostoja (Start-SecureBootRolloutOrchestrator.ps1 odotetut tiedostonimet) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (Test-Path $notUpdatedJsonPath) { kokeile { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json jos ($nuData.Määrä -gt 0) { # NotUptodate CSV - orchestrator hakee hakua *NotUptodate*.csv $nuData | Export-Csv -Polku (liitospolku $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -KOODAUS UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) devices)" -ForegroundColor Gray } } saalis { } } # JSON-tietojen kirjoittaminen koontinäyttöön $stats | ConvertTo-Json -Syvyys 3 | Set-Content (liitospolku $dataDir "summary.json") -KOODaus UTF8 # HISTORIALLINEN SEURANTA: Arvopisteen tallentaminen trendikaavioon # Käytä vakaata välimuistisijaintia, jotta trenditiedot säilyvät aikaleimatuissa koostekansioissa. # Jos OutputPath näyttää "...\Aggregation_yyyyMMdd_HHmmss", välimuisti siirtyy pääkansioon.# Muussa tapauksessa välimuisti siirtyy Itse OutputPathiin.$parentDir = Split-Path $OutputPath -Parent $leafName = Split-Path $OutputPath -Lehti jos ($leafName -match '^Aggregation_\d{8}' -tai $leafName -eq 'Aggregation_Current') { # Orchestratorin luoma aikaleimattu kansio – käytä päätasoa vakaassa välimuistissa $historyPath = Join-Path $parentDir ".cache\trend_history.json" } muu { $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) { kokeile { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } saalis { $historyData = @() } } # Tarkista myös kohdasta OutputPath\.cache\ (aiempien versioiden sijainti) # Yhdistä kaikki arvopisteet, jotka eivät vielä ole ensisijaisessa historiassa jos ($leafName -eq 'Aggregation_Current' -tai $leafName -match '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { kokeile { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. Päivämäärä }) foreach ($entry in $innerData) { jos ($entry. Päivämäärä ja $entry. Date -notin $existingDates) { $historyData += $entry } } jos ($innerData.Määrä -gt 0) { Write-Host " Yhdistetyt $($innerData.Count) -arvopisteet sisäisestä välimuistista" -EtualallaColor DarkGray } } saalis { } } }
# BOOTSTRAP: Jos trendihistoria on tyhjä/harva, rekonstruoi historiatiedoista jos ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host " Bootstrapping trend history from historical data..." -ForegroundColor Yellow $dailyData = @{} # Lähde 1: Yhteenveto CSV-tiedostoista nykyisessä kansiossa (Aggregation_Current säilyttää kaikki csv-yhteenvetotiedostot) $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object nimi foreach ($summCsv in $localSummaries) { kokeile { $summ = Import-Csv $summCsv.FullName | Select-Object -Ensimmäinen 1 jos ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -ja $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yy-MM-dd") $updated = jos ($summ. Päivitetty) { [int]$summ. Päivitetty } muu { 0 } $notUpd = jos ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Date = $dateStr; Total = [int]$summ. TotalDevices; Päivitetty = $updated; NotUpdated = $notUpd NeedsReboot = 0; Virheet = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } muu { 0 } } } } saalis { } } # Lähde 2: Vanhat aikaleimatut Aggregation_* -kansiot (vanhat, jos ne ovat edelleen olemassa) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. Nimi -match '^Aggregation_\d{8}' } | Sort-Object nimi foreach ($folder in $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -Ensimmäinen 1 jos ($summCsv) { kokeile { $summ = Import-Csv $summCsv.FullName | Select-Object -Ensimmäinen 1 jos ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Nimi -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = jos ($summ. Päivitetty) { [int]$summ. Päivitetty } muu { 0 } $notUpd = jos ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ Date = $dateStr; Total = [int]$summ. TotalDevices; Päivitetty = $updated; NotUpdated = $notUpd NeedsReboot = 0; Virheet = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } muu { 0 } } } } saalis { } } } # Lähde 3: RolloutState.json WaveHistory (sisältää aaltokohtaisia aikaleimat päivästä 1 alkaen) # Tämä antaa perusaikataulun arvopisteitä, vaikka vanhoja koostekansioita ei olisi $rolloutStatePaths = @( (Liitospolku-$parentDir "RolloutState\RolloutState.json"), (Liitospolku-$OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath in $rolloutStatePaths) { if (Test-Path $rsPath) { kokeile { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # Käytä aallon aloituspäivämääriä trendien arvopisteinä # Laske kuhunkin aaltoon kohdistetut kumulatiiviset laitteet $cumulativeTargeted = 0 foreach ($wave in $rsData.WaveHistory) { jos ($wave. StartedAt ja $wave. DeviceCount) { $waveDate = ([datetime]$wave. StartedAt). ToString("yy-MM-dd") $cumulativeTargeted += [int]$wave. DeviceCount if (-not $dailyData.ContainsKey($waveDate)) { # Likimääräinen: aaltojen alkamisaikana päivitettiin vain aiempien aaltojen laitteita $dailyData[$waveDate] = [PSCustomObject]@{ Date = $waveDate; Yhteensä = $c.Yhteensä; Päivitetty = [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount) NeedsReboot = 0; Virheet = 0; ActionRequired = 0 } } } } } } saalis { } break # Use first found } }
jos ($dailyData.Määrä -gt 0) { $historyData = @($dailyData.GetEnumerator() | Sort-Object key | ForEach-Object { $_. Arvo }) Write-Host " Bootstrapped $($historyData.Count) -arvopisteet historiallisista yhteenvetoista" -EtualallaColor Green } }
# Lisää nykyinen arvopiste (deduplicate by day - keep latest per day) $todayKey = (get-date). ToString("yy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" } jos ($existingToday) { # Korvaa tämän päivän merkintä $historyData = @($historyData | Where-Object { "$($_). Date)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ Date = $todayKey Yhteensä = $c.Yhteensä Päivitetty = $c.Päivitetty NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot Virheet = $c.WithErrors ActionRequired = $c.ActionReq } # Poista virheellinen arvopiste (yhteensä 0) ja säilytä viimeiset 90 $historyData = @($historyData | Where-Object { [int]$_. Yhteensä -gt 0 }) # Ei ylärajaa – trenditiedot ovat ~100 tavua/merkintä, koko vuosi = ~36 KT $historyData | ConvertTo-Json -Syvyys 3 | Set-Content $historyPath -KOODaus UTF8 Write-Host " Trendihistoria: $($historyData.Count) arvopisteet" -EtualallaColor DarkGray # Html-kielen trendikaavion tietojen luominen $trendLabels = ($historyData | ForEach-Object { "'$($_). Date)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. Päivitetty }) -join "," $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Yhteensä }) -join "," # Projektio: laajenna trendiviivaa eksponentiaalisen kaksinkertaistamisen avulla (2,4,8,16...) # Johtaa aallon koon ja havaintojakson todellisista trendihistoriatiedoista.# - Aaltokoko = historian suurin yksittäisen jakson lisäys (viimeisin käytössä oleva aalto) # - Havaintopäivät = keskimääräinen kalenteripäivä trendiarvopisteiden välillä (kuinka usein suoritamme) # Sitten kaksinkertaistaa aallon koon jokaisella jaksolla, joka vastaa orkestraattorin 2x-kasvustrategiaa.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Päivitetty $remaining = $stNotUptodate # Vain SB-ON ei-päivitetyt laitteet (pois lukien SecureBoot EI KÄYTÖSSÄ) $projDates = @(); $projValues = @(); $projNotUpdValues = @() $projDate = Get-Date
# Johtaa aaltojen kokoa ja havaintojaksoa trendihistoriasta $increments = @() $dayGaps = @() kohteelle ($hi = 1; $hi -lt $historyData.Count; $hi++) { $inc = $historyData[$hi]. Päivitetty – $historyData[$hi-1]. Päivitetty jos ($inc -gt 0) { $increments += $inc } kokeile { $d 1 = [datetime]::P arse($historyData[$hi-1]. Päivämäärä) $d 2 = [datetime]::P arse($historyData[$hi]. Päivämäärä) $gap = ($d 2 – $d 1). TotalDays jos ($gap -gt 0) { $dayGaps += $gap } } saalis {} } # Aallon koko = viimeisin positiivinen lisäys (nykyinen aalto), varautuminen keskiarvoon, vähintään 2 $waveSize = jos ($increments. Määrä -gt 0) { [math]::Max(2, $increments[-1]) } muu { 2 } # Havaintojakso = arvopisteiden (kalenteripäivien aaltokohtainen) keskimääräinen ero, vähimmäisarvo 1 $waveDays = jos ($dayGaps.Määrä -gt 0) { [math]::Max(1, [math]::Round(($dayGaps | Measure-Object -Average). Keskiarvo, 0)) } muu { 1 }
Write-Host " Projektio: waveSize=$waveSize (viimeisestä lisäydestä), waveDays=$waveDays (avg-aukko historiasta)" -EtualallaColor DarkGray
$dayCounter = 0 # Project, kunnes kaikki laitteet on päivitetty tai enintään 365 päivää kohteelle ($pi = 1; $pi -le 365; $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # Jokaisella havaintojakson rajalla, ota aalto käyttöön ja sitten kaksoisnapauta jos ($dayCounter -ge $waveDays) { $devicesThisWave = [matematiikka]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave jos ($lastUpdated -gt ($c.Päivitetty + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # Double wave size for next period (orchestrator 2x strategy) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd")'" $projValues += $lastUpdated $projNotUpdValues += [matematiikka]::Maks.(0, $remaining) jos ($remaining -le 0) { break } } $projLabels = $projDates -join "," $projUpdated = $projValues -join "," $projNotUpdated = $projNotUpdValues -join "," $hasProjection = $projDates.Määrä -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host " Projektio: tarvitset vähintään kaksi trendiarvopistettä aaltojen ajoituksen määrittämiseen" -EtualallaColor DarkGray } # Muodosta yhdistetyt kaavion tietomerkkijonot tätä merkkijonoa varten $allChartLabels = jos ($hasProjection) { "$trendLabels,$projLabels" } muu { $trendLabels } $projDataJS = jos ($hasProjection) { $projUpdated } muu { "" } $projNotUpdJS = jos ($hasProjection) { $projNotUpdated } muu { "" } $histCount = ($historyData | Mittaobjekti). Laskea $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Laskeva | ForEach-Object { @{ name=$_. Avain; total=$_. Arvo.Summa; updated=$_. Arvo.Päivitetty; highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Syvyys 3 | Set-Content (liitospolku $dataDir "manufacturers.json") -KOODaus UTF8 # Muunna JSON-datatiedostot CSV:ksi ihmisten luettavia Excel-latauksia varten Write-Host "Laitetietojen muuntaminen CSV for Excel -lataukseksi..." -EtualallaColor Harmaa foreach ($dfName in $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" if (Test-Path $jsonFile) { kokeile { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json jos ($jsonData.Määrä -gt 0) { # Sisällytä csv-update_pending ylimääräiset sarakkeet $selectProps = jos ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } muu { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'OnUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownOwnIssue') } $jsonData | Select-Object $selectProps | Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8 Write-Host " $dfName -> $($jsonData.Count) rivit -> CSV" -ForegroundColor DarkGray } } catch { Write-Host " $dfName - skipped" -ForegroundColor DarkYellow } } } # Luo itsenäinen HTML-koontinäyttö $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "Itsenäisen HTML-koontinäytön luominen..." -EtualallaVäri keltainen # VELOCITY PROJECTION: Laske skannaushistoriasta tai edellisestä yhteenvedosta $stDeadline = [datetime]"2026-06-24" # KEK cert expiry $stDaysToDeadline = [math]::Max(0, ($stDeadline - (Get-Date)). Päivät) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "PUUTTUU" $stWorkingDays = 0 $stCalendarDays = 0 # Kokeile ensin trendihistoriaa (kevyt, koostimen ylläpitämä – korvaa paisuneet ScanHistory.json) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Yhteensä -gt 0 -ja [int]$_. Päivitetty -ge 0 }) jos ($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 jos ($daysDiff -gt 0) { $updDiff = [int]$curr. Päivitetty – [int]$prev. Päivitetty jos ($updDiff -gt 0) { $stDevicesPerDay = [math]::Round($updDiff / $daysDiff, 0) $stVelocitySource = "TrendHistory" } } } } # Kokeile orkestraattorin käyttöönottoyhteenvetoa (jossa on ennalta laskettu nopeus) jos ($stVelocitySource -eq "N/A" -ja $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { kokeile { $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) { $stDevicesPerDay = [math]::Round([double]$rolloutSummary.DevicesPerDay, 1) $stVelocitySource = "Orchestrator" if ($rolloutSummary.ProjectedCompletionDate) { $stProjectedDate = $rolloutSummary.ProjectedCompletionDate } if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } saalis { } } # Fallback: kokeile edellistä yhteenvetoa CSV (hae nykyisestä kansiosta JA pääkansiosta tai rinnakkaisesta koostekansiosta) jos ($stVelocitySource -eq "PUUTTUU") { $searchPaths = @( (Liitospolku-$OutputPath "SecureBoot_Summary_*.csv") ) # Hae myös rinnakkaisia koostekansioita (orkestrointi luo uuden kansion joka suorituskerrat) $parentPath = Split-Path $OutputPath -Parent jos ($parentPath) { $searchPaths += (Liitospolku-$parentPath "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += (Liitospolku-$parentPath "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Laskeva | Select-Object -First 1 jos ($prevSummary) { kokeile { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) – $prevDate). TotalDays jos ($daysSinceLast -gt 0,01) { $prevUpdated = [int]$prevStats.Päivitetty $updDelta = $c.Päivitetty – $prevUpdated jos ($updDelta -gt 0) { $stDevicesPerDay = [math]::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = "PreviousReport" } } } saalis { } } } # Fallback: laske nopeus koko trendihistorian ajalta (ensimmäinen vs. viimeisin arvopiste) jos ($stVelocitySource -eq "N/A" -ja $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Yhteensä -gt 0 -ja [int]$_. Päivitetty -ge 0 }) jos ($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 jos ($daysDiff -gt 0) { $updDiff = [int]$last. Päivitetty – [int]$first. Päivitetty jos ($updDiff -gt 0) { $stDevicesPerDay = [matematiikka]::Pyöristä($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # Laske projektio käyttämällä eksponentiaalista kaksinkertaistamista (yhdenmukainen trendikaavion kanssa) # Käytä kaaviolle jo laskettuja projektiotietoja uudelleen, jos ne ovat käytettävissä jos ($hasProjection -ja $projDates.Count -gt 0) { # Käytä viimeistä arvioitua päivämäärää (kun kaikki laitteet päivitetään) $lastProjDateStr = $projDates[-1] -korvaa "'", "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). Päivää $stWorkingDays = 0 $d = Get-Date kohteelle ($i = 0; $i -lt $stCalendarDays; $i++) { $d = $d.AddDays(1) jos ($d.DayOfWeek -ne 'lauantai' -ja $d.DayOfWeek -ne 'Sunnuntai') { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -ja $stNotUptodate -gt 0) { # Fallback: lineaarinen projektio, jos eksponentiaalisia tietoja ei ole saatavilla $daysNeeded = [matematiikka]::Katto($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy") $stWorkingDays = 0; $stCalendarDays = $daysNeeded $d = Get-Date kohteelle ($i = 0; $i -lt $daysNeeded; $i++) { $d = $d.AddDays(1) jos ($d.DayOfWeek -ne 'Lauantai' -ja $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ } } } # Build velocity HTML $velocityHtml = jos ($stDevicesPerDay -gt 0) { "<div><vahva>🚀 Laitteet/päivä:</strong> $($stDevicesPerDay.ToString('N0')) (lähde: $stVelocitySource)</div>" + "<div><vahva>📅 Arvioitu valmistuminen:</strong> $stProjectedDate" + $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>⚠ PAST DEADLINE</span>" } else { " <span style='color:#28a745'>✓ Before deadline</span>" }) + "</div>" + "<div><vahva>🕐 Työpäivät:</strong> $stWorkingDays | <vahva>Calendar Päivät:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>Deadline: 24. kesäkuuta 2026 (KEK-varmenteen voimassaolo päättyy) | Jäljellä olevat päivät: $stDaysToDeadline</div>" } muu { "<div style='pehmuste:8px; background:#fff3cd; reunan säde:4px; vasen reuna:3px kiinteä #ffc107'>" + "<vahva>📅 Ennustettu valmistuminen:</strong> Riittämättömät tiedot nopeuden laskentaan. " + "Suorita kooste vähintään kaksi kertaa tietojen muutoksilla, jotta saadaan aikaan korko.<br/>" + "<vahva>Määräaika:</strong> 24.6.2026 (KEK-varmenteen voimassaolon päättyminen) | <vahva>Jäljellä olevat päivät:</strong> $stDaysToDeadline</div>" } # Varmente erääntymislaskuri $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematiikka]::Maks(0, ($certKekExpiry - $certToday). Päivät) $daysToUefi = [matematiikka]::Maks(0, ($certUefiExpiry - $certToday). Päivät) $daysToPca = [matematiikka]::Maks(0, ($certPcaExpiry - $certToday). Päivät) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } muu { '#28a745' } # Helper: Lue tietueet JSON:sta, koontiversion säilön yhteenveto + ensimmäiset N-laiterivit $maxInlineRows = 200 funktio Build-InlineTable { param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 if (Test-Path $JsonPath) { kokeile { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. Laskea # BUCKET SUMMARY: Group by BucketId, show counts per bucket with Updated from global bucket stats jos ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | Sort-Object laskeva määrä $bucketSummary = "><2 h3 style='font-size:.95em; väri:#333; margin:10px 0 5px'><3 Hardware Bucket ($($buckets). Laske) säilöt)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto; margin-bottom:15px'><table><thead><tr><th><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Päivitetty</th><th style='text-align:right; color:#dc3545'>Not Updated</th><th><1 Manufacturer><2 /th></tr></thead><tbody>" foreach ($b $buckets) { $bid = if ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer # Nouda päivitetty määrä yleisistä säilötilastoista (kaikki tämän säilön laitteet koko tietojoukossa) $lookupKey = $bid $globalBucket = jos ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } muu { $null } $bUpdatedGlobal = jos ($globalBucket) { $globalBucket.Updated } muu { 0 } $bTotalGlobal = jos ($globalBucket) { $globalBucket.Count } muu { $b.Count } $bNotUpdatedGlobal = $bTotalGlobal – $bUpdatedGlobal $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; fontin leveys:bold'>$bTotalGlobal><8 /td><td style='text-align:right; color:#28a745; fontin leveys:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; color:#dc3545; fontin leveys:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n" } $bucketSummary += "</tbody></table></div>" } # LAITTEEN TIEDOT: Ensimmäiset N rivit litteänä luettelona $slice = $data | Select-Object -First $MaxRows foreach ($d in $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge badge-danger">Not Supported><6 /span>' } elseif ($conf -match "Under") { '<span class="badge badge-info">Under Obs><0 /span>' } elseif ($conf -match "Paused") { '<span class="badge badge-warning">Paused><4 /span>' } muu { '<span class="badge badge-warning">Action Req><8 /span>' } $statusBadge = if ($d.IsUpdated) { '><00 span class="badge-success"><01 Updated</span>' } elseif ($d.UEFICA2023Error) { '><04 span class="badge-danger"><05 Error</span>' } muu { '><08 span class="badge badge-warning"><09 Pending><0 /span>' } $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 /td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n" } } saalis { } } jos ($totalCount -eq 0) { palauta "><44 div style='pehmuste:20px; väri:#888; fonttityyli:kursivointi><45 Tässä luokassa ei ole laitteita.><46 /div>" } $showing = [matematiikka]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; fonttikoko:.85em; väri:#666'><49 Yhteensä: $($totalCount.ToString("N0")) laitteet" jos ($CsvFileName) { $header += " | ><50 href='$CsvFileName' style='color:#1a237e; fontin leveys:bold'>📄 Lataa Excelin täydellinen CSV-tiedosto><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; väri:#333; margin:10px 0 5px'><58 Device Details (näyttää ensimmäisen $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><0 HostName><1 /th><th><4 Manufacturer><5 /th><th><8 Model><9 /th><th><2 Confidence><3 /th><th><6 Status><7 /th><th><0 Error><1 /th><th><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>" palauta "$header$bucketSummary$deviceHeader$deviceTable" } # Muodosta taulukoita JSON-tiedostoista, jotka ovat jo levyllä, linkittäen CSV-tiedostoihin $tblErrors = Build-InlineTable -JsonPath (Join-Path $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath (Join-Path $dataDir "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath (Join-Path $dataDir "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath (Join-Path $dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath (Join-Path $dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath (Join-Path $dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath (Join-Path $dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath (Join-Path $dataDir "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath (Join-Path $dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath (Join-Path $dataDir "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath (Join-Path $dataDir "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath (join-path $dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath (Join-Path $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # Odottavan päivityksen mukautettu taulukko – sisältää UEFICA2023Status- ja UEFICA2023Error-sarakkeet $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (Test-Path $upJsonPath) { kokeile { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Määrä jos ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; fonttikoko:.85em; väri:#666'>Yhteensä: $($upCount.ToString("N0")) laitteet | <href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; fontin leveys:bold'>📄 Lataa Excelin täydellinen CSV-tiedosto><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object -First $maxInlineRows foreach ($d in $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } muu { '<span style="color:#999">null><0 /span>' } $uefiErr = jos ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } muu { '-' } $policyVal = jos ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } muu { '-' } $wincsVal = jos ($d.WinCSKeyApplied) { '<span class="badge-success">Yes><8 /span>' } muu { '-' } $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 = [math]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; väri:#333; margin:10px 0 5px'>Device Details (näyttää ensimmäisen $upShowing)</h3>" $upTable = "<div style='max-height:500px; overflow-y:auto'><table><thead><tr><th><9 HostName><0 /th><th><3 Manufacturer><4 /th><th><7 Model><8 /th><th><1 UEFICA2023Status><2 /th><th><5 UEFICA2023Virhe><6 /th><th><9 Policy</th><th>WinCS Key</th><th><BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } muu { $tblUpdatePending = "<div style='pehmuste:20px; väri:#888; fonttityyli:kursivointi >Ei laitteita tässä luokassa.</div>" } } saalis { $tblUpdatePending = "<div style='pehmuste:20px; väri:#888; fonttityyli:kursivointi >Ei laitteita tässä luokassa.</div>" } } muu { $tblUpdatePending = "<div style='pehmuste:20px; väri:#888; fonttityyli:kursivointi'>Ei laitteita tässä luokassa.</div>" } # Varmente erääntymislaskuri $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [matematiikka]::Maks(0, ($certKekExpiry - $certToday). Päivät) $daysToUefi = [matematiikka]::Maks(0, ($certUefiExpiry - $certToday). Päivät) $daysToPca = [matematiikka]::Maks(0, ($certPcaExpiry - $certToday). Päivät) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } muu { '#28a745' } # Koontiversion valmistajan kaavion tiedot tekstissä (ensimmäiset 10 laitemäärän mukaan) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Laskeva | Select-Object -Ensimmäinen 10 $mfrChartTitle = jos ($stMfrCounts.Count -le 10) { "Valmistajan mukaan" } muu { "Top 10 Manufacturers" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_). Näppäin)'" }) -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 "," # Koontiversion valmistajan taulukko $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Laskeva | 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' } # HTML close-tag fragments: broken into parts so web CMS platforms don't # tulkitse ne todellisiksi HTML-merkeiksi ja lisää näkymättömiä Unicode-merkkejä niiden ympärille.$endScript = "</scr" + "ipt>" $endStyle = '</sty' + 'le>' $endHead = "</he" + "ad>" $endBody = '</bo' + 'dy>' $endHtml = "</ht" + "ml>" $htmlContent = @" <! DOCTYPE html -> <html lang="en"> <pään> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <otsikko suojatun käynnistyksen varmenteen tilan koontinäytön></title> <komentosarja src="https://cdn.jsdelivr.net/npm/chart.js">$endScript <tyylin> *{box-sizing:border-box; reunus:0; täyttö:0} body{font-family:'Segoe UI',Tahoma,sans-serif; background:#f0f2f5; väri:#333} .header{background:lineaarinen liukuväri(135deg,#1a237e,#0d47a1); color:#fff; täyttö:20px 30px} .header h1{font-size:1.6em; margin-bottom:5px} .header .meta{font-size:.85em; läpinäkymättömyys:.9} .container{max-width:1400px; reunus:0 automaattinen; täyttö:20px} .cards{display:grid; ruudukkomallisarakkeet:repeat(auto-fill,minmax(170px,1fr)); gap:12px; margin:20px 0} .card{background:#fff; reunasäde:10px; täyttö:15px; box-shadow:0 2px 8px rgba(0,0,0,08); reuna vasen:4px kiinteä #ccc;siirtymä:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,12)} .card .value{font-size:1.8em; fonttipaino:700} .card .label{font-size:.8em; väri:#666; margin-top:4px} .card .pct{font-size:.75em; väri:#888} .section{background:#fff; reunasäde:10px; täyttö:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,08)} .section h2{font-size:1.2em; color:#1a237e; margin-bottom:10px; kohdistin:osoitin; user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; ruudukkomallisarakkeet:1fr 1fr; gap:20px; margin:20px 0} .chart-box{background:#fff; reunasäde:10px; täyttö:20px; box-shadow:0 2px 8px rgba(0,0,0,08)} taulukko{width:100%; reunan kutistaminen:kutistaminen; fonttikoko:.85em} th{background:#e8eaf6; täyttö:8 x 10 kuvapistettä; tekstin tasaaminen:vasen; sijainti:tahmea; ylhäällä:0; z-indeksi:1} td{padding:6px 10px; reunan alareuna:1px kiinteä #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; täyttö:2px 8px;reunasäde:10px; fonttikoko:.75em; fonttipaino:700} .badge-success{background:#d4edda; väri:#155724} .badge-danger{background:#f8d7da; color:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; color:#0c5460} .top-link{float:right; fonttikoko:.8em; color:#1a237e; text-decoration:none} .footer{text-align:center; täyttö:20px; väri:#999; fonttikoko:.8em} a{color:#1a237e}$endStyle $endHead <leipätekstin> <div class="header"> <h1>suojatun käynnistyksen varmenteen tilan koontinäyttö</h1> <div class="meta">Luotu: $($stats. ReportGeneratedAt) | Laitteita yhteensä: $($c.Total.ToString("N0")) | Yksilölliset säilöt: $($stAllBuckets.Count)</div> </div> <div class="container">
<!-- KPI-kortit – napsautettavissa, linkitetty osiin --> <div class="cards"> <class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; sijainti:suhteellinen"><div style="position:absolute; top:8px; oikea:8px; background:#dc3545; color:#fff; täyttö:1 x 6 kuvapistettä; reunan säde:8px; fonttikoko:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString("N0")))</div><div class="label">NOT UPDATED><6 /div><div class="pct">$($stats. PercentNotUptodate)% - NEEDS ACTION><0 /div></a><3 <class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; sijainti:suhteellinen"><div style="position:absolute; top:8px; oikea:8px; background:#28a745; color:#fff; täyttö:1 x 6 kuvapistettä; reunan säde:8px; fonttikoko:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString("N0"))</div><div class="label">Päivitetty><6 /div><div class="pct">$($stats). PercentCertUpdated)%</div></a><3 <class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; text-decoration:none; sijainti:suhteellinen"><div style="position:absolute; top:8px; oikea:8px; background:#6c757d; color:#fff; täyttö:1 x 6 kuvapistettä; reunan säde:8px; fonttikoko:.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 <class="card" href="#s-nrb" onclick="openSection('d-nrb')" style="border-left-color:#ffc107; text-decoration:none"><div class="value" style="color:#ffc107">$($c.NeedsReboot.ToString("N0")))</div><div class="label">Needs Reboot><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}muu{0})% – odottaa uudelleenkäynnistystä><6 /div></a><9 <class="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0")))</div><div class="label">Update Pending</div><$c div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Käytäntö/WinCS käytössä, odottaa päivitystä><2 /div></a><5 <class="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Rollout In Progress><4 /div><div class="pct">>Rollout In Progress $(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}muuten{0})%</div></a><11 <class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0")))</div><div class="label">High Confidence><20 /div><div class="pct">$($stats). PercentHighConfidence)% – Turvallinen käyttöönottoon><24 /div></a><27 <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)}muuten{0})%</div></a><3 <class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0")))</div><div class="label">Action Required><2 /div><div class="pct">$($stats. PercentActionRequired)% – on testattava><6 /div></a><9 <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)% – Samanlainen kuin epäonnistunut><2 /div></a><5 <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">$(jos($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100,1)}muu{0})% - Estetty><8 /div></a><91 <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. Keskeytetty</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a> <class="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Known Issues><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3 <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)}muuten{0})%</div></a> <class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithErrors.ToString("N0")))</div><div class="label">With Errors</div><div class="pct"><1 $($stats). PercentWithErrors)% - UEFI-virheet</div></a> ><6 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. Virheet</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}muuten{0})%</div></a> <class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0")))</div><div class="label">Not Supported><6 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100,1)}muuten{0})%</div></a><3 </div>
<!-- käyttöönoton nopeus & varmenteet - > <div id="s-velocity" style="display:grid; ruudukkomallisarakkeet:1fr 1fr; gap:20px; margin:15px 0"> <div class="section" style="margin:0"> <h2>📅 Käyttöönoton nopeus</h2> ><2 div class="section-body open"><3 ><4 div style="font-size:2.5em; fonttipaino:700; color:#28a745"><5 $($c.Updated.ToString("N0"))><6 /div> ><8 div style="color:#666"><9 laitteet päivitetty $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; background:#e8eaf6; korkeus:20px; reunasäde:10px; overflow:hidden"><div style="background:#28a745; korkeus:100%; leveys:$($stats. PercentCertUpdated)%; border-radius:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% complete</div> <div style="margin-top:10px; täyttö:10px; background:#f8f9fa; reunan säde:8px; fonttikoko:.85em"> <div><strong>Remaining:</strong> $($stNotUptodate.ToString("N0")) -laitteet tarvitsevat toimia</div> <div><strong>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) -laitteet (virheet + pysyvä + tehtävä poistettu käytöstä)</div> <div><strong>Safe käyttöönottoa:</strong> $($stSafeList.ToString("N0")) laitteet (sama säilö kuin onnistunut)</div> $velocityHtml </div> </div> </div> <div class="section" style="margin:0; reunasta vasemmalle:4px kiinteä #dc3545"> <h2 style="color:#dc3545">⚠ Certificate Expiry Countdown</h2> <div class="section-body open"> <div style="display:flex; gap:15px; margin-top:10px"> <div style="text-align:center; täyttö:15px; reunan säde:8px; min-width:120px; tausta:lineaarinen liukuväri(135deg,#fff5f5,#ffe0e0); border:2px kiinteä #dc3545; flex:1"> <div style="font-size:.65em; color:#721c24; text-transform:isot kirjaimet; fontin leveys:bold">⚠ ENSIN VANHENEE</div> ><4 div style="font-size:.85em; fontin leveys:lihavointi; color:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; fonttipaino:700; color:#dc3545; viivan korkeus:1"><9 $daysToKek><0 /div><1 <div style="font-size:.8em; color:#721c24">päivää (24.6.2026)</div><5 </div><7 <div style="text-align:center; täyttö:15px; reunan säde:8px; min-width:120px; tausta:lineaarinen liukuväri(135deg,#fffef5,#fff3cd); border:2px kiinteä #ffc107; flex:1"> ><00 div style="font-size:.65em; väri:#856404; text-transform:isot kirjaimet; fontin leveys:bold"><01 UEFI CA 2011</div> ><04 div id="daysUefi" style="font-size:2.2em; fonttipaino:700; väri:#856404; viivan korkeus:1; margin:5px 0"><05 $daysToUefi</div> ><08 div style="font-size:.8em; color:#856404"><09 päivää (27. kesäkuuta 2026)><10 /div> ><12 /div> ><14 div style="text-align:center; täyttö:15px; reunan säde:8px; min-width:120px; tausta:lineaarinen liukuväri(135deg,#f0f8ff,#d4edff); border:2px kiinteä #0078d4; flex:1"><15 ><16 div style="font-size:.65em; color:#0078d4; text-transform:isot kirjaimet; font-weight:bold"><17 Windows PCA</div> ><20 div id="daysPca" style="font-size:2.2em; fonttipaino:700; color:#0078d4; viivan korkeus:1; margin:5px 0"><21 $daysToPca><2 /div><3 ><24 div style="font-size:.8em; color:#0078d4"><25 päivää (19. lokakuuta 2026)><26 /div><7 ><28 /div><9 ><30 /div><1 ><32 div style="margin-top:15px; täyttö:10px; background:#f8d7da; reunan säde:8px; fonttikoko:.85em; reunasta vasemmalle:4px kiinteä #dc3545"><33 ><34 vahva>⚠ CRITICAL:><37 /strong> Kaikki laitteet on päivitettävä ennen varmenteen vanhenemista. Laitteet, joita ei ole päivitetty määräaikaan mennessä, eivät voi ottaa käyttöön tulevia suojauspäivityksiä käynnistyksen hallintaan ja suojattuun käynnistykseen vanhentumisen jälkeen.</div> </div> </div> </div>
<!-- --> <div class="charts"> <div class="chart-box"><h3>Käyttöönoton tila</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) { "<!-- historiallinen trendikaavio – > <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 Päivitä edistyminen ajan mittaan <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; väri:#888; margin-top:5px'>Kiinteät viivat = todelliset tiedot$(jos ($historyData.Count -ge 2) { " | Katkoviiva = projisoitu (eksponentiaalinen kaksinkertainen: 2→4→8→16... laitteet aaltoa kohti)" } muu { " | Suorita kooste uudelleen huomenna nähdäksesi trendiviivat ja projektion" })</div> </div> </div>" })
<!-- CSV-lataukset --> <div class="section"> <h2 onclick="toggle('dl-csv')">📥 Lataa Täydelliset tiedot (CSV for Excel) <class="top-link" href="#">Top</a></h2> <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap:5px"> <href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; background:#dc3545; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; fonttikoko:.8em">Ei päivitetty ($($stNotUptodate.ToString("N0")))</a> <href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; background:#dc3545; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; fonttikoko:.8em">virheet ($($c.WithErrors.ToString("N0")))</a><2 <href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; background:#fd7e14; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; fonttikoko:.8em">pakollinen toiminto ($($c.ActionReq.ToString("N0")))</a><6 <href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; background:#dc3545; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; font-size:.8em">Tunnetut ongelmat ($($c.WithKnownIssues.ToString("N0")))</a> <href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; background:#dc3545; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; fonttikoko:.8em">Tehtävä poistettu käytöstä ($($c.TaskDisabled.ToString("N0")))</a> <href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; background:#28a745; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; fonttikoko:.8em">Päivitetty ($($c.Updated.ToString("N0")))</a> <href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; background:#6c757d; color:#fff; täyttö:6 x 14 kuvapistettä; reunan säde:5px; text-decoration:none; fonttikoko:.8em">Yhteenveto</a> <div style="width:100%; fonttikoko:.75em; väri:#888; margin-top:5px">CSV-tiedostot avautuvat Excelissä. Käytettävissä verkkopalvelimessa isännöitäessä.</div> </div> </div>
<!-- valmistajan erittely --> <div class="section"> <h2 onclick="toggle('mfr')">Valmistajan mukaan <class="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> <taulukko><ad><tr><th><1 Manufacturer><2 /th><th><5 Total><6 /th><th><9 Updated><9><0 /th><th><3 High Confidence><4 /th><th><7 Tarvitaan><8 /th></tr></thead><3 <><5 $mfrTableRows><6 /tbody></table><9 </div><1 </div>
<!-- laiteosat (ensimmäinen 200 tekstiin sitoutuva + CSV-lataus) --> <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 Laitteet, joissa on virheitä ($($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">🔴 Tunnetut ongelmat ($($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')">🟠 PuuttuvaT KEK – Tapahtuma 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">🟠 Toimenpide Pakollinen ($($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">🔵 Kohdassa Havainnointi ($($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">🔴 Ei päivitetty ($($stNotUptodate.ToString("N0"))) <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">🔴 Tehtävä poistettu käytöstä ($($c.TaskDisabled.ToString("N0"))) >↑ 5 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">🔴 Väliaikaiset virheet ($($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">🔴 Pysyviä virheitä / ei tueta ($($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")))) - Käytäntö/WinCS käytössä, Odotetaan päivitystä <class="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Laitteet, joissa on käytössä AvailableUpdatesPolicy- tai WinCS-näppäin, mutta UEFICA2023Status on edelleen Ei käynnistynyt, InProgress tai null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 Käyttöönotto käynnissä ($($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"))) - Out of Scope <a class="top-link" href="#">↑ Top</a></h2> <div id="d-sboff" class="section-body">$tblSBOff</div> </div> <div class="section" id="s-upd"> <h2 onclick="toggle('d-upd')" style="color:#28a745">🟢 Päivitetyt laitteet ($($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">🔄 Päivitetty – Needs Reboot ($($c.NeedsReboot.ToString("N0")))) <a class="top-link" href="#">↑ Top</a></h2> <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>
<div class="footer">suojatun käynnistysvarmenteen käyttöönoton koontinäyttö | Luotu $($stats. ReportGeneratedAt) | StreamingMode | Huippumuisti: ${stPeakMemMB} Mt</div> </div><!-- /container -->
<komentosarja> function toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} function openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} new Chart(document.getElementById('deployChart'),{type:'rengas',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Action Required','Temp. Paused','Not Supported','SecureBoot OFF','With Errors';datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24 #20c997','#fd7e14','#6c757d',','#adb5bd','#dc3545']}},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); new Chart(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasets:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ tarra:'Temp. Paused',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},,{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}}} Historiallinen trendikaavio 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 peddedUpdated = actualUpdated.concat(Array(projLen).fill(null)); var peddedNotUpdated = actualNotUpdated.concat(Array(projLen).fill(null)); var peddedTotal = actualTotal.concat(Array(projLen).fill(null)); var projLine = Array(histLen).fill(null); var projNotUpdLine = Array(histLen).fill(null); jos (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdData); } var-tietojoukot = [ {label:'Updated',data:peddedUpdated,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:peddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; if (projLen > 0) { datasets.push({label:'Projected Updated (2x renewal)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } new Chart(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasets:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display:true,text:'Devices'}},x:{title:{display:true,text:'Date'}}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}}); } Dynaaminen lähtölaskenta (funktio(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19'); var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca'); if(dk)dk.textContent=Math.max(0,Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0,Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0,Math.ceil((p-t)/864e5))})();$endScript $endBody $endHtml "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # Säilytä aina vakaa Uusimmat-kopio, jotta järjestelmänvalvojien ei tarvitse seurata aikaleimaa $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # Tallenna tiedostoluettelo vaiheittaista tilaa varten (pikatunnistus ilman muutoksia seuraavalla kerralla) jos ($IncrementalMode -tai $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 "Tiedoston luettelotiedoston tallentaminen lisäävässä tilassa..." -EdustaväriVärin harmaa foreach ($jf in $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") Koko = $jf. Pituus } } Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath Write-Host " $($stNewManifest.Count) -tiedostojen tallennettu luettelo" -EdustaväriNen TummaGray } # SÄILYTYSTEN PUHDISTUS # Orchestrator reusable folder (Aggregation_Current): keep only latest run (1) # Hallinta manuaaliset lataukset / muut kansiot: pidä viimeiset 7 juoksua # CsV-yhteenvetoa EI KOSKAAN poisteta – ne ovat pieniä (~1 KT) ja ovat trendihistorian varmuuskopiolähde $outputLeaf = Split-Path $OutputPath -Lehti $retentionCount = jos ($outputLeaf -eq 'Aggregation_Current') { 1 } muu { 7 } # Tiedostojen etuliitteet ovat turvallisia siivota (ajastettavat tilannevedokset) $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_" ) # Etsi kaikki yksilölliset aikaleimat vain puhdistettavissa olevissa tiedostoissa $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. Nimi; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Määrä -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { jos ($_. Nimi -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -Unique -Laskeva) jos ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object -Ohita $retentionCount $removedFiles = 0; $freedBytes = 0 foreach ($oldTs in $oldTimestamps) { foreach ($prefix $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f in $oldFiles) { $freedBytes += $f.Length Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [matematiikka]::Pyöristä($freedBytes / 1 Mt, 1) Write-Host "Säilytysten uudelleenjärjestäminen: poistettu $removedFiles tiedostot $($oldTimestamps.Count) vanhoista ajojavoista, vapautettu ${freedMB} Mt (säilytetään viimeiset $retentionCount + kaikki yhteenveto / ei ylläpidä csv-tiedostoja)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Cyan Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -EtualallaVärisyan Write-Host " Laitteet yhteensä: $($c.Total.ToString("N0")))" -EdustaVäri Valkoinen Write-Host " EI PÄIVITETTY: $($stNotUptodate.ToString("N0")) ($($stats). PercentNotUptodate)%)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" }) Write-Host " Päivitetty: $($c.Updated.ToString("N0")) ($($stats). PercentCertUpdated)%)" -ForegroundColor Green Write-Host " Virheiden kanssa: $($c.WithErrors.ToString("N0"))" -EtualallaColor $(jos ($c.WithErrors -gt 0) { "Punainen" } muu { "Vihreä" }) Write-Host " Huippumuisti: ${stPeakMemMB} Mt" -EtualallaColor Cyan Write-Host " Time: $([math]::Round($stTotal/60,1)) min" -ForegroundColor White Write-Host Koontinäyttö: $htmlPath - Edustaväri Valkoinen return [PSCustomObject]$stats } #ENDREGION SUORATOISTOTILA } muu { Write-Error "Syötepolkua ei löydy: $InputPath" exit 1 }