Add ability to determine Bitlocker protectors (#31090)

For #31062:

Added new Fleetd table 'bitlocker_key_protectors' that can be used for
determining whether a TPM PIN protector is setup in a volume.
This commit is contained in:
Juan Fernandez 2025-07-24 18:30:55 -04:00 committed by GitHub
parent 5363ce1382
commit 128ee07cae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 253 additions and 1 deletions

View file

@ -0,0 +1 @@
* Added new Fleetd table 'bitlocker_key_protectors' that returns what key protectors are setup on the system.

View file

@ -0,0 +1,100 @@
//go:build windows
// +build windows
package bitlocker_key_protectors
import (
"context"
"encoding/json"
"fmt"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/tablehelpers"
"github.com/osquery/osquery-go/plugin/table"
"github.com/rs/zerolog"
)
const name = "bitlocker_key_protectors"
type BitLockerVolume struct {
MountPoint string `json:"MountPoint"`
KeyProtectorType int `json:"KeyProtectorType"`
}
type Table struct {
logger zerolog.Logger
}
func TablePlugin(logger zerolog.Logger) *table.Plugin {
columns := []table.ColumnDefinition{
table.TextColumn("drive_letter"),
table.IntegerColumn("key_protector_type"),
}
t := &Table{
logger: logger.With().Str("table", name).Logger(),
}
return table.NewPlugin(name, columns, t.generate)
}
func (t *Table) generate(
ctx context.Context,
queryContext table.QueryContext,
) ([]map[string]string, error) {
cmd := `Get-BitlockerVolume | ForEach-Object {
$vol = $_
$vol.KeyProtector | ForEach-Object {
[PSCustomObject]@{
MountPoint = $vol.MountPoint
KeyProtectorType = $_.KeyProtectorType
}
}
} | ConvertTo-Json`
output, err := tablehelpers.Exec(
ctx,
t.logger,
30,
[]string{"powershell.exe"},
[]string{"-NoProfile", "-Command", cmd},
true,
)
if err != nil {
t.logger.Info().Err(err).Msg("failed to get BitLocker volume data")
return nil, fmt.Errorf("failed to get BitLocker volume data: %w", err)
}
return t.parseOutput(output)
}
func (t *Table) parseOutput(output []byte) ([]map[string]string, error) {
if len(output) == 0 {
return nil, nil
}
var results []map[string]string
// The PS cmdlet might return a list if the system has more than one volume,
// so first we try to parse it as an array ...
var volumes []BitLockerVolume
if err := json.Unmarshal(output, &volumes); err != nil {
// If array parsing fails, try parsing as single object ...
var volume BitLockerVolume
if err := json.Unmarshal(output, &volume); err != nil {
t.logger.Info().Err(err).Msg("failed to parse BitLocker volume data")
return nil, fmt.Errorf("failed to parse BitLocker volume data: %w", err)
}
volumes = []BitLockerVolume{volume}
}
for _, volume := range volumes {
results = append(results, map[string]string{
"drive_letter": volume.MountPoint,
"key_protector_type": fmt.Sprintf("%d", volume.KeyProtectorType),
})
}
return results, nil
}

View file

@ -0,0 +1,100 @@
//go:build windows
// +build windows
package bitlocker_key_protectors
import (
"github.com/stretchr/testify/require"
"reflect"
"testing"
"github.com/rs/zerolog"
)
func TestTable_parseOutput(t *testing.T) {
testCases := []struct {
name string
input []byte
expected []map[string]string
}{
{
name: "as array",
input: []byte(`
[
{
"MountPoint": "C:",
"KeyProtectorType": 3
},
{
"MountPoint": "C:",
"KeyProtectorType": 1
}
]`),
expected: []map[string]string{
{
"drive_letter": "C:",
"key_protector_type": "3",
},
{
"drive_letter": "C:",
"key_protector_type": "1",
},
},
},
{
name: "as a single object",
input: []byte(`
{
"MountPoint": "C:",
"KeyProtectorType": 3
}
`),
expected: []map[string]string{
{
"drive_letter": "C:",
"key_protector_type": "3",
},
},
},
}
table := &Table{
logger: zerolog.New(zerolog.NewTestWriter(t)),
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
results, err := table.parseOutput(testCase.input)
require.NoError(t, err)
if !reflect.DeepEqual(results, testCase.expected) {
t.Errorf("parseOutput() = %v, want %v", results, testCase.expected)
}
})
}
}
func TestTable_parseOutput_InvalidJSON(t *testing.T) {
table := &Table{
logger: zerolog.New(zerolog.NewTestWriter(t)),
}
_, err := table.parseOutput([]byte(`invalid json`))
require.Error(t, err)
}
func TestTable_parseOutput_EmptyInput(t *testing.T) {
table := &Table{
logger: zerolog.New(zerolog.NewTestWriter(t)),
}
testCases := [][]byte{
[]byte(""),
[]byte(`[]`),
}
for _, testCase := range testCases {
results, err := table.parseOutput(testCase)
require.NoError(t, err)
require.Empty(t, results)
}
}

View file

@ -4,7 +4,7 @@ package table
import (
"fmt"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/bitlocker_key_protectors"
cisaudit "github.com/fleetdm/fleet/v4/orbit/pkg/table/cis_audit"
mdmbridge "github.com/fleetdm/fleet/v4/orbit/pkg/table/mdm"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/windowsupdatetable"
@ -20,6 +20,8 @@ func PlatformTables(_ PluginOpts) ([]osquery.OsqueryPlugin, error) {
// Fleet tables
table.NewPlugin("cis_audit", cisaudit.Columns(), cisaudit.Generate),
bitlocker_key_protectors.TablePlugin(log.Logger),
windowsupdatetable.TablePlugin(windowsupdatetable.UpdatesTable, log.Logger), // table name is "windows_updates"
}

View file

@ -2419,6 +2419,32 @@
],
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/bitlocker_info.yml"
},
{
"name": "bitlocker_key_protectors",
"description": "Returns what BitLocker key protectors are setup on the system.",
"evented": false,
"notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
"platforms": [
"windows"
],
"columns": [
{
"name": "drive_letter",
"description": "The drive letter of the volume.",
"required": false,
"type": "text"
},
{
"name": "key_protector_type",
"required": false,
"type": "integer",
"description": "An unsigned integer that specifies the type of key protector.\nSee https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectors-win32-encryptablevolume#parameters\nfor a list of possible values.\n"
}
],
"examples": "Determine whether 'C:' encryption key is protected by TPM and PIN\n```\nSELECT 1 FROM bitlocker_key_protectors WHERE drive_letter = 'C:' AND key_protector_type = 4;\n```",
"url": "https://fleetdm.com/tables/bitlocker_key_protectors",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/bitlocker_key_protectors.yml"
},
{
"name": "block_devices",
"description": "Block (buffered access) device file nodes: disks, ramdisks, and DMG containers.",

View file

@ -0,0 +1,23 @@
name: bitlocker_key_protectors
description: Returns what BitLocker key protectors are setup on the system.
evented: false
notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
platforms:
- windows
columns:
- name: drive_letter
description: The drive letter of the volume.
required: false
type: text
- name: key_protector_type
required: false
type: integer
description: |
An unsigned integer that specifies the type of key protector.
See https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectors-win32-encryptablevolume#parameters
for a list of possible values.
examples: |-
Determine whether 'C:' encryption key is protected by TPM and PIN
```
SELECT 1 FROM bitlocker_key_protectors WHERE drive_letter = 'C:' AND key_protector_type = 4;
```