انسخ هذا البرنامج النصي النموذجي والصقه وقم بتعديله حسب الحاجة لبيئتك:
<# . خلاصه منسق إطلاق التمهيد الآمن المستمر الذي يتم تشغيله حتى اكتمال النشر.
.DESCRIPTION يوفر هذا البرنامج النصي أتمتة كاملة من طرف إلى طرف لطرح شهادة التمهيد الآمن: 1. إنشاء موجات الإطلاق استنادا إلى بيانات التجميع 2. إنشاء مجموعات AD وGPO لكل موجة 3. مراقبة تحديثات الجهاز (الحدث 1808) 4. الكشف عن المستودعات المحظورة (الأجهزة التي لا يمكن الوصول إليها) 5. التقدم إلى الموجة التالية تلقائيا 6. يعمل حتى يتم تحديث جميع الأجهزة المؤهلة معايير الإكمال: - لا توجد أجهزة متبقية في: الإجراء مطلوب، ثقة عالية، مراقبة، متوقف مؤقتا - خارج النطاق (حسب التصميم): غير مدعوم، تم تعطيل التمهيد الآمن - يعمل بشكل مستمر حتى يكتمل - لا يوجد حد موجة عشوائي استراتيجية الإطلاق: - ثقة عالية: جميع الأجهزة في الموجة الأولى (آمنة) - الإجراء المطلوب: مزدوج تدريجي (1→2→4→8...) منطق الحظر: - بعد MaxWaitHours، يقوم المنسق ب pings الأجهزة التي لم يتم تحديثها - إذا كان الجهاز غير قابل للوصول (فشل اختبار الاتصال) → يتم حظر المستودع للتحقيق - إذا كان الجهاز قابلا للوصول ولكن لم يتم تحديثه → استمر في الانتظار (قد تحتاج إلى إعادة التشغيل) - يتم استبعاد المستودعات المحظورة حتى يقوم المسؤول بإلغاء حظرها إلغاء الحظر التلقائي: - إذا ظهر جهاز في مستودع محظور لاحقا على أنه محدث (الحدث 1808)، يتم إلغاء حظر المستودع تلقائيا ومتابعة الإطلاق - يعالج هذا الأجهزة التي كانت غير متصلة مؤقتا ولكنها عادت تعقب الجهاز: - يتعقب الأجهزة حسب اسم المضيف (يفترض أن الأسماء لا تتغير أثناء الإطلاق) - ملاحظة: لا تتضمن مجموعة JSON معرف جهاز فريد؛ إضافة واحدة لتعقب أفضل
.PARAMETER AggregationInputPath المسار إلى بيانات جهاز JSON الأولية (من الكشف عن البرنامج النصي)
.PARAMETER ReportBasePath المسار الأساسي لتقارير التجميع
.PARAMETER TargetOU الاسم المميز للوحدة التنظيمية لربط عناصر نهج المجموعة.اختياري - إذا لم يتم تحديده، يتم ربط عنصر نهج المجموعة بجذر المجال للتغطية على مستوى المجال.تضمن تصفية مجموعة الأمان تلقي الأجهزة المستهدفة فقط للنهج.
.PARAMETER MaxWaitHours ساعات انتظار تحديث الأجهزة قبل التحقق من إمكانية الوصول.بعد هذا الوقت، يتم اختبار اتصال الأجهزة التي لم يتم تحديثها.تتسبب الأجهزة غير القابلة للوصول في حظر المستودع.الافتراضي: 72 (3 أيام)
.PARAMETER PollIntervalMinutes الدقائق بين عمليات التحقق من الحالة. الافتراضي: 1440 (يوم واحد)
.PARAMETER AllowListPath المسار إلى ملف يحتوي على أسماء مضيفين للسماح بالطرح (الإطلاق المستهدف).يدعم .txt (اسم مضيف واحد لكل سطر) أو .csv (مع عمود اسم المضيف/اسم الكمبيوتر/الاسم).عند تحديدها، سيتم تضمين هذه الأجهزة فقط في الإطلاق.لا تزال قائمة الحظر مطبقة بعد AllowList.
.PARAMETER AllowADGroup اسم مجموعة أمان AD تحتوي على حسابات كمبيوتر ل ALLOW.مثال: "SecureBoot-Pilot-Computers" أو "Wave1-Devices" عند تحديدها، سيتم تضمين الأجهزة فقط في هذه المجموعة في الإطلاق.ادمج مع AllowListPath لكل من الاستهداف المستند إلى الملف و AD.
.PARAMETER ExclusionListPath المسار إلى ملف يحتوي على أسماء مضيفين لاستبعاد من الإطلاق (أجهزة VIP/تنفيذية).يدعم .txt (اسم مضيف واحد لكل سطر) أو .csv (مع عمود اسم المضيف/اسم الكمبيوتر/الاسم).لن يتم تضمين هذه الأجهزة أبدا في أي موجة إطلاق.يتم تطبيق قائمة الحظر بعد تصفية قائمة السماح. . PARAMETER ExcludeADGroup اسم مجموعة أمان AD تحتوي على حسابات كمبيوتر لاستبعادها.مثال: "VIP-Computers" أو "Executive-Devices" ادمج مع ExclusionListPath لكل من الاستثناءات المستندة إلى الملف و AD.
.PARAMETER UseWinCS استخدم WinCS (نظام تكوين Windows) بدلا من GPO/AvailableUpdatesPolicy.يقوم WinCS بتوزيع تمكين التمهيد الآمن عن طريق تشغيل WinCsFlags.exe مباشرة على كل نقطة نهاية.يتم تشغيل WinCsFlags.exe ضمن سياق SYSTEM عبر مهمة مجدولة.هذا الأسلوب مفيد ل: - إطلاقات أسرع (تأثير فوري مقابل انتظار معالجة عنصر نهج المجموعة) - الأجهزة غير المرتبطة بالمجال - بيئات بدون بنية أساسية ل AD/GPO المرجع: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe
.PARAMETER WinCSKey مفتاح WinCS لاستخدامه لتمكين التمهيد الآمن.الافتراضي: F33E0C8E002 يتوافق هذا المفتاح مع تكوين إطلاق التمهيد الآمن. . تشغيل المعلمة إظهار ما سيتم القيام به دون إجراء تغييرات
.PARAMETER ListBlockedBuckets عرض جميع المستودعات المحظورة حاليا والخروج منها
.PARAMETER UnblockBucket إلغاء حظر مستودع معين بواسطة المفتاح والخروج
.PARAMETER UnblockAll إلغاء حظر جميع المستودعات والخروج منها
.PARAMETER EnableTaskOnDisabled انشر Enable-SecureBootUpdateTask.ps1 على جميع الأجهزة ذات المهمة المجدولة المعطلة.إنشاء عنصر نهج المجموعة مع مهمة مجدولة لمرة واحدة تقوم بتشغيل الخيار تمكين البرنامج النصي مع -الهدوء.هذا مفيد لإصلاح الأجهزة التي تم تعطيل مهمة Secure-Boot-Update.
.EXAMPLE .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -TargetOU "OU=Workstations,DC=contoso,DC=com"
.EXAMPLE # قائمة المستودعات المحظورة .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets
.EXAMPLE # إلغاء حظر مستودع معين .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -إلغاء حظرBucket "Dell_Latitude5520_BIOS1.2.3"
.EXAMPLE # استبعاد أجهزة VIP من الإطلاق باستخدام ملف نصي .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExclusionListPath "C:\Admin\VIP-Devices.txt"
.EXAMPLE # استبعاد الأجهزة في مجموعة أمان AD (على سبيل المثال، أجهزة الكمبيوتر المحمولة التنفيذية) .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExcludeADGroup "VIP-Computers"
.EXAMPLE # استخدم WinCS (نظام تكوين Windows) بدلا من GPO/AvailableUpdatesPolicy # WinCsFlags.exe يعمل ضمن سياق النظام على كل نقطة نهاية عبر المهمة المجدولة .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -UseWinCS ' -WinCSKey "F33E0C8E002" #>
[CmdletBinding()] param( [Parameter(إلزامي = $false)] [سلسلة]$AggregationInputPath، [Parameter(إلزامي = $false)] [سلسلة]$ReportBasePath، [Parameter(إلزامي = $false)] [سلسلة]$TargetOU، [Parameter(إلزامي = $false)] [string]$WavePrefix = "SecureBoot-Rollout"، [Parameter(إلزامي = $false)] [int]$MaxWaitHours = 72، [Parameter(إلزامي = $false)] [int]$PollIntervalMinutes = 1440،
[Parameter(Mandatory = $false)] [int]$ProcessingBatchSize = 5000،
[Parameter(Mandatory = $false)] [int]$DeviceLogSampleSize = 25،
[Parameter(Mandatory = $false)] [switch]$LargeScaleMode، # ============================================================================ # معلمات قائمة السماح / قائمة الحظر # ============================================================================ # AllowList = تضمين هذه الأجهزة فقط (الإطلاق المستهدف) # BlockList = استبعاد هذه الأجهزة (لن يتم طرحها أبدا) # ترتيب المعالجة: AllowList أولا (إذا تم تحديده)، ثم BlockList [Parameter(إلزامي = $false)] [string]$AllowListPath، [Parameter(إلزامي = $false)] [string]$AllowADGroup، [Parameter(إلزامي = $false)] [string]$ExclusionListPath، [Parameter(إلزامي = $false)] [string]$ExcludeADGroup، # ============================================================================ معلمات #WinCS (نظام تكوين Windows) # ============================================================================ # WinCS هو بديل لتوزيع AvailableUpdatesPolicy GPO. # يستخدم WinCsFlags.exe على كل نقطة نهاية لتمكين إطلاق التمهيد الآمن.# WinCsFlags.exe يعمل ضمن سياق SYSTEM على نقطة النهاية.مرجع # : https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe [Parameter(إلزامي = $false)] [switch]$UseWinCS، [Parameter(إلزامي = $false)] [string]$WinCSKey = "F33E0C8E002"، [Parameter(إلزامي = $false)] [switch]$DryRun، [Parameter(إلزامي = $false)] [switch]$ListBlockedBuckets، [Parameter(إلزامي = $false)] [سلسلة]$UnblockBucket، [Parameter(إلزامي = $false)] [switch]$UnblockAll، [Parameter(إلزامي = $false)] [switch]$EnableTaskOnDisabled )
$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "عينات التوزيع والمراقبة"
# ============================================================================ # التحقق من صحة التبعية # ============================================================================
function Test-ScriptDependencies { param( [Parameter(إلزامي = $true)] [string]$ScriptDirectory، [Parameter(إلزامي = $true)] [string[]]$RequiredScripts ) $missingScripts = @() foreach ($script في $RequiredScripts) { $scriptPath = Join-Path $ScriptDirectory $script if (-not (test-Path $scriptPath)) { $missingScripts += $script } } إذا ($missingScripts.Count -gt 0) { Write-Host "" Write-Host ("=" * 70) -ForegroundColor Red Write-Host "MISSING DEPENDENCIES" -ForegroundColor Red Write-Host ("=" * 70) -ForegroundColor Red Write-Host "" Write-Host "لم يتم العثور على البرامج النصية المطلوبة التالية:" -ForegroundColor Yellow foreach ($script في $missingScripts) { Write-Host " - $script" -ForegroundColor White } Write-Host "" Write-Host "يرجى تنزيل أحدث البرامج النصية من:" -ForegroundColor Cyan Write-Host " URL: $DownloadUrl" -ForegroundColor White Write-Host " انتقل إلى: "$DownloadSubPage" -ForegroundColor White Write-Host "" Write-Host "استخراج جميع البرامج النصية إلى نفس الدليل وتشغيلها مرة أخرى." -ForegroundColor Yellow Write-Host "" إرجاع $false } إرجاع $true }
# Required scripts for orchestrator $requiredScripts = @( "Aggregate-SecureBootData.ps1"، "Enable-SecureBootUpdateTask.ps1"، "Deploy-GPO-SecureBootCollection.ps1"، "Detect-SecureBootCertUpdateStatus.ps1" )
if (-not (Test-ScriptDependencies -ScriptDirectory $PSScriptRoot -RequiredScripts $requiredScripts)) { الخروج 1 }
# ============================================================================ # التحقق من صحة المعلمة # ============================================================================
# Admin commands only need ReportBasePath $isAdminCommand = $ListBlockedBuckets -أو $UnblockBucket -أو $UnblockAll -أو $EnableTaskOnDisabled
if (-not $ReportBasePath) { Write-Host "ERROR: -ReportBasePath مطلوب." -ForegroundColor Red الخروج 1 }
if (-not $isAdminCommand -and -not $AggregationInputPath) { Write-Host "ERROR: -AggregationInputPath مطلوب للطرح (غير مطلوب ل -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red الخروج 1 }
# ============================================================================ # الكشف عن عنصر نهج المجموعة - التحقق من الكشف عن عنصر نهج المجموعة # ============================================================================
if (-not $isAdminCommand -and -not $DryRun) { $CollectionGPOName = "SecureBoot-EventCollection" # تحقق مما إذا كانت الوحدة النمطية GroupPolicy متوفرة if (Get-Module -ListAvailable -Name GroupPolicy) { Import-Module GroupPolicy -ErrorAction SilentlyContinue Write-Host "التحقق من الكشف عن عنصر نهج المجموعة..." -ForegroundColor الأصفر جرب { # تحقق مما إذا كان عنصر نهج المجموعة موجودا $existingGpo = Get-GPO -name $CollectionGPOName -ErrorAction SilentlyContinue if ($existingGpo) { Write-Host " الكشف عن عنصر نهج المجموعة الذي تم العثور عليه: $CollectionGPOName" -ForegroundColor Green } آخر { Write-Host "" Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host " WARNING: DETECTION GPO NOT FOUND" -ForegroundColor Yellow Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host "" Write-Host "لم يتم العثور على عنصر نهج المجموعة للكشف "$CollectionGPOName". -ForegroundColor Yellow Write-Host "بدون عنصر نهج المجموعة هذا، لن يتم جمع بيانات الجهاز." -ForegroundColor Yellow Write-Host "" Write-Host "لنشر عنصر نهج المجموعة للكشف، قم بتشغيل:" -ForegroundColor Cyan Write-Host " .\Deploy-GPO-SecureBootCollection.ps1 -DomainName <المجال> -AutoDetectOU" -ForegroundColor White Write-Host "" Write-Host "متابعة على أي حال؟ (Y/N)" -ForegroundColor Yellow $response = Read-Host إذا ($response -notmatch '^[Yy]') { Write-Host "إجهاض" نشر عنصر نهج المجموعة للكشف أولا." -ForegroundColor Red الخروج 1 } } } التقاط { Write-Host " غير قادر على التحقق من عنصر نهج المجموعة: $($_. Exception.Message)" -ForegroundColor Yellow } } آخر { Write-Host " الوحدة النمطية GroupPolicy غير متوفرة - تخطي التحقق من عنصر نهج المجموعة" -ForegroundColor Gray } Write-Host "" }
# ============================================================================ # مسارات ملف الحالة # ============================================================================
$stateDir = Join-Path $ReportBasePath "RolloutState" if (-not (test-Path $stateDir)) { New-Item -ItemType Directory -Path $stateDir -Force | خارج فارغة }
$rolloutStatePath = Join-Path $stateDir "RolloutState.json" $blockedBucketsPath = Join-Path $stateDir "BlockedBuckets.json" $adminApprovedPath = Join-Path $stateDir "AdminApprovedBuckets.json" $deviceHistoryPath = Join-Path $stateDir "DeviceHistory.json" $processingCheckpointPath = Join-Path $stateDir "ProcessingCheckpoint.json"
# ============================================================================ # PS 5.1 التوافق: ConvertTo-Hashtable # ============================================================================ # ConvertFrom-Json -AsHashtable هو PS7+ فقط. يوفر هذا التوافق.
function ConvertTo-Hashtable { param( [Parameter(ValueFromPipeline = $true)] $InputObject ) العملية { إذا كان ($null -eq $InputObject) { إرجاع @{} } إذا ($InputObject -is [System.Collections.IDictionary]) { return $InputObject } إذا ($InputObject -is [PSCustomObject]) { # استخدم [ordered] لترتيب المفاتيح المتسقة ومعالجة التكرارات الآمنة $hash = [ordered]@{} foreach ($prop في $InputObject.PSObject.Properties) { # يعالج التعيين المفهرس التكرارات بأمان عن طريق الكتابة فوقها $hash[$prop. الاسم] = ConvertTo-Hashtable $prop. قيمه } إرجاع $hash } إذا ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { إرجاع @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ }) } إرجاع $InputObject } }
# ============================================================================ # أوامر المسؤول: قائمة/إلغاء حظر المستودعات # ============================================================================
if ($ListBlockedBuckets) { Write-Host "" Write-Host ("=" * 80) -ForegroundColor Yellow Write-Host " BLOCKED BUCKETS" -ForegroundColor Yellow Write-Host ("=" * 80) -ForegroundColor Yellow Write-Host "" إذا (مسار الاختبار $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable إذا ($blocked. Count -eq 0) { Write-Host "لا توجد مستودعات محظورة." -ForegroundColor Green } آخر { Write-Host "إجمالي الحظر: $($blocked. Count)" -ForegroundColor Red Write-Host "" foreach ($key في $blocked. المفاتيح) { $info = $blocked[$key] Write-Host "مستودع: $key" -ForegroundColor Red Write-Host " محظور في: $($info. BlockedAt)" -ForegroundColor Gray Write-Host " السبب: $($info. السبب)" -ForegroundColor Gray Write-Host " فشل الجهاز: $($info. FailedDevice)" -ForegroundColor Gray Write-Host " آخر تقرير: $($info. LastReported)" -ForegroundColor Gray Write-Host " موجة: $($info. WaveNumber)" -ForegroundColor Gray Write-Host " الأجهزة في المستودع: $($info. DevicesInBucket)" -ForegroundColor Gray Write-Host "" } Write-Host "لإلغاء حظر مستودع:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Cyan Write-Host "" Write-Host "لإلغاء حظر الكل:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Cyan } } آخر { Write-Host "لم يتم العثور على ملف مستودعات محظورة." -ForegroundColor Green } Write-Host "" إنهاء 0 }
if ($UnblockBucket) { Write-Host "" if (test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable إذا ($blocked. Contains($UnblockBucket)) { $blocked. إزالة($UnblockBucket) $blocked | ConvertTo-Json -Depth 10 | Out-File $blockedBucketsPath -ترميز UTF8 -Force # إضافة إلى القائمة المعتمدة من قبل المسؤول لمنع إعادة الحظر $adminApproved = @{} إذا (مسار الاختبار $adminApprovedPath) { $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } $adminApproved[$UnblockBucket] = @{ ApprovedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ApprovedBy = $env:USERNAME } $adminApproved | ConvertTo-Json -Depth 10 | Out-File $adminApprovedPath -ترميز UTF8 -Force Write-Host "مستودع غير محظور: $UnblockBucket" -ForegroundColor Green Write-Host "تمت إضافتها إلى القائمة المعتمدة من المسؤول (لن تتم إعادة حظرها تلقائيا)" -ForegroundColor Cyan } آخر { Write-Host "لم يتم العثور على المستودع: $UnblockBucket" -ForegroundColor Yellow Write-Host "المستودعات المتوفرة:" -ForegroundColor Gray $blocked. المفاتيح | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } } } آخر { Write-Host "لم يتم العثور على ملف مستودعات محظورة." -ForegroundColor Yellow } Write-Host "" إنهاء 0 }
if ($UnblockAll) { Write-Host "" if (test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable $count = $blocked. عدد @{} | ConvertTo-Json | Out-File $blockedBucketsPath -ترميز UTF8 -Force Write-Host "إلغاء حظر جميع مستودعات $count." -ForegroundColor Green } آخر { Write-Host "لم يتم العثور على ملف مستودعات محظورة." -ForegroundColor Yellow } Write-Host "" إنهاء 0 }
# ============================================================================ # وظائف المساعد # ============================================================================
function Get-RolloutState { if (test-Path $rolloutStatePath) { جرب { $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable # التحقق من صحة الخصائص المطلوبة الموجودة if ($null -eq $loaded. CurrentWave) { طرح "ملف حالة غير صالح - مفقود CurrentWave" } # تأكد من أن WaveHistory هو دائما صفيف (يصلح إلغاء تسلسل PS5.1 JSON) if ($null -eq $loaded. WaveHistory) { $loaded. WaveHistory = @() } elseif ($loaded. WaveHistory -isnot [array]) { $loaded. WaveHistory = @($loaded. تاريخ الموجة) } إرجاع $loaded } التقاط { Write-Log "تم الكشف عن RolloutState.json تالف: $($_. Exception.Message)" "WARN" Write-Log "النسخ الاحتياطي لملف تالف والبدء من جديد" "WARN" $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyyMMdd-HHmmss')" Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue } } إرجاع @{ CurrentWave = 0 StartedAt = $null LastAggregation = $null TotalDevicesTargeted = 0 TotalDevicesUpdated = 0 الحالة = "NotStarted" WaveHistory = @() } }
function Save-RolloutState { param($State) $State | ConvertTo-Json -Depth 10 | Out-File $rolloutStatePath -ترميز UTF8 -Force }
function Get-WeekdayProjection { <# . خلاصه حساب حساب تاريخ الإكمال المتوقع لعطلات نهاية الأسبوع (لا يوجد تقدم في السبت/الشمس) #> param( [int]$RemainingDevices، [مزدوج]$DevicesPerDay، [datetime]$StartDate = (Get-Date) ) إذا ($DevicesPerDay -le 0 -أو $RemainingDevices -le 0) { إرجاع @{ ProjectedDate = $null WorkDaysNeededed = 0 CalendarDaysNeededed = 0 } } # حساب أيام العمل المطلوبة (باستثناء عطلات نهاية الأسبوع) $workingDaysNeeded = [math]::Ceiling($RemainingDevices / $DevicesPerDay) # تحويل أيام العمل إلى أيام تقويمية (إضافة عطلات نهاية الأسبوع) $currentDate = $StartDate.Date $daysAdded = 0 $workingDaysAdded = 0 بينما ($workingDaysAdded -lt $workingDaysNeeded) { $currentDate = $currentDate.AddDays(1) $daysAdded++ # عد أيام الأسبوع فقط if ($currentDate.DayOfWeek -ne [DayOfWeek]::السبت -and $currentDate.DayOfWeek -ne [DayOfWeek]::الأحد) { $workingDaysAdded++ } } إرجاع @{ ProjectedDate = $currentDate.ToString("yyyy-MM-dd") WorkDaysNeededed = $workingDaysNeeded CalendarDaysNeededed = $daysAdded } }
function Save-RolloutSummary { <# . خلاصه حفظ ملخص الإطلاق مع معلومات الإسقاط لعرض لوحة المعلومات #> param( [hashtable]$State، [int]$TotalDevices، [int]$UpdatedDevices، [int]$NotUpdatedDevices، [مزدوج]$DevicesPerDay ) $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" # حساب الإسقاط المدرك لعطلة نهاية الأسبوع $projection = Get-WeekdayProjection -$NotUpdatedDevices -DevicesPerDay $DevicesPerDay $summary = @{ GeneratedAt = (Get-Date -Format "yyyy-MM-dd HH:mm:ss") RolloutStartDate = $State.StartedAt LastAggregation = $State.LastAggregation CurrentWave = $State.CurrentWave الحالة = $State.الحالة عدد الأجهزة # TotalDevices = $TotalDevices UpdatedDevices = $UpdatedDevices NotUpdatedDevices = $NotUpdatedDevices PercentUpdated = if ($TotalDevices -gt 0) { [math]::Round(($UpdatedDevices / $TotalDevices) * 100, 1) } else { 0 } # قياسات السرعة DevicesPerDay = [math]::Round($DevicesPerDay, 1) TotalDevicesTargeted = $State.TotalDevicesTargeted TotalWaves = $State.CurrentWave # إسقاط مدرك لعطلة نهاية الأسبوع ProjectedCompletionDate = $projection. تاريخ العرض WorkDaysRemaining = $projection. WorkDaysNeeded CalendarDaysRemaining = $projection. تم طلب التقويم # ملاحظة حول استبعاد عطلة نهاية الأسبوع ProjectionNote = "الإكمال المتوقع يستبعد عطلات نهاية الأسبوع (السبت/الشمس)" } $summary | ConvertTo-Json -عمق 5 | Out-File $summaryPath -ترميز UTF8 -Force Write-Log "تم حفظ ملخص الإطلاق: $summaryPath" "INFO" إرجاع $summary }
function Get-BlockedBuckets { if (test-Path $blockedBucketsPath) { إرجاع Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } إرجاع @{} }
function Save-BlockedBuckets { param($Blocked) $Blocked | ConvertTo-Json -Depth 10 | Out-File $blockedBucketsPath -ترميز UTF8 -Force }
function Get-AdminApproved { إذا (مسار الاختبار $adminApprovedPath) { إرجاع Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } إرجاع @{} }
function Get-DeviceHistory { if (test-Path $deviceHistoryPath) { إرجاع Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } إرجاع @{} }
function Save-DeviceHistory { param($History) $History | ConvertTo-Json -Depth 10 | Out-File $deviceHistoryPath -ترميز UTF8 -Force }
function Save-ProcessingCheckpoint { param( [سلسلة]$Stage، [int]$Processed، [int]$Total، [hashtable]$Metrics = @{} )
$checkpoint = @{ المرحلة = $Stage UpdatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" تمت المعالجة = $Processed الإجمالي = $Total Percent = if ($Total -gt 0) { [math]::Round(($Processed / $Total) * 100, 2) } else { 0 } المقاييس = $Metrics }
$checkpoint | ConvertTo-Json -Depth 6 | Out-File $processingCheckpointPath -Encoding UTF8 -Force }
function Get-NotUpdatedIndexes { param([array]$Devices)
$hostSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $bucketCounts = @{}
foreach ($device in $Devices) { $hostname = إذا ($device. اسم المضيف) { $device. اسم المضيف } elseif ($device. اسم المضيف) { $device. اسم المضيف } آخر { $null } إذا ($hostname) { [void]$hostSet.Add($hostname) }
$bucketKey = Get-BucketKey $device إذا ($bucketKey) { if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey]++ } آخر { $bucketCounts[$bucketKey] = 1 } } }
return @{ HostSet = $hostSet عدد المستودعات = $bucketCounts } }
function Write-Log { param([string]$Message, [string]$Level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $color = switch ($Level) { "موافق" { "أخضر" } "WARN" { "Yellow" } "ERROR" { "Red" } "BLOCKED" { "DarkRed" } "WAVE" { "Cyan" } الافتراضي { "أبيض" } } Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color # أيضا تسجيل الدخول إلى ملف $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMMdd').log" "[$timestamp] [$Level] $Message" | Out-File $logFile -إلحاق -ترميز UTF8 }
function Get-BucketKey { param($Device) # استخدم BucketId من الجهاز JSON إذا كان متوفرا (تجزئة SHA256 من البرنامج النصي للكشف) if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" } # الاحتياطي: إنشاء من الشركة المصنعة|model|bios $mfr = if ($Device.WMI_Manufacturer) { $Device.WMI_Manufacturer } else { $Device.Manufacturer } $model = if ($Device.WMI_Model) { $Device.WMI_Model } else { $Device.Model } $bios = if ($Device.BIOSDescription) { $Device.BIOSDescription } else { $Device.BIOS } إرجاع "$mfr|$model|$bios" }
# ============================================================================ # VIP/EXCLUSION LIST LOADING # ============================================================================
function Get-ExcludedHostnames { param( [string]$ExclusionFilePath، [سلسلة]$ADGroupName ) $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # تحميل من ملف (يدعم .txt أو .csv) if ($ExclusionFilePath -and (test-Path $ExclusionFilePath)) { $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower() if ($extension -eq ".csv") { تنسيق #CSV: يتوقع عمود "اسم المضيف" أو "اسم الكمبيوتر" $csvData = Import-Csv $ExclusionFilePath $hostCol = if ($csvData[0]. PSObject.Properties.Name -يحتوي على 'اسم المضيف') { 'اسم المضيف' } elseif ($csvData[0]. PSObject.Properties.Name -يحتوي على 'ComputerName') { 'ComputerName' } elseif ($csvData[0]. PSObject.Properties.Name -contains 'Name') { 'Name' } آخر { $null } إذا ($hostCol) { foreach ($row في $csvData) { if (![ string]::IsNullOrWhiteSpace($row.$hostCol)) { [فارغ]$excluded. Add($row.$hostCol.Trim()) } } } } آخر { # نص عادي: اسم مضيف واحد لكل سطر Get-Content $ExclusionFilePath | ForEach-Object { $line = $_. Trim() إذا ($line -و-not $line. StartsWith('#')) { [فارغ]$excluded. إضافة ($line) } } } Write-Log "تم تحميل $($excluded. Count) أسماء المضيفين من ملف الاستبعاد: $ExclusionFilePath" "INFO" } # تحميل من مجموعة أمان AD if ($ADGroupName) { جرب { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'computer' } foreach ($member في $groupMembers) { [فارغ]$excluded. إضافة($member. الاسم) } Write-Log أجهزة الكمبيوتر "تم تحميلها $($groupMembers.Count) من مجموعة AD: $ADGroupName" "INFO" } التقاط { Write-Log "تعذر تحميل مجموعة AD "$ADGroupName": $_" "WARN" } } إرجاع @($excluded) }
# ============================================================================ # السماح بتحميل القائمة (الإطلاق المستهدف) # ============================================================================
function Get-AllowedHostnames { <# . خلاصه تحميل أسماء المضيفين من ملف AllowList و/أو مجموعة AD للطرح المستهدف.عند تحديد قائمة السماح، سيتم تضمين هذه الأجهزة فقط في الإطلاق.#> param( [string]$AllowFilePath، [سلسلة]$ADGroupName ) $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # تحميل من ملف (يدعم .txt أو .csv) if ($AllowFilePath -and (test-Path $AllowFilePath)) { $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower() if ($extension -eq ".csv") { تنسيق #CSV: يتوقع عمود "اسم المضيف" أو "اسم الكمبيوتر" $csvData = Import-Csv $AllowFilePath إذا ($csvData.Count -gt 0) { $hostCol = if ($csvData[0]. PSObject.Properties.Name -يحتوي على 'اسم المضيف') { 'اسم المضيف' } elseif ($csvData[0]. PSObject.Properties.Name -يحتوي على 'ComputerName') { 'ComputerName' } elseif ($csvData[0]. PSObject.Properties.Name -contains 'Name') { 'Name' } آخر { $null } إذا ($hostCol) { foreach ($row في $csvData) { if (![ string]::IsNullOrWhiteSpace($row.$hostCol)) { [فارغ]$allowed. Add($row.$hostCol.Trim()) } } } } } آخر { # نص عادي: اسم مضيف واحد لكل سطر Get-Content $AllowFilePath | ForEach-Object { $line = $_. Trim() إذا ($line -و-not $line. StartsWith('#')) { [فارغ]$allowed. إضافة ($line) } } } Write-Log "تم تحميل $($allowed. Count) أسماء المضيفين من السماح لملف القائمة: $AllowFilePath" "INFO" } # تحميل من مجموعة أمان AD if ($ADGroupName) { جرب { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'computer' } foreach ($member في $groupMembers) { [فارغ]$allowed. إضافة($member. الاسم) } Write-Log أجهزة الكمبيوتر "تم تحميلها $($groupMembers.Count) من مجموعة السماح AD: $ADGroupName" "INFO" } التقاط { Write-Log "تعذر تحميل مجموعة AD "$ADGroupName": $_" "WARN" } } إرجاع @($allowed) }
# ============================================================================ # حداثة البيانات ومراقبتها # ============================================================================
function Get-DataFreshness { <# . خلاصه يتحقق من مدى تحديث بيانات الكشف عن طريق فحص الطوابع الزمنية لملف JSON.إرجاع إحصائيات حول وقت آخر تقرير لنقاط النهاية.#> param([string]$JsonPath) $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue إذا ($jsonFiles.Count -eq 0) { إرجاع @{ TotalFiles = 0 FreshFiles = 0 StaleFiles = 0 NoDataFiles = 0 OldestFile = $null NewestFile = $null AvgAgeHours = 0 تحذير = "لم يتم العثور على ملفات JSON - قد لا يتم نشر الكشف" } } $now = Get-Date $freshThresholdHours = 24 # Files تم تحديثها في آخر 24 ساعة "جديدة" $staleThresholdHours = 72 # Files أقدم من 72 ساعة "قديمة" $fresh = 0 $stale = 0 $ages = @() foreach ($file في $jsonFiles) { $ageHours = ($now - $file. LastWriteTime). إجمالي ساعات العمل $ages += $ageHours إذا ($ageHours -le $freshThresholdHours) { $fresh++ } elseif ($ageHours -ge $staleThresholdHours) { $stale++ } } $oldestFile = $jsonFiles | Sort-Object LastWriteTime | Select-Object -الأول 1 $newestFile = $jsonFiles | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 $warning = $null if ($stale -gt ($jsonFiles.Count * 0.5)) { $warning = "تحتوي أكثر من 50٪ من الأجهزة على بيانات قديمة (>72 ساعة) - تحقق من عنصر نهج المجموعة للكشف" } elseif ($fresh -lt ($jsonFiles.Count * 0.3)) { $warning = "تم الإبلاغ عن أقل من 30٪ من الأجهزة مؤخرا - قد لا يكون الكشف قيد التشغيل" } إرجاع @{ TotalFiles = $jsonFiles.Count FreshFiles = $fresh StaleFiles = $stale MediumFiles = $jsonFiles.Count - $fresh - $stale OldestFile = $oldestFile.LastWriteTime NewestFile = $newestFile.LastWriteTime AvgAgeHours = [math]::Round(($ages | Measure-Object -Average). متوسط، 1) تحذير = $warning } }
function Test-DetectionGPODeployed { <# . خلاصه التحقق من وجود البنية الأساسية للكشف/المراقبة.#> param([string]$JsonPath) # التحقق 1: مسار JSON موجود if (-not (test-Path $JsonPath)) { إرجاع @{ IsDeployed = $false الرسالة = "مسار إدخال JSON غير موجود: $JsonPath" } } # التحقق 2: توجد بعض ملفات JSON على الأقل $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). عدد if ($jsonCount -eq 0) { إرجاع @{ IsDeployed = $false الرسالة = "لا توجد ملفات JSON في $JsonPath - قد لا يتم نشر عنصر نهج المجموعة للكشف أو لم يتم الإبلاغ عن الأجهزة بعد" } } # التحقق 3: Files حديثة بشكل معقول (على الأقل بعضها في الأسبوع الماضي) $recentFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue | Where-Object { $_. LastWriteTime -gt (Get-Date). AddDays(-7) } إذا ($recentFiles.Count -eq 0) { إرجاع @{ IsDeployed = $false الرسالة = "لم يتم تحديث ملفات JSON في آخر 7 أيام - قد يكون الكشف عن عنصر نهج المجموعة مقطوعا أو أجهزة غير متصلة" } } إرجاع @{ IsDeployed = $true الرسالة = "يظهر الكشف نشطا: $jsonCount الملفات، $($recentFiles.Count) التي تم تحديثها مؤخرا" FileCount = $jsonCount RecentCount = $recentFiles.Count } }
# ============================================================================ # تعقب الجهاز (حسب اسم المضيف) # ============================================================================
function Update-DeviceHistory { <# . خلاصه يتعقب الأجهزة حسب اسم المضيف نظرا لأنه ليس لدينا معرف جهاز فريد.ملاحظة: BucketId هو واحد إلى متعدد (نفس تكوين الأجهزة = نفس المستودع).إذا تمت إضافة معرف فريد إلى مجموعة JSON، فقم بتحديث هذه الدالة.#> param( [array]$CurrentDevices، [hashtable]$DeviceHistory ) foreach ($device في $CurrentDevices) { $hostname = $device. المضيف إذا (-ليس $hostname) { continue } # تعقب الجهاز حسب اسم المضيف $DeviceHistory[$hostname] = @{ اسم المضيف = $hostname معرف المستودع = $device. معرف المستودع الشركة المصنعة = $device. WMI_Manufacturer النموذج = $device. WMI_Model LastSeen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" الحالة = $device. UpdateStatus } } }
# ============================================================================ # الكشف عن المستودع المحظور (استنادا إلى إمكانية الوصول إلى الجهاز) # ============================================================================
<# . وصف منطق الحظر: - يتم حظر المستودع فقط إذا: 1. تم استهداف الجهاز في موجة 2. لقد مر MaxWaitHours منذ بدء الموجة 3. الجهاز غير قابل للوصول (فشل اختبار الاتصال) - إذا كان الجهاز قابلا للوصول ولكن لم يتم تحديثه بعد، فإننا نستمر في الانتظار (قد يكون التحديث معلقا لإعادة التشغيل - يتم تشغيل الحدث 1808 فقط بعد إعادة التشغيل) - جهاز لا يمكن الوصول إليه يشير إلى حدوث خطأ ما ويحتاج إلى التحقيق إلغاء الحظر: - استخدم -ListBlockedBuckets لمشاهدة المستودعات المحظورة - استخدم -UnblockBucket "BucketKey" لإلغاء حظر مستودع معين - استخدم -إلغاء حظرAll لإلغاء حظر جميع المستودعات #>
function Test-DeviceReachable { param( [string]$Hostname، [string]$DataPath # Path to device JSON files ) # الطريقة 1: التحقق من الطابع الزمني لملف JSON (الأسرع — لا يلزم تحليل الملفات) # إذا تم تشغيل البرنامج النصي للكشف مؤخرا، فقد تمت كتابة/تحديث الملف، مما يثبت أن الجهاز على قيد الحياة إذا ($DataPath) { $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object -الأول 1 if ($deviceFile) { $hoursSinceWrite = ((Get-Date) - $deviceFile.LastWriteTime). إجمالي ساعات العمل إذا كان ($hoursSinceWrite -lt 72) { إرجاع $true } } } # الأسلوب 2: الرجوع إلى اختبار الاتصال (فقط إذا كان JSON قديما أو مفقودا) جرب { $ping = Test-Connection -ComputerName $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue إرجاع $ping } التقاط { إرجاع $false } }
function Update-BlockedBuckets { param( $RolloutState، $BlockedBuckets، $AdminApproved، [array]$NotUpdatedDevices، [hashtable]$NotUpdatedIndexes، [int]$MaxWaitHours، [bool]$DryRun = $false ) $now = Get-Date $newlyBlocked = @() $stillWaiting = @() $devicesToCheck = @() $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } $bucketCounts = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.BucketCounts) { $NotUpdatedIndexes.BucketCounts } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). عدد المستودعات } # اجمع الأجهزة التي تجاوزت فترة الانتظار ولا تزال غير محدثة foreach ($wave في $RolloutState.WaveHistory) { إذا (-ليس $wave. StartedAt) { continue } $waveStart = [DateTime]::P arse($wave. StartedAt) $hoursSinceWave = ($now - $waveStart). إجمالي ساعات العمل if ($hoursSinceWave -lt $MaxWaitHours) { # لا يزال ضمن فترة الانتظار -- لا تحقق حتى الآن مواصله } # تحقق من كل جهاز من هذه الموجة foreach ($deviceInfo في $wave. الأجهزة) { $hostname = $deviceInfo.اسم المضيف $bucketKey = $deviceInfo.BucketKey # تخطي ما إذا كان المستودع محظورا بالفعل إذا ($BlockedBuckets.Contains($bucketKey)) { continue } # تخطي ما إذا كان المستودع معتمدا من المسؤول وبدأت موجة قبل الموافقة # (تحقق فقط من الأجهزة المستهدفة بعد موافقة المسؤول لإعادة الحظر) if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) { $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. تمت الموافقة عليه) إذا ($waveStart -lt $approvalTime) { # تم استهداف هذا الجهاز قبل موافقة المسؤول - تخطي مواصله } # موجة بدأت بعد الموافقة -- هذا هو استهداف جديد ، يمكن التحقق } # هل هذا الجهاز لا يزال في قائمة NotUpdated؟ if ($hostSet.Contains($hostname)) { $devicesToCheck += @{ اسم المضيف = $hostname BucketKey = $bucketKey WaveNumber = $wave. رقم الموجة HoursSinceWave = [math]::Round($hoursSinceWave, 1) } } } } if ($devicesToCheck.Count -eq 0) { إرجاع $newlyBlocked } Write-Log "التحقق من إمكانية الوصول إلى أجهزة $($devicesToCheck.Count) خلال فترة الانتظار الماضية..." "INFO" # تعقب حالات الفشل لكل مستودع لاتخاذ القرار $bucketFailures = @{} # BucketKey -> @{ Unreachable=@()؛ Alive=@() } # تحقق من إمكانية الوصول لكل جهاز foreach ($device في $devicesToCheck) { $hostname = $device. المضيف $bucketKey = $device. مفتاح المستودع if ($DryRun) { Write-Log "[DRYRUN] التحقق من إمكانية الوصول $hostname" "INFO" مواصله } if (-not $bucketFailures.ContainsKey($bucketKey)) { $bucketFailures[$bucketKey] = @{ غير قابل للوصول = @()؛ AliveButFailed = @()؛ WaveNumber = $device. رقم الموجة; HoursSinceWave = $device. HoursSinceWave } } $isReachable = Test-DeviceReachable -اسم المضيف $hostname -DataPath $AggregationInputPath إذا (-ليس $isReachable) { $bucketFailures[$bucketKey]. لا يمكن الوصول إليها += $hostname } آخر { # الجهاز قابل للوصول ولكن لم يتم تحديثه بعد - قد يكون فشلا مؤقتا أو في انتظار إعادة التشغيل $bucketFailures[$bucketKey]. AliveButFailed += $hostname $stillWaiting += $hostname } } # القرار لكل مستودع: حظر فقط إذا كانت الأجهزة غير قابلة للوصول حقا # Alive الأجهزة التي بها حالات فشل = مؤقتة، تابع الإطلاق foreach ($bucketKey في $bucketFailures.Keys) { $bf = $bucketFailures[$bucketKey] $unreachableCount = $bf. عدد غير قابل للوصول $aliveFailedCount = $bf. AliveButFailed.Count # تحقق مما إذا كان لهذا المستودع أي نجاحات (من بيانات الأجهزة المحدثة) $bucketHasSuccesses = $stSuccessBuckets -and $stSuccessBuckets.Contains($bucketKey) if ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) { # جميع الأجهزة الفاشلة غير قابلة للوصول - حظر المستودع إذا ($newlyBlocked -notcontains $bucketKey) { $BlockedBuckets[$bucketKey] = @{ BlockedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" السبب = "لا يمكن الوصول إلى جميع الأجهزة $unreachableCount بعد $($bf. ساعات الدخول إلى غرب) ساعات" FailedDevices = ($bf. غير قابل للوصول -الانضمام "، ") WaveNumber = $bf. رقم الموجة DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } else { 0 } } $newlyBlocked += $bucketKey Write-Log "تم حظر المستودع: لا يمكن الوصول إلى $bucketKey (جهاز (أجهزة) $unreachableCount: $($bf. غير قابل للوصول -join ', '))" "BLOCKED" } } elseif ($aliveFailedCount -gt 0) { # الأجهزة على قيد الحياة ولكن لم يتم تحديثها - فشل مؤقت، كتلة DO NOT Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length))...: $aliveFailedCount device(s) على قيد الحياة ولكن معلق، $unreachableCount لا يمكن الوصول إليه - NOT blocking (temporary)" "INFO" إذا ($unreachableCount -gt 0) { Write-Log " لا يمكن الوصول إليها: $($bf. غير قابل للوصول -join ', ')" "WARN" } Write-Log " على قيد الحياة ولكن معلق: $($bf. AliveButFailed -join ', ')" "INFO" # تعقب عدد الفشل في حالة الإطلاق للمراقبة if (-not $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} } $RolloutState.TemporaryFailures[$bucketKey] = @{ AliveButFailed = $bf. AliveButFailed غير قابل للوصول = $bf. قابله LastChecked = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } } } إذا ($stillWaiting.Count -gt 0) { Write-Log "الأجهزة التي يمكن الوصول إليها ولكن التحديث المعلق (قد تحتاج إلى إعادة التشغيل): $($stillWaiting.Count)" "INFO" } إرجاع $newlyBlocked }
# ============================================================================ # AUTO-UNBLOCK: إلغاء حظر المستودعات عند تحديث الأجهزة بنجاح # ============================================================================
function Update-AutoUnblockedBuckets { <# . وصف التحقق من تحديث الأجهزة الموجودة في المستودعات المحظورة (الحدث 1808). إلغاء الحظر التلقائي إذا تم تحديث جميع الأجهزة المستهدفة في المستودع.إذا تم تحديث بعض الأجهزة فقط، يقوم بإعلام المسؤول الذي يمكنه إلغاء الحظر يدويا. يمكن مسؤول إلغاء الحظر يدويا باستخدام: .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "path" -UnblockBucket "BucketKey" #> param( $BlockedBuckets، $RolloutState، [array]$NotUpdatedDevices، [string]$ReportBasePath، [hashtable]$NotUpdatedIndexes، [int]$LogSampleSize = 25 ) $autoUnblocked = @() $bucketsToCheck = @($BlockedBuckets.Keys) $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } foreach ($bucketKey في $bucketsToCheck) { $bucketInfo = $BlockedBuckets[$bucketKey] # احصل على جميع الأجهزة التي استهدفناها من هذا المستودع تاريخيا $targetedDevicesInBucket = @() foreach ($wave في $RolloutState.WaveHistory) { $targetedDevicesInBucket += @($wave. الأجهزة | Where-Object { $_. BucketKey -eq $bucketKey }) } إذا ($targetedDevicesInBucket.Count -eq 0) { continue } # تحقق من عدد الأجهزة المستهدفة التي لا تزال في NotUpdated مقابل التحديث $updatedDevices = @() $stillPendingDevices = @() foreach ($targetedDevice في $targetedDevicesInBucket) { if ($hostSet.Contains($targetedDevice.Hostname)) { $stillPendingDevices += $targetedDevice.اسم المضيف } آخر { $updatedDevices += $targetedDevice.اسم المضيف } } if ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) { # تم تحديث جميع الأجهزة المستهدفة - إلغاء الحظر التلقائي! $BlockedBuckets.Remove($bucketKey) $autoUnblocked += @{ BucketKey = $bucketKey UpdatedDevices = $updatedDevices PreviouslyBlockedAt = $bucketInfo.BlockedAt السبب = "تم تحديث جميع الأجهزة المستهدفة $($updatedDevices.Count) بنجاح" } Write-Log "إلغاء الحظر التلقائي: تم تحديث $bucketKey (جميع الأجهزة المستهدفة $($updatedDevices.Count) بنجاح)" "موافق" # زيادة عدد موجة الشركة المصنعة للمعدات الأصلية (OEM) لهذا المستودع (تتبع لكل OEM) $bucketOEM = if ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } آخر { 'Unknown' } # استخراج OEM من مفتاح محدد بالأنابيب أو افتراضي إذا (-not $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } $currentWave = if ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } else { 0 } $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1 Write-Log " زيادة عدد موجة الشركة المصنعة للمعدات الأصلية "$bucketOEM" إلى أجهزة $($currentWave + 1) (التخصيص التالي: $([int][Math]::P ow(2, $currentWave + 1))) "INFO" } elseif ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -gt 0) { # تم تحديث بعض الأجهزة ولكن لا يزال البعض الآخر معلقا - إعلام المسؤول (مرة واحدة فقط) إذا (-not $bucketInfo.UnblockCandidate) { $bucketInfo.UnblockCandidate = $true $bucketInfo.UpdatedDevices = $updatedDevices $bucketInfo.PendingDevices = $stillPendingDevices $bucketInfo.NotifiedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") Write-Log "" "INFO" Write-Log "========== التحديث الجزئي في ========== المستودع المحظور" "INFO" Write-Log "مستودع: $bucketKey" "INFO" $updatedSample = @($updatedDevices | Select-Object -أول $LogSampleSize) $pendingSample = @($stillPendingDevices | Select-Object -أول $LogSampleSize) $updatedSuffix = if ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) المزيد)" } آخر { "" } $pendingSuffix = if ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) المزيد)" } آخر { "" } Write-Log "الأجهزة المحدثة ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix" "OK" Write-Log "لا يزال معلقا ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix" "WARN" Write-Log "" "INFO" Write-Log "لإلغاء حظر هذا المستودع يدويا بعد التحقق، قم بتشغيل:" "INFO" Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath ""$ReportBasePath"" -إلغاء حظرBucket ""$bucketKey"" "INFO" "INFO" Write-Log "=======================================================" Write-Log "" "INFO" } } } إرجاع $autoUnblocked }
# ============================================================================ # WAVE GENERATION (INLINED - يستبعد المستودعات المحظورة) # ============================================================================
function New-RolloutWave { param( [string]$AggregationPath، $BlockedBuckets، $RolloutState، [int]$MaxDevicesPerWave = 50، [string[]]$AllowedHostnames = @()، [string[]]$ExcludedHostnames = @() ) # تحميل بيانات التجميع $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" | Where-Object { $_. الاسم -notlike "*Buckets*" } | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 if (-not $notUptodateCsv) { Write-Log "No NotUptodate CSV found" "ERROR" إرجاع $null } $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName) # Normalize HostName -> Hostname للتناسق (يستخدم CSV اسم المضيف، تستخدم التعليمات البرمجية اسم المضيف) foreach ($device في $allNotUpdated) { إذا ($device. PSObject.Properties['HostName'] -and -not $device. PSObject.Properties['Hostname']) { $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName -Force } } # تصفية المستودعات المحظورة $eligibleDevices = @($allNotUpdated | Where-Object { $bucketKey = Get-BucketKey $_ -not $BlockedBuckets.Contains($bucketKey) }) # تصفية إلى الأجهزة المسموح بها فقط (إذا تم تحديد AllowList) # AllowList = الإطلاق المستهدف - سيتم النظر في هذه الأجهزة فقط if ($AllowedHostnames.Count -gt 0) { $beforeCount = $eligibleDevices.Count $eligibleDevices = @($eligibleDevices | Where-Object { $_. اسم المضيف -in $AllowedHostnames }) $allowedCount = $eligibleDevices.Count Write-Log "AllowList applied: $allowedCount من الأجهزة $beforeCount في قائمة السماح" "INFO" } # تصفية الأجهزة VIP/المستبعدة (BlockList) # يتم تطبيق قائمة الحظر بعد السماح إذا ($ExcludedHostnames.Count -gt 0) { $beforeCount = $eligibleDevices.Count $eligibleDevices = @($eligibleDevices | Where-Object { $_. اسم المضيف -notin $ExcludedHostnames }) $excludedCount = $beforeCount - $eligibleDevices.Count إذا ($excludedCount -gt 0) { Write-Log "الأجهزة المستبعدة $excludedCount VIP/المحمية من الإطلاق" "INFO" } } if ($eligibleDevices.Count -eq 0) { Write-Log "لا توجد أجهزة مؤهلة متبقية (تم تحديثها أو حظرها)" "موافق" إرجاع $null } # الحصول على الأجهزة قيد الإطلاق بالفعل (من الموجات السابقة) $devicesAlreadyInRollout = @() if ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) { $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object { $_. الأجهزة | ForEach-Object { $_. اسم المضيف } } | Where-Object { $_ }) } Write-Log "الأجهزة قيد الإطلاق بالفعل: $($devicesAlreadyInRollout.Count)" "INFO" # منفصل بمستوى الثقة $highConfidenceDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -eq "High Confidence" -and $_. اسم المضيف -notin $devicesAlreadyInRollout }) # الإجراء المطلوب يتضمن: # - صريح "الإجراء مطلوب" # - قيمة الثقة الفارغة/الفارغة # - أي قيمة ConfidenceLevel غير معروفة/غير معروفة (تعامل كإجراء مطلوب) $knownSafeCategories = @( "ثقة عالية"، "متوقف مؤقتا"، "تحت الملاحظة"، "تحت الملاحظة - مطلوب المزيد من البيانات"، "غير مدعوم"، "غير مدعوم - القيد المعروف" ) $actionRequiredDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -notin $knownSafeCategories -و $_. اسم المضيف -notin $devicesAlreadyInRollout }) Write-Log "ثقة عالية (غير قيد الإطلاق): $($highConfidenceDevices.Count)" "INFO" Write-Log "الإجراء مطلوب (ليس قيد الإطلاق): $($actionRequiredDevices.Count)" "INFO" # إنشاء أجهزة موجة $waveDevices = @() # HIGH CONFIDENCE: تضمين ALL (آمن للإدراق) if ($highConfidenceDevices.Count -gt 0) { Write-Log "إضافة جميع أجهزة الثقة العالية $($highConfidenceDevices.Count)" "WAVE" $waveDevices += $highConfidenceDevices } # ACTION مطلوبة: الإطلاق التدريجي (المستند إلى المستودع مع انتشار OEM لمستودعات صفر نجاح) # الاستراتيجية: # - المستودعات ذات النجاحات 0: تنتشر عبر الشركات المصنعة للمعدات الأصلية (1 لكل OEM -> 2 لكل OEM -> 4 لكل OEM) # - المستودعات ذات النجاح ≥1: مضاعفة بحرية دون تقييد الشركة المصنعة للمعدات الأصلية إذا ($actionRequiredDevices.Count -gt 0) { # تحميل عدد مرات نجاح المستودع من الأجهزة المحدثة CSV (الأجهزة التي تم تحديثها بنجاح) $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 $bucketStats = @{} إذا ($updatedCsv) { $updatedDevices = Import-Csv $updatedCsv.FullName عدد النجاحات لكل BucketId $updatedDevices | ForEach-Object { $key = Get-BucketKey $_ إذا ($key) { if (-not $bucketStats.ContainsKey($key)) { $bucketStats[$key] = @{ Successes = 0; معلق = 0؛ الإجمالي = 0 } } $bucketStats[$key]. Successes++ $bucketStats[$key]. Total++ } } Write-Log "تم تحميل $($updatedDevices.Count) الأجهزة المحدثة عبر مستودعات $($bucketStats.Count) " "INFO" } آخر { # الاحتياطي: حاول ActionRequired_Buckets CSV $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 إذا ($bucketsCsv) { Import-Csv $bucketsCsv.FullName | ForEach-Object { $key = if ($_. BucketId) { $_. BucketId } آخر { "$($_. الشركة المصنعة)|$($_. Model)|$($_. BIOS)" } $bucketStats[$key] = @{ النجاحات = [int]$_. النجاحات معلق = [int]$_. المعلقه الإجمالي = [int]$_. إجمالي الأجهزة } } } } # مجموعة أجهزة NotUpdated حسب المستودع (الشركة المصنعة|نموذج|BIOS) $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ } # مستودعات منفصلة: صفر نجاح مقابل نجاح ناجح $zeroSuccessBuckets = @() $hasSuccessBuckets = @() foreach ($bucket في $buckets) { $bucketKey = $bucket. اسم $bucketDevices = @($bucket. المجموعة) $bucketHostnames = @($bucketDevices | ForEach-Object { $_. اسم المضيف }) # عدد النجاحات في هذا المستودع $stats = $bucketStats[$bucketKey] $successes = if ($stats) { $stats. Successes } else { 0 } # البحث عن الأجهزة المنشورة في هذا المستودع من محفوظات الموجة $deployedToBucket = @() foreach ($wave في $RolloutState.WaveHistory) { foreach ($device في $wave. الأجهزة) { إذا ($device. BucketKey -eq $bucketKey -and $device. اسم المضيف) { $deployedToBucket += $device. المضيف } } } $deployedToBucket = @($deployedToBucket | Sort-Object -Unique) # تحقق مما إذا كانت جميع الأجهزة المنشورة قد أبلغت عن نجاح $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames }) $confirmedSuccess = $deployedToBucket.Count - $stillPending.Count # إذا كان معلقا، فتخط هذا المستودع حتى يتم تأكيد الكل إذا ($stillPending.Count -gt 0) { $parts = $bucketKey -split '\|' $displayName = "$($parts[0]) - $($parts[1])" Write-Log "مستودع: $displayName - Deployed=$($deployedToBucket.Count)، Confirmed=$confirmedSuccess، Pending=$($stillPending.Count) (waiting)" "INFO" مواصله } # البقاء مؤهلا = الأجهزة التي لم يتم نشرها بعد $devicesNotYetTargeted = @($bucketDevices | Where-Object { $_. اسم المضيف -notin $deployedToBucket }) إذا ($devicesNotYetTargeted.Count -eq 0) { continue } # تصنيف حسب عدد النجاحات $bucketInfo = @{ BucketKey = $bucketKey الأجهزة = $devicesNotYetTargeted ConfirmedSuccess = $confirmedSuccess النجاحات = $successes OEM = if ($bucket. Group[0]. WMI_Manufacturer) { $bucket. Group[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } آخر { 'Unknown' } } إذا ($successes -eq 0) { $zeroSuccessBuckets += $bucketInfo } آخر { $hasSuccessBuckets += $bucketInfo } } # === عمليات مستودعات HAS-SUCCESS (نجاح ≥1) === # مضاعفة عدد النجاحات — إذا نجح 14، فقم بنشر 28 بعد ذلك foreach ($bucketInfo في $hasSuccessBuckets) { $nextBatchSize = $bucketInfo.Successes * 2 $nextBatchSize = [Math]::Min($nextBatchSize, $MaxDevicesPerWave) $nextBatchSize = [Math]::Min($nextBatchSize, $bucketInfo.Devices.Count) إذا ($nextBatchSize -gt 0) { $selectedDevices = @($bucketInfo.Devices | Select-Object -أول $nextBatchSize) $waveDevices += $selectedDevices $parts = إذا ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length))) } $displayName = "$($parts[0]) - $($parts[1])" Write-Log " [HAS-SUCCESS] $displayName - Successes=$($bucketInfo.Successes)، Deploying=$nextBatchSize (2x confirmed)" "INFO" } } # === معالجة مستودعات ZERO-SUCCESS (المنتشرة عبر الشركات المصنعة للمعدات الأصلية باستخدام تعقب كل OEM) === # الهدف: نشر المخاطر عبر الشركات المصنعة للمعدات الأصلية المختلفة، وتتبع التقدم لكل الشركة المصنعة للمعدات الأصلية بشكل مستقل # تتقدم كل الشركة المصنعة للمعدات الأصلية استنادا إلى تاريخ نجاحها الخاص: # - الشركة المصنعة للمعدات الأصلية مع النجاحات: الحصول على المزيد من الأجهزة الموجة التالية (2^waveCount) # - الشركة المصنعة للمعدات الأصلية بدون نجاحات: تبقى على المستوى الحالي حتى يتم تأكيد النجاح إذا ($zeroSuccessBuckets.Count -gt 0) { # تهيئة عدد موجة الشركة المصنعة للمعدات الأصلية إذا لم يكن موجودا if (-not $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } # تجميع مستودعات النجاح الصفري من قبل الشركة المصنعة للمعدات الأصلية $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM } $totalZeroSuccessAdded = 0 $oemsDeployedTo = @() foreach ($oemGroup في $oemBuckets) { $oemName = $oemGroup.Name # الحصول على عدد موجة الشركة المصنعة للمعدات الأصلية (يبدأ من 0) $oemWaveCount = if ($RolloutState.OEMWaveCounts[$oemName]) { $RolloutState.OEMWaveCounts[$oemName] } آخر { 0 } # حساب الأجهزة لهذه الشركة المصنعة للمعدات الأصلية: 2^waveCount (1، 2، 4، 8...) $devicesForThisOEM = [int][Math]::P ow(2, $oemWaveCount) $devicesForThisOEM = [Math]::Max(1, $devicesForThisOEM) $oemDevicesAdded = 0 # اختر من كل مستودع ضمن الشركة المصنعة للمعدات الأصلية هذه foreach ($bucketInfo في $oemGroup.Group) { $remaining = $devicesForThisOEM - $oemDevicesAdded إذا ($remaining -le 0) { break } $toTake = [Math]::Min($remaining, $bucketInfo.Devices.Count) إذا ($toTake -gt 0) { $selectedDevices = @($bucketInfo.Devices | Select-Object -First $toTake) $waveDevices += $selectedDevices $oemDevicesAdded += $toTake $totalZeroSuccessAdded += $toTake $parts = إذا ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length)))) } $displayName = "$($parts[0]) - $($parts[1])" Write-Log " [ZERO-SUCCESS] $displayName - Deploying=$toTake (موجة OEM $oemWaveCount = ${devicesForThisOEM}/OEM)" "WARN" } } إذا ($oemDevicesAdded -gt 0) { Write-Log " OEM: $oemName - $oemWaveCount الموجة، أجهزة $oemDevicesAdded المضافة" "INFO" $oemsDeployedTo += $oemName } } # تعقب OEMs التي قمنا بنشرها (لزيادة في التحقق من النجاح التالي) إذا ($oemsDeployedTo.Count -gt 0) { $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo Write-Log "توزيع صفر نجاح: $totalZeroSuccessAdded الأجهزة عبر $($oemsDeployedTo.Count) OEMs" "INFO" } } } if (@($waveDevices). Count -eq 0) { إرجاع $null } إرجاع $waveDevices }
# ============================================================================ #GPO DEPLOYMENT (INLINED - ينشئ عنصر نهج المجموعة ومجموعة الأمان والارتباطات) # ============================================================================
function Deploy-GPOForWave { param( [string]$GPOName، [string]$TargetOU، [string]$SecurityGroupName، [array]$WaveHostnames، [bool]$DryRun = $false ) # ADMX Policy: SecureBoot.admx - SecureBoot_AvailableUpdatesPolicy # مسار التسجيل: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot # اسم القيمة: AvailableUpdatesPolicy # Enabled Value: 22852 (0x5944) - تحديث جميع مفاتيح التمهيد الآمن + bootmgr # القيمة المعطلة: 0 # # استخدام تفضيلات نهج المجموعة (GPP) لنشر مسار HKLM\SYSTEM الموثوق به # GPP ينشئ الإعدادات ضمن: تكوين الكمبيوتر > التفضيلات > إعدادات Windows > Registry $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" $RegistryValueName = "AvailableUpdatesPolicy" $RegistryValue = 22852 # 0x5944 - يطابق ADMX enabledValue Write-Log "توزيع عنصر نهج المجموعة: $GPOName" "WAVE" Write-Log "السجل: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X')))" "INFO" إذا ($DryRun) { Write-Log "[DRYRUN] إنشاء عنصر نهج المجموعة: $GPOName" "INFO" Write-Log "[DRYRUN] إنشاء مجموعة أمان: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] سيضيف $(@($WaveHostnames). Count) أجهزة الكمبيوتر المراد تجميعها" "INFO" Write-Log "[DRYRUN] ربط عنصر نهج المجموعة ب: $TargetOU" "INFO" إرجاع $true } جرب { # استيراد الوحدات المطلوبة Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } التقاط { Write-Log "فشل استيراد الوحدات النمطية المطلوبة (GroupPolicy، ActiveDirectory): $($_. Exception.Message)" "ERROR" إرجاع $false } # الخطوة 1: إنشاء عنصر نهج المجموعة أو الحصول عليه $existingGPO = Get-GPO -name $GPOName -ErrorAction SilentlyContinue إذا ($existingGPO) { Write-Log "GPO موجود بالفعل: $GPOName" "INFO" $gpo = $existingGPO } آخر { جرب { $gpo = New-GPO -name $GPOName -Comment "Secure Boot Certificate Rollout - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "تم إنشاء عنصر نهج المجموعة: $GPOName" "موافق" } التقاط { Write-Log "فشل إنشاء عنصر نهج المجموعة: $($_. Exception.Message)" "ERROR" إرجاع $false } } # الخطوة 2: تعيين قيمة التسجيل باستخدام تفضيلات نهج المجموعة (GPP) # GPP أكثر موثوقية لمسارات HKLM\SYSTEM من Set-GPRegistryValue جرب { # حاول أولا إزالة أي تفضيل موجود لهذه القيمة (لتجنب التكرارات) Remove-GPPrefRegistryValue -name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue # إنشاء تفضيل تسجيل GPP مع إجراء "استبدال" # استبدال = إنشاء إذا لم يكن موجودا، تحديث إذا كان موجودا (الأكثر موثوقية) # Update = التحديث فقط إذا كان موجودا (يفشل إذا لم تكن القيمة موجودة) Set-GPPrefRegistryValue -name $GPOName ' -Context Computer ' -استبدال الإجراء ' -مفتاح $RegistryKey ' -ValueName $RegistryValueName ' -اكتب DWord ' -قيمة $RegistryValue Write-Log "تكوين تفضيل سجل GPP: $RegistryValueName = 0x5944 (Action=Replace)" "OK" } التقاط { Write-Log "فشل GPP، جرب Set-GPRegistryValue: $($_. Exception.Message)" "WARN" # الرجوع إلى Set-GPRegistryValue (يعمل إذا تم نشر ADMX) جرب { Set-GPRegistryValue -اسم $GPOName ' -مفتاح $RegistryKey ' -ValueName $RegistryValueName ' -اكتب DWord ' -قيمة $RegistryValue Write-Log "السجل المكون عبر Set-GPRegistryValue: $RegistryValueName = 0x5944" "OK" } التقاط { Write-Log "فشل تعيين قيمة التسجيل: $($_. Exception.Message)" "ERROR" إرجاع $false } } # الخطوة 3: إنشاء مجموعة أمان أو الحصول عليها $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue إذا (-ليس $existingGroup) { جرب { $group = New-ADGroup -الاسم $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -وصف "أجهزة الكمبيوتر المستهدفة لطرح التمهيد الآمن - $GPOName" ' -PassThru Write-Log "تم إنشاء مجموعة أمان: $SecurityGroupName" "موافق" } التقاط { Write-Log "فشل إنشاء مجموعة أمان: $($_. Exception.Message)" "ERROR" إرجاع $false } } آخر { Write-Log "مجموعة الأمان موجودة: $SecurityGroupName" "INFO" $group = $existingGroup } # الخطوة 4: إضافة أجهزة كمبيوتر إلى مجموعة الأمان $added = 0 $failed = 0 foreach ($hostname في $WaveHostnames) { جرب { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -members $computer -ErrorAction SilentlyContinue $added++ } التقاط { $failed++ } } Write-Log "تمت إضافة أجهزة كمبيوتر $added إلى مجموعة الأمان ($failed لم يتم العثور عليها في AD)" "موافق" # الخطوة 5: تكوين تصفية الأمان على عنصر نهج المجموعة جرب { # إزالة الإذن الافتراضي "المستخدمون المصادق عليهم" (الاحتفاظ بالقراءة) Set-GPPermission -name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue # إضافة إذن تطبيق لمجموعة الأمان الخاصة بنا Set-GPPermission -name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "تصفية الأمان المكونة ل: $SecurityGroupName" "OK" } التقاط { Write-Log "فشل تكوين تصفية الأمان: $($_. Exception.Message)" "WARN" Write-Log "قد ينطبق عنصر نهج المجموعة على جميع أجهزة الكمبيوتر في الوحدة التنظيمية المرتبطة - تحقق يدويا" "WARN" } # الخطوة 6: ربط عنصر نهج المجموعة بالوحدة التنظيمية (حرج لتطبيق النهج) إذا ($TargetOU) { جرب { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } إذا (-ليس $existingLink) { New-GPLink -name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "عنصر نهج المجموعة المرتبط ب: $TargetOU" "موافق" Write-Log "سيتم تطبيق عنصر نهج المجموعة في gpupdate التالي على أجهزة الكمبيوتر الهدف" "INFO" } آخر { Write-Log "عنصر نهج المجموعة المرتبط بالفعل بالوحدة التنظيمية المستهدفة" "INFO" } } التقاط { Write-Log "CRITICAL: فشل ربط عنصر نهج المجموعة ب OU: $($_. Exception.Message)" "ERROR" Write-Log "تم إنشاء عنصر نهج المجموعة ولكن غير مرتبط - لن ينطبق على أي أجهزة كمبيوتر!" "خطأ" Write-Log "الإصلاح اليدوي مطلوب: New-GPLink -name '$GPOName' -Target '$TargetOU' -LinkEnabled Yes" "ERROR" إرجاع $false } } آخر { Write-Log "تحذير: لم يتم تحديد TargetOU - تم إنشاء عنصر نهج المجموعة ولكن NOT LINKED!" "خطأ" Write-Log "الارتباط اليدوي المطلوب لكي يدخل عنصر نهج المجموعة حيز التنفيذ" "ERROR" Write-Log "Run: New-GPLink -Name '$GPOName' -Target '<your-Domain-DN>' -LinkEnabled Yes" "ERROR" } # الخطوة 7: التحقق من تكوين عنصر نهج المجموعة Write-Log "التحقق من تكوين عنصر نهج المجموعة..." "INFO" جرب { $gpoReport = Get-GPO -name $GPOName -ErrorAction Stop Write-Log "حالة عنصر نهج المجموعة: $($gpoReport.GpoStatus)" "INFO" # تحقق مما إذا كان إعداد التسجيل قد تم تكوينه $regSettings = Get-GPRegistryValue -الاسم $GPOName -المفتاح "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue إذا (-ليس $regSettings) { # جرب التحقق من سجل GPP (مسار مختلف في GPO) Write-Log "التحقق من تفضيلات تسجيل GPP..." "INFO" } } التقاط { Write-Log "تعذر التحقق من عنصر نهج المجموعة: $($_. Exception.Message)" "WARN" } إرجاع $true }
# ============================================================================ # WINCS DEPLOYMENT (بديل ل AvailableUpdatesPolicy GPO) # ============================================================================ مرجع # : https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe # # أوامر WinCS (تشغيل على نقطة النهاية ضمن سياق النظام): # Query: WinCsFlags.exe /query --key F33E0C8E002 # Apply: WinCsFlags.exe /apply --key "F33E0C8E002" # Reset: WinCsFlags.exe /reset --key "F33E0C8E002" # # ينشر هذا الأسلوب عنصر نهج المجموعة مع مهمة مجدولة تعمل WinCsFlags.exe /apply # كنظام على نقاط النهاية المستهدفة. على غرار كيفية نشر البرنامج النصي للكشف، # ولكن يعمل مرة واحدة (عند بدء التشغيل) بدلا من يوميا.
function Deploy-WinCSGPOForWave { <# . خلاصه نشر تمكين التمهيد الآمن ل WinCS عبر المهمة المجدولة ل GPO.. وصف إنشاء عنصر نهج المجموعة الذي ينشر مهمة مجدولة لتشغيل WinCsFlags.exe /apply ضمن سياق النظام عند بدء تشغيل الجهاز. استهداف عناصر تحكم مجموعة الأمان.. PARAMETER GPOName اسم عنصر نهج المجموعة.. هدف المعلمة الوحدة التنظيمية لربط عنصر نهج المجموعة ب.. PARAMETER SecurityGroupName مجموعة أمان لتصفية عنصر نهج المجموعة.. أسماء موجة المعلمات أسماء المضيفين لإضافتها إلى مجموعة الأمان.. PARAMETER WinCSKey مفتاح WinCS المراد تطبيقه (افتراضي: F33E0C8E002).. تشغيل المعلمة إذا كان صحيحا، فقم بتسجيل ما سيتم القيام به فقط.#> param( [Parameter(إلزامي = $true)] [string]$GPOName، [Parameter(إلزامي = $false)] [سلسلة]$TargetOU، [Parameter(إلزامي = $true)] [سلسلة]$SecurityGroupName، [Parameter(إلزامي = $true)] [array]$WaveHostnames، [Parameter(إلزامي = $false)] [string]$WinCSKey = "F33E0C8E002"، [Parameter(إلزامي = $false)] [bool]$DryRun = $false ) # تكوين المهمة المجدولة WinCsFlags.exe $TaskName = "SecureBoot-WinCS-Apply" $TaskPath = "\Microsoft\Windows\SecureBoot\" $TaskDescription = "يطبق تكوين التمهيد الآمن عبر WinCS - المفتاح: $WinCSKey" Write-Log "Deploying WinCS GPO: $GPOName" "WAVE" Write-Log "سيتم تشغيل المهمة: WinCsFlags.exe /apply --key ""$WinCSKey"" "INFO" Write-Log "المشغل: عند بدء تشغيل النظام (يعمل مرة واحدة كنظام)" "INFO" if ($DryRun) { Write-Log "[DRYRUN] إنشاء عنصر نهج المجموعة: $GPOName" "INFO" Write-Log "[DRYRUN] إنشاء مجموعة أمان: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] سيضيف $(@($WaveHostnames). Count) أجهزة الكمبيوتر المراد تجميعها" "INFO" Write-Log "[DRYRUN] سيتم نشر المهمة المجدولة: $TaskName" "INFO" Write-Log "[DRYRUN] سيربط عنصر نهج المجموعة ب: $TargetOU" "INFO" إرجاع @{ Success = $true GPOCreated = $false GroupCreated = $false أجهزة الكمبيوتر المضافة = 0 } } جرب { # استيراد الوحدات المطلوبة Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } التقاط { Write-Log "فشل استيراد الوحدات النمطية المطلوبة (GroupPolicy، ActiveDirectory): $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } # الخطوة 1: إنشاء عنصر نهج المجموعة أو الحصول عليه $gpo = Get-GPO -name $GPOName -ErrorAction SilentlyContinue إذا ($gpo) { Write-Log "GPO موجود بالفعل: $GPOName" "INFO" } آخر { جرب { $gpo = New-GPO -name $GPOName -Comment "Secure Boot WinCS Deployment - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "تم إنشاء عنصر نهج المجموعة: $GPOName" "موافق" } التقاط { Write-Log "فشل إنشاء عنصر نهج المجموعة: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } } # الخطوة 2: إنشاء XML للمهمة المجدولة لتوزيع عنصر نهج المجموعة # يؤدي هذا إلى إنشاء مهمة تقوم بتشغيل WinCsFlags.exe /apply عند بدء التشغيل $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <إصدار المهمة="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> >معلومات تسجيل < >$TaskDescription</Description <الوصف> WinCsFlags.exe1 Author>SYSTEM</Author> >WinCsFlags.exe5 /RegistrationInfo >مشغلات WinCsFlags.exe7 >WinCsFlags.exe9 BootTrigger تمكين <>صحيح</تمكين> <تأخير>PT5M</delay> </BootTrigger> ></Triggers >أساسيات < <Principal id="Author"> <UserId>S-1-5-18</UserId> <RunLevel>أعلى</RunLevel> ></Principal ></Principals > إعدادات < <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>خطأ</disallowStartIfOnBatteries> <StopIfGoingOnBatteries></StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <RunOnlyIfNetworkAvailable>خطأ</RunOnlyIfNetworkAvailable> ><IdleSettings <StopOnIdleEnd>خطأ</StopOnIdleEnd> <RestartOnIdle>خطأ</RestartOnIdle> ></IdleSettings <AllowStartOnDemand>true</AllowStartOnDemand> <ممكن></Enabled> <مخفي></مخفي> <RunOnlyIfIdle>خطأ</RunOnlyIfIdle> WinCsFlags.exe03 عدم السماح بStartOnRemoteAppSession>خطأ</disallowStartOnRemoteAppSession> WinCsFlags.exe07 UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine> WinCsFlags.exe11 WakeToRun>خطأ</WakeToRun> WinCsFlags.exe15 ExecutionTimeLimit>PT1H</ExecutionTimeLimit> WinCsFlags.exe19 DeleteExpiredTaskAfter>P30D</DeleteExpiredTaskAfter> WinCsFlags.exe23 الأولوية>7</> الأولوية >WinCsFlags.exe27 /Settings سياق إجراءات WinCsFlags.exe29="Author"WinCsFlags.exe30 WinCsFlags.exe31 Exec> WinCsFlags.exe33 Command>WinCsFlags.exe</Command> وسيطات WinCsFlags.exe37>/apply --key "$WinCSKey"WinCsFlags.exe39 /Arguments> WinCsFlags.exe41 /Exec> >WinCsFlags.exe43 /Actions WinCsFlags.exe45 /> المهمة " @
# Step 3: Deploy scheduled task via GPO Preferences # تخزين XML للمهمة في SYSVOL للمهام المجدولة GPO المهمة الفورية جرب { $gpoId = $gpo. Id.ToString() $sysvolPath = "\\$((Get-ADDomain). DNSRoot)\SYSVOL\$((Get-ADDomain). DNSRoot)\Policies\{$gpoId}\Machine\Preferences\ScheduledTasks" if (-not (test-Path $sysvolPath)) { New-Item -ItemType Directory -Path $sysvolPath -Force | خارج فارغة } # إنشاء ScheduledTasks.xml ل GPP $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"> <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName" image="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="{$([guid]::NewGuid(). ToString(). ToUpper())}"> <Properties action="C" name="$TaskName" runAs="NT AUTHORITY\System" logonType="S4U"> <إصدار المهمة="1.3"> >معلومات تسجيل < >$TaskDescription</Description <الوصف> ></RegistrationInfo >كيانات < <Principal id="Author"> <UserId>NT AUTHORITY\System</UserId> <LogonType>S4U</LogonType> <RunLevel>أعلى</RunLevel> ></Principal ></Principals > إعدادات < ><IdleSettings مدة <>PT5M</Duration> <WaitTimeout>PT1H</WaitTimeout> <StopOnIdleEnd>خطأ</StopOnIdleEnd> <RestartOnIdle>خطأ</RestartOnIdle> ></IdleSettings <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <عدم السماح بStartIfOnBatteries>خطأ</disallowStartIfOnBatteries> <StopIfGoingOnBatteries></StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <AllowStartOnDemand>true</AllowStartOnDemand> تمكين <></تمكين> الحقيقي <مخفي>خطأ</مخفي> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <الأولوية>7</> الأولوية <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> ></Settings >مشغلات < >timeTrigger < <StartBoundary>$(Get-Date -Format 'yyyy-MM-dd')T00:00:00</StartBoundary> >تمكين <></Enabled ></TimeTrigger ></Triggers > إجراءات < <Exec> <Command>WinCsFlags.exe</Command> <الوسيطات>/apply --key "$WinCSKey" </Arguments> </Exec> ></Actions </> المهمة ></Properties ></ImmediateTaskV2 ></ScheduledTasks "@ $gppTaskXml | Out-File -FilePath (مسار الانضمام $sysvolPath "ScheduledTasks.xml") -ترميز UTF8 -Force Write-Log "المهمة المجدولة المنشورة إلى عنصر نهج المجموعة: $TaskName" "موافق" } التقاط { Write-Log "فشل نشر XML للمهمة المجدولة: $($_. Exception.Message)" "WARN" Write-Log "الرجوع إلى توزيع WinCS المستند إلى السجل" "INFO" # الاحتياطي: استخدم نهج تسجيل WinCS إذا فشلت مهمة GPP المجدولة # يمكن أيضا تشغيل WinCS عبر مفتاح التسجيل # (يعتمد التنفيذ على واجهة برمجة تطبيقات سجل WinCS إذا كان متوفرا) } # الخطوة 4: إنشاء مجموعة أمان أو الحصول عليها $group = Get-ADGroup -تصفية "الاسم -eq "$SecurityGroupName"" -ErrorAction SilentlyContinue إذا (-ليس $group) { جرب { $group = New-ADGroup -الاسم $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -وصف "أجهزة الكمبيوتر المستهدفة لطرح Secure Boot WinCS - $GPOName" " -PassThru Write-Log "تم إنشاء مجموعة أمان: $SecurityGroupName" "OK" } التقاط { Write-Log "فشل إنشاء مجموعة أمان: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } } آخر { Write-Log "مجموعة الأمان موجودة: $SecurityGroupName" "INFO" } # الخطوة 5: إضافة أجهزة كمبيوتر إلى مجموعة الأمان $added = 0 $failed = 0 foreach ($hostname في $WaveHostnames) { جرب { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } التقاط { $failed++ } } Write-Log "تمت إضافة أجهزة كمبيوتر $added إلى مجموعة الأمان ($failed لم يتم العثور عليها في AD)" "موافق" # الخطوة 6: تكوين تصفية الأمان على عنصر نهج المجموعة جرب { Set-GPPermission -name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue Set-GPPermission -name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "تصفية الأمان المكونة ل: $SecurityGroupName" "موافق" } التقاط { Write-Log "فشل تكوين تصفية الأمان: $($_. Exception.Message)" "WARN" } # الخطوة 7: ربط عنصر نهج المجموعة بالوحدة التنظيمية إذا ($TargetOU) { جرب { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } إذا (-ليس $existingLink) { New-GPLink -name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "عنصر نهج المجموعة المرتبط ب: $TargetOU" "موافق" } آخر { Write-Log "عنصر نهج المجموعة مرتبط بالفعل بالوحدة التنظيمية المستهدفة" "INFO" } } التقاط { Write-Log "حرج: فشل ربط عنصر نهج المجموعة بالوحدة التنظيمية: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = "فشل ارتباط عنصر نهج المجموعة: $($_. Exception.Message)" } } } Write-Log "اكتمال نشر WinCS GPO" "OK" Write-Log "سيتم تشغيل الأجهزة WinCsFlags.exe في تحديث عنصر نهج المجموعة التالي + إعادة التشغيل/بدء التشغيل" "INFO" إرجاع @{ Success = $true GPOCreated = $true GroupCreated = $true أجهزة الكمبيوتر المضافة = $added ComputersFailed = $failed } }
# Wrapper function to maintain compatibility with main loop الدالة Deploy-WinCSForWave { param( [Parameter(إلزامي = $true)] [array]$WaveHostnames، [Parameter(إلزامي = $false)] [string]$WinCSKey = "F33E0C8E002"، [Parameter(إلزامي = $false)] [string]$WavePrefix = "SecureBoot-Rollout"، [Parameter(إلزامي = $false)] [int]$WaveNumber = 1، [Parameter(إلزامي = $false)] [string]$TargetOU، [Parameter(إلزامي = $false)] [bool]$DryRun = $false ) $gpoName = "${WavePrefix}-WinCS-Wave${WaveNumber}" $securityGroup = "${WavePrefix}-WinCS-Wave${WaveNumber}" $result = Deploy-WinCSGPOForWave ' -GPOName $gpoName ' -TargetOU $TargetOU ' -SecurityGroupName $securityGroup ' -WaveHostnames $WaveHostnames ' -WinCSKey $WinCSKey ' -DryRun $DryRun # تحويل إلى تنسيق إرجاع متوقع إرجاع @{ Success = $result. النجاح مطبق = $result. أجهزة الكمبيوتر المضافة تم تخطيه = 0 فشل = إذا ($result. ComputersFailed) { $result. ComputersFailed } آخر { 0 } النتائج = @() } }
# ============================================================================ # ENABLE TASK DEPLOYMENT # ============================================================================ # انشر Enable-SecureBootUpdateTask.ps1 على الأجهزة ذات المهمة المجدولة المعطلة.# يستخدم عنصر نهج المجموعة مع مهمة مجدولة فورية يتم تشغيلها مرة واحدة.
function Deploy-EnableTaskGPO { <# . خلاصه انشر Enable-SecureBootUpdateTask.ps1 عبر المهمة المجدولة ل GPO.. وصف إنشاء عنصر نهج المجموعة الذي ينشر مهمة مجدولة لمرة واحدة لتمكين مهمة Secure-Boot-Update المجدولة على الأجهزة المستهدفة.. هدف المعلمة الوحدة التنظيمية لربط عنصر نهج المجموعة ب.. PARAMETER TargetHostnames أسماء مضيفي الأجهزة ذات المهمة المعطلة (من تقرير التجميع).. تشغيل المعلمة إذا كان صحيحا، فقم بتسجيل ما سيتم القيام به فقط.#> param( [Parameter(إلزامي = $false)] [string]$TargetOU، [Parameter(إلزامي = $true)] [array]$TargetHostnames، [Parameter(إلزامي = $false)] [bool]$DryRun = $false ) $GPOName = "SecureBoot-EnableTask-Remediation" $SecurityGroupName = "SecureBoot-EnableTask-Devices" $TaskName = "SecureBoot-EnableTask-OneTime" $TaskDescription = "مهمة لمرة واحدة لتمكين مهمة Secure-Boot-Update المجدولة" Write-Log "=" * 70 "INFO" Write-Log "DEPLOYING ENABLE TASK REMEDIATION" "INFO" Write-Log "=" * 70 "INFO" Write-Log "الأجهزة المستهدفة: $($TargetHostnames.Count)" "INFO" Write-Log "عنصر نهج المجموعة: $GPOName" "INFO" Write-Log "مجموعة الأمان: $SecurityGroupName" "INFO" إذا ($DryRun) { Write-Log "[DRYRUN] إنشاء عنصر نهج المجموعة: $GPOName" "INFO" Write-Log "[DRYRUN] إنشاء مجموعة أمان: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] من شأنه إضافة أجهزة كمبيوتر $($TargetHostnames.Count) لتجميع" "INFO" Write-Log "[DRYRUN] نشر مهمة مجدولة لمرة واحدة لتمكين Secure-Boot-Update" "INFO" Write-Log "[DRYRUN] ربط عنصر نهج المجموعة ب: $TargetOU" "INFO" إرجاع @{ Success = $true أجهزة الكمبيوتر المضافة = 0 DryRun = $true } } جرب { # استيراد الوحدات المطلوبة Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } التقاط { Write-Log "فشل استيراد الوحدات النمطية المطلوبة: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } # الخطوة 1: إنشاء عنصر نهج المجموعة أو الحصول عليه $gpo = Get-GPO -name $GPOName -ErrorAction SilentlyContinue if ($gpo) { Write-Log "GPO موجود بالفعل: $GPOName" "INFO" } آخر { جرب { $gpo = New-GPO -name $GPOName -Comment "Secure Boot Task Enable Remediation - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "تم إنشاء عنصر نهج المجموعة: $GPOName" "موافق" } التقاط { Write-Log "فشل إنشاء عنصر نهج المجموعة: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } } # الخطوة 2: نشر XML للمهمة المجدولة إلى GPO SYSVOL # تقوم المهمة بتشغيل أمر PowerShell لتمكين مهمة Secure-Boot-Update جرب { $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. المعرف)}\Machine\Preferences\ScheduledTasks" if (-not (test-Path $sysvolPath)) { New-Item -ItemType Directory -Path $sysvolPath -Force | خارج فارغة } # أمر PowerShell لتمكين مهمة Secure-Boot-Update $enableCommand = 'schtasks.exe /Change /TN "\Microsoft\Windows\PI\Secure-Boot-Update" /ENABLE 2>$null; if ($LASTEXITCODE -ne 0) { Get-ScheduledTask -TaskPath "\Microsoft\Windows\PI\" -TaskName "Secure-Boot-Update" -ErrorAction SilentlyContinue | Enable-ScheduledTask }' الأمر #Encode لتضمين XML الآمن $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand)) $taskGuid = [guid]::NewGuid(). ToString("B"). ToUpper() # GPP Scheduled Task XML - المهمة الفورية التي يتم تشغيلها مرة واحدة $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"> <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName" image="0CA8D43ADB5F}" name="$TaskName" image="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="$taskGuid" removePolicy="1" userContext="0"> <Properties action="C" name="$TaskName" runAs="NT AUTHORITY\SYSTEM" logonType="S4U"> <إصدار المهمة="1.3"> >معلومات تسجيل < <الوصف>$TaskDescription</الوصف> ></RegistrationInfo >كيانات < <Principal id="Author"> <UserId>S-1-5-18</UserId> <RunLevel>أعلى</RunLevel> </> الأساسي ></Principals > إعدادات < ><IdleSettings <المدة>PT5M</Duration> <WaitTimeout>PT1H</WaitTimeout> <StopOnIdleEnd>خطأ</StopOnIdleEnd> <RestartOnIdle>خطأ</RestartOnIdle> ></IdleSettings <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <عدم السماح بStartIfOnBatteries>خطأ</disallowStartIfOnBatteries> <StopIfGoingOnBatteries>خطأ</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <AllowStartOnDemand>true</AllowStartOnDemand> تمكين <></تمكين> <مخفي>خطأ</مخفي> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> >الأولوية <7</> الأولوية <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> ></Settings > إجراءات < <Exec> <Command>powershell.exe</Command> وسيطات <>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments> ></Exec ></Actions </> المهمة ></Properties ></ImmediateTaskV2 ></ScheduledTasks "@ $gppTaskXml | Out-File -FilePath (مسار الانضمام $sysvolPath "ScheduledTasks.xml") -ترميز UTF8 -Force Write-Log "تم نشر مهمة مجدولة لمرة واحدة إلى عنصر نهج المجموعة: $TaskName" "موافق" } التقاط { Write-Log "فشل نشر XML للمهمة المجدولة: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } # الخطوة 3: إنشاء مجموعة أمان أو الحصول عليها $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $group) { جرب { $group = New-ADGroup -الاسم $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -وصف "أجهزة الكمبيوتر ذات مهمة Secure-Boot-Update المعطلة - مستهدفة للمعالجة" ' -PassThru Write-Log "تم إنشاء مجموعة أمان: $SecurityGroupName" "موافق" } التقاط { Write-Log "فشل إنشاء مجموعة أمان: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = $_. Exception.Message } } } آخر { Write-Log "مجموعة الأمان موجودة: $SecurityGroupName" "INFO" } # الخطوة 4: إضافة أجهزة كمبيوتر إلى مجموعة الأمان $added = 0 $failed = 0 foreach ($hostname في $TargetHostnames) { جرب { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } التقاط { $failed++ Write-Log "لم يتم العثور على الكمبيوتر في AD: $hostname" "WARN" } } Write-Log "تمت إضافة أجهزة كمبيوتر $added إلى مجموعة الأمان ($failed لم يتم العثور عليها في AD)" "موافق" # الخطوة 5: تكوين تصفية الأمان على عنصر نهج المجموعة جرب { Set-GPPermission -name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue Set-GPPermission -name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "تصفية الأمان المكونة ل: $SecurityGroupName" "OK" } التقاط { Write-Log "فشل تكوين تصفية الأمان: $($_. Exception.Message)" "WARN" } # الخطوة 6: ربط عنصر نهج المجموعة بالوحدة التنظيمية if ($TargetOU) { جرب { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } إذا (-ليس $existingLink) { New-GPLink -name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "عنصر نهج المجموعة المرتبط ب: $TargetOU" "موافق" } آخر { Write-Log "عنصر نهج المجموعة مرتبط بالفعل بالوحدة التنظيمية المستهدفة" "INFO" } } التقاط { Write-Log "فشل ربط عنصر نهج المجموعة بالوحدة التنظيمية: $($_. Exception.Message)" "ERROR" إرجاع @{ Success = $false؛ خطأ = "فشل ارتباط عنصر نهج المجموعة: $($_. Exception.Message)" } } } آخر { Write-Log "لم يتم تحديد TargetOU - يجب ربط عنصر نهج المجموعة يدويا" "WARN" } Write-Log "" "INFO" Write-Log "ENABLE TASK DEPLOYMENT COMPLETE" "OK" Write-Log "ستقوم الأجهزة بتشغيل مهمة التمكين عند تحديث عنصر نهج المجموعة التالي (gpupdate)" "INFO" Write-Log "يتم تشغيل المهمة مرة واحدة كنظام وتمكن Secure-Boot-Update" "INFO" Write-Log "" "INFO" إرجاع @{ Success = $true أجهزة الكمبيوتر المضافة = $added ComputersFailed = $failed GPOName = $GPOName مجموعة الأمان = $SecurityGroupName } }
# ============================================================================ # تمكين المهمة على الأجهزة المعطلة # ============================================================================ إذا ($EnableTaskOnDisabled) { Write-Host "" Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host " ENABLE TASK REMEDIATION - Fixing Disabled Scheduled Tasks" -ForegroundColor Yellow Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host "" # البحث عن الأجهزة ذات المهمة المعطلة من بيانات التجميع if (-not $AggregationInputPath) { Write-Host "ERROR: -AggregationInputPath مطلوب لتحديد الأجهزة ذات المهمة المعطلة" -ForegroundColor Red Write-Host "Usage: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <path> -ReportBasePath <path>" -ForegroundColor Gray الخروج 1 } Write-Host "المسح الضوئي للأجهزة ذات مهمة Secure-Boot-Update المعطلة..." -ForegroundColor Cyan # تحميل ملفات JSON والبحث عن الأجهزة ذات المهمة المعطلة $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. الاسم -notmatch "ScanHistory|الإطلاق التدريجي|RolloutPlan" } $disabledTaskDevices = @() foreach ($file في $jsonFiles) { جرب { $device = Get-Content $file. FullName -Raw | ConvertFrom-Json إذا ($device. SecureBootTaskEnabled -eq $false -أو $device. SecureBootTaskStatus -eq 'Disabled' -or $device. SecureBootTaskStatus -eq 'NotFound') { # تتضمن فقط الأجهزة التي لم يتم تحديثها بالفعل (لا يوجد حدث 1808) if ([int]$device. Event1808Count -eq 0) { $disabledTaskDevices += $device. المضيف } } } التقاط { # تخطي الملفات غير الصالحة } } $disabledTaskDevices = $disabledTaskDevices | Select-Object -فريد if ($disabledTaskDevices.Count -eq 0) { Write-Host "" Write-Host "لم يتم العثور على أجهزة ذات مهمة Secure-Boot-Update معطلة." -ForegroundColor Green Write-Host "جميع الأجهزة إما تم تمكين المهمة أو تم تحديثها بالفعل." -ForegroundColor Gray إنهاء 0 } Write-Host "" Write-Host "تم العثور على أجهزة $($disabledTaskDevices.Count) ذات المهمة المعطلة:" -ForegroundColor Yellow $disabledTaskDevices | Select-Object -أول 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } if ($disabledTaskDevices.Count -gt 20) { Write-Host " ... و$($disabledTaskDevices.Count - 20) أكثر" -ForegroundColor Gray } Write-Host "" # نشر تمكين عنصر نهج المجموعة للمهمة $result = Deploy-EnableTaskGPO -TargetHostnames $disabledTaskDevices -TargetOU $TargetOU -DryRun $DryRun إذا ($result. نجاح) { Write-Host "" Write-Host "SUCCESS: Enable Task GPO deployed" -ForegroundColor Green Write-Host " أجهزة الكمبيوتر المضافة إلى مجموعة الأمان: $($result. أجهزة الكمبيوتر المضافة)" -ForegroundColor Cyan إذا ($result. ComputersFailed -gt 0) { Write-Host " لم يتم العثور على أجهزة الكمبيوتر في AD: $($result. ComputersFailed)" -ForegroundColor Yellow } Write-Host "" Write-Host "NEXT STEPS:" -ForegroundColor White Write-Host " 1. ستتلقى الأجهزة عنصر نهج المجموعة في التحديث التالي (gpupdate /force)" -ForegroundColor Gray Write-Host " 2. ستقوم المهمة لمرة واحدة بتمكين Secure-Boot-Update" -ForegroundColor Gray Write-Host " 3. إعادة تشغيل التجميع للتحقق من تمكين المهمة الآن" -ForegroundColor Gray } آخر { Write-Host "" Write-Host "فشل: تعذر نشر تمكين عنصر نهج المجموعة للمهمة" -ForegroundColor Red Write-Host "خطأ: $($result. خطأ)" -ForegroundColor Red } إنهاء 0 }
# ============================================================================ # حلقة التنسيق الرئيسي # ============================================================================
Write-Host "" Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host " SECURE BOOT ROLLOUT ORCHESTRATOR - CONTINUOUS DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host ""
if ($DryRun) { Write-Host "[وضع التشغيل الجاف]" -ForegroundColor Magenta }
if ($UseWinCS) { Write-Host "[WinCS MODE]" -ForegroundColor Yellow Write-Host "استخدام WinCsFlags.exe بدلا من GPO/AvailableUpdatesPolicy" -ForegroundColor Yellow Write-Host "مفتاح WinCS: $WinCSKey" -ForegroundColor Gray Write-Host "" }
Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "مسار الإدخال: $AggregationInputPath" "INFO" Write-Log "مسار التقرير: $ReportBasePath" "INFO" if ($UseWinCS) { Write-Log "طريقة التوزيع: WinCS (WinCsFlags.exe /apply --key ""$WinCSKey")" "INFO" } آخر { Write-Log "أسلوب النشر: GPO (AvailableUpdatesPolicy)" "INFO" }
# Resolve TargetOU - default to domain root for domain-wide coverage # مطلوب فقط لأسلوب توزيع GPO (لا يتطلب WinCS AD/GPO) إذا (-not $UseWinCS -and -not $TargetOU) { جرب { # جرب أساليب متعددة للحصول على DN المجال $domainDN = $null # الأسلوب 1: Get-ADDomain (يتطلب RSAT-AD-PowerShell) جرب { Import-Module ActiveDirectory -ErrorAction Stop $domainDN = (Get-ADDomain -ErrorAction Stop). الاسم المميز } التقاط { Write-Log "فشل Get-ADDomain: $($_. Exception.Message)" "WARN" } # الأسلوب 2: استخدام RootDSE عبر ADSI if (-not $domainDN) { جرب { $rootDSE = [ADSI]"LDAP://RootDSE" $domainDN = $rootDSE.defaultNamingContext.ToString() } التقاط { Write-Log "فشل ADSI RootDSE: $($_. Exception.Message)" "WARN" } } # الأسلوب 3: التحليل من عضوية مجال الكمبيوتر if (-not $domainDN) { جرب { $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() $domainDN = "DC=" + ($domain. Name -replace '\.', ',DC=') } التقاط { Write-Log "فشل GetComputerDomain: $($_. Exception.Message)" "WARN" } } if ($domainDN) { $TargetOU = $domainDN Write-Log "الهدف: جذر المجال ($domainDN) - سيطبق عنصر نهج المجموعة على مستوى المجال عبر تصفية مجموعة الأمان" "INFO" } آخر { Write-Log "تعذر تحديد DN المجال - سيتم إنشاء عنصر نهج المجموعة ولكن NOT LINKED!" "خطأ" Write-Log "الرجاء تحديد المعلمة -TargetOU أو ربط عنصر نهج المجموعة يدويا بعد الإنشاء" "ERROR" $TargetOU = $null } } التقاط { Write-Log "تعذر الحصول على DN المجال - سيتم إنشاء عنصر نهج المجموعة ولكن ليس مرتبطا. قم بالارتباط يدويا إذا لزم الأمر." "تحذير" Write-Log "خطأ: $($_. Exception.Message)" "WARN" $TargetOU = $null } } آخر { Write-Log "الهدف OU: $TargetOU" "INFO" }
Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "الفاصل الزمني للاستقصاء: $PollIntervalMinutes دقيقة" "INFO" إذا ($LargeScaleMode) { Write-Log "LargeScaleMode enabled (حجم الدفعة: $ProcessingBatchSize، نموذج السجل: $DeviceLogSampleSize)" "INFO" }
# ============================================================================ # التحقق من المتطلبات الأساسية: التحقق من نشر الكشف والعمل # ============================================================================
Write-Host "" Write-Log "التحقق من المتطلبات الأساسية..." "INFO"
$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath إذا (-not $detectionCheck.IsDeployed) { Write-Log $detectionCheck.الرسالة "خطأ" Write-Host "" Write-Host "مطلوب: نشر البنية الأساسية للكشف أولا:" -ForegroundColor Yellow Write-Host " 1. تشغيل: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\server\SecureBootLogs$'" -ForegroundColor Cyan Write-Host " 2. انتظر حتى تبلغ الأجهزة (12-24 ساعة)" -ForegroundColor Cyan Write-Host " 3. إعادة تشغيل هذا المنسق" -ForegroundColor Cyan Write-Host "" if (-not $DryRun) { العوده } } آخر { Write-Log $detectionCheck.الرسالة "موافق" }
# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "حداثة البيانات: $($freshness. ملفات TotalFiles)، $($freshness. FreshFiles) الطازجة (<24h)، $($freshness. StaleFiles) قديمة (>72h)" "INFO" إذا ($freshness. تحذير) { Write-Log $freshness. تحذير "تحذير" }
# Load Allow List (targeted rollout - ONLY these devices will be rolled out) $allowedHostnames = @() إذا ($AllowListPath -أو $AllowADGroup) { $allowedHostnames = Get-AllowedHostnames -AllowFilePath $AllowListPath -ADGroupName $AllowADGroup إذا ($allowedHostnames.Count -gt 0) { Write-Log "AllowList: سيتم النظر في أجهزة $($allowedHostnames.Count) فقط للإخراج" "INFO" } آخر { Write-Log "AllowList محددة ولكن لم يتم العثور على أي أجهزة - سيؤدي ذلك إلى حظر جميع عمليات الإطلاق!" "تحذير" } }
# Load VIP/exclusion list (BlockList) $excludedHostnames = @() إذا ($ExclusionListPath -أو $ExcludeADGroup) { $excludedHostnames = Get-ExcludedHostnames -ExclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup إذا ($excludedHostnames.Count -gt 0) { Write-Log "استبعاد VIP: سيتم تخطي أجهزة $($excludedHostnames.Count) من الإطلاق" "INFO" } }
# Load state $rolloutState = Get-RolloutState $blockedBuckets = Get-BlockedBuckets $adminApproved = Get-AdminApproved $deviceHistory = Get-DeviceHistory
if ($rolloutState.Status -eq "NotStarted") { $rolloutState.Status = "InProgress" $rolloutState.StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Write-Log "بدء إطلاق جديد" "WAVE" }
Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "المستودعات المحظورة: $($blockedBuckets.Count)" "INFO"
# Main loop - runs until all eligible devices are updated $iterationCount = 0 بينما ($true) { $iterationCount++ Write-Host "" Write-Host ("=" * 80) -ForegroundColor White Write-Log "=== ITERATION $iterationCount ===" "WAVE" Write-Host ("=" * 80) -ForegroundColor White # الخطوة 1: تشغيل التجميع Write-Log "الخطوة 1: تشغيل التجميع..." "INFO" # يعيد المنسق دائما استخدام مجلد واحد (LargeScaleMode) لتجنب انتفاث القرص # يحصل المسؤولون الذين يقومون بتشغيل المجمع يدويا على مجلدات ذات طابع زمني للقطات في نقطة زمنية $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current" # تحقق من حداثة البيانات قبل التجميع $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "حداثة البيانات: $($freshness. FreshFiles)/$($freshness. TotalFiles) تم الإبلاغ عن أجهزة في آخر 24 ساعة "INFO" إذا ($freshness. تحذير) { Write-Log $freshness. تحذير "تحذير" } $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1" $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json" $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" if (test-Path $aggregateScript) { if (-not $DryRun) { # يستخدم المنسق دائما الدفق + التزايدي لتحقيق الكفاءة # يرفع Aggregator تلقائيا إلى PS7 إذا كان متوفرا للحصول على أفضل أداء $aggregateParams = @{ InputPath = $AggregationInputPath OutputPath = $aggregationPath StreamingMode = $true IncrementalMode = $true SkipReportIfUnchanged = $true ParallelThreads = 8 } # ملخص إطلاق المرور إذا كان موجودا (لبيانات السرعة/الإسقاط) إذا (مسار الاختبار $rolloutSummaryPath) { $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath } & $aggregateScript @aggregateParams # إظهار الأمر لإنشاء لوحة معلومات HTML كاملة مع جداول الجهاز Write-Host "" Write-Host "لإنشاء لوحة معلومات HTML كاملة باستخدام جداول الشركة المصنعة/النموذج، قم بتشغيل:" -ForegroundColor Yellow Write-Host " $aggregateScript -InputPath ""$AggregationInputPath"" -OutputPath ""$aggregationPath"" -ForegroundColor Yellow Write-Host "" } آخر { Write-Log "[DRYRUN] تشغيل التجميع" "INFO" # في DryRun، استخدم بيانات التجميع الموجودة من ReportBasePath مباشرة $aggregationPath = $ReportBasePath } } $rolloutState.LastAggregation = Get-Date -Format "yyyy-MM-dd HH:mm:ss" # الخطوة 2: تحميل حالة الجهاز الحالي Write-Log "الخطوة 2: تحميل حالة الجهاز..." "INFO" $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue | Where-Object { $_. الاسم -notlike "*Buckets*" } | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 إذا (-not $notUptodateCsv -و-not $DryRun) { Write-Log "لم يتم العثور على بيانات تجميع. قيد الانتظار..." "تحذير" Start-Sleep -ثانية ($PollIntervalMinutes * 60) مواصله } $notUpdatedDevices = if ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } else { @() } Write-Log "الأجهزة غير محدثة: $($notUpdatedDevices.Count)" "INFO" $notUpdatedIndexes = Get-NotUpdatedIndexes -الأجهزة $notUpdatedDevices # الخطوة 3: تحديث محفوظات الجهاز (التعقب حسب اسم المضيف) Write-Log "الخطوة 3: تحديث محفوظات الجهاز..." "INFO" Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory $deviceHistory Save-DeviceHistory -History # الخطوة 4: التحقق من وجود مستودعات محظورة (أجهزة لا يمكن الوصول إليها) $existingBlockedCount = $blockedBuckets.Count Write-Log "الخطوة 4: التحقق من وجود مستودعات محظورة (أجهزة اختبار اتصال بعد فترة الانتظار)..." "INFO" إذا ($existingBlockedCount -gt 0) { Write-Log "المستودعات المحظورة حاليا من عمليات التشغيل السابقة: $existingBlockedCount" "INFO" } if ($adminApproved.Count -gt 0) { Write-Log "مستودعات تمت الموافقة عليها مسؤول (لن يتم إعادة حظرها): $($adminApproved.Count)" "INFO" } $newlyBlocked = Update-BlockedBuckets -RolloutState $rolloutState -BlockedBuckets $blockedBuckets -AdminApproved $adminApproved -NotUpdatedDevices $notUpdatedDevices -NotUpdatedIndexes $notUpdatedIndexes -MaxWaitHours $MaxWaitHours -DryRun:$DryRun if ($newlyBlocked.Count -gt 0) { Save-BlockedBuckets -حظر $blockedBuckets Write-Log "المستودعات المحظورة حديثا (هذا التكرار): $($newlyBlocked.Count)" "BLOCKED" } # الخطوة 4b: إلغاء حظر المستودعات تلقائيا حيث تم تحديث الأجهزة $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize إذا ($autoUnblocked.Count -gt 0) { $blockedBuckets المحظورة Save-BlockedBuckets Write-Log "مستودعات غير محظورة تلقائيا (تم تحديث الأجهزة): $($autoUnblocked.Count)" "OK" } # الخطوة 5: حساب الأجهزة المؤهلة المتبقية $eligibleCount = 0 foreach ($device في $notUpdatedDevices) { $bucketKey = Get-BucketKey $device if (-not $blockedBuckets.Contains($bucketKey)) { $eligibleCount++ } } Write-Log "الأجهزة المؤهلة المتبقية: $eligibleCount" "INFO" Write-Log "المستودعات المحظورة: $($blockedBuckets.Count)" "INFO" # الخطوة 6: التحقق من الاكتمال إذا ($eligibleCount -eq 0) { Write-Log "ROLLOUT COMPLETE - تم تحديث جميع الأجهزة المؤهلة!" "موافق" $rolloutState.Status = "Completed" $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Save-RolloutState -state $rolloutState كسر } # الخطوة 6: إنشاء الموجة التالية وتوزيعها Write-Log "الخطوة 6: إنشاء موجة الإطلاق..." "INFO" $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames # تحقق مما إذا كان لدينا أجهزة لنشرها ($waveDevices يمكن أن تكون $null أو فارغة أو مع الأجهزة الفعلية) $hasDevices = $waveDevices -and @($waveDevices | Where-Object { $_ }). Count -gt 0 إذا ($hasDevices) { # زيادة رقم الموجة فقط عندما يكون لدينا بالفعل أجهزة لنشرها $rolloutState.CurrentWave++ Write-Log "Wave $($rolloutState.CurrentWave): $(@($waveDevices). عدد الأجهزة" "WAVE" # توزيع عنصر نهج المجموعة باستخدام دالة مدمجة $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $hostnames = @($waveDevices | ForEach-Object { if ($_. اسم المضيف) { $_. اسم المضيف } elseif ($_. اسم المضيف) { $_. اسم المضيف } آخر { $null } } | Where-Object { $_ }) # حفظ ملف أسماء المضيفين للمرجع/التدقيق $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt" $hostnames | Out-File $hostnamesFile -ترميز UTF8 # تحقق من صحة أن لدينا أسماء مضيفين للنشر إليها إذا ($hostnames. Count -eq 0) { Write-Log "لم يتم العثور على أسماء مضيفين صالحة في الموجة $($rolloutState.CurrentWave) - قد تفتقد الأجهزة خاصية Hostname" "WARN" Write-Log "تخطي التوزيع لهذه الموجة - تحقق من بيانات الجهاز" "WARN" # لا تزال تنتظر قبل التكرار التالي if (-not $DryRun) { Write-Log "النوم لمدة $PollIntervalMinutes دقائق قبل إعادة المحاولة..." "INFO" Start-Sleep -ثانية ($PollIntervalMinutes * 60) } مواصله } Write-Log "التوزيع إلى $($hostnames. Count) أسماء المضيفين في Wave $($rolloutState.CurrentWave)" "INFO" # التوزيع باستخدام أسلوب WinCS أو GPO استنادا إلى المعلمة -UseWinCS if ($UseWinCS) { #WinCS Method: إنشاء عنصر نهج المجموعة مع مهمة مجدولة لتشغيل WinCsFlags.exe كنظام على كل نقطة نهاية Write-Log "استخدام أسلوب توزيع WinCS (المفتاح: $WinCSKey)" "WAVE" $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames ' -WinCSKey $WinCSKey ' -WavePrefix $WavePrefix ' -WaveNumber $rolloutState.CurrentWave ' -TargetOU $TargetOU ' -DryRun:$DryRun if (-not $wincsResult.Success) { Write-Log "فشل توزيع WinCS - تم تطبيقه: $($wincsResult.Applied)، Failed: $($wincsResult.Failed)" "WARN" } آخر { Write-Log "نجاح توزيع WinCS - مطبق: $($wincsResult.Applied)، تم تخطيه: $($wincsResult.تخطي)" "موافق" } # حفظ نتائج WinCS للتدقيق $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json" $wincsResult | ConvertTo-Json -Depth 5 | Out-File $wincsResultFile -ترميز UTF8 } آخر { #GPO Method: إنشاء عنصر نهج المجموعة مع إعداد سجل AvailableUpdatesPolicy $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun if (-not $gpoResult) { Write-Log "فشل نشر عنصر نهج المجموعة - سيعيد محاولة التكرار التالي" "خطأ" } } # موجة قياسية في الحالة $waveRecord = @{ WaveNumber = $rolloutState.CurrentWave StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" DeviceCount = @($waveDevices). عدد الأجهزة = @($waveDevices | ForEach-Object { @{ اسم المضيف = if ($_. اسم المضيف) { $_. اسم المضيف } elseif ($_. اسم المضيف) { $_. اسم المضيف } آخر { $null } BucketKey = Get-BucketKey $_ } }) } # تأكد من أن WaveHistory هو دائما صفيف قبل إلحاق (يمنع مشكلات الدمج القابلة للتجزئة) $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord) $rolloutState.TotalDevicesTargeted += @($waveDevices). عدد Save-RolloutState -state $rolloutState Write-Log "Wave $($rolloutState.CurrentWave) تم نشرها. الانتظار $PollIntervalMinutes دقيقة..." "موافق" } آخر { # إظهار حالة الأجهزة المنشورة في انتظار التحديثات Write-Log "" "INFO" Write-Log "========== جميع الأجهزة المنشورة - في انتظار ========== الحالة" "INFO" # احصل على جميع الأجهزة المنشورة من محفوظات الموجة $allDeployedLookup = @{} foreach ($wave في $rolloutState.WaveHistory) { foreach ($device في $wave. الأجهزة) { إذا ($device. اسم المضيف) { $allDeployedLookup[$device. اسم المضيف] = @{ اسم المضيف = $device. المضيف BucketKey = $device. مفتاح المستودع DeployedAt = $wave. بدء الاستخدام WaveNumber = $wave. رقم الموجة } } } } $allDeployedDevices = @($allDeployedLookup.Values) إذا ($allDeployedDevices.Count -gt 0) { # ابحث عن الأجهزة المنشورة التي لا تزال معلقة (في القائمة NotUpdated) $stillPendingCount = 0 $noLongerPendingCount = 0 $pendingSample = @() foreach ($deployed في $allDeployedDevices) { if ($notUpdatedIndexes.HostSet.Contains($deployed. اسم المضيف)) { $stillPendingCount++ إذا ($pendingSample.Count -lt $DeviceLogSampleSize) { $pendingSample += $deployed. المضيف } } آخر { $noLongerPendingCount++ } } # الحصول على أعداد محدثة فعلية من التجميع - تمييز الحدث 1808 مقابل UEFICA2023Status $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Summary*.csv" | Sort-Object LastWriteTime -تنازلي | Select-Object -الأول 1 $actualUpdated = 0 $totalDevicesFromSummary = 0 $event 1808Count = 0 $uefiStatusUpdated = 0 $needsRebootSample = @() if ($summaryCsv) { $summary = Import-Csv $summaryCsv.FullName | Select-Object -الأول 1 إذا ($summary. محدث) { $actualUpdated = [int]$summary. تم التحديث } إذا ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices } } # حساب السرعة من محفوظات الموجة (الأجهزة المحدثة يوميا) $devicesPerDay = 0 if ($rolloutState.StartedAt -and $actualUpdated -gt 0) { $startDate = [datetime]::P arse($rolloutState.StartedAt) $daysElapsed = ((Get-Date) - $startDate). TotalDays إذا ($daysElapsed -gt 0) { $devicesPerDay = $actualUpdated / $daysElapsed } } #Save rollout summary with weekend-aware projections # استخدم عدد NotUptodate للمجمع (باستثناء أجهزة SB OFF) للتناسق $notUpdatedCount = if ($summary -and $summary. NotUptodate) { [int]$summary. NotUptodate } else { $totalDevicesFromSummary - $actualUpdated } Save-RolloutSummary -state $rolloutState ' -TotalDevices $totalDevicesFromSummary ' -updatedDevices $actualUpdated ' -NotUpdatedDevices $notUpdatedCount ' -DevicesPerDay $devicesPerDay # تحقق من البيانات الأولية للأجهزة التي تستخدم UEFICA2023Status=Updated ولكن لا يوجد حدث 1808 (يحتاج إلى إعادة التشغيل) $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue $totalDataFiles = @($dataFiles). عدد $batchSize = [Math]::Max(500, $ProcessingBatchSize) إذا ($LargeScaleMode) { $batchSize = [Math]::Max(2000, $ProcessingBatchSize) }
if ($totalDataFiles -gt 0) { ل ($idx = 0؛ $idx -lt $totalDataFiles؛ $idx += $batchSize) { $end = [Math]::Min($idx + $batchSize - 1، $totalDataFiles - 1) $batchFiles = $dataFiles[$idx.. $end]
foreach ($file in $batchFiles) { جرب { $deviceData = Get-Content $file. FullName -Raw | ConvertFrom-Json $hostname = $deviceData.اسم المضيف إذا (-ليس $hostname) { continue } $has 1808 = [int]$deviceData.Event1808Count -gt 0 $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Updated" إذا ($has 1808) { $event 1808Count++ } elseif ($hasUefiUpdated) { $uefiStatusUpdated++ إذا ($needsRebootSample.Count -lt $DeviceLogSampleSize) { $needsRebootSample += $hostname } } } التقاط { } }
Save-ProcessingCheckpoint -Stage "RebootStatusScan" -Processed ($end + 1) -Total $totalDataFiles -Metrics @{ Event1808Count = $event 1808Count UEFIUpdatedAwaitingReboot = $uefiStatusUpdated } } } Write-Log "إجمالي النشر: $($allDeployedDevices.Count)" "INFO" Write-Log "محدث (تم تأكيد الحدث 1808): $event 1808Count" "OK" إذا ($uefiStatusUpdated -gt 0) { Write-Log "محدث (UEFICA2023Status=Updated، في انتظار إعادة التشغيل): $uefiStatusUpdated" "OK" $rebootSuffix = if ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) المزيد)" } آخر { "" } Write-Log " الأجهزة التي تحتاج إلى إعادة التشغيل للحدث 1808 (عينة): $($needsRebootSample -join ', ')) $rebootSuffix" "INFO" Write-Log " ستقوم هذه الأجهزة بالإبلاغ عن الحدث 1808 بعد إعادة التشغيل التالي" "INFO" } Write-Log "لم يعد معلقا: $noLongerPendingCount (بما في ذلك SecureBoot OFF، الأجهزة المفقودة)" "INFO" Write-Log "في انتظار الحالة: $stillPendingCount" "INFO" إذا ($stillPendingCount -gt 0) { $pendingSuffix = if ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) المزيد)" } آخر { "" } Write-Log "الأجهزة المعلقة (عينة): $($pendingSample -join ', ')$pendingSuffix" "WARN" } } آخر { Write-Log "لم يتم نشر أي أجهزة حتى الآن" "INFO" } "INFO" Write-Log "================================================================" Write-Log "" "INFO" } # انتظر قبل التكرار التالي إذا (-ليس $DryRun) { Write-Log "النوم لمدة $PollIntervalMinutes دقيقة..." "INFO" Start-Sleep -ثانية ($PollIntervalMinutes * 60) } آخر { Write-Log "[DRYRUN] ستنتظر $PollIntervalMinutes دقيقة" "INFO" break # Exit بعد تكرار واحد في التشغيل الجاف } }
# ============================================================================ # الملخص النهائي # ============================================================================
Write-Host "" Write-Host ("=" * 80) -ForegroundColor Green Write-Host " ROLLOUT ORCHESTRATOR SUMMARY" -ForegroundColor Green Write-Host ("=" * 80) -ForegroundColor Green Write-Host ""
$finalState = Get-RolloutState $finalBlocked = Get-BlockedBuckets
Write-Host "Status: $($finalState.Status)" -ForegroundColor $(if ($finalState.Status -eq "Completed") { "Green" } else { "Yellow" }) Write-Host "إجمالي الموجات: $($finalState.CurrentWave)" Write-Host "الأجهزة المستهدفة: $($finalState.TotalDevicesTargeted)" Write-Host "المستودعات المحظورة: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } else { "Green" }) Write-Host "الأجهزة المتعقبة: $($deviceHistory.Count)" -ForegroundColor Gray Write-Host ""
if ($finalBlocked.Count -gt 0) { Write-Host "المستودعات المحظورة (تتطلب مراجعة يدوية):" -ForegroundColor Red foreach ($key في $finalBlocked.Keys) { $info = $finalBlocked[$key] Write-Host " - $key" -ForegroundColor Red Write-Host " السبب: $($info. السبب)" -ForegroundColor Gray } Write-Host "" Write-Host "ملف المستودعات المحظورة: $blockedBucketsPath" -ForegroundColor Yellow }
Write-Host "" Write-Host "ملفات الحالة:" -ForegroundColor Cyan Write-Host "حالة الإطلاق: $rolloutStatePath" Write-Host "المستودعات المحظورة: $blockedBucketsPath" Write-Host "سجل الأجهزة: $deviceHistoryPath" Write-Host ""