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