Kopirajte i zalijepite tu oglednu skriptu i po potrebi izmijenite za svoje okruženje:
<# . SINOPSIS Orkestrator kontinuiranog uvođenja sigurnog pokretanja koji se pokreće dok se implementacija ne dovrši.
.DESCRIPTION Ova skripta pruža potpunu automatizaciju za izdavanje certifikata sigurnog pokretanja: 1. Generira valove za početak rada na temelju podataka o agregaciji 2. Stvara AD grupe i GPO za svaki val 3. Monitori za ažuriranja uređaja (događaj 1808) 4. Otkriva blokirane grupe (nedostupne uređaje) 5. Automatski se prebacuje na sljedeći val 6. Pokreće se dok se ne ažuriraju svi uređaji koji ispunjavaju uvjete Kriteriji dovršetka: - Nema uređaja preostalih u: Akcija Obavezno, Visoka pouzdanost, Promatranje, Privremeno pauzirano - Izvan dosega (po dizajnu): nije podržano, sigurno pokretanje onemogućeno - Neprekidno se pokreće do dovršetka – nema proizvoljnog ograničenja vala Strategija razvoja: - VISOKA POUZDANOST: svi uređaji u prvom valu (sigurni) - POTREBNA RADNJA: progresivne dvostruke akcije (1→2→4→8...) Logika blokiranja: - Nakon MaxWaitHours, orchestrator pings uređaje koji nisu ažurirani - Ako je uređaj UNREACHABLE (ping ne uspije) → grupa blokirana za istragu - Ako je uređaj REACHABLE, ali nije ažuriran → čekanja (možda je potrebno ponovno pokretanje) - Blokirane grupe su isključene dok ih administrator ne deblokira Automatsko deblokiranje: - Ako se uređaj u blokiranoj grupi kasnije prikazuje kao ažuriran (događaj 1808), grupa se automatski deblokira i nastavlja se - Time se rukuje uređajima koji su privremeno izvan mreže, ali su se vratili Praćenje uređaja: - prati uređaje prema nazivu glavnog računala (pretpostavlja da se nazivi ne mijenjaju tijekom postavljanja) - Napomena: JSON zbirka ne sadrži jedinstveni ID računala; dodajte ga radi boljeg praćenja
.PARAMETER AggregationInputPath Put do neobrađenih JSON podataka uređaja (iz skripte Otkrivanje)
.PARAMETER ReportBasePath Osnovni put za izvješća o agregaciji
.PARAMETER TargetOU Razlikovni naziv OU-a za povezivanje GPO-ova.Neobavezno – ako nije navedeno, GPO je povezan s korijenom domene radi pokrivenosti na razini domene.Filtriranje sigurnosnih grupa osigurava da samo ciljani uređaji primaju pravilnik.
.PARAMETER MaxWaitHours Sati čekanja ažuriranja uređaja prije provjere dostupnosti.Nakon tog vremena, uređajima koji nisu ažurirani ping se.Nedostupni uređaji uzrokuju blokiranje grupe.Zadano: 72 (3 dana)
.PARAMETER PollIntervalMinutes Minute između provjera stanja. Zadano: 1440 (1 dan)
.PARAMETER AllowListPath Put do datoteke koja sadrži nazive glavnog računala do mogućnosti ALLOW za rollout (ciljano rollout).Podržava .txt (jedan naziv glavnog računala po retku) ili .csv (s stupcem Naziv Računala/Naziv Računala).Kada je to navedeno, samo će ti uređaji biti uključeni u početak.BlockList je i dalje primijenjen nakon AllowList.
.PARAMETER AllowADGroup Naziv ad-sigurnosne grupe koja sadrži računalne račune za ALLOW.Primjer: "SecureBoot-Pilot-Computers" ili "Wave1-Devices" Kada je navedeno, samo uređaji u ovoj grupi bit će uključeni u rollout.Kombiniranje s allowListPathom za ciljanje na temelju datoteka i za AD.
.PARAMETER ExclusionListPath Put do datoteke koja sadrži nazive glavnog računala za ISKLJUČIVANJE iz primjene (VIP/izvršni uređaji).Podržava .txt (jedan naziv glavnog računala po retku) ili .csv (s stupcem Naziv Računala/Naziv Računala).Ti uređaji nikad neće biti obuhvaćeni valom za uvršćitenje.BlockList se primjenjuje NAKON allowList filtriranja. . PARAMETER ExcludeADGroup Naziv sigurnosne grupe ad-a koja sadrži račune računala koje treba izuzeti.Primjer: "VIP-Computers" ili "Executive-Devices" Kombinirajte s ExclusionListPath za izuzimanja na temelju datoteka i za AD.
.PARAMETER UseWinCS Koristite WinCS (Windows Configuration System) umjesto GPO/AvailableUpdatesPolicy.WinCS implementira omogućivanje sigurnog pokretanja pokretanjem WinCsFlags.exe izravno na svakoj krajnjoj točki.WinCsFlags.exe se u odjeljku KONTEKST SUSTAVA putem zakazanog zadatka.Ta je metoda korisna za: - brža uvodi (neposredni učinak u odnosu na čekanje na obradu GPO-a) - uređaji koji nisu pridruženi domeni - okruženja bez ad/GPO infrastrukture Referenca: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe
.PARAMETER WinCSKey Tipka WinCS koja se koristi za omogućivanje sigurnog pokretanja.Zadano: F33E0C8E002 Taj ključ odgovara konfiguraciji implementacije sigurnog pokretanja. . PARAMETAR DryRun Pokaži što će se učiniti bez promjene
.PARAMETER ListBlockedBuckets Prikaži sve trenutno blokirane grupe i izađi
.PARAMETER UnblockBucket Deblokiranje određene grupe prema ključu i izlaz
.PARAMETER UnblockAll Deblokiranje svih grupa i izlaz
.PARAMETER EnableTaskOnDisabled Implementacija Enable-SecureBootUpdateTask.ps1 svim uređajima s onemogućenim zakazanim zadatkom.Stvara GPO s jednokratno zakazanim zadatkom koji pokreće skriptu Omogući s mogućnošću -Quiet.To je korisno za popravak uređaja s onemogućenim zadatkom sigurnog pokretanja.
.EXAMPLE .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -TargetOU "OU=Workstations,DC=contoso,DC=com"
.EXAMPLE # Popis blokiranih grupa .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets
.EXAMPLE # Deblokiranje određene grupe .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -UnblockBucket "Dell_Latitude5520_BIOS1.2.3"
.EXAMPLE # Izuzimanje VIP uređaja iz primjene pomoću tekstne datoteke .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExclusionListPath "C:\Admin\VIP-Devices.txt"
.EXAMPLE # Izuzimanje uređaja u sigurnosnoj grupi servisa AD (npr. izvršna prijenosna računala) .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExcludeADGroup "VIP-Računala"
.EXAMPLE # Koristite WinCS (Windows Configuration System) umjesto GPO/AvailableUpdatesPolicy # WinCsFlags.exe se u kontekstu sustava na svakoj krajnjoj točki putem zakazanog zadatka .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -UseWinCS ' -WinCSKey "F33E0C8E002" #>
[CmdletBinding()] param( [Parameter(Mandatory = $false)] [niz]$AggregationInputPath, [Parameter(Mandatory = $false)] [niz]$ReportBasePath, [Parameter(Mandatory = $false)] [niz]$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 Parameters # ============================================================================ # AllowList = Uvrstite samo ove uređaje (ciljano pokretanje) # BlockList = Isključi ove uređaje (nikad se neće uvoditi) # Redoslijed obrade: AllowList prvi (ako je naveden), a zatim BlockList [Parameter(Mandatory = $false)] [niz]$AllowListPath, [Parameter(Mandatory = $false)] [niz]$AllowADGroup, [Parameter(Mandatory = $false)] [niz]$ExclusionListPath, [Parameter(Mandatory = $false)] [niz]$ExcludeADGroup, # ============================================================================ Parametri sustava # WinCS (Windows Configuration System) # ============================================================================ # WinCS je alternativa implementaciji AvailableUpdatesPolicy GPO. # Koristi se WinCsFlags.exe na svakoj krajnjoj točki da bi omogućio rollout sigurnog pokretanja.# WinCsFlags.exe se u kontekstu sustava na krajnjoj točki.# Referenca: 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)] [niz]$UnblockBucket, [Parameter(Mandatory = $false)] [switch]$UnblockAll, [Parameter(Mandatory = $false)] [switch]$EnableTaskOnDisabled )
$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Uzorci implementacije i nadzora"
# ============================================================================ # PROVJERA OVISNOSTI # ============================================================================
function Test-ScriptDependencies { param( [Parameter(Mandatory = $true)] [niz]$ScriptDirectory, [Parameter(Mandatory = $true)] [string[]]$RequiredScripts ) $missingScripts = @() foreach ($script in $RequiredScripts) { $scriptPath = Join-Path $ScriptDirectory $script if (-not (Test-Path $scriptPath)) { $missingScripts += $script } } if ($missingScripts.Count -gt 0) { Write-Host "" Write-Host ("=" * 70) -Boja prednjeg plana Crvena Write-Host " NEDOSTAJE ZAVISNOSTI" -Prednji planBoja Crvena Write-Host ("=" * 70) -Crveni prednji planBoja Write-Host "" Write-Host "Nisu pronađene sljedeće potrebne skripte:" -Boja prednjeg plana Žuta foreach ($script u $missingScripts) { Write-Host " - $script" -ForegroundColor White } Write-Host "" Write-Host "Preuzmite najnovije skripte iz:" -ForegroundColor Cyan Write-Host " URL: $DownloadUrl" -ForegroundColor White Write-Host " Idite na: '$DownloadSubPage'" -ForegroundColor White Write-Host "" Write-Host "Izdvoji sve skripte u isti direktorij i ponovno pokreni." -Boja prednjeg plana Žuta Write-Host "" povratna $false } vraćanje $true }
# Required scripts for orchestrator $requiredScripts = @( "Aggregate-SecureBootData.ps1", "Enable-SecureBootUpdateTask.ps1", "Deploy-GPO-SecureBootCollection.ps1", "Detect-SecureBootCertUpdateStatus.ps1" )
if (-not (Test-ScriptDependencies -ScriptDirectory $PSScriptRoot -RequiredScripts $requiredScripts)) { izlaz 1 }
# ============================================================================ # PROVJERA VALJANOSTI PARAMETARA # ============================================================================
# Admin commands only need ReportBasePath $isAdminCommand = $ListBlockedBuckets -ili $UnblockBucket -ili $UnblockAll -ili $EnableTaskOnDisabled
if (-not $ReportBasePath) { Write-Host "ERROR: -ReportBasePath is required". -ForegroundColor Red izlaz 1 }
if (-not $isAdminCommand -and -not $AggregationInputPath) { Write-Host "ERROR: -AggregationInputPath is required for rollout (not needed for -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red izlaz 1 }
# ============================================================================ # GPO DETECTION - CHECK FOR DETECTION GPO # ============================================================================
if (-not $isAdminCommand -and -not $DryRun) { $CollectionGPOName = "SecureBoot-EventCollection" # Provjerite je li dostupan modul GroupPolicy if (Get-Module -ListAvailable -Name GroupPolicy) { Import-Module GroupPolicy –ErrorAction SilentlyContinue Write-Host "Provjera otkrivanja GPO..." - Boja prednjeg plana Žuta pokušajte { # Provjerite postoji li GPO $existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue if ($existingGpo) { Write-Host pronađen je " Detection GPO: $CollectionGPOName" -ForegroundColor Green } još { Write-Host "" Write-Host ("=" * 70) -Boja prednjeg plana Žuta Write-Host " UPOZORENJE: OTKRIVANJE GPO NIJE PRONAĐENO" -Boja prednjeg plana Žuta Write-Host ("=" * 70) -Boja prednjeg plana Žuta Write-Host "" Write-Host "Otkrivanje GPO '$CollectionGPOName' nije pronađeno." -Boja prednjeg plana Žuta Write-Host "Bez ovog GPO-a neće se prikupljati podaci o uređaju". -Boja prednjeg plana Žuta Write-Host "" Write-Host "Za implementaciju GPO otkrivanja, pokreni:" -ForegroundColor Cyan Write-Host " .\Deploy-GPO-SecureBootCollection.ps1 -DomainName <domena> -AutoDetectOU" -ForegroundColor White Write-Host "" Write-Host "Želite li ipak nastaviti? (Y/N)" - Boja prednjeg plana Žuta $response = Glavno računalo za čitanje if ($response -notmatch '^[Yy]') { Write-Host "Prekid. Najprije implementirajte GPO otkrivanja." -ForegroundColor Red izlaz 1 } } } uhvatiti { Write-Host " Nije moguće provjeriti GPO: $($_. Exception.Message)" -ForegroundColor Yellow } } još { Write-Host " GroupPolicy modul nije dostupan - preskače GPO provjeru" -ForegroundColor Gray } Write-Host "" }
# ============================================================================ # PUTOVI DATOTEKA STANJA # ============================================================================
$stateDir = Join-Path $ReportBasePath "RolloutState" if (-not (Test-Path $stateDir)) { New-Item -ItemType Direktorij -Put $stateDir -Force | Izlazna vrijednost -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 KOMPATIBILNOST: ConvertTo-Hashtable Ne, ne============================================================================ # ConvertFrom-Json -AsHashtable je samo PS7+. To omogućuje kompatibilnost.
function ConvertTo-Hashtable { param( [Parameter(ValueFromPipeline = $true)] $InputObject ) postupak { if ($null -eq $InputObject) { return @{} } if ($InputObject -is [System.Collections.IDictionary]) { return $InputObject } if ($InputObject -is [PSCustomObject]) { # Koristi [naručeno] za dosljedno naručivanje ključeva i sigurno rukovanje duplikatima $hash = [naručeno]@{} foreach ($prop u $InputObject.PSObject.Properties) { # Indeksirani zadatak sigurno rukuje duplikatima pisanjem preko $hash[$prop. Naziv] = ConvertTo-Hashtable $prop. Vrijednost } povratna $hash } if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { return @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ }) } povratna $InputObject } }
# ============================================================================ # ADMINISTRATORSKE NAREDBE: Popis/deblokiranje grupa # ============================================================================
if ($ListBlockedBuckets) { Write-Host "" Write-Host ("=" * 80) -Boja prednjeg plana Žuta Write-Host " BLOKIRANE KANTE" - Boja prednjeg plana Žuta Write-Host ("=" * 80) -Boja prednjeg plana Žuta Write-Host "" if (Test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable ako ($blocked. Count -eq 0) { Write-Host "Nema blokiranih grupa". -Prednji planBoja Zelena } još { Write-Host "Ukupno blokirano: $($blocked. Count)" -ForegroundColor Red Write-Host "" ($key $blocked. tipke) { $info = $blocked[$key] Write-Host "Bucket: $key" -ForegroundColor Red Write-Host " blokirano na: $($info. BlockedAt)" - Boja prednjeg planaSiva Write-Host " Razlog: $($info. Reason)" -ForegroundColor Gray Write-Host " Neuspjeli uređaj: $($info. FailedDevice)" -Boja prednjeg planaSiva Write-Host " Zadnje izvješće: $($info. LastReported)" -ForegroundColor Gray Write-Host " Val: $($info. WaveNumber)" - Boja prednjeg planaSiva Write-Host " Uređaji u grupi: $($info. DevicesInBucket)" -Boja prednjeg plana Siva Write-Host "" } Write-Host "Deblokiranje grupe:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Cyan Write-Host "" Write-Host "Deblokiranje svega:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Cyan } } još { Write-Host "Nije pronađena datoteka blokiranih grupa". -ForegroundColor Green } Write-Host "" izlaz 0 }
if ($UnblockBucket) { Write-Host "" if (Test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable ako ($blocked. Contains($UnblockBucket)) { $blocked je. Ukloni($UnblockBucket) $blocked | ConvertTo-Json -Dubina 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force # Dodaj na popis koji je odobrio administrator da biste spriječili ponovno blokiranje $adminApproved = @{} if (Test-Path $adminApprovedPath) { $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } $adminApproved[$UnblockBucket] = @{ ApprovedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ApprovedBy = $env:USERNAME } $adminApproved | ConvertTo-Json -Dubina 10 | Out-File $adminApprovedPath -Encoding UTF8 -Force Write-Host "Unblocked bucket: $UnblockBucket" -ForegroundColor Green Write-Host "Dodano na popis odobren od strane administratora (neće se automatski ponovno blokirati)" -ForegroundColor Cyan } još { Write-Host "Bucket not found: $UnblockBucket" -ForegroundColor Yellow Write-Host "Dostupne grupe:" - Boja prednjeg planaSiva $blocked je. Tipke | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } } } još { Write-Host "Nije pronađena datoteka blokiranih grupa". -Boja prednjeg plana Žuta } Write-Host "" izlaz 0 }
if ($UnblockAll) { Write-Host "" if (Test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable $count = $blocked. Računati @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Encoding UTF8 -Force Write-Host "Unblocked all $count buckets". -ForegroundColor Green } još { Write-Host "Nije pronađena datoteka blokiranih grupa". -Boja prednjeg plana Žuta } Write-Host "" izlaz 0 }
# ============================================================================ # FUNKCIJE POMOĆNIH PROGRAMA # ============================================================================
function Get-RolloutState { if (Test-Path $rolloutStatePath) { pokušajte { $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable # Provjerite postoje li obavezna svojstva ako ($null -eq $loaded. CurrentWave) { throw "Invalid state file - missing CurrentWave" } # Provjerite je li WaveHistory uvijek polje (popravci PS5.1 JSON deserijalizacija) ako ($null -eq $loaded. WaveHistory) ( $loaded je. WaveHistory = @() } elseif ($loaded. WaveHistory -isnot [polje]) { $loaded je. WaveHistory = @($loaded. WaveHistory) } povratna $loaded } uhvatiti { Write-Log "Otkriveno RolloutState.json oštećeno: $($_. Exception.Message)" "WARN" Write-Log "Sigurnosno kopiranje oštećene datoteke i pokretanje nove" "UPOZORENJE" $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyyMMdd-HHmmss')" Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue } } vrati @{ CurrentWave = 0 StartedAt = $null LastAggregation = $null TotalDevicesTargeted = 0 TotalDevicesUpdated = 0 Status = "NotStarted" WaveHistory = @() } }
function Save-RolloutState { param($State) $State | ConvertTo-Json -Dubina 10 | Out-File $rolloutStatePath -Encoding UTF8 -Force }
function Get-WeekdayProjection { < broj . SINOPSIS Izračun projicirani datum završetka koji se računa za vikende (nema napretka na sub/ned) #> param( [int]$RemainingDevices, [double]$DevicesPerDay, [datetime]$StartDate = (Get-Date) ) if ($DevicesPerDay -le 0 -or $RemainingDevices -le 0) { vrati @{ ProjectedDate = $null WorkingDaysNeeded = 0 CalendarDaysNeeded = 0 } } # Izračun potrebnih radnih dana (osim vikenda) $workingDaysNeeded = [matematika]::Ceiling($RemainingDevices / $DevicesPerDay) # Pretvaranje radnih dana u kalendarske dane (dodavanje vikenda) $currentDate = $StartDate.Date $daysAdded = 0 $workingDaysAdded = 0 while ($workingDaysAdded -lt $workingDaysNeeded) { $currentDate = $currentDate.AddDays(1) $daysAdded++ Brojanje samo radnih dana if ($currentDate.DayOfWeek -ne [DayOfWeek]::saturday -and $currentDate.DayOfWeek -ne [DayOfWeek]::nedjelja) { $workingDaysAdded++ } } vrati @{ ProjectedDate = $currentDate.ToString("yyyy-MM-dd") WorkingDaysNeeded = $workingDaysNeeded CalendarDaysNeeded = $daysAdded } }
function Save-RolloutSummary { < broj . SINOPSIS Spremanje sažetka rollouta s informacijama o projekciji za prikaz nadzorne ploče #> param( [raspršivanje]$State, [int]$TotalDevices, [int]$UpdatedDevices, [int]$NotUpdatedDevices, [double]$DevicesPerDay ) $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" # Izračun projekcije u obliku vikenda $projection = Get-WeekdayProjection -RemainingDevices $NotUpdatedDevices -DevicesPerDay $DevicesPerDay $summary = @{ GeneratedAt = (Get-Date -Format "yyyy-MM-dd HH:mm:ss") RolloutStartDate = $State.StartedAt LastAggregation = $State.LastAggregation CurrentWave = $State.CurrentWave Status = $State.Status # Broj uređaja TotalDevices = $TotalDevices UpdatedDevices = $UpdatedDevices NotUpdatedDevices = $NotUpdatedDevices PercentUpdated = ako ($TotalDevices -gt 0) { [matematika]:Round(($UpdatedDevices / $TotalDevices) * 100, 1) } else { 0 } # Velocity metrics DevicesPerDay = [matematika]::Round($DevicesPerDay, 1) TotalDevicesTargeted = $State.TotalDevicesTargeted TotalWaves = $State.CurrentWave # Projekcija u obliku vikenda ProjectedCompletionDate = $projection. ProjectedDate WorkingDaysRemaining = $projection. WorkingDaysNeeded CalendarDaysRemaining = $projection. CalendarDaysNeeded # Napomena o isključenju vikenda ProjectionNote = "Projektirani dovršetak isključuje vikende (sub/ned)" } $summary | ConvertTo-Json -Dubina 5 | Out-File $summaryPath -Encoding UTF8 -Force Write-Log " Rollout summary saved: $summaryPath" "INFO" vraćanje $summary }
function Get-BlockedBuckets { if (test-path $blockedBucketsPath) { return Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } vrati @{} }
function Save-BlockedBuckets { param($Blocked) $Blocked | ConvertTo-Json -Dubina 10 | Out-File $blockedBucketsPath -Encoding UTF8 -Force }
function Get-AdminApproved { if (Test-Path $adminApprovedPath) { return Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } vrati @{} }
function Get-DeviceHistory { if (Test-Path $deviceHistoryPath) { return Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } vrati @{} }
function Save-DeviceHistory { param($History) $History | ConvertTo-Json -Dubina 10 | Out-File $deviceHistoryPath -Encoding UTF8 -Force }
function Save-ProcessingCheckpoint { param( [niz]$Stage, [int]$Processed, [int]$Total, [raspršivanje]$Metrics = @{} )
$checkpoint = @{ Stage = $Stage UpdatedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Obrađeno = $Processed Ukupno = $Total Postotak = ako ($Total -gt 0) { [matematika]::Round(($Processed / $Total) * 100, 2) } još { 0 } Metrika = $Metrics }
$checkpoint | ConvertTo-Json -Depth 6 | Out-File $processingCheckpointPath -Encoding UTF8 -Force }
function Get-NotUpdatedIndexes { param([polje]$Devices)
$hostSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $bucketCounts = @{}
foreach ($device in $Devices) { $hostname = ako ($device. Naziv glavnog računala) { $device. Hostname } elseif ($device. HostName) { $device. HostName } još { $null } if ($hostname) { [void]$hostSet.Add($hostname) }
$bucketKey = Get-BucketKey $device if ($bucketKey) { if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey]++ } još { $bucketCounts[$bucketKey] = 1 } } }
return @{ HostSet = $hostSet BucketCounts = $bucketCounts } }
function Write-Log { param([niz]$Message; [niz]$Level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $color = prekidač ($Level) { "OK" { "Green" } "WARN" { "Yellow" } "ERROR" { "Red" } "BLOCKED" { "DarkRed" } "WAVE" { "Cyan" } zadano { "Bijelo" } } Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color # Prijavite se i u datoteku $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMMdd').log" "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Encoding UTF8 }
function Get-BucketKey { param($Device) # Upotrijebite BucketId s JSON uređaja ako je dostupan (SHA256 raspršivanje iz skripte otkrivanja) if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" } # Rezervni: konstrukt proizvođača|model|bios $mfr = ako ($Device.WMI_Manufacturer) { $Device.WMI_Manufacturer } još { $Device.Manufacturer } $model = if ($Device.WMI_Model) { $Device.WMI_Model } još { $Device.Model } $bios = if ($Device.BIOSDescription) { $Device.BIOSDescription } else { $Device.BIOS } return "$mfr|$model|$bios" }
# ============================================================================ # UČITAVANJE POPISA VIP/IZUZETAKA # ============================================================================
function Get-ExcludedHostnames { param( [niz]$ExclusionFilePath, [niz]$ADGroupName ) $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Učitaj iz datoteke (podržava .txt ili .csv) if ($ExclusionFilePath -and (Test-Path $ExclusionFilePath)) { $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower() if ($extension -eq ".csv") { # CSV oblik: očekuje stupac "Naziv Glavnog računala" ili "Naziv Računala" $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 u $csvData) { ako je (![ niz]::IsNullOrWhiteSpace($row.$hostCol)) { [void]$excluded. Add($row.$hostCol.Trim()) } } } } još { # Običan tekst: jedan naziv glavnog računala po retku Get-Content $ExclusionFilePath | ForEach-Object { $line = $_. Skraćivanje() ako ($line -and -not $line. StartsWith('#')) { [void]$excluded. Add($line) } } } Write-Log "Učitan $($excluded. Count) hostnames from exclusion file: $ExclusionFilePath" "INFO" } # Učitaj iz ad-sigurnosne grupe if ($ADGroupName) { pokušajte { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Rekursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'računalo' } foreach ($member u $groupMembers) { [void]$excluded. Add($member. Naziv) } Write-Log "Učitana računala $($groupMembers.Count) iz grupe AD: $ADGroupName" "INFO" } uhvatiti { Write-Log "Ad grupu '$ADGroupName' nije moguće učitati: $_" "UPOZORENJE" } } return @($excluded) }
# ============================================================================ # ALLOW LIST LOADING (Targeted Rollout) # ============================================================================
function Get-AllowedHostnames { < broj . SINOPSIS Učitava nazive glavnog računala iz datoteke AllowList i/ili grupe AD za ciljano pokretanje.Kada je naveden AllowList, samo će ti uređaji biti uključeni u početak rada.#> param( [niz]$AllowFilePath, [niz]$ADGroupName ) $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Učitaj iz datoteke (podržava .txt ili .csv) if ($AllowFilePath -and (Test-Path $AllowFilePath)) { $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower() if ($extension -eq ".csv") { # CSV oblik: očekuje stupac "Naziv Glavnog računala" ili "Naziv Računala" $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 u $csvData) { ako je (![ niz]::IsNullOrWhiteSpace($row.$hostCol)) { [void]$allowed. Add($row.$hostCol.Trim()) } } } } } još { # Običan tekst: jedan naziv glavnog računala po retku Get-Content $AllowFilePath | ForEach-Object{ $line = $_. Skraćivanje() ako ($line -and -not $line. StartsWith('#')) { [void]$allowed. Add($line) } } } Write-Log "Učitan $($allowed. Count) hostnames from allow list file: $AllowFilePath" "INFO" } # Učitaj iz ad-sigurnosne grupe if ($ADGroupName) { pokušajte { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Rekursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'računalo' } foreach ($member u $groupMembers) { [void]$allowed. Add($member. Naziv) } Write-Log "Učitana računala $($groupMembers.Count) iz grupe dopuštenih servisa AD: $ADGroupName" "INFO" } uhvatiti { Write-Log "Ad grupu '$ADGroupName' nije moguće učitati: $_" "UPOZORENJE" } } return @($allowed) }
# ============================================================================ # SVJEŽINA PODATAKA I PRAĆENJE # ============================================================================
function Get-DataFreshness { < broj . SINOPSIS Provjerava koliko su novi podaci otkrivanja pregledom vremenskih oznaka JSON datoteke.Vraća statistiku o zadnjim prijavljenim krajnjim točkama.#> param([niz]$JsonPath) $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue if ($jsonFiles.Count -eq 0) { vrati @{ TotalFiles = 0 FreshFiles = 0 StaleFiles = 0 NoDataFiles = 0 OldestFile = $null NewestFile = $null AvgAgeHours = 0 Upozorenje = "Nije pronađena nijedna JSON datoteka – otkrivanje nije moguće implementirati" } } $now = Get-Date $freshThresholdHours = 24 # Files ažurirana u zadnja 24 sata su "svježe" $staleThresholdHours = 72 # Files stariji od 72 sata su "zastarjeli" $fresh = 0 $stale = 0 $ages = @() foreach ($file u $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 - Prvi 1 $newestFile = $jsonFiles | Sort-Object LastWriteTime -Descending | Select-Object - Prvi 1 $warning = $null if ($stale -gt ($jsonFiles.Count * 0,5)) { $warning = "Više od 50% uređaja ima zastarjele podatke (>72 sata) – provjerite otkrivanje GPO" } elseif ($fresh -lt ($jsonFiles.Count * 0.3)) { $warning = "Nedavno prijavljeno manje od 30 % uređaja – otkrivanje možda nije pokrenuto" } vrati @{ TotalFiles = $jsonFiles.Count FreshFiles = $fresh StaleFiles = $stale MediumFiles = $jsonFiles.Count - $fresh - $stale OldestFile = $oldestFile.LastWriteTime NewestFile = $newestFile.LastWriteTime AvgAgeHours = [matematika]::Round(($ages | Measure-Object -Average). Prosjek, 1) Upozorenje = $warning } }
function Test-DetectionGPODeployed { < broj . SINOPSIS Provjerava je li infrastruktura otkrivanja/nadzora na mjestu.#> param([niz]$JsonPath) # Check 1: JSON path exists if (-not (Test-Path $JsonPath)) { vrati @{ Uvedeno = $false Poruka = "Put unosa JSON-a ne postoji: $JsonPath" } } # 2. provjera: postoje barem neke JSON datoteke $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). Računati if ($jsonCount -eq 0) { vrati @{ Uvedeno = $false Message = "Nema JSON datoteka u $JsonPath – otkrivanje GPO-a možda nije implementirano ili uređaji još nisu prijavili" } } # Check 3: Files su razumno nedavno (barem neki u prošlog tjedna) $recentFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue | Where-Object { $_. LastWriteTime -gt (Get-Date). AddDays(-7) } if ($recentFiles.Count -eq 0) { vrati @{ Uvedeno = $false Message = "Nema JSON datoteka ažuriranih u zadnjih 7 dana – otkrivanje GPO-a možda je neispravno ili uređaji izvan mreže" } } vrati @{ Uvedeno = $true Poruka = "Otkrivanje je aktivno: $jsonCount datoteke, $($recentFiles.Count) nedavno ažurirano" FileCount = $jsonCount RecentCount = $recentFiles.Count } }
# ============================================================================ # PRAĆENJE UREĐAJA (PREMA NAZIVU GLAVNOG RAČUNALA) # ============================================================================
function Update-DeviceHistory { < broj . SINOPSIS Prati uređaje prema nazivu glavnog računala jer nemamo jedinstveni identifikator računala.Napomena: BucketId je jedan-prema-više (ista hardverska konfiguracija = ista grupa).Ako se u JSON zbirku doda jedinstveni identifikator, ažurirajte ovu funkciju.#> param( [polje]$CurrentDevices, [raspršivanje]$DeviceHistory ) foreach ($device u $CurrentDevices) { $hostname = $device. Hostname if (-not $hostname) { continue } # Praćenje uređaja prema nazivu glavnog računala $DeviceHistory[$hostname] = @{ Naziv glavnog računala = $hostname BucketId = $device. Id kante Proizvođač = $device. WMI_Manufacturer Model = $device. WMI_Model LastSeen = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Status = $device. UpdateStatus } } }
# ============================================================================ # BLOKIRANO OTKRIVANJE KANTICE (na temelju doseg uređaja) # ============================================================================
<# . OPIS Logika blokiranja: - Grupa je blokirana samo ako: 1. Uređaj je ciljan u valu 2. MaxWaitHours je prošao od pokretanja vala 3. Uređaj NIJE DOSTUPAN (ping ne uspijeva) - Ako je uređaj dostupan, ali još nije ažuriran, čekamo (ažuriranje možda čeka ponovno pokretanje – događaj 1808 pokreće se samo nakon ponovnog pokretanja) - Uređaj koji nije dostupan označava da je došlo do pogreške i da je potrebna istraga Deblokiranje: - Koristite -ListBlockedBuckets za prikaz blokiranih grupa - Koristite -UnblockBucket "BucketKey" za deblokiranje određene grupe - Koristite -DeblokiranjeSve za deblokiranje svih grupa #>
function Test-DeviceReachable { param( [niz]$Hostname, [string]$DataPath # Put do JSON datoteka uređaja ) # Method 1: Check JSON file timestamp (fastest — no file parsing needed) # Ako je skripta otkrivanja nedavno pokrenuta, datoteka je napisana/ažurirana, dokazujući da je uređaj živ if ($DataPath) { $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object - Prvi 1 if ($deviceFile) { $hoursSinceWrite = ((Get-Date) - $deviceFile.LastWriteTime). TotalHours if ($hoursSinceWrite -lt 72) { return $true } } } # Method 2: Fallback to ping (only if JSON is stale or missing) pokušajte { $ping = Test-Connection -ComputerName $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue povratna $ping } uhvatiti { povratna $false } }
function Update-BlockedBuckets { param( $RolloutState, $BlockedBuckets, $AdminApproved, [polje]$NotUpdatedDevices, [raspršivanje]$NotUpdatedIndexes, [int]$MaxWaitHours, [bool]$DryRun = $false ) $now = Get-Date $newlyBlocked = @() $stillWaiting = @() $devicesToCheck = @() $hostSet = ako ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } još { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } $bucketCounts = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.BucketCounts) { $NotUpdatedIndexes.BucketCounts } else { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). BucketCounts } # Prikupite uređaje koji su prošli razdoblje čekanja i još nisu ažurirani foreach ($wave in $RolloutState.WaveHistory) { if (-not $wave. StartedAt) { continue } $waveStart = [DateTime]::P arse($wave. StartedAt) $hoursSinceWave = ($now - $waveStart). TotalHours if ($hoursSinceWave -lt $MaxWaitHours) { # Još uvijek unutar razdoblja čekanja – još ne provjeravaj Nastaviti } # Provjerite svaki uređaj iz ovog vala ($deviceInfo $wave. Uređaji) { $hostname = $deviceInfo.Hostname $bucketKey = $deviceInfo.BucketKey # Preskoči ako je grupa već blokirana if ($BlockedBuckets.Contains($bucketKey)) { continue } # Preskoči ako je grupa odobrena za administratore AND val je pokrenut PRIJE odobrenja # (samo provjerite uređaje koji su ciljani nakon administratorskog odobrenja za ponovno blokiranje) if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) { $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. Odobreno) if ($waveStart -lt $approvalTime) { # Ovaj je uređaj bio ciljan prije odobrenja administratora – preskoči Nastaviti } # Val je započeo nakon odobrenja - ovo je svježe ciljanje, može provjeriti } # Je li ovaj uređaj i dalje na popisu NotUpdated? if ($hostSet.Contains($hostname)) { $devicesToCheck += @{ Naziv glavnog računala = $hostname BucketKey = $bucketKey WaveNumber = $wave. WaveNumber HoursSinceWave = [matematika]::Round($hoursSinceWave; 1) } } } } if ($devicesToCheck.Count -eq 0) { vraćanje $newlyBlocked } Write-Log "Provjera dostupnosti uređaja $($devicesToCheck.Count) nakon razdoblja čekanja..." "INFO" # Praćenje pogrešaka po grupi za donošenje odluka $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() } # Provjerite pristupačnost svakog uređaja foreach ($device u $devicesToCheck) { $hostname = $device. Hostname $bucketKey = $device. Ključ kante if ($DryRun) { Write-Log "[DRYRUN] Provjerava $hostname pristupačnosti" "INFO" Nastaviti } if (-not $bucketFailures.ContainsKey($bucketKey)) { $bucketFailures[$bucketKey] = @{ Nedostupan = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. HoursSinceWave } } $isReachable = Test-DeviceReachable -Hostname $hostname -DataPath $AggregationInputPath if (-not $isReachable) { $bucketFailures[$bucketKey]. Nedostupan += $hostname } još { # Uređaj je dostupan, ali još nije ažuriran – može biti privremena pogreška ili čekanje na ponovno pokretanje $bucketFailures[$bucketKey]. AliveButFailed += $hostname $stillWaiting += $hostname } } # Decision per bucket: only block if devices are truly UNREACHABLE # Alive devices with failures = temporary, continue rollout foreach ($bucketKey in $bucketFailures.Keys) { $bf = $bucketFailures[$bucketKey] $unreachableCount = $bf. Unreachable.Count $aliveFailedCount = $bf. AliveButFailed.Count # Provjerite ima li ova grupa uspjeha (iz ažuriranih podataka o uređajima) $bucketHasSuccesses = $stSuccessBuckets -i $stSuccessBuckets.Contains($bucketKey) if ($unreachableCount -gt 0 -and $aliveFailedCount -eq 0) { # SVI neispravni uređaji nisu dostupni – blokirajte grupu if ($newlyBlocked -notcontains $bucketKey) { $BlockedBuckets[$bucketKey] = @{ BlockedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Reason = "All $unreachableCount device(s) unreachable after $($bf. HoursSinceWave) sati" FailedDevices = ($bf. Unreachable -join ", ") WaveNumber = $bf. WaveNumber DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } else { 0 } } $newlyBlocked += $bucketKey Write-Log "BUCKET BLOCKED: $bucketKey ($unreachableCount device(s) unreachable: $($bf. Unreachable -join ', '))" "BLOCKED" } } elseif ($aliveFailedCount -gt 0) { # Uređaji su živi, ali se ne ažuriraju – privremeni kvar, NE blokiraj Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length)))...: $aliveFailedCount device(s) alive but pending, $unreachableCount unreachable - NOT blocking (temporary)" "INFO" if ($unreachableCount -gt 0) { Write-Log " Nedostupan: $($bf. Unreachable -join ', ')" "WARN" } Write-Log " Živ, ali na čekanju: $($bf. AliveButFailed -join ', ')" "INFO" # Praćenje broja neuspjeha u stanju pokretanja za praćenje if (-not $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} } $RolloutState.TemporaryFailures[$bucketKey] = @{ AliveButFailed = $bf. AliveButFailed Unreachable = $bf. Nedostupan LastChecked = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } } } if ($stillWaiting.Count -gt 0) { Write-Log "Uređaji dostupni, ali ažuriranje na čekanju (možda će trebati ponovno pokretanje): $($stillWaiting.Count)" "INFO" } povratna $newlyBlocked }
# ============================================================================ # AUTOMATSKO DEBLOKIRANJE: Deblokiranje grupa kada se uređaji uspješno ažuriraju # ============================================================================
function Update-AutoUnblockedBuckets { < broj . OPIS Provjerava jesu li uređaji u blokiranim grupama ažurirani (događaj 1808). Automatsko deblokiranje ako su ažurirani svi ciljani uređaji u grupi.Ako su samo NEKI uređaji ažurirani, obavijestit će administratora koji može ručno deblokirati. Administrator možete ručno deblokirati pomoću: .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "path" -UnblockBucket "BucketKey" #> param( $BlockedBuckets, $RolloutState, [polje]$NotUpdatedDevices, [niz]$ReportBasePath, [raspršivanje]$NotUpdatedIndexes, [int]$LogSampleSize = 25 ) $autoUnblocked = @() $bucketsToCheck = @($BlockedBuckets.Tipke) $hostSet = ako ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } još { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } foreach ($bucketKey u $bucketsToCheck) { $bucketInfo = $BlockedBuckets[$bucketKey] # Get all devices we targeted from this bucket historically $targetedDevicesInBucket = @() foreach ($wave in $RolloutState.WaveHistory) { $targetedDevicesInBucket += @($wave. Uređaji | Where-Object { $_. BucketKey -eq $bucketKey }) } if ($targetedDevicesInBucket.Count -eq 0) { continue } # Provjerite koliko je ciljanih uređaja još uvijek u aplikaciji NotUpdated ili ažurirano $updatedDevices = @() $stillPendingDevices = @() foreach ($targetedDevice u $targetedDevicesInBucket) { if ($hostSet.Contains($targetedDevice.Hostname)) { $stillPendingDevices += $targetedDevice.Naziv glavnog računala } još { $updatedDevices += $targetedDevice.Naziv glavnog računala } } if ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) { # Svi su ciljani uređaji ažurirani – automatsko deblokiranje! $BlockedBuckets.Remove($bucketKey) $autoUnblocked += @{ BucketKey = $bucketKey UpdatedDevices = $updatedDevices PreviouslyBlockedAt = $bucketInfo.BlockedAt Reason = "All $($updatedDevices.Count) targeted device(s) successfully updated" } Write-Log "AUTOMATSKO DEBLOKIRANJE: $bucketKey (svi su se ciljani uređaji s vrijednostima $($updatedDevices.Count) uspješno ažurirali)" "U redu" # Inkrement OEM val brojanje za ovu grupu OEM (per-OEM praćenje) $bucketOEM = ako ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } else { 'Unknown' } # Extract OEM from pipe-delimited key or default if (-not $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } $currentWave = if ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } else { 0 } $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1 Write-Log "OEM '$bucketOEM' broj valova povećava se na $($currentWave + 1) (sljedeća dodjela: $([int][Matematika]::P ow(2, $currentWave + 1)) uređaji)" "INFO" } elseif ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -gt 0) { # NEKI su uređaji ažurirani, ali su drugi i dalje na čekanju – obavijestite administratora (samo jedanput) if (-not $bucketInfo.UnblockCandidate) { $bucketInfo.UnblockCandidate = $true $bucketInfo.UpdatedDevices = $updatedDevices $bucketInfo.PendingDevices = $stillPendingDevices $bucketInfo.NotifiedAt = (Get-Date). ToString("yyyy-MM-dd HH:mm:ss") Write-Log "" "INFO" Write-Log "========== DJELOMIČNO AŽURIRANJE U BLOKIRANOJ GRUPI ==========" "INFO" Write-Log "Grupa: $bucketKey" "INFO" $updatedSample = @($updatedDevices | Select-Object -First $LogSampleSize) $pendingSample = @($stillPendingDevices | Select-Object -First $LogSampleSize) $updatedSuffix = ako ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) više)" } još { "" } $pendingSuffix = ako ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) više)" } još { "" } Write-Log "Ažurirani uređaji ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix" "OK" Write-Log "Još uvijek na čekanju ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix" "WARN" Write-Log "" "INFO" Write-Log "Da biste ručno deblokirali ovu grupu nakon provjere, pokrenite:" "INFO" Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '"$ReportBasePath'" -UnblockBucket '"$bucketKey'"" "INFO" Write-Log "=======================================================" "INFO" Write-Log "" "INFO" } } } povratna $autoUnblocked }
# ============================================================================ # WAVE GENERATION (INLINED – isključuje blokirane grupe) # ============================================================================
function New-RolloutWave { param( [niz]$AggregationPath, $BlockedBuckets, $RolloutState, [int]$MaxDevicesPerWave = 50, [string[]]$AllowedHostnames = @(), [string[]]$ExcludedHostnames = @() ) # Učitavanje podataka o agregaciji $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" | Where-Object { $_. Naziv -notlike "*Buckets*" } | Sort-Object LastWriteTime -Descending | Select-Object - Prvi 1 if (-not $notUptodateCsv) { Write-Log "PRONAĐEN JE CSV BEZ AŽURIRANJA" "POGREŠKA" vraćanje $null } $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName) # Normalizirajte HostName -> hostname radi dosljednosti (CSV koristi Naziv Glavnog računala, kod koristi Naziv Glavnog računala) foreach ($device u $allNotUpdated) { ako ($device. PSObject.Properties['HostName'] -and -not $device. PSObject.Properties['Hostname']) { $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName – sila } } # Filtriranje blokiranih grupa $eligibleDevices = @($allNotUpdated | Where-Object { $bucketKey = Get-BucketKey $_ -nije $BlockedBuckets.Contains($bucketKey) Ne, ne, ne, ne. # Filtriraj samo na dopuštene uređaje (ako je naveden AllowList) # AllowList = ciljano rollout - samo ti uređaji će se smatrati if ($AllowedHostnames.Count -gt 0) { $beforeCount = $eligibleDevices.Count $eligibleDevices = @($eligibleDevices | Where-Object { $_. Hostname -in $AllowedHostnames Ne, ne, ne, ne. $allowedCount = $eligibleDevices.Count Write-Log "AllowList applied: $allowedCount of $beforeCount devices are in allow list" "INFO" } # FiltrirajTE VIP/isključene uređaje (BlockList) # BlockList se primjenjuje NAKON AllowList if ($ExcludedHostnames.Count -gt 0) { $beforeCount = $eligibleDevices.Count $eligibleDevices = @($eligibleDevices | Where-Object { $_. Hostname -notin $ExcludedHostnames Ne, ne, ne, ne. $excludedCount = $beforeCount - $eligibleDevices.Count if ($excludedCount -gt 0) { Write-Log "Isključeno $excludedCount VIP/zaštićenih uređaja od primjene" "INFO" } } if ($eligibleDevices.Count -eq 0) { Write-Log "Nema preostalih uređaja koji ispunjavaju uvjete (svi ažurirani ili blokirani)" "U redu" povratna $null } # Nabavite uređaje koji su već u tijeku (iz prethodnih valova) $devicesAlreadyInRollout = @() if ($RolloutState.WaveHistory -and $RolloutState.WaveHistory.Count -gt 0) { $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object { $_. Uređaji | ForEach-Object { $_. Naziv glavnog računala } } | Where-Object { $_ }) } Write-Log "Uređaji koji su već u implementaciji: $($devicesAlreadyInRollout.Count)" "INFO" # Odvojite prema razini pouzdanosti $highConfidenceDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -eq "Visoka pouzdanost" -and $_. Hostname -notin $devicesAlreadyInRollout Ne, ne, ne, ne. # Akcija Obavezno uključuje: # – eksplicitno "Potrebna je radnja" # – prazno/null razina pouzdanosti # – nepoznata/nepoznata vrijednost confidenceLevel (tretira se kao potrebna radnja) $knownSafeCategories = @( "Visoko povjerenje", "Privremeno pauzirano", "Pod opažanjima", "Pod promatranjom – potrebno je više podataka", "Nije podržano", "Nije podržano – poznato ograničenje" ) $actionRequiredDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -notin $knownSafeCategories -and $_. Hostname -notin $devicesAlreadyInRollout Ne, ne, ne, ne. Write-Log "Visoka pouzdanost (nije u implementaciji): $($highConfidenceDevices.Count)" "INFO" Write-Log "Potrebna je akcija (nije u implementaciji): $($actionRequiredDevices.Count)" "INFO" # Izradite valne uređaje $waveDevices = @() # VISOKA POUZDANOST: Uključi SVE (sigurno za rollout) if ($highConfidenceDevices.Count -gt 0) { Write-Log "Adding all $($highConfidenceDevices.Count) High Confidence devices" "WAVE" $waveDevices += $highConfidenceDevices } # POTREBNA RADNJA: progresivno uvodinje (utemeljeno na grupi s OEM-širenjem za grupe nultog uspjeha) # Strategija: # - Kante s 0 uspjeha: Spread across OEMs (1 per OEM -> 2 per OEM -> 4 per OEM) # - Kante s ≥1 uspjeha: dvostruko slobodno bez ograničenja OEM-a if ($actionRequiredDevices.Count -gt 0) { # Učitaj broj uspjeha grupe s ažuriranih CSV uređaja (uređaji koji su uspješno ažurirani) $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" | Sort-Object LastWriteTime -Descending | Select-Object - Prvi 1 $bucketStats = @{} if ($updatedCsv) { $updatedDevices = Import-Csv $updatedCsv.FullName # Brojanje uspjeha po BucketId-u $updatedDevices | ForEach-Object { $key = Get-BucketKey $_ if ($key) { if (-not $bucketStats.ContainsKey($key)) { $bucketStats[$key] = @{ Uspjeh = 0; Na čekanju = 0; Ukupno = 0 } } $bucketStats[$key]. Uspjeh ++ $bucketStats[$key]. Ukupno + + } } Write-Log ažuriranim uređajima "Loaded $($updatedDevices.Count) u grupama $($bucketStats.Count) "INFO" } još { # Rezervni: pokušajte ActionRequired_Buckets CSV $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" | Sort-Object LastWriteTime -Descending | Select-Object - Prvi 1 if ($bucketsCsv) { Import-Csv $bucketsCsv.FullName | ForEach-Object { $key = ako ($_. BucketId) { $_. BucketId } još { "$($_. Proizvođač)|$($_. Model)|$($_. BIOS)" } $bucketStats[$key] = @{ Successes = [int]$_. Uspjehe Na čekanju = [int]$_. Neriješene Ukupno = [int]$_. TotalDevices } } } } # Group NotUpdated uređaji po grupi (proizvođač|Model|BIOS) $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ } # Odvojene grupe: nula-uspjeh vs has-success $zeroSuccessBuckets = @() $hasSuccessBuckets = @() foreach ($bucket u $buckets) { $bucketKey = $bucket. Ime $bucketDevices = @($bucket. Grupa) $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Naziv glavnog računala }) Brojanje uspjeha u ovoj grupi $stats = $bucketStats[$bucketKey] $successes = ako ($stats) { $stats. Successes } else { 0 } # Pronađi uređaje implementirane u ovu grupu iz povijesti vala $deployedToBucket = @() foreach ($wave in $RolloutState.WaveHistory) { ($device $wave. Uređaji) { ako ($device. BucketKey -eq $bucketKey -and $device. Naziv glavnog računala) { $deployedToBucket += $device. Hostname } } } $deployedToBucket = @($deployedToBucket | Sort-Object -Unique) # Provjerite je li sve implementirane uređaje prijavilo uspjeh $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames }) $confirmedSuccess = $deployedToBucket.Count - $stillPending.Count # Ako je na čekanju, preskočite ovu grupu dok svi ne potvrde if ($stillPending.Count -gt 0) { $parts = $bucketKey -split '\|' $displayName = "$($parts[0]) - $($parts[1])" Write-Log " Bucket: $displayName - Deployed=$($deployedToBucket.Count), Confirmed=$confirmedSuccess, Pending=$($stillPending.Count) (waiting)" "INFO" Nastaviti } # Preostali kvalificirani broj = uređaji koji još nisu implementirali $devicesNotYetTargeted = @($bucketDevices | Where-Object { $_. Naziv glavnog računala -notin $deployedToBucket Ne, ne, ne, ne. if ($devicesNotYetTargeted.Count -eq 0) { continue } # Kategoriziraj prema zbroju uspjeha $bucketInfo = @{ BucketKey = $bucketKey Uređaji = $devicesNotYetTargeted ConfirmedSuccess = $confirmedSuccess Successes = $successes OEM = ako ($bucket. Grupa[0]. WMI_Manufacturer) { $bucket. Grupa[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } još { 'Nepoznato' } } if ($successes -eq 0) { $zeroSuccessBuckets += $bucketInfo } još { $hasSuccessBuckets += $bucketInfo } } # === PROCESS HAS-SUCCESS BUCKETS (≥1 success) === # Udvostruči broj uspjeha – ako je 14 uspjelo, implementiraj 28 dalje foreach ($bucketInfo u $hasSuccessBuckets) { $nextBatchSize = $bucketInfo.Successes * 2 $nextBatchSize = [Matematika]::Min($nextBatchSize, $MaxDevicesPerWave) $nextBatchSize = [Matematika]::Min($nextBatchSize, $bucketInfo.Devices.Count) if ($nextBatchSize -gt 0) { $selectedDevices = @($bucketInfo.Uređaji | Select-Object -First $nextBatchSize) $waveDevices += $selectedDevices $parts = ako ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Matematika]::Min(12, $bucketInfo.BucketKey.Length))) } $displayName = "$($parts[0]) - $($parts[1])" Write-Log " [HAS-SUCCESS] $displayName - Successes=$($bucketInfo.Successes), Deploying=$nextBatchSize (2x confirmed)" "INFO" } } # === OBRADA GRUPA NULTOG USPJEHA (proširivanje preko OEM-a s praćenjem po OEM-u) === # Cilj: širenje rizika na različite OEM-ove, samostalno praćenje napretka po OEM-u # Svaki OEM napreduje na temelju vlastite povijesti uspjeha: # – OEM s uspjehom: dobiva više uređaja sljedećeg vala (2^waveCount) # – OEM bez uspjeha: ostaje na trenutnoj razini do potvrde uspjeha if ($zeroSuccessBuckets.Count -gt 0) { # Inicijaliziraj broj per-OEM vala ako ne postoji if (-not $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } # Group zero-success buckets by OEM $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM } $totalZeroSuccessAdded = 0 $oemsDeployedTo = @() foreach ($oemGroup u $oemBuckets) { $oemName = $oemGroup.Name # Get this OEM's wave count (starts at 0) $oemWaveCount = if ($RolloutState.OEMWaveCounts[$oemName]) { $RolloutState.OEMWaveCounts[$oemName] } još { 0 } # Izračun uređaja za THIS OEM: 2^waveCount (1, 2, 4, 8...) $devicesForThisOEM = [int][Matematika]::P ow(2, $oemWaveCount) $devicesForThisOEM = [Matematika]::Max(1, $devicesForThisOEM) $oemDevicesAdded = 0 # Pick from each bucket under this OEM foreach ($bucketInfo in $oemGroup.Group) { $remaining = $devicesForThisOEM - $oemDevicesAdded if ($remaining -le 0) { break } $toTake = [Matematika]::Min($remaining, $bucketInfo.Devices.Count) if ($toTake -gt 0) { $selectedDevices = @($bucketInfo.Uređaji | Select-Object -First $toTake) $waveDevices += $selectedDevices $oemDevicesAdded += $toTake $totalZeroSuccessAdded += $toTake $parts = ako ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Matematika]::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, Added $oemDevicesAdded devices" "INFO" $oemsDeployedTo += $oemName } } # Pratite u koje smo OEM-ove implementirali (radi povećavanja na sljedeću provjeru uspjeha) if ($oemsDeployedTo.Count -gt 0) { $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo Write-Log "Implementacija bez uspjeha: $totalZeroSuccessAdded na uređajima $($oemsDeployedTo.Count) OEM-ove" "INFO" } } } if (@($waveDevices). Count -eq 0) { povratna $null } povratna $waveDevices }
# ============================================================================ # GPO DEPLOYMENT (INLINED – stvara GPO, sigurnosnu grupu, veze) # ============================================================================
function Deploy-GPOForWave { param( [niz]$GPOName, [niz]$TargetOU, [niz]$SecurityGroupName, [polje]$WaveHostnames, [bool]$DryRun = $false ) # ADMX pravilnik: SecureBoot.admx – SecureBoot_AvailableUpdatesPolicy # Put registra: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot # Naziv vrijednosti: AvailableUpdatesPolicy # Enabled Value: 22852 (0x5944) - Update all Secure Boot keys + bootmgr # Onemogućena vrijednost: 0 # # Using pravilnik grupe Preferences (GPP) for reliable HKLM\SYSTEM path deployment # GPP stvara postavke u odjeljku: Konfiguracija računala > Preference > postavke sustava Windows > registra $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" $RegistryValueName = "AvailableUpdatesPolicy" $RegistryValue = 22852 # 0x5944 - podudara se s admx enabledValue Write-Log "Implementacija GPO: $GPOName" "WAVE" Write-Log "Registar: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X')))" "INFO" if ($DryRun) { Write-Log "[DRYRUN] Bi stvorio GPO: $GPOName" "INFO" Write-Log "[DRYRUN] Bi stvorio sigurnosnu grupu: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] Bi dodao $(@($WaveHostnames). Brojanje) računala za grupiranje" "INFO" Write-Log "[DRYRUN] Želi povezati GPO na: $TargetOU" "INFO" povratna $true } pokušajte { # Uvoz obaveznih modula Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory – zaustavljanje pogreške } uhvatiti { Write-Log "Uvoz obaveznih modula nije uspio (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR" povratna $false } # Prvi korak: stvaranje ili dohvaćanje GPO-a $existingGPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue if ($existingGPO) { Write-Log "GPO već postoji: $GPOName" "INFO" $gpo = $existingGPO } još { pokušajte { $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Certificate Rollout - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "Created GPO: $GPOName" "OK" } uhvatiti { Write-Log "Stvaranje GPO-a nije uspjelo: $($_. Exception.Message)" "ERROR" vraćanje $false } } # Drugi korak: postavljanje vrijednosti registra pomoću pravilnik grupe Preference (GPP) # GPP je pouzdaniji za HKLM\SYSTEM putove nego Set-GPRegistryValue pokušajte { # Najprije pokušajte ukloniti bilo koju postojeću preferencu za tu vrijednost (da biste izbjegli duplikate) Remove-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue # Stvaranje preferenci registra GPP-a pomoću akcije "Zamijeni" # Zamijeni = Stvori ako ne postoji, ažuriraj ako postoji (najpouzdanije) # Ažuriranje = ažuriranje samo ako postoji (ne uspijeva ako vrijednost ne postoji) Set-GPPrefRegistryValue -Name $GPOName ' -Kontekstno računalo ' -Zamjena akcije ' -Tipka $RegistryKey ' -ValueName $RegistryValueName ' -Upišite DWord ' -Vrijednost $RegistryValue Write-Log "Configured GPP registry preference: $RegistryValueName = 0x5944 (Action=Replace)" "OK" } uhvatiti { Write-Log "GPP failed, trying Set-GPRegistryValue: $($_. Exception.Message)" "WARN" # Fallback to Set-GPRegistryValue (funkcionira ako se implementira ADMX) pokušajte { Set-GPRegistryValue -Name $GPOName ' -Tipka $RegistryKey ' -ValueName $RegistryValueName ' -Upišite DWord ' -Vrijednost $RegistryValue Write-Log "Configured Registry via Set-GPRegistryValue: $RegistryValueName = 0x5944" "OK" } uhvatiti { Write-Log "Postavljanje vrijednosti registra nije uspjelo: $($_. Exception.Message)" "ERROR" povratna $false } } # Treći korak: stvaranje ili dohvaćanje sigurnosne grupe $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $existingGroup) { pokušajte { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers targeted for Secure Boot rollout - $GPOName" ' -Prolaz Write-Log "Stvorena sigurnosna grupa: $SecurityGroupName" "U redu" } uhvatiti { Write-Log "Stvaranje sigurnosne grupe nije uspjelo: $($_. Exception.Message)" "ERROR" povratna $false } } još { Write-Log "Sigurnosna grupa postoji: $SecurityGroupName" "INFO" $group = $existingGroup } # Četvrti korak: dodavanje računala u sigurnosnu grupu $added = 0 $failed = 0 foreach ($hostname u $WaveHostnames) { pokušajte { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } uhvatiti { $failed++ } } Write-Log "Dodano $added računala u sigurnosnu grupu ($failed nije pronađeno u AD)" "U redu" # Peti korak: konfiguriranje sigurnosnog filtriranja na servisu GPO pokušajte { # Ukloni zadanu dozvolu "Autorizirani korisnici" Primijeni dozvolu (zadrži čitanje) Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue # Dodaj dozvolu Primijeni za našu sigurnosnu grupu Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Konfigurirano sigurnosno filtriranje za: $SecurityGroupName" "U redu" } uhvatiti { Write-Log "Konfiguriranje sigurnosnog filtriranja nije uspjelo: $($_. Exception.Message)" "WARN" Write-Log "GPO se može primijeniti na sva računala u povezanom OU - provjerite ručno" "UPOZORENJE" } # Šesti korak: povezivanje GPO-a s OU-om (kritično za primjenu pravilnika) if ($TargetOU) { pokušajte { $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 Da -ErrorAction Stop Write-Log "Linked GPO to: $TargetOU" "OK" Write-Log "GPO će se primijeniti na sljedeći gpupdate na ciljnim računalima" "INFO" } još { Write-Log "GPO je već povezan s ciljnim OU" "INFO" } } uhvatiti { Write-Log "KRITIČNO: povezivanje GPO-a s OU- om nije uspjelo: $($_. Exception.Message)" "ERROR" Write-Log "GPO je stvoren, ali NOT LINKED – neće se primjenjivati ni na koja računala!" "POGREŠKA" Write-Log "Ručno popravak obavezno: New-GPLink -Name '$GPOName' -Target '$TargetOU' -LinkEnabled Da" "ERROR" vraćanje $false } } još { Write-Log "UPOZORENJE: Nije naveden TargetOU – stvoren je GPO, ali NOT LINKED!" "POGREŠKA" Write-Log "Ručno povezivanje potrebno da bi GPO snazi" "ERROR" Write-Log "Pokreni: New-GPLink -Name '$GPOName' -Target '<Your-Domain-DN>' -LinkEnabled Da" "ERROR" } # Sedmi korak: provjera konfiguracije GPO-a Write-Log "Provjera konfiguracije servisa GPO..." "INFO" pokušajte { $gpoReport = Get-GPO -Name $GPOName -ErrorAction Stop Write-Log "GPO status: $($gpoReport.GpoStatus)" "INFO" # Provjerite je li postavka registra konfigurirana $regSettings = Get-GPRegistryValue -Name $GPOName -Key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue if (-not $regSettings) { # Isprobajte provjeru registra GPP -a (drugačiji put u GPO-u) Write-Log "Provjera preferenci registra GPP...". "INFO" } } uhvatiti { Write-Log "Nije moguće provjeriti GPO: $($_. Exception.Message)" "WARN" } vraćanje $true }
# ============================================================================ # WINCS DEPLOYMENT (alternative to AvailableUpdatesPolicy GPO) Ne, ne============================================================================ # Referenca: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe # # WinCS Naredbe (pokretanje na krajnjoj točki u kontekstu sustava): # Upit: WinCsFlags.exe /query --key F33E0C8E002 # Primijeni: WinCsFlags.exe /apply --key "F33E0C8E002" # Ponovno postavi: WinCsFlags.exe /reset --key "F33E0C8E002" # # Ova metoda implementira GPO s zakazanim zadatkom koji se pokreće WinCsFlags.exe /apply # kao SUSTAV na ciljnim krajnjim točkama. Slično načinu na koji se implementira skripta otkrivanja, # ali se pokreće jednom (pri pokretanju) umjesto dnevno.
function Deploy-WinCSGPOForWave { < broj . SINOPSIS Implementirajte WinCS Secure Boot enablement putem zakazanog GPO zadatka.. OPIS Stvara GPO koji implementira zakazani zadatak za pokretanje WinCsFlags.exe /apply u odjeljku KONTEKST SUSTAVA pri pokretanju računala. Ciljne kontrole sigurnosne grupe.. PARAMETAR GPOName Naziv GPO-a.. PARAMETAR TargetOU OU za povezivanje GPO-a s.. PARAMETER SecurityGroupName Sigurnosna grupa za GPO filtriranje.. Nazivi parametra WaveHostnames Nazivi glavnog računala za dodavanje u sigurnosnu grupu.. PARAMETAR WinCSKey Tipka WinCS koja će se primijeniti (zadano: F33E0C8E002).. PARAMETAR DryRun Ako je istinito, zapisujte samo što će biti učinjeno.#> param( [Parameter(Mandatory = $true)] [niz]$GPOName, [Parameter(Mandatory = $false)] [niz]$TargetOU, [Parameter(Mandatory = $true)] [niz]$SecurityGroupName, [Parameter(Mandatory = $true)] [polje]$WaveHostnames, [Parameter(Mandatory = $false)] [string]$WinCSKey = "F33E0C8E002", [Parameter(Mandatory = $false)] [bool]$DryRun = $false ) # Zakazana konfiguracija zadatka za WinCsFlags.exe $TaskName = "SecureBoot-WinCS-Apply" $TaskPath = "\Microsoft\Windows\SecureBoot\" $TaskDescription = "Primjenjuje konfiguraciju sigurnog pokretanja putem WinCS - Ključ: $WinCSKey" Write-Log "Deploying WinCS GPO: $GPOName" "WAVE" Write-Log "Zadatak će se pokrenuti: WinCsFlags.exe /apply --key '"$WinCSKey'"" "INFO" Write-Log "Trigger: At system startup (runs once as SYSTEM)" "INFO" if ($DryRun) { Write-Log "[DRYRUN] Bi stvorio GPO: $GPOName" "INFO" Write-Log "[DRYRUN] Bi stvorio sigurnosnu grupu: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] dodao bi $(@($WaveHostnames). Brojanje) računala za grupiranje" "INFO" Write-Log "[DRYRUN] Implementira zakazani zadatak: $TaskName" "INFO" Write-Log "[DRYRUN] Želi povezati GPO na: $TargetOU" "INFO" vrati @{ Success = $true GPOCreated = $false GroupCreated = $false ComputersAdded = 0 } } pokušajte { # Uvoz obaveznih modula Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory – zaustavljanje pogreške } uhvatiti { Write-Log "Uvoz obaveznih modula nije uspio (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } # Prvi korak: stvaranje ili dohvaćanje GPO-a $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue if ($gpo) { Write-Log "GPO već postoji: $GPOName" "INFO" } još { pokušajte { $gpo = New-GPO -Name $GPOName -Comment "Secure Boot WinCS Deployment - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "Created GPO: $GPOName" "OK" } uhvatiti { Write-Log "Stvaranje GPO-a nije uspjelo: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } } # Drugi korak: stvaranje zakazanog XML-a zadatka za GPO implementaciju # Time se stvara zadatak koji se pokreće WinCsFlags.exe /apply pri pokretanju $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <verzija zadatka="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <opisa>$TaskDescription</Description> WinCsFlags.exe1 stvaranje>sustava</Author> WinCsFlags.exe5 /RegistrationInfo> WinCsFlags.exe7 okidači> WinCsFlags.exe9 boottrigger> <omogućeno>true</Enabled> <kašnjenje>PT5M</Delay> </BootTrigger> </Triggers> <glavnice> <id="Autor"> <UserId>S-1-5-18</UserId> <RunLevel>Najvišadostupna</RunLevel> </Glavni> </Glavnice> <postavke> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <neautotostartIfOnBatteries>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> <omogućeno>true</Enabled> <skriveno>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> WinCsFlags.exe03 DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession> WinCsFlags.exe07 UseUnifiedSchedulingEngine>istinito</UseUnifiedSchedulingEngine> WinCsFlags.exe11 WakeToRun>false</WakeToRun> WinCsFlags.exe15 ExecutionTimeLimit>PT1H</ExecutionTimeLimit> WinCsFlags.exe19 DeleteExpiredTaskAfter>P30D</DeleteExpiredTaskNakon> WinCsFlags.exe23 prioritet>7</Priority> WinCsFlags.exe27 /Settings> WinCsFlags.exe29 akcije Context="Author"WinCsFlags.exe30 WinCsFlags.exe31 exec> WinCsFlags.exe33 naredbeni>WinCsFlags.exe</Command> WinCsFlags.exe37 argumenti>/apply --key "$WinCSKey"WinCsFlags.exe39 /Argumenti> WinCsFlags.exe41 /Exec> WinCsFlags.exe43 /Akcije> WinCsFlags.exe45 /Task> " @
# Step 3: Deploy scheduled task via GPO Preferences # Spremi XML zadatka u SYSVOL za GPO zakazane zadatke Neposredni zadatak pokušajte { $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 Direktorij -Put $sysvolPath -Force | Izlazna vrijednost -Null } # Stvaranje ScheduledTasks.xml za GPP $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"> <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" naziv="$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"> <verzija zadatka="1.3"> <RegistracijaInfo> <opisa>$TaskDescription</Description> </RegistrationInfo> <glavnice> <id="Autor"> <UserId>NT AUTHORITY\System</UserId> <vrsta>S4U</LogonType> <RunLevel>NajvišaDostupna</RunLevel> </Glavni> </Glavnice> <postavke> <idleSettings> <trajanje>PT5M</Duration> <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>istinito</AllowStartOnDemand> <omogućeno>true</Enabled> <skriveno>false</Hidden> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <prioritet>7</Priority> <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> </Settings> <okidači> <timetrigger> <StartBoundary>$(Get-Date -Format 'yyyy-MM-dd')T00:00:00</StartBoundary> <omogućeno>true</Enabled> </TimeTrigger> </Triggers> <akcije> <exec> <naredbeni>WinCsFlags.exe</Command> <argumenti>/apply --key "$WinCSKey"</Argumenti> </Exec> </Akcije> </Task> </Properties> </ImmediateTaskV2> </ScheduledTasks> "@ ("@") $gppTaskXml | Out-File -FilePath (join-path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force Write-Log "Uveden zakazani zadatak u GPO: $TaskName" "OK" } uhvatiti { Write-Log "Implementacija zakazanog XML-a zadatka nije uspjela: $($_. Exception.Message)" "WARN" Write-Log "Vraćanje na implementaciju winCS-a utemeljenu na registru" "INFO" # Fallback: Use WinCS registry approach if GPP scheduled task fails # WinCS također se može pokrenuti putem ključa registra # (Implementacija ovisi o API-ju registra WinCS ako je dostupan) } Četvrti korak: stvaranje ili dohvaćanje sigurnosne grupe $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $group) { pokušajte { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers targeted for Secure Boot WinCS rollout - $GPOName" ' -Prolaz Write-Log "Stvorena sigurnosna grupa: $SecurityGroupName" "U redu" } uhvatiti { Write-Log "Stvaranje sigurnosne grupe nije uspjelo: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } } još { Write-Log "Sigurnosna grupa postoji: $SecurityGroupName" "INFO" } # Peti korak: dodavanje računala u sigurnosnu grupu $added = 0 $failed = 0 foreach ($hostname u $WaveHostnames) { pokušajte { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } uhvatiti { $failed++ } } Write-Log "Dodano $added računala u sigurnosnu grupu ($failed nije pronađeno u AD)" "U redu" # Šesti korak: konfiguriranje sigurnosnog filtriranja na servisu GPO pokušajte { Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Konfigurirano sigurnosno filtriranje za: $SecurityGroupName" "U redu" } uhvatiti { Write-Log "Konfiguriranje sigurnosnog filtriranja nije uspjelo: $($_. Exception.Message)" "WARN" } # Sedmi korak: povezivanje GPO-a s OU-om if ($TargetOU) { pokušajte { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } if (-not $existingLink) { New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "Linked GPO to: $TargetOU" "OK" } još { Write-Log "GPO je već povezan s ciljnim OU" "INFO" } } uhvatiti { Write-Log "KRITIČNO: povezivanje GPO-a s OU- om nije uspjelo: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = "GPO veza nije uspjela: $($_. Exception.Message)" } } } Write-Log "Dovršena je implementacija GPO-a za WinCS" "U redu" Write-Log "Računala će se pokrenuti WinCsFlags.exe sljedećem GPO osvježavanju + ponovno pokretanje/pokretanje" "INFO" vrati @{ Success = $true GPOCreated = $true GroupCreated = $true ComputersAdded = $added ComputersFailed = $failed } }
# Wrapper function to maintain compatibility with main loop funkcija Deploy-WinCSForWave { param( [Parameter(Mandatory = $true)] [polje]$WaveHostnames, [Parameter(Mandatory = $false)] [string]$WinCSKey = "F33E0C8E002", [Parameter(Mandatory = $false)] [string]$WavePrefix = "SecureBoot-Rollout", [Parameter(Mandatory = $false)] [int]$WaveNumber = 1, [Parameter(Mandatory = $false)] [niz]$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 # Pretvori u očekivani oblik povrata vrati @{ Success = $result. Uspjeh Primijenjeno = $result. ComputersAdded Preskočeno = 0 Nije uspjelo = ako ($result. ComputersFailed) { $result. ComputersFailed } else { 0 } Rezultati = @() } }
# ============================================================================ # OMOGUĆI IMPLEMENTACIJU ZADATKA Ne, ne============================================================================ # Implementacija Enable-SecureBootUpdateTask.ps1 na uređaje s onemogućenim zakazanim zadatkom.# Koristi GPO s neposrednim zakazanim zadatkom koji se pokreće jednom.
function Deploy-EnableTaskGPO { < broj . SINOPSIS Implementacija Enable-SecureBootUpdateTask.ps1 putem zakazanog GPO zadatka.. OPIS Stvara GPO koji implementira jednokratni zakazani zadatak radi omogućivanja Zakazani zadatak sigurnog pokretanja na ciljnim uređajima.. PARAMETAR TargetOU OU za povezivanje GPO-a s.. NAZIVI PARAMETRA TargetHostnames Nazivi glavnog računala uređaja s onemogućenim zadatkom (iz izvješća o zbirci).. PARAMETAR DryRun Ako je istinito, zapisujte samo što će biti učinjeno.#> param( [Parameter(Mandatory = $false)] [niz]$TargetOU, [Parameter(Mandatory = $true)] [polje]$TargetHostnames, [Parameter(Mandatory = $false)] [bool]$DryRun = $false ) $GPOName = "SecureBoot-EnableTask-Remediation" $SecurityGroupName = "SecureBoot-EnableTask-Devices" $TaskName = "SecureBoot-EnableTask-OneTime" $TaskDescription = "Jednokratni zadatak za omogućavanje zakazanog zadatka sigurnog pokretanja" Write-Log "=" * 70 "INFO" Write-Log "IMPLEMENTACIJA POPRAVKA ZADATKA" "INFORMACIJE" Write-Log "=" * 70 "INFO" Write-Log "Ciljni uređaji: $($TargetHostnames.Count)" "INFO" Write-Log "GPO: $GPOName" "INFO" Write-Log "Sigurnosna grupa: $SecurityGroupName" "INFO" if ($DryRun) { Write-Log "[DRYRUN] Bi stvorio GPO: $GPOName" "INFO" Write-Log "[DRYRUN] Bi stvorio sigurnosnu grupu: $SecurityGroupName" "INFO" Write-Log "[DRYRUN] Dodao bi $($TargetHostnames.Count) računala u grupu" "INFO" Write-Log "[DRYRUN] implementira jednokratni zakazani zadatak radi omogućivanja ažuriranja sigurnog pokretanja" "INFO" Write-Log "[DRYRUN] Želi povezati GPO na: $TargetOU" "INFO" vrati @{ Success = $true ComputersAdded = 0 DryRun = $true } } pokušajte { # Uvoz obaveznih modula Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory – zaustavljanje pogreške } uhvatiti { Write-Log "Uvoz obaveznih modula nije uspio: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } # Prvi korak: stvaranje ili dohvaćanje GPO-a $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue if ($gpo) { Write-Log "GPO već postoji: $GPOName" "INFO" } još { pokušajte { $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Task Enable Remediation - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "Created GPO: $GPOName" "OK" } uhvatiti { Write-Log "Stvaranje GPO-a nije uspjelo: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } } # Drugi korak: implementacija zakazanog XML-a zadatka u GPO SYSVOL # Zadatak pokreće naredbu ljuske PowerShell radi omogućivanja zadatka sigurnog pokretanja i ažuriranja pokušajte { $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. ID)}\Računalo\Preference\Zakazanizadaci" if (-not (Test-Path $sysvolPath)) { New-Item -ItemType Direktorij -Put $sysvolPath -Force | Izlazna vrijednost -Null } # PowerShell naredba za omogućivanje zadatka 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 }' # Naredba Kodiraj za sigurno ugrađivanje XML-a $encodedCommand = [Pretvori]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand)) $taskGuid = [guid]::NewGuid(). ToString("B"). ToUpper() # GPP Scheduled Task XML – neposredni zadatak koji se pokreće jednom $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"> <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName" image=""0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="$taskGuid" removePolicy="1" userContext="0"> <properties action="C" name="$TaskName" runAs="NT AUTHORITY\SYSTEM" logonType="S4U"> <verzija zadatka="1.3"> <RegistrationInfo> <opisa>$TaskDescription</Description> </RegistrationInfo> <glavnice> <id="Autor"> <UserId>S-1-5-18</UserId> <RunLevel>Najvišadostupna</RunLevel> </Glavni> </Glavnice> <postavke> <idleSettings> <trajanje>PT5M</Duration> <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>istinito</AllowStartOnDemand> <omogućeno>true</Enabled> <skriveno>false</Hidden> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <prioritet>7</priority> <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> </Settings> <akcije> <exec> <naredbeni>powershell.exe</Command> <->-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</argumenti> </Exec> </Akcije> </Task> </Properties> </ImmediateTaskV2> </ScheduledTasks> "@ ("@") $gppTaskXml | Out-File -FilePath (join-path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force Write-Log "Implementiran jednokratni zakazani zadatak u GPO: $TaskName" "U redu" } uhvatiti { Write-Log "Implementacija zakazanog XML-a zadatka nije uspjela: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } # Treći korak: stvaranje ili dohvaćanje sigurnosne grupe $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue if (-not $group) { pokušajte { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers with disabled Secure-Boot-Update task - targeted for remediation" ' -Prolaz Write-Log "Stvorena sigurnosna grupa: $SecurityGroupName" "U redu" } uhvatiti { Write-Log "Stvaranje sigurnosne grupe nije uspjelo: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = $_. Exception.Message } } } još { Write-Log "Sigurnosna grupa postoji: $SecurityGroupName" "INFO" } # Četvrti korak: dodavanje računala u sigurnosnu grupu $added = 0 $failed = 0 foreach ($hostname u $TargetHostnames) { pokušajte { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } uhvatiti { $failed++ Write-Log "Računalo nije pronađeno u AD: $hostname" "WARN" } } Write-Log "Dodano $added računala u sigurnosnu grupu ($failed nije pronađeno u AD)" "U redu" # Peti korak: konfiguriranje sigurnosnog filtriranja na servisu GPO pokušajte { Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Konfigurirano sigurnosno filtriranje za: $SecurityGroupName" "U redu" } uhvatiti { Write-Log "Konfiguriranje sigurnosnog filtriranja nije uspjelo: $($_. Exception.Message)" "WARN" } # 6. korak: povezivanje GPO-a s OU-om if ($TargetOU) { pokušajte { $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 Da -ErrorAction Stop Write-Log "Linked GPO to: $TargetOU" "OK" } još { Write-Log "GPO je već povezan s ciljnim OU" "INFO" } } uhvatiti { Write-Log "Nije uspjelo povezivanje GPO-a s OU: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Pogreška = "GPO veza nije uspjela: $($_. Exception.Message)" } } } još { Write-Log "Nije naveden TargetOU – GPO će se morati ručno povezati" "UPOZORENJE" } Write-Log "" "INFO" Write-Log "OMOGUĆI DOVRŠENJE IMPLEMENTACIJE ZADATKA" "U redu" Write-Log "Uređaji će pokrenuti zadatak omogućivanja pri sljedećem GPO osvježavanju (gpupdate)" "INFO" Write-Log " zadatak runs jedanput kao SISTEM i enables Secure-Boot-Update" "INFO" Write-Log "" "INFO" vrati @{ Success = $true ComputersAdded = $added ComputersFailed = $failed GPOName = $GPOName SecurityGroup = $SecurityGroupName } }
# ============================================================================ # OMOGUĆI ZADATAK NA ONEMOGUĆENIM UREĐAJIMA Ne, ne============================================================================ if ($EnableTaskOnDisabled) { Write-Host "" Write-Host ("=" * 70) -Boja prednjeg plana Žuta Write-Host " ENABLE TASK REMEDIATION - Fixing Disabled Scheduled Tasks" -ForegroundColor Yellow Write-Host ("=" * 70) -Boja prednjeg plana Žuta Write-Host "" # Pronalaženje uređaja s onemogućenim zadatkom iz podataka o agregaciji if (-not $AggregationInputPath) { Write-Host "POGREŠKA: -AggregationInputPath je potreban za identifikaciju uređaja s onemogućenim zadatkom" -ForegroundColor Red Write-Host "Korištenje: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <put> -ReportBasePath <put>" -ForegroundColor Gray izlaz 1 } Write-Host "Skeniranje za uređaje s onemogućenim zadatkom secure-boot-update..." -ForegroundColor Cyan # Učitaj JSON datoteke i pronađi uređaje s onemogućenim zadatkom $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Name -notmatch "ScanHistory|RolloutState|RolloutPlan" } $disabledTaskDevices = @() foreach ($file u $jsonFiles) { pokušajte { $device = Get-Content $file. FullName – neobrađeno | ConvertFrom-Json ako ($device. SecureBootTaskEnabled -eq $false -ili $device je. SecureBootTaskStatus -eq 'Onemogućeno' -ili $device je. SecureBootTaskStatus -eq 'NotFound') { # Obuhvaća samo uređaje koji još nisu ažurirani (nema događaja 1808) if ([int]$device. Event1808Count -eq 0) { $disabledTaskDevices += $device. Hostname } } } uhvatiti { # Preskoči datoteke koje nisu valjane } } $disabledTaskDevices = $disabledTaskDevices | Select-Object – jedinstveno if ($disabledTaskDevices.Count -eq 0) { Write-Host "" Write-Host "Nije pronađen nijedan uređaj s onemogućenim zadatkom secure-boot-update". -ForegroundColor Green Write-Host "Svi uređaji imaju omogućen zadatak ili su već ažurirani." -ForegroundColor Gray izlaz 0 } Write-Host "" Write-Host "Pronađeno $($disabledTaskDevices.Count) uređaji s onemogućenim zadatkom:" -ForegroundColor Yellow $disabledTaskDevices | Select-Object - Prvih 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } if ($disabledTaskDevices.Count -gt 20) { Write-Host " ... and $($disabledTaskDevices.Count - 20) more" -ForegroundColor Gray } Write-Host "" # Implementacija GPO-a omogućivanja zadatka $result = Deploy-EnableTaskGPO -TargetHostnames $disabledTaskDevices -TargetOU $TargetOU -DryRun $DryRun ako ($result. Uspjeh) { Write-Host "" Write-Host "SUCCESS: Enable Task GPO deployed" -ForegroundColor Green Write-Host " Računala dodana u sigurnosnu grupu: $($result. ComputersAdded)" -ForegroundColor Cyan ako ($result. ComputersFailed -gt 0) { Write-Host " Računala nisu pronađena u ad: $($result. ComputersFailed)" - Boja prednjeg plana Žuta } Write-Host "" Write-Host "SLJEDEĆI KORACI:" -ForegroundColor White Write-Host " 1. Uređaji će primiti GPO prilikom sljedećeg osvježavanja (gpupdate /force)" -ForegroundColor Gray Write-Host " 2. Jednokratni zadatak omogućit će sigurnosno ažuriranje" -ForegroundColor Gray Write-Host " 3. Ponovno pokretanje agregacije radi provjere je li zadatak sada omogućen" -ForegroundColor Gray } još { Write-Host "" Write-Host "NIJE USPJELO: Nije moguće implementirati Enable Task GPO" -ForegroundColor Red Write-Host "Pogreška: $($result. Error)" -ForegroundColor Red } izlaz 0 }
# ============================================================================ # GLAVNA PETLJA ORKESTRACIJE # ============================================================================
Write-Host "" Write-Host ("=" * 80) -Boja prednjeg plana Cyan Write-Host " SECURE BOOT ROLLOUT ORCHESTRATOR - CONTINUOUS DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 80) -Boja prednjeg plana Cyan Write-Host ""
if ($DryRun) { Write-Host "[DRY RUN MODE]" -ForegroundColor Magenta }
if ($UseWinCS) { Write-Host "[WinCS MODE]" - Boja prednjeg plana Žuta Write-Host "Korištenje WinCsFlags.exe umjesto GPO/AvailableUpdatesPolicy" -ForegroundColor Yellow Write-Host "WinCS ključ: $WinCSKey" -Boja prednjeg plana Siva Write-Host "" }
Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "Put unosa: $AggregationInputPath" "INFO" Write-Log "Put izvješća: $ReportBasePath" "INFO" if ($UseWinCS) { Write-Log "Način implementacije: WinCS (WinCsFlags.exe /apply --key '"$WinCSKey'")" "INFO" } još { Write-Log "Način implementacije: GPO (AvailableUpdatesPolicy)" "INFO" }
# Resolve TargetOU - default to domain root for domain-wide coverage # Potrebno je samo za način implementacije GPO-a (WinCS ne zahtijeva AD/GPO) if (-not $UseWinCS -and -not $TargetOU) { pokušajte { # Isprobajte više metoda za dohvaćanje DN domene $domainDN = $null # Metoda 1: Get-ADDomain (potreban je RSAT-AD-PowerShell) pokušajte { Import-Module ActiveDirectory – zaustavljanje pogreške $domainDN = (Get-ADDomain -ErrorAction Stop). Razlikovni Naziv } uhvatiti { Write-Log "Get-ADDomain nije uspio: $($_. Exception.Message)" "WARN" } # Metoda 2: Koristite RootDSE putem ADSI if (-not $domainDN) { pokušajte { $rootDSE = [ADSI]"LDAP://RootDSE" $domainDN = $rootDSE.defaultNamingContext.ToString() } uhvatiti { Write-Log "ADSI RootDSE nije uspio: $($_. Exception.Message)" "WARN" } } # Metoda 3: Raščlanjivanje iz članstva u domeni računala if (-not $domainDN) { pokušajte { $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() $domainDN = "DC=" + ($domain. Naziv -replace '\.', ',DC=') } uhvatiti { Write-Log "GetComputerDomain nije uspio: $($_. Exception.Message)" "WARN" } } if ($domainDN) { $TargetOU = $domainDN Write-Log "Target: Domain Root ($domainDN) - GPO will apply domain-wide via security group filtering" "INFO" } još { Write-Log "Ne može se odrediti domena DN – GPO će se stvoriti, ali NOT LINKED!" "POGREŠKA" Write-Log "Navedite parametar -TargetOU ili povežite GPO ručno nakon stvaranja" "ERROR" $TargetOU = $null } } uhvatiti { Write-Log "Nije moguće dohvatiti DN domenu – GPO će se stvoriti, ali neće biti povezan. Po potrebi ručno povežite vezu." "UPOZORI" Write-Log "Pogreška: $($_. Exception.Message)" "WARN" $TargetOU = $null } } još { Write-Log "Cilj OU: $TargetOU" "INFO" }
Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Interval ankete: $PollIntervalMinutes minuta" "INFO" if ($LargeScaleMode) { Write-Log "LargeScaleMode enabled (veličina grupe: $ProcessingBatchSize, uzorak zapisnika: $DeviceLogSampleSize)" "INFO" }
# ============================================================================ # PROVJERA PREDUVJETA: provjera je li otkrivanje implementiran i radi # ============================================================================
Write-Host "" Write-Log "Provjera preduvjeta..." "INFO"
$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath if (-not $detectionCheck.IsDeployed) { Write-Log $detectionCheck.Poruka "POGREŠKA" Write-Host "" Write-Host "OBAVEZNO: prvo implementiraj infrastrukturu otkrivanja:" -Boja prednjeg plana Žuta Write-Host " 1. Pokreni: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\server\SecureBootLogs$'" -ForegroundColor Cyan Write-Host 2. Pričekajte da uređaji prijave (12-24 sata)" -ForegroundColor Cyan Write-Host " 3. Re-run this orchestrator" -ForegroundColor Cyan Write-Host "" if (-not $DryRun) { Vratiti } } još { Write-Log $detectionCheck.Poruka "U redu" }
# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Svježina podataka: $($freshness. Datoteke TotalFiles, $($freshness. FreshFiles) fresh (<24h), $($freshness. StaleFiles) zastarjelo (>72h)" "INFO" ako ($freshness. Upozorenje) { Write-Log $freshness, ne. Upozorenje "UPOZORENJE" }
# Load Allow List (targeted rollout - ONLY these devices will be rolled out) $allowedHostnames = @() if ($AllowListPath -ili $AllowADGroup) { $allowedHostnames = Get-AllowedHostnames -AllowFilePath $AllowListPath -ADGroupName $AllowADGroup if ($allowedHostnames.Count -gt 0) { Write-Log "AllowList: ONLY $($allowedHostnames.Count) uređaji će se smatrati za implementaciju" "INFO" } još { Write-Log "AllowList naveden, ali nije pronađen nijedan uređaj – time će se blokirati sva rollouts!" "UPOZORI" } }
# 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 izuzetak: $($excludedHostnames.Count) uređaji će se preskočiti s implementacije" "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-dd HH:mm:ss" Write-Log "Pokretanje novog izdanja" "WAVE" }
Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "Blokirane grupe: $($blockedBuckets.Count)" "INFO"
# Main loop - runs until all eligible devices are updated $iterationCount = 0 dok ($true) { $iterationCount++ Write-Host "" Write-Host ("=" * 80) -Boja prednjeg plana Bijela Write-Log "=== ITERACIJA $iterationCount ===" "VAL" Write-Host ("=" * 80) -Boja prednjeg plana Bijela # Korak 1: Pokretanje agregacije Write-Log "Korak 1: pokretanje agregacije..." "INFO" # Orchestrator uvijek ponovno koristi jednu mapu (LargeScaleMode) da bi izbjegao bloat diska # Administratori koji izvode agregator ručno dohvaćaju vremenske oznake mapa za brze snimke u točkama u vremenu $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current" # Provjerite svježinu podataka prije agregacije $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Svježina podataka: $($freshness. FreshFiles)/$($freshness. TotalFiles) uređaji prijavljeni u posljednjih 24h" "INFO" ako ($freshness. Upozorenje) { Write-Log $freshness, ne. Upozorenje "UPOZORENJE" } $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1" $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json" $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" if (test-path $aggregateScript) { if (-not $DryRun) { # Orchestrator uvijek koristi strujanje + inkrementalno za učinkovitost # Agregator se automatski uzdiže na PS7 ako je dostupan za najbolje performanse $aggregateParams = @{ InputPath = $AggregationInputPath OutputPath = $aggregationPath StreamingMode = $true IncrementalMode = $true SkipReportIfUnchanged = $true ParallelThreads = 8 } # Sažetak o unošavanja prolaza ako postoji (za podatke o brzini/projekciji) if (test-path $rolloutSummaryPath) { $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath } & $aggregateScript @aggregateParams # Pokaži naredbu za generiranje pune HTML nadzorne ploče s tablicama uređaja Write-Host "" Write-Host "Za generiranje pune HTML nadzorne ploče s tablicama proizvođača/modela, pokreni:" -ForegroundColor Yellow Write-Host " $aggregateScript -InputPath '"$AggregationInputPath'" -OutputPath '"$aggregationPath'"" -ForegroundColor Yellow Write-Host "" } još { Write-Log "[DRYRUN] Bi pokrenuti agregaciju" "INFO" # Na servisu DryRun izravno koristite postojeće podatke o agregaciji iz programa ReportBasePath $aggregationPath = $ReportBasePath } } $rolloutState.LastAggregation = Get-Date -Format "yyyy-MM-dd HH:mm:ss" # Korak 2: Učitavanje trenutnog statusa uređaja Write-Log "2. korak: učitavanje statusa uređaja..." "INFO" $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue | Where-Object { $_. Naziv -notlike "*Buckets*" } | Sort-Object LastWriteTime -Descending | Select-Object - Prvi 1 if (-not $notUptodateCsv -and -not $DryRun) { Write-Log "Nisu pronađeni podaci o agregaciji. Čekanje..." "UPOZORI" Start-Sleep -seconds ($PollIntervalMinutes * 60) Nastaviti } $notUpdatedDevices = ako ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } još { @() } Write-Log "Uređaji nisu ažurirani: $($notUpdatedDevices.Count)" "INFO" $notUpdatedIndexes = Get-NotUpdatedIndexes -Devices $notUpdatedDevices # Treći korak: ažuriranje povijesti uređaja (praćenje prema nazivu glavnog računala) Write-Log "Treći korak: ažuriranje povijesti uređaja..." "INFO" Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory Save-DeviceHistory -Povijest $deviceHistory # Četvrti korak: traženje blokiranih grupa (nedostupnih uređaja) $existingBlockedCount = $blockedBuckets.Count Write-Log "Korak 4: Provjera blokiranih grupa (pinging uređaji nakon razdoblja čekanja)..." "INFO" if ($existingBlockedCount -gt 0) { Write-Log "Trenutno blokirane grupe iz prethodnih izvođenja: $existingBlockedCount" "INFO" } if ($adminApproved.Count -gt 0) { Write-Log "Administrator-odobrene grupe (neće se ponovno blokirati): $($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 -blokirano $blockedBuckets Write-Log "Novo blokirane grupe (ova iteracija): $($newlyBlocked.Count)" "BLOKIRANO" } # Korak 4b: automatsko deblokiranje grupa u kojima su se uređaji ažurirali $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize if ($autoUnblocked.Count -gt 0) { Save-BlockedBuckets -blokirano $blockedBuckets Write-Log "Automatski deblokirane grupe (ažurirani uređaji): $($autoUnblocked.Count)" "U redu" } # Peti korak: izračun preostalih uređaja koji ispunjavaju uvjete $eligibleCount = 0 foreach ($device u $notUpdatedDevices) { $bucketKey = Get-BucketKey $device if (-not $blockedBuckets.Contains($bucketKey)) { $eligibleCount++ } } Write-Log "Preostali uređaji koji ispunjavaju uvjete: $eligibleCount" "INFO" Write-Log "Blokirane grupe: $($blockedBuckets.Count)" "INFO" # 6. korak: provjera dovršetka if ($eligibleCount -eq 0) { Write-Log "ROLLOUT COMPLETE – svi uređaji koji ispunjavaju uvjete su ažurirani!" "U redu" $rolloutState.Status = "Dovršeno" $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Save-RolloutState - savezna $rolloutState Slomiti } # 6. korak: generiranje i implementacija sljedećeg vala Write-Log "6. korak: generiranje vala za postavljanje..." "INFO" $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames # Provjerite imamo li uređaje za implementaciju ($waveDevices mogu biti $null, prazni ili sa stvarnim uređajima) $hasDevices = $waveDevices -i @($waveDevices | Where-Object { $_ }). Broj -gt 0 if ($hasDevices) { # Samo broj vala inkrementa kada zapravo imamo uređaje za implementaciju $rolloutState.CurrentWave++ Write-Log "Val $($rolloutState.CurrentWave): $(@($waveDevices). Count) uređaji" "WAVE" # Implementirajte GPO pomoću inlined funkcije $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $hostnames = @($waveDevices | ForEach-Object { ako ($_. Naziv glavnog računala) { $_. Hostname } elseif ($_. HostName) { $_. HostName } još { $null } } | Where-Object { $_ }) # Spremi datoteku naziva glavnog računala za referencu/nadzor $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt" $hostnames | Out-File $hostnamesFile kodiranje UTF8 # Provjerite imamo li nazive glavnog računala za implementaciju ako ($hostnames. Count -eq 0) { Write-Log "Valjani nazivi glavnog računala nisu pronađeni u valu $($rolloutState.CurrentWave) – na uređajima možda nedostaje svojstvo Hostname" "WARN" Write-Log "Preskakanje implementacije za ovaj val – provjera podataka uređaja" "UPOZORENJE" # Još uvijek čekati prije sljedećeg iteracija if (-not $DryRun) { Write-Log "Spavanje na $PollIntervalMinutes minuta prije ponovnog pokušaja..." "INFO" Start-Sleep sekundi ($PollIntervalMinutes * 60) } Nastaviti } Write-Log "Implementacija u $($hostnames. Count) nazivi glavnog računala u valu $($rolloutState.CurrentWave)" "INFO" # Implementirajte pomoću metode WinCS ili GPO na temelju parametra -UseWinCS if ($UseWinCS) { # Metoda WinCS: stvorite GPO s zakazanim zadatkom koji će se WinCsFlags.exe kao SUSTAV na svakoj krajnjoj točki Write-Log "Using WinCS deployment method (Key: $WinCSKey)" "WAVE" $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames ' -WinCSKey $WinCSKey ' -WavePrefix $WavePrefix ' -WaveNumber $rolloutState.CurrentWave ' -TargetOU $TargetOU ' -DryRun:$DryRun if (-not $wincsResult.Success) { Write-Log "Implementacija winCS-a nije uspjela – primijenjeno: $($wincsResult.Applied), Failed: $($wincsResult.Failed)" "WARN" } još { Write-Log "Uspješna implementacija winCS-a – primijenjeno: $($wincsResult.Applied), Preskočeno: $($wincsResult.Skipped)" "U redu" } # Save WinCS results for audit $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json" $wincsResult | ConvertTo-Json -Dubina 5 | Out-File $wincsResultFile kodiranje UTF8 } još { # Metoda GPO: stvaranje GPO-a s postavkom registra AvailableUpdatesPolicy $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun if (-not $gpoResult) { Write-Log "GPO implementacija nije uspjela – ponovno će pokušati sljedeće iteraciju" "ERROR" } } # Record wave in state $waveRecord = @{ WaveNumber = $rolloutState.CurrentWave StartedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" DeviceCount = @($waveDevices). Računati Uređaji = @($waveDevices | ForEach-Object { @{ (1000 Naziv glavnog računala = ako ($_. Naziv glavnog računala) { $_. Hostname } elseif ($_. HostName) { $_. HostName } još { $null } BucketKey = Get-BucketKey $_ } Ne, ne, ne, ne. } # Provjerite je li WaveHistory uvijek polje prije dodavanja (sprječava probleme sa spajanjem raspršivanja) $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord) $rolloutState.TotalDevicesTargeted += @($waveDevices). Računati Save-RolloutState - savezna $rolloutState Write-Log "Val $($rolloutState.CurrentWave). Pričekajte $PollIntervalMinutes minuta..." "U redu" } još { # Prikaži status implementiranih uređaja koji čekaju ažuriranja Write-Log "" "INFO" Write-Log "========== SVI UREĐAJI IMPLEMENTIRANA - ČEKANJE NA STATUS ==========" "INFO" # Nabavite sve implementirane uređaje iz povijesti vala $allDeployedLookup = @{} foreach ($wave in $rolloutState.WaveHistory) { ($device $wave. Uređaji) { ako ($device. Naziv glavnog računala) { $allDeployedLookup[$device. Naziv glavnog računala] = @{ Naziv glavnog računala = $device. Hostname BucketKey = $device. Ključ kante DeployedAt = $wave. StartedAt WaveNumber = $wave. WaveNumber } } } } $allDeployedDevices = @($allDeployedLookup.Vrijednosti) if ($allDeployedDevices.Count -gt 0) { # Pronađite koji su implementirani uređaji i dalje na čekanju (na popisu NotUpdated) $stillPendingCount = 0 $noLongerPendingCount = 0 $pendingSample = @() foreach ($deployed u $allDeployedDevices) { if ($notUpdatedIndexes.HostSet.Contains($deployed. Naziv glavnog računala)) { $stillPendingCount++ if ($pendingSample.Count -lt $DeviceLogSampleSize) { $pendingSample += $deployed. Hostname } } još { $noLongerPendingCount++ } } # Get actual Updated counts from aggregation - differentiate Event 1808 vs UEFICA2023Status $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Summary*.csv" | Sort-Object LastWriteTime -Descending | Select-Object - Prvi 1 $actualUpdated = 0 $totalDevicesFromSummary = 0 $event 1808Count = 0 $uefiStatusUpdated = 0 $needsRebootSample = @() if ($summaryCsv) { $summary = Import-Csv $summaryCsv.FullName | Select-Object - Prvi 1 ako ($summary. Ažurirano) { $actualUpdated = [int]$summary. Ažurirano } ako ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices } } # Izračun brzine iz povijesti vala (uređaji ažurirani po danu) $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 } } # Save rollout summary with weekend-aware projections # Koristite zbroj za agregator NotUptodate (isključuje SB OFF uređaje) radi dosljednosti $notUpdatedCount = ako ($summary -i $summary. NotUptodate) { [int]$summary. NotUptodate } još { $totalDevicesFromSummary - $actualUpdated } Save-RolloutSummary -State $rolloutState ' -TotalDevices $totalDevicesFromSummary ' -UpdatedDevices $actualUpdated ' -NotUpdatedDevices $notUpdatedCount ' -DevicesPerDay $devicesPerDay # Provjerite neobrađene podatke za uređaje s UEFICA2023Status=Ažurirano, ali bez događaja 1808 (potrebno je ponovno pokretanje) $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue $totalDataFiles = @($dataFiles). Računati $batchSize = [Matematika]::Max(500, $ProcessingBatchSize) if ($LargeScaleMode) { $batchSize = [Matematika]::Max(2000, $ProcessingBatchSize) }
if ($totalDataFiles -gt 0) { for ($idx = 0; $idx -lt $totalDataFiles; $idx += $batchSize) { $end = [Matematika]::Min($idx + $batchSize - 1, $totalDataFiles - 1) $batchFiles = $dataFiles[$idx.. $end je.
foreach ($file in $batchFiles) { pokušajte { $deviceData = Get-Content $file. FullName – neobrađeno | ConvertFrom-Json $hostname = $deviceData.Hostname if (-not $hostname) { continue } $has 1808 = [int]$deviceData.Event1808Count -gt 0 $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Ažurirano" if ($has 1808) { $event 1808Count++ } elseif ($hasUefiUpdated) { $uefiStatusUpdated++ if ($needsRebootSample.Count -lt $DeviceLogSampleSize) { $needsRebootSample += $hostname } } } uhvatiti { } }
Save-ProcessingCheckpoint -Stage "RebootStatusScan" -Processed ($end + 1) -Total $totalDataFiles -Metrics @{ Event1808Count = $event 1808Count UEFIUpdatedAwaitingReboot = $uefiStatusUpdated } } } Write-Log "Ukupna implementacija: $($allDeployedDevices.Count)" "INFO" Write-Log "Ažurirano (događaj 1808 potvrđen): $event 1808Count" "U redu" if ($uefiStatusUpdated -gt 0) { Write-Log "Ažurirano (UEFICA2023Status=Ažurirano, čeka se ponovno pokretanje): $uefiStatusUpdated" "U redu" $rebootSuffix = ako ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) više)" } još { "" } Write-Log " Uređaji kojima je potrebno ponovno pokretanje za događaj 1808 (uzorak): $($needsRebootSample -join ', ')$rebootSuffix" "INFO" Write-Log " Ti će uređaji prijaviti događaj 1808 nakon sljedećeg ponovnog pokretanja" "INFO" } Write-Log "Više nije na čekanju: $noLongerPendingCount (obuhvaća SecureBoot OFF, nedostaju uređaji)" "INFO" Write-Log "Čeka se status: $stillPendingCount" "INFO" if ($stillPendingCount -gt 0) { $pendingSuffix = ako ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) više)" } još { "" } Write-Log "Uređaji na čekanju (uzorak): $($pendingSample -join ', ')$pendingSuffix" "UPOZORENJE" } } još { Write-Log "Još nisu implementirali nijedan uređaj" "INFO" } Write-Log "================================================================" "INFO" Write-Log "" "INFO" } # Čekaj prije sljedećeg iteracije if (-not $DryRun) { Write-Log "Spavanje na $PollIntervalMinutes minuta..." "INFO" Start-Sleep sekundi ($PollIntervalMinutes * 60) } još { Write-Log "[DRYRUN] Bi $PollIntervalMinutes minuta" "INFO" break # Izlaz nakon jedne iteracije na suhom } }
# ============================================================================ # KONAČNI SAŽETAK # ============================================================================
Write-Host "" Write-Host ("=" * 80) -Boja prednjeg plana Zelena Write-Host " ROLLOUT ORCHESTRATOR SUMMARY" -ForegroundColor Green Write-Host ("=" * 80) - Boja prednjeg plana Zelena 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 "Ciljani uređaji: $($finalState.TotalDevicesTargeted)" Write-Host "Blokirane grupe: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } else { "Green" }) Write-Host "Uređaji praćeni: $($deviceHistory.Count)" -ForegroundColor Gray Write-Host ""
if ($finalBlocked.Count -gt 0) { Write-Host "BLOKIRANE KANTE (zahtijeva ručno pregledavanje):" -ForegroundColor Red foreach ($key in $finalBlocked.Keys) { $info = $finalBlocked[$key] Write-Host " - $key" - Crveni prednji plan Write-Host " Razlog: $($info. Reason)" -ForegroundColor Gray } Write-Host "" Write-Host "Blocked buckets file: $blockedBucketsPath" -ForegroundColor Yellow }
Write-Host "" Write-Host "State files:" - ForegroundColor Cyan Write-Host " Stanje rollout: $rolloutStatePath" Write-Host " Blokirane grupe: $blockedBucketsPath" Write-Host " Povijest uređaja: $deviceHistoryPath" Write-Host ""