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

​​​​​​​

¿Necesita más ayuda?

¿Quiere más opciones?

Explore las ventajas de las suscripciones, examine los cursos de aprendizaje, aprenda a proteger su dispositivo y mucho más.