From f0d77ab3db4444132c439afff674745cfaeb004b Mon Sep 17 00:00:00 2001 From: Marcos Oviedo Date: Fri, 6 Oct 2023 19:04:33 -0300 Subject: [PATCH] Merging Bitlocker feature branch (#14350) This relates to #12577 --------- Co-authored-by: gillespi314 <73313222+gillespi314@users.noreply.github.com> Co-authored-by: Roberto Dip --- changes/12927-disk-encryption-settings | 1 + changes/12932-bitlocker-api-updates | 4 + changes/12933-bitlocker-host-details-api | 1 + ...953-changes-to-controls-page-for-bitlocker | 1 + changes/issue-13954-orbit-disk-encryption-key | 1 + ...e-14007-support-get-windows-encryption-key | 1 + cmd/fleet/cron.go | 3 +- cmd/fleetctl/apply_test.go | 16 +- cmd/fleetctl/get.go | 17 +- cmd/fleetctl/get_test.go | 13 +- .../expectedGetConfigAppConfigJson.json | 4 +- .../expectedGetConfigAppConfigYaml.yml | 2 +- ...ectedGetConfigIncludeServerConfigJson.json | 4 +- ...pectedGetConfigIncludeServerConfigYaml.yml | 2 +- .../testdata/expectedGetTeamsJson.json | 8 +- .../testdata/expectedGetTeamsYaml.yml | 4 +- .../macosSetupExpectedAppConfigEmpty.yml | 2 +- .../macosSetupExpectedAppConfigSet.yml | 2 +- .../macosSetupExpectedTeam1And2Empty.yml | 4 +- .../macosSetupExpectedTeam1And2Set.yml | 4 +- .../testdata/macosSetupExpectedTeam1Empty.yml | 2 +- docs/Contributing/API-for-contributors.md | 44 +- docs/Contributing/FAQ.md | 1 + docs/REST API/rest-api.md | 87 +-- docs/Using Fleet/manage-access.md | 2 +- ee/server/service/mdm.go | 56 +- ee/server/service/teams.go | 58 +- frontend/__mocks__/configMock.ts | 1 + frontend/__mocks__/hostMock.ts | 13 +- frontend/__mocks__/mdmMock.ts | 5 + .../StatusIndicatorWithIcon.tsx | 10 +- .../StatusIndicatorWithIcon/_styles.scss | 7 + frontend/interfaces/config.ts | 90 +-- frontend/interfaces/host.ts | 24 +- frontend/interfaces/mdm.ts | 24 +- frontend/interfaces/team.ts | 1 + .../AggregateMacSettingsIndicators.tsx | 99 --- .../AggregateMacSettingsIndicators/index.ts | 1 - .../OSSettings/OSSettings.tsx | 15 +- .../ProfileStatusAggregate.tsx | 95 +++ .../ProfileStatusAggregateOptions.ts | 43 ++ .../_styles.scss | 12 +- .../ProfileStatusAggregate/index.ts | 1 + .../cards/DiskEncryption/DiskEncryption.tsx | 21 +- .../cards/DiskEncryption/_styles.scss | 1 + .../DiskEncryptionTable.tsx | 29 +- .../DiskEncryptionTableConfig.tsx | 111 +++- .../DiskEncryptionTable/_styles.scss | 3 - .../TurnOnMdmMessage/TurnOnMdmMessage.tsx | 2 +- .../hosts/ManageHostsPage/HostsPageConfig.tsx | 3 +- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 13 +- .../DiskEncryptionStatusFilter.tsx | 6 +- .../HostsFilterBlock/HostsFilterBlock.tsx | 17 +- .../details/DeviceUserPage/DeviceUserPage.tsx | 2 + .../HostDetailsPage/HostDetailsPage.tsx | 18 +- .../DiskEncryptionKeyModal.tsx | 38 +- .../details/MacSettingsIndicator/index.ts | 1 - .../MacSettingsModal/MacSettingsModal.tsx | 20 +- .../MacSettingStatusCell.tsx | 70 +- .../MacSettingsTableConfig.tsx | 40 +- .../ProfileStatusIndicator.tests.tsx} | 13 +- .../ProfileStatusIndicator.tsx} | 10 +- .../_styles.scss | 2 +- .../details/ProfileStatusIndicator/index.ts | 1 + .../details/cards/HostSummary/HostSummary.tsx | 44 +- .../MacSettingsIndicator.tsx | 16 +- frontend/pages/hosts/details/helpers.ts | 33 + frontend/services/entities/host_count.ts | 4 +- frontend/services/entities/hosts.ts | 11 +- frontend/services/entities/mdm.ts | 91 ++- frontend/utilities/endpoints.ts | 2 +- frontend/utilities/url/index.ts | 12 +- go.mod | 1 + go.sum | 2 + .../changes/12842-orbit-bitlocker-management | 1 + orbit/cmd/orbit/orbit.go | 2 + orbit/pkg/bitlocker/bitlocker_management.go | 17 + .../bitlocker_management_notwindows.go | 19 + .../bitlocker/bitlocker_management_windows.go | 573 +++++++++++++++++ orbit/pkg/update/execwinapi_stub.go | 4 + orbit/pkg/update/execwinapi_windows.go | 14 + orbit/pkg/update/notifications.go | 117 ++++ orbit/pkg/update/notifications_test.go | 64 ++ pkg/optjson/optjson.go | 39 ++ pkg/optjson/optjson_test.go | 81 +++ pkg/rawjson/rawjson.go | 55 ++ pkg/rawjson/rawjson_test.go | 104 +++ server/datastore/mysql/app_configs.go | 15 + server/datastore/mysql/app_configs_test.go | 48 +- server/datastore/mysql/apple_mdm.go | 26 +- server/datastore/mysql/apple_mdm_test.go | 259 ++++---- server/datastore/mysql/hosts.go | 208 +++++- server/datastore/mysql/hosts_test.go | 201 ++++-- server/datastore/mysql/labels.go | 26 +- server/datastore/mysql/labels_test.go | 116 +++- server/datastore/mysql/microsoft_mdm.go | 234 ++++++- server/datastore/mysql/microsoft_mdm_test.go | 389 +++++++++++ ...0230918221115_MoveDiskEncryptionSetting.go | 32 + ...18221115_MoveDiskEncryptionSetting_test.go | 67 ++ server/datastore/mysql/schema.sql | 6 +- server/fleet/app.go | 116 +++- server/fleet/app_test.go | 152 +++++ server/fleet/datastore.go | 22 +- server/fleet/hosts.go | 70 +- server/fleet/hosts_test.go | 51 ++ server/fleet/mdm.go | 13 + server/fleet/orbit.go | 10 + server/fleet/service.go | 16 + server/fleet/teams.go | 20 +- server/fleet/windows_mdm.go | 16 + server/mdm/apple/cert.go | 17 - server/mdm/mdm.go | 25 + .../mdm/{apple/cert_test.go => mdm_test.go} | 2 +- server/mdm/microsoft/microsoft_mdm.go | 17 +- server/mock/datastore_mock.go | 62 +- server/service/appconfig.go | 78 ++- server/service/appconfig_test.go | 25 +- server/service/apple_mdm.go | 8 +- server/service/apple_mdm_test.go | 8 +- server/service/handler.go | 13 +- server/service/hosts.go | 87 ++- server/service/hosts_test.go | 170 ++++- server/service/integration_core_test.go | 23 +- server/service/integration_enterprise_test.go | 8 +- server/service/integration_mdm_test.go | 604 ++++++++++++++++-- server/service/mdm.go | 58 ++ server/service/mdm_test.go | 215 ++++++- server/service/microsoft_mdm.go | 32 +- .../middleware/mdmconfigured/mdmconfigured.go | 12 + .../mdmconfigured/mdmconfigured_test.go | 54 ++ server/service/orbit.go | 93 +++ server/service/orbit_client.go | 15 + server/service/osquery_utils/queries.go | 8 +- server/service/osquery_utils/queries_test.go | 2 +- server/service/testing_utils.go | 4 +- server/service/transport.go | 32 +- 136 files changed, 5367 insertions(+), 930 deletions(-) create mode 100644 changes/12927-disk-encryption-settings create mode 100644 changes/12932-bitlocker-api-updates create mode 100644 changes/12933-bitlocker-host-details-api create mode 100644 changes/issue-13953-changes-to-controls-page-for-bitlocker create mode 100644 changes/issue-13954-orbit-disk-encryption-key create mode 100644 changes/issue-14007-support-get-windows-encryption-key delete mode 100644 frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/AggregateMacSettingsIndicators.tsx delete mode 100644 frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/index.ts create mode 100644 frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregate.tsx create mode 100644 frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts rename frontend/pages/ManageControlsPage/OSSettings/{AggregateMacSettingsIndicators => ProfileStatusAggregate}/_styles.scss (77%) create mode 100644 frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/index.ts delete mode 100644 frontend/pages/hosts/details/MacSettingsIndicator/index.ts rename frontend/pages/hosts/details/{MacSettingsIndicator/MacSettingsIndicator.tests.tsx => ProfileStatusIndicator/ProfileStatusIndicator.tests.tsx} (87%) rename frontend/pages/hosts/details/{MacSettingsIndicator/MacSettingsIndicator.tsx => ProfileStatusIndicator/ProfileStatusIndicator.tsx} (92%) rename frontend/pages/hosts/details/{MacSettingsIndicator => ProfileStatusIndicator}/_styles.scss (88%) create mode 100644 frontend/pages/hosts/details/ProfileStatusIndicator/index.ts create mode 100644 frontend/pages/hosts/details/helpers.ts create mode 100644 orbit/changes/12842-orbit-bitlocker-management create mode 100644 orbit/pkg/bitlocker/bitlocker_management.go create mode 100644 orbit/pkg/bitlocker/bitlocker_management_notwindows.go create mode 100644 orbit/pkg/bitlocker/bitlocker_management_windows.go create mode 100644 pkg/rawjson/rawjson.go create mode 100644 pkg/rawjson/rawjson_test.go create mode 100644 server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go create mode 100644 server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go create mode 100644 server/fleet/windows_mdm.go create mode 100644 server/mdm/mdm.go rename server/mdm/{apple/cert_test.go => mdm_test.go} (99%) diff --git a/changes/12927-disk-encryption-settings b/changes/12927-disk-encryption-settings new file mode 100644 index 000000000..a9464b7d5 --- /dev/null +++ b/changes/12927-disk-encryption-settings @@ -0,0 +1 @@ +* Deprecate `mdm.macos_settings.enable_disk_encryption` in favor of `mdm.enable_disk_encryption` diff --git a/changes/12932-bitlocker-api-updates b/changes/12932-bitlocker-api-updates new file mode 100644 index 000000000..0ce9b45e8 --- /dev/null +++ b/changes/12932-bitlocker-api-updates @@ -0,0 +1,4 @@ +- Added `GET /mdm/disk_encryption/summary` endpoint to get the disk encryption summary for macOS and + Windows devices. +- Added `os_settings` and `os_settings_disk_encryption` filters to `GET /hosts`, `GET /hosts/count`, + `GET /api/v1/fleet/labels/{id}/hosts` endpoints to filter hosts by OS settings. diff --git a/changes/12933-bitlocker-host-details-api b/changes/12933-bitlocker-host-details-api new file mode 100644 index 000000000..ccb11df8b --- /dev/null +++ b/changes/12933-bitlocker-host-details-api @@ -0,0 +1 @@ +- Added `mdm.os_settings` to `GET /api/v1/hosts/{id}` response. diff --git a/changes/issue-13953-changes-to-controls-page-for-bitlocker b/changes/issue-13953-changes-to-controls-page-for-bitlocker new file mode 100644 index 000000000..728d93122 --- /dev/null +++ b/changes/issue-13953-changes-to-controls-page-for-bitlocker @@ -0,0 +1 @@ +- change Controls/Disk Encryption and host details page to include windows bitlocker information. diff --git a/changes/issue-13954-orbit-disk-encryption-key b/changes/issue-13954-orbit-disk-encryption-key new file mode 100644 index 000000000..82767942e --- /dev/null +++ b/changes/issue-13954-orbit-disk-encryption-key @@ -0,0 +1 @@ +* Added the `POST /api/fleet/orbit/disk_encryption_key` endpoint for Windows hosts to report the bitlocker encryption key. diff --git a/changes/issue-14007-support-get-windows-encryption-key b/changes/issue-14007-support-get-windows-encryption-key new file mode 100644 index 000000000..0705f8e97 --- /dev/null +++ b/changes/issue-14007-support-get-windows-encryption-key @@ -0,0 +1 @@ +* Added support to return the decrypted disk encryption key of a Windows host. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 4835eb3e1..50120b043 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -18,6 +18,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/policies" "github.com/fleetdm/fleet/v4/server/ptr" @@ -838,7 +839,7 @@ func verifyDiskEncryptionKeys( if key.UpdatedAt.After(latest) { latest = key.UpdatedAt } - if _, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey); err != nil { + if _, err := mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey); err != nil { undecryptable = append(undecryptable, key.HostID) continue } diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 395fe0a6c..8110239ca 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -1044,13 +1044,13 @@ spec: foo: qux name: Team1 mdm: + enable_disk_encryption: false macos_updates: minimum_version: 10.10.10 deadline: 1992-03-01 macos_settings: custom_settings: - %s - enable_disk_encryption: false secrets: - secret: BBB `, mobileConfigPath)) @@ -1062,9 +1062,9 @@ spec: require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name})) assert.JSONEq(t, string(json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)), string(*savedTeam.Config.AgentOptions)) assert.Equal(t, fleet.TeamMDM{ + EnableDiskEncryption: false, MacOSSettings: fleet.MacOSSettings{ - CustomSettings: []string{mobileConfigPath}, - EnableDiskEncryption: false, + CustomSettings: []string{mobileConfigPath}, }, MacOSUpdates: fleet.MacOSUpdates{ MinimumVersion: optjson.SetString("10.10.10"), @@ -1097,9 +1097,9 @@ spec: require.True(t, ds.NewJobFuncInvoked) // all left untouched, only setup assistant added assert.Equal(t, fleet.TeamMDM{ + EnableDiskEncryption: false, MacOSSettings: fleet.MacOSSettings{ - CustomSettings: []string{mobileConfigPath}, - EnableDiskEncryption: false, + CustomSettings: []string{mobileConfigPath}, }, MacOSUpdates: fleet.MacOSUpdates{ MinimumVersion: optjson.SetString("10.10.10"), @@ -1129,9 +1129,9 @@ spec: require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name})) // all left untouched, only bootstrap package added assert.Equal(t, fleet.TeamMDM{ + EnableDiskEncryption: false, MacOSSettings: fleet.MacOSSettings{ - CustomSettings: []string{mobileConfigPath}, - EnableDiskEncryption: false, + CustomSettings: []string{mobileConfigPath}, }, MacOSUpdates: fleet.MacOSUpdates{ MinimumVersion: optjson.SetString("10.10.10"), @@ -2886,7 +2886,7 @@ spec: macos_settings: enable_disk_encryption: true `, - wantErr: `Couldn't update macos_settings because MDM features aren't turned on in Fleet.`, + wantErr: `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on`, }, { desc: "app config macos_settings.enable_disk_encryption false", diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 3136dfd5b..baa6cf5c6 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -13,6 +13,7 @@ import ( "time" "github.com/fatih/color" + "github.com/fleetdm/fleet/v4/pkg/rawjson" "github.com/fleetdm/fleet/v4/pkg/secure" kithttp "github.com/go-kit/kit/transport/http" "gopkg.in/guregu/null.v3" @@ -167,12 +168,15 @@ func (eacp enrichedAppConfigPresenter) MarshalJSON() ([]byte, error) { *fleet.VulnerabilitiesConfig } - return json.Marshal(&struct { - fleet.EnrichedAppConfig + enrichedJSON, err := json.Marshal(fleet.EnrichedAppConfig(eacp)) + if err != nil { + return nil, err + } + + extraFieldsJSON, err := json.Marshal(&struct { UpdateInterval UpdateIntervalConfigPresenter `json:"update_interval,omitempty"` Vulnerabilities VulnerabilitiesConfigPresenter `json:"vulnerabilities,omitempty"` }{ - EnrichedAppConfig: fleet.EnrichedAppConfig(eacp), UpdateInterval: UpdateIntervalConfigPresenter{ eacp.UpdateInterval.OSQueryDetail.String(), eacp.UpdateInterval.OSQueryPolicy.String(), @@ -184,6 +188,13 @@ func (eacp enrichedAppConfigPresenter) MarshalJSON() ([]byte, error) { eacp.Vulnerabilities, }, }) + if err != nil { + return nil, err + } + + // we need to marshal and combine both groups separately because + // enrichedAppConfig has a custom marshaler. + return rawjson.CombineRoots(enrichedJSON, extraFieldsJSON) } func printConfig(c *cli.Context, config interface{}) error { diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index a57d06413..187264987 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -168,15 +167,15 @@ func TestGetTeams(t *testing.T) { }, nil } - b, err := ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsText.txt")) + b, err := os.ReadFile(filepath.Join("testdata", "expectedGetTeamsText.txt")) require.NoError(t, err) expectedText := string(b) - b, err = ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsYaml.yml")) + b, err = os.ReadFile(filepath.Join("testdata", "expectedGetTeamsYaml.yml")) require.NoError(t, err) expectedYaml := string(b) - b, err = ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsJson.json")) + b, err = os.ReadFile(filepath.Join("testdata", "expectedGetTeamsJson.json")) require.NoError(t, err) // must read each JSON value separately and compact it var buf bytes.Buffer @@ -206,8 +205,8 @@ func TestGetTeams(t *testing.T) { errBuffer.Reset() actualJSON, err := runWithErrWriter([]string{"get", "teams", "--json"}, &errBuffer) require.NoError(t, err) - require.Equal(t, expectedJson, actualJSON.String()) require.Equal(t, errBuffer.String() == expiredBanner.String(), tt.shouldHaveExpiredBanner) + require.Equal(t, expectedJson, actualJSON.String()) errBuffer.Reset() actualYaml, err := runWithErrWriter([]string{"get", "teams", "--yaml"}, &errBuffer) @@ -433,7 +432,7 @@ func TestGetHosts(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expected, err := ioutil.ReadFile(filepath.Join("testdata", tt.goldenFile)) + expected, err := os.ReadFile(filepath.Join("testdata", tt.goldenFile)) require.NoError(t, err) expectedResults := tt.scanner(string(expected)) actualResult := tt.scanner(runAppForTest(t, tt.args)) @@ -536,7 +535,7 @@ func TestGetHostsMDM(t *testing.T) { } if tt.goldenFile != "" { - expected, err := ioutil.ReadFile(filepath.Join("testdata", tt.goldenFile)) + expected, err := os.ReadFile(filepath.Join("testdata", tt.goldenFile)) require.NoError(t, err) if ext := filepath.Ext(tt.goldenFile); ext == ".json" { // the output of --json is not a json array, but a list of diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index e6ae712e2..2f0c98c74 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -85,6 +85,7 @@ "enabled_and_configured": false, "apple_bm_default_team": "", "windows_enabled_and_configured": false, + "enable_disk_encryption": false, "macos_updates": { "minimum_version": null, "deadline": null @@ -95,8 +96,7 @@ "webhook_url": "" }, "macos_settings": { - "custom_settings": null, - "enable_disk_encryption": false + "custom_settings": null }, "macos_setup": { "bootstrap_package": null, diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index 1c0d77868..e7a584321 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -19,6 +19,7 @@ spec: enabled_and_configured: false apple_bm_default_team: "" windows_enabled_and_configured: false + enable_disk_encryption: false macos_migration: enable: false mode: "" @@ -28,7 +29,6 @@ spec: deadline: null macos_settings: custom_settings: - enable_disk_encryption: false macos_setup: bootstrap_package: enable_end_user_authentication: false diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index 2030db5af..94c6e70a7 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -43,6 +43,7 @@ "apple_bm_enabled_and_configured": false, "enabled_and_configured": false, "windows_enabled_and_configured": false, + "enable_disk_encryption": false, "macos_updates": { "minimum_version": null, "deadline": null @@ -53,8 +54,7 @@ "webhook_url": "" }, "macos_settings": { - "custom_settings": null, - "enable_disk_encryption": false + "custom_settings": null }, "macos_setup": { "bootstrap_package": null, diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index 9d3bf00ac..1b03fe13d 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -19,6 +19,7 @@ spec: apple_bm_terms_expired: false enabled_and_configured: false windows_enabled_and_configured: false + enable_disk_encryption: false macos_migration: enable: false mode: "" @@ -28,7 +29,6 @@ spec: deadline: null macos_settings: custom_settings: - enable_disk_encryption: false macos_setup: bootstrap_package: enable_end_user_authentication: false diff --git a/cmd/fleetctl/testdata/expectedGetTeamsJson.json b/cmd/fleetctl/testdata/expectedGetTeamsJson.json index 19152a690..6a99943e9 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsJson.json +++ b/cmd/fleetctl/testdata/expectedGetTeamsJson.json @@ -24,13 +24,13 @@ "enable_software_inventory": true }, "mdm": { + "enable_disk_encryption": false, "macos_updates": { "minimum_version": null, "deadline": null }, "macos_settings": { - "custom_settings": null, - "enable_disk_encryption": false + "custom_settings": null }, "macos_setup": { "bootstrap_package": null, @@ -84,13 +84,13 @@ } }, "mdm": { + "enable_disk_encryption": false, "macos_updates": { "minimum_version": "12.3.1", "deadline": "2021-12-14" }, "macos_settings": { - "custom_settings": null, - "enable_disk_encryption": false + "custom_settings": null }, "macos_setup": { "bootstrap_package": null, diff --git a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml index 2b571ae8b..a6905cf56 100644 --- a/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetTeamsYaml.yml @@ -7,12 +7,12 @@ spec: enable_host_users: true enable_software_inventory: true mdm: + enable_disk_encryption: false macos_updates: minimum_version: null deadline: null macos_settings: custom_settings: - enable_disk_encryption: false macos_setup: bootstrap_package: enable_end_user_authentication: false @@ -36,12 +36,12 @@ spec: enable_host_users: false enable_software_inventory: false mdm: + enable_disk_encryption: false macos_updates: minimum_version: "12.3.1" deadline: "2021-12-14" macos_settings: custom_settings: - enable_disk_encryption: false macos_setup: bootstrap_package: enable_end_user_authentication: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 4fc311a8d..d641e98b0 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -19,13 +19,13 @@ spec: apple_bm_terms_expired: false enabled_and_configured: true windows_enabled_and_configured: false + enable_disk_encryption: false macos_migration: enable: false mode: "" webhook_url: "" macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: null enable_end_user_authentication: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 72b5d2c59..433d80c58 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -19,13 +19,13 @@ spec: apple_bm_terms_expired: false enabled_and_configured: true windows_enabled_and_configured: false + enable_disk_encryption: false macos_migration: enable: false mode: "" webhook_url: "" macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: %s enable_end_user_authentication: false diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml index 346bbc2eb..a3668e64b 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Empty.yml @@ -7,9 +7,9 @@ spec: enable_host_users: true enable_software_inventory: true mdm: + enable_disk_encryption: false macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: null enable_end_user_authentication: false @@ -27,9 +27,9 @@ spec: enable_host_users: true enable_software_inventory: true mdm: + enable_disk_encryption: false macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: null macos_setup_assistant: null diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml index 45f173301..95e49d032 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1And2Set.yml @@ -7,9 +7,9 @@ spec: enable_host_users: true enable_software_inventory: true mdm: + enable_disk_encryption: false macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: %s enable_end_user_authentication: false @@ -27,9 +27,9 @@ spec: enable_host_users: false enable_software_inventory: false mdm: + enable_disk_encryption: false macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: %s macos_setup_assistant: %s diff --git a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml index 21d9b9d8d..8ad10fc6c 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedTeam1Empty.yml @@ -7,9 +7,9 @@ spec: enable_host_users: false enable_software_inventory: false mdm: + enable_disk_encryption: false macos_settings: custom_settings: null - enable_disk_encryption: false macos_setup: bootstrap_package: null enable_end_user_authentication: false diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index 7724b770b..ddefdb2e9 100644 --- a/docs/Contributing/API-for-contributors.md +++ b/docs/Contributing/API-for-contributors.md @@ -533,6 +533,7 @@ The MDM endpoints exist to support the related command-line interface sub-comman - [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment) - [Preassign profiles to devices](#preassign-profiles-to-devices) - [Match preassigned profiles](#match-preassigned-profiles) +- [Get FileVault statistics](#get-filevault-statistics) ### Generate Apple DEP Key Pair @@ -701,6 +702,44 @@ This endpoint stores a profile to be assigned to a host at some point in the fut `Status: 204` +### Get FileVault statistics + +_Available in Fleet Premium_ + +Get aggregate status counts of disk encryption enforced on macOS hosts. + +The summary can optionally be filtered by team id. + +`GET /api/v1/fleet/mdm/apple/filevault/summary` + +#### Parameters + +| Name | Type | In | Description | +| ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | +| team_id | string | query | _Available in Fleet Premium_ The team id to filter the summary. | + +#### Example + +Get aggregate status counts of Apple disk encryption profiles applying to macOS hosts enrolled to Fleet's MDM that are not assigned to any team. + +`GET /api/v1/fleet/mdm/apple/filevault/summary` + +##### Default response + +`Status: 200` + +```json +{ + "verified": 123, + "verifying": 123, + "action_required": 123, + "enforcing": 123, + "failed": 123, + "removing_enforcement": 123 +} +``` + + ### Match preassigned profiles _Available in Fleet Premium_ @@ -2291,7 +2330,9 @@ Gets all information required by Fleet Desktop, this includes things like the nu { "failing_policies_count": 3, "notifications": { - "needs_mdm_migration": true + "needs_mdm_migration": true, + "renew_enrollment_profile": false, + "enforce_bitlocker_encryption": false, }, "config": { "org_info": { @@ -2313,6 +2354,7 @@ In regards to the `notifications` key: - `needs_mdm_migration` means that the device fits all the requirements to allow the user to initiate an MDM migration to Fleet. - `renew_enrollment_profile` means that the device is currently unmanaged from MDM but should be DEP enrolled into Fleet. +- `enforce_bitlocker_encryption` applies only to Windows devices and means that it should encrypt the disk and report the encryption key back to Fleet. #### Get device's policies diff --git a/docs/Contributing/FAQ.md b/docs/Contributing/FAQ.md index 3d3f69c92..c7522aa53 100644 --- a/docs/Contributing/FAQ.md +++ b/docs/Contributing/FAQ.md @@ -93,6 +93,7 @@ If you also have Fleetd running on hosts, it will need access to these API endpo * `/api/fleet/orbit/ping` * `/api/fleet/orbit/scripts/request` * `/api/fleet/orbit/scripts/result` +* `/api/fleet/orbit/disk_encryption_key` * `/api/osquery/log` diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index d41904dc9..753288389 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -1829,14 +1829,14 @@ the `software` table. | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | | order_key | string | query | What to order results by. Can be any column in the hosts table. | -| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. **Note:** Use `page` instead of `after`. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | -| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. | -| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). | -| additional_info_filters | string | query | A comma-delimited list of fields to include in each host's additional information object. See [Fleet Configuration Options](https://fleetdm.com/docs/using-fleet/fleetctl-cli#fleet-configuration-options) for an example configuration with hosts' additional information. Use `*` to get all stored fields. | +| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. **Note:** Use `page` instead of `after` | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. | +| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | +| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). | +| additional_info_filters | string | query | A comma-delimited list of fields to include in each host's additional information object. See [Fleet Configuration Options](https://fleetdm.com/docs/using-fleet/fleetctl-cli#fleet-configuration-options) for an example configuration with hosts' additional information. Use '*' to get all stored fields. | | team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. | | policy_id | integer | query | The ID of the policy to filter hosts by. | -| policy_response | string | query | Valid options are `passing` or `failing`. `policy_id` must also be specified with `policy_response`. | +| policy_response | string | query | Valid options are 'passing' or 'failing'. `policy_id` must also be specified with `policy_response`. | | software_id | integer | query | The ID of the software to filter hosts by. | | os_id | integer | query | The ID of the operating system to filter hosts by. | | os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` | @@ -1849,8 +1849,11 @@ the `software` table. | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | disable_failing_policies| boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | -| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of `verified`, `verifying`, `action_required`, `enforcing`, `failed`, or `removing_enforcement`. | -| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. | +| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. | +| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. | +| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | + If `additional_info_filters` is not specified, no `additional` information will be returned. @@ -1858,9 +1861,9 @@ If `software_id` is specified, an additional top-level key `"software"` is retur If `mdm_id` is specified, an additional top-level key `"mobile_device_management_solution"` is returned with the information corresponding to the `mdm_id`. -If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. +If `mdm_id`, `mdm_name`, `mdm_enrollment_status`, `os_settings`, or `os_settings_disk_encryption` is specified, then Windows Servers are excluded from the results. -If `munki_issue_id` is specified, an additional top-level key `"munki_issue"` is returned with the information corresponding to the `munki_issue_id`. +If `munki_issue_id` is specified, an additional top-level key `munki_issue` is returned with the information corresponding to the `munki_issue_id`. If `after` is being used with `created_at` or `updated_at`, the table must be specified in `order_key`. Those columns become `h.created_at` and `h.updated_at`. @@ -1988,13 +1991,13 @@ Response payload with the `munki_issue_id` filter provided: | Name | Type | In | Description | | ----------------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | order_key | string | query | What to order results by. Can be any column in the hosts table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. | | after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. | -| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. | -| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). | +| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | +| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). | | team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. | | policy_id | integer | query | The ID of the policy to filter hosts by. | -| policy_response | string | query | Valid options are `passing` or `failing`. `policy_id` must also be specified with `policy_response`. | +| policy_response | string | query | Valid options are 'passing' or 'failing'. `policy_id` must also be specified with `policy_response`. | | software_id | integer | query | The ID of the software to filter hosts by. | | os_id | integer | query | The ID of the operating system to filter hosts by. | | os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` | @@ -2006,8 +2009,10 @@ Response payload with the `munki_issue_id` filter provided: | macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | -| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of `verified`, `verifying`, `action_required`, `enforcing`, `failed`, or `removing_enforcement`. | -| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. | +| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | If `additional_info_filters` is not specified, no `additional` information will be returned. @@ -2555,6 +2560,9 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id "bootstrap_package_status": "installed", "detail": "" }, + "os_settings": { + "disk_encryption": null + }, "profiles": [ { "profile_id": 999, @@ -2743,6 +2751,9 @@ This is the API route used by the **My device** page in Fleet desktop to display "detail": "", "bootstrap_package_name": "test.pkg" }, + "os_settings": { + "disk_encryption": null + }, "profiles": [ { "profile_id": 999, @@ -3291,12 +3302,12 @@ requested by a web browser. | format | string | query | **Required**, must be "csv" (only supported format for now). | | columns | string | query | Comma-delimited list of columns to include in the report (returns all columns if none is specified). | | order_key | string | query | What to order results by. Can be any column in the hosts table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | -| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. | +| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | | query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). | | team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. | | policy_id | integer | query | The ID of the policy to filter hosts by. | -| policy_response | string | query | Valid options are `passing` or `failing`. `policy_id` must also be specified with `policy_response`. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** | +| policy_response | string | query | Valid options are 'passing' or 'failing'. `policy_id` must also be specified with `policy_response`. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** | | software_id | integer | query | The ID of the software to filter hosts by. | | os_id | integer | query | The ID of the operating system to filter hosts by. | | os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` | @@ -3308,7 +3319,7 @@ requested by a web browser. | munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | | label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `status`, `query` and `team_id`. | -| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | disable_failing_policies | boolean | query | If `true`, hosts will return failing policies as 0 (returned as the `issues` column) regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. @@ -3330,7 +3341,7 @@ created_at,updated_at,id,detail_updated_at,label_updated_at,policy_updated_at,la ### Get host's disk encryption key -Requires the [macadmins osquery extension](https://github.com/macadmins/osquery-extension) which comes bundled +For macOS, requires the [macadmins osquery extension](https://github.com/macadmins/osquery-extension) which comes bundled in [Fleet's osquery installers](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer). Requires Fleet's MDM properly [enabled and configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup). @@ -3724,9 +3735,9 @@ Returns a list of the hosts that belong to the specified label. | page | integer | query | Page number of the results to fetch. | | per_page | integer | query | Results per page. | | order_key | string | query | What to order results by. Can be any column in the hosts table. | -| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. | +| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. | | after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. | -| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. | +| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. | | query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, and `ipv4`. | | team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. | | disable_failing_policies | boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. | @@ -3735,10 +3746,12 @@ Returns a list of the hosts that belong to the specified label. | mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. | | macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | | low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. | -| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of `verified`, `verifying`, `action_required`, `enforcing`, `failed`, or `removing_enforcement`. | -| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. | +| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | +| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** | -If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results. +If `mdm_id`, `mdm_name`, `mdm_enrollment_status`, `os_settings`, or `os_settings_disk_encryption` is specified, then Windows Servers are excluded from the results. #### Example @@ -4090,23 +4103,23 @@ _Available in Fleet Premium_ _Available in Fleet Premium_ -Get aggregate status counts of disk encryption enforced on hosts. +Get aggregate status counts of disk encryption enforced on macOS and Windows hosts. The summary can optionally be filtered by team id. -`GET /api/v1/fleet/mdm/apple/filevault/summary` +`GET /api/v1/fleet/mdm/disk_encryption/summary` #### Parameters | Name | Type | In | Description | | ------------------------- | ------ | ----- | ------------------------------------------------------------------------- | -| team_id | string | query | _Available in Fleet Premium_ The team id to filter the summary. | +| team_id | string | query | _Available in Fleet Premium_ The team id to filter the summary. | #### Example -Get aggregate status counts of Apple disk encryption profiles applying to macOS hosts enrolled to Fleet's MDM that are not assigned to any team. +Get aggregate disk encryption status counts of macOS and Windows hosts enrolled to Fleet's MDM that are not assigned to any team. -`GET /api/v1/fleet/mdm/apple/filevault/summary` +`GET /api/v1/fleet/mdm/disk_encryption/summary` ##### Default response @@ -4114,12 +4127,12 @@ Get aggregate status counts of Apple disk encryption profiles applying to macOS ```json { - "verified": 123, - "verifying": 123, - "action_required": 123, - "enforcing": 123, - "failed": 123, - "removing_enforcement": 123 + "verified": {"macos": 123, "windows": 123}, + "verifying": {"macos": 123, "windows": 0}, + "action_required": {"macos": 123, "windows": 0}, + "enforcing": {"macos": 123, "windows": 123}, + "failed": {"macos": 123, "windows": 123}, + "removing_enforcement": {"macos": 123, "windows": 0}, } ``` diff --git a/docs/Using Fleet/manage-access.md b/docs/Using Fleet/manage-access.md index 48c1a2152..90774c86e 100644 --- a/docs/Using Fleet/manage-access.md +++ b/docs/Using Fleet/manage-access.md @@ -75,7 +75,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | View Apple mobile device management (MDM) certificate information | | | | ✅ | | | View Apple business manager (BM) information | | | | ✅ | | | Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ | | -| View disk encryption key for macOS hosts | ✅ | ✅ | ✅ | ✅ | | +| View disk encryption key for macOS and Windows hosts | ✅ | ✅ | ✅ | ✅ | | | Create edit and delete configuration profiles for macOS hosts | | | ✅ | ✅ | ✅ | | Execute MDM commands on macOS and Windows hosts*** | | | ✅ | ✅ | | | View results of MDM commands executed on macOS and Windows hosts*** | ✅ | ✅ | ✅ | ✅ | | diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index afd304a6c..37cf53721 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/fleetdm/fleet/v4/pkg/file" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxdb" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -890,12 +891,7 @@ func (svc *Service) getOrCreatePreassignTeam(ctx context.Context, groups []strin } payload.MDM = &fleet.TeamPayloadMDM{ - MacOSSettings: &fleet.MacOSSettings{ - // teams created by the match endpoint have disk encryption - // enabled by default. - // TODO: maybe make this configurable? - EnableDiskEncryption: true, - }, + EnableDiskEncryption: optjson.SetBool(true), MacOSSetup: &fleet.MacOSSetup{ MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant, // NOTE: BootstrapPackage is currently ignored by svc.ModifyTeam and gets set @@ -968,3 +964,51 @@ func teamNameFromPreassignGroups(groups []string) string { return strings.Join(groups, " - ") } + +func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*fleet.MDMDiskEncryptionSummary, error) { + // TODO: Consider adding a new generic OSSetting type or Windows-specific type for authz checks + // like this. + if err := svc.authz.Authorize(ctx, fleet.MDMAppleConfigProfile{TeamID: teamID}, fleet.ActionRead); err != nil { + return nil, ctxerr.Wrap(ctx, err) + } + var macOS fleet.MDMAppleFileVaultSummary + if m, err := svc.ds.GetMDMAppleFileVaultSummary(ctx, teamID); err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting filevault summary") + } else if m != nil { + macOS = *m + } + + var windows fleet.MDMWindowsBitLockerSummary + if w, err := svc.ds.GetMDMWindowsBitLockerSummary(ctx, teamID); err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting bitlocker summary") + } else if w != nil { + windows = *w + } + + return &fleet.MDMDiskEncryptionSummary{ + Verified: fleet.MDMPlatformsCounts{ + MacOS: macOS.Verified, + Windows: windows.Verified, + }, + Verifying: fleet.MDMPlatformsCounts{ + MacOS: macOS.Verifying, + Windows: windows.Verifying, + }, + ActionRequired: fleet.MDMPlatformsCounts{ + MacOS: macOS.ActionRequired, + Windows: windows.ActionRequired, + }, + Enforcing: fleet.MDMPlatformsCounts{ + MacOS: macOS.Enforcing, + Windows: windows.Enforcing, + }, + Failed: fleet.MDMPlatformsCounts{ + MacOS: macOS.Failed, + Windows: windows.Failed, + }, + RemovingEnforcement: fleet.MDMPlatformsCounts{ + MacOS: macOS.RemovingEnforcement, + Windows: windows.RemovingEnforcement, + }, + }, nil +} diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 33a777323..529408de4 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -150,13 +150,13 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T } } - if payload.MDM.MacOSSettings != nil { - if !appCfg.MDM.EnabledAndConfigured && payload.MDM.MacOSSettings.EnableDiskEncryption { + if payload.MDM.EnableDiskEncryption.Valid { + macOSDiskEncryptionUpdated = team.Config.MDM.EnableDiskEncryption != payload.MDM.EnableDiskEncryption.Value + if macOSDiskEncryptionUpdated && !appCfg.MDM.EnabledAndConfigured { return nil, fleet.NewInvalidArgumentError("macos_settings.enable_disk_encryption", `Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`) } - macOSDiskEncryptionUpdated = team.Config.MDM.MacOSSettings.EnableDiskEncryption != payload.MDM.MacOSSettings.EnableDiskEncryption - team.Config.MDM.MacOSSettings.EnableDiskEncryption = payload.MDM.MacOSSettings.EnableDiskEncryption + team.Config.MDM.EnableDiskEncryption = payload.MDM.EnableDiskEncryption.Value } if payload.MDM.MacOSSetup != nil { @@ -225,7 +225,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T } if macOSDiskEncryptionUpdated { var act fleet.ActivityDetails - if team.Config.MDM.MacOSSettings.EnableDiskEncryption { + if team.Config.MDM.EnableDiskEncryption { act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name} if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil { return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow") @@ -802,6 +802,17 @@ func (svc *Service) createTeamFromSpec( `Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)) } } + enableDiskEncryption := spec.MDM.EnableDiskEncryption.Value + if !spec.MDM.EnableDiskEncryption.Valid { + if de := macOSSettings.DeprecatedEnableDiskEncryption; de != nil { + enableDiskEncryption = *de + } + } + + if enableDiskEncryption && !defaults.MDM.AtLeastOnePlatformEnabledAndConfigured() { + return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", + `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)) + } if dryRun { return &fleet.Team{Name: spec.Name}, nil @@ -813,9 +824,10 @@ func (svc *Service) createTeamFromSpec( AgentOptions: agentOptions, Features: features, MDM: fleet.TeamMDM{ - MacOSUpdates: spec.MDM.MacOSUpdates, - MacOSSettings: macOSSettings, - MacOSSetup: macOSSetup, + EnableDiskEncryption: enableDiskEncryption, + MacOSUpdates: spec.MDM.MacOSUpdates, + MacOSSettings: macOSSettings, + MacOSSetup: macOSSetup, }, }, Secrets: secrets, @@ -824,7 +836,7 @@ func (svc *Service) createTeamFromSpec( return nil, err } - if macOSSettings.EnableDiskEncryption { + if enableDiskEncryption && defaults.MDM.EnabledAndConfigured { if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil { return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow") } @@ -871,11 +883,23 @@ func (svc *Service) editTeamFromSpec( team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates } - oldMacOSDiskEncryption := team.Config.MDM.MacOSSettings.EnableDiskEncryption + oldMacOSDiskEncryption := team.Config.MDM.EnableDiskEncryption if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil { return err } - newMacOSDiskEncryption := team.Config.MDM.MacOSSettings.EnableDiskEncryption + + // 1. if the spec has the new setting, use that + // 2. else if the spec has the deprecated setting, use that + // 3. otherwise, leave the setting untouched + if spec.MDM.EnableDiskEncryption.Valid { + team.Config.MDM.EnableDiskEncryption = spec.MDM.EnableDiskEncryption.Value + } else if de := team.Config.MDM.MacOSSettings.DeprecatedEnableDiskEncryption; de != nil { + team.Config.MDM.EnableDiskEncryption = *de + } + if team.Config.MDM.EnableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm", + `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)) + } oldMacOSSetup := team.Config.MDM.MacOSSetup if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set || spec.MDM.MacOSSetup.BootstrapPackage.Set { @@ -925,9 +949,9 @@ func (svc *Service) editTeamFromSpec( return err } } - if oldMacOSDiskEncryption != newMacOSDiskEncryption { + if appCfg.MDM.EnabledAndConfigured && oldMacOSDiskEncryption != team.Config.MDM.EnableDiskEncryption { var act fleet.ActivityDetails - if team.Config.MDM.MacOSSettings.EnableDiskEncryption { + if team.Config.MDM.EnableDiskEncryption { act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name} if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil { return ctxerr.Wrap(ctx, err, "enable team filevault and escrow") @@ -982,7 +1006,7 @@ func (svc *Service) applyTeamMacOSSettings(ctx context.Context, spec *fleet.Team } if (setFields["custom_settings"] && len(applyUpon.CustomSettings) > 0) || - (setFields["enable_disk_encryption"] && applyUpon.EnableDiskEncryption) { + (setFields["enable_disk_encryption"] && *applyUpon.DeprecatedEnableDiskEncryption) { field := "custom_settings" if !setFields["custom_settings"] { field = "enable_disk_encryption" @@ -1016,8 +1040,8 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) { func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Team, payload fleet.MDMAppleSettingsPayload) error { var didUpdate, didUpdateMacOSDiskEncryption bool if payload.EnableDiskEncryption != nil { - if tm.Config.MDM.MacOSSettings.EnableDiskEncryption != *payload.EnableDiskEncryption { - tm.Config.MDM.MacOSSettings.EnableDiskEncryption = *payload.EnableDiskEncryption + if tm.Config.MDM.EnableDiskEncryption != *payload.EnableDiskEncryption { + tm.Config.MDM.EnableDiskEncryption = *payload.EnableDiskEncryption didUpdate = true didUpdateMacOSDiskEncryption = true } @@ -1029,7 +1053,7 @@ func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Te } if didUpdateMacOSDiskEncryption { var act fleet.ActivityDetails - if tm.Config.MDM.MacOSSettings.EnableDiskEncryption { + if tm.Config.MDM.EnableDiskEncryption { act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name} if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil { return ctxerr.Wrap(ctx, err, "enable team filevault and escrow") diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts index c51d2895d..f225356c8 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -125,6 +125,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = { }, fleet_desktop: { transparency_url: "https://fleetdm.com/transparency" }, mdm: { + enable_disk_encryption: false, windows_enabled_and_configured: true, apple_bm_default_team: "Apples", apple_bm_enabled_and_configured: true, diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts index c2e099364..6834d0c70 100644 --- a/frontend/__mocks__/hostMock.ts +++ b/frontend/__mocks__/hostMock.ts @@ -1,7 +1,7 @@ import { IHost } from "interfaces/host"; -import { IHostMacMdmProfile } from "interfaces/mdm"; +import { IHostMdmProfile } from "interfaces/mdm"; -const DEFAULT_HOST_PROFILE_MOCK: IHostMacMdmProfile = { +const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = { profile_id: 1, name: "Test Profile", operation_type: "install", @@ -10,8 +10,8 @@ const DEFAULT_HOST_PROFILE_MOCK: IHostMacMdmProfile = { }; export const createMockHostMacMdmProfile = ( - overrides?: Partial -): IHostMacMdmProfile => { + overrides?: Partial +): IHostMdmProfile => { return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides }; }; @@ -53,6 +53,11 @@ const DEFAULT_HOST_MOCK: IHost = { enrollment_status: "Off", server_url: "https://www.example.com/1", profiles: [], + os_settings: { + disk_encryption: { + status: null, + }, + }, macos_settings: { disk_encryption: null, action_required: null, diff --git a/frontend/__mocks__/mdmMock.ts b/frontend/__mocks__/mdmMock.ts index c0584bf66..5ffebcdbc 100644 --- a/frontend/__mocks__/mdmMock.ts +++ b/frontend/__mocks__/mdmMock.ts @@ -36,6 +36,11 @@ const DEFAULT_HOST_MDM_DATA: IHostMdmData = { name: "MDM Solution", id: 1, profiles: [], + os_settings: { + disk_encryption: { + status: "verified", + }, + }, macos_settings: { disk_encryption: null, action_required: null, diff --git a/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx b/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx index e510748a7..95ae8d565 100644 --- a/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx +++ b/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx @@ -23,7 +23,10 @@ interface IStatusIndicatorWithIconProps { tooltipText: string | JSX.Element; position?: "top" | "bottom"; }; + layout?: "horizontal" | "vertical"; className?: string; + /** Classname to add to the value text */ + valueClassName?: string; } const statusIconNameMapping: Record = { @@ -38,13 +41,18 @@ const StatusIndicatorWithIcon = ({ status, value, tooltip, + layout = "horizontal", className, + valueClassName, }: IStatusIndicatorWithIconProps) => { const classNames = classnames(baseClass, className); const id = `status-${uniqueId()}`; + const valueClasses = classnames(`${baseClass}__value`, valueClassName, { + [`${baseClass}__value-vertical`]: layout === "vertical", + }); const valueContent = ( - + {value} diff --git a/frontend/components/StatusIndicatorWithIcon/_styles.scss b/frontend/components/StatusIndicatorWithIcon/_styles.scss index 6dea8de69..12d60c0f2 100644 --- a/frontend/components/StatusIndicatorWithIcon/_styles.scss +++ b/frontend/components/StatusIndicatorWithIcon/_styles.scss @@ -1,4 +1,5 @@ .status-indicator-with-icon { + // default layout is horizontal &__value { display: inline-flex; align-items: center; @@ -8,4 +9,10 @@ margin-right: $pad-xsmall; } } + + // overrides for different layout + &__value-vertical { + flex-direction: column; + gap: $pad-xsmall; + } } diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index c604629c0..2bfbe6fad 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -1,95 +1,11 @@ /* Config interface is a flattened version of the fleet/config API response */ - import { IWebhookHostStatus, IWebhookFailingPolicies, IWebhookSoftwareVulnerabilities, } from "interfaces/webhook"; -import PropTypes from "prop-types"; import { IIntegrations } from "./integration"; -export default PropTypes.shape({ - org_name: PropTypes.string, - org_logo_url: PropTypes.string, - contact_url: PropTypes.string, - server_url: PropTypes.string, - live_query_disabled: PropTypes.bool, - enable_analytics: PropTypes.bool, - enable_smtp: PropTypes.bool, - configured: PropTypes.bool, - sender_address: PropTypes.string, - server: PropTypes.string, - port: PropTypes.number, - authentication_type: PropTypes.string, - user_name: PropTypes.string, - password: PropTypes.string, - enable_ssl_tls: PropTypes.bool, - authentication_method: PropTypes.string, - domain: PropTypes.string, - verify_sll_certs: PropTypes.bool, - enable_start_tls: PropTypes.bool, - entity_id: PropTypes.string, - idp_image_url: PropTypes.string, - metadata: PropTypes.string, - metadata_url: PropTypes.string, - idp_name: PropTypes.string, - enable_sso: PropTypes.bool, - enable_sso_idp_login: PropTypes.bool, - enable_jit_provisioning: PropTypes.bool, - host_expiry_enabled: PropTypes.bool, - host_expiry_window: PropTypes.number, - agent_options: PropTypes.string, - tier: PropTypes.string, - organization: PropTypes.string, - device_count: PropTypes.number, - expiration: PropTypes.string, - mdm: PropTypes.shape({ - enabled_and_configured: PropTypes.bool, - apple_bm_terms_expired: PropTypes.bool, - apple_bm_enabled_and_configured: PropTypes.bool, - windows_enabled_and_configured: PropTypes.bool, - macos_updates: PropTypes.shape({ - minimum_version: PropTypes.string, - deadline: PropTypes.string, - }), - }), - note: PropTypes.string, - // vulnerability_settings: PropTypes.any, TODO - enable_host_status_webhook: PropTypes.bool, - destination_url: PropTypes.string, - host_percentage: PropTypes.number, - days_count: PropTypes.number, - logging: PropTypes.shape({ - debug: PropTypes.bool, - json: PropTypes.bool, - result: PropTypes.shape({ - plugin: PropTypes.string, - config: PropTypes.shape({ - status_log_file: PropTypes.string, - result_log_file: PropTypes.string, - enable_log_rotation: PropTypes.bool, - enable_log_compression: PropTypes.bool, - }), - }), - status: PropTypes.shape({ - plugin: PropTypes.string, - config: PropTypes.shape({ - status_log_file: PropTypes.string, - result_log_file: PropTypes.string, - enable_log_rotation: PropTypes.bool, - enable_log_compression: PropTypes.bool, - }), - }), - }), - email: PropTypes.shape({ - backend: PropTypes.string, - config: PropTypes.shape({ - region: PropTypes.string, - source_arn: PropTypes.string, - }), - }), -}); - export interface ILicense { tier: string; device_count: number; @@ -113,6 +29,7 @@ export interface IMacOsMigrationSettings { } export interface IMdmConfig { + enable_disk_encryption: boolean; enabled_and_configured: boolean; apple_bm_default_team?: string; apple_bm_terms_expired: boolean; @@ -285,7 +202,10 @@ export interface IConfig { }; }; mdm: IMdmConfig; - mdm_enabled?: boolean; // TODO: remove when windows MDM is released. Only used for windows MDM dev currently. + /** This is the flag that determines if the windwos mdm feature flag is enabled. + TODO: WINDOWS FEATURE FLAG: remove when windows MDM is released. Only used for windows MDM dev currently. + */ + mdm_enabled?: boolean; } export interface IWebhookSettings { diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 1927351b7..ebfde247f 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -8,9 +8,10 @@ import hostQueryResult from "./campaign"; import queryStatsInterface, { IQueryStats } from "./query_stats"; import { ILicense, IDeviceGlobalConfig } from "./config"; import { - IHostMacMdmProfile, + IHostMdmProfile, MdmEnrollmentStatus, BootstrapPackageStatus, + DiskEncryptionStatus, } from "./mdm"; export default PropTypes.shape({ @@ -90,18 +91,16 @@ export interface IMunkiData { version: string; } -type MacDiskEncryptionState = - | "applied" - | "action_required" - | "enforcing" - | "failed" - | "removing_enforcement" - | null; - type MacDiskEncryptionActionRequired = "log_out" | "rotate_key" | null; +export interface IOSSettings { + disk_encryption: { + status: DiskEncryptionStatus | null; + }; +} + interface IMdmMacOsSettings { - disk_encryption: MacDiskEncryptionState | null; + disk_encryption: DiskEncryptionStatus | null; action_required: MacDiskEncryptionActionRequired | null; } @@ -117,7 +116,8 @@ export interface IHostMdmData { name?: string; server_url: string | null; id?: number; - profiles: IHostMacMdmProfile[] | null; + profiles: IHostMdmProfile[] | null; + os_settings?: IOSSettings; macos_settings?: IMdmMacOsSettings; macos_setup?: IMdmMacOsSetup; } @@ -210,7 +210,7 @@ export interface IHost { osquery_version: string; os_version: string; build: string; - platform_like: string; + platform_like: string; // TODO: replace with more specific union type code_name: string; uptime: number; memory: number; diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 25f0f0d7e..1120e1ab6 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -22,8 +22,6 @@ export const MDM_ENROLLMENT_STATUS = { export type MdmEnrollmentStatus = keyof typeof MDM_ENROLLMENT_STATUS; -export type ProfileSummaryResponse = Record; - export interface IMdmStatusCardData { status: MdmEnrollmentStatus; hosts: number; @@ -74,16 +72,15 @@ export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed"; export type MacMdmProfileOperationType = "remove" | "install"; -export interface IHostMacMdmProfile { +export interface IHostMdmProfile { profile_id: number; name: string; - // identifier?: string; // TODO: add when API is updated to return this - operation_type: MacMdmProfileOperationType; + operation_type: MacMdmProfileOperationType | null; status: MdmProfileStatus; detail: string; } -export type FileVaultProfileStatus = +export type DiskEncryptionStatus = | "verified" | "verifying" | "action_required" @@ -91,9 +88,18 @@ export type FileVaultProfileStatus = | "failed" | "removing_enforcement"; -// // TODO: update when list profiles API returns identifier -// export const FLEET_FILEVAULT_PROFILE_IDENTIFIER = -// "com.fleetdm.fleet.mdm.filevault"; +/** Currently windows disk enxryption status will only be one of these four +values. In the future we may add more. */ +export type IWindowsDiskEncryptionStatus = Extract< + DiskEncryptionStatus, + "verified" | "verifying" | "enforcing" | "failed" +>; + +export const isWindowsDiskEncryptionStatus = ( + status: DiskEncryptionStatus +): status is IWindowsDiskEncryptionStatus => { + return !["action_required", "removing_enforcement"].includes(status); +}; export const FLEET_FILEVAULT_PROFILE_DISPLAY_NAME = "Disk encryption"; diff --git a/frontend/interfaces/team.ts b/frontend/interfaces/team.ts index 3c06c11c3..4487dba48 100644 --- a/frontend/interfaces/team.ts +++ b/frontend/interfaces/team.ts @@ -44,6 +44,7 @@ export interface ITeam extends ITeamSummary { secrets?: IEnrollSecret[]; role?: UserRole; // role value is included when the team is in the context of a user mdm?: { + enable_disk_encryption: boolean; macos_updates: { minimum_version: string; deadline: string; diff --git a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/AggregateMacSettingsIndicators.tsx b/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/AggregateMacSettingsIndicators.tsx deleted file mode 100644 index 7c4b49060..000000000 --- a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/AggregateMacSettingsIndicators.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from "react"; - -import paths from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; -import { MdmProfileStatus, ProfileSummaryResponse } from "interfaces/mdm"; -import MacSettingsIndicator from "pages/hosts/details/MacSettingsIndicator"; - -import { IconNames } from "components/icons"; -import Spinner from "components/Spinner"; - -const baseClass = "aggregate-mac-settings-indicators"; - -interface IAggregateDisplayOption { - value: MdmProfileStatus; - text: string; - iconName: IconNames; - tooltipText: string; -} - -const AGGREGATE_STATUS_DISPLAY_OPTIONS: IAggregateDisplayOption[] = [ - { - value: "verified", - text: "Verified", - iconName: "success", - tooltipText: - "These hosts installed all configuration profiles. Fleet verified with osquery.", - }, - { - value: "verifying", - text: "Verifying", - iconName: "success-partial", - tooltipText: - "These hosts acknowledged all MDM commands to install configuration profiles. " + - "Fleet is verifying the profiles are installed with osquery.", - }, - { - value: "pending", - text: "Pending", - iconName: "pending-partial", - tooltipText: - "These hosts will receive MDM commands to install configuration profiles when the hosts come online.", - }, - { - value: "failed", - text: "Failed", - iconName: "error", - tooltipText: - "These hosts failed to install configuration profiles. Click on a host to view error(s).", - }, -]; - -interface AggregateMacSettingsIndicatorsProps { - isLoading: boolean; - teamId: number; - aggregateProfileStatusData?: ProfileSummaryResponse; -} - -const AggregateMacSettingsIndicators = ({ - isLoading, - teamId, - aggregateProfileStatusData, -}: AggregateMacSettingsIndicatorsProps) => { - const indicators = AGGREGATE_STATUS_DISPLAY_OPTIONS.map((status) => { - if (!aggregateProfileStatusData) return null; - - const { value, text, iconName, tooltipText } = status; - const count = aggregateProfileStatusData[value]; - - return ( - - ); - }); - - if (isLoading) { - return ( -
- -
- ); - } - - return
{indicators}
; -}; - -export default AggregateMacSettingsIndicators; diff --git a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/index.ts b/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/index.ts deleted file mode 100644 index 1032f0ac5..000000000 --- a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AggregateMacSettingsIndicators"; diff --git a/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx index dddbffc04..9eb28475b 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx @@ -4,12 +4,11 @@ import { useQuery } from "react-query"; import { AppContext } from "context/app"; import SideNav from "pages/admin/components/SideNav"; -import { ProfileSummaryResponse } from "interfaces/mdm"; import { API_NO_TEAM_ID, APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import mdmAPI from "services/entities/mdm"; import OS_SETTINGS_NAV_ITEMS from "./OSSettingsNavItems"; -import AggregateMacSettingsIndicators from "./AggregateMacSettingsIndicators"; +import ProfileStatusAggregate from "./ProfileStatusAggregate"; import TurnOnMdmMessage from "../components/TurnOnMdmMessage"; const baseClass = "os-settings"; @@ -40,9 +39,10 @@ const OSSettings = ({ data: aggregateProfileStatusData, refetch: refetchAggregateProfileStatus, isLoading: isLoadingAggregateProfileStatus, - } = useQuery( + } = useQuery( ["aggregateProfileStatuses", teamId], - () => mdmAPI.getAggregateProfileStatuses(teamId), + () => + mdmAPI.getAggregateProfileStatuses(teamId, config?.mdm_enabled ?? false), { refetchOnWindowFocus: false, retry: false, @@ -50,7 +50,10 @@ const OSSettings = ({ ); // MDM is not on so show messaging for user to enable it. - if (!config?.mdm.enabled_and_configured) { + if ( + !config?.mdm.enabled_and_configured && + !config?.mdm.windows_enabled_and_configured + ) { return ; } @@ -67,7 +70,7 @@ const OSSettings = ({

Remotely enforce settings on macOS hosts assigned to this team.

- { + const generateFilterHostsByStatusLink = () => { + return `${paths.MANAGE_HOSTS}?${buildQueryStringFromParams({ + team_id: teamId, + macos_settings: statusValue, + })}`; + }; + + return ( + + ); +}; + +interface ProfileStatusAggregateProps { + isLoading: boolean; + teamId: number; + aggregateProfileStatusData?: ProfileStatusSummaryResponse; +} + +const ProfileStatusAggregate = ({ + isLoading, + teamId, + aggregateProfileStatusData, +}: ProfileStatusAggregateProps) => { + if (!aggregateProfileStatusData) return null; + + if (isLoading) { + return ( +
+ +
+ ); + } + + const indicators = AGGREGATE_STATUS_DISPLAY_OPTIONS.map((status) => { + const { value, text, iconName, tooltipText } = status; + const count = aggregateProfileStatusData[value]; + + return ( + + ); + }); + + return
{indicators}
; +}; + +export default ProfileStatusAggregate; diff --git a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts new file mode 100644 index 000000000..8dbe94abf --- /dev/null +++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregateOptions.ts @@ -0,0 +1,43 @@ +import { MdmProfileStatus } from "interfaces/mdm"; +import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon"; + +interface IAggregateDisplayOption { + value: MdmProfileStatus; + text: string; + iconName: IndicatorStatus; + tooltipText: string; +} + +const AGGREGATE_STATUS_DISPLAY_OPTIONS: IAggregateDisplayOption[] = [ + { + value: "verified", + text: "Verified", + iconName: "success", + tooltipText: + "These hosts applied all OS settings. Fleet verified with osquery.", + }, + { + value: "verifying", + text: "Verifying", + iconName: "successPartial", + tooltipText: + "These hosts acknowledged all MDM commands to apply OS settings. " + + "Fleet is verifying the OS settings are applied with osquery.", + }, + { + value: "pending", + text: "Pending", + iconName: "pendingPartial", + tooltipText: + "These hosts will receive MDM command to apply OS settings when the host come online.", + }, + { + value: "failed", + text: "Failed", + iconName: "error", + tooltipText: + "These host failed to apply the latest OS settings. Click on a host to view error(s).", + }, +]; + +export default AGGREGATE_STATUS_DISPLAY_OPTIONS; diff --git a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss similarity index 77% rename from frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss rename to frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss index ac31595db..89744cf6c 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss @@ -1,4 +1,4 @@ -.aggregate-mac-settings-indicators { +.profile-status-aggregate { display: flex; height: 94px; border-top: 1px solid #e2e4ea; @@ -10,7 +10,7 @@ margin: auto; } - .aggregate-mac-settings-indicator { + &__profile-status-count { flex-grow: 1; display: flex; @@ -29,13 +29,17 @@ font-weight: $regular; } - .settings-indicator { + .profile-status-indicator { flex-direction: column; } } - .aggregate-mac-settings-indicator:last-child { + &__profile-status-count:last-child { border-top-right-radius: 6px; border-bottom-right-radius: 6px; } + + &__status-indicator-value { + font-weight: $bold; + } } diff --git a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/index.ts b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/index.ts new file mode 100644 index 000000000..a29bd5e10 --- /dev/null +++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/index.ts @@ -0,0 +1 @@ +export { default } from "./ProfileStatusAggregate"; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx index 6edd4f35f..c8cd1d2bc 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx @@ -31,7 +31,7 @@ const DiskEncryption = ({ const defaultShowDiskEncryption = currentTeamId ? false - : config?.mdm.macos_settings.enable_disk_encryption ?? false; + : config?.mdm.enable_disk_encryption ?? false; const [isLoadingTeam, setIsLoadingTeam] = useState(true); @@ -67,8 +67,7 @@ const DiskEncryption = ({ enabled: currentTeamId !== 0, select: (res) => res.team, onSuccess: (res) => { - const enableDiskEncryption = - res.mdm?.macos_settings.enable_disk_encryption ?? false; + const enableDiskEncryption = res.mdm?.enable_disk_encryption ?? false; setDiskEncryptionEnabled(enableDiskEncryption); setShowAggregate(enableDiskEncryption); setIsLoadingTeam(false); @@ -100,6 +99,19 @@ const DiskEncryption = ({ setIsLoadingTeam(false); } + const createDescriptionText = () => { + // table is showing disk encryption status. + if (showAggregate) { + return "If turned on, hosts' disk encryption keys will be stored in Fleet. "; + } + + const isWindowsFeatureFlagEnabled = config?.mdm_enabled ?? false; + const dynamicText = isWindowsFeatureFlagEnabled + ? " and “BitLocker” on Windows" + : ""; + return `Also known as “FileVault” on macOS${dynamicText}. If turned on, hosts' disk encryption keys will be stored in Fleet. `; + }; + return (

Disk encryption

@@ -124,8 +136,7 @@ const DiskEncryption = ({ On

- Apple calls this “FileVault.” If turned on, hosts' disk - encryption keys will be stored in Fleet.{" "} + {createDescriptionText()} { + const { config } = useContext(AppContext); + const { data: diskEncryptionStatusData, error: diskEncryptionStatusError, - } = useQuery( + } = useQuery( ["disk-encryption-summary", currentTeamId], - () => mdmAPI.getDiskEncryptionAggregate(currentTeamId), + () => mdmAPI.getDiskEncryptionSummary(currentTeamId), { refetchOnWindowFocus: false, retry: false, } ); - const tableHeaders = generateTableHeaders(); - - const tableData = generateTableData(diskEncryptionStatusData, currentTeamId); + // TODO: WINDOWS FEATURE FLAG: remove this when windows feature flag is removed. + // this is used to conditianlly show "View all hosts" link in table cells. + const windowsFeatureFlagEnabled = config?.mdm_enabled ?? false; + const tableHeaders = generateTableHeaders(windowsFeatureFlagEnabled); + const tableData = generateTableData( + windowsFeatureFlagEnabled, + diskEncryptionStatusData, + currentTeamId + ); if (diskEncryptionStatusError) { return ; @@ -53,8 +59,7 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => { isLoading={false} showMarkAllPages={false} isAllPagesSelected={false} - defaultSortHeader={DEFAULT_SORT_HEADER} - defaultSortDirection={DEFAULT_SORT_DIRECTION} + manualSortBy disableTableHeader disablePagination disableCount diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx index 068459d69..9a5b7baf5 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx @@ -1,7 +1,11 @@ import React from "react"; -import { FileVaultProfileStatus } from "interfaces/mdm"; -import { IFileVaultSummaryResponse } from "services/entities/mdm"; +import { DiskEncryptionStatus } from "interfaces/mdm"; +import { + IDiskEncryptionStatusAggregate, + IDiskEncryptionSummaryResponse, +} from "services/entities/mdm"; +import { DISK_ENCRYPTION_QUERY_PARAM_NAME } from "services/entities/hosts"; import TextCell from "components/TableContainer/DataTable/TextCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -12,7 +16,7 @@ import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndica interface IStatusCellValue { displayName: string; statusName: IndicatorStatus; - value: FileVaultProfileStatus; + value: DiskEncryptionStatus; tooltip?: string | JSX.Element; } @@ -28,6 +32,7 @@ interface ICellProps { }; row: { original: { + includeWindows: boolean; status: IStatusCellValue; teamId: number; }; @@ -72,15 +77,53 @@ const defaultTableHeaders: IDataColumn[] = [ }, }, { - title: "Hosts", + title: "macOS hosts", Header: (cellProps: IHeaderProps) => ( ), - accessor: "hosts", + disableSortBy: true, + accessor: "macosHosts", + Cell: ({ + cell: { value: aggregateCount }, + row: { original }, + }: ICellProps) => { + return ( +

+ <>{val}} /> + {/* TODO: WINDOWS FEATURE FLAG: remove this conditional when windows mdm + is released. the view all UI will show in the windows column when we + release the feature. */} + {!original.includeWindows && ( + + )} +
+ ); + }, + }, +]; + +const windowsTableHeader: IDataColumn[] = [ + { + title: "Windows hosts", + Header: (cellProps: IHeaderProps) => ( + + ), + disableSortBy: true, + accessor: "windowsHosts", Cell: ({ cell: { value: aggregateCount }, row: { original }, @@ -91,7 +134,7 @@ const defaultTableHeaders: IDataColumn[] = [ @@ -101,15 +144,17 @@ const defaultTableHeaders: IDataColumn[] = [ }, ]; -type StatusNames = keyof IFileVaultSummaryResponse; - -type StatusEntry = [StatusNames, number]; - -export const generateTableHeaders = (): IDataColumn[] => { +// TODO: WINDOWS FEATURE FLAG: return all headers when windows feature flag is removed. +export const generateTableHeaders = ( + includeWindows: boolean +): IDataColumn[] => { + return includeWindows + ? [...defaultTableHeaders, ...windowsTableHeader] + : defaultTableHeaders; return defaultTableHeaders; }; -const STATUS_CELL_VALUES: Record = { +const STATUS_CELL_VALUES: Record = { verified: { displayName: "Verified", statusName: "success", @@ -122,8 +167,8 @@ const STATUS_CELL_VALUES: Record = { statusName: "successPartial", value: "verifying", tooltip: - "These hosts acknowledged the MDM command to install disk encryption profile. " + - "Fleet is verifying with osquery and retrieving the disk encryption key. This may take up to one hour.", + "These hosts acknowledged the MDM command to turn on disk encryption. Fleet is verifying with " + + "osquery and retrieving the disk encryption key. This may take up to one hour.", }, action_required: { displayName: "Action required (pending)", @@ -141,7 +186,7 @@ const STATUS_CELL_VALUES: Record = { statusName: "pendingPartial", value: "enforcing", tooltip: - "These hosts will receive the MDM command to install the disk encryption profile when the hosts come online.", + "These hosts will receive the MDM command to turn on disk encryption when the hosts come online.", }, failed: { displayName: "Failed", @@ -153,21 +198,41 @@ const STATUS_CELL_VALUES: Record = { statusName: "pendingPartial", value: "removing_enforcement", tooltip: - "These hosts will receive the MDM command to remove the disk encryption profile when the hosts come online.", + "These hosts will receive the MDM command to turn off disk encryption when the hosts come online.", }, }; +type StatusEntry = [DiskEncryptionStatus, IDiskEncryptionStatusAggregate]; + +// Order of the status column. We want the order to always be the same. +const STATUS_ORDER = [ + "verified", + "verifying", + "failed", + "action_required", + "enforcing", + "removing_enforcement", +] as const; + export const generateTableData = ( - data?: IFileVaultSummaryResponse, + // TODO: WINDOWS FEATURE FLAG: remove includeWindows when windows feature flag is removed. + // This is used to conditionally show "View all hosts" link in table cells. + includeWindows: boolean, + data?: IDiskEncryptionSummaryResponse, currentTeamId?: number ) => { if (!data) return []; - const entries = Object.entries(data) as StatusEntry[]; - return entries.map(([status, numHosts]) => ({ - // eslint-disable-next-line object-shorthand + const rowFromStatusEntry = ( + status: DiskEncryptionStatus, + statusAggregate: IDiskEncryptionStatusAggregate + ) => ({ + includeWindows, status: STATUS_CELL_VALUES[status], - hosts: numHosts, + macosHosts: statusAggregate.macos, + windowsHosts: statusAggregate.windows, teamId: currentTeamId, - })); + }); + + return STATUS_ORDER.map((status) => rowFromStatusEntry(status, data[status])); }; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss index ee3e1c025..c2e35efe6 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/_styles.scss @@ -1,7 +1,4 @@ .disk-encryption-table { - padding: $pad-xxlarge; - border: 1px solid $ui-fleet-black-10; - border-radius: $border-radius; margin-bottom: $pad-xxlarge; .data-table-block .data-table tbody td .w250 { diff --git a/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx b/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx index 545887a8f..ce052c31e 100644 --- a/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx +++ b/frontend/pages/ManageControlsPage/components/TurnOnMdmMessage/TurnOnMdmMessage.tsx @@ -30,7 +30,7 @@ const TurnOnMdmMessage = ({ router }: ITurnOnMdmMessageProps) => { return ( diff --git a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx index 2fbaac795..b4102d321 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostsPageConfig.tsx @@ -1,6 +1,7 @@ import React from "react"; import Icon from "components/Icon"; +import { DISK_ENCRYPTION_QUERY_PARAM_NAME } from "services/entities/hosts"; export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [ "query", @@ -17,7 +18,7 @@ export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [ "os_version", "munki_issue_id", "low_disk_space", - "macos_settings_disk_encryption", + DISK_ENCRYPTION_QUERY_PARAM_NAME, "bootstrap_package", ] as const; diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index ab6a3b4de..6eedf3062 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -18,6 +18,7 @@ import labelsAPI, { ILabelsResponse } from "services/entities/labels"; import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams"; import globalPoliciesAPI from "services/entities/global_policies"; import hostsAPI, { + DISK_ENCRYPTION_QUERY_PARAM_NAME, ILoadHostsQueryKey, ILoadHostsResponse, ISortOption, @@ -49,7 +50,7 @@ import { IOperatingSystemVersion } from "interfaces/operating_system"; import { IPolicy, IStoredPolicyResponse } from "interfaces/policy"; import { ITeam } from "interfaces/team"; import { IEmptyTableProps } from "interfaces/empty_table"; -import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm"; +import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm"; import sortUtils from "utilities/sort"; import { @@ -232,8 +233,8 @@ const ManageHostsPage = ({ ? parseInt(queryParams.low_disk_space, 10) : undefined; const missingHosts = queryParams?.status === "missing"; - const diskEncryptionStatus: FileVaultProfileStatus | undefined = - queryParams?.macos_settings_disk_encryption; + const diskEncryptionStatus: DiskEncryptionStatus | undefined = + queryParams?.[DISK_ENCRYPTION_QUERY_PARAM_NAME]; const bootstrapPackageStatus: BootstrapPackageStatus | undefined = queryParams?.bootstrap_package; @@ -558,7 +559,7 @@ const ManageHostsPage = ({ }; const handleChangeDiskEncryptionStatusFilter = ( - newStatus: FileVaultProfileStatus + newStatus: DiskEncryptionStatus ) => { handleResetPageIndex(); @@ -569,7 +570,7 @@ const ManageHostsPage = ({ routeParams, queryParams: { ...queryParams, - macos_settings_disk_encryption: newStatus, + [DISK_ENCRYPTION_QUERY_PARAM_NAME]: newStatus, page: 0, // resets page index }, }) @@ -768,7 +769,7 @@ const ManageHostsPage = ({ newQueryParams.os_version = osVersion; } else if (diskEncryptionStatus && isPremiumTier) { // Premium feature only - newQueryParams.macos_settings_disk_encryption = diskEncryptionStatus; + newQueryParams[DISK_ENCRYPTION_QUERY_PARAM_NAME] = diskEncryptionStatus; } else if (bootstrapPackageStatus && isPremiumTier) { newQueryParams.bootstrap_package = bootstrapPackageStatus; } diff --git a/frontend/pages/hosts/ManageHostsPage/components/DiskEncryptionStatusFilter/DiskEncryptionStatusFilter.tsx b/frontend/pages/hosts/ManageHostsPage/components/DiskEncryptionStatusFilter/DiskEncryptionStatusFilter.tsx index 2405fd6d1..4e4912391 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/DiskEncryptionStatusFilter/DiskEncryptionStatusFilter.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/DiskEncryptionStatusFilter/DiskEncryptionStatusFilter.tsx @@ -4,7 +4,7 @@ import { IDropdownOption } from "interfaces/dropdownOption"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; -import { FileVaultProfileStatus } from "interfaces/mdm"; +import { DiskEncryptionStatus } from "interfaces/mdm"; const baseClass = "disk-encryption-status-filter"; @@ -42,8 +42,8 @@ const DISK_ENCRYPTION_STATUS_OPTIONS: IDropdownOption[] = [ ]; interface IDiskEncryptionStatusFilterProps { - diskEncryptionStatus: FileVaultProfileStatus; - onChange: (value: FileVaultProfileStatus) => void; + diskEncryptionStatus: DiskEncryptionStatus; + onChange: (value: DiskEncryptionStatus) => void; } const DiskEncryptionStatusFilter = ({ diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index 0e64c52c7..1ff7287cb 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx @@ -7,7 +7,7 @@ import { IOperatingSystemVersion, } from "interfaces/operating_system"; import { - FileVaultProfileStatus, + DiskEncryptionStatus, BootstrapPackageStatus, IMdmSolution, MDM_ENROLLMENT_STATUS, @@ -15,7 +15,10 @@ import { import { IMunkiIssuesAggregate } from "interfaces/macadmins"; import { ISoftware } from "interfaces/software"; import { IPolicy } from "interfaces/policy"; -import { MacSettingsStatusQueryParam } from "services/entities/hosts"; +import { + DISK_ENCRYPTION_QUERY_PARAM_NAME, + MacSettingsStatusQueryParam, +} from "services/entities/hosts"; import { PLATFORM_LABEL_DISPLAY_NAMES, @@ -60,7 +63,7 @@ interface IHostsFilterBlockProps { osVersions?: IOperatingSystemVersion[]; softwareDetails: ISoftware | null; mdmSolutionDetails: IMdmSolution | null; - diskEncryptionStatus?: FileVaultProfileStatus; + diskEncryptionStatus?: DiskEncryptionStatus; bootstrapPackageStatus?: BootstrapPackageStatus; }; selectedLabel?: ILabel; @@ -68,9 +71,7 @@ interface IHostsFilterBlockProps { handleClearRouteParam: () => void; handleClearFilter: (omitParams: string[]) => void; onChangePoliciesFilter: (response: PolicyResponse) => void; - onChangeDiskEncryptionStatusFilter: ( - response: FileVaultProfileStatus - ) => void; + onChangeDiskEncryptionStatusFilter: (response: DiskEncryptionStatus) => void; onChangeBootstrapPackageStatusFilter: ( response: BootstrapPackageStatus ) => void; @@ -376,8 +377,8 @@ const HostsFilterBlock = ({ onChange={onChangeDiskEncryptionStatusFilter} /> handleClearFilter(["macos_settings_disk_encryption"])} + label="OS settings: Disk encryption" + onClear={() => handleClearFilter([DISK_ENCRYPTION_QUERY_PARAM_NAME])} /> ); diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 285c0f890..d5550026d 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -417,6 +417,7 @@ const DeviceUserPage = ({ showRefetchSpinner={showRefetchSpinner} onRefetchHost={onRefetchHost} renderActionButtons={renderActionButtons} + osSettings={host?.mdm.os_settings} deviceUser /> @@ -489,6 +490,7 @@ const DeviceUserPage = ({ )} {showMacSettingsModal && ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 0d1a0b6b4..e388493de 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -65,6 +65,7 @@ import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown"; import MacSettingsModal from "../MacSettingsModal"; import BootstrapPackageModal from "./modals/BootstrapPackageModal"; import SelectQueryModal from "./modals/SelectQueryModal"; +import { isSupportedPlatform } from "./modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal"; const baseClass = "host-details"; @@ -720,6 +721,7 @@ const HostDetailsPage = ({ showRefetchSpinner={showRefetchSpinner} onRefetchHost={onRefetchHost} renderActionButtons={renderActionButtons} + osSettings={host?.mdm.os_settings} /> @@ -852,12 +855,15 @@ const HostDetailsPage = ({ {showUnenrollMdmModal && !!host && ( )} - {showDiskEncryptionModal && host && ( - setShowDiskEncryptionModal(false)} - /> - )} + {showDiskEncryptionModal && + host && + isSupportedPlatform(host.platform) && ( + setShowDiskEncryptionModal(false)} + /> + )} {showBootstrapPackageModal && bootstrapPackageData.details && bootstrapPackageData.name && ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx index 8acdc6622..104682267 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx @@ -9,15 +9,32 @@ import CustomLink from "components/CustomLink"; import Button from "components/buttons/Button"; import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent"; import DataError from "components/DataError"; +import { SupportedPlatform } from "interfaces/platform"; const baseClass = "disk-encryption-key-modal"; +// currently these are the only supported platforms for the disk encryption +// key modal. +export type ModalSupportedPlatform = Extract< + SupportedPlatform, + "darwin" | "windows" +>; + +// Checks to see if the platform is supported by the modal. +export const isSupportedPlatform = ( + platform: string +): platform is ModalSupportedPlatform => { + return ["darwin", "windows"].includes(platform); +}; + interface IDiskEncryptionKeyModal { + platform: ModalSupportedPlatform; hostId: number; onCancel: () => void; } const DiskEncryptionKeyModal = ({ + platform, hostId, onCancel, }: IDiskEncryptionKeyModal) => { @@ -33,6 +50,18 @@ const DiskEncryptionKeyModal = ({ select: (data) => data.encryption_key.key, }); + const isMacOS = platform === "darwin"; + const descriptionText = isMacOS + ? "The disk encryption key refers to the FileVault recovery key for macOS." + : "The disk encryption key refers to the BitLocker recovery key for Windows."; + + const recoveryText = isMacOS + ? "Use this key to log in to the host if you forgot the password." + : "Use this key to unlock the encrypted drive."; + const recoveryUrl = isMacOS + ? "https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#reset-a-macos-hosts-password-using-the-disk-encryption-key" + : "https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#unlock-a-windows-hosts-drive-using-the-disk-encryption-key"; + return ( {encryptionKeyError ? ( @@ -40,15 +69,12 @@ const DiskEncryptionKeyModal = ({ ) : ( <> +

{descriptionText}

- The disk encryption key refers to the FileVault recovery key for - macOS. -

-

- Use this key to log in to the host if you forgot the password.{" "} + {recoveryText}{" "}

diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/index.ts b/frontend/pages/hosts/details/MacSettingsIndicator/index.ts deleted file mode 100644 index 47e975206..000000000 --- a/frontend/pages/hosts/details/MacSettingsIndicator/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./MacSettingsIndicator"; diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx index a055bae71..eab9e92bb 100644 --- a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsModal.tsx @@ -7,20 +7,28 @@ import MacSettingsTable from "./MacSettingsTable"; import { generateTableData } from "./MacSettingsTable/MacSettingsTableConfig"; interface IMacSettingsModalProps { - hostMDMData?: Pick; + platform?: string; + hostMDMData?: IHostMdmData; onClose: () => void; } const baseClass = "mac-settings-modal"; -const MacSettingsModal = ({ hostMDMData, onClose }: IMacSettingsModalProps) => { - const memoizedTableData = useMemo(() => generateTableData(hostMDMData), [ - hostMDMData, - ]); +const MacSettingsModal = ({ + platform, + hostMDMData, + onClose, +}: IMacSettingsModalProps) => { + const memoizedTableData = useMemo( + () => generateTableData(hostMDMData, platform), + [hostMDMData, platform] + ); + + if (!platform) return null; return ( innerProps.isDiskEncryptionProfile - ? "The host will receive the MDM command to install the disk encryption profile when the " + - "host comes online." + ? "The hosts will receive the MDM command to turn on disk encryption " + + "when the hosts come online." : "The host will receive the MDM command to install the configuration profile when the " + "host comes online.", }, @@ -56,8 +59,8 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = { iconName: "success", tooltip: (innerProps) => innerProps.isDiskEncryptionProfile - ? "The host turned disk encryption on and " + - "sent their key to Fleet. Fleet verified with osquery." + ? "The host turned disk encryption on and sent the key to Fleet. " + + "Fleet verified with osquery." : "The host installed the configuration profile. Fleet verified with osquery.", }, verifying: { @@ -65,8 +68,9 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = { iconName: "success-partial", tooltip: (innerProps) => innerProps.isDiskEncryptionProfile - ? "The host acknowledged the MDM command to install disk encryption profile. Fleet is " + - "verifying with osquery and retrieving the disk encryption key. This may take up to one hour." + ? "The host acknowledged the MDM command to turn on disk encryption. " + + "Fleet is verifying with osquery and retrieving the disk encryption key. " + + "This may take up to one hour." : "The host acknowledged the MDM command to install the configuration profile. Fleet is " + "verifying with osquery.", }, @@ -98,9 +102,41 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = { }, }; +type WindowsDiskEncryptionDisplayConfig = Omit< + OperationTypeOption, + "action_required" +>; + +const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = { + verified: { + statusText: "Verified", + iconName: "success", + tooltip: () => + "The host turned disk encryption on and sent the key to Fleet. Fleet verified with osquery.", + }, + verifying: { + statusText: "Verifying", + iconName: "success-partial", + tooltip: () => + "The host acknowledged the MDM command to turn on disk encryption. Fleet is verifying with osquery and retrieving " + + "the disk encryption key. This may take up to one hour.", + }, + pending: { + statusText: "Enforcing (pending)", + iconName: "pending-partial", + tooltip: () => + "The host will receive the MDM command to turn on disk encryption when the host comes online.", + }, + failed: { + statusText: "Failed", + iconName: "error", + tooltip: null, + }, +}; + interface IMacSettingStatusCellProps { status: MacSettingsTableStatusValue; - operationType: MacMdmProfileOperationType; + operationType: MacMdmProfileOperationType | null; profileName: string; } @@ -108,8 +144,18 @@ const MacSettingStatusCell = ({ status, operationType, profileName = "", -}: IMacSettingStatusCellProps): JSX.Element => { - const diplayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status]; +}: IMacSettingStatusCellProps) => { + let displayOption: ProfileDisplayOption = null; + + // windows hosts do not have an operation type at the moment and their display options are + // different than mac hosts. + if (!operationType && isMdmProfileStatus(status)) { + displayOption = WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG[status]; + } + + if (operationType) { + displayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status]; + } const isDeviceUser = window.location.pathname .toLowerCase() @@ -118,8 +164,8 @@ const MacSettingStatusCell = ({ const isDiskEncryptionProfile = profileName === FLEET_FILEVAULT_PROFILE_DISPLAY_NAME; - if (diplayOption) { - const { statusText, iconName, tooltip } = diplayOption; + if (displayOption) { + const { statusText, iconName, tooltip } = displayOption; const tooltipId = uniqueId(); return ( diff --git a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx index 39bd02122..7b2364af2 100644 --- a/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx +++ b/frontend/pages/hosts/details/MacSettingsModal/MacSettingsTable/MacSettingsTableConfig.tsx @@ -5,20 +5,27 @@ import { IHostMdmData } from "interfaces/host"; import { FLEET_FILEVAULT_PROFILE_DISPLAY_NAME, // FLEET_FILEVAULT_PROFILE_IDENTIFIER, - IHostMacMdmProfile, + IHostMdmProfile, MdmProfileStatus, + isWindowsDiskEncryptionStatus, } from "interfaces/mdm"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell"; import MacSettingStatusCell from "./MacSettingStatusCell"; +import { generateWinDiskEncryptionProfile } from "../../helpers"; -export interface IMacSettingsTableRow - extends Omit { +export interface IMacSettingsTableRow extends Omit { status: MacSettingsTableStatusValue; } export type MacSettingsTableStatusValue = MdmProfileStatus | "action_required"; +export const isMdmProfileStatus = ( + status: string +): status is MdmProfileStatus => { + return status !== "action_required"; +}; + interface IHeaderProps { column: { title: string; @@ -92,20 +99,41 @@ const tableHeaders: IDataColumn[] = [ ]; export const generateTableData = ( - hostMDMData?: Pick + hostMDMData?: IHostMdmData, + platform?: string ) => { + if (!platform) return []; + let rows: IMacSettingsTableRow[] = []; if (!hostMDMData) { return rows; } + if ( + platform === "windows" && + hostMDMData.os_settings?.disk_encryption.status && + isWindowsDiskEncryptionStatus( + hostMDMData.os_settings.disk_encryption.status + ) + ) { + rows.push( + generateWinDiskEncryptionProfile( + hostMDMData.os_settings.disk_encryption.status + ) + ); + return rows; + } + const { profiles, macos_settings } = hostMDMData; + if (!profiles) { return rows; } - rows = profiles; - if (macos_settings?.disk_encryption === "action_required") { + if ( + platform === "darwin" && + macos_settings?.disk_encryption === "action_required" + ) { rows = profiles.map((p) => { // TODO: this is a brittle check for the filevault profile // it would be better to match on the identifier but it is not diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tests.tsx b/frontend/pages/hosts/details/ProfileStatusIndicator/ProfileStatusIndicator.tests.tsx similarity index 87% rename from frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tests.tsx rename to frontend/pages/hosts/details/ProfileStatusIndicator/ProfileStatusIndicator.tests.tsx index a71467903..73044dd21 100644 --- a/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tests.tsx +++ b/frontend/pages/hosts/details/ProfileStatusIndicator/ProfileStatusIndicator.tests.tsx @@ -1,12 +1,15 @@ import React from "react"; import { fireEvent, render, screen } from "@testing-library/react"; -import MacSettingsIndicator from "./MacSettingsIndicator"; +import ProfileStatusIndicator from "./ProfileStatusIndicator"; -describe("MacSettingsIndicator", () => { +describe("ProfileStatusIndicator component", () => { it("Renders the text and icon", () => { const indicatorText = "test text"; render( - + ); const renderedIndicatorText = screen.getByText(indicatorText); const renderedIcon = screen.getByTestId("success-icon"); @@ -19,7 +22,7 @@ describe("MacSettingsIndicator", () => { const indicatorText = "test text"; const tooltipText = "test tooltip text"; render( - { document.body.appendChild(newDiv); }; render( - { diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tsx b/frontend/pages/hosts/details/ProfileStatusIndicator/ProfileStatusIndicator.tsx similarity index 92% rename from frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tsx rename to frontend/pages/hosts/details/ProfileStatusIndicator/ProfileStatusIndicator.tsx index 473745ee3..f3bbe6cd0 100644 --- a/frontend/pages/hosts/details/MacSettingsIndicator/MacSettingsIndicator.tsx +++ b/frontend/pages/hosts/details/ProfileStatusIndicator/ProfileStatusIndicator.tsx @@ -4,9 +4,9 @@ import { IconNames } from "components/icons"; import Icon from "components/Icon"; import Button from "components/buttons/Button"; -const baseClass = "settings-indicator"; +const baseClass = "profile-status-indicator"; -export interface IMacSettingsIndicator { +export interface IProfileStatusIndicatorProps { indicatorText: string; iconName: IconNames; onClick?: () => void; @@ -16,12 +16,12 @@ export interface IMacSettingsIndicator { }; } -const MacSettingsIndicator = ({ +const ProfileStatusIndicator = ({ indicatorText, iconName, onClick, tooltip, -}: IMacSettingsIndicator): JSX.Element => { +}: IProfileStatusIndicatorProps) => { const getIndicatorTextWrapped = () => { if (onClick && tooltip?.tooltipText) { return ( @@ -103,4 +103,4 @@ const MacSettingsIndicator = ({ ); }; -export default MacSettingsIndicator; +export default ProfileStatusIndicator; diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/_styles.scss b/frontend/pages/hosts/details/ProfileStatusIndicator/_styles.scss similarity index 88% rename from frontend/pages/hosts/details/MacSettingsIndicator/_styles.scss rename to frontend/pages/hosts/details/ProfileStatusIndicator/_styles.scss index fce8265c9..a7ed40ad9 100644 --- a/frontend/pages/hosts/details/MacSettingsIndicator/_styles.scss +++ b/frontend/pages/hosts/details/ProfileStatusIndicator/_styles.scss @@ -1,4 +1,4 @@ -.settings-indicator { +.profile-status-indicator { display: flex; gap: 4px; diff --git a/frontend/pages/hosts/details/ProfileStatusIndicator/index.ts b/frontend/pages/hosts/details/ProfileStatusIndicator/index.ts new file mode 100644 index 000000000..99de4100c --- /dev/null +++ b/frontend/pages/hosts/details/ProfileStatusIndicator/index.ts @@ -0,0 +1 @@ +export { default } from "./ProfileStatusIndicator"; diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index f18194844..45be0eaba 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -1,7 +1,12 @@ import React from "react"; - import ReactTooltip from "react-tooltip"; -import { IHostMacMdmProfile, BootstrapPackageStatus } from "interfaces/mdm"; + +import { + IHostMdmProfile, + BootstrapPackageStatus, + isWindowsDiskEncryptionStatus, +} from "interfaces/mdm"; +import { IOSSettings } from "interfaces/host"; import getHostStatusTooltipText from "pages/hosts/helpers"; import TooltipWrapper from "components/TooltipWrapper"; @@ -9,6 +14,7 @@ import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; import DiskSpaceGraph from "components/DiskSpaceGraph"; import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; +import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; import { getHostDiskEncryptionTooltipMessage, humanHostMemory, @@ -16,10 +22,11 @@ import { } from "utilities/helpers"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import StatusIndicator from "components/StatusIndicator"; -import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; + import MacSettingsIndicator from "./MacSettingsIndicator"; import HostSummaryIndicator from "./HostSummaryIndicator"; import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPackageIndicator"; +import { generateWinDiskEncryptionProfile } from "../../helpers"; const baseClass = "host-summary"; @@ -38,7 +45,7 @@ interface IHostSummaryProps { toggleOSPolicyModal?: () => void; toggleMacSettingsModal?: () => void; toggleBootstrapPackageModal?: () => void; - hostMdmProfiles?: IHostMacMdmProfile[]; + hostMdmProfiles?: IHostMdmProfile[]; mdmName?: string; showRefetchSpinner: boolean; onRefetchHost: ( @@ -46,6 +53,7 @@ interface IHostSummaryProps { ) => void; renderActionButtons: () => JSX.Element | null; deviceUser?: boolean; + osSettings?: IOSSettings; } const HostSummary = ({ @@ -64,8 +72,9 @@ const HostSummary = ({ onRefetchHost, renderActionButtons, deviceUser, + osSettings, }: IHostSummaryProps): JSX.Element => { - const { status, id, platform } = titleData; + const { status, platform } = titleData; const renderRefetch = () => { const isOnline = titleData.status === "online"; @@ -179,6 +188,22 @@ const HostSummary = ({ }; const renderSummary = () => { + // for windows hosts we have to manually add a profile for disk encryption + // as this is not currently included in the `profiles` value from the API + // response for windows hosts. + if ( + platform === "windows" && + osSettings?.disk_encryption?.status && + isWindowsDiskEncryptionStatus(osSettings.disk_encryption.status) + ) { + const winDiskEncryptionProfile: IHostMdmProfile = generateWinDiskEncryptionProfile( + osSettings.disk_encryption.status + ); + hostMdmProfiles = hostMdmProfiles + ? [...hostMdmProfiles, winDiskEncryptionProfile] + : [winDiskEncryptionProfile]; + } + return (
@@ -198,12 +223,15 @@ const HostSummary = ({ {isPremiumTier && renderHostTeam()} - {platform === "darwin" && + {/* Rendering of OS Settings data */} + {(platform === "darwin" || platform === "windows") && isPremiumTier && - mdmName === "Fleet" && // show if 1 - host is enrolled in Fleet MDM, and + // TODO: API INTEGRATION: change this when we figure out why the API is + // returning "Fleet" or "FleetDM" for the MDM name. + mdmName?.includes("Fleet") && // show if 1 - host is enrolled in Fleet MDM, and hostMdmProfiles && hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced - + { const statuses = hostMacSettings.map((setting) => setting.status); if (statuses.includes("failed")) { @@ -68,7 +68,7 @@ const getMacProfileStatus = ( }; interface IMacSettingsIndicatorProps { - profiles: IHostMacMdmProfile[]; + profiles: IHostMdmProfile[]; onClick?: () => void; } const MacSettingsIndicator = ({ diff --git a/frontend/pages/hosts/details/helpers.ts b/frontend/pages/hosts/details/helpers.ts new file mode 100644 index 000000000..2c1283be1 --- /dev/null +++ b/frontend/pages/hosts/details/helpers.ts @@ -0,0 +1,33 @@ +/** Helpers used across the host details and my device pages and components. */ + +import { + IHostMdmProfile, + IWindowsDiskEncryptionStatus, + MdmProfileStatus, +} from "interfaces/mdm"; + +const convertWinDiskEncryptionStatusToProfileStatus = ( + diskEncryptionStatus: IWindowsDiskEncryptionStatus +): MdmProfileStatus => { + return diskEncryptionStatus === "enforcing" + ? "pending" + : diskEncryptionStatus; +}; + +/** + * Manually generates a profile for the windows disk encryption status. We need + * this as we don't have a windows disk encryption profile in the `profiles` + * attribute coming back from the GET /hosts/:id API response. + */ +// eslint-disable-next-line import/prefer-default-export +export const generateWinDiskEncryptionProfile = ( + diskEncryptionStatus: IWindowsDiskEncryptionStatus +): IHostMdmProfile => { + return { + profile_id: 0, // This s the only type of profile that can have this number + name: "Disk Encryption", + status: convertWinDiskEncryptionStatusToProfileStatus(diskEncryptionStatus), + detail: "", + operation_type: null, + }; +}; diff --git a/frontend/services/entities/host_count.ts b/frontend/services/entities/host_count.ts index 9e4bba000..5511dcfc1 100644 --- a/frontend/services/entities/host_count.ts +++ b/frontend/services/entities/host_count.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import sendRequest from "services"; import endpoints from "utilities/endpoints"; -import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm"; +import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm"; import { HostStatus } from "interfaces/host"; import { buildQueryStringFromParams, @@ -43,7 +43,7 @@ export interface IHostCountLoadOptions { osId?: number; osName?: string; osVersion?: string; - diskEncryptionStatus?: FileVaultProfileStatus; + diskEncryptionStatus?: DiskEncryptionStatus; bootstrapPackageStatus?: BootstrapPackageStatus; } diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index b8abf7061..e4d95353d 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -11,7 +11,7 @@ import { import { SelectedPlatform } from "interfaces/platform"; import { ISoftware } from "interfaces/software"; import { - FileVaultProfileStatus, + DiskEncryptionStatus, BootstrapPackageStatus, IMdmSolution, } from "interfaces/mdm"; @@ -29,6 +29,11 @@ export interface ILoadHostsResponse { mobile_device_management_solution: IMdmSolution; } +// the source of truth for the filter option names. +// there are used on many other pages but we define them here. +// TODO: add other filter options here. +export const DISK_ENCRYPTION_QUERY_PARAM_NAME = "os_settings_disk_encryption"; + export interface ILoadHostsQueryKey extends ILoadHostsOptions { scope: "hosts"; } @@ -57,7 +62,7 @@ export interface ILoadHostsOptions { device_mapping?: boolean; columns?: string; visibleColumns?: string; - diskEncryptionStatus?: FileVaultProfileStatus; + diskEncryptionStatus?: DiskEncryptionStatus; bootstrapPackageStatus?: BootstrapPackageStatus; } @@ -83,7 +88,7 @@ export interface IExportHostsOptions { device_mapping?: boolean; columns?: string; visibleColumns?: string; - diskEncryptionStatus?: FileVaultProfileStatus; + diskEncryptionStatus?: DiskEncryptionStatus; } export interface IActionByFilter { diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index 55416203d..41301f3d3 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -1,19 +1,61 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { FileVaultProfileStatus } from "interfaces/mdm"; +import { DiskEncryptionStatus, MdmProfileStatus } from "interfaces/mdm"; import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { buildQueryStringFromParams } from "utilities/url"; -export type IFileVaultSummaryResponse = Record; - export interface IEulaMetadataResponse { name: string; token: string; created_at: string; } -export default { +export type ProfileStatusSummaryResponse = Record; + +export interface IDiskEncryptionStatusAggregate { + macos: number; + windows: number; +} + +export type IDiskEncryptionSummaryResponse = Record< + DiskEncryptionStatus, + IDiskEncryptionStatusAggregate +>; + +// This function combines the profile status summary and the disk encryption summary +// to generate the aggregate profile status summary. We are doing this as a temporary +// solution until we have the API that will return the aggregate profile status summary +// from one call. +// TODO: API INTEGRATION: remove when API is implemented that returns windows +// data in the aggregate profile status summary. +const generateCombinedProfileStatusSummary = ( + profileStatuses: ProfileStatusSummaryResponse, + diskEncryptionSummary: IDiskEncryptionSummaryResponse +): ProfileStatusSummaryResponse => { + const { verified, verifying, failed, pending } = profileStatuses; + const { + verified: verifiedDiskEncryption, + verifying: verifyingDiskEncryption, + failed: failedDiskEncryption, + action_required: actionRequiredDiskEncryption, + enforcing: enforcingDiskEncryption, + removing_enforcement: removingEnforcementDiskEncryption, + } = diskEncryptionSummary; + + return { + verified: verified + verifiedDiskEncryption.windows, + verifying: verifying + verifyingDiskEncryption.windows, + failed: failed + failedDiskEncryption.windows, + pending: + pending + + actionRequiredDiskEncryption.windows + + enforcingDiskEncryption.windows + + removingEnforcementDiskEncryption.windows, + }; +}; + +const mdmService = { downloadDeviceUserEnrollmentProfile: (token: string) => { const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints; return sendRequest("GET", DEVICE_USER_MDM_ENROLLMENT_PROFILE(token)); @@ -72,24 +114,51 @@ export default { return sendRequest("DELETE", MDM_PROFILE(profileId)); }, - getAggregateProfileStatuses: (teamId = APP_CONTEXT_NO_TEAM_ID) => { + // TODO: API INTEGRATION: we need to rework this when we create API call that + // will return the aggregate statuses for windows included in the response. + // Currently to get windows data included we will need to make a separate call. + // We will likely change this to go back to single "getProfileStatusSummary" API call. + getAggregateProfileStatuses: async ( + teamId = APP_CONTEXT_NO_TEAM_ID, + // TODO: WINDOWS FEATURE FLAG: remove when we windows feature is released. + includeWindows: boolean + ) => { + // if we are not including windows we can just call the existing profile summary API + if (!includeWindows) { + return mdmService.getProfileStatusSummary(teamId); + } + + // otherwise we have to make two calls and combine the results. + return mdmService + .getAggregateProfileStatusesWithWindows(teamId) + .then((res) => generateCombinedProfileStatusSummary(...res)); + }, + + getAggregateProfileStatusesWithWindows: async (teamId: number) => { + return Promise.all([ + mdmService.getProfileStatusSummary(teamId), + mdmService.getDiskEncryptionSummary(teamId), + ]); + }, + + getProfileStatusSummary: (teamId = APP_CONTEXT_NO_TEAM_ID) => { const path = `${ endpoints.MDM_PROFILES_AGGREGATE_STATUSES }?${buildQueryStringFromParams({ team_id: teamId })}`; - return sendRequest("GET", path); }, - getDiskEncryptionAggregate: (teamId?: number) => { - let { MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: path } = endpoints; + getDiskEncryptionSummary: (teamId?: number) => { + let { MDM_DISK_ENCRYPTION_SUMMARY: path } = endpoints; if (teamId) { path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`; } - return sendRequest("GET", path); }, + // TODO: API INTEGRATION: change when API is implemented that works for windows + // disk encryption too. updateAppleMdmSettings: (enableDiskEncryption: boolean, teamId?: number) => { const { MDM_UPDATE_APPLE_SETTINGS: teamsEndpoint, @@ -98,7 +167,9 @@ export default { if (teamId === 0) { return sendRequest("PATCH", noTeamsEndpoint, { mdm: { + // TODO: API INTEGRATION: remove macos_settings when API change is merged in. macos_settings: { enable_disk_encryption: enableDiskEncryption }, + // enable_disk_encryption: enableDiskEncryption, }, }); } @@ -179,3 +250,5 @@ export default { }); }, }; + +export default mdmService; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index ce6d89b95..4a0c18974 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -51,7 +51,7 @@ export default { MDM_PROFILE: (id: number) => `/${API_VERSION}/fleet/mdm/apple/profiles/${id}`, MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`, MDM_PROFILES_AGGREGATE_STATUSES: `/${API_VERSION}/fleet/mdm/apple/profiles/summary`, - MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: `/${API_VERSION}/fleet/mdm/apple/filevault/summary`, + MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`, MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`, MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => { const query = new URLSearchParams({ token }); diff --git a/frontend/utilities/url/index.ts b/frontend/utilities/url/index.ts index c535c8013..d6eb7b6c8 100644 --- a/frontend/utilities/url/index.ts +++ b/frontend/utilities/url/index.ts @@ -1,6 +1,10 @@ -import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm"; import { isEmpty, reduce, omitBy, Dictionary } from "lodash"; -import { MacSettingsStatusQueryParam } from "services/entities/hosts"; + +import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm"; +import { + DISK_ENCRYPTION_QUERY_PARAM_NAME, + MacSettingsStatusQueryParam, +} from "services/entities/hosts"; type QueryValues = string | number | boolean | undefined | null; export type QueryParams = Record; @@ -24,7 +28,7 @@ interface IMutuallyExclusiveHostParams { osId?: number; osName?: string; osVersion?: string; - diskEncryptionStatus?: FileVaultProfileStatus; + diskEncryptionStatus?: DiskEncryptionStatus; bootstrapPackageStatus?: BootstrapPackageStatus; } @@ -123,7 +127,7 @@ export const reconcileMutuallyExclusiveHostParams = ({ case !!lowDiskSpaceHosts: return { low_disk_space: lowDiskSpaceHosts }; case !!diskEncryptionStatus: - return { macos_settings_disk_encryption: diskEncryptionStatus }; + return { [DISK_ENCRYPTION_QUERY_PARAM_NAME]: diskEncryptionStatus }; case !!bootstrapPackageStatus: return { bootstrap_package: bootstrapPackageStatus }; default: diff --git a/go.mod b/go.mod index 7913243bd..48a36f955 100644 --- a/go.mod +++ b/go.mod @@ -272,6 +272,7 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/slack-go/slack v0.9.4 // indirect diff --git a/go.sum b/go.sum index ca02b6bc3..9c99a41dc 100644 --- a/go.sum +++ b/go.sum @@ -1080,6 +1080,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e/go.mod h1:9Tc1SKnfACJb9N7cw2eyuI6xzy845G7uZONBsi5uPEA= +github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg= +github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= diff --git a/orbit/changes/12842-orbit-bitlocker-management b/orbit/changes/12842-orbit-bitlocker-management new file mode 100644 index 000000000..97d7e6fe1 --- /dev/null +++ b/orbit/changes/12842-orbit-bitlocker-management @@ -0,0 +1 @@ +* Adding support to manage Bitlocker operations through Orbit notifications diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index 3b91e700f..3e9f56df1 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -622,6 +622,7 @@ func main() { const ( renewEnrollmentProfileCommandFrequency = time.Hour windowsMDMEnrollmentCommandFrequency = time.Hour + windowsMDMBitlockerCommandFrequency = time.Hour ) configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL) configFetcher = update.ApplyRunScriptsConfigFetcherMiddleware(configFetcher, c.Bool("enable-scripts"), orbitClient) @@ -638,6 +639,7 @@ func main() { configFetcher = update.ApplySwiftDialogDownloaderMiddleware(configFetcher, updateRunner) case "windows": configFetcher = update.ApplyWindowsMDMEnrollmentFetcherMiddleware(configFetcher, windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient) + configFetcher = update.ApplyWindowsMDMBitlockerFetcherMiddleware(configFetcher, windowsMDMBitlockerCommandFrequency, orbitClient) } const orbitFlagsUpdateInterval = 30 * time.Second diff --git a/orbit/pkg/bitlocker/bitlocker_management.go b/orbit/pkg/bitlocker/bitlocker_management.go new file mode 100644 index 000000000..e21056892 --- /dev/null +++ b/orbit/pkg/bitlocker/bitlocker_management.go @@ -0,0 +1,17 @@ +package bitlocker + +// Encryption Status +type EncryptionStatus struct { + ProtectionStatusDesc string + ConversionStatusDesc string + EncryptionPercentage string + EncryptionFlags string + WipingStatusDesc string + WipingPercentage string +} + +// Volume Encryption Status +type VolumeStatus struct { + DriveVolume string + Status *EncryptionStatus +} diff --git a/orbit/pkg/bitlocker/bitlocker_management_notwindows.go b/orbit/pkg/bitlocker/bitlocker_management_notwindows.go new file mode 100644 index 000000000..4263ba270 --- /dev/null +++ b/orbit/pkg/bitlocker/bitlocker_management_notwindows.go @@ -0,0 +1,19 @@ +//go:build !windows + +package bitlocker + +func GetRecoveryKeys(targetVolume string) (map[string]string, error) { + return nil, nil +} + +func EncryptVolume(targetVolume string) (string, error) { + return "", nil +} + +func DecryptVolume(targetVolume string) error { + return nil +} + +func GetEncryptionStatus() ([]VolumeStatus, error) { + return nil, nil +} diff --git a/orbit/pkg/bitlocker/bitlocker_management_windows.go b/orbit/pkg/bitlocker/bitlocker_management_windows.go new file mode 100644 index 000000000..4d9bb3683 --- /dev/null +++ b/orbit/pkg/bitlocker/bitlocker_management_windows.go @@ -0,0 +1,573 @@ +//go:build windows + +package bitlocker + +import ( + "fmt" + "syscall" + + "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole/oleutil" + "github.com/scjalliance/comshim" +) + +// Encryption Methods +// https://docs.microsoft.com/en-us/windows/win32/secprov/getencryptionmethod-win32-encryptablevolume +type EncryptionMethod int32 + +const ( + None EncryptionMethod = iota + AES128WithDiffuser + AES256WithDiffuser + AES128 + AES256 + HardwareEncryption + XtsAES128 + XtsAES256 +) + +// Encryption Flags +// https://docs.microsoft.com/en-us/windows/win32/secprov/encrypt-win32-encryptablevolume +type EncryptionFlag int32 + +const ( + EncryptDataOnly EncryptionFlag = 0x00000001 + EncryptDemandWipe EncryptionFlag = 0x00000002 + EncryptSynchronous EncryptionFlag = 0x00010000 + + // Error Codes + ERROR_IO_DEVICE int32 = -2147023779 + FVE_E_EDRIVE_INCOMPATIBLE_VOLUME int32 = -2144272206 + FVE_E_NO_TPM_WITH_PASSPHRASE int32 = -2144272212 + FVE_E_PASSPHRASE_TOO_LONG int32 = -2144272214 + FVE_E_POLICY_PASSPHRASE_NOT_ALLOWED int32 = -2144272278 + FVE_E_NOT_DECRYPTED int32 = -2144272327 + FVE_E_INVALID_PASSWORD_FORMAT int32 = -2144272331 + FVE_E_BOOTABLE_CDDVD int32 = -2144272336 + FVE_E_PROTECTOR_EXISTS int32 = -2144272335 +) + +// DiscoveryVolumeType specifies the type of discovery volume to be used by Prepare. +// https://docs.microsoft.com/en-us/windows/win32/secprov/preparevolume-win32-encryptablevolume +type DiscoveryVolumeType string + +const ( + // VolumeTypeNone indicates no discovery volume. This value creates a native BitLocker volume. + VolumeTypeNone DiscoveryVolumeType = "" + // VolumeTypeDefault indicates the default behavior. + VolumeTypeDefault DiscoveryVolumeType = "" + // VolumeTypeFAT32 creates a FAT32 discovery volume. + VolumeTypeFAT32 DiscoveryVolumeType = "FAT32" +) + +// ForceEncryptionType specifies the encryption type to be used when calling Prepare on the volume. +// https://docs.microsoft.com/en-us/windows/win32/secprov/preparevolume-win32-encryptablevolume +type ForceEncryptionType int32 + +const ( + // EncryptionTypeUnspecified indicates that the encryption type is not specified. + EncryptionTypeUnspecified ForceEncryptionType = 0 + // EncryptionTypeSoftware specifies software encryption. + EncryptionTypeSoftware ForceEncryptionType = 1 + // EncryptionTypeHardware specifies hardware encryption. + EncryptionTypeHardware ForceEncryptionType = 2 +) + +func encryptErrHandler(val int32) error { + switch val { + case ERROR_IO_DEVICE: + return fmt.Errorf("an I/O error has occurred during encryption; the device may need to be reset") + case FVE_E_EDRIVE_INCOMPATIBLE_VOLUME: + return fmt.Errorf("the drive specified does not support hardware-based encryption") + case FVE_E_NO_TPM_WITH_PASSPHRASE: + return fmt.Errorf("a TPM key protector cannot be added because a password protector exists on the drive") + case FVE_E_PASSPHRASE_TOO_LONG: + return fmt.Errorf("the passphrase cannot exceed 256 characters") + case FVE_E_POLICY_PASSPHRASE_NOT_ALLOWED: + return fmt.Errorf("group Policy settings do not permit the creation of a password") + case FVE_E_NOT_DECRYPTED: + return fmt.Errorf("the drive must be fully decrypted to complete this operation") + case FVE_E_INVALID_PASSWORD_FORMAT: + return fmt.Errorf("the format of the recovery password provided is invalid") + case FVE_E_BOOTABLE_CDDVD: + return fmt.Errorf("bitLocker Drive Encryption detected bootable media (CD or DVD) in the computer") + case FVE_E_PROTECTOR_EXISTS: + return fmt.Errorf("key protector cannot be added; only one key protector of this type is allowed for this drive") + default: + return fmt.Errorf("error code returned during encryption: %d", val) + } +} + +///////////////////////////////////////////////////// +// Volume represents a Bitlocker encryptable volume +///////////////////////////////////////////////////// + +type Volume struct { + letter string + handle *ole.IDispatch + wmiIntf *ole.IDispatch + wmiSvc *ole.IDispatch +} + +// bitlockerClose frees all resources associated with a volume. +func (v *Volume) bitlockerClose() { + if v.handle != nil { + v.handle.Release() + } + + if v.wmiIntf != nil { + v.wmiIntf.Release() + } + + if v.wmiSvc != nil { + v.wmiSvc.Release() + } + + comshim.Done() +} + +// encrypt encrypts the volume +// Example: vol.encrypt(bitlocker.XtsAES256, bitlocker.EncryptDataOnly) +// https://docs.microsoft.com/en-us/windows/win32/secprov/encrypt-win32-encryptablevolume +func (v *Volume) encrypt(method EncryptionMethod, flags EncryptionFlag) error { + resultRaw, err := oleutil.CallMethod(v.handle, "Encrypt", int32(method), int32(flags)) + if err != nil { + return fmt.Errorf("encrypt(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return fmt.Errorf("encrypt(%s): %w", v.letter, encryptErrHandler(val)) + } + + return nil +} + +// decrypt encrypts the volume +// Example: vol.decrypt() +// https://learn.microsoft.com/en-us/windows/win32/secprov/decrypt-win32-encryptablevolume +func (v *Volume) decrypt() error { + resultRaw, err := oleutil.CallMethod(v.handle, "Decrypt") + if err != nil { + return fmt.Errorf("decrypt(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return fmt.Errorf("decrypt(%s): %w", v.letter, encryptErrHandler(val)) + } + + return nil +} + +// prepareVolume prepares a new Bitlocker Volume. This should be called BEFORE any key protectors are added. +// Example: vol.prepareVolume(bitlocker.VolumeTypeDefault, bitlocker.EncryptionTypeHardware) +// https://docs.microsoft.com/en-us/windows/win32/secprov/preparevolume-win32-encryptablevolume +func (v *Volume) prepareVolume(volType DiscoveryVolumeType, encType ForceEncryptionType) error { + resultRaw, err := oleutil.CallMethod(v.handle, "PrepareVolume", string(volType), int32(encType)) + if err != nil { + return fmt.Errorf("prepareVolume(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return fmt.Errorf("prepareVolume(%s): %w", v.letter, encryptErrHandler(val)) + } + return nil +} + +// protectWithNumericalPassword adds a numerical password key protector. +// Leave password as a blank string to have one auto-generated by Windows +// https://docs.microsoft.com/en-us/windows/win32/secprov/protectkeywithnumericalpassword-win32-encryptablevolume +func (v *Volume) protectWithNumericalPassword() (string, error) { + var volumeKeyProtectorID ole.VARIANT + ole.VariantInit(&volumeKeyProtectorID) + var resultRaw *ole.VARIANT + var err error + + resultRaw, err = oleutil.CallMethod(v.handle, "ProtectKeyWithNumericalPassword", nil, nil, &volumeKeyProtectorID) + if err != nil { + return "", fmt.Errorf("ProtectKeyWithNumericalPassword(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return "", fmt.Errorf("ProtectKeyWithNumericalPassword(%s): %w", v.letter, encryptErrHandler(val)) + } + + var recoveryKey ole.VARIANT + ole.VariantInit(&recoveryKey) + resultRaw, err = oleutil.CallMethod(v.handle, "GetKeyProtectorNumericalPassword", volumeKeyProtectorID.ToString(), &recoveryKey) + + if err != nil { + return "", fmt.Errorf("GetKeyProtectorNumericalPassword(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return "", fmt.Errorf("GetKeyProtectorNumericalPassword(%s): %w", v.letter, encryptErrHandler(val)) + } + + return recoveryKey.ToString(), nil +} + +// protectWithPassphrase adds a passphrase key protector +// https://docs.microsoft.com/en-us/windows/win32/secprov/protectkeywithpassphrase-win32-encryptablevolume +func (v *Volume) protectWithPassphrase(passphrase string) (string, error) { + var volumeKeyProtectorID ole.VARIANT + ole.VariantInit(&volumeKeyProtectorID) + + resultRaw, err := oleutil.CallMethod(v.handle, "ProtectKeyWithPassphrase", nil, passphrase, &volumeKeyProtectorID) + if err != nil { + return "", fmt.Errorf("protectWithPassphrase(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return "", fmt.Errorf("protectWithPassphrase(%s): %w", v.letter, encryptErrHandler(val)) + } + + return volumeKeyProtectorID.ToString(), nil +} + +// protectWithTPM adds the TPM key protector +// https://docs.microsoft.com/en-us/windows/win32/secprov/protectkeywithtpm-win32-encryptablevolume +func (v *Volume) protectWithTPM(platformValidationProfile *[]uint8) error { + var volumeKeyProtectorID ole.VARIANT + ole.VariantInit(&volumeKeyProtectorID) + var resultRaw *ole.VARIANT + var err error + + if platformValidationProfile == nil { + resultRaw, err = oleutil.CallMethod(v.handle, "ProtectKeyWithTPM", nil, nil, &volumeKeyProtectorID) + } else { + resultRaw, err = oleutil.CallMethod(v.handle, "ProtectKeyWithTPM", nil, *platformValidationProfile, &volumeKeyProtectorID) + } + if err != nil { + return fmt.Errorf("protectKeyWithTPM(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return fmt.Errorf("protectKeyWithTPM(%s): %w", v.letter, encryptErrHandler(val)) + } + + return nil +} + +// getBitlockerStatus returns the current status of the volume +// https://learn.microsoft.com/en-us/windows/win32/secprov/getprotectionstatus-win32-encryptablevolume +func (v *Volume) getBitlockerStatus() (*EncryptionStatus, error) { + var ( + conversionStatus int32 + encryptionPercentage int32 + encryptionFlags int32 + wipingStatus int32 + wipingPercentage int32 + precisionFactor int32 = 4 + protectionStatus int32 + ) + + resultRaw, err := oleutil.CallMethod(v.handle, "GetConversionStatus", &conversionStatus, &encryptionPercentage, &encryptionFlags, &wipingStatus, &wipingPercentage, precisionFactor) + if err != nil { + return nil, fmt.Errorf("GetConversionStatus(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return nil, fmt.Errorf("GetConversionStatus(%s): %w", v.letter, encryptErrHandler(val)) + } + + resultRaw, err = oleutil.CallMethod(v.handle, "GetProtectionStatus", &protectionStatus) + if err != nil { + return nil, fmt.Errorf("GetProtectionStatus(%s): %w", v.letter, err) + } else if val, ok := resultRaw.Value().(int32); val != 0 || !ok { + return nil, fmt.Errorf("GetProtectionStatus(%s): %w", v.letter, encryptErrHandler(val)) + } + + // Creating the encryption status struct + encStatus := &EncryptionStatus{ + ProtectionStatusDesc: getProtectionStatusDescription(fmt.Sprintf("%d", protectionStatus)), + ConversionStatusDesc: getConversionStatusDescription(fmt.Sprintf("%d", conversionStatus)), + EncryptionPercentage: intToPercentage(encryptionPercentage), + EncryptionFlags: fmt.Sprintf("%d", encryptionFlags), + WipingStatusDesc: getWipingStatusDescription(fmt.Sprintf("%d", wipingStatus)), + WipingPercentage: intToPercentage(wipingPercentage), + } + + return encStatus, nil +} + +// getProtectorsKeys returns the recovery keys for the volume +// https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume +func (v *Volume) getProtectorsKeys() (map[string]string, error) { + keys, err := getKeyProtectors(v.handle) + if err != nil { + return nil, fmt.Errorf("getKeyProtectors: %w", err) + } + + recoveryKeys := make(map[string]string) + for _, k := range keys { + var recoveryKey ole.VARIANT + ole.VariantInit(&recoveryKey) + recoveryKeyResultRaw, err := oleutil.CallMethod(v.handle, "GetKeyProtectorNumericalPassword", k, &recoveryKey) + if err != nil { + continue // No recovery key for this protector + } else if val, ok := recoveryKeyResultRaw.Value().(int32); val != 0 || !ok { + continue // No recovery key for this protector + } + recoveryKeys[k] = recoveryKey.ToString() + } + + return recoveryKeys, nil +} + +///////////////////////////////////////////////////// +// Helper functions +///////////////////////////////////////////////////// + +// bitlockerConnect connects to an encryptable volume in order to manage it. +func bitlockerConnect(driveLetter string) (Volume, error) { + comshim.Add(1) + v := Volume{letter: driveLetter} + + unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator") + if err != nil { + comshim.Done() + return v, fmt.Errorf("createObject: %w", err) + } + defer unknown.Release() + + v.wmiIntf, err = unknown.QueryInterface(ole.IID_IDispatch) + if err != nil { + comshim.Done() + return v, fmt.Errorf("queryInterface: %w", err) + } + serviceRaw, err := oleutil.CallMethod(v.wmiIntf, "ConnectServer", nil, `\\.\ROOT\CIMV2\Security\MicrosoftVolumeEncryption`) + if err != nil { + v.bitlockerClose() + return v, fmt.Errorf("connectServer: %w", err) + } + v.wmiSvc = serviceRaw.ToIDispatch() + + raw, err := oleutil.CallMethod(v.wmiSvc, "ExecQuery", "SELECT * FROM Win32_EncryptableVolume WHERE DriveLetter = '"+driveLetter+"'") + if err != nil { + v.bitlockerClose() + return v, fmt.Errorf("execQuery: %w", err) + } + result := raw.ToIDispatch() + defer result.Release() + + itemRaw, err := oleutil.CallMethod(result, "ItemIndex", 0) + if err != nil { + v.bitlockerClose() + return v, fmt.Errorf("failed to fetch result row while processing BitLocker info: %w", err) + } + v.handle = itemRaw.ToIDispatch() + + return v, nil +} + +// getConversionStatusDescription returns the current status of the volume +// https://learn.microsoft.com/en-us/windows/win32/secprov/getconversionstatus-win32-encryptablevolume +func getConversionStatusDescription(input string) string { + switch input { + case "0": + return "FullyDecrypted" + case "1": + return "FullyEncrypted" + case "2": + return "EncryptionInProgress" + case "3": + return "DecryptionInProgress" + case "4": + return "EncryptionPaused" + case "5": + return "DecryptionPaused" + } + + return "Status " + input +} + +// getWipingStatusDescription returns the current wiping status of the volume +// https://learn.microsoft.com/en-us/windows/win32/secprov/getconversionstatus-win32-encryptablevolume +func getWipingStatusDescription(input string) string { + switch input { + case "0": + return "FreeSpaceNotWiped" + case "1": + return "FreeSpaceWiped" + case "2": + return "FreeSpaceWipingInProgress" + case "3": + return "FreeSpaceWipingPaused" + } + + return "Status " + input +} + +// getProtectionStatusDescription returns the current protection status of the volume +// https://learn.microsoft.com/en-us/windows/win32/secprov/getprotectionstatus-win32-encryptablevolume +func getProtectionStatusDescription(input string) string { + switch input { + case "0": + return "Unprotected" + case "1": + return "Protected" + case "2": + return "Unknown" + } + + return "Status " + input +} + +// intToPercentage converts an int to a percentage string +func intToPercentage(num int32) string { + percentage := float64(num) / 10000.0 + return fmt.Sprintf("%.2f%%", percentage) +} + +// getKeyProtectors returns the key protectors for the volume +// https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectors-win32-encryptablevolume +func getKeyProtectors(item *ole.IDispatch) ([]string, error) { + kp := []string{} + var keyProtectorResults ole.VARIANT + ole.VariantInit(&keyProtectorResults) + + keyIDResultRaw, err := oleutil.CallMethod(item, "GetKeyProtectors", 3, &keyProtectorResults) + if err != nil { + return nil, fmt.Errorf("unable to get Key Protectors while getting BitLocker info. %s", err.Error()) + } else if val, ok := keyIDResultRaw.Value().(int32); val != 0 || !ok { + return nil, fmt.Errorf("unable to get Key Protectors while getting BitLocker info. Return code %d", val) + } + + keyProtectorValues := keyProtectorResults.ToArray().ToValueArray() + for _, keyIDItemRaw := range keyProtectorValues { + keyIDItem, ok := keyIDItemRaw.(string) + if !ok { + return nil, fmt.Errorf("keyProtectorID wasn't a string") + } + kp = append(kp, keyIDItem) + } + + return kp, nil +} + +// bitsToDrives converts a bit map to a list of drives +func bitsToDrives(bitMap uint32) (drives []string) { + availableDrives := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"} + + for i := range availableDrives { + if bitMap&1 == 1 { + drives = append(drives, availableDrives[i]+":") + } + bitMap >>= 1 + } + + return +} + +func getLogicalVolumes() ([]string, error) { + kernel32, err := syscall.LoadLibrary("kernel32.dll") + if err != nil { + return nil, fmt.Errorf("failed to load kernel32.dll: %v", err) + } + defer syscall.FreeLibrary(kernel32) + + getLogicalDrivesHandle, err := syscall.GetProcAddress(kernel32, "GetLogicalDrives") + if err != nil { + return nil, fmt.Errorf("failed to get procedure address: %v", err) + } + + ret, _, callErr := syscall.SyscallN(uintptr(getLogicalDrivesHandle), 0, 0, 0, 0) + if callErr != 0 { + return nil, fmt.Errorf("syscall to GetLogicalDrives failed: %v", callErr) + } + + return bitsToDrives(uint32(ret)), nil +} + +func getBitlockerStatus(targetVolume string) (*EncryptionStatus, error) { + // Connect to the volume + vol, err := bitlockerConnect(targetVolume) + if err != nil { + return nil, fmt.Errorf("there was an error connecting to the volume - error: %v", err) + } + defer vol.bitlockerClose() + + // Get volume status + status, err := vol.getBitlockerStatus() + if err != nil { + return nil, fmt.Errorf("there was an error starting decryption - error: %v", err) + } + + return status, nil +} + +///////////////////////////////////////////////////// +// Bitlocker Management interface implementation +///////////////////////////////////////////////////// + +func GetRecoveryKeys(targetVolume string) (map[string]string, error) { + // Connect to the volume + vol, err := bitlockerConnect(targetVolume) + if err != nil { + return nil, fmt.Errorf("there was an error connecting to the volume - error: %v", err) + } + defer vol.bitlockerClose() + + // Get recovery keys + keys, err := vol.getProtectorsKeys() + if err != nil { + return nil, fmt.Errorf("there was an error retreving protection keys: %v", err) + } + + return keys, nil +} + +func EncryptVolume(targetVolume string) (string, error) { + // Connect to the volume + vol, err := bitlockerConnect(targetVolume) + if err != nil { + return "", fmt.Errorf("there was an error connecting to the volume - error: %v", err) + } + defer vol.bitlockerClose() + + // Prepare for encryption + if err := vol.prepareVolume(VolumeTypeDefault, EncryptionTypeSoftware); err != nil { + return "", fmt.Errorf("there was an error preparing the volume for encryption - error: %v", err) + } + + // Add a recovery protector + recoveryKey, err := vol.protectWithNumericalPassword() + if err != nil { + return "", fmt.Errorf("there was an error adding a recovery protector - error: %v", err) + } + + // Protect with TPM + if err := vol.protectWithTPM(nil); err != nil { + return "", fmt.Errorf("there was an error protecting with TPM - error: %v", err) + } + + // Start encryption + if err := vol.encrypt(XtsAES256, EncryptDataOnly); err != nil { + return "", fmt.Errorf("there was an error starting encryption - error: %v", err) + } + + return recoveryKey, nil +} + +func DecryptVolume(targetVolume string) error { + // Connect to the volume + vol, err := bitlockerConnect(targetVolume) + if err != nil { + return fmt.Errorf("there was an error connecting to the volume - error: %v", err) + } + defer vol.bitlockerClose() + + // Start decryption + if err := vol.decrypt(); err != nil { + return fmt.Errorf("there was an error starting decryption - error: %v", err) + } + + return nil +} + +func GetEncryptionStatus() ([]VolumeStatus, error) { + drives, err := getLogicalVolumes() + if err != nil { + return nil, fmt.Errorf("logical volumen enumeration %v", err) + } + + // iterate drives + var volumeStatus []VolumeStatus + for _, drive := range drives { + status, err := getBitlockerStatus(drive) + if err == nil { + // Skipping errors on purpose + driveStatus := VolumeStatus{ + DriveVolume: drive, + Status: status, + } + volumeStatus = append(volumeStatus, driveStatus) + } + } + + return volumeStatus, nil +} diff --git a/orbit/pkg/update/execwinapi_stub.go b/orbit/pkg/update/execwinapi_stub.go index e4957bc2c..50d6a9a41 100644 --- a/orbit/pkg/update/execwinapi_stub.go +++ b/orbit/pkg/update/execwinapi_stub.go @@ -9,3 +9,7 @@ func RunWindowsMDMEnrollment(args WindowsMDMEnrollmentArgs) error { func RunWindowsMDMUnenrollment(args WindowsMDMEnrollmentArgs) error { return nil } + +func IsRunningOnWindowsServer() (bool, error) { + return false, nil +} diff --git a/orbit/pkg/update/execwinapi_windows.go b/orbit/pkg/update/execwinapi_windows.go index ca28089da..3c0988a0f 100644 --- a/orbit/pkg/update/execwinapi_windows.go +++ b/orbit/pkg/update/execwinapi_windows.go @@ -174,3 +174,17 @@ func generateWindowsMDMAccessTokenPayload(args WindowsMDMEnrollmentArgs) ([]byte pld.Payload.OrbitNodeKey = args.OrbitNodeKey return json.Marshal(pld) } + +// IsRunningOnWindowsServer determines if the process is running on a Windows server. Exported so it can be used across packages. +func IsRunningOnWindowsServer() (bool, error) { + installType, err := readInstallationType() + if err != nil { + return false, err + } + + if strings.ToLower(installType) == "server" { + return true, nil + } + + return false, nil +} diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go index 18076ba92..e07c6648c 100644 --- a/orbit/pkg/update/notifications.go +++ b/orbit/pkg/update/notifications.go @@ -7,6 +7,7 @@ import ( "sync/atomic" "time" + "github.com/fleetdm/fleet/v4/orbit/pkg/bitlocker" "github.com/fleetdm/fleet/v4/orbit/pkg/profiles" "github.com/fleetdm/fleet/v4/orbit/pkg/scripts" "github.com/fleetdm/fleet/v4/server/fleet" @@ -397,3 +398,119 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) { } return cfg, err } + +type DiskEncryptionKeySetter interface { + SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error +} + +type execEncryptVolumeFunc func(string) (string, error) + +type windowsMDMBitlockerConfigFetcher struct { + // Fetcher is the OrbitConfigFetcher that will be wrapped. It is responsible + // for actually returning the orbit configuration or an error. + Fetcher OrbitConfigFetcher + + // Frequency is the minimum amount of time that must pass between two + // executions of the windows MDM enrollment attempt. + Frequency time.Duration + + // Bitlocker Operation Results + EncryptionResult DiskEncryptionKeySetter + + // tracks last time the enrollment command was executed + lastEnrollRun time.Time + + // ensures only one script execution runs at a time + mu sync.Mutex + + // for tests, to be able to mock API commands. If nil, will use + // EncryptVolume + execEncryptVolumeFn execEncryptVolumeFunc +} + +func ApplyWindowsMDMBitlockerFetcherMiddleware( + fetcher OrbitConfigFetcher, + frequency time.Duration, + encryptionResult DiskEncryptionKeySetter, +) OrbitConfigFetcher { + return &windowsMDMBitlockerConfigFetcher{ + Fetcher: fetcher, + Frequency: frequency, + EncryptionResult: encryptionResult, + } +} + +// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet +// server set the "EnforceBitLockerEncryption" flag to true, executes the command +// to attempt BitlockerEncryption (or not, if the device is a Windows Server). +func (w *windowsMDMBitlockerConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) { + cfg, err := w.Fetcher.GetConfig() + if err == nil && cfg.Notifications.EnforceBitLockerEncryption { + if w.mu.TryLock() { + defer w.mu.Unlock() + + w.attemptBitlockerEncryption(cfg.Notifications) + } + } + + return cfg, err +} + +func (w *windowsMDMBitlockerConfigFetcher) attemptBitlockerEncryption(notifs fleet.OrbitConfigNotifications) { + // do not trigger Bitlocker encryption if running on a Windwos server + isWindowsServer, err := IsRunningOnWindowsServer() + if err != nil { + log.Error().Err(err).Msg("checking if the host is a Windows server") + return + } + + if isWindowsServer { + log.Debug().Msg("device is a Windows Server, encryption is not going to be performed") + return + } + + if time.Since(w.lastEnrollRun) <= w.Frequency { + log.Debug().Msg("skipped encryption process, last run was too recent") + return + } + + // Performing Bitlocker encryption operation against C: volume + + // We are supporting only C: volume for now + targetVolume := "C:" + + // Performing actual encryption + + // Getting Bitlocker encryption mock operation function if any + fn := w.execEncryptVolumeFn + if fn == nil { + // Otherwise, using the real one + fn = bitlocker.EncryptVolume + } + recoveryKey, err := fn(targetVolume) + + // Getting Bitlocker encryption operation error message if any + bitlockerError := "" + if err != nil { + bitlockerError = err.Error() + } + + // Update Fleet Server with encryption result + payload := fleet.OrbitHostDiskEncryptionKeyPayload{ + EncryptionKey: []byte(recoveryKey), + ClientError: bitlockerError, + } + + if err != nil { + log.Error().Err(err).Msg("failed to encrypt the volume") + return + } + + err = w.EncryptionResult.SetOrUpdateDiskEncryptionKey(payload) + if err != nil { + log.Error().Err(err).Msg("failed to send encryption result to Fleet Server") + return + } + + w.lastEnrollRun = time.Now() +} diff --git a/orbit/pkg/update/notifications_test.go b/orbit/pkg/update/notifications_test.go index c4512b12f..901dcd527 100644 --- a/orbit/pkg/update/notifications_test.go +++ b/orbit/pkg/update/notifications_test.go @@ -573,3 +573,67 @@ func TestRunScripts(t *testing.T) { require.Contains(t, logBuf.String(), "running scripts [c] succeeded") }) } + +type mockDiskEncryptionKeySetter struct{} + +func (m mockDiskEncryptionKeySetter) SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error { + return nil +} + +func TestBitlockerOperations(t *testing.T) { + var logBuf bytes.Buffer + + oldLog := log.Logger + log.Logger = log.Output(&logBuf) + t.Cleanup(func() { log.Logger = oldLog }) + + var ( + shouldEncrypt = true + shouldReturnError = false + ) + + fetcher := &dummyConfigFetcher{ + cfg: &fleet.OrbitConfig{ + Notifications: fleet.OrbitConfigNotifications{ + EnforceBitLockerEncryption: shouldEncrypt, + }, + }, + } + + enrollFetcher := &windowsMDMBitlockerConfigFetcher{ + Fetcher: fetcher, + Frequency: time.Hour, // doesn't matter for this test + EncryptionResult: mockDiskEncryptionKeySetter{}, + execEncryptVolumeFn: func(string) (string, error) { + if shouldReturnError { + return "", errors.New("error") + } + + return "123456", nil + }, + } + + t.Run("bitlocker encryption is performed", func(t *testing.T) { + shouldEncrypt = true + shouldReturnError = false + cfg, err := enrollFetcher.GetConfig() + require.NoError(t, err) // the dummy fetcher never returns an error + require.Equal(t, fetcher.cfg, cfg) // the bitlocker wrapper properly returns the expected config + }) + + t.Run("bitlocker encryption is not performed", func(t *testing.T) { + shouldEncrypt = false + shouldReturnError = false + cfg, err := enrollFetcher.GetConfig() + require.NoError(t, err) // the dummy fetcher never returns an error + require.Equal(t, fetcher.cfg, cfg) // the bitlocker wrapper properly returns the expected config + }) + + t.Run("bitlocker encryption returns an error", func(t *testing.T) { + shouldEncrypt = true + shouldReturnError = true + cfg, err := enrollFetcher.GetConfig() + require.NoError(t, err) // the dummy fetcher never returns an error + require.Equal(t, fetcher.cfg, cfg) // the bitlocker wrapper properly returns the expected config + }) +} diff --git a/pkg/optjson/optjson.go b/pkg/optjson/optjson.go index ec045070c..a665b3bb5 100644 --- a/pkg/optjson/optjson.go +++ b/pkg/optjson/optjson.go @@ -53,3 +53,42 @@ func (s *String) UnmarshalJSON(data []byte) error { s.Valid = true return nil } + +// Bool represents an optional boolean value. +type Bool struct { + Set bool + Valid bool + Value bool +} + +func SetBool(b bool) Bool { + return Bool{Set: true, Valid: true, Value: b} +} + +func (b Bool) MarshalJSON() ([]byte, error) { + if !b.Valid { + return []byte("null"), nil + } + return json.Marshal(b.Value) +} + +func (b *Bool) UnmarshalJSON(data []byte) error { + // If this method was called, the value was set. + b.Set = true + b.Valid = false + + if bytes.Equal(data, []byte("null")) { + // The key was set to null, blank the value + b.Value = false + return nil + } + + // The key isn't set to null + var v bool + if err := json.Unmarshal(data, &v); err != nil { + return err + } + b.Value = v + b.Valid = true + return nil +} diff --git a/pkg/optjson/optjson_test.go b/pkg/optjson/optjson_test.go index 868963d4a..264834a63 100644 --- a/pkg/optjson/optjson_test.go +++ b/pkg/optjson/optjson_test.go @@ -88,3 +88,84 @@ func TestString(t *testing.T) { } }) } + +func TestBool(t *testing.T) { + t.Run("plain string", func(t *testing.T) { + cases := []struct { + data string + wantErr string + wantRes Bool + marshalAs string + }{ + {`true`, "", Bool{Set: true, Valid: true, Value: true}, `true`}, + {`null`, "", Bool{Set: true, Valid: false, Value: false}, `null`}, + {`123`, "cannot unmarshal number into Go value of type bool", Bool{Set: true, Valid: false, Value: false}, `null`}, + {`{"v": "foo"}`, "cannot unmarshal object into Go value of type bool", Bool{Set: true, Valid: false, Value: false}, `null`}, + } + + for _, c := range cases { + t.Run(c.data, func(t *testing.T) { + var s Bool + err := json.Unmarshal([]byte(c.data), &s) + + if c.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, c.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, c.wantRes, s) + + b, err := json.Marshal(s) + require.NoError(t, err) + require.Equal(t, c.marshalAs, string(b)) + }) + } + }) + + t.Run("struct", func(t *testing.T) { + type N struct { + B2 Bool `json:"b2"` + } + type T struct { + I int `json:"i"` + B Bool `json:"b"` + N N `json:"n"` + } + + cases := []struct { + data string + wantErr string + wantRes T + marshalAs string + }{ + {`{}`, "", T{}, `{"i": 0, "b": null, "n": {"b2": null}}`}, + {`{"x": "nope"}`, "", T{}, `{"i": 0, "b": null, "n": {"b2": null}}`}, + {`{"i": 1, "b": true}`, "", T{I: 1, B: Bool{Set: true, Valid: true, Value: true}}, `{"i": 1, "b": true, "n": {"b2": null}}`}, + {`{"i": 1, "b": null, "n": {}}`, "", T{I: 1, B: Bool{Set: true, Valid: false, Value: false}}, `{"i": 1, "b": null, "n": {"b2": null}}`}, + {`{"i": 1, "b": false, "n": {"b2": true}}`, "", T{I: 1, B: Bool{Set: true, Valid: true, Value: false}, N: N{B2: Bool{Set: true, Valid: true, Value: true}}}, `{"i": 1, "b": false, "n": {"b2": true}}`}, + {`{"i": 1, "b": true, "n": {"b2": null}}`, "", T{I: 1, B: Bool{Set: true, Valid: true, Value: true}, N: N{B2: Bool{Set: true, Valid: false, Value: false}}}, `{"i": 1, "b": true, "n": {"b2": null}}`}, + {`{"i": 1, "b": ""}`, "cannot unmarshal string into Go struct", T{I: 1, B: Bool{Set: true, Valid: false, Value: false}}, `{"i": 1, "b": null, "n": {"b2": null}}`}, + {`{"i": 1, "n": {"b2": 123}}`, "cannot unmarshal number into Go struct", T{I: 1, N: N{B2: Bool{Set: true, Valid: false, Value: false}}}, `{"i": 1, "b": null, "n": {"b2": null}}`}, + } + + for _, c := range cases { + t.Run(c.data, func(t *testing.T) { + var tt T + err := json.Unmarshal([]byte(c.data), &tt) + + if c.wantErr != "" { + require.Error(t, err) + require.ErrorContains(t, err, c.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, c.wantRes, tt) + + b, err := json.Marshal(tt) + require.NoError(t, err) + require.JSONEq(t, c.marshalAs, string(b)) + }) + } + }) +} diff --git a/pkg/rawjson/rawjson.go b/pkg/rawjson/rawjson.go new file mode 100644 index 000000000..d6bb2189a --- /dev/null +++ b/pkg/rawjson/rawjson.go @@ -0,0 +1,55 @@ +package rawjson + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" +) + +// CombineRoots "concatenates" two JSON objects into a single object. +// +// By virtue of its implementation it: +// +// - Doesn't take into account nested keys +// - Assumes the JSON string is well formed and was marshaled by the standard +// library +func CombineRoots(a, b json.RawMessage) (json.RawMessage, error) { + if err := validate(a); err != nil { + return nil, fmt.Errorf("validating first object: %w", err) + } + + if err := validate(b); err != nil { + return nil, fmt.Errorf("validating second object: %w", err) + } + + emptyObject := []byte{'{', '}'} + if bytes.Equal(a, emptyObject) { + return b, nil + } + if bytes.Equal(b, emptyObject) { + return a, nil + } + + // remove '}' from the first object and add a trailing ',' + combined := append(a[:len(a)-1], ',') + // remove '{' from the second object and combine the two + combined = append(combined, b[1:]...) + return combined, nil +} + +func validate(j json.RawMessage) error { + if len(j) < 2 { + return errors.New("incomplete json object") + } + + if j[0] != '{' || j[len(j)-1] != '}' { + return errors.New("json object must be surrounded by '{' and '}'") + } + + if len(j) > 2 && j[len(j)-2] == ',' { + return errors.New("trailing comma at the end of the object") + } + + return nil +} diff --git a/pkg/rawjson/rawjson_test.go b/pkg/rawjson/rawjson_test.go new file mode 100644 index 000000000..03b38a3df --- /dev/null +++ b/pkg/rawjson/rawjson_test.go @@ -0,0 +1,104 @@ +package rawjson + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCombineRoots(t *testing.T) { + tests := []struct { + name string + a json.RawMessage + b json.RawMessage + want json.RawMessage + wantErr string + }{ + { + name: "both empty", + a: []byte("{}"), + b: []byte("{}"), + want: []byte("{}"), + }, + { + name: "first incomplete", + a: []byte("{"), + b: []byte("{}"), + wantErr: "incomplete json object", + }, + { + name: "second incomplete", + a: []byte("{}"), + b: []byte("{"), + wantErr: "incomplete json object", + }, + { + name: "first empty array", + a: []byte{}, + b: []byte("{}"), + wantErr: "incomplete json object", + }, + { + name: "second empty array", + a: []byte("{}"), + b: []byte{}, + wantErr: "incomplete json object", + }, + { + name: "first empty", + a: []byte("{}"), + b: []byte(`{"key":"value"}`), + want: []byte(`{"key":"value"}`), + }, + { + name: "second empty", + a: []byte(`{"key":"value"}`), + b: []byte("{}"), + want: []byte(`{"key":"value"}`), + }, + { + name: "both with data", + a: []byte(`{"key1":"value1"}`), + b: []byte(`{"key2":"value2"}`), + want: []byte(`{"key1":"value1","key2":"value2"}`), + }, + { + name: "first incomplete", + a: []byte(`{"key1":"value1"`), + b: []byte(`{"key2":"value2"}`), + wantErr: "json object must be surrounded by '{' and '}'", + }, + { + name: "second incomplete", + a: []byte(`{"key2":"value2"}`), + b: []byte(`{"key1":"value1"`), + wantErr: "json object must be surrounded by '{' and '}'", + }, + { + name: "first trailing comma", + a: []byte(`{"key1":"value1",}`), + b: []byte(`{"key2":"value2"}`), + wantErr: "trailing comma at the end of the object", + }, + { + name: "second trailing comma", + a: []byte(`{"key1":"value1"}`), + b: []byte(`{"key2":"value2",}`), + wantErr: "trailing comma at the end of the object", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CombineRoots(tt.a, tt.b) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + require.Nil(t, got) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 0da61f93a..c14340fa1 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -213,3 +213,18 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet } return secrets, nil } + +func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { + if teamID != nil && *teamID > 0 { + tc, err := ds.TeamMDMConfig(ctx, *teamID) + if err != nil { + return false, err + } + return tc.EnableDiskEncryption, nil + } + ac, err := ds.AppConfig(ctx) + if err != nil { + return false, err + } + return ac.MDM.EnableDiskEncryption.Value, nil +} diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index 1580d40f2..66c14b157 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/fleet" @@ -30,6 +31,7 @@ func TestAppConfig(t *testing.T) { {"AggregateEnrollSecretPerTeam", testAggregateEnrollSecretPerTeam}, {"Defaults", testAppConfigDefaults}, {"Backwards Compatibility", testAppConfigBackwardsCompatibility}, + {"GetConfigEnableDiskEncryption", testGetConfigEnableDiskEncryption}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -309,7 +311,6 @@ func testAppConfigEnrollSecretRoundtrip(t *testing.T, ds *Datastore) { secrets, err = ds.GetEnrollSecrets(context.Background(), nil) require.NoError(t, err) require.Len(t, secrets, 2) - } func testAppConfigEnrollSecretUniqueness(t *testing.T, ds *Datastore) { @@ -431,3 +432,48 @@ func testAggregateEnrollSecretPerTeam(t *testing.T, ds *Datastore) { {TeamID: ptr.Uint(3), Secret: "team_3_secret_1"}, }, agg) } + +func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { + ctx := context.Background() + defer TruncateTables(t, ds) + + ac, err := ds.AppConfig(ctx) + require.NoError(t, err) + require.False(t, ac.MDM.EnableDiskEncryption.Value) + + enabled, err := ds.getConfigEnableDiskEncryption(ctx, nil) + require.NoError(t, err) + require.False(t, enabled) + + // Enable disk encryption for no team + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + err = ds.SaveAppConfig(ctx, ac) + require.NoError(t, err) + ac, err = ds.AppConfig(ctx) + require.NoError(t, err) + require.True(t, ac.MDM.EnableDiskEncryption.Value) + + enabled, err = ds.getConfigEnableDiskEncryption(ctx, nil) + require.NoError(t, err) + require.True(t, enabled) + + // Create team + team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) + require.NoError(t, err) + + tm, err := ds.Team(ctx, team1.ID) + require.NoError(t, err) + require.NotNil(t, tm) + require.False(t, tm.Config.MDM.EnableDiskEncryption) + + enabled, err = ds.getConfigEnableDiskEncryption(ctx, &team1.ID) + require.NoError(t, err) + require.False(t, enabled) + + // Enable disk encryption for the team + tm.Config.MDM.EnableDiskEncryption = true + tm, err = ds.SaveTeam(ctx, tm) + require.NoError(t, err) + require.NotNil(t, tm) + require.True(t, tm.Config.MDM.EnableDiskEncryption) +} diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index cb1133ac6..1e95d96f1 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -2082,7 +2082,7 @@ func (ds *Datastore) GetMDMIdPAccount(ctx context.Context, uuid string) (*fleet. return &acct, nil } -func subqueryDiskEncryptionVerifying() (string, []interface{}) { +func subqueryFileVaultVerifying() (string, []interface{}) { sql := ` SELECT 1 FROM host_mdm_apple_profiles hmap @@ -2100,7 +2100,7 @@ func subqueryDiskEncryptionVerifying() (string, []interface{}) { return sql, args } -func subqueryDiskEncryptionVerified() (string, []interface{}) { +func subqueryFileVaultVerified() (string, []interface{}) { sql := ` SELECT 1 FROM host_mdm_apple_profiles hmap @@ -2118,7 +2118,7 @@ func subqueryDiskEncryptionVerified() (string, []interface{}) { return sql, args } -func subqueryDiskEncryptionActionRequired() (string, []interface{}) { +func subqueryFileVaultActionRequired() (string, []interface{}) { sql := ` SELECT 1 FROM host_mdm_apple_profiles hmap @@ -2138,7 +2138,7 @@ func subqueryDiskEncryptionActionRequired() (string, []interface{}) { return sql, args } -func subqueryDiskEncryptionEnforcing() (string, []interface{}) { +func subqueryFileVaultEnforcing() (string, []interface{}) { sql := ` SELECT 1 FROM host_mdm_apple_profiles hmap @@ -2168,7 +2168,7 @@ func subqueryDiskEncryptionEnforcing() (string, []interface{}) { return sql, args } -func subqueryDiskEncryptionFailed() (string, []interface{}) { +func subqueryFileVaultFailed() (string, []interface{}) { sql := ` SELECT 1 FROM host_mdm_apple_profiles hmap @@ -2180,7 +2180,7 @@ func subqueryDiskEncryptionFailed() (string, []interface{}) { return sql, args } -func subqueryDiskEncryptionRemovingEnforcement() (string, []interface{}) { +func subqueryFileVaultRemovingEnforcement() (string, []interface{}) { sql := ` SELECT 1 FROM host_mdm_apple_profiles hmap @@ -2224,20 +2224,20 @@ FROM hosts h LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id WHERE - %s` + h.platform = 'darwin' AND %s` var args []interface{} - subqueryVerified, subqueryVerifiedArgs := subqueryDiskEncryptionVerified() + subqueryVerified, subqueryVerifiedArgs := subqueryFileVaultVerified() args = append(args, subqueryVerifiedArgs...) - subqueryVerifying, subqueryVerifyingArgs := subqueryDiskEncryptionVerifying() + subqueryVerifying, subqueryVerifyingArgs := subqueryFileVaultVerifying() args = append(args, subqueryVerifyingArgs...) - subqueryActionRequired, subqueryActionRequiredArgs := subqueryDiskEncryptionActionRequired() + subqueryActionRequired, subqueryActionRequiredArgs := subqueryFileVaultActionRequired() args = append(args, subqueryActionRequiredArgs...) - subqueryEnforcing, subqueryEnforcingArgs := subqueryDiskEncryptionEnforcing() + subqueryEnforcing, subqueryEnforcingArgs := subqueryFileVaultEnforcing() args = append(args, subqueryEnforcingArgs...) - subqueryFailed, subqueryFailedArgs := subqueryDiskEncryptionFailed() + subqueryFailed, subqueryFailedArgs := subqueryFileVaultFailed() args = append(args, subqueryFailedArgs...) - subqueryRemovingEnforcement, subqueryRemovingEnforcementArgs := subqueryDiskEncryptionRemovingEnforcement() + subqueryRemovingEnforcement, subqueryRemovingEnforcementArgs := subqueryFileVaultRemovingEnforcement() args = append(args, subqueryRemovingEnforcementArgs...) teamFilter := "h.team_id IS NULL" diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 3c9173b63..2d5977334 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -782,7 +782,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) { var hostID uint err = sqlx.GetContext(context.Background(), ds.reader(context.Background()), &hostID, `SELECT id FROM hosts WHERE uuid = ?`, testUUID) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf", "", nil) require.NoError(t, err) key, err := ds.GetHostDiskEncryptionKey(ctx, hostID) @@ -1474,7 +1474,7 @@ func upsertHostCPs( func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) { ctx := context.Background() - checkListHosts := func(status fleet.MacOSSettingsStatus, teamID *uint, expected []*fleet.Host) bool { + checkListHosts := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool { expectedIDs := []uint{} for _, h := range expected { expectedIDs = append(expectedIDs, h.ID) @@ -1556,7 +1556,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "foo") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "foo", "", nil) require.NoError(t, err) res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, nil) require.NoError(t, err) @@ -1596,7 +1596,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(1), res.Verified) // hosts[0] now has filevault fully enforced and verified - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1].ID, "bar") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1].ID, "bar", "", nil) require.NoError(t, err) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, false, time.Now().Add(1*time.Hour)) require.NoError(t, err) @@ -1619,10 +1619,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(1), res.Verified) // check that list hosts by status matches summary - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, hosts[1:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, hosts[0:1])) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[1:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, hosts[0:1])) // create a team team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"}) @@ -1662,7 +1662,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9].ID, "baz") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9].ID, "baz", "", nil) require.NoError(t, err) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour)) require.NoError(t, err) @@ -1675,10 +1675,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verified) // check that list hosts by status matches summary - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{})) upsertHostCPs(hosts[9:10], append(teamCPs, fvTeam), fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t) res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &team.ID) @@ -1701,10 +1701,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(0), res.Verified) // check that list hosts by status matches summary - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{})) // set decryptable back to true for hosts[9] err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour)) @@ -1718,21 +1718,22 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) require.Equal(t, uint(1), res.Verified) // hosts[9] goes back to verified // check that list hosts by status matches summary - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, hosts[9:10])) } func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { ctx := context.Background() - checkListHosts := func(status fleet.MacOSSettingsStatus, teamID *uint, expected []*fleet.Host) bool { + checkFilterHostsByMacOSSettings := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool { expectedIDs := []uint{} for _, h := range expected { expectedIDs = append(expectedIDs, h.ID) } + // check that list hosts by macos settings status matches summary gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID}) gotIDs := []uint{} for _, h := range gotHosts { @@ -1742,6 +1743,26 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs) } + // check that list hosts by os settings status matches summary + checkFilterHostsByOSSettings := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool { + expectedIDs := []uint{} + for _, h := range expected { + expectedIDs = append(expectedIDs, h.ID) + } + + gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{OSSettingsFilter: status, TeamFilter: teamID}) + gotIDs := []uint{} + for _, h := range gotHosts { + gotIDs = append(gotIDs, h.ID) + } + + return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs) + } + + checkListHosts := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool { + return checkFilterHostsByMacOSSettings(status, teamID, expected) && checkFilterHostsByOSSettings(status, teamID, expected) + } + var hosts []*fleet.Host for i := 0; i < 10; i++ { h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", @@ -1766,14 +1787,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // all hosts pending install of all profiles upsertHostCPs(hosts, noTeamCPs, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryPending, ctx, ds, t) @@ -1784,14 +1805,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // hosts[0] and hosts[1] failed one profile upsertHostCPs(hosts[0:2], noTeamCPs[0:1], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryFailed, ctx, ds, t) @@ -1810,14 +1831,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(2), res.Failed) // only count one failure per host (hosts[0] failed two profiles but only counts once) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts[2:])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // hosts[0:3] installed a third profile upsertHostCPs(hosts[0:3], noTeamCPs[2:3], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) @@ -1828,14 +1849,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(2), res.Failed) // no change require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts[2:])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // hosts[6] deletes all its profiles tx, err := ds.writer(ctx).BeginTxx(ctx, nil) @@ -1850,14 +1871,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(2), res.Failed) // no change require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // hosts[9] installed all profiles but one is with status nil (pending) upsertHostCPs(hosts[9:10], noTeamCPs[:9], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) @@ -1870,14 +1891,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(2), res.Failed) // no change require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // hosts[9] installed all profiles upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) @@ -1889,14 +1910,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(2), res.Failed) // no change require.Equal(t, uint(1), res.Verifying) // add one host that has installed all profiles require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) // create a team tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "rocket"}) @@ -1908,10 +1929,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) // no profiles yet require.Equal(t, uint(0), res.Verifying) // no profiles yet require.Equal(t, uint(0), res.Verified) // no profiles yet - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{})) // transfer hosts[9] to new team err = ds.AddHostsToTeam(ctx, &tm.ID, []uint{hosts[9].ID}) @@ -1926,14 +1947,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(len(hosts)-4), res.Pending) // hosts[9] is still not pending, transferred to team require.Equal(t, uint(2), res.Failed) // no change require.Equal(t, uint(0), res.Verifying) // hosts[9] was transferred so this is now zero - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &tm.ID) // get summary for new team require.NoError(t, err) @@ -1942,10 +1963,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{})) // create somes config profiles for the new team var teamCPs []*fleet.MDMAppleConfigProfile @@ -1964,10 +1985,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{})) // hosts[9] successfully removed old profiles upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeRemove, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) @@ -1978,10 +1999,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(1), res.Verifying) // hosts[9] is verifying all new profiles require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{})) // verify one profile on hosts[9] upsertHostCPs(hosts[9:10], teamCPs[0:1], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t) @@ -1992,10 +2013,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(1), res.Verifying) // hosts[9] is still verifying other profiles require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, hosts[9:10])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{})) // verify the other profiles on hosts[9] upsertHostCPs(hosts[9:10], teamCPs[1:], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t) @@ -2006,10 +2027,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(0), res.Failed) require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(1), res.Verified) // hosts[9] is all verified - require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, hosts[9:10])) + require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, hosts[9:10])) // confirm no changes in summary for profiles with no team res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, ptr.Uint(0)) // team id zero represents no team @@ -2020,14 +2041,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { require.Equal(t, uint(2), res.Failed) // two failed hosts require.Equal(t, uint(0), res.Verifying) // hosts[9] transferred to new team so is not counted under no team require.Equal(t, uint(0), res.Verified) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) - require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) - require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) - require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts)) + require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2])) + require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) + require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) } func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) { @@ -2166,7 +2187,7 @@ func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) { } func createDiskEncryptionRecord(ctx context.Context, ds *Datastore, t *testing.T, hostId uint, key string, decryptable bool, threshold time.Time) { - err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostId, key) + err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostId, key, "", nil) require.NoError(t, err) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hostId}, decryptable, threshold) require.NoError(t, err) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 10124e68e..0ad9c6e00 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -887,7 +887,11 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt } leftJoinFailingPolicies := !useHostPaginationOptim - sql, params = ds.applyHostFilters(opt, sql, filter, params, leftJoinFailingPolicies) + + sql, params, err := ds.applyHostFilters(ctx, opt, sql, filter, params, leftJoinFailingPolicies) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "list hosts: apply host filters") + } hosts := []*fleet.Host{} if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, sql, params...); err != nil { @@ -906,7 +910,7 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt } // TODO(Sarah): Do we need to reconcile mutually exclusive filters? -func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, filter fleet.TeamFilter, params []interface{}, leftJoinFailingPolicies bool) (string, []interface{}) { +func (ds *Datastore) applyHostFilters(ctx context.Context, opt fleet.HostListOptions, sql string, filter fleet.TeamFilter, params []interface{}, leftJoinFailingPolicies bool) (string, []interface{}, error) { opt.OrderKey = defaultHostColumnTableAlias(opt.OrderKey) deviceMappingJoin := `LEFT JOIN ( @@ -1004,12 +1008,20 @@ func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, fil sql, params = filterHostsByMDM(sql, opt, params) sql, params = filterHostsByMacOSSettingsStatus(sql, opt, params) sql, params = filterHostsByMacOSDiskEncryptionStatus(sql, opt, params) + if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { + return "", nil, err + } else if opt.OSSettingsFilter.IsValid() { + sql, params = ds.filterHostsByOSSettingsStatus(sql, opt, params, enableDiskEncryption) + } else if opt.OSSettingsDiskEncryptionFilter.IsValid() { + sql, params = ds.filterHostsByOSSettingsDiskEncryptionStatus(sql, opt, params, enableDiskEncryption) + } + sql, params = filterHostsByMDMBootstrapPackageStatus(sql, opt, params) sql, params = filterHostsByOS(sql, opt, params) sql, params, _ = hostSearchLike(sql, params, opt.MatchQuery, hostSearchColumns...) sql, params = appendListOptionsWithCursorToSQL(sql, params, &opt.ListOptions) - return sql, params + return sql, params, nil } func filterHostsByTeam(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { @@ -1115,13 +1127,13 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par var subquery string var subqueryParams []interface{} switch opt.MacOSSettingsFilter { - case fleet.MacOSSettingsFailed: + case fleet.OSSettingsFailed: subquery, subqueryParams = subqueryHostsMacOSSettingsStatusFailing() - case fleet.MacOSSettingsPending: + case fleet.OSSettingsPending: subquery, subqueryParams = subqueryHostsMacOSSettingsStatusPending() - case fleet.MacOSSettingsVerifying: + case fleet.OSSettingsVerifying: subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying() - case fleet.MacOSSettingsVerified: + case fleet.OSSettingsVerified: subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified() } if subquery != "" { @@ -1140,22 +1152,131 @@ func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOption var subqueryParams []interface{} switch opt.MacOSSettingsDiskEncryptionFilter { case fleet.DiskEncryptionVerified: - subquery, subqueryParams = subqueryDiskEncryptionVerified() + subquery, subqueryParams = subqueryFileVaultVerified() case fleet.DiskEncryptionVerifying: - subquery, subqueryParams = subqueryDiskEncryptionVerifying() + subquery, subqueryParams = subqueryFileVaultVerifying() case fleet.DiskEncryptionActionRequired: - subquery, subqueryParams = subqueryDiskEncryptionActionRequired() + subquery, subqueryParams = subqueryFileVaultActionRequired() case fleet.DiskEncryptionEnforcing: - subquery, subqueryParams = subqueryDiskEncryptionEnforcing() + subquery, subqueryParams = subqueryFileVaultEnforcing() case fleet.DiskEncryptionFailed: - subquery, subqueryParams = subqueryDiskEncryptionFailed() + subquery, subqueryParams = subqueryFileVaultFailed() case fleet.DiskEncryptionRemovingEnforcement: - subquery, subqueryParams = subqueryDiskEncryptionRemovingEnforcement() + subquery, subqueryParams = subqueryFileVaultRemovingEnforcement() } return sql + fmt.Sprintf(` AND EXISTS (%s)`, subquery), append(params, subqueryParams...) } +func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostListOptions, params []interface{}, isDiskEncryptionEnabled bool) (string, []interface{}) { + if !opt.OSSettingsFilter.IsValid() { + return sql, params + } + + sqlFmt := ` AND h.platform IN('windows', 'darwin')` + if opt.TeamFilter == nil { + // macOS settings filter is not compatible with the "all teams" option so append the "no + // team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) + sqlFmt += ` AND h.team_id IS NULL` + } + sqlFmt += ` AND ((h.platform = 'windows' AND (%s)) OR (h.platform = 'darwin' AND (%s)))` + + var subqueryMacOS string + var subqueryParams []interface{} + whereWindows := "FALSE" + whereMacOS := "FALSE" + + switch opt.OSSettingsFilter { + case fleet.OSSettingsFailed: + subqueryMacOS, subqueryParams = subqueryHostsMacOSSettingsStatusFailing() + if isDiskEncryptionEnabled { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionFailed) + } + case fleet.OSSettingsPending: + subqueryMacOS, subqueryParams = subqueryHostsMacOSSettingsStatusPending() + if isDiskEncryptionEnabled { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing) + } + case fleet.OSSettingsVerifying: + subqueryMacOS, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying() + if isDiskEncryptionEnabled { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying) + } + case fleet.OSSettingsVerified: + subqueryMacOS, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified() + if isDiskEncryptionEnabled { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerified) + } + } + + if subqueryMacOS != "" { + whereMacOS = "EXISTS (" + subqueryMacOS + ")" + } + + return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...) +} + +func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}, enableDiskEncryption bool) (string, []interface{}) { + if !opt.OSSettingsDiskEncryptionFilter.IsValid() { + return sql, params + } + + sqlFmt := " AND h.platform IN('windows', 'darwin')" + // TODO: Should we add no team filter here? It isn't included for the FileVault filter but is + // for the general macOS settings filter. + if opt.TeamFilter == nil { + // macOS settings filter is not compatible with the "all teams" option so append the "no + // team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) + sqlFmt += ` AND h.team_id IS NULL` + } + sqlFmt += ` AND ((h.platform = 'windows' AND %s) OR (h.platform = 'darwin' AND %s))` + + var subqueryMacOS string + var subqueryParams []interface{} + whereWindows := "FALSE" + whereMacOS := "FALSE" + + switch opt.OSSettingsDiskEncryptionFilter { + case fleet.DiskEncryptionVerified: + if enableDiskEncryption { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerified) + } + subqueryMacOS, subqueryParams = subqueryFileVaultVerified() + + case fleet.DiskEncryptionVerifying: + if enableDiskEncryption { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying) + } + subqueryMacOS, subqueryParams = subqueryFileVaultVerifying() + + case fleet.DiskEncryptionActionRequired: + // Windows hosts cannot be action required status in the current implementation. + subqueryMacOS, subqueryParams = subqueryFileVaultActionRequired() + + case fleet.DiskEncryptionEnforcing: + if enableDiskEncryption { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing) + } + subqueryMacOS, subqueryParams = subqueryFileVaultEnforcing() + + case fleet.DiskEncryptionFailed: + if enableDiskEncryption { + whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionFailed) + } + subqueryMacOS, subqueryParams = subqueryFileVaultFailed() + + case fleet.DiskEncryptionRemovingEnforcement: + // Windows hosts cannot be removing enforcement status in the current implementation. + subqueryMacOS, subqueryParams = subqueryFileVaultRemovingEnforcement() + } + + if subqueryMacOS != "" { + whereMacOS = "EXISTS (" + subqueryMacOS + ")" + } + + return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...) +} + func filterHostsByMDMBootstrapPackageStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) { if opt.MDMBootstrapPackageFilter == nil || !opt.MDMBootstrapPackageFilter.IsValid() { return sql, params @@ -1210,7 +1331,11 @@ func (ds *Datastore) CountHosts(ctx context.Context, filter fleet.TeamFilter, op leftJoinFailingPolicies := false var params []interface{} - sql, params = ds.applyHostFilters(opt, sql, filter, params, leftJoinFailingPolicies) + + sql, params, err := ds.applyHostFilters(ctx, opt, sql, filter, params, leftJoinFailingPolicies) + if err != nil { + return 0, ctxerr.Wrap(ctx, err, "count hosts: apply host filters") + } var count int if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, sql, params...); err != nil { @@ -1742,13 +1867,14 @@ func (ds *Datastore) LoadHostByNodeKey(ctx context.Context, nodeKey string) (*fl type hostWithMDMInfo struct { fleet.Host - HostID *uint `db:"host_id"` - Enrolled *bool `db:"enrolled"` - ServerURL *string `db:"server_url"` - InstalledFromDep *bool `db:"installed_from_dep"` - IsServer *bool `db:"is_server"` - MDMID *uint `db:"mdm_id"` - Name *string `db:"name"` + HostID *uint `db:"host_id"` + Enrolled *bool `db:"enrolled"` + ServerURL *string `db:"server_url"` + InstalledFromDep *bool `db:"installed_from_dep"` + IsServer *bool `db:"is_server"` + MDMID *uint `db:"mdm_id"` + Name *string `db:"name"` + EncryptionKeyAvailable *bool `db:"encryption_key_available"` } // LoadHostByOrbitNodeKey loads the whole host identified by the node key. @@ -1804,7 +1930,9 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) COALESCE(hm.is_server, false) AS is_server, COALESCE(mdms.name, ?) AS name, COALESCE(hdek.reset_requested, false) AS disk_encryption_reset_requested, + COALESCE(hdek.decryptable, false) as encryption_key_available, IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet, + hd.encrypted as disk_encryption_enabled, t.name as team_name FROM hosts h @@ -1824,6 +1952,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) host_disk_encryption_keys hdek ON hdek.host_id = h.id + LEFT OUTER JOIN + host_disks hd + ON + hd.host_id = h.id LEFT OUTER JOIN teams t ON @@ -1846,6 +1978,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string) MDMID: hostWithMDM.MDMID, Name: *hostWithMDM.Name, } + + host.MDM = fleet.MDMHostData{ + EncryptionKeyAvailable: *hostWithMDM.EncryptionKeyAvailable, + } } return &host, nil case errors.Is(err, sql.ErrNoRows): @@ -3013,19 +3149,30 @@ func (ds *Datastore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID ) } -func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string) error { +func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error { _, err := ds.writer(ctx).ExecContext(ctx, ` - INSERT INTO host_disk_encryption_keys (host_id, base64_encrypted) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE - /* if the key has changed, NULLify this value so it can be calculated again */ - decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, NULL), - base64_encrypted = VALUES(base64_encrypted) - `, hostID, encryptedBase64Key) +INSERT INTO host_disk_encryption_keys + (host_id, base64_encrypted, client_error, decryptable) +VALUES + (?, ?, ?, ?) +ON DUPLICATE KEY UPDATE + /* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */ + decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, VALUES(decryptable)), + base64_encrypted = VALUES(base64_encrypted), + client_error = VALUES(client_error) +`, hostID, encryptedBase64Key, clientError, decryptable) return err } func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { + // NOTE(mna): currently we only verify encryption keys for macOS, + // Windows/bitlocker uses a different approach where orbit sends the + // encryption key and we encrypt it server-side with the WSTEP certificate, + // so it is always decryptable once received. + // + // To avoid sending Windows-related keys to verify as part of this call, we + // only return rows that have a non-empty encryption key (for Windows, the + // key is blanked if an error occurred trying to retrieve it on the host). var keys []fleet.HostDiskEncryptionKey err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, ` SELECT @@ -3035,7 +3182,8 @@ func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fle FROM host_disk_encryption_keys WHERE - decryptable IS NULL + decryptable IS NULL AND + base64_encrypted != '' `) return keys, err } diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 58367713b..0a89f4f32 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -21,6 +21,7 @@ import ( "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/google/uuid" @@ -111,7 +112,7 @@ func TestHosts(t *testing.T) { {"HostsListBySoftwareChangedAt", testHostsListBySoftwareChangedAt}, {"HostsListByOperatingSystemID", testHostsListByOperatingSystemID}, {"HostsListByOSNameAndVersion", testHostsListByOSNameAndVersion}, - {"HostsListByDiskEncryptionStatus", testHostsListDiskEncryptionStatus}, + {"HostsListByDiskEncryptionStatus", testHostsListMacOSSettingsDiskEncryptionStatus}, {"HostsListFailingPolicies", printReadsInTest(testHostsListFailingPolicies)}, {"HostsExpiration", testHostsExpiration}, {"HostsAllPackStats", testHostsAllPackStats}, @@ -722,8 +723,13 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { var hosts []*fleet.Host for i := 0; i < 10; i++ { + var opts []test.NewHostOption + switch i { + case 5, 6: + opts = append(opts, test.WithPlatform("windows")) + } h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", - fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now()) + fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...) hosts = append(hosts, h) } userFilter := fleet.TeamFilter{User: test.UserAdmin} @@ -763,12 +769,12 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { Checksum: []byte("csum"), }, })) - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[0] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team // macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { @@ -781,12 +787,39 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) { Checksum: []byte("csum"), }, })) - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[0] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team // macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9] - listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + + // test team filter in combination with os settings filter + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team + // os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerifying}, 1) + + // test team filter in combination with os settings disk encryptionfilter + require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + ProfileID: 1, + ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier, + HostUUID: hosts[8].UUID, // hosts[8] is assgined to no team + CommandUUID: "command-uuid-3", + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryPending, + Checksum: []byte("disk-encryption-csum"), + }, + })) + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // hosts[0] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // wrong team + // os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8] + listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8] } func testHostsListFilterAdditional(t *testing.T, ds *Datastore) { @@ -2920,7 +2953,7 @@ func testHostsListByOSNameAndVersion(t *testing.T, ds *Datastore) { } } -func testHostsListDiskEncryptionStatus(t *testing.T, ds *Datastore) { +func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore) { ctx := context.Background() // seed hosts @@ -5740,7 +5773,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) { err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0") require.NoError(t, err) // set an encryption key - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY") + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY", "", nil) require.NoError(t, err) // set an mdm profile prof, err := ds.NewMDMAppleConfigProfile(context.Background(), *configProfileForTest(t, "N1", "I1", "U1")) @@ -6586,23 +6619,26 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) { require.NoError(t, err) require.Equal(t, hFleet.ID, loadFleet.ID) require.False(t, loadFleet.MDMInfo.IsServer) + + // fill in disk encryption information + require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true)) + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hFleet.ID, "test-key", "", nil) + require.NoError(t, err) + err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hFleet.ID}, true, time.Now()) + require.NoError(t, err) + loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey) + require.NoError(t, err) + require.True(t, loadFleet.MDM.EncryptionKeyAvailable) + require.NoError(t, err) + require.NotNil(t, loadFleet.DiskEncryptionEnabled) + require.True(t, *loadFleet.DiskEncryptionEnabled) } -func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected *bool) { - ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error { - var actual *bool - - row := tx.QueryRowxContext( - context.Background(), - "SELECT decryptable FROM host_disk_encryption_keys WHERE host_id = ?", - hostID, - ) - - err := row.Scan(&actual) - require.NoError(t, err) - require.Equal(t, expected, actual) - return nil - }) +func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedDecryptable *bool) { + got, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID) + require.NoError(t, err) + require.Equal(t, expectedKey, got.Base64Encrypted) + require.Equal(t, expectedDecryptable, got.Decryptable) } func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { @@ -6632,49 +6668,81 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { PrimaryMac: "30-65-EC-6F-C4-59", }) require.NoError(t, err) - - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA") + host3, err := ds.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String("3"), + UUID: "3", + OsqueryHostID: ptr.String("3"), + Hostname: "foo.local3", + PrimaryIP: "192.168.1.3", + PrimaryMac: "30-65-EC-6F-C4-60", + }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB") + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil) require.NoError(t, err) - checkEncryptionKey := func(hostID uint, expected string) { - actual, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID) - require.NoError(t, err) - require.Equal(t, expected, actual.Base64Encrypted) - } + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB", "", nil) + require.NoError(t, err) h, err := ds.Host(context.Background(), host.ID) require.NoError(t, err) - checkEncryptionKey(h.ID, "AAA") + checkEncryptionKeyStatus(t, ds, h.ID, "AAA", nil) h, err = ds.Host(context.Background(), host2.ID) require.NoError(t, err) - checkEncryptionKey(h.ID, "BBB") + checkEncryptionKeyStatus(t, ds, h.ID, "BBB", nil) - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC") + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC", "", nil) require.NoError(t, err) h, err = ds.Host(context.Background(), host2.ID) require.NoError(t, err) - checkEncryptionKey(h.ID, "CCC") + checkEncryptionKeyStatus(t, ds, h.ID, "CCC", nil) // setting the encryption key to an existing value doesn't change its // encryption status err = ds.SetHostsDiskEncryptionKeyStatus(context.Background(), []uint{host.ID}, true, time.Now().Add(time.Hour)) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true)) + checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true)) // same key doesn't change encryption status - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA") + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true)) + checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true)) // different key resets encryption status - err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY") + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY", "", nil) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, nil) + checkEncryptionKeyStatus(t, ds, host.ID, "XZY", nil) + + // set the key with an initial decrypted status of true + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(true)) + require.NoError(t, err) + checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true)) + + // same key, provided decrypted status is ignored (stored one is kept) + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(false)) + require.NoError(t, err) + checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true)) + + // client error, key is removed and decrypted status is nulled + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "fail", nil) + require.NoError(t, err) + checkEncryptionKeyStatus(t, ds, host3.ID, "", nil) + + // new key, provided decrypted status is applied + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "def", "", ptr.Bool(true)) + require.NoError(t, err) + checkEncryptionKeyStatus(t, ds, host3.ID, "def", ptr.Bool(true)) + + // different key, provided decrypted status is applied + err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "ghi", "", ptr.Bool(false)) + require.NoError(t, err) + checkEncryptionKeyStatus(t, ds, host3.ID, "ghi", ptr.Bool(false)) } func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { @@ -6692,7 +6760,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { PrimaryMac: "30-65-EC-6F-C4-58", }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil) require.NoError(t, err) host2, err := ds.NewHost(context.Background(), &fleet.Host{ @@ -6709,7 +6777,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil) require.NoError(t, err) threshold := time.Now().Add(time.Hour) @@ -6717,31 +6785,31 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { // empty set err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{}, false, threshold) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, nil) - checkEncryptionKeyStatus(t, ds, host2.ID, nil) + checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil) + checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil) // keys that changed after the provided threshold are not updated err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold.Add(-24*time.Hour)) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, nil) - checkEncryptionKeyStatus(t, ds, host2.ID, nil) + checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil) + checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil) // single host err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, threshold) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true)) - checkEncryptionKeyStatus(t, ds, host2.ID, nil) + checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true)) + checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil) // multiple hosts err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true)) - checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(true)) + checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true)) + checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(true)) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold) require.NoError(t, err) - checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(false)) - checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(false)) + checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(false)) + checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(false)) } func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { @@ -6773,9 +6841,9 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { }) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil) require.NoError(t, err) - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil) require.NoError(t, err) keys, err := ds.GetUnverifiedDiskEncryptionKeys(ctx) @@ -6794,6 +6862,17 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx) require.NoError(t, err) require.Len(t, keys, 1) + require.Equal(t, host2.ID, keys[0].HostID) + + // update key of host 1 to empty with a client error, should not be reported + // by GetUnverifiedDiskEncryptionKeys + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "failed", nil) + require.NoError(t, err) + + keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx) + require.NoError(t, err) + require.Len(t, keys, 1) + require.Equal(t, host2.ID, keys[0].HostID) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold) require.NoError(t, err) @@ -6992,7 +7071,7 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) { require.Equal(t, -1, *got.MDM.TestGetRawDecryptable()) // create the encryption key row, but unknown decryptable - err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc") + err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc", "", nil) require.NoError(t, err) got, err = ds.Host(ctx, host.ID) diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index 551ad0463..2573a53da 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -552,10 +552,13 @@ func (ds *Datastore) ListHostsInLabel(ctx context.Context, filter fleet.TeamFilt query := fmt.Sprintf(queryFmt, hostMDMSelect, failingPoliciesSelect, deviceMappingSelect, hostMDMJoin, failingPoliciesJoin, deviceMappingJoin) - query, params := ds.applyHostLabelFilters(filter, lid, query, opt) + query, params, err := ds.applyHostLabelFilters(ctx, filter, lid, query, opt) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "applying label query filters") + } hosts := []*fleet.Host{} - err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, query, params...) + err = sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, query, params...) if err != nil { return nil, ctxerr.Wrap(ctx, err, "selecting label query executions") } @@ -563,7 +566,7 @@ func (ds *Datastore) ListHostsInLabel(ctx context.Context, filter fleet.TeamFilt } // NOTE: the hosts table must be aliased to `h` in the query passed to this function. -func (ds *Datastore) applyHostLabelFilters(filter fleet.TeamFilter, lid uint, query string, opt fleet.HostListOptions) (string, []interface{}) { +func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.TeamFilter, lid uint, query string, opt fleet.HostListOptions) (string, []interface{}, error) { params := []interface{}{lid} if opt.ListOptions.OrderKey == "display_name" { @@ -582,26 +585,33 @@ func (ds *Datastore) applyHostLabelFilters(filter fleet.TeamFilter, lid uint, qu query, params = filterHostsByMacOSSettingsStatus(query, opt, params) query, params = filterHostsByMacOSDiskEncryptionStatus(query, opt, params) query, params = filterHostsByMDMBootstrapPackageStatus(query, opt, params) + if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { + return "", nil, err + } else if opt.OSSettingsFilter.IsValid() { + query, params = ds.filterHostsByOSSettingsStatus(query, opt, params, enableDiskEncryption) + } else if opt.OSSettingsDiskEncryptionFilter.IsValid() { + query, params = ds.filterHostsByOSSettingsDiskEncryptionStatus(query, opt, params, enableDiskEncryption) + } query, params = searchLike(query, params, opt.MatchQuery, hostSearchColumns...) query, params = appendListOptionsWithCursorToSQL(query, params, &opt.ListOptions) - return query, params + return query, params, nil } func (ds *Datastore) CountHostsInLabel(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) (int, error) { query := `SELECT count(*) FROM label_membership lm JOIN hosts h ON (lm.host_id = h.id) LEFT JOIN host_seen_times hst ON (h.id=hst.host_id) + LEFT JOIN host_disks hd ON (h.id=hd.host_id) ` query += hostMDMJoin - if opt.LowDiskSpaceFilter != nil { - query += ` LEFT JOIN host_disks hd ON (h.id=hd.host_id) ` + query, params, err := ds.applyHostLabelFilters(ctx, filter, lid, query, opt) + if err != nil { + return 0, err } - query, params := ds.applyHostLabelFilters(filter, lid, query, opt) - var count int if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, query, params...); err != nil { return 0, ctxerr.Wrap(ctx, err, "count hosts") diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go index db4ef8049..377cf2243 100644 --- a/server/datastore/mysql/labels_test.go +++ b/server/datastore/mysql/labels_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" @@ -66,6 +67,7 @@ func TestLabels(t *testing.T) { {"ListHostsInLabelFailingPolicies", testListHostsInLabelFailingPolicies}, {"ListHostsInLabelDiskEncryptionStatus", testListHostsInLabelDiskEncryptionStatus}, {"HostMemberOfAllLabels", testHostMemberOfAllLabels}, + {"ListHostsInLabelOSSettings", testLabelsListHostsInLabelOSSettings}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -497,12 +499,12 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da Checksum: []byte("csum"), }, })) - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h1 - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h1 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team // macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ { @@ -515,12 +517,12 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da Checksum: []byte("csum"), }, })) - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h1 - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h1 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team // macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2 - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2 - listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2 + listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2 } func testLabelsBuiltIn(t *testing.T, db *Datastore) { @@ -1329,3 +1331,97 @@ func testHostMemberOfAllLabels(t *testing.T, ds *Datastore) { }) } } + +func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) { + h1, err := db.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("1"), + NodeKey: ptr.String("1"), + UUID: "1", + Hostname: "foo.local", + Platform: "windows", + }) + require.NoError(t, err) + + h2, err := db.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("2"), + NodeKey: ptr.String("2"), + UUID: "2", + Hostname: "bar.local", + Platform: "windows", + }) + require.NoError(t, err) + h3, err := db.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("3"), + NodeKey: ptr.String("3"), + UUID: "3", + Hostname: "baz.local", + Platform: "centos", + }) + require.NoError(t, err) + + l1 := &fleet.LabelSpec{ + ID: 1, + Name: "label foo", + Query: "query1", + } + err = db.ApplyLabelSpecs(context.Background(), []*fleet.LabelSpec{l1}) + require.Nil(t, err) + + filter := fleet.TeamFilter{User: test.UserAdmin} + // add all hosts to label + for _, h := range []*fleet.Host{h1, h2, h3} { + require.NoError(t, db.RecordLabelQueryExecutions(context.Background(), h, map[uint]*bool{l1.ID: ptr.Bool(true)}, time.Now(), false)) + } + + // turn on disk encryption + ac, err := db.AppConfig(context.Background()) + require.NoError(t, err) + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + require.NoError(t, db.SaveAppConfig(context.Background(), ac)) + + // add two hosts to MDM to enforce disk encryption, fleet doesn't enforce settings on centos so h3 is not included + for _, h := range []*fleet.Host{h1, h2} { + require.NoError(t, db.SetOrUpdateMDMData(context.Background(), h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet)) + } + // add disk encryption key for h1 + require.NoError(t, db.SetOrUpdateHostDiskEncryptionKey(context.Background(), h1.ID, "test-key", "", ptr.Bool(true))) + // add disk encryption for h1 + require.NoError(t, db.SetOrUpdateHostDisksEncryption(context.Background(), h1.ID, true)) + + checkHosts := func(t *testing.T, gotHosts []*fleet.Host, expectedIDs []uint) { + require.Len(t, gotHosts, len(expectedIDs)) + for _, h := range gotHosts { + require.Contains(t, expectedIDs, h.ID) + } + } + + // baseline no filter + hosts := listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{}, 3) + checkHosts(t, hosts, []uint{h1.ID, h2.ID, h3.ID}) + + t.Run("os_settings", func(t *testing.T) { + hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1) + checkHosts(t, hosts, []uint{h1.ID}) + hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) + checkHosts(t, hosts, []uint{h2.ID}) + }) + + t.Run("os_settings_disk_encryption", func(t *testing.T) { + hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerified}, 1) + checkHosts(t, hosts, []uint{h1.ID}) + hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsPending}, 1) + checkHosts(t, hosts, []uint{h2.ID}) + }) +} diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 863f9c290..7dd3a696f 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -3,13 +3,16 @@ package mysql import ( "context" "database/sql" + "errors" + "fmt" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/go-kit/kit/log/level" "github.com/jmoiron/sqlx" ) -// MDMWindowsGetEnrolledDevice receives a Windows MDM device id and returns the device information. +// MDMWindowsGetEnrolledDevice receives a Windows MDM HW Device id and returns the device information. func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceHWID string) (*fleet.MDMWindowsEnrolledDevice, error) { stmt := `SELECT mdm_device_id, @@ -36,6 +39,33 @@ func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceH return &winMDMDevice, nil } +// MDMWindowsGetEnrolledDeviceWithDeviceID receives a Windows MDM device id and returns the device information. +func (ds *Datastore) MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) { + stmt := `SELECT + mdm_device_id, + mdm_hardware_id, + device_state, + device_type, + device_name, + enroll_type, + enroll_user_id, + enroll_proto_version, + enroll_client_version, + not_in_oobe, + created_at, + updated_at + FROM mdm_windows_enrollments WHERE mdm_device_id = ?` + + var winMDMDevice fleet.MDMWindowsEnrolledDevice + if err := sqlx.GetContext(ctx, ds.reader(ctx), &winMDMDevice, stmt, mdmDeviceID); err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsGetEnrolledDeviceWithDeviceID").WithMessage(mdmDeviceID)) + } + return nil, ctxerr.Wrap(ctx, err, "get MDMWindowsGetEnrolledDeviceWithDeviceID") + } + return &winMDMDevice, nil +} + // MDMWindowsInsertEnrolledDevice inserts a new MDMWindowsEnrolledDevice in the database func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error { stmt := ` @@ -74,7 +104,8 @@ func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device return nil } -// MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database using the device id. +// MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database +// using the HW Device ID. func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error { stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?" @@ -90,3 +121,202 @@ func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDevi return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice")) } + +// MDMWindowsDeleteEnrolledDeviceWithDeviceID deletes a give MDMWindowsEnrolledDevice entry from the database using the device id. +func (ds *Datastore) MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error { + stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_device_id = ?" + + res, err := ds.writer(ctx).ExecContext(ctx, stmt, mdmDeviceID) + if err != nil { + return ctxerr.Wrap(ctx, err, "delete MDMWindowsDeleteEnrolledDeviceWithDeviceID") + } + + deleted, _ := res.RowsAffected() + if deleted == 1 { + return nil + } + + return ctxerr.Wrap(ctx, notFound("MDMWindowsDeleteEnrolledDeviceWithDeviceID")) +} + +// whereBitLockerStatus returns a string suitable for inclusion within a SQL WHERE clause to filter by +// the given status. The caller is responsible for ensuring the status is valid. In the case of an invalid +// status, the function will return the string "FALSE". The caller should also ensure that the query in +// which this is used joins the following tables with the specified aliases: +// - host_disk_encryption_keys: hdek +// - host_mdm: hmdm +// - host_disks: hd +func (ds *Datastore) whereBitLockerStatus(status fleet.DiskEncryptionStatus) string { + const ( + whereNotServer = `(hmdm.is_server IS NOT NULL AND hmdm.is_server = 0)` + whereKeyAvailable = `(hdek.base64_encrypted IS NOT NULL AND hdek.base64_encrypted != '' AND hdek.decryptable IS NOT NULL AND hdek.decryptable = 1)` + whereEncrypted = `(hd.encrypted IS NOT NULL AND hd.encrypted = 1)` + whereHostDisksUpdated = `(hd.updated_at IS NOT NULL AND hdek.updated_at IS NOT NULL AND hd.updated_at >= hdek.updated_at)` + whereClientError = `(hdek.client_error IS NOT NULL AND hdek.client_error != '')` + withinGracePeriod = `(hdek.updated_at IS NOT NULL AND hdek.updated_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR))` + ) + + // TODO: what if windows sends us a key for an already encrypted volumne? could it get stuck + // in pending or verifying? should we modify SetOrUpdateHostDiskEncryption to ensure that we + // increment the updated_at timestamp on the host_disks table for all encrypted volumes + // host_disks if the hdek timestamp is newer? What about SetOrUpdateHostDiskEncryptionKey? + + switch status { + case fleet.DiskEncryptionVerified: + return whereNotServer + ` +AND NOT ` + whereClientError + ` +AND ` + whereKeyAvailable + ` +AND ` + whereEncrypted + ` +AND ` + whereHostDisksUpdated + + case fleet.DiskEncryptionVerifying: + // Possible verifying scenarios: + // - we have the key and host_disks already encrypted before the key but hasn't been updated yet + // - we have the key and host_disks reported unencrypted during the 1-hour grace period after key was updated + return whereNotServer + ` +AND NOT ` + whereClientError + ` +AND ` + whereKeyAvailable + ` +AND ( + (` + whereEncrypted + ` AND NOT ` + whereHostDisksUpdated + `) + OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `) +)` + + case fleet.DiskEncryptionEnforcing: + // Possible enforcing scenarios: + // - we don't have the key + // - we have the key and host_disks reported unencrypted before the key was updated or outside the 1-hour grace period after key was updated + return whereNotServer + ` +AND NOT ` + whereClientError + ` +AND ( + NOT ` + whereKeyAvailable + ` + OR (` + whereKeyAvailable + ` + AND (NOT ` + whereEncrypted + ` + AND (NOT ` + whereHostDisksUpdated + ` OR NOT ` + withinGracePeriod + `) + ) + ) +)` + + case fleet.DiskEncryptionFailed: + return whereNotServer + ` AND ` + whereClientError + + default: + level.Debug(ds.logger).Log("msg", "unknown bitlocker status", "status", status) + return "FALSE" + } +} + +func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) { + enabled, err := ds.getConfigEnableDiskEncryption(ctx, teamID) + if err != nil { + return nil, err + } + if !enabled { + return &fleet.MDMWindowsBitLockerSummary{}, nil + } + + // Note action_required and removing_enforcement are not applicable to Windows hosts + sqlFmt := ` +SELECT + COUNT(if((%s), 1, NULL)) AS verified, + COUNT(if((%s), 1, NULL)) AS verifying, + 0 AS action_required, + COUNT(if((%s), 1, NULL)) AS enforcing, + COUNT(if((%s), 1, NULL)) AS failed, + 0 AS removing_enforcement +FROM + hosts h + LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id + LEFT JOIN host_mdm hmdm ON h.id = hmdm.host_id + LEFT JOIN host_disks hd ON h.id = hd.host_id +WHERE + h.platform = 'windows' AND hmdm.is_server = 0 AND %s` + + var args []interface{} + teamFilter := "h.team_id IS NULL" + if teamID != nil && *teamID > 0 { + teamFilter = "h.team_id = ?" + args = append(args, *teamID) + } + + var res fleet.MDMWindowsBitLockerSummary + stmt := fmt.Sprintf( + sqlFmt, + ds.whereBitLockerStatus(fleet.DiskEncryptionVerified), + ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying), + ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing), + ds.whereBitLockerStatus(fleet.DiskEncryptionFailed), + teamFilter, + ) + if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, args...); err != nil { + return nil, err + } + + return &res, nil +} + +func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fleet.Host) (*fleet.DiskEncryptionStatus, error) { + if host == nil { + return nil, errors.New("host cannot be nil") + } + + if host.Platform != "windows" { + // Generally, the caller should have already checked this, but just in case we log and + // return nil + level.Debug(ds.logger).Log("msg", "cannot get bitlocker status for non-windows host", "host_id", host.ID) + return nil, nil + } + + if host.MDMInfo != nil && host.MDMInfo.IsServer { + // It is currently expected that server hosts do not have a bitlocker status so we can skip + // the query and return nil. We log for potential debugging in case this changes in the future. + level.Debug(ds.logger).Log("msg", "no bitlocker status for server host", "host_id", host.ID) + return nil, nil + } + + enabled, err := ds.getConfigEnableDiskEncryption(ctx, host.TeamID) + if err != nil { + return nil, err + } + if !enabled { + return nil, nil + } + + // Note action_required and removing_enforcement are not applicable to Windows hosts + stmt := fmt.Sprintf(` +SELECT + CASE + WHEN (%s) THEN '%s' + WHEN (%s) THEN '%s' + WHEN (%s) THEN '%s' + WHEN (%s) THEN '%s' + END AS status +FROM + host_mdm hmdm + LEFT JOIN host_disk_encryption_keys hdek ON hmdm.host_id = hdek.host_id + LEFT JOIN host_disks hd ON hmdm.host_id = hd.host_id +WHERE + hmdm.host_id = ?`, + ds.whereBitLockerStatus(fleet.DiskEncryptionVerified), + fleet.DiskEncryptionVerified, + ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying), + fleet.DiskEncryptionVerifying, + ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing), + fleet.DiskEncryptionEnforcing, + ds.whereBitLockerStatus(fleet.DiskEncryptionFailed), + fleet.DiskEncryptionFailed, + ) + + var des fleet.DiskEncryptionStatus + if err := sqlx.GetContext(ctx, ds.reader(ctx), &des, stmt, host.ID); err != nil { + if err == sql.ErrNoRows { + // At this point we know disk encryption is enabled so if we don't have a record for the + // host then we treat it as enforcing and log for potential debugging + level.Debug(ds.logger).Log("msg", "no bitlocker status found for host", "host_id", host.ID) + des = fleet.DiskEncryptionEnforcing + return &des, nil + } + return nil, err + } + + return &des, nil +} diff --git a/server/datastore/mysql/microsoft_mdm_test.go b/server/datastore/mysql/microsoft_mdm_test.go index 00e2a1526..e1fe97b2c 100644 --- a/server/datastore/mysql/microsoft_mdm_test.go +++ b/server/datastore/mysql/microsoft_mdm_test.go @@ -3,9 +3,15 @@ package mysql import ( "context" // nolint:gosec // used only to hash for efficient comparisons "testing" + "time" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" + "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" "github.com/google/uuid" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" ) @@ -66,4 +72,387 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) { err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) require.ErrorAs(t, err, &nfe) + + // Test using device ID instead of hardware ID + err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) + require.NoError(t, err) + + err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice) + require.ErrorAs(t, err, &ae) + + gotEnrolledDevice, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) + require.NoError(t, err) + require.NotZero(t, gotEnrolledDevice.CreatedAt) + require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID) + require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID) + + err = ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) + require.NoError(t, err) + + _, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID) + require.ErrorAs(t, err, &nfe) + + err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) + require.ErrorAs(t, err, &nfe) +} + +func TestMDMWindowsDiskEncryption(t *testing.T) { + ds := CreateMySQLDS(t) + ctx := context.Background() + + checkBitLockerSummary := func(t *testing.T, teamID *uint, expected fleet.MDMWindowsBitLockerSummary) { + bls, err := ds.GetMDMWindowsBitLockerSummary(ctx, teamID) + require.NoError(t, err) + require.NotNil(t, bls) + require.Equal(t, expected, *bls) + } + + checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) { + gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status}) + require.NoError(t, err) + require.Len(t, gotHosts, len(expectedIDs)) + for _, h := range gotHosts { + require.Contains(t, expectedIDs, h.ID) + } + } + + checkListHostsFilterDiskEncryption := func(t *testing.T, teamID *uint, status fleet.DiskEncryptionStatus, expectedIDs []uint) { + gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsDiskEncryptionFilter: status}) + require.NoError(t, err) + require.Len(t, gotHosts, len(expectedIDs), "status: %s", status) + for _, h := range gotHosts { + require.Contains(t, expectedIDs, h.ID) + } + } + + checkHostBitLockerStatus := func(t *testing.T, expected fleet.DiskEncryptionStatus, hostIDs []uint) { + for _, id := range hostIDs { + h, err := ds.Host(ctx, id) + require.NoError(t, err) + require.NotNil(t, h) + bls, err := ds.GetMDMWindowsBitLockerStatus(ctx, h) + require.NoError(t, err) + require.NotNil(t, bls) + require.Equal(t, expected, *bls) + } + } + + type hostIDsByStatus map[fleet.DiskEncryptionStatus][]uint + + checkExpected := func(t *testing.T, teamID *uint, expected hostIDsByStatus) { + for _, status := range []fleet.DiskEncryptionStatus{ + fleet.DiskEncryptionVerified, + fleet.DiskEncryptionVerifying, + fleet.DiskEncryptionFailed, + fleet.DiskEncryptionEnforcing, + fleet.DiskEncryptionRemovingEnforcement, + fleet.DiskEncryptionActionRequired, + } { + hostIDs, ok := expected[status] + if !ok { + hostIDs = []uint{} + } + checkListHostsFilterDiskEncryption(t, teamID, status, hostIDs) + checkHostBitLockerStatus(t, status, hostIDs) + } + + checkBitLockerSummary(t, teamID, fleet.MDMWindowsBitLockerSummary{ + Verified: uint(len(expected[fleet.DiskEncryptionVerified])), + Verifying: uint(len(expected[fleet.DiskEncryptionVerifying])), + Failed: uint(len(expected[fleet.DiskEncryptionFailed])), + Enforcing: uint(len(expected[fleet.DiskEncryptionEnforcing])), + RemovingEnforcement: uint(len(expected[fleet.DiskEncryptionRemovingEnforcement])), + ActionRequired: uint(len(expected[fleet.DiskEncryptionActionRequired])), + }) + + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, expected[fleet.DiskEncryptionVerified]) + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, expected[fleet.DiskEncryptionVerifying]) + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, expected[fleet.DiskEncryptionFailed]) + var expectedPending []uint + expectedPending = append(expectedPending, expected[fleet.DiskEncryptionEnforcing]...) + expectedPending = append(expectedPending, expected[fleet.DiskEncryptionRemovingEnforcement]...) + expectedPending = append(expectedPending, expected[fleet.DiskEncryptionActionRequired]...) + checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, expectedPending) + } + + updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) { + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?` + _, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID) + return err + }) + } + + setKeyUpdatedAt := func(t *testing.T, hostID uint, keyUpdatedAt time.Time) { + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + stmt := `UPDATE host_disk_encryption_keys SET updated_at = ? where host_id = ?` + _, err := q.ExecContext(ctx, stmt, keyUpdatedAt, hostID) + return err + }) + } + + // Create some hosts + var hosts []*fleet.Host + for i := 0; i < 10; i++ { + p := "windows" + if i >= 5 { + p = "darwin" + } + u := uuid.New().String() + h, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: &u, + UUID: u, + Hostname: u, + Platform: p, + }) + require.NoError(t, err) + require.NotNil(t, h) + hosts = append(hosts, h) + + require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet)) + } + + t.Run("Disk encryption disabled", func(t *testing.T) { + ac, err := ds.AppConfig(ctx) + require.NoError(t, err) + require.False(t, ac.MDM.EnableDiskEncryption.Value) + + checkExpected(t, nil, hostIDsByStatus{}) // no hosts are counted because disk encryption is not enabled + }) + + t.Run("Disk encryption enabled", func(t *testing.T) { + ac, err := ds.AppConfig(ctx) + require.NoError(t, err) + ac.MDM.EnableDiskEncryption = optjson.SetBool(true) + require.NoError(t, ds.SaveAppConfig(ctx, ac)) + ac, err = ds.AppConfig(ctx) + require.NoError(t, err) + require.True(t, ac.MDM.EnableDiskEncryption.Value) + + t.Run("Bitlocker enforcing status", func(t *testing.T) { + // all windows hosts are counted as enforcing because they have not reported any disk encryption status yet + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, + }) + + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true))) + checkExpected(t, nil, hostIDsByStatus{ + // status is still pending because hosts_disks hasn't been updated yet + fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, + }) + + require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, + fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, + }) + + cases := []struct { + name string + hostDisksEncrypted bool + reportedAfterKey bool + expectedWithinGracePeriod fleet.DiskEncryptionStatus + expectedOutsideGracePeriod fleet.DiskEncryptionStatus + }{ + { + name: "encrypted reported after key", + hostDisksEncrypted: true, + reportedAfterKey: true, + expectedWithinGracePeriod: fleet.DiskEncryptionVerified, + expectedOutsideGracePeriod: fleet.DiskEncryptionVerified, + }, + { + name: "encrypted reported before key", + hostDisksEncrypted: true, + reportedAfterKey: false, + expectedWithinGracePeriod: fleet.DiskEncryptionVerifying, + expectedOutsideGracePeriod: fleet.DiskEncryptionVerifying, + }, + { + name: "not encrypted reported before key", + hostDisksEncrypted: false, + reportedAfterKey: false, + expectedWithinGracePeriod: fleet.DiskEncryptionEnforcing, + expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing, + }, + { + name: "not encrypted reported after key", + hostDisksEncrypted: false, + reportedAfterKey: true, + expectedWithinGracePeriod: fleet.DiskEncryptionVerifying, + expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing, + }, + } + + testHostID := hosts[0].ID + otherWindowsHostIDs := []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID} + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var keyUpdatedAt, hostDisksUpdatedAt time.Time + + t.Run("within grace period", func(t *testing.T) { + expected := make(hostIDsByStatus) + if c.expectedWithinGracePeriod == fleet.DiskEncryptionEnforcing { + expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...) + } else { + expected[c.expectedWithinGracePeriod] = []uint{testHostID} + expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs + } + + keyUpdatedAt = time.Now().Add(-10 * time.Minute) + setKeyUpdatedAt(t, testHostID, keyUpdatedAt) + + if c.reportedAfterKey { + hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute) + } else { + hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute) + } + updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt) + + checkExpected(t, nil, expected) + }) + + t.Run("outside grace period", func(t *testing.T) { + expected := make(hostIDsByStatus) + if c.expectedOutsideGracePeriod == fleet.DiskEncryptionEnforcing { + expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...) + } else { + expected[c.expectedOutsideGracePeriod] = []uint{testHostID} + expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs + } + + keyUpdatedAt = time.Now().Add(-2 * time.Hour) + setKeyUpdatedAt(t, testHostID, keyUpdatedAt) + + if c.reportedAfterKey { + hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute) + } else { + hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute) + } + updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt) + + checkExpected(t, nil, expected) + }) + }) + } + }) + + // ensure hosts[0] is set to verified for the rest of the tests + require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true))) + require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true)) + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, + fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}, + }) + + t.Run("BitLocker failed status", func(t *testing.T) { + // TODO: Update test to use methods to set windows disk encryption when they are implemented + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, + `INSERT INTO host_disk_encryption_keys (host_id, decryptable, client_error) VALUES (?, ?, ?)`, + hosts[1].ID, + false, + "test-error") + return err + }) + + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, + fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, + fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID}, + }) + }) + + t.Run("BitLocker team filtering", func(t *testing.T) { + // Test team filtering + team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"}) + require.NoError(t, err) + + tm, err := ds.Team(ctx, team.ID) + require.NoError(t, err) + require.NotNil(t, tm) + require.False(t, tm.Config.MDM.EnableDiskEncryption) // disk encryption is not enabled for team + + // Transfer hosts[2] to the team + require.NoError(t, ds.AddHostsToTeam(ctx, &team.ID, []uint{hosts[2].ID})) + + // Check the summary for the team + checkExpected(t, &team.ID, hostIDsByStatus{}) // disk encryption is not enabled for team so hosts[2] is not counted + + // Check the summary for no team + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, + fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, + fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID}, // hosts[2] is no longer included in the no team summary + }) + + // Enable disk encryption for the team + tm.Config.MDM.EnableDiskEncryption = true + tm, err = ds.SaveTeam(ctx, tm) + require.NoError(t, err) + require.NotNil(t, tm) + require.True(t, tm.Config.MDM.EnableDiskEncryption) + + // Check the summary for the team + checkExpected(t, &team.ID, hostIDsByStatus{ + fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID}, // disk encryption is enabled for team so hosts[2] is counted + }) + + // Check the summary for no team (should be unchanged) + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, + fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, + fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID}, + }) + }) + + t.Run("BitLocker Windows server excluded", func(t *testing.T) { + require.NoError(t, ds.SetOrUpdateMDMData(ctx, + hosts[3].ID, + true, // set is_server to true for hosts[3] + true, "https://example.com", false, fleet.WellKnownMDMFleet)) + + // Check Windows servers not counted + checkExpected(t, nil, hostIDsByStatus{ + fleet.DiskEncryptionVerified: []uint{hosts[0].ID}, + fleet.DiskEncryptionFailed: []uint{hosts[1].ID}, + fleet.DiskEncryptionEnforcing: []uint{hosts[4].ID}, // hosts[3] is not counted + }) + }) + + t.Run("OS settings filters include Windows and macOS hosts", func(t *testing.T) { + // Make macOS host fail disk encryption + require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ + { + HostUUID: hosts[5].UUID, + ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier, + ProfileName: "Disk encryption", + ProfileID: 1, + CommandUUID: uuid.New().String(), + OperationType: fleet.MDMAppleOperationTypeInstall, + Status: &fleet.MDMAppleDeliveryFailed, + Checksum: []byte("checksum"), + }, + })) + + // Check that BitLocker summary does not include macOS hosts + checkBitLockerSummary(t, nil, fleet.MDMWindowsBitLockerSummary{ + Verified: 1, + Verifying: 0, + Failed: 1, + Enforcing: 1, + RemovingEnforcement: 0, + ActionRequired: 0, + }) + + // Check that filtered lists do include macOS hosts + checkListHostsFilterDiskEncryption(t, nil, fleet.DiskEncryptionFailed, []uint{hosts[1].ID, hosts[5].ID}) + checkListHostsFilterOSSettings(t, nil, fleet.OSSettingsFailed, []uint{hosts[1].ID, hosts[5].ID}) + }) + }) } diff --git a/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go b/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go new file mode 100644 index 000000000..793432c20 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting.go @@ -0,0 +1,32 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20230918221115, Down_20230918221115) +} + +func Up_20230918221115(tx *sql.Tx) error { + stmt := ` +UPDATE teams +SET + config = JSON_SET(config, '$.mdm.enable_disk_encryption', + JSON_EXTRACT(config, '$.mdm.macos_settings.enable_disk_encryption')), + config = JSON_REMOVE(config, '$.mdm.macos_settings.enable_disk_encryption') +WHERE + JSON_EXTRACT(config, '$.mdm.macos_settings.enable_disk_encryption') IS NOT NULL; + ` + + if _, err := tx.Exec(stmt); err != nil { + return fmt.Errorf("move team mdm.macos_settings.enable_disk_encryption setting to mdm.enable_disk_encryption: %w", err) + } + + return nil +} + +func Down_20230918221115(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go b/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go new file mode 100644 index 000000000..90538968f --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230918221115_MoveDiskEncryptionSetting_test.go @@ -0,0 +1,67 @@ +package tables + +import ( + "encoding/json" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestUp_20230918221115(t *testing.T) { + db := applyUpToPrev(t) + + dataStmts := ` + INSERT INTO teams VALUES + (1,'2023-07-21 20:32:42','Team 1','','{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": true}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"agent_options\": {\"config\": {\"options\": {\"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"webhook_settings\": {\"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}}'), + (2,'2023-07-21 20:32:47','Team 2','','{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": true}}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": true}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"agent_options\": {\"config\": {\"options\": {\"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"webhook_settings\": {\"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}}'); + ` + _, err := db.Exec(dataStmts) + require.NoError(t, err) + + var rawConfigs []json.RawMessage + err = sqlx.Select(db, &rawConfigs, "SELECT config FROM teams ORDER BY id") + require.NoError(t, err) + + var wantConfigs []map[string]any + for _, c := range rawConfigs { + var wantConfig map[string]any + err = json.Unmarshal(c, &wantConfig) + require.NoError(t, err) + wantConfigs = append(wantConfigs, wantConfig) + } + + applyNext(t, db) + + rawConfigs = []json.RawMessage{} + err = sqlx.Select(db, &rawConfigs, "SELECT JSON_EXTRACT(config, '$') FROM teams ORDER BY id") + require.NoError(t, err) + + var gotConfigs []map[string]any + for _, c := range rawConfigs { + var gotConfig map[string]any + err = json.Unmarshal(c, &gotConfig) + require.NoError(t, err) + gotConfigs = append(gotConfigs, gotConfig) + } + + // simulate the ideal behavior with the oldConfigs + for i, config := range wantConfigs { + if mdmMap, ok := config["mdm"].(map[string]interface{}); ok { + // Delete 'mdm.macos_settings.enable_disk_encryption' + if macosSettings, ok := mdmMap["macos_settings"].(map[string]interface{}); ok { + delete(macosSettings, "enable_disk_encryption") + } + + // Set 'mdm.enable_disk_encryption' + if i == 0 { + mdmMap["enable_disk_encryption"] = false + } else { + mdmMap["enable_disk_encryption"] = true + } + } + wantConfigs[i] = config + } + + require.ElementsMatch(t, wantConfigs, gotConfigs) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 93c1e2703..932d1d9a2 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -40,7 +40,7 @@ CREATE TABLE `app_config_json` ( UNIQUE KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null, \"enable_disk_encryption\": false}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"deferred_save_host\": false, \"live_query_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `carve_blocks` ( @@ -685,9 +685,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=209 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=210 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20230918221115,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/app.go b/server/fleet/app.go index 6a7719073..17016a7e3 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -13,7 +13,9 @@ import ( "time" "github.com/fleetdm/fleet/v4/pkg/optjson" + "github.com/fleetdm/fleet/v4/pkg/rawjson" "github.com/fleetdm/fleet/v4/server/config" + "github.com/fleetdm/fleet/v4/server/ptr" ) // SMTP settings names returned from API, these map to SMTPAuthType and @@ -157,12 +159,23 @@ type MDM struct { // with the similarly named macOS-specific fields. WindowsEnabledAndConfigured bool `json:"windows_enabled_and_configured"` + EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"` + ///////////////////////////////////////////////////////////////// // WARNING: If you add to this struct make sure it's taken into // account in the AppConfig Clone implementation! ///////////////////////////////////////////////////////////////// } +// AtLeastOnePlatformEnabledAndConfigured returns true if at least one supported platform +// (macOS or Windows) has MDM enabled and configured. +func (m MDM) AtLeastOnePlatformEnabledAndConfigured() bool { + // explicitly check for the feature flag to account for the edge case of: + // 1. FF enabled, windows is turned on + // 2. FF disabled on server restart + return m.EnabledAndConfigured || (config.IsMDMFeatureFlagEnabled() && m.WindowsEnabledAndConfigured) +} + // versionStringRegex is used to validate that a version string is in the x.y.z // format only (no prerelease or build metadata). var versionStringRegex = regexp.MustCompile(`^\d+(\.\d+)?(\.\d+)?$`) @@ -222,10 +235,8 @@ type MacOSSettings struct { // // NOTE: These are only present here for informational purposes. // (The source of truth for profiles is in MySQL.) - CustomSettings []string `json:"custom_settings"` - // EnableDiskEncryption enables disk encryption on hosts such that the hosts' - // disk encryption keys will be stored in Fleet. - EnableDiskEncryption bool `json:"enable_disk_encryption"` + CustomSettings []string `json:"custom_settings"` + DeprecatedEnableDiskEncryption *bool `json:"enable_disk_encryption,omitempty"` // NOTE: make sure to update the ToMap/FromMap methods when adding/updating fields. } @@ -233,7 +244,7 @@ type MacOSSettings struct { func (s MacOSSettings) ToMap() map[string]interface{} { return map[string]interface{}{ "custom_settings": s.CustomSettings, - "enable_disk_encryption": s.EnableDiskEncryption, + "enable_disk_encryption": s.DeprecatedEnableDiskEncryption, } } @@ -274,11 +285,11 @@ func (s *MacOSSettings) FromMap(m map[string]interface{}) (map[string]bool, erro // error, must be a bool return nil, &json.UnmarshalTypeError{ Value: fmt.Sprintf("%T", v), - Type: reflect.TypeOf(s.EnableDiskEncryption), + Type: reflect.TypeOf(s.DeprecatedEnableDiskEncryption).Elem(), Field: "macos_settings.enable_disk_encryption", } } - s.EnableDiskEncryption = b + s.DeprecatedEnableDiskEncryption = ptr.Bool(b) } return set, nil @@ -344,7 +355,8 @@ type AppConfig struct { SMTPSettings *SMTPSettings `json:"smtp_settings,omitempty"` HostExpirySettings HostExpirySettings `json:"host_expiry_settings"` // Features allows to globally enable or disable features - Features Features `json:"features"` + Features Features `json:"features"` + DeprecatedHostSettings *Features `json:"host_settings,omitempty"` // AgentOptions holds osquery configuration. // // This field is a pointer to avoid returning this information to non-global-admins. @@ -392,12 +404,6 @@ func (c *AppConfig) Obfuscate() { } } -// legacyConfig holds settings that have been replaced, superceded or -// deprecated by other AppConfig settings. -type legacyConfig struct { - HostSettings *Features `json:"host_settings"` -} - // Clone implements cloner. func (c *AppConfig) Clone() (interface{}, error) { return c.Copy(), nil @@ -509,6 +515,31 @@ func (e *EnrichedAppConfig) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaler interface to make sure we serialize +// both AppConfig and enrichedAppConfigFields properly: +// +// - If this function is not defined, AppConfig.MarshalJSON gets promoted and +// will be called instead. +// - If we try to unmarshal everything in one go, AppConfig.MarshalJSON doesn't get +// called. +func (e *EnrichedAppConfig) MarshalJSON() ([]byte, error) { + // Marshal only the enriched fields + enrichedData, err := json.Marshal(e.enrichedAppConfigFields) + if err != nil { + return nil, err + } + + // Marshal the base AppConfig + appConfigData, err := json.Marshal(e.AppConfig) + if err != nil { + return nil, err + } + + // we need to marshal and combine both groups separately because + // AppConfig has a custom marshaler. + return rawjson.CombineRoots(enrichedData, appConfigData) +} + type Duration struct { time.Duration } @@ -628,16 +659,13 @@ func (c *AppConfig) DidUnmarshalLegacySettings() []string { return c.didUnmarsha func (c *AppConfig) UnmarshalJSON(b []byte) error { // Define a new type, this is to prevent infinite recursion when // unmarshalling the AppConfig struct. - type cfgStructUnmarshal AppConfig + type aliasConfig AppConfig compatConfig := struct { - *legacyConfig - *cfgStructUnmarshal + *aliasConfig }{ - &legacyConfig{}, - (*cfgStructUnmarshal)(c), + (*aliasConfig)(c), } - c.didUnmarshalLegacySettings = nil decoder := json.NewDecoder(bytes.NewReader(b)) if c.strictDecoding { decoder.DisallowUnknownFields() @@ -649,16 +677,56 @@ func (c *AppConfig) UnmarshalJSON(b []byte) error { return errors.New("unexpected extra tokens found in config") } + c.assignDeprecatedFields() + + return nil +} + +func (c AppConfig) MarshalJSON() ([]byte, error) { + // Define a new type, this is to prevent infinite recursion when + // marshalling the AppConfig struct. + c.assignDeprecatedFields() + + // requirements are that if this value is not set, defaults to false. + // The default mashaler of optjson.Bool will convert this to `null` if + // it's not valid. + if !c.MDM.EnableDiskEncryption.Valid { + c.MDM.EnableDiskEncryption = optjson.SetBool(false) + } + + type aliasConfig AppConfig + aa := aliasConfig(c) + return json.Marshal(aa) +} + +func (c *AppConfig) assignDeprecatedFields() { + c.didUnmarshalLegacySettings = nil // Define and assign legacy settings to new fields. // This has the drawback of legacy fields taking precedence over new fields // if both are defined. - if compatConfig.legacyConfig.HostSettings != nil { + // + // TODO: with optjson + the new approach we're using to handle legacy + // fields, legacy fields don't have to take precedence over new fields. + // Is it worth changing this behavior for `host_settings`/`features` at this point? + if c.DeprecatedHostSettings != nil { c.didUnmarshalLegacySettings = append(c.didUnmarshalLegacySettings, "host_settings") - c.Features = *compatConfig.legacyConfig.HostSettings + c.Features = *c.DeprecatedHostSettings } - sort.Strings(c.didUnmarshalLegacySettings) - return nil + // if disk encryption is not set in the root config + // try to read the value from the legacy config + if !c.MDM.EnableDiskEncryption.Valid { + if c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption != nil { + c.didUnmarshalLegacySettings = append(c.didUnmarshalLegacySettings, "mdm.macos_settings.enable_disk_encryption") + c.MDM.EnableDiskEncryption = optjson.SetBool(*c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption) + } + } + + // ensure the legacy configs are always nil + c.DeprecatedHostSettings = nil + c.MDM.MacOSSettings.DeprecatedEnableDiskEncryption = nil + + sort.Strings(c.didUnmarshalLegacySettings) } // OrgInfo contains general info about the organization using Fleet. diff --git a/server/fleet/app_test.go b/server/fleet/app_test.go index 268eb2277..bc7a4173d 100644 --- a/server/fleet/app_test.go +++ b/server/fleet/app_test.go @@ -1,6 +1,7 @@ package fleet import ( + "encoding/json" "testing" "github.com/fleetdm/fleet/v4/pkg/optjson" @@ -159,3 +160,154 @@ func TestMacOSMigrationModeIsValid(t *testing.T) { require.False(t, (MacOSMigrationMode("")).IsValid()) require.False(t, (MacOSMigrationMode("foo")).IsValid()) } + +func TestAppConfigDeprecatedFields(t *testing.T) { + cases := []struct { + msg string + in json.RawMessage + wantFeatures Features + wantDiskEncryption bool + }{ + {"both empty", json.RawMessage(`{}`), Features{}, false}, + {"only one feature set", json.RawMessage(`{"host_settings": {"enable_host_users": true}}`), Features{EnableHostUsers: true}, false}, + { + "a feature and disk encryption set", + json.RawMessage(`{"host_settings": {"enable_host_users": true}, "mdm": {"macos_settings": {"enable_disk_encryption": true}}}`), + Features{EnableHostUsers: true}, + true, + }, + { + "features legacy and new setting set", + json.RawMessage(`{"host_settings": {"enable_host_users": true}, "features": {"enable_host_users": false}}`), + Features{EnableHostUsers: true}, + false, + }, + { + "disk encryption legacy and new setting set", + json.RawMessage(`{"mdm": {"enable_disk_encryption": false, "macos_settings": {"enable_disk_encryption": true}}}`), + Features{}, + false, + }, + } + + for _, c := range cases { + t.Run(c.msg, func(t *testing.T) { + ac := AppConfig{} + err := json.Unmarshal(c.in, &ac) + require.NoError(t, err) + require.Nil(t, ac.DeprecatedHostSettings) + require.Nil(t, ac.MDM.MacOSSettings.DeprecatedEnableDiskEncryption) + require.Equal(t, c.wantFeatures, ac.Features) + require.Equal(t, c.wantDiskEncryption, ac.MDM.EnableDiskEncryption.Value) + + // marshalling the fields again doesn't contain deprecated fields + acJSON, err := json.Marshal(ac) + require.NoError(t, err) + var resultMap map[string]interface{} + err = json.Unmarshal(acJSON, &resultMap) + require.NoError(t, err) + + // host_settings is not present + _, exists := resultMap["host_settings"] + require.False(t, exists) + + // mdm.macos_settings.enable_disk_encryption is not present + mdm, ok := resultMap["mdm"].(map[string]interface{}) + require.True(t, ok) + macosSettings, ok := mdm["macos_settings"].(map[string]interface{}) + require.True(t, ok) + _, exists = macosSettings["enable_disk_encryption"] + require.False(t, exists) + + diskEncryption, exists := mdm["enable_disk_encryption"] + require.True(t, exists) + require.EqualValues(t, c.wantDiskEncryption, diskEncryption) + + }) + } + +} + +func TestAtLeastOnePlatformEnabledAndConfigured(t *testing.T) { + tests := []struct { + name string + macOSEnabledAndConfigured bool + windowsEnabledAndConfigured bool + isMDMFeatureFlagEnabled bool + expectedResult bool + }{ + { + name: "None enabled, feature flag disabled", + macOSEnabledAndConfigured: false, + windowsEnabledAndConfigured: false, + isMDMFeatureFlagEnabled: false, + expectedResult: false, + }, + { + name: "MacOS enabled, feature flag disabled", + macOSEnabledAndConfigured: true, + windowsEnabledAndConfigured: false, + isMDMFeatureFlagEnabled: false, + expectedResult: true, + }, + { + name: "Windows enabled, feature flag disabled", + macOSEnabledAndConfigured: false, + windowsEnabledAndConfigured: true, + isMDMFeatureFlagEnabled: false, + expectedResult: false, + }, + { + name: "Both enabled, feature flag disabled", + macOSEnabledAndConfigured: true, + windowsEnabledAndConfigured: true, + isMDMFeatureFlagEnabled: false, + expectedResult: true, + }, + { + name: "None enabled, feature flag enabled", + macOSEnabledAndConfigured: false, + windowsEnabledAndConfigured: false, + isMDMFeatureFlagEnabled: true, + expectedResult: false, + }, + { + name: "MacOS enabled, feature flag enabled", + macOSEnabledAndConfigured: true, + windowsEnabledAndConfigured: false, + isMDMFeatureFlagEnabled: true, + expectedResult: true, + }, + { + name: "Windows enabled, feature flag enabled", + macOSEnabledAndConfigured: false, + windowsEnabledAndConfigured: true, + isMDMFeatureFlagEnabled: true, + expectedResult: true, + }, + { + name: "Both enabled, feature flag enabled", + macOSEnabledAndConfigured: true, + windowsEnabledAndConfigured: true, + isMDMFeatureFlagEnabled: true, + expectedResult: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.isMDMFeatureFlagEnabled { + t.Setenv("FLEET_DEV_MDM_ENABLED", "1") + } else { + t.Setenv("FLEET_DEV_MDM_ENABLED", "0") + } + + mdm := MDM{ + EnabledAndConfigured: test.macOSEnabledAndConfigured, + WindowsEnabledAndConfigured: test.windowsEnabledAndConfigured, + } + result := mdm.AtLeastOnePlatformEnabledAndConfigured() + require.Equal(t, test.expectedResult, result) + }) + } +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 13db70dc9..2aa92b13d 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -694,12 +694,12 @@ type Datastore interface { SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for // a host - SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string) error + SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error // GetUnverifiedDiskEncryptionKeys returns all the encryption keys that // are collected but their decryptable status is not known yet (ie: // we're able to decrypt the key using a private key in the server) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]HostDiskEncryptionKey, error) - // SetHostDiskEncryptionKeyStatus sets the encryptable status for the set + // SetHostsDiskEncryptionKeyStatus sets the encryptable status for the set // of encription keys provided SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error // GetHostDiskEncryptionKey returns the encryption key information for a given host @@ -1023,12 +1023,26 @@ type Datastore interface { // WSTEPAssociateCertHash associates a certificate hash with a device. WSTEPAssociateCertHash(ctx context.Context, deviceUUID string, hash string) error - // MDMWindowsGetEnrolledDevice receives a Windows MDM device id and returns the device information. + // MDMWindowsGetEnrolledDevice receives a Windows MDM HW device id and returns the device information. MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceID string) (*MDMWindowsEnrolledDevice, error) // MDMWindowsInsertEnrolledDevice inserts a new MDMWindowsEnrolledDevice in the database MDMWindowsInsertEnrolledDevice(ctx context.Context, device *MDMWindowsEnrolledDevice) error - // MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database using the device id. + // MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database using the HW device id. MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceID string) error + // MDMWindowsGetEnrolledDeviceWithDeviceID receives a Windows MDM device id and returns the device information + MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*MDMWindowsEnrolledDevice, error) + // MDMWindowsDeleteEnrolledDeviceWithDeviceID deletes a give MDMWindowsEnrolledDevice entry from the database using the device id + MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error + + // GetMDMWindowsBitLockerSummary summarizes the current state of Windows disk encryption on + // each Windows host in the specified team (or, if no team is specified, each host that is not assigned + // to any team). + GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*MDMWindowsBitLockerSummary, error) + // GetMDMWindowsBitLockerStatus returns the disk encryption status for a given host + // + // Note that the returned status will be nil if the host is reported to be a Windows + // server or if disk encryption is disabled for the host's team (or no team, as applicable). + GetMDMWindowsBitLockerStatus(ctx context.Context, host *Host) (*DiskEncryptionStatus, error) /////////////////////////////////////////////////////////////////////////////// // Host Script Results diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 87a401f71..9e22fa92c 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -53,20 +53,20 @@ const ( MDMEnrollStatusEnrolled = MDMEnrollStatus("enrolled") // combination of "manual" and "automatic" ) -// MacOSSettingsStatus defines the possible statuses of the host's macOS settings, which is derived from the -// status of MDM configuration profiles applied to the host. -type MacOSSettingsStatus string +// OSSettingsStatus defines the possible statuses of the host's OS settings, which is derived from the +// status of MDM configuration profiles and non-profile settings applied the host. +type OSSettingsStatus string const ( - MacOSSettingsVerified MacOSSettingsStatus = "verified" - MacOSSettingsVerifying MacOSSettingsStatus = "verifying" - MacOSSettingsPending MacOSSettingsStatus = "pending" - MacOSSettingsFailed MacOSSettingsStatus = "failed" + OSSettingsVerified OSSettingsStatus = "verified" + OSSettingsVerifying OSSettingsStatus = "verifying" + OSSettingsPending OSSettingsStatus = "pending" + OSSettingsFailed OSSettingsStatus = "failed" ) -func (s MacOSSettingsStatus) IsValid() bool { +func (s OSSettingsStatus) IsValid() bool { switch s { - case MacOSSettingsFailed, MacOSSettingsPending, MacOSSettingsVerifying, MacOSSettingsVerified: + case OSSettingsFailed, OSSettingsPending, OSSettingsVerifying, OSSettingsVerified: return true default: return false @@ -139,12 +139,19 @@ type HostListOptions struct { // MacOSSettingsFilter filters the hosts by the status of MDM configuration profiles // applied to the hosts. - MacOSSettingsFilter MacOSSettingsStatus + MacOSSettingsFilter OSSettingsStatus // MacOSSettingsDiskEncryptionFilter filters the hosts by the status of the disk encryption // MDM profile. MacOSSettingsDiskEncryptionFilter DiskEncryptionStatus + // OSSettingsFilter filters the hosts by the status of MDM configuration profiles and + // non-profile settings applied to the hosts. + OSSettingsFilter OSSettingsStatus + // OSSettingsDiskEncryptionFilter filters the hosts by the status of the disk encryption + // OS setting. + OSSettingsDiskEncryptionFilter DiskEncryptionStatus + // MDMBootstrapPackageFilter filters the hosts by the status of the MDM bootstrap package. MDMBootstrapPackageFilter *MDMBootstrapPackageStatus @@ -186,7 +193,9 @@ func (h HostListOptions) Empty() bool { h.MDMNameFilter == nil && h.MDMEnrollmentStatusFilter == "" && h.MunkiIssueIDFilter == nil && - h.LowDiskSpaceFilter == nil + h.LowDiskSpaceFilter == nil && + h.OSSettingsFilter == "" && + h.OSSettingsDiskEncryptionFilter == "" } type HostUser struct { @@ -336,6 +345,12 @@ type MDMHostData struct { // gets filled. rawDecryptable *int + // OSSettings contains information related to operating systems settings that are managed for + // MDM-enrolled hosts. + // + // Note: Additional information for macOS hosts is currently stored in MacOSSettings. + OSSettings *HostMDMOSSettings `json:"os_settings,omitempty" db:"-" csv:"-"` + // Profiles is a list of HostMDMProfiles for the host. Note that as for many // other host fields, it is not filled in by all host-returning datastore methods. // @@ -358,6 +373,14 @@ type MDMHostData struct { MacOSSetup *HostMDMMacOSSetup `json:"macos_setup,omitempty" db:"-" csv:"-"` } +type HostMDMOSSettings struct { + DiskEncryption HostMDMDiskEncryption `json:"disk_encryption" db:"-" csv:"-"` +} + +type HostMDMDiskEncryption struct { + Status *DiskEncryptionStatus `json:"status" db:"-" csv:"-"` +} + type DiskEncryptionStatus string const ( @@ -411,11 +434,11 @@ type HostMDMMacOSSetup struct { BootstrapPackageName string `db:"bootstrap_package_name" json:"bootstrap_package_name" csv:"-"` } -// DetermineDiskEncryptionStatus determines the disk encryption status for the +// DetermineMacOSDiskEncryptionStatus determines the disk encryption status for the // host based on the file-vault profile in its list of profiles and whether its // disk encryption key is available and decryptable. The file-vault profile // identifier is received as argument to avoid a circular dependency. -func (d *MDMHostData) DetermineDiskEncryptionStatus(profiles []HostMDMAppleProfile, fileVaultIdentifier string) { +func (d *MDMHostData) DetermineMacOSDiskEncryptionStatus(profiles []HostMDMAppleProfile, fileVaultIdentifier string) { var settings MDMHostMacOSSettings var fvprof *HostMDMAppleProfile @@ -577,6 +600,24 @@ func (h *Host) IsEligibleForWindowsMDMUnenrollment() bool { (h.MDMInfo == nil || !h.MDMInfo.IsServer) } +// IsEligibleForBitLockerEncryption checks if the host needs to enforce disk +// encryption using Fleet MDM features. +// +// Note: the *Host structs needs disk encryption data and MDM data filled in to +// perform the check. +func (h *Host) IsEligibleForBitLockerEncryption() bool { + isServer := h.MDMInfo != nil && h.MDMInfo.IsServer + isWindows := h.FleetPlatform() == "windows" + needsEncryption := h.DiskEncryptionEnabled != nil && !*h.DiskEncryptionEnabled + encryptedWithoutKey := h.DiskEncryptionEnabled != nil && *h.DiskEncryptionEnabled && !h.MDM.EncryptionKeyAvailable + + return isWindows && + h.IsOsqueryEnrolled() && + h.MDMInfo.IsFleetEnrolled() && + !isServer && + (needsEncryption || encryptedWithoutKey) +} + // DisplayName returns ComputerName if it isn't empty. Otherwise, it returns Hostname if it isn't // empty. If Hostname is empty and both HardwareSerial and HardwareModel are not empty, it returns a // composite string with HardwareModel and HardwareSerial. If all else fails, it returns an empty @@ -829,6 +870,9 @@ func (h *HostMDM) IsManualFleetEnrolled() bool { // it is in enrolled state for Fleet MDM, regardless of automatic or manual // enrollment method. func (h *HostMDM) IsFleetEnrolled() bool { + if h == nil { + return false + } return h.IsDEPFleetEnrolled() || h.IsManualFleetEnrolled() } diff --git a/server/fleet/hosts_test.go b/server/fleet/hosts_test.go index 2458e8d7f..2bba30a7d 100644 --- a/server/fleet/hosts_test.go +++ b/server/fleet/hosts_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/WatchBeam/clock" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -240,3 +241,53 @@ func TestIsDEPCapable(t *testing.T) { require.Equal(t, tc.expected, tc.hostMDM.IsDEPCapable()) } } + +func TestIsEligibleForBitLockerEncryption(t *testing.T) { + require.False(t, (&Host{}).IsEligibleForBitLockerEncryption()) + + hostThatNeedsEnforcement := Host{ + Platform: "windows", + OsqueryHostID: ptr.String("test"), + MDMInfo: &HostMDM{ + Name: WellKnownMDMFleet, + Enrolled: true, + IsServer: false, + InstalledFromDep: true, + }, + MDM: MDMHostData{ + EncryptionKeyAvailable: false, + }, + DiskEncryptionEnabled: ptr.Bool(false), + } + require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + + // macOS hosts are not elegible + hostThatNeedsEnforcement.Platform = "darwin" + require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + hostThatNeedsEnforcement.Platform = "windows" + require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + + // hosts with disk encryption already enabled are elegible only if we + // can't decrypt the key + hostThatNeedsEnforcement.DiskEncryptionEnabled = ptr.Bool(true) + require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + hostThatNeedsEnforcement.MDM.EncryptionKeyAvailable = true + require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + + hostThatNeedsEnforcement.DiskEncryptionEnabled = ptr.Bool(false) + hostThatNeedsEnforcement.MDM.EncryptionKeyAvailable = false + require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + + // hosts without MDMinfo are not elegible + oldMDMInfo := hostThatNeedsEnforcement.MDMInfo + hostThatNeedsEnforcement.MDMInfo = nil + require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + hostThatNeedsEnforcement.MDMInfo = oldMDMInfo + require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + + // hosts that are not enrolled in MDM are not elegible + hostThatNeedsEnforcement.MDMInfo.Enrolled = false + require.False(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) + hostThatNeedsEnforcement.MDMInfo.Enrolled = true + require.True(t, hostThatNeedsEnforcement.IsEligibleForBitLockerEncryption()) +} diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 9bac69829..9a68a6d6b 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -135,3 +135,16 @@ type HostMDMProfileRetryCount struct { ProfileIdentifier string `db:"profile_identifier"` Retries uint `db:"retries"` } + +type MDMPlatformsCounts struct { + MacOS uint `db:"macos" json:"macos"` + Windows uint `db:"windows" json:"windows"` +} +type MDMDiskEncryptionSummary struct { + Verified MDMPlatformsCounts `db:"verified" json:"verified"` + Verifying MDMPlatformsCounts `db:"verifying" json:"verifying"` + ActionRequired MDMPlatformsCounts `db:"action_required" json:"action_required"` + Enforcing MDMPlatformsCounts `db:"enforcing" json:"enforcing"` + Failed MDMPlatformsCounts `db:"failed" json:"failed"` + RemovingEnforcement MDMPlatformsCounts `db:"removing_enforcement" json:"removing_enforcement"` +} diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index 77ecd683f..b8c2b2fb6 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -29,6 +29,10 @@ type OrbitConfigNotifications struct { // execution on that host. The scripts pending execution are those that // haven't received a result yet. PendingScriptExecutionIDs []string `json:"pending_script_execution_ids,omitempty"` + + // EnforceBitLockerEncryption is sent as true if Windows MDM is + // enabled and the device should encrypt its disk volumes with BitLocker. + EnforceBitLockerEncryption bool `json:"enforce_bitlocker_encryption,omitempty"` } type OrbitConfig struct { @@ -81,3 +85,9 @@ func (es *Extensions) FilterByHostPlatform(hostPlatform string) { } } } + +// OrbitHostDiskEncryptionKeyPayload contains the disk encryption key for a host. +type OrbitHostDiskEncryptionKeyPayload struct { + EncryptionKey []byte `json:"encryption_key"` + ClientError string `json:"client_error"` +} diff --git a/server/fleet/service.go b/server/fleet/service.go index 9554c7815..b9cb37544 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -711,6 +711,11 @@ type Service interface { // error can be raised to the user. VerifyMDMWindowsConfigured(ctx context.Context) error + // VerifyMDMAppleOrWindowsConfigured verifies that the server is configured + // for either Apple or Windows MDM. If an error is returned, authorization is + // skipped so the error can be raised to the user. + VerifyMDMAppleOrWindowsConfigured(ctx context.Context) error + MDMAppleUploadBootstrapPackage(ctx context.Context, name string, pkg io.Reader, teamID uint) error GetMDMAppleBootstrapPackageBytes(ctx context.Context, token string) (*MDMAppleBootstrapPackage, error) @@ -790,6 +795,17 @@ type Service interface { // GetMDMWindowsTOSContent returns TOS content GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) + // Set or update the disk encryption key for a host. + SetOrUpdateDiskEncryptionKey(ctx context.Context, encryptionKey, clientError string) error + + /////////////////////////////////////////////////////////////////////////////// + // Common MDM + + // GetMDMDiskEncryptionSummary returns the current disk encryption status of all macOS and + // Windows hosts in the specified team (or, if no team is specified, each host that is not + // assigned to any team). + GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*MDMDiskEncryptionSummary, error) + /////////////////////////////////////////////////////////////////////////////// // Host Script Execution diff --git a/server/fleet/teams.go b/server/fleet/teams.go index 4c6ba72e0..191ef0f3b 100644 --- a/server/fleet/teams.go +++ b/server/fleet/teams.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "time" + + "github.com/fleetdm/fleet/v4/pkg/optjson" ) const ( @@ -29,9 +31,10 @@ type TeamPayload struct { // need to be able which part of the MDM config was provided in the request, // so the fields are pointers to structs. type TeamPayloadMDM struct { - MacOSUpdates *MacOSUpdates `json:"macos_updates"` - MacOSSettings *MacOSSettings `json:"macos_settings"` - MacOSSetup *MacOSSetup `json:"macos_setup"` + EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"` + MacOSUpdates *MacOSUpdates `json:"macos_updates"` + MacOSSettings *MacOSSettings `json:"macos_settings"` + MacOSSetup *MacOSSetup `json:"macos_setup"` } // Team is the data representation for the "Team" concept (group of hosts and @@ -143,13 +146,16 @@ type TeamWebhookSettings struct { } type TeamMDM struct { - MacOSUpdates MacOSUpdates `json:"macos_updates"` - MacOSSettings MacOSSettings `json:"macos_settings"` - MacOSSetup MacOSSetup `json:"macos_setup"` + EnableDiskEncryption bool `json:"enable_disk_encryption"` + MacOSUpdates MacOSUpdates `json:"macos_updates"` + MacOSSettings MacOSSettings `json:"macos_settings"` + MacOSSetup MacOSSetup `json:"macos_setup"` // NOTE: TeamSpecMDM must be kept in sync with TeamMDM. } type TeamSpecMDM struct { + EnableDiskEncryption optjson.Bool `json:"enable_disk_encryption"` + MacOSUpdates MacOSUpdates `json:"macos_updates"` // A map is used for the macos settings so that we can easily detect if its @@ -364,7 +370,9 @@ func TeamSpecFromTeam(t *Team) (*TeamSpec, error) { var mdmSpec TeamSpecMDM mdmSpec.MacOSUpdates = t.Config.MDM.MacOSUpdates mdmSpec.MacOSSettings = t.Config.MDM.MacOSSettings.ToMap() + delete(mdmSpec.MacOSSettings, "enable_disk_encryption") mdmSpec.MacOSSetup = t.Config.MDM.MacOSSetup + mdmSpec.EnableDiskEncryption = optjson.SetBool(t.Config.MDM.EnableDiskEncryption) return &TeamSpec{ Name: t.Name, AgentOptions: agentOptions, diff --git a/server/fleet/windows_mdm.go b/server/fleet/windows_mdm.go new file mode 100644 index 000000000..0a72f5aea --- /dev/null +++ b/server/fleet/windows_mdm.go @@ -0,0 +1,16 @@ +package fleet + +// MDMWindowsBitLockerSummary reports the number of Windows hosts being managed by Fleet with +// BitLocker. Each host may be counted in only one of six mutually-exclusive categories: +// Verified, Verifying, ActionRequired, Enforcing, Failed, RemovingEnforcement. +// +// Note that it is expected that each of Verifying, ActionRequired, and RemovingEnforcement will be +// zero because these states are not in Fleet's current implementation of BitLocker management. +type MDMWindowsBitLockerSummary struct { + Verified uint `json:"verified" db:"verified"` + Verifying uint `json:"verifying" db:"verifying"` + ActionRequired uint `json:"action_required" db:"action_required"` + Enforcing uint `json:"enforcing" db:"enforcing"` + Failed uint `json:"failed" db:"failed"` + RemovingEnforcement uint `json:"removing_enforcement" db:"removing_enforcement"` +} diff --git a/server/mdm/apple/cert.go b/server/mdm/apple/cert.go index 8b06ded4d..aa300c459 100644 --- a/server/mdm/apple/cert.go +++ b/server/mdm/apple/cert.go @@ -2,12 +2,10 @@ package apple_mdm import ( "bytes" - "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" - "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -17,7 +15,6 @@ import ( "github.com/micromdm/nanodep/tokenpki" "github.com/micromdm/scep/v2/depot" - "go.mozilla.org/pkcs7" ) const ( @@ -160,17 +157,3 @@ func NewDEPKeyPairPEM() ([]byte, []byte, error) { return publicKeyPEM, privateKeyPEM, nil } - -func DecryptBase64CMS(p7Base64 string, cert *x509.Certificate, key crypto.PrivateKey) ([]byte, error) { - p7Bytes, err := base64.StdEncoding.DecodeString(p7Base64) - if err != nil { - return nil, err - } - - p7, err := pkcs7.Parse(p7Bytes) - if err != nil { - return nil, err - } - - return p7.Decrypt(cert, key) -} diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go new file mode 100644 index 000000000..79a55dd50 --- /dev/null +++ b/server/mdm/mdm.go @@ -0,0 +1,25 @@ +package mdm + +import ( + "crypto" + "crypto/x509" + "encoding/base64" + + "go.mozilla.org/pkcs7" +) + +// DecryptBase64CMS decrypts a base64 encoded pkcs7-encrypted value using the +// provided certificate and private key. +func DecryptBase64CMS(p7Base64 string, cert *x509.Certificate, key crypto.PrivateKey) ([]byte, error) { + p7Bytes, err := base64.StdEncoding.DecodeString(p7Base64) + if err != nil { + return nil, err + } + + p7, err := pkcs7.Parse(p7Bytes) + if err != nil { + return nil, err + } + + return p7.Decrypt(cert, key) +} diff --git a/server/mdm/apple/cert_test.go b/server/mdm/mdm_test.go similarity index 99% rename from server/mdm/apple/cert_test.go rename to server/mdm/mdm_test.go index f6289e992..35f151ac1 100644 --- a/server/mdm/apple/cert_test.go +++ b/server/mdm/mdm_test.go @@ -1,4 +1,4 @@ -package apple_mdm +package mdm import ( "crypto/tls" diff --git a/server/mdm/microsoft/microsoft_mdm.go b/server/mdm/microsoft/microsoft_mdm.go index 552c8ffc9..8a52f6bc3 100644 --- a/server/mdm/microsoft/microsoft_mdm.go +++ b/server/mdm/microsoft/microsoft_mdm.go @@ -1,7 +1,11 @@ package microsoft_mdm import ( + "crypto/x509" + "encoding/base64" + "github.com/fleetdm/fleet/v4/server/mdm/internal/commonmdm" + "go.mozilla.org/pkcs7" ) const ( @@ -174,7 +178,7 @@ const ( WstepRenewRetryInterval = "4" // The PROVIDER-ID paramer specifies the server identifier for a management server used in the current management session - DocProvisioningAppProviderID = "FleetDM" + DocProvisioningAppProviderID = "Fleet" // The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity DocProvisioningAppName = DocProvisioningAppProviderID @@ -275,3 +279,14 @@ func ResolveWindowsMDMAuth(serverURL string) (string, error) { func ResolveWindowsMDMManagement(serverURL string) (string, error) { return commonmdm.ResolveURL(serverURL, MDE2ManagementPath, false) } + +// Encrypt uses pkcs7 to encrypt a raw value using the provided certificate. +// The returned encrypted value is base64-encoded. +func Encrypt(rawValue string, cert *x509.Certificate) (string, error) { + encrypted, err := pkcs7.Encrypt([]byte(rawValue), []*x509.Certificate{cert}) + if err != nil { + return "", err + } + b64Enc := base64.StdEncoding.EncodeToString(encrypted) + return b64Enc, nil +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 1b049e602..ecc0b814e 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -480,7 +480,7 @@ type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAv type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error -type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string) error +type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) @@ -674,6 +674,14 @@ type MDMWindowsInsertEnrolledDeviceFunc func(ctx context.Context, device *fleet. type MDMWindowsDeleteEnrolledDeviceFunc func(ctx context.Context, mdmDeviceID string) error +type MDMWindowsGetEnrolledDeviceWithDeviceIDFunc func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) + +type MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc func(ctx context.Context, mdmDeviceID string) error + +type GetMDMWindowsBitLockerSummaryFunc func(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) + +type GetMDMWindowsBitLockerStatusFunc func(ctx context.Context, host *fleet.Host) (*fleet.DiskEncryptionStatus, error) + type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) type SetHostScriptExecutionResultFunc func(ctx context.Context, result *fleet.HostScriptResultPayload) error @@ -1667,6 +1675,18 @@ type DataStore struct { MDMWindowsDeleteEnrolledDeviceFunc MDMWindowsDeleteEnrolledDeviceFunc MDMWindowsDeleteEnrolledDeviceFuncInvoked bool + MDMWindowsGetEnrolledDeviceWithDeviceIDFunc MDMWindowsGetEnrolledDeviceWithDeviceIDFunc + MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked bool + + MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc + MDMWindowsDeleteEnrolledDeviceWithDeviceIDFuncInvoked bool + + GetMDMWindowsBitLockerSummaryFunc GetMDMWindowsBitLockerSummaryFunc + GetMDMWindowsBitLockerSummaryFuncInvoked bool + + GetMDMWindowsBitLockerStatusFunc GetMDMWindowsBitLockerStatusFunc + GetMDMWindowsBitLockerStatusFuncInvoked bool + NewHostScriptExecutionRequestFunc NewHostScriptExecutionRequestFunc NewHostScriptExecutionRequestFuncInvoked bool @@ -3299,11 +3319,11 @@ func (s *DataStore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID u return s.SetOrUpdateHostDisksEncryptionFunc(ctx, hostID, encrypted) } -func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string) error { +func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error { s.mu.Lock() s.SetOrUpdateHostDiskEncryptionKeyFuncInvoked = true s.mu.Unlock() - return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key) + return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key, clientError, decryptable) } func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { @@ -3957,11 +3977,11 @@ func (s *DataStore) WSTEPAssociateCertHash(ctx context.Context, deviceUUID strin return s.WSTEPAssociateCertHashFunc(ctx, deviceUUID, hash) } -func (s *DataStore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) { +func (s *DataStore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceHWID string) (*fleet.MDMWindowsEnrolledDevice, error) { s.mu.Lock() s.MDMWindowsGetEnrolledDeviceFuncInvoked = true s.mu.Unlock() - return s.MDMWindowsGetEnrolledDeviceFunc(ctx, mdmDeviceID) + return s.MDMWindowsGetEnrolledDeviceFunc(ctx, mdmDeviceHWID) } func (s *DataStore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error { @@ -3971,11 +3991,39 @@ func (s *DataStore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device * return s.MDMWindowsInsertEnrolledDeviceFunc(ctx, device) } -func (s *DataStore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceID string) error { +func (s *DataStore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error { s.mu.Lock() s.MDMWindowsDeleteEnrolledDeviceFuncInvoked = true s.mu.Unlock() - return s.MDMWindowsDeleteEnrolledDeviceFunc(ctx, mdmDeviceID) + return s.MDMWindowsDeleteEnrolledDeviceFunc(ctx, mdmDeviceHWID) +} + +func (s *DataStore) MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) { + s.mu.Lock() + s.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked = true + s.mu.Unlock() + return s.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc(ctx, mdmDeviceID) +} + +func (s *DataStore) MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error { + s.mu.Lock() + s.MDMWindowsDeleteEnrolledDeviceWithDeviceIDFuncInvoked = true + s.mu.Unlock() + return s.MDMWindowsDeleteEnrolledDeviceWithDeviceIDFunc(ctx, mdmDeviceID) +} + +func (s *DataStore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) { + s.mu.Lock() + s.GetMDMWindowsBitLockerSummaryFuncInvoked = true + s.mu.Unlock() + return s.GetMDMWindowsBitLockerSummaryFunc(ctx, teamID) +} + +func (s *DataStore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fleet.Host) (*fleet.DiskEncryptionStatus, error) { + s.mu.Lock() + s.GetMDMWindowsBitLockerStatusFuncInvoked = true + s.mu.Unlock() + return s.GetMDMWindowsBitLockerStatusFunc(ctx, host) } func (s *DataStore) NewHostScriptExecutionRequest(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 0b279e5b8..a3008831c 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" + "github.com/fleetdm/fleet/v4/pkg/rawjson" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz" @@ -78,6 +79,31 @@ func (r *appConfigResponse) UnmarshalJSON(data []byte) error { return nil } +// MarshalJSON implements the json.Marshaler interface to make sure we serialize +// both AppConfig and responseFields properly: +// +// - If this function is not defined, AppConfig.MarshalJSON gets promoted and +// will be called instead. +// - If we try to unmarshal everything in one go, AppConfig.MarshalJSON doesn't get +// called. +func (r appConfigResponse) MarshalJSON() ([]byte, error) { + // Marshal only the response fields + responseData, err := json.Marshal(r.appConfigResponseFields) + if err != nil { + return nil, err + } + + // Marshal the base AppConfig + appConfigData, err := json.Marshal(r.AppConfig) + if err != nil { + return nil, err + } + + // we need to marshal and combine both groups separately because + // AppConfig has a custom marshaler. + return rawjson.CombineRoots(responseData, appConfigData) +} + func (r appConfigResponse) error() error { return r.Err } func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { @@ -340,6 +366,15 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + // TODO: move this logic to the AppConfig unmarshaller? we need to do + // this because we unmarshal twice into appConfig: + // + // 1. To get the JSON value from the database + // 2. To update fields with the incoming values + if newAppConfig.MDM.EnableDiskEncryption.Valid { + appConfig.MDM.EnableDiskEncryption = newAppConfig.MDM.EnableDiskEncryption + } + fleet.ValidateEnabledVulnerabilitiesIntegrations(appConfig.WebhookSettings.VulnerabilitiesWebhook, appConfig.Integrations, invalid) fleet.ValidateEnabledFailingPoliciesIntegrations(appConfig.WebhookSettings.FailingPoliciesWebhook, appConfig.Integrations, invalid) fleet.ValidateEnabledHostStatusIntegrations(appConfig.WebhookSettings.HostStatusWebhook, invalid) @@ -502,22 +537,24 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } - if oldAppConfig.MDM.MacOSSettings.EnableDiskEncryption != appConfig.MDM.MacOSSettings.EnableDiskEncryption { - var act fleet.ActivityDetails - if appConfig.MDM.MacOSSettings.EnableDiskEncryption { - act = fleet.ActivityTypeEnabledMacosDiskEncryption{} - if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, nil); err != nil { - return nil, ctxerr.Wrap(ctx, err, "enable no-team filevault and escrow") + if appConfig.MDM.EnableDiskEncryption.Valid && oldAppConfig.MDM.EnableDiskEncryption.Value != appConfig.MDM.EnableDiskEncryption.Value { + if oldAppConfig.MDM.EnabledAndConfigured { + var act fleet.ActivityDetails + if appConfig.MDM.EnableDiskEncryption.Value { + act = fleet.ActivityTypeEnabledMacosDiskEncryption{} + if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, nil); err != nil { + return nil, ctxerr.Wrap(ctx, err, "enable no-team filevault and escrow") + } + } else { + act = fleet.ActivityTypeDisabledMacosDiskEncryption{} + if err := svc.EnterpriseOverrides.MDMAppleDisableFileVaultAndEscrow(ctx, nil); err != nil { + return nil, ctxerr.Wrap(ctx, err, "disable no-team filevault and escrow") + } } - } else { - act = fleet.ActivityTypeDisabledMacosDiskEncryption{} - if err := svc.EnterpriseOverrides.MDMAppleDisableFileVaultAndEscrow(ctx, nil); err != nil { - return nil, ctxerr.Wrap(ctx, err, "disable no-team filevault and escrow") + if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + return nil, ctxerr.Wrap(ctx, err, "create activity for app config macos disk encryption") } } - if err := svc.ds.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { - return nil, ctxerr.Wrap(ctx, err, "create activity for app config macos disk encryption") - } } mdmEnableEndUserAuthChanged := oldAppConfig.MDM.MacOSSetup.EnableEndUserAuthentication != appConfig.MDM.MacOSSetup.EnableEndUserAuthentication @@ -565,7 +602,7 @@ func (svc *Service) validateMDM( mdm *fleet.MDM, invalid *fleet.InvalidArgumentError, ) { - if mdm.MacOSSettings.EnableDiskEncryption && !license.IsPremium() { + if mdm.EnableDiskEncryption.Value && !license.IsPremium() { invalid.Append("macos_settings.enable_disk_encryption", ErrMissingLicense.Error()) } if oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value && !license.IsPremium() { @@ -586,11 +623,6 @@ func (svc *Service) validateMDM( `Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`) } - if mdm.MacOSSettings.EnableDiskEncryption { - invalid.Append("macos_settings.enable_disk_encryption", - `Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`) - } - if oldMdm.MacOSSetup.MacOSSetupAssistant.Value != mdm.MacOSSetup.MacOSSetupAssistant.Value { invalid.Append("macos_setup.macos_setup_assistant", `Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`) @@ -683,6 +715,14 @@ func (svc *Service) validateMDM( return } } + + // if either macOS or Windows MDM is enabled, this setting can be set. + if !mdm.AtLeastOnePlatformEnabledAndConfigured() { + if mdm.EnableDiskEncryption.Valid && mdm.EnableDiskEncryption.Value != oldMdm.EnableDiskEncryption.Value { + invalid.Append("mdm.enable_disk_encryption", + `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`) + } + } } func validateSSOProviderSettings(incoming, existing fleet.SSOProviderSettings, invalid *fleet.InvalidArgumentError) { diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 38598287e..6efc8a602 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -810,8 +810,9 @@ func TestMDMAppleConfig(t *testing.T) { name: "nochange", licenseTier: "free", expectedMDM: fleet.MDM{ - MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, - MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, + MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + EnableDiskEncryption: optjson.Bool{Set: true, Valid: false}, }, }, { name: "newDefaultTeamNoLicense", @@ -835,9 +836,10 @@ func TestMDMAppleConfig(t *testing.T) { findTeam: true, newMDM: fleet.MDM{AppleBMDefaultTeam: "foobar"}, expectedMDM: fleet.MDM{ - AppleBMDefaultTeam: "foobar", - MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, - MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + AppleBMDefaultTeam: "foobar", + MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, + MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + EnableDiskEncryption: optjson.Bool{Set: true, Valid: false}, }, }, { name: "foundEdit", @@ -846,9 +848,10 @@ func TestMDMAppleConfig(t *testing.T) { oldMDM: fleet.MDM{AppleBMDefaultTeam: "bar"}, newMDM: fleet.MDM{AppleBMDefaultTeam: "foobar"}, expectedMDM: fleet.MDM{ - AppleBMDefaultTeam: "foobar", - MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, - MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + AppleBMDefaultTeam: "foobar", + MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, + MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + EnableDiskEncryption: optjson.Bool{Set: true, Valid: false}, }, }, { name: "ssoFree", @@ -866,6 +869,7 @@ func TestMDMAppleConfig(t *testing.T) { EndUserAuthentication: fleet.MDMEndUserAuthentication{SSOProviderSettings: fleet.SSOProviderSettings{EntityID: "foo"}}, MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + EnableDiskEncryption: optjson.Bool{Set: true, Valid: false}, }, }, { name: "ssoAllFields", @@ -884,8 +888,9 @@ func TestMDMAppleConfig(t *testing.T) { MetadataURL: "http://isser.metadata.com", IDPName: "onelogin", }}, - MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, - MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + MacOSSetup: fleet.MacOSSetup{BootstrapPackage: optjson.String{Set: true}, MacOSSetupAssistant: optjson.String{Set: true}}, + MacOSUpdates: fleet.MacOSUpdates{MinimumVersion: optjson.String{Set: true}, Deadline: optjson.String{Set: true}}, + EnableDiskEncryption: optjson.Bool{Set: true, Valid: false}, }, }, { name: "ssoShortEntityID", diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 5417b8f09..53270d295 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -18,6 +18,7 @@ import ( "github.com/VividCortex/mysqlerr" "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/pkg/file" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -609,7 +610,6 @@ func getMdmAppleFileVaultSummaryEndpoint(ctx context.Context, request interface{ }, nil } -// QUESTION: workflow for developing new APIs? whats your setup quickly test code working? func (svc *Service) GetMDMAppleFileVaultSummary(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) { if err := svc.authz.Authorize(ctx, fleet.MDMAppleConfigProfile{TeamID: teamID}, fleet.ActionRead); err != nil { return nil, ctxerr.Wrap(ctx, err) @@ -1716,8 +1716,8 @@ func (svc *Service) updateAppConfigMDMAppleSettings(ctx context.Context, payload var didUpdate, didUpdateMacOSDiskEncryption bool if payload.EnableDiskEncryption != nil { - if ac.MDM.MacOSSettings.EnableDiskEncryption != *payload.EnableDiskEncryption { - ac.MDM.MacOSSettings.EnableDiskEncryption = *payload.EnableDiskEncryption + if ac.MDM.EnableDiskEncryption.Value != *payload.EnableDiskEncryption { + ac.MDM.EnableDiskEncryption = optjson.SetBool(*payload.EnableDiskEncryption) didUpdate = true didUpdateMacOSDiskEncryption = true } @@ -1729,7 +1729,7 @@ func (svc *Service) updateAppConfigMDMAppleSettings(ctx context.Context, payload } if didUpdateMacOSDiskEncryption { var act fleet.ActivityDetails - if ac.MDM.MacOSSettings.EnableDiskEncryption { + if ac.MDM.EnableDiskEncryption.Value { act = fleet.ActivityTypeEnabledMacosDiskEncryption{} if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, nil); err != nil { return ctxerr.Wrap(ctx, err, "enable no-team filevault and escrow") diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 49f941ce3..8815b63a2 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -697,15 +697,15 @@ func TestHostDetailsMDMProfiles(t *testing.T) { } ds.HostFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) { if hostID == uint(42) { - return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337"}, nil + return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337", Platform: "darwin"}, nil } - return &fleet.Host{ID: hostID, UUID: "WR0N6-UU1D"}, nil + return &fleet.Host{ID: hostID, UUID: "WR0N6-UU1D", Platform: "darwin"}, nil } ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) { if identifier == "h0571d3n71f13r" { - return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337"}, nil + return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337", Platform: "darwin"}, nil } - return &fleet.Host{ID: uint(21), UUID: "WR0N6-UU1D"}, nil + return &fleet.Host{ID: uint(21), UUID: "WR0N6-UU1D", Platform: "darwin"}, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { return nil diff --git a/server/service/handler.go b/server/service/handler.go index aa76a30af..4b027d44f 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -448,7 +448,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // Only Fleet MDM specific endpoints should be within the root /mdm/ path. // NOTE: remember to update - // `service.mdmAppleConfigurationRequiredEndpoints` when you add an + // `service.mdmConfigurationRequiredEndpoints` when you add an // endpoint that's behind the mdmConfiguredMiddleware, this applies // both to this set of endpoints and to any public/token-authenticated // endpoints using `neMDM` below in this file. @@ -484,7 +484,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // host-specific mdm routes mdmAppleMW.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmAppleCommandRemoveEnrollmentProfileEndpoint, mdmAppleCommandRemoveEnrollmentProfileRequest{}) - mdmAppleMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) + mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/lock", deviceLockEndpoint, deviceLockRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/wipe", deviceWipeEndpoint, deviceWipeRequest{}) mdmAppleMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/profiles", getHostProfilesEndpoint, getHostProfilesRequest{}) @@ -500,6 +500,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles/preassign", preassignMDMAppleProfileEndpoint, preassignMDMAppleProfileRequest{}) mdmAppleMW.POST("/api/_version_/fleet/mdm/apple/profiles/match", matchMDMApplePreassignmentEndpoint, matchMDMApplePreassignmentRequest{}) + mdmAppleOrWinMW := ue.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleOrWindowsMDM()) + mdmAppleOrWinMW.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{}) + mdmAppleOrWinMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{}) + // the following set of mdm endpoints must always be accessible (even // if MDM is not configured) as it bootstraps the setup of MDM // (generates CSR request for APNs, plus the SCEP and ABM keypairs). @@ -587,6 +591,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC oe.POST("/api/fleet/orbit/scripts/request", getOrbitScriptEndpoint, orbitGetScriptRequest{}) oe.POST("/api/fleet/orbit/scripts/result", postOrbitScriptResultEndpoint, orbitPostScriptResultRequest{}) + oeWindowsMDM := oe.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM()) + oeWindowsMDM.POST("/api/fleet/orbit/disk_encryption_key", postOrbitDiskEncryptionKeyEndpoint, orbitPostDiskEncryptionKeyRequest{}) + // unauthenticated endpoints - most of those are either login-related, // invite-related or host-enrolling. So they typically do some kind of // one-time authentication by verifying that a valid secret token is provided @@ -597,7 +604,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC // These endpoint are token authenticated. // NOTE: remember to update - // `service.mdmAppleConfigurationRequiredEndpoints` when you add an + // `service.mdmConfigurationRequiredEndpoints` when you add an // endpoint that's behind the mdmConfiguredMiddleware, this applies // both to this set of endpoints and to any user authenticated // endpoints using `mdmAppleMW.*` above in this file. diff --git a/server/service/hosts.go b/server/service/hosts.go index 840807bde..db568e87b 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "crypto/tls" "encoding/csv" "encoding/json" "errors" @@ -19,7 +20,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" - apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + "github.com/fleetdm/fleet/v4/server/mdm" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/worker" "github.com/gocarina/gocsv" @@ -918,21 +919,34 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f var profiles []fleet.HostMDMAppleProfile if ac.MDM.EnabledAndConfigured { - profs, err := svc.ds.GetHostMDMProfiles(ctx, host.UUID) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "get host mdm profiles") - } - - // determine disk encryption and action required here based on profiles and - // raw decryptable key status. - host.MDM.DetermineDiskEncryptionStatus(profs, mobileconfig.FleetFileVaultPayloadIdentifier) - - for _, p := range profs { - if p.Identifier == mobileconfig.FleetFileVaultPayloadIdentifier { - p.Status = host.MDM.ProfileStatusFromDiskEncryptionState(p.Status) + host.MDM.OSSettings = &fleet.HostMDMOSSettings{} + switch host.Platform { + case "windows": + if license.IsPremium(ctx) { + bls, err := svc.ds.GetMDMWindowsBitLockerStatus(ctx, host) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get host mdm bitlocker status") + } + host.MDM.OSSettings.DiskEncryption.Status = bls + } + case "darwin": + profs, err := svc.ds.GetHostMDMProfiles(ctx, host.UUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get host mdm profiles") + } + + // determine disk encryption and action required here based on profiles and + // raw decryptable key status. + host.MDM.DetermineMacOSDiskEncryptionStatus(profs, mobileconfig.FleetFileVaultPayloadIdentifier) + host.MDM.OSSettings.DiskEncryption.Status = host.MDM.MacOSSettings.DiskEncryption + + for _, p := range profs { + if p.Identifier == mobileconfig.FleetFileVaultPayloadIdentifier { + p.Status = host.MDM.ProfileStatusFromDiskEncryptionState(p.Status) + } + p.Detail = fleet.HostMDMProfileDetail(p.Detail).Message() + profiles = append(profiles, p) } - p.Detail = fleet.HostMDMProfileDetail(p.Detail).Message() - profiles = append(profiles, p) } } host.MDM.Profiles = &profiles @@ -1563,25 +1577,48 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host return nil, err } + // The middleware checks that either Apple or Windows MDM are configured and + // enabled, but here we must check if the specific one is enabled for that + // particular host's platform. + var decryptCert *tls.Certificate + switch host.FleetPlatform() { + case "windows": + if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil { + return nil, err + } + + // use Microsoft's WSTEP certificate for decrypting + cert, _, _, err := svc.config.MDM.MicrosoftWSTEP() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting Microsoft WSTEP certificate to decrypt key") + } + decryptCert = cert + + default: + if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { + return nil, err + } + + // use Apple's SCEP certificate for decrypting + cert, _, _, err := svc.config.MDM.AppleSCEP() + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting Apple SCEP certificate to decrypt key") + } + decryptCert = cert + } + key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") } - if key.Decryptable == nil || !*key.Decryptable { - return nil, ctxerr.Wrap(ctx, newNotFoundError(), "getting host encryption key") + return nil, ctxerr.Wrap(ctx, newNotFoundError(), "host encryption key is not decryptable") } - cert, _, _, err := svc.config.MDM.AppleSCEP() + decryptedKey, err := mdm.DecryptBase64CMS(key.Base64Encrypted, decryptCert.Leaf, decryptCert.PrivateKey) if err != nil { - return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") + return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key") } - - decryptedKey, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") - } - key.DecryptedValue = string(decryptedKey) err = svc.ds.NewActivity( diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index e62fe936c..1b77fa3f7 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -14,6 +14,7 @@ import ( "github.com/WatchBeam/clock" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" + "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" @@ -83,7 +84,7 @@ func TestHostDetails(t *testing.T) { require.Nil(t, hostDetail.MDM.MacOSSettings) } -func TestHostDetailsMDMDiskEncryption(t *testing.T) { +func TestHostDetailsMDMAppleDiskEncryption(t *testing.T) { ds := new(mock.Store) svc := &Service{ds: ds} @@ -308,7 +309,7 @@ func TestHostDetailsMDMDiskEncryption(t *testing.T) { } require.NoError(t, mdmData.Scan([]byte(fmt.Sprintf(`{"raw_decryptable": %s}`, rawDecrypt)))) - host := &fleet.Host{ID: 3, MDM: mdmData, UUID: "abc"} + host := &fleet.Host{ID: 3, MDM: mdmData, UUID: "abc", Platform: "darwin"} opts := fleet.HostDetailOptions{ IncludeCVEScores: false, IncludePolicies: false, @@ -322,12 +323,16 @@ func TestHostDetailsMDMDiskEncryption(t *testing.T) { } hostDetail, err := svc.getHostDetails(test.UserContext(context.Background(), test.UserAdmin), host, opts) require.NoError(t, err) + require.NotNil(t, hostDetail.MDM.MacOSSettings) if c.wantState == "" { require.Nil(t, hostDetail.MDM.MacOSSettings.DiskEncryption) + require.Nil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) } else { require.NotNil(t, hostDetail.MDM.MacOSSettings.DiskEncryption) require.Equal(t, c.wantState, *hostDetail.MDM.MacOSSettings.DiskEncryption) + require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) + require.Equal(t, c.wantState, *hostDetail.MDM.OSSettings.DiskEncryption.Status) } if c.wantAction == "" { require.Nil(t, hostDetail.MDM.MacOSSettings.ActionRequired) @@ -346,6 +351,100 @@ func TestHostDetailsMDMDiskEncryption(t *testing.T) { } } +func TestHostDetailsOSSettings(t *testing.T) { + ds := new(mock.Store) + svc := &Service{ds: ds} + + ctx := context.Background() + + ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { + return nil, nil + } + ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Pack, error) { + return nil, nil + } + ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { + return nil + } + ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + return nil, nil + } + ds.ListHostBatteriesFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostBattery, error) { + return nil, nil + } + ds.GetHostMDMMacOSSetupFunc = func(ctx context.Context, hid uint) (*fleet.HostMDMMacOSSetup, error) { + return nil, nil + } + + type testCase struct { + name string + host *fleet.Host + licenseTier string + wantStatus fleet.DiskEncryptionStatus + } + cases := []testCase{ + {"windows", &fleet.Host{ID: 42, Platform: "windows"}, fleet.TierPremium, fleet.DiskEncryptionEnforcing}, + {"darwin", &fleet.Host{ID: 42, Platform: "darwin"}, fleet.TierPremium, ""}, + {"ubuntu", &fleet.Host{ID: 42, Platform: "ubuntu"}, fleet.TierPremium, ""}, + {"not premium", &fleet.Host{ID: 42, Platform: "windows"}, fleet.TierFree, ""}, + } + + setupDS := func(c testCase) { + ds.AppConfigFuncInvoked = false + ds.GetMDMWindowsBitLockerStatusFuncInvoked = false + ds.GetHostMDMProfilesFuncInvoked = false + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } + ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.DiskEncryptionStatus, error) { + if c.wantStatus == "" { + return nil, nil + } + return &c.wantStatus, nil + } + ds.GetHostMDMProfilesFunc = func(ctx context.Context, uuid string) ([]fleet.HostMDMAppleProfile, error) { + return nil, nil + } + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + setupDS(c) + + ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: c.licenseTier}) + + hostDetail, err := svc.getHostDetails(test.UserContext(ctx, test.UserAdmin), c.host, fleet.HostDetailOptions{ + IncludeCVEScores: false, + IncludePolicies: false, + }) + require.NoError(t, err) + require.NotNil(t, hostDetail) + require.True(t, ds.AppConfigFuncInvoked) + + switch c.host.Platform { + case "windows": + require.False(t, ds.GetHostMDMProfilesFuncInvoked) + if c.wantStatus != "" { + require.True(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) + require.NotNil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) + require.Equal(t, c.wantStatus, *hostDetail.MDM.OSSettings.DiskEncryption.Status) + } else { + require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) + require.Nil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) + } + case "darwin": + require.True(t, ds.GetHostMDMProfilesFuncInvoked) + require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) + require.Nil(t, hostDetail.MDM.OSSettings.DiskEncryption.Status) + default: + require.False(t, ds.GetHostMDMProfilesFuncInvoked) + require.False(t, ds.GetMDMWindowsBitLockerStatusFuncInvoked) + } + }) + } +} + func TestHostAuth(t *testing.T) { ds := new(mock.Store) svc, ctx := newTestService(t, ds, nil, nil) @@ -902,9 +1001,18 @@ func TestHostEncryptionKey(t *testing.T) { require.NoError(t, err) base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey) + wstep, _, _, err := fleetCfg.MDM.MicrosoftWSTEP() + require.NoError(t, err) + winEncryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{wstep.Leaf}) + require.NoError(t, err) + winBase64EncryptedKey := base64.StdEncoding.EncodeToString(winEncryptedKey) + for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { ds := new(mock.Store) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { @@ -951,7 +1059,10 @@ func TestHostEncryptionKey(t *testing.T) { t.Run("test error cases", func(t *testing.T) { ds := new(mock.Store) - svc, ctx := newTestService(t, ds, nil, nil) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } + svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) ctx = test.UserContext(ctx, test.UserAdmin) hostErr := errors.New("host error") @@ -981,6 +1092,56 @@ func TestHostEncryptionKey(t *testing.T) { _, err = svc.HostEncryptionKey(ctx, 1) require.Error(t, err) }) + + t.Run("host platform mdm enabled", func(t *testing.T) { + cases := []struct { + hostPlatform string + macMDMEnabled bool + winMDMEnabled bool + shouldFail bool + }{ + {"windows", true, false, true}, + {"windows", false, true, false}, + {"windows", true, true, false}, + {"darwin", true, false, false}, + {"darwin", false, true, true}, + {"darwin", true, true, false}, + } + for _, c := range cases { + t.Run(fmt.Sprintf("%s: mac mdm: %t; win mdm: %t", c.hostPlatform, c.macMDMEnabled, c.winMDMEnabled), func(t *testing.T) { + ds := new(mock.Store) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: c.macMDMEnabled, WindowsEnabledAndConfigured: c.winMDMEnabled}}, nil + } + ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { + return &fleet.Host{Platform: c.hostPlatform}, nil + } + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + key := base64EncryptedKey + if c.hostPlatform == "windows" { + key = winBase64EncryptedKey + } + return &fleet.HostDiskEncryptionKey{ + Base64Encrypted: key, + Decryptable: ptr.Bool(true), + }, nil + } + ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { + return nil + } + + svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + _, err := svc.HostEncryptionKey(ctx, 1) + if c.shouldFail { + require.Error(t, err) + require.ErrorContains(t, err, fleet.ErrMDMNotConfigured.Error()) + } else { + require.NoError(t, err) + } + }) + } + }) } func TestHostMDMProfileDetail(t *testing.T) { @@ -1005,7 +1166,8 @@ func TestHostMDMProfileDetail(t *testing.T) { ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { return &fleet.Host{ - ID: 1, + ID: 1, + Platform: "darwin", }, nil } ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index d3ab73919..9bc27ec2f 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -4970,11 +4970,18 @@ func (s *integrationTestSuite) TestAppConfig() { // set the macos disk encryption field, fails due to license res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": true } } + "mdm": { "enable_disk_encryption": true } }`), http.StatusUnprocessableEntity) errMsg := extractServerErrorText(res.Body) assert.Contains(t, errMsg, "missing or invalid license") + // legacy config + res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "macos_settings": { "enable_disk_encryption": true } } + }`), http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + assert.Contains(t, errMsg, "missing or invalid license") + // try to set the apple bm default team, which is premium only s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "apple_bm_default_team": "xyz" } @@ -6425,6 +6432,16 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() { s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostLin.ID), nil, http.StatusOK, &getHostResp) require.Equal(t, hostLin.ID, getHostResp.Host.ID) require.Nil(t, getHostResp.Host.DiskEncryptionEnabled) + + // the orbit endpoint to set the disk encryption key always fails in this + // suite because MDM is not configured. + orbitHost := createOrbitEnrolledHost(t, "windows", "diskenc", s.ds) + res := s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{ + OrbitNodeKey: *orbitHost.OrbitNodeKey, + EncryptionKey: []byte("testkey"), + }, http.StatusBadRequest) + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, fleet.ErrMDMNotConfigured.Error()) } func (s *integrationTestSuite) TestOSVersions() { @@ -6477,14 +6494,14 @@ func (s *integrationTestSuite) TestPingEndpoints() { s.DoRawNoAuth("HEAD", "/api/fleet/device/ping", nil, http.StatusOK) } -func (s *integrationTestSuite) TestAppleMDMNotConfigured() { +func (s *integrationTestSuite) TestMDMNotConfiguredEndpoints() { t := s.T() // create a host with device token to test device authenticated routes tkn := "D3V1C370K3N" createHostAndDeviceToken(t, s.ds, tkn) - for _, route := range mdmAppleConfigurationRequiredEndpoints() { + for _, route := range mdmConfigurationRequiredEndpoints() { which := fmt.Sprintf("%s %s", route.method, route.path) var expectedErr fleet.ErrWithStatusCode = fleet.ErrMDMNotConfigured if route.premiumOnly && route.deviceAuthenticated { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 83f4976de..60d909344 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -239,7 +239,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamSpecs() { require.NoError(t, err) require.Contains(t, string(*team.Config.AgentOptions), `"foo": "bar"`) // unchanged require.Empty(t, team.Config.MDM.MacOSSettings.CustomSettings) // unchanged - require.False(t, team.Config.MDM.MacOSSettings.EnableDiskEncryption) // unchanged + require.False(t, team.Config.MDM.EnableDiskEncryption) // unchanged // apply without agent options specified teamSpecs = map[string]any{ @@ -764,7 +764,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { // modify team's disk encryption, impossible without mdm enabled res := s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), fleet.TeamPayload{ MDM: &fleet.TeamPayloadMDM{ - MacOSSettings: &fleet.MacOSSettings{EnableDiskEncryption: true}, + EnableDiskEncryption: optjson.SetBool(true), }, }, http.StatusUnprocessableEntity) errMsg := extractServerErrorText(res.Body) @@ -2532,14 +2532,14 @@ func (s *integrationEnterpriseTestSuite) TestListHosts() { require.Nil(t, summaryResp.LowDiskSpaceCount) } -func (s *integrationEnterpriseTestSuite) TestAppleMDMNotConfigured() { +func (s *integrationEnterpriseTestSuite) TestMDMNotConfiguredEndpoints() { t := s.T() // create a host with device token to test device authenticated routes tkn := "D3V1C370K3N" createHostAndDeviceToken(t, s.ds, tkn) - for _, route := range mdmAppleConfigurationRequiredEndpoints() { + for _, route := range mdmConfigurationRequiredEndpoints() { var expectedErr fleet.ErrWithStatusCode = fleet.ErrMDMNotConfigured path := route.path if route.deviceAuthenticated { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index ddf83f81a..618d6c2dc 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -32,6 +32,7 @@ import ( "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/datastore/redis/redistest" "github.com/fleetdm/fleet/v4/server/fleet" + servermdm "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" @@ -238,7 +239,7 @@ func (s *integrationMDMTestSuite) TearDownTest() { // ensure windows mdm is always enabled for the next test appCfg.MDM.WindowsEnabledAndConfigured = true // ensure global disk encryption is disabled on exit - appCfg.MDM.MacOSSettings.EnableDiskEncryption = false + appCfg.MDM.EnableDiskEncryption = optjson.SetBool(false) err := s.ds.SaveAppConfig(ctx, &appCfg.AppConfig) require.NoError(t, err) @@ -1059,7 +1060,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) // filevault is enabled by default - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // setup assistant settings are copyied from "no team" teamAsst, err := s.ds.GetMDMAppleSetupAssistant(ctx, &tm1.ID) require.NoError(t, err) @@ -1146,7 +1147,7 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() { // simulate having its profiles installed mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.MacOSSettingsVerifying, mdmHost2.UUID) + _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ?`, fleet.OSSettingsVerifying, mdmHost2.UUID) return err }) @@ -1297,7 +1298,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // host2 checks in puppetRun(host2) @@ -1318,7 +1319,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) require.Equal(t, prof4, []byte(profs[3].Mobileconfig)) - require.True(t, tm2.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm2.Config.MDM.EnableDiskEncryption) // host3 checks in puppetRun(host3) @@ -1345,7 +1346,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) require.NotEqual(t, oldProf2, []byte(profs[1].Mobileconfig)) - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // host2 checks in, still belongs to the same team puppetRun(host2) @@ -1362,7 +1363,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) require.Equal(t, prof4, []byte(profs[3].Mobileconfig)) require.NotEqual(t, oldProf2, []byte(profs[1].Mobileconfig)) - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // the puppet manifest is changed, and prof3 is removed // node default { @@ -1423,7 +1424,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Len(t, profs, 2) require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // same for host2 puppetRun(host2) @@ -1436,7 +1437,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) require.Equal(t, prof4, []byte(profs[2].Mobileconfig)) - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // The puppet manifest is drastically updated, this time to use exclusions on host3: // @@ -1515,7 +1516,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) require.Equal(t, prof3, []byte(profs[2].Mobileconfig)) - require.True(t, tm1.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm1.Config.MDM.EnableDiskEncryption) // host2 checks in puppetRun(host2) @@ -1544,7 +1545,7 @@ func (s *integrationMDMTestSuite) TestPuppetRun() { require.Len(t, profs, 2) require.Equal(t, prof1, []byte(profs[0].Mobileconfig)) require.Equal(t, prof2, []byte(profs[1].Mobileconfig)) - require.True(t, tm3.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, tm3.Config.MDM.EnableDiskEncryption) } func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestMDMClient) { @@ -2174,6 +2175,261 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() { require.Equal(t, "", hostResp.Host.MDM.Name) } +func (s *integrationMDMTestSuite) TestMDMDiskEncryptionSettingBackwardsCompat() { + t := s.T() + + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "enable_disk_encryption": false } + }`), http.StatusOK, &acResp) + assert.False(t, acResp.MDM.EnableDiskEncryption.Value) + + // new config takes precedence over old config + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "enable_disk_encryption": false, "macos_settings": {"enable_disk_encryption": true} } + }`), http.StatusOK, &acResp) + assert.False(t, acResp.MDM.EnableDiskEncryption.Value) + + s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false) + + // if new config is not present, old config is applied + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "macos_settings": {"enable_disk_encryption": true} } + }`), http.StatusOK, &acResp) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) + s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true) + + // new config takes precedence over old config again + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "enable_disk_encryption": false, "macos_settings": {"enable_disk_encryption": true} } + }`), http.StatusOK, &acResp) + assert.False(t, acResp.MDM.EnableDiskEncryption.Value) + s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false) + + // unrelated change doesn't affect the disk encryption setting + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "macos_settings": {"custom_settings": ["test.mobileconfig"]} } + }`), http.StatusOK, &acResp) + assert.False(t, acResp.MDM.EnableDiskEncryption.Value) + + // Same tests, but for teams + team, err := s.ds.NewTeam(context.Background(), &fleet.Team{ + Name: "team1_" + t.Name(), + Description: "desc team1_" + t.Name(), + }) + require.NoError(t, err) + + checkTeamDiskEncryption := func(wantSetting bool) { + var teamResp getTeamResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) + require.Equal(t, wantSetting, teamResp.Team.Config.MDM.EnableDiskEncryption) + } + + // after creation, disk encryption is off + checkTeamDiskEncryption(false) + + // new config takes precedence over old config + teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ + Name: team.Name, + MDM: fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(false), + MacOSSettings: map[string]interface{}{"enable_disk_encryption": true}, + }, + }}} + s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + checkTeamDiskEncryption(false) + s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false) + + // if new config is not present, old config is applied + teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ + Name: team.Name, + MDM: fleet.TeamSpecMDM{ + MacOSSettings: map[string]interface{}{"enable_disk_encryption": true}, + }, + }}} + s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + checkTeamDiskEncryption(true) + s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true) + + // new config takes precedence over old config again + teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ + Name: team.Name, + MDM: fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(false), + MacOSSettings: map[string]interface{}{"enable_disk_encryption": true}, + }, + }}} + s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + checkTeamDiskEncryption(false) + s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false) + + // unrelated change doesn't affect the disk encryption setting + teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ + Name: team.Name, + MDM: fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(false), + MacOSSettings: map[string]interface{}{"custom_settings": []interface{}{"A", "B"}}, + }, + }}} + s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + checkTeamDiskEncryption(false) + s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false) +} + +func (s *integrationMDMTestSuite) TestDiskEncryptionSharedSetting() { + t := s.T() + + // create a team + teamName := t.Name() + team := &fleet.Team{ + Name: teamName, + Description: "desc " + teamName, + } + var createTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp) + require.NotZero(t, createTeamResp.Team.ID) + + setMDMEnabled := func(macMDM, windowsMDM bool) { + appConf, err := s.ds.AppConfig(context.Background()) + require.NoError(s.T(), err) + appConf.MDM.WindowsEnabledAndConfigured = windowsMDM + appConf.MDM.EnabledAndConfigured = macMDM + err = s.ds.SaveAppConfig(context.Background(), appConf) + require.NoError(s.T(), err) + } + + // before doing any modifications, grab the current values and make + // sure they're set to the same ones on cleanup to not interfere with + // other tests. + origAppConf, err := s.ds.AppConfig(context.Background()) + require.NoError(s.T(), err) + t.Cleanup(func() { + err := s.ds.SaveAppConfig(context.Background(), origAppConf) + require.NoError(s.T(), err) + }) + + checkConfigSetErrors := func() { + // try to set app config + res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "enable_disk_encryption": true } + }`), http.StatusUnprocessableEntity) + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.") + + // try to create a new team using specs + teamSpecs := map[string]any{ + "specs": []any{ + map[string]any{ + "name": teamName + uuid.NewString(), + "mdm": map[string]any{ + "enable_disk_encryption": true, + }, + }, + }, + } + res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.") + + // try to edit the existing team using specs + teamSpecs = map[string]any{ + "specs": []any{ + map[string]any{ + "name": teamName, + "mdm": map[string]any{ + "enable_disk_encryption": true, + }, + }, + }, + } + res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.") + } + + checkConfigSetSucceeds := func() { + res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "enable_disk_encryption": true } + }`), http.StatusOK) + errMsg := extractServerErrorText(res.Body) + require.Empty(t, errMsg) + + // try to create a new team using specs + teamSpecs := map[string]any{ + "specs": []any{ + map[string]any{ + "name": teamName + uuid.NewString(), + "mdm": map[string]any{ + "enable_disk_encryption": true, + }, + }, + }, + } + res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + errMsg = extractServerErrorText(res.Body) + require.Empty(t, errMsg) + + // edit the existing team using specs + teamSpecs = map[string]any{ + "specs": []any{ + map[string]any{ + "name": teamName, + "mdm": map[string]any{ + "enable_disk_encryption": true, + }, + }, + }, + } + res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + errMsg = extractServerErrorText(res.Body) + require.Empty(t, errMsg) + + // always try to set the value to `false` so we start fresh + s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "enable_disk_encryption": false } + }`), http.StatusOK) + teamSpecs = map[string]any{ + "specs": []any{ + map[string]any{ + "name": teamName, + "mdm": map[string]any{ + "enable_disk_encryption": false, + }, + }, + }, + } + s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + } + + // 1. disable both windows and mac mdm + // 2. turn off windows feature flag + // we should get an error + setMDMEnabled(false, false) + t.Setenv("FLEET_DEV_MDM_ENABLED", "0") + checkConfigSetErrors() + + // turn on windows feature flag + // we should get an error + t.Setenv("FLEET_DEV_MDM_ENABLED", "1") + checkConfigSetErrors() + + // enable windows mdm, no errors + setMDMEnabled(false, true) + checkConfigSetSucceeds() + + // enable mac mdm, no errors + setMDMEnabled(true, true) + checkConfigSetSucceeds() + + // only macos mdm enabled, no errors + setMDMEnabled(true, false) + checkConfigSetSucceeds() +} + func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() { t := s.T() ctx := context.Background() @@ -2196,9 +2452,9 @@ func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() { acResp := appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": true } } + "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) fileVaultProf := s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true) hostCmdUUID := uuid.New().String() err = s.ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{ @@ -2246,7 +2502,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() { require.NoError(t, err) base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey) - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64EncryptedKey) + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64EncryptedKey, "", nil) require.NoError(t, err) // get that host - it has an encryption key with unknown decryptability, so @@ -2321,7 +2577,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() { teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: "team1_" + t.Name(), MDM: fleet.TeamSpecMDM{ - MacOSSettings: map[string]interface{}{"enable_disk_encryption": true}, + EnableDiskEncryption: optjson.SetBool(true), }, }}} s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) @@ -2420,6 +2676,52 @@ func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() { s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusForbidden, &resp) } +func (s *integrationMDMTestSuite) TestWindowsMDMGetEncryptionKey() { + t := s.T() + ctx := context.Background() + + // create a host and enroll it in Fleet + host := createOrbitEnrolledHost(t, "windows", "h1", s.ds) + err := s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet) + require.NoError(t, err) + + // request encryption key with no auth token + res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusUnauthorized) + res.Body.Close() + + // no encryption key + resp := getHostEncryptionKeyResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp) + + // invalid host id + resp = getHostEncryptionKeyResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID+999), nil, http.StatusNotFound, &resp) + + // add an encryption key for the host + cert, _, _, err := s.fleetCfg.MDM.MicrosoftWSTEP() + require.NoError(t, err) + recoveryKey := "AAA-BBB-CCC" + encryptedKey, err := microsoft_mdm.Encrypt(recoveryKey, cert.Leaf) + require.NoError(t, err) + + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, encryptedKey, "", ptr.Bool(true)) + require.NoError(t, err) + + resp = getHostEncryptionKeyResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp) + require.Equal(t, host.ID, resp.HostID) + require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue) + s.lastActivityOfTypeMatches(fleet.ActivityTypeReadHostDiskEncryptionKey{}.ActivityName(), + fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID), 0) + + // update the key to blank with a client error + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "failed", nil) + require.NoError(t, err) + + resp = getHostEncryptionKeyResponse{} + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp) +} + func (s *integrationMDMTestSuite) TestMDMAppleListConfigProfiles() { t := s.T() ctx := context.Background() @@ -2663,9 +2965,9 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { // make fleet add a FileVault profile acResp := appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": true } } + "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) profile := s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true) // try to delete the profile @@ -2693,7 +2995,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleProfiles() { // field, should not remove them acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": {"enable_disk_encryption": true} } + "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp) assert.Equal(t, []string{"foo", "bar"}, acResp.MDM.MacOSSettings.CustomSettings) @@ -2719,9 +3021,9 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { // set the macos disk encryption field acResp := appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": true } } + "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) enabledDiskActID := s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), `{"team_id": null, "team_name": null}`, 0) @@ -2731,7 +3033,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { // check that they are returned by a GET /config acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) // patch without specifying the macos disk encryption and an unrelated field, // should not alter it @@ -2739,7 +3041,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "macos_settings": {"custom_settings": ["a"]} } }`), http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) assert.Equal(t, []string{"a"}, acResp.MDM.MacOSSettings.CustomSettings) s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), ``, enabledDiskActID) @@ -2747,9 +3049,9 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { // patch with false, would reset it but this is a dry-run acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": false } } + "mdm": { "enable_disk_encryption": false } }`), http.StatusOK, &acResp, "dry_run", "true") - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) assert.Equal(t, []string{"a"}, acResp.MDM.MacOSSettings.CustomSettings) s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), ``, enabledDiskActID) @@ -2757,9 +3059,9 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { // patch with false, resets it acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": false, "custom_settings": ["b"] } } + "mdm": { "enable_disk_encryption": false, "macos_settings": { "custom_settings": ["b"] } } }`), http.StatusOK, &acResp) - assert.False(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.False(t, acResp.MDM.EnableDiskEncryption.Value) assert.Equal(t, []string{"b"}, acResp.MDM.MacOSSettings.CustomSettings) s.lastActivityMatches(fleet.ActivityTypeDisabledMacosDiskEncryption{}.ActivityName(), `{"team_id": null, "team_name": null}`, 0) @@ -2778,7 +3080,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) assert.Equal(t, []string{"b"}, acResp.MDM.MacOSSettings.CustomSettings) // call update endpoint with no changes @@ -2792,7 +3094,7 @@ func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() { acResp = appConfigResponse{} s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) assert.Equal(t, []string{"b"}, acResp.MDM.MacOSSettings.CustomSettings) } @@ -2856,7 +3158,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleDiskEncryptionAggregate() { }) require.NoError(t, err) oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute) - err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "test-key") + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "test-key", "", nil) require.NoError(t, err) err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, decryptable, oneMinuteAfterThreshold) require.NoError(t, err) @@ -3043,7 +3345,7 @@ func (s *integrationMDMTestSuite) TestApplyTeamsMDMAppleProfiles() { teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: teamName, MDM: fleet.TeamSpecMDM{ - MacOSSettings: map[string]interface{}{"enable_disk_encryption": false}, + EnableDiskEncryption: optjson.SetBool(false), }, }}} s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) @@ -3098,7 +3400,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: teamName, MDM: fleet.TeamSpecMDM{ - MacOSSettings: map[string]interface{}{"enable_disk_encryption": true}, + EnableDiskEncryption: optjson.SetBool(true), }, }}} s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) @@ -3111,7 +3413,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { // retrieving the team returns the disk encryption setting var teamResp getTeamResponse s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) - require.True(t, teamResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption) // apply with invalid disk encryption value should fail teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ @@ -3142,7 +3444,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) - require.True(t, teamResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption) require.Equal(t, []string{"a"}, teamResp.Team.Config.MDM.MacOSSettings.CustomSettings) s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), ``, lastDiskActID) @@ -3151,13 +3453,13 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: teamName, MDM: fleet.TeamSpecMDM{ - MacOSSettings: map[string]interface{}{"enable_disk_encryption": false}, + EnableDiskEncryption: optjson.SetBool(false), }, }}} s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, "dry_run", "true") teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) - require.True(t, teamResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption) s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), ``, lastDiskActID) @@ -3171,7 +3473,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) - require.False(t, teamResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.False(t, teamResp.Team.Config.MDM.EnableDiskEncryption) s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosDiskEncryption{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0) @@ -3182,10 +3484,11 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { var modResp teamResponse s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ MDM: &fleet.TeamPayloadMDM{ - MacOSSettings: &fleet.MacOSSettings{EnableDiskEncryption: true}, + EnableDiskEncryption: optjson.SetBool(true), + MacOSSettings: &fleet.MacOSSettings{}, }, }, http.StatusOK, &modResp) - require.True(t, modResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, modResp.Team.Config.MDM.EnableDiskEncryption) s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0) @@ -3197,10 +3500,10 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{ Description: ptr.String("foobar"), MDM: &fleet.TeamPayloadMDM{ - MacOSSettings: &fleet.MacOSSettings{EnableDiskEncryption: false}, + EnableDiskEncryption: optjson.SetBool(false), }, }, http.StatusOK, &modResp) - require.False(t, modResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.False(t, modResp.Team.Config.MDM.EnableDiskEncryption) require.Equal(t, "foobar", modResp.Team.Description) s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosDiskEncryption{}.ActivityName(), fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0) @@ -3219,7 +3522,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) - require.True(t, teamResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption) // use the MDM settings endpoint with no changes s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", @@ -3232,7 +3535,7 @@ func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() { teamResp = getTeamResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp) - require.True(t, teamResp.Team.Config.MDM.MacOSSettings.EnableDiskEncryption) + require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption) // use the MDM settings endpoint with an unknown team id s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", @@ -3449,7 +3752,7 @@ func (s *integrationMDMTestSuite) TestHostMDMProfilesStatus() { // profile deployment is asynchronous, so we simulate it here by // updating any "pending" (not NULL) profiles to "verifying" mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error { - _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.MacOSSettingsVerifying, fleet.MacOSSettingsPending) + _, err := q.ExecContext(ctx, `UPDATE host_mdm_apple_profiles SET status = ? WHERE status = ?`, fleet.OSSettingsVerifying, fleet.OSSettingsPending) return err }) } @@ -5381,9 +5684,9 @@ func (s *integrationMDMTestSuite) TestGitOpsUserActions() { // Attempt to edit global MDM settings, should allow. acResp := appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": { "enable_disk_encryption": true } } + "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp) - assert.True(t, acResp.MDM.MacOSSettings.EnableDiskEncryption) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) // Attempt to setup Apple MDM, will fail but the important thing is that it // fails with 422 (cannot enable end user auth because no IdP is configured) @@ -5407,9 +5710,9 @@ func (s *integrationMDMTestSuite) TestGitOpsUserActions() { teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: t1.Name, MDM: fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(true), MacOSSettings: map[string]interface{}{ - "enable_disk_encryption": true, - "custom_settings": []interface{}{"foo", "bar"}, + "custom_settings": []interface{}{"foo", "bar"}, }, }, }}} @@ -5434,9 +5737,9 @@ func (s *integrationMDMTestSuite) TestGitOpsUserActions() { teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ Name: t1.Name, MDM: fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(true), MacOSSettings: map[string]interface{}{ - "enable_disk_encryption": true, - "custom_settings": []interface{}{"foo", "bar"}, + "custom_settings": []interface{}{"foo", "bar"}, }, }, }}} @@ -5590,7 +5893,7 @@ func (s *integrationMDMTestSuite) TestSSO() { // field, should not remove them acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "macos_settings": {"enable_disk_encryption": true} } + "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp) assert.Equal(t, wantSettings, acResp.MDM.EndUserAuthentication.SSOProviderSettings) @@ -6703,8 +7006,28 @@ func (s *integrationMDMTestSuite) TestValidSyncMLRequestNoAuth() { // Target Endpoint URL for the management endpoint targetEndpointURL := microsoft_mdm.MDE2ManagementPath + // Target DeviceID to use + deviceID := "DB257C3A08778F4FB61E2749066C1F27" + + // Inserting new device + enrolledDevice := &fleet.MDMWindowsEnrolledDevice{ + MDMDeviceID: deviceID, + MDMHardwareID: uuid.New().String() + uuid.New().String(), + MDMDeviceState: uuid.New().String(), + MDMDeviceType: "CIMClient_Windows", + MDMDeviceName: "DESKTOP-1C3ARC1", + MDMEnrollType: "ProgrammaticEnrollment", + MDMEnrollUserID: "upn@domain.com", + MDMEnrollProtoVersion: "5.0", + MDMEnrollClientVersion: "10.0.19045.2965", + MDMNotInOOBE: false, + } + + err := s.ds.MDMWindowsInsertEnrolledDevice(context.Background(), enrolledDevice) + require.NoError(t, err) + // Preparing the SyncML request - requestBytes, err := s.newSyncMLSessionMsg(targetEndpointURL) + requestBytes, err := s.newSyncMLSessionMsg(deviceID, targetEndpointURL) require.NoError(t, err) resp := s.DoRaw("POST", targetEndpointURL, requestBytes, http.StatusOK) @@ -6727,6 +7050,179 @@ func (s *integrationMDMTestSuite) TestValidSyncMLRequestNoAuth() { require.True(t, s.isXMLTagContentPresent("Add", resSoapMsg)) } +func (s *integrationMDMTestSuite) TestBitLockerEnforcementNotifications() { + t := s.T() + ctx := context.Background() + windowsHost := createOrbitEnrolledHost(t, "windows", t.Name(), s.ds) + + checkNotification := func(want bool) { + resp := orbitGetConfigResponse{} + s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *windowsHost.OrbitNodeKey)), http.StatusOK, &resp) + require.Equal(t, want, resp.Notifications.EnforceBitLockerEncryption) + } + + // notification is false by default + checkNotification(false) + + // enroll the host into Fleet MDM + encodedBinToken, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *windowsHost.OrbitNodeKey) + require.NoError(t, err) + requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true, false) + require.NoError(t, err) + s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK) + + // simulate osquery checking in and updating this info + // TODO: should we automatically fill these fields on MDM enrollment? + require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), windowsHost.ID, false, true, "https://example.com", true, fleet.WellKnownMDMFleet)) + + // notification is still false + checkNotification(false) + + // configure disk encryption for the global team + acResp := appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "macos_settings": { "enable_disk_encryption": true } } }`), http.StatusOK, &acResp) + assert.True(t, acResp.MDM.EnableDiskEncryption.Value) + + // host still doesn't get the notification because we don't have disk + // encryption information yet. + checkNotification(false) + + // host has disk encryption off, gets the notification + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, false)) + checkNotification(true) + + // host has disk encryption on, we don't have disk encryption info. Gets the notification + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, true)) + checkNotification(true) + + // host has disk encryption on, we don't know if the key is decriptable. Gets the notification + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost.ID, "test-key", "", nil) + require.NoError(t, err) + checkNotification(true) + + // host has disk encryption on, the key is not decryptable by fleet. Gets the notification + err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, false, time.Now()) + require.NoError(t, err) + checkNotification(true) + + // host has disk encryption on, the disk was encrypted by fleet. Doesn't get the notification + err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, true, time.Now()) + require.NoError(t, err) + checkNotification(false) + + // create a new team + tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{ + Name: t.Name(), + Description: "desc", + }) + require.NoError(t, err) + // add the host to the team + err = s.ds.AddHostsToTeam(context.Background(), &tm.ID, []uint{windowsHost.ID}) + require.NoError(t, err) + + // notification is false now since the team doesn't have disk encryption enabled + checkNotification(false) + + // enable disk encryption on the team + teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{ + Name: tm.Name, + MDM: fleet.TeamSpecMDM{ + EnableDiskEncryption: optjson.SetBool(true), + }, + }}} + s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK) + + // host gets the notification + checkNotification(true) + + // host has disk encryption off, gets the notification + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, false)) + checkNotification(true) + + // host has disk encryption on, we don't have disk encryption info. Gets the notification + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, true)) + checkNotification(true) + + // host has disk encryption on, we don't know if the key is decriptable. Gets the notification + err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost.ID, "test-key", "", nil) + require.NoError(t, err) + checkNotification(true) + + // host has disk encryption on, the key is not decryptable by fleet. Gets the notification + err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, false, time.Now()) + require.NoError(t, err) + checkNotification(true) + + // host has disk encryption on, the disk was encrypted by fleet. Doesn't get the notification + err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, true, time.Now()) + require.NoError(t, err) + checkNotification(false) +} + +func (s *integrationMDMTestSuite) TestHostDiskEncryptionKey() { + t := s.T() + ctx := context.Background() + + host := createOrbitEnrolledHost(t, "windows", "h1", s.ds) + + // try to call the endpoint while the host is not MDM-enrolled + res := s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{ + OrbitNodeKey: *host.OrbitNodeKey, + EncryptionKey: []byte("WILL-FAIL"), + }, http.StatusBadRequest) + msg := extractServerErrorText(res.Body) + require.Contains(t, msg, "host is not enrolled with fleet") + + // mark it as enrolled in Fleet + err := s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet) + require.NoError(t, err) + + // set its encryption key + s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{ + OrbitNodeKey: *host.OrbitNodeKey, + EncryptionKey: []byte("ABC"), + }, http.StatusNoContent) + + hdek, err := s.ds.GetHostDiskEncryptionKey(ctx, host.ID) + require.NoError(t, err) + require.NotNil(t, hdek.Decryptable) + require.True(t, *hdek.Decryptable) + + // the key is encrypted the same way as the macOS keys (except with the WSTEP + // certificate), so it can be decrypted using the same decryption function. + wstepCert, _, _, err := s.fleetCfg.MDM.MicrosoftWSTEP() + require.NoError(t, err) + decrypted, err := servermdm.DecryptBase64CMS(hdek.Base64Encrypted, wstepCert.Leaf, wstepCert.PrivateKey) + require.NoError(t, err) + require.Equal(t, "ABC", string(decrypted)) + + // set it with a client error + s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{ + OrbitNodeKey: *host.OrbitNodeKey, + ClientError: "fail", + }, http.StatusNoContent) + + hdek, err = s.ds.GetHostDiskEncryptionKey(ctx, host.ID) + require.NoError(t, err) + require.Nil(t, hdek.Decryptable) + require.Empty(t, hdek.Base64Encrypted) + + // set a different key + s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{ + OrbitNodeKey: *host.OrbitNodeKey, + EncryptionKey: []byte("DEF"), + }, http.StatusNoContent) + + hdek, err = s.ds.GetHostDiskEncryptionKey(ctx, host.ID) + require.NoError(t, err) + require.NotNil(t, hdek.Decryptable) + require.True(t, *hdek.Decryptable) + + decrypted, err = servermdm.DecryptBase64CMS(hdek.Base64Encrypted, wstepCert.Leaf, wstepCert.PrivateKey) + require.NoError(t, err) + require.Equal(t, "DEF", string(decrypted)) +} + // /////////////////////////////////////////////////////////////////////////// // Common helpers @@ -6923,7 +7419,7 @@ func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, de } // TODO: Add support to add custom DeviceID when DeviceAuth is in place -func (s *integrationMDMTestSuite) newSyncMLSessionMsg(managementUrl string) ([]byte, error) { +func (s *integrationMDMTestSuite) newSyncMLSessionMsg(deviceID string, managementUrl string) ([]byte, error) { if len(managementUrl) == 0 { return nil, errors.New("managementUrl is empty") } @@ -6939,7 +7435,7 @@ func (s *integrationMDMTestSuite) newSyncMLSessionMsg(managementUrl string) ([]b ` + managementUrl + ` - DB257C3A08778F4FB61E2749066C1F27 + ` + deviceID + ` @@ -6963,7 +7459,7 @@ func (s *integrationMDMTestSuite) newSyncMLSessionMsg(managementUrl string) ([]b ./DevInfo/DevId - DB257C3A08778F4FB61E2749066C1F27 + ` + deviceID + ` diff --git a/server/service/mdm.go b/server/service/mdm.go index f0312e417..7632eb4cf 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -404,3 +404,61 @@ func (svc *Service) VerifyMDMWindowsConfigured(ctx context.Context) error { return nil } + +//////////////////////////////////////////////////////////////////////////////// +// Apple or Windows MDM Middleware +//////////////////////////////////////////////////////////////////////////////// + +func (svc *Service) VerifyMDMAppleOrWindowsConfigured(ctx context.Context) error { + appCfg, err := svc.ds.AppConfig(ctx) + if err != nil { + // skipauth: Authorization is currently for user endpoints only. + svc.authz.SkipAuthorization(ctx) + return err + } + + // Apple or Windows MDM configuration setting + if !appCfg.MDM.EnabledAndConfigured && !appCfg.MDM.WindowsEnabledAndConfigured { + // skipauth: Authorization is currently for user endpoints only. + svc.authz.SkipAuthorization(ctx) + return fleet.ErrMDMNotConfigured + } + + return nil +} + +//////////////////////////////////////////////////////////////////////////////// +// GET /mdm/disk_encryption/summary +//////////////////////////////////////////////////////////////////////////////// + +type getMDMDiskEncryptionSummaryRequest struct { + TeamID *uint `query:"team_id,optional"` +} + +type getMDMDiskEncryptionSummaryResponse struct { + *fleet.MDMDiskEncryptionSummary + Err error `json:"error,omitempty"` +} + +func (r getMDMDiskEncryptionSummaryResponse) error() error { return r.Err } + +func getMDMDiskEncryptionSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*getMDMDiskEncryptionSummaryRequest) + + des, err := svc.GetMDMDiskEncryptionSummary(ctx, req.TeamID) + if err != nil { + return getMDMDiskEncryptionSummaryResponse{Err: err}, nil + } + + return &getMDMDiskEncryptionSummaryResponse{ + MDMDiskEncryptionSummary: des, + }, nil +} + +func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*fleet.MDMDiskEncryptionSummary, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index 13809587e..f395d26a9 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -15,8 +15,10 @@ import ( "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/config" authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz" + "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/micromdm/scep/v2/cryptoutil/x509util" "github.com/stretchr/testify/require" @@ -111,6 +113,12 @@ func TestVerifyMDMAppleConfigured(t *testing.T) { ds.AppConfigFuncInvoked = false require.True(t, authzCtx.Checked()) + err = svc.VerifyMDMAppleOrWindowsConfigured(ctx) + require.ErrorIs(t, err, fleet.ErrMDMNotConfigured) + require.True(t, ds.AppConfigFuncInvoked) + ds.AppConfigFuncInvoked = false + require.True(t, authzCtx.Checked()) + // error retrieving app config authzCtx = &authz_ctx.AuthorizationContext{} ctx = authz_ctx.NewContext(baseCtx, authzCtx) @@ -124,6 +132,12 @@ func TestVerifyMDMAppleConfigured(t *testing.T) { ds.AppConfigFuncInvoked = false require.True(t, authzCtx.Checked()) + err = svc.VerifyMDMAppleOrWindowsConfigured(ctx) + require.ErrorIs(t, err, testErr) + require.True(t, ds.AppConfigFuncInvoked) + ds.AppConfigFuncInvoked = false + require.True(t, authzCtx.Checked()) + // mdm configured authzCtx = &authz_ctx.AuthorizationContext{} ctx = authz_ctx.NewContext(baseCtx, authzCtx) @@ -135,9 +149,14 @@ func TestVerifyMDMAppleConfigured(t *testing.T) { require.True(t, ds.AppConfigFuncInvoked) ds.AppConfigFuncInvoked = false require.False(t, authzCtx.Checked()) + + err = svc.VerifyMDMAppleOrWindowsConfigured(ctx) + require.NoError(t, err) + require.True(t, ds.AppConfigFuncInvoked) + ds.AppConfigFuncInvoked = false + require.False(t, authzCtx.Checked()) } -// TODO: update this test with the correct config option func TestVerifyMDMWindowsConfigured(t *testing.T) { ds := new(mock.Store) license := &fleet.LicenseInfo{Tier: fleet.TierPremium} @@ -148,7 +167,7 @@ func TestVerifyMDMWindowsConfigured(t *testing.T) { authzCtx := &authz_ctx.AuthorizationContext{} ctx := authz_ctx.NewContext(baseCtx, authzCtx) ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { - return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: false}}, nil + return &fleet.AppConfig{MDM: fleet.MDM{WindowsEnabledAndConfigured: false}}, nil } err := svc.VerifyMDMWindowsConfigured(ctx) @@ -157,6 +176,12 @@ func TestVerifyMDMWindowsConfigured(t *testing.T) { ds.AppConfigFuncInvoked = false require.True(t, authzCtx.Checked()) + err = svc.VerifyMDMAppleOrWindowsConfigured(ctx) + require.ErrorIs(t, err, fleet.ErrMDMNotConfigured) + require.True(t, ds.AppConfigFuncInvoked) + ds.AppConfigFuncInvoked = false + require.True(t, authzCtx.Checked()) + // error retrieving app config authzCtx = &authz_ctx.AuthorizationContext{} ctx = authz_ctx.NewContext(baseCtx, authzCtx) @@ -171,6 +196,12 @@ func TestVerifyMDMWindowsConfigured(t *testing.T) { ds.AppConfigFuncInvoked = false require.True(t, authzCtx.Checked()) + err = svc.VerifyMDMAppleOrWindowsConfigured(ctx) + require.ErrorIs(t, err, testErr) + require.True(t, ds.AppConfigFuncInvoked) + ds.AppConfigFuncInvoked = false + require.True(t, authzCtx.Checked()) + // mdm configured authzCtx = &authz_ctx.AuthorizationContext{} ctx = authz_ctx.NewContext(baseCtx, authzCtx) @@ -183,6 +214,12 @@ func TestVerifyMDMWindowsConfigured(t *testing.T) { require.True(t, ds.AppConfigFuncInvoked) ds.AppConfigFuncInvoked = false require.False(t, authzCtx.Checked()) + + err = svc.VerifyMDMAppleOrWindowsConfigured(ctx) + require.NoError(t, err) + require.True(t, ds.AppConfigFuncInvoked) + ds.AppConfigFuncInvoked = false + require.False(t, authzCtx.Checked()) } func TestMicrosoftWSTEPConfig(t *testing.T) { @@ -195,7 +232,7 @@ func TestMicrosoftWSTEPConfig(t *testing.T) { ds.WSTEPStoreCertificateFunc = func(ctx context.Context, name string, crt *x509.Certificate) error { require.Equal(t, "test-client", name) require.Equal(t, "test-client", crt.Subject.CommonName) - require.Equal(t, "FleetDM", crt.Subject.OrganizationalUnit[0]) + require.Equal(t, "Fleet", crt.Subject.OrganizationalUnit[0]) return nil } @@ -251,5 +288,175 @@ func TestMicrosoftWSTEPConfig(t *testing.T) { parsedCert, err := x509.ParseCertificate(rawDER) require.NoError(t, err) require.Equal(t, "test-client", parsedCert.Subject.CommonName) - require.Equal(t, "FleetDM", parsedCert.Subject.OrganizationalUnit[0]) + require.Equal(t, "Fleet", parsedCert.Subject.OrganizationalUnit[0]) +} + +func TestMDMCommonAuthorization(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + + ds.GetMDMAppleFileVaultSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) { + return &fleet.MDMAppleFileVaultSummary{}, nil + } + ds.GetMDMWindowsBitLockerSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) { + return &fleet.MDMWindowsBitLockerSummary{}, nil + } + + mockTeamFuncWithUser := func(u *fleet.User) mock.TeamFunc { + return func(ctx context.Context, teamID uint) (*fleet.Team, error) { + if len(u.Teams) > 0 { + for _, t := range u.Teams { + if t.ID == teamID { + return &fleet.Team{ID: teamID, Users: []fleet.TeamUser{{User: *u, Role: t.Role}}}, nil + } + } + } + return &fleet.Team{}, nil + } + } + + testCases := []struct { + name string + user *fleet.User + shouldFailGlobal bool + shouldFailTeam bool + }{ + { + "global admin", + &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}, + false, + false, + }, + { + "global maintainer", + &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)}, + false, + false, + }, + { + "global observer", + &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)}, + true, + true, + }, + { + "team admin, belongs to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}}, + true, + false, + }, + { + "team admin, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleAdmin}}}, + true, + true, + }, + { + "team maintainer, belongs to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}}, + true, + false, + }, + { + "team maintainer, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleMaintainer}}}, + true, + true, + }, + { + "team observer, belongs to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}}, + true, + true, + }, + { + "team observer, DOES NOT belong to team", + &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 2}, Role: fleet.RoleObserver}}}, + true, + true, + }, + { + "user no roles", + &fleet.User{ID: 1337}, + true, + true, + }, + } + + checkShouldFail := func(err error, shouldFail bool) { + if !shouldFail { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), authz.ForbiddenErrorMessage) + } + } + + for _, tt := range testCases { + ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user}) + ds.TeamFunc = mockTeamFuncWithUser(tt.user) + + t.Run(tt.name, func(t *testing.T) { + // test authz get disk encryptions summary (no team) + _, err := svc.GetMDMDiskEncryptionSummary(ctx, nil) + checkShouldFail(err, tt.shouldFailGlobal) + + // test authz get disk encryptions summary (team 1) + _, err = svc.GetMDMDiskEncryptionSummary(ctx, ptr.Uint(1)) + checkShouldFail(err, tt.shouldFailTeam) + }) + } +} + +func TestGetMDMDiskEncryptionSummary(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license}) + + ctx = test.UserContext(ctx, test.UserAdmin) + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil + } + ds.GetMDMAppleFileVaultSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleFileVaultSummary, error) { + require.Nil(t, teamID) + return &fleet.MDMAppleFileVaultSummary{Verified: 1, Verifying: 2, ActionRequired: 3, Failed: 4, Enforcing: 5, RemovingEnforcement: 6}, nil + } + ds.GetMDMWindowsBitLockerSummaryFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) { + require.Nil(t, teamID) + // Use default zeros verifying, action_required, or removing_enforcement + return &fleet.MDMWindowsBitLockerSummary{Verified: 7, Failed: 8, Enforcing: 9}, nil + } + + // Test that the summary properly combines the results of the two methods + des, err := svc.GetMDMDiskEncryptionSummary(ctx, nil) + require.NoError(t, err) + require.NotNil(t, des) + require.Equal(t, *des, fleet.MDMDiskEncryptionSummary{ + Verified: fleet.MDMPlatformsCounts{ + MacOS: 1, + Windows: 7, + }, + Verifying: fleet.MDMPlatformsCounts{ + MacOS: 2, + Windows: 0, + }, + ActionRequired: fleet.MDMPlatformsCounts{ + MacOS: 3, + Windows: 0, + }, + Failed: fleet.MDMPlatformsCounts{ + MacOS: 4, + Windows: 8, + }, + Enforcing: fleet.MDMPlatformsCounts{ + MacOS: 5, + Windows: 9, + }, + RemovingEnforcement: fleet.MDMPlatformsCounts{ + MacOS: 6, + Windows: 0, + }, + }) } diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 071c5ca3b..2ebaf1bfc 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -13,6 +13,7 @@ import ( "io" "net/http" "net/url" + "regexp" "strconv" "strings" "text/template" @@ -1184,6 +1185,29 @@ func (svc *Service) GetMDMWindowsTOSContent(ctx context.Context, redirectUri str return htmlBuf.String(), nil } +// isValidUPN checks if the provided user ID is a valid UPN +func isValidUPN(userID string) bool { + const upnRegex = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + re := regexp.MustCompile(upnRegex) + return re.MatchString(userID) +} + +// isDeviceProgrammaticallyEnrolled checks if the device was enrolled through programmatic flow +func (svc *Service) isDeviceProgrammaticallyEnrolled(ctx context.Context, deviceID string) (bool, error) { + enrolledDevice, err := svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID) + + if err != nil || enrolledDevice == nil { + return false, errors.New("device not found") + } + + // If user identity is a MS-MDM UPN it means that the device was enrolled through user-driven flow + if isValidUPN(enrolledDevice.MDMEnrollUserID) { + return false, nil + } + + return true, nil +} + func (svc *Service) getManagementResponse(ctx context.Context, reqSyncML *fleet.SyncMLMessage) (*string, error) { if reqSyncML == nil { return nil, fleet.NewInvalidArgumentError("syncml req message", "message is not present") @@ -1212,9 +1236,15 @@ func (svc *Service) getManagementResponse(ctx context.Context, reqSyncML *fleet. return nil, err } + // Checking if the device was enrolled through programmatic flow + isProgrammaticEnrollment, err := svc.isDeviceProgrammaticallyEnrolled(ctx, deviceID) + if err != nil { + return nil, err + } + // Checking the SyncML message types var response string - if isSessionInitializationMessage(reqSyncML.Body) { + if isSessionInitializationMessage(reqSyncML.Body) && !isProgrammaticEnrollment { // Create response payload - MDM SyncML configuration profiles commands will be enforced here response = ` diff --git a/server/service/middleware/mdmconfigured/mdmconfigured.go b/server/service/middleware/mdmconfigured/mdmconfigured.go index be6b5ddc4..75343ad65 100644 --- a/server/service/middleware/mdmconfigured/mdmconfigured.go +++ b/server/service/middleware/mdmconfigured/mdmconfigured.go @@ -17,6 +17,18 @@ func NewMDMConfigMiddleware(svc fleet.Service) *Middleware { return &Middleware{svc: svc} } +func (m *Middleware) VerifyAppleOrWindowsMDM() endpoint.Middleware { + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req interface{}) (interface{}, error) { + if err := m.svc.VerifyMDMAppleOrWindowsConfigured(ctx); err != nil { + return nil, err + } + + return next(ctx, req) + } + } +} + func (m *Middleware) VerifyAppleMDM() endpoint.Middleware { return func(next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, req interface{}) (interface{}, error) { diff --git a/server/service/middleware/mdmconfigured/mdmconfigured_test.go b/server/service/middleware/mdmconfigured/mdmconfigured_test.go index 7ac09b6bf..2c6a01337 100644 --- a/server/service/middleware/mdmconfigured/mdmconfigured_test.go +++ b/server/service/middleware/mdmconfigured/mdmconfigured_test.go @@ -2,6 +2,7 @@ package mdmconfigured import ( "context" + "fmt" "sync/atomic" "testing" @@ -32,6 +33,13 @@ func (m *mockService) VerifyMDMWindowsConfigured(ctx context.Context) error { return nil } +func (m *mockService) VerifyMDMAppleOrWindowsConfigured(ctx context.Context) error { + if !m.mdmConfigured.Load() && !m.msMdmConfigured.Load() { + return fleet.ErrMDMNotConfigured + } + return nil +} + func TestMDMConfigured(t *testing.T) { svc := mockService{} svc.mdmConfigured.Store(true) @@ -99,3 +107,49 @@ func TestWindowsMDMNotConfigured(t *testing.T) { require.ErrorIs(t, err, fleet.ErrMDMNotConfigured) require.False(t, nextCalled) } + +func TestAppleOrWindowsMDMConfigured(t *testing.T) { + svc := mockService{} + mw := NewMDMConfigMiddleware(&svc) + + cases := []struct { + apple bool + windows bool + }{ + {true, false}, + {false, true}, + {true, true}, + } + for _, c := range cases { + t.Run(fmt.Sprintf("apple:%t;windows:%t", c.apple, c.windows), func(t *testing.T) { + svc.mdmConfigured.Store(c.apple) + svc.msMdmConfigured.Store(c.windows) + nextCalled := false + next := func(ctx context.Context, req interface{}) (interface{}, error) { + nextCalled = true + return struct{}{}, nil + } + + f := mw.VerifyAppleOrWindowsMDM()(next) + _, err := f(context.Background(), struct{}{}) + require.NoError(t, err) + require.True(t, nextCalled) + }) + } +} + +func TestAppleOrWindowsMDMNotConfigured(t *testing.T) { + svc := mockService{} + mw := NewMDMConfigMiddleware(&svc) + + nextCalled := false + next := func(ctx context.Context, req interface{}) (interface{}, error) { + nextCalled = true + return struct{}{}, nil + } + + f := mw.VerifyAppleOrWindowsMDM()(next) + _, err := f(context.Background(), struct{}{}) + require.ErrorIs(t, err, fleet.ErrMDMNotConfigured) + require.False(t, nextCalled) +} diff --git a/server/service/orbit.go b/server/service/orbit.go index 28df989cd..40a0bac9f 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -14,6 +14,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/ptr" "github.com/go-kit/kit/log/level" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" @@ -270,6 +271,12 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro } } + if config.IsMDMFeatureFlagEnabled() && + mdmConfig.EnableDiskEncryption && + host.IsEligibleForBitLockerEncryption() { + notifs.EnforceBitLockerEncryption = true + } + return fleet.OrbitConfig{ Flags: opts.CommandLineStartUpFlags, Extensions: extensionsFiltered, @@ -300,6 +307,13 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro } } + if appConfig.MDM.WindowsEnabledAndConfigured && + config.IsMDMFeatureFlagEnabled() && + appConfig.MDM.EnableDiskEncryption.Value && + host.IsEligibleForBitLockerEncryption() { + notifs.EnforceBitLockerEncryption = true + } + return fleet.OrbitConfig{ Flags: opts.CommandLineStartUpFlags, Extensions: extensionsFiltered, @@ -503,3 +517,82 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host return fleet.ErrMissingLicense } + +///////////////////////////////////////////////////////////////////////////////// +// Post Orbit disk encryption key +///////////////////////////////////////////////////////////////////////////////// + +type orbitPostDiskEncryptionKeyRequest struct { + OrbitNodeKey string `json:"orbit_node_key"` + EncryptionKey []byte `json:"encryption_key"` + ClientError string `json:"client_error"` +} + +// interface implementation required by the OrbitClient +func (r *orbitPostDiskEncryptionKeyRequest) setOrbitNodeKey(nodeKey string) { + r.OrbitNodeKey = nodeKey +} + +// interface implementation required by orbit authentication +func (r *orbitPostDiskEncryptionKeyRequest) orbitHostNodeKey() string { + return r.OrbitNodeKey +} + +type orbitPostDiskEncryptionKeyResponse struct { + Err error `json:"error,omitempty"` +} + +func (r orbitPostDiskEncryptionKeyResponse) error() error { return r.Err } +func (r orbitPostDiskEncryptionKeyResponse) Status() int { return http.StatusNoContent } + +func postOrbitDiskEncryptionKeyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*orbitPostDiskEncryptionKeyRequest) + if err := svc.SetOrUpdateDiskEncryptionKey(ctx, string(req.EncryptionKey), req.ClientError); err != nil { + return orbitPostDiskEncryptionKeyResponse{Err: err}, nil + } + return orbitPostDiskEncryptionKeyResponse{}, nil +} + +func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryptionKey, clientError string) error { + // this is not a user-authenticated endpoint + svc.authz.SkipAuthorization(ctx) + + host, ok := hostctx.FromContext(ctx) + if !ok { + return newOsqueryError("internal error: missing host from request context") + } + if !host.MDMInfo.IsFleetEnrolled() { + return badRequest("host is not enrolled with fleet") + } + + var ( + encryptedEncryptionKey string + decryptable *bool + ) + + // only set the encryption key if there was no client error + if clientError == "" && encryptionKey != "" { + wstepCert, _, _, err := svc.config.MDM.MicrosoftWSTEP() + if err != nil { + // should never return an error because the WSTEP is first parsed and + // cached at the start of the fleet serve process. + return ctxerr.Wrap(ctx, err, "get WSTEP certificate") + } + enc, err := microsoft_mdm.Encrypt(encryptionKey, wstepCert.Leaf) + if err != nil { + return ctxerr.Wrap(ctx, err, "encrypt the key with WSTEP certificate") + } + encryptedEncryptionKey = enc + decryptable = ptr.Bool(true) + } + + if err := svc.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, encryptedEncryptionKey, clientError, decryptable); err != nil { + return ctxerr.Wrap(ctx, err, "set or update disk encryption key") + } + if encryptedEncryptionKey != "" { + if err := svc.ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, true); err != nil { + return ctxerr.Wrap(ctx, err, "set or update host disks encryption") + } + } + return nil +} diff --git a/server/service/orbit_client.go b/server/service/orbit_client.go index b4a8ca3e6..b7374a58f 100644 --- a/server/service/orbit_client.go +++ b/server/service/orbit_client.go @@ -338,3 +338,18 @@ func OrbitRetryInterval() time.Duration { } return constant.OrbitEnrollRetrySleep } + +// SetOrUpdateDiskEncryptionKey sends a request to the server to set or update the disk +// encryption keys and result of the encryption process +func (oc *OrbitClient) SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error { + verb, path := "POST", "/api/fleet/orbit/disk_encryption_key" + + var resp orbitPostDiskEncryptionKeyResponse + if err := oc.authenticatedRequest(verb, path, &orbitPostDiskEncryptionKeyRequest{ + EncryptionKey: diskEncryptionStatus.EncryptionKey, + ClientError: diskEncryptionStatus.ClientError, + }, &resp); err != nil { + return err + } + return nil +} diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 9887a1243..165b7e2cc 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -446,7 +446,7 @@ var extraDetailQueries = map[string]DetailQuery{ ) UNION ALL SELECT * FROM ( - SELECT "is_federated" AS "key", data as "value" FROM registry + SELECT "is_federated" AS "key", data as "value" FROM registry WHERE path LIKE 'HKEY_LOCAL_MACHINE\Software\Microsoft\Enrollments\%\IsFederated' LIMIT 1 ) @@ -609,7 +609,7 @@ var mdmQueries = map[string]DetailQuery{ // [1]: https://developer.apple.com/documentation/devicemanagement/fderecoverykeyescrow "mdm_disk_encryption_key_file_lines_darwin": { Query: fmt.Sprintf(` - WITH + 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), @@ -1460,7 +1460,7 @@ func directIngestDiskEncryptionKeyFileDarwin( // 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, rows[0]["filevault_key"]) + return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, rows[0]["filevault_key"], "", nil) } // directIngestDiskEncryptionKeyFileLinesDarwin ingests the FileVault key from the `file_lines` @@ -1511,7 +1511,7 @@ func directIngestDiskEncryptionKeyFileLinesDarwin( // 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)) + return ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64.StdEncoding.EncodeToString(b), "", nil) } func directIngestMacOSProfiles( diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 2c9e3d60e..6e4224625 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -1130,7 +1130,7 @@ func TestDirectIngestDiskEncryptionKeyDarwin(t *testing.T) { } } - ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint, encryptedBase64Key string) error { + ds.SetOrUpdateHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error { if base64.StdEncoding.EncodeToString([]byte(wantKey)) != encryptedBase64Key { return errors.New("key mismatch") } diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index cfd3365aa..7b90c43c0 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -586,7 +586,7 @@ func mockSuccessfulPush(pushes []*mdm.Push) (map[string]*push.Response, error) { return res, nil } -func mdmAppleConfigurationRequiredEndpoints() []struct { +func mdmConfigurationRequiredEndpoints() []struct { method, path string deviceAuthenticated bool premiumOnly bool @@ -630,6 +630,8 @@ func mdmAppleConfigurationRequiredEndpoints() []struct { {"POST", "/api/latest/fleet/device/%s/migrate_mdm", true, true}, {"POST", "/api/latest/fleet/mdm/apple/profiles/preassign", false, true}, {"POST", "/api/latest/fleet/mdm/apple/profiles/match", false, true}, + {"POST", "/api/fleet/orbit/disk_encryption_key", false, false}, + {"GET", "/api/latest/fleet/mdm/disk_encryption/summary", false, true}, } } diff --git a/server/service/transport.go b/server/service/transport.go index c9caee032..4170695e1 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -317,9 +317,9 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error) } macOSSettingsStatus := r.URL.Query().Get("macos_settings") - switch fleet.MacOSSettingsStatus(macOSSettingsStatus) { - case fleet.MacOSSettingsFailed, fleet.MacOSSettingsPending, fleet.MacOSSettingsVerifying, fleet.MacOSSettingsVerified: - hopt.MacOSSettingsFilter = fleet.MacOSSettingsStatus(macOSSettingsStatus) + switch fleet.OSSettingsStatus(macOSSettingsStatus) { + case fleet.OSSettingsFailed, fleet.OSSettingsPending, fleet.OSSettingsVerifying, fleet.OSSettingsVerified: + hopt.MacOSSettingsFilter = fleet.OSSettingsStatus(macOSSettingsStatus) case "": // No error when unset default: @@ -342,6 +342,32 @@ func hostListOptionsFromRequest(r *http.Request) (fleet.HostListOptions, error) return hopt, ctxerr.Errorf(r.Context(), "invalid macos_settings_disk_encryption status %s", macOSSettingsDiskEncryptionStatus) } + osSettingsStatus := r.URL.Query().Get("os_settings") + switch fleet.OSSettingsStatus(osSettingsStatus) { + case fleet.OSSettingsFailed, fleet.OSSettingsPending, fleet.OSSettingsVerifying, fleet.OSSettingsVerified: + hopt.OSSettingsFilter = fleet.OSSettingsStatus(osSettingsStatus) + case "": + // No error when unset + default: + return hopt, ctxerr.Errorf(r.Context(), "invalid os_settings status %s", osSettingsStatus) + } + + osSettingsDiskEncryptionStatus := r.URL.Query().Get("os_settings_disk_encryption") + switch fleet.DiskEncryptionStatus(osSettingsDiskEncryptionStatus) { + case + fleet.DiskEncryptionVerifying, + fleet.DiskEncryptionVerified, + fleet.DiskEncryptionActionRequired, + fleet.DiskEncryptionEnforcing, + fleet.DiskEncryptionFailed, + fleet.DiskEncryptionRemovingEnforcement: + hopt.OSSettingsDiskEncryptionFilter = fleet.DiskEncryptionStatus(osSettingsDiskEncryptionStatus) + case "": + // No error when unset + default: + return hopt, ctxerr.Errorf(r.Context(), "invalid os_settings_disk_encryption status %s", macOSSettingsDiskEncryptionStatus) + } + mdmBootstrapPackageStatus := r.URL.Query().Get("bootstrap_package") switch fleet.MDMBootstrapPackageStatus(mdmBootstrapPackageStatus) { case fleet.MDMBootstrapPackageFailed, fleet.MDMBootstrapPackagePending, fleet.MDMBootstrapPackageInstalled: