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

​​​​​​​

Tarvitsetko lisäohjeita?

Haluatko lisää vaihtoehtoja?

Tutustu tilausetuihin, selaa harjoituskursseja, opi suojaamaan laitteesi ja paljon muuta.