yuzu

bitlocker

What it doesDisk encryption status — BitLocker, LUKS, FileVault
Version0.2.0
KindCollector · read-only · gathered (security.encryption.state)
PlatformsWindows ✅ · macOS ✅ · Linux ✅
Actionsstate (definition security.encryption.state)
Securitysecurable Security · operation Read · risk Medium · dispatch ReadOnly · approval gate None
Rolesexecute: endpoint-admin, endpoint-operator, security-admin · author: content-author

How it works

The plugin has one action, state, which branches to a different acquisition path per OS behind #ifdef and always emits an honest “nothing found” row rather than silence. On Windows it queries Win32_EncryptableVolume in-process over WMI for every BitLocker-encryptable volume, then makes two further in-process WMI method calls per volume — GetConversionStatus and GetEncryptionMethod; either call failing degrades only that one field to Unknown rather than aborting the scan. On Linux it enumerates raw crypto_LUKS superblocks via libblkid and separately reads /sys/class/block/dm-*/dm/uuid to learn which of those devices are currently open (mapped) — no subprocess either way. On macOS it runs fdesetup status for a global on/off signal and diskutil apfs list for per-volume detail, both as direct argv through the bounded subprocess runner with a 20-second deadline, then parses the diskutil transcript with a pure text parser. It is deliberately not an encrypt/decrypt/enable action — every leg is a pure status read — and it never fabricates a value it can’t verify: an unreadable field reports unknown/Unknown rather than a guess.

flowchart LR
  OP[Operator / workflow] --> SRV[Server<br/>authz: Security.Read]
  SRV -- gRPC mTLS --> HOST[Agent plugin host] --> EX[bitlocker.execute]
  EX --> WIN[Windows leg<br/>Win32_EncryptableVolume WMI +<br/>GetConversionStatus / GetEncryptionMethod]
  EX --> MAC[macOS leg<br/>fdesetup status + diskutil apfs list]
  EX --> LIN[Linux leg<br/>libblkid crypto_LUKS +<br/>/sys/class/block/dm-*]
  WIN & MAC & LIN --> ROWS[rows + typed result status] --> RS[(ResponseStore)] --> API[REST /api/responses]

OS capability

ActionWindowsmacOSLinux
state✅ supported · rung 1 · wmi_encryptable_volume✅ supported · rung 2 · fdesetup+diskutil✅ supported · rung 1 · libblkid+sysfs

Privileges and prerequisites

OSRuns asExtra grant neededMeasuredIf the read is refused
WindowsLocalSystem today (#1442; intended target is the virtual service account NT SERVICE\YuzuAgent)The WMI namespace root\CIMV2\Security\MicrosoftVolumeEncryption is admin-only per Microsoft’s docs (bitlocker_plugin.cpp:97-99); docs/agent-privilege-model.md names the Administrators group at both its rows for bitlocker.state (lines 99 and 122), matching that ACL — see Caveat 42026-09-07, bare-metal, SYSTEM (docs/samples/windows.txt:1)typed sentinel row error|permission_denied: administrator privilege required to read BitLocker status (bitlocker_plugin.cpp:114-115); execute() returns 1
macOSroot — the shipped LaunchDaemon has no UserName key (docs/agent-privilege-model.md:15)none observed in this plugin’s code — both fdesetup and diskutil calls are unprivileged reads2026-09-07, bare-metal, euid 501 (jsmith) — unprivileged (docs/samples/macos.txt:1)no distinct denial path: a timed-out or failed tool call still returns whatever text it produced, logged via spdlog::warn (bitlocker_plugin.cpp:78-83); the parser reports filevault|unknown|<diagnostic> rather than a typed permission error
Linuxyuzu, an unprivileged system account by design (docs/agent-privilege-model.md:47-52); the sample below was captured as root in a container, not the account’s real identitynone — libblkid TYPE probing and /sys/class/block/dm-*/dm/uuid reads need no elevated capability (not listed in either agent-privilege-model.md table)2026-09-06, container, euid 0 (docs/samples/linux.txt:1) — captured privileged, not representative of the production accountno distinct denial path: a blkid_get_cache failure or unreadable /sys entry silently yields fewer devices/mappings (bitlocker_plugin.cpp:187-189, 253-256), surfacing as volume|none|no_encrypted_volumes if nothing is found

Binaries/subprocesses: macOS only — /usr/bin/fdesetup status and /usr/bin/diskutil apfs list, both invoked as direct argv through the bounded subprocess runner (run_bounded_subprocess) with a 20-second deadline (bitlocker_plugin.cpp:68-77, 299, 317). Windows and Linux use no subprocess at all — in-process WMI/COM and libblkid//sys reads only. No network access on any OS.

Data contract

Inputs

The action takes no parameters.

Outputs

Rows are pipe-delimited, but the row shape is genuinely different per OS, not one schema with blanks. Windows emits volume|<drive>|<conversion>|<pct>|<method>|<protection> — six fields, one row per encryptable volume (bitlocker_windows_wmi.hpp format_volume_row). Linux emits volume|<device>|crypto_LUKS|<active|inactive> — four fields, one row per LUKS superblock (bitlocker_linux_parsers.hpp format_volume_row). macOS emits a single global filevault|enabled / filevault|disabled / filevault|unknown|<diagnostic> row, followed by zero or more volume|<label>|<type>|<encrypted|not_encrypted|unknown> rows, one per APFS volume diskutil reported (bitlocker_plugin.cpp:300-333). The YAML’s five declared result.columns (volume, conversion_status, percentage_encrypted, encryption_method, protection_status) match the Windows row’s five post-prefix fields exactly; Linux and macOS populate only the volume identifier field from that set — their other fields carry a type/role/state string, not a BitLocker-shaped value, so this table’s Platforms column is genuinely narrow rather than a stylistic omission. A host that finds nothing still emits one row (volume|none|no_encryptable_volumes on Windows, volume|none|no_encrypted_volumes on Linux) rather than zero rows, so an empty result is never misread as “the query didn’t run”.

security.encryption.state — volume|conversion_status|percentage_encrypted|encryption_method|protection_status

FieldTypeValuesAvailableExampleDescription
volumestring-Windows, Linux, macOSC:Per-OS volume identifier: drive letter on Windows, block-device basename on Linux, or the APFS volume’s display label on macOS. Values: free text.
conversion_statusstringFully Decrypted Fully Encrypted Encryption In Progress Decryption In Progress Encryption Paused Decryption Paused UnknownWindowsFully DecryptedBitLocker’s live conversion/encryption state for the volume, decoded from Win32_EncryptableVolume::GetConversionStatus(). Populated only on Windows; Linux and macOS rows do not carry this field.
percentage_encryptedstring-Windows0%Percentage of the volume currently encrypted, from BitLocker’s EncryptionPercentage out-parameter. Populated only on Windows. Values: <integer>% or unknown.
encryption_methodstringNone AES 128 with Diffuser AES 256 with Diffuser AES 128 AES 256 Hardware Encryption XTS-AES 128 XTS-AES 256 UnknownWindowsNoneThe disk-encryption cipher/mode BitLocker is using for the volume, from GetEncryptionMethod(). Populated only on Windows.
protection_statusstringProtection On Protection Off Protection UnknownWindowsProtection OffWhether BitLocker protection is currently on or off for the volume, from Win32_EncryptableVolume::ProtectionStatus. Populated only on Windows.

Result status

StatusCompletenessProvenanceWhen
OKPARTIALbitlocker:volume_enumeration_truncatedWindows only — the shared bounded-WMI-query row cap truncated the encryptable-volume list (bitlocker_plugin.cpp:136-137)

This is the plugin’s only set_result_status call. Every other run leaves the status unset: the agent records UNDECLARED, and all three sample captures show UNDECLARED / UNKNOWN / for a clean run.

Where the data goes

Sample output

Windows — captured: windows Microsoft Windows NT 10.0.26200.0 x64 · bare-metal · 2026-09-07 · SYSTEM · leg-hash ba3cbe6a5b56

== action=state
volume|C:|Fully Decrypted|0%|None|Protection Off
volume|D:|Fully Decrypted|0%|None|Protection Off
[result_status] UNDECLARED / UNKNOWN

macOS — captured: macos macOS 26.6.2 arm64 · bare-metal · 2026-09-07 · euid 501 (jsmith) · leg-hash ba3cbe6a5b56

== action=state
filevault|enabled
[result_status] UNDECLARED / UNKNOWN

Linux — captured: linux Debian GNU/Linux 13 (trixie) aarch64 · container · 2026-09-06 · euid 0 · leg-hash ba3cbe6a5b56

== action=state
volume|none|no_encrypted_volumes
[result_status] UNDECLARED / UNKNOWN

Caveats and known gaps

  1. The Linux fixtures backing the parser are synthetic, not captured. bitlocker_linux_parsers.hpp’s file header states a privileged Docker loopback-LUKS capture was attempted for this migration but no Docker daemon was reachable in the sandbox it was written in, so the dm-crypt /sys/class/block/dm-*/dm/uuid fixtures are built to the documented CRYPT-<SUBSYS>-<uuid>-<name> shape rather than a real capture. The live sample above (docs/samples/linux.txt) has zero LUKS volumes, so it does not exercise this code path either.
  2. The YAML’s result.columns are Windows-shaped only. conversion_status, percentage_encrypted, encryption_method, and protection_status are populated exclusively by the Windows leg; Linux and macOS rows carry a different field count and different (non-BitLocker) values in those positions. See the Outputs paragraph above.
  3. macOS’s global FileVault row is invisible to the fleet chart. The whereField: 0, whereEquals: volume visualization filter excludes the filevault|... row emitted once per macOS run, because that row’s first field is filevault, not volume (tracked generally under issue #626, which covers the same plugin’s coarser labelField limitation).
  4. The Windows grant is the Administrators group, not a privilege. Both privilege-model rows (docs/agent-privilege-model.md:99 and :122) name the Administrators group, matching the admin-ACL’d root\CIMV2\Security\MicrosoftVolumeEncryption namespace the plugin queries (bitlocker_windows_wmi.hpp:270-278); an unprivileged caller gets the typed permission_denied sentinel (bitlocker_plugin.cpp:114-115), not a raw WMI error.
  5. DeviceIoControl-style WMI calls have no explicit per-call timeout. run_bounded_wmi_query / exec_object_method are the shared helper’s responsibility, not this plugin’s; a wedged WMI provider is bounded only by whatever deadline wmi_bounded.hpp applies, which this plugin’s code does not override.

Source and tests