Remediate-SecureBootCA2023.ps1

<#PSScriptInfo
.VERSION 2.0.1
.GUID 3c9f7b2e-4d1a-4e8c-9c22-8f7d1a6b4c91
.AUTHOR Mert Efe Kanlikilic
.COMPANYNAME mertefekanlikilic.com
.COPYRIGHT (c) 2026 Mert Efe Kanlikilic
.TAGS SecureBoot UEFI Intune Remediation Compliance PCA2023
.LICENSEURI https://github.com/mertefekanlikilic
.PROJECTURI https://github.com/mertefekanlikilic
.DESCRIPTION Secure Boot CA 2023 certificate update remediation script for Intune compliance.
#>


<#
.SYNOPSIS
    Secure Boot CA 2023 certificate update remediation script.

.NOTES
    Author : Mert Efe Kanlikilic -- mertefekanlikilic.com
    Version : 2.0.1
    Platform: Windows 11 | Secure Boot enabled devices
    Run as : SYSTEM, 64-bit PowerShell
    Date : May 2026

    Registry paths:
      HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot
        AvailableUpdates : 0x5944 -- triggers CA2023 update
        RemediationTimestamp : First remediation date (ISO 8601) -- written once
        RemediationAttemptCount : Total run count
        LastNotificationDay : Day threshold of last toast sent

    Toast notification timeline:
      Day 1 : Informational -- restart requested
      Day 3 : Reminder -- restart still pending
      Day 5 : Urgent -- security update delayed
      Day 7+ : IT Support -- escalation required
#>



$LOG_PATH          = "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs\SecureBoot-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"
$TOAST_APPID       = "IT.SecureBootNotification"

$NOTIFY_DAY1 = 0
$NOTIFY_DAY3 = 3
$NOTIFY_DAY5 = 5
$NOTIFY_DAY7 = 7

$TOAST_TEMPLATES = @{
    Day1 = @{
        Title   = "Security Update -- Restart Required"
        Message = "A Secure Boot certificate update has been applied to your device. Please restart your computer today to complete the update."
        Action1 = "Restart Now"
        Action2 = "Remind Me Later"
        Urgency = "default"
    }
    Day3 = @{
        Title   = "Security Update -- Restart Still Pending"
        Message = "Your Secure Boot certificate update is waiting for a restart. This is a security requirement. Please restart your computer as soon as possible."
        Action1 = "Restart Now"
        Action2 = "Remind Me Later"
        Urgency = "default"
    }
    Day5 = @{
        Title   = "Security Update -- Urgent Restart Needed"
        Message = "Your device security update has been pending for 5 days. Please restart your computer today. Continuing to delay may affect your device's security posture."
        Action1 = "Restart Now"
        Action2 = "I Understand"
        Urgency = "urgent"
    }
    Day7 = @{
        Title   = "Security Update -- IT Support Required"
        Message = "Your Secure Boot certificate update has not completed after multiple attempts. Please contact IT support or restart your device immediately. Reference: SecureBoot-CA2023."
        Action1 = "Restart Now"
        Action2 = "Contact IT Support"
        Urgency = "urgent"
    }
}

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)" -Level WARN }
}

function Get-RemediationMetadata {
    $meta = [PSCustomObject]@{
        FirstRunDate        = $null
        AttemptCount        = 0
        LastNotificationDay = -1
        DaysSinceFirst      = 0
    }
    try {
        $sb = Get-ItemProperty -Path $SECUREBOOT_KEY -ErrorAction SilentlyContinue
        if ($sb.RemediationTimestamp) {
            $meta.FirstRunDate    = [datetime]$sb.RemediationTimestamp
            $meta.DaysSinceFirst  = [math]::Floor(((Get-Date) - $meta.FirstRunDate).TotalDays)
        }
        if ($sb.RemediationAttemptCount) { $meta.AttemptCount        = [int]$sb.RemediationAttemptCount }
        if ($null -ne $sb.LastNotificationDay) { $meta.LastNotificationDay = [int]$sb.LastNotificationDay }
    }
    catch { }
    return $meta
}

function Set-RemediationMetadata {
    param([datetime]$FirstRunDate, [int]$AttemptCount, [int]$LastNotificationDay)
    try {
        if (-not (Test-Path $SECUREBOOT_KEY)) { New-Item -Path $SECUREBOOT_KEY -Force | Out-Null }
        $existing = (Get-ItemProperty -Path $SECUREBOOT_KEY -ErrorAction SilentlyContinue).RemediationTimestamp
        if (-not $existing) {
            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
        Set-ItemProperty -Path $SECUREBOOT_KEY -Name "LastNotificationDay"     -Value $LastNotificationDay -Type DWord -Force
    }
    catch { Write-Log "Metadata write failed: $($_.Exception.Message)" -Level 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: 0x$('{0:X}' -f $current)) -- skipping write"
            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 -- got: 0x$('{0:X}' -f $verify)" -Level ERROR
        return $false
    }
    catch {
        Write-Log "AvailableUpdates set failed: $($_.Exception.Message)" -Level ERROR
        return $false
    }
}

function Start-SecureBootTask {
    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}" -Level 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)" -Level WARN
        return $false
    }
}

function Get-NotificationTier {
    param([int]$DaysSince, [int]$LastNotifiedDay)
    if ($DaysSince -ge $NOTIFY_DAY7 -and $LastNotifiedDay -lt $NOTIFY_DAY7) { return "Day7" }
    elseif ($DaysSince -ge $NOTIFY_DAY5 -and $LastNotifiedDay -lt $NOTIFY_DAY5) { return "Day5" }
    elseif ($DaysSince -ge $NOTIFY_DAY3 -and $LastNotifiedDay -lt $NOTIFY_DAY3) { return "Day3" }
    elseif ($DaysSince -ge $NOTIFY_DAY1 -and $LastNotifiedDay -lt $NOTIFY_DAY1) { return "Day1" }
    return $null
}

# WTSQueryUserToken + CreateProcessAsUser ile SYSTEM'den user session'a process inject etme.
# Scheduled task yontemi AzureAD joined cihazlarda (S-1-12-1-... SID) calismiyor;
# bu yontem dogrudan session token'i alip kullanici context'inde powershell baslatir.
$UserProcessLauncher = @"
using System;
using System.Runtime.InteropServices;

public class UserProcessLauncher {
    [DllImport("wtsapi32.dll", SetLastError=true)]
    static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token);

    [DllImport("userenv.dll", SetLastError=true)]
    static extern bool CreateEnvironmentBlock(out IntPtr env, IntPtr token, bool inherit);

    [DllImport("userenv.dll", SetLastError=true)]
    static extern bool DestroyEnvironmentBlock(IntPtr env);

    [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
    static extern bool CreateProcessAsUser(
        IntPtr token, string app, string cmd,
        IntPtr procAttr, IntPtr threadAttr, bool inherit,
        uint flags, IntPtr env, string dir,
        ref STARTUPINFO si, out PROCESS_INFORMATION pi);

    [DllImport("kernel32.dll", SetLastError=true)]
    static extern bool CloseHandle(IntPtr h);

    [DllImport("kernel32.dll")]
    static extern uint WaitForSingleObject(IntPtr h, uint ms);

    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
    struct STARTUPINFO {
        public int cb; public string reserved; public string desktop;
        public string title; public uint x,y,xSize,ySize,xCount,yCount,fill;
        public uint flags; public short show, reserved2; public IntPtr reserved3;
        public IntPtr stdIn, stdOut, stdErr;
    }
    [StructLayout(LayoutKind.Sequential)]
    struct PROCESS_INFORMATION {
        public IntPtr process, thread; public uint pid, tid;
    }

    const uint CREATE_UNICODE_ENV = 0x00000400;
    const uint CREATE_NO_WINDOW = 0x08000000;

    public static int Launch(uint sessionId, string cmdLine, uint waitMs = 15000) {
        IntPtr token = IntPtr.Zero, env = IntPtr.Zero;
        if (!WTSQueryUserToken(sessionId, out token))
            throw new Exception("WTSQueryUserToken failed: " + Marshal.GetLastWin32Error());
        try {
            CreateEnvironmentBlock(out env, token, false);
            var si = new STARTUPINFO { cb = Marshal.SizeOf(typeof(STARTUPINFO)), desktop = "winsta0\\default" };
            PROCESS_INFORMATION pi;
            if (!CreateProcessAsUser(token, null, cmdLine, IntPtr.Zero, IntPtr.Zero,
                    false, CREATE_UNICODE_ENV | CREATE_NO_WINDOW, env, null, ref si, out pi))
                throw new Exception("CreateProcessAsUser failed: " + Marshal.GetLastWin32Error());
            WaitForSingleObject(pi.process, waitMs);
            CloseHandle(pi.process); CloseHandle(pi.thread);
            return (int)pi.pid;
        } finally {
            if (env != IntPtr.Zero) DestroyEnvironmentBlock(env);
            CloseHandle(token);
        }
    }
}
"@


function Send-ToastNotification {
    param([string]$Tier)
    $tmpl = $TOAST_TEMPLATES[$Tier]
    if (-not $tmpl) { Write-Log "Unknown toast tier: $Tier" -Level WARN; return }

    $scenarioAttr = if ($tmpl.Urgency -eq "urgent") { 'scenario="urgent"' } else { "" }
    $toastXml = @"
<toast $scenarioAttr>
    <visual>
        <binding template="ToastGeneric">
            <text>$($tmpl.Title)</text>
            <text>$($tmpl.Message)</text>
        </binding>
    </visual>
    <actions>
        <action content="$($tmpl.Action1)" arguments="restart-now" activationType="background"/>
        <action content="$($tmpl.Action2)" arguments="remind-later" activationType="background"/>
    </actions>
</toast>
"@


    try {
        # Explorer.exe'den aktif kullanici ve session bilgisi al
        $explorerProc = Get-WmiObject Win32_Process -Filter "Name='explorer.exe'" -ErrorAction Stop |
            Select-Object -First 1
        if (-not $explorerProc) {
            Write-Log "No explorer.exe -- no user logged on, skipping toast" -Level WARN
            return
        }

        $owner      = $explorerProc.GetOwner()
        $activeUser = "$($owner.Domain)\$($owner.User)"
        $sessionId  = (Get-Process -Id $explorerProc.ProcessId -ErrorAction Stop).SessionId
        Write-Log "Active user: $activeUser | SessionId: $sessionId"

        # Toast PS1 dosyasini C:\Windows\Temp'e yaz
        # Log: user'in kesin yazabildigi TEMP, sonra Intune log klasorune kopyalanir
        $toastLogPath  = "C:\Windows\Temp\SecureBoot-Toast.log"
        $intuneLogPath = "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\SecureBoot-Toast.log"
        $tempScript    = "C:\Windows\Temp\SecureBoot-Toast-$(Get-Random).ps1"
        $escapedXml    = $toastXml -replace "'", "''"

        $toastScript = @"
`$logPath = '$toastLogPath'
function TLog(`$msg) { try { Add-Content -Path `$logPath -Value "[`$(Get-Date -f 'HH:mm:ss')] `$msg" -Encoding UTF8 } catch {} }

TLog "=== Toast START | User:`$env:USERNAME | PID:`$PID ==="

try {
    [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
    [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
    TLog "Step1 OK: WinRT loaded"
} catch { TLog "Step1 FAIL: `$(`$_.Exception.Message)"; exit 1 }

try {
    `$appId = '$TOAST_APPID'
    `$regPath = "HKCU:\SOFTWARE\Classes\AppUserModelId\`$appId"
    if (-not (Test-Path `$regPath)) { New-Item -Path `$regPath -Force | Out-Null }
    Set-ItemProperty -Path `$regPath -Name "DisplayName" -Value "Security Update" -Type String -Force
    Set-ItemProperty -Path `$regPath -Name "ShowInSettings" -Value 1 -Type DWord -Force
    TLog "Step2 OK: AppId registered"
} catch { TLog "Step2 FAIL: `$(`$_.Exception.Message)"; exit 1 }

try {
    `$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()
    `$xml.LoadXml('$escapedXml')
    TLog "Step3 OK: XML parsed"
} catch { TLog "Step3 FAIL: `$(`$_.Exception.Message)"; exit 1 }

try {
    `$toast = [Windows.UI.Notifications.ToastNotification]::new(`$xml)
    `$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(`$appId)
    `$notifier.Show(`$toast)
    TLog "Step4 OK: Toast shown SUCCESS"
} catch { TLog "Step4 FAIL: `$(`$_.Exception.Message)"; exit 1 }

TLog "=== Toast END ==="
"@

        [System.IO.File]::WriteAllText($tempScript, $toastScript, [System.Text.UTF8Encoding]::new($true))

        # Everyone ReadAndExecute -- user context PS okuyabilsin
        try {
            $acl  = Get-Acl $tempScript
            $rule = New-Object System.Security.AccessControl.FileSystemAccessRule("Everyone","ReadAndExecute","Allow")
            $acl.AddAccessRule($rule)
            Set-Acl -Path $tempScript -AclObject $acl
        } catch { Write-Log "ACL set skipped: $($_.Exception.Message)" -Level WARN }

        Write-Log "Toast script written: $tempScript"

        # UserProcessLauncher type'i yukle (sadece bir kez)
        if (-not ([System.Management.Automation.PSTypeName]'UserProcessLauncher').Type) {
            Add-Type -TypeDefinition $UserProcessLauncher -Language CSharp `
                -ReferencedAssemblies @('System.Runtime.InteropServices') -ErrorAction Stop
        }

        # user session'inda powershell'i WTSQueryUserToken + CreateProcessAsUser ile calistir
        $cmdLine = "powershell.exe -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$tempScript`""
        [UserProcessLauncher]::Launch([uint32]$sessionId, $cmdLine, 15000) | Out-Null
        Write-Log "Process launched in user session -- Tier: $Tier | User: $activeUser"

        # Toast log'u Intune log klasorune kopyala
        Start-Sleep -Seconds 1
        if (Test-Path $toastLogPath) {
            $toastResult = (Get-Content $toastLogPath -Raw -ErrorAction SilentlyContinue) -replace "`r`n","|"
            Write-Log "Toast log: $toastResult"
            try { Copy-Item $toastLogPath $intuneLogPath -Force } catch {}
            Remove-Item $toastLogPath -Force -ErrorAction SilentlyContinue
        } else {
            Write-Log "SecureBoot-Toast.log NOT created -- PS process failed silently" -Level WARN
        }
        Remove-Item -Path $tempScript -Force -ErrorAction SilentlyContinue
    }
    catch {
        Write-Log "Toast delivery failed: $($_.Exception.Message)" -Level WARN
    }
}

# --- Main ---
Write-Log "Remediation v2.0 starting -- $env:COMPUTERNAME"

if (-not [Environment]::Is64BitProcess) {
    Write-Log "32-bit PowerShell -- 64-bit required" -Level ERROR
    exit 1
}

$sbEnabled = $false
try   { $sbEnabled = Confirm-SecureBootUEFI -ErrorAction Stop }
catch { $sbEnabled = $false }

Write-Log "Secure Boot enabled: $sbEnabled"
if (-not $sbEnabled) {
    Write-Log "Secure Boot disabled -- skipping remediation"
    exit 0
}

try {
    $serv = Get-ItemProperty -Path $SERVICING_KEY -ErrorAction SilentlyContinue
    if ($serv.UEFICA2023Status -eq "Updated" -and [int]$serv.WindowsUEFICA2023Capable -eq 2) {
        # Registry shows Updated but detection script triggered remediation
        # This means UEFI DB verification failed -- send restart notification and exit
        Write-Log "Status=Updated but remediation triggered -- UEFI DB not verified, sending restart notification"
        $meta = Get-RemediationMetadata
        $firstRun = if ($meta.FirstRunDate) { $meta.FirstRunDate } else { Get-Date }
        $daysSince = if ($meta.FirstRunDate) { $meta.DaysSinceFirst } else { 0 }
        $tier = Get-NotificationTier -DaysSince $daysSince -LastNotifiedDay $meta.LastNotificationDay
        if ($tier) {
            Send-ToastNotification -Tier $tier
            $newLastNotifiedDay = switch ($tier) {
                "Day7" { $NOTIFY_DAY7 }
                "Day5" { $NOTIFY_DAY5 }
                "Day3" { $NOTIFY_DAY3 }
                "Day1" { $NOTIFY_DAY1 }
            }
            Set-RemediationMetadata -FirstRunDate $firstRun -AttemptCount ($meta.AttemptCount + 1) -LastNotificationDay $newLastNotifiedDay
            Write-Log "Restart notification sent -- Tier: $tier"
        }
        else {
            Write-Log "No notification due this run"
        }
        Write-EventEntry -Message "PendingRestart: Awaiting restart to complete UEFI DB update -- $env:COMPUTERNAME" -EventId 1002 -EntryType "Warning"
        exit 0
    }
}
catch { }

$meta = Get-RemediationMetadata
Write-Log "Metadata -- FirstRun: $($meta.FirstRunDate) | Attempts: $($meta.AttemptCount) | Days: $($meta.DaysSinceFirst) | LastNotified: $($meta.LastNotificationDay)"

$regOk = Set-AvailableUpdates
if (-not $regOk) {
    Write-Log "Registry write failed -- aborting remediation" -Level ERROR
    Write-EventEntry -Message "AvailableUpdates write failed -- $env:COMPUTERNAME" -EventId 1023 -EntryType "Error"
    exit 1
}

$taskOk = Start-SecureBootTask
if (-not $taskOk) {
    Write-Log "Scheduled task not triggered -- Windows will run it automatically every 12 hours" -Level WARN
}

$newCount    = $meta.AttemptCount + 1
$firstRun    = if ($meta.FirstRunDate) { $meta.FirstRunDate } else { Get-Date }
$daysSince   = if ($meta.FirstRunDate) { $meta.DaysSinceFirst } else { 0 }

$tier = Get-NotificationTier -DaysSince $daysSince -LastNotifiedDay $meta.LastNotificationDay

$newLastNotifiedDay = $meta.LastNotificationDay
if ($tier) {
    Send-ToastNotification -Tier $tier
    $newLastNotifiedDay = switch ($tier) {
        "Day7" { $NOTIFY_DAY7 }
        "Day5" { $NOTIFY_DAY5 }
        "Day3" { $NOTIFY_DAY3 }
        "Day1" { $NOTIFY_DAY1 }
    }
    Write-Log "Toast tier: $tier -- LastNotificationDay updated: $newLastNotifiedDay"
}
else {
    Write-Log "No notification due this run (threshold not reached)"
}

Set-RemediationMetadata -FirstRunDate $firstRun -AttemptCount $newCount -LastNotificationDay $newLastNotifiedDay

$eventMsg = "Remediation complete -- Attempts: $newCount | Days: $daysSince | Tier: $tier | $env:COMPUTERNAME"
Write-EventEntry -Message $eventMsg -EventId 1020 -EntryType "Information"
Write-Log $eventMsg
Write-Log "Remediation done -- awaiting device restart"

exit 0