Remediate-SecureBootCA2023SCCM.ps1

<#PSScriptInfo
.VERSION 1.1.0
.GUID 8c5d0c1b-4f2c-4e8f-9f3a-7b2d9f0c2023
.AUTHOR Mert Efe Kanlikilic
.COMPANYNAME mertefekanlikilic.com
.COPYRIGHT (c) 2026 Mert Efe Kanlikilic. All rights reserved.
.TAGS SecureBoot UEFI ConfigMgr SCCM Compliance Remediation PCA2023 RestartNotification
.LICENSEURI https://github.com/kanlikilicmertefe
.PROJECTURI https://mertefekanlikilic.com
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
Initial release of Secure Boot CA 2023 remediation script for Configuration Manager.
#>


<#
.SYNOPSIS
    SCCM / Configuration Manager remediation script for Secure Boot CA 2023 certificate update.
 
.DESCRIPTION
    This remediation script performs the following actions:
      - Ensures AvailableUpdates includes 0x5944 (Secure Boot CA 2023).
      - Triggers the Windows Secure Boot update scheduled task when available.
      - Writes remediation metadata to registry.
      - Displays a restart notification popup in the active user session.
      - Allows SCCM to re-run remediation while the device is PendingRestart.
 
.NOTES
    Run as: SYSTEM
    PowerShell: 64-bit
    Log path: C:\Windows\CCM\Logs\SecureBootCA2023-Remediation.log
#>


param(
    [string]$NonCompliantValue
)

$LOG_PATH          = "$env:WinDir\CCM\Logs\SecureBootCA2023-Remediation.log"
$EVENTLOG_SOURCE   = "SecureBootCA2023"
$EVENTLOG_LOG      = "Application"
$SECUREBOOT_KEY    = "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot"
$SERVICING_KEY     = "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing"
$AVAILABLE_UPDATES = 0x5944
$TASK_PATH         = "\Microsoft\Windows\PI\"
$TASK_NAME         = "Secure-Boot-Update"

$NOTIFICATION_COOLDOWN_HOURS = 0

function Write-Log {
    param([string]$Message, [ValidateSet("INFO","WARN","ERROR")]$Level = "INFO")
    $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    try { Add-Content -Path $LOG_PATH -Value "[$ts][$Level] $Message" -Encoding UTF8 } catch {}
}

function Write-EventEntry {
    param([string]$Message, [int]$EventId, [string]$EntryType = "Information")
    try {
        if (-not [System.Diagnostics.EventLog]::SourceExists($EVENTLOG_SOURCE)) {
            New-EventLog -LogName $EVENTLOG_LOG -Source $EVENTLOG_SOURCE -ErrorAction Stop
        }
        Write-EventLog -LogName $EVENTLOG_LOG -Source $EVENTLOG_SOURCE -EventId $EventId -EntryType $EntryType -Message $Message
    } catch {
        Write-Log "Event log write failed: $($_.Exception.Message)" "WARN"
    }
}

function Get-RemediationMetadata {
    $meta = [PSCustomObject]@{
        FirstRunDate         = $null
        AttemptCount         = 0
        LastNotificationTime = $null
    }

    try {
        $sb = Get-ItemProperty -Path $SECUREBOOT_KEY -ErrorAction SilentlyContinue
        if ($sb.RemediationTimestamp) { $meta.FirstRunDate = [datetime]$sb.RemediationTimestamp }
        if ($null -ne $sb.RemediationAttemptCount) { $meta.AttemptCount = [int]$sb.RemediationAttemptCount }
        if ($sb.LastRestartNotificationTime) { $meta.LastNotificationTime = [datetime]$sb.LastRestartNotificationTime }
    } catch {}

    return $meta
}

function Set-RemediationMetadata {
    param([datetime]$FirstRunDate, [int]$AttemptCount, [datetime]$LastNotificationTime)

    try {
        if (-not (Test-Path $SECUREBOOT_KEY)) { New-Item -Path $SECUREBOOT_KEY -Force | Out-Null }

        $existingTimestamp = (Get-ItemProperty -Path $SECUREBOOT_KEY -ErrorAction SilentlyContinue).RemediationTimestamp
        if (-not $existingTimestamp) {
            Set-ItemProperty -Path $SECUREBOOT_KEY -Name "RemediationTimestamp" -Value $FirstRunDate.ToString("o") -Type String -Force
        }

        Set-ItemProperty -Path $SECUREBOOT_KEY -Name "RemediationAttemptCount" -Value $AttemptCount -Type DWord -Force

        if ($LastNotificationTime) {
            Set-ItemProperty -Path $SECUREBOOT_KEY -Name "LastRestartNotificationTime" -Value $LastNotificationTime.ToString("o") -Type String -Force
        }
    } catch {
        Write-Log "Metadata write failed: $($_.Exception.Message)" "WARN"
    }
}

function Set-AvailableUpdates {
    try {
        if (-not (Test-Path $SECUREBOOT_KEY)) { New-Item -Path $SECUREBOOT_KEY -Force | Out-Null }

        $current = (Get-ItemProperty -Path $SECUREBOOT_KEY -Name "AvailableUpdates" -ErrorAction SilentlyContinue).AvailableUpdates
        if ($current -and (($current -band $AVAILABLE_UPDATES) -eq $AVAILABLE_UPDATES)) {
            Write-Log "AvailableUpdates already contains 0x5944. Current value: 0x$('{0:X}' -f $current)."
            return $true
        }

        $newValue = if ($current) { $current -bor $AVAILABLE_UPDATES } else { $AVAILABLE_UPDATES }
        Set-ItemProperty -Path $SECUREBOOT_KEY -Name "AvailableUpdates" -Value $newValue -Type DWord -Force

        $verify = (Get-ItemProperty -Path $SECUREBOOT_KEY -Name "AvailableUpdates").AvailableUpdates
        if (($verify -band $AVAILABLE_UPDATES) -eq $AVAILABLE_UPDATES) {
            Write-Log "AvailableUpdates set successfully. Value: 0x$('{0:X}' -f $verify)."
            return $true
        }

        Write-Log "AvailableUpdates verification failed. Value: 0x$('{0:X}' -f $verify)." "ERROR"
        return $false
    } catch {
        Write-Log "AvailableUpdates set failed: $($_.Exception.Message)" "ERROR"
        return $false
    }
}

function Start-SecureBootUpdateTask {
    try {
        $task = Get-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction SilentlyContinue
        if ($null -eq $task) {
            Write-Log "Scheduled task not found: ${TASK_PATH}${TASK_NAME}. Windows may still process it automatically." "WARN"
            return $false
        }

        Start-ScheduledTask -TaskPath $TASK_PATH -TaskName $TASK_NAME -ErrorAction Stop
        Write-Log "Scheduled task triggered: ${TASK_PATH}${TASK_NAME}."
        return $true
    } catch {
        Write-Log "Scheduled task trigger failed: $($_.Exception.Message)" "WARN"
        return $false
    }
}

function Test-NotificationCooldown {
    param([datetime]$LastNotificationTime)

    if ($NOTIFICATION_COOLDOWN_HOURS -le 0) { return $true }
    if (-not $LastNotificationTime) { return $true }

    return (((Get-Date) - $LastNotificationTime).TotalHours -ge $NOTIFICATION_COOLDOWN_HOURS)
}

function Register-RestartNotificationHelper {
    param([datetime]$LastNotificationTime)

    if (-not (Test-NotificationCooldown -LastNotificationTime $LastNotificationTime)) {
        Write-Log "Restart notification helper skipped due to cooldown. LastNotificationTime=$LastNotificationTime."
        return $false
    }

    try {
        $HelperRoot = "C:\ProgramData\SecureBootCA2023"
        $PopupScript = Join-Path $HelperRoot "RestartNotification.ps1"
        $UserLogPath = Join-Path $HelperRoot "RestartPopup-UserSession.log"
        $TaskName = "SecureBoot_Remediation_Notice"
        $TaskPath = "\SecureBootCA2023\"
        $HelperRegPath = "HKLM:\SOFTWARE\WPN\SecureBootCA2023"

        if (-not (Test-Path $HelperRoot)) {
            New-Item -Path $HelperRoot -ItemType Directory -Force | Out-Null
        }

        if (-not (Test-Path $HelperRegPath)) {
            New-Item -Path $HelperRegPath -Force | Out-Null
        }

        New-ItemProperty -Path $HelperRegPath -Name "RestartRequired" -PropertyType DWord -Value 1 -Force | Out-Null
        New-ItemProperty -Path $HelperRegPath -Name "Reason" -PropertyType String -Value "Secure Boot CA 2023 migration requires a restart." -Force | Out-Null
        New-ItemProperty -Path $HelperRegPath -Name "LastRemediationTime" -PropertyType String -Value (Get-Date).ToString("o") -Force | Out-Null

        $PopupContent = @'
<--- popup script content unchanged --->
'@


        Set-Content -Path $PopupScript -Value $PopupContent -Encoding UTF8 -Force

        try {
            $acl = Get-Acl $HelperRoot
            $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("BUILTIN\Users", "Modify", "ContainerInherit,ObjectInherit", "None", "Allow")
            $acl.SetAccessRule($accessRule)
            Set-Acl -Path $HelperRoot -AclObject $acl
        } catch {
            Write-Log "Failed to set helper folder ACL: $($_.Exception.Message)" "WARN"
        }

        Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$false -ErrorAction SilentlyContinue

        $Action = New-ScheduledTaskAction `
            -Execute "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe" `
            -Argument "-NoProfile -ExecutionPolicy Bypass -Sta -File `"$PopupScript`""

        $TriggerTime = (Get-Date).AddSeconds(15)
        $Trigger = New-ScheduledTaskTrigger -Once -At $TriggerTime

        $UsersGroup = ([System.Security.Principal.SecurityIdentifier]"S-1-5-32-545").Translate([System.Security.Principal.NTAccount]).Value
        $Principal = New-ScheduledTaskPrincipal -GroupId $UsersGroup

        Register-ScheduledTask `
            -TaskName $TaskName `
            -TaskPath $TaskPath `
            -Action $Action `
            -Trigger $Trigger `
            -Principal $Principal `
            -Force | Out-Null

        Write-Log "Restart notification task registered successfully."
        return $true
    }
    catch {
        Write-Log "Failed to register restart notification task. Error: $($_.Exception.Message)" "WARN"
        return $false
    }
}

# Main
Write-Log "Remediation starting on $env:COMPUTERNAME. NonCompliantValue='$NonCompliantValue'."

if (-not [Environment]::Is64BitProcess) {
    Write-Log "64-bit PowerShell is required. Script ran in 32-bit process." "ERROR"
    Write-EventEntry -Message "Secure Boot CA 2023 remediation failed: 64-bit PowerShell required on $env:COMPUTERNAME." -EventId 2023 -EntryType "Error"
    exit 1
}

try { $secureBootEnabled = Confirm-SecureBootUEFI -ErrorAction Stop } catch { $secureBootEnabled = $false }
Write-Log "Secure Boot enabled: $secureBootEnabled."

if (-not $secureBootEnabled) {
    Write-Log "Secure Boot is disabled or UEFI unavailable. Skipping remediation."
    Write-EventEntry -Message "Secure Boot CA 2023 remediation skipped: Secure Boot disabled or UEFI unavailable on $env:COMPUTERNAME." -EventId 2024 -EntryType "Warning"
    exit 0
}

$meta = Get-RemediationMetadata
Write-Log "Metadata before remediation: FirstRun=$($meta.FirstRunDate); Attempts=$($meta.AttemptCount); LastNotificationTime=$($meta.LastNotificationTime)."

$registryUpdated = Set-AvailableUpdates
if (-not $registryUpdated) {
    Write-Log "Registry update failed. Aborting remediation." "ERROR"
    Write-EventEntry -Message "Secure Boot CA 2023 remediation failed: could not set AvailableUpdates on $env:COMPUTERNAME." -EventId 2026 -EntryType "Error"
    exit 1
}

$taskTriggered = Start-SecureBootUpdateTask

$newAttemptCount = $meta.AttemptCount + 1
$firstRunDate = if ($meta.FirstRunDate) { $meta.FirstRunDate } else { Get-Date }

$notificationSent = Register-RestartNotificationHelper -LastNotificationTime $meta.LastNotificationTime
$lastNotificationTime = if ($notificationSent) { Get-Date } else { $meta.LastNotificationTime }

Set-RemediationMetadata -FirstRunDate $firstRunDate -AttemptCount $newAttemptCount -LastNotificationTime $lastNotificationTime

$message = "Secure Boot CA 2023 remediation completed. Attempts=$newAttemptCount; TaskTriggered=$taskTriggered; RestartNotificationSent=$notificationSent; Restart may be required on $env:COMPUTERNAME."
Write-Log $message
Write-EventEntry -Message $message -EventId 2027 -EntryType "Information"

Write-Output "RemediationCompleted"
exit 0