From 87fe00db71d3feb8ac5426dfda07a02dbf1b62ce Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:23:59 -0500 Subject: [PATCH] Create new Fleet osquery extension table to read escrowed FileVault key (#12198) --- changes/issue-11861-filevault-key | 2 + orbit/pkg/table/extension_darwin.go | 2 + .../filevault_prk/filevault_prk_darwin.go | 38 ++ schema/osquery_fleet_schema.json | 613 +++++++++--------- schema/tables/filevault_prk.yml | 11 + server/service/osquery_test.go | 3 +- server/service/osquery_utils/queries.go | 137 +++- server/service/osquery_utils/queries_test.go | 121 +++- 8 files changed, 576 insertions(+), 351 deletions(-) create mode 100644 changes/issue-11861-filevault-key create mode 100644 orbit/pkg/table/filevault_prk/filevault_prk_darwin.go create mode 100644 schema/tables/filevault_prk.yml diff --git a/changes/issue-11861-filevault-key b/changes/issue-11861-filevault-key new file mode 100644 index 0000000000..75adc53f85 --- /dev/null +++ b/changes/issue-11861-filevault-key @@ -0,0 +1,2 @@ +- Fixed bug when reading filevault key in osquery and created new Fleet osquery + extension table to read the file directly rather than via filelines table. diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go index b3856391a0..1849c850f3 100644 --- a/orbit/pkg/table/extension_darwin.go +++ b/orbit/pkg/table/extension_darwin.go @@ -8,6 +8,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/apfs" "github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/corestorage" "github.com/fleetdm/fleet/v4/orbit/pkg/table/dscl" + "github.com/fleetdm/fleet/v4/orbit/pkg/table/filevault_prk" "github.com/fleetdm/fleet/v4/orbit/pkg/table/firmware_eficheck_integrity_check" "github.com/fleetdm/fleet/v4/orbit/pkg/table/nvram_info" "github.com/fleetdm/fleet/v4/orbit/pkg/table/pmset" @@ -44,6 +45,7 @@ func PlatformTables() []osquery.OsqueryPlugin { table.NewPlugin("apfs_physical_stores", apfs.PhysicalStoresColumns(), apfs.PhysicalStoresGenerate), table.NewPlugin("corestorage_logical_volumes", corestorage.LogicalVolumesColumns(), corestorage.LogicalVolumesGenerate), table.NewPlugin("corestorage_logical_volume_families", corestorage.LogicalVolumeFamiliesColumns(), corestorage.LogicalVolumeFamiliesGenerate), + table.NewPlugin("filevault_prk", filevault_prk.Columns(), filevault_prk.Generate), // Macadmins extension tables table.NewPlugin("filevault_users", filevaultusers.FileVaultUsersColumns(), filevaultusers.FileVaultUsersGenerate), diff --git a/orbit/pkg/table/filevault_prk/filevault_prk_darwin.go b/orbit/pkg/table/filevault_prk/filevault_prk_darwin.go new file mode 100644 index 0000000000..d72c46d774 --- /dev/null +++ b/orbit/pkg/table/filevault_prk/filevault_prk_darwin.go @@ -0,0 +1,38 @@ +//go:build darwin +// +build darwin + +package filevault_prk + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io/fs" + "os" + + "github.com/osquery/osquery-go/plugin/table" +) + +// Columns is the schema of the table. +func Columns() []table.ColumnDefinition { + return []table.ColumnDefinition{ + table.TextColumn("base64_encrypted"), + } +} + +// Generate is called to return the results for the table at query time. +// +// Constraints for generating can be retrieved from the queryContext. +func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { + encryptedKey, err := os.ReadFile("/var/db/FileVaultPRK.dat") + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, nil + } + return nil, fmt.Errorf("generate failed: %w", err) + } + encoded := base64.StdEncoding.EncodeToString(encryptedKey) + + return []map[string]string{{"base64_encrypted": encoded}}, nil +} diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json index ac47697a2c..52e26e1b2e 100644 --- a/schema/osquery_fleet_schema.json +++ b/schema/osquery_fleet_schema.json @@ -27212,73 +27212,6 @@ ], "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/yum_sources.yml" }, - { - "name": "apfs_physical_stores", - "platforms": [ - "darwin" - ], - "description": "Information about APFS physical stores from the `diskutil apfs list -plist` command.", - "columns": [ - { - "name": "container_uuid", - "type": "text", - "required": false, - "description": "The UUID of the APFS Contianer" - }, - { - "name": "container_designated_physical_store", - "type": "text", - "required": false, - "description": "The disk displayed as the backing store of the container. There may be multiple,\nuse `apfs_physical_stores` to see all actual physical stores\n" - }, - { - "name": "container_reference", - "type": "text", - "required": false, - "description": "The current reference for the APFS container, e.g. \"disk3\"" - }, - { - "name": "container_fusion", - "type": "text", - "required": false, - "description": "Whether this container is on a \"fusion drive\" (i.e. SSHD)" - }, - { - "name": "container_capacity_ceiling", - "type": "bigint", - "required": false, - "description": "The total amount of space in the container" - }, - { - "name": "container_capacity_free", - "type": "bigint", - "required": false, - "description": "The amount of remaining free space in the container" - }, - { - "name": "uuid", - "type": "text", - "required": false, - "description": "The UUID of the physical store" - }, - { - "name": "identifier", - "type": "text", - "required": false, - "description": "The current identifier of the physical store (e.g. disk1s2)" - }, - { - "name": "size", - "type": "bigint", - "required": false, - "description": "The size of the physical store in byptes" - } - ], - "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", - "evented": false, - "url": "https://fleetdm.com/tables/apfs_physical_stores", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/apfs_physical_stores.yml" - }, { "name": "apfs_volumes", "platforms": [ @@ -27394,6 +27327,73 @@ "url": "https://fleetdm.com/tables/apfs_volumes", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/apfs_volumes.yml" }, + { + "name": "apfs_physical_stores", + "platforms": [ + "darwin" + ], + "description": "Information about APFS physical stores from the `diskutil apfs list -plist` command.", + "columns": [ + { + "name": "container_uuid", + "type": "text", + "required": false, + "description": "The UUID of the APFS Contianer" + }, + { + "name": "container_designated_physical_store", + "type": "text", + "required": false, + "description": "The disk displayed as the backing store of the container. There may be multiple,\nuse `apfs_physical_stores` to see all actual physical stores\n" + }, + { + "name": "container_reference", + "type": "text", + "required": false, + "description": "The current reference for the APFS container, e.g. \"disk3\"" + }, + { + "name": "container_fusion", + "type": "text", + "required": false, + "description": "Whether this container is on a \"fusion drive\" (i.e. SSHD)" + }, + { + "name": "container_capacity_ceiling", + "type": "bigint", + "required": false, + "description": "The total amount of space in the container" + }, + { + "name": "container_capacity_free", + "type": "bigint", + "required": false, + "description": "The amount of remaining free space in the container" + }, + { + "name": "uuid", + "type": "text", + "required": false, + "description": "The UUID of the physical store" + }, + { + "name": "identifier", + "type": "text", + "required": false, + "description": "The current identifier of the physical store (e.g. disk1s2)" + }, + { + "name": "size", + "type": "bigint", + "required": false, + "description": "The size of the physical store in byptes" + } + ], + "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", + "evented": false, + "url": "https://fleetdm.com/tables/apfs_physical_stores", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/apfs_physical_stores.yml" + }, { "name": "authdb", "platforms": [ @@ -27820,6 +27820,51 @@ "url": "https://fleetdm.com/tables/dscl", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/dscl.yml" }, + { + "name": "filevault_prk", + "platforms": [ + "darwin" + ], + "description": "Returns contents of `/var/db/FileVaultPRK.dat`.", + "columns": [ + { + "name": "base64_encrypted", + "type": "text", + "required": false, + "description": "The base64-encoded contents of the encrypted FileVault personal recovery key stored at `/var/db/FileVaultPRK.dat` (see also https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow)" + } + ], + "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", + "evented": false, + "url": "https://fleetdm.com/tables/filevault_prk", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/filevault_prk.yml" + }, + { + "name": "filevault_users", + "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", + "description": "Information on the users able to unlock the current boot volume if protected with FileVault.", + "platforms": [ + "darwin" + ], + "evented": false, + "examples": "List the usernames able to unlock and boot a computer protected by FileVault, joined to [users.username](http://fleetdm.com/tables/users) to obtain the description of the operating system account that owns it.\n```\nSELECT fu.username, u.description FROM filevault_users fu JOIN users u ON fu.uuid=u.uuid;\n```", + "columns": [ + { + "name": "username", + "description": "Username of the FileVault user.", + "required": false, + "type": "text" + }, + { + "name": "uuid", + "description": "UUID of the FileVault user, which can be joined to [users.uuid](http://fleetdm.com/tables/users).", + "required": false, + "type": "text" + } + ], + "url": "https://fleetdm.com/tables/filevault_users", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/filevault_users.yml" + }, { "name": "file_lines", "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", @@ -27849,30 +27894,65 @@ "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/file_lines.yml" }, { - "name": "filevault_users", - "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", - "description": "Information on the users able to unlock the current boot volume if protected with FileVault.", + "name": "firmware_eficheck_integrity_check", "platforms": [ "darwin" ], - "evented": false, - "examples": "List the usernames able to unlock and boot a computer protected by FileVault, joined to [users.username](http://fleetdm.com/tables/users) to obtain the description of the operating system account that owns it.\n```\nSELECT fu.username, u.description FROM filevault_users fu JOIN users u ON fu.uuid=u.uuid;\n```", + "description": "Performs eficheck's integrity check on macOS Intel T1 chips (CIS 5.9).", "columns": [ { - "name": "username", - "description": "Username of the FileVault user.", + "name": "chip", + "type": "text", "required": false, - "type": "text" + "description": "Contains the chip type, values are \"apple\", \"intel-t1\" and \"intel-t2\".\nIf chip type is \"apple\" or \"intel-t2\" then no eficheck integrity check is executed.\n" }, { - "name": "uuid", - "description": "UUID of the FileVault user, which can be joined to [users.uuid](http://fleetdm.com/tables/users).", + "name": "output", + "type": "text", "required": false, - "type": "text" + "description": "Output of the `/usr/libexec/firmwarecheckers/eficheck/eficheck --integrity-check` command.\nThis value is only valid when chip is \"intel-t1\".\n" } ], - "url": "https://fleetdm.com/tables/filevault_users", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/filevault_users.yml" + "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", + "evented": false, + "url": "https://fleetdm.com/tables/firmware_eficheck_integrity_check", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/firmware_eficheck_integrity_check.yml" + }, + { + "name": "geolocation", + "evented": false, + "platforms": [ + "chrome" + ], + "description": "Last reported geolocation", + "columns": [ + { + "name": "ip", + "type": "text", + "required": false, + "description": "IP address" + }, + { + "name": "city", + "type": "text", + "required": false, + "description": "City" + }, + { + "name": "country", + "type": "text", + "required": false, + "description": "Country" + }, + { + "name": "region", + "type": "text", + "required": false, + "description": "Region" + } + ], + "url": "https://fleetdm.com/tables/geolocation", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/geolocation.yml" }, { "name": "google_chrome_profiles", @@ -27914,42 +27994,6 @@ "url": "https://fleetdm.com/tables/google_chrome_profiles", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/google_chrome_profiles.yml" }, - { - "name": "geolocation", - "evented": false, - "platforms": [ - "chrome" - ], - "description": "Last reported geolocation", - "columns": [ - { - "name": "ip", - "type": "text", - "required": false, - "description": "IP address" - }, - { - "name": "city", - "type": "text", - "required": false, - "description": "City" - }, - { - "name": "country", - "type": "text", - "required": false, - "description": "Country" - }, - { - "name": "region", - "type": "text", - "required": false, - "description": "Region" - } - ], - "url": "https://fleetdm.com/tables/geolocation", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/geolocation.yml" - }, { "name": "icloud_private_relay", "platforms": [ @@ -27969,31 +28013,6 @@ "url": "https://fleetdm.com/tables/icloud_private_relay", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/icloud_private_relay.yml" }, - { - "name": "firmware_eficheck_integrity_check", - "platforms": [ - "darwin" - ], - "description": "Performs eficheck's integrity check on macOS Intel T1 chips (CIS 5.9).", - "columns": [ - { - "name": "chip", - "type": "text", - "required": false, - "description": "Contains the chip type, values are \"apple\", \"intel-t1\" and \"intel-t2\".\nIf chip type is \"apple\" or \"intel-t2\" then no eficheck integrity check is executed.\n" - }, - { - "name": "output", - "type": "text", - "required": false, - "description": "Output of the `/usr/libexec/firmwarecheckers/eficheck/eficheck --integrity-check` command.\nThis value is only valid when chip is \"intel-t1\".\n" - } - ], - "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", - "evented": false, - "url": "https://fleetdm.com/tables/firmware_eficheck_integrity_check", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/firmware_eficheck_integrity_check.yml" - }, { "name": "macadmins_unified_log", "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", @@ -28344,44 +28363,6 @@ "url": "https://fleetdm.com/tables/mdm_bridge", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/mdm_bridge.yml" }, - { - "name": "munki_installs", - "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
Code based on work by [Kolide](https://github.com/kolide/launcher).", - "description": "Software packages and other items [Munki](https://github.com/munki/munki) is managing.", - "platforms": [ - "darwin" - ], - "evented": false, - "examples": "See the version of software that has been deployed by Munki.\n```\nSELECT name, installed_version FROM munki_installs WHERE installed='true';\n```", - "columns": [ - { - "name": "end_time", - "description": "The end time of the last Munki run.", - "required": false, - "type": "text" - }, - { - "name": "installed", - "description": "Shows if Munki installed an item (true) or if it is simply available but not installed (false).", - "required": false, - "type": "text" - }, - { - "name": "installed_version", - "description": "The version number of installed items.", - "required": false, - "type": "text" - }, - { - "name": "name", - "description": "The name of items managed by Munki.", - "required": false, - "type": "text" - } - ], - "url": "https://fleetdm.com/tables/munki_installs", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/munki_installs.yml" - }, { "name": "munki_info", "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
Code based on work by [Kolide](https://github.com/kolide/launcher).", @@ -28450,6 +28431,44 @@ "url": "https://fleetdm.com/tables/munki_info", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/munki_info.yml" }, + { + "name": "munki_installs", + "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
Code based on work by [Kolide](https://github.com/kolide/launcher).", + "description": "Software packages and other items [Munki](https://github.com/munki/munki) is managing.", + "platforms": [ + "darwin" + ], + "evented": false, + "examples": "See the version of software that has been deployed by Munki.\n```\nSELECT name, installed_version FROM munki_installs WHERE installed='true';\n```", + "columns": [ + { + "name": "end_time", + "description": "The end time of the last Munki run.", + "required": false, + "type": "text" + }, + { + "name": "installed", + "description": "Shows if Munki installed an item (true) or if it is simply available but not installed (false).", + "required": false, + "type": "text" + }, + { + "name": "installed_version", + "description": "The version number of installed items.", + "required": false, + "type": "text" + }, + { + "name": "name", + "description": "The name of items managed by Munki.", + "required": false, + "type": "text" + } + ], + "url": "https://fleetdm.com/tables/munki_installs", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/munki_installs.yml" + }, { "name": "network_interfaces", "evented": false, @@ -28582,6 +28601,124 @@ "url": "https://fleetdm.com/tables/pmset", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/pmset.yml" }, + { + "name": "puppet_info", + "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", + "description": "Information on the last [Puppet](https://puppet.com/) run. This table uses data from the `last_run_report` that Puppet creates.", + "platforms": [ + "darwin", + "windows", + "linux" + ], + "evented": false, + "examples": "List all the information available about the last Puppet run.\n```\nSELECT * FROM puppet_info;\n```", + "columns": [ + { + "name": "cached_catalog_status", + "description": "The status of Puppet catalogs cached on the system.", + "required": false, + "type": "text" + }, + { + "name": "catalog_uuid", + "description": "The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) of the catalog downloaded by Puppet.", + "required": false, + "type": "text" + }, + { + "name": "code_id", + "description": "The `code_id` links the catalog with the compile-time version of file resources using the `puppet:///` URI.", + "required": false, + "type": "text" + }, + { + "name": "configuration_version", + "description": "The version of the Puppet configuration.", + "required": false, + "type": "text" + }, + { + "name": "corrective_change", + "description": "A corrective change is triggered when Puppet detects a discrepency between the current state and the expected state of a value.", + "required": false, + "type": "text" + }, + { + "name": "environment", + "description": "The environment name.", + "required": false, + "type": "text" + }, + { + "name": "host", + "description": "The host on which Puppet is used.", + "required": false, + "type": "text" + }, + { + "name": "kind", + "description": "Kind of Puppet run.", + "required": false, + "type": "text" + }, + { + "name": "master_used", + "description": "The Puppet server used.", + "required": false, + "type": "text" + }, + { + "name": "noop", + "description": "Indicates if Puppet was run in [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) mode.", + "required": false, + "type": "text" + }, + { + "name": "noop_prending", + "description": "Items pending from a [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) run.", + "required": false, + "type": "text" + }, + { + "name": "puppet_version", + "description": "The version of Puppet used during the last run.", + "required": false, + "type": "text" + }, + { + "name": "report_format", + "description": "The format the Puppet report was exported as.", + "required": false, + "type": "text" + }, + { + "name": "status", + "description": "The status of Puppet on this system.", + "required": false, + "type": "text" + }, + { + "name": "time", + "description": "The time of the last Puppet run.", + "required": false, + "type": "text" + }, + { + "name": "transaction_completed", + "description": "Indicates if the transaction completed or not.", + "required": false, + "type": "text" + }, + { + "name": "transaction_uuid", + "description": "The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) of the transaction.", + "required": false, + "type": "text" + } + ], + "url": "https://fleetdm.com/tables/puppet_info", + "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/puppet_info.yml" + }, { "name": "puppet_logs", "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", @@ -28728,124 +28865,6 @@ "url": "https://fleetdm.com/tables/puppet_state", "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/puppet_state.yml" }, - { - "name": "puppet_info", - "notes": "This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).", - "description": "Information on the last [Puppet](https://puppet.com/) run. This table uses data from the `last_run_report` that Puppet creates.", - "platforms": [ - "darwin", - "windows", - "linux" - ], - "evented": false, - "examples": "List all the information available about the last Puppet run.\n```\nSELECT * FROM puppet_info;\n```", - "columns": [ - { - "name": "cached_catalog_status", - "description": "The status of Puppet catalogs cached on the system.", - "required": false, - "type": "text" - }, - { - "name": "catalog_uuid", - "description": "The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) of the catalog downloaded by Puppet.", - "required": false, - "type": "text" - }, - { - "name": "code_id", - "description": "The `code_id` links the catalog with the compile-time version of file resources using the `puppet:///` URI.", - "required": false, - "type": "text" - }, - { - "name": "configuration_version", - "description": "The version of the Puppet configuration.", - "required": false, - "type": "text" - }, - { - "name": "corrective_change", - "description": "A corrective change is triggered when Puppet detects a discrepency between the current state and the expected state of a value.", - "required": false, - "type": "text" - }, - { - "name": "environment", - "description": "The environment name.", - "required": false, - "type": "text" - }, - { - "name": "host", - "description": "The host on which Puppet is used.", - "required": false, - "type": "text" - }, - { - "name": "kind", - "description": "Kind of Puppet run.", - "required": false, - "type": "text" - }, - { - "name": "master_used", - "description": "The Puppet server used.", - "required": false, - "type": "text" - }, - { - "name": "noop", - "description": "Indicates if Puppet was run in [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) mode.", - "required": false, - "type": "text" - }, - { - "name": "noop_prending", - "description": "Items pending from a [noop](https://puppet.com/docs/puppet/latest/metaparameter.html#noop) run.", - "required": false, - "type": "text" - }, - { - "name": "puppet_version", - "description": "The version of Puppet used during the last run.", - "required": false, - "type": "text" - }, - { - "name": "report_format", - "description": "The format the Puppet report was exported as.", - "required": false, - "type": "text" - }, - { - "name": "status", - "description": "The status of Puppet on this system.", - "required": false, - "type": "text" - }, - { - "name": "time", - "description": "The time of the last Puppet run.", - "required": false, - "type": "text" - }, - { - "name": "transaction_completed", - "description": "Indicates if the transaction completed or not.", - "required": false, - "type": "text" - }, - { - "name": "transaction_uuid", - "description": "The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) of the transaction.", - "required": false, - "type": "text" - } - ], - "url": "https://fleetdm.com/tables/puppet_info", - "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/puppet_info.yml" - }, { "name": "pwd_policy", "platforms": [ diff --git a/schema/tables/filevault_prk.yml b/schema/tables/filevault_prk.yml new file mode 100644 index 0000000000..bdab68851f --- /dev/null +++ b/schema/tables/filevault_prk.yml @@ -0,0 +1,11 @@ +name: filevault_prk +platforms: + - darwin +description: Returns contents of `/var/db/FileVaultPRK.dat`. +columns: + - name: base64_encrypted + type: text + required: false + description: The base64-encoded contents of the encrypted FileVault personal recovery key stored at `/var/db/FileVaultPRK.dat` (see also https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow) +notes: This table is not a core osquery table. It is included as part of [Fleetd](https://fleetdm.com/docs/using-fleet/orbit), the osquery manager from Fleet. Fleetd can be built with [fleetctl](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). +evented: false diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 47bcc333e5..625bb8761e 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -1433,7 +1433,8 @@ func TestMDMQueries(t *testing.T) { discoveryTable string }{ {"fleet_detail_query_mdm_config_profiles_darwin", "macos_profiles"}, - {"fleet_detail_query_mdm_disk_encryption_key_darwin", "file_lines"}, + {"fleet_detail_query_mdm_disk_encryption_key_file_darwin", "filevault_prk"}, + {"fleet_detail_query_mdm_disk_encryption_key_file_lines_darwin", "file_lines"}, } mdmEnabled := true diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 8a3ad95194..c8ad47ca1e 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -2,6 +2,8 @@ package osquery_utils import ( "context" + "encoding/base64" + "encoding/hex" "fmt" "net" "net/url" @@ -575,37 +577,57 @@ var extraDetailQueries = map[string]DetailQuery{ // They are only sent to the device when Fleet's MDM is on and properly // configured var mdmQueries = map[string]DetailQuery{ - "mdm_disk_encryption_key_darwin": { - // This query has two pre-requisites: - // - // 1. FileVault must be enabled with a personal recovery key. - // 2. The "FileVault Recovery Key Escrow" profile must be configured - // in the host. - // - // This file is safe to access and well [documented by Apple][1]: - // - // > If FileVault is enabled after this payload is installed on the system, - // > the FileVault PRK will be encrypted with the specified certificate, - // > wrapped with a CMS envelope and stored at /var/db/FileVaultPRK.dat. The - // > encrypted data will be made available to the MDM server as part of the - // > SecurityInfo command. - // > - // > Alternatively, if a site uses its own administration - // > software, it can extract the PRK from the foregoing - // > location at any time. - // - // [1]: https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow - Query: fmt.Sprintf(`SELECT to_base64(group_concat(line, x'0a')) as filevault_key, COALESCE((%s), 0) as encrypted FROM file_lines WHERE path='/var/db/FileVaultPRK.dat'`, usesMacOSDiskEncryptionQuery), - Platforms: []string{"darwin"}, - DirectIngestFunc: directIngestDiskEncryptionKeyDarwin, - Discovery: discoveryTable("file_lines"), - }, "mdm_config_profiles_darwin": { Query: `SELECT display_name, identifier, install_date FROM macos_profiles where type = "Configuration";`, Platforms: []string{"darwin"}, DirectIngestFunc: directIngestMacOSProfiles, Discovery: discoveryTable("macos_profiles"), }, + // There are two mutually-exclusive queries used to read the FileVaultPRK depending on which + // extension tables are discovered on the agent. The preferred query uses the newer custom + // `filevault_prk` extension table rather than the macadmins `file_lines` table. It is preferred + // because the `file_lines` implementation uses bufio.ScanLines which drops end of line + // characters. + // + // Both queries depend on the same pre-requisites: + // + // 1. FileVault must be enabled with a personal recovery key. + // 2. The "FileVault Recovery Key Escrow" profile must be configured + // in the host. + // + // This file is safe to access and well [documented by Apple][1]: + // + // > If FileVault is enabled after this payload is installed on the system, + // > the FileVault PRK will be encrypted with the specified certificate, + // > wrapped with a CMS envelope and stored at /var/db/FileVaultPRK.dat. The + // > encrypted data will be made available to the MDM server as part of the + // > SecurityInfo command. + // > + // > Alternatively, if a site uses its own administration + // > software, it can extract the PRK from the foregoing + // > location at any time. + // + // [1]: https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow + "mdm_disk_encryption_key_file_lines_darwin": { + Query: fmt.Sprintf(` + WITH + de AS (SELECT IFNULL((%s), 0) as encrypted), + fl AS (SELECT line FROM file_lines WHERE path = '/var/db/FileVaultPRK.dat') + SELECT encrypted, hex(line) as hex_line FROM de LEFT JOIN fl;`, usesMacOSDiskEncryptionQuery), + Platforms: []string{"darwin"}, + DirectIngestFunc: directIngestDiskEncryptionKeyFileLinesDarwin, + Discovery: fmt.Sprintf(`SELECT 1 WHERE EXISTS (%s) AND NOT EXISTS (%s);`, strings.Trim(discoveryTable("file_lines"), ";"), strings.Trim(discoveryTable("filevault_prk"), ";")), + }, + "mdm_disk_encryption_key_file_darwin": { + Query: fmt.Sprintf(` + WITH + de AS (SELECT IFNULL((%s), 0) as encrypted), + fv AS (SELECT base64_encrypted as filevault_key FROM filevault_prk) + SELECT encrypted, filevault_key FROM de LEFT JOIN fv;`, usesMacOSDiskEncryptionQuery), + Platforms: []string{"darwin"}, + DirectIngestFunc: directIngestDiskEncryptionKeyFileDarwin, + Discovery: discoveryTable("filevault_prk"), + }, } // discoveryTable returns a query to determine whether a table exists or not. @@ -1373,7 +1395,9 @@ func directIngestDiskEncryption(ctx context.Context, logger log.Logger, host *fl return ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, encrypted) } -func directIngestDiskEncryptionKeyDarwin( +// directIngestDiskEncryptionKeyFileDarwin ingests the FileVault key from the `filevault_prk` +// extension table. It is the preferred method when a host has the extension table available. +func directIngestDiskEncryptionKeyFileDarwin( ctx context.Context, logger log.Logger, host *fleet.Host, @@ -1384,7 +1408,7 @@ func directIngestDiskEncryptionKeyDarwin( // assume the extension is not there level.Debug(logger).Log( "component", "service", - "method", "directIngestDiskEncryptionKeyDarwin", + "method", "directIngestDiskEncryptionKeyFileDarwin", "msg", "no rows or failed", "host", host.Hostname, ) @@ -1394,16 +1418,16 @@ func directIngestDiskEncryptionKeyDarwin( if len(rows) > 1 { level.Debug(logger).Log( "component", "service", - "method", "directIngestDiskEncryptionKeyDarwin", - "msg", fmt.Sprintf("/var/db/FileVaultPRK.dat should have a single line, but got %d", len(rows)), + "method", "directIngestDiskEncryptionKeyFileDarwin", + "msg", fmt.Sprintf("filevault_prk should have a single row, but got %d", len(rows)), "host", host.Hostname, ) } - if rows[0]["encrypted"] == "0" { + if rows[0]["encrypted"] != "1" { level.Debug(logger).Log( "component", "service", - "method", "directIngestDiskEncryptionKeyDarwin", + "method", "directIngestDiskEncryptionKeyFileDarwin", "msg", "host does not use disk encryption", "host", host.Hostname, ) @@ -1415,6 +1439,57 @@ func directIngestDiskEncryptionKeyDarwin( return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, rows[0]["filevault_key"]) } +// directIngestDiskEncryptionKeyFileLinesDarwin ingests the FileVault key from the `file_lines` +// extension table. It is the fallback method in cases where the preferred `filevault_prk` extension +// table is not available on the host. +func directIngestDiskEncryptionKeyFileLinesDarwin( + ctx context.Context, + logger log.Logger, + host *fleet.Host, + ds fleet.Datastore, + rows []map[string]string, +) error { + if len(rows) == 0 { + // assume the extension is not there + level.Debug(logger).Log( + "component", "service", + "method", "directIngestDiskEncryptionKeyFileLinesDarwin", + "msg", "no rows or failed", + "host", host.Hostname, + ) + return nil + } + + var hexLines []string + for _, row := range rows { + if row["encrypted"] != "1" { + level.Debug(logger).Log( + "component", "service", + "method", "directIngestDiskEncryptionKeyDarwin", + "msg", "host does not use disk encryption", + "host", host.Hostname, + ) + return nil + } + hexLines = append(hexLines, row["hex_line"]) + } + // We concatenate the lines in Go rather than using SQL `group_concat` because the order in + // which SQL appends the lines is not deterministic, nor guaranteed to be the right order. + // We assume that hexadecimal 0A (i.e. new line) was the delimiter used to split all lines; + // however, there are edge cases where this will not be true. It is a known limitation + // with the `file_lines` extension table and its reliance on bufio.ScanLines that carriage + // returns will be lost if the source file contains hexadecimal 0D0A (i.e. carriage + // return preceding new line). In such cases, the stored key will be incorrect. + b, err := hex.DecodeString(strings.Join(hexLines, "0A")) + if err != nil { + return ctxerr.Wrap(ctx, err, "decoding hex string") + } + + // it's okay if the key comes empty, this can happen and if the disk is + // encrypted it means we need to reset the encryption key + return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64.StdEncoding.EncodeToString(b)) +} + func directIngestMacOSProfiles( ctx context.Context, logger log.Logger, diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index e84fa3a11d..7483c9ee10 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -1,7 +1,11 @@ package osquery_utils import ( + "bufio" + "bytes" "context" + "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -541,7 +545,6 @@ func TestDirectIngestMDMMac(t *testing.T) { } }) } - } func TestDirectIngestMDMWindows(t *testing.T) { @@ -1007,37 +1010,111 @@ func TestDirectIngestDiskEncryptionKeyDarwin(t *testing.T) { ds := new(mock.Store) ctx := context.Background() logger := log.NewNopLogger() - wantKey := "OTM5ODRDQTYtOUY1Mi00NERELTkxOUEtMDlBN0ZBOUUzNUY5Cg==" host := &fleet.Host{ID: 1} + var wantKey string + + mockFileLines := func(wantKey string, wantEncrypted string) []map[string]string { + var output []map[string]string + scanner := bufio.NewScanner(bytes.NewBuffer([]byte(wantKey))) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + line := scanner.Text() + item := make(map[string]string) + item["hex_line"] = hex.EncodeToString([]byte(line)) + item["encrypted"] = wantEncrypted + output = append(output, item) + } + return output + } + + mockFilevaultPRK := func(wantKey string, wantEncrypted string) []map[string]string { + return []map[string]string{ + {"filevault_key": base64.StdEncoding.EncodeToString([]byte(wantKey)), "encrypted": wantEncrypted}, + } + } + ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint, encryptedBase64Key string) error { - require.Empty(t, encryptedBase64Key) - require.Equal(t, host.ID, hostID) + if base64.StdEncoding.EncodeToString([]byte(wantKey)) != encryptedBase64Key { + return errors.New("key mismatch") + } + if host.ID != hostID { + return errors.New("host ID mismatch") + } return nil } - err := directIngestDiskEncryptionKeyDarwin(ctx, logger, host, ds, []map[string]string{}) - require.NoError(t, err) - require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + t.Run("empty key", func(t *testing.T) { + err := directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{}) + require.NoError(t, err) + require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) - err = directIngestDiskEncryptionKeyDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "0"}}) - require.NoError(t, err) - require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{}) + require.NoError(t, err) + require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) - err = directIngestDiskEncryptionKeyDarwin(ctx, logger, host, ds, []map[string]string{{"filevault_key": ""}}) - require.NoError(t, err) - require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) - ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + err = directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "0"}}) + require.NoError(t, err) + require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) - ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint, encryptedBase64Key string) error { - require.Equal(t, wantKey, encryptedBase64Key) - require.Equal(t, host.ID, hostID) - return nil - } + err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "0"}}) + require.NoError(t, err) + require.False(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) - err = directIngestDiskEncryptionKeyDarwin(ctx, logger, host, ds, []map[string]string{{"filevault_key": wantKey}}) - require.NoError(t, err) - require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + err = directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1"}}) + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + + err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1"}}) + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + + err = directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1", "hex_line": ""}}) + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + + err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, []map[string]string{{"encrypted": "1", "filevault_key": ""}}) + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + }) + + t.Run("key contains new lines and carriage return", func(t *testing.T) { + wantKey = "This is only a \n\r\n\n test." + + err := directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, mockFileLines(wantKey, "1")) + // it is a known limitation with the current file_lines implementation that causes this to fail + // because it relies on bufio.ScanLines, which drops "\r" from "\r\n" + require.ErrorContains(t, err, "key mismatch") + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + + err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, mockFilevaultPRK(wantKey, "1")) + // filevault_prk does not have the scan lines limitation + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + }) + + t.Run("key contains new lines", func(t *testing.T) { + wantKey = "This is only a \n\n\n test." + + err := directIngestDiskEncryptionKeyFileLinesDarwin(ctx, logger, host, ds, mockFileLines(wantKey, "1")) + // new lines are not a problem if they are not preceded by carriage return + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + + err = directIngestDiskEncryptionKeyFileDarwin(ctx, logger, host, ds, mockFilevaultPRK(wantKey, "1")) + // filevault_prk does not have the scan lines limitation + require.NoError(t, err) + require.True(t, ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked) + ds.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = false + }) } func TestDirectIngestHostMacOSProfiles(t *testing.T) {