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