Copiare e incollare questo script di esempio e modificarlo in base alle esigenze dell'ambiente:

<# . SINOSSI     L'agente di orchestrazione dell'implementazione dell'avvio protetto continuo viene eseguito fino al completamento della distribuzione.

.DESCRIPTION     Questo script fornisce automazione completa end-to-end per l'implementazione del certificato di avvio protetto:     1.      Genera le onde di implementazione in base ai dati di aggregazione     2. Crea gruppi ACTIVE Directory e oggetto Criteri di gruppo per ogni onda     3. Monitor per gli aggiornamenti dei dispositivi (evento 1808)     4. Rileva contenitori bloccati (dispositivi non raggiungibili)     5. Avanza automaticamente alla fase successiva     6. Viene eseguito fino all'aggiornamento      di TUTTI i dispositivi idonei     Criteri di completamento:     - Nessun dispositivo rimanente: azione richiesta, alta probabilità, osservazione, temporaneamente in pausa     - Fuori ambito (da progettazione): non supportato, avvio protetto disabilitato     - Funziona continuamente fino al completamento - nessun limite      d'onda arbitrario     Strategia di implementazione:     - ALTA PROBABILITÀ: tutti i dispositivi in prima ondata (cassaforte)     - AZIONE RICHIESTA: doppie progressive (1→2→4→8...)          Logica di blocco:     - Dopo MaxWaitHours, l'agente di orchestrazione esegue il ping dei dispositivi che non sono stati aggiornati     - Se il dispositivo non è RAGGIUNGIBILE (ping non riesce) → bucket è BLOCCATO per l'indagine     - Se il dispositivo è raggiungibile ma non aggiornato → continua ad aspettare (potrebbe essere necessario riavviare)     - I bucket bloccati vengono esclusi finché l'amministratore non li      sblocca     Sblocco automatico:     - Se un dispositivo in un contenitore bloccato viene successivamente visualizzato come aggiornato (evento 1808),       il contenitore viene sbloccato automaticamente e i proventi dell'implementazione     - Gestisce i dispositivi che erano temporaneamente offline ma che sono tornati          Monitoraggio dispositivi:     - Tiene traccia dei dispositivi per nome host (presuppone che i nomi non cambino durante l'implementazione)     - Nota: la raccolta JSON non include un ID computer univoco; aggiungerne uno per tenere traccia meglio

.PARAMETER AggregationInputPath     Percorso dei dati dei dispositivi JSON non elaborati (da Rileva script)

.PARAMETER ReportBasePath     Percorso di base per i report di aggregazione

.PARAMETER TargetOU     Nome distinto dell'unità organizzativa per collegare oggetti Criteri di gruppo.Facoltativo: se non è specificato, l'oggetto Criteri di gruppo è collegato alla radice del dominio per la copertura a livello di dominio.Il filtro dei gruppi di sicurezza assicura che solo i dispositivi assegnati ricevano i criteri.

.PARAMETER MaxWaitHours     Ore di attesa per l'aggiornamento dei dispositivi prima di verificare la raggiungibilità.Trascorso questo tempo, viene effettuato il ping dei dispositivi che non sono stati aggiornati.I dispositivi non raggiungibili causano il blocco del contenitore.Impostazione predefinita: 72 (3 giorni)

.PARAMETER PollIntervalMinutes     Minuti tra i controlli di stato. Impostazione predefinita: 1440 (1 giorno)

.PARAMETER AllowListPath     Percorso di un file contenente i nomi host da CONSENTIRE per l'implementazione (implementazione mirata).Supporta .txt (un nome host per riga) o .csv (con la colonna Nome Host/NomeComputer/Nome).Se specificato, SOLO questi dispositivi verranno inclusi nell'implementazione.BlockList viene ancora applicato dopo AllowList.

.PARAMETER AllowADGroup     Nome di un gruppo di sicurezza Active Directory contenente account computer da CONSENTIRE.Esempio: "SecureBoot-Pilot-Computers" o "Wave1-Devices"     Se specificato, nell'implementazione verranno inclusi SOLO i dispositivi di questo gruppo.Combinare con AllowListPath sia per il targeting basato su file che su Active Directory.

.PARAMETER ExclusionListPath     Percorso di un file contenente i nomi host da ESCLUDERE dall'implementazione (dispositivi VIP/dirigenti).Supporta .txt (un nome host per riga) o .csv (con la colonna Nome Host/NomeComputer/Nome).Questi dispositivi non saranno mai inclusi in nessuna fase di implementazione.BlockList viene applicato dopo il filtro AllowList.     . PARAMETER ExcludeADGroup     Nome di un gruppo di sicurezza Active Directory contenente gli account computer da escludere.Esempio: "COMPUTER VIP" o "Executive-Devices"     Combina con ExclusionListPath per le esclusioni basate su file e AD.

.PARAMETER UseWinCS     Usa WinCS (Windows Configuration System) invece dell'oggetto Criteri di gruppo/AvailableUpdatesPolicy.WinCS distribuisce l'abilitazione dell'avvio protetto eseguendo WinCsFlags.exe direttamente in ogni endpoint.WinCsFlags.exe viene eseguito in contesto SISTEMA tramite un'attività pianificata.Questo metodo è utile per:     - Implementi più veloci (effetto immediato rispetto all'attesa dell'elaborazione dell'oggetto Criteri di gruppo)     - Dispositivi non appartenenti a un dominio     - Ambienti senza infrastruttura AD/GPO     Riferimento: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe

.PARAMETER WinCSKey     Chiave WinCS da usare per l'abilitazione dell'avvio protetto.Impostazione predefinita: F33E0C8E002     Questa chiave corrisponde alla configurazione dell'implementazione di avvio protetto.     . PARAMETER DryRun     Mostrare cosa fare senza apportare modifiche

.PARAMETER ListBlockedBuckets     Visualizzare tutti i contenitori attualmente bloccati e uscire

.PARAMETER UnblockBucket     Sbloccare un contenitore specifico per chiave e uscire

.PARAMETER UnblockAll     Sbloccare tutti i contenitori e uscire

.PARAMETER EnableTaskOnDisabled     Distribuisci Enable-SecureBootUpdateTask.ps1 in tutti i dispositivi con attività pianificata disabilitata.Crea un oggetto Criteri di gruppo con un'attività programmata una tantum che esegue lo script Enable con l'opzione -Quiet.Ciò è utile per risolvere i problemi dei dispositivi con l'attività Secure-Boot-Update disabilitata.

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

.EXAMPLE     # Elenco bucket bloccati     .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets

.EXAMPLE     # Sblocca un contenitore specifico     .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -UnblockBucket "Dell_Latitude5520_BIOS1.2.3"

.EXAMPLE     # Escludere i dispositivi VIP dall'implementazione utilizzando un file di testo     \Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports" '         -ExclusionListPath "C:\Admin\VIP-Devices.txt"

.EXAMPLE     # Escludi i dispositivi in un gruppo di sicurezza Active Directory (ad esempio, portatili dirigenti)     \Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports" '         -ExcludeADGroup "VIP-Computers"

.EXAMPLE     # Usa WinCS (Windows Configuration System) invece di GPO/AvailableUpdatesPolicy     # WinCsFlags.exe viene eseguito in contesto SYSTEM in ogni endpoint tramite attività pianificata     \Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports" '         -UseWinCS '         -WinCSKey "F33E0C8E002" #>

[CmdletBinding()] param(     [Parametro(Obbligatorio = $false)]     [stringa]$AggregationInputPath,     [Parametro(Obbligatorio = $false)]     [stringa]$ReportBasePath,     [Parametro(Obbligatorio = $false)]     [stringa]$TargetOU,     [Parametro(Obbligatorio = $false)]     [stringa]$WavePrefix = "SecureBoot-Rollout",     [Parametro(Obbligatorio = $false)]     [int]$MaxWaitHours = 72,     [Parametro(Obbligatorio = $false)]     [int]$PollIntervalMinutes = 1440,                         

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

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

    [Parameter(Mandatory = $false)]     [opzione]$LargeScaleMode,     # ============================================================================     # AllowList / BlockList Parameters     N. ============================================================================     # AllowList = Includi solo questi dispositivi (implementazione mirata)     # BlockList = Escludi questi dispositivi (non verranno mai implementati)     # Ordine di elaborazione: AllowList prima (se specificato), poi BlockList     [Parametro(Obbligatorio = $false)]     [stringa]$AllowListPath,     [Parametro(Obbligatorio = $false)]     [stringa]$AllowADGroup,     [Parametro(Obbligatorio = $false)]     [stringa]$ExclusionListPath,     [Parametro(Obbligatorio = $false)]     [stringa]$ExcludeADGroup,     # ============================================================================     # Parametri WinCS (Windows Configuration System)     N. ============================================================================     # WinCS è un'alternativa alla distribuzione dell'oggetto Criteri di gruppo AvailableUpdatesPolicy.                              # Usa WinCsFlags.exe su ogni endpoint per abilitare l'implementazione di avvio protetto.# WinCsFlags.exe viene eseguito in contesto SYSTEM sull'endpoint.# Riferimento: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe          [Parametro(Obbligatorio = $false)]     [opzione]$UseWinCS,          [Parametro(Obbligatorio = $false)]     [stringa]$WinCSKey = "F33E0C8E002",          [Parametro(Obbligatorio = $false)]     [opzione]$DryRun,          [Parametro(Obbligatorio = $false)]     [opzione]$ListBlockedBuckets,          [Parametro(Obbligatorio = $false)]     [stringa]$UnblockBucket,          [Parametro(Obbligatorio = $false)]     [opzione]$UnblockAll,          [Parametro(Obbligatorio = $false)]     [opzione]$EnableTaskOnDisabled )

$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Esempi di distribuzione e monitoraggio"

# ============================================================================ # CONVALIDA DIPENDENZA # ============================================================================

function Test-ScriptDependencies {     param(         [Parametro(Obbligatorio = $true)]         [stringa]$ScriptDirectory,         [Parametro(Obbligatorio = $true)]         [string[]]$RequiredScripts     )     $missingScripts = @()     foreach ($script in $RequiredScripts) {         $scriptPath = Join-Path $ScriptDirectory $script         if (-not (Test-Path $scriptPath)) {             $script += $missingScripts         }     }     if ($missingScripts.Count -gt 0) {         Write-Host ""         Write-Host ("=" * 70) -ForegroundColor Red         Write-Host "DIPENDENZE MANCANTI" -Primo pianoColore rosso         Write-Host ("=" * 70) -ForegroundColor Red         Write-Host ""         Write-Host "Non sono stati trovati gli script obbligatori seguenti:" -ForegroundColor Yellow         foreach ($script in $missingScripts) {             Write-Host " - $script" -ForegroundColore bianco         }         Write-Host ""         Write-Host "Scarica gli script più recenti da:" -ForegroundColor Cyan         Write-Host " URL: $DownloadUrl" -ForegroundColor white         Write-Host " Passa a: '$DownloadSubPage'" -Primo pianoColore bianco         Write-Host ""         Write-Host "Estrai tutti gli script nella stessa directory ed esegui di nuovo". -ForegroundColor Yellow         Write-Host ""         $false reso     }     reso $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)) {     uscita 1 }

# ============================================================================ CONVALIDA DEI PARAMETRI # # ============================================================================

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

if (-not $ReportBasePath) {     Write-Host "ERRORE: -ReportBasePath è obbligatorio". -ForegroundColor red     uscita 1 }

if (-not $isAdminCommand -and -not $AggregationInputPath) {     Write-Host "ERRORE: -AggregationInputPath è necessario per l'implementazione (non necessario per -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red     uscita 1 }

# ============================================================================ # RILEVAMENTO OGGETTO CRITERI DI GRUPPO - CONTROLLA L'OGGETTO CRITERI DI GRUPPO RILEVAMENTO # ============================================================================

if (-not $isAdminCommand -and -not $DryRun) {     $CollectionGPOName = "SecureBoot-EventCollection"     # Controlla se il modulo GroupPolicy è disponibile     if (Get-Module -ListAvailable -Name GroupPolicy) {         Import-Module GroupPolicy -ErrorAction SilentlyContinue         Write-Host "Controllo dell'oggetto Criteri di gruppo rilevamento..." -ForegroundColor yellow         prova {             # Controlla se esiste un oggetto Criteri di gruppo             $existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue             if ($existingGpo) {                 Write-Host " Oggetto Criteri di gruppo rilevamento trovato: $CollectionGPOName" -ForegroundColor green             } else {                 Write-Host ""                 Write-Host ("=" * 70) -ForegroundColor Yellow                 Write-Host " AVVISO: OGGETTO CRITERI DI GRUPPO RILEVAMENTO NON TROVATO" -ForegroundColor yellow                 Write-Host ("=" * 70) -ForegroundColor Yellow                 Write-Host ""                 Write-Host "Non è stato trovato l'oggetto Criteri di gruppo di rilevamento '$CollectionGPOName'". -ForegroundColor Yellow                 Write-Host "Senza questo oggetto Criteri di gruppo, i dati del dispositivo non verranno raccolti". -ForegroundColor Yellow                 Write-Host ""                 Write-Host "Per distribuire l'oggetto Criteri di gruppo rilevamento, esegui:" -ForegroundColor Ciano                 Write-Host " .\Deploy-GPO-SecureBootCollection.ps1 -DomainName <dominio> -AutoDetectOU" -ForegroundColor White                 Write-Host ""                 Write-Host "Continua comunque?                                     (Y/N)" -Primo pianoColore giallo                 $response = Read-Host                 if ($response -notmatch '^[Yy]') {                     Write-Host "Interruzione. Distribuire prima l'oggetto Criteri di gruppo Rilevamento". -ForegroundColor Red                     uscita 1                 }             }         } catch {             Write-Host " Impossibile controllare l'oggetto Criteri di gruppo: $($_. Exception.Message)" -ForegroundColor yellow         }     } else {         Write-Host " Modulo GroupPolicy non disponibile - saltando il controllo oggetto Criteri di gruppo" -ForegroundColor Gray     }     Write-Host "" }

# ============================================================================ # STATE FILE PATHS # ============================================================================

$stateDir = Join-Path $ReportBasePath "RolloutState" if (-not (Test-Path $stateDir)) {     New-Item -ItemType Directory -Path $stateDir -Force | Out-Null }

$rolloutStatePath = Join-Path $stateDir "RolloutState.json" $blockedBucketsPath = Join-Path $stateDir "BlockedBuckets.json" $adminApprovedPath = Join-Path $stateDir "AdminApprovedBuckets.json" $deviceHistoryPath = Join-Path $stateDir "DeviceHistory.json" $processingCheckpointPath = Join-Path $stateDir "ProcessingCheckpoint.json"

# ============================================================================ # PS 5.1 COMPATIBILITY: ConvertTo-Hashtable N. ============================================================================ # ConvertFrom-Json -AsHashtable è solo PS7+. In questo modo si garantisce la compatibilità.

function ConvertTo-Hashtable {     param(         [Parameter(ValueFromPipeline = $true)]         $InputObject     )     process {         if ($null -eq $InputObject) { return @{} }         if ($InputObject -is [System.Collections.IDictionary]) { restituisce $InputObject }         if ($InputObject -is [PSCustomObject]) {             # Usare [ordered] per un ordine delle chiavi coerente e una gestione sicura dei duplicati             $hash = [ordered]@{}             foreach ($prop in $InputObject.PSObject.Properties) {                 # L'attività indicizzata gestisce in modo sicuro i duplicati sovrascrivendo                 $hash[$prop. Name] = ConvertTo-Hashtable $prop. Valore             }             reso $hash         }         if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {             restituire @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ })         }         restituire $InputObject     } }

# ============================================================================ # COMANDI DI AMMINISTRAZIONE: Elenchi/Sblocca contenitori # ============================================================================

if ($ListBlockedBuckets) {     Write-Host ""     Write-Host ("=" * 80) -ForegroundColor Yellow     Write-Host "CONTENITORI BLOCCATI" -Primo pianoColore giallo     Write-Host ("=" * 80) -ForegroundColor Yellow     Write-Host ""     if (Test-Path $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         se ($blocked. Conteggio -eq 0) {             Write-Host "Nessun contenitore bloccato". -ForegroundColor green         } else {             Write-Host "Totale bloccato: $($blocked. Conteggio)" -Primo pianoColore rosso             Write-Host ""             foreach ($key in $blocked. Tasti) {                 $info = $blocked[$key]                 Write-Host "Bucket: $key" -ForegroundColor red                 Write-Host " Bloccato a: $($info. BlockedAt)" -ForegroundColor gray                 Write-Host " Motivo: $($info. Reason)" -ForegroundColor Gray                 Write-Host " Dispositivo non riuscito: $($info. FailedDevice)" -ForegroundColor gray                 Write-Host " Ultimo report: $($info. LastReported)" -ForegroundColor gray                 Write-Host " Onda: $($info. WaveNumber)" -ForegroundColor Gray                 Write-Host " Dispositivi nel contenitore: $($info. DevicesInBucket)" -ForegroundColor gray                 Write-Host ""             }             Write-Host "Per sbloccare un contenitore:"             Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Ciano             Write-Host ""             Write-Host "Per sbloccare tutto:"             Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Ciano         }     } else {         Write-Host "Nessun file di contenitori bloccati trovato". -ForegroundColor green     }     Write-Host ""     uscita 0 }     

if ($UnblockBucket) {     Write-Host ""     if (Test-Path $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         se ($blocked. Contains($UnblockBucket)) {             $blocked. Rimuovi($UnblockBucket)             $blocked | ConvertTo-Json -Profondità 10 | Out-File $blockedBucketsPath -Codifica UTF8 -Force             # Aggiungi all'elenco approvato dall'amministratore per impedire il blocco di nuovo             $adminApproved = @{}             if (Test-Path $adminApprovedPath) {                 $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable             }             $adminApproved[$UnblockBucket] = @{                 ApprovedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"                 ApprovedBy = $env:USERNAME             }             $adminApproved | ConvertTo-Json -Profondità 10 | Out-File $adminApprovedPath -Encoding UTF8 -Force             Write-Host "Contenitore sbloccato: $UnblockBucket" -ForegroundColor green             Write-Host "Aggiunto all'elenco approvato dall'amministratore (non verrà ri-bloccato automaticamente)" -ForegroundColor Ciano         } else {             Write-Host "Bucket not found: $UnblockBucket" -ForegroundColor yellow             Write-Host "Contenitori disponibili:" -ForegroundColor gray             $blocked. Tasti | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }         }     } else {         Write-Host "Nessun file di contenitori bloccati trovato". -ForegroundColor yellow     }     Write-Host ""     uscita 0 }                          

if ($UnblockAll) {     Write-Host ""     if (Test-Path $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         $count = $blocked. Conteggio         @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Codifica UTF8 -Force         Write-Host "Sblocca tutti i contenitori $count". -Primo pianoColore verde     } else {         Write-Host "Nessun file di bucket bloccati trovato". -ForegroundColor yellow     }     Write-Host ""     uscita 0 }

# ============================================================================ # FUNZIONI HELPER # ============================================================================

function Get-RolloutState {     if (Test-Path $rolloutStatePath) {         prova {             $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable             # Verificare l'esistenza delle proprietà obbligatorie             if ($null -eq $loaded. CurrentWave) {                 genera "File di stato non valido - CurrentWave mancante"             }             # Assicurarsi che WaveHistory sia sempre una matrice (corregge la deserializzazione JSON PS5.1)             if ($null -eq $loaded. WaveHistory) {                 $loaded. WaveHistory = @()             } elseif ($loaded. WaveHistory -isnot [array]) {                 $loaded. WaveHistory = @($loaded. WaveHistory)             }             $loaded reso         } catch {             Write-Log "RolloutState.json danneggiato rilevato: $($_. Exception.Message)" "WARN"             Write-Log "Backup del file danneggiato e avvio da zero" "AVVISO"             $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyyMMdd-HHmmss')"             Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue         }     }     restituire @{         CurrentWave = 0         StartedAt = $null         LastAggregation = $null         TotalDevicesTargeted = 0         TotalDevicesUpdated = 0         Status = "NotStarted"         WaveHistory = @()     } }

function Save-RolloutState {     param($State)     $State | ConvertTo-Json -Profondità 10 | Out-File $rolloutStatePath -Codifica UTF8 -Force }

function Get-WeekdayProjection {     <#     . SINOSSI         Calcolare la data di completamento prevista per i fine settimana (nessun avanzamento il sabato/dom)     #>     param(         [int]$RemainingDevices,         [double]$DevicesPerDay,         [datetime]$StartDate = (Get-Date)     )     if ($DevicesPerDay -le 0 -or $RemainingDevices -le 0) {         restituire @{             ProjectedDate = $null             WorkingDaysNeeded = 0             CalendarDaysNeeded = 0         }     }     # Calcolare i giorni lavorativi necessari (esclusi i fine settimana)     $workingDaysNeeded = [matematica]::Soffitto($RemainingDevices / $DevicesPerDay)     # Convertire i giorni lavorativi in giorni del calendario (aggiungere i fine settimana)     $currentDate = $StartDate.Date     $daysAdded = 0     $workingDaysAdded = 0     while ($workingDaysAdded -lt $workingDaysNeeded) {         $currentDate = $currentDate.AddDays(1)         $daysAdded++         # Contare solo i giorni feriali         if ($currentDate.DayOfWeek -ne [DayOfWeek]::Saturday -and             $currentDate.DayOfWeek -ne [DayOfWeek]::Sunday) {             $workingDaysAdded++         }     }     restituire @{         ProjectedDate = $currentDate.ToString("yyyy-MM-dd")         WorkingDaysNeeded = $workingDaysNeeded         CalendarDaysNeeded = $daysAdded     } }                                  

function Save-RolloutSummary {     <#     . SINOSSI         Salvare il riepilogo dell'implementazione con le informazioni di proiezione per la visualizzazione del dashboard     #>     param(         [tabella hash]$State,         [int]$TotalDevices,         [int]$UpdatedDevices,         [int]$NotUpdatedDevices,         [double]$DevicesPerDay     )     $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     # Calcolare la proiezione compatibile con il fine settimana     $projection = Get-WeekdayProjection -RemainingDevices $NotUpdatedDevices -DevicesPerDay $DevicesPerDay     $summary = @{         GeneratedAt = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")         RolloutStartDate = $State.StartedAt         LastAggregation = $State.LastAggregation         CurrentWave = $State.CurrentWave         Status = $State.Status         # Numero di dispositivi         TotalDevices = $TotalDevices         UpdatedDevices = $UpdatedDevices         NotUpdatedDevices = $NotUpdatedDevices         PercentUpdated = if ($TotalDevices -gt 0) { [math]::Round(($UpdatedDevices / $TotalDevices) * 100, 1) } else { 0 }         Metriche # Velocità         DevicesPerDay = [math]::Round($DevicesPerDay, 1)         TotalDevicesTargeted = $State.TotalDevicesTargeted         TotalWaves = $State.CurrentWave         # Proiezione compatibile con il fine settimana         ProjectedCompletionDate = $projection. ProjectedDate         WorkingDaysRemaining = $projection. WorkingDaysNeeded         CalendarDaysRemaining = $projection. CalendarDaysNeeded         # Nota sull'esclusione per i fine settimana         ProjectionNote = "Il completamento previsto esclude i fine settimana (sab/dom)"     }     $summary | ConvertTo-Json -Profondità 5 | Out-File $summaryPath -Encoding UTF8 -Force     Write-Log "Riepilogo dell'implementazione salvato: $summaryPath" "INFO"     reso $summary }                                                             

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

function Save-BlockedBuckets {     param($Blocked)     $Blocked | ConvertTo-Json -Profondità 10 | Out-File $blockedBucketsPath -Codifica UTF8 -Force }

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

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

function Save-DeviceHistory {     param($History)     $History | ConvertTo-Json -Profondità 10 | Out-File $deviceHistoryPath -Codifica UTF8 -Force }

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

    $checkpoint = @{         Fase = $Stage         UpdatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"         Processed = $Processed         Totale = $Total         Percent = if ($Total -gt 0) { [math]::Round(($Processed / $Total) * 100, 2) } else { 0 }         Metriche = $Metrics     }

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

function Get-NotUpdatedIndexes {     param([matrice]$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 {     param([string]$Message, [string]$Level = "INFO")     $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"     $color = interruttore ($Level) {         "OK" { "Verde" }         "WARN" { "Yellow" }         "ERRORE" { "Rosso" }         "BLOCCATO" { "DarkRed" }         "WAVE" { "Ciano" }         predefinito { "Bianco" }     }     Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color     # Esegui anche l'accesso al file     $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMMdd').log"     "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Codifica UTF8 }               

function Get-BucketKey {     param($Device)     # Usa BucketId dal JSON del dispositivo se disponibile (hash SHA256 dallo script di rilevamento)     if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { restituisce "$($Device.BucketId)" }     # Fallback: costrutto dal produttore|modello|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 }     restituisce "$mfr|$model|$bios" }

# ============================================================================ # CARICAMENTO ELENCO VIP/ESCLUSIONI # ============================================================================

function Get-ExcludedHostnames {     param(         [stringa]$ExclusionFilePath,         [stringa]$ADGroupName     )     $excluded = [System.Collections.Generic.HashSet[string]]:new([StringComparer]::OrdinalIgnoreCase)     # Carica da file (supporta .txt o .csv)     if ($ExclusionFilePath -and (test-Path $ExclusionFilePath)) {         $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower()         if ($extension -eq ".csv") {             # Formato CSV: prevede una colonna "Nome host" o "NomeComputer"             $csvData = Import-Csv $ExclusionFilePath             $hostCol = if ($csvData[0]. PSObject.Properties.Name -contains 'Hostname') { 'Hostname' }                        elseif ($csvData[0]. PSObject.Properties.Name -contains 'ComputerName') { 'ComputerName' }                        elseif ($csvData[0]. PSObject.Properties.Name -contains 'Name') { 'Name' }                        else { $null }             if ($hostCol) {                 foreach ($row in $csvData) {                     if (![ stringa]::IsNullOrWhiteSpace($row.$hostCol)) {                         [void]$excluded. Add($row.$hostCol.Trim())                     }                 }             }         } else {             # Testo normale: un nome host per riga             Get-Content $ExclusionFilePath | ForEach-Object {                 $line = $_. Trim()                 se ($line e -non $line. StartsWith('#')) {                     [void]$excluded. Aggiungi($line)                 }             }         }         Write-Log "Caricato $($excluded. Count) hostnames from exclusion file: $ExclusionFilePath" "INFO"     }     # Carica dal gruppo di sicurezza Active Directory     if ($ADGroupName) {         prova {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'computer' }             foreach ($member in $groupMembers) {                 [void]$excluded. Add($member. Nome)             }             Write-Log computer "$groupMembers.Count) caricati dal gruppo Active Directory: $ADGroupName" "INFO"         } catch {             Write-Log "Impossibile caricare il gruppo AD '$ADGroupName': $_" "WARN"         }     }     restituire @($excluded) }                                                                             

# ============================================================================ # ALLOW LIST LOADING (Targeted Rollout) # ============================================================================

function Get-AllowedHostnames {     <#     . SINOSSI         Carica i nomi host da un file AllowList e/o da un gruppo active directory per l'implementazione mirata.Quando viene specificato un AllowList, SOLO questi dispositivi verranno inclusi nell'implementazione.#>     param(         [stringa]$AllowFilePath,         [stringa]$ADGroupName     )          $allowed = [System.Collections.Generic.HashSet[string]]:new([StringComparer]::OrdinalIgnoreCase)          # Carica da file (supporta .txt o .csv)     if ($AllowFilePath -and (test-path $AllowFilePath)) {         $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower()                  if ($extension -eq ".csv") {             # Formato CSV: prevede una colonna "Nome host" o "NomeComputer"             $csvData = Import-Csv $AllowFilePath             if ($csvData.Count -gt 0) {                 $hostCol = if ($csvData[0]. PSObject.Properties.Name -contains 'Hostname') { 'Hostname' }                            elseif ($csvData[0]. PSObject.Properties.Name -contains 'ComputerName') { 'ComputerName' }                            elseif ($csvData[0]. PSObject.Properties.Name -contains 'Name') { 'Name' }                            else { $null }                                  if ($hostCol) {                     foreach ($row in $csvData) {                         if (![ stringa]::IsNullOrWhiteSpace($row.$hostCol)) {                             [void]$allowed. Add($row.$hostCol.Trim())                         }                     }                 }             }         } else {             # Testo normale: un nome host per riga             Get-Content $AllowFilePath | ForEach-Object {                 $line = $_. Trim()                 se ($line e -non $line. StartsWith('#')) {                     [void]$allowed. Aggiungi($line)                 }             }         }                  Write-Log "Caricato $($allowed. Count) hostnames from allow list file: $AllowFilePath" "INFO"     }          # Carica dal gruppo di sicurezza Active Directory     if ($ADGroupName) {         prova {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'computer' }                          foreach ($member in $groupMembers) {                 [void]$allowed. Add($member. Nome)             }                          Write-Log "Caricamento di computer $($groupMembers.Count) da ACTIVE Directory consenti gruppo: $ADGroupName" "INFO"         } catch {             Write-Log "Impossibile caricare il gruppo AD '$ADGroupName': $_" "WARN"         }     }          restituire @($allowed) }

# ============================================================================ # AGGIORNAMENTO E MONITORAGGIO DEI DATI # ============================================================================

function Get-DataFreshness {     <#     . SINOSSI         Controlla quanto sono freschi i dati di rilevamento esaminando i timestamp dei file JSON.Restituisce le statistiche sull'ultima segnalazione degli endpoint.#>     param([stringa]$JsonPath)     $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue     if ($jsonFiles.Count -eq 0) {         restituire @{             TotalFiles = 0             FreshFiles = 0             StaleFiles = 0             NoDataFiles = 0             OldestFile = $null             NewestFile = $null             AvgAgeHours = 0             Warning = "Nessun file JSON trovato - il rilevamento potrebbe non essere distribuito"         }     }     $now = Get-Date     $freshThresholdHours = 24 # Files aggiornati nelle ultime 24 ore sono "fresh"     $staleThresholdHours = 72 # Files più vecchi di 72 ore sono "obsoleti"     $fresh = 0     $stale = 0     $ages = @()     foreach ($file in $jsonFiles) {         $ageHours = ($now - $file. LastWriteTime). TotalHours         $ageHours $ages +=         if ($ageHours -le $freshThresholdHours) {             $fresh++         } elseif ($ageHours -ge $staleThresholdHours) {             $stale++         }     }     $oldestFile = $jsonFiles | Sort-Object LastWriteTime | Select-Object -Primo 1     $newestFile = $jsonFiles | Sort-Object LastWriteTime -Descending | Select-Object -Primo 1     $warning = $null     if ($stale -gt ($jsonFiles.Count * 0,5)) {         $warning = "Più del 50% dei dispositivi dispone di dati non aggiornati (>72 ore) - controllare l'oggetto Criteri di gruppo di rilevamento"     } elseif ($fresh -lt ($jsonFiles.Count * 0,3)) {         $warning = "Meno del 30% dei dispositivi segnalati di recente - il rilevamento potrebbe non essere in esecuzione"     }     restituire @{         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). Media, 1)         Avviso = $warning     } }                                                 

function Test-DetectionGPODeployed {     <#     . SINOSSI         Verifica che l'infrastruttura di rilevamento/monitoraggio sia presente.#>     param([stringa]$JsonPath)     # Check 1: esiste un percorso JSON     if (-not (Test-Path $JsonPath)) {         restituire @{             IsDeployed = $false             Message = "JSON input path does not exist: $JsonPath"         }     }     # Controlla 2: esistono almeno alcuni file JSON     $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). Conteggio     if ($jsonCount -eq 0) {         restituire @{             IsDeployed = $false             Messaggio = "Nessun file JSON in $JsonPath - È possibile che non sia stato distribuito un oggetto Criteri di gruppo di rilevamento o che i dispositivi non siano ancora stati segnalati"         }     }     # Controlla 3: Files sono ragionevolmente recenti (almeno alcuni nella scorsa settimana)     $recentFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue |         Where-Object { $_. LastWriteTime -gt (Get-Date). AddDays(-7) }     if ($recentFiles.Count -eq 0) {         restituire @{             IsDeployed = $false             Messaggio = "Nessun file JSON aggiornato negli ultimi 7 giorni - L'oggetto Criteri di gruppo di rilevamento potrebbe essere interrotto o i dispositivi offline"         }     }     restituire @{         IsDeployed = $true         Messaggio = "Rilevamento visualizzato attivo: $jsonCount file, $($recentFiles.Count) aggiornato di recente"         FileCount = $jsonCount         RecentCount = $recentFiles.Count     } }                         

# ============================================================================ # DEVICE TRACKING (BY HOSTNAME) # ============================================================================

function Update-DeviceHistory {     <#     . SINOSSI         Tiene traccia dei dispositivi per nome host perché non abbiamo un identificatore univoco del computer.Nota: BucketId è uno-a-molti (stessa configurazione hardware = stesso bucket).Se viene aggiunto un identificatore univoco alla raccolta JSON, aggiornare questa funzione.#>     param(         [matrice]$CurrentDevices,         [tabella hash]$DeviceHistory     )          foreach ($device in $CurrentDevices) {         $hostname = $device. Hostname         if (-not $hostname) { continue }                  # Tieni traccia del dispositivo in base al nome host         $DeviceHistory[$hostname] = @{             Hostname = $hostname             BucketId = $device. Id contenitore             Manufacturer = $device. WMI_Manufacturer             Modello = $device. WMI_Model             LastSeen = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             Stato = $device. UpdateStatus         }     } }

# ============================================================================ # RILEVAMENTO BUCKET BLOCCATO (in base alla raggiungibilità del dispositivo) # ============================================================================

<# . DESCRIZIONE     Logica di blocco:     - Un contenitore viene bloccato SOLO se:       1. Il dispositivo è stato preso di mira in un'onda       2. MaxWaitHours è passato da quando è iniziata l'onda       3. Il dispositivo NON è RAGGIUNGIBILE (il ping non riesce)          - Se il dispositivo è raggiungibile ma non è ancora aggiornato, continuiamo ad aspettare       (l'aggiornamento potrebbe essere in attesa di riavvio - L'evento 1808 viene attivato solo dopo il riavvio)          - Il dispositivo non raggiungibile indica che si è verificato un problema e ha bisogno di un'indagine          Sbloccare:     - Usare -ListBlockedBuckets per visualizzare i bucket bloccati     - Usa -UnblockBucket "BucketKey" per sbloccare un bucket specifico     - Usa -UnblockAll per sbloccare tutti i bucket #>

function Test-DeviceReachable {     param(         [stringa]$Hostname,         [string]$DataPath # Path to device JSON files     )     # Metodo 1: controllare il timestamp del file JSON (più veloce, senza l'analisi dei file necessaria)     # Se lo script di rilevamento è stato eseguito di recente, il file è stato scritto/aggiornato, dimostrando che il dispositivo è in vita     if ($DataPath) {         $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object -Primo 1         if ($deviceFile) {             $hoursSinceWrite = ((Get-Date) - $deviceFile.LastWriteTime). TotalHours             if ($hoursSinceWrite -lt 72) { restituisce $true }         }     }     # Metodo 2: Fallback al ping (solo se JSON non è aggiornato o mancante)     prova {         $ping = Test-Connection -ComputerName $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue         restituire $ping     } catch {         restituire $false     } }          

function Update-BlockedBuckets {     param(         $RolloutState,         $BlockedBuckets,         $AdminApproved,         [matrice]$NotUpdatedDevices,         [tabella hash]$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 }     # Raccogli i dispositivi che hanno superato il periodo di attesa e che non sono ancora aggiornati     foreach ($wave in $RolloutState.WaveHistory) {         se (-, non $wave. StartedAt) { continue }         $waveStart = [DateTime]::P arse($wave. StartedAt)         $hoursSinceWave = ($now - $waveStart). TotalHours         if ($hoursSinceWave -lt $MaxWaitHours) {             # Ancora entro il periodo di attesa- non controllare ancora             Continuare         }         # Controlla ogni dispositivo di questa ondata         foreach ($deviceInfo in $wave. Dispositivi) {             $hostname = $deviceInfo.Hostname             $bucketKey = $deviceInfo.BucketKey             # Ignora se il contenitore è già bloccato             if ($BlockedBuckets.Contains($bucketKey)) { continue }             # Ignora se il contenitore è approvato dall'amministratore E fase di avvio PRIMA dell'approvazione             # (controlla solo i dispositivi assegnati all'approvazione dell'amministratore DOPO il blocco)             if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) {                 $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. ApprovatoAt)                 if ($waveStart -lt $approvalTime) {                     # Questo dispositivo è stato preso di mira prima dell'approvazione dell'amministratore - ignora                     Continuare                 }                 # Wave avviato dopo l'approvazione - si tratta di un nuovo targeting, può controllare             }             # Il dispositivo è ancora nell'elenco NotUpdated?             if ($hostSet.Contains($hostname)) {                 $devicesToCheck += @{                     Hostname = $hostname                     BucketKey = $bucketKey                     WaveNumber = $wave. WaveNumber                     HoursSinceWave = [math]::Round($hoursSinceWave, 1)                 }             }         }     }     if ($devicesToCheck.Count -eq 0) {         $newlyBlocked reso     }     Write-Log "Controllo della raggiungibilità dei dispositivi $($devicesToCheck.Count) dopo il periodo di attesa..." "INFO"     # Tenere traccia degli errori per contenitore per il processo decisionale     $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() }     # Controlla la raggiungibilità di ogni dispositivo     foreach ($device in $devicesToCheck) {         $hostname = $device. Hostname         $bucketKey = $device. BucketKey         if ($DryRun) {             Write-Log "[DRYRUN] Verifica $hostname raggiungibilità" "INFO"             Continuare         }         if (-not $bucketFailures.ContainsKey($bucketKey)) {             $bucketFailures[$bucketKey] = @{ Non raggiungibile = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. HoursSinceWave }         }         $isReachable = Test-DeviceReachable -Hostname $hostname -DataPath $AggregationInputPath         if (-$isReachable) {             $bucketFailures[$bucketKey]. Irraggiungibile += $hostname         } else {             # Il dispositivo è raggiungibile ma non ancora aggiornato: potrebbe essere un errore temporaneo o in attesa di riavvio             $bucketFailures[$bucketKey]. AliveButFailed += $hostname             $hostname $stillWaiting +=         }     }     # Decisione per contenitore: blocca solo se i dispositivi sono veramente IRRAGGIUNGIBILI     # Dispositivi vivi con errori = temporanei, continua l'implementazione     foreach ($bucketKey in $bucketFailures.Keys) {         $bf = $bucketFailures[$bucketKey]         $unreachableCount = $bf. Non raggiungibile.Conteggio         $aliveFailedCount = $bf. AliveButFailed.Count         # Controlla se questo contenitore ha successi (dai dati dei dispositivi aggiornati)         $bucketHasSuccesses = $stSuccessBuckets -and $stSuccessBuckets.Contains($bucketKey)         if ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) {             # TUTTI i dispositivi che hanno esito negativo non sono raggiungibili: blocca il contenitore             if ($newlyBlocked -notcontains $bucketKey) {                 $BlockedBuckets[$bucketKey] = @{                     BlockedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"                     Reason = "Tutti i dispositivi $unreachableCount non raggiungibili dopo $($bf. HoursSinceWave) ore"                     FailedDevices = ($bf. Unreachable -join ", ")                     WaveNumber = $bf. WaveNumber                     DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } else { 0 }                 }                 $newlyBlocked += $bucketKey                 Write-Log "BUCKET BLOCCATO: $bucketKey (dispositivi $unreachableCount) non raggiungibili: $($bf. Irraggiungibile -join ', '))" "BLOCCATO"             }         } elseif ($aliveFailedCount -gt 0) {             # I dispositivi sono vivi ma non aggiornati - errore temporaneo, NON bloccare             Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length))...: $aliveFailedCount dispositivi vivi ma in sospeso, $unreachableCount non raggiungibile - NOT blocca (temporaneo)" "INFO"             if ($unreachableCount -gt 0) {                 Write-Log " Irraggiungibile: $($bf. Irraggiungibile -join ', ')" "WARN"             }             Write-Log " Alive but pending: $($bf. AliveButFailed -join ', ')" "INFO"             # Tenere traccia del numero di errori nello stato di implementazione per il monitoraggio             if (-not $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} }             $RolloutState.TemporaryFailures[$bucketKey] = @{                 AliveButFailed = $bf. AliveButFailed                 Irraggiungibile = $bf. Irraggiungibile                 LastChecked = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             }         }     }     if ($stillWaiting.Count -gt 0) {         Write-Log "Dispositivi raggiungibili ma in attesa di aggiornamento (potrebbe essere necessario riavviare): $($stillWaiting.Count)" "INFO"     }     restituire $newlyBlocked }                                                                                                                                                                                  

# ============================================================================ # AUTO-UNBLOCK: sbloccare i contenitori quando i dispositivi si aggiornano correttamente # ============================================================================

function Update-AutoUnblockedBuckets {     <#     . DESCRIZIONE         Controlla se i dispositivi in contenitori bloccati sono stati aggiornati (evento 1808).         Sblocca automaticamente se tutti i dispositivi assegnati nel contenitore sono stati aggiornati.Se solo ALCUNI dispositivi sono stati aggiornati, notifica all'amministratore chi può sbloccare manualmente.                  Amministrazione possibile sbloccare manualmente usando:           .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "path" -UnblockBucket "BucketKey"     #>     param(         $BlockedBuckets,         $RolloutState,         [matrice]$NotUpdatedDevices,         [stringa]$ReportBasePath,         [tabella hash]$NotUpdatedIndexes,         [int]$LogSampleSize = 25     )     $autoUnblocked = @()     $bucketsToCheck = @($BlockedBuckets.Keys)     $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet }     foreach ($bucketKey in $bucketsToCheck) {         $bucketInfo = $BlockedBuckets[$bucketKey]         # Scarica tutti i dispositivi che abbiamo preso di mira da questo contenitore storicamente         $targetedDevicesInBucket = @()         foreach ($wave in $RolloutState.WaveHistory) {             $targetedDevicesInBucket += @($wave. Dispositivi | Where-Object { $_. BucketKey -eq $bucketKey })         }         if ($targetedDevicesInBucket.Count -eq 0) { continue }         # Controlla quanti dispositivi di destinazione sono ancora in NotUpdated rispetto all'aggiornamento         $updatedDevices = @()         $stillPendingDevices = @()         foreach ($targetedDevice in $targetedDevicesInBucket) {             if ($hostSet.Contains($targetedDevice.Hostname)) {                 $stillPendingDevices += $targetedDevice.Hostname             } else {                 $updatedDevices += $targetedDevice.Hostname             }         }         if ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) {             # TUTTI i dispositivi mirati sono stati aggiornati - sblocco automatico!             $BlockedBuckets.Remove($bucketKey)             $autoUnblocked += @{                 BucketKey = $bucketKey                 UpdatedDevices = $updatedDevices                 PreviouslyBlockedAt = $bucketInfo.BlockedAt                 Reason = "Tutti i dispositivi di destinazione $($updatedDevices.Count) aggiornati"             }             Write-Log "SBLOCCATO AUTOMATICAMENTE: $bucketKey (Tutti i dispositivi di destinazione $($updatedDevices.Count) aggiornati correttamente)" "OK"             # Incrementa il numero di onde OEM per l'OEM di questo contenitore (monitoraggio per OEM)             $bucketOEM = if ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' } # Extract OEM from pipe-delimited key or default             if (-not $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             }             $currentWave = if ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } else { 0 }             $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1             Write-Log " Numero di onde '$bucketOEM' OEM incrementato a dispositivi $($currentWave + 1) (allocazione successiva: $([int][Math]::P ow(2, $currentWave + 1))) " "INFO"         }         elseif ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -gt 0) {             # ALCUNI dispositivi sono stati aggiornati ma altri sono ancora in sospeso- avvisa l'amministratore (una sola volta)             if (-not $bucketInfo.UnblockCandidate) {                 $bucketInfo.UnblockCandidate = $true                 $bucketInfo.UpdatedDevices = $updatedDevices                 $bucketInfo.PendingDevices = $stillPendingDevices                 $bucketInfo.NotifiedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss")                 Write-Log "" "INFO"                 Write-Log "========== AGGIORNAMENTO PARZIALE IN ========== BUCKET BLOCCATO" "INFO"                 Write-Log "Contenitore: $bucketKey" "INFO"                 $updatedSample = @($updatedDevices | Select-Object -First $LogSampleSize)                 $pendingSample = @($stillPendingDevices | Select-Object -First $LogSampleSize)                 $updatedSuffix = if ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) altro)" } else { "" }                 $pendingSuffix = if ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) altro)" } else { "" }                 Write-Log "Dispositivi aggiornati ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix" "OK"                 Write-Log "Ancora in sospeso ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix" "WARN"                 Write-Log "" "INFO"                 Write-Log "Per sbloccare manualmente questo contenitore dopo la verifica, esegui:" "INFO"                 Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '"$ReportBasePath'" -UnblockBucket '"$bucketKey'"" "INFO"                 Write-Log "=======================================================" "INFO"                 Write-Log "" "INFO"             }         }     }     restituire $autoUnblocked }                                                                                          

# ============================================================================ # WAVE GENERATION (INLINED - esclude i bucket bloccati) # ============================================================================

function New-RolloutWave {     param(         [stringa]$AggregationPath,         $BlockedBuckets,         $RolloutState,         [int]$MaxDevicesPerWave = 50,         [string[]]$AllowedHostnames = @(),         [string[]]$ExcludedHostnames = @()     )     # Caricare i dati di aggregazione     $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" |          Where-Object { $_. Nome -notlike "*Buckets*" } |          Sort-Object LastWriteTime -Descending |          Select-Object -First 1     if (-$notUptodateCsv) {         Write-Log "Nessun CSV notUptodate trovato" "ERRORE"         restituire $null     }     $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName)     # Normalize HostName -> Hostname per la coerenza (CSV usa HostName, il codice usa Hostname)     foreach ($device in $allNotUpdated) {         se ($device. PSObject.Properties['HostName'] -and -not $device. PSObject.Properties['Hostname']) {             $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName -Force         }     }     # Filtra i contenitori bloccati     $eligibleDevices = @($allNotUpdated | Where-Object {         $bucketKey = Get-BucketKey $_         -not $BlockedBuckets.Contains($bucketKey)     })     # Filtra in base a SOLO dispositivi consentiti (se è specificato AllowList)     # AllowList = implementazione mirata: verranno considerati solo questi dispositivi     if ($AllowedHostnames.Count -gt 0) {         $beforeCount = $eligibleDevices.Count         $eligibleDevices = @($eligibleDevices | Where-Object {             $_. Hostname -in $AllowedHostnames         })         $allowedCount = $eligibleDevices.Count         Write-Log "AllowList applied: $allowedCount of $beforeCount devices are in allow list" "INFO"     }     # Filtra i dispositivi VIP/esclusi (BlockList)     # BlockList viene applicato DOPO 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 "Excluded $excludedCount VIP/protected devices from rollout" "INFO"         }     }     if ($eligibleDevices.Count -eq 0) {         Write-Log "Nessun dispositivo idoneo rimanente (tutti aggiornati o bloccati)" "OK"         restituire $null     }     # Scarica i dispositivi già in fase di implementazione (dalle onde precedenti)     $devicesAlreadyInRollout = @()     if ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) {         $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object {             $_. Dispositivi | ForEach-Object { $_. Nome host }         } | Where-Object { $_ })     }     Write-Log "Dispositivi già in distribuzione: $($devicesAlreadyInRollout.Count)" "INFO"     # Separato per livello di probabilità     $highConfidenceDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel -eq "Alta probabilità" -and         $_. Hostname -notin $devicesAlreadyInRollout     })     # L'azione richiesta include:     # - Esplicita "Azione richiesta"     # - Empty/Null ConfidenceLevel     # - QUALSIASI valore ConfidenceLevel sconosciuto/non riconosciuto (considerato come Azione richiesta)     $knownSafeCategories = @(         "Alta probabilità",         "Temporaneamente sospeso",         "Sotto osservazione",         "Sotto osservazione - Ulteriori dati necessari",         "Non supportato",         "Non supportato - Limitazione nota"     )     $actionRequiredDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel -notin $knownSafeCategories -and         $_. Hostname -notin $devicesAlreadyInRollout     })     Write-Log "Confidenza elevata (non nell'implementazione): $($highConfidenceDevices.Count)" "INFO"     Write-Log "Azione richiesta (non nell'implementazione): $($actionRequiredDevices.Count)" "INFO"     # Crea dispositivi wave     $waveDevices = @()     # CONFIDENZA ELEVATA: includi TUTTO (sicuro per l'implementazione)     if ($highConfidenceDevices.Count -gt 0) {         Write-Log "Aggiunta di tutti i dispositivi $($highConfidenceDevices.Count)High Confidence" "WAVE"         $waveDevices += $highConfidenceDevices     } # AZIONE RICHIESTA: implementazione progressiva (bucket-based with OEM-spread for zero-success buckets)     # Strategia:     # - Bucket con 0 successi: Distribuiti tra OEM (1 per OEM -> 2 per OEM -> 4 per OEM)     # - Bucket con successo ≥1: raddoppia liberamente senza restrizioni OEM     if ($actionRequiredDevices.Count -gt 0) {         # Caricamento dei conteggi di successo del contenitore dai dispositivi aggiornati CSV (dispositivi aggiornati)         $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" |             Sort-Object LastWriteTime -Descending | Select-Object -Primo 1         $bucketStats = @{}         if ($updatedCsv) {             $updatedDevices = Import-Csv $updatedCsv.FullName             # Contare i successi per BucketId             $updatedDevices | ForEach-Object {                 $key = Get-BucketKey $_                 if ($key) {                     if (-not $bucketStats.ContainsKey($key)) {                         $bucketStats[$key] = @{ Successi = 0; In sospeso = 0; Totale = 0 }                     }                     $bucketStats[$key]. Successi++                     $bucketStats[$key]. Totale++                 }             }             Write-Log "Loaded $($updatedDevices.Count) updated devices across $($bucketStats.Count) buckets" "INFO"         } else {             # Fallback: prova ActionRequired_Buckets CSV             $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" |                 Sort-Object LastWriteTime -Descending | Select-Object -Primo 1             if ($bucketsCsv) {                 Import-Csv $bucketsCsv.FullName | ForEach-Object {                     $key = if ($_. BucketId) { $_. BucketId } else { "$($_. Produttore)|$($_. Modello)|$($_. BIOS)" }                     $bucketStats[$key] = @{                         Successi = [int]$_. Successi                         Pending = [int]$_. In sospeso                         Totale = [int]$_. TotalDevices                     }                 }             }         }         # Raggruppa i dispositivi NotUpdated per contenitore (produttore|Modello|BIOS)         $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ }         # Contenitori separati: zero-success vs has-success         $zeroSuccessBuckets = @()         $hasSuccessBuckets = @()         foreach ($bucket in $buckets) {             $bucketKey = $bucket. Nome             $bucketDevices = @($bucket. Gruppo)             $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Hostname })             # Contare i successi in questo contenitore             $stats = $bucketStats[$bucketKey]             $successes = if ($stats) { $stats. Successi } else { 0 }             # Trova i dispositivi distribuiti in questo contenitore dalla cronologia delle onde             $deployedToBucket = @()             foreach ($wave in $RolloutState.WaveHistory) {                 foreach ($device in $wave. Dispositivi) {                     se ($device. BucketKey -eq $bucketKey -and $device. Hostname) {                         $deployedToBucket += $device. Hostname                     }                 }             }             $deployedToBucket = @($deployedToBucket | Sort-Object -Univoco)             # Controlla se TUTTI i dispositivi distribuiti hanno segnalato un successo             $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames })             $confirmedSuccess = $deployedToBucket.Count - $stillPending.Count             # Se è in sospeso, ignora questo contenitore finché non tutti confermi             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"                 Continuare             }             # Idoneo rimanente = dispositivi non ancora distribuiti             $devicesNotYetTargeted = @($bucketDevices | Where-Object {                 $_. Hostname -notin $deployedToBucket             })             if ($devicesNotYetTargeted.Count -eq 0) { continue }             # Categorizza per numero di successi             $bucketInfo = @{                 BucketKey = $bucketKey                 Dispositivi = $devicesNotYetTargeted                 ConfirmedSuccess = $confirmedSuccess                 Successi = $successes                 OEM = if ($bucket. Gruppo[0]. WMI_Manufacturer) { $bucket. Gruppo[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' }             }             if ($successes -eq 0) {                 $bucketInfo += $zeroSuccessBuckets             } else {                 $hasSuccessBuckets += $bucketInfo             }         }         # []) PROCESS HAS-SUCCESS BUCKETS (≥1 success) [])         # Raddoppia il numero di successi : se 14 ha avuto successo, distribuisci 28 avanti         foreach ($bucketInfo in $hasSuccessBuckets) {             $nextBatchSize = $bucketInfo.Successi * 2             $nextBatchSize = [Matematica]::Min($nextBatchSize, $MaxDevicesPerWave)             $nextBatchSize = [Matematica]::Min($nextBatchSize, $bucketInfo.Dispositivi.Conteggio)             if ($nextBatchSize -gt 0) {                 $selectedDevices = @($bucketInfo.Dispositivi | Select-Object -First $nextBatchSize)                 $selectedDevices $waveDevices +=                 $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"             }         }         # []) PROCESS ZERO-SUCCESS BUCKETS (distribuiti tra OEM con il monitoraggio per OEM) }         # Obiettivo: distribuire il rischio tra diversi OEM, tenere traccia dello stato di avanzamento per OEM in modo indipendente         # Ogni OEM progredisce in base alla propria cronologia di successo:         # - OEM con successi: ottiene più dispositivi della prossima fase (2^waveCount)         # - OEM senza successi: rimane al livello corrente fino alla conferma del successo         if ($zeroSuccessBuckets.Count -gt 0) {             # Inizializzare i conteggi delle onde per OEM, se non esiste             if (-not $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             }             # Bucket di gruppo con successo zero per OEM             $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM }             $totalZeroSuccessAdded = 0             $oemsDeployedTo = @()             foreach ($oemGroup in $oemBuckets) {                 $oemName = $oemGroup.Name                 # Ottieni il numero di onde di questo OEM (inizia da 0)                 $oemWaveCount = if ($RolloutState.OEMWaveCounts[$oemName]) {                     $RolloutState.OEMWaveCounts[$oemName]                 } altro { 0 }                 # Calcola i dispositivi per QUESTO OEM: 2^waveCount (1, 2, 4, 8...)                 $devicesForThisOEM = [int][Matematica]::P ow(2, $oemWaveCount)                 $devicesForThisOEM = [Matematica]::Max(1, $devicesForThisOEM)                 $oemDevicesAdded = 0                 # Scegli da ogni contenitore in questo OEM                 foreach ($bucketInfo in $oemGroup.Group) {                     $remaining = $devicesForThisOEM - $oemDevicesAdded                     if ($remaining -le 0) { break }                     $toTake = [Matematica]::Min($remaining, $bucketInfo.Dispositivi.Conteggio)                     if ($toTake -gt 0) {                         $selectedDevices = @($bucketInfo.Dispositivi | Select-Object -First $toTake)                         $selectedDevices += $waveDevices                         $toTake += $oemDevicesAdded                         $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 (OEM wave $oemWaveCount = ${devicesForThisOEM}/OEM)" "WARN"                     }                 }                 if ($oemDevicesAdded -gt 0) {                     Write-Log " OEM: $oemName - Wave $oemWaveCount, Aggiunti dispositivi $oemDevicesAdded" "INFO"                     $oemName += $oemsDeployedTo                 }             }             # Tenere traccia degli OEM distribuiti (per incrementare il prossimo controllo di successo)             if ($oemsDeployedTo.Count -gt 0) {                 $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo                 Write-Log "Zero success deployment: $totalZeroSuccessAdded devices across $($oemsDeployedTo.Count) OEM" "INFO"             }         }     }     if (@($waveDevices). Conteggio -eq 0) {         reso $null     }     reso $waveDevices }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

# ============================================================================ # DISTRIBUZIONE OGGETTO Criteri di gruppo (INLINED - crea oggetto Criteri di gruppo, gruppo di sicurezza, collegamenti) # ============================================================================

function Deploy-GPOForWave {     param(         [stringa]$GPOName,         [stringa]$TargetOU,         [stringa]$SecurityGroupName,         [matrice]$WaveHostnames,         [bool]$DryRun = $false     )     # Criteri ADMX: SecureBoot.admx - SecureBoot_AvailableUpdatesPolicy     # Percorso del Registro di sistema: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot     # Nome valore: AvailableUpdatesPolicy     # Valore abilitato: 22852 (0x5944) - Aggiorna tutti i tasti di avvio protetto + bootmgr     # Valore disabilitato: 0     #     # Utilizzo di Criteri di gruppo Preferences (GPP) per una distribuzione affidabile del percorso HKLM\SYSTEM     # GPP crea le impostazioni in: Configurazione computer > Preferenze > Impostazioni di Windows > Registro di sistema     $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot"     $RegistryValueName = "AvailableUpdatesPolicy"     $RegistryValue = 22852 # 0x5944 - corrisponde ad ADMX enabledValue     Write-Log "Distribuzione dell'oggetto Criteri di gruppo: $GPOName" "WAVE"     Write-Log "Registro di sistema: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X'))))" "INFO"     if ($DryRun) {         Write-Log "[DRYRUN] Creazione dell'oggetto Criteri di gruppo: $GPOName" "INFO"         Write-Log "[DRYRUN] Creare un gruppo di sicurezza: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Aggiungerebbe $(@($WaveHostnames). Contare) computer da raggruppare" "INFO"         Write-Log "[DRYRUN] Collega l'oggetto Criteri di gruppo a: $TargetOU" "INFO"         reso $true     }     prova {         # Importare i moduli necessari         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } catch {         Write-Log "Impossibile importare i moduli obbligatori (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERRORE"         restituire $false     }     # Passaggio 1: Creare o ottenere l'oggetto Criteri di gruppo     $existingGPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     if ($existingGPO) {         Write-Log "L'oggetto Criteri di gruppo esiste già: $GPOName" "INFO"         $gpo = $existingGPO     } else {         prova {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Certificate Rollout - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Oggetto Criteri di gruppo creato: $GPOName" "OK"         } catch {             Write-Log "Impossibile creare un oggetto Criteri di gruppo: $($_. Exception.Message)" "ERRORE"             restituire $false         }     }     # Passaggio 2: Impostare il valore del Registro di sistema utilizzando Criteri di gruppo Preferences (GPP)     # GPP è più affidabile per I percorsi HKLM\SYSTEM rispetto a Set-GPRegistryValue     prova {         # Prima di tutto, provare a rimuovere qualsiasi preferenza esistente per questo valore (per evitare duplicati)         Remove-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue         # Crea la preferenza del Registro di sistema GPP con l'azione "Sostituisci"         # Sostituisci = Crea se non esiste, Aggiorna se esistente (più affidabile)         # Update = Aggiornamento solo se esistente (errore se il valore non esiste)         Set-GPPrefRegistryValue -Name $GPOName '             -Context Computer '             -Action Replace '             -Key $RegistryKey '             -ValueName $RegistryValueName '             -Type DWord '             -Valore $RegistryValue         Write-Log "Preferenza del Registro di sistema GPP configurata: $RegistryValueName = 0x5944 (Action=Replace)" "OK"     } catch {         Write-Log "GPP failed, trying Set-GPRegistryValue: $($_. Exception.Message)" "WARN"         # Fallback to Set-GPRegistryValue (funziona se ADMX è distribuito)         prova {             Set-GPRegistryValue -Name $GPOName '                 -Key $RegistryKey'                 -ValueName $RegistryValueName '                 -Type DWord '                 -Valore $RegistryValue             Write-Log "Registro di sistema configurato tramite Set-GPRegistryValue: $RegistryValueName = 0x5944" "OK"         } catch {             Write-Log "Impossibile impostare il valore del Registro di sistema: $($_. Exception.Message)" "ERRORE"             restituire $false         }     }     # Passaggio 3: Creare o ottenere un gruppo di sicurezza     $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-$existingGroup) {         prova {             $group = $SecurityGroupName New-ADGroup -Name '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Description "Computers targeted for Secure Boot rollout - $GPOName" '                 -Passthru             Write-Log "Gruppo di sicurezza creato: $SecurityGroupName" "OK"         } catch {             Write-Log "Impossibile creare un gruppo di sicurezza: $($_. Exception.Message)" "ERRORE"             restituire $false         }     } else {         Write-Log "Esiste un gruppo di sicurezza: $SecurityGroupName" "INFO"         $group = $existingGroup     }     # Passaggio 4: Aggiungere computer al gruppo di sicurezza     $added = 0     $failed = 0     foreach ($hostname in $WaveHostnames) {         prova {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } catch {             $failed++         }     }     Write-Log "Aggiunto $added computer al gruppo di sicurezza ($failed non trovato in AD)" "OK"     # Passaggio 5: Configurare il filtro di sicurezza nell'oggetto Criteri di gruppo     prova {         # Rimuovi l'autorizzazione predefinita "Utenti autenticati" Applica l'autorizzazione (mantieni lettura)         Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         # Aggiungi l'autorizzazione Applica per il nostro gruppo di sicurezza         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Filtro di sicurezza configurato per: $SecurityGroupName" "OK"     } catch {         Write-Log "Impossibile configurare il filtro di sicurezza: $($_. Exception.Message)" "WARN"         Write-Log "GpO may apply to all computers in the linked OU - verify manually" "WARN"     }     # Passaggio 6: Collegare l'oggetto Criteri di gruppo all'unità organizzativa (critico per l'applicazione dei criteri)     if ($TargetOU) {         prova {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }             if (-$existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop                 Write-Log "Oggetto Criteri di gruppo collegati a: $TargetOU" "OK"                 Write-Log "GpO apply at next gpupdate on target computers" "INFO"             } else {                 Write-Log "Oggetto Criteri di gruppo già collegato all'unità organizzativa di destinazione" "INFO"             }         } catch {             Write-Log "CRITICO: Impossibile collegare l'oggetto Criteri di gruppo all'unità organizzativa: $($_. Exception.Message)" "ERRORE"             Write-Log "GpO was created but NOT LINKED - it will NOT apply to any computers!" "ERRORE"             Write-Log "Correzione manuale necessaria: New-GPLink -Name '$GPOName' -Target '$TargetOU' -LinkEnabled Yes" "ERROR"             $false reso         }     } else {         Write-Log "AVVISO: nessun oggetto TargetOU specificato - Oggetto Criteri di gruppo creato ma NON COLLEGATO!" "ERRORE"         Write-Log "Collegamento manuale necessario per rendere effettivo l'oggetto Criteri di gruppo" "ERRORE"         Write-Log "Esegui: New-GPLink -Name '$GPOName' -Target '<Your-Domain-DN>' -LinkEnabled Yes" "ERROR"     }     # Passaggio 7: Verificare la configurazione dell'oggetto Criteri di gruppo     Write-Log "Verifica della configurazione dell'oggetto Criteri di gruppo..." "INFO"     prova {         $gpoReport = Get-GPO -Name $GPOName -ErrorAction Stop         Write-Log "Stato oggetto Criteri di gruppo: $($gpoReport.GpoStatus)" "INFO"         # Controlla se l'impostazione del Registro di sistema è configurata         $regSettings = Get-GPRegistryValue -Name $GPOName -Key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue         if (-$regSettings) {             # Prova il controllo del Registro di sistema GPP (percorso diverso nell'oggetto Criteri di gruppo)             Write-Log "Controllo delle preferenze del Registro di sistema GPP..." "INFO"         }     } catch {         Write-Log "Impossibile verificare l'oggetto Criteri di gruppo: $($_. Exception.Message)" "WARN"     }     reso $true }                                                                                                

# ============================================================================ # WINCS DEPLOYMENT (Alternativa a AvailableUpdatesPolicy GPO) N. ============================================================================ # Riferimento: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe # # WinCS Commands (eseguito sull'endpoint nel contesto SYSTEM): # Query: WinCsFlags.exe /query --key F33E0C8E002 # Applica: WinCsFlags.exe /apply --key "F33E0C8E002" # Reimposta: WinCsFlags.exe /reset --tasto "F33E0C8E002" # # Questo metodo distribuisce un oggetto Criteri di gruppo con un'attività pianificata che viene eseguita WinCsFlags.exe /apply # come SISTEMA sugli endpoint di destinazione. Analogamente a come viene distribuito lo script di rilevamento, # ma viene eseguito una sola volta (all'avvio) anziché ogni giorno.

function Deploy-WinCSGPOForWave {     <#     . SINOSSI         Distribuisci l'abilitazione dell'avvio protetto WinCS tramite l'attività pianificata dell'oggetto Criteri di gruppo.. DESCRIZIONE         Crea un oggetto Criteri di gruppo che distribuisce un'attività pianificata per l'esecuzione WinCsFlags.exe /apply         in Contesto SYSTEM all'avvio del computer. Controlli gruppo di sicurezza per l'assegnazione.. PARAMETER GPOName         Nome dell'oggetto Criteri di gruppo.. PARAMETER TargetOU         Unità organizzativa a cui collegare l'oggetto Criteri di gruppo.. PARAMETER SecurityGroupName         Gruppo di sicurezza per il filtro dell'oggetto Criteri di gruppo.. PARAMETER WaveHostnames         Nomi host da aggiungere al gruppo di sicurezza.. PARAMETER WinCSKey         Il tasto WinCS da applicare (impostazione predefinita: F33E0C8E002).. PARAMETER DryRun         Se true, registrare solo le operazioni da eseguire.#>     param(         [Parametro(Obbligatorio = $true)]         [stringa]$GPOName,                  [Parametro(Obbligatorio = $false)]         [stringa]$TargetOU,                  [Parametro(Obbligatorio = $true)]         [stringa]$SecurityGroupName,                  [Parametro(Obbligatorio = $true)]         [matrice]$WaveHostnames,                  [Parametro(Obbligatorio = $false)]         [stringa]$WinCSKey = "F33E0C8E002",                  [Parametro(Obbligatorio = $false)]         [bool]$DryRun = $false     )          # Scheduled Task configuration for WinCsFlags.exe     $TaskName = "SecureBoot-WinCS-Apply"     $TaskPath = "\Microsoft\Windows\SecureBoot\"     $TaskDescription = "Applica la configurazione di avvio protetto tramite WinCS - Chiave: $WinCSKey"          Write-Log "Deploying WinCS GPO: $GPOName" "WAVE"     Write-Log "Verrà eseguita l'attività: WinCsFlags.exe /apply --key '"$WinCSKey'"" "INFO"     Write-Log "Trigger: All'avvio del sistema (eseguito una volta come SISTEMA)" "INFO"          if ($DryRun) {         Write-Log "[DRYRUN] Creazione dell'oggetto Criteri di gruppo: $GPOName" "INFO"         Write-Log "[DRYRUN] Creare un gruppo di sicurezza: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Aggiungerebbe $(@($WaveHostnames). Contare) computer da raggruppare" "INFO"         Write-Log "[DRYRUN] Distribuzione dell'attività programmata: $TaskName" "INFO"         Write-Log "[DRYRUN] Collega l'oggetto Criteri di gruppo a: $TargetOU" "INFO"         restituire @{             Success = $true             Oggetto Criteri di gruppoCreato = $false             GroupCreated = $false             ComputersAdded = 0         }     }          prova {         # Importare i moduli necessari         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } catch {         Write-Log "Impossibile importare i moduli obbligatori (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERRORE"         restituire @{ Success = $false; Errore = $_. Exception.Message }     }          # Passaggio 1: Creare o ottenere l'oggetto Criteri di gruppo     $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     if ($gpo) {         Write-Log "L'oggetto Criteri di gruppo esiste già: $GPOName" "INFO"     } else {         prova {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot WinCS Deployment - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Oggetto Criteri di gruppo creato: $GPOName" "OK"         } catch {             Write-Log "Impossibile creare un oggetto Criteri di gruppo: $($_. Exception.Message)" "ERRORE"             restituire @{ Success = $false; Errore = $_. Exception.Message }         }     }          # Passaggio 2: Creare il codice XML delle attività pianificate per la distribuzione dell'oggetto Criteri di gruppo     # Viene creata un'attività che viene eseguita WinCsFlags.exe /apply all'avvio     $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">  >RegistrationInfo <     <Descrizione>$TaskDescription</Descrizione>     WinCsFlags.exe1 author>SISTEMA</Author>  >WinCsFlags.exe5 /RegistrationInfo  >trigger WinCsFlags.exe7     WinCsFlags.exe9> Boot Trigger       <Abilitato>true</Enabled>       <ritardo></Delay di PT5M>     <>/BootBoot  >/Trigger <  ><principals     <Principal id="Author">       <UserId><<S-1-5-18 /UserId>       <<>RunLevel >HighestAvailable    >/Principal di <  ></Principals  >Impostazioni <     <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>     <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>     <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>     <allowHardTerminate>true</AllowHardTerminate>     <StartWhenAvailable>true</StartWhenAvailable>     <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>     <> IdleSettings       <StopOnIdleEnd>false</StopOnIdleEnd>       <RestartOnIdle>false</RestartOnIdle>     </IdleSettings>     <AllowStartOnDemand>true</AllowStartOnDemand>     <Abilitato>true</Enabled>     <False>Hidden</Hidden>     <RunOnlyIfIdle>false</RunOnlyIfIdle>     WinCsFlags.exe03 DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>     WinCsFlags.exe07 UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>     WinCsFlags.exe11 WakeToRun>false</WakeToRun>     WinCsFlags.exe15 ExecutionTimeLimit><PT1H /ExecutionTimeLimit>     WinCsFlags.exe19 DeleteExpiredTaskAfter>P30D</DeleteExpiredTaskAfter>     WinCsFlags.exe23 priorità>7</> priorità  >WinCsFlags.exe27 /Settings   WinCsFlags.exe29 Actions Context="Author"WinCsFlags.exe30    >WinCsFlags.exe31 Exec      >comando>WinCsFlags.exe</Command WinCsFlags.exe33       WinCsFlags.exe37 Argomenti>/apply --key "$WinCSKey" WinCsFlags.exe39 /Arguments>     WinCsFlags.exe41>/Exec   WinCsFlags.exe43 /Actions> WinCsFlags.exe45 /> attività " @

    # Step 3: Deploy scheduled task via GPO Preferences     # Archiviare il codice XML delle attività in SYSVOL per attività pianificate dell'oggetto Criteri di gruppo - Attività immediate     prova {         $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         }         # Create ScheduledTasks.xml for GPP         $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">   <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName" image="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="{$([guid]::NewGuid(). ToString(). ToUpper())}">     <Properties action="C" name="$TaskName" runAs="NT AUTHORITY\System" logonType="S4U">       <Task version="1.3">        >RegistrationInfo <           <Descrizione>$TaskDescription</Descrizione>        ></RegistrationInfo        ><Principals           <Principal id="Author">             <UserId>NT AUTHORITY\System</UserId>             <>LogonType>S4U</LogonType             <><HighestAvailable> RunLevel           <> /Principal         </Principals>        >Impostazioni <          ><IdleSettings             <Durata></Durata PT5M>             <WaitTimeout></WaitTimeout PT1H>             <StopOnIdleEnd>false</StopOnIdleEnd>             <RestartOnIdle>false</RestartOnIdle>           </IdleSettings>           <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>           <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>           <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>           <allowHardTerminate>true</AllowHardTerminate>           <StartWhenAvailable>true</StartWhenAvailable>           <AllowStartOnDemand>true</AllowStartOnDemand>           <Abilitato>true</Enabled>           <Falsi>nascosti</> nascosti           <ExecutionTimeLimit></ExecutionTimeLimit>PT1H>           <>Priorità>7</Priorità           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>        ></Settings        >trigger <          ><Time Trigger             <StartBoundary>$(Get-Date -Format 'yyyy-MM-dd')T00:00:00</StartBoundary>             <Abilitato>true</Enabled>          >/Time Trigger <        >/Trigger <        >Azioni <          ><Exec            >comando <>WinCsFlags.exe</Command             <Argomenti>/apply --key "$WinCSKey" </Arguments>           <>/Exec         </Actions>       </>attività    >/Properties <  ></ImmediateTaskV2 </ScheduledTasks> "@         $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Codifica UTF8 -Force         Write-Log "Attività pianificata distribuita all'oggetto Criteri di gruppo: $TaskName" "OK"     } catch {         Write-Log "Impossibile distribuire il codice XML delle attività pianificate: $($_. Exception.Message)" "WARN"         Write-Log "Ritorno alla distribuzione WinCS basata sul Registro di sistema" "INFO"         # Fallback: usare l'approccio del Registro di sistema WinCS se l'attività pianificata GPP non riesce         # WinCS può essere attivato anche tramite la chiave del Registro di sistema         # (L'implementazione dipende dall'API del Registro di sistema WinCS, se disponibile)     }     # Passaggio 4: Creare o ottenere un gruppo di sicurezza     $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-$group) {         prova {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Description "Computers targeted for Secure Boot WinCS rollout - $GPOName" '                 -Passthru             Write-Log "Gruppo di sicurezza creato: $SecurityGroupName" "OK"         } catch {             Write-Log "Impossibile creare un gruppo di sicurezza: $($_. Exception.Message)" "ERRORE"             restituire @{ Success = $false; Errore = $_. Exception.Message }         }     } else {         Write-Log "Esiste un gruppo di sicurezza: $SecurityGroupName" "INFO"     }     # Passaggio 5: Aggiungere computer al gruppo di sicurezza     $added = 0     $failed = 0     foreach ($hostname in $WaveHostnames) {         prova {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } catch {             $failed++         }     }     Write-Log "Aggiunto $added computer al gruppo di sicurezza ($failed non trovato in AD)" "OK"     # Passaggio 6: Configurare il filtro di sicurezza nell'oggetto Criteri di gruppo     prova {         Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Filtro di sicurezza configurato per: $SecurityGroupName" "OK"     } catch {         Write-Log "Impossibile configurare il filtro di sicurezza: $($_. Exception.Message)" "WARN"     }     # Passaggio 7: Collega l'oggetto Criteri di gruppo all'unità organizzativa     if ($TargetOU) {         prova {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }             if (-$existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop                 Write-Log "Oggetto Criteri di gruppo collegato a: $TargetOU" "OK"             } else {                 Write-Log "Oggetto Criteri di gruppo già collegato all'unità organizzativa di destinazione" "INFO"             }         } catch {             Write-Log "CRITICO: Impossibile collegare l'oggetto Criteri di gruppo all'unità organizzativa: $($_. Exception.Message)" "ERRORE"             restituire @{ Success = $false; Errore = "Collegamento oggetto Criteri di gruppo non riuscito: $($_. Exception.Message)" }         }     }     Write-Log "Distribuzione dell'oggetto Criteri di gruppo WinCS completata" "OK"     Write-Log "I computer verranno eseguiti WinCsFlags.exe al prossimo aggiornamento dell'oggetto Criteri di gruppo + riavvio/avvio" "INFO"     restituire @{         Success = $true         Oggetto Criteri di gruppoCreato = $true         GroupCreated = $true         ComputersAdded = $added         ComputersFailed = $failed     } }                                                                                        

# Wrapper function to maintain compatibility with main loop funzione Deploy-WinCSForWave {     param(         [Parametro(Obbligatorio = $true)]         [matrice]$WaveHostnames,         [Parametro(Obbligatorio = $false)]         [stringa]$WinCSKey = "F33E0C8E002",         [Parametro(Obbligatorio = $false)]         [string]$WavePrefix = "SecureBoot-Rollout",         [Parametro(Obbligatorio = $false)]         [int]$WaveNumber = 1,         [Parametro(Obbligatorio = $false)]         [stringa]$TargetOU,         [Parametro(Obbligatorio = $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     # Converti nel formato restituito previsto     restituire @{         Success = $result. Successo         Applied = $result. ComputerAggiunta         Ignorato = 0         Non riuscito = if ($result. ComputersFailed) { $result. ComputerFailed } else { 0 }         Risultati = @()     } }                                                            

# ============================================================================ # ABILITA DISTRIBUZIONE ATTIVITÀ N. ============================================================================ # Distribuisci Enable-SecureBootUpdateTask.ps1 nei dispositivi con attività pianificata disabilitata.# Usa un oggetto Criteri di gruppo con un'attività programmata immediata che viene eseguita una sola volta.

function Deploy-EnableTaskGPO {     <#     . SINOSSI         Distribuisci Enable-SecureBootUpdateTask.ps1 tramite l'attività pianificata dell'oggetto Criteri di gruppo.. DESCRIZIONE         Crea un oggetto Criteri di gruppo che distribuisce un'attività programmata una tantum per abilitare         Attività pianificata Secure-Boot-Update nei dispositivi di destinazione.. PARAMETER TargetOU         Unità organizzativa a cui collegare l'oggetto Criteri di gruppo.. PARAMETER TargetHostnames         Nomi host dei dispositivi con attività disabilitata (dal report di aggregazione).. PARAMETER DryRun         Se true, registrare solo le operazioni da eseguire.#>     param(         [Parametro(Obbligatorio = $false)]         [stringa]$TargetOU,                  [Parametro(Obbligatorio = $true)]         [matrice]$TargetHostnames,                  [Parametro(Obbligatorio = $false)]         [bool]$DryRun = $false     )          $GPOName = "SecureBoot-EnableTask-Remediation"     $SecurityGroupName = "SecureBoot-EnableTask-Devices"     $TaskName = "SecureBoot-EnableTask-OneTime"     $TaskDescription = "Attività una tantum per abilitare l'attività programmata Secure-Boot-Update"          Write-Log "=" * 70 "INFO"     Write-Log "DEPLOYING ENABLE TASK REMEDIATION" "INFO"     Write-Log "=" * 70 "INFO"     Write-Log "Dispositivi di destinazione: $($TargetHostnames.Count)" "INFO"     Write-Log "OGGETTO Criteri di gruppo: $GPOName" "INFO"     Write-Log "Gruppo di sicurezza: $SecurityGroupName" "INFO"          if ($DryRun) {         Write-Log "[DRYRUN] Creazione dell'oggetto Criteri di gruppo: $GPOName" "INFO"         Write-Log "[DRYRUN] Creare un gruppo di sicurezza: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Aggiungerei computer $($TargetHostnames.Count) per raggruppare" "INFO"         Write-Log "[DRYRUN] Distribuirebbe un'attività programmata una tantum per abilitare l'aggiornamento di avvio protetto" "INFO"         Write-Log "[DRYRUN] Collega l'oggetto Criteri di gruppo a: $TargetOU" "INFO"         restituire @{             Success = $true             ComputersAdded = 0             DryRun = $true         }     }          prova {         # Importare i moduli necessari         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } catch {         Write-Log "Impossibile importare i moduli necessari: $($_. Exception.Message)" "ERRORE"         restituire @{ Success = $false; Errore = $_. Exception.Message }     }          # Passaggio 1: Creare o ottenere l'oggetto Criteri di gruppo     $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     if ($gpo) {         Write-Log "L'oggetto Criteri di gruppo esiste già: $GPOName" "INFO"     } else {         prova {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Task Enable Remediation - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Oggetto Criteri di gruppo creato: $GPOName" "OK"         } catch {             Write-Log "Impossibile creare un oggetto Criteri di gruppo: $($_. Exception.Message)" "ERRORE"             restituire @{ Success = $false; Errore = $_. Exception.Message }         }     }          # Passaggio 2: Distribuire il codice XML delle attività pianificate in SYSVOL dell'oggetto Criteri di gruppo     # L'attività esegue un comando di PowerShell per abilitare l'attività Secure-Boot-Update     prova {         $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. Id)}\Computer\Preferenze\Attività Pianificate"                  if (-not (Test-Path $sysvolPath)) {             New-Item -ItemType Directory -Path $sysvolPath -Force | Out-Null         }                  # Comando di PowerShell per abilitare l'attività 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 # Codifica per l'incorporamento XML sicuro         $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand))                  $taskGuid = [guid]::NewGuid(). ToString("B"). ToUpper()                  # GPP Scheduled Task XML - Immediate task that runs once         $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">   <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName" image="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="$taskGuid" removePolicy="1" userContext="0">     <Properties action="C" name="$TaskName" runAs="NT AUTHORITY\SYSTEM" logonType="S4U">       <Task version="1.3">        >RegistrationInfo <           <Descrizione>$TaskDescription</Descrizione>        ></RegistrationInfo        ><Principals           <Principal id="Author">             <UserId>S-1-5-18</UserId>             <><HighestAvailable /RunLevel>          >/Principal di <        >/Principals <        >Impostazioni <           <> IdleSettings             <durata></durata PT5M>             <WaitTimeout></WaitTimeout PT1H>             <StopOnIdleEnd>false</StopOnIdleEnd>             <RestartOnIdle>false</RestartOnIdle>           </IdleSettings>           <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>           <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>           <>false</StopIfGoingOnBatteries>           <>True AllowHardTerminate</AllowHardTerminate>           <StartWhenAvailable>true</StartWhenAvailable>           <AllowStartOnDemand>true</AllowStartOnDemand>           <Abilitato>true</Enabled>           <false>nascoste />nascoste<           <ExecutionTimeLimit>pt1h</ExecutionTimeLimit>           <priorità>7</> priorità           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>        ></Settings        >Azioni <          ><Exec             <>powershell.exe<Comandi /> Comando             <argomenti>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments>          ></Exec         </Actions>       </> attività    >/Properties <   </ImmediateTaskV2> </ScheduledTasks> "@                  $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Codifica UTF8 -Force         Write-Log "Distribuita un'attività programmata una tantum all'oggetto Criteri di gruppo: $TaskName" "OK"              } catch {         Write-Log "Impossibile distribuire il codice XML delle attività pianificate: $($_. Exception.Message)" "ERRORE"         restituire @{ Success = $false; Errore = $_. Exception.Message }     }          # Passaggio 3: Creare o ottenere un gruppo di sicurezza     $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-$group) {         prova {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Descrizione "Computer con attività di secure-boot-update disabilitata - mirato per la correzione" '                 -Passthru             Write-Log "Gruppo di sicurezza creato: $SecurityGroupName" "OK"         } catch {             Write-Log "Impossibile creare un gruppo di sicurezza: $($_. Exception.Message)" "ERRORE"             restituire @{ Success = $false; Errore = $_. Exception.Message }         }     } else {         Write-Log "Esiste un gruppo di sicurezza: $SecurityGroupName" "INFO"     }          # Passaggio 4: Aggiungere computer al gruppo di sicurezza     $added = 0     $failed = 0     foreach ($hostname in $TargetHostnames) {         prova {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } catch {             $failed++             Write-Log "Computer non trovato in Active Directory: $hostname" "WARN"         }     }     Write-Log "Aggiunto $added computer al gruppo di sicurezza ($failed non trovato in AD)" "OK"          # Passaggio 5: Configurare il filtro di sicurezza nell'oggetto Criteri di gruppo     prova {         Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Filtro di sicurezza configurato per: $SecurityGroupName" "OK"     } catch {         Write-Log "Impossibile configurare il filtro di sicurezza: $($_. Exception.Message)" "WARN"     }          # Passaggio 6: Collega l'oggetto Criteri di gruppo all'unità organizzativa     if ($TargetOU) {         prova {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }                          if (-$existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop                 Write-Log "Oggetto Criteri di gruppo collegato a: $TargetOU" "OK"             } else {                 Write-Log "Oggetto Criteri di gruppo già collegato all'unità organizzativa di destinazione" "INFO"             }         } catch {             Write-Log "Impossibile collegare l'oggetto Criteri di gruppo all'unità organizzativa: $($_. Exception.Message)" "ERRORE"             restituire @{ Success = $false; Errore = "Collegamento oggetto Criteri di gruppo non riuscito: $($_. Exception.Message)" }         }     } else {         Write-Log "Nessun targetou specificato - L'oggetto Criteri di gruppo dovrà essere collegato manualmente" "WARN"     }          Write-Log "" "INFO"     Write-Log "ENABLE TASK DEPLOYMENT COMPLETE" "OK"     Write-Log "I dispositivi eseguono l'attività di abilitazione al prossimo aggiornamento dell'oggetto Criteri di gruppo (gpupdate)" "INFO"     Write-Log "L'attività viene eseguita una volta come SISTEMA e abilita Secure-Boot-Update" "INFO"     Write-Log "" "INFO"          restituire @{         Success = $true         ComputersAdded = $added         ComputersFailed = $failed         GPOName = $GPOName         SecurityGroup = $SecurityGroupName     } }

# ============================================================================ # ABILITA ATTIVITÀ NEI DISPOSITIVI DISABILITATI N. ============================================================================ 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 ""     # Trovare i dispositivi con un'attività disabilitata dall'aggregazione dei dati     if (-$AggregationInputPath) {         Write-Host "ERRORE: -AggregationInputPath è necessario per identificare i dispositivi con attività disabilitata" -ForegroundColor Red         Write-Host "Utilizzo: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <percorso> -ReportBasePath percorso <>" -ForegroundColor gray         uscita 1     }     Write-Host "Scansione per dispositivi con attività secure-boot-update disabilitata..." -ForegroundColor Ciano     # Carica i file JSON e trova dispositivi con attività disabilitata     $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                  Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }     $disabledTaskDevices = @()     foreach ($file in $jsonFiles) {         prova {             $device = Get-Content $file. FullName -Raw | ConvertFrom-Json             se ($device. SecureBootTaskEnabled -eq $false -or                 $device. SecureBootTaskStatus -eq 'Disabled' -or                 $device. SecureBootTaskStatus -eq 'NotFound') {                 # Includi solo i dispositivi che non sono già stati aggiornati (nessun evento 1808)                 if ([int]$device. Event1808Count -eq 0) {                     $disabledTaskDevices += $device. Hostname                 }             }         } catch {             # Ignora file non validi         }     }     $disabledTaskDevices = $disabledTaskDevices | Select-Object -Univoco     if ($disabledTaskDevices.Count -eq 0) {         Write-Host ""         Write-Host "Nessun dispositivo trovato con l'attività Secure-Boot-Update disabilitata". -ForegroundColor Green         Write-Host "Tutti i dispositivi hanno abilitato l'attività o sono già stati aggiornati". -ForegroundColor gray         uscita 0     }     Write-Host ""     Write-Host dispositivi "Trovati dispositivi $($disabledTaskDevices.Count) con attività disabilitata:" -ForegroundColor Yellow     $disabledTaskDevices | Select-Object -First 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }     if ($disabledTaskDevices.Count -gt 20) {         Write-Host " ... e $($disabledTaskDevices.Count - 20) more" -ForegroundColor gray     }     Write-Host ""     # Distribuire l'oggetto Criteri di gruppo Abilita attività     $result = Deploy-EnableTaskGPO -TargetHostnames $disabledTaskDevices -TargetOU $TargetOU -DryRun $DryRun     se ($result. Operazione riuscita) {         Write-Host ""         Write-Host "SUCCESS: Enable Task GPO deployed" -ForegroundColor Green         Write-Host " Computers added to security group: $($result. ComputersAdded)" -ForegroundColor Ciano         se ($result. ComputersFailed -gt 0) {             Write-Host " Computer non trovati in Active Directory: $($result. ComputersFailed)" -ForegroundColor yellow         }         Write-Host ""         Write-Host "PASSAGGI SUCCESSIVI:" -Primo pianoColore bianco         Write-Host " 1.                                              I dispositivi riceveranno l'oggetto Criteri di gruppo al prossimo aggiornamento (gpupdate /force)" -ForegroundColor Gray         Write-Host " 2. L'attività una tantum abilita Secure-Boot-Update" -ForegroundColor gray         Write-Host " 3. Eseguire di nuovo l'aggregazione per verificare che l'attività sia abilitata" -ForegroundColor Gray     } else {         Write-Host ""         Write-Host "FAILED: Could not deploy Enable Task GPO" -ForegroundColor Red         Write-Host "Errore: $($result. Errore)" -Primo pianoColore rosso     }          uscita 0 }

# ============================================================================ # CICLO DI ORCHESTRAZIONE PRINCIPALE # ============================================================================

Write-Host "" Write-Host ("=" * 80) -ForegroundColor Ciano Write-Host " SECURE BOOT ROLLOUT ORCHESTRATOR - DISTRIBUZIONE CONTINUA" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Ciano Write-Host ""

if ($DryRun) {     Write-Host "[MODALITÀ DI ESECUZIONE A SECCO]" -ForegroundColor Magenta }

if ($UseWinCS) {     Write-Host "[MODALITÀ WINCS]" -ForegroundColor yellow     Write-Host "Uso di WinCsFlags.exe anziché oggetto Criteri di gruppo/AvailableUpdatesPolicy" -ForegroundColor yellow     Write-Host "Tasto WINCS: $WinCSKey" -ForegroundColor gray     Write-Host "" }

Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "Percorso di input: $AggregationInputPath" "INFO" Write-Log "Percorso report: $ReportBasePath" "INFO" if ($UseWinCS) {     Write-Log "Metodo di distribuzione: WinCS (WinCsFlags.exe /apply --key '"$WinCSKey'")" "INFO" } else {     Write-Log "Metodo di distribuzione: Oggetto Criteri di gruppo (AvailableUpdatesPolicy)" "INFO" }

# Resolve TargetOU - default to domain root for domain-wide coverage # Necessario solo per il metodo di distribuzione dell'oggetto Criteri di gruppo (WinCS non richiede AD/GPO) if (-not $UseWinCS -and -not $TargetOU) {     prova {         # Prova a usare più metodi per ottenere il DN del dominio         $domainDN = $null         # Metodo 1: Get-ADDomain (richiede RSAT-AD-PowerShell)         prova {             Import-Module ActiveDirectory -ErrorAction Stop             $domainDN = (Get-ADDomain -ErrorAction Stop). Distinguishedname         } catch {             Write-Log "Get-ADDomain non riuscito: $($_. Exception.Message)" "WARN"         }         # Metodo 2: usare RootDSE tramite ADSI         if (-$domainDN) {             prova {                 $rootDSE = [ADSI]"LDAP://RootDSE"                 $domainDN = $rootDSE.defaultNamingContext.ToString()             } catch {                 Write-Log "ADSI RootDSE failed: $($_. Exception.Message)" "WARN"             }         }         # Metodo 3: analizzare dall'appartenenza al dominio del computer         if (-$domainDN) {             prova {                 $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()                 $domainDN = "DC=" + ($domain. Name -replace '\.', ',DC=')             } catch {                 Write-Log "GetComputerDomain non riuscito: $($_. Exception.Message)" "WARN"             }         }         if ($domainDN) {             $TargetOU = $domainDN             Write-Log "Destinazione: radice del dominio ($domainDN) - L'oggetto Criteri di gruppo applica l'oggetto Criteri di gruppo a livello di dominio tramite il filtro dei gruppi di sicurezza" "INFO"         } else {             Write-Log "Impossibile determinare il DN di dominio - Oggetto Criteri di gruppo verrà creato ma NON COLLEGATO!" "ERRORE"             Write-Log "Specificare il parametro -TargetOU o collegare manualmente l'oggetto Criteri di gruppo dopo la creazione" "ERRORE"             $TargetOU = $null         }     } catch {         Write-Log "Non è stato possibile ottenere il DN del dominio - L'oggetto Criteri di gruppo verrà creato ma non collegato.                                     Collega manualmente, se necessario". "AVVISO"         Write-Log "Errore: $($_. Exception.Message)" "WARN"         $TargetOU = $null     } } else {     Write-Log "UNITÀ ORGANIZZATIVA di destinazione: $TargetOU" "INFO" }

Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Intervallo sondaggio: $PollIntervalMinutes minuti" "INFO" if ($LargeScaleMode) {     Write-Log "LargeScaleMode enabled (batch size: $ProcessingBatchSize, log sample: $DeviceLogSampleSize)" "INFO" }

# ============================================================================ # CONTROLLO PREREQUISITO: verifica che il rilevamento sia distribuito e funzionante # ============================================================================

Write-Host "" Write-Log "Controllo dei prerequisiti..." "INFO"

$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath if (-not $detectionCheck.IsDeployed) {     Write-Log $detectionCheck.Messaggio "ERRORE"     Write-Host ""     Write-Host "OBBLIGATORIO: Distribuire prima l'infrastruttura di rilevamento:" -ForegroundColor Yellow     Write-Host " 1. Esegui: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\server\SecureBootLogs$'" -ForegroundColor Cyan     Write-Host " 2. Attendi il report dei dispositivi (12-24 ore)" -ForegroundColor Cyan     Write-Host " 3. Esegui nuovamente questo orchestratore" -ForegroundColor Cyan     Write-Host ""     if (-$DryRun) {         Ritorno     } } else {     Write-Log $detectionCheck.Messaggio "OK" }

# Check data freshness $freshness = $AggregationInputPath Get-DataFreshness -JsonPath Write-Log "Aggiornamento dei dati: $($freshness. TotalFiles), $($freshness. FreshFiles) fresh (<24h), $($freshness. StaleFiles) non aggiornato (>72h)" "INFO" se ($freshness. Avviso) {     Write-Log $freshness. Avviso "WARN" }

# Load Allow List (targeted rollout - ONLY these devices will be rolled out) $allowedHostnames = @() if ($AllowListPath -or $AllowADGroup) {     $allowedHostnames = Get-AllowedHostnames $AllowADGroup -AllowFilePath $AllowListPath -ADGroupName     if ($allowedHostnames.Count -gt 0) {         Write-Log "AllowList: ONLY $($allowedHostnames.Count) devices will be considered for rollout" "INFO"     } else {         Write-Log "AllowList specified but no devices found- this will blockouts all rollouts!" "AVVISO"     } }

# Load VIP/exclusion list (BlockList) $excludedHostnames = @() if ($ExclusionListPath -or $ExcludeADGroup) {     $excludedHostnames = Get-ExcludedHostnames -ExclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup     if ($excludedHostnames.Count -gt 0) {         Write-Log "Esclusione VIP: i dispositivi $($excludedHostnames.Count) verranno ignorati dall'implementazione" "INFO"     } }

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

if ($rolloutState.Status -eq "NotStarted") {     $rolloutState.Status = "In Progress"     $rolloutState.StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"     Write-Log "Avvio di una nuova implementazione" "WAVE" }

Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "Bucket bloccati: $($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 "== ITERAZIONE $iterationCount "ONDA"     Write-Host ("=" * 80) -ForegroundColor white     # Passaggio 1: Eseguire l'aggregazione     Write-Log "Passaggio 1: Esecuzione dell'aggregazione..." "INFO"     # Orchestrator riutilizza sempre una singola cartella (LargeScaleMode) per evitare un gonfiore del disco     # Gli amministratori che eseguono l'aggregatore ottengono manualmente cartelle con timestamp per snapshot point-in-time     $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current"     # Controllare l'aggiornamento dei dati prima di aggregare     $freshness = $AggregationInputPath Get-DataFreshness -JsonPath     Write-Log "Aggiornamento dei dati: $($freshness. FreshFiles)/$($freshness. TotalFiles) dispositivi segnalati negli ultimi 24h" "INFO"     se ($freshness. Avviso) {         Write-Log $freshness. Avviso "WARN"     }     $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1"     $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json"     $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     if (Test-Path $aggregateScript) {         if (-$DryRun) {             # L'agente di orchestrazione usa sempre lo streaming + incrementale per l'efficienza             # Aggregator eleva automaticamente a PS7 se disponibile per prestazioni ottimali             $aggregateParams = @{                 InputPath = $AggregationInputPath                 OutputPath = $aggregationPath                 StreamingMode = $true                 IncrementalMode = $true                 SkipReportIfUnchanged = $true                 ParallelThreads = 8             }             # Pass rollout summary if it exists (for velocity/projection data)             if (Test-Path $rolloutSummaryPath) {                 $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath             }             & $aggregateScript @aggregateParams             # Mostra comando per generare dashboard HTML completo con tabelle di dispositivi             Write-Host ""             Write-Host "Per generare dashboard HTML completo con tabelle produttore/modello, eseguire:" -ForegroundColor Yellow             Write-Host " $aggregateScript -InputPath '"$AggregationInputPath'" -OutputPath '"$aggregationPath'"" -ForegroundColor yellow             Write-Host ""         } else {             Write-Log "[DRYRUN] Eseguire l'aggregazione" "INFO"             # In DryRun usare direttamente i dati di aggregazione esistenti da ReportBasePath             $aggregationPath = $ReportBasePath         }     }     $rolloutState.LastAggregation = Get-Date -Format "yyyy-MM-dd HH:mm:ss"     # Passaggio 2: Caricare lo stato corrente del dispositivo     Write-Log "Passaggio 2: Caricamento dello stato del dispositivo in corso..." "INFO"     $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue |          Where-Object { $_. Nome -notlike "*Buckets*" } |          Sort-Object LastWriteTime -Descending |          Select-Object -Primo 1     if (-not $notUptodateCsv -and -not $DryRun) {         Write-Log "Nessun dato di aggregazione trovato.                                            In attesa..." "AVVISO"         Start-Sleep -Seconds ($PollIntervalMinutes * 60)         Continuare     }     $notUpdatedDevices = if ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } else { @() }     Write-Log "Dispositivi non aggiornati: $($notUpdatedDevices.Count)" "INFO"     $notUpdatedIndexes = Get-NotUpdatedIndexes -Dispositivi $notUpdatedDevices     # Passaggio 3: Aggiornare la cronologia dei dispositivi (monitoraggio per nome host)     Write-Log "Passaggio 3: Aggiornamento della cronologia dispositivi..." "INFO"     Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory     Save-DeviceHistory -$deviceHistory Cronologia     # Passaggio 4: Verificare la presenza di contenitori bloccati (dispositivi non raggiungibili)     $existingBlockedCount = $blockedBuckets.Count     Write-Log "Passaggio 4: Controllo della presenza di bucket bloccati (ping ai dispositivi oltre il periodo di attesa)..." "INFO"     if ($existingBlockedCount -gt 0) {         Write-Log "Bucket attualmente bloccati da esecuzioni precedenti: $existingBlockedCount" "INFO"     }     if ($adminApproved.Count -gt 0) {         Write-Log "bucket approvati Amministrazione (non verranno bloccati nuovamente): $($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 bloccati         Write-Log "Nuovi bucket bloccati (questa iterazione): $($newlyBlocked.Count)" "BLOCCATO"     }     # Passaggio 4b: sbloccare automaticamente i contenitori in cui i dispositivi sono stati aggiornati     $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize     if ($autoUnblocked.Count -gt 0) {         Save-BlockedBuckets - $blockedBuckets bloccati         Write-Log "Bucket sbloccati automaticamente (dispositivi aggiornati): $($autoUnblocked.Count)" "OK"     }     # Passaggio 5: Calcolare i dispositivi idonei rimanenti     $eligibleCount = 0     foreach ($device in $notUpdatedDevices) {         $bucketKey = Get-BucketKey $device         if (-not $blockedBuckets.Contains($bucketKey)) {             $eligibleCount++         }     }     Write-Log "Dispositivi idonei rimanenti: $eligibleCount" "INFO"     Write-Log "Bucket bloccati: $($blockedBuckets.Count)" "INFO"     # Passaggio 6: Verificare il completamento     if ($eligibleCount -eq 0) {         Write-Log "ROLLOUT COMPLETATO - Tutti i dispositivi idonei aggiornati!" "OK"         $rolloutState.Status = "Completato"         $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"         Save-RolloutState -State $rolloutState         Pausa     }     # Passaggio 6: Generare e distribuire la fase successiva     Write-Log "Passaggio 6: Generazione onda di implementazione..." "INFO"     $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames     # Controlla se abbiamo dispositivi da distribuire ($waveDevices potrebbero essere $null, vuoti o con dispositivi effettivi)     $hasDevices = $waveDevices -and @($waveDevices | Where-Object { $_ }). Conteggio -gt 0     if ($hasDevices) {         # Incrementa il numero d'onda solo quando abbiamo dispositivi da distribuire         $rolloutState.CurrentWave++         Write-Log "Wave $($rolloutState.CurrentWave): $(@($waveDevices). Count) dispositivi" "WAVE"         # Distribuire l'oggetto Criteri di gruppo con la funzione inline         $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $hostnames = @($waveDevices | ForEach-Object {             if ($_. Hostname) { $_. Hostname } elseif ($_. HostName) { $_. HostName } else { $null }         } | Where-Object { $_ })         # Save hostnames file for reference/audit         $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt"         $hostnames | Out-File $hostnamesFile - Codifica UTF8         # Verificare che siano presenti nomi host da distribuire in         se ($hostnames. Conteggio -eq 0) {             Write-Log "Nessun nome host valido trovato nell'onda $($rolloutState.CurrentWave) - I dispositivi potrebbero non avere la proprietà Hostname" "WARN"             Write-Log "Omissione della distribuzione per questa fase - controllare i dati del dispositivo" "WARN"             # Attendi ancora prima dell'iterazione successiva             if (-non $DryRun) {                 Write-Log "Dormire per $PollIntervalMinutes minuti prima di riprovare..." "INFO"                 Start-Sleep -Seconds ($PollIntervalMinutes * 60)             }             Continuare         }         Write-Log "Distribuzione a $($hostnames. Count) hostnames in Wave $($rolloutState.CurrentWave)" "INFO"         # Distribuisci usando il metodo WinCS o GPO basato sul parametro -UseWinCS         if ($UseWinCS) {             # Metodo WinCS: creare un oggetto Criteri di gruppo con un'attività pianificata per l'esecuzione WinCsFlags.exe come SISTEMA in ogni endpoint             Write-Log "Using WinCS deployment method (Key: $WinCSKey)" "WAVE"             $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames '                 -WinCSKey $WinCSKey '                 -WavePrefix $WavePrefix '                 -WaveNumber $rolloutState.CurrentWave '                 -TargetOU $TargetOU '                 -DryRun:$DryRun             if (-not $wincsResult.Success) {                 Write-Log "Distribuzione WinCS con errori - Applicata: $($wincsResult.Applied), Failed: $($wincsResult.Failed)" "WARN"             } else {                 Write-Log "Distribuzione WinCS riuscita - Applicata: $($wincsResult.Applied), Ignorata: $($wincsResult.Skipped)" "OK"             }             # Salvare i risultati winCS per il controllo             $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json"             $wincsResult | ConvertTo-Json -Profondità 5 | Out-File $wincsResultFile - Codifica UTF8         } else {             # Metodo oggetto Criteri di gruppo: creare un oggetto Criteri di gruppo con l'impostazione del Registro di sistema AvailableUpdatesPolicy             $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun             if (-$gpoResult) {                 Write-Log "Distribuzione dell'oggetto Criteri di gruppo non riuscita - eseguirà di nuovo l'iterazione successiva" "ERRORE"             }         }         # Onda record in stato         $waveRecord = @{             WaveNumber = $rolloutState.CurrentWave             StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss"             DeviceCount = @($waveDevices). Conteggio             Dispositivi = @($waveDevices | ForEach-Object {                 @{                     Hostname = if ($_. Hostname) { $_. Hostname } elseif ($_. HostName) { $_. HostName } else { $null }                     BucketKey = Get-BucketKey $_                 }             })         }         # Assicurarsi che WaveHistory sia sempre una matrice prima dell'accodamento (impedisce problemi di unione hashtable)         $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord)         $rolloutState.TotalDevicesTargeted += @($waveDevices). Conteggio         Save-RolloutState -State $rolloutState         Write-Log "Wave $($rolloutState.CurrentWave) distribuito.                                                                                                                                                                                        In attesa di $PollIntervalMinutes minuti..." "OK"     } else {         # Mostra lo stato dei dispositivi distribuiti in attesa di aggiornamenti         Write-Log "" "INFO"         Write-Log "========== TUTTI I DISPOSITIVI DISTRIBUITI - IN ATTESA DI ========== DI STATO" "INFO"                  # Scarica tutti i dispositivi distribuiti dalla cronologia delle onde         $allDeployedLookup = @{}         foreach ($wave in $rolloutState.WaveHistory) {             foreach ($device in $wave. Dispositivi) {                 se ($device. Hostname) {                     $allDeployedLookup[$device. Hostname] = @{                         Hostname = $device. Hostname                         BucketKey = $device. BucketKey                         DeployedAt = $wave. StartedAt                         WaveNumber = $wave. WaveNumber                     }                 }             }         }         $allDeployedDevices = @($allDeployedLookup.Valori)                  if ($allDeployedDevices.Count -gt 0) {             # Individuare i dispositivi distribuiti ancora in sospeso (nell'elenco NotUpdated)             $stillPendingCount = 0             $noLongerPendingCount = 0             $pendingSample = @()             foreach ($deployed in $allDeployedDevices) {                 if ($notUpdatedIndexes.HostSet.Contains($deployed. Hostname)) {                     $stillPendingCount++                     if ($pendingSample.Count -lt $DeviceLogSampleSize) {                         $pendingSample += $deployed. Hostname                     }                 } else {                     $noLongerPendingCount++                 }             }                          # Ottenere i conteggi aggiornati effettivi dall'aggregazione - differenziare l'evento 1808 rispetto a UEFICA2023Status             $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Summary*.csv" |                  Sort-Object LastWriteTime -Descending | Select-Object -Primo 1             $actualUpdated = 0             $totalDevicesFromSummary = 0             $event 1808Count = 0             $uefiStatusUpdated = 0             $needsRebootSample = @()                          if ($summaryCsv) {                 $summary = Import-Csv $summaryCsv.FullName | Select-Object -Primo 1                 se ($summary. Aggiornato) { $actualUpdated = [int]$summary. Aggiornato }                 se ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices }             }                          # Calcola la velocità dalla cronologia delle onde (dispositivi aggiornati al giorno)             $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                 }             }                          # Salvare il riepilogo dell'implementazione con proiezioni basate sul fine settimana             # Usare il numero di dati NotUptodate dell'aggregatore (esclude i dispositivi SB OFF) per garantire la coerenza             $notUpdatedCount = if ($summary -and $summary. NotUptodate) { [int]$summary. NotUptodate } else { $totalDevicesFromSummary - $actualUpdated }             Save-RolloutSummary -State $rolloutState '                 -TotalDevices $totalDevicesFromSummary '                 -UpdatedDevices $actualUpdated '                 -NotUpdatedDevices $notUpdatedCount '                 -DevicesPerDay $devicesPerDay                          # Controllare i dati non elaborati per i dispositivi con UEFICA2023Status=Updated ma nessun evento 1808 (è necessario riavviare)             $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue             $totalDataFiles = @($dataFiles). Conteggio             $batchSize = [Matematica]::Max(500, $ProcessingBatchSize)             if ($LargeScaleMode) {                 $batchSize = [Matematica]::Max(2000, $ProcessingBatchSize)             }

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

                    foreach ($file in $batchFiles) {                         prova {                             $deviceData = Get-Content $file. FullName -Raw | ConvertFrom-Json                             $hostname = $deviceData.Nome Host                             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                                 }                             }                         } cattura { }                     }                                                          

                    Save-ProcessingCheckpoint -Stage "RebootStatusScan" -Processed ($end + 1) -Total $totalDataFiles -Metrics @{                         Event1808Count = $event 1808Count                         UEFIUpdatedAwaitingReboot = $uefiStatusUpdated                     }                 }             }             Write-Log "Totale distribuito: $($allDeployedDevices.Count)" "INFO"             Write-Log "Aggiornato (evento 1808 confermato): $event 1808Count" "OK"             if ($uefiStatusUpdated -gt 0) {                 Write-Log "Aggiornato (UEFICA2023Status=Updated, in attesa di riavvio): $uefiStatusUpdated" "OK"                 $rebootSuffix = if ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) altro)" } else { "" }                 Write-Log " Dispositivi che devono essere riavviato per l'evento 1808 (esempio): $($needsRebootSample -join ', ')$rebootSuffix" "INFO"                 Write-Log " Questi dispositivi segnalano l'evento 1808 dopo il prossimo riavvio" "INFO"             }             Write-Log "Non più in sospeso: $noLongerPendingCount (include SecureBoot OFF, dispositivi mancanti)" "INFO"             Write-Log "In attesa di stato: $stillPendingCount" "INFO"             if ($stillPendingCount -gt 0) {                 $pendingSuffix = if ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) altro)" } else { "" }                 Write-Log "Dispositivi in sospeso (esempio): $($pendingSample -join ', ')$pendingSuffix" "WARN"             }         } else {             Write-Log "Nessun dispositivo è stato ancora distribuito" "INFO"         }         Write-Log "================================================================" "INFO"         Write-Log "" "INFO"     }     # Attendi prima dell'iterazione successiva     if (-$DryRun) {         Write-Log "Dormire per $PollIntervalMinutes minuti..." "INFO"         Start-Sleep -Seconds ($PollIntervalMinutes * 60)     } else {         Write-Log "[DRYRUN] Attendi $PollIntervalMinutes minuti" "INFO"         break # Exit after one iteration in dry run     } }                               

# ============================================================================ # RIEPILOGO FINALE # ============================================================================

Write-Host "" Write-Host ("=" * 80) -ForegroundColor green Write-Host " ROLLOUT ORCHESTRATOR SUMMARY" -ForegroundColor green Write-Host ("=" * 80) -ForegroundColor green Write-Host ""

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

Write-Host "Status:              $($finalState.Status)" -ForegroundColor $(if ($finalState.Status -eq "Completed") { "Green" } else { "Yellow" }) Write-Host "Totale onde: $($finalState.CurrentWave)" Write-Host "Dispositivi assegnati: $($finalState.TotalDevicesTargeted)" Write-Host "Bucket bloccati: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } else { "Green" }) Write-Host "Dispositivi monitorati: $($deviceHistory.Count)" -ForegroundColor gray Write-Host ""

if ($finalBlocked.Count -gt 0) {     Write-Host "BUCKET BLOCCATI (richiede revisione manuale):" -ForegroundColor red     foreach ($key in $finalBlocked.Keys) {         $info = $finalBlocked[$key]         Write-Host " - $key" -ForegroundColor red         Write-Host " Motivo: $($info. Reason)" -ForegroundColor Gray     }     Write-Host ""     Write-Host "File bucket bloccati: $blockedBucketsPath" -ForegroundColor yellow }

Write-Host "" Write-Host "File di stato:" -ForegroundColor Ciano Write-Host " Stato implementazione: $rolloutStatePath" Write-Host " Bucket bloccati: $blockedBucketsPath" Write-Host "Cronologia dispositivi: $deviceHistoryPath" Write-Host ""  

​​​​​​​

Serve aiuto?

Vuoi altre opzioni?

Esplorare i vantaggi dell'abbonamento e i corsi di formazione, scoprire come proteggere il dispositivo e molto altro ancora.