mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
feat(agent): verify Secure Boot CA-2023 state against firmware, not just servicing registry
Customer report: ANALYZE-SEC-001 warned on devices whose firmware already carried the Windows UEFI CA 2023 certificate — the agent only read the SecureBoot\Servicing registry value, which mirrors the Windows Update rollout and lags behind reality (OEM factory image, WinCS, WU not run yet). Agent: new UefiSecureBootCertReader reads the UEFI db/KEK variables natively (GetFirmwareEnvironmentVariableExW; SeSystemEnvironmentPrivilege enabled on the process token and restored — first privilege-adjustment code in the repo). Fail-soft, never throws; legacy BIOS maps to not_uefi. secureboot_status gains per-certificate booleans, uefiFirmwareReadStatus, and the one-sided marker uefiCa2023FirmwareConfirmed (emitted only on positive confirmation, never as false). Rule v3.0.0: not_exists precondition on the marker suppresses the rule when the firmware confirms the certificate; the marker's absence keeps old agents on exact v2 behavior, so the rule is safe to seed before the agent rollout. Firmware-verified-missing raises confidence (+20 db, +10 KEK 2K CA 2023). Verified: full solution build; backend 3201, agent 1988, web 582 tests green; tsc/ajv clean; elevated cross-check on real firmware matches Get-SecureBootUEFI on all seven certificate probes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
---
|
||||
type: Concept
|
||||
title: Secure Boot CA-2023 Detection — firmware evidence vs. Windows servicing status
|
||||
description: Why ANALYZE-SEC-001 v3 reads the UEFI db/KEK variables directly (GetFirmwareEnvironmentVariableExW + SeSystemEnvironmentPrivilege) instead of trusting the SecureBoot\Servicing registry key, and how the one-sided uefiCa2023FirmwareConfirmed marker plus a not_exists precondition give absence-tolerant rule suppression across old and new agents.
|
||||
resource: /src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Interop/UefiSecureBootCertReader.cs
|
||||
tags:
|
||||
- agent
|
||||
- secureboot
|
||||
- ca2023
|
||||
- analyze-rules
|
||||
- backward-compatibility
|
||||
timestamp: 2026-07-23T00:00:00+02:00
|
||||
---
|
||||
|
||||
# Secure Boot CA-2023 Detection
|
||||
|
||||
## Schema
|
||||
|
||||
Two independent evidence sources describe the Windows UEFI CA 2023 rollout state:
|
||||
|
||||
1. **Servicing registry** (`HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\UEFICA2023Status`)
|
||||
— reflects whether the *Windows Update* rollout ran and self-verified. Lags reality:
|
||||
devices whose firmware received the certificate another way (OEM factory image, WinCS
|
||||
CLI, manual deployment) or where WU simply has not run yet show `unknown`/`notfound`.
|
||||
2. **Firmware itself** — the UEFI variables `db` (signature database, GUID
|
||||
`{d719b2cb-3d3a-4596-a3bc-dad00e67656f}`) and `KEK` (GUID
|
||||
`{8be4df61-93ca-11d2-aa0d-00e098032b8c}`). Authoritative. Certificate subject CNs are
|
||||
ASCII bytes inside the DER signature lists, so a byte-substring search for
|
||||
`Windows UEFI CA 2023` / `Microsoft Corporation KEK 2K CA 2023` decides presence —
|
||||
the same check `Get-SecureBootUEFI` enables from PowerShell.
|
||||
|
||||
`UefiSecureBootCertReader` (agent, `Monitoring/Interop`) reads both variables natively via
|
||||
`GetFirmwareEnvironmentVariableExW` — no PowerShell runspace during OOBE. The call needs
|
||||
`SeSystemEnvironmentPrivilege`: SYSTEM holds it default-disabled, so the reader enables it
|
||||
on the process token (`AdjustTokenPrivileges`) and restores the previous state afterwards.
|
||||
Fail-soft contract like `OobeStateReader`: never throws; failures map to status strings
|
||||
(`not_uefi` = legacy BIOS via `ERROR_INVALID_FUNCTION`, `variable_not_found`,
|
||||
`privilege_denied`, `error_<code>`).
|
||||
|
||||
Fields land on the existing `secureboot_status` event (once per agent start,
|
||||
restart-deduped by the StartupEventGate): `uefiFirmwareReadStatus`, per-cert booleans
|
||||
(`uefiDbHasWindowsUefiCa2023`, `uefiDbHasWindowsProductionPca2011`,
|
||||
`uefiKekHasMicrosoftKek2kCa2023`, …) — emitted only when the respective read succeeded —
|
||||
plus the marker below.
|
||||
|
||||
## The one-sided marker (absence-tolerant suppression)
|
||||
|
||||
Rule-engine constraints (pinned by `RuleEnginePreconditionTests`):
|
||||
|
||||
* **Conditions** never match on an absent field (null guard) — new fields must stay
|
||||
`required: false` or the rule dies for every old agent.
|
||||
* **Preconditions**: `not_exists` passes while the field is absent/empty and fails as soon
|
||||
as the field is present with *any* value; comparison operators fail closed on absence.
|
||||
|
||||
A two-sided boolean therefore cannot drive suppression: `not_exists` on
|
||||
`uefiDbHasWindowsUefiCa2023` would also suppress the rule when the field is present with
|
||||
`false`. Hence the **one-sided marker** `uefiCa2023FirmwareConfirmed = true`, emitted
|
||||
ONLY when the db read succeeded AND the 2023 certificate was found — never as `false`.
|
||||
ANALYZE-SEC-001 v3 adds a `not_exists` precondition on it:
|
||||
|
||||
| Device state | Marker | Rule |
|
||||
|---|---|---|
|
||||
| Old agent (no firmware fields) | absent | evaluates exactly like v2 |
|
||||
| New agent, cert missing or firmware unreadable | absent | evaluates; optional conditions `uefiDbHasWindowsUefiCa2023=false` (+20) / `uefiKekHasMicrosoftKek2kCa2023=false` (+10) raise confidence |
|
||||
| New agent, firmware confirms cert | present | silently suppressed (the customer-reported false positive) |
|
||||
|
||||
Because the marker exists nowhere until the new agent ships, the v3 rule is safe to seed
|
||||
**before** the agent rollout.
|
||||
|
||||
## Examples
|
||||
|
||||
Customer-equivalent verification from PowerShell (authoritative, matches the reader):
|
||||
|
||||
```powershell
|
||||
[Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name db).Bytes).Contains('Windows UEFI CA 2023')
|
||||
[Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name KEK).Bytes).Contains('Microsoft Corporation KEK 2K CA 2023')
|
||||
```
|
||||
|
||||
## Citations
|
||||
|
||||
* `src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Interop/UefiSecureBootCertReader.cs` — native reader
|
||||
* `src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs` — `CollectSecureBootStatus` / `AppendFirmwareCertFields`
|
||||
* `rules/analyze/ANALYZE-SEC-001.json` — v3.0.0 rule definition
|
||||
* `src/Backend/AutopilotMonitor.Functions.Tests/RuleEnginePreconditionTests.cs` — pinned precondition semantics + v3 mirror tests
|
||||
* Microsoft: [Act now — Secure Boot certificates expire June 2026](https://techcommunity.microsoft.com/blog/windows-itpro-blog/act-now-secure-boot-certificates-expire-in-june-2026/4426856)
|
||||
@@ -21,6 +21,7 @@ https://docs.autopilotmonitor.com (separate repository).
|
||||
* [Build Counter Blob](agent/build-counter-blob.md) - How agent build numbers are minted: shared counter blob with ETag-CAS so local and CI builds never collide; CI additionally attests build provenance (Sigstore).
|
||||
* [Agent Endpoint Migration](agent/endpoint-migration.md) - How the backend re-homes agents to a new API base URL via the config channel (backend move or per-tenant region move): live-fetch-only, allowlist-validated on both sides, one hop, kill wins.
|
||||
* [MDM Reboot Coalescing](agent/mdm-reboot-coalescing.md) - Attributing the mid-ESP coalesced reboot ("second sign-in") to the device-assigned policy URIs that forced it: aggregated DM-Enterprise 2800 observations (neutral, flush-time watermark) + the ANALYZE-ESP-005 advisory gated on an actually-observed reboot.
|
||||
* [Secure Boot CA-2023 Detection](agent/secureboot-ca2023-detection.md) - Why ANALYZE-SEC-001 v3 reads the UEFI db/KEK variables directly instead of trusting the SecureBoot\Servicing registry, and how the one-sided uefiCa2023FirmwareConfirmed marker plus a not_exists precondition give absence-tolerant suppression across old and new agents.
|
||||
|
||||
# Rules
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## 2026-07-23
|
||||
|
||||
* **Creation**: Added `agent/secureboot-ca2023-detection.md` — ANALYZE-SEC-001 warned on devices whose firmware already carried the 2023 certificate, because the agent only read the `SecureBoot\Servicing\UEFICA2023Status` registry value, which mirrors the Windows Update rollout rather than the firmware (customer report). The agent now reads the UEFI `db`/`KEK` variables natively (`GetFirmwareEnvironmentVariableExW` with `SeSystemEnvironmentPrivilege` enabled on the process token — first privilege-adjustment code in the repo) and emits per-certificate booleans on `secureboot_status`. Suppression rides on a one-sided marker, `uefiCa2023FirmwareConfirmed`, emitted only on positive confirmation: the engine's `not_exists` precondition fails on any present value, so a two-sided boolean could not distinguish "confirmed" from "checked and missing", while the marker's absence keeps old agents and firmware-missing devices on the v2 path. Rule v3.0.0 is therefore safe to seed before the agent release.
|
||||
|
||||
* **Update**: `url-registry.md` — the first release after the alias migration surfaced Front Door caching as a correctness hazard: the alias served the previous `version.json` (`X-Cache: TCP_REMOTE_HIT`) after the origin had rotated, and since manifest and ZIP are independent cache entries, a stale manifest with a fresh ZIP fails the bootstrap SHA-256 check. Route caching on `/agent/*` is now disabled and every blob upload stamps `Cache-Control: no-cache` (CI `Send-Blob`, local `build.ps1`, `versions/*.json` manifests) — all agent-container blobs rotate in place, so nothing there is safely cacheable.
|
||||
|
||||
* **Update**: `agent/decision-engine.md` — the RealmJoin completion gate has a third terminal, `FirstDeploymentIncomplete`. Session 224b2087 showed why: an interactive logon 17 s after DeploymentPhase 101 makes RealmJoin reclassify the run as a secondary-user deployment, whose branch writes 200/210 and never 110 — the gate stayed closed until the 60-min hard timeout although Hello and desktop were long resolved. `realmjoin_phase_changed` is now a typed `RealmJoinPhaseChanged` signal, so `RealmJoinFacts.LastDeploymentPhase` is persisted across agent restarts (this also fixes every `realmjoin_timeout` event claiming "last phase: 0"); a transition out of 100/101 into 200/210 releases the gate with the `realmjoin_first_deployment_incomplete` Warning, and the `RealmJoinDetected` replay path evaluates the same rule for restarts that swallow the transition. A first observation of 210 deliberately does not release — session 6f1959c0 started at 210 and only deployed afterwards.
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"$schema": "../schema/analyze-rule.schema.json",
|
||||
"ruleId": "ANALYZE-SEC-001",
|
||||
"title": "Secure Boot UEFI CA 2023 certificate not deployed",
|
||||
"description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. The 2011 certificates expired in June 2026 — affected devices no longer receive Secure Boot security updates until the 2023 certificate is deployed.",
|
||||
"description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. The 2011 certificates expired in June 2026 — affected devices no longer receive Secure Boot security updates until the 2023 certificate is deployed. Since v3 the agent reads the firmware db/KEK variables directly: when the firmware itself confirms the 2023 certificate, the rule is suppressed even if the Windows servicing status has not caught up yet.",
|
||||
"severity": "warning",
|
||||
"category": "security",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"enabled": true,
|
||||
"baseConfidence": 70,
|
||||
"preconditions": [
|
||||
@@ -16,6 +16,14 @@
|
||||
"operator": "equals",
|
||||
"value": "false",
|
||||
"description": "Skip on virtual machines — Secure Boot DB is host-controlled (Hyper-V/VMware/etc), the inside-the-VM UEFICA2023Status registry value is not the right indicator and not actionable for the VM admin."
|
||||
},
|
||||
{
|
||||
"source": "event_data",
|
||||
"eventType": "secureboot_status",
|
||||
"dataField": "uefiCa2023FirmwareConfirmed",
|
||||
"operator": "not_exists",
|
||||
"value": "",
|
||||
"description": "Skip when the firmware db itself confirms the Windows UEFI CA 2023 certificate — the marker is only emitted on positive firmware confirmation, so old agents (field absent) and unconfirmed devices keep evaluating the rule. The servicing registry status lags behind actual firmware state (OEM factory image, WinCS, manual deployment, or WU simply not run yet)."
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
@@ -54,13 +62,33 @@
|
||||
"operator": "exists",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"signal": "db_ca2023_firmware_missing",
|
||||
"source": "event_data",
|
||||
"eventType": "secureboot_status",
|
||||
"dataField": "uefiDbHasWindowsUefiCa2023",
|
||||
"operator": "equals",
|
||||
"value": "false",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"signal": "kek_2023_missing",
|
||||
"source": "event_data",
|
||||
"eventType": "secureboot_status",
|
||||
"dataField": "uefiKekHasMicrosoftKek2kCa2023",
|
||||
"operator": "equals",
|
||||
"value": "false",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"confidenceFactors": [
|
||||
{ "signal": "ca2023_notfound", "condition": "exists", "weight": 20 },
|
||||
{ "signal": "ca2023_error", "condition": "exists", "weight": 10 }
|
||||
{ "signal": "ca2023_error", "condition": "exists", "weight": 10 },
|
||||
{ "signal": "db_ca2023_firmware_missing", "condition": "exists", "weight": 20 },
|
||||
{ "signal": "kek_2023_missing", "condition": "exists", "weight": 10 }
|
||||
],
|
||||
"explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB.\n\nThe original Windows UEFI CA 2011 certificates **expired on June 27, 2026 — this deadline has passed**. The automatic pre-expiry rollout window is over: this device no longer receives Secure Boot security updates (DB/DBX) and cannot trust 2023-signed boot managers until the 2023 certificate is deployed. Deploying it now requires active remediation (Windows servicing opt-in and, on some models, an OEM firmware update) rather than waiting for an automatic update.\n\nCurrent UEFI CA 2023 status: `{{uefiCA2023Status}}`",
|
||||
"explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB.\n\nThe original Windows UEFI CA 2011 certificates **expired on June 27, 2026 — this deadline has passed**. The automatic pre-expiry rollout window is over: this device no longer receives Secure Boot security updates (DB/DBX) and cannot trust 2023-signed boot managers until the 2023 certificate is deployed. Deploying it now requires active remediation (Windows servicing opt-in and, on some models, an OEM firmware update) rather than waiting for an automatic update.\n\nOn devices running a current agent this finding is verified against the firmware itself: the agent reads the UEFI `db` signature database directly, and this warning is automatically suppressed when the firmware already contains the 2023 certificate — regardless of the Windows servicing status below.\n\nCurrent UEFI CA 2023 servicing status: `{{uefiCA2023Status}}`",
|
||||
"remediation": [
|
||||
{
|
||||
"title": "Deploy UEFI CA 2023 certificate now",
|
||||
@@ -69,7 +97,7 @@
|
||||
"Use Intune policy to enable Secure Boot DB update (Windows UEFI CA 2023)",
|
||||
"Check with the device OEM whether a firmware update is required for the DB update to apply on this model",
|
||||
"Alternatively, use the WinCS CLI tool from Microsoft for manual deployment",
|
||||
"Verify deployment: check registry HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\Servicing\\UEFICA2023Status = 'Updated'",
|
||||
"Verify deployment against the firmware directly: [Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name db).Bytes).Contains('Windows UEFI CA 2023') — this is authoritative; the registry value HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\Servicing\\UEFICA2023Status = 'Updated' only reflects the Windows Update rollout",
|
||||
"After DB update: ensure the boot manager is updated to a 2023-signed version via monthly cumulative update"
|
||||
]
|
||||
}
|
||||
|
||||
+40
-4
@@ -2178,10 +2178,10 @@
|
||||
{
|
||||
"ruleId": "ANALYZE-SEC-001",
|
||||
"title": "Secure Boot UEFI CA 2023 certificate not deployed",
|
||||
"description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. The 2011 certificates expired in June 2026 — affected devices no longer receive Secure Boot security updates until the 2023 certificate is deployed.",
|
||||
"description": "Detects devices where the Windows UEFI CA 2023 certificate has not been deployed to the Secure Boot DB. The 2011 certificates expired in June 2026 — affected devices no longer receive Secure Boot security updates until the 2023 certificate is deployed. Since v3 the agent reads the firmware db/KEK variables directly: when the firmware itself confirms the 2023 certificate, the rule is suppressed even if the Windows servicing status has not caught up yet.",
|
||||
"severity": "warning",
|
||||
"category": "security",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"enabled": true,
|
||||
"baseConfidence": 70,
|
||||
"preconditions": [
|
||||
@@ -2192,6 +2192,14 @@
|
||||
"operator": "equals",
|
||||
"value": "false",
|
||||
"description": "Skip on virtual machines — Secure Boot DB is host-controlled (Hyper-V/VMware/etc), the inside-the-VM UEFICA2023Status registry value is not the right indicator and not actionable for the VM admin."
|
||||
},
|
||||
{
|
||||
"source": "event_data",
|
||||
"eventType": "secureboot_status",
|
||||
"dataField": "uefiCa2023FirmwareConfirmed",
|
||||
"operator": "not_exists",
|
||||
"value": "",
|
||||
"description": "Skip when the firmware db itself confirms the Windows UEFI CA 2023 certificate — the marker is only emitted on positive firmware confirmation, so old agents (field absent) and unconfirmed devices keep evaluating the rule. The servicing registry status lags behind actual firmware state (OEM factory image, WinCS, manual deployment, or WU simply not run yet)."
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
@@ -2230,6 +2238,24 @@
|
||||
"operator": "exists",
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"signal": "db_ca2023_firmware_missing",
|
||||
"source": "event_data",
|
||||
"eventType": "secureboot_status",
|
||||
"dataField": "uefiDbHasWindowsUefiCa2023",
|
||||
"operator": "equals",
|
||||
"value": "false",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"signal": "kek_2023_missing",
|
||||
"source": "event_data",
|
||||
"eventType": "secureboot_status",
|
||||
"dataField": "uefiKekHasMicrosoftKek2kCa2023",
|
||||
"operator": "equals",
|
||||
"value": "false",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"confidenceFactors": [
|
||||
@@ -2242,9 +2268,19 @@
|
||||
"signal": "ca2023_error",
|
||||
"condition": "exists",
|
||||
"weight": 10
|
||||
},
|
||||
{
|
||||
"signal": "db_ca2023_firmware_missing",
|
||||
"condition": "exists",
|
||||
"weight": 20
|
||||
},
|
||||
{
|
||||
"signal": "kek_2023_missing",
|
||||
"condition": "exists",
|
||||
"weight": 10
|
||||
}
|
||||
],
|
||||
"explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB.\n\nThe original Windows UEFI CA 2011 certificates **expired on June 27, 2026 — this deadline has passed**. The automatic pre-expiry rollout window is over: this device no longer receives Secure Boot security updates (DB/DBX) and cannot trust 2023-signed boot managers until the 2023 certificate is deployed. Deploying it now requires active remediation (Windows servicing opt-in and, on some models, an OEM firmware update) rather than waiting for an automatic update.\n\nCurrent UEFI CA 2023 status: `{{uefiCA2023Status}}`",
|
||||
"explanation": "This device has Secure Boot enabled but the **Windows UEFI CA 2023** certificate has not been deployed to the Secure Boot DB.\n\nThe original Windows UEFI CA 2011 certificates **expired on June 27, 2026 — this deadline has passed**. The automatic pre-expiry rollout window is over: this device no longer receives Secure Boot security updates (DB/DBX) and cannot trust 2023-signed boot managers until the 2023 certificate is deployed. Deploying it now requires active remediation (Windows servicing opt-in and, on some models, an OEM firmware update) rather than waiting for an automatic update.\n\nOn devices running a current agent this finding is verified against the firmware itself: the agent reads the UEFI `db` signature database directly, and this warning is automatically suppressed when the firmware already contains the 2023 certificate — regardless of the Windows servicing status below.\n\nCurrent UEFI CA 2023 servicing status: `{{uefiCA2023Status}}`",
|
||||
"remediation": [
|
||||
{
|
||||
"title": "Deploy UEFI CA 2023 certificate now",
|
||||
@@ -2253,7 +2289,7 @@
|
||||
"Use Intune policy to enable Secure Boot DB update (Windows UEFI CA 2023)",
|
||||
"Check with the device OEM whether a firmware update is required for the DB update to apply on this model",
|
||||
"Alternatively, use the WinCS CLI tool from Microsoft for manual deployment",
|
||||
"Verify deployment: check registry HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\Servicing\\UEFICA2023Status = 'Updated'",
|
||||
"Verify deployment against the firmware directly: [Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name db).Bytes).Contains('Windows UEFI CA 2023') — this is authoritative; the registry value HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecureBoot\\Servicing\\UEFICA2023Status = 'Updated' only reflects the Windows Update rollout",
|
||||
"After DB update: ensure the boot manager is updated to a 2023-signed version via monthly cumulative update"
|
||||
]
|
||||
}
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Pins the pure parts of the UEFI Secure Boot certificate probe: the ASCII substring
|
||||
/// search over raw signature-list bytes and the <c>secureboot_status</c> payload shaping
|
||||
/// (in particular the one-sided <c>uefiCa2023FirmwareConfirmed</c> marker whose ABSENCE
|
||||
/// is load-bearing for ANALYZE-SEC-001's not_exists precondition). The actual firmware
|
||||
/// read is only smoke-tested: test machines may be VMs, non-elevated, or legacy BIOS,
|
||||
/// so only the fail-soft contract is asserted.
|
||||
/// </summary>
|
||||
public sealed class UefiSecureBootCertReaderTests
|
||||
{
|
||||
// --- ContainsAscii ---
|
||||
|
||||
[Fact]
|
||||
public void ContainsAscii_finds_needle_embedded_in_binary_noise()
|
||||
{
|
||||
var bytes = Concat(new byte[] { 0x00, 0xA1, 0x30, 0x82 },
|
||||
Encoding.ASCII.GetBytes("Windows UEFI CA 2023"),
|
||||
new byte[] { 0x00, 0xFF });
|
||||
|
||||
Assert.True(UefiSecureBootCertReader.ContainsAscii(bytes, "Windows UEFI CA 2023"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsAscii_finds_needle_at_the_very_end()
|
||||
{
|
||||
var bytes = Concat(new byte[] { 0x01, 0x02 }, Encoding.ASCII.GetBytes("KEK 2K CA 2023"));
|
||||
|
||||
Assert.True(UefiSecureBootCertReader.ContainsAscii(bytes, "KEK 2K CA 2023"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsAscii_rejects_partial_overlap_and_absent_needle()
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes("Windows UEFI CA 2011 something else");
|
||||
|
||||
Assert.False(UefiSecureBootCertReader.ContainsAscii(bytes, "Windows UEFI CA 2023"));
|
||||
// Repeated prefix bytes must not confuse the scan.
|
||||
var tricky = Encoding.ASCII.GetBytes("ababac");
|
||||
Assert.True(UefiSecureBootCertReader.ContainsAscii(tricky, "abac"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsAscii_handles_null_empty_and_oversized_needle()
|
||||
{
|
||||
Assert.False(UefiSecureBootCertReader.ContainsAscii(null, "x"));
|
||||
Assert.False(UefiSecureBootCertReader.ContainsAscii(new byte[0], "x"));
|
||||
Assert.False(UefiSecureBootCertReader.ContainsAscii(new byte[] { 1, 2 }, ""));
|
||||
Assert.False(UefiSecureBootCertReader.ContainsAscii(new byte[] { 1, 2 }, "abc"));
|
||||
}
|
||||
|
||||
// --- Payload shaping ---
|
||||
|
||||
[Fact]
|
||||
public void Marker_emitted_only_when_db_ok_and_ca2023_present()
|
||||
{
|
||||
var data = new Dictionary<string, object>();
|
||||
DeviceInfoCollector.AppendFirmwareCertFields(data, new UefiSecureBootCertSnapshot
|
||||
{
|
||||
DbStatus = UefiSecureBootCertReader.StatusOk,
|
||||
KekStatus = UefiSecureBootCertReader.StatusOk,
|
||||
DbHasWindowsUefiCa2023 = true,
|
||||
KekHasMicrosoftKek2kCa2023 = true
|
||||
});
|
||||
|
||||
Assert.Equal(true, data["uefiCa2023FirmwareConfirmed"]);
|
||||
Assert.Equal("ok", data["uefiFirmwareReadStatus"]);
|
||||
Assert.Equal(true, data["uefiDbHasWindowsUefiCa2023"]);
|
||||
Assert.Equal(true, data["uefiKekHasMicrosoftKek2kCa2023"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Marker_absent_when_ca2023_missing_even_though_read_succeeded()
|
||||
{
|
||||
var data = new Dictionary<string, object>();
|
||||
DeviceInfoCollector.AppendFirmwareCertFields(data, new UefiSecureBootCertSnapshot
|
||||
{
|
||||
DbStatus = UefiSecureBootCertReader.StatusOk,
|
||||
KekStatus = UefiSecureBootCertReader.StatusOk,
|
||||
DbHasWindowsUefiCa2023 = false,
|
||||
DbHasWindowsProductionPca2011 = true
|
||||
});
|
||||
|
||||
// The marker must be OMITTED, not false — not_exists fails on any present value.
|
||||
Assert.False(data.ContainsKey("uefiCa2023FirmwareConfirmed"));
|
||||
Assert.Equal(false, data["uefiDbHasWindowsUefiCa2023"]);
|
||||
Assert.Equal(true, data["uefiDbHasWindowsProductionPca2011"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Failed_db_read_emits_status_but_no_cert_booleans_and_no_marker()
|
||||
{
|
||||
var data = new Dictionary<string, object>();
|
||||
DeviceInfoCollector.AppendFirmwareCertFields(data, new UefiSecureBootCertSnapshot
|
||||
{
|
||||
DbStatus = UefiSecureBootCertReader.StatusPrivilegeDenied,
|
||||
KekStatus = UefiSecureBootCertReader.StatusPrivilegeDenied
|
||||
});
|
||||
|
||||
Assert.Equal(UefiSecureBootCertReader.StatusPrivilegeDenied, data["uefiFirmwareReadStatus"]);
|
||||
Assert.False(data.ContainsKey("uefiDbHasWindowsUefiCa2023"));
|
||||
Assert.False(data.ContainsKey("uefiKekHasMicrosoftKek2kCa2023"));
|
||||
Assert.False(data.ContainsKey("uefiCa2023FirmwareConfirmed"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Db_ok_but_kek_failed_keeps_db_booleans_and_reports_kek_status()
|
||||
{
|
||||
var data = new Dictionary<string, object>();
|
||||
DeviceInfoCollector.AppendFirmwareCertFields(data, new UefiSecureBootCertSnapshot
|
||||
{
|
||||
DbStatus = UefiSecureBootCertReader.StatusOk,
|
||||
KekStatus = UefiSecureBootCertReader.StatusVariableNotFound,
|
||||
DbHasWindowsUefiCa2023 = true
|
||||
});
|
||||
|
||||
Assert.Equal(UefiSecureBootCertReader.StatusVariableNotFound, data["uefiFirmwareReadStatus"]);
|
||||
Assert.Equal(true, data["uefiDbHasWindowsUefiCa2023"]);
|
||||
Assert.Equal(true, data["uefiCa2023FirmwareConfirmed"]);
|
||||
Assert.False(data.ContainsKey("uefiKekHasMicrosoftKek2kCa2023"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Null_snapshot_leaves_payload_untouched()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "uefiSecureBootEnabled", true } };
|
||||
DeviceInfoCollector.AppendFirmwareCertFields(data, null);
|
||||
|
||||
Assert.Single(data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DescribeFirmwareVerdict_covers_all_shapes()
|
||||
{
|
||||
Assert.Equal("unavailable", DeviceInfoCollector.DescribeFirmwareVerdict(null));
|
||||
Assert.Equal(UefiSecureBootCertReader.StatusNotUefi, DeviceInfoCollector.DescribeFirmwareVerdict(
|
||||
new UefiSecureBootCertSnapshot { DbStatus = UefiSecureBootCertReader.StatusNotUefi }));
|
||||
Assert.Equal("CA2023 confirmed", DeviceInfoCollector.DescribeFirmwareVerdict(
|
||||
new UefiSecureBootCertSnapshot { DbStatus = UefiSecureBootCertReader.StatusOk, DbHasWindowsUefiCa2023 = true }));
|
||||
Assert.Equal("CA2023 missing", DeviceInfoCollector.DescribeFirmwareVerdict(
|
||||
new UefiSecureBootCertSnapshot { DbStatus = UefiSecureBootCertReader.StatusOk, DbHasWindowsUefiCa2023 = false }));
|
||||
}
|
||||
|
||||
// --- Reader smoke (real firmware; machine state unknown) ---
|
||||
|
||||
[Fact]
|
||||
public void Read_never_throws_and_reports_a_status_for_both_variables()
|
||||
{
|
||||
var snapshot = UefiSecureBootCertReader.Read();
|
||||
|
||||
Assert.NotNull(snapshot);
|
||||
Assert.False(string.IsNullOrEmpty(snapshot.DbStatus));
|
||||
Assert.False(string.IsNullOrEmpty(snapshot.KekStatus));
|
||||
}
|
||||
|
||||
private static byte[] Concat(params byte[][] parts)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var part in parts) total += part.Length;
|
||||
var result = new byte[total];
|
||||
var offset = 0;
|
||||
foreach (var part in parts)
|
||||
{
|
||||
System.Array.Copy(part, 0, result, offset, part.Length);
|
||||
offset += part.Length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the UEFI Secure Boot signature databases <c>db</c> and <c>KEK</c> directly from
|
||||
/// firmware via <c>GetFirmwareEnvironmentVariableExW</c> and checks which Microsoft
|
||||
/// signing certificates they contain. This is the authoritative source for the 2023
|
||||
/// certificate rollout state — the <c>SecureBoot\Servicing</c> registry key only reflects
|
||||
/// whether the *Windows Update* rollout ran and verified, and lags behind when the
|
||||
/// certificate reached the firmware another way (OEM factory image, WinCS, manual).
|
||||
/// <para>
|
||||
/// The certificate subject CNs are stored as ASCII bytes inside the DER-encoded
|
||||
/// signature lists, so a plain byte-substring search is sufficient — the same check
|
||||
/// Microsoft documents via <c>Get-SecureBootUEFI</c> (which wraps this exact API).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Contract:</b> observational only — never feed this into decision-engine logic.
|
||||
/// <see cref="Read"/> never throws. Reading firmware variables requires
|
||||
/// <c>SeSystemEnvironmentPrivilege</c>, which SYSTEM holds but must be enabled on the
|
||||
/// process token first; the previous privilege state is restored after the read.
|
||||
/// On legacy BIOS the API fails with <c>ERROR_INVALID_FUNCTION</c> → status
|
||||
/// <c>not_uefi</c>; every failure maps to a status string instead of an exception.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class UefiSecureBootCertReader
|
||||
{
|
||||
public const string StatusOk = "ok";
|
||||
public const string StatusNotUefi = "not_uefi";
|
||||
public const string StatusVariableNotFound = "variable_not_found";
|
||||
public const string StatusPrivilegeDenied = "privilege_denied";
|
||||
|
||||
// EFI_IMAGE_SECURITY_DATABASE_GUID — namespace of the "db" variable.
|
||||
private const string DbVariableGuid = "{d719b2cb-3d3a-4596-a3bc-dad00e67656f}";
|
||||
// EFI_GLOBAL_VARIABLE — namespace of the "KEK" variable.
|
||||
private const string KekVariableGuid = "{8be4df61-93ca-11d2-aa0d-00e098032b8c}";
|
||||
|
||||
// Subject CNs searched for in db. 2023 set per Microsoft's June-2026 expiry guidance;
|
||||
// 2011 set kept for context (which legacy CAs are still present).
|
||||
internal const string CertWindowsUefiCa2023 = "Windows UEFI CA 2023";
|
||||
internal const string CertMicrosoftUefiCa2023 = "Microsoft UEFI CA 2023";
|
||||
internal const string CertMicrosoftOptionRomCa2023 = "Microsoft Option ROM UEFI CA 2023";
|
||||
internal const string CertWindowsProductionPca2011 = "Microsoft Windows Production PCA 2011";
|
||||
internal const string CertMicrosoftUefiCa2011 = "Microsoft Corporation UEFI CA 2011";
|
||||
// Subject CNs searched for in KEK.
|
||||
internal const string CertMicrosoftKek2kCa2023 = "Microsoft Corporation KEK 2K CA 2023";
|
||||
internal const string CertMicrosoftKekCa2011 = "Microsoft Corporation KEK CA 2011";
|
||||
|
||||
private const int ErrorInvalidFunction = 1; // legacy BIOS / no UEFI
|
||||
private const int ErrorInsufficientBuffer = 122;
|
||||
private const int ErrorEnvvarNotFound = 203;
|
||||
private const int ErrorPrivilegeNotHeld = 1314;
|
||||
private const int ErrorNotAllAssigned = 1300;
|
||||
|
||||
private const int InitialBufferSize = 64 * 1024;
|
||||
private const int RetryBufferSize = 256 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Reads db and KEK and evaluates the certificate presence flags. Never throws;
|
||||
/// per-variable failures surface as <see cref="UefiSecureBootCertSnapshot.DbStatus"/> /
|
||||
/// <see cref="UefiSecureBootCertSnapshot.KekStatus"/>. Certificate flags are only
|
||||
/// meaningful when the corresponding status is <see cref="StatusOk"/>.
|
||||
/// </summary>
|
||||
public static UefiSecureBootCertSnapshot Read()
|
||||
{
|
||||
var snapshot = new UefiSecureBootCertSnapshot();
|
||||
try
|
||||
{
|
||||
using (var privilege = SystemEnvironmentPrivilege.TryEnable())
|
||||
{
|
||||
if (!privilege.Enabled)
|
||||
{
|
||||
snapshot.DbStatus = StatusPrivilegeDenied;
|
||||
snapshot.KekStatus = StatusPrivilegeDenied;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
byte[] db;
|
||||
snapshot.DbStatus = TryReadVariable("db", DbVariableGuid, out db);
|
||||
if (snapshot.DbStatus == StatusOk)
|
||||
{
|
||||
snapshot.DbHasWindowsUefiCa2023 = ContainsAscii(db, CertWindowsUefiCa2023);
|
||||
snapshot.DbHasMicrosoftUefiCa2023 = ContainsAscii(db, CertMicrosoftUefiCa2023);
|
||||
snapshot.DbHasMicrosoftOptionRomCa2023 = ContainsAscii(db, CertMicrosoftOptionRomCa2023);
|
||||
snapshot.DbHasWindowsProductionPca2011 = ContainsAscii(db, CertWindowsProductionPca2011);
|
||||
snapshot.DbHasMicrosoftUefiCa2011 = ContainsAscii(db, CertMicrosoftUefiCa2011);
|
||||
}
|
||||
|
||||
byte[] kek;
|
||||
snapshot.KekStatus = TryReadVariable("KEK", KekVariableGuid, out kek);
|
||||
if (snapshot.KekStatus == StatusOk)
|
||||
{
|
||||
snapshot.KekHasMicrosoftKek2kCa2023 = ContainsAscii(kek, CertMicrosoftKek2kCa2023);
|
||||
snapshot.KekHasMicrosoftKekCa2011 = ContainsAscii(kek, CertMicrosoftKekCa2011);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var status = "error_" + ex.GetType().Name;
|
||||
if (snapshot.DbStatus == null) snapshot.DbStatus = status;
|
||||
if (snapshot.KekStatus == null) snapshot.KekStatus = status;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads one firmware variable into <paramref name="bytes"/> (trimmed to the actual
|
||||
/// length). Returns a status string; <paramref name="bytes"/> is null unless the
|
||||
/// status is <see cref="StatusOk"/>.
|
||||
/// </summary>
|
||||
private static string TryReadVariable(string name, string guid, out byte[] bytes)
|
||||
{
|
||||
bytes = null;
|
||||
var buffer = new byte[InitialBufferSize];
|
||||
var length = NativeMethods.GetFirmwareEnvironmentVariableExW(name, guid, buffer, (uint)buffer.Length, IntPtr.Zero);
|
||||
if (length == 0)
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
if (error == ErrorInsufficientBuffer)
|
||||
{
|
||||
buffer = new byte[RetryBufferSize];
|
||||
length = NativeMethods.GetFirmwareEnvironmentVariableExW(name, guid, buffer, (uint)buffer.Length, IntPtr.Zero);
|
||||
if (length == 0)
|
||||
error = Marshal.GetLastWin32Error();
|
||||
}
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
switch (error)
|
||||
{
|
||||
case ErrorInvalidFunction: return StatusNotUefi;
|
||||
case ErrorEnvvarNotFound: return StatusVariableNotFound;
|
||||
case ErrorPrivilegeNotHeld: return StatusPrivilegeDenied;
|
||||
default: return "error_" + error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes = new byte[length];
|
||||
Array.Copy(buffer, bytes, (int)length);
|
||||
return StatusOk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Byte-level ASCII substring search (ordinal, case-sensitive). The signature lists
|
||||
/// are at most a few hundred KB and read once per agent start — a naive scan is fine.
|
||||
/// </summary>
|
||||
internal static bool ContainsAscii(byte[] haystack, string needle)
|
||||
{
|
||||
if (haystack == null || string.IsNullOrEmpty(needle) || needle.Length > haystack.Length)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i <= haystack.Length - needle.Length; i++)
|
||||
{
|
||||
var j = 0;
|
||||
while (j < needle.Length && haystack[i + j] == (byte)needle[j])
|
||||
j++;
|
||||
if (j == needle.Length)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables <c>SeSystemEnvironmentPrivilege</c> on the process token for the lifetime
|
||||
/// of the instance and restores the previous privilege state on dispose. SYSTEM holds
|
||||
/// the privilege (default-disabled); without elevation <see cref="Enabled"/> stays
|
||||
/// false and the caller reports <see cref="StatusPrivilegeDenied"/>.
|
||||
/// </summary>
|
||||
private sealed class SystemEnvironmentPrivilege : IDisposable
|
||||
{
|
||||
public bool Enabled { get; private set; }
|
||||
|
||||
private IntPtr _token = IntPtr.Zero;
|
||||
private NativeMethods.TOKEN_PRIVILEGES _previousState;
|
||||
private bool _restoreNeeded;
|
||||
|
||||
public static SystemEnvironmentPrivilege TryEnable()
|
||||
{
|
||||
var instance = new SystemEnvironmentPrivilege();
|
||||
try
|
||||
{
|
||||
if (!NativeMethods.OpenProcessToken(NativeMethods.GetCurrentProcess(),
|
||||
NativeMethods.TOKEN_ADJUST_PRIVILEGES | NativeMethods.TOKEN_QUERY, out instance._token))
|
||||
return instance;
|
||||
|
||||
NativeMethods.LUID luid;
|
||||
if (!NativeMethods.LookupPrivilegeValueW(null, "SeSystemEnvironmentPrivilege", out luid))
|
||||
return instance;
|
||||
|
||||
var newState = new NativeMethods.TOKEN_PRIVILEGES
|
||||
{
|
||||
PrivilegeCount = 1,
|
||||
Luid = luid,
|
||||
Attributes = NativeMethods.SE_PRIVILEGE_ENABLED
|
||||
};
|
||||
|
||||
uint returnLength;
|
||||
if (!NativeMethods.AdjustTokenPrivileges(instance._token, false, ref newState,
|
||||
(uint)Marshal.SizeOf(typeof(NativeMethods.TOKEN_PRIVILEGES)),
|
||||
out instance._previousState, out returnLength))
|
||||
return instance;
|
||||
|
||||
// AdjustTokenPrivileges returns TRUE even when the privilege is not held —
|
||||
// the real verdict is ERROR_NOT_ALL_ASSIGNED in the last error.
|
||||
if (Marshal.GetLastWin32Error() == ErrorNotAllAssigned)
|
||||
return instance;
|
||||
|
||||
// Restore only when the privilege was not already enabled before.
|
||||
instance._restoreNeeded = instance._previousState.PrivilegeCount > 0
|
||||
&& (instance._previousState.Attributes & NativeMethods.SE_PRIVILEGE_ENABLED) == 0;
|
||||
instance.Enabled = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail-soft: Enabled stays false.
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_token != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_restoreNeeded)
|
||||
{
|
||||
NativeMethods.TOKEN_PRIVILEGES ignored;
|
||||
uint returnLength;
|
||||
NativeMethods.AdjustTokenPrivileges(_token, false, ref _previousState,
|
||||
(uint)Marshal.SizeOf(typeof(NativeMethods.TOKEN_PRIVILEGES)),
|
||||
out ignored, out returnLength);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort — the process token privilege state is not load-bearing
|
||||
// for anything else the agent does.
|
||||
}
|
||||
NativeMethods.CloseHandle(_token);
|
||||
_token = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NativeMethods
|
||||
{
|
||||
public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
|
||||
public const uint TOKEN_QUERY = 0x0008;
|
||||
public const uint SE_PRIVILEGE_ENABLED = 0x0002;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LUID
|
||||
{
|
||||
public uint LowPart;
|
||||
public int HighPart;
|
||||
}
|
||||
|
||||
// Single-privilege variant of the variable-length native struct — sufficient here
|
||||
// because exactly one privilege is adjusted per call.
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public uint PrivilegeCount;
|
||||
public LUID Luid;
|
||||
public uint Attributes;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern uint GetFirmwareEnvironmentVariableExW(
|
||||
string lpName, string lpGuid, byte[] pBuffer, uint nSize, IntPtr pdwAttributes);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool LookupPrivilegeValueW(string lpSystemName, string lpName, out LUID lpLuid);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool AdjustTokenPrivileges(IntPtr tokenHandle, bool disableAllPrivileges,
|
||||
ref TOKEN_PRIVILEGES newState, uint bufferLength, out TOKEN_PRIVILEGES previousState, out uint returnLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of one firmware read. Certificate flags are only meaningful when the
|
||||
/// corresponding status equals <see cref="UefiSecureBootCertReader.StatusOk"/>.
|
||||
/// </summary>
|
||||
internal sealed class UefiSecureBootCertSnapshot
|
||||
{
|
||||
public string DbStatus { get; set; }
|
||||
public string KekStatus { get; set; }
|
||||
|
||||
public bool DbHasWindowsUefiCa2023 { get; set; }
|
||||
public bool DbHasMicrosoftUefiCa2023 { get; set; }
|
||||
public bool DbHasMicrosoftOptionRomCa2023 { get; set; }
|
||||
public bool DbHasWindowsProductionPca2011 { get; set; }
|
||||
public bool DbHasMicrosoftUefiCa2011 { get; set; }
|
||||
|
||||
public bool KekHasMicrosoftKek2kCa2023 { get; set; }
|
||||
public bool KekHasMicrosoftKekCa2011 { get; set; }
|
||||
}
|
||||
}
|
||||
+55
-1
@@ -8,6 +8,7 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AutopilotMonitor.Agent.V2.Core.Logging;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals;
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using AutopilotMonitor.Agent.V2.Core.Orchestration;
|
||||
using AutopilotMonitor.Agent.V2.Core.Security;
|
||||
using AutopilotMonitor.DecisionCore.Engine;
|
||||
@@ -493,7 +494,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
}
|
||||
}
|
||||
|
||||
EmitDeviceInfoEvent(Constants.EventTypes.SecureBootStatus, $"SecureBoot: {data["uefiSecureBootEnabled"]}, CA2023: {data["uefiCA2023Status"]}", data);
|
||||
// Direct firmware evidence (UEFI db/KEK variables) — authoritative for the
|
||||
// CA-2023 rollout state. The Servicing key above only mirrors whether the
|
||||
// Windows Update rollout ran; it lags behind when the certificate reached
|
||||
// the firmware another way (OEM factory image, WinCS, manual deployment).
|
||||
var firmware = UefiSecureBootCertReader.Read();
|
||||
AppendFirmwareCertFields(data, firmware);
|
||||
|
||||
EmitDeviceInfoEvent(Constants.EventTypes.SecureBootStatus,
|
||||
$"SecureBoot: {data["uefiSecureBootEnabled"]}, CA2023: {data["uefiCA2023Status"]}, firmware: {DescribeFirmwareVerdict(firmware)}",
|
||||
data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -501,6 +511,50 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="UefiSecureBootCertSnapshot"/> onto the <c>secureboot_status</c>
|
||||
/// payload. Certificate booleans are only emitted when the respective variable read
|
||||
/// succeeded — an absent boolean means "firmware state unknown", never "missing".
|
||||
/// </summary>
|
||||
internal static void AppendFirmwareCertFields(Dictionary<string, object> data, UefiSecureBootCertSnapshot firmware)
|
||||
{
|
||||
if (firmware == null) return;
|
||||
|
||||
data["uefiFirmwareReadStatus"] = firmware.DbStatus != UefiSecureBootCertReader.StatusOk
|
||||
? firmware.DbStatus
|
||||
: firmware.KekStatus;
|
||||
|
||||
if (firmware.DbStatus == UefiSecureBootCertReader.StatusOk)
|
||||
{
|
||||
data["uefiDbHasWindowsUefiCa2023"] = firmware.DbHasWindowsUefiCa2023;
|
||||
data["uefiDbHasMicrosoftUefiCa2023"] = firmware.DbHasMicrosoftUefiCa2023;
|
||||
data["uefiDbHasMicrosoftOptionRomCa2023"] = firmware.DbHasMicrosoftOptionRomCa2023;
|
||||
data["uefiDbHasWindowsProductionPca2011"] = firmware.DbHasWindowsProductionPca2011;
|
||||
data["uefiDbHasMicrosoftUefiCa2011"] = firmware.DbHasMicrosoftUefiCa2011;
|
||||
|
||||
// One-sided marker consumed by ANALYZE-SEC-001's not_exists precondition.
|
||||
// The engine's not_exists passes only while the field is absent/empty, so the
|
||||
// marker must be OMITTED (never emitted as false) unless the firmware db
|
||||
// positively confirms the 2023 certificate — old agents and unconfirmed
|
||||
// devices then keep evaluating the rule.
|
||||
if (firmware.DbHasWindowsUefiCa2023)
|
||||
data["uefiCa2023FirmwareConfirmed"] = true;
|
||||
}
|
||||
|
||||
if (firmware.KekStatus == UefiSecureBootCertReader.StatusOk)
|
||||
{
|
||||
data["uefiKekHasMicrosoftKek2kCa2023"] = firmware.KekHasMicrosoftKek2kCa2023;
|
||||
data["uefiKekHasMicrosoftKekCa2011"] = firmware.KekHasMicrosoftKekCa2011;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string DescribeFirmwareVerdict(UefiSecureBootCertSnapshot firmware)
|
||||
{
|
||||
if (firmware == null) return "unavailable";
|
||||
if (firmware.DbStatus != UefiSecureBootCertReader.StatusOk) return firmware.DbStatus ?? "unavailable";
|
||||
return firmware.DbHasWindowsUefiCa2023 ? "CA2023 confirmed" : "CA2023 missing";
|
||||
}
|
||||
|
||||
private void CollectBitLockerStatus()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -235,6 +235,80 @@ public class RuleEnginePreconditionTests
|
||||
Assert.Single(outcome.Results);
|
||||
}
|
||||
|
||||
// ===== ANALYZE-SEC-001 v3: firmware-confirmation marker (uefiCa2023FirmwareConfirmed) =====
|
||||
|
||||
/// <summary>
|
||||
/// v3 core fix: when the agent's direct UEFI db read confirms the 2023 certificate, the
|
||||
/// one-sided marker is present and the not_exists precondition suppresses the rule — even
|
||||
/// though the servicing status ("unknown") would still satisfy both required conditions.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sec001V3_FirmwareConfirmed_MarkerSuppressesRule()
|
||||
{
|
||||
var rule = MakeSecureBootRuleV3();
|
||||
var events = new List<EnrollmentEvent>
|
||||
{
|
||||
HardwareSpecEvent(isVirtualMachine: "false"),
|
||||
SecureBootStatusEvent(uefiCA2023Status: "unknown", secureBootEnabled: "true",
|
||||
extraData: new Dictionary<string, object>
|
||||
{
|
||||
["uefiDbHasWindowsUefiCa2023"] = true,
|
||||
["uefiCa2023FirmwareConfirmed"] = true
|
||||
})
|
||||
};
|
||||
|
||||
var outcome = await RunAsync(rule, events);
|
||||
|
||||
Assert.Empty(outcome.Results);
|
||||
Assert.Single(outcome.EvaluatedRules);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backward compatibility: an old-agent payload (no firmware fields at all) must behave
|
||||
/// exactly like v2 — marker absent → precondition passes → rule fires on the servicing
|
||||
/// status, at base confidence (no firmware factors).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sec001V3_OldAgentPayload_FiresLikeV2()
|
||||
{
|
||||
var rule = MakeSecureBootRuleV3();
|
||||
var events = new List<EnrollmentEvent>
|
||||
{
|
||||
HardwareSpecEvent(isVirtualMachine: "false"),
|
||||
SecureBootStatusEvent(uefiCA2023Status: "unknown", secureBootEnabled: "true")
|
||||
};
|
||||
|
||||
var outcome = await RunAsync(rule, events);
|
||||
|
||||
Assert.Single(outcome.Results);
|
||||
Assert.Equal(70, outcome.Results[0].ConfidenceScore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// New agent, firmware-verified missing: the marker is omitted (never emitted as false),
|
||||
/// so the rule fires and the firmware evidence raises confidence (+20 db, +10 KEK).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Sec001V3_FirmwareVerifiedMissing_RaisesConfidence()
|
||||
{
|
||||
var rule = MakeSecureBootRuleV3();
|
||||
var events = new List<EnrollmentEvent>
|
||||
{
|
||||
HardwareSpecEvent(isVirtualMachine: "false"),
|
||||
SecureBootStatusEvent(uefiCA2023Status: "unknown", secureBootEnabled: "true",
|
||||
extraData: new Dictionary<string, object>
|
||||
{
|
||||
["uefiDbHasWindowsUefiCa2023"] = false,
|
||||
["uefiKekHasMicrosoftKek2kCa2023"] = false
|
||||
})
|
||||
};
|
||||
|
||||
var outcome = await RunAsync(rule, events);
|
||||
|
||||
Assert.Single(outcome.Results);
|
||||
Assert.Equal(100, outcome.Results[0].ConfidenceScore);
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
private static AnalyzeRule MakeSecureBootRule(List<RulePrecondition>? preconditions)
|
||||
@@ -258,6 +332,29 @@ public class RuleEnginePreconditionTests
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the shipped ANALYZE-SEC-001 v3.0.0 (rules/analyze/ANALYZE-SEC-001.json):
|
||||
/// VM-skip + firmware-confirmed-marker preconditions, the two required servicing
|
||||
/// conditions, and the optional firmware conditions with their confidence weights.
|
||||
/// Keep in sync when the rule JSON changes.
|
||||
/// </summary>
|
||||
private static AnalyzeRule MakeSecureBootRuleV3()
|
||||
{
|
||||
var rule = MakeSecureBootRule(preconditions: new List<RulePrecondition>
|
||||
{
|
||||
new() { Source = "event_data", EventType = "hardware_spec", DataField = "isVirtualMachine", Operator = "equals", Value = "false" },
|
||||
new() { Source = "event_data", EventType = "secureboot_status", DataField = "uefiCa2023FirmwareConfirmed", Operator = "not_exists", Value = "" }
|
||||
});
|
||||
rule.Conditions.Add(new RuleCondition { Signal = "db_ca2023_firmware_missing", Source = "event_data", EventType = "secureboot_status", DataField = "uefiDbHasWindowsUefiCa2023", Operator = "equals", Value = "false", Required = false });
|
||||
rule.Conditions.Add(new RuleCondition { Signal = "kek_2023_missing", Source = "event_data", EventType = "secureboot_status", DataField = "uefiKekHasMicrosoftKek2kCa2023", Operator = "equals", Value = "false", Required = false });
|
||||
rule.ConfidenceFactors = new List<ConfidenceFactor>
|
||||
{
|
||||
new() { Signal = "db_ca2023_firmware_missing", Condition = "exists", Weight = 20 },
|
||||
new() { Signal = "kek_2023_missing", Condition = "exists", Weight = 10 }
|
||||
};
|
||||
return rule;
|
||||
}
|
||||
|
||||
private static EnrollmentEvent HardwareSpecEvent(string isVirtualMachine) => new()
|
||||
{
|
||||
EventId = Guid.NewGuid().ToString(),
|
||||
@@ -280,20 +377,29 @@ public class RuleEnginePreconditionTests
|
||||
Data = new Dictionary<string, object> { ["edition"] = edition }
|
||||
};
|
||||
|
||||
private static EnrollmentEvent SecureBootStatusEvent(string uefiCA2023Status, string secureBootEnabled) => new()
|
||||
private static EnrollmentEvent SecureBootStatusEvent(string uefiCA2023Status, string secureBootEnabled,
|
||||
Dictionary<string, object>? extraData = null)
|
||||
{
|
||||
EventId = Guid.NewGuid().ToString(),
|
||||
TenantId = TenantId,
|
||||
SessionId = SessionId,
|
||||
EventType = "secureboot_status",
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Sequence = 3,
|
||||
Data = new Dictionary<string, object>
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["uefiCA2023Status"] = uefiCA2023Status,
|
||||
["uefiSecureBootEnabled"] = secureBootEnabled
|
||||
}
|
||||
};
|
||||
};
|
||||
if (extraData != null)
|
||||
foreach (var kvp in extraData)
|
||||
data[kvp.Key] = kvp.Value;
|
||||
|
||||
return new EnrollmentEvent
|
||||
{
|
||||
EventId = Guid.NewGuid().ToString(),
|
||||
TenantId = TenantId,
|
||||
SessionId = SessionId,
|
||||
EventType = "secureboot_status",
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Sequence = 3,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<AnalysisOutcome> RunAsync(AnalyzeRule rule, List<EnrollmentEvent> events)
|
||||
{
|
||||
|
||||
@@ -249,6 +249,12 @@ export const DEVICE_PROPERTIES_CATALOG = {
|
||||
},
|
||||
secureboot_status: {
|
||||
'secureboot_status.uefiSecureBootEnabled': { type: 'boolean', description: 'Secure Boot enabled' },
|
||||
'secureboot_status.uefiCA2023Status': { type: 'string', description: 'Windows servicing rollout status of the UEFI CA 2023 certificate (registry; "Updated" = rollout verified). Lags behind actual firmware state — prefer the firmware fields below' },
|
||||
'secureboot_status.uefiFirmwareReadStatus': { type: 'string', description: 'Result of the direct UEFI variable read: ok, not_uefi (legacy BIOS), variable_not_found, privilege_denied, or error_<code>' },
|
||||
'secureboot_status.uefiDbHasWindowsUefiCa2023': { type: 'boolean', description: 'Firmware db contains "Windows UEFI CA 2023" (authoritative 2023-cert deployment state; read directly from UEFI, only present when the read succeeded)' },
|
||||
'secureboot_status.uefiDbHasWindowsProductionPca2011': { type: 'boolean', description: 'Firmware db still contains the expired "Microsoft Windows Production PCA 2011"' },
|
||||
'secureboot_status.uefiKekHasMicrosoftKek2kCa2023': { type: 'boolean', description: 'Firmware KEK contains "Microsoft Corporation KEK 2K CA 2023" (required to keep receiving db/dbx updates after the 2011 KEK expiry)' },
|
||||
'secureboot_status.uefiCa2023FirmwareConfirmed': { type: 'boolean', description: 'One-sided marker: present (true) only when the firmware db positively confirms the 2023 certificate — suppresses ANALYZE-SEC-001 via not_exists precondition. Never emitted as false' },
|
||||
},
|
||||
bitlocker_status: {
|
||||
'bitlocker_status.systemDriveProtected': { type: 'boolean', description: 'System drive (C:) BitLocker protection' },
|
||||
|
||||
+19
-1
@@ -297,7 +297,25 @@ export default function DeviceDetailsCard({ events, latestAgentVersion }: { even
|
||||
{(bitLockerStatus || secureBootStatus || tpmStatus) && (
|
||||
<DetailSection title="Security">
|
||||
{secureBootStatus && (
|
||||
<DetailRow label="SecureBoot" value={secureBootStatus.uefiSecureBootEnabled ? "Enabled" : "Disabled"} />
|
||||
<>
|
||||
<DetailRow label="SecureBoot" value={secureBootStatus.uefiSecureBootEnabled ? "Enabled" : "Disabled"} />
|
||||
{/* Firmware verdict (direct UEFI db read, new agents) beats the servicing
|
||||
registry status, which only reflects the Windows Update rollout. */}
|
||||
{secureBootStatus.uefiDbHasWindowsUefiCa2023 !== undefined ? (
|
||||
<DetailRow
|
||||
label="UEFI CA 2023 (firmware)"
|
||||
value={secureBootStatus.uefiDbHasWindowsUefiCa2023 ? "Deployed" : "Missing"}
|
||||
/>
|
||||
) : secureBootStatus.uefiFirmwareReadStatus ? (
|
||||
<DetailRow
|
||||
label="UEFI CA 2023 (firmware)"
|
||||
value={`Unknown (${secureBootStatus.uefiFirmwareReadStatus})`}
|
||||
/>
|
||||
) : null}
|
||||
{secureBootStatus.uefiCA2023Status && (
|
||||
<DetailRow label="UEFI CA 2023 (servicing)" value={`${secureBootStatus.uefiCA2023Status}`} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{tpmStatus && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user