انسخ هذا البرنامج النصي النموذجي والصقه وقم بتعديله حسب الحاجة لبيئتك:
<# . خلاصه تجميع بيانات JSON لحالة التمهيد الآمن من أجهزة متعددة في تقارير موجزة.
. وصف يقرأ ملفات JSON لحالة التمهيد الآمن التي تم جمعها وينشئ: - لوحة معلومات HTML مع المخططات والتصفية - ملخص حسب مستوى الثقة - تحليل مستودع الجهاز الفريد لاستراتيجية الاختبار يدعم: - الملفات لكل جهاز: HOSTNAME_latest.json (مستحسن) - ملف JSON واحد إلغاء التكرارات تلقائيا بواسطة HostName، مع الاحتفاظ بأحدث CollectionTime. بشكل افتراضي، يتضمن فقط الأجهزة ذات الثقة "Action Req" أو "High" للتركيز على المستودعات القابلة للتنفيذ. استخدم -IncludeAllConfidenceLevels للتجاوز.
. مسار إدخال المعلمة المسار إلى ملف (ملفات) JSON: - المجلد: يقرأ جميع ملفات *_latest.json (أو *.json إذا لم يكن هناك ملفات _latest) - ملف: يقرأ ملف JSON واحد
. مسار إخراج المعلمة مسار التقارير التي تم إنشاؤها (افتراضي: .\SecureBootReports)
. المثال # التجميع من مجلد الملفات لكل جهاز (مستحسن) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" # Reads: \\contoso\SecureBootLogs$\*_latest.json
. المثال # موقع الإخراج المخصص .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"
. المثال # تضمين Action Req فقط والثقة العالية (السلوك الافتراضي) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" #Excludes: المراقبة، متوقف مؤقتا، غير مدعوم
. المثال # تضمين جميع مستويات الثقة (عامل تصفية التجاوز) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels
. المثال # عامل تصفية مستوى الثقة المخصص .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")
. المثال # ENTERPRISE SCALE: الوضع التزايدي - معالجة الملفات التي تم تغييرها فقط (عمليات التشغيل اللاحقة السريعة) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode # التشغيل الأول: التحميل الكامل ~2 ساعة لأجهزة 500K # عمليات التشغيل اللاحقة: الثوان إذا لم تكن هناك تغييرات، دقائق دلتا
. المثال # تخطي HTML إذا لم يتغير أي شيء (الأسرع للمراقبة) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged # إذا لم تتغير أي ملفات منذ آخر تشغيل: ~5 ثوان
. المثال # وضع الملخص فقط - تخطي جداول الأجهزة الكبيرة (1-2 دقيقة مقابل 20+ دقيقة) .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly # ينشئ CSVs ولكنه يتخطى لوحة معلومات HTML مع جداول الأجهزة الكاملة
. تلاحظ الاقتران مع Detect-SecureBootCertUpdateStatus.ps1 لتوزيع المؤسسة.راجع GPO-DEPLOYMENT-GUIDE.md للحصول على دليل التوزيع الكامل. يستبعد السلوك الافتراضي أجهزة المراقبة والإيقاف المؤقت وغير المدعومة لتركيز إعداد التقارير على مستودعات الأجهزة القابلة للتنفيذ فقط.#>
param( [Parameter(إلزامي = $true)] [string]$InputPath، [Parameter(إلزامي = $false)] [string]$OutputPath = ".\SecureBootReports"، [Parameter(إلزامي = $false)] [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json"، [Parameter(إلزامي = $false)] [string]$RolloutStatePath، # Path to RolloutState.json لتحديد أجهزة InProgress [Parameter(إلزامي = $false)] [string]$RolloutSummaryPath، # Path to SecureBootRolloutSummary.json من Orchestrator (يحتوي على بيانات الإسقاط) [Parameter(إلزامي = $false)] [string[]]$IncludeConfidenceLevels = @("Action Required", "High Confidence"), # تضمين مستويات الثقة هذه فقط (افتراضي: مستودعات قابلة للتنفيذ فقط) [Parameter(إلزامي = $false)] [switch]$IncludeAllConfidenceLevels، # تجاوز عامل التصفية لتضمين جميع مستويات الثقة [Parameter(إلزامي = $false)] [switch]$SkipHistoryTracking، [Parameter(إلزامي = $false)] [switch]$IncrementalMode، # Enable delta processing - تحميل الملفات التي تم تغييرها فقط منذ آخر تشغيل [Parameter(إلزامي = $false)] [string]$CachePath، # Path to cache directory (default: OutputPath\.cache) [Parameter(إلزامي = $false)] [int]$ParallelThreads = 8، # عدد مؤشرات الترابط المتوازية لتحميل الملفات (PS7+) [Parameter(إلزامي = $false)] [switch]$ForceFullRefresh، # فرض إعادة التحميل الكامل حتى في الوضع التزايدي [Parameter(إلزامي = $false)] [switch]$SkipReportIfUnchanged، # Skip HTML/CSV generation إذا لم يتم تغيير أي ملفات (إحصائيات الإخراج فقط) [Parameter(إلزامي = $false)] [switch]$SummaryOnly، # Generate summary stats only (no large device tables) - أسرع بكثير [Parameter(إلزامي = $false)] [switch]$StreamingMode # وضع كفاءة الذاكرة: مجموعات المعالجة، كتابة CSVs بشكل متزايد، الاحتفاظ بالملخصات فقط في الذاكرة )
# رفع تلقائي إلى PowerShell 7 إذا كان متوفرا (أسرع ب 6 أضعاف لمجموعات البيانات الكبيرة) إذا ($PSVersionTable.PSVersion.Major -lt 7) { $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source إذا ($pwshPath) { Write-Host "تم الكشف عن PowerShell $($PSVersionTable.PSVersion) - إعادة التشغيل باستخدام PowerShell 7 للمعالجة الأسرع..." -ForegroundColor Yellow # إعادة إنشاء قائمة الوسيطات من المعلمات المرتبطة $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path) foreach ($key في $PSBoundParameters.Keys) { $val = $PSBoundParameters[$key] إذا ($val -is [switch]) { إذا ($val. IsPresent) { $relaunchArgs += "-$key" } } elseif ($val -is [array]) { $relaunchArgs += "-$key" $relaunchArgs += ($val -join ',') } آخر { $relaunchArgs += "-$key" $relaunchArgs += "$val" } } & $pwshPath @relaunchArgs إنهاء $LASTEXITCODE } }
$ErrorActionPreference = "متابعة" $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "عينات التوزيع والمراقبة"
# ملاحظة: لا يحتوي هذا البرنامج النصي على تبعيات على البرامج النصية الأخرى. # لمجموعة الأدوات الكاملة، قم بالتنزيل من: $DownloadUrl -> $DownloadSubPage
إعداد #region Write-Host "=" * 60 -ForegroundColor Cyan Write-Host "تجميع بيانات التمهيد الآمن" -ForegroundColor Cyan Write-Host "=" * 60 -ForegroundColor Cyan
# إنشاء دليل الإخراج if (-not (test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | خارج فارغة }
# تحميل البيانات - يدعم تنسيقات CSV (القديمة) وJSON (الأصلية) Write-Host "'nLoading data from: $InputPath" -ForegroundColor Yellow
# وظيفة المساعد لتطبيع كائن الجهاز (التعامل مع اختلافات اسم الحقل) الدالة Normalize-DeviceRecord { param($device) # معالجة اسم المضيف مقابل اسم المضيف (يستخدم JSON اسم المضيف، يستخدم CSV اسم المضيف) إذا ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) { $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. اسم المضيف -Force } # Handle Confidence vs ConfidenceLevel (يستخدم JSON الثقة، يستخدم CSV ConfidenceLevel) # ConfidenceLevel هو اسم الحقل الرسمي - تعيين الثقة إليه إذا ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) { $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. الثقة -القوة } # تعقب حالة التحديث عبر Event1808Count OR UEFICA2023Status="Updated" # يسمح هذا بتعقب عدد الأجهزة في كل مستودع ثقة تم تحديثه $event 1808 = 0 إذا ($device. PSObject.Properties['Event1808Count']) { $event 1808 = [int]$device. Event1808Count } $uefiCaUpdated = $false إذا ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Updated") { $uefiCaUpdated = $true } إذا ($event 1808 -gt 0 -أو $uefiCaUpdated) { # وضع علامة كمحدث للوحة المعلومات/منطق الإطلاق - ولكن لا تتجاوز ConfidenceLevel $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force } آخر { $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force # تصنيف مستوى الثقة: # - "ثقة عالية"، "تحت الملاحظة..."، "متوقف مؤقتا..."، "غير مدعوم..." = استخدام كما هو # - يقع كل شيء آخر (خال، فارغ، "UpdateType:..."، "غير معروف"، "N/A") = إلى الإجراء المطلوب في العدادات # لا توجد حاجة إلى تسوية - يتعامل فرع آخر لعداد الدفق معه } # معالجة OEMManufacturerName مقابل WMI_Manufacturer (يستخدم JSON OEM*، يستخدم القديم WMI_*) إذا ($device. PSObject.Properties['OEMManufacturerName'] -and -not $device. PSObject.Properties['WMI_Manufacturer']) { $device | Add-Member -NotePropertyName 'WMI_Manufacturer' -NotePropertyValue $device. OEMManufacturerName -Force } # التعامل مع OEMModelNumber مقابل WMI_Model إذا ($device. PSObject.Properties['OEMModelNumber'] -and -not $device. PSObject.Properties['WMI_Model']) { $device | Add-Member -NotePropertyName 'WMI_Model' -NotePropertyValue $device. OEMModelNumber -Force } # التعامل مع FirmwareVersion مقابل BIOSDescription إذا ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) { $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion -Force } إرجاع $device }
#region معالجة تزايدية / إدارة ذاكرة التخزين المؤقت # إعداد مسارات ذاكرة التخزين المؤقت if (-not $CachePath) { $CachePath = Join-Path $OutputPath ".ذاكرة التخزين المؤقت" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"
# وظائف إدارة ذاكرة التخزين المؤقت الدالة Get-FileManifest { param([string]$Path) إذا (مسار الاختبار $Path) { جرب { $json = Get-Content $Path -Raw | ConvertFrom-Json # تحويل PSObject إلى hashtable (PS5.1 متوافق - PS7 لديه -AsHashtable) $ht = @{} $json. PSObject.Properties | ForEach-Object { $ht[$_. Name] = $_. القيمة } إرجاع $ht } التقاط { إرجاع @{} } } إرجاع @{} }
الدالة Save-FileManifest { param([hashtable]$Manifest, [string]$Path) $dir = Split-Path $Path -الأصل if (-not (test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | خارج فارغة } $Manifest | ConvertTo-Json -العمق 3 -ضغط | Set-Content $Path -Force }
الدالة Get-DeviceCache { param([string]$Path) إذا (مسار الاختبار $Path) { جرب { $cacheData = Get-Content $Path -Raw | ConvertFrom-Json Write-Host " ذاكرة التخزين المؤقت للجهاز المحملة: $($cacheData.Count) devices" -ForegroundColor DarkGray إرجاع $cacheData } التقاط { Write-Host " تالفة ذاكرة التخزين المؤقت، سيتم إعادة بناء" -ForegroundColor الأصفر إرجاع @() } } إرجاع @() }
الدالة Save-DeviceCache { param($Devices, [string]$Path) $dir = Split-Path $Path -الأصل if (-not (test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | خارج فارغة } # تحويل إلى صفيف وحفظ $deviceArray = @($Devices) $deviceArray | ConvertTo-Json -العمق 10 -ضغط | Set-Content $Path -Force Write-Host " ذاكرة التخزين المؤقت للجهاز المحفوظة: $($deviceArray.Count) devices" -ForegroundColor DarkGray }
الدالة Get-ChangedFiles { param( [System.IO.FileInfo[]]$AllFiles، [hashtable]$Manifest ) $changed = [System.Collections.ArrayList]::new() $unchanged = [System.Collections.ArrayList]::new() $newManifest = @{} # إنشاء بحث غير حساس لحالة الأحرف من البيان (التسوية إلى الأحرف الصغيرة) $manifestLookup = @{} foreach ($mk في $Manifest.Keys) { $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk] } foreach ($file في $AllFiles) { $key = $file. FullName.ToLowerInvariant() # Normalize path to lowercase $lwt = $file. LastWriteTimeUtc.ToString("o") $newManifest[$key] = @{ LastWriteTimeUtc = $lwt الحجم = $file. طول } if ($manifestLookup.ContainsKey($key)) { $cached = $manifestLookup[$key] إذا ($cached. LastWriteTimeUtc -eq $lwt -and $cached. الحجم -eq $file. الطول) { [فارغ]$unchanged. إضافة ($file) مواصله } } [فارغ]$changed. إضافة ($file) } إرجاع @{ تم التغيير = $changed دون تغيير = $unchanged NewManifest = $newManifest } }
# تحميل ملف متوازي فائق السرعة باستخدام المعالجة المجمعة الدالة Load-FilesParallel { param( [System.IO.FileInfo[]]$Files، [int]$Threads = 8 )
$totalFiles = $Files. عدد # استخدم دفعات من ~1000 ملف لكل منها للتحكم في الذاكرة بشكل أفضل $batchSize = [math]::Min(1000, [math]::Ceiling($totalFiles / [math]::Max(1, $Threads))) $batches = [System.Collections.Generic.List[object]]::new()
ل ($i = 0؛ $i -lt $totalFiles؛ $i += $batchSize) { $end = [math]::Min($i + $batchSize، $totalFiles) $batch = $Files[$i.. ($end-1)] $batches. إضافة ($batch) } Write-Host " ($($batches. Count) دفعات من ~$batchSize الملفات لكل منها)" -NoNewline -ForegroundColor DarkGray $flatResults = [System.Collections.Generic.List[object]]::new() # تحقق مما إذا كان PowerShell 7+ متوازيا متوفرا $canParallel = $PSVersionTable.PSVersion.Major -ge 7 if ($canParallel -and $Threads -gt 1) { # PS7+: معالجة الدفعات بالتوازي $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel { $batchFiles = $_ $batchResults = [System.Collections.Generic.List[object]]::new() foreach ($file في $batchFiles) { جرب { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $batchResults.Add($content) } التقاط { } } $batchResults.ToArray() } foreach ($batch في $results) { if ($batch) { foreach ($item in $batch) { $flatResults.Add($item) } } } } آخر { # PS5.1 الاحتياطي: المعالجة التسلسلية (لا تزال سريعة لملفات <10K) foreach ($file في $Files) { جرب { $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json $flatResults.Add($content) } التقاط { } } } إرجاع $flatResults.ToArray() } #endregion
$allDevices = @() if (Test-Path $InputPath -PathType Leaf) { # ملف JSON واحد if ($InputPath -like "*.json") { $jsonContent = Get-Content -Path $InputPath -Raw | ConvertFrom-Json $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ } Write-Host "تم تحميل سجلات $($allDevices.Count) من الملف" } آخر { Write-Error "يتم دعم تنسيق JSON فقط. يجب أن يحتوي الملف على ملحق .json." الخروج 1 } } elseif (Test-Path $InputPath -PathType Container) { #Folder - JSON فقط $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. الاسم -notmatch "ScanHistory|الإطلاق التدريجي|RolloutPlan" }) # يفضل *_latest.json الملفات إذا كانت موجودة (الوضع لكل جهاز) $latestJson = $jsonFiles | Where-Object { $_. Name -like "*_latest.json" } إذا ($latestJson.Count -gt 0) { $jsonFiles = $latestJson } $totalFiles = $jsonFiles.Count if ($totalFiles -eq 0) { Write-Error "لم يتم العثور على ملفات JSON في: $InputPath" الخروج 1 } Write-Host "تم العثور على ملفات JSON $totalFiles" -ForegroundColor Gray # وظيفة المساعد لمطابقة مستويات الثقة (يعالج كل من النماذج القصيرة والكاملة) # معرفة مبكرا بحيث يمكن لكل من StreamingMode والمسارات العادية استخدامه الدالة Test-ConfidenceLevel { param([string]$Value, [string]$Match) if ([string]::IsNullOrEmpty($Value)) { return $false } switch ($Match) { "HighConfidence" { return $Value -eq "High Confidence" } "UnderObservation" { return $Value -like "Under Monitoring*" } "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") } "مؤقتاPaused" { إرجاع $Value -مثل "مؤقتا متوقف مؤقتا*" } "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") } الافتراضي { إرجاع $false } } } #region وضع الدفق - المعالجة الفعالة للذاكرة لمجموعات البيانات الكبيرة # استخدم StreamingMode دائما للمعالجة الفعالة للذاكرة ولوحة المعلومات ذات النمط الجديد if (-not $StreamingMode) { Write-Host "Auto-enabling StreamingMode (لوحة معلومات النمط الجديد)" -ForegroundColor Yellow $StreamingMode = $true if (-not $IncrementalMode) { $IncrementalMode = $true } } # عند تمكين -StreamingMode، قم بمعالجة الملفات في مجموعات مع الاحتفاظ بالعدادات فقط في الذاكرة.# تتم كتابة البيانات على مستوى الجهاز إلى ملفات JSON لكل مجموعة للتحميل عند الطلب في لوحة المعلومات.# استخدام الذاكرة: ~1.5 غيغابايت بغض النظر عن حجم مجموعة البيانات (مقابل 10-20 غيغابايت دون دفق).إذا ($StreamingMode) { Write-Host "تمكين وضع الدفق - المعالجة الفعالة للذاكرة" -ForegroundColor Green $streamSw = [System.Diagnostics.Stopwatch]::StartNew() # INCREMENTAL CHECK: إذا لم يتم تغيير أي ملفات منذ آخر تشغيل، فتخط المعالجة بالكامل if ($IncrementalMode -and -not $ForceFullRefresh) { $stManifestDir = Join-Path $OutputPath ".ذاكرة التخزين المؤقت" $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" إذا (مسار الاختبار $stManifestPath) { Write-Host "التحقق من التغييرات منذ آخر تشغيل دفق..." -ForegroundColor Cyan $stOldManifest = Get-FileManifest -مسار $stManifestPath إذا ($stOldManifest.Count -gt 0) { $stChanged = $false # فحص سريع: نفس عدد الملفات؟ if ($stOldManifest.Count -eq $totalFiles) { # تحقق من 100 ملف أحدث (تم فرزها حسب LastWriteTime تنازليا) # إذا تم تغيير أي ملف، فسيكون له أحدث طابع زمني وسيظهر أولا $sampleSize = [math]::Min(100, $totalFiles) $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -تنازلي | Select-Object -أول $sampleSize foreach ($sf في $sampleFiles) { $sfKey = $sf. FullName.ToLowerInvariant() إذا (-not $stOldManifest.ContainsKey($sfKey)) { $stChanged = $true كسر } # مقارنة الطوابع الزمنية - قد تكون مخزنة مؤقتا DateTime أو سلسلة بعد الجولة JSON $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc $fileDT = $sf. LastWriteTimeUtc جرب { # إذا كانت ذاكرة التخزين المؤقت هي DateTime بالفعل (تحويل تلقائي إلى ConvertFrom-Json)، فاستخدم مباشرة إذا ($cachedLWT -is [DateTime]) { $cachedDT = $cachedLWT.ToUniversalTime() } آخر { $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { $stChanged = $true كسر } } التقاط { $stChanged = $true كسر } } } آخر { $stChanged = $true } if (-not $stChanged) { # تحقق مما إذا كانت ملفات الإخراج موجودة $stSummaryExists = Get-ChildItem (مسار الانضمام $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -الأول 1 $stDashExists = Get-ChildItem (join-Path $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -الأول 1 if ($stSummaryExists -and $stDashExists) { Write-Host " لم يتم الكشف عن أي تغييرات ($totalFiles الملفات دون تغيير) - تخطي المعالجة" -ForegroundColor Green Write-Host " لوحة المعلومات الأخيرة: $($stDashExists.FullName)" -ForegroundColor White $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv Write-Host " الأجهزة: $($cachedStats.TotalDevices) | محدث: $($cachedStats.Updated) | الأخطاء: $($cachedStats.WithErrors)" -ForegroundColor Gray Write-Host " مكتملة في $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (لا توجد معالجة مطلوبة)" -ForegroundColor Green إرجاع $cachedStats } } آخر { # DELTA PATCH: ابحث بالضبط عن الملفات التي تم تغييرها Write-Host " تم الكشف عن التغييرات - تحديد الملفات التي تم تغييرها..." -ForegroundColor Yellow $changedFiles = [System.Collections.ArrayList]::new() $newFiles = [System.Collections.ArrayList]::new() foreach ($jf في $jsonFiles) { $jfKey = $jf. FullName.ToLowerInvariant() if (-not $stOldManifest.ContainsKey($jfKey)) { [void]$newFiles.Add($jf) } آخر { $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc $fileDT = $jf. LastWriteTimeUtc جرب { $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime } if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) } } catch { [void]$changedFiles.Add($jf) } } } $totalChanged = $changedFiles.Count + $newFiles.Count $changePct = [math]::Round(($totalChanged / $totalFiles) * 100, 1) Write-Host " Changed: $($changedFiles.Count) | جديد: $($newFiles.Count) | الإجمالي: $totalChanged ($changePct٪)" -ForegroundColor Yellow if ($totalChanged -gt 0 -and $changePct -lt 10) { # DELTA PATCH MODE: <تم تغييرها بنسبة 10٪ وتصحيح البيانات الموجودة Write-Host " وضع تصحيح دلتا ($changePct٪ < 10٪) - تصحيح ملفات $totalChanged..." -ForegroundColor Green $dataDir = Join-Path $OutputPath "البيانات" # تحميل سجلات الجهاز المتغيرة/الجديدة $deltaDevices = @{} $allDeltaFiles = @($changedFiles) + @($newFiles) foreach ($df في $allDeltaFiles) { جرب { $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json $dev = Normalize-DeviceRecord $devData إذا ($dev. اسم المضيف) { $deltaDevices[$dev. HostName] = $dev } } التقاط { } } Write-Host " تم تحميل $($deltaDevices.Count) سجلات الأجهزة المتغيرة" -ForegroundColor Gray # لكل فئة JSON: إزالة الإدخالات القديمة لأسماء المضيفين التي تم تغييرها، وإضافة إدخالات جديدة $categoryFiles = @("errors", "known_issues", "missing_kek", "not_updated", "task_disabled"، "temp_failures"، "perm_failures"، "updated_devices"، "action_required"، "secureboot_off"، "rollout_inprogress") $changedHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($hn في $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) } foreach ($cat في $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" إذا (مسار الاختبار $catPath) { جرب { $catData = Get-Content $catPath -Raw | ConvertFrom-Json # إزالة الإدخالات القديمة لأسماء المضيفين التي تم تغييرها $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. اسم المضيف) }) # إعادة تصنيف كل جهاز تم تغييره إلى فئات # (ستتم إضافته أدناه بعد التصنيف) $catData | ConvertTo-Json -عمق 5 | Set-Content $catPath -ترميز UTF8 } التقاط { } } } # تصنيف كل جهاز تم تغييره وإلحاقه بملفات الفئة الصحيحة foreach ($dev في $deltaDevices.Values) { $slim = [ordered]@{ اسم المضيف = $dev. المضيف WMI_Manufacturer = إذا ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } آخر { "" } WMI_Model = إذا ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } آخر { "" } معرف المستودع = إذا ($dev. PSObject.Properties['BucketId']) { $dev. معرف المستودع } آخر { "" } ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } IsUpdated = $dev. IsUpdated UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } آخر { $null } SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } else { "" } KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } else { $null } SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null } } $isUpd = $dev. IsUpdated -eq $true $conf = إذا ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($dev. UEFICA2023Error) -and $dev. UEFICA2023Error -ne "0" -and $dev. UEFICA2023Error -ne "") $tskDis = ($dev. SecureBootTaskEnabled -eq $false -أو $dev. SecureBootTaskStatus -eq "Disabled" -أو $dev. SecureBootTaskStatus -eq 'NotFound') $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound') $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "False") $e 1801 = إذا ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Event1801Count } آخر { 0 } $e 1808 = إذا ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Event1808Count } آخر { 0 } $e 1803 = إذا ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Event1803Count } آخر { 0 } $mKEK = ($e 1803 -gt 0 -أو $dev. MissingKEK -eq $true) $hKI = ((-not [string]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -أو (-not [string]::IsNullOrEmpty($dev. KnownIssueId))) $rStat = إذا ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" } # إلحاق بملفات الفئة المطابقة $targets = @() if ($isUpd) { $targets += "updated_devices" } if ($hasErr) { $targets += "errors" } if ($hKI) { $targets += "known_issues" } if ($mKEK) { $targets += "missing_kek" } إذا (-not $isUpd -and $sbOn) { $targets += "not_updated" } if ($tskDis) { $targets += "task_disabled" } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += "temp_failures" } if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" } إذا (-not $isUpd -and (Test-ConfidenceLevel $conf "ActionRequired")) { $targets += "action_required" } if (-not $sbOn) { $targets += "secureboot_off" } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" } foreach ($tgt في $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 -عمق 5 | Set-Content $tgtPath -ترميز UTF8 } } } # إعادة إنشاء CSVs من JSONs المصححة Write-Host " إعادة إنشاء CSVs من البيانات المصححة..." -ForegroundColor Gray $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" foreach ($cat في $categoryFiles) { $catJsonPath = Join-Path $dataDir "$cat.json" $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv" if (test-Path $catJsonPath) { جرب { $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json إذا ($catJsonData.Count -gt 0) { $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -ترميز UTF8 } } التقاط { } } } # إعادة حساب الإحصائيات من ملفات JSON المصححة Write-Host " إعادة حساب الملخص من البيانات المصححة..." -ForegroundColor Gray $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 في $categoryFiles) { $catPath = Join-Path $dataDir "$cat.json" $cnt = 0 if (test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } catch { } } switch ($cat) { "updated_devices" { $pUpdated = $cnt } "الأخطاء" { $pErrors = $cnt } "known_issues" { $pKI = $cnt } "missing_kek" { $pKEK = $cnt } "not_updated" { } # المحسوبة "task_disabled" { $pTaskDis = $cnt } "temp_failures" { $pTempFail = $cnt } "perm_failures" { $pPermFail = $cnt } "action_required" { $pActionReq = $cnt } "secureboot_off" { $pSBOff = $cnt } "rollout_inprogress" { $pRIP = $cnt } } } $pNotUpdated = (Get-Content (Join-Path $dataDir "not_updated.json") -Raw | ConvertFrom-Json). عدد $pTotal = $pUpdated + $pNotUpdated + $pSBOff Write-Host "اكتمال تصحيح دلتا: تم تحديث أجهزة $totalChanged" -ForegroundColor Green Write-Host " الإجمالي: $pTotal | محدث: $pUpdated | NotUpdated: $pNotUpdated | الأخطاء: $pErrors" -ForegroundColor White # تحديث البيان $stManifestDir = Join-Path $OutputPath ".ذاكرة التخزين المؤقت" $stNewManifest = @{} foreach ($jf في $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o")؛ الحجم = $jf. طول } } Save-FileManifest -بيان $stNewManifest -مسار $stManifestPath Write-Host " Completed in $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (delta patch - $totalChanged devices)" -ForegroundColor Green # اسقط إلى إعادة معالجة الدفق الكامل لإعادة إنشاء لوحة معلومات HTML # ملفات البيانات مصححة بالفعل، لذلك يضمن هذا بقاء لوحة المعلومات محدثة Write-Host " إعادة إنشاء لوحة المعلومات من البيانات المصححة..." -ForegroundColor Yellow } آخر { Write-Host " تم تغيير الملفات $changePct٪ (>= 10٪) - إعادة معالجة الدفق الكاملة مطلوبة" -ForegroundColor Yellow } } } } } # إنشاء دليل فرعي للبيانات لملفات JSON للجهاز عند الطلب $dataDir = Join-Path $OutputPath "البيانات" if (-not (test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } #Deduplication عبر HashSet (O(1) لكل بحث، ~50 ميغابايت ل 600 ألف اسم مضيف) $seenHostnames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # عدادات التلخيص الخفيفة (يستبدل $allDevices + $uniqueDevices في الذاكرة) $c = @{ الإجمالي = 0؛ SBEnabled = 0؛ SBOff = 0 محدث = 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 تحديث معلق = 0 } # تتبع المستودع ل AtRisk/SafeList (مجموعات خفيفة الوزن) $stFailedBuckets = [System.Collections.Generic.HashSet[string]]::new() $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]::new() $stAllBuckets = @{} $stMfrCounts = @{} $stErrorCodeCounts = @{}؛ $stErrorCodeSamples = @{} $stKnownIssueCounts = @{} # ملفات بيانات الجهاز في وضع الدفعات: تتراكم لكل مجموعة، وتتدفق عند حدود المجموعة $stDeviceFiles = @("errors", "known_issues", "missing_kek", "not_updated", "task_disabled"، "temp_failures"، "perm_failures"، "updated_devices"، "action_required"، "secureboot_off" و"rollout_inprogress" و"under_observation" و"needs_reboot" و"update_pending") $stDeviceFilePaths = @{}; $stDeviceFileCounts = @{} foreach ($dfName في $stDeviceFiles) { $dfPath = Join-Path $dataDir "$dfName.json" [System.IO.File]::WriteAllText($dfPath, "['n", [System.Text.Encoding]::UTF8) $stDeviceFilePaths[$dfName] = $dfPath; $stDeviceFileCounts[$dfName] = 0 } # سجل جهاز ضئيل لإخراج JSON (الحقول الأساسية فقط، ~200 بايت مقابل ~2KB كامل) الدالة Get-SlimDevice { param($Dev) إرجاع [تم الطلب]@{ HostName = $Dev.HostName WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" } WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" } BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" } ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" } IsUpdated = $Dev.IsUpdated UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null } SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" } KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } else { $null } SkipReasonKnownIssue = if ($Dev.PSObject.Properties['SkipReasonKnownIssue']) { $Dev.SkipReasonKnownIssue } else { $null } UEFICA2023Status = if ($Dev.PSObject.Properties['UEFICA2023Status']) { $Dev.UEFICA2023Status } else { $null } AvailableUpdatesPolicy = if ($Dev.PSObject.Properties['AvailableUpdatesPolicy']) { $Dev.AvailableUpdatesPolicy } else { $null } WinCSKeyApplied = if ($Dev.PSObject.Properties['WinCSKeyApplied']) { $Dev.WinCSKeyApplied } else { $null } } } # Flush batch إلى ملف JSON (وضع الإلحاق) الدالة Flush-DeviceBatch { param([string]$StreamName, [System.Collections.Generic.List[object]]$Batch) إذا كان ($Batch.Count -eq 0) { return } $fPath = $stDeviceFilePaths[$StreamName] $fSb = [System.Text.StringBuilder]::new() foreach ($fDev في $Batch) { if ($stDeviceFileCounts[$StreamName] -gt 0) { [void]$fSb.Append(",'n") } [void]$fSb.Append(($fDev | ConvertTo-Json -Compress)) $stDeviceFileCounts[$StreamName]++ } [System.IO.File]::AppendAllText($fPath, $fSb.ToString(), [System.Text.Encoding]::UTF8) } # حلقة البث الرئيسية $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 } $stTotalChunks = [math]::Ceiling($totalFiles / $stChunkSize) $stPeakMemMB = 0 إذا ($stTotalChunks -gt 1) { Write-Host "معالجة ملفات $totalFiles في مجموعات $stTotalChunks من $stChunkSize (دفق، مؤشرات ترابط $ParallelThreads):" -ForegroundColor Cyan } آخر { Write-Host "معالجة ملفات $totalFiles (دفق، مؤشرات ترابط $ParallelThreads):" -ForegroundColor Cyan } ل ($ci = 0؛ $ci -lt $stTotalChunks؛ $ci++) { $cStart = $ci * $stChunkSize $cEnd = [math]::Min($cStart + $stChunkSize, $totalFiles) - 1 $cFiles = $jsonFiles[$cStart.. $cEnd] إذا ($stTotalChunks -gt 1) { Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) files): " -NoNewline -ForegroundColor Gray } آخر { Write-Host " تحميل ملفات $($cFiles.Count): " -NoNewline -ForegroundColor Gray } $cSw = [System.Diagnostics.Stopwatch]::StartNew() $rawDevices = Load-FilesParallel -Files $cFiles -مؤشرات الترابط $ParallelThreads # قوائم الدفعات لكل مجموعة $cBatches = @{} foreach ($df في $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]::new() } $cNew = 0؛ $cDupe = 0 foreach ($raw في $rawDevices) { إذا (-not $raw) { continue } $device = Normalize-DeviceRecord $raw $hostname = $device. المضيف إذا (-not $hostname) { continue } إذا ($seenHostnames.Contains($hostname)) { $cDupe++; متابعة } [void]$seenHostnames.Add($hostname) $cNew++; $c.Total++ $sbOn = ($device. SecureBootEnabled -ne $false -و"$($device. SecureBootEnabled)" -ne "False") if ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) } $isUpd = $device. IsUpdated -eq $true $conf = إذا ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } else { "" } $hasErr = (-not [string]::IsNullOrEmpty($device. UEFICA2023Error) -and "$($device. UEFICA2023Error)" -ne "0" -and "$($device. UEFICA2023Error)" -ne "") $tskDis = ($device. SecureBootTaskEnabled -eq $false -أو "$($device. SecureBootTaskStatus)" -eq "Disabled" -أو "$($device. SecureBootTaskStatus)" -eq 'NotFound') $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound') $bid = إذا ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } آخر { "" } $e 1808 = إذا ($device. PSObject.Properties['Event1808Count']) { [int]$device. Event1808Count } آخر { 0 } $e 1801 = إذا ($device. PSObject.Properties['Event1801Count']) { [int]$device. Event1801Count } آخر { 0 } $e 1803 = إذا ($device. PSObject.Properties['Event1803Count']) { [int]$device. Event1803Count } آخر { 0 } $mKEK = ($e 1803 -gt 0 -أو $device. MissingKEK -eq $true -أو "$($device. MissingKEK)" -eq "True") $hKI = ((-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -أو (-not [string]::IsNullOrEmpty($device. KnownIssueId))) $rStat = إذا ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" } $mfr = إذا ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } آخر { "غير معروف" } $bid = if (-not [string]::IsNullOrEmpty($bid)) { $bid } else { "" } # علامة التحديث المعلق قبل الحساب (تم تطبيق النهج/WinCS، الحالة لم يتم تحديثها بعد، SB ON، المهمة غير معطلة) $uefiStatus = إذا ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } آخر { "" } $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 = ([string]::IsNullOrEmpty($uefiStatus) -أو $uefiStatus -eq 'NotStarted' -أو $uefiStatus -eq 'InProgress') $isUpdatePending = (($hasPolicy -أو $hasWinCS) -and $statusPending -and-not $isUpd -and $sbOn -and-not $tskDis) إذا ($isUpd) { $c.Updated++؛ [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device)) # تعقب الأجهزة المحدثة التي تحتاج إلى إعادة التشغيل (UEFICA2023Status=Updated ولكن Event1808=0) if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) } } elseif (-not $sbOn) { # SecureBoot OFF — خارج النطاق، لا تصنف حسب الثقة } آخر { إذا كان ($isUpdatePending) { } # يتم حسابه بشكل منفصل في تحديث معلق — حصري بشكل متبادل للمخطط الدائري elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $c.TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $c.NotSupported++ } else { $c.ActionReq++ } if ([string]::IsNullOrEmpty($conf)) { $c.NoConfData++ } } if ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) } إذا ($tskNF) { $c.TaskNotFound++ } if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ } إذا ($hasErr) { $c.WithErrors++؛ [void]$stFailedBuckets.Add($bid); $cBatches["الأخطاء"]. Add((Get-SlimDevice $device)) $ec = $device. UEFICA2023Error if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() } $stErrorCodeCounts[$ec]++ if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname } } إذا ($hKI) { $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device)) $ki = if (-not [string]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } else { $device. KnownIssueId } if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++ } if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and (Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $c.RolloutInProgress++; $cBatches["rollout_inprogress"]. Add((Get-SlimDevice $device)) } if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -ne "InProgress") { $c.NotYetInitiated++ } if ($rStat -eq "InProgress" -and $e 1808 -eq 0) { $c.InProgress++ } # تحديث معلق: النهج أو WinCS المطبق، الحالة المعلقة، SB ON، المهمة غير معطلة إذا ($isUpdatePending) { $c.UpdatePending++؛ $cBatches["update_pending"]. Add((Get-SlimDevice $device)) } if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) } # ضمن أجهزة المراقبة (منفصلة عن الإجراء المطلوب) if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) } # الإجراء مطلوب: غير محدث، SB ON، لا يطابق فئات الثقة الأخرى، وليس تحديث معلق إذا (-not $isUpd -and $sbOn -and -not $isUpdatePending -and-not (Test-ConfidenceLevel $conf "HighConfidence") -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'TemporarilyPaused') -and -not (Test-ConfidenceLevel $conf 'NotSupported')) { $cBatches["action_required"]. Add((Get-SlimDevice $device)) } if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; محدث=0؛ UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0؛ NotSupported=0; SBOff=0; WithErrors=0 } } $stMfrCounts[$mfr]. Total++ if ($isUpd) { $stMfrCounts[$mfr]. تم التحديث++ } elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ } elseif ($isUpdatePending) { $stMfrCounts[$mfr]. تحديثPending++ } elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ } elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ } elseif (Test-ConfidenceLevel $conf "TemporarilyPaused") { $stMfrCounts[$mfr]. TempPaused++ } elseif (Test-ConfidenceLevel $conf "NotSupported") { $stMfrCounts[$mfr]. NotSupported++ } else { $stMfrCounts[$mfr]. ActionReq++ } if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ } # تعقب جميع الأجهزة حسب المستودع (بما في ذلك BucketId الفارغ) $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" } if (-not $stAllBuckets.ContainsKey($bucketKey)) { $stAllBuckets[$bucketKey] = @{ Count=0; محدث=0؛ الشركة المصنعة=$mfr؛ Model=""; BIOS="" } إذا ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. النموذج = $device. WMI_Model } إذا ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription } } $stAllBuckets[$bucketKey]. Count++ if ($isUpd) { $stAllBuckets[$bucketKey]. تم التحديث++ } } # مسح الدفعات إلى القرص foreach ($df في $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] } $rawDevices = $null؛ $cBatches = $null؛ [System.GC]::Collect() $cSw.Stop() $cTime = [Math]::Round($cSw.Elapsed.TotalSeconds, 1) $cRem = $stTotalChunks - $ci - 1 $cEta = if ($cRem -gt 0) { " | ETA: ~$([Math]::Round($cRem * $cSw.Elapsed.TotalSeconds / 60, 1)) دقيقة" } آخر { "" } $cMem = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 0) if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem } Write-Host " +$cNew جديد، $cDupe dupes، ${cTime}s | Mem: ${cMem}MB$cEta" -ForegroundColor Green } # إنهاء صفائف JSON foreach ($dfName في $stDeviceFiles) { [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8) Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) devices" -ForegroundColor DarkGray } # حساب الإحصائيات المشتقة $stAtRisk = 0؛ $stSafeList = 0 foreach ($bid في $stAllBuckets.Keys) { $b = $stAllBuckets[$bid]؛ $nu = $b.Count - $b.Updated if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu } elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu } } $stAtRisk = [math]::Max(0, $stAtRisk - $c.WithErrors) # NotUptodate = العدد من دفعة not_updated (الأجهزة ذات SB ON وغير المحدثة) $stNotUptodate = $stDeviceFileCounts["not_updated"] $stats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff محدث = $c.محدث؛ HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs ActionRequired = $c.ActionReq; تم الدفع مؤقتا = $c.TempPaused; NotSupported = $c.NotSupported NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated CertificatesUpdated = $c.Updated; NotUptodate = $stNotUptodate; تم تحديثه بالكامل = $c.محدث التحديثات المعلقة = $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; تحديث معلق = $c.تحديث معلق AtRiskDevices = $stAtRisk؛ SafeListDevices = $stSafeList PercentWithErrors = if ($c.Total -gt 0) { [math]::Round(($c.WithErrors/$c.Total)*100,2) } else { 0 } PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } آخر { 0 } PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } آخر { 0 } PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } else { 0 } PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } else { 0 } PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } else { 0 } PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 } UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB؛ ProcessingMode = "Streaming" } # كتابة CSVs [PSCustomObject]$stats | Export-Csv -Path (join-Path $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -ترميز UTF8 $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -تنازلي | ForEach-Object { [PSCustomObject]@{ Manufacturer=$_. مفتاح; Count=$_. القيمة.الإجمالي؛ تم التحديث=$_. القيمة.محدثة؛ HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq } } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -ترميز UTF8 $stErrorCodeCounts.GetEnumerator() | قيمة Sort-Object -تنازلي | ForEach-Object { [PSCustomObject]@{ ErrorCode=$_. مفتاح; Count=$_. قيمه; SampleDevices=($stErrorCodeSamples[$_. Key] -join ", ") } } | Export-Csv -Path (join-Path $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -ترميز UTF8 $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -تنازلي | ForEach-Object { [PSCustomObject]@{ BucketId=$_. مفتاح; Count=$_. القيمة.العدد؛ تم التحديث=$_. القيمة.محدثة؛ NotUpdated=$_. Value.Count-$_. القيمة.محدثة؛ الشركة المصنعة=$_. Value.Manufacturer } } | Export-Csv -Path (join-Path $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -ترميز UTF8 # إنشاء CSVs متوافقة مع المنسق (أسماء الملفات المتوقعة Start-SecureBootRolloutOrchestrator.ps1) $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json" if (test-Path $notUpdatedJsonPath) { جرب { $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json إذا ($nuData.Count -gt 0) { # NotUptodate CSV - يبحث المنسق عن *NotUptodate*.csv $nuData | Export-Csv -Path (join-Path $OutputPath "SecureBoot_NotUptodate_timestamp.csv$timestamp.csv") -NoTypeInformation -ترميز UTF8 Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count) devices)" -ForegroundColor Gray } } التقاط { } } # كتابة بيانات JSON للوحة المعلومات $stats | ConvertTo-Json -Depth 3 | Set-Content (مسار الانضمام $dataDir "summary.json") -ترميز UTF8 # التتبع التاريخي: حفظ نقطة البيانات لمخطط الاتجاه # استخدم موقع ذاكرة تخزين مؤقت مستقرة حتى تستمر بيانات الاتجاه عبر مجلدات التجميع ذات الطابع الزمني. # إذا كان OutputPath يبدو مثل "...\Aggregation_yyyyMMdd_HHmmss"، تنتقل ذاكرة التخزين المؤقت إلى المجلد الأصل.# وإلا، تنتقل ذاكرة التخزين المؤقت داخل OutputPath نفسه.$parentDir = Split-Path $OutputPath -الأصل $leafName = Split-Path $OutputPath -Leaf إذا ($leafName -match '^Aggregation_\d{8}' -أو $leafName -eq 'Aggregation_Current') { # مجلد الطابع الزمني الذي تم إنشاؤه بواسطة Orchestrator — استخدم الأصل لذاكرة التخزين المؤقت الثابتة $historyPath = Join-Path $parentDir ".cache\trend_history.json" } آخر { $historyPath = Join-Path $OutputPath ".cache\trend_history.json" } $historyDir = Split-Path $historyPath -Parent if (-not (test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null } $historyData = @() إذا (مسار الاختبار $historyPath) { جرب { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } catch { $historyData = @() } } # تحقق أيضا داخل OutputPath\.cache\ (الموقع القديم من الإصدارات القديمة) # دمج أي نقاط بيانات غير موجودة بالفعل في المحفوظات الأساسية إذا ($leafName -eq 'Aggregation_Current' -أو $leafName -match '^Aggregation_\d{8}') { $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json" if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) { جرب { $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json) $existingDates = @($historyData | ForEach-Object { $_. التاريخ }) foreach ($entry في $innerData) { إذا ($entry. التاريخ -$entry. التاريخ -notin $existingDates) { $historyData += $entry } } if ($innerData.Count -gt 0) { Write-Host " دمج نقاط بيانات $($innerData.Count) من ذاكرة التخزين المؤقت الداخلية" -ForegroundColor DarkGray } } التقاط { } } }
# BOOTSTRAP: إذا كانت محفوظات الاتجاه فارغة/متفرقة، فعادة البناء من البيانات التاريخية إذا ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) { Write-Host " Bootstrapping trend history from history data..." -ForegroundColor Yellow $dailyData = @{} # المصدر 1: ملخص CSVs داخل المجلد الحالي (Aggregation_Current يحتفظ بجميع CSVs الملخص) $localSummaries = Get-ChildItem $OutputPath -تصفية "SecureBoot_Summary_*.csv" -EA SilentlyContinue | اسم Sort-Object foreach ($summCsv في $localSummaries) { جرب { $summ = Import-Csv $summCsv.FullName | Select-Object -الأول 1 إذا ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -and $summ. ReportGeneratedAt) { $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("yyyy-MM-dd") $updated = إذا ($summ. محدث) { [int]$summ. تم التحديث } آخر { 0 } $notUpd = إذا ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ التاريخ = $dateStr؛ الإجمالي = [int]$summ. إجمالي الأجهزة؛ محدث = $updated؛ NotUpdated = $notUpd NeedsReboot = 0; الأخطاء = 0؛ ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } التقاط { } } # المصدر 2: مجلدات Aggregation_* الطابع الزمني القديم (قديمة، إذا كانت لا تزال موجودة) $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue | Where-Object { $_. الاسم -مطابقة '^Aggregation_\d{8}' } | اسم Sort-Object foreach ($folder في $aggFolders) { $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -الأول 1 إذا ($summCsv) { جرب { $summ = Import-Csv $summCsv.FullName | Select-Object -الأول 1 إذا ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) { $dateStr = $folder. Name -replace '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3' $updated = إذا ($summ. محدث) { [int]$summ. تم التحديث } آخر { 0 } $notUpd = إذا ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated } $dailyData[$dateStr] = [PSCustomObject]@{ التاريخ = $dateStr؛ الإجمالي = [int]$summ. إجمالي الأجهزة؛ محدث = $updated؛ NotUpdated = $notUpd NeedsReboot = 0; الأخطاء = 0؛ ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 } } } } التقاط { } } } # المصدر 3: RolloutState.json WaveHistory (يحتوي على طوابع زمنية لكل موجة من اليوم 1) # يوفر هذا نقاط بيانات أساسية حتى عندما لا توجد مجلدات تجميع قديمة $rolloutStatePaths = @( (مسار الانضمام $parentDir "RolloutState\RolloutState.json")، (مسار الانضمام $OutputPath "RolloutState\RolloutState.json") ) foreach ($rsPath في $rolloutStatePaths) { إذا (مسار الاختبار $rsPath) { جرب { $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json if ($rsData.WaveHistory) { # استخدام تواريخ بدء الموجة كنقاط بيانات الاتجاه # حساب الأجهزة التراكمية المستهدفة في كل موجة $cumulativeTargeted = 0 foreach ($wave في $rsData.WaveHistory) { إذا ($wave. StartedAt -and $wave. عدد الأجهزة) { $waveDate = ([datetime]$wave. StartedAt). ToString("yyyy-MM-dd") $cumulativeTargeted += [int]$wave. عدد الأجهزة إذا (-not $dailyData.ContainsKey($waveDate)) { # تقريبي: في وقت بدء الموجة، تم تحديث الأجهزة من الموجات السابقة فقط $dailyData[$waveDate] = [PSCustomObject]@{ التاريخ = $waveDate؛ Total = $c.Total; محدث = [math]::Max(0, $cumulativeTargeted - [int]$wave. عدد الأجهزة) NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. عدد الأجهزة) NeedsReboot = 0; الأخطاء = 0؛ ActionRequired = 0 } } } } } } التقاط { } break # Use first found } }
إذا ($dailyData.Count -gt 0) { $historyData = @($dailyData.GetEnumerator() | مفتاح Sort-Object | ForEach-Object { $_. القيمة }) Write-Host " نقاط بيانات $($historyData.Count) من الملخصات التاريخية" -ForegroundColor Green } }
# إضافة نقطة البيانات الحالية (إلغاء التكرار حسب اليوم - احتفظ بأحدث نقطة في اليوم) $todayKey = (Get-Date). ToString("yyyy-MM-dd") $existingToday = $historyData | Where-Object { "$($_. التاريخ)" -مثل "$todayKey*" } إذا ($existingToday) { # استبدال إدخال اليوم $historyData = @($historyData | Where-Object { "$($_. التاريخ)" -notlike "$todayKey*" }) } $historyData += [PSCustomObject]@{ التاريخ = $todayKey الإجمالي = $c.الإجمالي محدث = $c.محدث NotUpdated = $stNotUptodate NeedsReboot = $c.NeedsReboot الأخطاء = $c.WithErrors ActionRequired = $c.ActionReq } # إزالة نقاط البيانات السيئة (إجمالي 0) والحفاظ على آخر 90 $historyData = @($historyData | Where-Object { [int]$_. الإجمالي -gt 0 }) # لا يوجد حد أقصى — بيانات الاتجاه هي ~100 بايت/إدخال، سنة كاملة = ~36 كيلوبايت $historyData | ConvertTo-Json -Depth 3 | Set-Content $historyPath -ترميز UTF8 Write-Host " محفوظات الاتجاه: $($historyData.Count) data points" -ForegroundColor DarkGray # إنشاء بيانات مخطط الاتجاه ل HTML $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join "," $trendUpdated = ($historyData | ForEach-Object { $_. محدث }) -الانضمام "،" $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join "," $trendTotal = ($historyData | ForEach-Object { $_. Total }) -join "," # الإسقاط: توسيع خط الاتجاه باستخدام مضاعفة أسية (2,4,8,16...) # اشتقاق حجم الموجة وفترة الملاحظة من بيانات محفوظات الاتجاه الفعلية.# - حجم الموجة = أكبر زيادة لفترة واحدة شوهدت في التاريخ (أحدث موجة تم نشرها) # - أيام المراقبة = متوسط الأيام التقويمية بين نقاط بيانات الاتجاه (عدد مرات التشغيل) # ثم يضاعف حجم الموجة في كل فترة، ويطابق استراتيجية النمو 2x للمنسق.$projLabels = ""؛ $projUpdated = ""؛ $projNotUpdated = ""؛ $hasProjection = $false if ($historyData.Count -ge 2) { $lastUpdated = $c.Updated $remaining = $stNotUptodate # الأجهزة غير المحدثة فقط SB-ON (باستثناء SecureBoot OFF) $projDates = @()؛ $projValues = @()؛ $projNotUpdValues = @() $projDate = Get-Date
# اشتقاق حجم الموجة وفترة الملاحظة من تاريخ الاتجاه $increments = @() $dayGaps = @() ل ($hi = 1؛ $hi -lt $historyData.Count؛ $hi++) { $inc = $historyData[$hi]. محدث - $historyData[$hi-1]. تحديث إذا ($inc -gt 0) { $increments += $inc } جرب { $d 1 = [datetime]::P arse($historyData[$hi-1]. التاريخ) $d 2 = [datetime]::P arse($historyData[$hi]. التاريخ) $gap = ($d 2 - $d 1). TotalDays إذا ($gap -gt 0) { $dayGaps += $gap } } التقاط {} } حجم الموجة = أحدث زيادة إيجابية (الموجة الحالية)، الرجوع إلى المتوسط، الحد الأدنى 2 $waveSize = إذا ($increments. Count -gt 0) { [math]::Max(2, $increments[-1]) } آخر { 2 } # فترة المراقبة = متوسط الفجوة بين نقاط البيانات (أيام التقويم لكل موجة)، الحد الأدنى 1 $waveDays = if ($dayGaps.Count -gt 0) { [math]::Max(1, [math]::Round(($dayGaps | Measure-Object -Average). متوسط، 0)) } آخر { 1}
Write-Host " Projection: waveSize=$waveSize (من الزيادة الأخيرة)، waveDays=$waveDays (avg gap from history)" -ForegroundColor DarkGray
$dayCounter = 0 # Project حتى يتم تحديث جميع الأجهزة أو 365 يوما كحد أقصى ل ($pi = 1؛ $pi -le 365؛ $pi++) { $projDate = $projDate.AddDays(1) $dayCounter++ # في كل حد لفترة المراقبة، انشر موجة ثم مزدوجة إذا ($dayCounter -ge $waveDays) { $devicesThisWave = [math]::Min($waveSize, $remaining) $lastUpdated += $devicesThisWave $remaining -= $devicesThisWave if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 } # حجم الموجة المزدوجة للفترة القادمة (استراتيجية المنسق 2x) $waveSize = $waveSize * 2 $dayCounter = 0 } $projDates += "'$($projDate.ToString("yyyy-MM-dd"))"" $projValues += $lastUpdated $projNotUpdValues += [math]::Max(0, $remaining) إذا ($remaining -le 0) { break } } $projLabels = $projDates -join "،" $projUpdated = $projValues -join "،" $projNotUpdated = $projNotUpdValues -join "،" $hasProjection = $projDates.Count -gt 0 } elseif ($historyData.Count -eq 1) { Write-Host " الإسقاط: تحتاج إلى نقطتي بيانات اتجاه على الأقل لاشتقاق توقيت الموجة" -ForegroundColor DarkGray } # إنشاء سلاسل بيانات المخطط المجمعة للسلسلة هنا $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } else { $trendLabels } $projDataJS = if ($hasProjection) { $projUpdated } else { "" } $projNotUpdJS = if ($hasProjection) { $projNotUpdated } else { "" } $histCount = ($historyData | Measure-Object). عدد $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -تنازلي | ForEach-Object { @{ name=$_. مفتاح; total=$_. القيمة.الإجمالي؛ تم التحديث=$_. القيمة.محدثة؛ highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq } } | ConvertTo-Json -Depth 3 | Set-Content (مسار الانضمام $dataDir "manufacturers.json") -ترميز UTF8 # تحويل ملفات بيانات JSON إلى CSV لتنزيلات Excel القابلة للقراءة من قبل الإنسان Write-Host "تحويل بيانات الجهاز إلى CSV لتنزيل Excel..." -ForegroundColor Gray foreach ($dfName في $stDeviceFiles) { $jsonFile = Join-Path $dataDir "$dfName.json" $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv" إذا (مسار الاختبار $jsonFile) { جرب { $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json إذا ($jsonData.Count -gt 0) { # تضمين أعمدة إضافية update_pending CSV $selectProps = if ($dfName -eq "update_pending") { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus') } آخر { @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue') } $jsonData | Select-Object $selectProps | Export-Csv -Path $csvFile -NoTypeInformation -ترميز UTF8 Write-Host " $dfName -> $($jsonData.Count) الصفوف -> CSV" -ForegroundColor DarkGray } } التقاط { Write-Host " $dfName - تخطي" -ForegroundColor DarkYellow } } } # إنشاء لوحة معلومات HTML قائمة بذاتها $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html" Write-Host "إنشاء لوحة معلومات HTML قائمة بذاتها..." -ForegroundColor Yellow # إسقاط السرعة: الحساب من محفوظات الفحص أو الملخص السابق $stDeadline = [datetime]"2026-06-24" # انتهاء صلاحية شهادة KEK $stDaysToDeadline = [math]::Max(0, ($stDeadline - (Get-Date)). أيام) $stDevicesPerDay = 0 $stProjectedDate = $null $stVelocitySource = "N/A" $stWorkingDays = 0 $stCalendarDays = 0 # جرب محفوظات الاتجاهات أولا (خفيف الوزن، تم صيانته بالفعل بواسطة مجمع — يستبدل ScanHistory.json المنفخة) if ($historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. محدث -ge 0 }) إذا ($validHistory.Count -ge 2) { $prev = $validHistory[-2]; $curr = $validHistory[-1] $prevDate = [datetime]::P arse($prev. Date.Substring(0, [Math]::Min(10, $prev. التاريخ.الطول))) $currDate = [datetime]::P arse($curr. Date.Substring(0, [Math]::Min(10, $curr. التاريخ.الطول))) $daysDiff = ($currDate - $prevDate). TotalDays إذا ($daysDiff -gt 0) { $updDiff = [int]$curr. محدث - [int]$prev. تحديث إذا ($updDiff -gt 0) { $stDevicesPerDay = [math]::Round($updDiff / $daysDiff, 0) $stVelocitySource = "TrendHistory" } } } } # جرب ملخص إطلاق المنسق (له سرعة حوسبة مسبقة) if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) { جرب { $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.WorkDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkDaysRemaining } if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining } } } التقاط { } } # الاحتياطي: جرب الملخص السابق CSV (البحث في المجلد الحالي ومجلدات التجميع الأصل/التابع) if ($stVelocitySource -eq "N/A") { $searchPaths = @( ($OutputPath "SecureBoot_Summary_*.csv") ) # أيضا البحث في مجلدات التجميع التابعة (ينشئ المنسق مجلدا جديدا كل تشغيل) $parentPath = Split-Path $OutputPath -Parent إذا ($parentPath) { $searchPaths += ($parentPath Join-Path "Aggregation_*\SecureBoot_Summary_*.csv") $searchPaths += ($parentPath Join-Path "SecureBoot_Summary_*.csv") } $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue} | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 إذا ($prevSummary) { جرب { $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv $prevDate = [datetime]$prevStats.ReportGeneratedAt $daysSinceLast = ((Get-Date) - $prevDate). TotalDays إذا ($daysSinceLast -gt 0.01) { $prevUpdated = [int]$prevStats.Updated $updDelta = $c.Updated - $prevUpdated إذا ($updDelta -gt 0) { $stDevicesPerDay = [math]::Round($updDelta / $daysSinceLast, 0) $stVelocitySource = "PreviousReport" } } } التقاط { } } } # الاحتياطي: حساب السرعة من امتداد محفوظات الاتجاه الكامل (الأول مقابل أحدث نقطة بيانات) if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) { $validHistory = @($historyData | Where-Object { [int]$_. Total -gt 0 -and [int]$_. محدث -ge 0 }) إذا ($validHistory.Count -ge 2) { $first = $validHistory[0] $last = $validHistory[-1] $firstDate = [datetime]::P arse($first. Date.Substring(0, [Math]::Min(10, $first. التاريخ.الطول))) $lastDate = [datetime]::P arse($last. Date.Substring(0, [Math]::Min(10, $last. التاريخ.الطول))) $daysDiff = ($lastDate - $firstDate). TotalDays إذا ($daysDiff -gt 0) { $updDiff = [int]$last. محدث - [int]$first. تحديث إذا ($updDiff -gt 0) { $stDevicesPerDay = [math]::Round($updDiff / $daysDiff, 1) $stVelocitySource = "TrendHistory" } } } } # حساب الإسقاط باستخدام المضاعفة الأسية (بما يتماشى مع مخطط الاتجاه) # أعد استخدام بيانات الإسقاط المحسوبة بالفعل للمخطط إذا كانت متوفرة if ($hasProjection -and $projDates.Count -gt 0) { # استخدم آخر تاريخ تم توقعه (عند تحديث جميع الأجهزة) $lastProjDateStr = $projDates[-1] -استبدال ""، "" $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, yyyy") $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Get-Date)). ايام $stWorkingDays = 0 $d = Get-Date ل ($i = 0؛ $i -lt $stCalendarDays؛ $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'السبت' -and $d.DayOfWeek -ne 'sunday') { $stWorkingDays++ } } } elseif ($stDevicesPerDay -gt 0 -and $stNotUptodate -gt 0) { # الاحتياطي: إسقاط خطي إذا لم تتوفر بيانات أسية $daysNeeded = [math]::Ceiling($stNotUptodate / $stDevicesPerDay) $stProjectedDate = (Get-Date). AddDays($daysNeeded). ToString("MMM dd, yyyy") $stWorkingDays = 0؛ $stCalendarDays = $daysNeeded $d = Get-Date ل ($i = 0؛ $i -lt $daysNeeded؛ $i++) { $d = $d.AddDays(1) if ($d.DayOfWeek -ne 'السبت' -and $d.DayOfWeek -ne 'sunday') { $stWorkingDays++ } } } # بناء سرعة HTML $velocityHtml = if ($stDevicesPerDay -gt 0) { "<div><قوية>🚀 الأجهزة/اليوم:</strong> $($stDevicesPerDay.ToString('N0')) (المصدر: $stVelocitySource)</div>" + "<div><>📅 قوية; الإكمال المتوقع:</strong> $stProjectedDate" + $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>⚠ last DEADLINE</span>" } else { " <span style='color:#28a745'>✓ Before deadline</span>" }) + "</div>" + "<div><>🕐 قوية; أيام العمل:</> $stWorkingDays قوية | <أيام>Calendar قوية:</strong> $stCalendarDays</div>" + "<div style='font-size:.8em; color:#888'>الموعد النهائي: 24 يونيو 2026 (انتهاء صلاحية شهادة KEK) | الأيام المتبقية: $stDaysToDeadline</div>" } آخر { "<div style='padding:8px; الخلفية:#fff3cd؛ نصف قطر الحدود:4 بكسل؛ الحد الأيسر:3px > #ffc107 الصلبة" + "<قوي>📅 الإكمال المتوقع:</strong> بيانات غير كافية لحساب السرعة. " + "تشغيل التجميع مرتين على الأقل مع تغييرات البيانات لإنشاء rate.<br/>" + "<نهائي>قوي:</strong> 24 يونيو 2026 (انتهاء صلاحية شهادة KEK) | <أيام>قوية متبقية:</> $stDaysToDeadline</div>" } # العد التنازلي لانتهاء صلاحية الشهادة $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [math]::Max(0, ($certKekExpiry - $certToday). أيام) $daysToUefi = [math]::Max(0, ($certUefiExpiry - $certToday). أيام) $daysToPca = [math]::Max(0, ($certPcaExpiry - $certToday). أيام) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } آخر { '#28a745' } # Helper: قراءة السجلات من JSON، ملخص مستودع الإنشاء + صفوف جهاز N الأولى $maxInlineRows = 200 الدالة Build-InlineTable { param([string]$JsonPath, [int]$MaxRows = 200, [string]$CsvFileName = "") $bucketSummary = "" $deviceRows = "" $totalCount = 0 إذا (مسار الاختبار $JsonPath) { جرب { $data = Get-Content $JsonPath -Raw | ConvertFrom-Json $totalCount = $data. عدد # BUCKET SUMMARY: Group by BucketId، إظهار العد لكل مستودع مع تحديثه من إحصائيات المستودع العمومي إذا ($totalCount -gt 0) { $buckets = $data | Group-Object BucketId | عدد Sort-Object -تنازلي $bucketSummary = "><2 h3 style='font-size:.95em; color:#333; margin:10px 0 5px'><3 By Hardware Bucket ($($buckets. عدد) المستودعات)><4 /h3>" $bucketSummary += "><6 div style='max-height:300px; overflow-y:auto;><الجدول ><margin-bottom:15px'><thead><tr><><5 BucketID><6 /th><النمط='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>محدثة</th><النمط='text-align:right; color:#dc3545'>غير محدثة</th><><1 الشركة المصنعة><2 /th></tr></thead><tbody>" foreach ($b في $buckets) { $bid = if ($b.Name) { $b.Name } else { "(empty)" } $mfr = ($b.Group | Select-Object -الأول 1). WMI_Manufacturer # الحصول على العدد المحدث من إحصائيات المستودع العمومي (جميع الأجهزة في هذا المستودع عبر مجموعة البيانات بأكملها) $lookupKey = $bid $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null } $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } else { 0 } $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } else { $b.Count } $bNotUpdatedGlobal = $bTotalGlobal - $bUpdatedGlobal $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; font-weight:bold'>$bTotalGlobal><8 /td><td style='text-align:right; color:#28a745; font-weight:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; color:#dc3545; font-weight:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n' } $bucketSummary += "</tbody></table></div>" } # تفاصيل الجهاز: الصفوف N الأولى كقوائم مسطحة $slice = $data | Select-Object -أول $MaxRows foreach ($d في $slice) { $conf = $d.ConfidenceLevel $confBadge = if ($conf -match "High") { '<span class="badge-success">High Conf><2 /span>' } elseif ($conf -match "Not Sup") { '<span class="badge-danger">Not Supported><6 /span>' } elseif ($conf -match "Under") { '<span class="badge-info">ضمن Obs><0 /span>' } elseif ($conf -match "Paused") { '<span class="badge-warning">Paused><4 /span>' } else { '<span class="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>' } else { '><08 span class="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' } } التقاط { } } إذا ($totalCount -eq 0) { إرجاع "><44 div style='padding:20px; color:#888; نمط الخط:مائل'><45 لا توجد أجهزة في هذه الفئة.><46 /div>" } $showing = [math]::Min($MaxRows, $totalCount) $header = "><48 div style='margin:5px 0; حجم الخط:.85em؛ color:#666'><49 Total: $($totalCount.ToString("N0")) devices" if ($CsvFileName) { $header += " | ><50 نمط href='$CsvFileName' ='color:#1a237e; font-weight:bold'>📄 تنزيل CSV الكامل ل Excel><3 /a>" } $header += "><55 /div>" $deviceHeader = "><57 h3 style='font-size:.95em; color:#333; margin:10px 0 5px'><58 Device Details (showing first $showing)><59 /h3>" $deviceTable = "><61 div style='max-height:500px; جدول overflow-y:auto'><><thead><tr><><0 HostName><1 /th><><4><5 /th><><8 Model><9 /th><><2 الثقة><3 /th><><6 الحالة><7 /th><خطأ><0><1 /th><><4><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>" إرجاع "$header$bucketSummary$deviceHeader$deviceTable" } # إنشاء جداول مضمنة من ملفات JSON الموجودة بالفعل على القرص، والارتباط ب CSVs $tblErrors = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv" $tblKI = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv" $tblKEK = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv" $tblNotUpd = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv" $tblTaskDis = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv" $tblTemp = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv" $tblPerm = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv" $tblUpdated = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv" $tblActionReq = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv" $tblUnderObs = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv" $tblNeedsReboot = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv" $tblSBOff = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv" $tblRolloutIP = Build-InlineTable -JsonPath (مسار الانضمام $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv" # جدول مخصص للتحديث معلق — يتضمن أعمدة UEFICA2023Status وUEFICA2023Error $tblUpdatePending = "" $upJsonPath = Join-Path $dataDir "update_pending.json" if (test-Path $upJsonPath) { جرب { $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json $upCount = $upData.Count إذا ($upCount -gt 0) { $upHeader = "<div style='margin:5px 0; حجم الخط:.85em؛ color:#666'>Total: $($upCount.ToString("N0")) الأجهزة | <نمط href='SecureBoot_update_pending_$timestamp.csv' ='color:#1a237e; font-weight:bold'>📄 تنزيل CSV الكامل ل Excel><4 /a></div>" $upRows = "" $upSlice = $upData | Select-Object -أول $maxInlineRows foreach ($d في $upSlice) { $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' } $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } آخر { '-' } $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' } $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge-success">Yes><8 /span>' } else { '-' } $upRows += "<tr><td><3 $($d.HostName)</td><td><7 $($d.WMI_Manufacturer)</td><td><1 $($d.WMI_Model)</td><td><5 $uefiSt><6 /td><td><9 $uefiErr><50 /td><td><53 $policyVal><54 /td><td><57 $wincsVal><58 /td><td style='font-size:.75em'>$($d.BucketId)</td></tr><65 'n" } $upShowing = [math]::Min($maxInlineRows, $upCount) $upDevHeader = "<h3 style='font-size:.95em; color:#333; margin:10px 0 5px'>Device Details (showing first $upShowing)</h3>" $upTable = "<div style='max-height:500px; جدول overflow-y:auto'><><><tr><><9 HostName><0 /th><><3 الشركة المصنعة><4 /><8 نموذج><7></th><><1 UEFICA2023Status><2 /th><><5 UEFICA 2023الخطأ><6 /th><نهج><9</th><>مفتاح WinCS</th><><BucketId /th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>" $tblUpdatePending = "$upHeader$upDevHeader$upTable" } آخر { $tblUpdatePending = "<div style='padding:20px; color:#888; نمط الخط:>لا توجد أجهزة في هذه الفئة.</div>" } } التقاط { $tblUpdatePending = "<div style='padding:20px; color:#888; نمط الخط:مائل'>لا توجد أجهزة في هذه الفئة.</div>" } } آخر { $tblUpdatePending = "<div style='padding:20px; color:#888; نمط الخط:مائل'>لا توجد أجهزة في هذه الفئة.</div>" } # العد التنازلي لانتهاء صلاحية الشهادة $certToday = Get-Date $certKekExpiry = [datetime]"2026-06-24" $certUefiExpiry = [datetime]"2026-06-27" $certPcaExpiry = [datetime]"2026-10-19" $daysToKek = [math]::Max(0, ($certKekExpiry - $certToday). أيام) $daysToUefi = [math]::Max(0, ($certUefiExpiry - $certToday). أيام) $daysToPca = [math]::Max(0, ($certPcaExpiry - $certToday). أيام) $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } آخر { '#28a745' } # إنشاء بيانات مخطط الشركة المصنعة المضمنة (أعلى 10 حسب عدد الأجهزة) $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -تنازلي | Select-Object -أول 10 $mfrChartTitle = إذا ($stMfrCounts.Count -le 10) { "By Manufacturer" } else { "Top 10 Manufacturers" } $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Key)'" }) -join "," $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join "," $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join "," $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) -join "," $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join "," $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join "," $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) -join "," $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join "," $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join "," $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join "," # إنشاء جدول الشركة المصنعة $mfrTableRows = "" $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -تنازلي | ForEach-Object { $mfrTableRows += "<tr><td><7 $($_. Key)</td><td>$($_. Value.Total.ToString("N0")) </td><td>$($_. Value.Updated.ToString("N0")) </td><td>$($_. Value.HighConf.ToString("N0")) ><0 /td><td>$($_. Value.ActionReq.ToString("N0")) ><4 /td></tr>'n" } $htmlContent = @" <!>HTML ل DOCTYPE <html lang="en"> ><3 رأس < <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> عنوان <><9 لوحة معلومات حالة شهادة التمهيد الآمن><0 /title><1 <البرنامج النصي src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 ><7 نمط < *{box-sizing:border-box; الهامش:0؛ ترك مساحة:0} body{font-family:'Segoe UI',Tahoma,sans-serif; الخلفية:#f0f2f5؛ اللون:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); color:#fff; padding:20px 30px} .header h1{font-size:1.6em; هامش-أسفل:5px} .header .meta{font-size:.85em; الشفافية:.9} .container{max-width:1400px; الهامش:0 تلقائي؛ padding:20px} .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); الفجوة: 12 بكسل؛ margin:20px 0} .card{background:#fff; نصف قطر الحدود:10 بكسل؛ ترك المساحة:15 بكسل؛ box-shadow:0 2px 8px rgba(0,0,0,.08); الحد الأيسر:4px #ccc الصلبة;انتقال:تحويل .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; الخط-الوزن:700} .card .label{font-size:.8em; color:#666; الهامش العلوي:4px} .card .pct{font-size:.75em; color:#888} .section{background:#fff; نصف قطر الحدود:10 بكسل؛ ترك مساحة:20 بكسل؛ margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; color:#1a237e; هامش-أسفل:10 بكسل; المؤشر:المؤشر؛ user-select:none} .section h2:hover{text-decoration:underline} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; grid-template-columns:1fr 1fr; الفجوة:20 بكسل؛ margin:20px 0} .chart-box{background:#fff; نصف قطر الحدود:10 بكسل؛ ترك مساحة:20 بكسل؛ box-shadow:0 2px 8px rgba(0,0,0,.08)} الجدول{width:100٪; طي الحدود:انهيار؛ حجم الخط:.85em} {background:#e8eaf6; padding:8px 10px; محاذاة النص:لليسار؛ position:sticky; أعلى:0؛ z-index:1} td{padding:6px 10px; الحد السفلي:1px #eee الصلبة} tr:hover{background:#f5f5f5} .badge{display:inline-block; padding:2px 8px;border-radius:10px; حجم الخط:.75em؛ الخط-الوزن:700} .badge-success{background:#d4edda; color:#155724} .badge-danger{background:#f8d7da; color:#721c24} .badge-warning{background:#fff3cd; color:#856404} .badge-info{background:#d1ecf1; color:#0c5460} .top-link{float:right; حجم الخط:.8em؛ color:#1a237e; text-decoration:none} .تذييل{text-align:center; ترك مساحة:20 بكسل؛ اللون:#999; حجم الخط:.8em} a{color:#1a237e} </style><9 ></head >الجسم < <div class="header"> <h1>لوحة معلومات حالة شهادة التمهيد الآمن</h1> <div class="meta">تم إنشاؤه: $($stats. ReportGeneratedAt) | إجمالي الأجهزة: $($c.Total.ToString("N0")) | المستودعات الفريدة: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">
<!-- بطاقات KPI - قابلة للنقر، مرتبطة بالمقاطع --> <div class="cards"> <class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; text-decoration:none; position:relative"><div style="position:absolute; top:8px; اليمين:8 بكسل؛ الخلفية:#dc3545؛ color:#fff; padding:1px 6px; نصف قطر الحدود:8 بكسل؛ حجم الخط:.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)٪ - يحتاج الإجراء><0 /div></a><3 <class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; text-decoration:none; position:relative"><div style="position:absolute; top:8px; اليمين:8 بكسل؛ الخلفية:#28a745؛ color:#fff; padding:1px 6px; نصف قطر الحدود:8 بكسل؛ حجم الخط:.65em؛ font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString() "N0"))</div><div class="label">Updated><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; position:relative"><div style="position:absolute; top:8px; اليمين:8 بكسل؛ الخلفية:#6c757d؛ color:#fff; padding:1px 6px; نصف قطر الحدود:8 بكسل؛ حجم الخط:.65em؛ font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><><0 div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.SBOff/$c.Total)*100,1)}else{0})٪ - خارج النطاق><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="label">Needs Reboot><2 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round((($c.NeedsReboot/$c.Total)*100,1)}else{0})٪ - في انتظار إعادة التشغيل><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><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})٪ - تم تطبيق النهج/WinCS، في انتظار التحديث><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">الإطلاق قيد التقدم><4 /div><div class="pct">الإطلاق قيد التقدم><4 /div><div class="pct">>$(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{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)٪ - آمن للإطلاق><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 ضمن Monitoring><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100,1)}else{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 مطلوبة><2 /div><div class="pct">$($stats. PercentActionRequired)٪ - يجب اختبار><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)٪ - مشابهة لفشل><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">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100,1)}else{0})٪ - حظر><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. تم إيقاف</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)}else{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</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. حالات الفشل</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}else{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)}else{0})٪</div></a><3 </div>
<!-- سرعة التوزيع & انتهاء صلاحية الشهادة --> <div id="s-velocity" style="display:grid; grid-template-columns:1fr 1fr; الفجوة:20 بكسل؛ margin:15px 0"> <div class="section" style="margin:0"> <h2>📅 سرعة التوزيع</h2> <div class="section-body open"> <div style="font-size:2.5em; الخط-الوزن:700; color:#28a745">$($c.Updated.ToString("N0"))) </div> <div style="color:#666">تم تحديث الأجهزة من $($c.Total.ToString("N0"))</div> <div style="margin:10px 0; الخلفية:#e8eaf6؛ الارتفاع:20 بكسل؛ نصف قطر الحدود:10 بكسل؛ overflow:hidden"><div style="background:#28a745; الارتفاع:100٪؛ العرض:$($stats. PercentCertUpdated)٪; حد نصف قطره:10px"></div></div> <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)٪ complete</div> <div style="margin-top:10px; ترك مساحة:10 بكسل؛ الخلفية:#f8f9fa؛ نصف قطر الحدود:8 بكسل؛ حجم الخط:.85em"> تحتاج أجهزة <div><قوية>المتبقية:</strong> $($stNotUptodate.ToString("N0")) إلى إجراء</div> <div><أجهزة>Blocking:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated) (الأخطاء + دائم + تعطيل المهمة)</div> <div><أجهزة>Safe قوية لنشر:</strong> $($stSafeList.ToString("N0")) (نفس المستودع الناجح)</div> $velocityHtml ></div </div> </div> <div class="section" style="margin:0; الحد الأيسر:4px #dc3545 الصلبة"> <h2 style="color:#dc3545">⚠ Certificate Expiry Countdown</h2> <div class="section-body open"> <div style="display:flex; الفجوة:15 بكسل؛ هامش أعلى:10 بكسل"> <div style="text-align:center; ترك المساحة:15 بكسل؛ نصف قطر الحدود:8 بكسل؛ الحد الأدنى للعرض:120 بكسل؛ background:linear-gradient(135deg,#fff5f5,#ffe0e0); حد:2px #dc3545 الصلبة; flex:1"> <div style="font-size:.65em; color:#721c24; تحويل النص: أحرف كبيرة؛ font-weight:bold">⚠ FIRST TO EXPIRE</div> ><4 div style="font-size:.85em; الخط-الوزن:غامق; color:#dc3545; margin:3px 0"><5 KEK CA 2011</div> ><8 div id="daysKek" style="font-size:2.5em; الخط-الوزن:700; color:#dc3545; ارتفاع السطر:1"><9 $daysToKek</div> ><2 div style="font-size:.8em; color:#721c24"><3 days (24 يونيو 2026)><4 /div> ><6 /div> ><8 div style="text-align:center; ترك المساحة:15 بكسل؛ نصف قطر الحدود:8 بكسل؛ الحد الأدنى للعرض:120 بكسل؛ background:linear-gradient(135deg,#fffef5,#fff3cd); حد:2px #ffc107 الصلبة; flex:1"><9 <div style="font-size:.65em; color:#856404; تحويل النص: أحرف كبيرة؛ font-weight:bold">UEFI CA 2011</div> <div id="daysUefi" style="font-size:2.2em; الخط-الوزن:700; color:#856404; ارتفاع السطر:1؛ margin:5px 0">$daysToUefi</div> <div style="font-size:.8em; color:#856404">days (27 يونيو 2026)</div> ></div <div style="text-align:center; ترك المساحة:15 بكسل؛ نصف قطر الحدود:8 بكسل؛ الحد الأدنى للعرض:120 بكسل؛ background:linear-gradient(135deg,#f0f8ff,#d4edff); حد:2px #0078d4 الصلبة; flex:1"> <div style="font-size:.65em; color:#0078d4; تحويل النص: أحرف كبيرة؛ font-weight:bold">Windows PCA</div> <div id="daysPca" style="font-size:2.2em; الخط-الوزن:700; color:#0078d4; ارتفاع السطر:1؛ margin:5px 0">$daysToPca><2 /div><3 <div style="font-size:.8em; color:#0078d4">days (19 أكتوبر 2026)</div><7 </div><9 </div><1 <div style="margin-top:15px; ترك مساحة:10 بكسل؛ الخلفية:#f8d7da؛ نصف قطر الحدود:8 بكسل؛ حجم الخط:.85em؛ #dc3545 الصلبة على الحدود اليسرى:4px"> <يجب تحديث>⚠ strong>⚠ CRITICAL:</strong> جميع الأجهزة قبل انتهاء صلاحية الشهادة. لا يمكن للأجهزة التي لم يتم تحديثها بحلول الموعد النهائي تطبيق تحديثات الأمان المستقبلية لإدارة التمهيد والتمهيد الآمن بعد انتهاء الصلاحية.</div> </div> </div> </div>
<!-- المخططات --> <div class="charts"> <div class="chart-box"><h3>Deployment Status</h3><canvas id="deployChart" height="200"></canvas></div><5 <div class="chart-box"><h3><9 $mfrChartTitle</h3><canvas id="mfrChart" height="200"></canvas></div> </div>
$(if ($historyData.Count -ge 1) { "<!-- مخطط الاتجاه التاريخي --> <div class='section'> <h2 onclick='"toggle('d-trend')'">📈 تحديث التقدم بمرور الوقت <فئة ='top-link' href='#'>↑ أعلى</a></h2> <div id='d-trend' class='section-body open'> <canvas id='trendChart' height='120'></canvas> <div style='font-size:.75em; color:#888; margin-top:5px'>Solid lines = actual data$(if ($historyData.Count -ge 2) { " | خط متقطع = متوقع (مضاعفة أسية: 2→4→8→16... جهاز لكل موجة)" } آخر { " | تشغيل التجميع مرة أخرى غدا لمشاهدة خطوط الاتجاه والإسقاط" })</div> </div> </div>" })
تنزيلات<!-- CSV --> <div class="section"> <h2 onclick="toggle('dl-csv')">📥 تنزيل البيانات الكاملة (CSV ل Excel) <فئة="ارتباط علوي" href="#">أعلى</a></h2><2 <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; gap:5px"> <href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; الخلفية:#dc3545؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; حجم الخط:.8em">غير محدث ($($stNotUptodate.ToString("N0")))) </a><8 <نمط href="SecureBoot_errors_$timestamp.csv" ="display:inline-block; الخلفية:#dc3545؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))) </a> <href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; الخلفية:#fd7e14؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; font-size:.8em">Action مطلوبة ($($c.ActionReq.ToString("N0"))) </a> <href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; الخلفية:#dc3545؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; font-size:.8em">المشكلات المعروفة ($($c.WithKnownIssues.ToString("N0")))) </a> <نمط href="SecureBoot_task_disabled_$timestamp.csv" ="display:inline-block; الخلفية:#dc3545؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; font-size:.8em">Task Disabled ($($c.TaskDisabled.ToString("N0")))) </a> <href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; الخلفية:#28a745؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; font-size:.8em">Updated ($($c.Updated.ToString("N0")))) </a> <href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; الخلفية:#6c757d؛ color:#fff; ترك مساحة:6px 14px; نصف قطر الحدود:5 بكسل؛ text-decoration:none; حجم الخط:.8em">ملخص</a> <div style="width:100٪; حجم الخط:.75em؛ color:#888; هامش أعلى:5px">ملفات CSV مفتوحة في Excel. متوفر عند استضافته على خادم الويب.</div> </div> </div>
<!-- تصنيف الشركة المصنعة --> <div class="section"> <h2 onclick="toggle('mfr')">By Manufacturer <فئة="top-link" href="#">Top</a></h2><1 <div id="mfr" class="section-body open"> جدول <><><الإضافة><><1 الشركة المصنعة><2 /th><><5 إجمالي><6 /th><تحديث><9><0 /th><><3 الثقة العالية><4 /th><الإجراء><7 المطلوب><8 /th></tr></><3 <tbody><5 $mfrTableRows><6 /tbody></table><9 </div><1 > </div
<!-- أقسام الجهاز (أول 200 تنزيل مضمن + CSV) --> <div class="section" id="s-err"> <h2 onclick="toggle('d-err')">🔴 الأجهزة التي بها أخطاء ($($c.WithErrors.ToString("N0"))) <فئة="top-link" href="#">↑ أعلى</a></h2> <div id="d-err" class="section-body">$tblErrors</div> ></div <div class="section" id="s-ki"> <h2 onclick="toggle('d-ki')" style="color:#dc3545">🔴 المشاكل المعروفة ($($c.WithKnownIssues.ToString("N0"))) <class="top-link" href="#">↑ Top</a></h2> <div id="d-ki" class="section-body">$tblKI</div> ></div <div class="section" id="s-kek"> <h2 onclick="toggle('d-kek')">🟠 مفقود KEK - الحدث 1803 ($($c.WithMissingKEK.ToString("N0"))) <فئة="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">🟠 الإجراء المطلوب ($($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">🔵 ضمن المراقبة ($($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">🔴 غير محدث ($($stNotUptodate.ToString("N0"))) <فئة="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">🔴 تم تعطيل المهمة ($($c.TaskDisabled.ToString("N0"))) >↑ 5 فئة="top-link" href="#">↑ أعلى</a></h2><1 <div id="d-td" class="section-body">$tblTaskDis><4 /div><5 </div><7 <div class="section" id="s-tf"> <h2 onclick="toggle('d-tf')" style="color:#dc3545">🔴 حالات الفشل المؤقتة ($($c.TempFailures.ToString("N0"))) <فئة="top-link" href="#">↑ أعلى</a></h2> <div id="d-tf" class="section-body">$tblTemp</div> </div> <div class="section" id="s-pf"> <h2 onclick="toggle('d-pf')" style="color:#721c24">🔴 حالات الفشل الدائمة / غير مدعومة ($($c.PermFailures.ToString("N0")) <فئة="top-link" href="#">↑ Top</a></h2> <div id="d-pf" class="section-body">$tblPerm</div> </div> <div class="section" id="s-upd-pend"> <h2 onclick="toggle('d-upd-pend')" style="color:#6f42c1">⏳ Update Pending ($($c.UpdatePending.ToString("N0"))) - Policy/WinCS Applied، في انتظار التحديث <فئة="top-link" href="#">↑ Top</a></h2> <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Devices حيث يتم تطبيق AvailableUpdatesPolicy أو مفتاح WinCS ولكن UEFICA2023Status لا يزال NotStarted أو InProgress أو null.</p>$tblUpdatePending</div> ></div <div class="section" id="s-rip"> <h2 onclick="toggle('d-rip')" style="color:#17a2b8">🔵 الإطلاق قيد التقدم ($($c.RolloutInProgress.ToString("N0"))) <فئة="top-link" href="#">↑ أعلى</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"))) - خارج النطاق <فئة="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">🟢 الأجهزة المحدثة ($($c.Updated.ToString("N0"))) <فئة="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">🔄 محدث - يحتاج إلى إعادة التشغيل ($($c.NeedsReboot.ToString("N0"))) <فئة="top-link" href="#">↑ Top</a></h2> <div id="d-nrb" class="section-body">$tblNeedsReboot</div> </div>
<div class="تذييل">لوحة معلومات إطلاق شهادة التمهيد الآمن | تم إنشاؤه $($stats. ReportGeneratedAt) | StreamingMode | ذروة الذاكرة: ${stPeakMemMB} ميغابايت</div> </div><!-- /container -->
>البرنامج النصي< تبديل الدالة(id){var e=document.getElementById(id); e.classList.toggle('open')} الدالة openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} مخطط جديد(document.getElementById('deployChart'),{type:'doughnut',data:{labels:['Updated','Update Pending','High Confidence','Under Monitoring','Action Required','Temp. متوقف مؤقتا','غير مدعوم','SecureBoot OFF','With Errors'],datasets:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff)،$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','', #721c24','#adb5bd','#dc3545']}},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); مخطط جديد(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 Monitoring',data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. متوقف مؤقتا',data:[$mfrTempPaused],backgroundColor:'#6c757d'},{label:'Not Supported',data:[$mfrNotSupported],backgroundColor:'#721c24'},{label:'SecureBoot OFF',data:[$mfrSBOff],backgroundColor:'#adb5bd'},{label:'With Errors',data:[$mfrWithErrors],backgroundColor:'#dc3545'}]},options:{responsive:true,scales:{x:{stacked:true},y:{stacked:true}},plugins:{legend:{position:'top'}}}})؛ مخطط الاتجاه التاريخي if (document.getElementById('trendChart')) { var allLabels = [$allChartLabels]; var actualUpdated = [$trendUpdated]; var actualNotUpdated = [$trendNotUpdated]; var actualTotal = [$trendTotal]; var projData = [$projDataJS]; var projNotUpdData = [$projNotUpdJS]; var histLen = actualUpdated.length; var projLen = projData.length; var paddedUpdated = actualUpdated.concat(Array(projLen).fill(null)); var paddedNotUpdated = actualNotUpdated.concat(Array(projLen).fill(null)); var paddedTotal = actualTotal.concat(Array(projLen).fill(null)); var projLine = Array(histLen).fill(null); var projNotUpdLine = Array(histLen).fill(null); if (projLen > 0) { projLine[histLen-1] = actualUpdated[histLen-1]; projLine = projLine.concat(projData); projNotUpdLine[histLen-1] = actualNotUpdated[histLen-1]; projNotUpdLine = projNotUpdLine.concat(projNotUpdData); } مجموعات بيانات var = [ {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2}, {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]؛ if (projLen > 0) { datasets.push({label:'Projected Updated (2x الضعف)',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'})؛ } مخطط جديد(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'}}}}); } العد التنازلي الديناميكي (function(){var t=new Date(),k=new Date('2026-06-24'),u=new Date('2026-06-27'),p=new Date('2026-10-19')؛ var dk=document.getElementById('daysKek'),du=document.getElementById('daysUefi'),dp=document.getElementById('daysPca')؛ if(dk)dk.textContent=Math.max(0,Math.ceil((k-t)/864e5)); if(du)du.textContent=Math.max(0,Math.ceil((u-t)/864e5)); if(dp)dp.textContent=Math.max(0,Math.ceil((p-t)/864e5))}))(); ></script </> الجسم </html> "@ [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]::new($false)) # احتفظ دائما بنسخة "أحدث" مستقرة حتى لا يحتاج المسؤولون إلى تعقب الطوابع الزمنية $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html" Copy-Item $htmlPath $latestPath -Force $stTotal = $streamSw.Elapsed.TotalSeconds # حفظ بيان الملف للوضع التزايدي (الكشف السريع عن عدم التغيير في التشغيل التالي) إذا ($IncrementalMode -أو $StreamingMode) { $stManifestDir = Join-Path $OutputPath ".ذاكرة التخزين المؤقت" if (-not (test-Path $stManifestDir)) { New-Item -ItemType Directory -Path $stManifestDir -Force | Out-Null } $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json" $stNewManifest = @{} Write-Host "حفظ بيان الملف لوضع تزايدي..." -ForegroundColor Gray foreach ($jf في $jsonFiles) { $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{ LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o") الحجم = $jf. طول } } Save-FileManifest -بيان $stNewManifest -مسار $stManifestPath Write-Host " بيان محفوظ لملفات $($stNewManifest.Count) " -ForegroundColor DarkGray } # تنظيف الاستبقاء # مجلد Orchestrator القابل لإعادة الاستخدام (Aggregation_Current): احتفظ بأحدث تشغيل فقط (1) # مسؤول عمليات التشغيل اليدوية / المجلدات الأخرى: الاحتفاظ بآخر 7 عمليات تشغيل # لا يتم حذف ملفات CSV الموجزة مطلقا — فهي صغيرة (~1 كيلوبايت) وهي مصدر النسخ الاحتياطي لمحفوظات الاتجاهات $outputLeaf = Split-Path $OutputPath -Leaf $retentionCount = if ($outputLeaf -eq 'Aggregation_Current') { 1 } else { 7 } # بادئات الملفات آمنة للتنظيف (لقطات سريعة الزوال لكل تشغيل) $cleanupPrefixes = @( "SecureBoot_Dashboard_"، "SecureBoot_action_required_"، "SecureBoot_ByManufacturer_"، "SecureBoot_ErrorCodes_"، "SecureBoot_errors_"، "SecureBoot_known_issues_"، "SecureBoot_missing_kek_"، "SecureBoot_needs_reboot_"، "SecureBoot_not_updated_"، "SecureBoot_secureboot_off_"، "SecureBoot_task_disabled_"، "SecureBoot_temp_failures_"، "SecureBoot_perm_failures_"، "SecureBoot_under_observation_"، "SecureBoot_UniqueBuckets_"، "SecureBoot_update_pending_"، "SecureBoot_updated_devices_"، "SecureBoot_rollout_inprogress_"، "SecureBoot_NotUptodate_"، "SecureBoot_Kusto_" ) # البحث عن جميع الطوابع الزمنية الفريدة من الملفات القابلة للتنظيف فقط $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue | Where-Object { $f = $_. اسم; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Count -gt 0 } $allTimestamps = @($cleanableFiles | ForEach-Object { if ($_. Name -match '(\d{8}-\d{6})') { $Matches[1] } } | Sort-Object -فريد -تنازلي) if ($allTimestamps.Count -gt $retentionCount) { $oldTimestamps = $allTimestamps | Select-Object -تخطي $retentionCount $removedFiles = 0؛ $freedBytes = 0 foreach ($oldTs في $oldTimestamps) { foreach ($prefix في $cleanupPrefixes) { $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue foreach ($f في $oldFiles) { $freedBytes += $f.Length Remove-Item $f.FullName -Force -EA SilentlyContinue $removedFiles++ } } } $freedMB = [math]::Round($freedBytes / 1MB, 1) Write-Host "تنظيف الاستبقاء: تمت إزالة ملفات $removedFiles من عمليات التشغيل القديمة $($oldTimestamps.Count)، التي تم تحريرها ${freedMB} MB (الاحتفاظ $retentionCount الأخيرة + جميع الملخص/NotUptodate CSVs)" -ForegroundColor DarkGray } Write-Host "'n$("=" * 60)" -ForegroundColor Cyan Write-Host "STREAMING AGGREGATION COMPLETE" -ForegroundColor Green Write-Host ("=" * 60) -ForegroundColor Cyan Write-Host " إجمالي الأجهزة: $($c.Total.ToString("N0"))" -ForegroundColor White Write-Host " NOT UPDATED: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)٪)" -ForegroundColor $(if ($stNotUptodate -gt 0) { "Yellow" } else { "Green" }) Write-Host " Updated: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)٪)" -ForegroundColor Green Write-Host " مع الأخطاء: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" }) Write-Host " Peak Memory: ${stPeakMemMB} MB" -ForegroundColor Cyan Write-Host " Time: $([math]::Round($stTotal/60,1)) min" -ForegroundColor White Write-Host " Dashboard: $htmlPath" -ForegroundColor White إرجاع [PSCustomObject]$stats } وضع دفق #endregion } آخر { Write-Error "لم يتم العثور على مسار الإدخال: $InputPath" الخروج 1 }