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/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/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/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/REST API/rest-api.md b/docs/REST API/rest-api.md
index d41904dc9cd5..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},
}
```
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 c51d2895d506..f225356c8d9c 100644
--- a/frontend/__mocks__/configMock.ts
+++ b/frontend/__mocks__/configMock.ts
@@ -125,6 +125,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
},
fleet_desktop: { transparency_url: "https://fleetdm.com/transparency" },
mdm: {
+ enable_disk_encryption: false,
windows_enabled_and_configured: true,
apple_bm_default_team: "Apples",
apple_bm_enabled_and_configured: true,
diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts
index 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/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 c604629c0c79..2bfbe6fada8e 100644
--- a/frontend/interfaces/config.ts
+++ b/frontend/interfaces/config.ts
@@ -1,95 +1,11 @@
/* Config interface is a flattened version of the fleet/config API response */
-
import {
IWebhookHostStatus,
IWebhookFailingPolicies,
IWebhookSoftwareVulnerabilities,
} from "interfaces/webhook";
-import PropTypes from "prop-types";
import { IIntegrations } from "./integration";
-export default PropTypes.shape({
- org_name: PropTypes.string,
- org_logo_url: PropTypes.string,
- contact_url: PropTypes.string,
- server_url: PropTypes.string,
- live_query_disabled: PropTypes.bool,
- enable_analytics: PropTypes.bool,
- enable_smtp: PropTypes.bool,
- configured: PropTypes.bool,
- sender_address: PropTypes.string,
- server: PropTypes.string,
- port: PropTypes.number,
- authentication_type: PropTypes.string,
- user_name: PropTypes.string,
- password: PropTypes.string,
- enable_ssl_tls: PropTypes.bool,
- authentication_method: PropTypes.string,
- domain: PropTypes.string,
- verify_sll_certs: PropTypes.bool,
- enable_start_tls: PropTypes.bool,
- entity_id: PropTypes.string,
- idp_image_url: PropTypes.string,
- metadata: PropTypes.string,
- metadata_url: PropTypes.string,
- idp_name: PropTypes.string,
- enable_sso: PropTypes.bool,
- enable_sso_idp_login: PropTypes.bool,
- enable_jit_provisioning: PropTypes.bool,
- host_expiry_enabled: PropTypes.bool,
- host_expiry_window: PropTypes.number,
- agent_options: PropTypes.string,
- tier: PropTypes.string,
- organization: PropTypes.string,
- device_count: PropTypes.number,
- expiration: PropTypes.string,
- mdm: PropTypes.shape({
- enabled_and_configured: PropTypes.bool,
- apple_bm_terms_expired: PropTypes.bool,
- apple_bm_enabled_and_configured: PropTypes.bool,
- windows_enabled_and_configured: PropTypes.bool,
- macos_updates: PropTypes.shape({
- minimum_version: PropTypes.string,
- deadline: PropTypes.string,
- }),
- }),
- note: PropTypes.string,
- // vulnerability_settings: PropTypes.any, TODO
- enable_host_status_webhook: PropTypes.bool,
- destination_url: PropTypes.string,
- host_percentage: PropTypes.number,
- days_count: PropTypes.number,
- logging: PropTypes.shape({
- debug: PropTypes.bool,
- json: PropTypes.bool,
- result: PropTypes.shape({
- plugin: PropTypes.string,
- config: PropTypes.shape({
- status_log_file: PropTypes.string,
- result_log_file: PropTypes.string,
- enable_log_rotation: PropTypes.bool,
- enable_log_compression: PropTypes.bool,
- }),
- }),
- status: PropTypes.shape({
- plugin: PropTypes.string,
- config: PropTypes.shape({
- status_log_file: PropTypes.string,
- result_log_file: PropTypes.string,
- enable_log_rotation: PropTypes.bool,
- enable_log_compression: PropTypes.bool,
- }),
- }),
- }),
- email: PropTypes.shape({
- backend: PropTypes.string,
- config: PropTypes.shape({
- region: PropTypes.string,
- source_arn: PropTypes.string,
- }),
- }),
-});
-
export interface ILicense {
tier: string;
device_count: number;
@@ -113,6 +29,7 @@ export interface IMacOsMigrationSettings {
}
export interface IMdmConfig {
+ enable_disk_encryption: boolean;
enabled_and_configured: boolean;
apple_bm_default_team?: string;
apple_bm_terms_expired: boolean;
@@ -285,7 +202,10 @@ export interface IConfig {
};
};
mdm: IMdmConfig;
- mdm_enabled?: boolean; // TODO: remove when windows MDM is released. Only used for windows MDM dev currently.
+ /** This is the flag that determines if the windwos mdm feature flag is enabled.
+ TODO: WINDOWS FEATURE FLAG: remove when windows MDM is released. Only used for windows MDM dev currently.
+ */
+ mdm_enabled?: boolean;
}
export interface IWebhookSettings {
diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts
index 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/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 (
-
;
+};
+
+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 77%
rename from frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss
rename to frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss
index ac31595dbcf8..89744cf6cc71 100644
--- a/frontend/pages/ManageControlsPage/OSSettings/AggregateMacSettingsIndicators/_styles.scss
+++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/_styles.scss
@@ -1,4 +1,4 @@
-.aggregate-mac-settings-indicators {
+.profile-status-aggregate {
display: flex;
height: 94px;
border-top: 1px solid #e2e4ea;
@@ -10,7 +10,7 @@
margin: auto;
}
- .aggregate-mac-settings-indicator {
+ &__profile-status-count {
flex-grow: 1;
display: flex;
@@ -29,13 +29,17 @@
font-weight: $regular;
}
- .settings-indicator {
+ .profile-status-indicator {
flex-direction: column;
}
}
- .aggregate-mac-settings-indicator:last-child {
+ &__profile-status-count:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
+
+ &__status-indicator-value {
+ font-weight: $bold;
+ }
}
diff --git a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/index.ts b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/index.ts
new file mode 100644
index 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/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/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 0d1a0b6b46d3..e388493de6d4 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -65,6 +65,7 @@ import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
import MacSettingsModal from "../MacSettingsModal";
import BootstrapPackageModal from "./modals/BootstrapPackageModal";
import SelectQueryModal from "./modals/SelectQueryModal";
+import { isSupportedPlatform } from "./modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal";
const baseClass = "host-details";
@@ -720,6 +721,7 @@ const HostDetailsPage = ({
showRefetchSpinner={showRefetchSpinner}
onRefetchHost={onRefetchHost}
renderActionButtons={renderActionButtons}
+ osSettings={host?.mdm.os_settings}
/>
@@ -852,12 +855,15 @@ const HostDetailsPage = ({
{showUnenrollMdmModal && !!host && (
)}
- {showDiskEncryptionModal && host && (
- setShowDiskEncryptionModal(false)}
- />
- )}
+ {showDiskEncryptionModal &&
+ host &&
+ isSupportedPlatform(host.platform) && (
+ setShowDiskEncryptionModal(false)}
+ />
+ )}
{showBootstrapPackageModal &&
bootstrapPackageData.details &&
bootstrapPackageData.name && (
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
index 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/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/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/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/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index ce6d89b9528b..4a0c18974165 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/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 + `
@@ -6963,7 +7459,7 @@ func (s *integrationMDMTestSuite) newSyncMLSessionMsg(managementUrl string) ([]b
- DB257C3A08778F4FB61E2749066C1F27
+ ` + deviceID + `