Note
IMPORTANT This article containing this sample script has been retired. Starting with the Windows updates released on and after May 12, 2026, the sample script is located in the %systemroot%\SecureBoot\ExampleRolloutScripts folder on your device.
Copy and paste this sample script and modify as needed for your environment:
Note
- `<#`
.SYNOPSIS
Deploys the Secure Boot Rollout Orchestrator as a Windows Scheduled Task.
- `.DESCRIPTION`
Creates a scheduled task that runs the orchestrator continuously in the background.
The task runs with bypass execution policy so no security prompts appear.
The orchestrator will:
- Poll for device updates on the specified interval
- Automatically generate waves and deploy GPOs
- Continue until all eligible devices are updated
Monitor progress using: Get-SecureBootRolloutStatus.ps1
- `.PARAMETER AggregationInputPath`
UNC path to JSON device data (from detection GPO)
- `.PARAMETER ReportBasePath`
Local path for reports and state files
- `.PARAMETER TargetOU`
OU to link GPOs (optional - defaults to domain root)
- `.PARAMETER PollIntervalMinutes`
Minutes between status checks. Default: 30
- `.PARAMETER UseWinCS`
Use WinCS (Windows Configuration System) instead of AvailableUpdatesPolicy GPO.
When enabled, deploys WinCsFlags.exe scheduled task to endpoints instead of registry GPO.
- `.PARAMETER WinCSKey`
The WinCS key for Secure Boot configuration. Default: F33E0C8E002
- `.PARAMETER ServiceAccount`
Account to run the task. Default: SYSTEM
For domain operations, use a domain admin service account.
- `.PARAMETER AllowListPath`
Path to a file containing hostnames to ALLOW for rollout (targeted/pilot rollout).
Supports .txt (one hostname per line) or .csv (with Hostname/ComputerName/Name column).
When specified, ONLY these devices will be included in rollout.
- `.PARAMETER AllowADGroup`
Name of an AD security group containing computer accounts to ALLOW.
Example: "SecureBoot-Pilot-Computers"
- `.PARAMETER ExclusionListPath`
Path to a file containing hostnames to EXCLUDE from rollout (VIP/executive devices).
Supports .txt (one hostname per line) or .csv (with Hostname/ComputerName/Name column).
- `.PARAMETER ExcludeADGroup`
Name of an AD security group containing computer accounts to exclude.
Example: "VIP-Computers"
- `.PARAMETER ScriptPath`
Path to the orchestrator script. Default: Same folder as this script.
- `.PARAMETER Uninstall`
Remove the scheduled task
- `.EXAMPLE`
.\\Deploy-OrchestratorTask.ps1 -AggregationInputPath "\\\\server\\SecureBootData$" -ReportBasePath "C:\\SecureBootReports" -ServiceAccount "DOMAIN\\svc\_secureboot"
- `.EXAMPLE`
.\\Deploy-OrchestratorTask.ps1 -AggregationInputPath "\\\\server\\SecureBootData$" -ReportBasePath "C:\\SecureBootReports"
- `.EXAMPLE`
# Deploy using WinCS method instead of AvailableUpdatesPolicy
.\\Deploy-OrchestratorTask.ps1 -AggregationInputPath "\\\\server\\SecureBootData$" -ReportBasePath "C:\\SecureBootReports" -UseWinCS
- `.EXAMPLE`
.\\Deploy-OrchestratorTask.ps1 -Uninstall
#>
- `[CmdletBinding()]`
param(
\[Parameter(Mandatory = $false)\]
\[string\]$AggregationInputPath,
\[Parameter(Mandatory = $false)\]
\[string\]$ReportBasePath,
\[Parameter(Mandatory = $false)\]
\[string\]$TargetOU,
\[Parameter(Mandatory = $false)\]
\[int\]$PollIntervalMinutes = 30,
\[Parameter(Mandatory = $false)\]
\[switch\]$UseWinCS,
\[Parameter(Mandatory = $false)\]
\[string\]$WinCSKey = "F33E0C8E002",
\[Parameter(Mandatory = $false)\]
\[string\]$ServiceAccount = "SYSTEM",
\[Parameter(Mandatory = $false)\]
\[string\]$AllowListPath,
\[Parameter(Mandatory = $false)\]
\[string\]$AllowADGroup,
\[Parameter(Mandatory = $false)\]
\[string\]$ExclusionListPath,
\[Parameter(Mandatory = $false)\]
\[string\]$ExcludeADGroup,
\[Parameter(Mandatory = $false)\]
\[string\]$ScriptPath,
\[Parameter(Mandatory = $false)\]
\[switch\]$Uninstall
)
- `$ErrorActionPreference = "Stop"`
$TaskName = "SecureBoot-Rollout-Orchestrator"
$DownloadUrl = "https://aka.ms/getsecureboot"
$DownloadSubPage = "Deployment and Monitoring Samples"
- `# ==================================================================`
# DEPENDENCY VALIDATION
`# ==================================================================`
- `function Test-ScriptDependencies {`
<#
.SYNOPSIS
Validates that all required scripts are present.
.DESCRIPTION
Checks for required script dependencies and provides download instructions if missing.
#>
param(
\[Parameter(Mandatory = $true)\]
\[string\]$ScriptDirectory,
\[Parameter(Mandatory = $true)\]
\[string\[\]\]$RequiredScripts
)
$missingScripts = @()
foreach ($script in $RequiredScripts) {
$scriptPath = Join-Path $ScriptDirectory $script
if (-not (Test-Path $scriptPath)) {
$missingScripts += $script
}
}
if ($missingScripts.Count -gt 0) {
Write-Host ""
Write-Host ("=" \* 70) -ForegroundColor Red
Write-Host " MISSING DEPENDENCIES" -ForegroundColor Red
Write-Host ("=" \* 70) -ForegroundColor Red
Write-Host ""
Write-Host "The following required scripts were not found:" -ForegroundColor Yellow
foreach ($script in $missingScripts) {
Write-Host " - $script" -ForegroundColor White
}
Write-Host ""
Write-Host "Please download the latest scripts from:" -ForegroundColor Cyan
Write-Host " URL: $DownloadUrl" -ForegroundColor White
Write-Host " Navigate to: '$DownloadSubPage'" -ForegroundColor White
Write-Host ""
Write-Host "Extract all scripts to the same directory and run again." -ForegroundColor Yellow
Write-Host ""
return $false
}
return $true
}
- `# Required scripts for orchestrator deployment`
$requiredScripts = @(
"Start-SecureBootRolloutOrchestrator.ps1",
"Aggregate-SecureBootData.ps1",
"Deploy-GPO-SecureBootCollection.ps1",
"Detect-SecureBootCertUpdateStatus.ps1",
"Get-SecureBootRolloutStatus.ps1",
"Enable-SecureBootUpdateTask.ps1"
)
- `if (-not (Test-ScriptDependencies -ScriptDirectory $PSScriptRoot -RequiredScripts $requiredScripts)) {`
exit 1
}
- `# ==================================================================`
# UNINSTALL
`# ==================================================================`
- `if ($Uninstall) {`
Write-Host ""
Write-Host "Removing scheduled task: $TaskName" -ForegroundColor Yellow
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existingTask) {
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Host "Task removed successfully." -ForegroundColor Green
} else {
Write-Host "Task not found." -ForegroundColor Gray
}
exit 0
}
- `# ==================================================================`
# VALIDATION
`# ==================================================================`
- `if (-not $AggregationInputPath -or -not $ReportBasePath) {`
Write-Host "ERROR: -AggregationInputPath and -ReportBasePath are required." -ForegroundColor Red
Write-Host ""
Write-Host "Example:" -ForegroundColor Yellow
Write-Host ' .\\Deploy-OrchestratorTask.ps1 -AggregationInputPath "\\\\server\\SecureBootData$" -ReportBasePath "C:\\SecureBootReports"'
exit 1
}
- `# Find orchestrator script`
if (-not $ScriptPath) {
$ScriptPath = Join-Path $PSScriptRoot "Start-SecureBootRolloutOrchestrator.ps1"
}
- `if (-not (Test-Path $ScriptPath)) {`
Write-Host "ERROR: Orchestrator script not found: $ScriptPath" -ForegroundColor Red
exit 1
}
- `# Find aggregation script (needed by orchestrator)`
$aggregateScript = Join-Path $PSScriptRoot "Aggregate-SecureBootData.ps1"
if (-not (Test-Path $aggregateScript)) {
Write-Host "WARNING: Aggregate-SecureBootData.ps1 not found in script directory" -ForegroundColor Yellow
Write-Host " Orchestrator may fail if it cannot find this script." -ForegroundColor Yellow
}
- `Write-Host ""`
Write-Host ("=" \* 70) -ForegroundColor Cyan
Write-Host " Secure Boot Rollout Orchestrator - Task Deployment" -ForegroundColor Cyan
Write-Host ("=" \* 70) -ForegroundColor Cyan
Write-Host ""
- `# For display, show relative paths (script names only)`
$displayScriptPath = Split-Path $ScriptPath -Leaf
- `Write-Host "Configuration:" -ForegroundColor Yellow`
Write-Host " Task Name: $TaskName"
Write-Host " Orchestrator: $displayScriptPath"
Write-Host " Input Path: $AggregationInputPath"
Write-Host " Report Path: $ReportBasePath"
Write-Host " Target OU: $(if ($TargetOU) { $TargetOU } else { '(domain root)' })"
Write-Host " Poll Interval: $PollIntervalMinutes minutes"
Write-Host " Service Account: $ServiceAccount"
Write-Host " Deployment Method: $(if ($UseWinCS) { "WinCS (WinCsFlags.exe)" } else { "AvailableUpdatesPolicy (GPO)" })"
if ($UseWinCS) {
Write-Host " WinCS Key: $WinCSKey"
}
Write-Host ""
- `# ==================================================================`
# GPO DETECTION - AUTO-DEPLOY IF MISSING
`# ==================================================================`
- `$CollectionGPOName = "SecureBoot-EventCollection"`
$deployGpoScript = Join-Path $PSScriptRoot "Deploy-GPO-SecureBootCollection.ps1"
- `# Check if GroupPolicy module is available`
if (Get-Module -ListAvailable -Name GroupPolicy) {
Import-Module GroupPolicy -ErrorAction SilentlyContinue
Write-Host "Checking for Detection GPO..." -ForegroundColor Yellow
try {
# Get the domain from the AggregationInputPath (e.g., \\\\domain\\share)
$domainFromPath = if ($AggregationInputPath -match '^\\\\\\\\(\[^\\\\\]+)\\\\') {
$matches\[1\]
} else {
$env:USERDNSDOMAIN
}
# Check if GPO exists
$existingGpo = Get-GPO -Name $CollectionGPOName -ErrorAction SilentlyContinue
if ($existingGpo) {
Write-Host " Detection GPO found: $CollectionGPOName" -ForegroundColor Green
} else {
Write-Host ""
Write-Host ("=" \* 70) -ForegroundColor Yellow
Write-Host " DETECTION GPO NOT FOUND" -ForegroundColor Yellow
Write-Host ("=" \* 70) -ForegroundColor Yellow
Write-Host ""
Write-Host "The detection GPO '$CollectionGPOName' was not found." -ForegroundColor Yellow
Write-Host "This GPO is required to collect device status data." -ForegroundColor Yellow
Write-Host ""
# Ask user if they want to deploy the GPO now
Write-Host "Would you like to deploy the Detection GPO now? (Y/N)" -ForegroundColor Cyan
$response = Read-Host
if ($response -match '^\[Yy\]') {
Write-Host ""
Write-Host "Launching GPO Deployment..." -ForegroundColor Cyan
Write-Host ""
# Build parameters for GPO deployment
$gpoParams = @{
DomainName = $domainFromPath
CollectionSharePath = $AggregationInputPath
ScriptSourcePath = Join-Path $PSScriptRoot "Detect-SecureBootCertUpdateStatus.ps1"
}
if ($TargetOU) {
$gpoParams.OUPath = $TargetOU
} else {
# Use AutoDetectOU to let user select
$gpoParams.AutoDetectOU = $true
}
# Run the GPO deployment script
& $deployGpoScript @gpoParams
if ($LASTEXITCODE -ne 0) {
Write-Host "GPO deployment may have encountered issues. Review the output above." -ForegroundColor Yellow
Write-Host "You can continue with orchestrator deployment or press Ctrl+C to abort." -ForegroundColor Yellow
Write-Host ""
Read-Host "Press Enter to continue"
} else {
Write-Host ""
Write-Host "Detection GPO deployed successfully!" -ForegroundColor Green
Write-Host ""
}
} else {
Write-Host ""
Write-Host "Skipping GPO deployment. The orchestrator will not receive device data" -ForegroundColor Yellow
Write-Host "until the Detection GPO is deployed manually." -ForegroundColor Yellow
Write-Host ""
Write-Host "To deploy the Detection GPO later, run:" -ForegroundColor Cyan
Write-Host " .\\Deploy-GPO-SecureBootCollection.ps1 -DomainName \`"$domainFromPath\`" -AutoDetectOU" -ForegroundColor White
Write-Host ""
}
}
} catch {
Write-Host " Unable to check for GPO: $($\_.Exception.Message)" -ForegroundColor Yellow
Write-Host " Continuing with orchestrator deployment..." -ForegroundColor Gray
}
} else {
Write-Host " GroupPolicy module not available - skipping GPO check" -ForegroundColor Gray
Write-Host " Ensure Detection GPO is deployed separately." -ForegroundColor Gray
}
- `Write-Host ""`
- `# ==================================================================`
# BUILD ARGUMENTS
`# ==================================================================`
- `$arguments = @(`
"-NoProfile"
"-ExecutionPolicy Bypass"
"-File \`"$ScriptPath\`""
"-AggregationInputPath \`"$AggregationInputPath\`""
"-ReportBasePath \`"$ReportBasePath\`""
"-PollIntervalMinutes $PollIntervalMinutes"
)
- `if ($TargetOU) {`
$arguments += "-TargetOU \`"$TargetOU\`""
}
- `if ($UseWinCS) {`
$arguments += "-UseWinCS"
$arguments += "-WinCSKey \`"$WinCSKey\`""
}
- `if ($AllowListPath) {`
$arguments += "-AllowListPath \`"$AllowListPath\`""
}
- `if ($AllowADGroup) {`
$arguments += "-AllowADGroup \`"$AllowADGroup\`""
}
- `if ($ExclusionListPath) {`
$arguments += "-ExclusionListPath \`"$ExclusionListPath\`""
}
- `if ($ExcludeADGroup) {`
$arguments += "-ExcludeADGroup \`"$ExcludeADGroup\`""
}
- `$argumentString = $arguments -join " "`
- `# Don't display raw arguments with full paths - it's confusing for published scripts`
# The task will use the full path internally
-
`# ==================================================================`
# CREATE SCHEDULED TASK
`# ==================================================================`
- `# Check for existing task`
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existingTask) {
Write-Host "Task already exists. Updating..." -ForegroundColor Yellow
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
}
- `# Create task action`
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $argumentString -WorkingDirectory $PSScriptRoot
- `# Create trigger - run once, immediately (orchestrator loops internally)`
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1)
- `# Create principal`
if ($ServiceAccount -eq "SYSTEM") {
$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
} else {
# Normalize local account format: COMPUTERNAME\\User -> User
# Register-ScheduledTask does not accept COMPUTERNAME\\User or .\\User for local accounts
$taskUser = $ServiceAccount
if ($ServiceAccount -match "^$(\[regex\]::Escape($env:COMPUTERNAME))\\\\(.+)$") {
$taskUser = $Matches\[1\]
Write-Host " Note: Local account detected, using '$taskUser' for task registration" -ForegroundColor Gray
}
- ` # Prompt for password`
Write-Host "Enter password for $ServiceAccount" -ForegroundColor Yellow
$cred = Get-Credential -UserName $taskUser -Message "Service account credentials for scheduled task"
if (-not $cred) {
Write-Host "Credential entry cancelled. Aborting." -ForegroundColor Red
exit 1
}
$principal = New-ScheduledTaskPrincipal -UserId $taskUser -LogonType Password -RunLevel Highest
}
- `# Task settings`
$settings = New-ScheduledTaskSettingsSet \`
-AllowStartIfOnBatteries \`
-DontStopIfGoingOnBatteries \`
-StartWhenAvailable \`
-RunOnlyIfNetworkAvailable \`
-RestartCount 3 \`
-RestartInterval (New-TimeSpan -Minutes 5) \`
-ExecutionTimeLimit (New-TimeSpan -Days 30) # Allow long-running
- `# Register task`
try {
if ($ServiceAccount -eq "SYSTEM") {
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Secure Boot Certificate Rollout - Automated GPO deployment"
} else {
# Use New-ScheduledTask to embed the principal (with RunLevel Highest) into a task object,
# then register with -InputObject + -User/-Password (separate parameter set from -Principal)
$taskDefinition = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings
$taskDefinition.Description = "Secure Boot Certificate Rollout - Automated GPO deployment"
# Note: Register-ScheduledTask requires plain text password - no SecureString overload exists
$netCred = $cred.GetNetworkCredential()
Register-ScheduledTask -TaskName $TaskName -InputObject $taskDefinition -User $taskUser -Password $netCred.Password -ErrorAction Stop
$netCred = $null
}
Write-Host "Scheduled task created successfully!" -ForegroundColor Green
} catch {
Write-Host "Failed to create scheduled task: $($\_.Exception.Message)" -ForegroundColor Red
exit 1
}
- `# ==================================================================`
# CREATE STATUS SHORTCUT
`# ==================================================================`
- `$statusScript = Join-Path $PSScriptRoot "Get-SecureBootRolloutStatus.ps1"`
if (Test-Path $statusScript) {
Write-Host ""
Write-Host "To check rollout status, run:" -ForegroundColor Yellow
Write-Host " .\\Get-SecureBootRolloutStatus.ps1 -ReportBasePath \`"$ReportBasePath\`"" -ForegroundColor Cyan
}
- `# ==================================================================`
# OUTPUT
`# ==================================================================`
- `Write-Host ""`
Write-Host ("=" \* 70) -ForegroundColor Green
Write-Host " DEPLOYMENT COMPLETE" -ForegroundColor Green
Write-Host ("=" \* 70) -ForegroundColor Green
Write-Host ""
Write-Host "The orchestrator will start in approximately 1 minute." -ForegroundColor White
Write-Host ""
Write-Host "MONITORING:" -ForegroundColor Yellow
Write-Host " View task status: Get-ScheduledTask -TaskName '$TaskName' | Select State"
Write-Host " View task log: Get-Content '$ReportBasePath\\RolloutState\\Orchestrator\_$(Get-Date -Format 'yyyyMMdd').log' -Tail 50"
Write-Host " View rollout state: Get-Content '$ReportBasePath\\RolloutState\\RolloutState.json' | ConvertFrom-Json"
Write-Host " View dashboard: Start '$ReportBasePath\\Aggregation\_\*\\SecureBoot\_Dashboard\*.html'"
Write-Host " Quick status: .\\Get-SecureBootRolloutStatus.ps1 -ReportBasePath '$ReportBasePath'"
Write-Host ""
Write-Host "MANAGEMENT:" -ForegroundColor Yellow
Write-Host " Start manually: Start-ScheduledTask -TaskName '$TaskName'"
Write-Host " Stop: Stop-ScheduledTask -TaskName '$TaskName'"
Write-Host " Remove: .\\Deploy-OrchestratorTask.ps1 -Uninstall"
Write-Host ""