Copie y pegue este script de ejemplo y modifíquelo según sea necesario para su entorno:
<# . SINOOPSIS Organizador de implementación de arranque seguro continuo que se ejecuta hasta que se complete la implementación.
.DESCRIPTION Este script proporciona automatización completa de un extremo a otro para la implementación de certificados de arranque seguro: 1. Genera ondas de implementación basadas en datos de agregación 2. Crea grupos DED y GPO para cada tanda 3. Monitores para actualizaciones de dispositivos (Evento 1808) 4. Detecta cubos bloqueados (dispositivos no accesibles) 5. Progresa automáticamente a la siguiente ola 6. Se ejecuta hasta que todos los dispositivos aptos se actualicen Criterios de finalización: - No hay dispositivos restantes en: Acción requerida, alta confianza, observación, temporalmente pausada - Fuera del ámbito (por diseño): No compatible, arranque seguro deshabilitado - Se ejecuta continuamente hasta completarse: sin límite de onda arbitrario Estrategia de implementación: - ALTA CONFIANZA: Todos los dispositivos en primera tanda (seguro) - ACCIÓN REQUERIDA: Dobles progresivos (1→2→4→8...) Lógica de bloqueo: - Después de MaxWaitHours, el organizador hace ping a los dispositivos que no se han actualizado - Si el dispositivo es INALCANZABLE (error de ping) → bucket está BLOQUEADO para investigación - Si el dispositivo es ACCESIBLE pero no se actualiza → seguir esperando (puede que sea necesario reiniciar) - Los cubos bloqueados se excluyen hasta que el administrador los desbloquee Desbloqueo automático: - Si posteriormente se muestra como actualizado un dispositivo en un cubo bloqueado (evento 1808), el depósito se desbloquea automáticamente y el lanzamiento continúa - Esto controla los dispositivos que estaban temporalmente sin conexión pero volvieron Seguimiento de dispositivos: - Realiza un seguimiento de los dispositivos por nombre de host (se supone que los nombres no cambian durante la implementación) - Nota: la colección JSON no incluye un id. de equipo único; agregar una para un mejor seguimiento
.PARAMETER AggregationInputPath Ruta de acceso a datos de dispositivo JSON sin procesar (desde Detect script)
.PARAMETER ReportBasePath Ruta base para los informes de agregación
.PARAMETER TargetOU Nombre distintivo de la unidad organizativa para vincular GPO.Opcional: si no se especifica, el GPO está vinculado a la raíz del dominio para la cobertura de todo el dominio.El filtrado de grupos de seguridad garantiza que solo los dispositivos de destino reciban la directiva.
.PARAMETER MaxWaitHours Horas para esperar a que los dispositivos se actualicen antes de comprobar el alcance.Después de este tiempo, se hace ping a los dispositivos que no se han actualizado.Los dispositivos que no son accesibles hacen que el cubo se bloquee.Valor predeterminado: 72 (3 días)
.PARAMETER PollIntervalMinutes Minutos entre comprobaciones de estado. Valor predeterminado: 1440 (1 día)
.PARAMETER AllowListPath Ruta de acceso a un archivo que contiene nombres de host para PERMITIR para su implementación (implementación dirigida).Admite .txt (un nombre de host por línea) o .csv (con la columna NombreDeHoja/NombreEquipo/Nombre).Cuando se especifique, SOLO estos dispositivos se incluirán en la implementación.BlockList se sigue aplicando después de AllowList.
.PARAMETER AllowADGroup Nombre de un grupo de seguridad de AD que contiene cuentas de equipo para PERMITIR.Ejemplo: "SecureBoot-Pilot-Computers" o "Wave1-Devices" Cuando se especifique, SOLO se incluirán en la implementación los dispositivos de este grupo.Combinar con AllowListPath para la identificación basada en AD y en archivos.
.PARAMETER ExclusionListPath Ruta de acceso a un archivo que contiene nombres de host para EXCLUIR del lanzamiento (dispositivos VIP/ejecutivos).Admite .txt (un nombre de host por línea) o .csv (con la columna NombreDeHoja/NombreEquipo/Nombre).Estos dispositivos nunca se incluirán en ninguna ola de implementación.BlockList se aplica DESPUÉS del filtrado AllowList. . PARAMETER ExcludeADGroup Nombre de un grupo de seguridad de AD que contiene las cuentas de equipo que se excluyen.Ejemplo: "Vip-Computers" o "Executive-Devices" Combine con ExclusionListPath para exclusiones basadas en AD y en archivos.
.PARAMETER UseWinCS Use WinCS (Sistema de configuración de Windows) en lugar de GPO/AvailableUpdatesPolicy.WinCS implementa la habilitación del arranque seguro ejecutando WinCsFlags.exe directamente en cada punto de conexión.WinCsFlags.exe se ejecuta en el contexto DEL SISTEMA a través de una tarea programada.Este método es útil para: - Lanzamientos más rápidos (efecto inmediato frente a esperar el procesamiento de GPO) - Dispositivos que no están unidos a un dominio - Entornos sin infraestructura de AD/GPO Referencia: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe
.PARAMETER WinCSKey La clave de WinCS que se usará para habilitar el arranque seguro.Valor predeterminado: F33E0C8E002 Esta clave corresponde a la configuración de implementación de arranque seguro. . PARAMETER DryRun Mostrar lo que se debería hacer sin realizar cambios
.PARAMETER ListBlockedBuckets Mostrar todos los cubos bloqueados actualmente y salir
.PARAMETER UnblockBucket Desbloquear un cubo específico por tecla y salir
.PARAMETER UnblockAll Desbloquear todos los cubos y salir
.PARAMETER EnableTaskOnDisabled Implementa Enable-SecureBootUpdateTask.ps1 en todos los dispositivos con la tarea programada deshabilitada.Crea un GPO con una tarea programada de una sola vez que ejecuta la opción Habilitar script con -Quiet.Esto es útil para corregir los dispositivos que tienen deshabilitada la tarea Actualización de arranque seguro.
.EXAMPLE .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\servidor\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -TargetOU "OU=Workstations,DC=contoso,DC=com"
.EXAMPLE # Lista de cubos bloqueados .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets
.EXAMPLE # Desbloquear un cubo específico .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -UnblockBucket "Dell_Latitude5520_BIOS1.2.3"
.EXAMPLE # Excluir dispositivos VIP de la implementación con un archivo de texto .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\servidor\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExclusionListPath "C:\Admin\VIP-Devices.txt"
.EXAMPLE # Excluir dispositivos en un grupo de seguridad de AD (por ejemplo, portátiles ejecutivos) .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\servidor\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExcludeADGroup "VIP-Computers"
.EXAMPLE # Use WinCS (Sistema de configuración de Windows) en lugar de GPO/AvailableUpdatesPolicy # WinCsFlags.exe se ejecuta en el contexto del SISTEMA en cada punto de conexión a través de una tarea programada .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\servidor\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -UseWinCS ' -WinCSKey "F33E0C8E002" #>
[CmdletBinding()] parámetro( [Parámetro(Obligatorio = $false)] [cadena]$AggregationInputPath, [Parámetro(Obligatorio = $false)] [cadena]$ReportBasePath, [Parámetro(Obligatorio = $false)] [cadena]$TargetOU, [Parámetro(Obligatorio = $false)] [string]$WavePrefix = "SecureBoot-Rollout", [Parámetro(Obligatorio = $false)] [int]$MaxWaitHours = 72, [Parámetro(Obligatorio = $false)] [int]$PollIntervalMinutes = 1440,
[Parameter(Mandatory = $false)] [int]$ProcessingBatchSize = 5000,
[Parameter(Mandatory = $false)] [int]$DeviceLogSampleSize = 25,
[Parameter(Mandatory = $false)] [conmutador]$LargeScaleMode, # ============================================================================ # Parámetros AllowList /BlockList # ============================================================================ # Lista de permitidos = Incluir solo estos dispositivos (implementación dirigida) # Lista de bloqueados = Excluir estos dispositivos (nunca se implementarán) # Procesando orden: Primero AllowList (si se especifica) y luego BlockList [Parámetro(Obligatorio = $false)] [cadena]$AllowListPath, [Parámetro(Obligatorio = $false)] [cadena]$AllowADGroup, [Parámetro(Obligatorio = $false)] [cadena]$ExclusionListPath, [Parámetro(Obligatorio = $false)] [cadena]$ExcludeADGroup, # ============================================================================ # Parámetros de WinCS (sistema de configuración de Windows) # ============================================================================ # WinCS es una alternativa a la implementación de GPO AvailableUpdatesPolicy. # Usa WinCsFlags.exe en cada punto de conexión para habilitar la implementación de arranque seguro.# WinCsFlags.exe se ejecuta en el contexto SYSTEM del punto de conexión.Referencia de n.º : https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe [Parameter(Mandatory = $false)] [conmutador]$UseWinCS, [Parámetro(Obligatorio = $false)] [cadena]$WinCSKey = "F33E0C8E002", [Parámetro(Obligatorio = $false)] [conmutador]$DryRun, [Parámetro(Obligatorio = $false)] [conmutador]$ListBlockedBuckets, [Parameter(Mandatory = $false)] [cadena]$UnblockBucket, [Parámetro(Obligatorio = $false)] [conmutador]$UnblockAll, [Parameter(Mandatory = $false)] [conmutador]$EnableTaskOnDisabled )
$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Ejemplos de implementación y supervisión"
# ============================================================================ VALIDACIÓN DE DEPENDENCIA # # ============================================================================
function Test-ScriptDependencies { parámetro( [Parámetro(Obligatorio = $true)] [cadena]$ScriptDirectory, [Parámetro(Obligatorio = $true)] [cadena[]]$RequiredScripts ) $missingScripts = @() foreach ($script en $RequiredScripts) { $scriptPath = Join-Path $ScriptDirectory $script if (-not (Test-Path $scriptPath)) { $missingScripts += $script } } if ($missingScripts.Count -gt 0) { Write-Host "" Write-Host ("=" * 70) -ForegroundColor Red Write-Host " DEPENDENCIAS QUE FALTAN" -ForegroundColor Red Write-Host ("=" * 70) -ForegroundColor Red Write-Host "" Write-Host "No se encontraron los siguientes scripts necesarios:" -ForegroundColor Yellow foreach ($script en $missingScripts) { Write-Host " - $script" -ForegroundColor White } Write-Host "" Write-Host "Descarga los scripts más recientes de:" -ForegroundColor Cyan Write-Host " URL: $DownloadUrl" -ForegroundColor White Write-Host " Vaya a: '$DownloadSubPage'" -ForegroundColor White Write-Host "" Write-Host "Extraer todos los scripts al mismo directorio y volver a ejecutarlos". -ForegroundColor Yellow 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)) { salir 1 }
# ============================================================================ # VALIDACIÓN DE PARÁMETROS # ============================================================================
# Admin commands only need ReportBasePath $isAdminCommand = $ListBlockedBuckets o $UnblockBucket o $UnblockAll o $EnableTaskOnDisabled
if (-not $ReportBasePath) { Write-Host "ERROR: -ReportBasePath es obligatorio". -ForegroundColor Red salir 1 }
if (-not $isAdminCommand -and -not $AggregationInputPath) { Write-Host "ERROR: -AggregationInputPath es necesario para la implementación (no es necesario para -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red salir 1 }
# ============================================================================ # DETECCIÓN DE GPO: COMPROBAR GPO DE DETECCIÓN # ============================================================================
if (-not $isAdminCommand -and -not $DryRun) { $CollectionGPOName = "SecureBoot-EventCollection" # Comprueba si el módulo GroupPolicy está disponible if (Get-Module -ListAvailable -Name GroupPolicy) { Import-Module GroupPolicy -ErrorAction SilentlyContinue Write-Host "Comprobación de GPO de detección..." -ForegroundColor Yellow prueba { # Comprobar si existe GPO $existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue if ($existingGpo) { Write-Host "GPO de detección encontrado: $CollectionGPOName" -ForegroundColor green } else { Write-Host "" Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host " ADVERTENCIA: GPO NO ENCONTRADO DE DETECCIÓN" -ForegroundColor Yellow Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host "" Write-Host "No se encontró el '$CollectionGPOName' de GPO de detección". -ForegroundColor Yellow Write-Host "Sin este GPO, no se recopilarán datos del dispositivo". -ForegroundColor Yellow Write-Host "" Write-Host "Para implementar el GPO de detección, ejecute:" -ForegroundColor Cyan Write-Host " .\Deploy-GPO-SecureBootCollection.ps1 -DomainName <dominio> -AutoDetectOU" -ForegroundColor White Write-Host "" Write-Host "¿Continuar de todos modos? (Y/N)" -ForegroundColor Yellow $response = Host de lectura if ($response -notmatch '^[Yy]') { Write-Host "Abortando. Implemente primero el GPO de detección". -ForegroundColor Red salir 1 } } } captura { Write-Host " No se puede comprobar el GPO: $($_. Exception.Message)" -ForegroundColor Yellow } } else { Write-Host " Módulo GroupPolicy no disponible: omitir la comprobación de GPO" -ForegroundColor Gray } Write-Host "" }
# ============================================================================ RUTAS DE ACCESO A ARCHIVOS 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"
# ============================================================================ Compatibilidad con PS 5.1: ConvertTo-Hashtable # ============================================================================ # ConvertFrom-Json -AsHashtable es solo PS7+. Esto proporciona compatibilidad.
function ConvertTo-Hashtable { parámetro( [Parameter(ValueFromPipeline = $true)] $InputObject ) proceso { if ($null -eq $InputObject) { return @{} } if ($InputObject -is [System.Collections.IDictionary]) { return $InputObject } if ($InputObject -is [PSCustomObject]) { # Usar [pedido] para un pedido de claves coherente y un control seguro de duplicados $hash = [ordered]@{} foreach ($prop en $InputObject.PSObject.Properties) { # Asignación indexada controla de forma segura los duplicados mediante la sobrescritura $hash[$prop. Name] = 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 # ADMIN: Lista/Desbloquear cubos # ============================================================================
if ($ListBlockedBuckets) { Write-Host "" Write-Host ("=" * 80) -ForegroundColor Yellow Write-Host "CUBOS BLOQUEADOS" -ForegroundColor Yellow Write-Host ("=" * 80) -ForegroundColor Yellow Write-Host "" if ($blockedBucketsPath ruta de prueba) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable if ($blocked. Count -eq 0) { Write-Host "No hay cubos bloqueados". -ForegroundColor verde } else { Write-Host "Total bloqueado: $($blocked. Count)" -ForegroundColor Red Write-Host "" foreach ($key en $blocked. Teclas) { $info = $blocked[$key] Write-Host "Cubo: $key" -ForegroundColor Red Write-Host " Bloqueado en: $($info. BlockedAt)" -ForegroundColor Gray Write-Host " Motivo: $($info. Reason)" -ForegroundColor Gray Write-Host " Error de dispositivo: $($info. FailedDevice)" -ForegroundColor Gray Write-Host " Último informe: $($info. LastReported)" -ForegroundColor Gray Write-Host " Ola: $($info. WaveNumber)" -ForegroundColor Gray Write-Host " Dispositivos en cubo: $($info. DevicesInBucket)" -ForegroundColor Gray Write-Host "" } Write-Host "Para desbloquear un cubo:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Cyan Write-Host "" Write-Host "Para desbloquear todo:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Cyan } } else { Write-Host "No se ha encontrado ningún archivo de cubos bloqueados". -ForegroundColor verde } Write-Host "" salir 0 }
if ($UnblockBucket) { Write-Host "" if ($blockedBucketsPath ruta de prueba) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable if ($blocked. Contains($UnblockBucket)) { $blocked. Quitar($UnblockBucket) $blocked | ConvertTo-Json -Profundidad 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force # Agregar a la lista aprobada por el administrador para evitar volver a bloquear $adminApproved = @{} if ($adminApprovedPath ruta de prueba) { $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } $adminApproved[$UnblockBucket] = @{ ApprovedAt = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" ApprovedBy = $env:USERNAME } $adminApproved | ConvertTo-Json -Profundidad 10 | Out-File $adminApprovedPath -Encoding UTF8 -Force Write-Host "Cubo desbloqueado: $UnblockBucket" -ForegroundColor green Write-Host "Agregado a la lista aprobada por el administrador (no se volverá a bloquear automáticamente)" -ForegroundColor Cyan } else { Write-Host "Cubo no encontrado: $UnblockBucket" -ForegroundColor Yellow Write-Host "Cubos disponibles:" -ForegroundColor Gray $blocked. Teclas | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } } } else { Write-Host "No se ha encontrado ningún archivo de cubos bloqueados". -ForegroundColor Yellow } Write-Host "" salir 0 }
if ($UnblockAll) { Write-Host "" if ($blockedBucketsPath ruta de prueba) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable $count = $blocked. Contar @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Encoding UTF8 -Force Write-Host "Desbloquee todos los cubos $count". -ForegroundColor green } else { Write-Host "No se ha encontrado ningún archivo de cubos bloqueados". -ForegroundColor Yellow } Write-Host "" salir 0 }
# ============================================================================ # FUNCIONES AUXILIARES # ============================================================================
function Get-RolloutState { if ($rolloutStatePath ruta de prueba) { prueba { $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable # Validar que existen propiedades necesarias if ($null -eq $loaded. CurrentWave) { throw "Invalid state file - missing CurrentWave" } # Asegúrese de que WaveHistory es siempre una matriz (corrige la deserialización JSON de PS5.1) if ($null -eq $loaded. WaveHistory) { $loaded. WaveHistory = @() } elseif ($loaded. WaveHistory -isnot [matriz]) { $loaded. WaveHistory = @($loaded. WaveHistory) } devolver $loaded } captura { Write-Log "RolloutState.json dañadas detectadas: $($_. Exception.Message)" "WARN" Write-Log "Hacer una copia de seguridad del archivo dañado y empezar de cero" "AVISAR" $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'aaaaMMdd-HHmmss')" Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue } } devolver @{ CurrentWave = 0 StartedAt = $null LastAggregation = $null TotalDevicesTargeted = 0 TotalDevicesUpdated = 0 Status = "NotStarted" WaveHistory = @() } }
function Save-RolloutState { parámetro($State) $State | ConvertTo-Json -Profundidad 10 | Out-File $rolloutStatePath -Encoding UTF8 -Force }
function Get-WeekdayProjection { <# . SINOOPSIS Calcular la fecha de finalización prevista para los fines de semana (sin progreso en Sáb/Sol) #> parámetro( [int]$RemainingDevices, [doble]$DevicesPerDay, [datetime]$StartDate = (Get-Date) ) if ($DevicesPerDay -le 0 -or $RemainingDevices -le 0) { devolver @{ ProjectedDate = $null WorkingDaysNeeded = 0 CalendarDaysNeeded = 0 } } # Calcular los días laborables necesarios (excluyendo los fines de semana) $workingDaysNeeded = [matemáticas]::Techo($RemainingDevices / $DevicesPerDay) # Convertir días laborables en días del calendario (agregar fines de semana) $currentDate = $StartDate.Date $daysAdded = 0 $workingDaysAdded = 0 while ($workingDaysAdded -lt $workingDaysNeeded) { $currentDate = $currentDate.AddDays(1) $daysAdded++ # Contar solo los días laborables 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 { <# . SINOOPSIS Guardar resumen de implementación con información de proyección para la visualización del panel #> parámetro( [hashtable]$State, [int]$TotalDevices, [int]$UpdatedDevices, [int]$NotUpdatedDevices, [double]$DevicesPerDay ) $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" # Calcular la proyección con conocimiento de fin de semana $projection = Get-WeekdayProjection -RemainingDevices $NotUpdatedDevices -DevicesPerDay $DevicesPerDay $summary = @{ GeneratedAt = (Get-Date -Format "aaaa-MM-dd HH:mm:ss") RolloutStartDate = $State.StartedAt LastAggregation = $State.LastAggregation CurrentWave = $State.CurrentWave Status = $State.Status # Recuento de dispositivos TotalDevices = $TotalDevices UpdatedDevices = $UpdatedDevices NotUpdatedDevices = $NotUpdatedDevices PercentUpdated = if ($TotalDevices -gt 0) { [math]::Round(($UpdatedDevices / $TotalDevices) * 100, 1) } else { 0 } # Métricas de velocidad DevicesPerDay = [math]::Round($DevicesPerDay, 1) TotalDevicesTargeted = $State.TotalDevicesTargeted TotalWaves = $State.CurrentWave # Proyección con conocimiento de fin de semana ProjectedCompletionDate = $projection. Fecha Projected WorkingDaysRemaining = $projection. WorkingDaysNeeded CalendarDaysRemaining = $projection. CalendarDaysNeeded # Nota sobre la exclusión de fin de semana ProjectionNote = "La finalización proyectada excluye los fines de semana (Sáb/Dom)" } $summary | ConvertTo-Json -Profundidad 5 | Out-File $summaryPath -Encoding UTF8 -Force Write-Log "Resumen de la implementación guardado: $summaryPath" "INFO" devolver $summary }
function Get-BlockedBuckets { if ($blockedBucketsPath ruta de prueba) { return Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } devolver @{} }
function Save-BlockedBuckets { parámetro($Blocked) $Blocked | ConvertTo-Json -Profundidad 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force }
function Get-AdminApproved { if ($adminApprovedPath ruta de prueba) { return Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } devolver @{} }
function Get-DeviceHistory { if ($deviceHistoryPath ruta de prueba) { return Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } devolver @{} }
function Save-DeviceHistory { parámetro($History) $History | ConvertTo-Json -Profundidad 10 | Out-File $deviceHistoryPath -Encoding UTF8 -Force }
function Save-ProcessingCheckpoint { parámetro( [cadena]$Stage, [int]$Processed, [int]$Total, [hashtable]$Metrics = @{} )
$checkpoint = @{ Stage = $Stage UpdatedAt = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" Procesado = $Processed Total = $Total Percent = if ($Total -gt 0) { [math]::Round(($Processed / $Total) * 100, 2) } else { 0 } Métrica = $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 = if ($device. Hostname) { $device. Hostname } elseif ($device. HostName) { $device. HostName } else { $null } if ($hostname) { [void]$hostSet.Add($hostname) }
$bucketKey = Get-BucketKey $device if ($bucketKey) { if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey]++ } else { $bucketCounts[$bucketKey] = 1 } } }
return @{ HostSet = $hostSet BucketCounts = $bucketCounts } }
function Write-Log { parámetro([cadena]$Message, [cadena]$Level = "INFO") $timestamp = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" $color = conmutador ($Level) { "Correcto" { "Verde" } "WARN" { "Yellow" } "ERROR" { "Rojo" } "BLOQUEADO" { "DarkRed" } "WAVE" { "Cyan" } predeterminado { "Blanco" } } Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color # También inicia sesión en el archivo $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMMdd').log" "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Encoding UTF8 }
function Get-BucketKey { parámetro($Device) # Use BucketId del JSON del dispositivo si está disponible (hash SHA256 del script de detección) if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" } # Fallback: construcción del fabricante|modelo|bios $mfr = if ($Device.WMI_Manufacturer) { $Device.WMI_Manufacturer } else { $Device.Manufacturer } $model = if ($Device.WMI_Model) { $Device.WMI_Model } else { $Device.Model } $bios = if ($Device.BIOSDescription) { $Device.BIOSDescription } else { $Device.BIOS } devolver "$mfr|$model|$bios" }
# ============================================================================ # CARGA DE LA LISTA VIP/EXCLUSIÓN # ============================================================================
function Get-ExcludedHostnames { parámetro( [cadena]$ExclusionFilePath, [cadena]$ADGroupName ) $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Cargar desde archivo (admite .txt o .csv) if ($ExclusionFilePath -and (Test-Path $ExclusionFilePath)) { $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower() if ($extension -eq ".csv") { # Formato CSV: espera una columna "Nombre de host" o "NombreDeEquipo" $csvData = Import-Csv $ExclusionFilePath $hostCol = si ($csvData[0]. PSObject.Properties.Name -contiene 'Hostname') { 'Hostname' } elseif ($csvData[0]. PSObject.Properties.Name -contiene "NombreDeEquipo") { 'NombreDeEquipo' } elseif ($csvData[0]. PSObject.Properties.Name -contiene "Nombre") { 'Nombre' } más { $null } if ($hostCol) { foreach ($row en $csvData) { si (![ string]::IsNullOrWhiteSpace($row.$hostCol)) { [void]$excluded. Add($row.$hostCol.Trim()) } } } } else { # Texto sin formato: un nombre de host por línea Get-Content $ExclusionFilePath | ForEach-Object { $line = $_. Trim() if ($line y -not $line. StartsWith('#')) { [void]$excluded. Add($line) } } } Write-Log "$($excluded cargado. Count) nombres de host del archivo de exclusión: $ExclusionFilePath" "INFO" } # Cargar desde el grupo de seguridad de AD if ($ADGroupName) { prueba { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'computer' } foreach ($member en $groupMembers) { [void]$excluded. Add($member. Nombre) } Write-Log equipos "$($groupMembers.Count) cargados del grupo AD: $ADGroupName" "INFO" } captura { Write-Log "No se pudo cargar el grupo de AD '$ADGroupName': $_" "WARN" } } devolver @($excluded) }
# ============================================================================ # PERMITIR CARGA DE LISTA (Implementación dirigida) # ============================================================================
function Get-AllowedHostnames { <# . SINOOPSIS Carga nombres de host de un archivo AllowList o un grupo de AD para la implementación dirigida.Cuando se especifica una lista de permitidos, SOLO se incluirán estos dispositivos en la implementación.#> parámetro( [cadena]$AllowFilePath, [cadena]$ADGroupName ) $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Cargar desde archivo (admite .txt o .csv) if ($AllowFilePath -and (Test-Path $AllowFilePath)) { $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower() if ($extension -eq ".csv") { # Formato CSV: espera una columna "Nombre de host" o "NombreDeEquipo" $csvData = Import-Csv $AllowFilePath if ($csvData.Count -gt 0) { $hostCol = si ($csvData[0]. PSObject.Properties.Name -contiene 'Hostname') { 'Hostname' } elseif ($csvData[0]. PSObject.Properties.Name -contiene "NombreDeEquipo") { 'NombreDeEquipo' } elseif ($csvData[0]. PSObject.Properties.Name -contiene "Nombre") { 'Nombre' } else { $null } if ($hostCol) { foreach ($row en $csvData) { si (![ string]::IsNullOrWhiteSpace($row.$hostCol)) { [void]$allowed. Add($row.$hostCol.Trim()) } } } } } else { # Texto sin formato: un nombre de host por línea Get-Content $AllowFilePath | ForEach-Object { $line = $_. Trim() if ($line y -not $line. StartsWith('#')) { [void]$allowed. Add($line) } } } Write-Log "$($allowed cargado. Count) nombres de host del archivo de lista de permitidos: $AllowFilePath" "INFO" } # Cargar desde el grupo de seguridad de AD if ($ADGroupName) { prueba { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'computer' } foreach ($member en $groupMembers) { [void]$allowed. Add($member. Nombre) } Write-Log equipos "$($groupMembers.Count) cargados del grupo de permitidos de AD: $ADGroupName" "INFO" } captura { Write-Log "No se pudo cargar el grupo de AD '$ADGroupName': $_" "WARN" } } devolver @($allowed) }
# ============================================================================ # CALIDAD Y SUPERVISIÓN DE LOS DATOS # ============================================================================
function Get-DataFreshness { <# . SINOOPSIS Comprueba lo fresco que son los datos de detección examinando las marcas de tiempo del archivo JSON.Devuelve estadísticas sobre cuándo se notificaron por última vez los puntos de conexión.#> parámetro([cadena]$JsonPath) $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue if ($jsonFiles.Count -eq 0) { devolver @{ TotalFiles = 0 FreshFiles = 0 StaleFiles = 0 NoDataFiles = 0 OldestFile = $null NewestFile = $null AvgAgeHours = 0 Advertencia = "No se encontraron archivos JSON; es posible que no se implemente la detección" } } $now = Get-Date $freshThresholdHours = 24 # Files actualizado en las últimas 24 horas son "frescas" $staleThresholdHours = 72 # Files más de 72 horas son "obsoletos" $fresh = 0 $stale = 0 $ages = @() foreach ($file en $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 -Primero 1 $newestFile = $jsonFiles | Sort-Object LastWriteTime -Descending | Select-Object - Primeros 1 $warning = $null if ($stale -gt ($jsonFiles.Count * 0,5)) { $warning = "Más del 50 % de los dispositivos tienen datos obsoletos (>72 horas): GPO de detección de comprobación" } elseif ($fresh -lt ($jsonFiles.Count * 0,3)) { $warning = "Es posible que no se esté ejecutando menos del 30 % de los dispositivos notificados recientemente: es posible que no se esté ejecutando la detección" } devolver @{ TotalFiles = $jsonFiles.Count FreshFiles = $fresh StaleFiles = $stale MediumFiles = $jsonFiles.Count - $fresh - $stale OldestFile = $oldestFile.LastWriteTime NewestFile = $newestFile.LastWriteTime AvgAgeHours = [math]::Round(($ages | Measure-Object -Average). Promedio, 1) Advertencia = $warning } }
function Test-DetectionGPODeployed { <# . SINOOPSIS Comprueba que la infraestructura de detección/supervisión esté en su lugar.#> parámetro([cadena]$JsonPath) # Comprobar 1: existe la ruta JSON if (-not (Test-Path $JsonPath)) { devolver @{ IsDeployed = $false Message = "JSON input path does not exist: $JsonPath" } } # Marca 2: Existen al menos algunos archivos JSON $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). Contar if ($jsonCount -eq 0) { devolver @{ IsDeployed = $false Message = "No hay archivos JSON en $JsonPath: es posible que los GPO de detección no estén implementados o que los dispositivos aún no se hayan notificado" } } # Marca 3: Files son razonablemente recientes (al menos algunas en la semana pasada) $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 Message = "No hay archivos JSON actualizados en los últimos 7 días: es posible que el GPO de detección esté roto o que los dispositivos estén desconectados" } } devolver @{ IsDeployed = $true Message = "Detección aparece activa: $jsonCount archivos, $($recentFiles.Count) actualizados recientemente" FileCount = $jsonCount RecentCount = $recentFiles.Count } }
# ============================================================================ # SEGUIMIENTO DE DISPOSITIVOS (POR NOMBRE DE HOST) # ============================================================================
function Update-DeviceHistory { <# . SINOOPSIS Realiza un seguimiento de los dispositivos por nombre de host, ya que no tenemos un identificador de máquina único.Nota: BucketId es uno a varios (misma configuración de hardware = mismo bucket).Si se agrega un identificador único a la colección JSON, actualice esta función.#> parámetro( [matriz]$CurrentDevices, [hashtable]$DeviceHistory ) foreach ($device en $CurrentDevices) { $hostname = $device. Nombre de host if (-not $hostname) { continue } # Realizar un seguimiento del dispositivo por nombre de host $DeviceHistory[$hostname] = @{ Hostname = $hostname BucketId = $device. BucketId Manufacturer = $device. WMI_Manufacturer Model = $device. WMI_Model LastSeen = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" Estado = $device. Estado de actualización } } }
# ============================================================================ # BLOCKED BUCKET DETECTION (Basado en la accesibilidad del dispositivo) # ============================================================================
<# . DESCRIPCIÓN Lógica de bloqueo: - Un cubo SOLO se bloquea si: 1. El dispositivo fue dirigido en una ola 2. MaxWaitHours ha pasado desde que comenzó la ola 3. El dispositivo no es ACCESIBLE (error de ping) - Si el dispositivo es accesible pero aún no se ha actualizado, seguimos esperando (es posible que haya una actualización pendiente de reinicio: el evento 1808 solo se activa después del reinicio) - El dispositivo inalcanzable indica que se ha producido un error y necesita investigación Desbloqueo: - Usa -ListBlockedBuckets para ver cubos bloqueados - Usa -UnblockBucket "BucketKey" para desbloquear un bucket específico - Use -UnblockAll para desbloquear todos los cubos #>
function Test-DeviceReachable { parámetro( [cadena]$Hostname, [string]$DataPath # Ruta de acceso a los archivos JSON del dispositivo ) # Método 1: Comprobar la marca de tiempo del archivo JSON (lo más rápido: no es necesario analizar archivos) # Si el script de detección se ejecutó recientemente, el archivo se escribió o actualizó, lo que demuestra que el dispositivo está activo if ($DataPath) { $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object -Primero 1 if ($deviceFile) { $hoursSinceWrite = ((Get-Date) - $deviceFile.LastWriteTime). TotalHours if ($hoursSinceWrite -lt 72) { return $true } } } # Método 2: fallback to ping (solo si JSON está obsoleto o falta) prueba { $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, [hashtable]$NotUpdatedIndexes, [int]$MaxWaitHours, [bool]$DryRun = $false ) $now = Get-Date $newlyBlocked = @() $stillWaiting = @() $devicesToCheck = @() $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } $bucketCounts = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.BucketCounts) { $NotUpdatedIndexes.BucketCounts } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). BucketCounts } # Recopila dispositivos que han pasado el período de espera y aún no se han actualizado foreach ($wave en $RolloutState.WaveHistory) { si (no $wave. StartedAt) { continue } $waveStart = [DateTime]::P arse($wave. StartedAt) $hoursSinceWave = ($now - $waveStart). TotalHours if ($hoursSinceWave -lt $MaxWaitHours) { # Aún dentro del período de espera: no lo compruebes todavía continuar } # Comprueba cada dispositivo de esta ola foreach ($deviceInfo en $wave. Dispositivos) { $hostname = $deviceInfo.Hostname $bucketKey = $deviceInfo.BucketKey # Omitir si el cubo ya está bloqueado if ($BlockedBuckets.Contains($bucketKey)) { continue } # Omitir si el cubo está aprobado por el administrador Y la tanda se inició ANTES de la aprobación # (solo comprueba los dispositivos dirigidos DESPUÉS de la aprobación del administrador para volver a bloquearlo) if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) { $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. ApprovedAt) if ($waveStart -lt $approvalTime) { # Este dispositivo estaba dirigido antes de la aprobación del administrador: omitir continuar } # Ola iniciada después de la aprobación: esto es una nueva segmentación, puede comprobar } # ¿Sigue este dispositivo en la lista NotUpdated? 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 "Comprobación del alcance de los dispositivos $($devicesToCheck.Count) pasado el período de espera..." "INFORMACIÓN" # Realizar un seguimiento de errores por cubo para la toma de decisiones $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() } # Comprobar la accesibilidad de cada dispositivo foreach ($device en $devicesToCheck) { $hostname = $device. Nombre de host $bucketKey = $device. BucketKey if ($DryRun) { Write-Log "[DRYRUN] Comprobaría $hostname alcance" "INFO" continuar } if (-not $bucketFailures.ContainsKey($bucketKey)) { $bucketFailures[$bucketKey] = @{ Unreachable = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. HoursSinceWave } } $isReachable = Test-DeviceReachable -Hostname $hostname -DataPath $AggregationInputPath if (-not $isReachable) { $bucketFailures[$bucketKey]. No accesible += $hostname } else { # El dispositivo es accesible pero aún no actualizado: podría ser un error temporal o esperar a reiniciar $bucketFailures[$bucketKey]. AliveButFailed += $hostname $stillWaiting += $hostname } } # Decisión por cubo: bloquear solo si los dispositivos son realmente INALCANZABLES # Dispositivos vivos con errores = temporales, continuar lanzamiento foreach ($bucketKey en $bucketFailures.Keys) { $bf = $bucketFailures[$bucketKey] $unreachableCount = $bf. Unreachable.Count $aliveFailedCount = $bf. AliveButFailed.Count # Comprueba si este bucket tiene éxitos (de datos actualizados de dispositivos) $bucketHasSuccesses = $stSuccessBuckets y $stSuccessBuckets.Contains($bucketKey) if ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) { # No se puede acceder a todos los dispositivos con errores: bloquee el bucket if ($newlyBlocked -notcontains $bucketKey) { $BlockedBuckets[$bucketKey] = @{ BlockedAt = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" Reason = "Todos los dispositivos $unreachableCount no accesibles después de $($bf. HoursSinceWave) horas" FailedDevices = ($bf. Unreachable -join ", ") WaveNumber = $bf. WaveNumber DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } else { 0 } } $newlyBlocked += $bucketKey Write-Log "BUCKET BLOCKED: $bucketKey ($unreachableCount dispositivos) no accesibles: $($bf. Unreachable -join ', '))" "BLOCKED" } } elseif ($aliveFailedCount -gt 0) { # Los dispositivos están vivos pero no actualizados: error temporal, NO bloquear Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length)))..: $aliveFailedCount dispositivos vivos pero pendientes, $unreachableCount inalcanzable - NOT blocking (temporary)" "INFO" if ($unreachableCount -gt 0) { Write-Log " Inalcanzable: $($bf. No accesible -join ', ')" "WARN" } Write-Log " Vivo pero pendiente: $($bf. AliveButFailed -join ', ')" "INFO" # Realizar un seguimiento del recuento de errores en el estado de implementación para la supervisión if (-not $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} } $RolloutState.TemporaryFailures[$bucketKey] = @{ AliveButFailed = $bf. AliveButFailed Inalcanzable = $bf. No accesible LastChecked = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" } } } if ($stillWaiting.Count -gt 0) { Write-Log "Dispositivos accesibles pero pendientes de actualización (puede que sea necesario reiniciar): $($stillWaiting.Count)" "INFO" } devolver $newlyBlocked }
# ============================================================================ # AUTOBLOQUEAR: Desbloquear cubos cuando los dispositivos se actualicen correctamente # ============================================================================
function Update-AutoUnblockedBuckets { <# . DESCRIPCIÓN Comprueba si se han actualizado los dispositivos de los cubos bloqueados (evento 1808). Se desbloquea automáticamente si todos los dispositivos de destino del cubo se han actualizado.Si solo se han actualizado algunos dispositivos, notifica a los administradores que pueden desbloquear manualmente. Administración se pueden desbloquear manualmente mediante: .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "ruta" -UnblockBucket "BucketKey" #> parámetro( $BlockedBuckets, $RolloutState, [matriz]$NotUpdatedDevices, [cadena]$ReportBasePath, [hashtable]$NotUpdatedIndexes, [int]$LogSampleSize = 25 ) $autoUnblocked = @() $bucketsToCheck = @($BlockedBuckets.Keys) $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } foreach ($bucketKey en $bucketsToCheck) { $bucketInfo = $BlockedBuckets[$bucketKey] # Consigue históricamente todos los dispositivos a los que nos hemos dirigido desde este bucket $targetedDevicesInBucket = @() foreach ($wave en $RolloutState.WaveHistory) { $targetedDevicesInBucket += @($wave. Dispositivos | Where-Object { $_. BucketKey -eq $bucketKey }) } if ($targetedDevicesInBucket.Count -eq 0) { continue } # Comprobar cuántos dispositivos de destino siguen en NotUpdated frente a los actualizados $updatedDevices = @() $stillPendingDevices = @() foreach ($targetedDevice en $targetedDevicesInBucket) { if ($hostSet.Contains($targetedDevice.Hostname)) { $stillPendingDevices += $targetedDevice.Hostname } else { $updatedDevices += $targetedDevice.Hostname } } if ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) { # TODOS los dispositivos de destino se han actualizado: ¡desbloquee automáticamente! $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 "DESBLOQUEADO AUTOMÁTICAMENTE: $bucketKey (Todos los dispositivos de destino $($updatedDevices.Count) actualizados correctamente)" "Correcto" # Incremente el recuento de ondas de OEM para el OEM de este bucket (seguimiento por OEM) $bucketOEM = if ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' } # Extraer OEM de una clave delimitada por tuberías o de un valor predeterminado if (-not $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } $currentWave = if ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } más { 0 } $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1 Write-Log " Recuento de ondas de OEM '$bucketOEM' incrementado a dispositivos $($currentWave + 1) (siguiente asignación: $([int][Math]::P ow(2, $currentWave + 1)))" "INFO" } elseif ($updatedDevices.Count -gt 0 -y $stillPendingDevices.Count -gt 0) { # Algunos dispositivos actualizados, pero otros aún están pendientes: notificar al administrador (solo una vez) if (-not $bucketInfo.UnblockCandidate) { $bucketInfo.UnblockCandidate = $true $bucketInfo.UpdatedDevices = $updatedDevices $bucketInfo.PendingDevices = $stillPendingDevices $bucketInfo.NotificadoAt = (Get-Date). ToString("aaaa-MM-dd HH:mm:ss") Write-Log "" "INFO" Write-Log "========== ACTUALIZACIÓN PARCIAL EN EL ========== DE CUBO BLOQUEADO" "INFORMACIÓN" Write-Log "Cubo: $bucketKey" "INFORMACIÓN" $updatedSample = @($updatedDevices | Select-Object -First $LogSampleSize) $pendingSample = @($stillPendingDevices | Select-Object -First $LogSampleSize) $updatedSuffix = if ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) more)" } else { "" } $pendingSuffix = if ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) more)" } else { "" } Write-Log "Dispositivos actualizados ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix" "Ok" Write-Log "Todavía pendiente ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix" "WARN" Write-Log "" "INFO" Write-Log "Para desbloquear manualmente este bucket después de la verificación, ejecute:" "INFO" Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '"$ReportBasePath'" -UnblockBucket '"$bucketKey'"" "INFO" Write-Log "=======================================================" "INFORMACIÓN" Write-Log "" "INFO" } } } devolver $autoUnblocked }
# ============================================================================ # GENERACIÓN DE ONDA (INLINED - excluye cubos bloqueados) # ============================================================================
function New-RolloutWave { parámetro( [cadena]$AggregationPath, $BlockedBuckets, $RolloutState, [int]$MaxDevicesPerWave = 50, [cadena[]]$AllowedHostnames = @(), [cadena[]]$ExcludedHostnames = @() ) # Cargar datos de agregación $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" | Where-Object { $_. Name -notlike "*Buckets*" } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $notUptodateCsv) { Write-Log "No se encontró ningún CSV sin fechaDeFinalización" "ERROR" devolver $null } $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName) # Normalize HostName -> Hostname for consistency (CSV utiliza HostName, code uses Hostname) foreach ($device en $allNotUpdated) { if ($device. PSObject.Properties['HostName'] -y -not $device. PSObject.Properties['Hostname']) { $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName -Force } } # Filtrar cubos bloqueados $eligibleDevices = @($allNotUpdated | Where-Object { $bucketKey = Get-BucketKey $_ -not $BlockedBuckets.Contains($bucketKey) }) # Filtrar por SOLO dispositivos permitidos (si se especifica AllowList) # AllowList = implementación dirigida: solo se tendrán en cuenta estos dispositivos if ($AllowedHostnames.Count -gt 0) { $beforeCount = $eligibleDevices.Count $eligibleDevices = @($eligibleDevices | Where-Object { $_. Hostname -in $AllowedHostnames }) $allowedCount = $eligibleDevices.Count Write-Log "Lista de permitidos aplicada: $allowedCount de dispositivos $beforeCount están en la lista de permitidos" "INFORMACIÓN" } # Filtrar dispositivos VIP/excluidos (Lista de bloqueados) # Lista de bloqueados se aplica AFTER AllowList if ($ExcludedHostnames.Count -gt 0) { $beforeCount = $eligibleDevices.Count $eligibleDevices = @($eligibleDevices | Where-Object { $_. Hostname -notin $ExcludedHostnames }) $excludedCount = $beforeCount - $eligibleDevices.Count if ($excludedCount -gt 0) { Write-Log "Excluidos $excludedCount dispositivos VIP/protegidos del lanzamiento" "INFO" } } if ($eligibleDevices.Count -eq 0) { Write-Log "No quedan dispositivos aptos (todos actualizados o bloqueados)" "Correcto" devolver $null } # Obtener dispositivos que ya están en implementación (desde ondas anteriores) $devicesAlreadyInRollout = @() if ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) { $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object { $_. Dispositivos | ForEach-Object { $_. Nombre de host } } | Where-Object { $_ }) } Write-Log "Dispositivos ya en implementación: $($devicesAlreadyInRollout.Count)" "INFO" # Separe por nivel de confianza $highConfidenceDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -eq "High Confidence" -and $_. Hostname -notin $devicesAlreadyInRollout }) # Acción Requerida incluye: # - Explícita "Acción necesaria" # - Nivel De Confianza Vacío/Nulo # - CUALQUIER valor de ConfidenceLevel desconocido o no reconocido (tratado como Acción necesaria) $knownSafeCategories = @( "Alta confianza", "Temporalmente pausada", "En observación", "Bajo observación - Se necesitan más datos", "No compatible", "No compatible: limitación conocida" ) $actionRequiredDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -notin $knownSafeCategories y $_. Hostname -notin $devicesAlreadyInRollout }) Write-Log "Confianza alta (no en el lanzamiento): $($highConfidenceDevices.Count)" "INFO" Write-Log "Acción necesaria (no en el lanzamiento): $($actionRequiredDevices.Count)" "INFO" # Crea dispositivos wave $waveDevices = @() # ALTA CONFIANZA: Incluir TODO (seguro para su implementación) if ($highConfidenceDevices.Count -gt 0) { Write-Log "Agregando todos los dispositivos $($highConfidenceDevices.Count) de alta confianza" "WAVE" $waveDevices += $highConfidenceDevices } # ACCIÓN REQUERIDA: Implementación progresiva (basada en cubos con oem-spread para cubos de éxito cero) # Estrategia: # - Cubos con 0 éxitos: repartidos entre OEM (1 por OEM -> 2 por OEM -> 4 por OEM) # - Cubos con ≥1 correcto: doble libremente sin restricción de OEM if ($actionRequiredDevices.Count -gt 0) { # Cargar el número correcto del bucket desde dispositivos CSV actualizados (dispositivos que se han actualizado correctamente) $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" | Sort-Object LastWriteTime -Descending | Select-Object - Primeros 1 $bucketStats = @{} if ($updatedCsv) { $updatedDevices = Import-Csv $updatedCsv.FullName # Contar éxitos por BucketId $updatedDevices | ForEach-Object { $key = Get-BucketKey $_ if ($key) { if (-not $bucketStats.ContainsKey($key)) { $bucketStats[$key] = @{ Successes = 0; Pendiente = 0; Total = 0 } } $bucketStats[$key]. Successes++ $bucketStats[$key]. Total++ } } Write-Log dispositivos actualizados "Loaded $($updatedDevices.Count) en cubos $($bucketStats.Count)" "INFO" } else { # Fallback: prueba ActionRequired_Buckets CSV $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" | Sort-Object LastWriteTime -Descending | Select-Object - Primeros 1 if ($bucketsCsv) { Import-Csv $bucketsCsv.FullName | ForEach-Object { $key = si ($_. BucketId) { $_. BucketId } else { "$($_. Fabricante)|$($_. Modelo)|$($_. BIOS)" } $bucketStats[$key] = @{ Successes = [int]$_. Éxitos Pendiente = [int]$_. Pendiente Total = [int]$_. TotalDevices } } } } # Group NotUpdated dispositivos por cubo (fabricante|Modelo|BIOS) $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ } # Cubos separados: cero éxito vs tiene-éxito $zeroSuccessBuckets = @() $hasSuccessBuckets = @() foreach ($bucket en $buckets) { $bucketKey = $bucket. Nombre $bucketDevices = @($bucket. Grupo) $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Nombre de host }) # Contar éxitos en este bucket $stats = $bucketStats[$bucketKey] $successes = if ($stats) { $stats. Éxitos } más { 0 } # Busca dispositivos implementados en este bucket a partir del historial de olas $deployedToBucket = @() foreach ($wave en $RolloutState.WaveHistory) { foreach ($device en $wave. Dispositivos) { if ($device. BucketKey -eq $bucketKey y $device. Nombre de host) { $deployedToBucket += $device. Nombre de host } } } $deployedToBucket = @($deployedToBucket | Sort-Object -Unique) # Comprueba si TODOS los dispositivos implementados han notificado su éxito $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames }) $confirmedSuccess = $deployedToBucket.Count - $stillPending.Count # Si está pendiente, omite este bucket hasta que todos confirmen 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 } # Restante apto = dispositivos aún no implementados $devicesNotYetTargeted = @($bucketDevices | Where-Object { $_. Hostname -notin $deployedToBucket }) if ($devicesNotYetTargeted.Count -eq 0) { continue } # Categorizar por recuento de éxitos $bucketInfo = @{ BucketKey = $bucketKey Dispositivos = $devicesNotYetTargeted ConfirmedSuccess = $confirmedSuccess Successes = $successes OEM = if ($bucket. Grupo[0]. WMI_Manufacturer) { $bucket. Grupo[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } más { 'Desconocido' } } if ($successes -eq 0) { $zeroSuccessBuckets += $bucketInfo } else { $hasSuccessBuckets += $bucketInfo } } # === PROCESS HAS-SUCCESS BUCKETS (≥1 success) === # Duplica el número de éxitos: si 14 se han realizado correctamente, implementa 28 a continuación foreach ($bucketInfo en $hasSuccessBuckets) { $nextBatchSize = $bucketInfo.Éxitos * 2 $nextBatchSize = [Matemáticas]::Mín($nextBatchSize, $MaxDevicesPerWave) $nextBatchSize = [Matemáticas]::Mín($nextBatchSize, $bucketInfo.Dispositivos.Contar) if ($nextBatchSize -gt 0) { $selectedDevices = @($bucketInfo.Devices | Select-Object -First $nextBatchSize) $waveDevices += $selectedDevices $parts = if ($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" } } # === CUBOS PROCESS ZERO-SUCCESS (distribuidos entre OEM con seguimiento por OEM) === # Objetivo: repartir el riesgo entre diferentes OEM, realizar un seguimiento del progreso por OEM de forma independiente # Cada OEM progresa según su propio historial de éxitos: # - OEM con éxitos: obtiene más dispositivos en la siguiente tanda (2^waveCount) # - OEM sin éxitos: permanece en el nivel actual hasta que se confirme el éxito if ($zeroSuccessBuckets.Count -gt 0) { # Inicializar por ola de OEM cuenta si no existe if (-not $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } # Agrupar cubos de éxito cero por OEM $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM } $totalZeroSuccessAdded = 0 $oemsDeployedTo = @() foreach ($oemGroup en $oemBuckets) { $oemName = $oemGroup.Name # Obtén el recuento de ondas de este OEM (comienza en 0) $oemWaveCount = if ($RolloutState.OEMWaveCounts[$oemName]) { $RolloutState.OEMWaveCounts[$oemName] } else { 0 } # Calcular dispositivos para ESTE OEM: 2^waveCount (1, 2, 4, 8...) $devicesForThisOEM = [int][Matemáticas]::P ow(2, $oemWaveCount) $devicesForThisOEM = [Matemáticas]::Max(1, $devicesForThisOEM) $oemDevicesAdded = 0 # Elige en cada cubo de este OEM foreach ($bucketInfo en $oemGroup.Group) { $remaining = $devicesForThisOEM - $oemDevicesAdded if ($remaining -le 0) { break } $toTake = [Matemáticas]::Mín($remaining, $bucketInfo.Dispositivos.Contar) if ($toTake -gt 0) { $selectedDevices = @($bucketInfo.Devices | Select-Object -First $toTake) $waveDevices += $selectedDevices $oemDevicesAdded += $toTake $totalZeroSuccessAdded += $toTake $parts = if ($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 ($oemWaveCount de ola oem = ${devicesForThisOEM}/OEM)" "WARN" } } if ($oemDevicesAdded -gt 0) { Write-Log " OEM: $oemName - Wave $oemWaveCount, dispositivos $oemDevicesAdded agregados" "INFO" $oemsDeployedTo += $oemName } } # Realizar un seguimiento de los OEM en los que hemos implementado (para incrementar en la siguiente comprobación de éxito) if ($oemsDeployedTo.Count -gt 0) { $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo Write-Log "Implementación de éxito cero: $totalZeroSuccessAdded dispositivos en los OEM de $($oemsDeployedTo.Count)" "INFO" } } } si (@($waveDevices). Count -eq 0) { devolver $null } devolver $waveDevices }
# ============================================================================ # IMPLEMENTACIÓN DE GPO (INLINED: crea GPO, grupo de seguridad, vínculos) # ============================================================================
function Deploy-GPOForWave { parámetro( [cadena]$GPOName, [cadena]$TargetOU, [cadena]$SecurityGroupName, [matriz]$WaveHostnames, [bool]$DryRun = $false ) # Directiva ADMX: SecureBoot.admx - SecureBoot_AvailableUpdatesPolicy # Ruta del Registro: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot # Nombre del valor: AvailableUpdatesPolicy # Valor habilitado: 22852 (0x5944): actualizar todas las claves de arranque seguro + bootmgr # Valor deshabilitado: 0 # # Uso de preferencias de directiva de grupo (GPP) para una implementación fiable de la ruta de acceso HKLM\SYSTEM # GPP crea la configuración en: Preferencias > configuración del equipo > configuración de Windows > registro $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" $RegistryValueName = "AvailableUpdatesPolicy" $RegistryValue = 22852 # 0x5944: coincide con valor habilitado para ADMX Write-Log "Implementar GPO: $GPOName" "WAVE" Write-Log "Registro: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X'))" "INFO" if ($DryRun) { Write-Log "[DRYRUN] crearía GPO: $GPOName" "INFO" Write-Log "[DRYRUN] Crearía un grupo de seguridad: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] Agregaría $(@($WaveHostnames). Contar) equipos para agrupar" "INFO" Write-Log "[DRYRUN] vincularía GPO a: $TargetOU" "INFO" devolver $true } prueba { # Importar módulos necesarios Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } captura { Write-Log "Error al importar módulos necesarios (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR" devolver $false } # Paso 1: Crear u obtener GPO $existingGPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue if ($existingGPO) { Write-Log "GPO ya existe: $GPOName" "INFORMACIÓN" $gpo = $existingGPO } else { prueba { $gpo = New-GPO -Name $GPOName -Comment "Implementación de certificados de arranque seguro - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "GPO creado: $GPOName" "Correcto" } captura { Write-Log "Error al crear GPO: $($_. Exception.Message)" "ERROR" devolver $false } } # Paso 2: Establecer el valor del Registro mediante las preferencias de directiva de grupo (GPP) # GPP es más confiable para las rutas HKLM\SYSTEM que Set-GPRegistryValue prueba { # En primer lugar, intente quitar las preferencias existentes para este valor (para evitar duplicados) Remove-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue # Crear preferencias de registro GPP con la acción "Reemplazar" # Reemplazar = Crear si no existe, Actualizar si existe (la más confiable) # Update = Solo actualizar si existe (se produce un error si no existe el valor) Set-GPPrefRegistryValue -Name $GPOName ' -Context Computer ' -Acción Reemplazar ' -Key $RegistryKey ' -ValueName $RegistryValueName ' -Type DWord ' -Valor $RegistryValue Write-Log "Preferencia de registro GPP configurada: $RegistryValueName = 0x5944 (Action=Replace)" "Ok" } captura { Write-Log "Error de GPP, intentando Set-GPRegistryValue: $($_. Exception.Message)" "WARN" # Fallback to Set-GPRegistryValue (funciona si se implementa ADMX) prueba { Set-GPRegistryValue -Name $GPOName ' -Key $RegistryKey ' -ValueName $RegistryValueName ' -Type DWord ' -Valor $RegistryValue Write-Log "Registro configurado a través de Set-GPRegistryValue: $RegistryValueName = 0x5944" "Correcto" } captura { Write-Log "Error al establecer el valor del Registro: $($_. Exception.Message)" "ERROR" devolver $false } } # Paso 3: Crear u obtener un grupo de seguridad $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $existingGroup) { prueba { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers targeted for Secure Boot rollout - $GPOName" ' -PassThru Write-Log "Grupo de seguridad creado: $SecurityGroupName" "Correcto" } captura { Write-Log "Error al crear un grupo de seguridad: $($_. Exception.Message)" "ERROR" devolver $false } } else { Write-Log "Existe un grupo de seguridad: $SecurityGroupName" "INFO" $group = $existingGroup } # Paso 4: Agregar equipos a un grupo de seguridad $added = 0 $failed = 0 foreach ($hostname en $WaveHostnames) { prueba { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } captura { $failed++ } } Write-Log "Se han agregado equipos $added a un grupo de seguridad (no se $failed encuentra en AD)" "Correcto" # Paso 5: Configurar el filtrado de seguridad en GPO prueba { # Quitar "Usuarios autenticados" predeterminado Aplicar permiso (mantener leído) Set-GPPermission -Name $GPOName -TargetName "Usuarios autenticados" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue # Agregar permiso para nuestro grupo de seguridad Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Filtrado de seguridad configurado para: $SecurityGroupName" "Correcto" } captura { Write-Log "Error al configurar el filtrado de seguridad: $($_. Exception.Message)" "WARN" Write-Log "GPO puede aplicarse a todos los equipos de la unidad organizativa vinculada: comprobar manualmente" "AVISAR" } # Paso 6: Vincular GPO a ou (CRÍTICO para que se aplique la directiva) if ($TargetOU) { prueba { $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 vinculado a: $TargetOU" "Correcto" Write-Log "GPO se aplicará en la siguiente actualización gpupdate en equipos de destino" "INFO" } else { Write-Log "GPO ya vinculado a la unidad organizativa de destino" "INFO" } } captura { Write-Log "CRÍTICO: Error al vincular GPO a OU: $($_. Exception.Message)" "ERROR" Write-Log "GPO se ha creado pero no vinculado, no se aplicará a ningún equipo". "ERROR" Write-Log "Se requiere corrección manual: New-GPLink -Name '$GPOName' -Target '$TargetOU' -LinkEnabled Yes" "ERROR" devolver $false } } else { Write-Log "ADVERTENCIA: No targetOU specified - GPO creado pero NO VINCULADO!". "ERROR" Write-Log "Vinculación manual necesaria para que los GPO surtan efecto" "ERROR" Write-Log "Run: New-GPLink -Name '$GPOName' -Target '<your-Domain-DN>' -LinkEnabled Yes" "ERROR" } # Paso 7: Comprobar la configuración de GPO Write-Log "Verificando la configuración de GPO..." "INFORMACIÓN" prueba { $gpoReport = Get-GPO -Name $GPOName -ErrorAction Stop Write-Log "Estado de GPO: $($gpoReport.GpoStatus)" "INFO" # Comprobar si está configurada la configuración del Registro $regSettings = Get-GPRegistryValue -Name $GPOName -Key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue if (-not $regSettings) { # Prueba la comprobación del Registro GPP (ruta de acceso diferente en GPO) Write-Log "Comprobando preferencias de registro GPP..." "INFORMACIÓN" } } captura { Write-Log "No se pudo comprobar GPO: $($_. Exception.Message)" "WARN" } devolver $true }
# ============================================================================ # WINCS DEPLOYMENT (Alternativa a AvailableUpdatesPolicy GPO) # ============================================================================ Referencia de n.º: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe # # Comandos WinCS (se ejecutan en el punto de conexión en el contexto del SISTEMA): # Consulta: WinCsFlags.exe /query --key F33E0C8E002 # Aplicar: WinCsFlags.exe /apply --key "F33E0C8E002" # Restablecer: WinCsFlags.exe /reset --key "F33E0C8E002" # # Este método implementa un GPO con una tarea programada que ejecuta WinCsFlags.exe /apply # como SYSTEM en puntos de conexión de destino. De forma similar a cómo se implementa el script de detección, # pero se ejecuta una vez (al inicio) en lugar de a diario.
function Deploy-WinCSGPOForWave { <# . SINOOPSIS Implementar la habilitación de Arranque seguro de WinCS a través de una tarea programada por GPO.. DESCRIPCIÓN Crea un GPO que implementa una tarea programada para ejecutar WinCsFlags.exe /apply en el contexto DEL SISTEMA en el inicio del equipo. El grupo de seguridad controla la identificación.. PARAMETER GPOName Nombre del GPO.. PARAMETER TargetOU UO para vincular el GPO a.. PARAMETER SecurityGroupName Grupo de seguridad para el filtrado de GPO.. PARAMETER WaveHostnames Nombres de host para agregar al grupo de seguridad.. PARAMETER WinCSKey La tecla WinCS que se aplicará (predeterminado: F33E0C8E002).. PARAMETER DryRun Si es true, registre solo lo que se haría.#> parámetro( [Parámetro(Obligatorio = $true)] [cadena]$GPOName, [Parámetro(Obligatorio = $false)] [cadena]$TargetOU, [Parámetro(Obligatorio = $true)] [cadena]$SecurityGroupName, [Parámetro(Obligatorio = $true)] [matriz]$WaveHostnames, [Parameter(Mandatory = $false)] [string]$WinCSKey = "F33E0C8E002", [Parámetro(Obligatorio = $false)] [bool]$DryRun = $false ) # Configuración de tareas programadas para WinCsFlags.exe $TaskName = "SecureBoot-WinCS-Apply" $TaskPath = "\Microsoft\Windows\SecureBoot\" $TaskDescription = "Aplica la configuración de arranque seguro a través de WinCS - Clave: $WinCSKey" Write-Log "Implementar GPO de WinCS: $GPOName" "WAVE" Write-Log "Se ejecutará la tarea: WinCsFlags.exe /apply --key '"$WinCSKey'"" "INFO" Write-Log "Trigger: At system startup (runs once as SYSTEM)" "INFO" if ($DryRun) { Write-Log "[DRYRUN] Crearía GPO: $GPOName" "INFO" Write-Log "[DRYRUN] Crearía un grupo de seguridad: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] Agregaría $(@($WaveHostnames). Contar) equipos para agrupar" "INFO" Write-Log "[DRYRUN] Implementaría la tarea programada: $TaskName" "INFO" Write-Log "[DRYRUN] vincularía GPO a: $TargetOU" "INFO" devolver @{ Success = $true GPOCreated = $false GroupCreated = $false ComputersAdded = 0 } } prueba { # Importar módulos necesarios Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } captura { Write-Log "No se pudo importar los módulos necesarios (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } # Paso 1: Crear u obtener GPO $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue if ($gpo) { Write-Log "GPO ya existe: $GPOName" "INFORMACIÓN" } else { prueba { $gpo = New-GPO -Name $GPOName -Comment "Implementación winCS de arranque seguro - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "GPO creado: $GPOName" "Correcto" } captura { Write-Log "Error al crear GPO: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } } # Paso 2: Crear xml de tareas programadas para la implementación de GPO # Esto crea una tarea que ejecuta WinCsFlags.exe /apply al inicio $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> ><RegistrationInfo <descripción>$TaskDescription</Description> WinCsFlags.exe1>Author>SYSTEM</Author WinCsFlags.exe5 /RegistrationInfo> >desencadenadores de WinCsFlags.exe7 WinCsFlags.exe9> bootTrigger <>true</Enabled> <retraso>>de</Retraso de PT5M </BootTrigger> ></Desencadenadores >de entidades de seguridad de < <Principal id="Author"> <userId>S-1-5-18</UserId> <>RunLevel>HighestAvailable</RunLevel </Principal> <>/Entidades de seguridad >configuración de < <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <>false</DisallowStartIfOnBatteries> <stopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <>de</AllowHardTerminate true> AllowHardTerminate <InicioCuando esté disponible>true</StartWhenAvailable> <RunOnlyIfNetworkAvailable>falso</RunOnlyIfNetworkAvailable> <> IdleSettings <StopOnIdleEnd>false</StopOnIdleEnd> <RestartOnIdle>falso</RestartOnIdle> </IdleSettings> <allowStartOnDemand>verdadero</AllowStartOnDemand> <>>true</Enabled> <><falso> ocultos <>false</RunOnlyIfIdle> WinCsFlags.exe03>false</DisallowStartOnRemoteAppSession> WinCsFlags.exe07 useUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine> WinCsFlags.exe11 WakeToRun>falso</WakeToRun> WinCsFlags.exe15>ExecutionTimeLimit>PT1H< /ExecutionTimeLimit WinCsFlags.exe19 DeleteExpiredTaskAfter>P30D</DeleteExpiredTaskAfter> WinCsFlags.exe23>7</> de prioridad WinCsFlags.exe27 /Configuración> WinCsFlags.exe29 Actions Context="Author"WinCsFlags.exe30 >Exec de WinCsFlags.exe31 WinCsFlags.exe33>de comandos>WinCsFlags.exe</Command WinCsFlags.exe37>/apply --key "$WinCSKey"WinCsFlags.exe39 /Arguments> WinCsFlags.exe41> /Exec >de WinCsFlags.exe43 /Acciones WinCsFlags.exe45 /> de tareas " @
# Step 3: Deploy scheduled task via GPO Preferences # Almacenar XML de tareas en SYSVOL para tareas programadas por GPO Tarea inmediata prueba { $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 } # Crear 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 'aaaa-MM-dd HH:mm:ss')" uid="{$([guid]::NewGuid(). ToString(). ToUpper())}"> <Properties action="C" name="$TaskName" runAs="NT AUTHORITY\System" logonType="S4U"> <Task version="1.3"> <> RegistrationInfo <>$TaskDescription</Description> </RegistrationInfo> >de entidades de seguridad de < <Principal id="Author"> <UserId>NT AUTHORITY\System</UserId> <>LogonType>S4U< /LogonType <>RunLevel>HighestAvailable</RunLevel ></Principal <>/Entidades de seguridad >configuración de < <> IdleSettings <duración></duration de PT5M> <></WaitTimeout de PT1H> <StopOnIdleEnd>falso</StopOnIdleEnd> <RestartOnIdle>falso</RestartOnIdle> </IdleSettings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <>false</DisallowStartIfOnBatteries> <stopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <></AllowHardTerminate true> <StartWhenAvailable>true</StartWhenAvailable> <allowStartOnDemand>verdadero</AllowStartOnDemand> <>true</Enabled> <</>ocultos de> falso <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <>7</> de prioridad <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> </Configuración> >desencadenadores de < <> timeTrigger <StartBoundary>$(Get-Date -Format 'aaaa-MM-dd')T00:00:00</StartBoundary> <>>true</Enabled >de </TimeTrigger ></Desencadenadores >acciones de < >Exec de < <>de comandos>WinCsFlags.exe</Command <>/apply --key "$WinCSKey"</Arguments> </Exec> </> de acciones </> de tareas <> /Properties <> /ImmediateTaskV2 ></Tareas programadas "@ $gppTaskXml | Out-File -FilePath (join-path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force Write-Log "Tarea programada implementada en GPO: $TaskName" "Correcto" } captura { Write-Log "Error al implementar xml de tarea programada: $($_. Exception.Message)" "WARN" Write-Log "Volver a la implementación de WinCS basada en el Registro" "INFO" # Fallback: usar el enfoque del Registro WinCS si se produce un error en la tarea programada por GPP # WinCS también se puede activar a través de la clave del Registro # (La implementación depende de la API de registro de WinCS, si está disponible) } # Paso 4: Crear u obtener un grupo de seguridad $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $group) { prueba { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Descripción "Equipos destinados al lanzamiento de WinCS de arranque seguro - $GPOName" ' -PassThru Write-Log "Grupo de seguridad creado: $SecurityGroupName" "Correcto" } captura { Write-Log "Error al crear un grupo de seguridad: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } } else { Write-Log "Existe un grupo de seguridad: $SecurityGroupName" "INFO" } # Paso 5: Agregar equipos al grupo de seguridad $added = 0 $failed = 0 foreach ($hostname en $WaveHostnames) { prueba { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } captura { $failed++ } } Write-Log "Se han agregado equipos $added a un grupo de seguridad (no se $failed encuentra en AD)" "Correcto" # Paso 6: Configurar el filtrado de seguridad en GPO prueba { Set-GPPermission -Name $GPOName -TargetName "Usuarios autenticados" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Filtrado de seguridad configurado para: $SecurityGroupName" "Correcto" } captura { Write-Log "Error al configurar el filtrado de seguridad: $($_. Exception.Message)" "WARN" } # Paso 7: Vincular GPO a ou if ($TargetOU) { prueba { $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 vinculado a: $TargetOU" "Correcto" } else { Write-Log "GPO ya vinculado a la unidad organizativa de destino" "INFORMACIÓN" } } captura { Write-Log "CRÍTICO: Error al vincular GPO a OU: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = "Error en el vínculo GPO: $($_. Exception.Message)" } } } Write-Log "Implementación de GPO de WinCS completada" "Correcto" Write-Log "Los equipos se ejecutarán WinCsFlags.exe en la próxima actualización de GPO + reinicio/inicio" "INFO" devolver @{ Success = $true GPOCreated = $true GroupCreated = $true ComputersAdded = $added ComputersFailed = $failed } }
# Wrapper function to maintain compatibility with main loop Deploy-WinCSForWave de función { parámetro( [Parámetro(Obligatorio = $true)] [matriz]$WaveHostnames, [Parámetro(Obligatorio = $false)] [cadena]$WinCSKey = "F33E0C8E002", [Parámetro(Obligatorio = $false)] [string]$WavePrefix = "SecureBoot-Rollout", [Parámetro(Obligatorio = $false)] [int]$WaveNumber = 1, [Parameter(Mandatory = $false)] [cadena]$TargetOU, [Parámetro(Obligatorio = $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 # Convertir al formato de retorno esperado devolver @{ Success = $result. Éxito Applied = $result. ComputersAdded Omitido = 0 Error = if ($result. ComputersFailed) { $result. ComputersFailed } else { 0 } Resultados = @() } }
# ============================================================================ # HABILITAR IMPLEMENTACIÓN DE TAREAS # ============================================================================ # Implementa Enable-SecureBootUpdateTask.ps1 en dispositivos con tarea programada deshabilitada.# Usa un GPO con una tarea programada inmediata que se ejecute una vez.
function Deploy-EnableTaskGPO { <# . SINOOPSIS Implemente Enable-SecureBootUpdateTask.ps1 a través de una tarea programada por GPO.. DESCRIPCIÓN Crea un GPO que implementa una tarea programada de una sola vez para habilitar la Tarea programada de actualización de arranque seguro en dispositivos de destino.. PARAMETER TargetOU UO para vincular el GPO a.. PARAMETER TargetHostnames Nombres de host de dispositivos con tarea deshabilitada (del informe de agregación).. PARAMETER DryRun Si es true, registre solo lo que se haría.#> parámetro( [Parameter(Mandatory = $false)] [cadena]$TargetOU, [Parámetro(Obligatorio = $true)] [matriz]$TargetHostnames, [Parámetro(Obligatorio = $false)] [bool]$DryRun = $false ) $GPOName = "SecureBoot-EnableTask-Remediation" $SecurityGroupName = "SecureBoot-EnableTask-Devices" $TaskName = "SecureBoot-EnableTask-OneTime" $TaskDescription = "Tarea única para habilitar la tarea programada de actualización de arranque seguro" Write-Log "=" * 70 "INFO" Write-Log "IMPLEMENTAR HABILITAR CORRECCIÓN DE TAREAS" "INFORMACIÓN" Write-Log "=" * 70 "INFO" Write-Log "Dispositivos de destino: $($TargetHostnames.Count)" "INFO" Write-Log "GPO: $GPOName" "INFORMACIÓN" Write-Log "Grupo de seguridad: $SecurityGroupName" "INFORMACIÓN" if ($DryRun) { Write-Log "[DRYRUN] crearía GPO: $GPOName" "INFO" Write-Log "[DRYRUN] Crearía un grupo de seguridad: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] Agregaría equipos $($TargetHostnames.Count) al grupo" "INFO" Write-Log "[DRYRUN] Implementaría una tarea programada por una sola vez para habilitar la actualización de arranque seguro" "INFO" Write-Log "[DRYRUN] vincularía GPO a: $TargetOU" "INFO" devolver @{ Success = $true ComputersAdded = 0 DryRun = $true } } prueba { # Importar módulos necesarios Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } captura { Write-Log "No se pudo importar los módulos necesarios: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } # Paso 1: Crear u obtener GPO $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue if ($gpo) { Write-Log "GPO ya existe: $GPOName" "INFORMACIÓN" } else { prueba { $gpo = New-GPO -Name $GPOName -Comment "Corrección de habilitación de tareas de arranque seguro - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "GPO creado: $GPOName" "Correcto" } captura { Write-Log "Error al crear GPO: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } } # Paso 2: Implementar XML de tareas programadas en GPO SYSVOL # La tarea ejecuta un comando de PowerShell para habilitar la tarea Secure-Boot-Update prueba { $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. Id.)}\Equipo\Preferencias\Tareas Programadas" if (-not (Test-Path $sysvolPath)) { New-Item directorio -ItemType -Path $sysvolPath -Force | Out-Null } # Comando de PowerShell para habilitar la tarea 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 }' Comando #Codificar para incrustación de XML seguro $encodedCommand = [Convertir]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand)) $taskGuid = [guid]::NewGuid(). ToString("B"). ToUpper() # XML de tarea programada GPP: tarea inmediata que se ejecuta una vez $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"> <imagen ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName"="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="$taskGuid" removePolicy="1" userContext="0"> <Properties action="C" name="$TaskName" runAs="NT AUTHORITY\SYSTEM" logonType="S4U"> <Task version="1.3"> <> RegistrationInfo <>$TaskDescription</Description> </RegistrationInfo> >de entidades de seguridad de < <Principal id="Author"> <>userId>S-1-5-18</UserId> <>RunLevel>HighestAvailable</RunLevel <> /Principal <>/Entidades de seguridad >configuración de < <> IdleSettings <duración></Duration> PT5M <></WaitTimeout> de PT1H <StopOnIdleEnd>falso</StopOnIdleEnd> <RestartOnIdle>falso</RestartOnIdle> </IdleSettings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <></DisallowStartIfOnBatteries false > <stopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <></AllowHardTerminate true> <Cuando está disponible>true</StartWhenAvailable> <allowStartOnDemand>verdadero</AllowStartOnDemand> <></Enabled true> <>false</Hidden> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <>7</> de prioridad <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> </Configuración> >acciones de < >Exec de < <>powershell.exe<de comandos />de comandos <Arguments>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments> </Exec> ></Acciones </> de tareas <> /Properties <>/ImmediateTaskV2 ></Tareas programadas "@ $gppTaskXml | Out-File -FilePath (join-path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force Write-Log "Tarea programada de una sola vez implementada en GPO: $TaskName" "Correcto" } captura { Write-Log "Error al implementar xml de tarea programada: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } # Paso 3: Crear u obtener un grupo de seguridad $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $group) { prueba { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers with disabled Secure-Boot-Update task - targeted for remediation" ' -PassThru Write-Log "Grupo de seguridad creado: $SecurityGroupName" "Correcto" } captura { Write-Log "No se pudo crear el grupo de seguridad: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = $_. Exception.Message } } } else { Write-Log "Existe un grupo de seguridad: $SecurityGroupName" "INFORMACIÓN" } # Paso 4: Agregar equipos a un grupo de seguridad $added = 0 $failed = 0 foreach ($hostname en $TargetHostnames) { prueba { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } captura { $failed++ Write-Log "Equipo no encontrado en AD: $hostname" "WARN" } } Write-Log "Agregado $added equipos al grupo de seguridad (no $failed encuentra en AD)" "Correcto" # Paso 5: Configurar el filtrado de seguridad en GPO prueba { Set-GPPermission -Name $GPOName -TargetName "Usuarios autenticados" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Filtrado de seguridad configurado para: $SecurityGroupName" "Correcto" } captura { Write-Log "Error al configurar el filtrado de seguridad: $($_. Exception.Message)" "WARN" } # Paso 6: Vincular GPO a UO if ($TargetOU) { prueba { $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 vinculado a: $TargetOU" "Correcto" } else { Write-Log "GPO ya vinculado a la unidad organizativa de destino" "INFORMACIÓN" } } captura { Write-Log "Error al vincular GPO a OU: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Error = "Error en el vínculo GPO: $($_. Exception.Message)" } } } else { Write-Log "No targetOU specified - GPO will need to be manually linked" "WARN" } Write-Log "" "INFORMACIÓN" Write-Log "HABILITAR IMPLEMENTACIÓN DE TAREAS COMPLETADA" "Correcto" Write-Log "Los dispositivos ejecutarán la tarea de habilitación en la próxima actualización de GPO (gpupdate)" "INFO" Write-Log "La tarea se ejecuta una vez como SISTEMA y habilita la actualización de arranque seguro" "INFO" Write-Log "" "INFO" devolver @{ Success = $true ComputersAdded = $added ComputersFailed = $failed GPOName = $GPOName SecurityGroup = $SecurityGroupName } }
# ============================================================================ # HABILITAR TAREA EN DISPOSITIVOS DESHABILITADOS # ============================================================================ if ($EnableTaskOnDisabled) { Write-Host "" Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host " ENABLE TASK REMEDIATION - Fixing Disabled Scheduled Tasks" -ForegroundColor Yellow Write-Host ("=" * 70) -ForegroundColor Yellow Write-Host "" # Buscar dispositivos con tareas deshabilitadas a partir de datos de agregación if (-not $AggregationInputPath) { Write-Host "ERROR: -AggregationInputPath es necesario para identificar dispositivos con tarea deshabilitada" -ForegroundColor Red Write-Host "Usage: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <path> -ReportBasePath <path>" -ForegroundColor Gray salir 1 } Write-Host "Análisis de dispositivos con la tarea de actualización de arranque seguro deshabilitada..." -ForegroundColor Cyan # Cargar archivos JSON y buscar dispositivos con tarea deshabilitada $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" } $disabledTaskDevices = @() foreach ($file en $jsonFiles) { prueba { $device = Get-Content $file. FullName -Raw | ConvertFrom-Json if ($device. SecureBootTaskEnabled -eq $false o $device. SecureBootTaskStatus -eq 'Disabled' -or $device. SecureBootTaskStatus -eq 'NotFound') { # Incluir solo dispositivos que aún no se hayan actualizado (ningún evento 1808) si ([int]$device. Event1808Count -eq 0) { $disabledTaskDevices += $device. Nombre de host } } } captura { # Omitir archivos no válidos } } $disabledTaskDevices = $disabledTaskDevices | Select-Object: único if ($disabledTaskDevices.Count -eq 0) { Write-Host "" Write-Host "No se encontraron dispositivos con la tarea deshabilitada Secure-Boot-Update". -ForegroundColor Green Write-Host "Todos los dispositivos tienen habilitada la tarea o ya se han actualizado". -ForegroundColor Gray salir 0 } Write-Host "" Write-Host dispositivos "Encontrado $($disabledTaskDevices.Count) con tarea deshabilitada:" -ForegroundColor Yellow $disabledTaskDevices | Select-Object - Primeros 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } if ($disabledTaskDevices.Count -gt 20) { Write-Host " ... y $($disabledTaskDevices.Count - 20) more" -ForegroundColor Gray } Write-Host "" # Implementar el GPO Habilitar tarea $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 " Equipos agregados al grupo de seguridad: $($result. ComputersAdded)" -ForegroundColor Cyan if ($result. ComputersFailed -gt 0) { Write-Host " Los equipos no se encuentran en AD: $($result. ComputersFailed)" -ForegroundColor Yellow } Write-Host "" Write-Host "PASOS SIGUIENTES:" -ForegroundColor White Write-Host " 1. Los dispositivos recibirán el GPO en la próxima actualización (gpupdate /force)" -ForegroundColor Gray Write-Host " 2. La tarea de una sola vez habilitará Secure-Boot-Update" -ForegroundColor Gray Write-Host " 3. Volver a ejecutar la agregación para comprobar que la tarea está ahora habilitada" -ForegroundColor Gray } else { Write-Host "" Write-Host "FAILED: Could not deploy Enable Task GPO" -ForegroundColor Red Write-Host "Error: $($result. Error)" -ForegroundColor Red } salir 0 }
# ============================================================================ # BUCLE DE ORQUESTACIÓN PRINCIPAL # ============================================================================
Write-Host "" Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host " SECURE BOOT ROLLOUT ORCHESTRATOR - CONTINUOUS DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host ""
if ($DryRun) { Write-Host "[MODO DE EJECUCIÓN SECA]" -ForegroundColor Magenta }
if ($UseWinCS) { Write-Host "[MODO WINCS]" -ForegroundColor Yellow Write-Host "Usar WinCsFlags.exe en lugar de GPO/AvailableUpdatesPolicy" -ForegroundColor Yellow Write-Host "Tecla WinCS: $WinCSKey" -ForegroundColor Gray Write-Host "" }
Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "Ruta de entrada: $AggregationInputPath" "INFO" Write-Log "Ruta del informe: $ReportBasePath" "INFO" if ($UseWinCS) { Write-Log "Método de implementación: WinCS (WinCsFlags.exe /apply --key '"$WinCSKey'")" "INFO" } else { Write-Log "Deployment Method: GPO (AvailableUpdatesPolicy)" "INFO" }
# Resolve TargetOU - default to domain root for domain-wide coverage # Solo es necesario para el método de implementación de GPO (WinCS no requiere AD/GPO) if (-not $UseWinCS -and -not $TargetOU) { prueba { # Prueba varios métodos para obtener DN de dominio $domainDN = $null # Método 1: Get-ADDomain (requiere RSAT-AD-PowerShell) prueba { Import-Module ActiveDirectory -ErrorAction Stop $domainDN = (Get-ADDomain -ErrorAction Stop). DistinguishedName } captura { Write-Log "Error en Get-ADDomain: $($_. Exception.Message)" "WARN" } # Método 2: Usar RootDSE a través de ADSI if (-not $domainDN) { prueba { $rootDSE = [ADSI]"LDAP://RootDSE" $domainDN = $rootDSE.defaultNamingContext.ToString() } captura { Write-Log "ERROR ADSI RootDSE: $($_. Exception.Message)" "WARN" } } # Método 3: Analizar la pertenencia a dominios del equipo if (-not $domainDN) { prueba { $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() $domainDN = "DC=" + ($domain. Name -replace '\.', ',DC=') } captura { Write-Log "Error de GetComputerDomain: $($_. Exception.Message)" "WARN" } } if ($domainDN) { $TargetOU = $domainDN Write-Log "Destino: raíz de dominio ($domainDN): GPO aplicará el filtrado de grupos de seguridad a todo el dominio" "INFO" } else { Write-Log "No se ha podido determinar el DN de dominio: se creará un GPO, pero no se vinculará". "ERROR" Write-Log "Especifique manualmente el parámetro -TargetOU o vincule GPO después de su creación" "ERROR" $TargetOU = $null } } captura { Write-Log "No se pudo obtener el DN de dominio: se creará un GPO pero no se vinculará. Vincular manualmente si es necesario". "AVISAR" Write-Log "Error: $($_. Exception.Message)" "WARN" $TargetOU = $null } } else { Write-Log "Unidad organizativa de destino: $TargetOU" "INFORMACIÓN" }
Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Intervalo de sondeo: minutos de $PollIntervalMinutes" "INFORMACIÓN" if ($LargeScaleMode) { Write-Log "LargeScaleMode habilitado (tamaño de lote: $ProcessingBatchSize, ejemplo de registro: $DeviceLogSampleSize)" "INFO" }
# ============================================================================ # COMPROBACIÓN DE REQUISITO PREVIO: Comprobar que la detección está implementada y funciona # ============================================================================
Write-Host "" Write-Log "Comprobación de requisitos previos..." "INFORMACIÓN"
$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath if (-not $detectionCheck.IsDeployed) { Write-Log $detectionCheck.Mensaje "ERROR" Write-Host "" Write-Host "REQUERIDO: Implementar primero la infraestructura de detección:" -ForegroundColor Yellow Write-Host " 1. Ejecutar: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\servidor\SecureBootLogs$'" -ForegroundColor Cyan Write-Host " 2. Espera a que los dispositivos informen (12-24 horas)" -ForegroundColor Cyan Write-Host " 3. Volver a ejecutar este organizador": ForegroundColor Cyan Write-Host "" if (-not $DryRun) { devolución } } else { Write-Log $detectionCheck.Mensaje "Aceptar" }
# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Calidad de los datos: $($freshness. TotalFiles), $($freshness. FreshFiles) fresh (<24h), $($freshness. StaleFiles) obsoleto (>72h)" "INFO" if ($freshness. Advertencia) { Write-Log $freshness. Advertencia "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 "Lista de permitidos: SOLO los dispositivos $($allowedHostnames.Count) se considerarán para su implementación" "INFO" } else { Write-Log "AllowList specified but no devices found, this will block all rollouts!" "AVISAR" } }
# Load VIP/exclusion list (BlockList) $excludedHostnames = @() if ($ExclusionListPath o $ExcludeADGroup) { $excludedHostnames = Get-ExcludedHostnames -ExclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup if ($excludedHostnames.Count -gt 0) { Write-Log "Exclusión VIP: se omitirán los dispositivos $($excludedHostnames.Count) del lanzamiento" "INFO" } }
# Load state $rolloutState = Get-RolloutState $blockedBuckets = Get-BlockedBuckets $adminApproved = Get-AdminApproved $deviceHistory = Get-DeviceHistory
if ($rolloutState.Status -eq "NotStarted") { $rolloutState.Status = "InProgress" $rolloutState.StartedAt = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" Write-Log "Iniciar nueva implementación" "WAVE" }
Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "Cubos bloqueados: $($blockedBuckets.Count)" "INFO"
# Main loop - runs until all eligible devices are updated $iterationCount = 0 while ($true) { $iterationCount++ Write-Host "" Write-Host ("=" * 80) -ForegroundColor White Write-Log "=== ITERACIÓN $iterationCount ===" "WAVE" Write-Host ("=" * 80) -ForegroundColor White # Paso 1: Ejecutar agregación Write-Log "Paso 1: Ejecución de agregaciones..." "INFORMACIÓN" # Orchestrator siempre vuelve a usar una sola carpeta (LargeScaleMode) para evitar que se bloquee el disco # Los administradores que ejecutan el agregador obtienen manualmente carpetas con marcas de tiempo para instantáneas puntuales $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current" # Comprobar la calidad de los datos antes de agregarlos $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Frescor de los datos: $($freshness. FreshFiles)/$($freshness. TotalFiles) notificados en las últimas 24h" "INFO" if ($freshness. Advertencia) { Write-Log $freshness. Advertencia "AVISAR" } $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1" $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json" $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" if ($aggregateScript ruta de prueba) { if (-not $DryRun) { # Orchestrator siempre usa streaming + incremental para aumentar la eficiencia El agregador # eleva automáticamente a PS7 si está disponible para obtener el mejor rendimiento $aggregateParams = @{ InputPath = $AggregationInputPath OutputPath = $aggregationPath StreamingMode = $true IncrementalMode = $true SkipReportIfUnchanged = $true ParallelThreads = 8 } # Pasar resumen de lanzamiento si existe (para datos de velocidad/proyección) if ($rolloutSummaryPath ruta de prueba) { $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath } & $aggregateScript @aggregateParams # Mostrar comando para generar un panel HTML completo con tablas de dispositivos Write-Host "" Write-Host "Para generar un panel HTML completo con tablas del fabricante o modelo, ejecute:" -ForegroundColor Yellow Write-Host " $aggregateScript -InputPath '"$AggregationInputPath'" -OutputPath '"$aggregationPath'"" -ForegroundColor Yellow Write-Host "" } else { Write-Log "[DRYRUN] Ejecutaría agregación" "INFO" # En DryRun, use los datos de agregación existentes de ReportBasePath directamente $aggregationPath = $ReportBasePath } } $rolloutState.LastAggregation = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" # Paso 2: Cargar el estado actual del dispositivo Write-Log "Paso 2: Cargando el estado del dispositivo..." "INFORMACIÓN" $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue | Where-Object { $_. Name -notlike "*Buckets*" } | Sort-Object LastWriteTime -Descending | Select-Object - Primeros 1 if (-not $notUptodateCsv -and -not $DryRun) { Write-Log "No se encontraron datos de agregación. Esperando..." "AVISAR" Start-Sleep -Segundos ($PollIntervalMinutes * 60) continuar } $notUpdatedDevices = if ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } else { @() } Write-Log "Dispositivos no actualizados: $($notUpdatedDevices.Count)" "INFO" $notUpdatedIndexes = Get-NotUpdatedIndexes -Devices $notUpdatedDevices # Paso 3: Actualizar el historial de dispositivos (seguimiento por nombre de host) Write-Log "Paso 3: Actualización del historial de dispositivos..." "INFORMACIÓN" Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory Save-DeviceHistory : $deviceHistory de historial # Paso 4: Comprobar si hay cubos bloqueados (dispositivos no accesibles) $existingBlockedCount = $blockedBuckets.Count Write-Log "Paso 4: Comprobar si hay cubos bloqueados (hacer ping a dispositivos tras el período de espera)..." "INFORMACIÓN" if ($existingBlockedCount -gt 0) { Write-Log "Buckets bloqueados actualmente de ejecuciones anteriores: $existingBlockedCount" "INFO" } if ($adminApproved.Count -gt 0) { Write-Log "cubos aprobados por Administración (no se volverán a bloquear): $($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 bloqueado Write-Log "Cubos recién bloqueados (esta iteración): $($newlyBlocked.Count)" "BLOCKED" } # Paso 4b: Desbloquear automáticamente los cubos en los que los dispositivos se han actualizado $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize if ($autoUnblocked.Count -gt 0) { Save-BlockedBuckets: $blockedBuckets bloqueado Write-Log "Cubos desbloqueados automáticamente (dispositivos actualizados): $($autoUnblocked.Count)" "Correcto" } # Paso 5: Calcular los dispositivos aptos restantes $eligibleCount = 0 foreach ($device en $notUpdatedDevices) { $bucketKey = Get-BucketKey $device if (-not $blockedBuckets.Contains($bucketKey)) { $eligibleCount++ } } Write-Log "Dispositivos aptos restantes: $eligibleCount" "INFO" Write-Log "Cubos bloqueados: $($blockedBuckets.Count)" "INFO" # Paso 6: Comprobar la finalización if ($eligibleCount -eq 0) { Write-Log "ROLLOUT COMPLETE - All eligible devices updated!" "Correcto" $rolloutState.Status = "Completado" $rolloutState.CompletedAt = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" Save-RolloutState -$rolloutState de estado romper } # Paso 6: Generar e implementar la siguiente tanda Write-Log "Paso 6: Generar ola de implementación..." "INFORMACIÓN" $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames # Comprueba si tenemos dispositivos que implementar ($waveDevices podrían estar $null, vacíos o con dispositivos reales) $hasDevices = $waveDevices -y @($waveDevices | Where-Object { $_ }). Contar -gt 0 if ($hasDevices) { # Solo incrementa el número de onda cuando tenemos dispositivos para implementar $rolloutState.CurrentWave++ Write-Log "Onda $($rolloutState.CurrentWave): $(@($waveDevices). Contar) dispositivos" "WAVE" # Implementar GPO con la función inlined $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $hostnames = @($waveDevices | ForEach-Object { si ($_. Nombre de host) { $_. Nombre de host } elseif ($_. HostName) { $_. HostName } else { $null } } | Where-Object { $_ }) # Guardar el archivo de nombres de host para referencia/auditoría $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt" $hostnames | Out-File $hostnamesFile -Encoding UTF8 # Valida que tenemos nombres de host en los que implementar if ($hostnames. Count -eq 0) { Write-Log "No se encontraron nombres de host válidos en wave $($rolloutState.CurrentWave): es posible que falten dispositivos en la propiedad Hostname" "WARN" Write-Log "Omitiendo la implementación para esta ola: comprobar los datos del dispositivo" "AVISAR" # Esperar antes de la siguiente iteración if (-not $DryRun) { Write-Log "Dormir $PollIntervalMinutes minutos antes de reintentar..." "INFORMACIÓN" Start-Sleep -Segundos ($PollIntervalMinutes * 60) } continuar } Write-Log "Implementación en $($hostnames. Count) nombres de host en Wave $($rolloutState.CurrentWave)" "INFO" # Implementar mediante wincs o gpo método basado en -UseWinCS parámetro if ($UseWinCS) { # Método WinCS: Crear GPO con una tarea programada para ejecutar WinCsFlags.exe como SISTEMA en cada punto de conexión Write-Log "Uso del método de implementación de WinCS (clave: $WinCSKey)" "WAVE" $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames ' -WinCSKey $WinCSKey ' -WavePrefix $WavePrefix ' -WaveNumber $rolloutState.CurrentWave ' -TargetOU $TargetOU ' -DryRun:$DryRun if (-not $wincsResult.Success) { Write-Log "La implementación de WinCS tuvo errores: aplicado: $($wincsResult.Applied), Con errores: $($wincsResult.Failed)" "WARN" } else { Write-Log "Implementación de WinCS correcta: aplicada: $($wincsResult.Applied), Omitida: $($wincsResult.Skipped)" "Correcto" } # Guardar resultados de WinCS para auditoría $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json" $wincsResult | ConvertTo-Json -Profundidad 5 | Out-File $wincsResultFile -Encoding UTF8 } else { # Método GPO: Crear GPO con la configuración del Registro AvailableUpdatesPolicy $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun if (-not $gpoResult) { Write-Log "Error en la implementación de GPO: se reintentará la siguiente iteración" "ERROR" } } # Registrar ola en estado $waveRecord = @{ WaveNumber = $rolloutState.CurrentWave StartedAt = Get-Date -Formatear "aaaa-MM-dd HH:mm:ss" DeviceCount = @($waveDevices). Contar Dispositivos = @($waveDevices | ForEach-Object { @{ Nombre de host = if ($_. Nombre de host) { $_. Nombre de host } elseif ($_. HostName) { $_. HostName } else { $null } BucketKey = Get-BucketKey $_ } }) } # Asegúrese de que WaveHistory es siempre una matriz antes de anexar (evita problemas de combinación de hashtable) $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord) $rolloutState.TotalDevicesTargeted += @($waveDevices). Contar Save-RolloutState -$rolloutState de estado Write-Log "Wave $($rolloutState.CurrentWave) implementado. Esperando $PollIntervalMinutes minutos..." "Correcto" } else { # Mostrar el estado de los dispositivos implementados que esperan actualizaciones Write-Log "" "INFORMACIÓN" Write-Log "========== TODOS LOS DISPOSITIVOS IMPLEMENTADOS: ESPERANDO ========== DE ESTADO" "INFORMACIÓN" # Obtener todos los dispositivos implementados del historial de olas $allDeployedLookup = @{} foreach ($wave en $rolloutState.WaveHistory) { foreach ($device en $wave. Dispositivos) { if ($device. Nombre de host) { $allDeployedLookup[$device. Nombre de host] = @{ Hostname = $device. Nombre de host BucketKey = $device. BucketKey DeployedAt = $wave. Introducción WaveNumber = $wave. WaveNumber } } } } $allDeployedDevices = @($allDeployedLookup.Values) if ($allDeployedDevices.Count -gt 0) { # Busca qué dispositivos implementados siguen pendientes (en la lista NotUpdated) $stillPendingCount = 0 $noLongerPendingCount = 0 $pendingSample = @() foreach ($deployed en $allDeployedDevices) { if ($notUpdatedIndexes.HostSet.Contains($deployed. Nombre de host)) { $stillPendingCount++ if ($pendingSample.Count -lt $DeviceLogSampleSize) { $pendingSample += $deployed. Nombre de host } } else { $noLongerPendingCount++ } } # Obtener recuentos actualizados reales de la agregación: diferenciar evento 1808 vs UEFICA2023Status $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Resumen*.csv" | Sort-Object LastWriteTime -Descending | Select-Object - Primeros 1 $actualUpdated = 0 $totalDevicesFromSummary = 0 $event 1808Count = 0 $uefiStatusUpdated = 0 $needsRebootSample = @() if ($summaryCsv) { $summary = Import-Csv $summaryCsv.FullName | Select-Object - Primeros 1 if ($summary. Actualizado) { $actualUpdated = [int]$summary. Actualizado } if ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices } } # Calcular la velocidad del historial de onda (dispositivos actualizados por día) $devicesPerDay = 0 if ($rolloutState.StartedAt -and $actualUpdated -gt 0) { $startDate = [datetime]::P arse($rolloutState.StartedAt) $daysElapsed = ((Get-Date) - $startDate). TotalDays if ($daysElapsed -gt 0) { $devicesPerDay = $actualUpdated/$daysElapsed } } # Guardar resumen de implementación con proyecciones con información de fin de semana # Usa el recuento NotUptodate del agregador (excluye dispositivos SB OFF) para la coherencia $notUpdatedCount = if ($summary -y $summary. NotUptodate) { [int]$summary. NotUptodate } else { $totalDevicesFromSummary - $actualUpdated } Save-RolloutSummary -$rolloutState de estado ' -TotalDevices $totalDevicesFromSummary ' -UpdatedDevices $actualUpdated ' -NotUpdatedDispositivos $notUpdatedCount ' -DevicesPerDay $devicesPerDay # Comprobar datos sin procesar para dispositivos con UEFICA2023Status=Updated, pero ningún evento 1808 (es necesario reiniciar) $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue $totalDataFiles = @($dataFiles). Contar $batchSize = [Matemáticas]::Máx(500, $ProcessingBatchSize) if ($LargeScaleMode) { $batchSize = [Matemáticas]::Max(2000, $ProcessingBatchSize) }
if ($totalDataFiles -gt 0) { for ($idx = 0; $idx -lt $totalDataFiles; $idx += $batchSize) { $end = [Matemáticas]::Mín($idx + $batchSize - 1, $totalDataFiles - 1) $batchFiles = $dataFiles[$idx.. $end]
foreach ($file in $batchFiles) { prueba { $deviceData = Get-Content $file. FullName -Raw | ConvertFrom-Json $hostname = $deviceData.Hostname if (-not $hostname) { continue } $has 1808 = [int]$deviceData.Event1808Count -gt 0 $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Updated" 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 "Actualizado (evento 1808 confirmado): $event 1808Count" "Correcto" if ($uefiStatusUpdated -gt 0) { Write-Log "Actualizado (UEFICA2023Status=Updated, esperando reinicio): $uefiStatusUpdated" "Ok" $rebootSuffix = if ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) más)" } más { "" } Write-Log " Dispositivos que necesitan reiniciarse para el evento 1808 (muestra): $($needsRebootSample -join ', ')$rebootSuffix" "INFO" Write-Log " Estos dispositivos notificarán el evento 1808 después del próximo reinicio" "INFO" } Write-Log "Ya no pendiente: $noLongerPendingCount (incluye SecureBoot OFF, dispositivos que faltan)" "INFO" Write-Log "Estado en espera: $stillPendingCount" "INFORMACIÓN" if ($stillPendingCount -gt 0) { $pendingSuffix = if ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) más)" } más { "" } Write-Log "Dispositivos pendientes (muestra): $($pendingSample -join ', ')$pendingSuffix" "WARN" } } else { Write-Log "Todavía no se han implementado dispositivos" "INFO" } Write-Log "================================================================" "INFORMACIÓN" Write-Log "" "INFORMACIÓN" } # Esperar antes de la siguiente iteración if (-not $DryRun) { Write-Log "Dormir por $PollIntervalMinutes minutos..." "INFORMACIÓN" Start-Sleep -Segundos ($PollIntervalMinutes * 60) } else { Write-Log "[DRYRUN] Esperaría $PollIntervalMinutes minutos" "INFORMACIÓN" break # Exit after one iteración in dry run } }
# ============================================================================ # RESUMEN FINAL # ============================================================================
Write-Host "" Write-Host ("=" * 80) -ForegroundColor verde Write-Host " RESUMEN DEL ORGANIZADOR DE IMPLEMENTACIÓN" -ForegroundColor verde Write-Host ("=" * 80) -ForegroundColor 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 "Olas totales: $($finalState.CurrentWave)" Write-Host "Dispositivos dirigidos: $($finalState.TotalDevicesTargeted)" Write-Host "Cubos bloqueados: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } else { "Green" }) Write-Host "Dispositivos rastreados: $($deviceHistory.Count)" -ForegroundColor Gray Write-Host ""
if ($finalBlocked.Count -gt 0) { Write-Host "CUBOS BLOQUEADOS (requieren revisión manual):" -ForegroundColor Red foreach ($key en $finalBlocked.Keys) { $info = $finalBlocked[$key] Write-Host " - $key" -ForegroundColor Red Write-Host " Motivo: $($info. Reason)" -ForegroundColor Gray } Write-Host "" Write-Host "Archivo de cubos bloqueados: $blockedBucketsPath" -ForegroundColor Yellow }
Write-Host "" Write-Host "Archivos de estado:" -ForegroundColor Cyan Write-Host " Estado de implementación: $rolloutStatePath" Write-Host " Cubos bloqueados: $blockedBucketsPath" Write-Host " Historial de dispositivos: $deviceHistoryPath" Write-Host ""