Copie e cole este script de exemplo e modifique conforme necessário para o seu ambiente:

<# . SYNOPSIS     Orquestrador de implementação de Arranque Seguro Contínuo que é executado até que a implementação esteja concluída.

.DESCRIPTION     Este script fornece automatização ponto a ponto completa para a implementação de certificados de Arranque Seguro:     1.      Gera ondas de implementação com base nos dados de agregação     2. Cria grupos do AD e GPO para cada onda     3. Monitores para atualizações de dispositivos (Evento 1808)     4. Deteta registos bloqueados (dispositivos inacessíveis)     5. Progride automaticamente para a próxima onda     6. É executada até que todos os dispositivos elegíveis sejam atualizados          Critérios de Conclusão:     - Não restam dispositivos em: Ação Necessária, Alta Confiança, Observação, Temporariamente em Pausa     - Fora do âmbito (por predefinição): Não Suportado, Arranque Seguro Desativado     - É executado continuamente até estar concluído - sem limite      de ondas arbitrário     Estratégia de Implementação:     - ALTA CONFIANÇA: todos os dispositivos na primeira onda (seguro)     - AÇÃO NECESSÁRIA: Duplos progressivos (1→2→4→8...)          Lógica de Bloqueio:     - Depois de MaxWaitHours, o orquestrador pings dispositivos que não foram atualizados     - Se o dispositivo estiver INACESSÍVEL (o ping falhar) → registo estiver BLOQUEADO para investigação     - Se o dispositivo estiver ACESSÍVEL, mas não for atualizado → continuar à espera (poderá precisar de ser reiniciado)     - Os registos bloqueados são excluídos até que o administrador os      desbloqueie     Desbloqueio Automático:     - Se um dispositivo num registo bloqueado aparecer posteriormente como atualizado (Evento 1808),       o registo é desbloqueado automaticamente e a implementação continua     - Trata de dispositivos que estavam temporariamente offline, mas que voltaram          Controlo de Dispositivos:     - Monitoriza os dispositivos por nome de anfitrião (pressupõe que os nomes não são alterados durante a implementação)     - Nota: a coleção JSON não inclui um ID de computador exclusivo; adicionar um para um melhor controlo

.PARAMETER AggregationInputPath     Caminho para dados de dispositivos JSON não processados (a partir de Detetar script)

.PARAMETER ReportBasePath     Caminho base para relatórios de agregação

.PARAMETER TargetOU     Nome Único da UO para ligar GPOs.Opcional – se não for especificado, o GPO está ligado à raiz de domínio para uma cobertura ao nível do domínio.A filtragem de grupos de segurança garante que apenas os dispositivos visados recebem a política.

.PARAMETER MaxWaitHours     Horas para aguardar que os dispositivos atualizem antes de verificar a acessibilidade.Após este período, os dispositivos que não foram atualizados são colocados em ping.Os dispositivos inacessíveis fazem com que o registo seja bloqueado.Predefinição: 72 (3 dias)

.PARAMETER PollIntervalMinutes     Minutos entre verificações de status. Predefinição: 1440 (1 dia)

.PARAMETER AllowListPath     Caminho para um ficheiro que contém nomes de anfitrião para PERMITIR para implementação (implementação direcionada).Suporta .txt (um nome de anfitrião por linha) ou .csv (com a coluna Hostname/ComputerName/Name).Quando especificado, apenas estes dispositivos serão incluídos na implementação.BlockList continua a ser aplicado após AllowList.

.PARAMETER AllowADGroup     Nome de um grupo de segurança do AD que contém contas de computador para PERMITIR.Exemplo: "SecureBoot-Pilot-Computers" ou "Wave1-Devices"     Quando especificado, apenas os dispositivos neste grupo serão incluídos na implementação.Combine com AllowListPath para filtragem baseada em ficheiros e no AD.

.PARAMETER ExclusionListPath     Caminho para um ficheiro que contém nomes de anfitrião para EXCLUIR da implementação (dispositivos VIP/executivo).Suporta .txt (um nome de anfitrião por linha) ou .csv (com a coluna Hostname/ComputerName/Name).Estes dispositivos nunca serão incluídos em nenhuma fase de implementação.BlockList é aplicada após a filtragem AllowList.     . PARAMETER ExcludeADGroup     Nome de um grupo de segurança do AD que contém contas de computador a excluir.Exemplo: "VIP-Computers" ou "Executive-Devices"     Combine com ExclusionListPath para exclusões baseadas em ficheiros e no AD.

.PARAMETER UseWinCS     Utilize WinCS (Sistema de Configuração do Windows) em vez de GPO/AvailableUpdatesPolicy.O WinCS implementa a ativação de Arranque Seguro ao executar WinCsFlags.exe diretamente em cada ponto final.WinCsFlags.exe é executado no contexto SYSTEM através de uma tarefa agendada.Este método é útil para:     - Implementações mais rápidas (efeito imediato vs. aguardar o processamento de GPO)     - Dispositivos não associados a um domínio     - Ambientes sem infraestrutura do AD/GPO     Referência: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe

.PARAMETER WinCSKey     A chave WinCS a utilizar para ativação de Arranque Seguro.Predefinição: F33E0C8E002     Esta chave corresponde à configuração de implementação de Arranque Seguro.     . DryRun do PARÂMETRO     Mostrar o que seria feito sem fazer alterações

.PARAMETER ListBlockedBuckets     Apresentar todos os registos e saídas atualmente bloqueados

.PARAMETER UnblockBucket     Desbloquear um registo específico por chave e sair

.PARAMETER UnblockAll     Desbloquear todos os registos e sair

.PARAMETER EnableTaskOnDisabled     Implemente Enable-SecureBootUpdateTask.ps1 em todos os dispositivos com a tarefa agendada desativada.Cria um GPO com uma tarefa agendada única que executa a opção Ativar script com -Quiet.Isto é útil para corrigir dispositivos com a tarefa Atualização de Arranque Seguro desativada.

.EXAMPLE     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" "         -ReportBasePath "E:\SecureBootReports" "         -TargetOU "OU=Workstations,DC=contoso,DC=com"

.EXAMPLE     # Listar registos bloqueados     .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets

.EXAMPLE     # Desbloquear um registo específico     .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -UnblockBucket "Dell_Latitude5520_BIOS1.2.3"

.EXAMPLE     # Excluir dispositivos VIP da implementação com um ficheiro de texto     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" "         -ReportBasePath "E:\SecureBootReports" "         -ExclusionListPath "C:\Admin\VIP-Devices.txt"

.EXAMPLE     # Excluir dispositivos num grupo de segurança do AD (por exemplo, portáteis executivos)     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" "         -ReportBasePath "E:\SecureBootReports" "         -ExcludeADGroup "VIP-Computers"

.EXAMPLE     # Utilize WinCS (Sistema de Configuração do Windows) em vez de GPO/AvailableUpdatesPolicy     # WinCsFlags.exe é executado no contexto SYSTEM em cada ponto final através da tarefa agendada     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" "         -ReportBasePath "E:\SecureBootReports" "         -UseWincs '         -WinCSKey "F33E0C8E002" #>

[CmdletBinding()] parâmetro(     [Parameter(Mandatory = $false)]     [cadeia]$AggregationInputPath,     [Parameter(Mandatory = $false)]     [cadeia]$ReportBasePath,     [Parameter(Mandatory = $false)]     [cadeia]$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,     N.º ============================================================================     # AllowList/BlockList Parameters     N.º ============================================================================     # AllowList = Incluir apenas estes dispositivos (implementação direcionada)     # BlockList = Excluir estes dispositivos (nunca serão implementados)     # Ordem de processamento: AllowList primeiro (se especificado) e, em seguida, BlockList     [Parameter(Mandatory = $false)]     [cadeia]$AllowListPath,     [Parameter(Mandatory = $false)]     [cadeia]$AllowADGroup,     [Parameter(Mandatory = $false)]     [cadeia]$ExclusionListPath,     [Parameter(Mandatory = $false)]     [cadeia]$ExcludeADGroup,     N.º ============================================================================     # WinCS (Sistema de Configuração do Windows) Parâmetros     N.º ============================================================================     # WinCS é uma alternativa à implementação do GPO AvailableUpdatesPolicy.                              # Utiliza WinCsFlags.exe em cada ponto final para ativar a implementação de Arranque Seguro.# WinCsFlags.exe é executado no contexto SYSTEM no ponto final.# Referência: 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)]     [cadeia]$UnblockBucket,          [Parameter(Mandatory = $false)]     [switch]$UnblockAll,          [Parameter(Mandatory = $false)]     [switch]$EnableTaskOnDisabled )

$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Exemplos de Implementação e Monitorização"

# ============================================================================ # VALIDAÇÃO DE DEPENDÊNCIA # ============================================================================

function Test-ScriptDependencies {     parâmetro(         [Parameter(Mandatory = $true)]         [cadeia]$ScriptDirectory,         [Parameter(Mandatory = $true)]         [string[]]$RequiredScripts     )     $missingScripts = @()     foreach ($script no $RequiredScripts) {         $scriptPath = Join-Path $ScriptDirectory $script         if (-not (Test-Path $scriptPath)) {             $missingScripts += $script         }     }     if ($missingScripts.Count -gt 0) {         Write-Host ""         Write-Host ("=" * 70) -Primeiro PlanoColor Vermelho         Write-Host " DEPENDÊNCIAS EM FALTA" - Primeiro PlanoColor Vermelho         Write-Host ("=" * 70) -Primeiro PlanoColor Vermelho         Write-Host ""         Write-Host "Os seguintes scripts necessários não foram encontrados:" -ForegroundColor Amarelo         foreach ($script no $missingScripts) {             Write-Host " - $script" -ForegroundColor White         }         Write-Host ""         Write-Host "Transfira os scripts mais recentes de:" -ForegroundColor Cyan         Write-Host " URL: $DownloadUrl" -ForegroundColor White         Write-Host " Navegue para: '$DownloadSubPage'" -ForegroundColor White         Write-Host ""         Write-Host "Extrair todos os scripts para o mesmo diretório e executar novamente". -ForegroundColor Amarelo         Write-Host ""         devolver $false     }     devolver $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)) {     sair 1 }

# ============================================================================ # VALIDAÇÃO DO PARÂMETRO # ============================================================================

# Admin commands only need ReportBasePath $isAdminCommand = $ListBlockedBuckets ou $UnblockBucket ou $UnblockAll ou $EnableTaskOnDisabled

if (-not $ReportBasePath) {     Write-Host "ERROR: -ReportBasePath is required." -ForegroundColor Red     sair 1 }

if (-not $isAdminCommand -and -not $AggregationInputPath) {     Write-Host "ERROR: -AggregationInputPath is required for rollout (not needed for -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red     sair 1 }

# ============================================================================ # GPO DETECTION - CHECK FOR DETECTION GPO (DETEÇÃO DE GPO # GPO – PROCURAR GPO DE DETEÇÃO) # ============================================================================

if (-not $isAdminCommand -and -not $DryRun) {     $CollectionGPOName = "SecureBoot-EventCollection"     # Verifique se o módulo GroupPolicy está disponível     if (Get-Module -ListAvailable -Name GroupPolicy) {         Import-Module GroupPolicy -ErrorAction SilentlyContinue         Write-Host "A verificar a deteção de GPO..." -ForegroundColor Amarelo         experimente {             # Verificar se o GPO existe             $existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue             se ($existingGpo) {                 Write-Host " GPO de deteção encontrado: $CollectionGPOName" -ForegroundColor Green             } senão {                 Write-Host ""                 Write-Host ("=" * 70) -Primeiro PlanoColor Amarelo                 Write-Host " WARNING: DETECTION GPO NOT FOUND" -ForegroundColor Yellow                 Write-Host ("=" * 70) -Primeiro PlanoColor Amarelo                 Write-Host ""                 Write-Host "O GPO de deteção '$CollectionGPOName' não foi encontrado." -ForegroundColor Yellow                 Write-Host "Sem este GPO, não serão recolhidos dados do dispositivo." -ForegroundColor Yellow                 Write-Host ""                 Write-Host "Para implementar o GPO de Deteção, execute:" -ForegroundColor Cyan                 Write-Host " .\Deploy-GPO-SecureBootCollection.ps1 -DomainName <domain> -AutoDetectOU" -ForegroundColor White                 Write-Host ""                 Write-Host "Continuar mesmo assim?                                     (Y/N)" -Primeiro PlanoColor Amarelo                 $response = Read-Host                 if ($response -notmatch '^[Yy]') {                     Write-Host "A abortar. Implemente primeiro o GPO de Deteção." -ForegroundColor Red                     sair 1                 }             }         } captura {             Write-Host " Não é possível marcar para GPO: $($_. Exception.Message)" -ForegroundColor Yellow         }     } senão {         Write-Host " GroupPolicy module not available - skipping GPO marcar" -ForegroundColor Gray     }     Write-Host "" }

# ============================================================================ # CAMINHOS DO FICHEIRO DE ESTADO # ============================================================================

$stateDir = Join-Path $ReportBasePath "RolloutState" if (-not (Test-Path $stateDir)) {     New-Item -ItemType Directory -Path $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 N.º ============================================================================ # ConvertFrom-Json -AsHashtable é apenas PS7+. Isto fornece compatibilidade.

function ConvertTo-Hashtable {     parâmetro(         [Parameter(ValueFromPipeline = $true)]         $InputObject     )     processo {         if ($null -eq $InputObject) { return @{} }         if ($InputObject -is [System.Collections.IDictionary]) { return $InputObject }         if ($InputObject -is [PSCustomObject]) {             # Utilize [encomendado] para ordenação de chaves consistente e processamento de duplicados seguro             $hash = [encomendado]@{}             foreach ($prop em $InputObject.PSObject.Properties) {                 # A atribuição indexada processa com segurança duplicados ao substituir                 $hash[$prop. Nome] = ConvertTo-Hashtable $prop. Valor             }             devolver $hash         }         if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {             return @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ })         }         devolver $InputObject     } }

# ============================================================================ # COMANDOS DE ADMINISTRADOR: Listar/Desbloquear Registos # ============================================================================

if ($ListBlockedBuckets) {     Write-Host ""     Write-Host ("=" * 80) -Primeiro PlanoColor Amarelo     Write-Host " REGISTOS BLOQUEADOS" -Primeiro PlanoColor Amarelo     Write-Host ("=" * 80) -Primeiro PlanoColor Amarelo     Write-Host ""     if (Test-Path $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         if ($blocked. Contagem -eq 0) {             Write-Host "Sem registos bloqueados". -Primeiro PlanoColor Verde         } senão {             Write-Host "Total bloqueado: $($blocked. Count)" -ForegroundColor Red             Write-Host ""             foreach ($key no $blocked. Chaves) {                 $info = $blocked[$key]                 Write-Host "Bucket: $key" -ForegroundColor Red                 Write-Host " Bloqueado a: $($info. BlockedAt)" -ForegroundColor Gray                 Write-Host " Razão: $($info. Razão)" -ForegroundColor Gray                 Write-Host " Dispositivo Com Falha: $($info. FailedDevice)" -ForegroundColor Gray                 Write-Host " Última Comunicação: $($info. LastReported)" -ForegroundColor Gray                 Write-Host " Wave: $($info. WaveNumber)" -ForegroundColor Gray                 Write-Host " Dispositivos no Registo: $($info. DevicesInBucket)" -ForegroundColor Gray                 Write-Host ""             }             Write-Host "Para desbloquear um registo:"             Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Cyan             Write-Host ""             Write-Host "Para desbloquear tudo:"             Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Cyan         }     } senão {         Write-Host "Não foi encontrado nenhum ficheiro de registos bloqueados". -ForegroundColor Green     }     Write-Host ""     sair 0 }     

if ($UnblockBucket) {     Write-Host ""     if (Test-Path $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         if ($blocked. Contém($UnblockBucket)) {             $blocked. Remover($UnblockBucket)             $blocked | ConvertTo-Json -Profundidade 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force             # Adicionar à lista aprovada pelo administrador para impedir o novo bloqueio             $adminApproved = @{}             if (Test-Path $adminApprovedPath) {                 $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable             }             $adminApproved[$UnblockBucket] = @{                 ApprovedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"                 ApprovedBy = $env:USERNAME             }             $adminApproved | ConvertTo-Json -Profundidade 10 | Out-File $adminApprovedPath -Encoding UTF8 -Force             Write-Host "Registo desbloqueado: $UnblockBucket" -ForegroundColor Green             Write-Host "Adicionado à lista aprovada pelo administrador (não será bloqueado automaticamente)" -ForegroundColor Cyan         } senão {             Write-Host "Registo não encontrado: $UnblockBucket" -Primeiro PlanoColor Amarelo             Write-Host "Registos disponíveis:" -Primeiro PlanoColor Cinzento             $blocked. Teclas | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }         }     } senão {         Write-Host "Não foi encontrado nenhum ficheiro de registos bloqueados". -ForegroundColor Amarelo     }     Write-Host ""     sair 0 }                          

if ($UnblockAll) {     Write-Host ""     if (Test-Path $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         $count = $blocked. Contagem         @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Encoding UTF8 -Force         Write-Host "Desbloqueie todos os registos $count" -ForegroundColor Green     } senão {         Write-Host "Não foi encontrado nenhum ficheiro de registos bloqueados" -ForegroundColor Yellow     }     Write-Host ""     sair 0 }

# ============================================================================ # FUNÇÕES AUXILIARES # ============================================================================

function Get-RolloutState {     if (Test-Path $rolloutStatePath) {         experimente {             $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable             # Validar que as propriedades necessárias existem             if ($null -eq $loaded. CurrentWave) {                 emitir "Ficheiro de estado inválido - CurrentWave em falta"             }             # Certifique-se de que WaveHistory é sempre uma matriz (corrige a desserialização PS5.1 JSON)             if ($null -eq $loaded. WaveHistory) {                 $loaded. WaveHistory = @()             } elseif ($loaded. WaveHistory -isnot [matriz]) {                 $loaded. WaveHistory = @($loaded. WaveHistory)             }             devolver $loaded         } captura {             Write-Log "Foram detetados RolloutState.json danificados: $($_. Exception.Message)" "WARN"             Write-Log "Criar cópias de segurança de ficheiros danificados e começar de novo" "AVISO"             $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyMmdd-HHmmss')"             Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue         }     }     devolver @{         CurrentWave = 0         StartedAt = $null         LastAggregation = $null         TotalDevicesTargeted = 0         TotalDevicesUpdated = 0         Estado = "NotStarted"         WaveHistory = @()     } }

function Save-RolloutState {     parâmetro($State)     $State | ConvertTo-Json -Profundidade 10 | Out-File $rolloutStatePath -Encoding UTF8 -Force }

function Get-WeekdayProjection {     <#     . SYNOPSIS         Calcular a contabilização da data de conclusão prevista para fins de semana (sem progresso em Sáb/Dom)     #>     parâmetro(         [int]$RemainingDevices,         [duplo]$DevicesPerDay,         [datetime]$StartDate = (Get-Date)     )     if ($DevicesPerDay -le 0 -or $RemainingDevices -le 0) {         devolver @{             ProjectedDate = $null             WorkingDaysNeeded = 0             CalendarDaysNeeded = 0         }     }     # Calcular os dias úteis necessários (excluindo os fins de semana)     $workingDaysNeeded = [matemática]::Teto($RemainingDevices/$DevicesPerDay)     # Converter dias úteis em dias de calendário (adicionar fins de semana)     $currentDate = $StartDate.Date     $daysAdded = 0     $workingDaysAdded = 0     while ($workingDaysAdded -lt $workingDaysNeeded) {         $currentDate = $currentDate.AddDays(1)         $daysAdded++         # Apenas contar dias da semana         if ($currentDate.DayOfWeek -ne [DayOfWeek]::Saturday -and             $currentDate.DayOfWeek -ne [DayOfWeek]::Sunday) {             $workingDaysAdded++         }     }     devolver @{         ProjectedDate = $currentDate.ToString("aaaa-MM-dd")         WorkingDaysNeeded = $workingDaysNeeded         CalendarDaysNeeded = $daysAdded     } }                                  

function Save-RolloutSummary {     <#     . SYNOPSIS         Guardar resumo da implementação com informações de projeção para dashboard apresentação     #>     parâmetro(         [hashtable]$State,         [int]$TotalDevices,         [int]$UpdatedDevices,         [int]$NotUpdatedDevices,         [double]$DevicesPerDay     )     $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     # Calcular projeção com deteção de fim de semana     $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         Estado = $State.Status         # Contagens de dispositivos         TotalDevices = $TotalDevices         UpdatedDevices = $UpdatedDevices         NotUpdatedDevices = $NotUpdatedDevices         PercentUpdated = if ($TotalDevices -gt 0) { [math]::Round(($UpdatedDevices/ $TotalDevices) * 100, 1) } else { 0 }         # Métricas de velocidade         DevicesPerDay = [math]::Round($DevicesPerDay, 1)         TotalDevicesTargeted = $State.TotalDevicesTargeted         TotalWaves = $State.CurrentWave         # Projeção com deteção de fim de semana         ProjectedCompletionDate = $projection. ProjectedDate         WorkingDaysRemaining = $projection. WorkingDaysNeeded         CalendarDaysRemaining = $projection. CalendarDaysNeeded         # Nota sobre a exclusão de fim de semana         ProjectionNote = "A conclusão prevista exclui os fins de semana (Sáb/Dom)"     }     $summary | ConvertTo-Json -Profundidade 5 | Out-File $summaryPath -Encoding UTF8 -Force     Write-Log "Resumo da implementação guardado: $summaryPath" "INFO"     devolver $summary }                                                             

function Get-BlockedBuckets {     if (Test-Path $blockedBucketsPath) {         devolver Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable     }     devolver @{} }

function Save-BlockedBuckets {     parâmetro($Blocked)     $Blocked | ConvertTo-Json -Profundidade 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force }

function Get-AdminApproved {     if (Test-Path $adminApprovedPath) {         devolver Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable     }     devolver @{} }

function Get-DeviceHistory {     if (Test-Path $deviceHistoryPath) {         devolver Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable     }     devolver @{} }

function Save-DeviceHistory {     parâmetro($History)     $History | ConvertTo-Json -Profundidade 10 | Out-File $deviceHistoryPath -Encoding UTF8 -Force }

function Save-ProcessingCheckpoint {     parâmetro(         [cadeia]$Stage,         [int]$Processed,         [int]$Total,         [hashtable]$Metrics = @{}     )

    $checkpoint = @{         Fase = $Stage         UpdatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"         Processado = $Processed         Total = $Total         Percentagem = se ($Total -gt 0) { [matemática]::Round(($Processed/ $Total) * 100, 2) } senão { 0 }         Métricas = $Metrics     }

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

function Get-NotUpdatedIndexes {     parâmetro([matriz]$Devices)

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

    foreach ($device in $Devices) {         $hostname = se ($device. Hostname) { $device. Hostname } elseif ($device. HostName) { $device. HostName } else { $null }         se ($hostname) {             [void]$hostSet.Add($hostname)         }

        $bucketKey = Get-BucketKey $device         se ($bucketKey) {             if ($bucketCounts.ContainsKey($bucketKey)) {                 $bucketCounts[$bucketKey]++             } senão {                 $bucketCounts[$bucketKey] = 1             }         }     }

    return @{         HostSet = $hostSet         BucketCounts = $bucketCounts     } }

function Write-Log {     parâmetro([string]$Message, [string]$Level = "INFO")     $timestamp = Get-Date -Formatar "aaaa-MM-dd HH:mm:ss"     $color = parâmetro ($Level) {         "OK" { "Verde" }         "AVISO" { "Amarelo" }         "ERROR" { "Red" }         "BLOCKED" { "DarkRed" }         "WAVE" { "Cyan" }         predefinição { "Branco" }     }     Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color     # Também inicie sessão no ficheiro     $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyMMdd').log"     "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Encoding UTF8 }               

function Get-BucketKey {     parâmetro($Device)     # Utilize BucketId do dispositivo JSON se disponível (hash SHA256 a partir do script de deteção)     if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" }     # Contingência: construção a partir do fabricante|modelo|bios     $mfr = se ($Device.WMI_Manufacturer) { $Device.WMI_Manufacturer } else { $Device.Manufacturer }     $model = se ($Device.WMI_Model) { $Device.WMI_Model } else { $Device.Model }     $bios = se ($Device.BIOSDescription) { $Device.BIOSDescription } else { $Device.BIOS }     devolver "$mfr|$model|$bios" }

# ============================================================================ # VIP/EXCLUSION LIST LOADING # ============================================================================

function Get-ExcludedHostnames {     parâmetro(         [cadeia]$ExclusionFilePath,         [string]$ADGroupName     )     $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)     # Carregar a partir do ficheiro (suporta .txt ou .csv)     if ($ExclusionFilePath -and (Test-Path $ExclusionFilePath)) {         $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower()         if ($extension -eq ".csv") {             # Formato CSV: espera uma coluna "Hostname" ou "ComputerName"             $csvData = Import-Csv $ExclusionFilePath             $hostCol = se ($csvData[0]. PSObject.Properties.Name -contains 'Hostname') { 'Hostname' }                        elseif ($csvData[0]. PSObject.Properties.Name -contains 'ComputerName') { 'ComputerName' }                        elseif ($csvData[0]. PSObject.Properties.Name -contains 'Name') { 'Name' }                        else { $null }             se ($hostCol) {                 foreach ($row em $csvData) {                     se (![ string]::IsNullOrWhiteSpace($row.$hostCol)) {                         [void]$excluded. Add($row.$hostCol.Trim())                     }                 }             }         } senão {             # Texto simples: um nome de anfitrião por linha             Get-Content $ExclusionFilePath | ForEach-Object {                 $line = $_. Cortar()                 if ($line -and -not $line. StartsWith('#')) {                     [void]$excluded. Adicionar($line)                 }             }         }         Write-Log "Carregado $($excluded. Count) hostnames from exclusion file: $ExclusionFilePath" "INFO"     }     # Carregar a partir do grupo de segurança do AD     se ($ADGroupName) {         experimente {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'computer' }             foreach ($member no $groupMembers) {                 [void]$excluded. Adicionar($member. Nome)             }             Write-Log computadores "Carregados $($groupMembers.Count) do grupo do AD: $ADGroupName" "INFO"         } captura {             Write-Log "Não foi possível carregar o grupo do AD '$ADGroupName': $_" "WARN"         }     }     devolver @($excluded) }                                                                             

# ============================================================================ # PERMITIR CARREGAMENTO DA LISTA (Implementação Direcionada) # ============================================================================

function Get-AllowedHostnames {     <#     . SYNOPSIS         Carrega nomes de anfitrião de um ficheiro AllowList e/ou grupo do AD para a implementação direcionada.Quando uma Lista De Permissões é especificada, APENAS estes dispositivos serão incluídos na implementação.#>     parâmetro(         [cadeia]$AllowFilePath,         [cadeia]$ADGroupName     )          $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)          # Carregar a partir do ficheiro (suporta .txt ou .csv)     if ($AllowFilePath -and (Test-Path $AllowFilePath)) {         $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower()                  if ($extension -eq ".csv") {             # Formato CSV: espera uma coluna "Hostname" ou "ComputerName"             $csvData = Import-Csv $AllowFilePath             if ($csvData.Count -gt 0) {                 $hostCol = se ($csvData[0]. PSObject.Properties.Name -contains 'Hostname') { 'Hostname' }                            elseif ($csvData[0]. PSObject.Properties.Name -contains 'ComputerName') { 'ComputerName' }                            elseif ($csvData[0]. PSObject.Properties.Name -contains 'Name') { 'Name' }                            else { $null }                                  se ($hostCol) {                     foreach ($row no $csvData) {                         se (![ string]::IsNullOrWhiteSpace($row.$hostCol)) {                             [vazio]$allowed. Add($row.$hostCol.Trim())                         }                     }                 }             }         } senão {             # Texto simples: um nome de anfitrião por linha             Get-Content $AllowFilePath | ForEach-Object {                 $line = $_. Cortar()                 if ($line -and -not $line. StartsWith('#')) {                     [vazio]$allowed. Adicionar($line)                 }             }         }                  Write-Log "Carregado $($allowed. Count) hostnames from allow list file: $AllowFilePath" "INFO"     }          # Carregar a partir do grupo de segurança do AD     se ($ADGroupName) {         experimente {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'computer' }                          foreach ($member no $groupMembers) {                 [vazio]$allowed. Adicionar($member. Nome)             }                          Write-Log "Computadores carregados $($groupMembers.Count) do grupo de permissões do AD: $ADGroupName" "INFO"         } captura {             Write-Log "Não foi possível carregar o grupo do AD '$ADGroupName': $_" "WARN"         }     }          devolver @($allowed) }

# ============================================================================ # ATUALIZAÇÃO E MONITORIZAÇÃO DE DADOS # ============================================================================

function Get-DataFreshness {     <#     . SYNOPSIS         Verifica a atualização dos dados de deteção ao examinar os carimbos de data/hora do ficheiro JSON.Devolve estatísticas sobre quando os pontos finais foram reportados pela última vez.#>     parâmetro([string]$JsonPath)     $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue     if ($jsonFiles.Count -eq 0) {         devolver @{             TotalFiles = 0             FreshFiles = 0             StaleFiles = 0             NoDataFiles = 0             Ficheiro Mais Antigo = $null             NewestFile = $null             AvgAgeHours = 0             Aviso = "Não foram encontrados ficheiros JSON – a deteção pode não ser implementada"         }     }     $now = Data de Obtenção     $freshThresholdHours = 24 # Files atualizados nas últimas 24 horas estão "frescos"     $staleThresholdHours = 72 # Files mais de 72 horas estão "obsoletos"     $fresh = 0     $stale = 0     $ages = @()     foreach ($file no $jsonFiles) {         $ageHours = ($now - $file. LastWriteTime). TotalHours         $ages += $ageHours         if ($ageHours -le $freshThresholdHours) {             $fresh++         } elseif ($ageHours -ge $staleThresholdHours) {             $stale++         }     }     $oldestFile = $jsonFiles | Sort-Object LastWriteTime | Select-Object -Primeiro 1     $newestFile = $jsonFiles | Sort-Object LastWriteTime -Descending | Select-Object -Primeiro 1     $warning = $null     if ($stale -gt ($jsonFiles.Count * 0.5)) {         $warning = "Mais de 50% dos dispositivos têm dados obsoletos (>72 horas) - marcar GPO de deteção"     } elseif ($fresh -lt ($jsonFiles.Count * 0.3)) {         $warning = "Menos de 30% dos dispositivos comunicados recentemente - a deteção pode não estar em execução"     }     devolver @{         TotalFiles = $jsonFiles.Count         FreshFiles = $fresh         StaleFiles = $stale         MediumFiles = $jsonFiles.Count - $fresh - $stale         Ficheiro Mais Antigo = $oldestFile.LastWriteTime         NewestFile = $newestFile.LastWriteTime         AvgAgeHours = [matemática]::Round(($ages | Measure-Object -Average). Média, 1)         Aviso = $warning     } }                                                 

function Test-DetectionGPODeployed {     <#     . SYNOPSIS         Verifica se a infraestrutura de deteção/monitorização está implementada.#>     parâmetro([string]$JsonPath)     # Verificar 1: O caminho JSON existe     if (-not (Test-Path $JsonPath)) {         devolver @{             IsDeployed = $false             Mensagem = "O caminho de entrada JSON não existe: $JsonPath"         }     }     # Verificar 2: Existem, pelo menos, alguns ficheiros JSON     $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). Contagem     if ($jsonCount -eq 0) {         devolver @{             IsDeployed = $false             Message = "No JSON files in $JsonPath - Detection GPO may not be deployed or devices haven't reported yet"         }     }     # Verificar 3: Files são razoavelmente recentes (pelo menos alguns na semana passada)     $recentFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue |         Where-Object { $_. LastWriteTime -gt (Get-Date). AddDays(-7) }     if ($recentFiles.Count -eq 0) {         devolver @{             IsDeployed = $false             Mensagem = "Nenhum ficheiro JSON atualizado nos últimos 7 dias – O GPO de Deteção pode estar danificado ou os dispositivos offline"         }     }     devolver @{         IsDeployed = $true         Mensagem = "A deteção aparece ativa: $jsonCount ficheiros, $($recentFiles.Count) atualizados recentemente"         FileCount = $jsonCount         RecentCount = $recentFiles.Count     } }                         

# ============================================================================ # CONTROLO DE DISPOSITIVOS (POR HOSTNAME) # ============================================================================

function Update-DeviceHistory {     <#     . SYNOPSIS         Controla os dispositivos por nome de anfitrião, uma vez que não temos um identificador de computador exclusivo.Nota: BucketId é um-para-muitos (a mesma configuração de hardware = mesmo registo).Se for adicionado um identificador exclusivo à coleção JSON, atualize esta função.#>     parâmetro(         [matriz]$CurrentDevices,         [hashtable]$DeviceHistory     )          foreach ($device em $CurrentDevices) {         $hostname = $device. Nome do anfitrião         if (-not $hostname) { continue }                  # Controlar o dispositivo por nome de anfitrião         $DeviceHistory[$hostname] = @{             Hostname = $hostname             BucketId = $device. BucketId             Fabricante = $device. WMI_Manufacturer             Modelo = $device. WMI_Model             LastSeen = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             Estado = $device. UpdateStatus         }     } }

# ============================================================================ # BLOCKED BUCKET DETECTION (Com base na Acessibilidade do Dispositivo) # ============================================================================

<# . DESCRIÇÃO     Lógica de Bloqueio:     - Um registo só será bloqueado se:       1. O dispositivo foi visado numa onda       2. MaxWaitHours passou desde o início da onda       3. O dispositivo NÃO está ACESSÍVEL (o ping falha)          - Se o dispositivo estiver acessível, mas ainda não estiver atualizado, continuamos à espera       (a atualização pode estar pendente de reinício – O evento 1808 só é acionado após o reinício)          - Dispositivo inacessível indica que algo correu mal e precisa de investigação          Desbloquear:     - Utilize -ListBlockedBuckets para ver registos bloqueados     - Utilize -UnblockBucket "BucketKey" para desbloquear um registo específico     - Utilize -UnblockAll para desbloquear todos os registos #>

function Test-DeviceReachable {     parâmetro(         [cadeia]$Hostname,         [string]$DataPath # Path to device JSON files     )     # Method 1: Check JSON file timestamp (fastest — no file parsing needed)     # Se o script de deteção foi executado recentemente, o ficheiro foi escrito/atualizado, provando que o dispositivo está ativo     se ($DataPath) {         $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object -Primeiro 1         se ($deviceFile) {             $hoursSinceWrite = ((Get-Date) - $deviceFile.LastWriteTime). TotalHours             if ($hoursSinceWrite -lt 72) { return $true }         }     }     # Método 2: Contingência para ping (apenas se JSON estiver obsoleto ou em falta)     experimente {         $ping = Test-Connection -ComputerName $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue         devolver $ping     } captura {         devolver $false     } }          

function Update-BlockedBuckets {     parâmetro(         $RolloutState,         $BlockedBuckets,         $AdminApproved,         [matriz]$NotUpdatedDevices,         [tabela hash]$NotUpdatedIndexes,         [int]$MaxWaitHours,         [bool]$DryRun = $false     )     $now = Data de Obtenção     $newlyBlocked = @()     $stillWaiting = @()     $devicesToCheck = @()     $hostSet = se ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet }     $bucketCounts = se ($NotUpdatedIndexes -e $NotUpdatedIndexes.BucketCounts) { $NotUpdatedIndexes.BucketCounts } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). BucketCounts }     # Recolha dispositivos que já passaram do período de espera e que ainda não foram atualizados     foreach ($wave em $RolloutState.WaveHistory) {         if (-not $wave. StartedAt) { continue }         $waveStart = [DateTime]::P arse($wave. StartedAt)         $hoursSinceWave = ($now - $waveStart). TotalHours         if ($hoursSinceWave -lt $MaxWaitHours) {             # Ainda dentro do período de espera - ainda não marcar             continuar         }         # Verifique cada dispositivo a partir desta onda         foreach ($deviceInfo no $wave. Dispositivos) {             $hostname = $deviceInfo.Hostname             $bucketKey = $deviceInfo.BucketKey             # Ignorar se o registo já estiver bloqueado             se ($BlockedBuckets.Contains($bucketKey)) { continue }             # Ignorar se o registo for aprovado pelo administrador E fase iniciada ANTES da aprovação             # (apenas marcar dispositivos visados após a aprovação do administrador para novo bloqueio)             if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) {                 $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. ApprovedAt)                 if ($waveStart -lt $approvalTime) {                     # Este dispositivo foi visado antes da aprovação do administrador – ignorar                     continuar                 }                 # Wave started after approval - this is fresh targeting, can marcar             }             # Este dispositivo ainda está na lista Não Desatualizado?             if ($hostSet.Contains($hostname)) {                 $devicesToCheck += @{                     Hostname = $hostname                     BucketKey = $bucketKey                     WaveNumber = $wave. WaveNumber                     HoursSinceWave = [math]::Round($hoursSinceWave, 1)                 }             }         }     }     if ($devicesToCheck.Count -eq 0) {         devolver $newlyBlocked     }     Write-Log "Verificar a acessibilidade dos dispositivos $($devicesToCheck.Count) no último período de espera..." "INFORMAÇÕES"     # Controlar falhas por registo para tomar decisões     $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() }     # Verificar a acessibilidade de cada dispositivo     foreach ($device no $devicesToCheck) {         $hostname = $device. Nome do anfitrião         $bucketKey = $device. BucketKey         se ($DryRun) {             Write-Log "[DRYRUN] Would marcar $hostname reachability" "INFO"             continuar         }         if (-not $bucketFailures.ContainsKey($bucketKey)) {             $bucketFailures[$bucketKey] = @{ Inacessível = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. HoursSinceWave }         }         $isReachable = Test-DeviceReachable -Hostname $hostname -DataPath $AggregationInputPath         if (-not $isReachable) {             $bucketFailures[$bucketKey]. Inacessível += $hostname         } senão {             # O dispositivo está acessível, mas ainda não está atualizado – pode ser uma falha temporária ou aguardar pelo reinício             $bucketFailures[$bucketKey]. AliveButFailed += $hostname             $stillWaiting += $hostname         }     }     # Decisão por registo: apenas bloquear se os dispositivos estiverem verdadeiramente INACESSÍVEIS     #Alive devices with failures = temporary, continue a implementação     foreach ($bucketKey em $bucketFailures.Keys) {         $bf = $bucketFailures[$bucketKey]         $unreachableCount = $bf. Inacessível.Contagem         $aliveFailedCount = $bf. AliveButFailed.Count         # Verifique se este registo tem êxito (a partir de dados de dispositivos atualizados)         $bucketHasSuccesses = $stSuccessBuckets -and $stSuccessBuckets.Contains($bucketKey)         if ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) {             # TODOS os dispositivos com falhas estão inacessíveis – bloqueie o registo             if ($newlyBlocked -notcontains $bucketKey) {                 $BlockedBuckets[$bucketKey] = @{                     BlockedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"                     Reason = "All $unreachableCount device(s) unreachable after $($bf. HoursSinceWave) horas"                     FailedDevices = ($bf. Unreachable -join", ")                     WaveNumber = $bf. WaveNumber                     DevicesInBucket = se ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } senão { 0 }                 }                 $newlyBlocked += $bucketKey                 Write-Log "BUCKET BLOCKED: $bucketKey ($unreachableCount dispositivo(s) inacessível: $($bf). Inacessível -join ', '))' "BLOCKED"             }         } elseif ($aliveFailedCount -gt 0) {             # Os dispositivos estão ativos, mas não estão atualizados – falha temporária, BLOCO DO NOT             Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length)))...: $aliveFailedCount dispositivo(s) ativo mas pendente, $unreachableCount inacessível - NÃO bloquear (temporário)" "INFO"             se ($unreachableCount -gt 0) {                 Write-Log " Inacessível: $($bf. Inacessível -join ', ')' "WARN"             }             Write-Log " Alive, mas pendente: $($bf. AliveButFailed -join ', ')' "INFO"             # Controlar a contagem de falhas no estado de implementação para monitorização             if (-not $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} }             $RolloutState.TemporaryFailures[$bucketKey] = @{                 AliveButFailed = $bf. AliveButFailed                 Inacessível = $bf. Inacessível                 LastChecked = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             }         }     }     if ($stillWaiting.Count -gt 0) {         Write-Log "Dispositivos acessíveis, mas com atualização pendente (poderá ser necessário reiniciar): $($stillWaiting.Count)" "INFO"     }     devolver $newlyBlocked }                                                                                                                                                                                  

# ============================================================================ # AUTO-UNBLOCK: Desbloquear registos quando os dispositivos forem atualizados com êxito # ============================================================================

function Update-AutoUnblockedBuckets {     <#     . DESCRIÇÃO         Verifica se os dispositivos em registos bloqueados foram atualizados (Evento 1808).         Desbloqueia automaticamente se todos os dispositivos visados no registo tiverem sido atualizados.Se apenas alguns dispositivos forem atualizados, notifica o administrador que pode desbloquear manualmente.                  Administração pode desbloquear manualmente com:           .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "path" -UnblockBucket "BucketKey"     #>     parâmetro(         $BlockedBuckets,         $RolloutState,         [matriz]$NotUpdatedDevices,         [cadeia]$ReportBasePath,         [tabela hash]$NotUpdatedIndexes,         [int]$LogSampleSize = 25     )     $autoUnblocked = @()     $bucketsToCheck = @($BlockedBuckets.Keys)     $hostSet = se ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet }     foreach ($bucketKey no $bucketsToCheck) {         $bucketInfo = $BlockedBuckets[$bucketKey]         # Obtenha todos os dispositivos visados a partir deste registo historicamente         $targetedDevicesInBucket = @()         foreach ($wave em $RolloutState.WaveHistory) {             $targetedDevicesInBucket += @($wave. Dispositivos | Where-Object { $_. BucketKey -eq $bucketKey })         }         se ($targetedDevicesInBucket.Count -eq 0) { continue }         # Verifique quantos dispositivos visados ainda se encontram em Não Atualizados vs. atualizados         $updatedDevices = @()         $stillPendingDevices = @()         foreach ($targetedDevice no $targetedDevicesInBucket) {             if ($hostSet.Contains($targetedDevice.Hostname)) {                 $stillPendingDevices += $targetedDevice.Hostname             } senão {                 $updatedDevices += $targetedDevice.Hostname             }         }         if ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) {             # TODOS os dispositivos visados foram atualizados – desbloqueio automático!             $BlockedBuckets.Remove($bucketKey)             $autoUnblocked += @{                 BucketKey = $bucketKey                 UpdatedDevices = $updatedDevices                 PreviouslyBlockedAt = $bucketInfo.BlockedAt                 Reason = "All $($updatedDevices.Count) targeted device(s) targeted device(s) successfully updated"             }             Write-Log "AUTO-UNBLOCKED: $bucketKey (Todos os dispositivos visados por $($updatedDevices.Count) atualizados com êxito)" "OK"             # Incrementar contagem de ondas OEM para o OEM deste registo (controlo por OEM)             $bucketOEM = se ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' } # Extract OEM from pipe-delimited key or default             if (-not $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             }             $currentWave = se ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } else { 0 }             $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1             Write-Log " Contagem de ondas OEM '$bucketOEM' incrementada para $($currentWave + 1) (próxima alocação: $([int][Matemática]::P ow(2, $currentWave + 1)) dispositivos)" "INFO"         }         elseif ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -gt 0) {             # ALGUNS dispositivos atualizados, mas outros ainda estão pendentes – notifique o administrador (apenas uma vez)             if (-not $bucketInfo.UnblockCandidate) {                 $bucketInfo.UnblockCandidate = $true                 $bucketInfo.UpdatedDevices = $updatedDevices                 $bucketInfo.PendingDevices = $stillPendingDevices                 $bucketInfo.NotifiedAt = (Get-Date). ToString("aaaa-MM-dd HH:mm:ss")                 Write-Log "" "INFO"                 Write-Log "========== ATUALIZAÇÃO PARCIAL NO REGISTO BLOQUEADO ==========" "INFORMAÇÕES"                 Write-Log "Bucket: $bucketKey" "INFO"                 $updatedSample = @($updatedDevices | Select-Object -First $LogSampleSize)                 $pendingSample = @($stillPendingDevices | Select-Object -First $LogSampleSize)                 $updatedSuffix = se ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) more)" } senão { "" }                 $pendingSuffix = se ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) more)" } senão { "" }                 Write-Log "Dispositivos atualizados ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix' "OK"                 Write-Log "Ainda pendente ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix' "WARN"                 Write-Log "" "INFO"                 Write-Log "Para desbloquear manualmente este registo após a verificação, execute:" "INFO"                 Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '"$ReportBasePath"" -UnblockBucket '"$bucketKey"" "INFO"                 Write-Log "=======================================================" "INFO"                 Write-Log "" "INFO"             }         }     }     devolver $autoUnblocked }                                                                                          

# ============================================================================ # WAVE GENERATION (INLINED - exclui registos bloqueados) # ============================================================================

function New-RolloutWave {     parâmetro(         [cadeia]$AggregationPath,         $BlockedBuckets,         $RolloutState,         [int]$MaxDevicesPerWave = 50,         [string[]]$AllowedHostnames = @(),         [string[]]$ExcludedHostnames = @()     )     # Carregar dados de agregação     $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" |          Where-Object { $_. Nome -notlike "*Buckets*" } |          Sort-Object LastWriteTime -Descending |          Select-Object -Primeiro 1     if (-not $notUptodateCsv) {         Write-Log "Não foi encontrado nenhum CSV NotUptodate" "ERROR"         devolver $null     }     $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName)     # Normalize HostName -> Hostname for consistency (CSV uses HostName, code uses Hostname)     foreach ($device no $allNotUpdated) {         if ($device. PSObject.Properties['HostName'] -and -not $device. PSObject.Properties['Hostname']) {             $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName -Force         }     }     # Filtrar registos bloqueados     $eligibleDevices = @($allNotUpdated | Where-Object {         $bucketKey = Get-BucketKey $_         -not $BlockedBuckets.Contains($bucketKey)     })     # Filtre para apenas dispositivos permitidos (se a Lista De Permissões for especificada)     # AllowList = implementação direcionada - apenas estes dispositivos serão considerados     if ($AllowedHostnames.Count -gt 0) {         $beforeCount = $eligibleDevices.Count         $eligibleDevices = @($eligibleDevices | Where-Object {             $_. Hostname -in $AllowedHostnames         })         $allowedCount = $eligibleDevices.Count         Write-Log "AllowList applied: $allowedCount of $beforeCount devices are in allow list" "INFO"     }     # Filtre os dispositivos VIP/excluídos (BlockList)     # BlockList is applied AFTER AllowList     if ($ExcludedHostnames.Count -gt 0) {         $beforeCount = $eligibleDevices.Count         $eligibleDevices = @($eligibleDevices | Where-Object {             $_. Hostname -notin $ExcludedHostnames         })         $excludedCount = $beforeCount - $eligibleDevices.Count         se ($excludedCount -gt 0) {             Write-Log "Excluído $excludedCount VIP/dispositivos protegidos da implementação" "INFO"         }     }     if ($eligibleDevices.Count -eq 0) {         Write-Log "Não restam dispositivos elegíveis (todos atualizados ou bloqueados)" "OK"         devolver $null     }     # Obter dispositivos já em implementação (de ondas anteriores)     $devicesAlreadyInRollout = @()     if ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) {         $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object {             $_. Dispositivos | ForEach-Object { $_. Nome do anfitrião }         } | Where-Object { $_ })     }     Write-Log "Dispositivos já em implementação: $($devicesAlreadyInRollout.Count)" "INFO"     # Separe por nível de confiança     $highConfidenceDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel -eq "Alta Confiança" -e         $_. Hostname -notin $devicesAlreadyInRollout     })     #Ação Necessária inclui:     # - "Ação Necessária" Explícita     # - Empty/null ConfidenceLevel     # - QUALQUER valor desconhecido/não reconhecido ConfidenceLevel (tratado como Ação Necessária)     $knownSafeCategories = @(         "Alta Confiança",         "Temporariamente em Pausa",         "Em Observação",         "Em Observação – Mais Dados Necessários",         "Não Suportado",         "Não Suportado - Limitação Conhecida"     )     $actionRequiredDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel - notin $knownSafeCategories - e         $_. Hostname -notin $devicesAlreadyInRollout     })     Write-Log "Elevada Confiança (não na implementação): $($highConfidenceDevices.Count)" "INFO"     Write-Log "Ação Necessária (não na implementação): $($actionRequiredDevices.Count)" "INFO"     # Criar dispositivos de onda     $waveDevices = @()     # CONFIANÇA ELEVADA: Incluir TUDO (seguro para implementação)     if ($highConfidenceDevices.Count -gt 0) {         Write-Log "Adding all $($highConfidenceDevices.Count) High Confidence devices" "WAVE"         $waveDevices += $highConfidenceDevices     } # AÇÃO NECESSÁRIA: Implementação progressiva (baseada em registos com propagação OEM para registos sem êxito)     # Estratégia:     # - Registos com 0 êxitos: distribuídos por OEMs (1 por OEM -> 2 por OEM -> 4 por OEM)     # - Buckets with ≥1 success: Double freely without OEM restriction (Registos com êxito ≥1: duplo livremente sem restrição de OEM)     if ($actionRequiredDevices.Count -gt 0) {         # Load bucket success counts from updated devices CSV (devices that successfully updated)         $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" |             Sort-Object LastWriteTime -Descending | Select-Object -Primeiro 1         $bucketStats = @{}         se ($updatedCsv) {             $updatedDevices = Import-Csv $updatedCsv.FullName             # Contagem de êxitos por BucketId             $updatedDevices | ForEach-Object {                 $key = Get-BucketKey $_                 se ($key) {                     if (-not $bucketStats.ContainsKey($key)) {                         $bucketStats[$key] = @{ Successes = 0; Pendente = 0; Total = 0 }                     }                     $bucketStats[$key]. Êxitos++                     $bucketStats[$key]. Total++                 }             }             Write-Log dispositivos "Carregados $($updatedDevices.Count) atualizados em registos $($bucketStats.Count)" "INFO"         } senão {             # Contingência: experimente ActionRequired_Buckets CSV             $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" |                 Sort-Object LastWriteTime -Descending | Select-Object -Primeiro 1             se ($bucketsCsv) {                 Import-Csv $bucketsCsv.FullName | ForEach-Object {                     $key = se ($_. BucketId) { $_. BucketId } senão { "$($_. Fabricante)|$($_. Modelo)|$($_. BIOS)" }                     $bucketStats[$key] = @{                         Successes = [int]$_. Êxitos                         Pendente = [int]$_. Pendente                         Total = [int]$_. TotalDevices                     }                 }             }         }         # Agrupar dispositivos Não Desatualizados por registo (Fabricante|Modelo|BIOS)         $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ }         # Registos separados: zero-success vs has-success         $zeroSuccessBuckets = @()         $hasSuccessBuckets = @()         foreach ($bucket no $buckets) {             $bucketKey = $bucket. Nome             $bucketDevices = @($bucket. Grupo)             $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Nome do anfitrião })             # Contagem de êxitos neste registo             $stats = $bucketStats[$bucketKey]             $successes = se ($stats) { $stats. Successes } else { 0 }             # Localizar dispositivos implementados neste registo a partir do histórico de ondas             $deployedToBucket = @()             foreach ($wave em $RolloutState.WaveHistory) {                 foreach ($device em $wave. Dispositivos) {                     if ($device. BucketKey -eq $bucketKey - e $device. Nome do anfitrião) {                         $deployedToBucket += $device. Nome do anfitrião                     }                 }             }             $deployedToBucket = @($deployedToBucket | Sort-Object -Unique)             # Verifique se todos os dispositivos implementados reportaram êxito             $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames })             $confirmedSuccess = $deployedToBucket.Count - $stillPending.Count             # Se estiver pendente, ignore este registo até que todos confirmem             if ($stillPending.Count -gt 0) {                 $parts = $bucketKey -split '\|'                 $displayName = "$($parts[0]) - $($parts[1])"                 Write-Log " Bucket: $displayName - Deployed=$($deployedToBucket.Count), Confirmed=$confirmedSuccess, Pending=$($stillPending.Count) (waiting)" "INFO"                 continuar             }             # Elegível restante = dispositivos ainda não implementados             $devicesNotYetTargeted = @($bucketDevices | Where-Object {                 $_. Hostname -notin $deployedToBucket             })             se ($devicesNotYetTargeted.Count -eq 0) { continue }             # Categorizar por contagem de êxito             $bucketInfo = @{                 BucketKey = $bucketKey                 Dispositivos = $devicesNotYetTargeted                 ConfirmedSuccess = $confirmedSuccess                 Êxitos = $successes                 OEM = se ($bucket. Grupo[0]. WMI_Manufacturer) { $bucket. Grupo[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } senão { 'Desconhecido' }             }             if ($successes -eq 0) {                 $zeroSuccessBuckets += $bucketInfo             } senão {                 $hasSuccessBuckets += $bucketInfo             }         }         # === REGISTOS PROCESS HAS-SUCCESS (≥1 com êxito) ===         # Duplique o número de êxitos — se 14 tiver êxito, implemente 28 em seguida         foreach ($bucketInfo em $hasSuccessBuckets) {             $nextBatchSize = $bucketInfo.Successes * 2             $nextBatchSize = [Matemática]::Min($nextBatchSize, $MaxDevicesPerWave)             $nextBatchSize = [Matemática]::Min($nextBatchSize, $bucketInfo.Devices.Count)             se ($nextBatchSize -gt 0) {                 $selectedDevices = @($bucketInfo.Devices | Select-Object -First $nextBatchSize)                 $waveDevices += $selectedDevices                 $parts = se ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length))) }                 $displayName = "$($parts[0]) - $($parts[1])"                 Write-Log " [HAS-SUCCESS] $displayName - Successes=$($bucketInfo.Successes), Deploying=$nextBatchSize (2x confirmed)" "INFO"             }         }         # === PROCESS ZERO-SUCCESS BUCKETS (distribuído por OEMs com controlo por OEM) ===         # Objetivo: distribuir o risco por diferentes OEMs, controlar o progresso por OEM de forma independente         # Cada OEM progride com base no seu próprio histórico de sucesso:         # - OEM com êxitos: Obtém mais dispositivos na próxima onda (2^waveCount)         # - OEM sem êxitos: permanece no nível atual até que o sucesso seja confirmado         if ($zeroSuccessBuckets.Count -gt 0) {             # Initialize per-OEM wave counts if not exists             if (-not $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             }             # Agrupar registos sem êxito por OEM             $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM }             $totalZeroSuccessAdded = 0             $oemsDeployedTo = @()             foreach ($oemGroup no $oemBuckets) {                 $oemName = $oemGroup.Name                 # Obter a contagem de ondas deste OEM (começa em 0)                 $oemWaveCount = se ($RolloutState.OEMWaveCounts[$oemName]) {                     $RolloutState.OEMWaveCounts[$oemName]                 } senão { 0 }                 # Calcular dispositivos para ESTE OEM: 2^waveCount (1, 2, 4, 8...)                 $devicesForThisOEM = [int][Matemática]::P ow(2, $oemWaveCount)                 $devicesForThisOEM = [Matemática]::Máx.(1, $devicesForThisOEM)                 $oemDevicesAdded = 0                 # Escolha a partir de cada registo abaixo deste OEM                 foreach ($bucketInfo em $oemGroup.Group) {                     $remaining = $devicesForThisOEM - $oemDevicesAdded                     if ($remaining -le 0) { break }                     $toTake = [Matemática]::Min($remaining, $bucketInfo.Devices.Count)                     se ($toTake -gt 0) {                         $selectedDevices = @($bucketInfo.Devices | Select-Object -First $toTake)                         $waveDevices += $selectedDevices                         $oemDevicesAdded += $toTake                         $totalZeroSuccessAdded += $toTake                         $parts = se ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length))) }                         $displayName = "$($parts[0]) - $($parts[1])"                         Write-Log " [ZERO-SUCCESS] $displayName - Deploying=$toTake (OEM wave $oemWaveCount = ${devicesForThisOEM}/OEM)" "WARN"                     }                 }                 se ($oemDevicesAdded -gt 0) {                     Write-Log " OEM: $oemName - Wave $oemWaveCount, Added $oemDevicesAdded devices" "INFO"                     $oemsDeployedTo += $oemName                 }             }             # Controle os OEMs em que implementámos (para incrementar na próxima marcar de sucesso)             if ($oemsDeployedTo.Count -gt 0) {                 $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo                 Write-Log "Implementação sem êxito: $totalZeroSuccessAdded dispositivos em OEMs $($oemsDeployedTo.Count) " "INFO"             }         }     }     se (@($waveDevices). Contagem -eq 0) {         devolver $null     }     devolver $waveDevices }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

# ============================================================================ # IMPLEMENTAÇÃO DE GPO (INLINED - cria GPO, grupo de segurança, ligações) # ============================================================================

function Deploy-GPOForWave {     parâmetro(         [cadeia]$GPOName,         [cadeia]$TargetOU,         [cadeia]$SecurityGroupName,         [matriz]$WaveHostnames,         [bool]$DryRun = $false     )     # ADMX Policy: SecureBoot.admx - SecureBoot_AvailableUpdatesPolicy     # Caminho do Registo: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot     # Nome do Valor: AvailableUpdatesPolicy     # Valor Ativado: 22852 (0x5944) – Atualizar todas as teclas de Arranque Seguro + bootmgr     # Valor Desativado: 0     N.º     # Using Política de Grupo Preferences (GPP) for reliable HKLM\SYSTEM path deployment (Preferências de Política de Grupo (GPP) para a implementação fiável do caminho HKLM\SYSTEM)     # GPP cria definições em: Configuração do Computador > Preferências > Definições do Windows > Registo     $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot"     $RegistryValueName = "AvailableUpdatesPolicy"     $RegistryValue = 22852 # 0x5944 - corresponde a ADMX enabledValue     Write-Log "Deploying GPO: $GPOName" "WAVE"     Write-Log "Registo: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X')))" "INFO"     se ($DryRun) {         Write-Log "[DRYRUN] Iria criar GPO: $GPOName" "INFO"         Write-Log "[DRYRUN] Iria criar um grupo de segurança: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Adicionaria $(@($WaveHostnames). Contar) computadores a agrupar" "INFO"         Write-Log "[DRYRUN] Iria ligar o GPO a: $TargetOU" "INFO"         devolver $true     }     experimente {         # Importar módulos necessários         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } captura {         Write-Log "Falha ao importar os módulos necessários (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR"         devolver $false     }     # Passo 1: Criar ou obter GPO     $existingGPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     se ($existingGPO) {         Write-Log "GPO já existe: $GPOName" "INFO"         $gpo = $existingGPO     } senão {         experimente {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Certificate Rollout - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "GPO Criado: $GPOName" "OK"         } captura {             Write-Log "Falha ao criar GPO: $($_. Exception.Message)" "ERROR"             devolver $false         }     }     N.º Passo 2: definir o valor do registo com preferências de Política de Grupo (GPP)     # GPP é mais fiável para caminhos HKLM\SYSTEM do que Set-GPRegistryValue     experimente {         # Primeiro, tente remover qualquer preferência existente para este valor (para evitar duplicados)         Remove-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue         # Criar preferência de registo GPP com a ação "Substituir"         # Replace = Create if not exists, Update if exists (most reliable)         # Atualização = Apenas atualização se existir (falha se o valor não existir)         Set-GPPrefRegistryValue -Name $GPOName '             -Context Computer '             -Action Replace '             -Key $RegistryKey '             -ValueName $RegistryValueName '             -Escreva DWord '             -Value $RegistryValue         Write-Log "Preferência de registo GPP configurada: $RegistryValueName = 0x5944 (Action=Replace)" "OK"     } captura {         Write-Log "O GPP falhou ao tentar Set-GPRegistryValue: $($_. Exception.Message)" "WARN"         # Contingência para Set-GPRegistryValue (funciona se o ADMX estiver implementado)         experimente {             Set-GPRegistryValue -Name $GPOName '                 -Key $RegistryKey '                 -ValueName $RegistryValueName '                 -Escreva DWord '                 -Value $RegistryValue             Write-Log "Registo configurado através de Set-GPRegistryValue: $RegistryValueName = 0x5944" "OK"         } captura {             Write-Log "Falha ao definir o valor do registo: $($_. Exception.Message)" "ERROR"             devolver $false         }     }     N.º 3: Criar ou obter um grupo de segurança     $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-not $existingGroup) {         experimente {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Descrição "Computadores direcionados para a implementação de Arranque Seguro - $GPOName" "                 -PassThru             Write-Log "Grupo de segurança criado: $SecurityGroupName" "OK"         } captura {             Write-Log "Falha ao criar o grupo de segurança: $($_. Exception.Message)" "ERROR"             devolver $false         }     } senão {         Write-Log "Grupo de segurança existe: $SecurityGroupName" "INFO"         $group = $existingGroup     }     # Passo 4: Adicionar computadores ao grupo de segurança     $added = 0     $failed = 0     foreach ($hostname em $WaveHostnames) {         experimente {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } captura {             $failed++         }     }     Write-Log "Adicionado $added computadores ao grupo de segurança ($failed não encontrados no AD)" "OK"     N.º 5: Configurar a filtragem de segurança no GPO     experimente {         # Remova a permissão "Utilizadores Autenticados" predefinidos (manter Leitura)         Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         # Adicionar permissão Aplicar para o nosso grupo de segurança         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Configuração da filtragem de segurança para: $SecurityGroupName" "OK"     } captura {         Write-Log "Falha ao configurar a filtragem de segurança: $($_. Exception.Message)" "WARN"         Write-Log "O GPO pode aplicar-se a todos os computadores na UO ligada – verificar manualmente" "AVISAR"     }     # Passo 6: Ligar o GPO à UO (CRÍTICO para a aplicação da política)     se ($TargetOU) {         experimente {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }             if (-not $existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop                 Write-Log "GPO ligado a: $TargetOU" "OK"                 Write-Log "O GPO será aplicado no próximo gpupdate nos computadores de destino" "INFO"             } senão {                 Write-Log "GPO já ligado à UO de destino" "INFO"             }         } captura {             Write-Log "CRÍTICO: Falha ao ligar o GPO à UO: $($_. Exception.Message)" "ERROR"             Write-Log "O GPO foi criado, mas NÃO LIGADO - NÃO se aplicará a nenhum computador!" "ERRO"             Write-Log "Correção manual necessária: New-GPLink -Name '$GPOName' -Target '$TargetOU' -LinkEnabled Yes" "ERROR"             devolver $false         }     } senão {         Write-Log "AVISO: Nenhum TargetOU especificado - GPO criado, mas NÃO LIGADO!" "ERRO"         Write-Log "Ligação manual necessária para que o GPO entre em vigor" "ERRO"         Write-Log "Run: New-GPLink -Name '$GPOName' -Target '<Your-Domain-DN>' -LinkEnabled Yes" "ERROR"     }     # Passo 7: Verificar a configuração do GPO     Write-Log "Verificar a configuração do GPO..." "INFORMAÇÕES"     experimente {         $gpoReport = Get-GPO -Name $GPOName -ErrorAction Stop         Write-Log "Estado do GPO: $($gpoReport.GpoStatus)" "INFO"         # Verifique se a definição de registo está configurada         $regSettings = Get-GPRegistryValue -Name $GPOName -Key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue         if (-not $regSettings) {             # Experimente marcar de registo GPP (caminho diferente no GPO)             Write-Log "Verificar as preferências de registo GPP..." "INFORMAÇÕES"         }     } captura {         Write-Log "Não foi possível verificar o GPO: $($_. Exception.Message)" "WARN"     }     devolver $true }                                                                                                

# ============================================================================ # WINCS DEPLOYMENT (Alternativa a AvailableUpdatesPolicy GPO) N.º ============================================================================ # Referência: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe N.º # Comandos WinCS (executar no ponto final no contexto SYSTEM): # Consulta: WinCsFlags.exe /query --key F33E0C8E002 # Aplicar: WinCsFlags.exe /apply --key "F33E0C8E002" # Repor: WinCsFlags.exe /reset --key "F33E0C8E002" N.º # Este método implementa um GPO com uma tarefa agendada que é executada WinCsFlags.exe /apply # como SYSTEM nos pontos finais de destino. Semelhante à forma como o script de deteção é implementado, # mas é executado uma vez (no arranque) em vez de diariamente.

function Deploy-WinCSGPOForWave {     <#     . SYNOPSIS         Implementar a ativação de Arranque Seguro do WinCS através da tarefa agendada de GPO.. DESCRIÇÃO         Cria um GPO que implementa uma tarefa agendada para executar WinCsFlags.exe /apply         em Contexto DE SISTEMA no arranque do computador. Filtragem de controlos de grupo de segurança.. PARÂMETRO GPOName         Nome do GPO.. PARAMETER TargetOU         UO para ligar o GPO a.. PARAMETER SecurityGroupName         Grupo de segurança para filtragem de GPO.. PARAMETER WaveHostnames         Nomes de anfitrião a adicionar ao grupo de segurança.. Parâmetro WinCSKey         A chave WinCS a aplicar (predefinição: F33E0C8E002).. DryRun do PARÂMETRO         Se for verdadeiro, registe apenas o que seria feito.#>     parâmetro(         [Parameter(Mandatory = $true)]         [cadeia]$GPOName,                  [Parameter(Mandatory = $false)]         [cadeia]$TargetOU,                  [Parameter(Mandatory = $true)]         [cadeia]$SecurityGroupName,                  [Parameter(Mandatory = $true)]         [matriz]$WaveHostnames,                  [Parameter(Mandatory = $false)]         [string]$WinCSKey = "F33E0C8E002",                  [Parameter(Mandatory = $false)]         [bool]$DryRun = $false     )          # Configuração da Tarefa Agendada para WinCsFlags.exe     $TaskName = "SecureBoot-WinCS-Apply"     $TaskPath = "\Microsoft\Windows\SecureBoot\"     $TaskDescription = "Aplica a configuração de Arranque Seguro através do WinCS – Chave: $WinCSKey"          Write-Log "Deploying WinCS GPO: $GPOName" "WAVE"     Write-Log "A tarefa será executada: WinCsFlags.exe /apply --key '"$WinCSKey'" "INFO"     Write-Log "Acionador: no arranque do sistema (executado uma vez como SISTEMA)" "INFO"          se ($DryRun) {         Write-Log "[DRYRUN] Iria criar GPO: $GPOName" "INFO"         Write-Log "[DRYRUN] Iria criar um grupo de segurança: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Adicionaria $(@($WaveHostnames). Contar) computadores a agrupar" "INFO"         Write-Log "[DRYRUN] Implementaria a tarefa agendada: $TaskName" "INFO"         Write-Log "[DRYRUN] Iria ligar o GPO a: $TargetOU" "INFO"         devolver @{             Êxito = $true             GPOCreated = $false             GroupCreated = $false             ComputadoresAdded = 0         }     }          experimente {         # Importar módulos necessários         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } captura {         Write-Log "Falha ao importar os módulos necessários (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Erro = $_. Exception.Message }     }          # Passo 1: Criar ou obter GPO     $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     se ($gpo) {         Write-Log "GPO já existe: $GPOName" "INFO"     } senão {         experimente {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot WinCS Deployment - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "GPO Criado: $GPOName" "OK"         } captura {             Write-Log "Falha ao criar GPO: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Erro = $_. Exception.Message }         }     }          # Step 2: Create scheduled task XML for GPO deployment (Criar uma tarefa agendada XML para implementação de GPO)     # Isto cria uma tarefa que é executada WinCsFlags.exe /aplicar no arranque     $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <Versão da tarefa="1,4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">   <RegistrationInfo>     <Descrição>$TaskDescription</Descrição>     WinCsFlags.exe1 Author>SYSTEM</Author>   WinCsFlags.exe5 /RegistrationInfo>  >de Acionadores WinCsFlags.exe7    >WinCsFlags.exe9 BootTrigger       <Ativado>verdadeiro</> Ativado       <Atraso><PT5M /Atraso>     </BootTrigger>   </Acionadores>  >de Principais de <     <Principal id="Author">       <UserId>S-1-5-18</UserId>       <RunLevel></RunLevel>     </Principal>   </Principais>  >de Definições de <     <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>     <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>     <StopIfGoingOnBatteries>falso</StopIfGoingOnBatteries>     <AllowHardTerminate>verdadeiro</AllowHardTerminate>     <StartWhenAvailable>true</StartWhenAvailable>     <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>    ><IdleSettings       <StopOnIdleEnd><falso /StopOnIdleEnd>       <RestartOnIdle>falso</RestartOnIdle>     </IdleSettings>     <AllowStartOnDemand>verdadeiro</AllowStartOnDemand>     <Ativado>verdadeiro</> Ativado     <>ocultas</> Oculta     <RunOnlyIfIdle>falso</RunOnlyIfIdle>     WinCsFlags.exe03 DisallowStartOnRemoteAppSession>falso</DisallowStartOnRemoteAppSession>     WinCsFlags.exe07 UseUnifiedSchedulingEngine>verdadeiro</UseUnifiedSchedulingEngine>     WinCsFlags.exe11 WakeToRun>falso</WakeToRun>     WinCsFlags.exe15 ExecutionTimeLimit>pt1H</ExecutionTimeLimit>     WinCsFlags.exe19 DeleteExpiredTaskAfter><P30D /DeleteExpiredTaskAfter>     WinCsFlags.exe23 Priority>7</Priority>   WinCsFlags.exe27 /Definições>   WinCsFlags.exe29 Actions Context="Author"WinCsFlags.exe30    >WinCsFlags.exe31 Exec       WinCsFlags.exe33 comando>WinCsFlags.exe</>de comandos       argumentos WinCsFlags.exe37>/apply --key "$WinCSKey" WinCsFlags.exe39 /Arguments>     WinCsFlags.exe41 /Exec>   WinCsFlags.exe43 /Actions> WinCsFlags.exe45 />de Tarefas " @

    # Step 3: Deploy scheduled task via GPO Preferences     # Armazenar tarefa XML no SYSVOL para Tarefas Agendadas de GPO Tarefas Imediatas     experimente {         $gpoId = $gpo. Id.ToString()         $sysvolPath = "\\$((Get-ADDomain). DNSRoot)\SYSVOL\$((Get-ADDomain). DNSRoot)\Policies\{$gpoId}\Machine\Preferences\ScheduledTasks"         if (-not (Test-Path $sysvolPath)) {             New-Item -ItemType Directory -Path $sysvolPath -Force | Out-Null         }         # Criar ScheduledTasks.xml para 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">       <Versão da tarefa="1.3">         <RegistrationInfo>           <Descrição>$TaskDescription</Descrição>         </RegistrationInfo>        ><Principais           <Principal id="Author">             <UserId>NT AUTHORITY\System</UserId>             <LogonType>S4U</LogonType>             <RunLevel></RunLevel>           </Principal>         </Principais>        >de Definições do <          ><IdleSettings             Duração <><PT5M /Duração>             <WaitTimeout><PT1H /WaitTimeout>             <StopOnIdleEnd><falso /StopOnIdleEnd>             <RestartOnIdle>falso</RestartOnIdle>           </IdleSettings>           <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>           <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>           <StopIfGoingOnBatteries>falso</StopIfGoingOnBatteries>           <AllowHardTerminate>verdadeiro</AllowHardTerminate>           <IniciarQuando Disponível>verdadeiro</StartWhenAvailable>           <AllowStartOnDemand>verdadeiro</AllowStartOnDemand>           <Ativado>verdadeiro</> Ativado           <><falso /> Oculta           <ExecutionTimeLimit>pt1H</ExecutionTimeLimit>           <Priority>7</Priority>           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>         </Definições>        >de Acionadores <           <timeTrigger>             <StartBoundary>$(Get-Date -Format 'yyyy-MM-dd')T00:00:00</StartBoundary>             <Ativado>verdadeiro</> Ativado           </TimeTrigger>         </Triggers>        >de Ações <          ><Exec             <Comando>WinCsFlags.exe</> de Comandos             argumentos <>/apply --key "$WinCSKey"</Arguments>           </Exec>         </Actions>       </>de Tarefas     </Properties>   </ImmediateTaskV2> </ScheduledTasks> "@         $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force         Write-Log "Tarefa agendada implementada no GPO: $TaskName" "OK"     } captura {         Write-Log "Falha ao implementar a tarefa agendada XML: $($_. Exception.Message)" "WARN"         Write-Log "Reverter para a implementação do WinCS baseada no registo" "INFO"         # Fallback: Use WinCS registry approach if GPP scheduled task fails         # WinCS também pode ser acionado através da chave de registo         # (A implementação depende da API de registo do WinCS, se disponível)     }     # Passo 4: Criar ou obter o grupo de segurança     $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-not $group) {         experimente {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Description "Computers targeted for Secure Boot WinCS rollout - $GPOName" "                 -PassThru             Write-Log "Grupo de segurança criado: $SecurityGroupName" "OK"         } captura {             Write-Log "Falha ao criar o grupo de segurança: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Erro = $_. Exception.Message }         }     } senão {         Write-Log "Grupo de segurança existe: $SecurityGroupName" "INFO"     }     N.º 5: Adicionar computadores ao grupo de segurança     $added = 0     $failed = 0     foreach ($hostname em $WaveHostnames) {         experimente {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } captura {             $failed++         }     }     Write-Log "Adicionado $added computadores ao grupo de segurança ($failed não encontrados no AD)" "OK"     # Passo 6: Configurar a filtragem de segurança no GPO     experimente {         Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Configuração da filtragem de segurança para: $SecurityGroupName" "OK"     } captura {         Write-Log "Falha ao configurar a filtragem de segurança: $($_. Exception.Message)" "WARN"     }     # Passo 7: Ligar o GPO à UO     se ($TargetOU) {         experimente {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }             if (-not $existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop                 Write-Log "GPO ligado a: $TargetOU" "OK"             } senão {                 Write-Log "GPO já ligado à UO de destino" "INFO"             }         } captura {             Write-Log "CRÍTICO: Falha ao ligar o GPO à UO: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Erro = "Falha na ligação gpo: $($_. Exception.Message)" }         }     }     Write-Log "Implementação do GPO WinCS concluída" "OK"     Write-Log "As máquinas serão executadas WinCsFlags.exe na próxima atualização do GPO + reinício/arranque" "INFO"     devolver @{         Êxito = $true         GPOCreated = $true         GroupCreated = $true         ComputadoresAdded = $added         ComputersFailed = $failed     } }                                                                                        

# Wrapper function to maintain compatibility with main loop função Deploy-WinCSForWave {     parâmetro(         [Parameter(Mandatory = $true)]         [matriz]$WaveHostnames,         [Parameter(Mandatory = $false)]         [string]$WinCSKey = "F33E0C8E002",         [Parameter(Mandatory = $false)]         [string]$WavePrefix = "SecureBoot-Rollout",         [Parameter(Mandatory = $false)]         [int]$WaveNumber = 1,         [Parameter(Mandatory = $false)]         [cadeia]$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     # Converter para o formato de retorno esperado     devolver @{         Êxito = $result. Êxito         Aplicado = $result. ComputadoresAdded         Ignorado = 0         Falha = se ($result. ComputersFailed) { $result. ComputersFailed } senão { 0 }         Resultados = @()     } }                                                            

# ============================================================================ # ATIVAR IMPLEMENTAÇÃO DE TAREFAS N.º ============================================================================ # Implemente Enable-SecureBootUpdateTask.ps1 em dispositivos com a tarefa agendada desativada.# Utiliza um GPO com uma tarefa agendada imediata que é executada uma vez.

function Deploy-EnableTaskGPO {     <#     . SYNOPSIS         Implementar Enable-SecureBootUpdateTask.ps1 através da tarefa agendada de GPO.. DESCRIÇÃO         Cria um GPO que implementa uma tarefa agendada única para ativar o         Tarefa agendada de Atualização de Arranque Seguro em dispositivos de destino.. PARAMETER TargetOU         UO para ligar o GPO a.. PARAMETER TargetHostnames         Nomes de anfitrião de dispositivos com tarefa desativada (a partir do relatório de agregação).. DryRun do PARÂMETRO         Se for verdadeiro, registe apenas o que seria feito.#>     parâmetro(         [Parameter(Mandatory = $false)]         [cadeia]$TargetOU,                  [Parameter(Mandatory = $true)]         [matriz]$TargetHostnames,                  [Parameter(Mandatory = $false)]         [bool]$DryRun = $false     )          $GPOName = "SecureBoot-EnableTask-Remediation"     $SecurityGroupName = "SecureBoot-EnableTask-Devices"     $TaskName = "SecureBoot-EnableTask-OneTime"     $TaskDescription = "Tarefa única para ativar a tarefa agendada de Atualização de Arranque Seguro"          Write-Log "=" * 70 "INFO"     Write-Log "IMPLEMENTAR ATIVAR REMEDIAÇÃO DE TAREFAS" "INFORMAÇÕES"     Write-Log "=" * 70 "INFO"     Write-Log "Dispositivos de destino: $($TargetHostnames.Count)" "INFO"     Write-Log "GPO: $GPOName" "INFO"     Write-Log "Grupo de Segurança: $SecurityGroupName" "INFO"          se ($DryRun) {         Write-Log "[DRYRUN] Iria criar GPO: $GPOName" "INFO"         Write-Log "[DRYRUN] Iria criar um grupo de segurança: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Adicionaria computadores $($TargetHostnames.Count) ao grupo" "INFO"         Write-Log "[DRYRUN] Implementaria uma tarefa agendada única para ativar a "INFO" "Secure-Boot-Update"         Write-Log "[DRYRUN] Iria ligar o GPO a: $TargetOU" "INFO"         devolver @{             Êxito = $true             ComputadoresAdded = 0             DryRun = $true         }     }          experimente {         # Importar módulos necessários         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } captura {         Write-Log "Falha ao importar os módulos necessários: $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Erro = $_. Exception.Message }     }          # Passo 1: Criar ou obter GPO     $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     se ($gpo) {         Write-Log "GPO já existe: $GPOName" "INFO"     } senão {         experimente {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Task Enable Remediation - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "GPO Criado: $GPOName" "OK"         } captura {             Write-Log "Falha ao criar GPO: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Erro = $_. Exception.Message }         }     }          # Step 2: Deploy scheduled task XML to GPO SYSVOL (Passo 2: Implementar o XML da tarefa agendada no SYSVOL de GPO)     # A tarefa executa um comando do PowerShell para ativar a tarefa Secure-Boot-Update     experimente {         $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. Id)}\Machine\Preferences\ScheduledTasks"                  if (-not (Test-Path $sysvolPath)) {             New-Item -ItemType Directory -Path $sysvolPath -Force | Out-Null         }                  # Comando do PowerShell para ativar a tarefa 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 command for safe XML embedding (Comando #Encode para incorporação XML segura)         $encodedCommand = [Converter]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand))                  $taskGuid = [guid]::NewGuid(). ToString("B"). ToUpper()                  # GPP Scheduled Task XML - Tarefa imediata que é executada uma vez         $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="00" 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">       <Versão da tarefa="1.3">         <RegistrationInfo>           <Descrição>$TaskDescription</Descrição>         </RegistrationInfo>        >de Principais de <           <Principal id="Author">             <UserId>S-1-5-18</UserId>             <RunLevel></RunLevel>           </Principal>         </Principais>        >de Definições do <          ><IdleSettings             duração <>pt5M</duração>             <WaitTimeout><PT1H /WaitTimeout>             <StopOnIdleEnd>falso</StopOnIdleEnd>             <RestartOnIdle>falso</RestartOnIdle>           </IdleSettings>           <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>           <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>           <StopIfGoingOnBatteries>falso</StopIfGoingOnBatteries>           <AllowHardTerminate>verdadeiro</AllowHardTerminate>           <IniciarQuando Disponível>verdadeiro</StartWhenAvailable>           <AllowStartOnDemand>verdadeiro</AllowStartOnDemand>           <Ativado>verdadeiro</> Ativado           <>ocultas</> Oculta           <ExecutionTimeLimit>pt1H</ExecutionTimeLimit>           <Priority>7</Priority>           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>         </Definições>        >de Ações <          ><Exec             <comando>powershell.exe</> de comandos             <Argumentos>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments>           </Exec>         </Actions>       </>de Tarefas     </Properties>   </ImmediateTaskV2> </ScheduledTasks> "@                  $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force         Write-Log "Tarefa agendada única implementada no GPO: $TaskName" "OK"              } captura {         Write-Log "Falha ao implementar a tarefa agendada XML: $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Erro = $_. Exception.Message }     }          N.º 3: Criar ou obter um grupo de segurança     $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-not $group) {         experimente {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Descrição "Computadores com a tarefa Secure-Boot-Update desativada - direcionada para remediação" "                 -PassThru             Write-Log "Grupo de segurança criado: $SecurityGroupName" "OK"         } captura {             Write-Log "Falha ao criar o grupo de segurança: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Erro = $_. Exception.Message }         }     } senão {         Write-Log "Grupo de segurança existe: $SecurityGroupName" "INFO"     }          # Passo 4: Adicionar computadores ao grupo de segurança     $added = 0     $failed = 0     foreach ($hostname no $TargetHostnames) {         experimente {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } captura {             $failed++             Write-Log "Computador não encontrado no AD: $hostname" "AVISO"         }     }     Write-Log "Adicionado $added computadores ao grupo de segurança ($failed não encontrados no AD)" "OK"          N.º 5: Configurar a filtragem de segurança no GPO     experimente {         Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Configuração da filtragem de segurança para: $SecurityGroupName" "OK"     } captura {         Write-Log "Falha ao configurar a filtragem de segurança: $($_. Exception.Message)" "WARN"     }          # Passo 6: Ligar o GPO à UO     se ($TargetOU) {         experimente {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }                          if (-not $existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop                 Write-Log "GPO ligado a: $TargetOU" "OK"             } senão {                 Write-Log "GPO já ligado à UO de destino" "INFO"             }         } captura {             Write-Log "Falha ao ligar o GPO à UO: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Erro = "Falha na ligação gpo: $($_. Exception.Message)" }         }     } senão {         Write-Log "Sem TargetOU especificado – O GPO terá de ser ligado manualmente" "AVISO"     }          Write-Log "" "INFO"     Write-Log "ATIVAR IMPLEMENTAÇÃO DE TAREFAS CONCLUÍDA" "OK"     Write-Log "Os dispositivos irão executar a tarefa de ativação na próxima atualização de GPO (gpupdate)" "INFO"     Write-Log "A tarefa é executada uma vez como SISTEMA e ativa a Atualização de Arranque Seguro" "INFO"     Write-Log "" "INFO"          devolver @{         Êxito = $true         ComputadoresAdded = $added         ComputersFailed = $failed         GPOName = $GPOName         SecurityGroup = $SecurityGroupName     } }

# ============================================================================ # ATIVAR TAREFA EM DISPOSITIVOS DESATIVADOS N.º ============================================================================ se ($EnableTaskOnDisabled) {     Write-Host ""     Write-Host ("=" * 70) -Primeiro PlanoColor Amarelo     Write-Host " ENABLE TASK REMEDIATION - Fixing Disabled Scheduled Tasks" -ForegroundColor Yellow     Write-Host ("=" * 70) -Primeiro PlanoColor Amarelo     Write-Host ""     # Localizar dispositivos com tarefa desativada a partir de dados de agregação     if (-not $AggregationInputPath) {         Write-Host "ERROR: -AggregationInputPath is required to identify devices with disabled task" -ForegroundColor Red         Write-Host "Usage: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <path> -ReportBasePath <path>" -ForegroundColor Gray         sair 1     }     Write-Host "Procurar dispositivos com a tarefa Secure-Boot-Update desativada..." -ForegroundColor Cyan     # Carregar ficheiros JSON e localizar dispositivos com a tarefa desativada     $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                  Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }     $disabledTaskDevices = @()     foreach ($file em $jsonFiles) {         experimente {             $device = Get-Content $file. FullName -Raw | ConverterFrom-Json             if ($device. SecureBootTaskEnabled -eq $false -or                 $device. SecureBootTaskStatus -eq 'Disabled' -or                 $device. SecureBootTaskStatus -eq 'NotFound') {                 # Inclua apenas dispositivos que ainda não tenham sido atualizados (nenhum Evento 1808)                 se ([int]$device. Event1808Count -eq 0) {                     $disabledTaskDevices += $device. HostName                 }             }         } captura {             # Ignorar ficheiros inválidos         }     }     $disabledTaskDevices = $disabledTaskDevices | Select-Object - Exclusivo     if ($disabledTaskDevices.Count -eq 0) {         Write-Host ""         Write-Host "Não foram encontrados dispositivos com a tarefa Secure-Boot-Update desativada." -ForegroundColor Green         Write-Host "Todos os dispositivos têm a tarefa ativada ou já foram atualizados." -ForegroundColor Gray         sair 0     }     Write-Host ""     Write-Host "Foram encontrados dispositivos $($disabledTaskDevices.Count) com a tarefa desativada:" -ForegroundColor Amarelo     $disabledTaskDevices | Select-Object -Primeiros 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }     if ($disabledTaskDevices.Count -gt 20) {         Write-Host " ... and $($disabledTaskDevices.Count - 20) more" -ForegroundColor Gray     }     Write-Host ""     # Implemente o GPO Ativar Tarefa     $result = Deploy-EnableTaskGPO -TargetHostnames $disabledTaskDevices -TargetOU $TargetOU -DryRun $DryRun     if ($result. Êxito) {         Write-Host ""         Write-Host "SUCCESS: Enable Task GPO deployed" -ForegroundColor Green         Write-Host " Computadores adicionados ao grupo de segurança: $($result. ComputersAdded)" -ForegroundColor Cyan         if ($result. ComputersFailed -gt 0) {             Write-Host " Computadores não encontrados no AD: $($result. ComputersFailed)" -ForegroundColor Yellow         }         Write-Host ""         Write-Host "PASSOS SEGUINTES:" -Primeiro PlanoColor Branco         Write-Host " 1.                                              Os dispositivos receberão o GPO na próxima atualização (gpupdate /force)" -ForegroundColor Gray         Write-Host " 2. A tarefa única ativará Secure-Boot-Update" -ForegroundColor Gray         Write-Host " 3. Volte a executar a agregação para verificar se a tarefa está agora ativada" -ForegroundColor Gray     } senão {         Write-Host ""         Write-Host "FAILED: Could not deploy Enable Task GPO" -ForegroundColor Red         Write-Host "Erro: $($result. Erro)" -ForegroundColor Vermelho     }          sair 0 }

# ============================================================================ # CICLO DE ORQUESTRAÇÃO PRINCIPAL # ============================================================================

Write-Host "" Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host " SECURE BOOT DEPLOYMENT ORCHESTRATOR - CONTINUOUS DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host ""

if ($DryRun) {     Write-Host "[DRY RUN MODE]" -ForegroundColor Magenta }

if ($UseWinCS) {     Write-Host "[WinCS MODE]" -ForegroundColor Yellow     Write-Host "Using WinCsFlags.exe instead of GPO/AvailableUpdatesPolicy" -ForegroundColor Yellow     Write-Host "WinCS Key: $WinCSKey" -ForegroundColor Gray     Write-Host "" }

Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "Caminho de Entrada: $AggregationInputPath" "INFO" Write-Log "Caminho do Relatório: $ReportBasePath" "INFO" se ($UseWinCS) {     Write-Log "Método de Implementação: WinCS (WinCsFlags.exe /apply --key '"$WinCSKey'")" "INFO" } senão {     Write-Log "Método de Implementação: GPO (AvailableUpdatesPolicy)" "INFO" }

# Resolve TargetOU - default to domain root for domain-wide coverage # Apenas necessário para o método de implementação de GPO (o WinCS não requer AD/GPO) if (-not $UseWinCS -and -not $TargetOU) {     experimente {         # Experimente vários métodos para obter o DN de domínio         $domainDN = $null         # Método 1: Get-ADDomain (requer RSAT-AD-PowerShell)         experimente {             Import-Module ActiveDirectory -ErrorAction Stop             $domainDN = (Get-ADDomain -ErrorAction Stop). DistinguishedName         } captura {             Write-Log "Get-ADDomain falhou: $($_. Exception.Message)" "WARN"         }         # Method 2: Use RootDSE via ADSI         if (-not $domainDN) {             experimente {                 $rootDSE = [ADSI]"LDAP://RootDSE"                 $domainDN = $rootDSE.defaultNamingContext.ToString()             } captura {                 Write-Log "Falha do ADSI RootDSE: $($_. Exception.Message)" "WARN"             }         }         # Método 3: Analisar da associação ao domínio do computador         if (-not $domainDN) {             experimente {                 $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()                 $domainDN = "DC=" + ($domain. Nome - substitua '\.', ',DC=')             } captura {                 Write-Log "Falha no GetComputerDomain: $($_. Exception.Message)" "WARN"             }         }         se ($domainDN) {             $TargetOU = $domainDN             Write-Log "Destino: Raiz de Domínio ($domainDN) – O GPO aplicará o domínio ao nível do domínio através da filtragem de grupos de segurança" "INFO"         } senão {             Write-Log "Não foi possível determinar o domínio DN – O GPO será criado, mas NÃO LIGADO!" "ERRO"             Write-Log "Especifique o parâmetro -TargetOU ou ligue o GPO manualmente após a criação" "ERROR"             $TargetOU = $null         }     } captura {         Write-Log "Não foi possível obter o DN do domínio – o GPO será criado, mas não será ligado.                                     Ligue manualmente, se necessário." "AVISAR"         Write-Log "Erro: $($_. Exception.Message)" "WARN"         $TargetOU = $null     } } senão {     Write-Log "UO de Destino: $TargetOU" "INFO" }

Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Intervalo do Inquérito: $PollIntervalMinutes minutos" "INFO" se ($LargeScaleMode) {     Write-Log "LargeScaleMode ativado (tamanho do lote: $ProcessingBatchSize, exemplo de registo: $DeviceLogSampleSize)" "INFO" }

# ============================================================================ # VERIFICAÇÃO DE PRÉ-REQUISITOS: verificar se a deteção está implementada e a funcionar # ============================================================================

Write-Host "" Write-Log "Verificar pré-requisitos..." "INFORMAÇÕES"

$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath if (-not $detectionCheck.IsDeployed) {     Write-Log $detectionCheck.Message "ERRO"     Write-Host ""     Write-Host "REQUIRED: Deploy detection infrastructure first:" -ForegroundColor Yellow     Write-Host " 1. Executar: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\server\SecureBootLogs$'" -ForegroundColor Cyan     Write-Host " 2. Aguarde que os dispositivos comuniquem (12 a 24 horas)" -ForegroundColor Cyan     Write-Host " 3. Executar novamente este orquestrador" -ForegroundColor Cyan     Write-Host ""     if (-not $DryRun) {         devolver     } } senão {     Write-Log $detectionCheck.Message "OK" }

# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Atualização de dados: $($freshness. Ficheiros TotalFiles), $($freshness. FreshFiles) fresco (<24h), $($freshness. StaleFiles) obsoleto (>72h)" "INFO" se ($freshness. Aviso) {     Write-Log $freshness. Aviso "AVISAR" }

# Load Allow List (targeted rollout - ONLY these devices will be rolled out) $allowedHostnames = @() if ($AllowListPath -or $AllowADGroup) {     $allowedHostnames = Get-AllowedHostnames -AllowFilePath $AllowListPath -ADGroupName $AllowADGroup     if ($allowedHostnames.Count -gt 0) {         Write-Log "AllowList: APENAS os dispositivos $($allowedHostnames.Count) serão considerados para implementação" "INFO"     } senão {         Write-Log "AllowList especificado, mas não foram encontrados dispositivos – isto irá bloquear todas as implementações!" "AVISAR"     } }

# Load VIP/exclusion list (BlockList) $excludedHostnames = @() if ($ExclusionListPath -or $ExcludeADGroup) {     $excludedHostnames = Get-ExcludedHostnames -ExclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup     if ($excludedHostnames.Count -gt 0) {         Write-Log "Exclusão VIP: os dispositivos $($excludedHostnames.Count) serão ignorados da implementação" "INFO"     } }

# Load state $rolloutState = Get-RolloutState $blockedBuckets = Get-BlockedBuckets $adminApproved = Get-AdminApproved $deviceHistory = Get-DeviceHistory

if ($rolloutState.Status -eq "NotStarted") {     $rolloutState.Status = "Entrada"     $rolloutState.StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"     Write-Log "A iniciar nova implementação" "WAVE" }

Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "Registos Bloqueados: $($blockedBuckets.Count)" "INFO"

# Main loop - runs until all eligible devices are updated $iterationCount = 0 while ($true) {     $iterationCount++     Write-Host ""     Write-Host ("=" * 80) -Primeiro PlanoColor Branco     Write-Log "=== ITERAÇÃO $iterationCount ===" "ONDA"     Write-Host ("=" * 80) -Primeiro PlanoColor Branco     # Passo 1: Executar agregação     Write-Log "Passo 1: Executar agregação..." "INFORMAÇÕES"     # Orchestrator reutiliza sempre uma única pasta (LargeScaleMode) para evitar o inchaço do disco     #Admins running the aggregator manually get timestamped folders for point-in-time snapshots     $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current"     # Verificar a atualização dos dados antes de agregar     $freshness = Get-DataFreshness -JsonPath $AggregationInputPath     Write-Log "Atualização de dados: $($freshness. FreshFiles)/$($freshness. TotalFiles) dispositivos reportados nos últimos 24h" "INFO"     se ($freshness. Aviso) {         Write-Log $freshness. Aviso "AVISAR"     }     $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1"     $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json"     $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     if (Test-Path $aggregateScript) {         if (-not $DryRun) {             # O Orchestrator utiliza sempre a transmissão em fluxo + incremental para eficiência             O agregador # eleva automaticamente para PS7, se disponível para o melhor desempenho             $aggregateParams = @{                 InputPath = $AggregationInputPath                 OutputPath = $aggregationPath                 StreamingMode = $true                 IncrementalMode = $true                 SkipReportIfUnchanged = $true                 ParallelThreads = 8             }             # Transmita o resumo da implementação se existir (para dados de velocidade/projeção)             if (Test-Path $rolloutSummaryPath) {                 $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath             }             & $aggregateScript @aggregateParams             # Mostrar comando para gerar dashboard HTML completos com tabelas de dispositivos             Write-Host ""             Write-Host "Para gerar dashboard HTML completos com tabelas Manufacturer/Model, execute:" -ForegroundColor Yellow             Write-Host " $aggregateScript -InputPath '"$AggregationInputPath"" -OutputPath '"$aggregationPath"" -ForegroundColor Yellow             Write-Host ""         } senão {             Write-Log "[DRYRUN] Would run aggregation" "INFO"             # In DryRun, use existing aggregation data from ReportBasePath directly             $aggregationPath = $ReportBasePath         }     }     $rolloutState.LastAggregation = Get-Date -Format "yyyy-MM-dd HH:mm:ss"     N.º 2: Carregar o dispositivo atual status     Write-Log "Passo 2: Carregar status do dispositivo..." "INFORMAÇÕES"     $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue |          Where-Object { $_. Nome -notlike "*Buckets*" } |          Sort-Object LastWriteTime -Descending |          Select-Object -Primeiro 1     if (-not $notUptodateCsv -and -not $DryRun) {         Write-Log "Não foram encontrados dados de agregação.                                            A aguardar..." "AVISAR"         Start-Sleep -Seconds ($PollIntervalMinutes * 60)         continuar     }     $notUpdatedDevices = se ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } else { @() }     Write-Log "Dispositivos não atualizados: $($notUpdatedDevices.Count)" "INFO"     $notUpdatedIndexes = Get-NotUpdatedIndexes -Devices $notUpdatedDevices     N.º 3: Atualizar o histórico de dispositivos (controlo por nome de anfitrião)     Write-Log "Passo 3: Atualizar o histórico de dispositivos..." "INFORMAÇÕES"     Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory     Save-DeviceHistory -Histórico $deviceHistory     # Passo 4: Verificar a existência de registos bloqueados (dispositivos inacessíveis)     $existingBlockedCount = $blockedBuckets.Count     Write-Log "Passo 4: Verificar a existência de registos bloqueados (ping dos dispositivos após o período de espera)..." "INFORMAÇÕES"     se ($existingBlockedCount -gt 0) {         Write-Log "Registos atualmente bloqueados de execuções anteriores: $existingBlockedCount" "INFO"     }     if ($adminApproved.Count -gt 0) {         Write-Log "registos aprovados Administração (não serão bloqueados novamente): $($adminApproved.Count)" "INFO"     }     $newlyBlocked = Update-BlockedBuckets -RolloutState $rolloutState -BlockedBuckets $blockedBuckets -AdminApproved $adminApproved -NotUpdatedDevices $notUpdatedDevices -NotUpdatedIndexes $notUpdatedIndexes -MaxWaitHours $MaxWaitHours -DryRun:$DryRun     if ($newlyBlocked.Count -gt 0) {         Save-BlockedBuckets -$blockedBuckets Bloqueados         Write-Log "Registos recentemente bloqueados (esta iteração): $($newlyBlocked.Count)" "BLOQUEADO"     }     # Step 4b: Auto-unblock buckets where devices have updated     $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize     if ($autoUnblocked.Count -gt 0) {         Save-BlockedBuckets -$blockedBuckets Bloqueados         Write-Log "Registos desbloqueados automaticamente (dispositivos atualizados): $($autoUnblocked.Count)" "OK"     }     N.º 5: Calcular os restantes dispositivos elegíveis     $eligibleCount = 0     foreach ($device no $notUpdatedDevices) {         $bucketKey = Get-BucketKey $device         if (-not $blockedBuckets.Contains($bucketKey)) {             $eligibleCount++         }     }     Write-Log "Dispositivos elegíveis restantes: $eligibleCount" "INFO"     Write-Log "Registos bloqueados: $($blockedBuckets.Count)" "INFO"     # Passo 6: Verificar a conclusão     if ($eligibleCount -eq 0) {         Write-Log "IMPLEMENTAÇÃO CONCLUÍDA - Todos os dispositivos elegíveis atualizados!" "OK"         $rolloutState.Status = "Concluído"         $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"         Save-RolloutState -State $rolloutState         quebra     }     # Step 6: Generate and deploy next wave (Passo 6: Gerar e implementar a próxima vaga)     Write-Log "Passo 6: Gerar onda de implementação..." "INFORMAÇÕES"     $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames     # Verifique se temos dispositivos para implementar ($waveDevices podem estar $null, vazios ou com dispositivos reais)     $hasDevices = $waveDevices -e @($waveDevices | Where-Object { $_ }). Contagem -gt 0     se ($hasDevices) {         # Apenas incrementar número de onda quando realmente temos dispositivos para implementar         $rolloutState.CurrentWave++         Write-Log "Wave $($rolloutState.CurrentWave): $(@($waveDevices). Contar) dispositivos" "WAVE"         # Implementar GPO com a função inlined         $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $hostnames = @($waveDevices | ForEach-Object {             se ($_. Nome do anfitrião) { $_. Nome do anfitrião } elseif ($_. HostName) { $_. HostName } else { $null }         } | Where-Object { $_ })         # Guarde o ficheiro hostnames para referência/auditoria         $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt"         $hostnames | Out-File $hostnamesFile -Encoding UTF8         # Valide que temos nomes de anfitrião para implementar         se ($hostnames. Contagem -eq 0) {             Write-Log "Não foram encontrados nomes de anfitrião válidos na onda $($rolloutState.CurrentWave) – os dispositivos podem estar em falta na propriedade Hostname" "WARN"             Write-Log "A ignorar a implementação para esta onda – marcar dados do dispositivo" "AVISO"             # Ainda aguarde antes da próxima iteração             if (-not $DryRun) {                 Write-Log "Dormir durante $PollIntervalMinutes minutos antes de tentar..." "INFORMAÇÕES"                 Start-Sleep -Seconds ($PollIntervalMinutes * 60)             }             continuar         }         Write-Log "Implementar em $($hostnames. Count) hostnames in Wave $($rolloutState.CurrentWave)" "INFO"         # Implementar com o método WinCS ou GPO com base no parâmetro -UseWinCS         se ($UseWinCS) {             # WinCS Method: Create GPO with scheduled task to run WinCsFlags.exe as SYSTEM on each endpoint             Write-Log "Utilizar o método de implementação WinCS (Chave: $WinCSKey)" "WAVE"             $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames '                 -WinCSKey $WinCSKey '                 -WavePrefix $WavePrefix '                 -WaveNumber $rolloutState.CurrentWave '                 -TargetOU $TargetOU '                 -DryRun:$DryRun             if (-not $wincsResult.Success) {                 Write-Log "A implementação do WinCS teve falhas – Aplicada: $($wincsResult.Applied), Falha: $($wincsResult.Failed)" "AVISO"             } senão {                 Write-Log "Implementação WinCS com êxito – Aplicada: $($wincsResult.Applied), Ignorada: $($wincsResult.Skipped)" "OK"             }             # Guardar resultados do WinCS para auditoria             $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json"             $wincsResult | ConvertTo-Json -Profundidade 5 | Out-File $wincsResultFile -Encoding UTF8         } senão {             Método # GPO: Criar GPO com a definição de registo AvailableUpdatesPolicy             $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun             if (-not $gpoResult) {                 Write-Log "Falha na implementação do GPO – repetirá a próxima iteração" "ERROR"             }         }         # Onda de registo no estado         $waveRecord = @{             WaveNumber = $rolloutState.CurrentWave             StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             DeviceCount = @($waveDevices). Contagem             Dispositivos = @($waveDevices | ForEach-Object {                 @{                     Hostname = if ($_. Nome do anfitrião) { $_. Nome do anfitrião } elseif ($_. HostName) { $_. HostName } else { $null }                     BucketKey = Get-BucketKey $_                 }             })         }         # Certifique-se de que WaveHistory é sempre uma matriz antes de acrescentar (impede problemas de intercalação de tabela hash)         $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord)         $rolloutState.TotalDevicesTargeted += @($waveDevices). Contagem         Save-RolloutState -State $rolloutState         Write-Log "Wave $($rolloutState.CurrentWave) implementado.                                                                                                                                                                                        A aguardar $PollIntervalMinutes minutos..." "OK"     } senão {         # Mostrar status de dispositivos implementados à espera de atualizações         Write-Log "" "INFO"         Write-Log "========== TODOS OS DISPOSITIVOS IMPLEMENTADOS – A AGUARDAR PELO ESTADO ==========" "INFORMAÇÕES"         # Obtenha todos os dispositivos implementados a partir do histórico de ondas         $allDeployedLookup = @{}         foreach ($wave em $rolloutState.WaveHistory) {             foreach ($device em $wave. Dispositivos) {                 if ($device. Nome do anfitrião) {                     $allDeployedLookup[$device. Nome do anfitrião] = @{                         Hostname = $device. Nome do anfitrião                         BucketKey = $device. BucketKey                         DeployedAt = $wave. IniciadoAt                         WaveNumber = $wave. WaveNumber                     }                 }             }         }         $allDeployedDevices = @($allDeployedLookup.Values)         if ($allDeployedDevices.Count -gt 0) {             # Determinar que dispositivos implementados ainda estão pendentes (na lista Não Desatualizada)             $stillPendingCount = 0             $noLongerPendingCount = 0             $pendingSample = @()             foreach ($deployed no $allDeployedDevices) {                 if ($notUpdatedIndexes.HostSet.Contains($deployed. Nome do anfitrião)) {                     $stillPendingCount++                     if ($pendingSample.Count -lt $DeviceLogSampleSize) {                         $pendingSample += $deployed. Nome do anfitrião                     }                 } senão {                     $noLongerPendingCount++                 }             }             # Obter contagens atualizadas reais da agregação – diferenciar o Evento 1808 vs UEFICA2023Status             $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Summary*.csv" |                  Sort-Object LastWriteTime -Descending | Select-Object -Primeiro 1             $actualUpdated = 0             $totalDevicesFromSummary = 0             $event 1808Count = 0             $uefiStatusUpdated = 0             $needsRebootSample = @()             se ($summaryCsv) {                 $summary = Import-Csv $summaryCsv.FullName | Select-Object -Primeiro 1                 if ($summary. Atualizado) { $actualUpdated = [int]$summary. Atualizado }                 if ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices }             }             # Calcular a velocidade do histórico de ondas (dispositivos atualizados por dia)             $devicesPerDay = 0             if ($rolloutState.StartedAt -and $actualUpdated -gt 0) {                 $startDate = [datetime]::P arse($rolloutState.StartedAt)                 $daysElapsed = ((Get-Date) - $startDate). TotalDays                 se ($daysElapsed -gt 0) {                     $devicesPerDay = $actualUpdated/$daysElapsed                 }             }             # Guarde o resumo da implementação com projeções com deteção de fim de semana             # Utilize a contagem NotUptodate do agregador (exclui dispositivos SB OFF) para consistência             $notUpdatedCount = se ($summary -e $summary. NotUptodate) { [int]$summary. NotUptodate } senão { $totalDevicesFromSummary - $actualUpdated }             Save-RolloutSummary -State $rolloutState '                 -TotalDevices $totalDevicesFromSummary '                 -UpdatedDevices $actualUpdated '                 -NotUpdatedDevices $notUpdatedCount '                 -DevicesPerDay $devicesPerDay             # Verifique os dados não processados dos dispositivos com UEFICA2023Status=Atualizado, mas nenhum Evento 1808 (precisa de ser reiniciado)             $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue             $totalDataFiles = @($dataFiles). Contagem             $batchSize = [Matemática]::Máx.(500, $ProcessingBatchSize)             se ($LargeScaleMode) {                 $batchSize = [Matemática]::Máx.(2000, $ProcessingBatchSize)             }                                                                                   

            if ($totalDataFiles -gt 0) {                 para ($idx = 0; $idx -lt $totalDataFiles; $idx += $batchSize) {                     $end = [Matemática]::Min($idx + $batchSize - 1, $totalDataFiles - 1)                     $batchFiles = $dataFiles[$idx.. $end]

                    foreach ($file in $batchFiles) {                         experimente {                             $deviceData = Get-Content $file. FullName -Raw | ConverterFrom-Json                             $hostname = $deviceData.Hostname                             if (-not $hostname) { continue }                             $has 1808 = [int]$deviceData.Event1808Count -gt 0                             $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Atualizado"                             if ($has 1808) {                                 $event 1808Count++                             } elseif ($hasUefiUpdated) {                                 $uefiStatusUpdated++                                 if ($needsRebootSample.Count -lt $DeviceLogSampleSize) {                                     $needsRebootSample += $hostname                                 }                             }                         } captura { }                     }                                                          

                    Save-ProcessingCheckpoint -Stage "RebootStatusScan" -Processed ($end + 1) -Total $totalDataFiles -Metrics @{                         Event1808Count = $event 1808Count                         UEFIUpdatedAwaitingReboot = $uefiStatusUpdated                     }                 }             }             Write-Log "Total implementado: $($allDeployedDevices.Count)" "INFO"             Write-Log "Atualizado (Evento 1808 confirmado): $event 1808Count" "OK"             se ($uefiStatusUpdated -gt 0) {                 Write-Log "Atualizado (UEFICA2023Status=Atualizado, a aguardar reinício): $uefiStatusUpdated" "OK"                 $rebootSuffix = se ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) mais)" } senão { "" }                 Write-Log " Dispositivos que precisam de ser reiniciados para o Evento 1808 (exemplo): $($needsRebootSample -join ', ')$rebootSuffix" "INFO"                 Write-Log " Estes dispositivos comunicarão o Evento 1808 após o próximo reinício" "INFO"             }             Write-Log "Já não está pendente: $noLongerPendingCount (inclui SecureBoot OFF, dispositivos em falta)" "INFO"             Write-Log "A aguardar status: $stillPendingCount" "INFO"             se ($stillPendingCount -gt 0) {                 $pendingSuffix = se ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) mais)" } senão { "" }                 Write-Log "Dispositivos pendentes (exemplo): $($pendingSample -join ', ')$pendingSuffix' "WARN"             }         } senão {             Write-Log "Ainda não foram implementados dispositivos" "INFO"         }         Write-Log "================================================================" "INFO"         Write-Log "" "INFO"     }     # Aguarde antes da próxima iteração     if (-not $DryRun) {         Write-Log "A dormir durante $PollIntervalMinutes minutos..." "INFORMAÇÕES"         Start-Sleep -Seconds ($PollIntervalMinutes * 60)     } senão {         Write-Log "[DRYRUN] Would wait $PollIntervalMinutes minutes" "INFO"         break # Exit after one iteration in dry run     } }                               

# ============================================================================ # RESUMO FINAL # ============================================================================

Write-Host "" Write-Host ("=" * 80) -Primeiro PlanoColor Verde Write-Host " RESUMO DO ORCHESTRATOR DE IMPLEMENTAÇÃO" -ForegroundColor Green Write-Host ("=" * 80) -Primeiro PlanoColor Verde Write-Host ""

$finalState = Get-RolloutState $finalBlocked = Get-BlockedBuckets

Write-Host "Status:              $($finalState.Status)" -ForegroundColor $(if ($finalState.Status -eq "Completed") { "Green" } else { "Yellow" }) Write-Host "Ondas Totais: $($finalState.CurrentWave)" Write-Host "Dispositivos Visados: $($finalState.TotalDevicesTargeted)" Write-Host "Registos Bloqueados: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } else { "Green" }) Write-Host "Devices Tracked: $($deviceHistory.Count)" -ForegroundColor Gray Write-Host ""

if ($finalBlocked.Count -gt 0) {     Write-Host "REGISTOS BLOQUEADOS (requer revisão manual):" -Primeiro PlanoColor Vermelho     foreach ($key em $finalBlocked.Keys) {         $info = $finalBlocked[$key]         Write-Host " - $key" - Primeiro PlanoColor Vermelho         Write-Host " Razão: $($info. Razão)" -ForegroundColor Gray     }     Write-Host ""     Write-Host "Ficheiro de registos bloqueados: $blockedBucketsPath" -Primeiro PlanoColor Amarelo }

Write-Host "" Write-Host "State files:" -ForegroundColor Cyan Write-Host " Estado de Implementação: $rolloutStatePath" Write-Host " Registos Bloqueados: $blockedBucketsPath" Write-Host " Histórico de Dispositivos: $deviceHistoryPath" Write-Host ""  

​​​​​​​

Precisa de mais ajuda?

Quer mais opções

Explore os benefícios da assinatura, procure cursos de treinamento, saiba como proteger seu dispositivo e muito mais.