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 ""