mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
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:
parent
5363ce1382
commit
128ee07cae
6 changed files with 253 additions and 1 deletions
1
orbit/changes/28133-require-bitlocker-pin
Normal file
1
orbit/changes/28133-require-bitlocker-pin
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added new Fleetd table 'bitlocker_key_protectors' that returns what key protectors are setup on the system.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
23
schema/tables/bitlocker_key_protectors.yml
Normal file
23
schema/tables/bitlocker_key_protectors.yml
Normal 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;
|
||||
```
|
||||
Loading…
Reference in a new issue