Kopioi ja liitä tämä esimerkkikomentosarja ja muokkaa sitä tarpeen mukaan ympäristössäsi:
<# . SYNOPSIS Jatkuva suojatun käynnistyksen käyttöönoton orkestraattori, joka suoritetaan, kunnes käyttöönotto on valmis.
.DESCRIPTION Tämä komentosarja tarjoaa täydellisen kokonaisvaltaisen automaation suojatun käynnistyksen varmenteen käyttöönottoa varten: 1. Luo koostetietoihin perustuvia käyttöönottoaaltoja 2. Luo AD-ryhmät ja ryhmäkäytäntöobjektin kullekin aallolle 3. Laitepäivitysten näytöt (tapahtuma 1808) 4. Tunnistaa estetyt säilöt (laitteet, joita ei voi käyttää) 5. Siirtyy automaattisesti seuraavaan aaltoon 6. Suoritetaan, kunnes KAIKKI vaatimukset täyttävät laitteet on päivitetty Valmistumisehdot: - Laitteita ei ole jäljellä: Toiminto pakollinen, suuri luottamus, tarkkailu, tilapäisesti keskeytetty - Vaikutusalueen ulkopuolelle (rakenteen mukaan): Ei tuettu, suojattu käynnistys poistettu käytöstä - Toimii jatkuvasti, kunnes se on valmis – ei satunnaista aaltorajoitusta Käyttöönottostrategia: - SUURI LUOTTAMUS: Kaikki ensimmäisen aallon laitteet (turvallinen) - TOIMINTO PAKOLLINEN: Progressiiviset kaksoisarvot (1→2→4→8...) Logiikan estäminen: - MaxWaitHoursin jälkeen orkestraattori lähettää ping-ping-laitteita, jotka eivät ole päivittyneet - Jos laite ei ole käytettävissä (ping epäonnistuu), → säilö on estetty tutkintaa varten - Jos laite on SAAVUTETTAVISSA, mutta sitä ei päivitetä → odota (saattaa edellyttää uudelleenkäynnistystä) - Estetyt säilöt jätetään pois, kunnes järjestelmänvalvoja poistaa niiden eston Eston poistaminen automaattisesti: - Jos estettyjen säilöjen laite näkyy myöhemmin päivitettynä (tapahtuma 1808), säilön esto poistetaan automaattisesti ja käyttöönotto etenee - Tämä toiminto käsittelee laitteita, jotka olivat tilapäisesti offline-tilassa, mutta jotka tulivat takaisin Laitteen seuranta: - Seuraa laitteita isäntänimen mukaan (olettaa, että nimet eivät muutu käyttöönoton aikana) - Huomautus: JSON-kokoelma ei sisällä yksilöllistä konetunnusta; lisää seurantaa parantava lisä
.PARAMETER AggregationInputPath Polku JSON-laitteen raakatietoihin (kohdasta Tunnista komentosarja)
.PARAMETER ReportBasePath Koosteraporttien peruspolku
.PARAMETER TargetOU Yhdistä ryhmäkäytäntöobjektit yhdistävän OU:n tunnus.Valinnainen – jos sitä ei määritetä, ryhmäkäytäntöobjekti on linkitetty toimialueen pääkansioon koko toimialueen kattavuutta varten.Käyttöoikeusryhmän suodatus varmistaa, että vain kohdennetut laitteet saavat käytännön.
.PARAMETER MaxWaitHours Tunteja odottaa, että laitteet päivittyvät ennen tavoitettavuuden tarkistamista.Tämän jälkeen laitteista, joita ei ole päivitetty, lähetetään ping-ping-kysely.Tavoittamattomat laitteet estävät säilön käytön.Oletus: 72 (3 päivää)
.PARAMETER PollIntervalMinutes Tilatarkistusten väliset minuutit. Oletus: 1 440 (1 päivä)
.PARAMETER AllowListPath Polku tiedostoon, joka sisältää isäntänimet, salli käyttöönottoon (kohdennettu käyttöönotto).Tukee .txt (yksi isäntänimi riviä kohti) tai .csv (Isäntänimi/TietokoneNimi/Nimi-sarakkeessa).Kun tämä on määritetty, vain nämä laitteet sisällytetään käyttöönottoon.BlockList-luetteloa käytetään edelleen Salliluettelo-toiminnolla.
.PARAMETER AllowADGroup Salli-asetuksen sisältävän AD-käyttöoikeusryhmän nimi.Esimerkki: SecureBoot-Pilot-Computers tai Wave1-Devices Kun tämä on määritetty, vain tämän ryhmän laitteet sisällytetään käyttöönottoon.Yhdistäminen AllowListPathin kanssa sekä tiedosto- että AD-pohjaista kohdentamista varten.
.PARAMETER ExclusionListPath Polku tiedostoon, joka sisältää isäntänimet, ja jätä pois käyttöönotosta (VIP/executive-laitteet).Tukee .txt (yksi isäntänimi riviä kohti) tai .csv (Isäntänimi/TietokoneNimi/Nimi-sarakkeessa).Näitä laitteita ei koskaan sisällytetä käyttöönottoaaltoihin.BlockList-suodatinta käytetään AllowList-suodatuksen jälkeen. . PARAMETER ExcludeADGroup Sen AD-käyttöoikeusryhmän nimi, joka sisältää pois jätettävät tietokonetilit.Esimerkki: "VIP-computers" tai "Executive-Devices" Yhdistämällä ExclusionListPathin kanssa voit käyttää sekä tiedosto- että AD-pohjaisia poissulkemisia.
.PARAMETER UseWinCS Käytä WinCS:iä (Windows configuration System) ryhmäkäytäntöobjektin/AvailableUpdatesPolicyn sijaan.WinCS ottaa suojatun käynnistyksen käyttöön suorittamalla WinCsFlags.exe suoraan kussakin päätepisteessä.WinCsFlags.exe suoritetaan SYSTEM-kontekstissa ajoitetun tehtävän kautta.Tästä menetelmästä on hyötyä seuraavissa: - Nopeammat käyttöönotot (välitön vaikutus verrattuna ryhmäkäytäntöobjektin käsittelyn odottamiseen) - Muut kuin toimialueeseen liitetyt laitteet - Ympäristöt, joilla ei ole AD/GPO-infrastruktuuria Viite: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe
.PARAMETER WinCSKey WinCS-näppäin suojatun käynnistyksen käyttöönottoon.Oletus: F33E0C8E002 Tämä avain vastaa suojatun käynnistyksen käyttöönottomääritystä. . PARAMETER DryRun Näytä, mitä tehdään tekemättä muutoksia
.PARAMETER ListBlockedBuckets Näytä kaikki tällä hetkellä estetyt säilöt ja poistu
.PARAMETER UnblockBucket Tietyn säilön eston poistaminen näppäimen mukaan ja poistuminen
.PARAMETER UnblockAll Kaikkien säilöjen eston poistaminen ja poistuminen
.PARAMETER EnableTaskOnDisabled Ota Enable-SecureBootUpdateTask.ps1 käyttöön kaikissa laitteissa, joiden ajoitettu tehtävä on poistettu käytöstä.Luo ryhmäkäytäntöobjektin, jossa on kertaluonteinen ajoitettu tehtävä, joka suorittaa Ota komentosarja käyttöön -Quiet-toiminnolla -vaihtoehdon.Tästä on hyötyä sellaisten laitteiden korjaamisessa, joissa secure-boot-update-tehtävä on poistettu käytöstä.
.EXAMPLE .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -TargetOU "OU=Workstations,DC=contoso,DC=com"
.EXAMPLE # Luettelo estetyt säilöt .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -ListBlockedBuckets
.EXAMPLE # Poista tietyn säilön esto .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "E:\SecureBootReports" -UnblockBucket "Dell_Latitude5520_BIOS1.2.3"
.EXAMPLE # Jätä VIP-laitteet pois käyttöönotosta tekstitiedoston avulla .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExclusionListPath "C:\Admin\VIP-Devices.txt"
.EXAMPLE # Sulje pois AD-käyttöoikeusryhmän laitteet (esim. executive laptopit) .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -ExcludeADGroup "VIP-Computers"
.EXAMPLE # Käytä WinCS:iä (Windows configuration System) ryhmäkäytäntöobjektin/AvailableUpdatesPolicyn sijaan # WinCsFlags.exe suoritetaan JÄRJESTELMÄ-kontekstissa jokaisessa päätepisteessä ajoitetun tehtävän kautta .\Start-SecureBootRolloutOrchestrator.ps1 ' -AggregationInputPath "\\server\SecureBootLogs$\Json" ' -ReportBasePath "E:\SecureBootReports" ' -UseWinCS ' -WinCSKey "F33E0C8E002" #>
[CmdletBinding()] param( [Parameter(Mandatory = $false)] [merkkijono]$AggregationInputPath, [Parametri(Pakollinen = $false)] [merkkijono]$ReportBasePath, [Parametri(Pakollinen = $false)] [merkkijono]$TargetOU, [Parametri(Pakollinen = $false)] [merkkijono]$WavePrefix = "SecureBoot-Rollout", [Parameter(Mandatory = $false)] [int]$MaxWaitHours = 72, [Parametri(Pakollinen = $false)] [int]$PollIntervalMinutes = 1440,
[Parameter(Mandatory = $false)] [int]$ProcessingBatchSize = 5 000,
[Parameter(Mandatory = $false)] [int]$DeviceLogSampleSize = 25,
[Parameter(Mandatory = $false)] [switch]$LargeScaleMode, #============================================================================ # AllowList/ BlockList-parametrit #============================================================================ # AllowList = Sisällytä vain nämä laitteet (kohdennettu käyttöönotto) # BlockList = Jätä nämä laitteet pois (niitä ei koskaan oteta käyttöön) # Käsittelyjärjestys: AllowList ensin (jos määritetty), sitten Lohkoluettelo [Parametri(Pakollinen = $false)] [merkkijono]$AllowListPath, [Parameter(Mandatory = $false)] [merkkijono]$AllowADGroup, [Parametri(Pakollinen = $false)] [merkkijono]$ExclusionListPath, [Parametri(Pakollinen = $false)] [merkkijono]$ExcludeADGroup, # ============================================================================ # WinCS (Windows configuration System) -parametrit #============================================================================ # WinCS on vaihtoehto AvailableUpdatesPolicy GPO -käyttöönotolle. # Se ottaa suojatun käynnistyksen käyttöön kunkin päätepisteen WinCsFlags.exe avulla.# WinCsFlags.exe suoritetaan päätepisteen SYSTEM-kontekstissa.# Viite: 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)] [merkkijono]$WinCSKey = "F33E0C8E002", [Parameter(Mandatory = $false)] [switch]$DryRun, [Parameter(Mandatory = $false)] [switch]$ListBlockedBuckets, [Parameter(Mandatory = $false)] [merkkijono]$UnblockBucket, [Parametri(Pakollinen = $false)] [switch]$UnblockAll, [Parameter(Mandatory = $false)] [switch]$EnableTaskOnDisabled )
$ErrorActionPreference = "Stop" $ScriptRoot = $PSScriptRoot $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = Käyttöönotto- ja valvontanäytteet
# ============================================================================ # DEPENDENCY VALIDATION # ============================================================================
function Test-ScriptDependencies { param( [Parametri(Pakollinen = $true)] [merkkijono]$ScriptDirectory, [Parametri(Pakollinen = $true)] [merkkijono[]]$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) -EtualallaColor Punainen Write-Host " PUUTTUVAT RIIPPUVUUDET" -EtualallaColor Punainen Write-Host ("=" * 70) -EtualallaColor Punainen Write-Host "" Write-Host "Seuraavia pakollisia komentosarjoja ei löytynyt:" -EtualallaVäri keltainen foreach ($script in $missingScripts) { Write-Host " - $script" -Edustaväri Valkoinen } Write-Host "" Write-Host "Lataa uusimmat komentosarjat kohteesta:" -ForegroundColor Cyan Write-Host "URL: $DownloadUrl" -Edustaväri Valkoinen Write-Host " Siirry kohteeseen: '$DownloadSubPage'" -Edustaväri Valkoinen Write-Host "" Write-Host "Pura kaikki komentosarjat samaan hakemistoon ja suorita uudelleen". -ForegroundColor Yellow Write-Host "" palauta $false } palauta $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)) { exit 1 }
# ============================================================================ # PARAMETRIN KELPOISUUDEN TARKISTAMINEN # ============================================================================
# Admin commands only need ReportBasePath $isAdminCommand = $ListBlockedBuckets - tai $UnblockBucket - tai $UnblockAll - tai $EnableTaskOnDisabled
if (-not $ReportBasePath) { Write-Host "VIRHE: -ReportBasePath on pakollinen". -Edustaväri Punainen exit 1 }
if (-not $isAdminCommand -and -not $AggregationInputPath) { Write-Host "ERROR: -AggregationInputPath is required for rollout (not needed for -ListBlockedBuckets, -UnblockBucket, -UnblockAll)" -ForegroundColor Red exit 1 }
# ============================================================================ # RYHMÄKÄYTÄNTÖOBJEKTIN TUNNISTUS - TUNNISTUSKÄYTÄNTÖOBJEKTIN TARKISTAMINEN # ============================================================================
if (-not $isAdminCommand -and -not $DryRun) { $CollectionGPOName = SecureBoot-EventCollection # Tarkista, onko GroupPolicy-moduuli käytettävissä if (Get-Module -ListAvailable -Name GroupPolicy) { Import-Module GroupPolicy -ErrorAction SilentlyContinue Write-Host "Tarkistetaan tunnistuskäytäntöobjektia..." -Edustaväri Keltainen kokeile { # Tarkista, onko ryhmäkäytäntöobjekti olemassa $existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue jos ($existingGpo) { Write-Host " Detection GPO found: $CollectionGPOName" -ForegroundColor Green } muu { Write-Host "" Write-Host ("=" * 70) -Edustaväri Keltainen Write-Host " VAROITUS: TUNNISTUSKÄYTÄNTÖOBJEKTIA EI LÖYDY" -EtualallaColor Keltainen Write-Host ("=" * 70) -EdustaVäri Keltainen Write-Host "" Write-Host "Tunnistuskäytäntöobjektin $CollectionGPOName" ei löytynyt." -EtualallaColor Yellow Write-Host "Ilman tätä ryhmäkäytäntöobjektia laitetietoja ei kerätä". -ForegroundColor Yellow Write-Host "" Write-Host "Ota tunnistuskäytäntöobjekti käyttöön suorittamalla:" -Edustavärisyan Write-Host ".\Deploy-GPO-SecureBootCollection.ps1 -DomainName <domain> -AutoDetectOU" -ForegroundColor White Write-Host "" Write-Host "Jatka silti? (Y/N)" -EtualallaColor Keltainen $response = Lukuisäntä jos ($response -notmatch '^[Yy]') { Write-Host "Keskeytys. Ota tunnistuksen ryhmäkäytäntöobjekti käyttöön ensin." -Edustaväri Punainen exit 1 } } } saalis { Write-Host " Ryhmäkäytäntöobjektia ei voi tarkistaa: $($_. Exception.Message)" -ForegroundColor Yellow } } muu { Write-Host " GroupPolicy-moduuli ei ole käytettävissä - ryhmäkäytäntöobjektin tarkistuksen ohittaminen" -EdustaVärin harmaa } Write-Host "" }
# ============================================================================ # OSAVALTIOTIEDOSTOPOLUT # ============================================================================
$stateDir = Join-Path $ReportBasePath "RolloutState" if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Path $stateDir -Force | Out-Null }
$rolloutStatePath = Join-Path $stateDir "RolloutState.json" $blockedBucketsPath = Join-Path $stateDir "BlockedBuckets.json" $adminApprovedPath = Join-Path $stateDir "AdminApprovedBuckets.json" $deviceHistoryPath = Join-Path $stateDir "DeviceHistory.json" $processingCheckpointPath = Join-Path $stateDir "ProcessingCheckpoint.json"
# ============================================================================ # PS 5.1 COMPATIBILITY: ConvertTo-Hashtable # ============================================================================ # ConvertFrom-Json -AsHashtable on vain PS7+. Tämä tarjoaa yhteensopivuuden.
function ConvertTo-Hashtable { param( [Parameter(ValueFromPipeline = $true)] $InputObject ) prosessi { jos ($null -eq $InputObject) { return @{} } jos ($InputObject -is [System.Collections.IDictionary]) { return $InputObject } jos ($InputObject -is [PSCustomObject]) { # Käytä [tilattu] yhdenmukaiseen avainten järjestykseen ja turvalliseen kaksoiskappaleiden käsittelyyn $hash = [ordered]@{} foreach ($prop kohteessa $InputObject.PSObject.Properties) { # Indeksoitu tehtävä käsittelee kaksoiskappaleet turvallisesti korvaamalla $hash[$prop. Nimi] = ConvertTo-Hashtable $prop. Arvo } palauta $hash } if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { return @($InputObject | ForEach-Object { ConvertTo-Hashtable $_ }) } palauta $InputObject } }
# ============================================================================ # JÄRJESTELMÄNVALVOJAN KOMENNOT: Säilöjen luettelo tai eston poistaminen # ============================================================================
if ($ListBlockedBuckets) { Write-Host "" Write-Host ("=" * 80) -Edustaväri Keltainen Write-Host " ESTETYT SÄILÖT" -Edustaväri Keltainen Write-Host ("=" * 80) -Edustaväri Keltainen Write-Host "" if (Test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable jos ($blocked. Määrä -eq 0) { Write-Host "Ei estettyjä säilöjä". -EdustaVäri vihreä } muu { Write-Host "Yhteensä estetty: $($blocked. Laske)" -EtualallaColor Punainen Write-Host "" ($key $blocked. Näppäimet) { $info = $blocked[$key] Write-Host "Bucket: $key" -ForegroundColor Red Write-Host " Estetty osoitteessa: $($info. BlockedAt)" -ForegroundColor Gray Write-Host " Syy: $($info. Syy)" -ForegroundColor Harmaa Write-Host " Epäonnistunut laite: $($info. FailedDevice)" -ForegroundColor Harmaa Write-Host " Viimeksi raportoitu: $($info. LastReported)" -ForegroundColor Gray Write-Host " Wave: $($info. WaveNumber)" -EtualallaColor Harmaa Write-Host " Laitteet säilössä: $($info. DevicesInBucket)" -ForegroundColor Gray Write-Host "" } Write-Host "Säilön eston poistaminen:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockBucket 'BUCKET_KEY'" -ForegroundColor Cyan Write-Host "" Write-Host "Kaikkien eston poistaminen:" Write-Host " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '$ReportBasePath' -UnblockAll" -ForegroundColor Cyan } } muu { Write-Host "Estettyjä säilöjä sisältävää tiedostoa ei löytynyt". -EdustaväriVäri vihreä } Write-Host "" exit 0 }
if ($UnblockBucket) { Write-Host "" if (Test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable jos ($blocked. Sisältää($UnblockBucket)) { $blocked. Poista($UnblockBucket) $blocked | ConvertTo-Json -Syvyys 10 | Out-File $blockedBucketsPath -Koodaus UTF8 -Force # Lisää järjestelmänvalvojan hyväksymään luetteloon uudelleenestoamisen estämiseksi $adminApproved = @{} if (Test-Path $adminApprovedPath) { $adminApproved = Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } $adminApproved[$UnblockBucket] = @{ ApprovedAt = Get-Date -Format "yy-MM-dd HH:mm:ss" ApprovedBy = $env:USERNAME } $adminApproved | ConvertTo-Json -Syvyys 10 | Out-File $adminApprovedPath -Koodaus UTF8 -Force Write-Host "Estoton säilö: $UnblockBucket" -EdustaväriVäri vihreä Write-Host "Lisätty järjestelmänvalvojan hyväksymään luetteloon (ei estetä automaattisesti uudelleen)" -EdustaVärisyan } muu { Write-Host "Bucket not found: $UnblockBucket" -ForegroundColor Yellow Write-Host Käytettävissä olevat säilöt:" -EdustaVäri harmaa $blocked. Näppäimet | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } } } muu { Write-Host "Estettyjä säilöjä sisältävää tiedostoa ei löytynyt". -Edustaväri Keltainen } Write-Host "" exit 0 }
if ($UnblockAll) { Write-Host "" if (Test-Path $blockedBucketsPath) { $blocked = Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable $count = $blocked. Laskea @{} | ConvertTo-Json | Out-File $blockedBucketsPath -Koodaus UTF8 -Force Write-Host "Poista kaikkien $count säilöjen esto". -EtualallaColor Green } muu { Write-Host "Estettyjä säilöjä sisältävää tiedostoa ei löytynyt". -Edustaväri Keltainen } Write-Host "" exit 0 }
# ============================================================================ # HELPER-FUNKTIOT # ============================================================================
function Get-RolloutState { if (Test-Path $rolloutStatePath) { kokeile { $loaded = Get-Content $rolloutStatePath -Raw | ConvertFrom-Json | ConvertTo-Hashtable # Vahvista, että pakolliset ominaisuudet ovat olemassa jos ($null -eq-$loaded. CurrentWave) { heitä "Virheellinen tilatiedosto – puuttuu CurrentWave" } # Varmista, että WaveHistory on aina matriisi (korjaa PS5.1 JSON-deserialisoinnin) jos ($null -eq-$loaded. WaveHistory) { $loaded. WaveHistory = @() } elseif ($loaded. WaveHistory -isnot [matriisi]) { $loaded. WaveHistory = @($loaded. WaveHistory) } palauta $loaded } saalis { Write-Log "Vioittunut RolloutState.json havaittu: $($_. Exception.Message)" "WARN" Write-Log "Varmuuskopioi vioittunut tiedosto ja aloita alusta" "VAROITA" $backupPath = "$rolloutStatePath.corrupted.$(Get-Date -Format 'yyyyMMdd-HHmmss')" Move-Item $rolloutStatePath $backupPath -Force -ErrorAction SilentlyContinue } } palauta @{ CurrentWave = 0 StartedAt = $null LastAggregation = $null TotalDevicesTargeted = 0 TotalDevicesUpdated = 0 Tila = "Ei käynnistynyt" WaveHistory = @() } }
function Save-RolloutState { param($State) $State | ConvertTo-Json -Syvyys 10 | Out-File $rolloutStatePath -Koodaus UTF8 -Force }
function Get-WeekdayProjection { <# . SYNOPSIS Laske viikonloppujen arvioitu valmistumispäivä (ei edistymistä la/su) #> param( [int]$RemainingDevices, [double]$DevicesPerDay, [datetime]$StartDate = (Get-Date) ) jos ($DevicesPerDay -le 0 -tai $RemainingDevices -le 0) { palauta @{ ProjectedDate = $null TyöpäivätVästetty = 0 CalendarDaysNeeded = 0 } } # Laske tarvittavat työpäivät (viikonloppuja lukuun ottamatta) $workingDaysNeeded = [matematiikka]::Katto($RemainingDevices / $DevicesPerDay) # Muunna työpäivät kalenteripäiviksi (lisää viikonloput) $currentDate = $StartDate.Date $daysAdded = 0 $workingDaysAdded = 0 ($workingDaysAdded -lt $workingDaysNeeded) { $currentDate = $currentDate.AddDays(1) $daysAdded++ # Laske vain arkipäivisin if ($currentDate.DayOfWeek -ne [DayOfWeek]::Saturday -and $currentDate.DayOfWeek -ne [DayOfWeek]::Sunday) { $workingDaysAdded++ } } palauta @{ ProjectedDate = $currentDate.ToString("yyyy-MM-dd") TyöpäivätVästetty = $workingDaysNeeded CalendarDaysNeeded = $daysAdded } }
function Save-RolloutSummary { <# . SYNOPSIS Koontiversion yhteenvedon tallentaminen raporttinäkymän näytön projektiotietojen avulla #> param( [hajautusarvo]$State, [int]$TotalDevices, [int]$UpdatedDevices, [int]$NotUpdatedDevices, [double]$DevicesPerDay ) $summaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" # Laske viikonlopputietoinen projektio $projection = Get-WeekdayProjection -RemainingDevices $NotUpdatedDevices -DevicesPerDay $DevicesPerDay $summary = @{ GeneratedAt = (Get-Date -Format "yy-MM-dd HH:mm:ss") RolloutStartDate = $State.StartedAt LastAggregation = $State.LastAggregation CurrentWave = $State.CurrentWave Tila = $State.Tila # Laitemäärät TotalDevices = $TotalDevices UpdatedDevices = $UpdatedDevices NotUpdatedDevices = $NotUpdatedDevices %Updated = if ($TotalDevices -gt 0) { [math]::Round(($UpdatedDevices / $TotalDevices) * 100, 1) } muu { 0 } # Nopeusmittarit DevicesPerDay = [math]::Round($DevicesPerDay, 1) TotalDevicesTargeted = $State.TotalDevicesTargeted TotalWaves = $State.CurrentWave # Viikonlopputietoinen projektio ProjectedCompletionDate = $projection. ProjectedDate WorkingDaysRemaining = $projection. Työpäivät on tehty CalendarDaysRemaining = $projection. CalendarDaysNeed # Huomautus viikonlopun poikkeuksesta ProjectionNote = "Arvioitu valmistumispäivä ei sisällä viikonloppuja (la/su)" } $summary | ConvertTo-Json -Syvyys 5 | Out-File $summaryPath -Koodaus UTF8 -Force Write-Log "Koontiversion yhteenveto tallennettu: $summaryPath" "TIEDOT" palauta $summary }
function Get-BlockedBuckets { if (Test-Path $blockedBucketsPath) { return Get-Content $blockedBucketsPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } palauta @{} }
function Save-BlockedBuckets { param($Blocked) $Blocked | ConvertTo-Json -Syvyys 10 | Out-File $blockedBucketsPath -Koodaus UTF8 -Force }
function Get-AdminApproved { if (Test-Path $adminApprovedPath) { return Get-Content $adminApprovedPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } palauta @{} }
function Get-DeviceHistory { if (Test-Path $deviceHistoryPath) { return Get-Content $deviceHistoryPath -Raw | ConvertFrom-Json | ConvertTo-Hashtable } palauta @{} }
function Save-DeviceHistory { param($History) $History | ConvertTo-Json -Syvyys 10 | Out-File $deviceHistoryPath -Koodaus UTF8 -Force }
function Save-ProcessingCheckpoint { param( [merkkijono]$Stage, [int]$Processed, [int]$Total, [hajautusarvo]$Metrics = @{} )
$checkpoint = @{ Stage = $Stage Päivitetty = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Käsitelty = $Processed Yhteensä = $Total Prosentti = jos ($Total -gt 0) { [math]::Round(($Processed / $Total) * 100, 2) } muu { 0 } Metrics = $Metrics }
$checkpoint | ConvertTo-Json -Depth 6 | Out-File $processingCheckpointPath -Encoding UTF8 -Force }
function Get-NotUpdatedIndexes { param([array]$Devices)
$hostSet = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $bucketCounts = @{}
foreach ($device in $Devices) { $hostname = jos ($device. Hostname) { $device. Hostname } elseif ($device. HostName) { $device. HostName } muu { $null } jos ($hostname) { [void]$hostSet.Add($hostname) }
$bucketKey = Get-BucketKey $device jos ($bucketKey) { if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey]++ } muu { $bucketCounts[$bucketKey] = 1 } } }
return @{ HostSet = $hostSet BucketCounts = $bucketCounts } }
function Write-Log { param([string]$Message, [string]$Level = "INFO") $timestamp = Get-Date -Format "yy-MM-dd HH:mm:ss" $color = valitsin ($Level) { "OK" { "Vihreä" } "WARN" { "Keltainen" } "VIRHE" { "Punainen" } "ESTETTY" { "DarkRed" } "WAVE" { "Cyan" } oletus { "Valkoinen" } } Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color # Kirjaudu myös tiedostoon $logFile = Join-Path $stateDir "Orchestrator_$(Get-Date -Format 'yyyyMMdd').log" "[$timestamp] [$Level] $Message" | Out-File $logFile -Append -Encoding UTF8 }
function Get-BucketKey { param($Device) # Käytä BucketId-tunnusta laitteesta JSON, jos käytettävissä (SHA256 hajautusarvo tunnistuskomentosarjasta) if ($Device.BucketId -and "$($Device.BucketId)" -ne "") { return "$($Device.BucketId)" } # Fallback: konstruktioita valmistajalta|malli|bios $mfr = jos ($Device.WMI_Manufacturer) { $Device.WMI_Manufacturer } muu { $Device.Manufacturer } $model = jos ($Device.WMI_Model) { $Device.WMI_Model } muu { $Device.Model } $bios = jos ($Device.BIOSDescription) { $Device.BIOSDescription } muu { $Device.BIOS } palauta "$mfr|$model|$bios" }
# ============================================================================ # VIP/EXCLUSION LIST LOADING # ============================================================================
function Get-ExcludedHostnames { param( [merkkijono]$ExclusionFilePath, [merkkijono]$ADGroupName ) $excluded = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Lataa tiedostosta (tukee .txt tai .csv) if ($ExclusionFilePath -and (Test-Path $ExclusionFilePath)) { $extension = [System.IO.Path]::GetExtension($ExclusionFilePath). ToLower() jos ($extension -eq ".csv") { # CSV-muoto: odottaa Hostname- tai ComputerName-saraketta $csvData = Import-Csv $ExclusionFilePath $hostCol = jos ($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' } muu { $null } if ($hostCol) { foreach ($row in $csvData) { jos (![ merkkijono]:IsNullOrWhiteSpace($row.$hostCol)) { $excluded. Add($row.$hostCol.Trim()) } } } } muu { # Vain teksti: yksi isäntänimi riviä kohti Get-Content $ExclusionFilePath | ForEach-Object { $line = $_. Rajaa() jos ($line -ja -not $line. StartsWith('#')) { $excluded. Add($line) } } } Write-Log "Ladattu $($excluded. Count) hostnames from exclusion file: $ExclusionFilePath" "INFO" } # Lataa AD-käyttöoikeusryhmästä jos ($ADGroupName) { kokeile { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'computer' } foreach ($member in $groupMembers) { $excluded. Lisää($member. Nimi) } Write-Log "Ladatut $($groupMembers.Count) -tietokoneet AD-ryhmästä: $ADGroupName" "TIEDOT" } saalis { Write-Log "AD-ryhmää ei voitu ladata $ADGroupName": $_" "WARN" } } palauta @($excluded) }
# ============================================================================ # SALLI LUETTELON LATAAMINEN (kohdennettu käyttöönotto) # ============================================================================
function Get-AllowedHostnames { <# . SYNOPSIS Lataa isäntänimet AllowList-tiedostosta ja/tai AD-ryhmästä kohdennettua käyttöönottoa varten.Kun Salliluettelo on määritetty, vain nämä laitteet sisällytetään käyttöönottoon.#> param( [merkkijono]$AllowFilePath, [merkkijono]$ADGroupName ) $allowed = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) # Lataa tiedostosta (tukee .txt tai .csv) if ($AllowFilePath -and (Test-Path $AllowFilePath)) { $extension = [System.IO.Path]::GetExtension($AllowFilePath). ToLower() jos ($extension -eq ".csv") { # CSV-muoto: odottaa Hostname- tai ComputerName-saraketta $csvData = Import-Csv $AllowFilePath if ($csvData.Count -gt 0) { $hostCol = jos ($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' } muu { $null } if ($hostCol) { ($row $csvData) { jos (![ string]::IsNullOrWhiteSpace($row.$hostCol)) { $allowed. Add($row.$hostCol.Trim()) } } } } } muu { # Vain teksti: yksi isäntänimi riviä kohti Get-Content $AllowFilePath | ForEach-Object { $line = $_. Rajaa() jos ($line -ja -not $line. StartsWith('#')) { $allowed. Add($line) } } } Write-Log "Ladattu $($allowed. Count) hostnames from allow list file: $AllowFilePath" "INFO" } # Lataa AD-käyttöoikeusryhmästä jos ($ADGroupName) { kokeile { $groupMembers = Get-ADGroupMember -Identity $ADGroupName -Recursive -ErrorAction Stop | Where-Object { $_.objectClass -eq 'tietokone' } foreach ($member in $groupMembers) { $allowed. Lisää($member. Nimi) } Write-Log "Ladatut $($groupMembers.Count) -tietokoneet AD-sallitusta ryhmästä: $ADGroupName" "TIEDOT" } saalis { Write-Log "AD-ryhmää ei voitu ladata $ADGroupName": $_" "WARN" } } palauta @($allowed) }
# ============================================================================ # TIETOJEN TUOREUS JA SEURANTA # ============================================================================
function Get-DataFreshness { <# . SYNOPSIS Tarkistaa tunnistustietojen tuoreuuden tutkimalla JSON-tiedoston aikaleimat.Palauttaa tilastotietoja siitä, milloin päätepisteet on viimeksi raportoitu.#> param([string]$JsonPath) $jsonFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue jos ($jsonFiles.Count -eq 0) { palauta @{ TotalFiles = 0 FreshFiles = 0 StaleFiles = 0 NoDataFiles = 0 OldestFile = $null NewestFile = $null AvgAgeHours = 0 Varoitus = "JSON-tiedostoja ei löytynyt - tunnistusta ei ehkä ole otettu käyttöön" } } $now = Get-Date $freshThresholdHours = 24 # Files päivitetty viimeisen 24 tunnin aikana ovat "tuoreita" $staleThresholdHours = 72 # Files vanhemmat kuin 72 tuntia ovat vanhentuneet $fresh = 0 $stale = 0 $ages = @() foreach ($file in $jsonFiles) { $ageHours = ($now – $file. LastWriteTime). TotalHours $ages += $ageHours jos ($ageHours -le $freshThresholdHours) { $fresh++ } elseif ($ageHours -ge $staleThresholdHours) { $stale++ } } $oldestFile = $jsonFiles | Sort-Object LastWriteTime | Select-Object -Ensimmäinen 1 $newestFile = $jsonFiles | Sort-Object LastWriteTime -Laskeva | Select-Object -Ensimmäinen 1 $warning = $null jos ($stale -gt ($jsonFiles.Määrä * 0,5)) { $warning = "Yli 50 prosentissa laitteista on käyttämänsä tiedot (>72 tuntia) – tarkistuksen tunnistuskäytäntöobjekti" } elseif ($fresh -lt ($jsonFiles.Count * 0,3)) { $warning = "Alle 30% äskettäin ilmoitetuista laitteista - havaitseminen ei ehkä ole käynnissä" } palauta @{ TotalFiles = $jsonFiles.Count FreshFiles = $fresh StaleFiles = $stale MediumFiles = $jsonFiles.Count - $fresh - $stale OldestFile = $oldestFile.LastWriteTime NewestFile = $newestFile.LastWriteTime AvgAgeHours = [math]::Round(($ages | Measure-Object -Average). Keskiarvo, 1) Varoitus = $warning } }
function Test-DetectionGPODeployed { <# . SYNOPSIS Tarkistaa, että havaitsemis- ja valvontainfrastruktuuri on käytössä.#> param([string]$JsonPath) # Tarkista 1: JSON-polku on olemassa if (-not (Test-Path $JsonPath)) { palauta @{ IsDeployed = $false Message = "JSON-syötepolkua ei ole: $JsonPath" } } # Check 2: Ainakin osa JSON-tiedostoista on olemassa $jsonCount = (Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue). Laskea jos ($jsonCount -eq 0) { palauta @{ IsDeployed = $false Message = "No JSON files in $JsonPath - Detection GPO may not be deployed or devices not not reported" } } # Tarkista 3: Files ovat kohtuullisen viimeaikaisia (ainakin osa viime viikolla) $recentFiles = Get-ChildItem -Path $JsonPath -Filter "*.json" -ErrorAction SilentlyContinue | Where-Object { $_. LastWriteTime -gt (Get-Date). AddDays(-7) } jos ($recentFiles.Count -eq 0) { palauta @{ IsDeployed = $false Message = "No JSON files updated in last 7 days - Detection GPO may be broken or devices offline" } } palauta @{ IsDeployed = $true Message = "Detection appears active: $jsonCount files, $($recentFiles.Count) updated recently" FileCount = $jsonCount RecentCount = $recentFiles.Count } }
# ============================================================================ # LAITTEEN SEURANTA (ISÄNTÄNIMEN MUKAAN) # ============================================================================
function Update-DeviceHistory { <# . SYNOPSIS Seuraa laitteita isäntänimen mukaan, koska meillä ei ole yksilöivää konetunnusta.Huomautus: BucketId on yksi-moni (sama laitteiston määritys = sama säilö).Jos JSON-kokoelmaan lisätään yksilöllinen tunniste, päivitä tämä funktio.#> param( [matriisi]$CurrentDevices, [hajautusarvo]$DeviceHistory ) foreach ($device in $CurrentDevices) { $hostname = $device. Hostname jos (ei $hostname) { continue } # Seuraa laitetta isäntänimen mukaan $DeviceHistory[$hostname] = @{ Hostname = $hostname BucketId = $device. BucketId Manufacturer = $device. WMI_Manufacturer Malli = $device. WMI_Model LastSeen = Get-Date -Format "yy-MM-dd HH:mm:ss" Tila = $device. UpdateStatus } } }
# ============================================================================ # ESTETTY SÄILÖN TUNNISTUS (Perustuu laitteen saavutettavuuteen) # ============================================================================
<# . KUVAUS Logiikan estäminen: - Säilö estetään vain, jos: 1. Laite oli kohteena aallossa 2. MaxWaitHours on ohittanut aallon alkamisen jälkeen 3. Laite EI OLE SAAVUTETTAVISSA (ping epäonnistuu) - Jos laite on tavoitettavissa, mutta sitä ei ole vielä päivitetty, odotamme (päivitys saattaa odottaa uudelleenkäynnistystä - Tapahtuma 1808 käynnistyy vasta uudelleenkäynnistyksen jälkeen) - Tavoittamaton laite osoittaa, että jokin meni vikaan ja se on tutkittava Unblocking: - Käytä -ListBlockedBuckets nähdäksesi estetyt säilöt - Käytä -UnblockBucket "BucketKey" tietyn säilön eston purkamiseen - Use -UnblockAll to unblockAll to unblock all buckets #>
function Test-DeviceReachable { param( [merkkijono]$Hostname, [string]$DataPath # Polku laitteen JSON-tiedostoihin ) # Menetelmä 1: Tarkista JSON-tiedoston aikaleima (nopein – tiedoston jäsennystä ei tarvita) # Jos tunnistuskomentosarja suoritettiin äskettäin, tiedosto on kirjoitettu/päivitetty, mikä todistaa laitteen olevan elossa jos ($DataPath) { $deviceFile = Get-ChildItem -Path $DataPath -Filter "${Hostname}*" -File -ErrorAction SilentlyContinue | Select-Object -Ensimmäinen 1 jos ($deviceFile) { $hoursSinceWrite = ((Get-Date) – $deviceFile.LastWriteTime). TotalHours jos ($hoursSinceWrite -lt 72) { return $true } } } # Menetelmä 2: Varauma pingiin (vain, jos JSON on stale tai puuttuu) kokeile { $ping = Test-Connection -ComputerName $Hostname -Count 1 -Quiet -ErrorAction SilentlyContinue palauta $ping } saalis { palauta $false } }
function Update-BlockedBuckets { param( $RolloutState $BlockedBuckets $AdminApproved [matriisi]$NotUpdatedDevices, [hajautusarvo]$NotUpdatedIndexes, [int]$MaxWaitHours, [bool]$DryRun = $false ) $now = Get-Date $newlyBlocked = @() $stillWaiting = @() $devicesToCheck = @() $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } muu { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } $bucketCounts = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.BucketCounts) { $NotUpdatedIndexes.BucketCounts } muu { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). BucketCounts } # Kerää laitteita, joiden odotusaika on ohitettu ja joita ei ole vielä päivitetty foreach ($wave kohteessa $RolloutState.WaveHistory) { jos (ei $wave. StartedAt) { continue } $waveStart = [DateTime]::P arse($wave. StartedAt) $hoursSinceWave = ($now – $waveStart). TotalHours jos ($hoursSinceWave -lt $MaxWaitHours) { # Vielä odotusajan sisällä - älä tarkista vielä Jatkaa } # Tarkista kaikki laitteet tältä aallolta ($deviceInfo $wave. Laitteet) { $hostname = $deviceInfo.Hostname $bucketKey = $deviceInfo.BucketKey # Ohita, jos säilö on jo estetty jos ($BlockedBuckets.Contains($bucketKey)) { continue } # Ohita, jos säilö on järjestelmänvalvojan hyväksymä JA aalto aloitettu ENNEN hyväksyntää # (tarkista vain laitteet, jotka on kohdistettu järjestelmänvalvojan hyväksynnän jälkeen uudelleenestoa varten) if ($AdminApproved -and $AdminApproved.Contains($bucketKey)) { $approvalTime = [DateTime]::P arse($AdminApproved[$bucketKey]. ApprovedAt) jos ($waveStart -lt $approvalTime) { # Tämä laite oli kohdennettu ennen järjestelmänvalvojan hyväksyntää - ohita Jatkaa } # Aalto alkoi hyväksynnän jälkeen - tämä on uusi kohdentaminen, voi tarkistaa } # Onko tämä laite edelleen Ei vanhentunut-luettelossa? if ($hostSet.Contains($hostname)) { $devicesToCheck += @{ Hostname = $hostname BucketKey = $bucketKey WaveNumber = $wave. WaveNumber HoursSinceWave = [math]::Round($hoursSinceWave, 1) } } } } if ($devicesToCheck.Count -eq 0) { palauta $newlyBlocked } Write-Log "$($devicesToCheck.Count) -laitteiden tavoittavuuden tarkistaminen odotusajan jälkeen..." "INFO" # Seuraa virheitä säilöä kohti päätöksentekoa varten $bucketFailures = @{} # BucketKey -> @{ Unreachable=@(); Alive=@() } # Tarkista kunkin laitteen saavutettavuus foreach ($device $devicesToCheck) { $hostname = $device. Hostname $bucketKey = $device. BucketKey jos ($DryRun) { Write-Log "[DRYRUN] Tarkistaa $hostname saavutettavuuden" "TIEDOT" Jatkaa } if (-not $bucketFailures.ContainsKey($bucketKey)) { $bucketFailures[$bucketKey] = @{ Unreachable = @(); AliveButFailed = @(); WaveNumber = $device. WaveNumber; HoursSinceWave = $device. HoursSinceWave } } $isReachable = Test-DeviceReachable -Hostname $hostname -DataPath $AggregationInputPath jos (ei $isReachable) { $bucketFailures[$bucketKey]. Tavoittamaton += $hostname } muu { # Laite on tavoitettavissa, mutta sitä ei ole vielä päivitetty – se voi olla tilapäinen virhe tai uudelleenkäynnistyksen odottaminen $bucketFailures[$bucketKey]. AliveButFailed += $hostname $stillWaiting += $hostname } } # Päätös säilöä kohti: estä vain, jos laitteet ovat todella saavuttamattomia # Elossa laitteet, joissa on virheitä = tilapäinen, jatka käyttöönottoa foreach ($bucketKey in $bucketFailures.Keys) { $bf = $bucketFailures[$bucketKey] $unreachableCount = $bf. Ei käytettävissä.Määrä $aliveFailedCount = $bf. AliveButFailed.Count # Tarkista, onko säilössä onnistunut (päivitetyistä laitetiedoista) $bucketHasSuccesses = $stSuccessBuckets -and $stSuccessBuckets.Contains($bucketKey) jos ($unreachableCount -gt 0 -ja $aliveFailedCount -eq 0) { # Kaikki epäonnistuneet laitteet eivät ole käytettävissä - estä säilö jos ($newlyBlocked -notcontains $bucketKey) { $BlockedBuckets[$bucketKey] = @{ BlockedAt = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Syy = "Kaikki $unreachableCount laitteet, jotka eivät ole käytettävissä $($bf) jälkeen. HoursSinceWave) hours" FailedDevices = ($bf. Tavoittamaton -join ", ") WaveNumber = $bf. WaveNumber DevicesInBucket = if ($bucketCounts.ContainsKey($bucketKey)) { $bucketCounts[$bucketKey] } muu { 0 } } $newlyBlocked += $bucketKey Write-Log "SÄILÖ ESTETTY: $bucketKey ($unreachableCount laitteita, jotka eivät ole käytettävissä: $($bf). Ei käytettävissä -liitos ', '))' "ESTETTY" } } elseif ($aliveFailedCount -gt 0) { # Laitteet ovat elossa, mutta niitä ei ole päivitetty - tilapäinen virhe, DO NOT block Write-Log "Bucket $($bucketKey.Substring(0, [Math]::Min(16, $bucketKey.Length)))...: $aliveFailedCount laitteet elossa, mutta odottavat, $unreachableCount tavoittamattomissa - EI estä (väliaikainen)" "INFO" jos ($unreachableCount -gt 0) { Write-Log " Tavoittamaton: $($bf. Tavoittamaton -liity ', ')' "VAROITA" } Write-Log " Elossa, mutta odottaa: $($bf. AliveButFailed -join ', ')' "INFO" # Seuraa virheiden määrää käyttöönoton tilassa seurantaa varten jos (ei $RolloutState.TemporaryFailures) { $RolloutState.TemporaryFailures = @{} } $RolloutState.TemporaryFailures[$bucketKey] = @{ AliveButFailed = $bf. AliveButFailed Ei käytettävissä = $bf. Saavuttamaton LastChecked = Get-Date -Format "yy-MM-dd HH:mm:ss" } } } jos ($stillWaiting.Määrä -gt 0) { Write-Log "Laitteet tavoitettavissa, mutta odottava päivitys (saattaa edellyttää uudelleenkäynnistystä): $($stillWaiting.Count)" "INFO" } palauta $newlyBlocked }
# ============================================================================ # AUTO-UNBLOCK: Poista säilöjen esto, kun laitteet päivittyvät onnistuneesti # ============================================================================
function Update-AutoUnblockedBuckets { <# . KUVAUS Tarkistaa, onko estettyjen säilöjen laitteet päivitetty (tapahtuma 1808). Poista esto automaattisesti, jos kaikki säilön kohdennetut laitteet ovat päivittyneet.Jos vain jotkin laitteet on päivitetty, järjestelmänvalvoja ilmoittaa, kuka voi poistaa eston manuaalisesti. Hallinta voi poistaa eston manuaalisesti käyttämällä: .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath "polku" -UnblockBucket "BucketKey" #> param( $BlockedBuckets $RolloutState [matriisi]$NotUpdatedDevices, [merkkijono]$ReportBasePath, [hajautusarvo]$NotUpdatedIndexes, [int]$LogSampleSize = 25 ) $autoUnblocked = @() $bucketsToCheck = @($BlockedBuckets.Keys) $hostSet = if ($NotUpdatedIndexes -and $NotUpdatedIndexes.HostSet) { $NotUpdatedIndexes.HostSet } muu { (Get-NotUpdatedIndexes -Devices $NotUpdatedDevices). HostSet } foreach ($bucketKey in $bucketsToCheck) { $bucketInfo = $BlockedBuckets[$bucketKey] # Hanki kaikki laitteet, jotka olemme kohdistaneet tästä säilöstä historiallisesti $targetedDevicesInBucket = @() foreach ($wave kohteessa $RolloutState.WaveHistory) { $targetedDevicesInBucket += @($wave. Laitteet | Where-Object { $_. BucketKey -eq $bucketKey }) } jos ($targetedDevicesInBucket.Count -eq 0) { continue } # Tarkista, kuinka monta kohdennettua laitetta ei ole vielä päivitetty $updatedDevices = @() $stillPendingDevices = @() foreach ($targetedDevice in $targetedDevicesInBucket) { if ($hostSet.Contains($targetedDevice.Hostname)) { $stillPendingDevices += $targetedDevice.Hostname } muu { $updatedDevices += $targetedDevice.Hostname } } if ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -eq 0) { # KAIKKI kohdennetut laitteet on päivitetty – poista esto automaattisesti! $BlockedBuckets.Remove($bucketKey) $autoUnblocked += @{ BucketKey = $bucketKey UpdatedDevices = $updatedDevices PreviouslyBlockedAt = $bucketInfo.BlockedAt Syy = "Kaikki $($updatedDevices.Count) kohdennetut laitteet on päivitetty onnistuneesti" } Write-Log "AUTO-UNBLOCKED: $bucketKey (Kaikki $($updatedDevices.Count) kohdennetut laitteet päivitetty onnistuneesti)" "OK" # Lisää OEM-aaltojen määrää tämän säilön alkuperäiselle laitevalmistajalle (OEM-seurantaa kohden) $bucketOEM = jos ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } muu { 'Tuntematon' } # Pura alkuperäinen laitevalmistaja putkierotinnäppäimestä tai oletusavaimesta jos (ei $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } $currentWave = jos ($RolloutState.OEMWaveCounts[$bucketOEM]) { $RolloutState.OEMWaveCounts[$bucketOEM] } muu { 0 } $RolloutState.OEMWaveCounts[$bucketOEM] = $currentWave + 1 Write-Log " Alkuperäinen laitevalmistaja '$bucketOEM' aaltojen määrä on kasvatettu $($currentWave + 1) (seuraava varaus: $([int][Math]::P ow(2, $currentWave + 1)) devices)" "INFO" } elseif ($updatedDevices.Count -gt 0 -and $stillPendingDevices.Count -gt 0) { # Jotkin laitteet päivitetty, mutta toiset odottavat edelleen – ilmoita järjestelmänvalvojalle (vain kerran) jos (ei $bucketInfo.UnblockCandidate) { $bucketInfo.UnblockCandidate = $true $bucketInfo.UpdatedDevices = $updatedDevices $bucketInfo.PendingDevices = $stillPendingDevices $bucketInfo.NotifiedAt = (get-date). ToString("yy-MM-dd HH:mm:ss") Write-Log "" "TIEDOT" Write-Log "========== OSITTAINEN PÄIVITYS ESTETTYJEN SÄILÖJEN ==========" "TIEDOT" Write-Log "Bucket: $bucketKey" "INFO" $updatedSample = @($updatedDevices | Select-Object -First $LogSampleSize) $pendingSample = @($stillPendingDevices | Select-Object -First $LogSampleSize) $updatedSuffix = jos ($updatedDevices.Count -gt $LogSampleSize) { " ... (+$($updatedDevices.Count - $LogSampleSize) lisää)" } muu { "" } $pendingSuffix = jos ($stillPendingDevices.Count -gt $LogSampleSize) { " ... (+$($stillPendingDevices.Count - $LogSampleSize) lisää)" } muu { "" } Write-Log "Päivitetyt laitteet ($($updatedDevices.Count)): $($updatedSample -join ', ')$updatedSuffix" "OK" Write-Log "Still pending ($($stillPendingDevices.Count)): $($pendingSample -join ', ')$pendingSuffix" "WARN" Write-Log "" "TIEDOT" Write-Log "Jos haluat poistaa säilön eston manuaalisesti tarkistamisen jälkeen, suorita:" "TIEDOT" Write-Log " .\Start-SecureBootRolloutOrchestrator.ps1 -ReportBasePath '"$ReportBasePath'" -UnblockBucket '"$bucketKey'"" "INFO" Write-Log "=======================================================" "TIEDOT" Write-Log "" "TIEDOT" } } } palauta $autoUnblocked }
# ============================================================================ # WAVE GENERATION (INLINED - sulkee pois estetyt säilöt) # ============================================================================
function New-RolloutWave { param( [merkkijono]$AggregationPath, $BlockedBuckets $RolloutState [int]$MaxDevicesPerWave = 50, [merkkijono[]]$AllowedHostnames = @(), [merkkijono[]]$ExcludedHostnames = @() ) # Lataa koostetiedot $notUptodateCsv = Get-ChildItem -Path $AggregationPath -Filter "*NotUptodate*.csv" | Where-Object { $_. Nimi -notlike "*Buckets*" } | Sort-Object LastWriteTime -Laskeva | Select-Object -First 1 jos (ei $notUptodateCsv) { Write-Log "Ei päivitystä CSV löytyi" "VIRHE" palauta $null } $allNotUpdated = @(Import-Csv $notUptodateCsv.FullName) # Normalize HostName -> Hostname for consistency (CSV uses HostName, code uses Hostname) foreach ($device in $allNotUpdated) { jos ($device. PSObject.Properties['HostName'] -and -not $device. PSObject.Properties['Hostname']) { $device | Add-Member -NotePropertyName 'Hostname' -NotePropertyValue $device. HostName -Force } } # Suodata estetyt säilöt pois $eligibleDevices = @($allNotUpdated | Where-Object { $bucketKey = Get-BucketKey $_ -not $BlockedBuckets.Contains($bucketKey) }) # Suodata vain sallittuihin laitteisiin (jos Sallittu luettelo on määritetty) # AllowList = kohdennettu käyttöönotto – vain näitä laitteita harkitaan jos ($AllowedHostnames.Määrä -gt 0) { $beforeCount = $eligibleDevices.Määrä $eligibleDevices = @($eligibleDevices | Where-Object { $_. Hostname -in-$AllowedHostnames }) $allowedCount = $eligibleDevices.Määrä Write-Log "AllowList applied: $allowedCount of $beforeCount devices are in allow list" "INFO" } # Suodata VIP/pois jätetyt laitteet pois (BlockList) # BlockList otetaan käyttöön Sallittujen luettelon jälkeen jos ($ExcludedHostnames.Määrä -gt 0) { $beforeCount = $eligibleDevices.Määrä $eligibleDevices = @($eligibleDevices | Where-Object { $_. Hostname -notin $ExcludedHostnames }) $excludedCount = $beforeCount – $eligibleDevices.Määrä jos ($excludedCount -gt 0) { Write-Log "Ei käytössä $excludedCount VIP/protected devices from rollout" "INFO" } } if ($eligibleDevices.Count -eq 0) { Write-Log "Ei voimassa olevia laitteita jäljellä (kaikki päivitetty tai estetty)" "OK" palauta $null } # Hanki laitteet valmiiksi käyttöön (aiemmista aalloista) $devicesAlreadyInRollout = @() jos ($RolloutState.WaveHistory -ja $RolloutState.WaveHistory.Count -gt 0) { $devicesAlreadyInRollout = @($RolloutState.WaveHistory | ForEach-Object { $_. Laitteet | ForEach-Object { $_. Hostname } } | Where-Object { $_ }) } Write-Log "Laitteet, jotka ovat jo käytössä: $($devicesAlreadyInRollout.Count)" "INFO" # Erotellaan luottamustasolla $highConfidenceDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel -eq "High Confidence" -ja $_. Hostname -notin $devicesAlreadyInRollout }) # Toiminto Pakollinen sisältää seuraavat: # - Eksplisiittinen "Toimenpide vaaditaan" # - Tyhjä/tyhjä Luottamusväli # - Mikä tahansa tuntematon/tunnistamaton Luottamusväli-arvo (käsitellään toiminnona pakollinen) $knownSafeCategories = @( "Suuri luottamus", "Tilapäisesti keskeytetty", "Tarkkailun alla", "Tarkkailun alla – tarvitaan lisää tietoja", "Ei tuettu", "Ei tuettu – tunnettu rajoitus" ) $actionRequiredDevices = @($eligibleDevices | Where-Object { $_. ConfidenceLevel - notin $knownSafeCategories -and $_. Hostname -notin $devicesAlreadyInRollout }) Write-Log "Suuri luottamus (ei käyttöönotossa): $($highConfidenceDevices.Count)" "INFO" Write-Log "Toiminto pakollinen (ei käytössä): $($actionRequiredDevices.Count)" "INFO" # Muodosta aaltolaitteita $waveDevices = @() # HIGH CONFIDENCE: Include ALL (turvallinen käyttöönottoa varten) jos ($highConfidenceDevices.Määrä -gt 0) { Write-Log "Kaikkien $($highConfidenceDevices.Count) High Confidence -laitteiden lisääminen" "WAVE" $waveDevices += $highConfidenceDevices } # TOIMINTO PAKOLLINEN: Asteittainen käyttöönotto (säilöpohjainen, jossa on OEM-levitys nollamenestyssäilöille) # Strategia: # - Säilöt, joissa on 0 menestystä: Levittäytyvät alkuperäisiin laitevalmistajiin (1 /OEM -> 2 /OEM -> 4 /OEM) # - Buckets with ≥1 success: Double freely without OEM restriction jos ($actionRequiredDevices.Määrä -gt 0) { # Lataa säilön onnistumisen määrä päivitetyistä laitteista CSV (laitteet, joiden päivitys onnistui) $updatedCsv = Get-ChildItem -Path $AggregationPath -Filter "*updated_devices*.csv" | Sort-Object LastWriteTime -Laskeva | Select-Object -Ensimmäinen 1 $bucketStats = @{} jos ($updatedCsv) { $updatedDevices = Import-Csv $updatedCsv.FullName # Laske onnistumiset BucketId-tunnuksen mukaan $updatedDevices | ForEach-Object { $key = Get-BucketKey $_ jos ($key) { jos (ei $bucketStats.ContainsKey($key)) { $bucketStats[$key] = @{ Successes = 0; Odottaa = 0; Yhteensä = 0 } } $bucketStats[$key]. Successes++ $bucketStats[$key]. Yhteensä++ } } Write-Log "Ladatut $($updatedDevices.Count) päivitetyt laitteet $($bucketStats.Count) säilöihin" "TIEDOT" } muu { # Fallback: kokeile ActionRequired_Buckets CSV $bucketsCsv = Get-ChildItem -Path $AggregationPath -Filter "*ActionRequired_Buckets*.csv" | Sort-Object LastWriteTime -Laskeva | Select-Object -Ensimmäinen 1 jos ($bucketsCsv) { Import-Csv $bucketsCsv.FullName | ForEach-Object { $key = jos ($_. BucketId) { $_. BucketId } muu { "$($_. Valmistaja)|$($_. Malli)|$($_. BIOS)" } $bucketStats[$key] = @{ Successes = [int]$_. Onnistumisia Odottaa = [int]$_. Odottavat Yhteensä = [int]$_. TotalDevices } } } } # Group NotUpdated devices by bucket (Manufacturer|Malli|BIOS) $buckets = $actionRequiredDevices | Group-Object { Get-BucketKey $_ } # Erilliset säilöt: nollamenestys vs on-success $zeroSuccessBuckets = @() $hasSuccessBuckets = @() foreach ($bucket in $buckets) { $bucketKey = $bucket. Nimi $bucketDevices = @($bucket. Ryhmä) $bucketHostnames = @($bucketDevices | ForEach-Object { $_. Hostname }) # Laske onnistumiset tässä säilössä $stats = $bucketStats[$bucketKey] $successes = jos ($stats) { $stats. Successes } muu { 0 } # Etsi tähän säilöön käyttöön otetut laitteet aaltohistoriasta $deployedToBucket = @() foreach ($wave kohteessa $RolloutState.WaveHistory) { ($device $wave. Laitteet) { jos ($device. BucketKey -eq $bucketKey -and $device. Hostname) { $deployedToBucket += $device. Hostname } } } $deployedToBucket = @($deployedToBucket | Sort-Object -Unique) # Tarkista, ovatko KAIKKI käyttöön otetut laitteet ilmoittaneet onnistumisesta $stillPending = @($deployedToBucket | Where-Object { $_ -in $bucketHostnames }) $confirmedSuccess = $deployedToBucket.Count – $stillPending.Count # Jos odottaa, ohita tämä säilö, kunnes kaikki on vahvistettu 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) (odottaa)" "TIEDOT" Jatkaa } # Jäljellä olevat vaatimukset = laitteet, joita ei ole vielä otettu käyttöön $devicesNotYetTargeted = @($bucketDevices | Where-Object { $_. Hostname -notin $deployedToBucket }) jos ($devicesNotYetTargeted.Count -eq 0) { continue } # Luokittele onnistumisten määrän mukaan $bucketInfo = @{ BucketKey = $bucketKey Devices = $devicesNotYetTargeted ConfirmedSuccess = $confirmedSuccess Successes = $successes OEM = if ($bucket. Ryhmä[0]. WMI_Manufacturer) { $bucket. Ryhmä[0]. WMI_Manufacturer } elseif ($bucketKey -match '\|') { ($bucketKey -split '\|')[0] } muu { 'Tuntematon' } } jos ($successes -eq 0) { $zeroSuccessBuckets += $bucketInfo } muu { $hasSuccessBuckets += $bucketInfo } } # === PROCESS HAS-SUCCESS BUCKETS (≥1 success) === # Kaksinkertainen onnistumisten määrä – jos 14 onnistui, ota käyttöön 28 seuraava foreach ($bucketInfo in $hasSuccessBuckets) { $nextBatchSize = $bucketInfo.Successes * 2 $nextBatchSize = [Matematiikka]::Min($nextBatchSize, $MaxDevicesPerWave) $nextBatchSize = [Matematiikka]::Min($nextBatchSize, $bucketInfo.Devices.Count) jos ($nextBatchSize -gt 0) { $selectedDevices = @($bucketInfo.Devices | Select-Object -First $nextBatchSize) $waveDevices += $selectedDevices $parts = jos ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length)) } $displayName = "$($parts[0]) - $($parts[1])" Write-Log " [HAS-SUCCESS] $displayName - Successes=$($bucketInfo.Successes), Deploying=$nextBatchSize (2x vahvistettu)" "INFO" } } # === PROSESSI NOLLA ONNISTUI -SÄILÖT (jaettuna alkuperäisiin laitevalmistajiin OEM-per-OEM-seurannalla) === # Tavoite: Riskien levittäminen eri alkuperäisille laitevalmistajille, seuraa edistymistä OEM-laitevalmistajaa kohti itsenäisesti # Jokainen alkuperäinen laitevalmistaja etenee oman menestyshistoriansa perusteella: # - Alkuperäinen laitevalmistaja menestyksin: Saa lisää laitteita seuraavan aallon (2^waveCount) # - OEM ilman onnistumisia: Pysyy nykyisellä tasolla, kunnes menestys on vahvistettu jos ($zeroSuccessBuckets.Määrä -gt 0) { # Alkuperäisten laitevalmistajien aaltojen alustusmäärät, jos ei ole olemassa jos (ei $RolloutState.OEMWaveCounts) { $RolloutState.OEMWaveCounts = @{} } # Ryhmittele tyhjät säilöt alkuperäisen laitevalmistajan mukaan $oemBuckets = $zeroSuccessBuckets | Group-Object { $_. OEM } $totalZeroSuccessAdded = 0 $oemsDeployedTo = @() foreach ($oemGroup in $oemBuckets) { $oemName = $oemGroup.Name # Hanki tämän alkuperäisen laitevalmistajan aaltomäärä (alkaa 0:sta) $oemWaveCount = if ($RolloutState.OEMWaveCounts[$oemName]) { $RolloutState.OEMWaveCounts[$oemName] } muu { 0 } # Laske laitteet tälle alkuperäiselle laitevalmistajalle: 2^waveCount (1, 2, 4, 8...) $devicesForThisOEM = [int][Math]::P ow(2, $oemWaveCount) $devicesForThisOEM = [Matematiikka]::Maks.(1, $devicesForThisOEM) $oemDevicesAdded = 0 # Valitse kustakin säilöstä tämän alkuperäisen laitevalmistajan alta foreach ($bucketInfo $oemGroup.Groupissa) { $remaining = $devicesForThisOEM – $oemDevicesAdded jos ($remaining -le 0) { break } $toTake = [Matematiikka]::Min($remaining, $bucketInfo.Devices.Count) jos ($toTake -gt 0) { $selectedDevices = @($bucketInfo.Devices | Select-Object -First $toTake) $waveDevices += $selectedDevices $oemDevicesAdded += $toTake $totalZeroSuccessAdded += $toTake $parts = jos ($bucketInfo.BucketKey -match '\|') { $bucketInfo.BucketKey -split '\|' } else { @($bucketInfo.OEM, $bucketInfo.BucketKey.Substring(0, [Math]::Min(12, $bucketInfo.BucketKey.Length)))) } $displayName = "$($parts[0]) - $($parts[1])" Write-Log " [ZERO-SUCCESS] $displayName - Deploying=$toTake (OEM wave $oemWaveCount = ${devicesForThisOEM}/OEM)" "WARN" } } jos ($oemDevicesAdded -gt 0) { Write-Log " OEM: $oemName - Wave $oemWaveCount, Added $oemDevicesAdded devices" "INFO" $oemsDeployedTo += $oemName } } # Seuraa, missä alkuperäisissä laitevalmistajissa otimme käyttöön (seuraavan onnistumistarkistuksen lisäämiseen) jos ($oemsDeployedTo.Määrä -gt 0) { $RolloutState.PendingOEMWaveIncrement = $oemsDeployedTo Write-Log "Zero-success deployment: $totalZeroSuccessAdded devices across $($oemsDeployedTo.Count) OEM" "INFO" } } } jos (@($waveDevices). Määrä -eq 0) { palauta $null } palauta $waveDevices }
# ============================================================================ # Ryhmäkäytäntöobjektin käyttöönotto (INLINED - luo ryhmäkäytäntöobjektin, käyttöoikeusryhmän, linkit) # ============================================================================
function Deploy-GPOForWave { param( [merkkijono]$GPOName, [merkkijono]$TargetOU, [merkkijono]$SecurityGroupName, [matriisi]$WaveHostnames, [bool]$DryRun = $false ) # ADMX-käytäntö: SecureBoot.admx - SecureBoot_AvailableUpdatesPolicy # Rekisteripolku: HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot # Arvon nimi: KäytettävissäUpdatesPolicy # Käytössä Arvo: 22852 (0x5944) - Päivitä kaikki suojatun käynnistyksen näppäimet + bootmgr # Ei käytössä Arvo: 0 ## # Luotettavan HKLM\SYSTEM-polun käyttöönoton ryhmäkäytäntö asetusten (GPP) käyttäminen # GPP luo asetukset kohdassa: Tietokoneen määritys > Asetukset > Windowsin asetukset > rekisteri $RegistryKey = "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" $RegistryValueName = "AvailableUpdatesPolicy" $RegistryValue = 22852 # 0x5944 - vastaa ADMX käytössäArvo Write-Log "Ryhmäkäytäntöobjektin käyttöönotto: $GPOName" "WAVE" Write-Log "Rekisteri: $RegistryKey\$RegistryValueName = $RegistryValue (0x$($RegistryValue.ToString('X')))" "INFO" jos ($DryRun) { Write-Log "[DRYRUN] Loisi ryhmäkäytäntöobjektin: $GPOName" "TIEDOT" Write-Log "[DRYRUN] Loisi käyttöoikeusryhmän: $SecurityGroupName" "TIEDOT" Write-Log "[DRYRUN] Lisäisi $(@($WaveHostnames). Laske) ryhmiteltavät tietokoneet" "TIEDOT" Write-Log "[DRYRUN] Linkittäisi ryhmäkäytäntöobjektin: $TargetOU" "INFO" palauta $true } kokeile { # Tuo pakolliset moduulit Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } saalis { Write-Log "Pakollisten moduulien tuominen epäonnistui (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR" palauta $false } # Vaihe 1: Ryhmäkäytäntöobjektin luominen tai hankkiminen $existingGPO = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue jos ($existingGPO) { Write-Log "Ryhmäkäytäntöobjekti on jo olemassa: $GPOName" "TIEDOT" $gpo = $existingGPO } muu { kokeile { $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Certificate rollout - AvailableUpdatesPolicy=0x5944 - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "Luotu ryhmäkäytäntöobjekti: $GPOName" "OK" } saalis { Write-Log "Ryhmäkäytäntöobjektin luominen epäonnistui: $($_. Exception.Message)" "ERROR" palauta $false } } # Vaihe 2: Rekisteriarvon määrittäminen ryhmäkäytäntö asetusten (GPP) avulla # GPP on luotettavampi HKLM\SYSTEM-poluille kuin Set-GPRegistryValue kokeile { # Yritä ensin poistaa tämän arvon olemassa olevat asetukset (kaksoiskappaleiden välttämiseksi) Remove-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegistryKey -ValueName $RegistryValueName -ErrorAction SilentlyContinue # GPP-rekisteriasetusten luominen Korvaa-toiminnolla # Replace = Luo, jos sitä ei ole, Päivitä, jos se on olemassa (luotettavin) # Update = Vain päivitys, jos on olemassa (epäonnistuu, jos arvoa ei ole) Set-GPPrefRegistryValue -Name $GPOName ' -Context Computer ' -Toiminnon korvaaminen ' -Key $RegistryKey ' -ValueName $RegistryValueName ' -Type DWord ' -Value $RegistryValue Write-Log "Määritetty GPP-rekisteriasetus: $RegistryValueName = 0x5944 (Action=Replace)" "OK" } saalis { Write-Log "GPP failed, trying Set-GPRegistryValue: $($_. Exception.Message)" "WARN" # Varatoiminto Set-GPRegistryValue (toimii, jos ADMX otetaan käyttöön) kokeile { Set-GPRegistryValue -Name $GPOName ' -Key $RegistryKey ' -ValueName $RegistryValueName ' -Type DWord ' -Arvon $RegistryValue Write-Log "Määritetty rekisteri Set-GPRegistryValue: $RegistryValueName = 0x5944" "OK" } saalis { Write-Log "Rekisteriarvon määrittäminen epäonnistui: $($_. Exception.Message)" "ERROR" palauta $false } } # Vaihe 3: Käyttöoikeusryhmän luominen tai hankkiminen $existingGroup = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue jos (ei $existingGroup) { kokeile { $group = New-ADGroup -Nimi $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers targeted for Secure Boot rollout - $GPOName" ' -PassThru Write-Log "Luotu käyttöoikeusryhmä: $SecurityGroupName" "OK" } saalis { Write-Log "Käyttöoikeusryhmän luominen epäonnistui: $($_. Exception.Message)" "ERROR" palauta $false } } muu { Write-Log "Käyttöoikeusryhmä on olemassa: $SecurityGroupName" "TIEDOT" $group = $existingGroup } # Vaihe 4: Tietokoneiden lisääminen käyttöoikeusryhmään $added = 0 $failed = 0 foreach ($hostname $WaveHostnames) { kokeile { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } saalis { $failed++ } } Write-Log "Lisätty $added tietokoneet käyttöoikeusryhmään ($failed ei löydy AD:stä)" "OK" # Vaihe 5: Suojaussuodatuksen määrittäminen ryhmäkäytäntöobjektissa kokeile { # Poista todennetut käyttäjät -oletuskäyttöoikeus (säilytä luku) Set-GPPermission -Name $GPOName -TargetName "Authenticated Users" -TargetType Group -PermissionLevel GpoRead -Replace -ErrorAction SilentlyContinue # Lisää käyttöoikeusryhmällemme käyttöoikeus Set-GPPermission -Name $GPOName -TargetName $SecurityGroupName -TargetType Group -PermissionLevel GpoApply -ErrorAction Stop Write-Log "Määritetty suojaussuodatus: $SecurityGroupName" "OK" } saalis { Write-Log "Suojaussuodatuksen määrittäminen epäonnistui: $($_. Exception.Message)" "WARN" Write-Log "Ryhmäkäytäntöobjekti saattaa koskea kaikkia linkitetyn OU:n tietokoneita – tarkista manuaalisesti" "VAROITA" } # Vaihe 6: Ryhmäkäytäntöobjektin linkittäminen OU:han (kriittinen, jotta käytäntöä sovelletaan) jos ($TargetOU) { kokeile { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } jos (ei $existingLink) { New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "Linkitetty ryhmäkäytäntöobjekti kohteeseen: $TargetOU" "OK" Write-Log "Ryhmäkäytäntöobjektia sovelletaan seuraavalla gpupdatella kohdetietokoneissa" "TIEDOT" } muu { Write-Log "Ryhmäkäytäntöobjekti, joka on jo linkitetty kohde-OU:han" "TIEDOT" } } saalis { Write-Log "KRIITTINEN: Ryhmäkäytäntöobjektin linkittäminen OU:hen epäonnistui: $($_. Exception.Message)" "ERROR" Write-Log "Ryhmäkäytäntöobjekti on luotu, mutta EI LINKITETTY – se EI koske tietokoneita!" "VIRHE" Write-Log "Manuaalinen korjaus vaaditaan: New-GPLink -Name '$GPOName' -Target '$TargetOU' -LinkEnabled Yes' "ERROR" palauta $false } } muu { Write-Log "VAROITUS: TargetOU:ta ei ole määritetty – ryhmäkäytäntöobjekti luotu, mutta EI LINKITETTY!" "VIRHE" Write-Log "Ryhmäkäytäntöobjektin voimaantulo edellyttää manuaalista linkitystä" "VIRHE" Write-Log "Suorita: New-GPLink -Name '$GPOName' -Target '<Your-Domain-DN>' -LinkEnabled Yes" "ERROR" } # Vaihe 7: Ryhmäkäytäntöobjektin määrityksen tarkistaminen Write-Log "Tarkistetaan ryhmäkäytäntöobjektin määritystä..." "INFO" kokeile { $gpoReport = Get-GPO -Name $GPOName -ErrorAction Stop Write-Log "Ryhmäkäytäntöobjektin tila: $($gpoReport.GpoStatus)" "INFO" # Tarkista, onko rekisteriasetus määritetty $regSettings = Get-GPRegistryValue -Name $GPOName -Key "HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot" -ErrorAction SilentlyContinue jos (ei $regSettings) { # Kokeile GPP-rekisterin tarkistusta (eri polku ryhmäkäytäntöobjektissa) Write-Log "Tarkistetaan GPP-rekisteriasetuksia..." "INFO" } } saalis { Write-Log "Ryhmäkäytäntöobjektia ei voitu vahvistaa: $($_. Exception.Message)" "WARN" } palauta $true }
# ============================================================================ # WINCS DEPLOYMENT (Vaihtoehto käytettävissä olevalleUpdatesPolicy-ryhmäkäytäntöobjektille) # ============================================================================ # Viite: https://support.microsoft.com/en-us/topic/windows-configuration-system-wincs-apis-for-secure-boot-d3e64aa0-6095-4f8a-b8e4-fbfda254a8fe ## # WinCS-komennot (suoritetaan päätepisteellä JÄRJESTELMÄ-kontekstissa): # Kysely: WinCsFlags.exe /query --key F33E0C8E002 # Käytä: WinCsFlags.exe /apply --key "F33E0C8E002" # Reset: WinCsFlags.exe /reset --key "F33E0C8E002" ## # Tämä menetelmä ottaa käyttöön ryhmäkäytäntöobjektin, jossa on ajoitettu tehtävä, joka suoritetaan WinCsFlags.exe /apply # järjestelmä kohdennetuissa päätepisteissä. Samalla tavalla kuin tunnistuskomentosarja otetaan käyttöön, # mutta suoritetaan kerran (käynnistyksen yhteydessä) päivittäisen sijaan.
function Deploy-WinCSGPOForWave { <# . SYNOPSIS Ota wincs-suojatun käynnistyksen käyttöönotto käyttöön ryhmäkäytäntöobjektin ajoitetun tehtävän kautta.. KUVAUS Luo ryhmäkäytäntöobjektin, joka ottaa käyttöön ajoitetun tehtävän suoritettavaksi WinCsFlags.exe /apply kohdassa JÄRJESTELMÄkonteksti tietokoneen käynnistyksen yhteydessä. Käyttöoikeusryhmän ohjausobjektien kohdentaminen.. PARAMETER GPOName Ryhmäkäytäntöobjektin nimi.. PARAMETER TargetOU OU, jos haluat linkittää ryhmäkäytäntöobjektin.. PARAMETER SecurityGroupName Ryhmäkäytäntöobjektin suodatuksen käyttöoikeusryhmä.. PARAMETER WaveHostnames Käyttöoikeusryhmään lisättävät isäntänimet.. PARAMETRI WinCSKey Käytettävä WinCS-näppäin (oletus: F33E0C8E002).. PARAMETER DryRun Jos se on tosi, kirjaa vain se, mitä tehdään.#> param( [Parametri(Pakollinen = $true)] [merkkijono]$GPOName, [Parameter(Mandatory = $false)] [merkkijono]$TargetOU, [Parametri(Pakollinen = $true)] [merkkijono]$SecurityGroupName, [Parametri(Pakollinen = $true)] [matriisi]$WaveHostnames, [Parameter(Mandatory = $false)] [merkkijono]$WinCSKey = "F33E0C8E002", [Parametri(Pakollinen = $false)] [bool]$DryRun = $false ) # Ajoitettu tehtävämääritys WinCsFlags.exe $TaskName = SecureBoot-WinCS-Apply $TaskPath = "\Microsoft\Windows\SecureBoot\" $TaskDescription = "Käyttää suojattua käynnistysmääritystä WinCS:n kautta – avain: $WinCSKey" Write-Log "WinCS-ryhmäkäytäntöobjektin käyttöönotto: $GPOName" "WAVE" Write-Log "Tehtävä suoritetaan: WinCsFlags.exe /apply --key '"$WinCSKey'"" "INFO" Write-Log "Trigger: At system startup (runs once as SYSTEM)" "INFO" jos ($DryRun) { Write-Log "[DRYRUN] Loisi ryhmäkäytäntöobjektin: $GPOName" "TIEDOT" Write-Log "[DRYRUN] Loisi käyttöoikeusryhmän: $SecurityGroupName" "TIEDOT" Write-Log "[DRYRUN] Lisäisi $(@($WaveHostnames). Laske) ryhmiteltavät tietokoneet" "TIEDOT" Write-Log "[DRYRUN] Ottaisi käyttöön ajoitetun tehtävän: $TaskName" "TIEDOT" Write-Log "[DRYRUN] Linkittäisi ryhmäkäytäntöobjektin kohteeseen: $TargetOU" "INFO" palauta @{ Success = $true RyhmäkäytäntöobjektiLuotu = $false GroupCreated = $false ComputersAdded = 0 } } kokeile { # Tuo pakolliset moduulit Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } saalis { Write-Log "Pakollisten moduulien tuominen epäonnistui (GroupPolicy, ActiveDirectory): $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } # Vaihe 1: Ryhmäkäytäntöobjektin luominen tai hankkiminen $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue jos ($gpo) { Write-Log "Ryhmäkäytäntöobjekti on jo olemassa: $GPOName" "TIEDOT" } muu { kokeile { $gpo = New-GPO -Name $GPOName -Comment "Secure Boot WinCS Deployment - $WinCSKey - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "Luotu ryhmäkäytäntöobjekti: $GPOName" "OK" } saalis { Write-Log "Ryhmäkäytäntöobjektin luominen epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } } # Vaihe 2: Ajoitetun tehtävän XML:n luominen ryhmäkäytäntöobjektin käyttöönottoa varten # Tämä luo tehtävän, joka suoritetaan WinCsFlags.exe /apply käynnistyksen yhteydessä $taskXml = @" <?xml version="1.0" encoding="UTF-16"?> <Tehtävän versio="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <kuvaus>$TaskDescription</Description> WinCsFlags.exe1 Author>SYSTEM</Author> WinCsFlags.exe5 /RegistrationInfo> WinCsFlags.exe7 käynnistää> WinCsFlags.exe9 BootTrigger> <Käytössä>tosi</Käytössä> <viive>PT5M</Delay> </BootTrigger> </Triggers> <principals> <Principal id="Author"> <UserId>S-1-5-18</UserId> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Asetukset-> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <AllowHardTerminate>tosi</AllowHardTerminate> <AloitusValintojen>tosi</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>false</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings-> <AllowStartOnDemand>tosi</AllowStartOnDemand> <Käytössä>tosi</Käytössä> <Piilotettu>epätosi</Piilotettu> <RunOnlyIfIdle>false</RunOnlyIfIdle> WinCsFlags.exe03 DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession> WinCsFlags.exe07 UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine> WinCsFlags.exe11 WakeToRun>epätosi</WakeToRun> WinCsFlags.exe15 ExecutionTimeLimit>PT1H</ExecutionTimeLimit> WinCsFlags.exe19 DeleteExpiredTaskAfter>P30D</DeleteExpiredTaskAfter> WinCsFlags.exe23 Prioriteetti>7</Priority> WinCsFlags.exe27 /Settings> WinCsFlags.exe29 Toiminnot Context="Tekijä"WinCsFlags.exe30 WinCsFlags.exe31 Exec> WinCsFlags.exe33 komento>WinCsFlags.exe</Komento-> WinCsFlags.exe37 argumentit>/apply -key "$WinCSKey"WinCsFlags.exe39 /Arguments> WinCsFlags.exe41 /Exec> WinCsFlags.exe43 /Actions> WinCsFlags.exe45 /Task> " @
# Step 3: Deploy scheduled task via GPO Preferences # Tallenna tehtävän XML SYSVOL-tiedostoon ryhmäkäytäntöobjektin ajoitettujen tehtävien välitöntä tehtävää varten kokeile { $gpoId = $gpo. Id.ToString() $sysvolPath = "\\$((Get-ADDomain). DNSRoot)\SYSVOL\$((Get-ADDomain). DNSRoot)\Policies\{$gpoId}\Machine\Preferences\ScheduledTasks" if (-not (Test-Path $sysvolPath)) { New-Item -ItemType Directory -Path $sysvolPath -Force | Out-Null } # ScheduledTasks.xml luominen GPP:tä varten $gppTaskXml = @" <?xml version="1.0" encoding="utf-8"?> <ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"> <ImmediateTaskV2 clsid="{9756B581-76EC-4169-9AFC-0CA8D43ADB5F}" name="$TaskName" image="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="{$([guid]::NewGuid(). ToString(). ToUpper())}"> <Ominaisuudet-toiminto="C" name="$TaskName" runAs="NT AUTHORITY\System" logonType="S4U"> <Tehtävän versio="1.3"> <RegistrationInfo> <kuvaus>$TaskDescription</Description> </RegistrationInfo> <principals> <Principal id="Author"> <UserId>NT AUTHORITY\System</UserId> <LogonType>S4U</LogonType> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Asetukset-> <IdleSettings> <Kesto>PT5M</Duration> <WaitTimeout>PT1H</WaitTimeout> <StopOnIdleEnd>false</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <AllowHardTerminate>tosi</AllowHardTerminate> <AloitusValintojen>tosi</StartWhenAvailable> <AllowStartOnDemand>tosi</AllowStartOnDemand> <Käytössä>tosi</Käytössä> <Piilotettu>epätosi</Hidden> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <Prioriteetti>7</Priority> <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> </Settings> <käynnistää> <TimeTrigger> <StartBoundary>$(Get-Date -Format 'yyyy-MM-dd')T00:00:00:00</StartBoundary> <Käytössä>tosi</Käytössä> </TimeTrigger> </Triggers> <Toiminnot-> <Exec> <Komento>WinCsFlags.exe</Komento> <argumentit>/apply --key "$WinCSKey"</Argumentit> </Exec> </Actions> </Task> </Properties> </ImmediateTaskV2> </ScheduledTasks> "@ $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force Write-Log "Otettu käyttöön ajoitettu tehtävä ryhmäkäytäntöobjektiin: $TaskName" "OK" } saalis { Write-Log "Ajoitetun tehtävän XML:n käyttöönotto epäonnistui: $($_. Exception.Message)" "WARN" Write-Log "Palaaminen rekisteripohjaiseen WinCS-käyttöönottoon" "INFO" # Fallback: Käytä WinCS-rekisteritapaa, jos GPP:n ajoitettu tehtävä epäonnistuu # WinCS voidaan käynnistää myös rekisteriavaimen kautta # (Käyttöönotto riippuu WinCS-rekisterin ohjelmointirajapinnasta, jos se on käytettävissä) } # Vaihe 4: Käyttöoikeusryhmän luominen tai hankkiminen $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue jos (ei $group) { kokeile { $group = New-ADGroup -Name $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Description "Computers targeted for Secure Boot WinCS rollout - $GPOName" ' -PassThru Write-Log "Luotu käyttöoikeusryhmä: $SecurityGroupName" "OK" } saalis { Write-Log "Käyttöoikeusryhmän luominen epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } } muu { Write-Log "Käyttöoikeusryhmä on olemassa: $SecurityGroupName" "TIEDOT" } # Vaihe 5: Tietokoneiden lisääminen käyttöoikeusryhmään $added = 0 $failed = 0 foreach ($hostname in $WaveHostnames) { kokeile { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } saalis { $failed++ } } Write-Log "Lisätty $added tietokoneet käyttöoikeusryhmään ($failed ei löydy AD:stä)" "OK" # Vaihe 6: Suojaussuodatuksen määrittäminen ryhmäkäytäntöobjektissa kokeile { 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 "Määritetty suojaussuodatus: $SecurityGroupName" "OK" } saalis { Write-Log "Suojaussuodatuksen määrittäminen epäonnistui: $($_. Exception.Message)" "WARN" } # Vaihe 7: Ryhmäkäytäntöobjektin linkittäminen OU:han jos ($TargetOU) { kokeile { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } jos (ei $existingLink) { New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "Linkitetty ryhmäkäytäntöobjekti kohteeseen: $TargetOU" "OK" } muu { Write-Log "Ryhmäkäytäntöobjekti, joka on jo linkitetty kohde-OU:han" "TIEDOT" } } saalis { Write-Log "KRIITTINEN: Ryhmäkäytäntöobjektin linkittäminen OU:hen epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = "Ryhmäkäytäntöobjektilinkki epäonnistui: $($_. Exception.Message)" } } } Write-Log "WinCS-ryhmäkäytäntöobjektin käyttöönotto valmis" "OK" Write-Log "Koneet suoritetaan WinCsFlags.exe seuraavassa ryhmäkäytäntöobjektin päivityksessä + uudelleenkäynnistys/käynnistys" "TIEDOT" palauta @{ Success = $true RyhmäkäytäntöobjektiLuotu = $true GroupCreated = $true ComputersAdded = $added Tietokoneet epäonnistui = $failed } }
# Wrapper function to maintain compatibility with main loop funktio Deploy-WinCSForWave { param( [Parametri(Pakollinen = $true)] [matriisi]$WaveHostnames, [Parameter(Mandatory = $false)] [merkkijono]$WinCSKey = "F33E0C8E002", [Parameter(Mandatory = $false)] [string]$WavePrefix = "SecureBoot-Rollout", [Parameter(Mandatory = $false)] [int]$WaveNumber = 1, [Parameter(Mandatory = $false)] [merkkijono]$TargetOU, [Parametri(Pakollinen = $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 # Muunna odotettuun palautusmuotoon palauta @{ Success = $result. Menestys Käytössä = $result. ComputersAdded Ohitettu = 0 Epäonnistui = jos ($result. Tietokoneet epäonnistuivat) { $result. Tietokoneet epäonnistui } muu { 0 } Tulokset = @() } }
# ============================================================================ # OTA TEHTÄVÄN KÄYTTÖÖNOTTO KÄYTTÖÖN # ============================================================================ # Ota Enable-SecureBootUpdateTask.ps1 käyttöön laitteissa, joiden ajoitettu tehtävä on poistettu käytöstä.# Käyttää ryhmäkäytäntöobjektia, jossa on välittömästi ajoitettu tehtävä, joka suoritetaan kerran.
function Deploy-EnableTaskGPO { <# . SYNOPSIS Ota Enable-SecureBootUpdateTask.ps1 käyttöön ryhmäkäytäntöobjektin ajoitetun tehtävän kautta.. KUVAUS Luo ryhmäkäytäntöobjektin, joka ottaa käyttöön kertaluonteisen ajoitetun tehtävän Suojatun käynnistyksen ja päivityksen ajoitettu tehtävä kohdelaitteissa.. PARAMETER TargetOU OU, jos haluat linkittää ryhmäkäytäntöobjektin.. PARAMETER TargetHostnames Käytöstä poistettujen laitteiden isäntänimet (koosteraportista).. PARAMETER DryRun Jos se on tosi, kirjaa vain se, mitä tehdään.#> param( [Parameter(Mandatory = $false)] [merkkijono]$TargetOU, [Parametri(Pakollinen = $true)] [matriisi]$TargetHostnames, [Parameter(Mandatory = $false)] [bool]$DryRun = $false ) $GPOName = SecureBoot-EnableTask-Remediation $SecurityGroupName = SecureBoot-EnableTask-Devices $TaskName = "SecureBoot-EnableTask-OneTime" $TaskDescription = "Kerta-aikainen tehtävä, joka ottaa käyttöön ajoitetun Secure-Boot-Update-tehtävän" Write-Log "=" * 70 "TIEDOT" Write-Log OTA TEHTÄVÄN KORJAUS KÄYTTÖÖN -TOIMINNON TIEDOT Write-Log "=" * 70 "TIEDOT" Write-Log "Kohdelaitteet: $($TargetHostnames.Count)" "TIEDOT" Write-Log "Ryhmäkäytäntöobjekti: $GPOName" "TIEDOT" Write-Log "Käyttöoikeusryhmä: $SecurityGroupName" "TIEDOT" jos ($DryRun) { Write-Log "[DRYRUN] Loisi ryhmäkäytäntöobjektin: $GPOName" "TIEDOT" Write-Log "[DRYRUN] Loisi käyttöoikeusryhmän: $SecurityGroupName" "TIEDOT" Write-Log "[DRYRUN] To add $($TargetHostnames.Count) computers to group" "INFO" Write-Log "[DRYRUN] Ottaa käyttöön kerta-ajoitetun tehtävän, joka ottaa käyttöön suojatun käynnistyspäivityksen" "TIEDOT" Write-Log "[DRYRUN] Linkittäisi ryhmäkäytäntöobjektin kohteeseen: $TargetOU" "INFO" palauta @{ Success = $true ComputersAdded = 0 DryRun = $true } } kokeile { # Tuo pakolliset moduulit Import-Module GroupPolicy -ErrorAction Stop Import-Module ActiveDirectory -ErrorAction Stop } saalis { Write-Log "Pakollisten moduulien tuonti epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } # Vaihe 1: Ryhmäkäytäntöobjektin luominen tai hankkiminen $gpo = Get-GPO -Name $GPOName -ErrorAction SilentlyContinue jos ($gpo) { Write-Log "Ryhmäkäytäntöobjekti on jo olemassa: $GPOName" "TIEDOT" } muu { kokeile { $gpo = New-GPO -Name $GPOName -Comment "Secure Boot Task Enable Remediation - Created $(Get-Date -Format 'yyyy-MM-dd HH:mm')" Write-Log "Luotu ryhmäkäytäntöobjekti: $GPOName" "OK" } saalis { Write-Log "Ryhmäkäytäntöobjektin luominen epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } } # Vaihe 2: Ajoitetun tehtävän XML:n käyttöönotto GPO SYSVOL :ssa # Tehtävä suorittaa PowerShell-komennon secure-boot-update-tehtävän käyttöönottoa varten kokeile { $sysvolPath = "\\$($env:USERDNSDOMAIN)\SYSVOL\$($env:USERDNSDOMAIN)\Policies\{$($gpo. Id)}\Machine\Preferences\ScheduledTasks" if (-not (Test-Path $sysvolPath)) { New-Item -ItemType Directory -Path $sysvolPath -Force | Out-Null } # PowerShell-komento secure-boot-update-tehtävän ottamiseksi käyttöön $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 }' # Turvallisen XML-upottamisen koodauskomento $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($enableCommand)) $taskGuid = [guid]::NewGuid(). ToString("B"). Toupper() # GPP:n ajoitettu tehtävän XML – välitön tehtävä, joka suoritetaan kerran $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"> <Ominaisuudet-toiminto="C" name="$TaskName" runAs="NT AUTHORITY\SYSTEM" logonType="S4U"> <Tehtävän versio="1.3"> <RegistrationInfo> <kuvaus>$TaskDescription</Description> </RegistrationInfo> <principals> <Principal id="Author"> <UserId>S-1-5-18</UserId> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Asetukset-> <IdleSettings-> <Kesto>PT5M</Duration> <WaitTimeout>PT1H</WaitTimeout> <StopOnIdleEnd>false</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings-> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <AllowHardTerminate>tosi</AllowHardTerminate> <AloitusValintojen>tosi</StartWhenAvailable> <AllowStartOnDemand>true</AllowStartOnDemand> <Käytössä>tosi</Käytössä> <Piilotettu>epätosi</Piilotettu> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <prioriteetti>7</Priority> <DeleteExpiredTaskAfter>PT0S</DeleteExpiredTaskAfter> </Settings> <Toiminnot-> <Exec> <Komento>powershell.exe</Komento> <argumentit>-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand</Arguments> </Exec> </Actions> </Task> </Properties> </ImmediateTaskV2> </ScheduledTasks> "@ $gppTaskXml | Out-File -FilePath (Join-Path $sysvolPath "ScheduledTasks.xml") -Encoding UTF8 -Force Write-Log "Otettu käyttöön kertaluonteinen ajoitettu tehtävä ryhmäkäytäntöobjektiin: $TaskName" "OK" } saalis { Write-Log "Ajoitetun tehtävän XML:n käyttöönotto epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } # Vaihe 3: Käyttöoikeusryhmän luominen tai hankkiminen $group = Get-ADGroup -Filter "Name -eq '$SecurityGroupName'" -ErrorAction SilentlyContinue jos (ei $group) { kokeile { $group = New-ADGroup -Nimi $SecurityGroupName ' -GroupCategory Security ' -GroupScope DomainLocal ' -Kuvaus "Tietokoneet, joissa on poistettu käytöstä suojatun käynnistyksen ja päivityksen tehtävä – kohdennettu korjattavaksi" ' -PassThru Write-Log "Luotu käyttöoikeusryhmä: $SecurityGroupName" "OK" } saalis { Write-Log "Käyttöoikeusryhmän luominen epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = $_. Exception.Message } } } muu { Write-Log "Käyttöoikeusryhmä on olemassa: $SecurityGroupName" "TIEDOT" } # Vaihe 4: Tietokoneiden lisääminen käyttöoikeusryhmään $added = 0 $failed = 0 foreach ($hostname in $TargetHostnames) { kokeile { $computer = Get-ADComputer -Identity $hostname -ErrorAction Stop Add-ADGroupMember -Identity $SecurityGroupName -Members $computer -ErrorAction SilentlyContinue $added++ } saalis { $failed++ Write-Log "Tietokonetta ei löydy AD:stä: $hostname" "WARN" } } Write-Log "Lisätty $added tietokoneita käyttöoikeusryhmään ($failed ei löydy AD:stä)" "OK" # Vaihe 5: Suojaussuodatuksen määrittäminen ryhmäkäytäntöobjektissa kokeile { 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 "Määritetty suojaussuodatus: $SecurityGroupName" "OK" } saalis { Write-Log "Suojaussuodatuksen määrittäminen epäonnistui: $($_. Exception.Message)" "WARN" } # Vaihe 6: Ryhmäkäytäntöobjektin linkittäminen OU:han jos ($TargetOU) { kokeile { $existingLink = Get-GPInheritance -Target $TargetOU -ErrorAction SilentlyContinue | Select-Object -ExpandProperty GpoLinks | Where-Object { $_. DisplayName -eq $GPOName } jos (ei $existingLink) { New-GPLink -Name $GPOName -Target $TargetOU -LinkEnabled Yes -ErrorAction Stop Write-Log "Linkitetty ryhmäkäytäntöobjekti kohteeseen: $TargetOU" "OK" } muu { Write-Log "Ryhmäkäytäntöobjekti, joka on jo linkitetty kohde-OU:han" "TIEDOT" } } saalis { Write-Log "Ryhmäkäytäntöobjektin linkittäminen OU:hen epäonnistui: $($_. Exception.Message)" "ERROR" return @{ Success = $false; Virhe = "Ryhmäkäytäntöobjektilinkki epäonnistui: $($_. Exception.Message)" } } } muu { Write-Log "TargetOU-kohdetta ei ole määritetty – ryhmäkäytäntöobjekti on linkitettävä manuaalisesti" "VAROITA" } Write-Log "" "TIEDOT" Write-Log "ENABLE TASK DEPLOYMENT COMPLETE" "OK" Write-Log "Laitteet suorittavat käyttöönottotehtävän seuraavassa ryhmäkäytäntöobjektin päivityksessä (gpupdate)" "INFO" Write-Log "Tehtävä suoritetaan kerran JÄRJESTELMÄNä ja ottaa käyttöön suojatun käynnistyksen päivityksen" "TIEDOT" Write-Log "" "TIEDOT" palauta @{ Success = $true ComputersAdded = $added Tietokoneet epäonnistui = $failed GPOName = $GPOName SecurityGroup = $SecurityGroupName } }
# ============================================================================ # OTA TEHTÄVÄ KÄYTTÖÖN KÄYTÖSTÄ POISTETUISSA LAITTEISSA # ============================================================================ jos ($EnableTaskOnDisabled) { Write-Host "" Write-Host ("=" * 70) -EtualallaColor Keltainen Write-Host " ENABLE TASK REMEDIATION - Fix Disabled Scheduled Tasks" -ForegroundColor Yellow Write-Host ("=" * 70) -EdustaVäri Keltainen Write-Host "" # Etsi koostetiedoista laitteita, joiden tehtävä on poistettu käytöstä jos (ei $AggregationInputPath) { Write-Host "VIRHE: -AggregationInputPath tarvitaan sellaisten laitteiden tunnistamiseen, joiden tehtävä on poistettu käytöstä" -EtualallaColor Red Write-Host "Käyttö: .\Start-SecureBootRolloutOrchestrator.ps1 -EnableTaskOnDisabled -AggregationInputPath <polku> -ReportBasePath <polku>" -EdustaVäri Harmaa exit 1 } Write-Host "Tarkistetaan laitteita, joiden suojattu käynnistyspäivitys on poistettu käytöstä..." -Edustavärisyan # Lataa JSON-tiedostoja ja etsi laitteita, joiden tehtävä on poistettu käytöstä $jsonFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_. Nimi -notmatch "ScanHistory|RolloutState|RolloutPlan" } $disabledTaskDevices = @() foreach ($file in $jsonFiles) { kokeile { $device = Get-Content $file. FullName -Raw | ConvertFrom-Json jos ($device. SecureBootTaskEnabled -eq $false -or $device. SecureBootTaskStatus -eq 'Disabled' -or $device. SecureBootTaskStatus -eq 'NotFound') { # Sisällytä vain laitteet, joita ei ole vielä päivitetty (ei tapahtumaa 1808) if ([int]$device. Event1808Count -eq 0) { $disabledTaskDevices += $device. Hostname } } } saalis { # Ohita virheelliset tiedostot } } $disabledTaskDevices = $disabledTaskDevices | Select-Object -Unique if ($disabledTaskDevices.Count -eq 0) { Write-Host "" Write-Host "Ei laitteita, joissa on poistettu käytöstä suojattu käynnistys-päivitys-tehtävä". -EtualallaColor Green Write-Host "Kaikissa laitteissa tehtävä on joko otettu käyttöön tai ne on jo päivitetty". -Edustavärin väri harmaa exit 0 } Write-Host "" Write-Host "Löydetyt $($disabledTaskDevices.Count) laitteet, joiden tehtävä on poistettu käytöstä:" -EdustaVäri Keltainen $disabledTaskDevices | Select-Object -Ensimmäinen 20 | ForEach-Object { Write-Host " - $_" -ForegroundColor Gray } jos ($disabledTaskDevices.Määrä -gt 20) { Write-Host " ... ja $($disabledTaskDevices.Count - 20) more" -ForegroundColor Gray } Write-Host "" # Ota tehtävän ryhmäkäytäntöobjekti käyttöön $result = Deploy-EnableTaskGPO -TargetHostnames $disabledTaskDevices -TargetOU $TargetOU -DryRun $DryRun jos ($result. Menestys) { Write-Host "" Write-Host "SUCCESS: Enable Task GPO deployed" -ForegroundColor Green Write-Host " Suojausryhmään lisätyt tietokoneet: $($result. ComputersAdded)" -ForegroundColor Cyan jos ($result. Tietokoneet epäonnistui -gt 0) { Write-Host " Tietokoneet, jotka eivät löydy AD:stä: $($result. Tietokoneet epäonnistui)" -EtualallaColor Keltainen } Write-Host "" Write-Host "NEXT STEPS:" -ForegroundColor White Write-Host " 1. Laitteet saavat ryhmäkäytäntöobjektin seuraavan päivityksen yhteydessä (gpupdate /force)" -ForegroundColor Gray Write-Host " 2. Kertakäyttöinen tehtävä ottaa käyttöön Secure-Boot-Updaten" -ForegroundColor Gray Write-Host " 3. Tarkista, että tehtävä on nyt käytössä suorittamalla kooste uudelleen" -Edustavärin värin harmaa } muu { Write-Host "" Write-Host "FAILED: Enable Task GPO" -ForegroundColor Red Write-Host "Virhe: $($result. Virhe)" -EtualallaColor Punainen } exit 0 }
# ============================================================================ # PÄÄORKESTROINTISILMUKKA # ============================================================================
Write-Host "" Write-Host ("=" * 80) -Edustavärisyan Write-Host " SECURE BOOT ROLLOUT ORCHESTRATOR - CONTINUOUS DEPLOYMENT" -ForegroundColor Cyan Write-Host ("=" * 80) -Edustavärisyan Write-Host ""
if ($DryRun) { Write-Host "[DRY RUN MODE]" -ForegroundColor Magenta }
if ($UseWinCS) { Write-Host "[WinCS MODE]" -ForegroundColor Yellow Write-Host "WinCsFlags.exe käyttäminen ryhmäkäytäntöobjektin/AvailableUpdatesPolicyn sijaan" -EtualallaColor Keltainen Write-Host "WinCS Key: $WinCSKey" -ForegroundColor Gray Write-Host "" }
Write-Log "Starting Secure Boot Rollout Orchestrator" "INFO" Write-Log "Syötepolku: $AggregationInputPath" "TIEDOT" Write-Log "Raporttipolku: $ReportBasePath" "TIEDOT" jos ($UseWinCS) { Write-Log "Käyttöönottotapa: WinCS (WinCsFlags.exe /apply --key '"$WinCSKey'")" "INFO" } muu { Write-Log "Deployment Method: GPO (AvailableUpdatesPolicy)" "INFO" }
# Resolve TargetOU - default to domain root for domain-wide coverage # Tarvitaan vain ryhmäkäytäntöobjektin käyttöönottomenetelmään (WinCS ei edellytä AD/GPO:tä) jos (ei $UseWinCS -eikä $TargetOU) { kokeile { # Kokeile useita tapoja hankkia toimialueen DN $domainDN = $null # Menetelmä 1: Get-ADDomain (edellyttää RSAT-AD-PowerShelliä) kokeile { Import-Module ActiveDirectory -ErrorAction Stop $domainDN = (Get-ADDomain -ErrorAction Stop). DistinguishedName } saalis { Write-Log "Get-ADDomain epäonnistui: $($_. Exception.Message)" "WARN" } # Menetelmä 2: RootDSE:n käyttäminen ADSI:n kautta jos (ei $domainDN) { kokeile { $rootDSE = [ADSI]"LDAP://RootDSE" $domainDN = $rootDSE.defaultNamingContext.ToString() } saalis { Write-Log "ADSI RootDSE epäonnistui: $($_. Exception.Message)" "WARN" } } # Menetelmä 3: Jäsennä tietokoneen toimialuejäsenyydestä jos (ei $domainDN) { kokeile { $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() $domainDN = "DC=" + ($domain. Nimi -replace '\.', ',DC=') } saalis { Write-Log "GetComputerDomain epäonnistui: $($_. Exception.Message)" "WARN" } } jos ($domainDN) { $TargetOU = $domainDN Write-Log "Target: Domain Root ($domainDN) – Ryhmäkäytäntöobjekti käyttää koko toimialuetta käyttöoikeusryhmän suodatuksella" "TIEDOT" } muu { Write-Log "Toimialueen DN selvittäminen epäonnistui – ryhmäkäytäntöobjekti luodaan, mutta EI LINKITEtä!" "VIRHE" Write-Log "Määritä -TargetOU-parametri tai linkitä ryhmäkäytäntöobjekti manuaalisesti luomisen jälkeen" "VIRHE" $TargetOU = $null } } saalis { Write-Log "Toimialueen DN saaminen epäonnistui – ryhmäkäytäntöobjekti luodaan, mutta sitä ei linkitetä. Linkitä tarvittaessa manuaalisesti." "VAROITA" Write-Log "Virhe: $($_. Exception.Message)" "WARN" $TargetOU = $null } } muu { Write-Log "Target OU: $TargetOU" "INFO" }
Write-Log "Max Wait Hours: $MaxWaitHours" "INFO" Write-Log "Kyselyväli: $PollIntervalMinutes minuuttia" "TIEDOT" jos ($LargeScaleMode) { Write-Log "LargeScaleMode enabled (batch size: $ProcessingBatchSize, log sample: $DeviceLogSampleSize)" "INFO" }
# ============================================================================ # EDELLYTYKSET TARKISTA: Tarkista, että tunnistus on käytössä ja toimii # ============================================================================
Write-Host "" Write-Log "Tarkistetaan edellytykset..." "INFO"
$detectionCheck = Test-DetectionGPODeployed -JsonPath $AggregationInputPath jos (ei $detectionCheck.IsDeployed) { Write-Log $detectionCheck.Message "ERROR" Write-Host "" Write-Host "REQUIRED: Deploy detection infrastructure first:" -ForegroundColor Yellow Write-Host " 1. Suorita: Deploy-GPO-SecureBootCollection.ps1 -OUPath 'OU=...' -OutputPath '\\server\SecureBootLogs$'" -ForegroundColor Cyan Write-Host " 2. Odota, että laitteet raportoivat (12–24 tuntia)" -Edustavärisyan Write-Host " 3. Suorita tämä orkestraattori uudelleen" -ForegroundColor Cyan Write-Host "" jos (ei $DryRun) { Palauttaa } } muu { Write-Log $detectionCheck.Message "OK" }
# Check data freshness $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Tietojen tuoreus: $($freshness. TotalFiles) -tiedostot, $($freshness. FreshFiles) tuore (<24h), $($freshness. StaleFiles) stale (>72h)" "INFO" jos ($freshness. Varoitus) { Write-Log $freshness. Varoitus "VAROITA" }
# Load Allow List (targeted rollout - ONLY these devices will be rolled out) $allowedHostnames = @() jos ($AllowListPath -tai $AllowADGroup) { $allowedHostnames = Get-AllowedHostnames -AllowFilePath $AllowListPath -ADGroupName $AllowADGroup jos ($allowedHostnames.Määrä -gt 0) { Write-Log "AllowList: ONLY $($allowedHostnames.Count) -laitteita harkitaan käyttöönottoa varten" "INFO" } muu { Write-Log "SallittuLuettelo määritetty, mutta laitteita ei löytynyt – tämä estää kaikki käyttöönotot!" "VAROITA" } }
# Load VIP/exclusion list (BlockList) $excludedHostnames = @() jos ($ExclusionListPath -tai $ExcludeADGroup) { $excludedHostnames = Get-ExcludedHostnames -ExclusionFilePath $ExclusionListPath -ADGroupName $ExcludeADGroup jos ($excludedHostnames.Määrä -gt 0) { Write-Log "VIP Exclusion: $($excludedHostnames.Count) -laitteet ohitetaan käyttöönotosta" "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 "yy-MM-dd HH:mm:mm:ss" Write-Log "Uuden käyttöönoton aloittaminen" "WAVE" }
Write-Log "Current Wave: $($rolloutState.CurrentWave)" "INFO" Write-Log "Estetyt säilöt: $($blockedBuckets.Count)" "TIEDOT"
# Main loop - runs until all eligible devices are updated $iterationCount = 0 kun ($true) { $iterationCount++ Write-Host "" Write-Host ("=" * 80) -EdustaVäri Valkoinen Write-Log "=== ITERAATIO $iterationCount ===" "WAVE" Write-Host ("=" * 80) -Edustaväri Valkoinen # Vaihe 1: Suorita kooste Write-Log "Vaihe 1: Koostamisen suorittaminen..." "INFO" # Orkestraattori käyttää aina yhtä kansiota (LargeScaleMode) uudelleen levyn paisumisen välttämiseksi # Koostajaa käyttävät järjestelmänvalvojat saavat manuaalisesti aikaleimatut kansiot aika-aikaisia tilannevedoksia varten $aggregationPath = Join-Path $ReportBasePath "Aggregation_Current" # Tarkista tietojen tuoreus ennen koostamista $freshness = Get-DataFreshness -JsonPath $AggregationInputPath Write-Log "Tietojen tuoreus: $($freshness. FreshFiles)/$($freshness. TotalFiles) laitteet raportoitu viimeisen 24h" "INFO" jos ($freshness. Varoitus) { Write-Log $freshness. Varoitus "VAROITA" } $aggregateScript = Join-Path $ScriptRoot "Aggregate-SecureBootData.ps1" $scanHistoryPath = Join-Path $ReportBasePath "ScanHistory.json" $rolloutSummaryPath = Join-Path $stateDir "SecureBootRolloutSummary.json" if (Test-Path $aggregateScript) { jos (ei $DryRun) { # Orkestraattori käyttää aina suoratoistoa + lisätehokkuutta # Aggregator nostaa automaattisesti PS7:ään, jos se on käytettävissä parhaan suorituskyvyn varmistamiseksi $aggregateParams = @{ InputPath = $AggregationInputPath OutputPath = $aggregationPath StreamingMode = $true IncrementalMode = $true SkipReportIfUnchanged = $true ParallelThreads = 8 } # Suorita käyttöönoton yhteenveto, jos se on olemassa (nopeus- ja projektiotiedot) if (Test-Path $rolloutSummaryPath) { $aggregateParams['RolloutSummaryPath'] = $rolloutSummaryPath } & $aggregateScript @aggregateParams # Näytä-komento, kun haluat luoda täyden HTML-koontinäytön laitetaulukoiden avulla Write-Host "" Write-Host "Jos haluat luoda täyden HTML-koontinäytön Valmistajan/mallin taulukoiden avulla, suorita:" -EdustaväriVäri Keltainen Write-Host " $aggregateScript -InputPath '"$AggregationInputPath'" -OutputPath '"$aggregationPath'"" -ForegroundColor Yellow Write-Host "" } muu { Write-Log "[DRYRUN] Suoritettaisiin kooste" "TIEDOT" # Käytä DryRunissa reportbasepathin aiemmin luotuja koostetietoja suoraan $aggregationPath = $ReportBasePath } } $rolloutState.LastAggregation = Get-Date -Format "yyyy-MM-dd HH:mm:mm:ss" # Vaihe 2: Lataa laitteen nykyinen tila Write-Log "Vaihe 2: Laitteen tilan lataaminen..." "INFO" $notUptodateCsv = Get-ChildItem -Path $aggregationPath -Filter "*NotUptodate*.csv" -ErrorAction SilentlyContinue | Where-Object { $_. Nimi -notlike "*Buckets*" } | Sort-Object LastWriteTime -Laskeva | Select-Object -Ensimmäinen 1 jos (ei $notUptodateCsv -eikä $DryRun) { Write-Log "Koostetietoja ei löytynyt. Odotetaan..." "VAROITA" Start-Sleep -Seconds ($PollIntervalMinutes * 60) Jatkaa } $notUpdatedDevices = jos ($notUptodateCsv) { Import-Csv $notUptodateCsv.FullName } muu { @() } Write-Log "Laitteita ei päivitetty: $($notUpdatedDevices.Count)" "INFO" $notUpdatedIndexes = Get-NotUpdatedIndexes -Devices $notUpdatedDevices # Vaihe 3: Päivitä laitehistoria (seuranta isäntänimen mukaan) Write-Log "Vaihe 3: Laitehistorian päivittäminen..." "INFO" Update-DeviceHistory -CurrentDevices $notUpdatedDevices -DeviceHistory $deviceHistory Save-DeviceHistory -Historia-$deviceHistory # Vaihe 4: Tarkista estetyt säilöt (tavoittamattomat laitteet) $existingBlockedCount = $blockedBuckets.Määrä Write-Log "Vaihe 4: Estettyjen säilöjen tarkistaminen (ping-laitteet odotusajan jälkeen)..." "INFO" jos ($existingBlockedCount -gt 0) { Write-Log "Tällä hetkellä estetyt säilöt aiemmista juoksuista: $existingBlockedCount" "TIEDOT" } jos ($adminApproved.Määrä -gt 0) { Write-Log "Hallinta hyväksyttyjä säilöjä (ei estetä uudelleen): $($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 estetty $blockedBuckets Write-Log "Äskettäin estetyt säilöt (tämä iteraatio): $($newlyBlocked.Count)" "ESTETTY" } # Vaihe 4b: Poista säilöjen esto automaattisesti, kun laitteet ovat päivittyneet $autoUnblocked = Update-AutoUnblockedBuckets -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -NotUpdatedDevices $notUpdatedDevices -ReportBasePath $ReportBasePath -NotUpdatedIndexes $notUpdatedIndexes -LogSampleSize $DeviceLogSampleSize jos ($autoUnblocked.Määrä -gt 0) { Save-BlockedBuckets -Estetyt $blockedBuckets Write-Log "Automaattisesti estottomat säilöt (laitteet päivitetty): $($autoUnblocked.Count)" "OK" } # Vaihe 5: Jäljellä olevien vaatimukset täyttävistä laitteista laskeminen $eligibleCount = 0 foreach ($device in $notUpdatedDevices) { $bucketKey = Get-BucketKey $device jos (ei $blockedBuckets.Sisältää($bucketKey)) { $eligibleCount++ } } Write-Log "Vaatimukset täyttävät laitteet jäljellä: $eligibleCount" "TIEDOT" Write-Log "Estetyt säilöt: $($blockedBuckets.Count)" "INFO" # Vaihe 6: Tarkista valmistuminen jos ($eligibleCount -eq 0) { Write-Log "ROLLOUT COMPLETE - Kaikki vaatimukset täyttävät laitteet päivitetty!" "OK" $rolloutState.Status = "Valmis" $rolloutState.CompletedAt = Get-Date -Format "yyyy-MM-dd HH:mm:mm:ss" Save-RolloutState -State $rolloutState Tauko } # Vaihe 6: Luo ja ota käyttöön seuraava aalto Write-Log "Vaihe 6: Käyttöönottoaallon luominen..." "INFO" $waveDevices = New-RolloutWave -AggregationPath $aggregationPath -BlockedBuckets $blockedBuckets -RolloutState $rolloutState -AllowedHostnames $allowedHostnames -ExcludedHostnames $excludedHostnames # Tarkista, onko meillä laitteita, jotka on otettava käyttöön ($waveDevices voi olla $null, tyhjä tai todellisten laitteiden kanssa) $hasDevices = $waveDevices -ja @($waveDevices | Where-Object { $_ }). Määrä -gt 0 jos ($hasDevices) { # Vain lisäysaallon numero, kun meillä on laitteita otettavana käyttöön $rolloutState.CurrentWave++ Write-Log "Wave $($rolloutState.CurrentWave): $(@($waveDevices). Laske) laitteet" "WAVE" # Ota ryhmäkäytäntöobjekti käyttöön käyttämällä rajattua funktiota $gpoName = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $securityGroup = "${WavePrefix}-Wave$($rolloutState.CurrentWave)" $hostnames = @($waveDevices | ForEach-Object { jos ($_. Hostname) { $_. Hostname } elseif ($_. HostName) { $_. HostName } muu { $null } } | Where-Object { $_ }) # Tallenna isäntänimitiedosto viite- tai valvontatiedostoa varten $hostnamesFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_Hostnames.txt" $hostnames | Out-File $hostnamesFile -KOODaus UTF8 # Vahvista, että meillä on isäntänimiä, jotka voit ottaa käyttöön jos ($hostnames. Määrä -eq 0) { Write-Log "Wave $($rolloutState.CurrentWave) -laitteesta ei löydy kelvollisia isäntänimiä – laitteista saattaa puuttua Hostname-ominaisuus" "WARN" Write-Log "Käyttöönoton ohittaminen tälle aallolle – tarkista laitteen tiedot" "WARN" Odota vielä ennen seuraavaa iterointia jos (ei $DryRun) { Write-Log "Nukkuu $PollIntervalMinutes minuuttia ennen uudelleenyritystä..." "INFO" Start-Sleep -Seconds ($PollIntervalMinutes * 60) } Jatkaa } Write-Log "Käyttöönotto kohteeseen $($hostnames. Count) hostnames in Wave $($rolloutState.CurrentWave)" "INFO" # Ota käyttöön joko WinCS- tai GPO-menetelmällä, joka perustuu -UseWinCS-parametriin jos ($UseWinCS) { # WinCS-menetelmä: Luo ryhmäkäytäntöobjekti, jossa on ajoitettu tehtävä, jotta voit suorittaa WinCsFlags.exe SYSTEM-muodossa kussakin päätepisteessä Write-Log "WinCS-käyttöönottotavan käyttäminen (avain: $WinCSKey)" "WAVE" $wincsResult = Deploy-WinCSForWave -WaveHostnames $hostnames ' -WinCSKey $WinCSKey ' -WavePrefix $WavePrefix ' -WaveNumber $rolloutState.CurrentWave ' -TargetOU $TargetOU ' -DryRun:$DryRun jos (ei $wincsResult.Success) { Write-Log "WinCS-käyttöönotossa oli virheitä – käytetty: $($wincsResult.Applied), Epäonnistui: $($wincsResult.Failed)" "WARN" } muu { Write-Log "WinCS-käyttöönotto onnistui – käytetty: $($wincsResult.Applied), Ohitettu: $($wincsResult.Skipped)" "OK" } # Tallenna WinCS-tulokset valvontaa varten $wincsResultFile = Join-Path $stateDir "Wave$($rolloutState.CurrentWave)_WinCS_Results.json" $wincsResult | ConvertTo-Json -Syvyys 5 | Out-File $wincsResultFile -KOODaus UTF8 } muu { # Ryhmäkäytäntöobjektimenetelmä: Ryhmäkäytäntöobjektin luominen AvailableUpdatesPolicy-rekisteriasetuksella $gpoResult = Deploy-GPOForWave -GPOName $gpoName -TargetOU $TargetOU -SecurityGroupName $securityGroup -WaveHostnames $hostnames -DryRun:$DryRun jos (ei $gpoResult) { Write-Log "Ryhmäkäytäntöobjektin käyttöönotto epäonnistui – yrittää uudelleen seuraavaa iterointia" "VIRHE" } } # Tallenna aalto tilassa $waveRecord = @{ WaveNumber = $rolloutState.CurrentWave StartedAt = Get-Date -Format "yy-MM-dd HH:mm:ss" DeviceCount = @($waveDevices). Laskea Laitteet = @($waveDevices | ForEach-Object { @{ Hostname = if ($_. Hostname) { $_. Hostname } elseif ($_. HostName) { $_. HostName } muu { $null } BucketKey = Get-BucketKey $_ } }) } # Varmista, että WaveHistory on aina matriisi ennen liittämistä (estää hajautetun yhdistämisen ongelmat) $rolloutState.WaveHistory = @($rolloutState.WaveHistory) + @($waveRecord) $rolloutState.TotalDevicesTargeted += @($waveDevices). Laskea Save-RolloutState -State $rolloutState Write-Log "Wave $($rolloutState.CurrentWave) otettu käyttöön. Odotetaan $PollIntervalMinutes minuuttia..." "OK" } muu { # Näytä päivityksiä odottavien käyttöön otettujen laitteiden tila Write-Log "" "TIEDOT" Write-Log "========== KAIKKI LAITTEET KÄYTÖSSÄ – TILAN ==========" "TIEDOT" # Hae kaikki käyttöön otetut laitteet aaltohistoriasta $allDeployedLookup = @{} foreach ($wave kohteessa $rolloutState.WaveHistory) { ($device $wave. Laitteet) { jos ($device. Hostname) { $allDeployedLookup[$device. Isäntänimi] = @{ Hostname = $device. Hostname BucketKey = $device. BucketKey DeployedAt = $wave. StartedAt WaveNumber = $wave. WaveNumber } } } } $allDeployedDevices = @($allDeployedLookup.Values) jos ($allDeployedDevices.Määrä -gt 0) { # Etsi, mitkä käyttöön otetut laitteet odottavat edelleen (Ei vanhentunut-luettelossa) $stillPendingCount = 0 $noLongerPendingCount = 0 $pendingSample = @() foreach ($deployed in $allDeployedDevices) { if ($notUpdatedIndexes.HostSet.Contains($deployed. Hostname)) { $stillPendingCount++ if ($pendingSample.Count -lt $DeviceLogSampleSize) { $pendingSample += $deployed. Hostname } } muu { $noLongerPendingCount++ } } # Nouda todelliset päivitetyt määrät koostuksesta – erottele tapahtuma 1808 vs UEFICA2023Status $summaryCsv = Get-ChildItem -Path $aggregationPath -Filter "*Summary*.csv" | Sort-Object LastWriteTime -Laskeva | Select-Object -Ensimmäinen 1 $actualUpdated = 0 $totalDevicesFromSummary = 0 $event 1808Laske = 0 $uefiStatusUpdated = 0 $needsRebootSample = @() jos ($summaryCsv) { $summary = Import-Csv $summaryCsv.FullName | Select-Object -Ensimmäinen 1 jos ($summary. Päivitetty) { $actualUpdated = [int]$summary. Päivitetty } jos ($summary. TotalDevices) { $totalDevicesFromSummary = [int]$summary. TotalDevices } } # Laske nopeus aaltohistoriasta (laitteet päivittyvät päivässä) $devicesPerDay = 0 jos ($rolloutState.StartedAt -ja $actualUpdated -gt 0) { $startDate = [datetime]::P arse($rolloutState.StartedAt) $daysElapsed = ((Get-Date) – $startDate). TotalDays jos ($daysElapsed -gt 0) { $devicesPerDay = $actualUpdated / $daysElapsed } } # Tallenna käyttöönottoyhteenveto viikonlopputietoisten ennusteiden avulla # Käytä koostimen NotUptodate-määrää (ei sisällä SB OFF -laitteita) yhdenmukaisuuden varmistamiseksi $notUpdatedCount = jos ($summary ja $summary. NotUptodate) { [int]$summary. NotUptodate } muu { $totalDevicesFromSummary - $actualUpdated } Save-RolloutSummary -State $rolloutState ' -TotalDevices $totalDevicesFromSummary ' -UpdatedDevices $actualUpdated ' -NotUpdatedDevices $notUpdatedCount ' -DevicesPerDay-$devicesPerDay # Tarkista UEFICA2023Status=Updated but no Event 1808 (needs reboot) -sovelluksen raakatiedot laitteille, joissa on UEFICA2023Status=Päivitetty, mutta ei tapahtumaa 1808 (uudelleenkäynnistystä tarvitaan) $dataFiles = Get-ChildItem -Path $AggregationInputPath -Filter "*.json" -ErrorAction SilentlyContinue $totalDataFiles = @($dataFiles). Laskea $batchSize = [Matematiikka]::Maks.500, $ProcessingBatchSize) jos ($LargeScaleMode) { $batchSize = [Matematiikka]::Maks.(2000, $ProcessingBatchSize) }
if ($totalDataFiles -gt 0) { kohteelle ($idx = 0; $idx -lt $totalDataFiles; $idx += $batchSize) { $end = [Matematiikka]::Min($idx + $batchSize - 1, $totalDataFiles - 1) $batchFiles = $dataFiles[$idx.. $end]
foreach ($file in $batchFiles) { kokeile { $deviceData = Get-Content $file. FullName -Raw | ConvertFrom-Json $hostname = $deviceData.Hostname jos (ei $hostname) { continue } $has 1808 = [int]$deviceData.Event1808Count -gt 0 $hasUefiUpdated = $deviceData.UEFICA2023Status -eq "Päivitetty" jos ($has 1808) { $event 1808Laske++ } elseif ($hasUefiUpdated) { $uefiStatusUpdated++ if ($needsRebootSample.Count -lt $DeviceLogSampleSize) { $needsRebootSample += $hostname } } } saalis { } }
Save-ProcessingCheckpoint -Stage "RebootStatusScan" -Processed ($end + 1) -Total $totalDataFiles -Metrics @{ Event1808Count = $event 1808Laske UEFIUpdatedAwaitingReboot = $uefiStatusUpdated } } } Write-Log "Käyttöön otettu yhteensä: $($allDeployedDevices.Count)" "INFO" Write-Log "Päivitetty (tapahtuma 1808 vahvistettu): $event 1808Laske" "OK" jos ($uefiStatusUpdated -gt 0) { Write-Log "Päivitetty (UEFICA2023Status=Päivitetty, odottaa uudelleenkäynnistystä): $uefiStatusUpdated" "OK" $rebootSuffix = jos ($uefiStatusUpdated -gt $DeviceLogSampleSize) { " ... (+$($uefiStatusUpdated - $DeviceLogSampleSize) lisää)" } muu { "" } Write-Log " Laitteet, jotka tarvitsevat uudelleenkäynnistyksen tapahtumaa 1808 varten (esimerkki): $($needsRebootSample -join ', ')$rebootSuffix" "INFO" Write-Log " Nämä laitteet raportoivat tapahtumasta 1808 seuraavan uudelleenkäynnistyksen jälkeen" "TIEDOT" } Write-Log "Ei enää odottavaa: $noLongerPendingCount (sisältää SecureBoot OFF, puuttuvat laitteet)" "INFO" Write-Log "Odottaa tilaa: $stillPendingCount" "INFO" jos ($stillPendingCount -gt 0) { $pendingSuffix = jos ($stillPendingCount -gt $DeviceLogSampleSize) { " ... (+$($stillPendingCount - $DeviceLogSampleSize) lisää)" } muuta { "" } Write-Log "Odottavat laitteet (esimerkki): $($pendingSample -join ', ')$pendingSuffix" "WARN" } } muu { Write-Log "Laitteita ei ole vielä otettu käyttöön" "INFO" } Write-Log "================================================================" "TIEDOT" Write-Log "" "TIEDOT" } # Odota ennen seuraavaa iteraatiota jos (ei $DryRun) { Write-Log "Nukkuu $PollIntervalMinutes minuuttia..." "INFO" Start-Sleep -Seconds ($PollIntervalMinutes * 60) } muu { Write-Log "[DRYRUN] Odottaa $PollIntervalMinutes minuuttia" "TIEDOT" break # Exit after one iteration in dry run } }
# ============================================================================ # LOPULLINEN YHTEENVETO # ============================================================================
Write-Host "" Write-Host ("=" * 80) -EdustaVäri vihreä Write-Host " ROLLOUT ORCHESTRATOR SUMMARY" -ForegroundColor Green Write-Host ("=" * 80) -EdustaVäri vihreä 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 "Kohdennetut laitteet: $($finalState.TotalDevicesTargeted)" Write-Host "Estetyt säilöt: $($finalBlocked.Count)" -ForegroundColor $(if ($finalBlocked.Count -gt 0) { "Red" } muu { "Vihreä" }) Write-Host "Seuratut laitteet: $($deviceHistory.Count)" -EtualallaColor Harmaa Write-Host ""
if ($finalBlocked.Count -gt 0) { Write-Host "ESTETYT SÄILÖT (edellytä manuaalista tarkistusta):" -Edustaväri Punainen foreach ($key $finalBlocked.Keysissä) { $info = $finalBlocked[$key] Write-Host " - $key" -EtualallaColor Punainen Write-Host " Syy: $($info. Syy)" -ForegroundColor Harmaa } Write-Host "" Write-Host "Estettyjen säilöjen tiedosto: $blockedBucketsPath" -EtualallaColor Keltainen }
Write-Host "" Write-Host "State files:" -ForegroundColor Cyan Write-Host "Käyttöönottotila: $rolloutStatePath" Write-Host " Estetyt säilöt: $blockedBucketsPath" Write-Host "Laitehistoria: $deviceHistoryPath" Write-Host ""