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 |