diff --git a/.github/workflows/build-orbit.yaml b/.github/workflows/build-orbit.yaml index 41ec1816c1be..229b6612b778 100644 --- a/.github/workflows/build-orbit.yaml +++ b/.github/workflows/build-orbit.yaml @@ -2,9 +2,15 @@ name: Build, Sign and Notarize Orbit for macOS on: workflow_dispatch: # allow manual action + push: + paths: + # The workflow can be triggered by modifying ORBIT_VERSION env. + - '.github/workflows/build-orbit.yaml' pull_request: paths: - 'orbit/**.go' + # The workflow can be triggered by modifying ORBIT_VERSION env. + - '.github/workflows/build-orbit.yaml' env: ORBIT_VERSION: 1.17.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c28a4c9857..c18bd7fe755c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Fleet 4.38.1 (Oct 5, 2023) + +### Bug Fixes + +* Fixed a bug that would cause live queries to stall if a detail query override was set for a team. + ## Fleet 4.38.0 (Sep 25, 2023) ### Changes diff --git a/CODEOWNERS b/CODEOWNERS index 4a7bc630f41f..b952d8777f0f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,7 +72,6 @@ go.mod @fleetdm/go # # (see website/config/custom.js for DRIs of other paths not listed here) ############################################################################################## -/website/views/pages/pricing.ejs @mikermcneil # « CEO is DRI for pricing /handbook/company/pricing-features-table.yml @mikermcneil # « CEO is current DRI for features table ############################################################################################## diff --git a/articles/fleet-4.37.0.md b/articles/fleet-4.37.0.md index 0f4cf5ed80f8..f3a2c7c37bac 100644 --- a/articles/fleet-4.37.0.md +++ b/articles/fleet-4.37.0.md @@ -1,4 +1,4 @@ -# Fleet 4.37.0 | Remote script execution & Puppet support. +# Fleet 4.37.0 | Puppet support. ![Fleet 4.37.0](../website/assets/images/articles/fleet-4.37.0-1600x900@2x.png) @@ -13,11 +13,12 @@ For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deplo * Puppet support * Web user interface improvements + ### Vulnerability dashboard diff --git a/articles/introducing-cross-platform-script-execution.md b/articles/introducing-cross-platform-script-execution similarity index 100% rename from articles/introducing-cross-platform-script-execution.md rename to articles/introducing-cross-platform-script-execution diff --git a/changes/12927-disk-encryption-settings b/changes/12927-disk-encryption-settings new file mode 100644 index 000000000000..a9464b7d5bac --- /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 000000000000..0ce9b45e8a1b --- /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 000000000000..ccb11df8b74e --- /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/bug-13894-failing-policies-styling b/changes/bug-13894-failing-policies-styling new file mode 100644 index 000000000000..5bd83a7b0961 --- /dev/null +++ b/changes/bug-13894-failing-policies-styling @@ -0,0 +1 @@ +* Fix styling for host details/device user failing policies call out \ No newline at end of file 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 000000000000..728d93122e02 --- /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 000000000000..82767942ec0b --- /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 000000000000..0705f8e974f6 --- /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/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index e9d974fbcbeb..655c606998bb 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -8,4 +8,4 @@ version: v5.0.1 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.38.0 +appVersion: v4.38.1 diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index d9394eea3407..4c01fb7ab5e7 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -2,7 +2,7 @@ # All settings related to how Fleet is deployed in Kubernetes hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy -imageTag: v4.38.0 # Version of Fleet to deploy +imageTag: v4.38.1 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 4835eb3e1930..50120b0434e0 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 395fe0a6cdc2..8110239cac80 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 3136dfd5b6a9..baa6cf5c613e 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 a57d06413bd2..187264987480 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 e6ae712e28c2..2f0c98c7408e 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 1c0d7786853f..e7a5843214ce 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 2030db5afecb..94c6e70a7795 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 9d3bf00ace46..1b03fe13d3e4 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 19152a690cd9..6a99943e9440 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 2b571ae8b5bb..a6905cf569ee 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 4fc311a8dd21..d641e98b0a1f 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 72b5d2c59969..433d80c586e8 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 346bbc2eb753..a3668e64b391 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 45f1733019c1..95e49d032146 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 21d9b9d8db20..8ad10fc6c51a 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/Configuration/configuration-files/README.md b/docs/Configuration/configuration-files/README.md index 9cc12b3321ac..8b8f34b8cf62 100644 --- a/docs/Configuration/configuration-files/README.md +++ b/docs/Configuration/configuration-files/README.md @@ -529,8 +529,10 @@ Use with caution as this may break Fleet ingestion of hosts data. ```yaml features: detail_query_overrides: - # null allows to disable the "users" query from running on hosts. + # null disables the "users" query from running on hosts. users: null + # "" disables the "disk_encryption_linux" query from running on hosts. + disk_encryption_linux: "" # this replaces the hardcoded "mdm" detail query. mdm: "SELECT enrolled, server_url, installed_from_dep, payload_identifier FROM mdm;" ``` diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md index 7724b770b74e..ddefdb2e9a53 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 3d3f69c92bfa..c7522aa537bc 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/Get started/anatomy.md b/docs/Get started/anatomy.md index 757c9614a148..7bc314d2454b 100644 --- a/docs/Get started/anatomy.md +++ b/docs/Get started/anatomy.md @@ -12,7 +12,7 @@ Fleetctl (pronouced “fleet control”) is a CLI (command line interface) tool ## Fleetd -Fleetd is a bundle of agents provided by Fleet to gather information about your devices. Fleetd includes [osquery](https://www.osquery.io/), Orbit, and Fleet Desktop. [Docs](https://fleetdm.com/docs/using-fleet/fleet-ui). +Fleetd is a bundle of agents provided by Fleet to gather information about your devices. Fleetd includes [osquery](https://www.osquery.io/), Orbit, and Fleet Desktop. [Docs](https://fleetdm.com/docs/using-fleet/fleetd). ## Osquery Osquery is an open-source tool for gathering information about the state of any device that the osquery agent has been installed on. [Learn more](https://www.osquery.io/). diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index a40f5daa3950..753288389aa9 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}, } ``` @@ -6600,7 +6613,8 @@ Deletes the session specified by ID. When the user associated with the session n "epss_probability": 0.01537, "cisa_known_exploit": false, "cve_published": "2022-01-01 12:32:00", - "cve_description": "In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match." + "cve_description": "In the GNU C Library (aka glibc or libc6) before 2.28, parse_reg_exp in posix/regcomp.c misparses alternatives, which allows attackers to cause a denial of service (assertion failure and application exit) or trigger an incorrect result by attempting a regular-expression match.", + "resolved_in_version": "2.28" } ], "hosts_count": 1 diff --git a/docs/Using Fleet/CIS-Benchmarks.md b/docs/Using Fleet/CIS-Benchmarks.md index 5d92bc4607ad..632caf0ae665 100644 --- a/docs/Using Fleet/CIS-Benchmarks.md +++ b/docs/Using Fleet/CIS-Benchmarks.md @@ -170,127 +170,11 @@ The following CIS benchmark checks cannot be automated and must be addressed man Fleet's policies have been written against v1.12.0 of the benchmark. You can refer to the [CIS website](https://www.cisecurity.org/cis-benchmarks) for full details about this version. -### Checks that require a Group Policy Template +### Checks that require a Group Policy template -38 items require Group Policy Template in place in order to audit them. +Several items require Group Policy templates in place in order to audit them. These items are tagged with the label `CIS_group_policy_template_required` in the YAML file, and details about the required Group Policy templates can be found in each item's `resolution`. -``` -18.3.1 CIS - Ensure 'Apply UAC restrictions to local accounts on network logons' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\Apply UAC restrictions to local accounts on network logons' - -18.3.2 CIS - Ensure 'Configure SMB v1 client driver' is set to 'Enabled: Disable driver (recommended)' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\Configure SMB v1 client driver' - -18.3.3 CIS - Ensure 'Configure SMB v1 server' is set to 'Disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\Configure SMB v1 server' - -18.3.4 CIS - Ensure 'Enable Structured Exception Handling Overwrite Protection (SEHOP)' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\Enable Structured Exception Handling Overwrite Protection (SEHOP)' - -18.3.5 CIS - Ensure 'Limits print driver installation to Administrators' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\Limits print driver installation to Administrators' - -18.3.6 CIS - Ensure 'NetBT NodeType configuration' is set to 'Enabled: P-node (recommended)' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\NetBT NodeType configuration' - -18.3.7 CIS - Ensure 'WDigest Authentication' is set to 'Disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MS Security Guide\WDigest Authentication (disabling may require KB2871997)' - -18.4.1 CIS - Ensure 'MSS: (AutoAdminLogon) Enable Automatic Logon (not recommended)' is set to 'Disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (AutoAdminLogon) Enable Automatic Logon (not recommended)' - -18.4.2 CIS - Ensure 'MSS: (DisableIPSourceRouting IPv6) IP source routing protection level (protects against packet spoofing)' is set to 'Enabled: Highest protection, source routing is completely disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (DisableIPSourceRouting IPv6) IP source routing protection level (protects against packet spoofing)' - -18.4.3 CIS - Ensure 'MSS: (DisableIPSourceRouting) IP source routing protection level (protects against packet spoofing)' is set to 'Enabled: Highest protection, source routing is completely disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (DisableIPSourceRouting) IP source routing protection level (protects against packet spoofing)' - -18.4.4 CIS - Ensure 'MSS: (DisableSavePassword) Prevent the dial-up password from being saved' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS:(DisableSavePassword) Prevent the dial-up password from being saved' - -18.4.5 CIS - Ensure 'MSS: (EnableICMPRedirect) Allow ICMP redirects to override OSPF generated routes' is set to 'Disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (EnableICMPRedirect) Allow ICMP redirects to override OSPF generated routes' - -18.4.6 CIS - Ensure 'MSS: (KeepAliveTime) How often keep-alive packets are sent in milliseconds' is set to 'Enabled: 300,000 or 5 minutes' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (KeepAliveTime) How often keep-alive packets are sent in milliseconds' - -18.4.7 CIS - Ensure 'MSS: (NoNameReleaseOnDemand) Allow the computer to ignore NetBIOS name release requests except from WINS servers' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (NoNameReleaseOnDemand) Allow the computer to ignore NetBIOS name release requests except from WINS servers' - -18.4.8 CIS - Ensure 'MSS: (PerformRouterDiscovery) Allow IRDP to detect and configure Default Gateway addresses (could lead to DoS)' is set to 'Disabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (PerformRouterDiscovery) Allow IRDP to detect and configure Default Gateway addresses (could lead to DoS)' - -18.4.9 CIS - Ensure 'MSS: (SafeDllSearchMode) Enable Safe DLL search mode (recommended)' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (SafeDllSearchMode) Enable Safe DLL search mode (recommended)' - -18.4.10 CIS - Ensure 'MSS: (ScreenSaverGracePeriod) The time in seconds before the screen saver grace period expires (0 recommended)' is set to 'Enabled: 5 or fewer seconds' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (ScreenSaverGracePeriod) The time in seconds before the screen saver grace period expires (0 recommended)' - -18.4.11 CIS - Ensure 'MSS: (TcpMaxDataRetransmissions IPv6) How many times unacknowledged data is retransmitted' is set to 'Enabled: 3' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS:(TcpMaxDataRetransmissions IPv6) How many times unacknowledged data is retransmitted' - -18.4.12 CIS - Ensure 'MSS: (TcpMaxDataRetransmissions) How many times unacknowledged data is retransmitted' is set to 'Enabled: 3' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS:(TcpMaxDataRetransmissions) How many times unacknowledged data is retransmitted' - -18.4.13 CIS - Ensure 'MSS: (WarningLevel) Percentage threshold for the security event log at which the system will generate a warning' is set to 'Enabled: 90% or less' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\MSS (Legacy)\MSS: (WarningLevel) Percentage threshold for the security event log at which the system will generate a warning' - -18.8.21.2 CIS - Ensure 'Configure registry policy processing: Do not apply during periodic background processing' is set to 'Enabled: FALSE' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Group Policy\Configure registry policy processing' - -18.8.22.1.1 CIS - Ensure 'Turn off access to the Store' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off access to the Store' - -18.8.22.1.2 CIS - Ensure 'Turn off downloading of print drivers over HTTP' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off downloading of print drivers over HTTP' - -18.8.22.1.3 CIS - Ensure 'Turn off handwriting personalization data sharing' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off handwriting personalization data sharing' - -18.8.22.1.4 CIS - Ensure 'Turn off handwriting recognition error reporting' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off handwriting recognition error reporting' - -18.8.22.1.5 CIS - Ensure 'Turn off Internet Connection Wizard if URL connection is referring to Microsoft.com' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off Internet Connection Wizard if URL connection is referring to Microsoft.com' - -18.8.22.1.6 CIS - Ensure 'Turn off Internet download for Web publishing and online ordering wizards' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off Internet download for Web publishing and online ordering wizards' - -18.8.22.1.7 CIS - Ensure 'Turn off printing over HTTP' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off printing over HTTP' - -18.8.22.1.8 CIS - Ensure 'Turn off Registration if URL connection is referring to Microsoft.com' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off Registration if URL connection is referring to Microsoft.com' - -18.8.22.1.9 CIS - Ensure 'Turn off Search Companion content file updates' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off Search Companion content file updates' - -18.8.22.1.10 CIS - Ensure 'Turn off the "Order Prints" picture task' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off the "Order Prints" picture task' - -18.8.22.1.11 CIS - Ensure 'Turn off the "Publish to Web" task for files and folders' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off the "Publish to Web" task for files and folders' - -18.8.22.1.12 CIS - Ensure 'Turn off the Windows Messenger Customer Experience Improvement Program' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off the Windows Messenger Customer Experience Improvement Program' - -18.8.22.1.13 CIS - Ensure 'Turn off Windows Customer Experience Improvement Program' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off Windows Customer Experience Improvement Program' - -18.8.22.1.14 CIS - Ensure 'Turn off Windows Error Reporting' is set to 'Enabled' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Internet Communication Management\Internet Communication settings\Turn off Windows Error Reporting' - -18.8.25.1 CIS - Ensure 'Support device authentication using certificate' is set to 'Enabled: Automatic' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Kerberos\Support device authentication using certificate' - -18.8.26.1 CIS - Ensure 'Enumeration policy for external devices incompatible with Kernel DMA Protection' is set to 'Enabled: Block All' -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Kernel DMA Protection\Enumeration policy for external devices incompatible with Kernel DMA Protection' - -18.8.27.1 CIS - Ensure 'Disallow copying of user input methods to the system account for sign-in' is set to 'Enabled' (Automated) -Requires this GPO in place: 'Computer Configuration\Policies\Administrative Templates\System\Locale Services\Disallow copying of user input methods to the system account for sign-in' -``` - ## Performance testing In August 2023, we completed scale testing on 10k Windows hosts and 70k macOS hosts. Ultimately, we validated both server and host performance at that scale. diff --git a/docs/Using Fleet/manage-access.md b/docs/Using Fleet/manage-access.md index 48c1a2152600..90774c86e1a7 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 afd304a6c8f9..37cf53721821 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 33a777323f1b..529408de47b0 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 9f9e8f638cca..7b5131a2da52 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -126,6 +126,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 c2e0993649e2..6834d0c7037a 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 c0584bf66aaf..5ffebcdbc30d 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/InfoBanner/InfoBanner.tsx b/frontend/components/InfoBanner/InfoBanner.tsx index f1af1558b52c..1e02b38b6e21 100644 --- a/frontend/components/InfoBanner/InfoBanner.tsx +++ b/frontend/components/InfoBanner/InfoBanner.tsx @@ -3,6 +3,7 @@ import classNames from "classnames"; import Icon from "components/Icon"; import Button from "components/buttons/Button"; +import { IconNames } from "components/icons"; const baseClass = "info-banner"; @@ -11,28 +12,36 @@ export interface IInfoBannerProps { className?: string; /** default light purple */ color?: "purple" | "purple-bold-border" | "yellow" | "grey"; + /** default 4px */ + borderRadius?: "large" | "xlarge"; pageLevel?: boolean; /** cta and link are mutually exclusive */ cta?: JSX.Element; /** closable and link are mutually exclusive */ closable?: boolean; link?: string; + icon?: IconNames; } const InfoBanner = ({ children, className, color = "purple", + borderRadius, pageLevel, cta, closable, link, + icon, }: IInfoBannerProps): JSX.Element => { const wrapperClasses = classNames( baseClass, `${baseClass}__${color}`, { + [`${baseClass}__${color}`]: !!color, + [`${baseClass}__border-radius-${borderRadius}`]: !!borderRadius, [`${baseClass}__page-banner`]: !!pageLevel, + [`${baseClass}__icon`]: !!icon, }, className ); @@ -42,6 +51,7 @@ const InfoBanner = ({ const content = ( <>
{children}
+ {(cta || closable) && (
{cta} diff --git a/frontend/components/InfoBanner/_styles.scss b/frontend/components/InfoBanner/_styles.scss index f5977b57f5c3..81635a106270 100644 --- a/frontend/components/InfoBanner/_styles.scss +++ b/frontend/components/InfoBanner/_styles.scss @@ -34,6 +34,20 @@ width: auto; } + &__border-radius-large { + border-radius: $border-radius-large; + } + + &__border-radius-xlarge { + border-radius: $border-radius-xlarge; + } + + &__info { + display: flex; + flex-direction: column; + gap: $pad-small; + } + &__cta { display: flex; align-items: center; @@ -59,4 +73,8 @@ } } } + + p { + margin: 0; + } } diff --git a/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx b/frontend/components/StatusIndicatorWithIcon/StatusIndicatorWithIcon.tsx index e510748a7a35..95ae8d5659e6 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 6dea8de69713..12d60c0f2d63 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 1d42508ee4b2..cf15ccca6463 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; @@ -286,7 +203,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 1927351b7dd8..ebfde247f461 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 25f0f0d7ec8b..1120e1ab625c 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 3c06c11c36c8..4487dba48d86 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/LoginSuccessfulPage/_styles.scss b/frontend/pages/LoginSuccessfulPage/_styles.scss index 07e1429fa755..b3b962d3c9b9 100644 --- a/frontend/pages/LoginSuccessfulPage/_styles.scss +++ b/frontend/pages/LoginSuccessfulPage/_styles.scss @@ -3,7 +3,7 @@ margin-top: 20px; padding: $pad-xxlarge; text-align: center; - border-radius: 10px; + border-radius: $border-radius-xlarge; z-index: 0; align-self: center; transform: translateY(80px); diff --git a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/AggregateMacSettingsIndicators.tsx b/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/AggregateMacSettingsIndicators.tsx deleted file mode 100644 index 7c4b4906011b..000000000000 --- 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 1032f0ac5014..000000000000 --- 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 dddbffc04805..9eb28475ba7b 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 000000000000..8dbe94abf8fe --- /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 73% rename from frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss rename to frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss index 76659544939a..89744cf6cc71 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss +++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss @@ -1,16 +1,16 @@ -.aggregate-mac-settings-indicators { +.profile-status-aggregate { display: flex; height: 94px; border-top: 1px solid #e2e4ea; border-bottom: 1px solid #e2e4ea; border-left: 1px solid #e2e4ea; - border-radius: 6px; + border-radius: $border-radius-large; &__loading-spinner { 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 000000000000..a29bd5e10d5f --- /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 6edd4f35f5f2..c8cd1d2bc37b 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 068459d69cfb..9a5b7baf5dbe 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 ee3e1c025ea8..c2e35efe6b8b 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 545887a8f5d2..ce052c31e180 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 2fbaac795a75..b4102d321e83 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 ab6a3b4debcd..6eedf30627c1 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/CustomLabelGroupHeading/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss index 38a15997083d..d4cb6bed093e 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss @@ -33,7 +33,7 @@ line-height: 1.5; background-color: $ui-light-grey; border: solid 1px $ui-fleet-black-10; - border-radius: 4px; + border-radius: $border-radius; font-size: $small; padding: 9.5px 12px 9.5px 36px; color: $core-fleet-blue; @@ -70,7 +70,7 @@ color: $core-vibrant-red; border: 1px solid $core-vibrant-red; box-sizing: border-box; - border-radius: 4px; + border-radius: $border-radius; &:focus { border-color: $ui-error; diff --git a/frontend/pages/hosts/ManageHostsPage/components/DiskEncryptionStatusFilter/DiskEncryptionStatusFilter.tsx b/frontend/pages/hosts/ManageHostsPage/components/DiskEncryptionStatusFilter/DiskEncryptionStatusFilter.tsx index 2405fd6d151b..4e49123919f1 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/FilterPill/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss index a7badf53a1f1..8f4a9c76a377 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss @@ -4,7 +4,7 @@ align-items: center; padding: 6px 12px; border: 1px solid $ui-fleet-black-25; - border-radius: 4px; + border-radius: $border-radius; box-shadow: none; color: $core-fleet-black; font-size: $xx-small; diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index 0e64c52c744f..1ff7287cb3c8 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 285c0f8908ad..d5550026d577 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 0493579f5e11..48212d6a10f0 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -72,6 +72,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"; @@ -725,6 +726,7 @@ const HostDetailsPage = ({ showRefetchSpinner={showRefetchSpinner} onRefetchHost={onRefetchHost} renderActionButtons={renderActionButtons} + osSettings={host?.mdm.os_settings} /> @@ -857,12 +860,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 8acdc6622ebd..1046822673b2 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/HostDetailsPage/modals/OSPolicyModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/OSPolicyModal/_styles.scss index 7f2f5f1ff1b7..c0b6053f3043 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/OSPolicyModal/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/OSPolicyModal/_styles.scss @@ -42,7 +42,7 @@ &__copy-message { background-color: $ui-light-grey; border: solid 1px #e2e4ea; - border-radius: 10px; + border-radius: $border-radius-xlarge; padding: 2px 6px; } } diff --git a/frontend/pages/hosts/details/MacSettingsIndicator/index.ts b/frontend/pages/hosts/details/MacSettingsIndicator/index.ts deleted file mode 100644 index 47e97520648e..000000000000 --- 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 a055bae71536..eab9e92bbbe0 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 39bd021222b4..7b2364af255c 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 a71467903cec..73044dd21805 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 473745ee3344..f3bbe6cd024c 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 fce8265c95eb..a7ed40ad9155 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 000000000000..99de4100ca68 --- /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 f18194844c59..45be0eaba57b 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/cards/Policies/HostPoliciesTable/PolicyFailingCount/PolicyFailingCount.tsx b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/PolicyFailingCount.tsx index ecb1f1b7c5f1..24a184a0d12b 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/PolicyFailingCount.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/PolicyFailingCount.tsx @@ -2,6 +2,7 @@ import { IHostPolicy } from "interfaces/policy"; import React from "react"; import Icon from "components/Icon/Icon"; +import InfoBanner from "components/InfoBanner"; const baseClass = "policy-failing-count"; @@ -18,7 +19,7 @@ const PolicyFailingCount = ({ }, 0); return failCount ? ( -
+
This device is failing @@ -27,10 +28,10 @@ const PolicyFailingCount = ({

Click a policy below to see if there are steps you can take to resolve the issue - {failCount > 1 ? "s" : ""}.{" "} + {failCount > 1 ? "s" : ""}. {deviceUser && " Once resolved, click “Refetch” above to confirm."}

-
+
) : null; }; diff --git a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/_styles.scss b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/_styles.scss index f3543ccc47a6..36ac989fdb94 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/_styles.scss +++ b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyFailingCount/_styles.scss @@ -1,24 +1,11 @@ .policy-failing-count { - font-size: $x-small; - background-color: $ui-off-white; - border: solid 1px $ui-fleet-black-50; - box-sizing: border-box; - border-radius: 10px; - overflow: auto; - margin-bottom: $pad-large; - padding: $pad-large; - padding-bottom: $pad-small; - - p { - padding-left: $pad-large; - margin-top: $pad-medium; - margin-bottom: $pad-medium; - } - &__count { display: flex; - align-content: center; - align-items: center; font-weight: $bold; + gap: $pad-small; + } + + p { + margin-left: $pad-large; // Align second line with first line and not with icon } } diff --git a/frontend/pages/hosts/details/cards/Policies/_styles.scss b/frontend/pages/hosts/details/cards/Policies/_styles.scss index 01a500f3590e..eb7e5d8f6a47 100644 --- a/frontend/pages/hosts/details/cards/Policies/_styles.scss +++ b/frontend/pages/hosts/details/cards/Policies/_styles.scss @@ -1,10 +1,6 @@ .section--policies { .info-banner { margin-bottom: 1rem; - p { - font-size: 0.875rem; - margin: 0; - } } .table-container__header { display: none; diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/SoftwareVulnCount.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/SoftwareVulnCount.tsx index 4b398201f48d..83dd3936d2e5 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/SoftwareVulnCount.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/SoftwareVulnCount.tsx @@ -2,6 +2,7 @@ import React from "react"; import { ISoftware } from "interfaces/software"; import Icon from "components/Icon/Icon"; +import InfoBanner from "components/InfoBanner"; const baseClass = "software-vuln-count"; @@ -18,14 +19,20 @@ const SoftwareVulnCount = ({ return software.vulnerabilities?.length ? sum + 1 : sum; }, 0); return vulnCount ? ( -
+
{vulnCount === 1 ? "1 software item with vulnerabilities detected" : `${vulnCount} software items with vulnerabilities detected`}
-
+ {!deviceUser && ( +

+ Click a vulnerable item below to see the associated Common + Vulnerabilites and Exposures (CVEs). +

+ )} + ) : ( <> ); diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/_styles.scss b/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/_styles.scss index 9562f27e079d..654886d39fb7 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/_styles.scss +++ b/frontend/pages/hosts/details/cards/Software/SoftwareVulnCount/_styles.scss @@ -1,25 +1,11 @@ .software-vuln-count { - font-size: $x-small; - background-color: $ui-off-white; - border: solid 1px $ui-fleet-black-50; - box-sizing: border-box; - border-radius: 10px; - overflow: auto; - margin-bottom: $pad-large; - padding: $pad-large; - - p { - padding-left: $pad-large; - margin-top: $pad-medium; - margin-bottom: 0; - } - &__count { display: flex; - align-content: center; - align-items: center; - font-size: $x-small; font-weight: $bold; gap: $pad-small; } + + p { + margin-left: $pad-large; // Align second line with first line and not with icon + } } diff --git a/frontend/pages/hosts/details/cards/Software/_styles.scss b/frontend/pages/hosts/details/cards/Software/_styles.scss index e1dc55e7c4ec..ceaae0b40792 100644 --- a/frontend/pages/hosts/details/cards/Software/_styles.scss +++ b/frontend/pages/hosts/details/cards/Software/_styles.scss @@ -1,4 +1,7 @@ .section--software { + .info-banner { + margin-bottom: 1rem; + } .text-muted { color: $ui-fleet-black-50; } diff --git a/frontend/pages/hosts/details/helpers.ts b/frontend/pages/hosts/details/helpers.ts new file mode 100644 index 000000000000..2c1283be180e --- /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/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index d8e20f56b27b..bf18675d9494 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -101,7 +101,7 @@ align-items: center; padding-left: $pad-medium; padding-right: $pad-medium; - border-radius: 4px; + border-radius: $border-radius; font-size: $x-small; font-family: "Inter", sans-serif; font-weight: $bold; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss index be5cf1dbb751..376aacc8597e 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss @@ -25,7 +25,7 @@ font-size: $x-small; font-weight: $bold; padding: 2px 4px; - border-radius: 4px; + border-radius: $border-radius; margin-left: 0.5rem; position: relative; top: -2px; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss index ec85f7f7a900..467d05f39eb0 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss @@ -4,7 +4,7 @@ background-color: $ui-off-white; color: $core-fleet-blue; border: 1px solid $ui-fleet-black-10; - border-radius: 4px; + border-radius: $border-radius; padding: 7px $pad-medium; margin: $pad-large 0 0 44px; } diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss index 1eb28ded67bf..f06935217810 100644 --- a/frontend/pages/policies/PolicyPage/_styles.scss +++ b/frontend/pages/policies/PolicyPage/_styles.scss @@ -59,7 +59,7 @@ background-color: $core-white; border: none; box-shadow: inset 0 0 0 1px $ui-fleet-black-25; - border-radius: 6px; + border-radius: $border-radius-large; cursor: pointer; display: flex; align-items: center; diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss index 4e110ad1dd6b..24618c22f97a 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss @@ -3,7 +3,7 @@ &__wrapper { border: 1px solid $ui-fleet-black-10; - border-radius: 4px; + border-radius: $border-radius; overflow: hidden; margin-top: $pad-medium; } diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss index d0d5d05d2867..65fa20a8a58d 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss @@ -3,7 +3,7 @@ &__wrapper { border: 1px solid $ui-fleet-black-10; - border-radius: 4px; + border-radius: $border-radius; overflow: hidden; margin-top: $pad-medium; } diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss index a0e403d93525..4acc671a1196 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/_styles.scss @@ -11,7 +11,7 @@ flex-direction: column; align-items: flex-start; align-self: stretch; - border-radius: 4px; + border-radius: $border-radius; border: 1px solid $ui-fleet-black-10; } diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss index 5bb83b84f0a8..2f6297388a27 100644 --- a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss @@ -4,7 +4,7 @@ background-color: $ui-off-white; color: $core-fleet-blue; border: 1px solid $ui-fleet-black-10; - border-radius: 4px; + border-radius: $border-radius; padding: 7px $pad-medium; margin: $pad-large 0 0 44px; } diff --git a/frontend/services/entities/host_count.ts b/frontend/services/entities/host_count.ts index 9e4bba000b85..5511dcfc10f9 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 b8abf7061bd4..e4d95353d567 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 55416203dc5e..41301f3d3bb2 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/styles/var/_global.scss b/frontend/styles/var/_global.scss index 0c772c21e80f..7876654c4cfb 100644 --- a/frontend/styles/var/_global.scss +++ b/frontend/styles/var/_global.scss @@ -1,2 +1,3 @@ $border-radius: 4px; $border-radius-large: 6px; +$border-radius-xlarge: 10px; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index e1a3880b9966..c78504a63424 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 c535c8013151..d6eb7b6c89bd 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 7913243bd99b..48a36f955912 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 ca02b6bc31b3..9c99a41dc39c 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/handbook/business-operations/README.md b/handbook/business-operations/README.md index 5c6f8c710e3b..17a9a3569032 100644 --- a/handbook/business-operations/README.md +++ b/handbook/business-operations/README.md @@ -35,6 +35,7 @@ Certain new team members, especially in go-to-market (GTM) roles, will need paid | 🐋 SC | ✅ | ✅ | ❌ | ❌ | ✅ | 🫧 SDR | ✅ | ✅ | ✅ | ❌ | ❌ | ⚗️ PM | ❌ | ❌ | ❌ | ✅ | ✅ +| ⚗️ PD | ❌ | ❌ | ❌ | ✅ | ✅ | 🔦 CEO | ✅ | ✅ | ✅ | ✅ | ✅ | Other roles | ❌ | ❌ | ❌ | ❌ | ❌ diff --git a/handbook/ceo.md b/handbook/ceo.md index 864baefee657..e74b1e3aa95b 100644 --- a/handbook/ceo.md +++ b/handbook/ceo.md @@ -17,7 +17,7 @@ The CEO is the [directly responsible individual](https://fleetdm.com/handbook/co - Please use issue comments and GitHub mentions to communicate follow-ups or answer questions related to your request. - Any Fleet team member can view the [🐈‍⬛#g-ceo kanban board](https://app.zenhub.com/workspaces/-g-ceo-645b0eab68a4d40c0795ff61/board?sprints=none) (confidential) for this team, including pending tasks and requests. - **Do not add events to the CEO's calendar.** events added directly to the CEO's calendar will be declined and removed. Even if the CEO asks you to set up a meeting or add him to a call, please [get scheduling help from the Apprentice](#schedule-time-with-the-ceo). -- **For personal or extremely urgent requests** that cannot wait one business day, send a Slack direct message (DM) to `@mikermcneil` right away. +- **For personal or extremely urgent requests** that cannot wait one business day, send a Slack direct message (DM) to `@mikermcneil` right away 🎵 - If you mention the CEO or reply from within a Slack thread, he [will not read your message](#why-not-mention-the-ceo-in-slack-threads). - **If you're a hiring manager**, you can [schedule a CEO interview](https://github.com/fleetdm/confidential/issues/new?assignees=sampfluger88&labels=%23g-ceo&projects=&template=&title=CEO%20interview%3a%20%7BCANDIDATE_NAME%7D&body=-%20[%20]%20I%20followed%20all%20the%20steps%20in%20https%3A%2F%2Ffleetdm.com%2Fhandbook%2Fcompany%2Fleadership%23hiring-a-new-team-member%20before%20submitting%20this%20issue.) - **If you're in Business Operations**, you can [request warehoused equipment be shipped from Fleet IT](#request-equipment-from-fleet-it). diff --git a/handbook/company/open-positions.yml b/handbook/company/open-positions.yml index 233d074e7ddf..5aaa55deae47 100644 --- a/handbook/company/open-positions.yml +++ b/handbook/company/open-positions.yml @@ -220,6 +220,25 @@ - ➕ Bonus: Experienced with Go. - 💭 3-5 years' of experience in cloud infrastructure (AWS/GCP/Azure). - 🦉 Proficient in infrastructure as code and container deployments. +- jobTitle: ⚗️ Product Manager, Security + department: Product + hiringManagerName: Mo Zhu + hiringManagerLinkedInUrl: linkedin.com/in/mo-zhu + hiringManagerGithubUsername: zhumo + responsibilities: | + - 🤝 Deeply understand customer needs and workflows through direct conversations + - 🛣️ Use gained customer understanding to inform product direction and strategy + - 💬 Translate between technical and business needs, orally and in writing + - 📝 Write user stories and requirements to guide design and engineering + - 🛫 Oversee product development lifecycle from concept to release to documentation and support + - 🎁 Develop go-to-market strategies and launch plans for new cybersecurity products, including pricing, positioning, and messaging. + - 🔎 Conduct competitive product analysis + - 👯 Communicate and build strong relationships with cross-functional teams and stakeholders + experience: | + - ⚗️ 2+ years experience as a product manager at a software technology firm + - ⚙️ 2+ years experience as a technical contributor at a software technology firm + - 🏃 Ability to work in a fast-paced startup environment + - 🔒 Bonus: experience in cybersecurity # Note: commenting out this open position because the page link did not exist in the current version of the company handbook page. (2023-08-31) # - jobTitle: 🐋 Solutions Consultant diff --git a/handbook/company/pricing-features-table.yml b/handbook/company/pricing-features-table.yml index 775b50b171f7..ad63dd38a30c 100644 --- a/handbook/company/pricing-features-table.yml +++ b/handbook/company/pricing-features-table.yml @@ -1,193 +1,268 @@ +- categoryName: Other + features: + - industryName: File integrity monitoring (FIM) # Short industry phrase + friendlyName: Detect changes to critical files # Short, Fleet one-liner for the feature, written in the imperative mood. (If easy to do, base this off of the words that an actual customer is saying.) + description: Specify files to monitor for changes or deletions, then log those events to your SIEM or data lake, including key information such as filepath and checksum. # Clear Mr. Rogers description + documentationUrl: https://fleetdm.com/guides/osquery-evented-tables-overview#file-integrity-monitoring-fim # URL of the single-best page within the docs which serves as a "jumping-off point" for this feature. + screenshotSrc: "" # A screenshot of the single, best, simplifying, obvious example + tier: Free # Either "Free" or "Premium" + usualDepartment: Security # or omit if there isn't a particular departmental leaning we've noticed + productCategories: [Endpoint operations] # or omit if this isn't associated with a single product category + dri: mikermcneil #GitHub user name + demos: + - description: A top gaming company needed a way to monitor critical files on production Debian servers. + quote: The FIM features are kind of a top priority. + moreInfoUrl: https://docs.google.com/document/d/1pE9U-1E4YDiy6h4TorszrTOiFAauFiORikSUFUqW7Pk/edit + cues: + - description: Monitor critical files on production Debian servers + - description: Detect illicit activity + moreInfoUrl: https://www.beyondtrust.com/resources/glossary/file-integrity-monitoring + - description: Pinpoint unintended changes + moreInfoUrl: https://www.beyondtrust.com/resources/glossary/file-integrity-monitoring + - description: Verify update status and monitoring system health + moreInfoUrl: https://www.beyondtrust.com/resources/glossary/file-integrity-monitoring + - description: Meet compliance mandates + moreInfoUrl: https://www.beyondtrust.com/resources/glossary/file-integrity-monitoring + - industryName: Human-endpoint mapping + friendlyName: See who logs in on every computer + description: Identify who logs in to any system, including login history and current sessions. Look up any host by the email address of the person using it. + documentationUrl: "" # todo + screenshotSrc: "" + tier: Free + productCategories: [Endpoint operations] + dri: mikermcneil + demos: + - description: Security engineers at a top gaming company wanted to get demographics off their macOS, Windows, and Linux machines about who the user is and who's logged in. + moreInfoUrl: https://docs.google.com/document/d/1qFYtMoKh3zyERLhbErJOEOo2me6Bc7KOOkjKn482Sqc/edit + cues: + - description: Human-to-device mapping + - description: Look up computer by ActiveDirectory account + - description: Find device by Google Chrome user + - description: Check user login history + moreInfoUrl: https://www.lepide.com/how-to/audit-who-logged-into-a-computer-and-when.html#:~:text=To%20find%20out%20the%20details,logs%20in%20%E2%80%9CWindows%20Logs%E2%80%9D. + - description: See currently logged in users + moreInfoUrl: https://www.top-password.com/blog/see-currently-logged-in-users-in-windows/ + - description: Get demographics off of our machines about who the user is and who's logged in + moreInfoUrl: https://docs.google.com/document/d/1qFYtMoKh3zyERLhbErJOEOo2me6Bc7KOOkjKn482Sqc/edit + - description: See what servers someone is logged-in on + moreInfoUrl: https://community.spiceworks.com/topic/138171-is-there-a-way-to-see-what-servers-someone-is-logged-in-on + - industryName: REST API + friendlyName: Automate any feature + description: "" + documentationUrl: https://fleetdm.com/docs/rest-api/rest-api + screenshotSrc: "" + tier: Free + dri: rachaelshaw + - industryName: Command line tool (CLI) + tier: Free - categoryName: Device management features: - - name: User-initiated enrollment of macOS computers + - industryName: User-initiated enrollment of macOS computers tier: Free - comingSoon: false - - name: Remotely enforce macOS settings + usualDepartment: IT + productCategories: [Device management] + - industryName: Remotely enforce macOS settings tier: Free - comingSoon: false - - name: Low-level macOS MDM commands (e.g. remote restart) + usualDepartment: IT + productCategories: [Device management] + - industryName: Low-level macOS MDM commands (e.g. remote restart) tier: Free - comingSoon: false - - name: Native macOS update reminders + usualDepartment: IT + productCategories: [Device management] + - industryName: Native macOS update reminders tier: Free - comingSoon: false - - name: Zero-touch setup for macOS computers + usualDepartment: IT + productCategories: [Device management] + - industryName: Zero-touch setup for macOS computers tier: Premium - comingSoon: false - - name: Safely execute custom scripts (macOS, Windows, and Linux) + usualDepartment: IT + productCategories: [Device management] + - industryName: Safely execute custom scripts (macOS, Windows, and Linux) tier: Premium - comingSoon: false - - name: End-user macOS update reminders (via Nudge) + productCategories: [Device management, Endpoint operations] + - industryName: End-user macOS update reminders (via Nudge) tier: Premium - comingSoon: false - - name: Encrypt macOS hard disks with FileVault + usualDepartment: IT + productCategories: [Device management] + - industryName: Encrypt macOS hard disks with FileVault tier: Premium - comingSoon: false - - name: Manage queued MDM commands on macOS + usualDepartment: IT + productCategories: [Device management] + - industryName: Manage queued MDM commands on macOS tier: Premium - comingSoon: true - - name: Remotely lock and wipe macOS computers + comingSoonOn: 2023-12-31 + usualDepartment: IT + productCategories: [Device management] + - industryName: Remotely lock and wipe macOS computers tier: Premium - comingSoon: false - - name: Update apps on macOS computers + usualDepartment: IT + productCategories: [Device management] + - industryName: Update apps on macOS computers tier: Premium - comingSoon: true - - name: Puppet integration # « Map macOS settings to computers with Puppet module + comingSoonOn: 2024-03-31 + usualDepartment: IT + productCategories: [Device management] + - industryName: Puppet integration + friendlyName: Map macOS settings to computers with Puppet module tier: Premium - comingSoon: false - - name: Interactive MDM migration # « end-user initiated MDM migration, with interactive UI + usualDepartment: IT + productCategories: [Device management] + - industryName: Interactive MDM migration # « end-user initiated MDM migration, with interactive UI tier: Premium - comingSoon: false + usualDepartment: IT + productCategories: [Device management] - categoryName: Support features: - - name: Public issue tracker (GitHub) + - industryName: Public issue tracker (GitHub) tier: Free - comingSoon: false - - name: Community Slack channel + - industryName: Community Slack channel tier: Free - comingSoon: false - - name: Unlimited email support (confidential) + - industryName: Unlimited email support (confidential) tier: Premium - comingSoon: false - - name: Phone and video call support + - industryName: Phone and video call support tier: Premium - comingSoon: false - categoryName: Inventory management features: - - name: Secure REST API - tier: Free - comingSoon: false - - name: Command line tool (CLI) - tier: Free - comingSoon: false - - name: Realtime device inventory dashboard + - industryName: Device inventory dashboard tier: Free - comingSoon: false - - name: Browse installed software packages + - industryName: Browse installed software packages tier: Free - comingSoon: false - - name: Search devices by IP, serial, hostname, UUID + - industryName: Search devices by IP, serial, hostname, UUID tier: Free - comingSoon: false - - name: Target and configure specific groups of devices + - industryName: Target and configure specific groups of devices tier: Premium - comingSoon: false - - name: Aggregate insights for groups of devices + - industryName: Generate reports for groups of devices tier: Premium - comingSoon: false - categoryName: Collaboration features: - - name: Shareable device health reports + - industryName: Shareable device health reports tier: Free - comingSoon: false - - name: Versionable queries and config (GitOps) + - industryName: Versionable queries and config (GitOps) tier: Free - comingSoon: false - - name: Human-to-device mapping + demos: + - description: A top financial services company needed to set up rolling deployments for changes to osquery agents running on their production servers. + moreInfoUrl: https://docs.google.com/document/d/1UdzZMyBLbs9SUXfSXN2x2wZQCbjZZUetYlNWH6-ryqQ/edit#heading=h.2lh6ehprpvl6 + - industryName: Scope transparency tier: Free - comingSoon: false - - name: Scope transparency - tier: Free - comingSoon: false + moreInfoUrl: https://fleetdm.com/transparency - categoryName: Security and compliance features: - - name: Single sign on (SSO, SAML) - tier: Free - comingSoon: false - - name: Report on disk encryption status (FileVault) - tier: Free - comingSoon: false - - name: Audit queries and user activities + - industryName: Single sign on (SSO, SAML) tier: Free - comingSoon: false - - name: Grant API-only access + - industryName: Disk encryption + friendlyName: Ensure hard disks are encrypted + description: Encrypt hard disks of macOS and Windows computers, manage escrowed encryption keys, and report on disk encryption status (FileVault, BitLocker). tier: Free - comingSoon: false - - name: Role-based access control + cues: + - description: Report on disk encryption status + - description: Encrypt hard disks on macOS with FileVault + - description: Escrow FileVault keys on macOS + - description: Encrypt hard disks on Windows with BitLocker + - industryName: Audit queries and user activities tier: Free - comingSoon: false - - name: Ship logs to Splunk, Snowflake, and more + usualDepartment: Security + - industryName: Grant API-only access tier: Free - comingSoon: false - - name: Programmable audit log + - industryName: Programmable audit log tier: Premium - comingSoon: false - - name: Just-in-time (JIT) provisioning + usualDepartment: Security + cues: + - description: Export activity of Fleet admins to your SIEM or data lake + - industryName: Just-in-time (JIT) provisioning tier: Premium - comingSoon: false - - name: Automated user role sync via Okta, AD, or any IDP + - industryName: Automated user role sync via Okta, AD, or any IDP tier: Premium - comingSoon: false - - name: Vanta integration + cue: + - description: Automatically set admin access to Fleet based on your IDP + - industryName: Vanta integration tier: Premium - comingSoon: false - - name: Trigger a workflow based on a failing policy + - industryName: Trigger a workflow based on a failing policy tier: Premium - comingSoon: true - - name: Granular role-based access control + - industryName: Role-based access control tier: Premium - comingSoon: false - categoryName: Monitoring features: - - name: Schedule and automate custom queries - tier: Free - comingSoon: false - - name: Detect vulnerable software - tier: Free - comingSoon: false - - name: Query performance monitoring - tier: Free - comingSoon: false - - name: Standard query and policy library - tier: Free - comingSoon: false - - name: Policy and vulnerability automations (webhook, Zendesk, JIRA, ServiceNow*) - tier: Free - comingSoon: false - - name: Detect and surface issues with devices (policies) - tier: Free - comingSoon: false - - name: Mark policies as critical + - industryName: Schedule and automate custom queries + tier: Free + usualDepartment: Security + cues: + - description: Ship logs to Splunk, Snowflake, and more + - description: Export the data to other systems + moreInfoUrl: https://docs.google.com/document/d/1pE9U-1E4YDiy6h4TorszrTOiFAauFiORikSUFUqW7Pk/edit + - description: Export data to a third-party SIEM tool + moreInfoUrl: https://www.websense.com/content/support/library/web/hosted/admin_guide/siem_integration_explain.aspx + - industryName: Detect vulnerable software + tier: Free + usualDepartment: Security + productCategories: [Vulnerability management] + demos: + - description: A top gaming company wanted to replace Qualys for infrastructure vulnerability detection. + quote: So we have some stuff today through Qualys, but it's just not very good. A lot of it is...it's just really noisy. I'm trying to find out specifically, actually what packages are installed where, and then the ability to live query them. + moreInfoUrl: https://docs.google.com/document/d/1JWtRsW1FUTCkZEESJj9-CvXjLXK4219by-C6vvVVyBY/edit + - industryName: Query performance monitoring + tier: Free + demos: + - description: A top software company needed to understand the performance impact of osquery queries before running them on all of their production Linux servers. + moreInfoUrl: https://docs.google.com/document/d/1WzMc8GJCRU6tTBb6gLsSTzFysqtXO8CtP2sXMPKgYSk/edit?disco=AAAA6xuVxGg + - description: A top software company wanted to detect regressions when adding/changing queries and fail builds if queries were too expensive. + moreInfoUrl: https://docs.google.com/document/d/1WzMc8GJCRU6tTBb6gLsSTzFysqtXO8CtP2sXMPKgYSk/edit?disco=AAAA6xuVxGg + - industryName: Device trust + tier: Free + cue: + - description: Standard query and policy library + - description: Beyondcorp + - description: Zero trust + - description: Conditional access + - industryName: Policy and vulnerability automations (webhook, Zendesk, JIRA, ServiceNow*) + tier: Free + - industryName: Detect and surface issues with devices (policies) + tier: Free + - industryName: Mark policies as critical tier: Premium - comingSoon: false - - name: Vulnerability scores (EPSS and CVSS) + - industryName: Vulnerability scores (EPSS and CVSS) tier: Premium - comingSoon: false - - name: CISA known exploited vulnerabilities + usualDepartment: Security + productCategories: [Vulnerability management] + - industryName: CISA known exploited vulnerabilities tier: Premium - comingSoon: false - - name: End-user self-service + usualDepartment: Security + productCategories: [Vulnerability management] + - industryName: End-user self-service tier: Premium - comingSoon: false + usualDepartment: IT + productCategories: [Device management, Endpoint operations] - categoryName: Data outputs features: - - name: Flexible log destinations (AWS Kinesis, Lambda, GCP, Kafka) + - industryName: Flexible log destinations (AWS Kinesis, Lambda, GCP, Kafka) tier: Free - comingSoon: false - - name: File carving (AWS S3) + usualDepartment: Security + productCategories: [Endpoint operations] + - industryName: File carving (AWS S3) tier: Free - comingSoon: false + usualDepartment: Security + productCategories: [Endpoint operations] - categoryName: Deployment features: - - name: Self-hosted + - industryName: Self-hosted tier: Free - comingSoon: false - - name: Deployment tools (Helm, Terraform) + cues: + - description: Self-managed + - description: Host it yourself + - industryName: Deployment tools (Terraform, Helm) tier: Free - comingSoon: false - - name: Configure osquery startup flags remotely + - industryName: Configure osquery startup flags remotely tier: Free - comingSoon: false - - name: Auto-update osquery agents + usualDepartment: Security + productCategories: [Endpoint operations] + - industryName: Auto-update osquery agents tier: Free - comingSoon: false - - name: Self-managed auto-update registry + productCategories: [Endpoint operations] + - industryName: Self-managed auto-update registry tier: Premium - comingSoon: false - - name: Manage osquery extensions remotely + usualDepartment: Security + productCategories: [Endpoint operations] + - industryName: Manage osquery extensions remotely tier: Premium - comingSoon: false - - name: Managed Cloud + productCategories: [Endpoint operations] + - industryName: Managed Cloud tier: Premium - comingSoon: false diff --git a/handbook/company/why-this-way.md b/handbook/company/why-this-way.md index 962c707227b1..071ea4a7c422 100644 --- a/handbook/company/why-this-way.md +++ b/handbook/company/why-this-way.md @@ -13,7 +13,7 @@ Fleet's source code, website, documentation, company handbook, and internal tool Meanwhile, the [company behind Fleet](https://twitter.com/fleetctl) is built on the [open-core](https://www.heavybit.com/library/video/commercial-open-source-business-strategies) business model. Openness is one of our core [values](https://fleetdm.com/handbook/company#values), and everything we do is [public by default](https://handbook.gitlab.com/handbook/values/#public-by-default). Even the [company handbook](https://fleetdm.com/handbook) is open to the world. -Is open-source collaboration _really_ worth all that? Is it any good? +Is open-source collaboration _all that_? Is it any good? Here are some of the reasons we build in the open: diff --git a/handbook/engineering/Load-testing.md b/handbook/engineering/Load-testing.md index bcdab25be482..ac93f165656e 100644 --- a/handbook/engineering/Load-testing.md +++ b/handbook/engineering/Load-testing.md @@ -58,7 +58,7 @@ After the hosts have been enrolled, you can add `-only_already_enrolled` to make ## Infrastructure setup -The deployment of Fleet was done through the loadtesting [terraform maintained in the repo](https://github.com/fleetdm/fleet/tree/main/tools/loadtesting/terraform) with the following command: +The deployment of Fleet was done through the loadtesting [terraform maintained in the repo](https://github.com/fleetdm/fleet/tree/main/infrastructure/loadtesting/terraform) with the following command: ```bash terraform apply -var tag= diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md index df351374302d..975185961c33 100644 --- a/handbook/engineering/README.md +++ b/handbook/engineering/README.md @@ -632,6 +632,17 @@ If the bug does not meet the criteria of a critical bug, the EM will determine i When fixing the bug, if the proposed solution requires changes that would affect the user experience (UI, API, or CLI), notify the EM and PM to align on the acceptability of the change. +Engineering teams coordinate on bug fixes with the product team during the joint sprint kick-off review. If one team is at capacity and a bug needs attention, another team can step in to assist by following these steps: + +For MDM support on CX bugs: +- Remove the `#g-cx` label and add `#g-mdm` label. +- Add `~assisting g-cx` to clarify the bug’s origin. + +For CX support on MDM bugs: +- Remove the `#g-mdm` label and add `#g-cx` label. +- Add `~assisting g-mdm` to clarify the bug’s origin. + + Fleet [always prioritizes bugs](https://fleetdm.com/handbook/product#prioritizing-improvements) into a release within six weeks. If a bug is not prioritized in the current release, and it is not prioritized in the next release, it is removed from the "Sprint backlog" and placed back in the "Product drafting" column with the `:product` label. Product will determine if the bug should be closed as accepted behavior, or if further drafting is necessary. #### Awaiting QA diff --git a/handbook/engineering/scaling-fleet.md b/handbook/engineering/scaling-fleet.md index 125e2f95f859..07387370281e 100644 --- a/handbook/engineering/scaling-fleet.md +++ b/handbook/engineering/scaling-fleet.md @@ -69,7 +69,7 @@ However, this database feature doesn’t come without a cost. The one to focus o The TLDR is: understand very well how a table will be used. If we do bulk inserts/updates, InnoDB might lock more than you anticipate and cause issues. This is not an argument to not do bulk inserts/updates, but to be very careful when you add a foreign key. -In particular, host_id is a foreign key we’ve been skipping in all the new additional host data tables, which is not something that comes for free, as with that, [we have to keep the data consistent by hand with cleanups](https://github.com/fleetdm/fleet/blob/main/server/datastore/mysql/hosts.go#L309-L309). +In particular, host_id is a foreign key we’ve been skipping in all the new additional host data tables, which is not something that comes for free, as with that, [we have to keep the data consistent by hand with cleanups](https://github.com/fleetdm/fleet/blob/71a237042a9c39a45bc8f9c76465e5ff6039eba9/server/datastore/mysql/hosts.go#L444). ### In this section diff --git a/handbook/marketing/README.md b/handbook/marketing/README.md index faac5f7210a7..84fcd33b613c 100644 --- a/handbook/marketing/README.md +++ b/handbook/marketing/README.md @@ -36,7 +36,7 @@ Fleet's community programs are rooted in several areas; created to nurture commu ### Social media Fleet's largest asset is our user community, the people actually using Fleet. Public conversations on social media create valuable opportunities for contributors to answer technical questions and collect feedback. -Fleet [does not self-promote](https://www.audible.com/pd/The-Impact-Equation-Audiobook/B00AR1VFBU). (Great brands are [magnanimous](https://en.wikipedia.org/wiki/Magnanimity).) +Fleet [does not self-promote](https://www.audible.com/pd/The-Impact-Equation-Audiobook/B00AR1VFBU). (Great brands are [magnanimous](https://en.wikipedia.org/wiki/Magnanimity).) In fact, conversations are already happening in our social spaces that open up opportunities for Fleet to [engage with the community](https://fleetdm.com/handbook/marketing#engage-with-the-community). Here are some topics for social media posts: - Fleet the product @@ -47,11 +47,13 @@ Here are some topics for social media posts: - Industry news about device management - Upcoming events, interviews, and podcasts - ### Ads Fleet uses advertising to spread awareness through a broader audience and foster greater engagement within user communities. The more people actively using Fleet, or contributing, the better Fleet will be. +### Events +It's important for Fleet to engage at events. This provides an opportunity to directly engage with potential users and contributors, build relationships, gather feedback, and create a stronger sense of community and trust. + ## Responsibilities ### Optimize ads through experimentation @@ -116,6 +118,31 @@ Any changes to the current running ads visible to a user, including designs, key 2. Compare existing ads against the newly proposed ad within the corresponding ad platform. ([Google Ads](https://ads.google.com/home/), [LinkedIn Campaign Manager](https://www.linkedin.com/campaignmanager/), etc.) 3. If your change is approved, Field Marketer makes changes and creates a calendar reminder to check performance two weeks from the date changes were made. + +### Engage with the community +Public conversations on social media create valuable opportunities for contributors to answer technical questions and collect feedback. + +Here are some links that filter relevant conversations on each platform: +- [LinkedIn](https://www.linkedin.com/search/results/content/?datePosted=%22past-week%22&keywords=osquery%20OR%20%22fleet%20device%20management%22%20OR%20%22fleetdm%22%20OR%20%22github.com%2Ffleetdm%2Ffleet%22%20OR%20%22fleetdm.com%22&origin=FACETED_SEARCH&sid=oxR) +- [Twitter](https://twitter.com/search?q=%22osquery%22%20OR%20%22github.com%2Fosquery%2Fosquery%22%20OR%20%22github.com%2Ffleetdm%2Ffleet%22%20OR%20%22github.com%2Fkolide%2Ffleet%22%20OR%20%22fleetdm%22%20OR%20%22fleet%20device%20management%22%20OR%20%22nanomdm%22%20OR%20%22micromdm%22%20OR%20%22swiftDialog%22&src=typed_query&f=live) + +1. Find conversations that are relevant to Fleet on both LinkedIn and Twitter +2. Reply to threads looking for solutions Fleet can solve with helpful information. If additional information is needed, find help in [#help-engineering](https://fleetdm.slack.com/archives/C019WG4GH0A) for accurate information. +3. Leave a like on threads and posts that are interesting, cool, celebratory, funny, etc. within our communities. +4. If a post is helpful to our audience, reshare it. + +### Book an event +For an event to be considered, booked, and scheduled, we follow the event issue template. + +1. Create a [new GitHub issue for the #g-marketing board](https://fleetdm.com/handbook/marketing#contact-us) and select the "Event-preparation" template.. +2. Drag the issue into the "🗓 Ideas for future events" column. + +Once approval has been received, move the event into the "🗓 Planned events" column. + +### Review ongoing events +Check the "🗓 Planned events" column in [#g-marketing board](https://app.zenhub.com/workspaces/g-marketing-64e6c8e2d35c7f001a457b7f/board) and continue to work through steps in each event's issue. + + ## Rituals diff --git a/handbook/marketing/marketing.rituals.yml b/handbook/marketing/marketing.rituals.yml index 7c43d7e48fee..2cb4d35329f3 100644 --- a/handbook/marketing/marketing.rituals.yml +++ b/handbook/marketing/marketing.rituals.yml @@ -21,3 +21,24 @@ description: "Complete draft orders." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)" moreInfoUrl: "https://fleetdm.com/handbook/marketing#process-pending-swag-requests-from-the-website" #URL used to highlight "description:" test in table dri: "drewbakerfdm" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) +- + task: "Engage with the community" + startedOn: "2023-09-20" + frequency: "Daily" + description: "Find relevant conversations with the community and contribute" + moreInfoUrl: "https://fleetdm.com/handbook/marketing#engage-with-the-community" + dri: "drewbakerfdm" +- + task: "Review ongoing events" + startedOn: "2023-10-02" + frequency: "Daily" + description: "Check 🗓️ Planned events and complete steps in each issue" + moreInfoUrl: "https://fleetdm.com/handbook/marketing#review-ongoing-events" + dri: "drewbakerfdm" +- + task: "Book an event" + startedOn: "2023-10-02" + frequency: "Weekly" + description: "Populate 🗓️ Ideas for future events" + moreInfoUrl: "https://fleetdm.com/handbook/marketing#book-an-event" + dri: "drewbakerfdm" diff --git a/handbook/product/README.md b/handbook/product/README.md index 4471388fe05d..30eaf8047786 100644 --- a/handbook/product/README.md +++ b/handbook/product/README.md @@ -77,9 +77,9 @@ For external contributors: please consider opening an issue with reference scree Once the draft has been approved, it moves to the "Settled" column on the drafting board. -Before assigning an engineering manager for [estimation](https://fleetdm.com/handbook/engineering#sprint-ceremonies), the product team should ensure the product section of the user story [checklist](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=story&projects=&template=story.md&title=) is complete. +Before assigning an engineering manager to [estimate](https://fleetdm.com/handbook/engineering#sprint-ceremonies) a user story, the product designer ensures the product section of the user story [checklist](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=story&projects=&template=story.md&title=) is complete. -> The story's designer is responsible for ensuring the checklist has been completed, the requirements section is consistent with the Figma, and the group engineering manager has been assigned. +Once a bug has gone through design and is considered "Settled", the designer removes the `:product` label and moves the issue to the 'To be scheduled' column on the "Bugs" board. The product manager then prioritizes the bug into the "Sprint backlog" and assigns the group engineering manager. Learn https://fleetdm.com/handbook/company/development-groups#making-changes diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf index 615976ac2eef..09388cd5605c 100644 --- a/infrastructure/dogfood/terraform/aws/variables.tf +++ b/infrastructure/dogfood/terraform/aws/variables.tf @@ -56,7 +56,7 @@ variable "database_name" { variable "fleet_image" { description = "the name of the container image to run" - default = "fleetdm/fleet:v4.38.0" + default = "fleetdm/fleet:v4.38.1" } variable "software_inventory" { diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf index 9306fe090440..752306212768 100644 --- a/infrastructure/dogfood/terraform/gcp/variables.tf +++ b/infrastructure/dogfood/terraform/gcp/variables.tf @@ -68,5 +68,5 @@ variable "redis_mem" { } variable "image" { - default = "fleet:v4.38.0" + default = "fleet:v4.38.1" } diff --git a/infrastructure/loadtesting/terraform/readme.md b/infrastructure/loadtesting/terraform/readme.md index bf25be2b06e0..e46cd0be42d2 100644 --- a/infrastructure/loadtesting/terraform/readme.md +++ b/infrastructure/loadtesting/terraform/readme.md @@ -1,7 +1,7 @@ ## Terraform for Loadtesting Environment The interface into this code is designed to be minimal. -If you require changes beyond whats described here, contact @zwinnerman-fleetdm. +If you require changes beyond whats described here, contact #g-infra. ### Deploying your code to the loadtesting environment diff --git a/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf b/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf index 71842e3cd54b..fe78454409ae 100644 --- a/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf +++ b/infrastructure/sandbox/JITProvisioner/jitprovisioner.tf @@ -206,7 +206,7 @@ resource "random_uuid" "jitprovisioner" { # Use the local to make the trigger work. locals { - fleet_tag = "v4.38.0" + fleet_tag = "v4.38.1" } resource "null_resource" "standard-query-library" { diff --git a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf index d56cbdbb944e..458d6677ccef 100644 --- a/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf +++ b/infrastructure/sandbox/PreProvisioner/lambda/deploy_terraform/main.tf @@ -165,7 +165,7 @@ resource "helm_release" "main" { set { name = "imageTag" - value = "v4.38.0" + value = "v4.38.1" } set { diff --git a/orbit/changes/12842-orbit-bitlocker-management b/orbit/changes/12842-orbit-bitlocker-management new file mode 100644 index 000000000000..97d7e6fe1ec5 --- /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 3b91e700f91c..3e9f56df12ff 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 000000000000..e2105689273a --- /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 000000000000..4263ba270e81 --- /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 000000000000..4d9bb3683808 --- /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 e4957bc2cd36..50d6a9a4140c 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 ca28089dab5c..3c0988a0fcff 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 18076ba92260..e07c6648cc1b 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 c4512b12f282..901dcd527630 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 ec045070c989..a665b3bb52cc 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 868963d4a09b..264834a63714 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 000000000000..d6bb2189a899 --- /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 000000000000..03b38a3dfa28 --- /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 0da61f93a33e..c14340fa12ad 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 1580d40f257e..66c14b157b9f 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 cb1133ac61fb..1e95d96f1405 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 3c9173b63e5e..2d5977334b10 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 10124e68e034..0ad9c6e005b9 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 58367713b142..0a89f4f32e36 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) -} -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, - ) + // 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) +} - 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, "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, host.ID, nil) + 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 551ad0463565..2573a53da5c8 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 db4ef804946b..377cf2243222 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 863f9c290fbd..7dd3a696fcc3 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 00e2a15265cb..e1fe97b2c329 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 000000000000..793432c20939 --- /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 000000000000..90538968f81f --- /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 93c1e2703d08..932d1d9a28ff 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 6a771907393a..17016a7e3eac 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 268eb2277ae8..bc7a4173d3b7 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 13db70dc96c0..2aa92b13d9fa 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 87a401f71b32..9e22fa92c3ba 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 2458e8d7f5ef..2bba30a7d785 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 9bac69829929..9a68a6d6b68d 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 77ecd683f028..b8c2b2fb6586 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 9554c78154a9..b9cb37544aba 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 4c6ba72e035a..191ef0f3b1e7 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 000000000000..0a72f5aea74a --- /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 8b06ded4d8ec..aa300c459691 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 000000000000..79a55dd50ab9 --- /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 f6289e99238d..35f151ac1d98 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 552c8ffc97a2..8a52f6bc378d 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 1b049e60200d..ecc0b814e36a 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 0b279e5b8ae5..a3008831c12c 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 38598287e8be..6efc8a602d99 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 5417b8f09456..53270d295046 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 49f941ce3c46..8815b63a2846 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 aa76a30afa36..4b027d44f6d6 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 840807bde9cf..db568e87b665 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") - } + 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.DetermineDiskEncryptionStatus(profs, mobileconfig.FleetFileVaultPayloadIdentifier) + // 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) + 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 } - key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") - } + // 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 + } - if key.Decryptable == nil || !*key.Decryptable { - return nil, ctxerr.Wrap(ctx, newNotFoundError(), "getting host encryption key") + // 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 } - cert, _, _, err := svc.config.MDM.AppleSCEP() + 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(), "host encryption key is not decryptable") + } - decryptedKey, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey) + 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") } - key.DecryptedValue = string(decryptedKey) err = svc.ds.NewActivity( diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index e62fe936c648..1b77fa3f70e2 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 d3ab73919f39..9bc27ec2fd6b 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 83f4976de840..60d90934498d 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 ddf83f81a248..618d6c2dcdd8 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 f0312e417785..7632eb4cf1d2 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 13809587ecc5..f395d26a988d 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 071c5ca3b5bd..2ebaf1bfc738 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 be6b5ddc4637..75343ad65c57 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 7ac09b6bf85c..2c6a01337f3c 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 28df989cd1b1..40a0bac9f93a 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 b4a8ca3e6bb7..b7374a58f21b 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 9887a1243dda..165b7e2cc7eb 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 2c9e3d60ee85..6e4224625922 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 cfd3365aacc4..7b90c43c0f03 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 c9caee032b0b..4170695e11af 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: diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf index 8e0e7049a215..e8a73e36fb84 100644 --- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf +++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf @@ -13,7 +13,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.38.0") + image = optional(string, "fleetdm/fleet:v4.38.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf index 3753cfe1ad01..e4700c923877 100644 --- a/terraform/byo-vpc/byo-db/variables.tf +++ b/terraform/byo-vpc/byo-db/variables.tf @@ -74,7 +74,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.38.0") + image = optional(string, "fleetdm/fleet:v4.38.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf index 03077d2911a2..49c62767a82e 100644 --- a/terraform/byo-vpc/example/main.tf +++ b/terraform/byo-vpc/example/main.tf @@ -17,7 +17,7 @@ provider "aws" { } locals { - fleet_image = "fleetdm/fleet:v4.38.0" + fleet_image = "fleetdm/fleet:v4.38.1" } resource "random_pet" "main" {} diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf index f6839f192bd6..eef43bfdfc48 100644 --- a/terraform/byo-vpc/variables.tf +++ b/terraform/byo-vpc/variables.tf @@ -163,7 +163,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.38.0") + image = optional(string, "fleetdm/fleet:v4.38.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/terraform/variables.tf b/terraform/variables.tf index b4afdb3d212d..8373dd056964 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -215,7 +215,7 @@ variable "fleet_config" { type = object({ mem = optional(number, 4096) cpu = optional(number, 512) - image = optional(string, "fleetdm/fleet:v4.38.0") + image = optional(string, "fleetdm/fleet:v4.38.1") family = optional(string, "fleet") sidecars = optional(list(any), []) depends_on = optional(list(any), []) diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json index da38159b8fbe..1d364cadb2d6 100644 --- a/tools/fleetctl-npm/package.json +++ b/tools/fleetctl-npm/package.json @@ -1,6 +1,6 @@ { "name": "fleetctl", - "version": "v4.38.0", + "version": "v4.38.1", "description": "Installer for the fleetctl CLI tool", "bin": { "fleetctl": "./run.js" diff --git a/website/api/controllers/view-integrations.js b/website/api/controllers/view-integrations.js new file mode 100644 index 000000000000..e996de3e6714 --- /dev/null +++ b/website/api/controllers/view-integrations.js @@ -0,0 +1,27 @@ +module.exports = { + + + friendlyName: 'View integrations', + + + description: 'Display "integrations" page.', + + + exits: { + + success: { + viewTemplatePath: 'pages/integrations' + } + + }, + + + fn: async function () { + + // Respond with view. + return {}; + + } + + +}; diff --git a/website/api/controllers/webhooks/receive-from-stripe.js b/website/api/controllers/webhooks/receive-from-stripe.js index 4b8a9e11386b..0af2abd1685e 100644 --- a/website/api/controllers/webhooks/receive-from-stripe.js +++ b/website/api/controllers/webhooks/receive-from-stripe.js @@ -69,10 +69,14 @@ module.exports = { let subscriptionForThisEvent = await Subscription.findOne({stripeSubscriptionId: subscriptionIdToFind}).populate('user'); let STRIPE_EVENTS_SENT_BEFORE_A_SUBSCRIPTION_RECORD_EXISTS = [ - 'invoice.created', - 'invoice.finalized', - 'invoice.paid', - 'invoice.payment_succeeded', + 'invoice.created',// Sent when a user submits the billing form on /customers/new-license, before the user's biliing card is charged. + 'invoice.finalized',// Sent when a user submits the billing form on /customers/new-license, before the user's biliing card is charged. + 'invoice.paid',//Sent when a user submits the billing form on /customers/new-license, when the user's biliing card is charged. + 'invoice.payment_succeeded',// Sent when payment for a users subscription is successful. The save-billing-info-and-subscribe action will check for this event before creating a license key. + 'invoice.payment_failed',// Sent when a users subscritpion payment fails. This can happen before we create a license key and save the subscription in the database. + 'invoice.payment_action_required',// Sent when a user's billing card requires additional verification from stripe. + 'invoice.updated',// Sent before an incomplete invoice is voided. (~24 hours after a payment fails) + 'invoice.voided',// Sent when an incomplete invoice is marked as voided. (~24 hours after a payment fails) ]; // If this event is for a subscription that was just created, we won't have a matching Subscription record in the database. This is because we wait until the subscription's invoice is paid to create the record in our database. diff --git a/website/assets/images/device-management-transparency-438x373@2x.png b/website/assets/images/device-management-transparency-438x373@2x.png new file mode 100644 index 000000000000..b8caf6252131 Binary files /dev/null and b/website/assets/images/device-management-transparency-438x373@2x.png differ diff --git a/website/assets/images/icon-checkmark-circle-green-16x16@2x.png b/website/assets/images/icon-checkmark-circle-green-16x16@2x.png new file mode 100644 index 000000000000..94f8b26a5719 Binary files /dev/null and b/website/assets/images/icon-checkmark-circle-green-16x16@2x.png differ diff --git a/website/assets/images/icon-idp-22x28@2x.png b/website/assets/images/icon-idp-22x28@2x.png new file mode 100644 index 000000000000..a1e03daf1314 Binary files /dev/null and b/website/assets/images/icon-idp-22x28@2x.png differ diff --git a/website/assets/images/icon-rest-api-35x28@2x.png b/website/assets/images/icon-rest-api-35x28@2x.png new file mode 100644 index 000000000000..fc170456d0bc Binary files /dev/null and b/website/assets/images/icon-rest-api-35x28@2x.png differ diff --git a/website/assets/images/icon-webhooks-30x28@2x.png b/website/assets/images/icon-webhooks-30x28@2x.png new file mode 100644 index 000000000000..284735a1d216 Binary files /dev/null and b/website/assets/images/icon-webhooks-30x28@2x.png differ diff --git a/website/assets/images/logo-active-directory-169x28@2x.png b/website/assets/images/logo-active-directory-169x28@2x.png new file mode 100644 index 000000000000..71d0d79f6221 Binary files /dev/null and b/website/assets/images/logo-active-directory-169x28@2x.png differ diff --git a/website/assets/images/logo-ansible-147x28@2x.png b/website/assets/images/logo-ansible-147x28@2x.png new file mode 100644 index 000000000000..9c4f070f3ac8 Binary files /dev/null and b/website/assets/images/logo-ansible-147x28@2x.png differ diff --git a/website/assets/images/logo-aws-46x28@2x.png b/website/assets/images/logo-aws-46x28@2x.png new file mode 100644 index 000000000000..50845d7929c5 Binary files /dev/null and b/website/assets/images/logo-aws-46x28@2x.png differ diff --git a/website/assets/images/logo-azure-169x28@2x.png b/website/assets/images/logo-azure-169x28@2x.png new file mode 100644 index 000000000000..d85479ed7ff6 Binary files /dev/null and b/website/assets/images/logo-azure-169x28@2x.png differ diff --git a/website/assets/images/logo-chef-169x28@2x.png b/website/assets/images/logo-chef-169x28@2x.png new file mode 100644 index 000000000000..ade25d93859b Binary files /dev/null and b/website/assets/images/logo-chef-169x28@2x.png differ diff --git a/website/assets/images/logo-deloitte-166x36@2x.png b/website/assets/images/logo-deloitte-166x36@2x.png new file mode 100644 index 000000000000..b8d0777dc740 Binary files /dev/null and b/website/assets/images/logo-deloitte-166x36@2x.png differ diff --git a/website/assets/images/logo-elastic-82x28@2x.png b/website/assets/images/logo-elastic-82x28@2x.png new file mode 100644 index 000000000000..d1b0b26ed0e6 Binary files /dev/null and b/website/assets/images/logo-elastic-82x28@2x.png differ diff --git a/website/assets/images/logo-github-89x28@2x.png b/website/assets/images/logo-github-89x28@2x.png new file mode 100644 index 000000000000..7784f46ee7fa Binary files /dev/null and b/website/assets/images/logo-github-89x28@2x.png differ diff --git a/website/assets/images/logo-gitlab-124x28@2x.png b/website/assets/images/logo-gitlab-124x28@2x.png new file mode 100644 index 000000000000..8efa1f81ed10 Binary files /dev/null and b/website/assets/images/logo-gitlab-124x28@2x.png differ diff --git a/website/assets/images/logo-google-chronicle-128x28@2x.png b/website/assets/images/logo-google-chronicle-128x28@2x.png new file mode 100644 index 000000000000..a5bd8cf760fc Binary files /dev/null and b/website/assets/images/logo-google-chronicle-128x28@2x.png differ diff --git a/website/assets/images/logo-google-cloud-174x28@2x.png b/website/assets/images/logo-google-cloud-174x28@2x.png new file mode 100644 index 000000000000..135f685cdfd0 Binary files /dev/null and b/website/assets/images/logo-google-cloud-174x28@2x.png differ diff --git a/website/assets/images/logo-jira-185x28@2x.png b/website/assets/images/logo-jira-185x28@2x.png new file mode 100644 index 000000000000..f82ca287f0e5 Binary files /dev/null and b/website/assets/images/logo-jira-185x28@2x.png differ diff --git a/website/assets/images/logo-munki-101x28@2x.png b/website/assets/images/logo-munki-101x28@2x.png new file mode 100644 index 000000000000..3c660093bae5 Binary files /dev/null and b/website/assets/images/logo-munki-101x28@2x.png differ diff --git a/website/assets/images/logo-okta-85x28@2x.png b/website/assets/images/logo-okta-85x28@2x.png new file mode 100644 index 000000000000..eec4d5ee1fa0 Binary files /dev/null and b/website/assets/images/logo-okta-85x28@2x.png differ diff --git a/website/assets/images/logo-puppet-79x28@2x.png b/website/assets/images/logo-puppet-79x28@2x.png new file mode 100644 index 000000000000..3d32eef9ed54 Binary files /dev/null and b/website/assets/images/logo-puppet-79x28@2x.png differ diff --git a/website/assets/images/logo-snowflake-color-117x28@2x.png b/website/assets/images/logo-snowflake-color-117x28@2x.png new file mode 100644 index 000000000000..b8c98e3e34b7 Binary files /dev/null and b/website/assets/images/logo-snowflake-color-117x28@2x.png differ diff --git a/website/assets/images/logo-splunk-95x28@2x.png b/website/assets/images/logo-splunk-95x28@2x.png new file mode 100644 index 000000000000..5586e071cc7b Binary files /dev/null and b/website/assets/images/logo-splunk-95x28@2x.png differ diff --git a/website/assets/images/logo-tines-90x28@2x.png b/website/assets/images/logo-tines-90x28@2x.png new file mode 100644 index 000000000000..40d5365a0957 Binary files /dev/null and b/website/assets/images/logo-tines-90x28@2x.png differ diff --git a/website/assets/js/components/scrollable-tweets.component.js b/website/assets/js/components/scrollable-tweets.component.js index ed7030fdff9a..f40a2e9f57c8 100644 --- a/website/assets/js/components/scrollable-tweets.component.js +++ b/website/assets/js/components/scrollable-tweets.component.js @@ -20,7 +20,7 @@ parasails.registerComponent('scrollableTweets', { data: function () { return { currentTweetPage: 0, - numberOfTweetCards: 6, + numberOfTweetCards: 7, numberOfTweetPages: 0, numberOfTweetsPerPage: 0, tweetCardWidth: 0, @@ -110,6 +110,19 @@ parasails.registerComponent('scrollableTweets', {
+ +
+
+ Deloitte logo +
+

One of the best teams out there to go work for and help shape security platforms.

+
+
+

Dhruv Majumdar

+

Director Of Cyber Risk & Advisory @Deloitte

+
+
+