Скопіюйте та вставте цей зразок сценарію та змініть його за потреби для свого середовища:

<# . ПІДСУМОК     Неперервне розгортання безпечного завантаження orchestrator, яке запускається до завершення розгортання.

.DESCRIPTION     Цей сценарій забезпечує повну автоматизацію розгортання сертифіката безпечного завантаження:     1.      Створення хвиль розгортання на основі даних агрегації     2. Створення груп AD і GPO для кожної хвилі     3. Монітори оновлень пристроїв (подія 1808)     4. Виявлення заблокованих блоків (пристроїв, які недоступні)     5. Автоматичне перебігу до наступної хвилі     6. Працює, доки не буде оновлено      всі пристрої, що відповідають вимогам     Умови завершення:     - Немає пристроїв, що залишилися: потрібна дія, висока впевненість, спостереження, тимчасово призупинено     - Поза областю (за дизайном): не підтримується, безпечне завантаження вимкнуто     - Працює безперервно до завершення - без довільного обмеження      хвилі     Стратегія розгортання:     - ВИСОКА ВПЕВНЕНІСТЬ: Всі пристрої в першій хвилі (безпечні)     - ПОТРІБНА ДІЯ: Прогресивний парний розряд (1→2→4→8...)          Логіка блокування:     - Після MaxWaitHours, orchestrator pings пристрої, які не оновили     - Якщо пристрій недоступний (ping не вдається) → блок заблоковано для розслідування     - Якщо пристрій ДОСТУПНИЙ, але не оновлюється, → продовжуйте чекати (може знадобитися перезавантаження)     - Заблоковані блоки виключаються, доки адміністратор їх      не розблокує     Автоматичне розблокування:     - Якщо пристрій у заблокованому блоці пізніше відображається як оновлений (подія 1808),       блок автоматично розблоковано, а подальше розгортання     - Це обробляє пристрої, які були тимчасово в автономному режимі, але повернулися          Відстеження пристроїв:     - Відстежує пристрої за іменем хоста (припускається, що імена не змінюються під час розгортання)     - Примітка. Колекція JSON не містить унікальний ідентифікатор комп'ютера; додати одну з них для кращого відстеження

.PARAMETER AggregationInputPath     Шлях до необроблених даних пристрою JSON (зі сценарію виявлення)

.PARAMETER ReportBasePath     Основний шлях для звітів про агрегацію

.PARAMETER TargetOU     Відмітне ім'я підрозділу для зв'язування об'єкти групових політик.Необов'язково– якщо не вказано, об'єкт GPO зв'язано з кореневим доменом для покриття в домені.Фільтрування групи безпеки забезпечує отримання політики лише цільовими пристроями.

.PARAMETER MaxWaitHours     Години очікування оновлення пристроїв перед перевіркою доступності.Після цього часу на пристроях, які не оновлюються, з'являться зв'язані зв'язок.Якщо пристрій недоступний, блок блокуватиметься.За замовчуванням: 72 (3 дні)

.PARAMETER PollIntervalMinutes     Хвилини між перевірками стану. За замовчуванням: 1440 (1 день)

.PARAMETER AllowListPath     Шлях до файлу, який містить імена хостів, щоб дозволити розгортання (цільове розгортання).Підтримує .txt (одне ім'я хоста в рядку) або .csv (зі стовпцем Hostname/ComputerName/Name).Якщо вказано цей параметр, лише ці пристрої буде включено до розгортання.BlockList все ще застосовується після AllowList.

.PARAMETER AllowADGroup     Ім'я групи безпеки AD, яка містить облікові записи комп'ютера, для яких потрібно дозволити.Приклад: "SecureBoot-Pilot-Computers" або "Wave1-Devices"     Якщо вказано цей параметр, до розгортання буде включено лише пристрої цієї групи.Об'єднання з AllowListPath для цільового використання файлів і ad-based.

.PARAMETER ExclusionListPath     Шлях до файлу, який містить імена хостів, щоб ВИКЛЮЧИТИ з розгортання (VIP/виконавчі пристрої).Підтримує .txt (одне ім'я хоста в рядку) або .csv (зі стовпцем Hostname/ComputerName/Name).Ці пристрої ніколи не будуть включені в будь-яку хвилю розгортання.BlockList застосовується після фільтрування Списку довірених списків.     . PARAMETER ExcludeADGroup     Ім'я групи безпеки AD, що містить облікові записи комп'ютера, які потрібно виключити.Приклад: "VIP-комп'ютери" або "Executive-Devices"     Об'єднання з exclusionListPath для винятків на основі файлів і AD.

.PARAMETER UseWinCS     Використовуйте WinCS (система конфігурації Windows) замість GPO/AvailableUpdatesPolicy.WinCS розгортає функцію безпечного завантаження, запускаючи WinCsFlags.exe безпосередньо на кожній кінцевій точці.WinCsFlags.exe запускається в контексті SYSTEM через заплановане завдання.Цей метод корисний для:     - Швидше розгортання (негайний ефект проти очікування обробки GPO)     – пристрої, підключені до інших доменів;     - Середовища без інфраструктури 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     Цей ключ відповідає конфігурації розгортання безпечного завантаження.     . Параметр DryRun     Відображення виконаних дій без внесення змін

.PARAMETER ListBlockedBuckets     Відображення всіх заблокованих блоків і вихід

.PARAMETER UnblockBucket     Розблокування певного блоку за допомогою клавіші та вихід

.PARAMETER UnblockAll     Розблокування всіх блоків і вихід

.PARAMETER EnableTaskOnDisabled     Розгорніть Enable-SecureBootUpdateTask.ps1 на всіх пристроях із вимкненим запланованим завданням.Створення об'єктної об'єктної роботи з одноразовим запланованим завданням, яке запускає параметр Увімкнути сценарій із параметром -Quiet.Це зручно, щоб виправити пристрої, на яких вимкнуто завдання 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" -UnblockBucket "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-комп'ютери"

.EXAMPLE     # Використовуйте WinCS (система конфігурації Windows) замість GPO/AvailableUpdatesPolicy     # WinCsFlags.exe запускається в контексті SYSTEM для кожної кінцевої точки за допомогою запланованого завдання     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports"         -UseWinCS '         -WinCSKey "F33E0C8E002" #>

[CmdletBinding()] param(     [Parameter(Mandatory = $false)]     [string]$AggregationInputPath,     [Parameter(Mandatory = $false)]     [string]$ReportBasePath,     [Parameter(Mandatory = $false)]     [string]$TargetOU,     [Parameter(Mandatory = $false)]     [string]$WavePrefix = "SecureBoot-Rollout",     [Parameter(Mandatory = $false)]     [int]$MaxWaitHours = 72,     [Parameter(Mandatory = $false)]     [int]$PollIntervalMinutes = 1440,                         

    [Parameter(Mandatory = $false)]     [int]$ProcessingBatchSize = 5000,

    [Parameter(Mandatory = $false)]     [int]$DeviceLogSampleSize = 25,

    [Parameter(Mandatory = $false)]     [switch]$LargeScaleMode,     # ============================================================================     # AllowList / BlockList Parameters     # ============================================================================     # AllowList = Включати лише ці пристрої (цільове розгортання)     # BlockList = Виключити ці пристрої (вони ніколи не будуть розгорнуті)     # Порядок обробки: AllowList спочатку (якщо вказано), а потім BlockList     [Parameter(Mandatory = $false)]     [string]$AllowListPath,     [Parameter(Mandatory = $false)]     [string]$AllowADGroup,     [Parameter(Mandatory = $false)]     [string]$ExclusionListPath,     [Parameter(Mandatory = $false)]     [string]$ExcludeADGroup,     # ============================================================================     # Параметри WinCS (система конфігурації Windows)     # ============================================================================     # WinCS – це альтернатива розгортанням GPO AvailableUpdatesPolicy.                              # Вона використовує 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(Mandatory = $false)]     [switch]$UseWinCS,          [Parameter(Mandatory = $false)]     [string]$WinCSKey = "F33E0C8E002",          [Parameter(Mandatory = $false)]     [switch]$DryRun,          [Parameter(Mandatory = $false)]     [switch]$ListBlockedBuckets,          [Parameter(Mandatory = $false)]     [string]$UnblockBucket,          [Parameter(Mandatory = $false)]     [switch]$UnblockAll,          [Parameter(Mandatory = $false)]     [switch]$EnableTaskOnDisabled )

$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Зразки розгортання та моніторингу"

# ============================================================================ # ПЕРЕВІРКА ЗАЛЕЖНОСТІ # ============================================================================

function Test-ScriptDependencies {     param(         [Parameter(Mandatory = $true)]         [string]$ScriptDirectory,         [Parameter(Mandatory = $true)]         [string[]]$RequiredScripts     )     $missingScripts = @()     foreach ($script in $RequiredScripts) {         $scriptPath = Join-Path $ScriptDirectory $script         якщо (-not (test-path $scriptPath)) {             $missingScripts += $script         } (})     } (})     якщо ($missingScripts.Count -gt 0) {         Write-Host ""         Write-Host ("=" * 70) -ForegroundColor Red         Write-Host "ВІДСУТНІ ЗАЛЕЖНОСТІ" - Червоний колір переднього плану         Write-Host ("=" * 70) - Червоний колір переднього плану         Write-Host ""         Write-Host "Не знайдено такі необхідні сценарії:" -ForegroundColor Yellow         foreach ($script in $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 is required". -ForegroundColor Red     вихід 1 }

if (-not $isAdminCommand -and -not $AggregationInputPath) {     Write-Host "ERROR: -AggregationInputPath потрібен для розгортання (не потрібен для -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red     вихід 1 }

# ============================================================================ # GPO DETECTION - CHECK FOR DETECTION GPO # ============================================================================

if (-not $isAdminCommand -and -not $DryRun) {     $CollectionGPOName = "SecureBoot-EventCollection"     # Перевірка доступності модуля GroupPolicy     якщо (Get-Module -ListAvailable -Name GroupPolicy) {         Import-Module GroupPolicy – errorAction SilentlyContinue         Write-Host "Перевірка наявності об'єктної об'єктної інформації..." -ForegroundColor Yellow         спробуйте {             # Перевірка наявності об'єктних об'єктів             $existingGpo = Get-GPO - Name $CollectionGPOName -ErrorAction SilentlyContinue             якщо ($existingGpo) {                 Write-Host "Знайдено об'єкт групової перевірки виявлення: $CollectionGPOName" -ForegroundColor Green             } інакше {                 Write-Host ""                 Write-Host ("=" * 70) -ForegroundColor Yellow                 Write-Host " ПОПЕРЕДЖЕННЯ: ВИЯВЛЕННЯ GPO НЕ ЗНАЙДЕНО" -ForegroundColor Yellow                 Write-Host ("=" * 70) -ForegroundColor Yellow                 Write-Host ""                 Write-Host "Виявлення GPO "$CollectionGPOName" не знайдено." -ForegroundColor Yellow                 Write-Host "Без цієї GPO дані пристрою не збиратимуться". -ForegroundColor Yellow                 Write-Host ""                 Write-Host "Щоб розгорнути GPO виявлення, запустіть:" -ForegroundColor Cyan                 Write-Host " .\Deploy-GPO-SecureBootCollection.ps1 -DomainName <домен> -AutoDetectOU" -ForegroundColor White                 Write-Host ""                 Write-Host "Усе одно продовжити?                                     (Y/N)" -ForegroundColor Yellow                 $response = читання хоста                 якщо ($response -notmatch '^[Yy]') {                     Write-Host "Перервано. Спочатку розгорніть GPO виявлення." -ForegroundColor Red                     вихід 1                 } (})             } (})         } зловити {             Write-Host " Не вдалося перевірити об'єкт групової перевірки: $($_. Exception.Message)" -ForegroundColor Yellow         } (})     } інакше {         Write-Host " Модуль GroupPolicy недоступний – пропуск перевірки GPO" -ForegroundColor Gray     } (})     Write-Host "" }

# ============================================================================ # ШЛЯХ ДО ФАЙЛУ СТАНУ # ============================================================================

$stateDir = Join-Path $ReportBasePath "RolloutState" якщо (-not (test-path $stateDir)) {     New-Item -ItemType Directory – шлях $stateDir -Force | Out-Null }

$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 COMPATIBILITY: ConvertTo-Hashtable # ============================================================================ # ConvertFrom-Json -AsHashtable тільки PS7+ Це забезпечує сумісність.

function ConvertTo-Hashtable {     param(         [Parameter(ValueFromPipeline = $true)]         $InputObject     )     процес {         якщо ($null -eq $InputObject) { return @{} }         якщо ($InputObject -is [System.Collections.IDictionary]) { return $InputObject }         якщо ($InputObject -це [PSCustomObject]) {             # Використовувати [замовлено] для узгодженого впорядкування ключів і безпечної обробки дублікатів             $hash = [ordered]@{}             foreach ($prop in $InputObject.PSObject.Properties) {                 # Індексовані призначення безпечно обробляють повторювані записи шляхом перезаписування                 $hash[$prop. Name] = ConvertTo-Hashtable $prop. Значення             } (})             повернути $hash         } (})         якщо ($InputObject -це [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {             return @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ })         } (})         повернути $InputObject     } (}) }

# ============================================================================ # КОМАНДИ АДМІНІСТРУВАННЯ: список/розблокування блоків # ============================================================================

if ($ListBlockedBuckets) {     Write-Host ""     Write-Host ("=" * 80) -ForegroundColor Yellow     Write-Host " ЗАБЛОКОВАНІ БЛОКИ" -ForegroundColor Yellow     Write-Host ("=" * 80) -ForegroundColor Yellow     Write-Host ""     якщо ($blockedBucketsPath тестового шляху) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю         якщо ($blocked. Count -eq 0) {             Write-Host "Немає заблокованих блоків"." -ForegroundColor Green         } інакше {             Write-Host "Усього заблоковано: $($blocked. Count)" -ForegroundColor Red             Write-Host ""             ($key в $blocked. Клавіші) {                 $info = $blocked[$key]                 Write-Host "Блок: $key" -ForegroundColor Red                 Write-Host " Заблоковано: $($info. BlockedAt)" -ForegroundColor Gray                 Write-Host " Причина: $($info. Reason)" -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 ""     якщо ($blockedBucketsPath тестового шляху) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю         якщо ($blocked. Contains($UnblockBucket)) {             $blocked. Remove($UnblockBucket)             $blocked | ConvertTo-Json -Глибина 10 | Out-File $blockedBucketsPath -Кодування UTF8 -Force             # Додати до затвердженого адміністратором списку, щоб запобігти повторному блокуванню             $adminApproved = @{}             якщо ($adminApprovedPath тестового шляху) {                 $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю             } (})             $adminApproved[$UnblockBucket] = @{                 ApprovedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"                 ApprovedBy = $env:USERNAME             } (})             $adminApproved | ConvertTo-Json -Глибина 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 ""     якщо ($blockedBucketsPath тестового шляху) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю         $count = $blocked. Розраховувати         @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Кодування UTF8 -Force         Write-Host "Розблокувати всі блоки $count". -ForegroundColor Green     } інакше {         Write-Host "Файл заблокованих блоків не знайдено".-ForegroundColor Yellow     } (})     Write-Host ""     вихід 0 }

# ============================================================================ # HELPER FUNCTIONS # ============================================================================

function Get-RolloutState {     якщо ($rolloutStatePath тестового шляху) {         спробуйте {             $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю             # Перевірка наявних обов'язкових властивостей             якщо ($null -eq $loaded. CurrentWave) {                 throw "Invalid state file – missing CurrentWave"             } (})             # Переконайтеся, що WaveHistory завжди є масивом (виправлення десеріалізації PS5.1 JSON)             якщо ($null -eq $loaded. WaveHistory) {                 $loaded. WaveHistory = @()             } elseif ($loaded. WaveHistory - isnot [масив]) {                 $loaded. WaveHistory = @($loaded. WaveHistory)             } (})             повернути $loaded         } зловити {             Write-Log "Виявлено пошкоджений RolloutState.json: $($_. Exception.Message)" "WARN"             Write-Log "Резервне копіювання пошкодженого файлу та створення свіжого файлу" "WARN"             $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyyMdd-HHmmss')"             Move-Item $rolloutStatePath $backupPath -Force - ErrorAction SilentlyContinue         } (})     } (})     повернути @{         CurrentWave = 0         StartedAt = $null         Остання агрегація = $null         TotalDevicesTargeted = 0         TotalDevicesUpdated = 0         Стан = "Непочато"         WaveHistory = @()     } (}) }

function Save-RolloutState {     param($State)     $State | ConvertTo-Json -Глибина 10 | Out-File $rolloutStatePath -Кодування UTF8 -Force }

function Get-WeekdayProjection {     <#     . ПІДСУМОК         Обчислити прогнозовану дату завершення з урахування вихідних (без перебігу виконання в СБ/Нд)     #>     param(         [int]$RemainingDevices,         [double]$DevicesPerDay,         [datetime]$StartDate = (Get-Date)     )     якщо ($DevicesPerDay -le 0 -або $RemainingDevices -le 0) {         повернути @{             ProjectedDate = $null             WorkingDaysNeeded = 0             CalendarDaysNeeded = 0         } (})     } (})     # Обчислення необхідних робочих днів (за винятком вихідних)     $workingDaysNeeded = [math]::Ceiling($RemainingDevices / $DevicesPerDay)     # Перетворення робочих днів на календарні дні (додавання вихідних)     $currentDate = $StartDate.Date     $daysAdded = 0     $workingDaysAdded = 0     в той час як ($workingDaysAdded -lt $workingDaysNeeded) {         $currentDate = $currentDate.AddDays(1)         $daysAdded++         # Лише кількість робочих днів         якщо ($currentDate.DayOfWeek -ne [DayOfWeek]::Saturday -and             $currentDate.DayOfWeek -ne [DayOfWeek]::Sunday) {             $workingDaysAdded++         } (})     } (})     повернути @{         ProjectedDate = $currentDate.ToString("yyyy-MM-dd")         WorkingDaysNeeded = $workingDaysNeeded         CalendarDaysNeeded = $daysAdded     } (}) }                                  

function Save-RolloutSummary {     <#     . ПІДСУМОК         Збереження зведення розгортання з відомостями про проекції для відображення приладної дошки     #>     param(         [hashtable]$State,         [int]$TotalDevices,         [int]$UpdatedDevices,         [int]$NotUpdatedDevices,         [double]$DevicesPerDay     )     $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     # Обчислення прогнозу у вихідні дні     $projection = Get-WeekdayProjection -RemainingDevices $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) } інакше { 0 }         # Показники швидкості         DevicesPerDay = [math]::Round($DevicesPerDay, 1)         TotalDevicesTargeted = $State.TotalDevicesTargeted         TotalWaves = $State.CurrentWave         # Проектування у вихідні дні         ProjectedCompletionDate = $projection. Дата проекту         WorkingDaysRemaining = $projection. Робочий день не заплановано         CalendarDaysRemaining = $projection. Календарне завершення дня         # Примітка про виключення вихідних         ProjectionNote = "Прогнозоване завершення виключає вихідні (Сб/Нд)"     } (})     $summary | ConvertTo-Json -Глибина 5 | Out-File $summaryPath -Кодування UTF8 -Force     Write-Log "Зведення про розгортання збережено: $summaryPath" "ВІДОМОСТІ"     повернути $summary }                                                             

function Get-BlockedBuckets {     якщо ($blockedBucketsPath "Тестовий шлях") {         повернення Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю     } (})     повернути @{} }

function Save-BlockedBuckets {     param($Blocked)     $Blocked | ConvertTo-Json -Глибина 10 | Out-File $blockedBucketsPath -Кодування UTF8 -Force }

function Get-AdminApproved {     якщо ($adminApprovedPath тестового шляху) {         повернення Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю     } (})     повернути @{} }

function Get-DeviceHistory {     якщо ($deviceHistoryPath тестового шляху) {         повернення Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | Перетворити на геш-таблицю     } (})     повернути @{} }

function Save-DeviceHistory {     param($History)     $History | ConvertTo-Json -Глибина 10 | Out-File $deviceHistoryPath -Кодування UTF8 -Force }

function Save-ProcessingCheckpoint {     param(         [string]$Stage,         [int]$Processed,         [int]$Total,         [hashtable]$Metrics = @{}     )

    $checkpoint = @{         Етап = $Stage         UpdatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"         Оброблено = $Processed         Усього = $Total         Відсоток = якщо ($Total -gt 0) { [математика]::Round(($Processed / $Total) * 100, 2) } інакше { 0 }         Показники = $Metrics     }

    $checkpoint | ConvertTo-Json -Depth 6 | Out-File $processingCheckpointPath -Encoding UTF8 -Force }

function Get-NotUpdatedIndexes {     param([масив]$Devices)

    $hostSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)     $bucketCounts = @{}

    foreach ($device in $Devices) {         $hostname = якщо ($device. Hostname (Ім'я хоста) { $device. Hostname } elseif ($device. HostName( Ім'я хоста) { $device. HostName } інакше { $null }         якщо ($hostname) {             [void]$hostSet.Add($hostname)         }

        $bucketKey = Get-BucketKey $device         якщо ($bucketKey) {             якщо ($bucketCounts.ContainsKey($bucketKey)) {                 $bucketCounts[$bucketKey]++             } інакше {                 $bucketCounts[$bucketKey] = 1             } (})         } (})     }

    return @{         HostSet = $hostSet         Кількість блоків = $bucketCounts     } (}) }

function Write-Log {     param([рядок]$Message;[рядок]$Level = "INFO")     $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"     $color = перемикач ($Level) {         "OK" { "Зелений" }         "WARN" { "Жовтий" }         "ERROR" { "Red" }         "ЗАБЛОКОВАНО" { "Темний" }         "WAVE" { "Блакитний" }         за замовчуванням { "Білий" }     } (})     Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color     # Також увійдіть у файл     $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMdd').log"     "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Encoding UTF8 }               

function Get-BucketKey {     param($Device)     # Використовуйте BucketId із пристрою JSON, якщо він доступний (геш-код SHA256 зі сценарію виявлення)     якщо ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" }     # Резервний варіант: конструкція від виробника|model|bios     $mfr = якщо ($Device.WMI_Manufacturer) { $Device.WMI_Manufacturer } ще { $Device.Manufacturer }     $model = якщо ($Device.WMI_Model) { $Device.WMI_Model } інакше { $Device.Model }     $bios = якщо ($Device.BIOSDescription) { $Device.BIOSDescription } інакше { $Device.BIOS }     повернути "$mfr|$model|$bios" }

# ============================================================================ # ЗАВАНТАЖЕННЯ СПИСКУ VIP/ВИНЯТКІВ # ============================================================================

function Get-ExcludedHostnames {     param(         [string]$ExclusionFilePath,         [string]$ADGroupName     )     $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)     # Завантажити з файлу (підтримує .txt або .csv)     якщо ($ExclusionFilePath -and (test-path $ExclusionFilePath)) {         $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower()         якщо ($extension -eq ".csv") {             Формат # CSV: очікується стовпець "Ім'я хоста" або "Ім'я комп'ютера"             $csvData = Import-Csv $ExclusionFilePath             $hostCol = якщо ($csvData[0]. PSObject.Properties.Name -містить "Ім'я хоста") { "Ім'я хоста" }                        elseif ($csvData[0]. PSObject.Properties.Name -містить "Ім'я комп'ютера") { "Ім'я комп'ютера" }                        elseif ($csvData[0]. PSObject.Properties.Name -містить "Ім'я") { "Ім'я" }                        інакше { $null }             якщо ($hostCol) {                 foreach ($row in $csvData) {                     якщо (![ рядок]::IsNullOrWhiteSpace($row.$hostCol)) {                         [void]$excluded. Add($row.$hostCol.Trim())                     } (})                 } (})             } (})         } інакше {             # Звичайний текст: одне ім'я хоста в рядку             Get-Content $ExclusionFilePath | ForEach-Object {                 $line = $_. Trim()                 якщо ($line -and -not $line. StartsWith('#')) {                     [void]$excluded. Add($line)                 } (})             } (})         } (})         Write-Log "Завантажено $($excluded. Кількість) імен хостів із файлу винятків: $ExclusionFilePath" "INFO"     } (})     # Завантажити з групи безпеки AD     якщо ($ADGroupName) {         спробуйте {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'комп'ютер' }             foreach ($member in $groupMembers) {                 [void]$excluded. Add($member. Ім'я)             } (})             Write-Log "Loaded $($groupMembers.Count) computers from AD group: $ADGroupName" "INFO"         } зловити {             Write-Log "Не вдалося завантажити групу AD "$ADGroupName": $_" "WARN"         } (})     } (})     return @($excluded) }                                                                             

# ============================================================================ # ДОЗВОЛИТИ ЗАВАНТАЖЕННЯ СПИСКУ (цільове розгортання) # ============================================================================

function Get-AllowedHostnames {     <#     . ПІДСУМОК         Завантажує імена хостів із файлу AllowList і/або групи AD для цільового розгортання.Якщо вказано Параметр AllowList, лише ці пристрої буде включено до розгортання.#>     param(         [string]$AllowFilePath,         [string]$ADGroupName     )          $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)          # Завантажити з файлу (підтримує .txt або .csv)     якщо ($AllowFilePath -and (test-Path $AllowFilePath)) {         $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower()                  якщо ($extension -eq ".csv") {             Формат # CSV: очікується стовпець "Ім'я хоста" або "Ім'я комп'ютера"             $csvData = Import-Csv $AllowFilePath             якщо ($csvData.Count -gt 0) {                 $hostCol = якщо ($csvData[0]. PSObject.Properties.Name -містить "Ім'я хоста") { "Ім'я хоста" }                            elseif ($csvData[0]. PSObject.Properties.Name -містить "Ім'я комп'ютера") { "Ім'я комп'ютера" }                            elseif ($csvData[0]. PSObject.Properties.Name -містить "Ім'я") { "Ім'я" }                            інакше { $null }                                  якщо ($hostCol) {                     foreach ($row in $csvData) {                         якщо (![ рядок]::IsNullOrWhiteSpace($row.$hostCol)) {                             [void]$allowed. Add($row.$hostCol.Trim())                         } (})                     } (})                 } (})             } (})         } інакше {             # Звичайний текст: одне ім'я хоста в рядку             Get-Content $AllowFilePath | ForEach-Object {                 $line = $_. Trim()                 якщо ($line -and -not $line. StartsWith('#')) {                     [void]$allowed. Add($line)                 } (})             } (})         } (})                  Write-Log "Завантажено $($allowed. Кількість) імен хостів із файлу списку дозволених: $AllowFilePath" "INFO"     } (})          # Завантажити з групи безпеки AD     якщо ($ADGroupName) {         спробуйте {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'комп'ютер' }                          foreach ($member in $groupMembers) {                 [void]$allowed. Add($member. Ім'я)             } (})                          Write-Log "Loaded $($groupMembers.Count) computers from AD allow group: $ADGroupName" "INFO"         } зловити {             Write-Log "Не вдалося завантажити групу AD "$ADGroupName": $_" "WARN"         } (})     } (})          return @($allowed) }

# ============================================================================ # АКТУАЛЬНІСТЬ І МОНІТОРИНГ ДАНИХ # ============================================================================

function Get-DataFreshness {     <#     . ПІДСУМОК         Перевіряє актуальність даних виявлення, вивчивши позначки часу файлу JSON.Повертає статистику про час останнього повідомлення про кінцеві точки.#>     param([рядок]$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 in $jsonFiles) {         $ageHours = ($now - $file. Час останнього записування). Загальна кількість         $ages += $ageHours         якщо ($ageHours -le $freshThresholdHours) {             $fresh++         } elseif ($ageHours -ge $staleThresholdHours) {             $stale++         } (})     } (})     $oldestFile = $jsonFiles | Sort-Object Час останнього записування | Select-Object –Перший 1     $newestFile = $jsonFiles | Sort-Object LastWriteTime – за спаданням | Select-Object -Перший 1     $warning = $null     якщо ($stale -gt ($jsonFiles.Count * 0,5)) {         $warning = "Більше 50% пристроїв мають застарілі дані (>72 години) - перевірка виявлення GPO"     } 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([рядок]$JsonPath)     # Перевірка 1. Шлях JSON існує     якщо (-not (test-path $JsonPath)) {         повернути @{             IsDeployed = $false             Message = "Шлях вводу JSON не існує: $JsonPath"         } (})     } (})     # Перевірка 2. Існує принаймні кілька файлів JSON     $jsonCount = (Get-ChildItem – шлях $JsonPath -filter "*.json" -ErrorAction SilentlyContinue). Розраховувати     якщо ($jsonCount -eq 0) {         повернути @{             IsDeployed = $false             Message = "No JSON files in $JsonPath - Detection GPO may not be deployed or devices haven't reported yet"         } (})     } (})     # Перевірка 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             Message = "No JSON files updated in last 7 days - Detection GPO may be broken or devices offline"         } (})     } (})     повернути @{         IsDeployed = $true         Message = "Виявлення відображається активним: $jsonCount файли, $($recentFiles.Count) оновлено нещодавно"         FileCount = $jsonCount         RecentCount = $recentFiles.Count     } (}) }                         

# ============================================================================ # ВІДСТЕЖЕННЯ ПРИСТРОЇВ (ЗА ІМЕНЕМ ХОСТА) # ============================================================================

function Update-DeviceHistory {     <#     . ПІДСУМОК         Відстежує пристрої за іменем хоста, оскільки в нас немає унікального ідентифікатора комп'ютера.Примітка. BucketId – це "один-до-багатьох" (така ж конфігурація обладнання = той самий блок).Якщо до колекції JSON додано унікальний ідентифікатор, оновіть цю функцію.#>     param(         [масив]$CurrentDevices,         [hashtable]$DeviceHistory     )          foreach ($device in $CurrentDevices) {         $hostname = $device. Ім'я хоста         якщо (-не $hostname) { continue }                  # Відстеження пристрою за іменем хоста         $DeviceHistory[$hostname] = @{             Hostname = $hostname             BucketId = $device. Код сегмента             Виробник = $device. WMI_Manufacturer             Model = $device. WMI_Model             LastSeen = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             Стан = $device. Оновити стан         } (})     } (}) }

# ============================================================================ # ВИЯВЛЕННЯ БЛОК-БЛОКІВ (на основі можливості доступу до пристрою) # ============================================================================

<# . ОПИС     Логіка блокування:     – Блок блокується, лише якщо:       1. Пристрій було націлено хвилею       2. MaxWaitHours минув з початку хвилі       3. Пристрій НЕДОСТУПНИЙ (помилка ping)          - Якщо пристрій доступний, але ще не оновлений, ми продовжуємо чекати       (оновлення може бути відкладено на перезавантаження – подія 1808 запускається лише після перезавантаження)          - Пристрій, який недоступний, вказує на те, що сталася помилка, і потребує розслідування          Розблокування:     - Використання -ListBlockedBuckets для перегляду заблокованих блоків     - Щоб розблокувати певний блок, скористайтеся програмою -UnblockBucket "BucketKey"     - Використайте функцію -UnblockAll, щоб розблокувати всі блоки #>

function Test-DeviceReachable {     param(         [string]$Hostname,         [string]$DataPath # Шлях до файлів JSON пристрою     )     # Метод 1. Перевірте позначку часу файлу JSON (найшвидший – не потрібно аналізувати файли)     # Якщо сценарій виявлення запущено нещодавно, файл був записаний/оновлений, довівши, що пристрій живий     якщо ($DataPath) {         $deviceFile = Get-ChildItem -Шлях $DataPath -Фільтр "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object –Перший 1         якщо ($deviceFile) {             $hoursSinceWrite = ((Get-Date) – $deviceFile.LastWriteTime). Загальна кількість             якщо ($hoursSinceWrite -lt 72) { return $true }         } (})     } (})     # Метод 2. Зворотний зв'язок (лише якщо JSON застарілий або відсутній)     спробуйте {         $ping = Test-Connection -Ім'я комп'ютера $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue         повернути $ping     } зловити {         повернути $false     } (}) }          

function Update-BlockedBuckets {     param(         $RolloutState,         $BlockedBuckets,         $AdminApproved,         [масив]$NotUpdatedDevices,         [hashtable]$NotUpdatedIndexes,         [int]$MaxWaitHours,         [bool]$DryRun = $false     )     $now = Get-Date     $newlyBlocked = @()     $stillWaiting = @()     $devicesToCheck = @()     $hostSet = якщо ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). Набір хостів }     $bucketCounts = якщо ($NotUpdatedIndexes -and $NotUpdatedIndexes.BucketCounts) { $NotUpdatedIndexes.BucketCounts } інакше { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). Кількість блоків }     # Зберіть пристрої, які минули протягом періоду очікування, але все одно не оновлюються     foreach ($wave in $RolloutState.WaveHistory) {         якщо (-не $wave. StartedAt) { continue }         $waveStart = [DateTime]::P arse($wave. StartedAt( Початок роботи)         $hoursSinceWave = ($now - $waveStart). Загальна кількість         якщо ($hoursSinceWave -lt $MaxWaitHours) {             # Ще протягом періоду очікування - не перевіряйте ще             Продовжити         } (})         # Перевірте кожен пристрій із цієї хвилі         ($deviceInfo у $wave. Пристрої) {             $hostname = $deviceInfo.Ім'я хоста             $bucketKey = $deviceInfo.BucketKey             # Пропустити, якщо блок уже заблоковано             якщо ($BlockedBuckets.Contains($bucketKey)) { continue }             # Пропустити, якщо блок затверджено адміністратором І хвиля почалася перед затвердженням             # (перевірте лише пристрої, націлені після затвердження адміністратором, для повторного блокування)             якщо ($AdminApproved -та $AdminApproved.Contains($bucketKey)) {                 $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. ApprovedAt)                 якщо ($waveStart -lt $approvalTime) {                     # Цей пристрій був націлений до затвердження адміністратором - пропустити                     Продовжити                 } (})                 # Wave started after approval - this is fresh targeting, can check             } (})             # Цей пристрій досі перебуває в списку NotUpdated?             якщо ($hostSet.Contains($hostname)) {                 $devicesToCheck += @{                     Hostname = $hostname                     Ключ сегмента = $bucketKey                     WaveNumber = $wave. Номер хвилі                     HoursSinceWave = [math]::Round($hoursSinceWave, 1)                 } (})             } (})         } (})     } (})     якщо ($devicesToCheck.Count -eq 0) {         повернути $newlyBlocked     } (})     Write-Log "Перевірка доступності пристроїв $($devicesToCheck.Count) за минулий період очікування..." "ВІДОМОСТІ"     # Відстеження помилок у сегменті для прийняття рішень     $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() }     # Перевірка доступності кожного пристрою     foreach ($device in $devicesToCheck) {         $hostname = $device. Ім'я хоста         $bucketKey = $device. Ключ сегмента         якщо ($DryRun) {             Write-Log "[DRYRUN] Перевірить $hostname досяжність" "INFO"             Продовжити         } (})         якщо (-не $bucketFailures.ContainsKey($bucketKey)) {             $bucketFailures[$bucketKey] = @{ Недоступно = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. Автозбереження }         } (})         $isReachable = Test-DeviceReachable –ім'я хоста $hostname -DataPath $AggregationInputPath         якщо (-не $isReachable) {             $bucketFailures[$bucketKey]. Недоступні += $hostname         } інакше {             # Пристрій доступний, але ще не оновлюється – це може бути тимчасова помилка або очікування перезавантаження             $bucketFailures[$bucketKey]. AliveButFailed += $hostname             $hostname $stillWaiting +=         } (})     } (})     # Рішення для кожного блоку: блокування, лише якщо пристрої дійсно недоступні     # Живі пристрої з помилками = тимчасові, продовжити розгортання     foreach ($bucketKey in $bucketFailures.Keys) {         $bf = $bucketFailures[$bucketKey]         $unreachableCount = $bf. Недоступно.Кількість         $aliveFailedCount = $bf. AliveButFailed.Count         # Переконайтеся, що цей сегмент має будь-які успіхи (з оновлених даних пристроїв)         $bucketHasSuccesses = $stSuccessBuckets -and $stSuccessBuckets.Contains($bucketKey)         якщо ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) {             # Усі пристрої з помилками недоступні – заблокуйте блок             якщо ($newlyBlocked -notcontains $bucketKey) {                 $BlockedBuckets[$bucketKey] = @{                     BlockedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"                     Reason = "All $unreachableCount device(s) unreachable after $($bf. HoursSinceWave) hours"                     FailedDevices = ($bf. Недоступний -join ", ")                     WaveNumber = $bf. Номер хвилі                     DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } інакше { 0 }                 } (})                 $newlyBlocked += $bucketKey                 Write-Log "БЛОК ЗАБЛОКОВАНО: $bucketKey ($unreachableCount пристрої) недоступні: $($bf. Unreachable -join ', '))" "BLOCKED"             } (})         } elseif ($aliveFailedCount -gt 0) {             # Пристрої живі, але не оновлюються - тимчасова помилка, DO NOT block             Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length))))...: $aliveFailedCount пристроїв живими, але відкладеними, $unreachableCount недоступними – NOT blocking (тимчасовий)" "INFO"             якщо ($unreachableCount -gt 0) {                 Write-Log " Недоступно: $($bf. Unreachable -join ', ')" "WARN"             } (})             Write-Log " Живий, але очікується: $ ($bf. AliveButFailed -join ', ')" "INFO"             # Відстеження кількості помилок у стані розгортання для моніторингу             якщо (-не $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: Unblock buckets when devices update successfully # ============================================================================

function Update-AutoUnblockedBuckets {     <#     . ОПИС         Перевіряє, чи оновлено пристрої в заблокованих блоках (подія 1808).         Автоматичне розблокування, якщо всі цільові пристрої в блоці оновлено.Якщо оновлено лише деякі пристрої, сповіщає адміністратора, який може розблокувати його вручну.                  Admin можна розблокувати вручну за допомогою:           .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "шлях" -UnblockBucket "BucketKey"     #>     param(         $BlockedBuckets,         $RolloutState,         [масив]$NotUpdatedDevices,         [string]$ReportBasePath,         [hashtable]$NotUpdatedIndexes,         [int]$LogSampleSize = 25     )     $autoUnblocked = @()     $bucketsToCheck = @($BlockedBuckets.Keys)     $hostSet = якщо ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). Набір хостів }     foreach ($bucketKey in $bucketsToCheck) {         $bucketInfo = $BlockedBuckets[$bucketKey]         # Отримати всі пристрої, на які ми націлилися, з цього блоку історично         $targetedDevicesInBucket = @()         foreach ($wave in $RolloutState.WaveHistory) {             $targetedDevicesInBucket += @($wave. Пристрої | Where-Object { $_. BucketKey –eq $bucketKey })         } (})         якщо ($targetedDevicesInBucket.Count -eq 0) { continue }         # Перевірте кількість цільових пристроїв, які ще зберігаються в засобі NotUpdated або оновлено         $updatedDevices = @()         $stillPendingDevices = @()         foreach ($targetedDevice in $targetedDevicesInBucket) {             якщо ($hostSet.Contains($targetedDevice.Hostname)) {                 $stillPendingDevices += $targetedDevice.Ім'я хоста             } інакше {                 $updatedDevices += $targetedDevice.Ім'я хоста             } (})         } (})         якщо ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) {             # Усі цільові пристрої оновлено – автоматичне розблокування!             $BlockedBuckets.Remove($bucketKey)             $autoUnblocked += @{                 Ключ сегмента = $bucketKey                 UpdatedDevices = $updatedDevices                 PreviouslyBlockedAt = $bucketInfo.BlockedAt                 Reason = "All $($updatedDevices.Count) цільові пристрої успішно оновлено"             } (})             Write-Log "АВТОМАТИЧНЕ РОЗБЛОКУВАННЯ: $bucketKey (усі цільові пристрої $($updatedDevices.Count) успішно оновлено)" "OK"             # Кількість хвиль OEM для цього блоку OEM (відстеження за виробником оригінального обладнання)             $bucketOEM = якщо ($bucketKey -match "\|") { ($bucketKey -split "\|")[0] } інакше { "Невідомо" } # Видобути OEM з ключа з роздільниками труб або за замовчуванням             якщо (-не $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             } (})             $currentWave = якщо ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } інакше { 0 }             $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1             Write-Log " OEM "$bucketOEM" wave count incremented to $($currentWave + 1) (наступне виділення: $([int][Математика]::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 "========== ЧАСТКОВЕ ОНОВЛЕННЯ В ЗАБЛОКОВАНОМУ БЛОЦІ ==========" "ВІДОМОСТІ"                 Write-Log "Блок: $bucketKey" "ВІДОМОСТІ"                 $updatedSample = @($updatedDevices | Select-Object -перший $LogSampleSize)                 $pendingSample = @($stillPendingDevices | Select-Object -перший $LogSampleSize)                 $updatedSuffix = якщо ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count – $LogSampleSize) більше)" } інакше { "" }                 $pendingSuffix = якщо ($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"" -UnblockBucket ""$bucketKey""" "INFO"                 Write-Log "=======================================================" "INFO"                 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 "*Блоки*" } |          Sort-Object LastWriteTime – за спаданням |          Select-Object –Перший 1     якщо (-не $notUptodateCsv) {         Write-Log "Не знайдено CSV-файл notUptodate" "ERROR"         повернути $null     } (})     $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName)     # Normalize HostName -> Hostname for consistency (CSV використовує HostName, code uses Hostname)     foreach ($device in $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 = цільове розгортання – розглядатимуться лише ці пристрої     якщо ($AllowedHostnames.Count -gt 0) {         $beforeCount = $eligibleDevices.Count         $eligibleDevices = @($eligibleDevices | Where-Object {             $_. Hostname –in $AllowedHostnames         })         $allowedCount = $eligibleDevices.Count         Write-Log "AllowList applied: $allowedCount $beforeCount devices are in allow list" "INFO"     } (})     # Відфільтруйте VIP/виключені пристрої (BlockList)     #BlockList is applied AFTER AllowList     якщо ($ExcludedHostnames.Count -gt 0) {         $beforeCount = $eligibleDevices.Count         $eligibleDevices = @($eligibleDevices | Where-Object {             $_. Hostname -notin $ExcludedHostnames         })         $excludedCount = $beforeCount - $eligibleDevices.Count         якщо ($excludedCount -gt 0) {             Write-Log "Вилучені $excludedCount VIP/захищені пристрої з розгортання" "INFO"         } (})     } (})     якщо ($eligibleDevices.Count -eq 0) {         Write-Log "Пристрої, які відповідають вимогам, не залишилися (усі оновлено або заблоковано)" "OK"         повернути $null     } (})     # Отримати пристрої вже в розгортанні (з попередніх хвиль)     $devicesAlreadyInRollout = @()     якщо ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) {         $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object {             $_. Пристрої | ForEach-Object { $_. Hostname }         } | Where-Object { $_ })     } (})     Write-Log "Пристрої, які вже в розгортанні: $($devicesAlreadyInRollout.Count)" "INFO"     # Відокремлювати за довірчим рівнем     $highConfidenceDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel – eq "Висока впевненість" - і         $_. Hostname -notin $devicesAlreadyInRollout     })     # Обов'язкова дія включає:     # - Явна "Потрібна дія"     # - Empty/null ConfidenceLevel     # - Будь-яке невідоме або нерозпізнане значення ConfidenceLevel (вважається обов'язковою дією)     $knownSafeCategories = @(         "Висока впевненість",         "Тимчасово призупинено",         "Під спостереженням",         "За спостереженням – потрібно більше даних",         "Не підтримується",         "Не підтримується – відоме обмеження"     )     $actionRequiredDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel – notin $knownSafeCategories -and         $_. Hostname -notin $devicesAlreadyInRollout     })     Write-Log "Висока впевненість (не в розгортанні): $($highConfidenceDevices.Count)" "INFO"     Write-Log "Потрібна дія (не в розгортанні): $($actionRequiredDevices.Count)" "INFO"     # Створення хвильових пристроїв     $waveDevices = @()     # HIGH CONFIDENCE: Include ALL (безпечний для розгортання)     якщо ($highConfidenceDevices.Count -gt 0) {         Write-Log "Adding all $($highConfidenceDevices.Count) High Confidence devices" "WAVE"         $highConfidenceDevices $waveDevices +=     } (}) # ПОТРІБНА ДІЯ: прогресивне розгортання (блок на основі OEM-поширення для блоків нульового успіху)     # Стратегія:     # - Блоки з 0 успіхами: Розподіл по виробника оригінального обладнання (1 на OEM -> 2 на OEM -> 4 на OEM)     # - Блоки з ≥1 успіху: подвійний вільно без обмеження OEM     якщо ($actionRequiredDevices.Count -gt 0) {         # Кількість успішних кроків завантаження сегмента з оновлених пристроїв CSV (пристроїв, які успішно оновлено)         $updatedCsv = Get-ChildItem -Шлях $AggregationPath -Фільтр "*updated_devices*.csv" |             Sort-Object LastWriteTime – за спаданням | Select-Object –Перший 1         $bucketStats = @{}         якщо ($updatedCsv) {             $updatedDevices = Import-Csv $updatedCsv.FullName             # Кількість успішних успіхів на BucketId             $updatedDevices | ForEach-Object {                 $key = Get-BucketKey $_                 якщо ($key) {                     якщо (-не $bucketStats.ContainsKey($key)) {                         $bucketStats[$key] = @{ Successes = 0; Очікування = 0; Всього = 0 }                     } (})                     $bucketStats[$key]. Успіхи++                     $bucketStats[$key]. Усього++                 } (})             } (})             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 = якщо ($_. BucketId) { $_. BucketId } інакше { "$($_. Виробник)|$($_. Модель)|$($_. BIOS)" }                     $bucketStats[$key] = @{                         Successes = [int]$_. Успіхи                         Очікування = [int]$_. Очікуванні                         Усього = [int]$_. TotalDevices                     } (})                 } (})             } (})         } (})         # Group NotUpdated devices by bucket (Виробник|Модель|BIOS)         $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ }         # Окремі блоки: нульовий успіх проти має успіх         $zeroSuccessBuckets = @()         $hasSuccessBuckets = @()         foreach ($bucket in $buckets) {             $bucketKey = $bucket. Ім'я             $bucketDevices = @($bucket. Група)             $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Hostname })             # Підрахунок успіхів у цьому блоці             $stats = $bucketStats[$bucketKey]             $successes = якщо ($stats) { $stats. Успіхи } ще { 0 }             # Знайти пристрої, розгорнуті в цьому блоці з журналу хвиль             $deployedToBucket = @()             foreach ($wave in $RolloutState.WaveHistory) {                 ($device в $wave. Пристрої) {                     якщо ($device. BucketKey – eq $bucketKey -and $device. Hostname (Ім'я хоста) {                         $deployedToBucket += $device. Ім'я хоста                     } (})                 } (})             } (})             $deployedToBucket = @($deployedToBucket | Sort-Object -Unique)             # Перевірте, чи всі розгорнуті пристрої повідомили про успішне виконання             $stillPending = @($deployedToBucket | Where-Object { $_ - у $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) (очікування)" "INFO"                 Продовжити             } (})             # Залишковий право = пристрої, які ще не розгорнуто             $devicesNotYetTargeted = @($bucketDevices | Where-Object {                 $_. Hostname -notin $deployedToBucket             })             якщо ($devicesNotYetTargeted.Count -eq 0) { continue }             # Категоризувати за кількістю успіхів             $bucketInfo = @{                 Ключ сегмента = $bucketKey                 Пристрої = $devicesNotYetTargeted                 ConfirmedSuccess = $confirmedSuccess                 Успіхи = $successes                 OEM = if ($bucket. Група[0]. WMI_Manufacturer) { $bucket. Група[0]. WMI_Manufacturer } elseif ($bucketKey -match "\|") { ($bucketKey -split "\|")[0] } інакше { "Невідомо" }             } (})             якщо ($successes -eq 0) {                 $zeroSuccessBuckets += $bucketInfo             } інакше {                 $hasSuccessBuckets += $bucketInfo             } (})         } (})         # === PROCESS HAS-SUCCESS BUCKETS (≥1 success) ===         # Подвоїть кількість успішних успіхів – якщо 14 успішно виконано, розгорніть 28 далі         foreach ($bucketInfo in $hasSuccessBuckets) {             $nextBatchSize = $bucketInfo.Успіхи * 2             $nextBatchSize = [Математика]::Min($nextBatchSize; $MaxDevicesPerWave)             $nextBatchSize = [Math]::Min($nextBatchSize, $bucketInfo.Devices.Count)             якщо ($nextBatchSize -gt 0) {                 $selectedDevices = @($bucketInfo.Пристрої | Select-Object -перший $nextBatchSize)                 $waveDevices += $selectedDevices                 $parts = якщо ($bucketInfo.BucketKey - match "\|") { $bucketInfo.BucketKey - split "\|" } інакше { @($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)" "INFO"             } (})         } (})         # === PROCESS ZERO-SUCCESS BUCKETS (spread across OEM with per-OEM tracking) ===         # Мета: розподіл ризику для різних виробників оригінального обладнання, відстеження перебігу виконання на виробника оригінального обладнання незалежно         # Кожен виробник оригінального обладнання виконується на основі власного журналу успіху:         # - OEM з успішними успіхами: отримує більше пристроїв наступної хвилі (2^waveCount)         # - OEM без успіхів: залишається на поточному рівні до підтвердження успіху         якщо ($zeroSuccessBuckets.Count -gt 0) {             # Ініціалізація кількості хвиль на OEM, якщо вона не існує             якщо (-не $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             } (})             # Групування блоків нульового успіху виробником оригінального обладнання             $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM }             $totalZeroSuccessAdded = 0             $oemsDeployedTo = @()             foreach ($oemGroup in $oemBuckets) {                 $oemName = $oemGroup.Name                 # Отримайте кількість хвиль цього виробника оригінального обладнання (починається з 0)                 $oemWaveCount = якщо ($RolloutState.OEMWaveCounts[$oemName]) {                     $RolloutState.OEMWaveCounts[$oemName]                 } ще { 0 }                 # Обчислення пристроїв для цього виробника оригінального обладнання: 2^waveCount (1, 2, 4, 8...)                 $devicesForThisOEM = [int][Математика]::P ow(2; $oemWaveCount)                 $devicesForThisOEM = [Математичний]::Макс(1; $devicesForThisOEM)                 $oemDevicesAdded = 0                 # Вибрати з кожного блоку під цим виробником оригінального обладнання                 foreach ($bucketInfo in $oemGroup.Group) {                     $remaining = $devicesForThisOEM – $oemDevicesAdded                     якщо ($remaining -le 0) { break }                     $toTake = [Математика]::Min($remaining, $bucketInfo.Devices.Count)                     якщо ($toTake -gt 0) {                         $selectedDevices = @($bucketInfo.Пристрої | Select-Object -перший $toTake)                         $waveDevices += $selectedDevices                         $oemDevicesAdded += $toTake                         $totalZeroSuccessAdded += $toTake                         $parts = якщо ($bucketInfo.BucketKey - match "\|") { $bucketInfo.BucketKey - split "\|" } інакше { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length)))) }                         $displayName = "$($parts[0]) - $($parts[1])"                         Write-Log " [ZERO-SUCCESS] $displayName – розгортання=$toTake (хвиля OEM $oemWaveCount = ${devicesForThisOEM}/OEM)" "WARN"                     } (})                 } (})                 якщо ($oemDevicesAdded -gt 0) {                     Write-Log " OEM: $oemName - Wave $oemWaveCount, Added $oemDevicesAdded devices" "INFO"                     $oemsDeployedTo += $oemName                 } (})             } (})             # Відстеження, для яких постачальників оригінального обладнання розгорнуто (для інкрементації під час наступної перевірки успіху)             якщо ($oemsDeployedTo.Count -gt 0) {                 $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo                 Write-Log "Розгортання з нульовим успіхом: $totalZeroSuccessAdded пристроїв через OEM $($oemsDeployedTo.Count) "INFO"             } (})         } (})     } (})     якщо (@($waveDevices). Count -eq 0) {         повернути $null     } (})     повернути $waveDevices }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

# ============================================================================ # GPO DEPLOYMENT (INLINED – створює GPO, групу безпеки, посилання) # ============================================================================

function Deploy-GPOForWave {     param(         [string]$GPOName,         [string]$TargetOU,         [string]$SecurityGroupName,         [масив]$WaveHostnames,         [bool]$DryRun = $false     )     # ADMX Policy: SecureBoot.admx – SecureBoot_AvailableUpdatesPolicy     # Шлях реєстру: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot     # Ім'я значення: AvailableUpdatesPolicy     # Увімкнуто значення: 22852 (0x5944) - Оновлення всіх ключів безпечного завантаження + bootmgr     # Вимкнуте значення: 0     #     # Використання параметрів Групова політика (GPP) для надійного розгортання шляху HKLM\SYSTEM     # GPP створює настройки в розділі: Конфігурація комп'ютера > Параметри > Настройки Windows > Реєстр     $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot"     $RegistryValueName = "AvailableUpdatesPolicy"     $RegistryValue = 22852 # 0x5944 – відповідає ADMX enabledValue     Write-Log "Розгортання GPO: $GPOName" "WAVE"     Write-Log "Реєстр: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X')))" "INFO"     якщо ($DryRun) {         Write-Log "[DRYRUN] Створить GPO: $GPOName" "INFO"         Write-Log "[DRYRUN] Створить групу безпеки: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] додасть $(@($WaveHostnames). Кількість) комп'ютерів для групування" "INFO"         Write-Log "[DRYRUN] Зв'яже GPO з: $TargetOU" "INFO"         повернути $true     } (})     спробуйте {         # Імпорт необхідних модулів         Import-Module GroupPolicy – зупинка дії помилки         Import-Module ActiveDirectory – зупинка дії помилки     } зловити {         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 "Розгортання сертифіката безпечного завантаження - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Створено об'єкт групової оновлення: $GPOName" "OK"         } зловити {             Write-Log "Не вдалося створити об'єкт групової політик: $($_. Exception.Message)" "ERROR"             повернути $false         } (})     } (})     # Крок 2. Установлення значення реєстру за допомогою Групова політика Preferences (GPP)     # GPP є більш надійним для шляхів HKLM\SYSTEM, ніж Set-GPRegistryValue     спробуйте {         # Спочатку спробуйте видалити будь-який наявний параметр для цього значення (щоб уникнути повторень)         Remove-GPPrefRegistryValue -Name $GPOName -Context Computer - Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue         # Створення параметра реєстру GPP за допомогою дії "Замінити"         # Replace = Create if not exists, Update if exists (most reliable)         # Update = Лише оновлення, якщо існує (не вдається, якщо значення не існує)         Set-GPPrefRegistryValue -Name $GPOName '             -Контекстний комп'ютер '             -Action Replace '             -Key $RegistryKey '             -ValueName $RegistryValueName '             -Введіть DWord '             -Значення $RegistryValue         Write-Log "Настроєно параметр реєстру GPP: $RegistryValueName = 0x5944 (Action=Replace)" "OK"     } зловити {         Write-Log "GPP failed, trying Set-GPRegistryValue: $($_. Exception.Message)" "WARN"         # Повернення до Set-GPRegistryValue (працює, якщо розгорнуто ADMX)         спробуйте {             Set-GPRegistryValue -Name $GPOName '                 -Key $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 -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Опис "Комп'ютери призначено для розгортання безпечного завантаження - $GPOName" "                 -PassThru             Write-Log "Створено групу безпеки: $SecurityGroupName" "OK"         } зловити {             Write-Log "Не вдалося створити групу безпеки: $($_. Exception.Message)" "ERROR"             повернути $false         } (})     } інакше {         Write-Log "Група безпеки існує: $SecurityGroupName" "INFO"         $group = $existingGroup     } (})     # Крок 4. Додавання комп'ютерів до групи безпеки     $added = 0     $failed = 0     foreach ($hostname in $WaveHostnames) {         спробуйте {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } зловити {             $failed++         } (})     } (})     Write-Log "Додано $added комп'ютери до групи безпеки ($failed не знайдено в AD)" "OK"     # Крок 5. Настроювання фільтрування безпеки в об'єктах групових рахунків     спробуйте {         # Видалити стандартний дозвіл "Автентифіковані користувачі" (зберегти читання)         Set-GPPermission -Name $GPOName -TargetName "Автентифіковані користувачі" -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 "GPO може застосовуватися до всіх комп'ютерів у пов'язаному підрозділу - перевірити вручну" "WARN"     } (})     # Крок 6. Зв'язування GPO з OU (КРИТИЧНО для застосування політики)     якщо ($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" "OK"                 Write-Log "GPO буде застосовано в наступному gpupdate на цільових комп'ютерах" "INFO"             } інакше {                 Write-Log "GPO вже пов'язано з цільовим підрозділу" "INFO"             } (})         } зловити {             Write-Log "CRITICAL: Failed to link GPO to OU: $($_. Exception.Message)" "ERROR"             Write-Log "GPO було створено, але НЕ ПОВ'язано - це НЕ буде застосовуватися до будь-яких комп'ютерів!" "ПОМИЛКА"             Write-Log "Потрібне виправлення вручну: New-GPLink -Name "$GPOName" -Target "$TargetOU" -LinkEnabled Yes" "ERROR"             повернути $false         } (})     } інакше {         Write-Log "ПОПЕРЕДЖЕННЯ: Не вказано TargetOU - GPO створено, але НЕ ПОВ'язано!" "ПОМИЛКА"         Write-Log "Ручне зв'язування, необхідне для набування GPO" "ERROR"         Write-Log "Виконати: New-GPLink -Name "$GPOName" - Target "<Your-Domain-DN>" -LinkEnabled Yes" "ERROR"     } (})     # Крок 7. Перевірка конфігурації GPO     Write-Log "Перевірка конфігурації GPO..." "ВІДОМОСТІ"     спробуйте {         $gpoReport = Get-GPO - Name $GPOName -ErrorAction Stop         Write-Log "Стан групової інформації: $($gpoReport.GpoStatus)" "INFO"         # Перевірте, чи налаштовано параметр реєстру         $regSettings = Get-GPRegistryValue -Name $GPOName -key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue         якщо (-не $regSettings) {             # Спробуйте перевірку реєстру GPP (інший шлях у GPO)             Write-Log "Перевірка параметрів реєстру GPP..." "ВІДОМОСТІ"         } (})     } зловити {         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 # Застосувати: WinCsFlags.exe /apply --key "F33E0C8E002" # Скидання: WinCsFlags.exe /reset --key "F33E0C8E002" # # Цей метод розгортає групову службу із запланованим завданням, яке запускається WinCsFlags.exe /apply # як СИСТЕМА для цільових кінцевих точок. Подібно до того, як розгортається сценарій виявлення, # але запускається один раз (під час запуску) замість щоденного.

function Deploy-WinCSGPOForWave {     <#     . ПІДСУМОК         Розгорніть функцію безпечного завантаження WinCS за допомогою запланованого завдання GPO.. ОПИС         Створення об'єктних об'єктів, який розгортає заплановане завдання для запуску WinCsFlags.exe /apply         у розділі Контекст системи під час запуску комп'ютера. Вибір цільових елементів керування групою безпеки.. ІМ'я GPOName ПАРАМЕТРА         Ім'я об'єкту групової замовлення.. PARAMETER TargetOU         OU, щоб зв'язати об'єкт групової групової замовлення.. PARAMETER SecurityGroupName         Група безпеки для фільтрування об'єктів групової політики.. Parameter WaveHostnames         Hostnames to add to the security group.. PARAMETER WinCSKey         Ключ WinCS, який потрібно застосувати (за замовчуванням: F33E0C8E002).. Параметр DryRun         Якщо значення true, записувати лише те, що потрібно зробити.#>     param(         [Parameter(Mandatory = $true)]         [string]$GPOName,                  [Parameter(Mandatory = $false)]         [string]$TargetOU,                  [Parameter(Mandatory = $true)]         [string]$SecurityGroupName,                  [Parameter(Mandatory = $true)]         [масив]$WaveHostnames,                  [Parameter(Mandatory = $false)]         [string]$WinCSKey = "F33E0C8E002",                  [Parameter(Mandatory = $false)]         [bool]$DryRun = $false     )          # Запланована конфігурація завдань для WinCsFlags.exe     $TaskName = "SecureBoot-WinCS-Apply"     $TaskPath = "\Microsoft\Windows\SecureBoot\"     $TaskDescription = "Застосовує конфігурацію безпечного завантаження через WinCS – ключ: $WinCSKey"          Write-Log "Розгортання WinCS GPO: $GPOName" "WAVE"     Write-Log "Завдання буде запущено: WinCsFlags.exe /apply --key ""$WinCSKey"" "INFO"     Write-Log "Trigger: At system startup (runs once as SYSTEM)" "INFO"          якщо ($DryRun) {         Write-Log "[DRYRUN] Створить GPO: $GPOName" "INFO"         Write-Log "[DRYRUN] Створить групу безпеки: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] додасть $(@($WaveHostnames). Кількість) комп'ютерів для групування" "INFO"         Write-Log "[DRYRUN] Розгорне заплановане завдання: $TaskName" "INFO"         Write-Log "[DRYRUN] Зв'яже GPO з: $TargetOU" "INFO"         повернути @{             Success = $true             GPOCreated = $false             GroupCreated = $false             ComputersAdded = 0         } (})     } (})          спробуйте {         # Імпорт необхідних модулів         Import-Module GroupPolicy – зупинка дії помилки         Import-Module ActiveDirectory – зупинка дії помилки     } зловити {         Write-Log "Не вдалося імпортувати необхідні модулі (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }     } (})          # Крок 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" "OK"         } зловити {             Write-Log "Не вдалося створити об'єкт групової політик: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }         } (})     } (})          # Крок 2. Створення XML запланованого завдання для розгортання GPO     # Створює завдання, яке запускається WinCsFlags.exe /apply під час запуску     $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">   <RegistrationInfo>     <опис>$TaskDescription</опис>     WinCsFlags.exe1 Автор>SYSTEM</Author>   WinCsFlags.exe5 /RegistrationInfo>   тригери WinCsFlags.exe7>     WinCsFlags.exe9>BootTrigger       <увімкнуто>true</Enabled>       <затримка>PT5M</затримка>     </BootTrigger>   </Triggers>  >принципалів <     <principal id="Author">       <Ідентифікатор користувача>S-1-5-18</UserId>       <runLevel>HighestAvailable</RunLevel>     </principal>   </Principals>  >настройок <     <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>     <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>     <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>     <AllowHardTerminate>true</AllowHardTerminate>     <початокУвімніть,>true</StartWhenAvailable>     <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>     <>IdleSettings       <StopOnIdleEnd>false</StopOnIdleEnd>       <RestartOnIdle>false</RestartOnIdle>     </IdleSettings>     <AllowStartOnDemand>true</AllowStartOnDemand>     <увімкнуто>true</Enabled>     <прихований>false</Hidden>     <RunOnlyIfIdle>false</RunOnlyIfIdle>     WinCsFlags.exe03 ЗаборонитиStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>     WinCsFlags.exe07 useUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>     WinCsFlags.exe11 WakeToRun>false</WakeToRun>     WinCsFlags.exe15 executionTimeLimit>PT1H</ExecutionTimeLimit>     WinCsFlags.exe19 DeleteExpiredTaskAfter>P30D</DeleteExpiredTaskAfter>     WinCsFlags.exe23 пріоритет>7</Priority>   WinCsFlags.exe27 /Settings>   WinCsFlags.exe29 actions Context="Author"WinCsFlags.exe30     WinCsFlags.exe31 Exec>      >WinCsFlags.exe<команд WinCsFlags.exe33 /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"         якщо (-not (test-path $sysvolPath)) {             New-Item -ItemType Directory – шлях $sysvolPath -Force | Out-Null         } (})         # Створення 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">         <RegistrationInfo>           <опис>$TaskDescription</опис>         </RegistrationInfo>        >принципалів <           <principal id="Author">             <UserId>NT AUTHORITY\System</UserId>             <LogonType>S4U</LogonType>             <runLevel>HighestAvailable</RunLevel>           </principal>         </Principals>        >настройок <           <>IdleSettings             <тривалість>PT5M</тривалість>             <waitTimeout>PT1H</WaitTimeout>             <StopOnIdleEnd>false</StopOnIdleEnd>             <RestartOnIdle>false</RestartOnIdle>           </IdleSettings>           <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>           <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>           <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>           <AllowHardTerminate>true</AllowHardTerminate>           <початокВиявний>true</StartWhenAvailable>           <AllowStartOnDemand>true</AllowStartOnDemand>           <увімкнуто>true</Enabled>           <прихований>false</Hidden>           <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>           <пріоритет>7</Priority>           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>         </Settings>         тригери <>           <> часовийтригер             <startBoundary>$(Get-Date –Format 'yyyy-MM-dd')T00:00:00</StartBoundary>             <увімкнуто>true</Enabled>           </> часовийтригер         </Triggers>        >дій <           <Exec>             <команди>WinCsFlags.exe</Command>             <аргументи>/apply --key "$WinCSKey"</Arguments>           </Exec>         </Actions>       </> завдань     </Properties>   </ImmediateTaskV2> </ScheduledTasks> "@         $gppTaskXml | Out-File -FilePath (join-path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force         Write-Log "Розгорнуте заплановане завдання в GPO: $TaskName" "OK"     } зловити {         Write-Log "Не вдалося розгорнути XML запланованого завдання: $($_. Exception.Message)" "WARN"         Write-Log "Повернення до розгортання WinCS на основі реєстру" "INFO"         # Резервний варіант: використання підходу до реєстру WinCS, якщо заплановане завдання GPP завершується невдало         # WinCS також можна ініціювати за допомогою розділу реєстру         # (Імплементація залежить від API реєстру WinCS, якщо він доступний)     } (})     # Крок 4. Створення або отримання групи безпеки     $group = Get-ADGroup -filter "Name -eq "$SecurityGroupName"" -ErrorAction SilentlyContinue     якщо (-не $group) {         спробуйте {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Опис "Комп'ютери призначено для безпечного завантаження WinCS розгортання - $GPOName" '                 -PassThru             Write-Log "Створено групу безпеки: $SecurityGroupName" "OK"         } зловити {             Write-Log "Не вдалося створити групу безпеки: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }         } (})     } інакше {         Write-Log "Група безпеки існує: $SecurityGroupName" "INFO"     } (})     # Крок 5. Додавання комп'ютерів до групи безпеки     $added = 0     $failed = 0     foreach ($hostname in $WaveHostnames) {         спробуйте {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } зловити {             $failed++         } (})     } (})     Write-Log "Додано комп'ютери $added до групи безпеки ($failed не знайдено в AD)" "OK"     # Крок 6. Настроювання фільтрування безпеки в об'єктах групової безпеки     спробуйте {         Set-GPPermission -Name $GPOName -TargetName "Автентифіковані користувачі" -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"     } (})     # Крок 7. Зв'язування GPO з OU     якщо ($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" "OK"             } інакше {                 Write-Log "GPO вже пов'язано з цільовим підрозділу" "INFO"             } (})         } зловити {             Write-Log "CRITICAL: Failed to link GPO to OU: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Помилка = "Помилка зв'язку GPO: $($_. Exception.Message)" }         } (})     } (})     Write-Log "Розгортання WinCS GPO завершено" "OK"     Write-Log "Комп'ютери запускатимуться WinCsFlags.exe під час наступного оновлення GPO + перезавантаження/завантаження" "INFO"     повернути @{         Success = $true         GPOCreated = $true         GroupCreated = $true         ComputersAdded = $added         ComputersFailed = $failed     } (}) }                                                                                        

# Wrapper function to maintain compatibility with main loop функція Deploy-WinCSForWave {     param(         [Parameter(Mandatory = $true)]         [масив]$WaveHostnames,         [Parameter(Mandatory = $false)]         [string]$WinCSKey = "F33E0C8E002",         [Parameter(Mandatory = $false)]         [string]$WavePrefix = "SecureBoot-Rollout",         [Parameter(Mandatory = $false)]         [int]$WaveNumber = 1,         [Parameter(Mandatory = $false)]         [string]$TargetOU,         [Parameter(Mandatory = $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-SecureBootUpdateTask.ps1 на пристроях із вимкненим запланованим завданням.# Використовує об'єкт групової підтримки з негайним запланованим завданням, яке запускається один раз.

function Deploy-EnableTaskGPO {     <#     . ПІДСУМОК         Розгортання Enable-SecureBootUpdateTask.ps1 за допомогою запланованого завдання GPO.. ОПИС         Створення об'єктної об'єктної мережі, яка розгортає одноразове заплановане завдання для ввімкнення         Заплановане завдання secure-Boot-Update на цільових пристроях.. PARAMETER TargetOU         OU, щоб зв'язати об'єкт групової групової замовлення.. PARAMETER TargetHostnames         Hostnames of devices with disabled task (from aggregation report).. Параметр DryRun         Якщо значення true, записувати лише те, що потрібно зробити.#>     param(         [Parameter(Mandatory = $false)]         [string]$TargetOU,                  [Parameter(Mandatory = $true)]         [масив]$TargetHostnames,                  [Parameter(Mandatory = $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 "РОЗГОРТАННЯ АКТИВАЦІЇ ВИПРАВЛЕННЯ ЗАВДАННЯ" "ВІДОМОСТІ"     Write-Log "=" * 70 "INFO"     Write-Log "Цільові пристрої: $($TargetHostnames.Count)" "INFO"     Write-Log "GPO: $GPOName" "INFO"     Write-Log "Група безпеки: $SecurityGroupName" "ВІДОМОСТІ"          якщо ($DryRun) {         Write-Log "[DRYRUN] Створить GPO: $GPOName" "INFO"         Write-Log "[DRYRUN] Створить групу безпеки: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Додає комп'ютери $($TargetHostnames.Count) до групи" "INFO"         Write-Log "[DRYRUN] Розгорне одноразове заплановане завдання для ввімкнення безпечного завантаження-оновлення" "INFO"         Write-Log "[DRYRUN] Зв'яже GPO з: $TargetOU" "INFO"         повернути @{             Success = $true             ComputersAdded = 0             DryRun = $true         } (})     } (})          спробуйте {         # Імпорт необхідних модулів         Import-Module GroupPolicy - ErrorAction Stop         Import-Module ActiveDirectory – зупинка дії помилки     } зловити {         Write-Log "Не вдалося імпортувати необхідні модулі: $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }     } (})          # Крок 1. Створення або отримання об'єктів групової роботи     $gpo = Get-GPO - Name $GPOName -ErrorAction SilentlyContinue     якщо ($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" "OK"         } зловити {             Write-Log "Не вдалося створити об'єкт групової політик: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }         } (})     } (})          # Крок 2. Розгортання XML запланованого завдання в GPO SYSVOL     # Завдання запускає команду PowerShell, щоб активувати завдання secure-Boot-Update     спробуйте {         $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. Id)}\Machine\Preferences\ScheduledTasks"                  якщо (-not (test-path $sysvolPath)) {             New-Item -ItemType Directory – шлях $sysvolPath -Force | Out-Null         } (})                  # Команда 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 = [Перетворити]::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=""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">         <RegistrationInfo>           <опис>$TaskDescription</опис>         </RegistrationInfo>        >принципалів <           <principal id="Author">             <UserId>S-1-5-18</UserId>             <runLevel>HighestAvailable</RunLevel>           </principal>         </Principals>        >настройок <           <>IdleSettings             <тривалість>PT5M</тривалість>             <WaitTimeout>PT1H</WaitTimeout>             <StopOnIdleEnd>false</StopOnIdleEnd>             <RestartOnIdle>false</RestartOnIdle>           </IdleSettings>           <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>           <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>           <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>           <AllowHardTerminate>true</AllowHardTerminate>           <початокУвімніть,>true</StartWhenAvailable>           <AllowStartOnDemand>true</AllowStartOnDemand>           <увімкнуто>true</Enabled>           <прихований>false</Hidden>           <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>           <пріоритет>7</Priority>           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>         </Settings>        >дій <           <Exec>            >powershell.exe<команд </Command>             <аргументи>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments>           </Exec>         </Actions>       </> завдань     </Properties>   </ImmediateTaskV2> </ScheduledTasks> "@                  $gppTaskXml | Out-File -FilePath (join-path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force         Write-Log "Розгорнуто одноразове заплановане завдання в GPO: $TaskName" "OK"              } зловити {         Write-Log "Не вдалося розгорнути XML запланованого завдання: $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }     } (})          # Крок 3. Створення або отримання групи безпеки     $group = Get-ADGroup -filter "Name -eq "$SecurityGroupName"" -ErrorAction SilentlyContinue     якщо (-не $group) {         спробуйте {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Опис "Комп'ютери з вимкненим завданням secure-Boot-Update – призначено для виправлення"                 -PassThru             Write-Log "Створено групу безпеки: $SecurityGroupName" "OK"         } зловити {             Write-Log "Не вдалося створити групу безпеки: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Помилка = $_. Виняток.Повідомлення }         } (})     } інакше {         Write-Log "Група безпеки існує: $SecurityGroupName" "INFO"     } (})          # Крок 4. Додавання комп'ютерів до групи безпеки     $added = 0     $failed = 0     foreach ($hostname in $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)" "OK"          # Крок 5. Настроювання фільтрування безпеки в об'єктах групових рахунків     спробуйте {         Set-GPPermission -Name $GPOName -TargetName "Автентифіковані користувачі" -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. Зв'язування GPO з OU     якщо ($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" "OK"             } інакше {                 Write-Log "GPO вже пов'язано з цільовим підрозділу" "INFO"             } (})         } зловити {             Write-Log "Не вдалося зв'язати GPO з OU: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Помилка = "Помилка зв'язку GPO: $($_. Exception.Message)" }         } (})     } інакше {         Write-Log "Не вказано TargetOU - GPO потрібно буде зв'язати вручну" "WARN"     } (})          Write-Log "" "INFO"     Write-Log "УВІМКНУТИ РОЗГОРТАННЯ ЗАВДАННЯ ЗАВЕРШЕНО" "OK"     Write-Log "Пристрої запускатимуть завдання ввімкнення під час наступного оновлення групової політики (gpupdate)" "INFO"     Write-Log "Завдання запускається один раз як СИСТЕМА та вмикає secure-Boot-Update" "INFO"     Write-Log "" "INFO"          повернути @{         Success = $true         ComputersAdded = $added         ComputersFailed = $failed         GPOName = $GPOName         SecurityGroup = $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 ""     # Пошук пристроїв із вимкненими завданнями з даних агрегації     якщо (-не $AggregationInputPath) {         Write-Host "ERROR: -AggregationInputPath is required to identify devices with disabled task" -ForegroundColor Red         Write-Host "Використання: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <шлях> -ReportBasePath <шлях>" -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|Розгорнути стан|План розгортання" }     $disabledTaskDevices = @()     foreach ($file in $jsonFiles) {         спробуйте {             $device = Get-Content $file. FullName -Raw | Перетворити файл із формату Json             якщо ($device. SecureBootTaskEnabled – eq $false -або                 $device. SecureBootTaskStatus – eq "Disabled" -або                 $device. SecureBootTaskStatus – eq "NotFound") {                 # Включають лише пристрої, які ще не оновили (жодна подія 1808)                 якщо ([int]$device. Подія1808Count –eq 0) {                     $device $disabledTaskDevices += Ім'я хоста                 } (})             } (})         } зловити {             # Пропустити неприпустимі файли         } (})     } (})     $disabledTaskDevices = $disabledTaskDevices | Select-Object -Unique     якщо ($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 -First 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }     якщо ($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. ComputersAdded)" -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. Одноразове завдання дозволить безпечне завантаження-оновлення" -ForegroundColor Gray         Write-Host " 3. Повторно запустити агрегацію, щоб переконатися, що завдання ввімкнуто" -ForegroundColor Gray     } інакше {         Write-Host ""         Write-Host "ПОМИЛКА: не вдалося розгорнути увімкнути GPO завдання" - Червоний колір переднього плану         Write-Host "Помилка: $($result. Error)" -ForegroundColor Red     } (})          вихід 0 }

# ============================================================================ # MAIN ORCHESTRATION LOOP # ============================================================================

Write-Host "" Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host "SECURE BOOT ROLLOUT ORCHESTRATOR - БЕЗПЕРЕРВНЕ РОЗГОРТАННЯ" -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" "ВІДОМОСТІ" Write-Log "Шлях звіту: $ReportBasePath" "ВІДОМОСТІ" якщо ($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) якщо (-не $UseWinCS -and -not $TargetOU) {     спробуйте {         # Спробуйте отримати DN домену кількома способами         $domainDN = $null         # Метод 1: Get-ADDomain (потрібна RSAT-AD-PowerShell)         спробуйте {             Import-Module ActiveDirectory – зупинка дії помилки             $domainDN = (Get-ADDomain - ErrorAction Stop). Відмітна назва         } зловити {             Write-Log "Get-ADDomain failed: $($_. Exception.Message)" "WARN"         } (})         Метод 2: Використання RootDSE через ADSI         якщо (-не $domainDN) {             спробуйте {                 $rootDSE = [ADSI]"LDAP://RootDSE"                 $domainDN = $rootDSE.defaultNamingContext.ToString()             } зловити {                 Write-Log "ПОМИЛКА ADSI RootDSE: $($_. Exception.Message)" "WARN"             } (})         } (})         Метод 3: Аналіз із членства в домені комп'ютера         якщо (-не $domainDN) {             спробуйте {                 $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()                 $domainDN = "DC=" + ($domain. Name -replace '\.', ',DC=')             } зловити {                 Write-Log "Помилка GetComputerDomain: $($_. Exception.Message)" "WARN"             } (})         } (})         якщо ($domainDN) {             $TargetOU = $domainDN             Write-Log "Target: Domain Root ($domainDN) – GPO застосує до домену за допомогою фільтрування групи безпеки" "INFO"         } інакше {             Write-Log "Не вдалося визначити DN домену – буде створено GPO, але НЕ LINKED!" "ПОМИЛКА"             Write-Log "Укажіть параметр -TargetOU або зв'яжіть GPO вручну після створення" "ERROR"             $TargetOU = $null         } (})     } зловити {         Write-Log "Не вдалося отримати домен DN – GPO буде створено, але не пов'язано.                                     За потреби зв'яжіть посилання вручну." "WARN" (ПОПЕРЕДЖЕННЯ)         Write-Log "Помилка: $($_. Exception.Message)" "WARN"         $TargetOU = $null     } (}) } інакше {     Write-Log "Цільовий підрозділу: $TargetOU" "ВІДОМОСТІ" }

Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Інтервал опитування: $PollIntervalMinutes хвилин" "INFO" якщо ($LargeScaleMode) {     Write-Log "LargeScaleMode enabled (розмір пакета: $ProcessingBatchSize, log sample: $DeviceLogSampleSize)" "INFO" }

# ============================================================================ # ОБОВ'ЯЗКОВА ПЕРЕВІРКА. Перевірка розгортання та роботи виявлення # ============================================================================

Write-Host "" Write-Log "Перевірка передумов..." "ВІДОМОСТІ"

$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath якщо (-не $detectionCheck.IsDeployed) {     Write-Log $detectionCheck.Повідомлення "ПОМИЛКА"     Write-Host ""     Write-Host "REQUIRED: Deploy detection infrastructure first:" -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 ""     якщо (-не $DryRun) {         Повернутися     } (}) } інакше {     Write-Log $detectionCheck.Повідомлення "OK" }

# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Актуальність даних: $($freshness. Файли TotalFiles, $($freshness. FreshFiles) fresh (<24h), $($freshness. StaleFiles) stale (>72h)" "INFO" якщо ($freshness. Увага! {     Write-Log $freshness. Попередження "WARN" }

# 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: ONLY $($allowedHostnames.Count) пристрої вважатимуться для розгортання" "INFO"     } інакше {         Write-Log "AllowList вказано, але пристрої не знайдено – це заблокує всі розгортання!" "WARN" (ПОПЕРЕДЖЕННЯ)     } (}) }

# Load VIP/exclusion list (BlockList) $excludedHostnames = @() якщо ($ExclusionListPath -або $ExcludeADGroup) {     $excludedHostnames = Get-ExcludedHostnames -exclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup     якщо ($excludedHostnames.Count -gt 0) {         Write-Log "ВИКЛЮЧЕННЯ VIP: пристрої $($excludedHostnames.Count) буде пропущено з розгортання" "ВІДОМОСТІ"     } (}) }

# 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)" "ВІДОМОСТІ"

# Main loop - runs until all eligible devices are updated $iterationCount = 0 в той час як ($true) {     $iterationCount++     Write-Host ""     Write-Host ("=" * 80) - Білий колір переднього плану     Write-Log "=== ІТЕРАЦІЯ $iterationCount ===" "WAVE"     Write-Host ("=" * 80) -ForegroundColor White     # Крок 1. Запуск агрегації     Write-Log "Крок 1: запуск агрегації..." "ВІДОМОСТІ"     # Orchestrator завжди повторно використовує одну папку (LargeScaleMode), щоб уникнути здутості диска     # Адміністратори, які запускають агрегатор, отримують папки із позначками часу для точкових знімків     $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current"     # Перевірка актуальності даних перед збиранням     $freshness = Get-DataFreshness -JsonPath $AggregationInputPath     Write-Log "Актуальність даних: $($freshness. FreshFiles)/$($freshness. Пристрої TotalFiles, про які повідомлялося за останні 24 год." "INFO"     якщо ($freshness. Увага! {         Write-Log $freshness. Попередження "WARN"     } (})     $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1"     $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json"     $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     якщо ($aggregateScript "Тестовий шлях") {         якщо (-не $DryRun) {             # Orchestrator завжди використовує потокове передавання + інкрементне для підвищення ефективності             # Агрегатор автовиявлюється до 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. Завантаження стану пристрою..." "ВІДОМОСТІ"     $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue |          Where-Object { $_. Назва -notlike "*Блоки*" } |          Sort-Object LastWriteTime – за спаданням |          Select-Object –Перший 1     якщо (-не $notUptodateCsv -and -not $DryRun) {         Write-Log "Дані агрегації не знайдено.                                            Очікування..." "WARN" (ПОПЕРЕДЖЕННЯ)         Start-Sleep -секунд ($PollIntervalMinutes * 60)         Продовжити     } (})     $notUpdatedDevices = якщо ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } інакше { @() }     Write-Log "Пристрої не оновлено: $($notUpdatedDevices.Count)" "INFO"     $notUpdatedIndexes = Get-NotUpdatedIndexes -пристрої $notUpdatedDevices     # Крок 3. Оновлення журналу пристроїв (відстеження за іменем хоста)     Write-Log "Крок 3. Оновлення журналу пристроїв..." "ВІДОМОСТІ"     Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory     Save-DeviceHistory –журнал $deviceHistory     # Крок 4. Перевірте наявність заблокованих блоків (недоступних пристроїв)     $existingBlockedCount = $blockedBuckets.Count     Write-Log "Крок 4. Перевірка заблокованих блоків (пінгування пристроїв за минулий період очікування)..." "ВІДОМОСТІ"     якщо ($existingBlockedCount -gt 0) {         Write-Log "Наразі заблоковані блоки з попередніх запусків: $existingBlockedCount" "ВІДОМОСТІ"     } (})     якщо ($adminApproved.Count -gt 0) {         Write-Log "Admin затверджені блоки (не буде повторно заблоковано): $($adminApproved.Count)" "INFO"     } (})     $newlyBlocked = Update-BlockedBuckets -RolloutState $rolloutState -BlockedBuckets $blockedBuckets -AdminApproved $adminApproved -NotUpdatedDevices $notUpdatedDevices -NotUpdatedIndexes $notUpdatedIndexes -MaxWaitHours $MaxWaitHours -DryRun:$DryRun     якщо ($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) {         Save-BlockedBuckets –заблоковані $blockedBuckets         Write-Log "Автоматично розблоковано блоки (пристрої оновлено): $($autoUnblocked.Count)" "OK"     } (})     # Крок 5. Обчислення пристроїв, які відповідають вимогам, що залишилися     $eligibleCount = 0     foreach ($device in $notUpdatedDevices) {         $bucketKey = Get-BucketKey $device         якщо (-не $blockedBuckets.Contains($bucketKey)) {             $eligibleCount++         } (})     } (})     Write-Log "Пристрої, які відповідають вимогам, що залишилися: $eligibleCount" "INFO"     Write-Log "Заблоковані блоки: $($blockedBuckets.Count)" "ВІДОМОСТІ"     # Крок 6. Перевірка виконання     якщо ($eligibleCount -eq 0) {         Write-Log "ROLLOUT COMPLETE – оновлено всі пристрої, які відповідають вимогам!" "OK" (OK)         $rolloutState.Status = "Завершено"         $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"         Save-RolloutState – стан $rolloutState         Перерва     } (})     # Крок 6. Створення та розгортання наступної хвилі     Write-Log "Крок 6. Створення хвилі розгортання..." "ВІДОМОСТІ"     $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames     # Перевірте, чи є в нас пристрої для розгортання ($waveDevices можуть бути $null, пусті або з фактичними пристроями)     $hasDevices = $waveDevices -і @($waveDevices | Where-Object { $_ }). Кількість -gt 0     якщо ($hasDevices) {         # Лише номер хвилі кроку, коли ми фактично маємо пристрої для розгортання         $rolloutState.CurrentWave++         Write-Log "Хвиля $($rolloutState.CurrentWave): $(@($waveDevices). Кількість) пристроїв" "WAVE"         # Розгортання GPO за допомогою нелінійованої функції         $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $hostnames = @($waveDevices | ForEach-Object {             якщо ($_. Hostname (Ім'я хоста) { $_. Hostname } elseif ($_. HostName( Ім'я хоста) { $_. HostName } інакше { $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"             # Дочекайтеся наступної ітерації             якщо (-не $DryRun) {                 Write-Log "Спати протягом $PollIntervalMinutes хвилин, перш ніж повторити спробу..." "ВІДОМОСТІ"                 Start-Sleep –секунди ($PollIntervalMinutes * 60)             } (})             Продовжити         } (})         Write-Log "Розгортання на $($hostnames. Кількість) імен хостів у Wave $($rolloutState.CurrentWave)" "INFO"         # Розгортання за допомогою методу WinCS або GPO на основі параметра -UseWinCS         якщо ($UseWinCS) {             # Метод WinCS: Створення об'єктної об'єктної системи із запланованим завданням для запуску WinCsFlags.exe як SYSTEM для кожної кінцевої точки             Write-Log "Використання методу розгортання WinCS (ключ: $WinCSKey)" "WAVE"             $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames '                 -WinCSKey $WinCSKey '                 -WavePrefix $WavePrefix '                 -WaveNumber $rolloutState.CurrentWave '                 -TargetOU $TargetOU '                 -DryRun:$DryRun             якщо (-не $wincsResult.Success) {                 Write-Log "Помилка розгортання WinCS: застосовано: $($wincsResult.Applied), помилка: $($wincsResult.Failed)" "WARN"             } інакше {                 Write-Log "Розгортання WinCS виконано – застосовано: $($wincsResult.Applied), пропущено: $($wincsResult.Skipped)" "OK"             } (})             # Збереження результатів WinCS для аудиту             $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json"             $wincsResult | ConvertTo-Json -Глибина 5 | Out-File $wincsResultFile -Кодування UTF8         } інакше {             # Метод GPO: Створення групової політики з параметром реєстру AvailableUpdatesPolicy             $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun             якщо (-не $gpoResult) {                 Write-Log "Не вдалося розгорнути об'єкт групової роботи – буде повториться наступна ітерація" "ERROR"             } (})         } (})         # Record wave in state         $waveRecord = @{             WaveNumber = $rolloutState.CurrentWave             StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             DeviceCount = @($waveDevices). Розраховувати             Пристрої = @($waveDevices | ForEach-Object {                 @{                     Hostname = if ($_. Hostname (Ім'я хоста) { $_. Hostname } elseif ($_. HostName( Ім'я хоста) { $_. HostName } інакше { $null }                     Ключ сегмента = Get-BucketKey $_                 } (})             })         } (})         # Переконайтеся, що WaveHistory завжди є масивом перед додаванням (запобігає виникненню проблем із об'єднанням геш-таблиць)         $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord)         $rolloutState.TotalDevicesTargeted += @($waveDevices). Розраховувати         Save-RolloutState – стан $rolloutState         Write-Log розгорнуто "Wave $($rolloutState.CurrentWave).                                                                                                                                                                                        Очікування $PollIntervalMinutes хвилин..." "OK" (OK)     } інакше {         # Показати стан розгорнутих пристроїв, які очікують на оновлення         Write-Log "" "INFO"         Write-Log "========== РОЗГОРНУТО ВСІ ПРИСТРОЇ – ОЧІКУЄТЬСЯ ========== СТАНУ" "ВІДОМОСТІ"                  # Завантажте всі розгорнуті пристрої з журналу хвиль         $allDeployedLookup = @{}         foreach ($wave in $rolloutState.WaveHistory) {             ($device в $wave. Пристрої) {                 якщо ($device. Hostname (Ім'я хоста) {                     $allDeployedLookup[$device. Hostname] = @{                         Hostname = $device. Ім'я хоста                         BucketKey = $device. Ключ сегмента                         DeployedAt = $wave. Початок роботи                         WaveNumber = $wave. Номер хвилі                     } (})                 } (})             } (})         } (})         $allDeployedDevices = @($allDeployedLookup.Значення)                  якщо ($allDeployedDevices.Count -gt 0) {             # Знайдіть, які розгорнуті пристрої все ще очікують (у списку NotUpdated)             $stillPendingCount = 0             $noLongerPendingCount = 0             $pendingSample = @()             foreach ($deployed in $allDeployedDevices) {                 якщо ($notUpdatedIndexes.HostSet.Contains($deployed. Hostname)) {                     $stillPendingCount++                     якщо ($pendingSample.Count -lt $DeviceLogSampleSize) {                         $pendingSample += $deployed. Ім'я хоста                     } (})                 } інакше {                     $noLongerPendingCount++                 } (})             } (})                          # Отримати фактичні оновлені лічильники від агрегації – диференціювати подію 1808 і UEFICA2023Status             $summaryCsv = Get-ChildItem -Шлях $aggregationPath -Фільтр "*Зведення*.csv" |                  Sort-Object LastWriteTime –за спаданням | Select-Object –Перший 1             $actualUpdated = 0             $totalDevicesFromSummary = 0             $event 1808Count = 0             $uefiStatusUpdated = 0             $needsRebootSample = @()                          якщо ($summaryCsv) {                 $summary = Import-Csv $summaryCsv.FullName | Select-Object –Перший 1                 якщо ($summary. Оновлено) { $actualUpdated = [int]$summary. Оновлено }                 якщо ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices }             } (})                          # Обчислення швидкості з журналу хвиль (пристрої, оновлені за день)             $devicesPerDay = 0             якщо ($rolloutState.StartedAt -and $actualUpdated -gt 0) {                 $startDate = [дата й час]::P arse($rolloutState.StartedAt)                 $daysElapsed = ((Get-Date) - $startDate). Усього днів                 якщо ($daysElapsed -gt 0) {                     $devicesPerDay = $actualUpdated / $daysElapsed                 } (})             } (})                          # Збереження зведення розгортання з прогнозами у вихідні дні             # Для узгодженості використовуйте кількість notUptodate агрегатора (без пристроїв SB OFF).             $notUpdatedCount = якщо ($summary -і $summary. NotUptodate) { [int]$summary. NotUptodate } else { $totalDevicesFromSummary - $actualUpdated }             Save-RolloutSummary – стан $rolloutState '                 -TotalDevices $totalDevicesFromSummary '                 -UpdatedDevices $actualUpdated '                 -NotUpdatedDevices $notUpdatedCount '                 -DevicesPerDay $devicesPerDay                          # Перевірка необроблених даних для пристроїв із UEFICA2023Status=Updated, але жодна подія 1808 (потребує перезавантаження)             $dataFiles = Get-ChildItem -шлях $AggregationInputPath -фільтр "*.json" -ErrorAction SilentlyContinue             $totalDataFiles = @($dataFiles). Розраховувати             $batchSize = [Математика]::Max(500; $ProcessingBatchSize)             якщо ($LargeScaleMode) {                 $batchSize = [Математика]::Max(2000; $ProcessingBatchSize)             }

            if ($totalDataFiles -gt 0) {                 для ($idx = 0; $idx -lt $totalDataFiles; $idx += $batchSize) {                     $end = [Математика]::Min($idx + $batchSize - 1, $totalDataFiles - 1)                     $batchFiles = $dataFiles[$idx. $end]

                    foreach ($file in $batchFiles) {                         спробуйте {                             $deviceData = Get-Content $file. FullName -Raw | Перетворити файл із формату Json                             $hostname = $deviceData.Ім'я хоста                             якщо (-не $hostname) { continue }                             $has 1808 = [int]$deviceData.Event1808Count -gt 0                             $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Оновлено"                             якщо ($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 = якщо ($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 = якщо ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) більше)" } інакше { "" }                 Write-Log "Пристрої, що очікують (зразок): $($pendingSample -join",")$pendingSuffix" "WARN"             } (})         } інакше {             Write-Log "Ще не розгорнуто жодного пристрою" "INFO"         } (})         Write-Log "================================================================" "INFO"         Write-Log "" "INFO"     } (})     # Зачекайте до наступної ітерації     якщо (-не $DryRun) {         Write-Log "Сон протягом $PollIntervalMinutes хвилин..." "ВІДОМОСТІ"         Start-Sleep -секунд ($PollIntervalMinutes * 60)     } інакше {         Write-Log "[DRYRUN] Буде чекати $PollIntervalMinutes хвилин" "INFO"         break # Вихід після однієї ітерації в сухому запуску     } (}) }                               

# ============================================================================ # ОСТАТОЧНЕ ЗВЕДЕННЯ # ============================================================================

Write-Host "" Write-Host ("=" * 80) -ForegroundColor Green Write-Host " ROLLOUT ORCHESTRATOR SUMMARY" -ForegroundColor Green Write-Host ("=" * 80) - Зелений колір переднього плану 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 $(якщо ($finalBlocked.Count -gt 0) { "Червоний" } інакше { "Зелений" }) Write-Host "Пристрої відстежуються: $($deviceHistory.Count)" -ForegroundColor Gray Write-Host ""

if ($finalBlocked.Count -gt 0) {     Write-Host "ЗАБЛОКОВАНІ БЛОКИ (потрібно вручну перевірити):" -ForegroundColor Red     foreach ($key in $finalBlocked.Keys) {         $info = $finalBlocked[$key]         Write-Host " - $key" -ForegroundColor Red         Write-Host " Причина: $($info. Reason)" -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 ""  

​​​​​​​

Потрібна додаткова довідка?

Потрібні додаткові параметри?

Ознайомтеся з перевагами передплати, перегляньте навчальні курси, дізнайтесь, як захистити свій пристрій тощо.