Kopieren Sie dieses Beispielskript, fügen Sie es ein, und ändern Sie es nach Bedarf für Ihre Umgebung:

<# . SYNOPSIS     Orchestrator für continuous Secure Boot-Rollouts, der ausgeführt wird, bis die Bereitstellung abgeschlossen ist.

.DESCRIPTION     Dieses Skript bietet eine vollständige End-to-End-Automatisierung für den Rollout von Zertifikaten für den sicheren Start:     1.      Generiert Rolloutwellen basierend auf Aggregationsdaten     2. Erstellt AD-Gruppen und Gruppenrichtlinienobjekt für jede Welle     3. Monitore für Geräteupdates (Ereignis 1808)     4. Erkennt blockierte Buckets (nicht erreichbare Geräte)     5. Automatischer Fortschritt zur nächsten Welle     6. Wird ausgeführt, bis ALLE berechtigten Geräte aktualisiert      wurden.     Abschlusskriterien:     - Keine Geräte verbleiben in: Aktion erforderlich, hohe Zuverlässigkeit, Beobachtung, vorübergehend angehalten     - Außerhalb des Bereichs (entwurfsbedingt): Nicht unterstützt, Sicherer Start deaktiviert     - Wird bis zum Abschluss kontinuierlich ausgeführt – kein beliebiges Wellenlimit          Rolloutstrategie:     - HOHE ZUVERLÄSSIGKEIT: Alle Geräte in der ersten Welle (sicher)     - AKTION ERFORDERLICH: Progressive Doppel (1→2→4→8...)          Blockierende Logik:     – Nach MaxWaitHours pingt Orchestrator Geräte, die nicht aktualisiert wurden     – Wenn das Gerät NICHT ERREICHBAR ist (Pingfehler), ist → Bucket zur Untersuchung BLOCKIERT.     – Wenn das Gerät ERREICHBAR ist, aber nicht aktualisiert → warten (muss möglicherweise neu gestartet werden)     – Blockierte Buckets werden ausgeschlossen, bis der Administrator sie entsperrt.          Automatisches Aufheben der Blockierung:     – Wenn ein Gerät in einem blockierten Bucket später als aktualisiert angezeigt wird (Ereignis 1808),       Die Blockierung des Buckets wird automatisch aufgehoben, und der Rollout wird fortgesetzt.     – Damit werden Geräte behandelt, die vorübergehend offline waren, aber wieder zurückkamen     .     Gerätenachverfolgung:     – Verfolgt Geräte nach Hostnamen nach (es wird davon ausgegangen, dass sich Die Namen während des Rollouts nicht ändern)     - Hinweis: Die JSON-Sammlung enthält keine eindeutige Computer-ID. Hinzufügen eines für eine bessere Nachverfolgung

.PARAMETER AggregationInputPath     Pfad zu unformatierten JSON-Gerätedaten (aus Detect script)

.PARAMETER ReportBasePath     Basispfad für Aggregationsberichte

.PARAMETER TargetOU     Distinguished Name der Organisationseinheit zum Verknüpfen von Gruppenrichtlinienobjekten.Optional: Falls nicht angegeben, wird das Gruppenrichtlinienobjekt zur domänenweiten Abdeckung mit dem Domänenstamm verknüpft.Durch das Filtern von Sicherheitsgruppen wird sichergestellt, dass nur Zielgeräte die Richtlinie erhalten.

.PARAMETER MaxWaitHours     Warten Sie stundenlang, bis Geräte aktualisiert wurden, bevor Sie die Erreichbarkeit überprüfen.Nach diesem Zeitpunkt werden Geräte, die nicht aktualisiert wurden, pingt.Nicht erreichbare Geräte führen dazu, dass der Bucket blockiert wird.Standard: 72 (3 Tage)

.PARAMETER PollIntervalMinutes     Minuten zwischen status Überprüfungen. Standard: 1440 (1 Tag)

.PARAMETER AllowListPath     Pfad zu einer Datei, die Hostnamen enthält, die für den Rollout (gezieltes Rollout) zulässig sind.Unterstützt .txt (ein Hostname pro Zeile) oder .csv (mit der Spalte Hostname/Computername/Name).Wenn angegeben, werden NUR diese Geräte in den Rollout einbezogen.BlockList wird nach AllowList weiterhin angewendet.

.PARAMETER AllowADGroup     Name einer AD-Sicherheitsgruppe, die Computerkonten enthält, die zugelassen werden sollen.Beispiel: "SecureBoot-Pilot-Computers" oder "Wave1-Devices"     Wenn angegeben, werden NUR Geräte in dieser Gruppe in den Rollout einbezogen.Kombinieren Mit AllowListPath für datei- und AD-basiertes Targeting.

.PARAMETER ExclusionListPath     Pfad zu einer Datei mit Hostnamen, die vom Rollout ausgeschlossen werden sollen (VIP/Executive-Geräte).Unterstützt .txt (ein Hostname pro Zeile) oder .csv (mit der Spalte Hostname/Computername/Name).Diese Geräte werden in keiner Rollout-Welle enthalten sein.BlockList wird nach der AllowList-Filterung angewendet.     . PARAMETER ExcludeADGroup     Name einer AD-Sicherheitsgruppe, die auszuschließende Computerkonten enthält.Beispiel: "VIP-Computer" oder "Executive-Devices"     Kombination mit ExclusionListPath für datei- und AD-basierte Ausschlüsse.

.PARAMETER UseWinCS     Verwenden Sie WinCS (Windows Configuration System) anstelle von GPO/AvailableUpdatesPolicy.WinCS stellt die Aktivierung des sicheren Starts bereit, indem WinCsFlags.exe direkt auf jedem Endpunkt ausgeführt wird.WinCsFlags.exe wird im SYSTEM-Kontext über einen geplanten Task ausgeführt.Diese Methode ist nützlich für:     - Schnellere Rollouts (sofortige Auswirkung im Vergleich zum Warten auf die GPO-Verarbeitung)     – Nicht in die Domäne eingebundene Geräte     - Umgebungen ohne AD/GPO-Infrastruktur     Referenz: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe

.PARAMETER WinCSKey     Der WinCS-Schlüssel, der für die Aktivierung des sicheren Starts verwendet werden soll.Standard: F33E0C8E002     Dieser Schlüssel entspricht der Rolloutkonfiguration für den sicheren Start.     . PARAMETER DryRun     Zeigen Sie, was ohne Änderungen ausgeführt werden würde.

.PARAMETER ListBlockedBuckets     Anzeigen aller derzeit blockierten Buckets und Beenden

.PARAMETER UnblockBucket     Aufheben der Blockierung eines bestimmten Buckets nach Schlüssel und Beenden

.PARAMETER UnblockAll     Aufheben der Blockierung aller Buckets und Beenden

.PARAMETER EnableTaskOnDisabled     Stellen Sie Enable-SecureBootUpdateTask.ps1 auf allen Geräten mit deaktivierter geplanter Aufgabe bereit.Erstellt ein Gruppenrichtlinienobjekt mit einer einmaligen geplanten Aufgabe, die die Option Skript mit -Quiet aktivieren ausführt.Dies ist nützlich, um Geräte zu beheben, auf denen der Task "Secure-Boot-Update" deaktiviert ist.

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

.EXAMPLE     # Blockierte Buckets auflisten     .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets

.EXAMPLE     # Entsperren eines bestimmten Buckets     .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -UnblockBucket "Dell_Latitude5520_BIOS1.2.3"

.EXAMPLE     # Ausschließen von VIP-Geräten vom Rollout mithilfe einer Textdatei     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports" '         -ExclusionListPath "C:\Admin\VIP-Devices.txt"

.EXAMPLE     # Ausschließen von Geräten in einer AD-Sicherheitsgruppe (z. B. Laptops für Führungskräfte)     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports" '         -ExcludeADGroup "VIP-Computers"

.EXAMPLE     # Verwenden Sie WinCS (Windows Configuration System) anstelle von GPO/AvailableUpdatesPolicy.     # WinCsFlags.exe wird über eine geplante Aufgabe auf jedem Endpunkt unter systemkontext ausgeführt.     .\Start-SecureBootRolloutOrchestrator.ps1 '         -AggregationInputPath "\\server\SecureBootLogs$\Json" '         -ReportBasePath "E:\SecureBootReports" '         -UseWinCS '         -WinCSKey "F33E0C8E002" #>

[CmdletBinding()] param(     [Parameter(Mandatory = $false)]     [Zeichenfolge]$AggregationInputPath,     [Parameter(Mandatory = $false)]     [Zeichenfolge]$ReportBasePath,     [Parameter(Mandatory = $false)]     [Zeichenfolge]$TargetOU,     [Parameter(Mandatory = $false)]     [string]$WavePrefix = "SecureBoot-Rollout",     [Parameter(Mandatory = $false)]     [int]$MaxWaitHours = 72,     [Parameter(Mandatory = $false)]     [int]$PollIntervalMinutes = 1440,                         

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

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

    [Parameter(Mandatory = $false)]     [Switch]$LargeScaleMode,     # ============================================================================     # AllowList-/BlockList-Parameter     # ============================================================================     # AllowList = Nur diese Geräte einschließen (gezieltes Rollout)     # BlockList = Diese Geräte ausschließen (sie werden nie eingeführt)     # Verarbeitungsreihenfolge: Zuerst AllowList (sofern angegeben), dann BlockList     [Parameter(Mandatory = $false)]     [Zeichenfolge]$AllowListPath,     [Parameter(Mandatory = $false)]     [Zeichenfolge]$AllowADGroup,     [Parameter(Mandatory = $false)]     [Zeichenfolge]$ExclusionListPath,     [Parameter(Mandatory = $false)]     [Zeichenfolge]$ExcludeADGroup,     # ============================================================================     # WinCS-Parameter (Windows-Konfigurationssystem)     # ============================================================================     # WinCS ist eine Alternative zur AvailableUpdatesPolicy-GPO-Bereitstellung.                              # Es verwendet WinCsFlags.exe auf jedem Endpunkt, um den Rollout des sicheren Starts zu aktivieren.# WinCsFlags.exe wird im SYSTEM-Kontext auf dem Endpunkt ausgeführt.# Referenz: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe          [Parameter(Mandatory = $false)]     [switch]$UseWinCS,          [Parameter(Mandatory = $false)]     [string]$WinCSKey = "F33E0C8E002",          [Parameter(Mandatory = $false)]     [switch]$DryRun,          [Parameter(Mandatory = $false)]     [switch]$ListBlockedBuckets,          [Parameter(Mandatory = $false)]     [Zeichenfolge]$UnblockBucket,          [Parameter(Mandatory = $false)]     [switch]$UnblockAll,          [Parameter(Mandatory = $false)]     [Switch]$EnableTaskOnDisabled )

$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Beispiele für Bereitstellung und Überwachung"

# ============================================================================ # ABHÄNGIGKEITSÜBERPRÜFUNG # ============================================================================

function Test-ScriptDependencies {     param(         [Parameter(Mandatory = $true)]         [Zeichenfolge]$ScriptDirectory,         [Parameter(Mandatory = $true)]         [string[]]$RequiredScripts     )     $missingScripts = @()     foreach ($script in $RequiredScripts) {         $scriptPath = Join-Path $ScriptDirectory $script         if (-not (Testpfad $scriptPath)) {             $missingScripts += $script         }     }     if ($missingScripts.Count -gt 0) {         Write-Host ""         Write-Host ("=" * 70) -ForegroundColor Red         Write-Host "MISSING DEPENDENCIES" -ForegroundColor Red         Write-Host ("=" * 70) -ForegroundColor Red         Write-Host ""         Write-Host "Die folgenden erforderlichen Skripts wurden nicht gefunden:" -ForegroundColor Yellow         foreach ($script in $missingScripts) {             Write-Host " - $script" -ForegroundColor White         }         Write-Host ""         Write-Host "Bitte laden Sie die neuesten Skripts von herunter: " -ForegroundColor Cyan         Write-Host " URL: $DownloadUrl" -ForegroundColor White         Write-Host "Navigieren Sie zu: '$DownloadSubPage'" -ForegroundColor White         Write-Host ""         Write-Host "Extrahieren Sie alle Skripts in dasselbe Verzeichnis, und führen Sie sie erneut aus." -ForegroundColor Yellow         Write-Host ""         $false zurückgeben     }     $true zurückgeben }                             

# 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)) {     Ausgang 1 }

# ============================================================================ # PARAMETERÜBERPRÜFUNG # ============================================================================

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

if (-not $ReportBasePath) {     Write-Host "ERROR: -ReportBasePath is required." -ForegroundColor Red     Ausgang 1 }

if (-not $isAdminCommand -and -not $AggregationInputPath) {     Write-Host "ERROR: -AggregationInputPath is required for rollout (not required for -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red     Ausgang 1 }

# ============================================================================ # GPO-ERKENNUNG – ÜBERPRÜFEN AUF ERKENNUNG GPO # ============================================================================

if (-not $isAdminCommand -and -not $DryRun) {     $CollectionGPOName = "SecureBoot-EventCollection"     # Überprüfen, ob das GroupPolicy-Modul verfügbar ist     if (Get-Module -ListAvailable -Name GroupPolicy) {         Import-Module GroupPolicy -ErrorAction SilentlyContinue         Write-Host "Überprüfung auf Erkennungs-GPO..." -ForegroundColor Yellow         try {             # Überprüfen, ob das Gruppenrichtlinienobjekt vorhanden ist             $existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue             if ($existingGpo) {                 Write-Host "Erkennungs-GPO gefunden: $CollectionGPOName" -ForegroundColor Green             } else {                 Write-Host ""                 Write-Host ("=" * 70) -ForegroundColor Yellow                 Write-Host "WARNING: DETECTION GPO NOT FOUND" -ForegroundColor Yellow                 Write-Host ("=" * 70) -ForegroundColor Yellow                 Write-Host ""                 Write-Host "Das Erkennungs-GPO '$CollectionGPOName' wurde nicht gefunden." -ForegroundColor Yellow                 Write-Host "Ohne dieses Gruppenrichtlinienobjekt werden keine Gerätedaten gesammelt." -ForegroundColor Yellow                 Write-Host ""                 Write-Host "So stellen Sie das Erkennungs-GPO bereit: " -ForegroundColor Cyan                 Write-Host ".\Deploy-GPO-SecureBootCollection.ps1 -DomainName <domain> -AutoDetectOU" -ForegroundColor White                 Write-Host ""                 Write-Host "Trotzdem fortfahren?                                     (Y/N)" -ForegroundColor Yellow                 $response = Read-Host                 if ($response -notmatch '^[Yy]') {                     Write-Host "Abbruch. Stellen Sie zuerst das Erkennungs-GPO bereit." -ForegroundColor Red                     Ausgang 1                 }             }         } catch {             Write-Host " GPO kann nicht überprüft werden: $($_. Exception.Message)" -ForegroundColor Yellow         }     } else {         Write-Host " GroupPolicy-Modul nicht verfügbar – GPO-Überprüfung übersprungen" -ForegroundColor Gray     }     Write-Host "" }

# ============================================================================ # ZUSTANDSDATEIPFADE # ============================================================================

$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 # ============================================================================ # ConvertFrom-Json -AsHashtable ist nur PS7 und höher. Dies sorgt für Kompatibilität.

function ConvertTo-Hashtable {     param(         [Parameter(ValueFromPipeline = $true)]         $InputObject     )     process {         if ($null -eq $InputObject) { return @{} }         if ($InputObject -is [System.Collections.IDictionary]) { return $InputObject }         if ($InputObject -is [PSCustomObject]) {             # Verwenden Sie [ordered] für konsistente Schlüsselreihenfolge und sichere Duplikatverarbeitung.             $hash = [ordered]@{}             foreach ($prop in $InputObject.PSObject.Properties) {                 # Indizierte Zuweisung behandelt Duplikate sicher durch Überschreiben                 $hash[$prop. Name] = ConvertTo-Hashtable $prop. Wert             }             $hash zurückgeben         }         if ($InputObject -is [System.Collections.IEnumerable] -und $InputObject -isnot [string]) {             return @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ })         }         $InputObject zurückgeben     } }

# ============================================================================ # ADMIN-BEFEHLE: Auflisten/Aufheben der Blockierung von Buckets # ============================================================================

if ($ListBlockedBuckets) {     Write-Host ""     Write-Host ("=" * 80) -ForegroundColor Yellow     Write-Host "BLOCKED BUCKETS" -ForegroundColor Yellow     Write-Host ("=" * 80) -ForegroundColor Yellow     Write-Host ""     if (Testpfad $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         if ($blocked. Count -eq 0) {             Write-Host "Keine blockierten Buckets". -ForegroundColor Green         } else {             Write-Host "Total blocked: $($blocked. Count)" -ForegroundColor Red             Write-Host ""             foreach ($key in $blocked. Schlüssel) {                 $info = $blocked[$key]                 Write-Host "Bucket: $key" -ForegroundColor Red                 Write-Host " Blockiert bei: $($info. BlockedAt)" -ForegroundColor Gray                 Write-Host " Grund: $($info. Reason)" -ForegroundColor Gray                 Write-Host " Fehlerhaftes Gerät: $($info. FailedDevice)" -ForegroundColor Gray                 Write-Host " Zuletzt gemeldet: $($info. LastReported)" -ForegroundColor Gray                 Write-Host " Wave: $($info. WaveNumber)" -ForegroundColor Gray                 Write-Host " Geräte im Bucket: $($info. DevicesInBucket)" -ForegroundColor Gray                 Write-Host ""             }             Write-Host "So entsperren Sie einen Bucket:"             Write-Host ".\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Cyan             Write-Host ""             Write-Host "So entsperren Sie alle:"             Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Cyan         }     } else {         Write-Host "Keine blockierte Bucketdatei gefunden" -ForegroundColor Green     }     Write-Host ""     Exit 0 }     

if ($UnblockBucket) {     Write-Host ""     if (Testpfad $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         if ($blocked. Contains($UnblockBucket)) {             $blocked. Remove($UnblockBucket)             $blocked | ConvertTo-Json -Tiefe 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force             # Zur vom Administrator genehmigten Liste hinzufügen, um eine erneute Blockierung zu verhindern             $adminApproved = @{}             if (Testpfad $adminApprovedPath) {                 $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable             }             $adminApproved[$UnblockBucket] = @{                 ApprovedAt = Get-Date -Format "yyyy-MM-tt HH:mm:ss"                 ApprovedBy = $env:USERNAME             }             $adminApproved | ConvertTo-Json -Tiefe 10 | Out-File $adminApprovedPath -Encoding UTF8 -Force             Write-Host "Unblocked bucket: $UnblockBucket" -ForegroundColor Green             Write-Host "Der vom Administrator genehmigten Liste hinzugefügt (wird nicht automatisch erneut blockiert)" -ForegroundColor Cyan         } else {             Write-Host "Bucket nicht gefunden: $UnblockBucket" -ForegroundColor Yellow             Write-Host "Verfügbare Buckets:" -ForegroundColor Gray             $blocked. Schlüssel | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }         }     } else {         Write-Host "Keine blockierte Bucketdatei gefunden." -ForegroundColor Yellow     }     Write-Host ""     Exit 0 }                          

if ($UnblockAll) {     Write-Host ""     if (Testpfad $blockedBucketsPath) {         $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable         $count = $blocked. Count         @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Encoding UTF8 -Force         Write-Host "Blockierung aller $count Buckets aufgehoben" -ForegroundColor Green     } else {         Write-Host "Keine blockierte Bucketdatei gefunden." -ForegroundColor Yellow     }     Write-Host ""     Exit 0 }

# ============================================================================ # HILFSFUNKTIONEN # ============================================================================

function Get-RolloutState {     if (Testpfad $rolloutStatePath) {         try {             $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable             # Überprüfen, ob erforderliche Eigenschaften vorhanden sind             if ($null -eq $loaded. CurrentWave) {                 "Ungültige Zustandsdatei – CurrentWave fehlt"             }             # Sicherstellen, dass WaveHistory immer ein Array ist (behebt die PS5.1-JSON-Deserialisierung)             if ($null -eq $loaded. WaveHistory) {                 $loaded. WaveHistory = @()             } elseif ($loaded. WaveHistory -isnot [Array]) {                 $loaded. WaveHistory = @($loaded. WaveHistory)             }             $loaded zurückgeben         } catch {             Write-Log "Beschädigte RolloutState.json erkannt: $($_. Exception.Message)" "WARN"             Write-Log "Beschädigte Datei sichern und neu starten" "WARN"             $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyyMMdd-HHmmss')"             Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue         }     }     return @{         CurrentWave = 0         StartedAt = $null         LastAggregation = $null         TotalDevicesTargeted = 0         TotalDevicesUpdated = 0         Status = "NotStarted"         WaveHistory = @()     } }

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

function Get-WeekdayProjection {     <#     . SYNOPSIS         Berechnen des voraussichtlichen Abschlussdatums für Wochenenden (kein Fortschritt am Sa/So)     #>     param(         [int]$RemainingDevices,         [double]$DevicesPerDay,         [datetime]$StartDate = (Get-Date)     )     if ($DevicesPerDay -le 0 -or $RemainingDevices -le 0) {         return @{             ProjectedDate = $null             WorkingDaysNeeded = 0             CalendarDaysNeeded = 0         }     }     # Berechnen erforderlicher Arbeitstage (ohne Wochenenden)     $workingDaysNeeded = [math]::Ceiling($RemainingDevices / $DevicesPerDay)     # Konvertieren von Arbeitstagen in Kalendertage (Hinzufügen von Wochenenden)     $currentDate = $StartDate.Date     $daysAdded = 0     $workingDaysAdded = 0     while ($workingDaysAdded -lt $workingDaysNeeded) {         $currentDate = $currentDate.AddDays(1)         $daysAdded++         # Nur Wochentage zählen         if ($currentDate.DayOfWeek -ne [DayOfWeek]::Saturday -and             $currentDate.DayOfWeek -ne [DayOfWeek]::Sunday) {             $workingDaysAdded++         }     }     return @{         ProjectedDate = $currentDate.ToString("jjjj-MM-tt")         WorkingDaysNeeded = $workingDaysNeeded         CalendarDaysNeeded = $daysAdded     } }                                  

function Save-RolloutSummary {     <#     . SYNOPSIS         Speichern der Rolloutzusammenfassung mit Projektionsinformationen für Dashboard Anzeige     #>     param(         [Hashtabelle]$State,         [int]$TotalDevices,         [int]$UpdatedDevices,         [int]$NotUpdatedDevices,         [double]$DevicesPerDay     )     $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     # Berechnen der Projektion mit Berücksichtigung von Wochenenden     $projection = Get-WeekdayProjection -RemainingDevices $NotUpdatedDevices -DevicesPerDay $DevicesPerDay     $summary = @{         GeneratedAt = (Get-Date -Format "yyyy-MM-tt HH:mm:ss")         RolloutStartDate = $State.StartedAt         LastAggregation = $State.LastAggregation         CurrentWave = $State.CurrentWave         Status = $State.Status         # Geräteanzahl         TotalDevices = $TotalDevices         UpdatedDevices = $UpdatedDevices         NotUpdatedDevices = $NotUpdatedDevices         PercentUpdated = if ($TotalDevices -gt 0) { [math]::Round(($UpdatedDevices / $TotalDevices) * 100, 1) } else { 0 }         # Geschwindigkeitsmetriken         DevicesPerDay = [math]::Round($DevicesPerDay, 1)         TotalDevicesTargeted = $State.TotalDevicesTargeted         TotalWaves = $State.CurrentWave         # Weekend-fähige Projektion         ProjectedCompletionDate = $projection. ProjectedDate         WorkingDaysRemaining = $projection. WorkingDaysNeeded         CalendarDaysRemaining = $projection. CalendarDaysNeeded         # Hinweis zum Ausschluss von Wochenenden         ProjectionNote = "Projizierter Abschluss schließt Wochenenden aus (Sa/So)"     }     $summary | ConvertTo-Json -Tiefe 5 | Out-File $summaryPath -Encoding UTF8 -Force     Write-Log "Rolloutzusammenfassung gespeichert: $summaryPath" "INFO"     $summary zurückgeben }                                                             

function Get-BlockedBuckets {     if (Testpfad $blockedBucketsPath) {         return Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable     }     return @{} }

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

function Get-AdminApproved {     if (Testpfad $adminApprovedPath) {         return Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable     }     return @{} }

function Get-DeviceHistory {     if (Testpfad $deviceHistoryPath) {         return Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable     }     return @{} }

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

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

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

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

function Get-NotUpdatedIndexes {     param([array]$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-tt HH:mm:ss"     $color = switch ($Level) {         "OK" { "Green" }         "WARN" { "Yellow" }         "ERROR" { "Red" }         "BLOCKED" { "DarkRed" }         "WAVE" { "Cyan" }         default { "White" }     }     Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color     # Auch protokollieren in Datei     $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMdd').log"     "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Encoding UTF8 }               

function Get-BucketKey {     param($Device)     # Verwenden Sie BucketId aus gerätebasiertem JSON(sofern verfügbar) (SHA256-Hash aus Erkennungsskript)     if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" }     # Fallback: Konstrukt vom Hersteller|modell|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 }     rückgabe "$mfr|$model|$bios" }

# ============================================================================ # VIP/AUSSCHLUSSLISTE WIRD GELADEN # ============================================================================

function Get-ExcludedHostnames {     param(         [Zeichenfolge]$ExclusionFilePath,         [Zeichenfolge]$ADGroupName     )     $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)     # Laden aus Datei (unterstützt .txt oder .csv)     if ($ExclusionFilePath -and (Test-Path $ExclusionFilePath)) {         $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower()         if ($extension -eq ".csv") {             # CSV-Format: Erwartet eine Spalte "Hostname" oder "ComputerName".             $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 (![ string]::IsNullOrWhiteSpace($row.$hostCol)) {                         [void]$excluded. Add($row.$hostCol.Trim())                     }                 }             }         } else {             # Nur-Text: ein Hostname pro Zeile             Get-Content $ExclusionFilePath | ForEach-Object {                 $line = $_. Trim()                 , wenn ($line -and -not $line. StartsWith('#')) {                     [void]$excluded. Add($line)                 }             }         }         Write-Log "Loaded $($excluded. Anzahl) Hostnamen aus der Ausschlussdatei: $ExclusionFilePath" "INFO"     }     # Laden aus AD-Sicherheitsgruppe     if ($ADGroupName) {         try {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'computer' }             foreach ($member in $groupMembers) {                 [void]$excluded. Add($member. Name)             }             Write-Log "Loaded $($groupMembers.Count) computers from AD group: $ADGroupName" "INFO"         } catch {             Write-Log "AD-Gruppe '$ADGroupName' konnte nicht geladen werden: $_" "WARN"         }     }     return @($excluded) }                                                                             

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

function Get-AllowedHostnames {     <#     . SYNOPSIS         Lädt Hostnamen aus einer AllowList-Datei und/oder EINER AD-Gruppe für einen gezielten Rollout.Wenn eine AllowList angegeben wird, werden NUR diese Geräte in den Rollout einbezogen.#>     param(         [Zeichenfolge]$AllowFilePath,         [Zeichenfolge]$ADGroupName     )          $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)          # Laden aus Datei (unterstützt .txt oder .csv)     if ($AllowFilePath -and (Test-Path $AllowFilePath)) {         $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower()                  if ($extension -eq ".csv") {             # CSV-Format: Erwartet eine Spalte "Hostname" oder "ComputerName".             $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 (![ string]::IsNullOrWhiteSpace($row.$hostCol)) {                             [void]$allowed. Add($row.$hostCol.Trim())                         }                     }                 }             }         } else {             # Nur-Text: ein Hostname pro Zeile             Get-Content $AllowFilePath | ForEach-Object {                 $line = $_. Trim()                 , wenn ($line -and -not $line. StartsWith('#')) {                     [void]$allowed. Add($line)                 }             }         }                  Write-Log "Loaded $($allowed. Anzahl) Hostnamen aus der Zulassungslistendatei: $AllowFilePath" "INFO"     }          # Laden aus AD-Sicherheitsgruppe     if ($ADGroupName) {         try {             $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop |                  Where-Object { $_.objectClass -eq 'computer' }                          foreach ($member in $groupMembers) {                 [void]$allowed. Add($member. Name)             }                          Write-Log "Loaded $($groupMembers.Count) computers from AD allow group: $ADGroupName" "INFO"         } catch {             Write-Log "AD-Gruppe '$ADGroupName' konnte nicht geladen werden: $_" "WARN"         }     }          return @($allowed) }

# ============================================================================ # AKTUALITÄT UND ÜBERWACHUNG VON DATEN # ============================================================================

function Get-DataFreshness {     <#     . SYNOPSIS         Überprüft, wie aktuell die Erkennungsdaten sind, indem die Zeitstempel der JSON-Datei untersucht werden.Gibt Statistiken zum zeitpunkt der letzten Meldung von Endpunkten zurück.#>     param([string]$JsonPath)     $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue     if ($jsonFiles.Count -eq 0) {         return @{             TotalFiles = 0             FreshFiles = 0             StaleFiles = 0             NoDataFiles = 0             OldestFile = $null             NewestFile = $null             AvgAgeHours = 0             Warnung = "Keine JSON-Dateien gefunden – Die Erkennung wurde möglicherweise nicht bereitgestellt"         }     }     $now = Get-Date     $freshThresholdHours = 24 # Files, die in den letzten 24 Stunden aktualisiert wurden, sind "frisch"     $staleThresholdHours = 72 # Files älter als 72 Stunden sind "veraltet"     $fresh = 0     $stale = 0     $ages = @()     foreach ($file in $jsonFiles) {         $ageHours = ($now - $file. LastWriteTime). TotalHours         $ages += $ageHours         if ($ageHours -le $freshThresholdHours) {             $fresh++         } elseif ($ageHours -ge $staleThresholdHours) {             $stale++         }     }     $oldestFile = $jsonFiles | Sort-Object LastWriteTime | Select-Object -First 1     $newestFile = $jsonFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1     $warning = $null     if ($stale -gt ($jsonFiles.Count * 0.5)) {         $warning = "Mehr als 50 % der Geräte verfügen über veraltete Daten (>72 Stunden) – Gruppenrichtlinienobjekt zur Überprüfung der Erkennung"     } elseif ($fresh -lt ($jsonFiles.Count * 0.3)) {         $warning = "Weniger als 30 % der kürzlich gemeldeten Geräte – Erkennung wird möglicherweise nicht ausgeführt"     }     return @{         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). Durchschnitt, 1)         Warnung = $warning     } }                                                 

function Test-DetectionGPODeployed {     <#     . SYNOPSIS         Überprüft, ob die Erkennungs-/Überwachungsinfrastruktur vorhanden ist.#>     param([string]$JsonPath)     # Überprüfung 1: JSON-Pfad vorhanden     if (-not (Test-Path $JsonPath)) {         return @{             IsDeployed = $false             Meldung = "JSON-Eingabepfad ist nicht vorhanden: $JsonPath"         }     }     # Check 2: Mindestens einige JSON-Dateien vorhanden     $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). Count     if ($jsonCount -eq 0) {         return @{             IsDeployed = $false             Meldung = "Keine JSON-Dateien in $JsonPath – Erkennungs-GPO wurde möglicherweise nicht bereitgestellt, oder Geräte haben noch keine Berichte gemeldet"         }     }     # Check 3: Files sind relativ aktuell (zumindest einige in der letzten Woche)     $recentFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue |         Where-Object { $_. LastWriteTime -gt (Get-Date). AddDays(-7) }     if ($recentFiles.Count -eq 0) {         return @{             IsDeployed = $false             Meldung = "In den letzten 7 Tagen wurden keine JSON-Dateien aktualisiert – Erkennungs-GPO ist möglicherweise fehlerhaft oder Geräte offline"         }     }     return @{         IsDeployed = $true         Meldung = "Erkennung wird aktiv angezeigt: $jsonCount Dateien, $($recentFiles.Count) kürzlich aktualisiert"         FileCount = $jsonCount         RecentCount = $recentFiles.Count     } }                         

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

function Update-DeviceHistory {     <#     . SYNOPSIS         Verfolgt Geräte nach Hostnamen nach, da wir keinen eindeutigen Computerbezeichner haben.Hinweis: BucketId ist 1:n (dieselbe Hardwarekonfiguration = derselbe Bucket).Wenn der JSON-Sammlung ein eindeutiger Bezeichner hinzugefügt wird, aktualisieren Sie diese Funktion.#>     param(         [Array]$CurrentDevices,         [Hashtabelle]$DeviceHistory     )          foreach ($device in $CurrentDevices) {         $hostname = $device. Hostname         if (-not $hostname) { continue }                  # Nachverfolgen des Geräts nach Hostnamen         $DeviceHistory[$hostname] = @{             Hostname = $hostname             BucketId = $device. BucketId             Hersteller = $device. WMI_Manufacturer             Model = $device. WMI_Model             LastSeen = Get-Date -Format "yyyy-MM-tt HH:mm:ss"             Status = $device. UpdateStatus         }     } }

# ============================================================================ # BLOCKIERTE BUCKETERKENNUNG (basierend auf der Gerätereichbarkeit) # ============================================================================

<# . BESCHREIBUNG     Blockierende Logik:     – Ein Bucket wird NUR blockiert, wenn:       1. Das Gerät wurde in einer Welle als Ziel verwendet       2. MaxWaitHours ist seit Beginn der Welle vorbei       3. Gerät ist NICHT ERREICHBAR (Pingfehler)          – Wenn das Gerät erreichbar, aber noch nicht aktualisiert ist, warten wir weiter       (Update steht möglicherweise ein Neustart aus – Ereignis 1808 wird nur nach dem Neustart ausgelöst)          – Nicht erreichbares Gerät weist darauf hin, dass ein Fehler aufgetreten ist und eine Untersuchung      erforderlich ist     Freigabe:     – Verwenden Sie -ListBlockedBuckets, um blockierte Buckets anzuzeigen.     – Verwenden Sie -UnblockBucket "BucketKey", um die Blockierung eines bestimmten Buckets aufzuheben.     – Verwenden Sie -UnblockAll, um die Blockierung aller Buckets aufzuheben. #>

function Test-DeviceReachable {     param(         [Zeichenfolge]$Hostname,         [Zeichenfolge]$DataPath # Pfad zu JSON-Dateien des Geräts     )     # Methode 1: Überprüfen des Zeitstempels für JSON-Dateien (am schnellsten – keine Dateianalyse erforderlich)     # Wenn das Erkennungsskript kürzlich ausgeführt wurde, wurde die Datei geschrieben/aktualisiert, um zu beweisen, dass das Gerät aktiv ist.     if ($DataPath) {         $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object -First 1         if ($deviceFile) {             $hoursSinceWrite = ((Get-Date) - $deviceFile.LastWriteTime). TotalHours             if ($hoursSinceWrite -lt 72) { return $true }         }     }     # Methode 2: Fallback auf Ping (nur, wenn JSON veraltet oder fehlt)     try {         $ping = Test-Connection -ComputerName $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue         $ping zurückgeben     } catch {         $false zurückgeben     } }          

function Update-BlockedBuckets {     param(         $RolloutState,         $BlockedBuckets,         $AdminApproved,         [Array]$NotUpdatedDevices,         [Hashtabelle]$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 }     # Erfassen von Geräten, die den Wartezeitzeitraum überdauern und immer noch nicht aktualisiert werden     foreach ($wave in $RolloutState.WaveHistory) {         , wenn (nicht $wave. StartedAt) { continue }         $waveStart = [DateTime]::P arse($wave. StartedAt)         $hoursSinceWave = ($now - $waveStart). TotalHours         if ($hoursSinceWave -lt $MaxWaitHours) {             # Noch innerhalb des Wartezeitraums – noch nicht überprüfen             Weiter         }         # Überprüfen Sie jedes Gerät aus dieser Welle         foreach ($deviceInfo in $wave. Geräte) {             $hostname = $deviceInfo.Hostname             $bucketKey = $deviceInfo.BucketKey             # Überspringen, wenn der Bucket bereits blockiert ist             if ($BlockedBuckets.Contains($bucketKey)) { continue }             # Überspringen, wenn der Bucket vom Administrator genehmigt und vor der Genehmigung gestartet wurde             # (überprüfen Sie nur Geräte, die nach der Genehmigung des Administrators auf erneutes Blockieren ausgerichtet sind)             if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) {                 $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. ApprovedAt)                 if ($waveStart -lt $approvalTime) {                     # Dieses Gerät wurde vor der Genehmigung durch den Administrator als Ziel verwendet– überspringen                     Weiter                 }                 # Welle nach Genehmigung gestartet – dies ist eine neue Zielrichtung, die überprüft werden kann             }             # Befindet sich dieses Gerät noch in der NotUpdated-Liste?             if ($hostSet.Contains($hostname)) {                 $devicesToCheck += @{                     Hostname = $hostname                     BucketKey = $bucketKey                     WaveNumber = $wave. WaveNumber                     HoursSinceWave = [math]::Round($hoursSinceWave, 1)                 }             }         }     }     if ($devicesToCheck.Count -eq 0) {         $newlyBlocked zurückgeben     }     Write-Log "Überprüfen der Erreichbarkeit von $($devicesToCheck.Count)-Geräten über den Wartezeitzeitraum..." "INFO"     # Nachverfolgen von Fehlern pro Bucket zur Entscheidungsfindung     $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() }     # Überprüfen der Erreichbarkeit jedes Geräts     foreach ($device in $devicesToCheck) {         $hostname = $device. Hostname         $bucketKey = $device. BucketKey         if ($DryRun) {             Write-Log "[DRYRUN] Würde $hostname Erreichbarkeit überprüfen" "INFO"             Weiter         }         if (-not $bucketFailures.ContainsKey($bucketKey)) {             $bucketFailures[$bucketKey] = @{ Unreachable = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. HoursSinceWave }         }         $isReachable = Test-DeviceReachable -Hostname $hostname -DataPath $AggregationInputPath         if (-not $isReachable) {             $bucketFailures[$bucketKey]. Nicht erreichbare += $hostname         } else {             # Das Gerät ist erreichbar, aber noch nicht aktualisiert. Dies kann ein vorübergehender Fehler oder das Warten auf einen Neustart sein.             $bucketFailures[$bucketKey]. AliveButFailed += $hostname             $stillWaiting += $hostname         }     }     # Entscheidung pro Bucket: Nur blockieren, wenn Geräte wirklich NICHT ERREICHBAR sind     # Alive-Geräte mit Fehlern = temporär, Rollout fortsetzen     foreach ($bucketKey in $bucketFailures.Keys) {         $bf = $bucketFailures[$bucketKey]         $unreachableCount = $bf. Unreachable.Count         $aliveFailedCount = $bf. AliveButFailed.Count         # Überprüfen Sie, ob dieser Bucket erfolgreich ist (aus aktualisierten Gerätedaten).         $bucketHasSuccesses = $stSuccessBuckets -and $stSuccessBuckets.Contains($bucketKey)         if ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) {             # Alle fehlerhaften Geräte sind nicht erreichbar – Blockieren des Buckets             if ($newlyBlocked -notcontains $bucketKey) {                 $BlockedBuckets[$bucketKey] = @{                     BlockedAt = Get-Date -Format "yyyy-MM-tt HH:mm:ss"                     Reason = "Alle $unreachableCount Geräte nach $($bf. HoursSinceWave) stunden"                     FailedDevices = ($bf. Unreachable -join ", ")                     WaveNumber = $bf. WaveNumber                     DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } else { 0 }                 }                 $newlyBlocked += $bucketKey                 Write-Log "BUCKET BLOCKED: $bucketKey ($unreachableCount Geräte nicht erreichbar: $($bf. Unreachable -join ', '))" "BLOCKED"             }         } elseif ($aliveFailedCount -gt 0) {             # Geräte sind aktiv, aber nicht aktualisiert – temporärer Fehler, NICHT blockieren             Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length))))...: $aliveFailedCount Geräte aktiv, aber ausstehend, $unreachableCount nicht erreichbar - NICHT blockierend (temporär)" "INFO"             if ($unreachableCount -gt 0) {                 Write-Log " Nicht erreichbar: $($bf. Unreachable -join ', ')" "WARN"             }             Write-Log " Aktiv, aber ausstehend: $($bf. AliveButFailed -join ', ')" "INFO"             # Nachverfolgen der Fehleranzahl im Rolloutzustand für die Überwachung             if (-not $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} }             $RolloutState.TemporaryFailures[$bucketKey] = @{                 AliveButFailed = $bf. AliveButFailed                 Nicht erreichbar = $bf. Unerreichbar                 LastChecked = Get-Date -Format "yyyy-MM-tt HH:mm:ss"             }         }     }     if ($stillWaiting.Count -gt 0) {         Write-Log "Geräte erreichbar, aber ein Update aussteht (muss möglicherweise neu gestartet werden): $($stillWaiting.Count)" "INFO"     }     $newlyBlocked zurückgeben }                                                                                                                                                                                  

# ============================================================================ # AUTO-UNBLOCK: Aufheben der Blockierung von Buckets bei erfolgreicher Geräteaktualisierung # ============================================================================

function Update-AutoUnblockedBuckets {     <#     . BESCHREIBUNG         Überprüft, ob Geräte in blockierten Buckets aktualisiert wurden (Ereignis 1808).         Hebt die Blockierung automatisch auf, wenn ALLE Zielgeräte im Bucket aktualisiert wurden.Wenn nur EINIGE Geräte aktualisiert wurden, benachrichtigt den Administrator, der die Blockierung manuell aufheben kann.                  Admin können die Blockierung manuell aufheben, indem Sie Folgendes verwenden:           .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "path" -UnblockBucket "BucketKey"     #>     param(         $BlockedBuckets,         $RolloutState,         [Array]$NotUpdatedDevices,         [Zeichenfolge]$ReportBasePath,         [Hashtabelle]$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]         # Abrufen aller Geräte, die wir in der Vergangenheit aus diesem Bucket als Ziel verwendet haben         $targetedDevicesInBucket = @()         foreach ($wave in $RolloutState.WaveHistory) {             $targetedDevicesInBucket += @($wave. Geräte | Where-Object { $_. BucketKey -eq $bucketKey })         }         if ($targetedDevicesInBucket.Count -eq 0) { continue }         # Überprüfen, wie viele Zielgeräte noch in NotUpdated oder aktualisiert sind         $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) {             # ALLE Zielgeräte wurden aktualisiert – automatisches Aufheben der Blockierung!             $BlockedBuckets.Remove($bucketKey)             $autoUnblocked += @{                 BucketKey = $bucketKey                 UpdatedDevices = $updatedDevices                 PreviouslyBlockedAt = $bucketInfo.BlockedAt                 Reason = "Alle $($updatedDevices.Count)-Zielgeräte wurden erfolgreich aktualisiert"             }             Write-Log "AUTO-UNBLOCKED: $bucketKey (Alle $($updatedDevices.Count)-Zielgeräte erfolgreich aktualisiert)" "OK"             # Inkrementierung der OEM-Wellenanzahl für den OEM dieses Buckets (Nachverfolgung pro OEM)             $bucketOEM = if ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' } # Oem aus einem durch Pipe getrennten Schlüssel oder Standard extrahieren             if (-not $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             }             $currentWave = if ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } else { 0 }             $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1             Write-Log " OEM '$bucketOEM'-Wellenanzahl inkrementiert auf $($currentWave + 1) (nächste Zuordnung: $([int][Math]::P ow(2, $currentWave + 1)) Geräte)" "INFO"         }         elseif ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -gt 0) {             # EINIGE Geräte wurden aktualisiert, aber andere sind noch ausstehend – Administrator benachrichtigen (nur einmal)             if (-not $bucketInfo.UnblockCandidate) {                 $bucketInfo.UnblockCandidate = $true                 $bucketInfo.UpdatedDevices = $updatedDevices                 $bucketInfo.PendingDevices = $stillPendingDevices                 $bucketInfo.NotifiedAt = (Get-Date). ToString("yyyy-MM-tt HH:mm:ss")                 Write-Log "" "INFO"                 Write-Log "========== TEILAKTUALISIERUNG IN BLOCKIERTEN BUCKET-==========" "INFO"                 Write-Log "Bucket: $bucketKey" "INFO"                 $updatedSample = @($updatedDevices | Select-Object -First $LogSampleSize)                 $pendingSample = @($stillPendingDevices | Select-Object -First $LogSampleSize)                 $updatedSuffix = if ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) more)" } else { "" }                 $pendingSuffix = if ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) more)" } else { "" }                 Write-Log "Aktualisierte Geräte ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix" "OK"                 Write-Log "Noch ausstehend ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix" "WARN"                 Write-Log "" "INFO"                 Write-Log "Um die Blockierung dieses Buckets nach der Überprüfung manuell aufzuheben, führen Sie aus: " INFO"                 Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '"$ReportBasePath'" -UnblockBucket '"$bucketKey'"" "INFO"                 Write-Log "=======================================================" "INFO"                 Write-Log "" "INFO"             }         }     }     $autoUnblocked zurückgeben }                                                                                          

# ============================================================================ # WAVE GENERATION (INLINED – schließt blockierte Buckets aus) # ============================================================================

function New-RolloutWave {     param(         [Zeichenfolge]$AggregationPath,         $BlockedBuckets,         $RolloutState,         [int]$MaxDevicesPerWave = 50,         [string[]]$AllowedHostnames = @(),         [string[]]$ExcludedHostnames = @()     )     # Laden von Aggregationsdaten     $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" |          Where-Object { $_. Name -notlike "*Buckets*" } |          Sort-Object LastWriteTime -Descending |          Select-Object -First 1     if (-not $notUptodateCsv) {         Write-Log "No NotUptodate CSV found" (Keine NotUptodate-CSV gefunden) "ERROR"         $null zurückgeben     }     $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName)     # Normalize HostName -> Hostname aus Konsistenzgründen (CSV verwendet HostName, Code verwendet Hostname)     foreach ($device in $allNotUpdated) {         , wenn ($device. PSObject.Properties['HostName'] -and -not $device. PSObject.Properties['Hostname']) {             $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName -Force         }     }     # Blockierte Buckets herausfiltern     $eligibleDevices = @($allNotUpdated | Where-Object {         $bucketKey = Get-BucketKey $_         -not $BlockedBuckets.Contains($bucketKey)     })     # Filter nach NUR zulässigen Geräten (wenn AllowList angegeben ist)     # AllowList = gezielter Rollout – nur diese Geräte werden berücksichtigt     if ($AllowedHostnames.Count -gt 0) {         $beforeCount = $eligibleDevices.Count         $eligibleDevices = @($eligibleDevices | Where-Object {             $_. Hostname in $AllowedHostnames         })         $allowedCount = $eligibleDevices.Count         Write-Log "Angewendete AllowList: $allowedCount von $beforeCount Geräten befinden sich in der Zulassungsliste" "INFO"     }     # Herausfiltern von VIP/ausgeschlossenen Geräten (BlockList)     # BlockList wird nach AllowList angewendet     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 "Ausgeschlossene $excludedCount VIP/geschützte Geräte vom Rollout" "INFO"         }     }     if ($eligibleDevices.Count -eq 0) {         Write-Log "Keine berechtigten Geräte verbleiben (alle aktualisiert oder blockiert)" "OK"         $null zurückgeben     }     # Geräte abrufen, die sich bereits im Rollout befinden (aus vorherigen Wellen)     $devicesAlreadyInRollout = @()     if ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) {         $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object {             $_. Geräte | ForEach-Object { $_. Hostname }         } | Where-Object { $_ })     }     Write-Log "Geräte, die sich bereits im Rollout befinden: $($devicesAlreadyInRollout.Count)" "INFO"     # Getrennt nach Konfidenzniveau     $highConfidenceDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel -eq "High Confidence" -and         $_. Hostname -notin $devicesAlreadyInRollout     })     # Erforderliche Aktion umfasst Folgendes:     # – Explizite "Aktion erforderlich"     # – Empty/NULL ConfidenceLevel     # – BELIEBIGEr unbekannter/unbekannter ConfidenceLevel-Wert (als Aktion erforderlich behandelt)     $knownSafeCategories = @(         "Hohes Vertrauen",         "Vorübergehend angehalten",         "Unter Beobachtung",         "Unter Beobachtung - mehr Daten erforderlich",         "Nicht unterstützt",         "Nicht unterstützt – Bekannte Einschränkung"     )     $actionRequiredDevices = @($eligibleDevices | Where-Object {         $_. ConfidenceLevel -notin $knownSafeCategories -and         $_. Hostname -notin $devicesAlreadyInRollout     })     Write-Log "Hohe Zuverlässigkeit (nicht im Rollout): $($highConfidenceDevices.Count)" "INFO"     Write-Log "Aktion erforderlich (nicht im Rollout): $($actionRequiredDevices.Count)" "INFO"     # Erstellen von Wave-Geräten     $waveDevices = @()     # HIGH CONFIDENCE: Include ALL (sicheres Rollout)     if ($highConfidenceDevices.Count -gt 0) {         Write-Log "Hinzufügen aller $($highConfidenceDevices.Count)-Geräte mit hoher Zuverlässigkeit" "WAVE"         $waveDevices += $highConfidenceDevices     } # ACTION REQUIRED: Progressiver Rollout (bucketbasiert mit OEM-Streuung für Buckets ohne Erfolg)     # Strategie:     # – Buckets mit 0 Erfolgen: Verteilt auf OEMs (1 pro OEM – > 2 pro OEM – > 4 pro OEM)     # - Buckets mit erfolglosem ≥1: Freies Double ohne OEM-Einschränkung     if ($actionRequiredDevices.Count -gt 0) {         # Erfolgreiche Anzahl von Buckets aus aktualisierten CSV-Geräten laden (Geräte, die erfolgreich aktualisiert wurden)         $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" |             Sort-Object LastWriteTime -Descending | Select-Object -First 1         $bucketStats = @{}         if ($updatedCsv) {             $updatedDevices = Import-Csv $updatedCsv.FullName             # Anzahl der Erfolge pro BucketId             $updatedDevices | ForEach-Object {                 $key = Get-BucketKey $_                 if ($key) {                     if (-not $bucketStats.ContainsKey($key)) {                         $bucketStats[$key] = @{ Erfolge = 0; Ausstehend = 0; Gesamt = 0 }                     }                     $bucketStats[$key]. Erfolge++                     $bucketStats[$key]. Total++                 }             }             Write-Log "Loaded $($updatedDevices.Count) updated devices across $($bucketStats.Count) buckets" (Loaded $($updatedDevices.Count) devices across $($bucketStats.Count) buckets" (INFO)         } else {             # Fallback: Probieren Sie ActionRequired_Buckets CSV aus.             $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" |                 Sort-Object LastWriteTime -Descending | Select-Object -First 1             if ($bucketsCsv) {                 Import-Csv $bucketsCsv.FullName | ForEach-Object {                     $key = if ($_. BucketId) { $_. BucketId } else { "$($_. Hersteller)|$($_. Modell)|$($_. BIOS)" }                     $bucketStats[$key] = @{                         Erfolge = [int]$_. Erfolge                         Ausstehend = [int]$_. Ausstehende                         Total = [int]$_. TotalDevices                     }                 }             }         }         # Gruppieren von nicht aktualisierten Geräten nach Bucket (Hersteller|Modell|BIOS)         $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ }         # Separate Buckets: zero-success vs has-success         $zeroSuccessBuckets = @()         $hasSuccessBuckets = @()         foreach ($bucket in $buckets) {             $bucketKey = $bucket. Namen             $bucketDevices = @($bucket. Gruppe)             $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Hostname })             # Anzahl der Erfolge in diesem Bucket             $stats = $bucketStats[$bucketKey]             $successes = if ($stats) { $stats. Erfolge } else { 0 }             # Suchen von Geräten, die in diesem Bucket aus dem Wellenverlauf bereitgestellt wurden             $deployedToBucket = @()             foreach ($wave in $RolloutState.WaveHistory) {                 foreach ($device in $wave. Geräte) {                     , wenn ($device. BucketKey -eq $bucketKey -und $device. Hostname) {                         $deployedToBucket += $device. Hostname                     }                 }             }             $deployedToBucket = @($deployedToBucket | Sort-Object -Unique)             # Überprüfen, ob ALLE bereitgestellten Geräte erfolgreich gemeldet wurden             $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames })             $confirmedSuccess = $deployedToBucket.Count – $stillPending.Count             # Wenn ausstehend, überspringen Sie diesen Bucket, bis alle bestätigen.             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) (wartend)" "INFO"                 Weiter             }             # Weiterhin berechtigt = Geräte, die noch nicht bereitgestellt wurden             $devicesNotYetTargeted = @($bucketDevices | Where-Object {                 $_. Hostname -notin $deployedToBucket             })             if ($devicesNotYetTargeted.Count -eq 0) { continue }             # Kategorisieren nach Erfolgsanzahl             $bucketInfo = @{                 BucketKey = $bucketKey                 Geräte = $devicesNotYetTargeted                 ConfirmedSuccess = $confirmedSuccess                 Erfolge = $successes                 OEM = if ($bucket. Gruppe[0]. WMI_Manufacturer) { $bucket. Gruppe[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' }             }             if ($successes -eq 0) {                 $zeroSuccessBuckets += $bucketInfo             } else {                 $hasSuccessBuckets += $bucketInfo             }         }         # === PROCESS HAS-SUCCESS BUCKETS (≥1 erfolg) ===         # Die Anzahl der Erfolge verdoppeln – wenn 14 erfolgreich waren, stellen Sie 28 als Nächstes bereit.         foreach ($bucketInfo in $hasSuccessBuckets) {             $nextBatchSize = $bucketInfo.Erfolge * 2             $nextBatchSize = [Math]::Min($nextBatchSize, $MaxDevicesPerWave)             $nextBatchSize = [Math]::Min($nextBatchSize, $bucketInfo.Devices.Count)             if ($nextBatchSize -gt 0) {                 $selectedDevices = @($bucketInfo.Devices | Select-Object -First $nextBatchSize)                 $waveDevices += $selectedDevices                 $parts = if ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length))) }                 $displayName = "$($parts[0]) - $($parts[1])"                 Write-Log " [HAS-SUCCESS] $displayName - Success=$($bucketInfo.Success), Deploying=$nextBatchSize (2x bestätigt)" "INFO"             }         }         # === PROCESS ZERO-SUCCESS BUCKETS (verteilt auf OEMs mit Nachverfolgung pro OEM) ===         # Ziel: Risiko auf verschiedene OEMs verteilen, Fortschritt pro OEM unabhängig verfolgen         # Jeder OEM schreitet basierend auf seinem eigenen Erfolgsverlauf fort:         # – OEM mit Erfolg: Ruft weitere Geräte der nächsten Welle ab (2^waveCount)         # - OEM ohne Erfolg: Bleibt auf dem aktuellen Niveau, bis der Erfolg bestätigt wird         if ($zeroSuccessBuckets.Count -gt 0) {             # Initialisieren der Wellenanzahl pro OEM, falls nicht vorhanden             if (-not $RolloutState.OEMWaveCounts) {                 $RolloutState.OEMWaveCounts = @{}             }             # Gruppieren von Buckets ohne Erfolg nach OEM             $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM }             $totalZeroSuccessAdded = 0             $oemsDeployedTo = @()             foreach ($oemGroup in $oemBuckets) {                 $oemName = $oemGroup.Name                 # Rufen Sie die Wellenanzahl dieses OEM ab (beginnt bei 0).                 $oemWaveCount = if ($RolloutState.OEMWaveCounts[$oemName]) {                     $RolloutState.OEMWaveCounts[$oemName]                 } else { 0 }                 # Geräte für DIESEN OEM berechnen: 2^waveCount (1, 2, 4, 8...)                 $devicesForThisOEM = [int][Math]::P ow(2, $oemWaveCount)                 $devicesForThisOEM = [Math]::Max(1, $devicesForThisOEM)                 $oemDevicesAdded = 0                 # Auswählen aus jedem Bucket unter diesem OEM                 foreach ($bucketInfo in $oemGroup.Group) {                     $remaining = $devicesForThisOEM – $oemDevicesAdded                     if ($remaining -le 0) { break }                     $toTake = [Math]::Min($remaining, $bucketInfo.Devices.Count)                     if ($toTake -gt 0) {                         $selectedDevices = @($bucketInfo.Devices | Select-Object -First $toTake)                         $waveDevices += $selectedDevices                         $oemDevicesAdded += $toTake                         $totalZeroSuccessAdded += $toTake                         $parts = if ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length))) }                         $displayName = "$($parts[0]) - $($parts[1])"                         Write-Log " [ZERO-SUCCESS] $displayName - Deploying=$toTake (OEM wave $oemWaveCount = ${devicesForThisOEM}/OEM)" "WARN"                     }                 }                 if ($oemDevicesAdded -gt 0) {                     Write-Log " OEM: $oemName – Wave $oemWaveCount, $oemDevicesAdded Geräte hinzugefügt" "INFO"                     $oemsDeployedTo += $oemName                 }             }             # Nachverfolgen, für welche OEMs wir bereitgestellt haben (zur Erhöhung bei der nächsten Erfolgsprüfung)             if ($oemsDeployedTo.Count -gt 0) {                 $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo                 Write-Log "Zero-Success deployment: $totalZeroSuccessAdded devices across $($oemsDeployedTo.Count) OEMs" "INFO"             }         }     }     if (@($waveDevices). Count -eq 0) {         $null zurückgeben     }     $waveDevices zurückgeben }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

# ============================================================================ # GPO DEPLOYMENT (INLINED – erstellt Gruppenrichtlinienobjekt, Sicherheitsgruppe, Links) # ============================================================================

function Deploy-GPOForWave {     param(         [Zeichenfolge]$GPOName,         [Zeichenfolge]$TargetOU,         [Zeichenfolge]$SecurityGroupName,         [Array]$WaveHostnames,         [bool]$DryRun = $false     )     # ADMX-Richtlinie: SecureBoot.admx – SecureBoot_AvailableUpdatesPolicy     # Registrierungspfad: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot     # Wertname: AvailableUpdatesPolicy     # Enabled Value: 22852 (0x5944) – Aktualisieren aller Schlüssel für den sicheren Start + bootmgr     # Deaktivierter Wert: 0     #     # Verwenden von Gruppenrichtlinie Preferences (GPP) für eine zuverlässige HKLM\SYSTEM-Pfadbereitstellung     # GPP erstellt Einstellungen unter: Computerkonfiguration > Einstellungen > Windows-Einstellungen > Registrierung     $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot"     $RegistryValueName = "AvailableUpdatesPolicy"     $RegistryValue = 22852 # 0x5944 – entspricht ADMX enabledValue     Write-Log "Bereitstellen des Gruppenrichtlinienobjekts: $GPOName" "WAVE"     Write-Log "Registry: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X')))" "INFO"     if ($DryRun) {         Write-Log "[DRYRUN] Würde GPO erstellen: $GPOName" "INFO"         Write-Log "[DRYRUN] Würde Sicherheitsgruppe erstellen: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Würde $(@($WaveHostnames) hinzufügen. Anzahl) zu gruppierende Computer" "INFO"         Write-Log "[DRYRUN] Würde GPO mit: $TargetOU" "INFO" verknüpfen         $true zurückgeben     }     try {         # Erforderliche Module importieren         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } catch {         Write-Log "Fehler beim Importieren erforderlicher Module (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR"         $false zurückgeben     }     # Schritt 1: Erstellen oder Abrufen eines Gruppenrichtlinienobjekts     $existingGPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     if ($existingGPO) {         Write-Log "GPO ist bereits vorhanden: $GPOName" "INFO"         $gpo = $existingGPO     } else {         try {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Certificate Rollout - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Erstelltes Gruppenrichtlinienobjekt: $GPOName" "OK"         } catch {             Write-Log "Fehler beim Erstellen des Gruppenrichtlinienobjekts: $($_. Exception.Message)" "ERROR"             $false zurückgeben         }     }     # Schritt 2: Festlegen des Registrierungswerts mithilfe von Gruppenrichtlinie Preferences (GPP)     # GPP ist für HKLM\SYSTEM-Pfade zuverlässiger als Set-GPRegistryValue     try {         # Versuchen Sie zunächst, alle vorhandenen Einstellungen für diesen Wert zu entfernen (um Duplikate zu vermeiden).         Remove-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue         # Erstellen einer GPP-Registrierungseinstellung mit der Aktion "Ersetzen"         # Replace = Create if not exists, Update if exists (am zuverlässigsten)         # Update = Nur aktualisieren, wenn vorhanden (Schlägt fehl, wenn der Wert nicht vorhanden ist)         Set-GPPrefRegistryValue -Name $GPOName '             -Context Computer '             -Action Replace '             -Key $RegistryKey '             -ValueName $RegistryValueName '             -Type DWord '             -Value $RegistryValue         Write-Log "Konfigurierte GPP-Registrierungseinstellung: $RegistryValueName = 0x5944 (Action=Replace)" "OK"     } catch {         Write-Log "GPP failed, trying Set-GPRegistryValue: $($_. Exception.Message)" "WARN"         # Fallback auf Set-GPRegistryValue (funktioniert, wenn ADMX bereitgestellt wird)         try {             Set-GPRegistryValue -Name $GPOName '                 -Key $RegistryKey '                 -ValueName $RegistryValueName '                 -Type DWord '                 -Value $RegistryValue             Write-Log "Konfigurierte Registrierung über Set-GPRegistryValue: $RegistryValueName = 0x5944" "OK"         } catch {             Write-Log "Fehler beim Festlegen des Registrierungswerts: $($_. Exception.Message)" "ERROR"             $false zurückgeben         }     }     # Schritt 3: Erstellen oder Abrufen einer Sicherheitsgruppe     $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-not $existingGroup) {         try {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Beschreibung "Computer, die für den Rollout des sicheren Starts vorgesehen sind – $GPOName" '                 -Passthru             Write-Log "Sicherheitsgruppe erstellt: $SecurityGroupName" "OK"         } catch {             Write-Log "Fehler beim Erstellen der Sicherheitsgruppe: $($_. Exception.Message)" "ERROR"             $false zurückgeben         }     } else {         Write-Log "Sicherheitsgruppe vorhanden: $SecurityGroupName" "INFO"         $group = $existingGroup     }     # Schritt 4: Hinzufügen von Computern zur Sicherheitsgruppe     $added = 0     $failed = 0     foreach ($hostname in $WaveHostnames) {         try {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } catch {             $failed++         }     }     Write-Log "$added Computer zur Sicherheitsgruppe hinzugefügt ($failed in AD nicht gefunden)" "OK"     # Schritt 5: Konfigurieren der Sicherheitsfilterung für GPO     try {         # Standardeinstellung "Authentifizierte Benutzer" entfernen Berechtigung anwenden (Lesen beibehalten)         Set-GPPermission -Name $GPOName -TargetName "Authentifizierte Benutzer" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         # Hinzufügen der Berechtigung "Anwenden" für unsere Sicherheitsgruppe         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Konfigurierte Sicherheitsfilterung für: $SecurityGroupName" "OK"     } catch {         Write-Log "Fehler beim Konfigurieren der Sicherheitsfilterung: $($_. Exception.Message)" "WARN"         Write-Log "GPO gilt möglicherweise für alle Computer in der verknüpften Organisationseinheit – manuelle Überprüfung" "WARN"     }     # Schritt 6: Verknüpfen des Gruppenrichtlinienobjekts mit der Organisationseinheit (KRITISCH für die Anwendung der Richtlinie)     if ($TargetOU) {         try {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }             if (-not $existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Ja -ErrorAction Stop                 Write-Log "Verknüpftes Gruppenrichtlinienobjekt: $TargetOU" "OK"                 Write-Log "GPO wird beim nächsten gpupdate auf Zielcomputern angewendet" "INFO"             } else {                 Write-Log "GPO bereits mit Ziel-ORGANISATIONSeinheit verknüpft" "INFO"             }         } catch {             Write-Log "KRITISCH: Fehler beim Verknüpfen des Gruppenrichtlinienobjekts mit der Organisationseinheit: $($_. Exception.Message)" "ERROR"             Write-Log "GPO wurde erstellt, aber NICHT VERKNÜPFT – es gilt NICHT für computer!" "ERROR"             Write-Log "Manuelle Korrektur erforderlich: New-GPLink -Name '$GPOName' -Target '$TargetOU' -LinkEnabled Yes" "ERROR"             $false zurückgeben         }     } else {         Write-Log "WARNING: No TargetOU specified - GPO created but NOT LINKED!" "ERROR"         Write-Log "Manuelle Verknüpfung erforderlich, damit GPO wirksam wird" "ERROR"         Write-Log "Run: New-GPLink -Name '$GPOName' -Target '<Your-Domain-DN>' -LinkEnabled Yes" "ERROR"     }     # Schritt 7: Überprüfen der GPO-Konfiguration     Write-Log "Überprüfen der GPO-Konfiguration..." "INFO"     try {         $gpoReport = Get-GPO -Name $GPOName -ErrorAction Stop         Write-Log "GPO-Status: $($gpoReport.GpoStatus)" "INFO"         # Überprüfen, ob die Registrierungseinstellung konfiguriert ist         $regSettings = Get-GPRegistryValue -Name $GPOName -Key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue         if (-not $regSettings) {             # Überprüfung der GPP-Registrierung (anderer Pfad im Gruppenrichtlinienobjekt)             Write-Log "Überprüfen der GPP-Registrierungseinstellungen..." "INFO"         }     } catch {         Write-Log "GPO konnte nicht überprüft werden: $($_. Exception.Message)" "WARN"     }     $true zurückgeben }                                                                                                

# ============================================================================ # WINCS DEPLOYMENT (Alternative zum AvailableUpdatesPolicy-GPO) # ============================================================================ # Referenz: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe # # WinCS-Befehle (ausführung auf dem Endpunkt im SYSTEM-Kontext): # Query: WinCsFlags.exe /query --key F33E0C8E002 # Apply: WinCsFlags.exe /apply --key "F33E0C8E002" # Reset: WinCsFlags.exe /reset --key "F33E0C8E002" # # Diese Methode stellt ein Gruppenrichtlinienobjekt mit einer geplanten Aufgabe bereit, die WinCsFlags.exe /apply ausführt. # als SYSTEM auf Zielendpunkten. Ähnlich wie bei der Bereitstellung des Erkennungsskripts # wird jedoch einmal (beim Start) statt täglich ausgeführt.

function Deploy-WinCSGPOForWave {     <#     . SYNOPSIS         Bereitstellen der WinCS Secure Boot-Aktivierung über eine geplante GPO-Aufgabe.. BESCHREIBUNG         Erstellt ein Gruppenrichtlinienobjekt, das einen geplanten Task zum Ausführen WinCsFlags.exe /apply bereitstellt.         unter SYSTEMkontext beim Computerstart. Sicherheitsgruppensteuerungen für die Zielgruppenadressierung.. PARAMETER GPOName         Name für das Gruppenrichtlinienobjekt.. PARAMETER TargetOU         Organisationseinheit, mit der das Gruppenrichtlinienobjekt verknüpft werden soll.. PARAMETER SecurityGroupName         Sicherheitsgruppe für GPO-Filterung.. PARAMETER WaveHostnames         Hostnamen, die der Sicherheitsgruppe hinzugefügt werden sollen.. PARAMETER WinCSKey         Die anzuwendende WinCS-Taste (Standard: F33E0C8E002).. PARAMETER DryRun         Wenn true, wird nur protokolliert, was ausgeführt wird.#>     param(         [Parameter(Mandatory = $true)]         [Zeichenfolge]$GPOName,                  [Parameter(Mandatory = $false)]         [Zeichenfolge]$TargetOU,                  [Parameter(Mandatory = $true)]         [Zeichenfolge]$SecurityGroupName,                  [Parameter(Mandatory = $true)]         [Array]$WaveHostnames,                  [Parameter(Mandatory = $false)]         [string]$WinCSKey = "F33E0C8E002",                  [Parameter(Mandatory = $false)]         [bool]$DryRun = $false     )          # Konfiguration des geplanten Tasks für WinCsFlags.exe     $TaskName = "SecureBoot-WinCS-Apply"     $TaskPath = "\Microsoft\Windows\SecureBoot\"     $TaskDescription = "Applies Secure Boot configuration via WinCS – Key: $WinCSKey"          Write-Log "Deploying WinCS GPO: $GPOName" "WAVE" (Bereitstellen des WinCS-GPO: $GPOName)     Write-Log "Task wird ausgeführt: WinCsFlags.exe /apply --key '"$WinCSKey'"" "INFO"     Write-Log "Trigger: Beim Systemstart (wird einmal als SYSTEM ausgeführt)" "INFO"          if ($DryRun) {         Write-Log "[DRYRUN] Würde GPO erstellen: $GPOName" "INFO"         Write-Log "[DRYRUN] Würde eine Sicherheitsgruppe erstellen: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Würde $(@($WaveHostnames) hinzufügen. Anzahl) zu gruppierende Computer" "INFO"         Write-Log "[DRYRUN] Würde einen geplanten Task bereitstellen: $TaskName" "INFO"         Write-Log "[DRYRUN] Würde gruppenrichtlinienobjekt mit: $TargetOU" "INFO" verknüpfen         return @{             Erfolg = $true             GPOCreated = $false             GroupCreated = $false             ComputerAdded = 0         }     }          try {         # Erforderliche Module importieren         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } catch {         Write-Log "Fehler beim Importieren erforderlicher Module (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Fehler = $_. Exception.Message }     }          # Schritt 1: Erstellen oder Abrufen eines Gruppenrichtlinienobjekts     $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     if ($gpo) {         Write-Log "GPO ist bereits vorhanden: $GPOName" "INFO"     } else {         try {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot WinCS Deployment - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Erstelltes Gruppenrichtlinienobjekt: $GPOName" "OK"         } catch {             Write-Log "Fehler beim Erstellen des Gruppenrichtlinienobjekts: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Fehler = $_. Exception.Message }         }     }          # Schritt 2: Erstellen eines XML-Vorgangs für die GPO-Bereitstellung     # Dadurch wird eine Aufgabe erstellt, die WinCsFlags.exe /apply beim Start ausführt.     $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">   <RegistrationInfo->     <description>$TaskDescription</Description>     WinCsFlags.exe1 Author>SYSTEM</Author>   WinCsFlags.exe5 /RegistrationInfo>   WinCsFlags.exe7 triggers>     WinCsFlags.exe9 BootTrigger->       <Aktiviert>true</Enabled>       <verzögerung>PT5M</Delay>     </BootTrigger>   </Trigger>   <prinzipale>     <Principal id="Author">       <UserId>S-1-5-18</UserId>       <RunLevel>höchsten verfügbaren</RunLevel>     </Principal>   </Principals>  > für <-Einstellungen     <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>     <Enabled>true</Enabled>     <Ausgeblendet>false</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 Priority>7</Priority>   WinCsFlags.exe27 /Settings>   WinCsFlags.exe29 Actions Context="Author"WinCsFlags.exe30     WinCsFlags.exe31 Exec>       WinCsFlags.exe33 Command>WinCsFlags.exe</Command>       WinCsFlags.exe37 Argumente>/apply --key "$WinCSKey"WinCsFlags.exe39 /Arguments>     WinCsFlags.exe41 /Exec>   WinCsFlags.exe43 /Actions> WinCsFlags.exe45 /Task> " @

    # Step 3: Deploy scheduled task via GPO Preferences     # Speichern von Aufgaben-XML in SYSVOL für GPO Scheduled Tasks Immediate Task     try {         $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         }         # Erstellen von ScheduledTasks.xml für 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->           <description>$TaskDescription</Description>         </RegistrationInfo>        > für <-Prinzipale           <Principal id="Author">             <UserId>NT AUTHORITY\System</UserId>             <LogonType>S4U</LogonType>             <RunLevel>highestAvailable</RunLevel>           </Principal>         </Principals>        > für <-Einstellungen           <IdleSettings>             <Dauer>PT5M</Dauer>             <WaitTimeout>PT1H</WaitTimeout>             <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>           <Aktiviert>true</Enabled>           <Hidden>false</Hidden>           <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>           <Priority>7</Priority>           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>         </Settings>         <triggers>           <TimeTrigger->             <StartBoundary>$(Get-Date -Format 'yyyyy-MM-dd')T00:00:00</StartBoundary>             <Aktiviert>true</Enabled>           </TimeTrigger>         </Trigger>        > für <-Aktionen           <Exec>             <Command>WinCsFlags.exe</Command>             <Argumente>/apply --key "$WinCSKey"</Arguments>           </Exec>         </Actions>       </Task>     </Properties>   </ImmediateTaskV2-> ></ScheduledTasks "@         $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force         Write-Log "Bereitgestellte geplante Aufgabe für GPO: $TaskName" "OK"     } catch {         Write-Log "Fehler beim Bereitstellen der XML-Datei für geplante Aufgaben: $($_. Exception.Message)" "WARN"         Write-Log "Fallback zur registrierungsbasierten WinCS-Bereitstellung" "INFO"         # Fallback: Verwenden Sie den WinCS-Registrierungsansatz, wenn die geplante GPP-Aufgabe fehlschlägt.         # WinCS kann auch über den Registrierungsschlüssel ausgelöst werden         # (Die Implementierung hängt von der WinCS-Registrierungs-API ab, falls verfügbar)     }     # Schritt 4: Erstellen oder Abrufen einer Sicherheitsgruppe     $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-not $group) {         try {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Description "Computer, die für winCS-Rollout für sicheren Start vorgesehen sind – $GPOName" '                 -Passthru             Write-Log "Sicherheitsgruppe erstellt: $SecurityGroupName" "OK"         } catch {             Write-Log "Fehler beim Erstellen der Sicherheitsgruppe: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Fehler = $_. Exception.Message }         }     } else {         Write-Log "Sicherheitsgruppe vorhanden: $SecurityGroupName" "INFO"     }     # Schritt 5: Hinzufügen von Computern zur Sicherheitsgruppe     $added = 0     $failed = 0     foreach ($hostname in $WaveHostnames) {         try {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } catch {             $failed++         }     }     Write-Log "$added Computer zur Sicherheitsgruppe hinzugefügt ($failed in AD nicht gefunden)" "OK"     # Schritt 6: Konfigurieren der Sicherheitsfilterung für GPO     try {         Set-GPPermission -Name $GPOName -TargetName "Authentifizierte Benutzer" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Konfigurierte Sicherheitsfilterung für: $SecurityGroupName" "OK"     } catch {         Write-Log "Fehler beim Konfigurieren der Sicherheitsfilterung: $($_. Exception.Message)" "WARN"     }     # Schritt 7: Verknüpfen des Gruppenrichtlinienobjekts mit der Organisationseinheit     if ($TargetOU) {         try {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }             if (-not $existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Ja -ErrorAction Stop                 Write-Log "Verknüpftes Gruppenrichtlinienobjekt: $TargetOU" "OK"             } else {                 Write-Log "GPO bereits mit Ziel-ORGANISATIONSeinheit verknüpft" "INFO"             }         } catch {             Write-Log "KRITISCH: Fehler beim Verknüpfen des Gruppenrichtlinienobjekts mit oe: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Error = "GPO link failed: $($_. Exception.Message)" }         }     }     Write-Log "WinCS-GPO-Bereitstellung abgeschlossen" "OK"     Write-Log "Computer werden bei der nächsten GPO-Aktualisierung WinCsFlags.exe ausgeführt + Neustart/Start" "INFO"     return @{         Erfolg = $true         GPOCreated = $true         GroupCreated = $true         ComputersAdded = $added         ComputersFailed = $failed     } }                                                                                        

# Wrapper function to maintain compatibility with main loop Funktion Deploy-WinCSForWave {     param(         [Parameter(Mandatory = $true)]         [Array]$WaveHostnames,         [Parameter(Mandatory = $false)]         [string]$WinCSKey = "F33E0C8E002",         [Parameter(Mandatory = $false)]         [string]$WavePrefix = "SecureBoot-Rollout",         [Parameter(Mandatory = $false)]         [int]$WaveNumber = 1,         [Parameter(Mandatory = $false)]         [Zeichenfolge]$TargetOU,         [Parameter(Mandatory = $false)]         [bool]$DryRun = $false     )     $gpoName = "${WavePrefix}-WinCS-Wave${WaveNumber}"     $securityGroup = "${WavePrefix}-WinCS-Wave${WaveNumber}"     $result = Deploy-WinCSGPOForWave '         -GPOName $gpoName '         -TargetOU $TargetOU '         -SecurityGroupName $securityGroup '         -WaveHostnames $WaveHostnames '         -WinCSKey $WinCSKey '         -DryRun $DryRun     # Konvertieren in das erwartete Rückgabeformat     return @{         Erfolg = $result. Erfolg         Angewendet = $result. ComputerHinfügt         Übersprungen = 0         Failed = if ($result. ComputersFailed) { $result. ComputersFailed } else { 0 }         Ergebnisse = @()     } }                                                            

# ============================================================================ # TASKBEREITSTELLUNG AKTIVIEREN # ============================================================================ # Stellen Sie Enable-SecureBootUpdateTask.ps1 auf Geräten mit deaktivierter geplanter Aufgabe bereit.# Verwendet ein Gruppenrichtlinienobjekt mit einer sofort geplanten Aufgabe, die einmal ausgeführt wird.

function Deploy-EnableTaskGPO {     <#     . SYNOPSIS         Stellen Sie Enable-SecureBootUpdateTask.ps1 über eine geplante GPO-Aufgabe bereit.. BESCHREIBUNG         Erstellt ein Gruppenrichtlinienobjekt, das eine einmalige geplante Aufgabe bereitstellt, um die         Geplante Aufgabe "Secure-Boot-Update" auf Zielgeräten.. PARAMETER TargetOU         Organisationseinheit, mit der das Gruppenrichtlinienobjekt verknüpft werden soll.. PARAMETER TargetHostnames         Hostnamen von Geräten mit deaktivierter Aufgabe (aus Aggregationsbericht).. PARAMETER DryRun         Wenn true, wird nur protokolliert, was ausgeführt wird.#>     param(         [Parameter(Mandatory = $false)]         [Zeichenfolge]$TargetOU,                  [Parameter(Mandatory = $true)]         [Array]$TargetHostnames,                  [Parameter(Mandatory = $false)]         [bool]$DryRun = $false     )          $GPOName = "SecureBoot-EnableTask-Remediation"     $SecurityGroupName = "SecureBoot-EnableTask-Devices"     $TaskName = "SecureBoot-EnableTask-OneTime"     $TaskDescription = "Einmalige Aufgabe zum Aktivieren des geplanten Tasks für Secure-Boot-Update"          Write-Log "=" * 70 "INFO"     Write-Log "DEPLOYING ENABLE TASK REMEDIATION" "INFO"     Write-Log "=" * 70 "INFO"     Write-Log "Zielgeräte: $($TargetHostnames.Count)" "INFO"     Write-Log "GPO: $GPOName" "INFO"     Write-Log "Sicherheitsgruppe: $SecurityGroupName" "INFO"          if ($DryRun) {         Write-Log "[DRYRUN] Würde GPO erstellen: $GPOName" "INFO"         Write-Log "[DRYRUN] Würde Sicherheitsgruppe erstellen: $SecurityGroupName" "INFO"         Write-Log "[DRYRUN] Würde $($TargetHostnames.Count)-Computer zur Gruppe hinzufügen" "INFO"         Write-Log "[DRYRUN] Würde einmalige geplante Aufgabe bereitstellen, um Secure-Boot-Update zu aktivieren" "INFO"         Write-Log "[DRYRUN] Würde gruppenrichtlinienobjekt mit: $TargetOU" "INFO" verknüpfen         return @{             Erfolg = $true             ComputerAdded = 0             DryRun = $true         }     }          try {         # Erforderliche Module importieren         Import-Module GroupPolicy -ErrorAction Stop         Import-Module ActiveDirectory -ErrorAction Stop     } catch {         Write-Log "Fehler beim Importieren der erforderlichen Module: $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Fehler = $_. Exception.Message }     }          # Schritt 1: Erstellen oder Abrufen eines Gruppenrichtlinienobjekts     $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue     if ($gpo) {         Write-Log "GPO ist bereits vorhanden: $GPOName" "INFO"     } else {         try {             $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Task Enable Remediation - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')"             Write-Log "Erstelltes Gruppenrichtlinienobjekt: $GPOName" "OK"         } catch {             Write-Log "Fehler beim Erstellen des Gruppenrichtlinienobjekts: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Fehler = $_. Exception.Message }         }     }          # Schritt 2: Bereitstellen von XML für geplante Aufgaben in GPO SYSVOL     # Die Aufgabe führt einen PowerShell-Befehl aus, um den Task "Secure-Boot-Update" zu aktivieren.     try {         $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. ID)}\Machine\Preferences\ScheduledTasks"                  if (-not (Test-Path $sysvolPath)) {             New-Item -ItemType Directory -Path $sysvolPath -Force | Out-Null         }                  # PowerShell-Befehl zum Aktivieren des Tasks "Secure-Boot-Update"         $enableCommand = 'schtasks.exe /Change /TN "\Microsoft\Windows\PI\Secure-Boot-Update" /ENABLE 2>$null; if ($LASTEXITCODE -ne 0) { Get-ScheduledTask -TaskPath "\Microsoft\Windows\PI\" -TaskName "Secure-Boot-Update" -ErrorAction SilentlyContinue | Enable-ScheduledTask }'                  # Encode-Befehl für sichere XML-Einbettung         $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand))                  $taskGuid = [guid]::NewGuid(). ToString("B"). ToUpper()                  # GPP Scheduled Task XML – Sofortiger Task, der einmal ausgeführt wird         $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-tt 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->           <description>$TaskDescription</Description>         </RegistrationInfo>         <prinzipale>           <Principal id="Author">             <UserId>S-1-5-18</UserId>             <RunLevel>highestAvailable</RunLevel>           </Principal>         </Principals>        > für <-Einstellungen           <IdleSettings>             <Dauer>PT5M</Dauer>             <WaitTimeout>PT1H</WaitTimeout>             <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>           <Aktiviert>true</Enabled>           <Hidden>false</Hidden>           <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>           <Priority>7</Priority>           <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter>        ></Settings         <Aktionen>           <Exec>             <Command>powershell.exe</Command>             <Argumente>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments>           </Exec>         </Actions>       </Task>    ></Properties   </ImmediateTaskV2-> </ScheduledTasks> "@                  $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force         Write-Log "Einmalige geplante Aufgabe für GPO bereitgestellt: $TaskName" "OK"              } catch {         Write-Log "Fehler beim Bereitstellen der XML-Datei für geplante Aufgaben: $($_. Exception.Message)" "ERROR"         return @{ Success = $false; Fehler = $_. Exception.Message }     }          # Schritt 3: Erstellen oder Abrufen einer Sicherheitsgruppe     $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue     if (-not $group) {         try {             $group = New-ADGroup -Name $SecurityGroupName '                 -GroupCategory Security '                 -GroupScope DomainLocal '                 -Beschreibung "Computer mit deaktivierter Secure-Boot-Update-Aufgabe – für die Korrektur vorgesehen" '                 -Passthru             Write-Log "Sicherheitsgruppe erstellt: $SecurityGroupName" "OK"         } catch {             Write-Log "Fehler beim Erstellen der Sicherheitsgruppe: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Fehler = $_. Exception.Message }         }     } else {         Write-Log "Sicherheitsgruppe vorhanden: $SecurityGroupName" "INFO"     }          # Schritt 4: Hinzufügen von Computern zur Sicherheitsgruppe     $added = 0     $failed = 0     foreach ($hostname in $TargetHostnames) {         try {             $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop             Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue             $added++         } catch {             $failed++             Write-Log "Computer in AD nicht gefunden: $hostname" "WARN"         }     }     Write-Log "$added Computer zur Sicherheitsgruppe hinzugefügt ($failed in AD nicht gefunden)" "OK"          # Schritt 5: Konfigurieren der Sicherheitsfilterung für GPO     try {         Set-GPPermission -Name $GPOName -TargetName "Authentifizierte Benutzer" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue         Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop         Write-Log "Konfigurierte Sicherheitsfilterung für: $SecurityGroupName" "OK"     } catch {         Write-Log "Fehler beim Konfigurieren der Sicherheitsfilterung: $($_. Exception.Message)" "WARN"     }          # Schritt 6: Verknüpfen des Gruppenrichtlinienobjekts mit der Organisationseinheit     if ($TargetOU) {         try {             $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue |                  Select-Object -ExpandProperty GpoLinks |                  Where-Object { $_. DisplayName -eq $GPOName }                          if (-not $existingLink) {                 New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Ja -ErrorAction Stop                 Write-Log "Verknüpftes Gruppenrichtlinienobjekt: $TargetOU" "OK"             } else {                 Write-Log "GPO bereits mit Ziel-ORGANISATIONSeinheit verknüpft" "INFO"             }         } catch {             Write-Log "Fehler beim Verknüpfen des Gruppenrichtlinienobjekts mit der Organisationseinheit: $($_. Exception.Message)" "ERROR"             return @{ Success = $false; Error = "GPO link failed: $($_. Exception.Message)" }         }     } else {         Write-Log "Kein TargetOU angegeben – GPO muss manuell verknüpft werden" "WARN"     }          Write-Log "" "INFO"     Write-Log "TASKBEREITSTELLUNG ABGESCHLOSSEN AKTIVIEREN" "OK"     Write-Log "Geräte führen die Aktivierungsaufgabe bei der nächsten GPO-Aktualisierung (gpupdate)" "INFO" aus.     Write-Log "Der Task wird einmal als SYSTEM ausgeführt und aktiviert Secure-Boot-Update" "INFO"     Write-Log "" "INFO"          return @{         Erfolg = $true         ComputersAdded = $added         ComputersFailed = $failed         GPOName = $GPOName         SecurityGroup = $SecurityGroupName     } }

# ============================================================================ # TASK AUF DEAKTIVIERTEN GERÄTEN AKTIVIEREN # ============================================================================ if ($EnableTaskOnDisabled) {     Write-Host ""     Write-Host ("=" * 70) -ForegroundColor Yellow     Write-Host "ENABLE TASK REMEDIATION – Fix Disabled Scheduled Tasks" -ForegroundColor Yellow     Write-Host ("=" * 70) -ForegroundColor Yellow     Write-Host ""     # Suchen von Geräten mit deaktivierter Aufgabe aus Aggregationsdaten     if (-not $AggregationInputPath) {         Write-Host "ERROR: -AggregationInputPath is required to identify devices with disabled task" -ForegroundColor Red         Write-Host "Usage: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <path> -ReportBasePath <path>" -ForegroundColor Gray         Ausgang 1     }     Write-Host "Suchen nach Geräten mit deaktiviertem Secure-Boot-Update-Task..." -ForegroundColor Cyan     # Laden von JSON-Dateien und Suchen von Geräten mit deaktivierter Aufgabe     $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                  Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" }     $disabledTaskDevices = @()     foreach ($file in $jsonFiles) {         try {             $device = Get-Content $file. FullName -Raw | ConvertFrom-Json             , wenn ($device. SecureBootTaskEnabled -eq $false -or                 $device. SecureBootTaskStatus -eq 'Disabled' -or                 $device. SecureBootTaskStatus -eq 'NotFound') {                 # Nur Geräte einschließen, die noch nicht aktualisiert wurden (kein Ereignis 1808)                 , wenn ([int]$device. Event1808Count -eq 0) {                     $disabledTaskDevices += $device. Hostname                 }             }         } catch {             # Ungültige Dateien überspringen         }     }     $disabledTaskDevices = $disabledTaskDevices | Select-Object -Unique     if ($disabledTaskDevices.Count -eq 0) {         Write-Host ""         Write-Host "Keine Geräte mit deaktivierter Secure-Boot-Update-Aufgabe gefunden." -ForegroundColor Green         Write-Host "Auf allen Geräten ist die Aufgabe entweder aktiviert oder bereits aktualisiert." -ForegroundColor Gray         Exit 0     }     Write-Host ""     Write-Host "Gefundene $($disabledTaskDevices.Count)-Geräte mit deaktivierter Aufgabe:" -ForegroundColor Yellow     $disabledTaskDevices | Select-Object -Erste 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray }     if ($disabledTaskDevices.Count -gt 20) {         Write-Host " ... und $($disabledTaskDevices.Count - 20) more" -ForegroundColor Gray     }     Write-Host ""     # Bereitstellen des Aufgaben-GPO aktivieren     $result = Deploy-EnableTaskGPO -TargetHostnames $disabledTaskDevices -TargetOU $TargetOU -DryRun $DryRun     if ($result. Erfolg) {         Write-Host ""         Write-Host "SUCCESS: Enable Task GPO deployed" -ForegroundColor Green         Write-Host " Zur Sicherheitsgruppe hinzugefügte Computer: $($result. ComputersAdded)" -ForegroundColor Cyan         if ($result. ComputersFailed -gt 0) {             Write-Host " Computer in AD nicht gefunden: $($result. ComputersFailed)" -ForegroundColor Yellow         }         Write-Host ""         Write-Host "NEXT STEPS:" -ForegroundColor White         Write-Host " 1.                                              Geräte erhalten das Gruppenrichtlinienobjekt bei der nächsten Aktualisierung (gpupdate /force)" -ForegroundColor Gray         Write-Host " 2. Die einmalige Aufgabe aktiviert Secure-Boot-Update" -ForegroundColor Gray         Write-Host " 3. Aggregation erneut ausführen, um zu überprüfen, ob der Task jetzt aktiviert ist" -ForegroundColor Gray     } else {         Write-Host ""         Write-Host "FAILED: Enable Task GPO konnte nicht bereitgestellt werden" -ForegroundColor Red         Write-Host "Error: $($result. Error)" -ForegroundColor Red     }          Exit 0 }

# ============================================================================ # HAUPTORCHESTRIERUNGSSCHLEIFE # ============================================================================

Write-Host "" Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host "SECURE BOOT ROLLOUT ORCHESTRATOR – CONTINUOUS DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host ""

if ($DryRun) {     Write-Host "[DRY RUN MODE]" -ForegroundColor Magenta }

if ($UseWinCS) {     Write-Host "[WinCS MODE]" -ForegroundColor Yellow     Write-Host "Verwenden von WinCsFlags.exe anstelle von GPO/AvailableUpdatesPolicy" -ForegroundColor Yellow     Write-Host "WinCS Key: $WinCSKey" -ForegroundColor Gray     Write-Host "" }

Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "Eingabepfad: $AggregationInputPath" "INFO" Write-Log "Berichtspfad: $ReportBasePath" "INFO" if ($UseWinCS) {     Write-Log "Bereitstellungsmethode: WinCS (WinCsFlags.exe /apply --key '"$WinCSKey'")" "INFO" } else {     Write-Log "Bereitstellungsmethode: GPO (AvailableUpdatesPolicy)" "INFO" }

# Resolve TargetOU - default to domain root for domain-wide coverage # Nur für die GPO-Bereitstellungsmethode erforderlich (WinCS erfordert kein AD/GPO) if (-not $UseWinCS -and -not $TargetOU) {     try {         # Probieren Sie mehrere Methoden aus, um Domänen-DN abzurufen.         $domainDN = $null         # Methode 1: Get-ADDomain (erfordert RSAT-AD-PowerShell)         try {             Import-Module ActiveDirectory -ErrorAction Stop             $domainDN = (Get-ADDomain -ErrorAction Stop). Distinguishedname         } catch {             Write-Log "Get-ADDomain failed: $($_. Exception.Message)" "WARN"         }         # Methode 2: Verwenden von RootDSE über ADSI         if (-not $domainDN) {             try {                 $rootDSE = [ADSI]"LDAP://RootDSE"                 $domainDN = $rootDSE.defaultNamingContext.ToString()             } catch {                 Write-Log "ADSI RootDSE failed: $($_. Exception.Message)" "WARN"             }         }         # Methode 3: Analysieren aus der Domänenmitgliedschaft des Computers         if (-not $domainDN) {             try {                 $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()                 $domainDN = "DC=" + ($domain. Name -replace '\.', ',DC=')             } catch {                 Write-Log "GetComputerDomain failed: $($_. Exception.Message)" "WARN"             }         }         if ($domainDN) {             $TargetOU = $domainDN             Write-Log "Ziel: Domänenstamm ($domainDN) – GPO wird domänenweit über Sicherheitsgruppenfilterung angewendet" "INFO"         } else {             Write-Log "Domänen-DN konnte nicht ermittelt werden – GPO wird erstellt, aber NICHT VERKNÜPFT!" "ERROR"             Write-Log "Geben Sie den Parameter -TargetOU an, oder verknüpfen Sie das Gruppenrichtlinienobjekt nach der Erstellung manuell" "ERROR".             $TargetOU = $null         }     } catch {         Write-Log "Domänen-DN konnte nicht abgerufen werden : GPO wird erstellt, aber nicht verknüpft.                                     Bei Bedarf manuell verknüpfen." "WARN"         Write-Log "Error: $($_. Exception.Message)" "WARN"         $TargetOU = $null     } } else {     Write-Log "Ziel-ORGANISATIONSeinheit: $TargetOU" "INFO" }

Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Abrufintervall: $PollIntervalMinutes Minuten" "INFO" if ($LargeScaleMode) {     Write-Log "LargeScaleMode aktiviert (Batchgröße: $ProcessingBatchSize, Protokollbeispiel: $DeviceLogSampleSize)" "INFO" }

# ============================================================================ # VORAUSSETZUNGSPRÜFUNG: Überprüfen, ob die Erkennung bereitgestellt wurde und funktioniert # ============================================================================

Write-Host "" Write-Log "Überprüfen der Voraussetzungen..." "INFO"

$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath if (-not $detectionCheck.IsDeployed) {     Write-Log $detectionCheck.Message "ERROR"     Write-Host ""     Write-Host "REQUIRED: Deploy detection infrastructure first:" -ForegroundColor Yellow     Write-Host " 1. Ausführen: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\server\SecureBootLogs$'" -ForegroundColor Cyan     Write-Host " 2. Warten, bis Geräte melden (12-24 Stunden)" -ForegroundColor Cyan     Write-Host " 3. Führen Sie diesen Orchestrator erneut aus" -ForegroundColor Cyan     Write-Host ""     if (-not $DryRun) {         Rückgabe     } } else {     Write-Log $detectionCheck.Message "OK" }

# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Daten aktuell: $($freshness. TotalFiles)-Dateien, $($freshness. FreshFiles) fresh (<24h), $($freshness. StaleFiles) veraltet (>72h)" "INFO" if ($freshness. Warnung) {     Write-Log $freshness. Warnung "WARN" }

# Load Allow List (targeted rollout - ONLY these devices will be rolled out) $allowedHostnames = @() if ($AllowListPath -or $AllowADGroup) {     $allowedHostnames = Get-AllowedHostnames -AllowFilePath $AllowListPath -ADGroupName $AllowADGroup     if ($allowedHostnames.Count -gt 0) {         Write-Log "AllowList: NUR $($allowedHostnames.Count)-Geräte werden für den Rollout berücksichtigt" "INFO"     } else {         Write-Log "AllowList angegeben, aber keine Geräte gefunden – dadurch werden alle Rollouts blockiert!" "WARN"     } }

# Load VIP/exclusion list (BlockList) $excludedHostnames = @() if ($ExclusionListPath -or $ExcludeADGroup) {     $excludedHostnames = Get-ExcludedHostnames -ExclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup     if ($excludedHostnames.Count -gt 0) {         Write-Log "VIP-Ausschluss: $($excludedHostnames.Count)-Geräte werden aus dem Rollout übersprungen" "INFO"     } }

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

if ($rolloutState.Status -eq "NotStarted") {     $rolloutState.Status = "InProgress"     $rolloutState.StartedAt = Get-Date -Format "yyyy-MM-tt HH:mm:ss"     Write-Log "Start des neuen Rollouts" "WAVE" }

Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "Blockierte Buckets: $($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 "=== ITERATION $iterationCount ===" "WAVE"     Write-Host ("=" * 80) -ForegroundColor White     # Schritt 1: Ausführen der Aggregation     Write-Log "Schritt 1: Ausführen der Aggregation..." "INFO"     # Orchestrator verwendet immer einen einzelnen Ordner (LargeScaleMode), um Datenträgerüberfrachtungen zu vermeiden.     # Administratoren, die den Aggregator ausführen, erhalten zeitstempelte Ordner für Zeitpunktmomentaufnahmen.     $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current"     # Überprüfen der Aktualität der Daten vor dem Aggregieren     $freshness = Get-DataFreshness -JsonPath $AggregationInputPath     Write-Log "Daten aktuell: $($freshness. FreshFiles)/$($freshness. TotalFiles) Geräte, die in den letzten 24h gemeldet wurden" "INFO"     if ($freshness. Warnung) {         Write-Log $freshness. Warnung "WARN"     }     $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1"     $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json"     $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json"     if (Testpfad $aggregateScript) {         if (-not $DryRun) {             # Orchestrator verwendet aus Effizienzgründen immer Streaming + inkrementell.             # Der Aggregator erhöht automatisch auf PS7, falls verfügbar, um eine optimale Leistung zu erzielen.             $aggregateParams = @{                 InputPath = $AggregationInputPath                 OutputPath = $aggregationPath                 StreamingMode = $true                 IncrementalMode = $true                 SkipReportIfUnchanged = $true                 ParallelThreads = 8             }             # Rolloutzusammenfassung bestehen, falls vorhanden (für Geschwindigkeits-/Projektionsdaten)             if (Testpfad $rolloutSummaryPath) {                 $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath             }             & $aggregateScript @aggregateParams             # Befehl anzeigen zum Generieren vollständiger HTML-Dashboard mit Gerätetabellen             Write-Host ""             Write-Host "Um vollständige HTML-Dashboard mit Hersteller-/Modelltabellen zu generieren, führen Sie aus:" -ForegroundColor Yellow             Write-Host " $aggregateScript -InputPath '"$AggregationInputPath'" -OutputPath '"$aggregationPath'"" -ForegroundColor Yellow             Write-Host ""         } else {             Write-Log "[DRYRUN] würde Aggregation ausführen" "INFO"             # Verwenden Sie in DryRun vorhandene Aggregationsdaten aus ReportBasePath direkt.             $aggregationPath = $ReportBasePath         }     }     $rolloutState.LastAggregation = Get-Date -Format "yyyy-MM-tt HH:mm:ss"     # Schritt 2: Laden des aktuellen Geräts status     Write-Log "Schritt 2: Laden des Geräts status..." "INFO"     $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue |          Where-Object { $_. Name -notlike "*Buckets*" } |          Sort-Object LastWriteTime -Descending |          Select-Object -First 1     if (-not $notUptodateCsv -and -not $DryRun) {         Write-Log "Keine Aggregationsdaten gefunden.                                            Warten..." "WARN"         Start-Sleep – Sekunden ($PollIntervalMinutes * 60)         Weiter     }     $notUpdatedDevices = if ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } else { @() }     Write-Log "Geräte nicht aktualisiert: $($notUpdatedDevices.Count)" "INFO"     $notUpdatedIndexes = Get-NotUpdatedIndexes -Devices $notUpdatedDevices     # Schritt 3: Aktualisieren des Geräteverlaufs (Nachverfolgung nach Hostnamen)     Write-Log "Schritt 3: Aktualisieren des Geräteverlaufs..." "INFO"     Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory     Save-DeviceHistory -History $deviceHistory     # Schritt 4: Überprüfen auf blockierte Buckets (nicht erreichbare Geräte)     $existingBlockedCount = $blockedBuckets.Count     Write-Log "Schritt 4: Überprüfen auf blockierte Buckets (pingen von Geräten über den Wartezeitraum hinweg)..." "INFO"     if ($existingBlockedCount -gt 0) {         Write-Log "Derzeit blockierte Buckets aus vorherigen Ausführungen: $existingBlockedCount" "INFO"     }     if ($adminApproved.Count -gt 0) {         Write-Log "Admin genehmigte Buckets (werden nicht erneut blockiert): $($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 blockierte $blockedBuckets         Write-Log "Neu blockierte Buckets (diese Iteration): $($newlyBlocked.Count)" "BLOCKED"     }     # Schritt 4b: Automatisches Aufheben der Blockierung von Buckets, in denen Geräte aktualisiert wurden     $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize     if ($autoUnblocked.Count -gt 0) {         Save-BlockedBuckets blockierte $blockedBuckets         Write-Log "Automatisch freigegebene Buckets (Geräte aktualisiert): $($autoUnblocked.Count)" "OK"     }     # Schritt 5: Berechnen der verbleibenden berechtigten Geräte     $eligibleCount = 0     foreach ($device in $notUpdatedDevices) {         $bucketKey = Get-BucketKey $device         if (-not $blockedBuckets.Contains($bucketKey)) {             $eligibleCount++         }     }     Write-Log "Verbleibende berechtigte Geräte: $eligibleCount" "INFO"     Write-Log "Blockierte Buckets: $($blockedBuckets.Count)" "INFO"     # Schritt 6: Überprüfen des Abschlusses     if ($eligibleCount -eq 0) {         Write-Log "ROLLOUT ABGESCHLOSSEN – Alle berechtigten Geräte aktualisiert!" "OK"         $rolloutState.Status = "Completed"         $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-tt HH:mm:ss"         Save-RolloutState -State $rolloutState         Brechen     }     # Schritt 6: Generieren und Bereitstellen der nächsten Welle     Write-Log "Schritt 6: Generieren einer Rolloutwelle..." "INFO"     $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames     # Überprüfen Sie, ob Geräte bereitgestellt werden müssen ($waveDevices kann $null, leer oder mit tatsächlichen Geräten sein)     $hasDevices = $waveDevices -and @($waveDevices | Where-Object { $_ }). Anzahl -gt 0     if ($hasDevices) {         # Nur inkrementieren der Wellenzahl, wenn tatsächlich Geräte bereitgestellt werden müssen         $rolloutState.CurrentWave++         Write-Log "Wave $($rolloutState.CurrentWave): $(@($waveDevices). Anzahl) devices" "WAVE"         # Bereitstellen des Gruppenrichtlinienobjekts mithilfe der Inlinefunktion         $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)"         $hostnames = @($waveDevices | ForEach-Object {             if ($_. Hostname) { $_. Hostname } elseif ($_. HostName) { $_. HostName } else { $null }         } | Where-Object { $_ })         # Hostnamendatei zur Referenz/Überwachung speichern         $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt"         $hostnames | Out-File $hostnamesFile -Encoding UTF8         Überprüfen Sie, ob hostnamen für die Bereitstellung vorhanden sind.         if ($hostnames. Count -eq 0) {             Write-Log "Keine gültigen Hostnamen in Welle "$($rolloutState.CurrentWave) gefunden – Geräte fehlen möglicherweise Hostnamen-Eigenschaft" "WARN"             Write-Log "Überspringen der Bereitstellung für diese Welle – Überprüfen der Gerätedaten" "WARN"             # Warten Sie noch vor der nächsten Iteration             if (-not $DryRun) {                 Write-Log "Schlafen für $PollIntervalMinutes Minuten, bevor es erneut versuchen..." "INFO"                 Start-Sleep – Sekunden ($PollIntervalMinutes * 60)             }             Weiter         }         Write-Log "Deploying to $($hostnames. Anzahl) Hostnamen in Wave $($rolloutState.CurrentWave)" "INFO"         # Bereitstellen mithilfe der WinCS- oder GPO-Methode basierend auf dem Parameter "-UseWinCS"         if ($UseWinCS) {             # WinCS-Methode: Erstellen eines Gruppenrichtlinienobjekts mit geplanter Aufgabe zum Ausführen WinCsFlags.exe als SYSTEM auf jedem Endpunkt             Write-Log "Verwenden der WinCS-Bereitstellungsmethode (Schlüssel: $WinCSKey)" "WAVE"             $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames '                 -WinCSKey $WinCSKey '                 -WavePrefix $WavePrefix '                 -WaveNumber $rolloutState.CurrentWave '                 -TargetOU $TargetOU '                 -DryRun:$DryRun             if (-not $wincsResult.Success) {                 Write-Log "Fehler bei der WinCS-Bereitstellung – Angewendet: $($wincsResult.Applied), Failed: $($wincsResult.Failed)" "WARN"             } else {                 Write-Log "WinCS deployment successful - Applied: $($wincsResult.Applied), Skipped: $($wincsResult.Skipped)" "OK"             }             # WinCS-Ergebnisse für die Überwachung speichern             $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json"             $wincsResult | ConvertTo-Json -Tiefe 5 | Out-File $wincsResultFile -Encoding UTF8         } else {             # GPO-Methode: Erstellen eines Gruppenrichtlinienobjekts mit der Registrierungseinstellung AvailableUpdatesPolicy             $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun             if (-not $gpoResult) {                 Write-Log "GPO-Bereitstellung fehlgeschlagen – nächste Iteration wird wiederholt" "ERROR"             }         }         # Aufzeichnen der Welle im Zustand         $waveRecord = @{             WaveNumber = $rolloutState.CurrentWave             StartedAt = Get-Date -Format "yyyy-MM-tt HH:mm:ss"             DeviceCount = @($waveDevices). Count             Geräte = @($waveDevices | ForEach-Object {                 @{                     Hostname = if ($_. Hostname) { $_. Hostname } elseif ($_. HostName) { $_. HostName } else { $null }                     BucketKey = Get-BucketKey $_                 }             })         }         # Stellen Sie sicher, dass WaveHistory vor dem Anfügen immer ein Array ist (verhindert Probleme bei der Zusammenführung mit Hashtabellen).         $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord)         $rolloutState.TotalDevicesTargeted += @($waveDevices). Count         Save-RolloutState -State $rolloutState         Write-Log "Wave $($rolloutState.CurrentWave) bereitgestellt.                                                                                                                                                                                        Warten $PollIntervalMinutes Minuten..." "OK"     } else {         # Anzeigen status bereitgestellter Geräte, die auf Updates warten         Write-Log "" "INFO"         Write-Log "========== ALLE BEREITGESTELLTEN GERÄTE – WARTEN AUF STATUS ==========" "INFO"         # Abrufen aller bereitgestellten Geräte aus dem Wellenverlauf         $allDeployedLookup = @{}         foreach ($wave in $rolloutState.WaveHistory) {             foreach ($device in $wave. Geräte) {                 , wenn ($device. Hostname) {                     $allDeployedLookup[$device. Hostname] = @{                         Hostname = $device. Hostname                         BucketKey = $device. BucketKey                         DeployedAt = $wave. StartedAt                         WaveNumber = $wave. WaveNumber                     }                 }             }         }         $allDeployedDevices = @($allDeployedLookup.Values)         if ($allDeployedDevices.Count -gt 0) {             # Ermitteln, welche bereitgestellten Geräte noch ausstehen (in der Liste 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++                 }             }             # Abrufen der tatsächlichen aktualisierten Anzahl von Aggregationen – Unterscheiden Von Ereignis 1808 und UEFICA2023Status             $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Summary*.csv" |                  Sort-Object LastWriteTime -Descending | Select-Object -First 1             $actualUpdated = 0             $totalDevicesFromSummary = 0             $event 1808Count = 0             $uefiStatusUpdated = 0             $needsRebootSample = @()             if ($summaryCsv) {                 $summary = Import-Csv $summaryCsv.FullName | Select-Object -First 1                 if ($summary. Aktualisiert) { $actualUpdated = [int]$summary. Aktualisiert }                 if ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices }             }             # Berechnen der Geschwindigkeit aus dem Wellenverlauf (Geräte werden pro Tag aktualisiert)             $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                 }             }             # Speichern der Rolloutzusammenfassung mit wochenendfähigen Projektionen             # Verwenden Sie die NotUptodate-Anzahl des Aggregators (ohne SB OFF-Geräte) für die Konsistenz.             $notUpdatedCount = if ($summary -and $summary. NotUptodate) { [int]$summary. NotUptodate } else { $totalDevicesFromSummary - $actualUpdated }             Save-RolloutSummary -State $rolloutState '                 -TotalDevices $totalDevicesFromSummary '                 -UpdatedDevices $actualUpdated '                 -NotUpdatedDevices $notUpdatedCount '                 -DevicesPerDay $devicesPerDay             # Überprüfen von Rohdaten für Geräte mit UEFICA2023Status=Updated, aber ohne Ereignis 1808 (Neustart erforderlich)             $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue             $totalDataFiles = @($dataFiles). Count             $batchSize = [Math]::Max(500, $ProcessingBatchSize)             if ($LargeScaleMode) {                 $batchSize = [Math]::Max(2000, $ProcessingBatchSize)             }                                                                                   

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

                    foreach ($file in $batchFiles) {                         try {                             $deviceData = Get-Content $file. FullName -Raw | ConvertFrom-Json                             $hostname = $deviceData.Hostname                             if (-not $hostname) { continue }                             $has 1808 = [int]$deviceData.Event1808Count -gt 0                             $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Updated"                             if ($has 1808) {                                 $event 1808Count++                             } elseif ($hasUefiUpdated) {                                 $uefiStatusUpdated++                                 if ($needsRebootSample.Count -lt $DeviceLogSampleSize) {                                     $needsRebootSample += $hostname                                 }                             }                         } catch { }                     }                                                          

                    Save-ProcessingCheckpoint -Stage "RebootStatusScan" -Processed ($end + 1) -Total $totalDataFiles -Metrics @{                         Event1808Count = $event 1808Count                         UEFIUpdatedAwaitingReboot = $uefiStatusUpdated                     }                 }             }             Write-Log "Insgesamt bereitgestellt: $($allDeployedDevices.Count)" "INFO"             Write-Log "Aktualisiert (Ereignis 1808 bestätigt): $event 1808Count" "OK"             if ($uefiStatusUpdated -gt 0) {                 Write-Log "Updated (UEFICA2023Status=Updated, awaiting reboot): $uefiStatusUpdated" "OK"                 $rebootSuffix = if ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) more)" } else { "" }                 Write-Log "Geräte, die für Ereignis 1808 neu gestartet werden müssen (Beispiel): $($needsRebootSample -join ', ')$rebootSuffix" "INFO"                 Write-Log "Diese Geräte melden Ereignis 1808 nach dem nächsten Neustart" "INFO"             }             Write-Log "Nicht mehr ausstehend: $noLongerPendingCount (enthält SecureBoot OFF, fehlende Geräte)" "INFO"             Write-Log "Awaiting status: $stillPendingCount" "INFO"             if ($stillPendingCount -gt 0) {                 $pendingSuffix = if ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) more)" } else { "" }                 Write-Log "Ausstehende Geräte (Beispiel): $($pendingSample -join ', ')$pendingSuffix" "WARN"             }         } else {             Write-Log "Es wurden noch keine Geräte bereitgestellt" "INFO"         }         Write-Log "================================================================" "INFO"         Write-Log "" "INFO"     }     # Warten Sie vor der nächsten Iteration     if (-not $DryRun) {         Write-Log "$PollIntervalMinutes Minuten schlafen..." "INFO"         Start-Sleep –Sekunden ($PollIntervalMinutes * 60)     } else {         Write-Log "[DRYRUN] Würde $PollIntervalMinutes Minuten warten" "INFO"         break # Beenden nach einer Iteration im Probelauf     } }                               

# ============================================================================ # ENDGÜLTIGE ZUSAMMENFASSUNG # ============================================================================

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 "Total Waves: $($finalState.CurrentWave)" Write-Host "Zielgeräte: $($finalState.TotalDevicesTargeted)" Write-Host "Blocked Buckets: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } else { "Green" }) Write-Host "Geräte nachverfolgt: $($deviceHistory.Count)" -ForegroundColor Gray Write-Host ""

if ($finalBlocked.Count -gt 0) {     Write-Host "BLOCKIERTE BUCKETS (manuelle Überprüfung erforderlich):" -ForegroundColor Red     foreach ($key in $finalBlocked.Keys) {         $info = $finalBlocked[$key]         Write-Host " - $key" -ForegroundColor Red         Write-Host " Grund: $($info. Reason)" -ForegroundColor Gray     }     Write-Host ""     Write-Host "Datei mit blockierten Buckets: $blockedBucketsPath" -ForegroundColor Yellow }

Write-Host "" Write-Host "Zustandsdateien:" -ForegroundColor Cyan Write-Host "Rolloutstatus: $rolloutStatePath" Write-Host "Blockierte Buckets: $blockedBucketsPath" Write-Host "Geräteverlauf: $deviceHistoryPath" Write-Host ""  

​​​​​​​

Benötigen Sie weitere Hilfe?

Möchten Sie weitere Optionen?

Erkunden Sie die Abonnementvorteile, durchsuchen Sie Trainingskurse, erfahren Sie, wie Sie Ihr Gerät schützen und vieles mehr.