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
GPO Deployment Script for Secure Boot Event Collection
Creates and links a GPO to deploy the collection script as a scheduled task
- `.DESCRIPTION`
This script automates the deployment of Secure Boot event collection via Group Policy.
It creates a GPO with:
- A scheduled task that runs the collection script daily
- Proper permissions for writing to the central share
- WMI filters for targeting specific OS versions
- `.PARAMETER GPOName`
Name for the new GPO
- `.PARAMETER DomainName`
Target domain FQDN
- `.PARAMETER OUPath`
Distinguished Name of the OU(s) to link the GPO to.
Accepts multiple OUs as an array. Not required if -AutoDetectOU is specified.
- `.PARAMETER AutoDetectOU`
Switch to interactively list and select OUs from Active Directory.
When specified, -OUPath is optional.
- `.PARAMETER CollectionSharePath`
UNC path where collection results will be stored
- `.PARAMETER ScriptSourcePath`
Path where the collection script is stored (will be copied to SYSVOL)
- `.PARAMETER RandomDelayHours`
Number of hours to randomly spread script execution across endpoints.
This prevents all machines from writing to the share simultaneously.
Default: 4 hours. Valid range: 1-24 hours.
Recommended values:
- 1-10K devices: 4 hours (default)
- 10K-50K devices: 8 hours
- 50K+ devices: 12-24 hours
- `.EXAMPLE`
.\\Deploy-GPO-SecureBootCollection.ps1 -DomainName "contoso.com" -OUPath "OU=Workstations,DC=contoso,DC=com"
- `.EXAMPLE`
.\\Deploy-GPO-SecureBootCollection.ps1 -DomainName "contoso.com" -OUPath "OU=Workstations,DC=contoso,DC=com" -RandomDelayHours 8
- `.EXAMPLE`
.\\Deploy-GPO-SecureBootCollection.ps1 -DomainName "contoso.com" -AutoDetectOU
Lists all OUs in the domain and prompts for selection.
- `.EXAMPLE`
.\\Deploy-GPO-SecureBootCollection.ps1 -DomainName "contoso.com" -OUPath @("OU=Workstations,DC=contoso,DC=com", "OU=Laptops,DC=contoso,DC=com")
Links GPO to multiple OUs in a single run.
- `.NOTES`
Requires: Active Directory PowerShell module, Group Policy module
Must be run with Domain Admin or delegated GPO creation rights
#>
- `[CmdletBinding()]`
param(
\[Parameter(Mandatory = $false)\]
\[string\]$GPOName = "SecureBoot-EventCollection",
\[Parameter(Mandatory = $false)\]
\[string\]$DomainName,
\[Parameter(Mandatory = $false)\]
\[string\[\]\]$OUPath,
\[Parameter(Mandatory = $false)\]
\[switch\]$AutoDetectOU,
\[Parameter(Mandatory = $false)\]
\[string\]$CollectionSharePath = "\\\\$DomainName\\NETLOGON\\SecureBootLogs",
\[Parameter(Mandatory = $false)\]
\[string\]$ScriptSourcePath = ".\\Detect-SecureBootCertUpdateStatus.ps1",
\[Parameter(Mandatory = $false)\]
\[ValidateSet("Daily", "Weekly", "AtStartup")\]
\[string\]$Schedule = "Daily",
\[Parameter(Mandatory = $false)\]
\[string\]$ScheduleTime = "14:00",
\[Parameter(Mandatory = $false)\]
\[ValidateRange(1, 24)\]
\[int\]$RandomDelayHours = 4
)
- `#Requires -Modules ActiveDirectory, GroupPolicy`
#Requires -Version 5.1
- `$ErrorActionPreference = "Stop"`
$DownloadUrl = "https://aka.ms/getsecureboot"
$DownloadSubPage = "Deployment and Monitoring Samples"
- `# ============================================================================`
# DEPENDENCY VALIDATION
# ============================================================================
- `function Test-ScriptDependencies {`
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
}
- `# The Detect script is required - it gets deployed to endpoints via GPO`
$requiredScripts = @(
"Detect-SecureBootCertUpdateStatus.ps1"
)
- `if (-not (Test-ScriptDependencies -ScriptDirectory $PSScriptRoot -RequiredScripts $requiredScripts)) {`
exit 1
}
- `# ============================================================================`
# AUTO-DETECT DOMAIN NAME
# ============================================================================
- `if (-not $DomainName) {`
$DomainName = $env:USERDNSDOMAIN
if (-not $DomainName) {
# Try to get from AD module
try {
Import-Module ActiveDirectory -ErrorAction Stop
$DomainName = (Get-ADDomain).DNSRoot
} catch {
Write-Host "ERROR: Could not auto-detect domain name." -ForegroundColor Red
Write-Host "Please specify -DomainName parameter." -ForegroundColor Yellow
Write-Host ""
Write-Host "Example:" -ForegroundColor Gray
Write-Host " .\\Deploy-GPO-SecureBootCollection.ps1 -DomainName contoso.com -AutoDetectOU" -ForegroundColor White
exit 1
}
}
Write-Host "Auto-detected domain: $DomainName" -ForegroundColor Green
}
- `# Set CollectionSharePath default if not explicitly provided`
if (-not $PSBoundParameters.ContainsKey('CollectionSharePath')) {
$CollectionSharePath = "\\\\$DomainName\\NETLOGON\\SecureBootLogs"
}
- `Write-Host "============================================" -ForegroundColor Cyan`
Write-Host "Secure Boot Collection - GPO Deployment" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
- `# Validate prerequisites`
Write-Host "\`n\[1/6\] Validating prerequisites..." -ForegroundColor Yellow
- `if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {`
throw "ActiveDirectory module not found. Install RSAT tools."
}
- `if (-not (Get-Module -ListAvailable -Name GroupPolicy)) {`
throw "GroupPolicy module not found. Install RSAT tools."
}
- `Import-Module ActiveDirectory`
Import-Module GroupPolicy
- `# Validate domain connectivity`
try {
$domain = Get-ADDomain -Server $DomainName
Write-Host " Connected to domain: $($domain.DNSRoot)" -ForegroundColor Green
} catch {
throw "Cannot connect to domain: $DomainName. Error: $\_"
}
- `# Handle OU selection`
if ($AutoDetectOU) {
Write-Host "\`n Discovering OUs in domain..." -ForegroundColor Cyan
$allOUs = Get-ADOrganizationalUnit -Filter \* -Server $DomainName |
Sort-Object DistinguishedName |
Select-Object @{N='Index';E={0}}, Name, DistinguishedName
# Assign indices
for ($i = 0; $i -lt $allOUs.Count; $i++) {
$allOUs\[$i\].Index = $i + 1
}
Write-Host "\`n Available OUs:" -ForegroundColor Yellow
Write-Host " ---------------------------------------------------------------------" -ForegroundColor DarkGray
$allOUs | ForEach-Object {
Write-Host (" {0,3}) {1}" -f $\_.Index, $\_.DistinguishedName) -ForegroundColor White
}
Write-Host " ---------------------------------------------------------------------" -ForegroundColor DarkGray
Write-Host " Tip: Enter comma-separated numbers to select multiple OUs (e.g., 1,3,5)" -ForegroundColor DarkGray
Write-Host " Enter 'A' to select ALL OUs" -ForegroundColor DarkGray
Write-Host ""
$selection = Read-Host " Select OU(s) to link GPO"
if ($selection -eq 'A' -or $selection -eq 'a') {
$OUPath = $allOUs.DistinguishedName
Write-Host " Selected ALL $($OUPath.Count) OUs" -ForegroundColor Green
} else {
$indices = $selection -split ',' | ForEach-Object { \[int\]$\_.Trim() }
$OUPath = @()
foreach ($idx in $indices) {
$selected = $allOUs | Where-Object { $\_.Index -eq $idx }
if ($selected) {
$OUPath += $selected.DistinguishedName
} else {
Write-Warning "Invalid index: $idx - skipping"
}
}
}
if ($OUPath.Count -eq 0) {
throw "No OUs selected. Aborting."
}
Write-Host "\`n Selected $($OUPath.Count) OU(s):" -ForegroundColor Green
$OUPath | ForEach-Object { Write-Host " - $\_" -ForegroundColor Gray }
} elseif (-not $OUPath -or $OUPath.Count -eq 0) {
throw "Either -OUPath or -AutoDetectOU must be specified."
} else {
# Validate each OU exists
foreach ($path in $OUPath) {
try {
$ou = Get-ADOrganizationalUnit -Identity $path -Server $DomainName
Write-Host " Target OU found: $($ou.Name)" -ForegroundColor Green
} catch {
throw "OU not found: $path"
}
}
}
- `# Validate source script exists`
if (-not (Test-Path $ScriptSourcePath)) {
throw "Collection script not found: $ScriptSourcePath"
}
- `# Step 2: Create collection share structure`
Write-Host "\`n\[2/6\] Setting up collection share..." -ForegroundColor Yellow
- `$sysvolScriptPath = "\\$DomainName\SYSVOL\$DomainName\Scripts\SecureBootCollection"`
- `# Create SYSVOL script folder`
if (-not (Test-Path $sysvolScriptPath)) {
New-Item -ItemType Directory -Path $sysvolScriptPath -Force | Out-Null
Write-Host " Created SYSVOL script folder: $sysvolScriptPath" -ForegroundColor Green
}
- `# Copy collection script to SYSVOL`
$destScript = Join-Path $sysvolScriptPath "Detect-SecureBootCertUpdateStatus.ps1"
- `# Remove existing destination if it's a directory (fix for Copy-Item bug)`
if (Test-Path $destScript -PathType Container) {
Remove-Item $destScript -Recurse -Force
}
- `Copy-Item -Path $ScriptSourcePath -Destination $destScript -Force`
Write-Host " Copied collection script to SYSVOL" -ForegroundColor Green
- `# Create a wrapper script that calls the main script with parameters`
$wrapperScript = @"
# Secure Boot Event Collection Wrapper
# Auto-generated by Deploy-GPO-SecureBootCollection.ps1
- `` `$ErrorActionPreference = 'SilentlyContinue' ``
- `# Configuration`
\`$CollectionShare = '$CollectionSharePath'
\`$ScriptPath = '$sysvolScriptPath\\Detect-SecureBootCertUpdateStatus.ps1'
- `# Run collection with -OutputPath parameter`
if (Test-Path \`$ScriptPath) {
& \`$ScriptPath -OutputPath \`$CollectionShare
} else {
Write-EventLog -LogName Application -Source "SecureBootCollection" -EventId 1001 -EntryType Error -Message "Collection script not found: \`$ScriptPath"
}
"@
- `$wrapperPath = Join-Path $sysvolScriptPath "Run-SecureBootCollection.ps1"`
$wrapperScript | Out-File -FilePath $wrapperPath -Encoding UTF8 -Force
Write-Host " Created wrapper script" -ForegroundColor Green
- `# Create collection share (if on a file server)`
Write-Host " Collection share path: $CollectionSharePath" -ForegroundColor Cyan
Write-Host " NOTE: Ensure this share exists with 'Domain Computers' write access" -ForegroundColor Yellow
- `# Step 3: Create the GPO`
Write-Host "\`n\[3/6\] Creating Group Policy Object..." -ForegroundColor Yellow
- `# Check if GPO already exists`
$existingGPO = Get-GPO -Name $GPOName -Domain $DomainName -ErrorAction SilentlyContinue
- `if ($existingGPO) {`
Write-Host " GPO '$GPOName' already exists. Updating..." -ForegroundColor Yellow
$gpo = $existingGPO
} else {
$gpo = New-GPO -Name $GPOName -Domain $DomainName -Comment "Deploys Secure Boot event collection script to endpoints"
Write-Host " Created GPO: $GPOName" -ForegroundColor Green
}
- `# Step 4: Configure Scheduled Task via GPO Preferences`
Write-Host "\`n\[4/6\] Configuring scheduled task..." -ForegroundColor Yellow
- `# Build the scheduled task XML`
# RandomDelay spreads execution across endpoints to prevent server overload
Write-Host " Random delay: $RandomDelayHours hours (spreads load across fleet)" -ForegroundColor Cyan
- `$taskTrigger = switch ($Schedule) {`
"Daily" {
@"
<CalendarTrigger>
<StartBoundary>2024-01-01T${ScheduleTime}:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
<RandomDelay>PT${RandomDelayHours}H</RandomDelay>
</CalendarTrigger>
"@
}
"Weekly" {
@"
<CalendarTrigger>
<StartBoundary>2024-01-01T${ScheduleTime}:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByWeek>
<WeeksInterval>1</WeeksInterval>
<DaysOfWeek>
<Wednesday />
</DaysOfWeek>
</ScheduleByWeek>
<RandomDelay>PT${RandomDelayHours}H</RandomDelay>
</CalendarTrigger>
"@
}
"AtStartup" {
# For startup triggers, use Delay to add random start time
# Each machine will start between 5 and (5 + RandomDelayHours\*60) minutes after boot
$maxDelayMinutes = 5 + ($RandomDelayHours \* 60)
@"
<BootTrigger>
<Enabled>true</Enabled>
<Delay>PT5M</Delay>
<RandomDelay>PT${RandomDelayHours}H</RandomDelay>
</BootTrigger>
"@
}
}
- `$scheduledTaskXML = @"`
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Collects Secure Boot event data for enterprise census</Description>
<Author>Enterprise Security</Author>
</RegistrationInfo>
<Triggers>
$taskTrigger
</Triggers>
<Principals>
<Principal id="Author">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>-NoProfile -ExecutionPolicy Bypass -File "$wrapperPath"</Arguments>
</Exec>
</Actions>
</Task>
"@
- `# Save task XML to SYSVOL for reference/backup`
$taskXmlPath = Join-Path $sysvolScriptPath "SecureBootCollection-Task.xml"
$scheduledTaskXML | Out-File -FilePath $taskXmlPath -Encoding Unicode -Force
Write-Host " Saved scheduled task XML to SYSVOL (backup)" -ForegroundColor Green
- `# Inject scheduled task into GPO Preferences`
Write-Host " Injecting scheduled task into GPO Preferences..." -ForegroundColor Cyan
- `$gpoId = $gpo.Id.ToString()`
$gpoPrefPath = "\\\\$DomainName\\SYSVOL\\$DomainName\\Policies\\{$gpoId}\\Machine\\Preferences\\ScheduledTasks"
- `# Create Preferences folder structure`
if (-not (Test-Path $gpoPrefPath)) {
New-Item -ItemType Directory -Path $gpoPrefPath -Force | Out-Null
}
- `# Generate unique GUID for the task`
$taskGuid = \[guid\]::NewGuid().ToString("B").ToUpper()
- `# Build GPO Preferences ScheduledTasks.xml format`
# This is different from standard Task Scheduler XML - it's GPP format
$gppScheduledTasksXml = @"
<?xml version="1.0" encoding="utf-8"?>
<ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">
<TaskV2 clsid="{D8896631-B747-47a7-84A6-C155337F3BC8}" name="SecureBoot-EventCollection" image="0" changed="$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" uid="$taskGuid" userContext="0" removePolicy="0">
<Properties action="C" name="SecureBoot-EventCollection" runAs="NT AUTHORITY\\System" logonType="S4U">
<Task version="1.3">
<RegistrationInfo>
<Author>Enterprise Security</Author>
<Description>Collects Secure Boot certificate status for enterprise compliance monitoring</Description>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>NT AUTHORITY\\System</UserId>
<LogonType>S4U</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<IdleSettings>
<Duration>PT10M</Duration>
<WaitTimeout>PT1H</WaitTimeout>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Triggers>
$taskTrigger
</Triggers>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>-NoProfile -ExecutionPolicy Bypass -File "$wrapperPath"</Arguments>
</Exec>
</Actions>
</Task>
</Properties>
</TaskV2>
</ScheduledTasks>
"@
- `# Write GPP ScheduledTasks.xml to GPO`
$gppXmlPath = Join-Path $gpoPrefPath "ScheduledTasks.xml"
$gppScheduledTasksXml | Out-File -FilePath $gppXmlPath -Encoding UTF8 -Force
Write-Host " \[OK\] Scheduled task injected into GPO" -ForegroundColor Green
Write-Host " Task schedule: $Schedule at $ScheduleTime with $RandomDelayHours hour random delay" -ForegroundColor Gray
- `# Step 5: Link GPO to OU(s)`
Write-Host "\`n\[5/6\] Linking GPO to OU(s)..." -ForegroundColor Yellow
- `$linkedCount = 0`
$skippedCount = 0
- `foreach ($targetOU in $OUPath) {`
$existingLink = Get-GPInheritance -Target $targetOU -Domain $DomainName |
Select-Object -ExpandProperty GpoLinks |
Where-Object { $\_.DisplayName -eq $GPOName }
- ` if (-not $existingLink) {`
New-GPLink -Name $GPOName -Target $targetOU -Domain $DomainName -LinkEnabled Yes | Out-Null
Write-Host " \[OK\] Linked to: $targetOU" -ForegroundColor Green
$linkedCount++
} else {
Write-Host " - Already linked: $targetOU" -ForegroundColor Yellow
$skippedCount++
}
}
- `` Write-Host "`n Summary: $linkedCount new links, $skippedCount already existed" -ForegroundColor Cyan ``
- `# Step 6: Create WMI Filter (optional - for Windows 10/11 only)`
Write-Host "\`n\[6/6\] Creating WMI filter..." -ForegroundColor Yellow
- `$wmiFilterName = "Windows 10 and 11 Workstations"`
$wmiQuery = 'SELECT \* FROM Win32\_OperatingSystem WHERE Version LIKE "10.%" AND ProductType = "1"'
- `Write-Host @"`
\[NOTE\] OPTIONAL: Create WMI Filter in GPMC
Filter Name: $wmiFilterName
Query: $wmiQuery
This filters the GPO to only apply to Windows 10/11 workstations.
- `"@ -ForegroundColor Yellow`
- `# Summary`
Write-Host "\`n============================================" -ForegroundColor Cyan
Write-Host "DEPLOYMENT COMPLETE" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Cyan
Write-Host @"
- `Summary:`
- GPO Name: $GPOName
- Target OU: $OUPath
- Collection Share: $CollectionSharePath
- Script Location: $sysvolScriptPath
- Schedule: $Schedule at $ScheduleTime
- `Next Steps:`
1. Create the collection share with proper permissions:
- Share: $CollectionSharePath
- Permissions: Domain Computers (Write), Domain Admins (Full)
- `2. Complete the scheduled task configuration in GPMC (see instructions above)`
- `3. Run 'gpupdate /force' on a test machine to verify deployment`
- `4. Monitor collection results in: $CollectionSharePath`
- `5. Run aggregation script to generate reports:`
.\\Aggregate-SecureBootData.ps1 -InputPath "$CollectionSharePath"
- `"@ -ForegroundColor White`