Merging Bitlocker feature branch (#14350)

This relates to #12577

---------

Co-authored-by: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Co-authored-by: Roberto Dip <dip.jesusr@gmail.com>
This commit is contained in:
Marcos Oviedo 2023-10-06 19:04:33 -03:00 committed by GitHub
parent cc547ba02c
commit f0d77ab3db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
136 changed files with 5367 additions and 930 deletions

View File

@ -0,0 +1 @@
* Deprecate `mdm.macos_settings.enable_disk_encryption` in favor of `mdm.enable_disk_encryption`

View File

@ -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.

View File

@ -0,0 +1 @@
- Added `mdm.os_settings` to `GET /api/v1/hosts/{id}` response.

View File

@ -0,0 +1 @@
- change Controls/Disk Encryption and host details page to include windows bitlocker information.

View File

@ -0,0 +1 @@
* Added the `POST /api/fleet/orbit/disk_encryption_key` endpoint for Windows hosts to report the bitlocker encryption key.

View File

@ -0,0 +1 @@
* Added support to return the decrypted disk encryption key of a Windows host.

View File

@ -18,6 +18,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/policies" "github.com/fleetdm/fleet/v4/server/policies"
"github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/ptr"
@ -838,7 +839,7 @@ func verifyDiskEncryptionKeys(
if key.UpdatedAt.After(latest) { if key.UpdatedAt.After(latest) {
latest = key.UpdatedAt 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) undecryptable = append(undecryptable, key.HostID)
continue continue
} }

View File

@ -1044,13 +1044,13 @@ spec:
foo: qux foo: qux
name: Team1 name: Team1
mdm: mdm:
enable_disk_encryption: false
macos_updates: macos_updates:
minimum_version: 10.10.10 minimum_version: 10.10.10
deadline: 1992-03-01 deadline: 1992-03-01
macos_settings: macos_settings:
custom_settings: custom_settings:
- %s - %s
enable_disk_encryption: false
secrets: secrets:
- secret: BBB - secret: BBB
`, mobileConfigPath)) `, mobileConfigPath))
@ -1062,9 +1062,9 @@ spec:
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name})) 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.JSONEq(t, string(json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)), string(*savedTeam.Config.AgentOptions))
assert.Equal(t, fleet.TeamMDM{ assert.Equal(t, fleet.TeamMDM{
EnableDiskEncryption: false,
MacOSSettings: fleet.MacOSSettings{ MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath}, CustomSettings: []string{mobileConfigPath},
EnableDiskEncryption: false,
}, },
MacOSUpdates: fleet.MacOSUpdates{ MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"), MinimumVersion: optjson.SetString("10.10.10"),
@ -1097,9 +1097,9 @@ spec:
require.True(t, ds.NewJobFuncInvoked) require.True(t, ds.NewJobFuncInvoked)
// all left untouched, only setup assistant added // all left untouched, only setup assistant added
assert.Equal(t, fleet.TeamMDM{ assert.Equal(t, fleet.TeamMDM{
EnableDiskEncryption: false,
MacOSSettings: fleet.MacOSSettings{ MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath}, CustomSettings: []string{mobileConfigPath},
EnableDiskEncryption: false,
}, },
MacOSUpdates: fleet.MacOSUpdates{ MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"), MinimumVersion: optjson.SetString("10.10.10"),
@ -1129,9 +1129,9 @@ spec:
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name})) require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
// all left untouched, only bootstrap package added // all left untouched, only bootstrap package added
assert.Equal(t, fleet.TeamMDM{ assert.Equal(t, fleet.TeamMDM{
EnableDiskEncryption: false,
MacOSSettings: fleet.MacOSSettings{ MacOSSettings: fleet.MacOSSettings{
CustomSettings: []string{mobileConfigPath}, CustomSettings: []string{mobileConfigPath},
EnableDiskEncryption: false,
}, },
MacOSUpdates: fleet.MacOSUpdates{ MacOSUpdates: fleet.MacOSUpdates{
MinimumVersion: optjson.SetString("10.10.10"), MinimumVersion: optjson.SetString("10.10.10"),
@ -2886,7 +2886,7 @@ spec:
macos_settings: macos_settings:
enable_disk_encryption: true 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", desc: "app config macos_settings.enable_disk_encryption false",

View File

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/fleetdm/fleet/v4/pkg/rawjson"
"github.com/fleetdm/fleet/v4/pkg/secure" "github.com/fleetdm/fleet/v4/pkg/secure"
kithttp "github.com/go-kit/kit/transport/http" kithttp "github.com/go-kit/kit/transport/http"
"gopkg.in/guregu/null.v3" "gopkg.in/guregu/null.v3"
@ -167,12 +168,15 @@ func (eacp enrichedAppConfigPresenter) MarshalJSON() ([]byte, error) {
*fleet.VulnerabilitiesConfig *fleet.VulnerabilitiesConfig
} }
return json.Marshal(&struct { enrichedJSON, err := json.Marshal(fleet.EnrichedAppConfig(eacp))
fleet.EnrichedAppConfig if err != nil {
return nil, err
}
extraFieldsJSON, err := json.Marshal(&struct {
UpdateInterval UpdateIntervalConfigPresenter `json:"update_interval,omitempty"` UpdateInterval UpdateIntervalConfigPresenter `json:"update_interval,omitempty"`
Vulnerabilities VulnerabilitiesConfigPresenter `json:"vulnerabilities,omitempty"` Vulnerabilities VulnerabilitiesConfigPresenter `json:"vulnerabilities,omitempty"`
}{ }{
EnrichedAppConfig: fleet.EnrichedAppConfig(eacp),
UpdateInterval: UpdateIntervalConfigPresenter{ UpdateInterval: UpdateIntervalConfigPresenter{
eacp.UpdateInterval.OSQueryDetail.String(), eacp.UpdateInterval.OSQueryDetail.String(),
eacp.UpdateInterval.OSQueryPolicy.String(), eacp.UpdateInterval.OSQueryPolicy.String(),
@ -184,6 +188,13 @@ func (eacp enrichedAppConfigPresenter) MarshalJSON() ([]byte, error) {
eacp.Vulnerabilities, 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 { func printConfig(c *cli.Context, config interface{}) error {

View File

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -168,15 +167,15 @@ func TestGetTeams(t *testing.T) {
}, nil }, nil
} }
b, err := ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsText.txt")) b, err := os.ReadFile(filepath.Join("testdata", "expectedGetTeamsText.txt"))
require.NoError(t, err) require.NoError(t, err)
expectedText := string(b) 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) require.NoError(t, err)
expectedYaml := string(b) 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) require.NoError(t, err)
// must read each JSON value separately and compact it // must read each JSON value separately and compact it
var buf bytes.Buffer var buf bytes.Buffer
@ -206,8 +205,8 @@ func TestGetTeams(t *testing.T) {
errBuffer.Reset() errBuffer.Reset()
actualJSON, err := runWithErrWriter([]string{"get", "teams", "--json"}, &errBuffer) actualJSON, err := runWithErrWriter([]string{"get", "teams", "--json"}, &errBuffer)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedJson, actualJSON.String())
require.Equal(t, errBuffer.String() == expiredBanner.String(), tt.shouldHaveExpiredBanner) require.Equal(t, errBuffer.String() == expiredBanner.String(), tt.shouldHaveExpiredBanner)
require.Equal(t, expectedJson, actualJSON.String())
errBuffer.Reset() errBuffer.Reset()
actualYaml, err := runWithErrWriter([]string{"get", "teams", "--yaml"}, &errBuffer) actualYaml, err := runWithErrWriter([]string{"get", "teams", "--yaml"}, &errBuffer)
@ -433,7 +432,7 @@ func TestGetHosts(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) require.NoError(t, err)
expectedResults := tt.scanner(string(expected)) expectedResults := tt.scanner(string(expected))
actualResult := tt.scanner(runAppForTest(t, tt.args)) actualResult := tt.scanner(runAppForTest(t, tt.args))
@ -536,7 +535,7 @@ func TestGetHostsMDM(t *testing.T) {
} }
if tt.goldenFile != "" { 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) require.NoError(t, err)
if ext := filepath.Ext(tt.goldenFile); ext == ".json" { if ext := filepath.Ext(tt.goldenFile); ext == ".json" {
// the output of --json is not a json array, but a list of // the output of --json is not a json array, but a list of

View File

@ -85,6 +85,7 @@
"enabled_and_configured": false, "enabled_and_configured": false,
"apple_bm_default_team": "", "apple_bm_default_team": "",
"windows_enabled_and_configured": false, "windows_enabled_and_configured": false,
"enable_disk_encryption": false,
"macos_updates": { "macos_updates": {
"minimum_version": null, "minimum_version": null,
"deadline": null "deadline": null
@ -95,8 +96,7 @@
"webhook_url": "" "webhook_url": ""
}, },
"macos_settings": { "macos_settings": {
"custom_settings": null, "custom_settings": null
"enable_disk_encryption": false
}, },
"macos_setup": { "macos_setup": {
"bootstrap_package": null, "bootstrap_package": null,

View File

@ -19,6 +19,7 @@ spec:
enabled_and_configured: false enabled_and_configured: false
apple_bm_default_team: "" apple_bm_default_team: ""
windows_enabled_and_configured: false windows_enabled_and_configured: false
enable_disk_encryption: false
macos_migration: macos_migration:
enable: false enable: false
mode: "" mode: ""
@ -28,7 +29,6 @@ spec:
deadline: null deadline: null
macos_settings: macos_settings:
custom_settings: custom_settings:
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: bootstrap_package:
enable_end_user_authentication: false enable_end_user_authentication: false

View File

@ -43,6 +43,7 @@
"apple_bm_enabled_and_configured": false, "apple_bm_enabled_and_configured": false,
"enabled_and_configured": false, "enabled_and_configured": false,
"windows_enabled_and_configured": false, "windows_enabled_and_configured": false,
"enable_disk_encryption": false,
"macos_updates": { "macos_updates": {
"minimum_version": null, "minimum_version": null,
"deadline": null "deadline": null
@ -53,8 +54,7 @@
"webhook_url": "" "webhook_url": ""
}, },
"macos_settings": { "macos_settings": {
"custom_settings": null, "custom_settings": null
"enable_disk_encryption": false
}, },
"macos_setup": { "macos_setup": {
"bootstrap_package": null, "bootstrap_package": null,

View File

@ -19,6 +19,7 @@ spec:
apple_bm_terms_expired: false apple_bm_terms_expired: false
enabled_and_configured: false enabled_and_configured: false
windows_enabled_and_configured: false windows_enabled_and_configured: false
enable_disk_encryption: false
macos_migration: macos_migration:
enable: false enable: false
mode: "" mode: ""
@ -28,7 +29,6 @@ spec:
deadline: null deadline: null
macos_settings: macos_settings:
custom_settings: custom_settings:
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: bootstrap_package:
enable_end_user_authentication: false enable_end_user_authentication: false

View File

@ -24,13 +24,13 @@
"enable_software_inventory": true "enable_software_inventory": true
}, },
"mdm": { "mdm": {
"enable_disk_encryption": false,
"macos_updates": { "macos_updates": {
"minimum_version": null, "minimum_version": null,
"deadline": null "deadline": null
}, },
"macos_settings": { "macos_settings": {
"custom_settings": null, "custom_settings": null
"enable_disk_encryption": false
}, },
"macos_setup": { "macos_setup": {
"bootstrap_package": null, "bootstrap_package": null,
@ -84,13 +84,13 @@
} }
}, },
"mdm": { "mdm": {
"enable_disk_encryption": false,
"macos_updates": { "macos_updates": {
"minimum_version": "12.3.1", "minimum_version": "12.3.1",
"deadline": "2021-12-14" "deadline": "2021-12-14"
}, },
"macos_settings": { "macos_settings": {
"custom_settings": null, "custom_settings": null
"enable_disk_encryption": false
}, },
"macos_setup": { "macos_setup": {
"bootstrap_package": null, "bootstrap_package": null,

View File

@ -7,12 +7,12 @@ spec:
enable_host_users: true enable_host_users: true
enable_software_inventory: true enable_software_inventory: true
mdm: mdm:
enable_disk_encryption: false
macos_updates: macos_updates:
minimum_version: null minimum_version: null
deadline: null deadline: null
macos_settings: macos_settings:
custom_settings: custom_settings:
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: bootstrap_package:
enable_end_user_authentication: false enable_end_user_authentication: false
@ -36,12 +36,12 @@ spec:
enable_host_users: false enable_host_users: false
enable_software_inventory: false enable_software_inventory: false
mdm: mdm:
enable_disk_encryption: false
macos_updates: macos_updates:
minimum_version: "12.3.1" minimum_version: "12.3.1"
deadline: "2021-12-14" deadline: "2021-12-14"
macos_settings: macos_settings:
custom_settings: custom_settings:
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: bootstrap_package:
enable_end_user_authentication: false enable_end_user_authentication: false

View File

@ -19,13 +19,13 @@ spec:
apple_bm_terms_expired: false apple_bm_terms_expired: false
enabled_and_configured: true enabled_and_configured: true
windows_enabled_and_configured: false windows_enabled_and_configured: false
enable_disk_encryption: false
macos_migration: macos_migration:
enable: false enable: false
mode: "" mode: ""
webhook_url: "" webhook_url: ""
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: null bootstrap_package: null
enable_end_user_authentication: false enable_end_user_authentication: false

View File

@ -19,13 +19,13 @@ spec:
apple_bm_terms_expired: false apple_bm_terms_expired: false
enabled_and_configured: true enabled_and_configured: true
windows_enabled_and_configured: false windows_enabled_and_configured: false
enable_disk_encryption: false
macos_migration: macos_migration:
enable: false enable: false
mode: "" mode: ""
webhook_url: "" webhook_url: ""
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: %s bootstrap_package: %s
enable_end_user_authentication: false enable_end_user_authentication: false

View File

@ -7,9 +7,9 @@ spec:
enable_host_users: true enable_host_users: true
enable_software_inventory: true enable_software_inventory: true
mdm: mdm:
enable_disk_encryption: false
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: null bootstrap_package: null
enable_end_user_authentication: false enable_end_user_authentication: false
@ -27,9 +27,9 @@ spec:
enable_host_users: true enable_host_users: true
enable_software_inventory: true enable_software_inventory: true
mdm: mdm:
enable_disk_encryption: false
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: null bootstrap_package: null
macos_setup_assistant: null macos_setup_assistant: null

View File

@ -7,9 +7,9 @@ spec:
enable_host_users: true enable_host_users: true
enable_software_inventory: true enable_software_inventory: true
mdm: mdm:
enable_disk_encryption: false
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: %s bootstrap_package: %s
enable_end_user_authentication: false enable_end_user_authentication: false
@ -27,9 +27,9 @@ spec:
enable_host_users: false enable_host_users: false
enable_software_inventory: false enable_software_inventory: false
mdm: mdm:
enable_disk_encryption: false
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: %s bootstrap_package: %s
macos_setup_assistant: %s macos_setup_assistant: %s

View File

@ -7,9 +7,9 @@ spec:
enable_host_users: false enable_host_users: false
enable_software_inventory: false enable_software_inventory: false
mdm: mdm:
enable_disk_encryption: false
macos_settings: macos_settings:
custom_settings: null custom_settings: null
enable_disk_encryption: false
macos_setup: macos_setup:
bootstrap_package: null bootstrap_package: null
enable_end_user_authentication: false enable_end_user_authentication: false

View File

@ -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) - [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment)
- [Preassign profiles to devices](#preassign-profiles-to-devices) - [Preassign profiles to devices](#preassign-profiles-to-devices)
- [Match preassigned profiles](#match-preassigned-profiles) - [Match preassigned profiles](#match-preassigned-profiles)
- [Get FileVault statistics](#get-filevault-statistics)
### Generate Apple DEP Key Pair ### 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` `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 ### Match preassigned profiles
_Available in Fleet Premium_ _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, "failing_policies_count": 3,
"notifications": { "notifications": {
"needs_mdm_migration": true "needs_mdm_migration": true,
"renew_enrollment_profile": false,
"enforce_bitlocker_encryption": false,
}, },
"config": { "config": {
"org_info": { "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. - `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. - `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 #### Get device's policies

View File

@ -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/ping`
* `/api/fleet/orbit/scripts/request` * `/api/fleet/orbit/scripts/request`
* `/api/fleet/orbit/scripts/result` * `/api/fleet/orbit/scripts/result`
* `/api/fleet/orbit/disk_encryption_key`
* `/api/osquery/log` * `/api/osquery/log`
<meta name="description" value="Find commonly asked questions and answers about contributing to Fleet as part of our community."> <meta name="description" value="Find commonly asked questions and answers about contributing to Fleet as part of our community.">

View File

@ -1829,14 +1829,14 @@ the `software` table.
| page | integer | query | Page number of the results to fetch. | | page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. | | 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_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`. | | 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`. | | 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`. | | 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.). | | 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. | | 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. | | 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_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. | | 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_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` | | 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). | | 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. | | 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. | | 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`. | | 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`. | | 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. 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` 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`. 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 | | Name | Type | In | Description |
| ----------------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ----------------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| order_key | string | query | What to order results by. Can be any column in the hosts table. | | 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. | | 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`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). | | 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. | | 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_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. | | 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_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` | | 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.** | | 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). | | 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. | | 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`. | | 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.** | | 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. 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", "bootstrap_package_status": "installed",
"detail": "" "detail": ""
}, },
"os_settings": {
"disk_encryption": null
},
"profiles": [ "profiles": [
{ {
"profile_id": 999, "profile_id": 999,
@ -2743,6 +2751,9 @@ This is the API route used by the **My device** page in Fleet desktop to display
"detail": "", "detail": "",
"bootstrap_package_name": "test.pkg" "bootstrap_package_name": "test.pkg"
}, },
"os_settings": {
"disk_encryption": null
},
"profiles": [ "profiles": [
{ {
"profile_id": 999, "profile_id": 999,
@ -3291,12 +3302,12 @@ requested by a web browser.
| format | string | query | **Required**, must be "csv" (only supported format for now). | | 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). | | 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_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'. |
| 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`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). | | 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. | | 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_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. | | 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_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` | | 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). | | 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. | | 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`. | | 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. | | 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. 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 ### 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). 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). 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. | | page | integer | query | Page number of the results to fetch. |
| per_page | integer | query | Results per page. | | 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_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. | | 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`. | | 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. | | 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. | | 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'. | | 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.** | | 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. | | 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`. | | 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.** | | 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 #### Example
@ -4090,11 +4103,11 @@ _Available in Fleet Premium_
_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. 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 #### Parameters
@ -4104,9 +4117,9 @@ The summary can optionally be filtered by team id.
#### Example #### 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 ##### Default response
@ -4114,12 +4127,12 @@ Get aggregate status counts of Apple disk encryption profiles applying to macOS
```json ```json
{ {
"verified": 123, "verified": {"macos": 123, "windows": 123},
"verifying": 123, "verifying": {"macos": 123, "windows": 0},
"action_required": 123, "action_required": {"macos": 123, "windows": 0},
"enforcing": 123, "enforcing": {"macos": 123, "windows": 123},
"failed": 123, "failed": {"macos": 123, "windows": 123},
"removing_enforcement": 123 "removing_enforcement": {"macos": 123, "windows": 0},
} }
``` ```

View File

@ -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 mobile device management (MDM) certificate information | | | | ✅ | |
| View Apple business manager (BM) information | | | | ✅ | | | View Apple business manager (BM) information | | | | ✅ | |
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ | | | 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 | | | ✅ | ✅ | ✅ | | Create edit and delete configuration profiles for macOS hosts | | | ✅ | ✅ | ✅ |
| Execute MDM commands on macOS and Windows hosts*** | | | ✅ | ✅ | | | Execute MDM commands on macOS and Windows hosts*** | | | ✅ | ✅ | |
| View results of MDM commands executed on macOS and Windows hosts*** | ✅ | ✅ | ✅ | ✅ | | | View results of MDM commands executed on macOS and Windows hosts*** | ✅ | ✅ | ✅ | ✅ | |

View File

@ -15,6 +15,7 @@ import (
"strings" "strings"
"github.com/fleetdm/fleet/v4/pkg/file" "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/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb" "github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "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{ payload.MDM = &fleet.TeamPayloadMDM{
MacOSSettings: &fleet.MacOSSettings{ EnableDiskEncryption: optjson.SetBool(true),
// teams created by the match endpoint have disk encryption
// enabled by default.
// TODO: maybe make this configurable?
EnableDiskEncryption: true,
},
MacOSSetup: &fleet.MacOSSetup{ MacOSSetup: &fleet.MacOSSetup{
MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant, MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant,
// NOTE: BootstrapPackage is currently ignored by svc.ModifyTeam and gets set // NOTE: BootstrapPackage is currently ignored by svc.ModifyTeam and gets set
@ -968,3 +964,51 @@ func teamNameFromPreassignGroups(groups []string) string {
return strings.Join(groups, " - ") 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
}

View File

@ -150,13 +150,13 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
} }
} }
if payload.MDM.MacOSSettings != nil { if payload.MDM.EnableDiskEncryption.Valid {
if !appCfg.MDM.EnabledAndConfigured && payload.MDM.MacOSSettings.EnableDiskEncryption { macOSDiskEncryptionUpdated = team.Config.MDM.EnableDiskEncryption != payload.MDM.EnableDiskEncryption.Value
if macOSDiskEncryptionUpdated && !appCfg.MDM.EnabledAndConfigured {
return nil, fleet.NewInvalidArgumentError("macos_settings.enable_disk_encryption", 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.`) `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.EnableDiskEncryption = payload.MDM.EnableDiskEncryption.Value
team.Config.MDM.MacOSSettings.EnableDiskEncryption = payload.MDM.MacOSSettings.EnableDiskEncryption
} }
if payload.MDM.MacOSSetup != nil { if payload.MDM.MacOSSetup != nil {
@ -225,7 +225,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
} }
if macOSDiskEncryptionUpdated { if macOSDiskEncryptionUpdated {
var act fleet.ActivityDetails var act fleet.ActivityDetails
if team.Config.MDM.MacOSSettings.EnableDiskEncryption { if team.Config.MDM.EnableDiskEncryption {
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name} act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil { if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow") 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.`)) `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 { if dryRun {
return &fleet.Team{Name: spec.Name}, nil return &fleet.Team{Name: spec.Name}, nil
@ -813,6 +824,7 @@ func (svc *Service) createTeamFromSpec(
AgentOptions: agentOptions, AgentOptions: agentOptions,
Features: features, Features: features,
MDM: fleet.TeamMDM{ MDM: fleet.TeamMDM{
EnableDiskEncryption: enableDiskEncryption,
MacOSUpdates: spec.MDM.MacOSUpdates, MacOSUpdates: spec.MDM.MacOSUpdates,
MacOSSettings: macOSSettings, MacOSSettings: macOSSettings,
MacOSSetup: macOSSetup, MacOSSetup: macOSSetup,
@ -824,7 +836,7 @@ func (svc *Service) createTeamFromSpec(
return nil, err return nil, err
} }
if macOSSettings.EnableDiskEncryption { if enableDiskEncryption && defaults.MDM.EnabledAndConfigured {
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil { if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow") 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 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 { if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil {
return err 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 oldMacOSSetup := team.Config.MDM.MacOSSetup
if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set || spec.MDM.MacOSSetup.BootstrapPackage.Set { if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set || spec.MDM.MacOSSetup.BootstrapPackage.Set {
@ -925,9 +949,9 @@ func (svc *Service) editTeamFromSpec(
return err return err
} }
} }
if oldMacOSDiskEncryption != newMacOSDiskEncryption { if appCfg.MDM.EnabledAndConfigured && oldMacOSDiskEncryption != team.Config.MDM.EnableDiskEncryption {
var act fleet.ActivityDetails var act fleet.ActivityDetails
if team.Config.MDM.MacOSSettings.EnableDiskEncryption { if team.Config.MDM.EnableDiskEncryption {
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name} act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil { if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil {
return ctxerr.Wrap(ctx, err, "enable team filevault and escrow") 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) || if (setFields["custom_settings"] && len(applyUpon.CustomSettings) > 0) ||
(setFields["enable_disk_encryption"] && applyUpon.EnableDiskEncryption) { (setFields["enable_disk_encryption"] && *applyUpon.DeprecatedEnableDiskEncryption) {
field := "custom_settings" field := "custom_settings"
if !setFields["custom_settings"] { if !setFields["custom_settings"] {
field = "enable_disk_encryption" 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 { func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Team, payload fleet.MDMAppleSettingsPayload) error {
var didUpdate, didUpdateMacOSDiskEncryption bool var didUpdate, didUpdateMacOSDiskEncryption bool
if payload.EnableDiskEncryption != nil { if payload.EnableDiskEncryption != nil {
if tm.Config.MDM.MacOSSettings.EnableDiskEncryption != *payload.EnableDiskEncryption { if tm.Config.MDM.EnableDiskEncryption != *payload.EnableDiskEncryption {
tm.Config.MDM.MacOSSettings.EnableDiskEncryption = *payload.EnableDiskEncryption tm.Config.MDM.EnableDiskEncryption = *payload.EnableDiskEncryption
didUpdate = true didUpdate = true
didUpdateMacOSDiskEncryption = true didUpdateMacOSDiskEncryption = true
} }
@ -1029,7 +1053,7 @@ func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Te
} }
if didUpdateMacOSDiskEncryption { if didUpdateMacOSDiskEncryption {
var act fleet.ActivityDetails var act fleet.ActivityDetails
if tm.Config.MDM.MacOSSettings.EnableDiskEncryption { if tm.Config.MDM.EnableDiskEncryption {
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name} act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name}
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil { if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
return ctxerr.Wrap(ctx, err, "enable team filevault and escrow") return ctxerr.Wrap(ctx, err, "enable team filevault and escrow")

View File

@ -125,6 +125,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
}, },
fleet_desktop: { transparency_url: "https://fleetdm.com/transparency" }, fleet_desktop: { transparency_url: "https://fleetdm.com/transparency" },
mdm: { mdm: {
enable_disk_encryption: false,
windows_enabled_and_configured: true, windows_enabled_and_configured: true,
apple_bm_default_team: "Apples", apple_bm_default_team: "Apples",
apple_bm_enabled_and_configured: true, apple_bm_enabled_and_configured: true,

View File

@ -1,7 +1,7 @@
import { IHost } from "interfaces/host"; 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, profile_id: 1,
name: "Test Profile", name: "Test Profile",
operation_type: "install", operation_type: "install",
@ -10,8 +10,8 @@ const DEFAULT_HOST_PROFILE_MOCK: IHostMacMdmProfile = {
}; };
export const createMockHostMacMdmProfile = ( export const createMockHostMacMdmProfile = (
overrides?: Partial<IHostMacMdmProfile> overrides?: Partial<IHostMdmProfile>
): IHostMacMdmProfile => { ): IHostMdmProfile => {
return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides }; return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides };
}; };
@ -53,6 +53,11 @@ const DEFAULT_HOST_MOCK: IHost = {
enrollment_status: "Off", enrollment_status: "Off",
server_url: "https://www.example.com/1", server_url: "https://www.example.com/1",
profiles: [], profiles: [],
os_settings: {
disk_encryption: {
status: null,
},
},
macos_settings: { macos_settings: {
disk_encryption: null, disk_encryption: null,
action_required: null, action_required: null,

View File

@ -36,6 +36,11 @@ const DEFAULT_HOST_MDM_DATA: IHostMdmData = {
name: "MDM Solution", name: "MDM Solution",
id: 1, id: 1,
profiles: [], profiles: [],
os_settings: {
disk_encryption: {
status: "verified",
},
},
macos_settings: { macos_settings: {
disk_encryption: null, disk_encryption: null,
action_required: null, action_required: null,

View File

@ -23,7 +23,10 @@ interface IStatusIndicatorWithIconProps {
tooltipText: string | JSX.Element; tooltipText: string | JSX.Element;
position?: "top" | "bottom"; position?: "top" | "bottom";
}; };
layout?: "horizontal" | "vertical";
className?: string; className?: string;
/** Classname to add to the value text */
valueClassName?: string;
} }
const statusIconNameMapping: Record<IndicatorStatus, IconNames> = { const statusIconNameMapping: Record<IndicatorStatus, IconNames> = {
@ -38,13 +41,18 @@ const StatusIndicatorWithIcon = ({
status, status,
value, value,
tooltip, tooltip,
layout = "horizontal",
className, className,
valueClassName,
}: IStatusIndicatorWithIconProps) => { }: IStatusIndicatorWithIconProps) => {
const classNames = classnames(baseClass, className); const classNames = classnames(baseClass, className);
const id = `status-${uniqueId()}`; const id = `status-${uniqueId()}`;
const valueClasses = classnames(`${baseClass}__value`, valueClassName, {
[`${baseClass}__value-vertical`]: layout === "vertical",
});
const valueContent = ( const valueContent = (
<span className={`${baseClass}__value`}> <span className={valueClasses}>
<Icon name={statusIconNameMapping[status]} /> <Icon name={statusIconNameMapping[status]} />
<span>{value}</span> <span>{value}</span>
</span> </span>

View File

@ -1,4 +1,5 @@
.status-indicator-with-icon { .status-indicator-with-icon {
// default layout is horizontal
&__value { &__value {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -8,4 +9,10 @@
margin-right: $pad-xsmall; margin-right: $pad-xsmall;
} }
} }
// overrides for different layout
&__value-vertical {
flex-direction: column;
gap: $pad-xsmall;
}
} }

View File

@ -1,95 +1,11 @@
/* Config interface is a flattened version of the fleet/config API response */ /* Config interface is a flattened version of the fleet/config API response */
import { import {
IWebhookHostStatus, IWebhookHostStatus,
IWebhookFailingPolicies, IWebhookFailingPolicies,
IWebhookSoftwareVulnerabilities, IWebhookSoftwareVulnerabilities,
} from "interfaces/webhook"; } from "interfaces/webhook";
import PropTypes from "prop-types";
import { IIntegrations } from "./integration"; 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 { export interface ILicense {
tier: string; tier: string;
device_count: number; device_count: number;
@ -113,6 +29,7 @@ export interface IMacOsMigrationSettings {
} }
export interface IMdmConfig { export interface IMdmConfig {
enable_disk_encryption: boolean;
enabled_and_configured: boolean; enabled_and_configured: boolean;
apple_bm_default_team?: string; apple_bm_default_team?: string;
apple_bm_terms_expired: boolean; apple_bm_terms_expired: boolean;
@ -285,7 +202,10 @@ export interface IConfig {
}; };
}; };
mdm: IMdmConfig; 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 { export interface IWebhookSettings {

View File

@ -8,9 +8,10 @@ import hostQueryResult from "./campaign";
import queryStatsInterface, { IQueryStats } from "./query_stats"; import queryStatsInterface, { IQueryStats } from "./query_stats";
import { ILicense, IDeviceGlobalConfig } from "./config"; import { ILicense, IDeviceGlobalConfig } from "./config";
import { import {
IHostMacMdmProfile, IHostMdmProfile,
MdmEnrollmentStatus, MdmEnrollmentStatus,
BootstrapPackageStatus, BootstrapPackageStatus,
DiskEncryptionStatus,
} from "./mdm"; } from "./mdm";
export default PropTypes.shape({ export default PropTypes.shape({
@ -90,18 +91,16 @@ export interface IMunkiData {
version: string; version: string;
} }
type MacDiskEncryptionState =
| "applied"
| "action_required"
| "enforcing"
| "failed"
| "removing_enforcement"
| null;
type MacDiskEncryptionActionRequired = "log_out" | "rotate_key" | null; type MacDiskEncryptionActionRequired = "log_out" | "rotate_key" | null;
export interface IOSSettings {
disk_encryption: {
status: DiskEncryptionStatus | null;
};
}
interface IMdmMacOsSettings { interface IMdmMacOsSettings {
disk_encryption: MacDiskEncryptionState | null; disk_encryption: DiskEncryptionStatus | null;
action_required: MacDiskEncryptionActionRequired | null; action_required: MacDiskEncryptionActionRequired | null;
} }
@ -117,7 +116,8 @@ export interface IHostMdmData {
name?: string; name?: string;
server_url: string | null; server_url: string | null;
id?: number; id?: number;
profiles: IHostMacMdmProfile[] | null; profiles: IHostMdmProfile[] | null;
os_settings?: IOSSettings;
macos_settings?: IMdmMacOsSettings; macos_settings?: IMdmMacOsSettings;
macos_setup?: IMdmMacOsSetup; macos_setup?: IMdmMacOsSetup;
} }
@ -210,7 +210,7 @@ export interface IHost {
osquery_version: string; osquery_version: string;
os_version: string; os_version: string;
build: string; build: string;
platform_like: string; platform_like: string; // TODO: replace with more specific union type
code_name: string; code_name: string;
uptime: number; uptime: number;
memory: number; memory: number;

View File

@ -22,8 +22,6 @@ export const MDM_ENROLLMENT_STATUS = {
export type MdmEnrollmentStatus = keyof typeof MDM_ENROLLMENT_STATUS; export type MdmEnrollmentStatus = keyof typeof MDM_ENROLLMENT_STATUS;
export type ProfileSummaryResponse = Record<MdmProfileStatus, number>;
export interface IMdmStatusCardData { export interface IMdmStatusCardData {
status: MdmEnrollmentStatus; status: MdmEnrollmentStatus;
hosts: number; hosts: number;
@ -74,16 +72,15 @@ export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
export type MacMdmProfileOperationType = "remove" | "install"; export type MacMdmProfileOperationType = "remove" | "install";
export interface IHostMacMdmProfile { export interface IHostMdmProfile {
profile_id: number; profile_id: number;
name: string; name: string;
// identifier?: string; // TODO: add when API is updated to return this operation_type: MacMdmProfileOperationType | null;
operation_type: MacMdmProfileOperationType;
status: MdmProfileStatus; status: MdmProfileStatus;
detail: string; detail: string;
} }
export type FileVaultProfileStatus = export type DiskEncryptionStatus =
| "verified" | "verified"
| "verifying" | "verifying"
| "action_required" | "action_required"
@ -91,9 +88,18 @@ export type FileVaultProfileStatus =
| "failed" | "failed"
| "removing_enforcement"; | "removing_enforcement";
// // TODO: update when list profiles API returns identifier /** Currently windows disk enxryption status will only be one of these four
// export const FLEET_FILEVAULT_PROFILE_IDENTIFIER = values. In the future we may add more. */
// "com.fleetdm.fleet.mdm.filevault"; 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"; export const FLEET_FILEVAULT_PROFILE_DISPLAY_NAME = "Disk encryption";

View File

@ -44,6 +44,7 @@ export interface ITeam extends ITeamSummary {
secrets?: IEnrollSecret[]; secrets?: IEnrollSecret[];
role?: UserRole; // role value is included when the team is in the context of a user role?: UserRole; // role value is included when the team is in the context of a user
mdm?: { mdm?: {
enable_disk_encryption: boolean;
macos_updates: { macos_updates: {
minimum_version: string; minimum_version: string;
deadline: string; deadline: string;

View File

@ -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 (
<div className="aggregate-mac-settings-indicator">
<MacSettingsIndicator
indicatorText={text}
iconName={iconName}
tooltip={{ tooltipText, position: "top" }}
/>
<a
href={`${paths.MANAGE_HOSTS}?${buildQueryStringFromParams({
team_id: teamId,
macos_settings: value,
})}`}
>
{count} hosts
</a>
</div>
);
});
if (isLoading) {
return (
<div className={baseClass}>
<Spinner className={`${baseClass}__loading-spinner`} centered={false} />
</div>
);
}
return <div className={baseClass}>{indicators}</div>;
};
export default AggregateMacSettingsIndicators;

View File

@ -1 +0,0 @@
export { default } from "./AggregateMacSettingsIndicators";

View File

@ -4,12 +4,11 @@ import { useQuery } from "react-query";
import { AppContext } from "context/app"; import { AppContext } from "context/app";
import SideNav from "pages/admin/components/SideNav"; import SideNav from "pages/admin/components/SideNav";
import { ProfileSummaryResponse } from "interfaces/mdm";
import { API_NO_TEAM_ID, APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import { API_NO_TEAM_ID, APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
import mdmAPI from "services/entities/mdm"; import mdmAPI from "services/entities/mdm";
import OS_SETTINGS_NAV_ITEMS from "./OSSettingsNavItems"; import OS_SETTINGS_NAV_ITEMS from "./OSSettingsNavItems";
import AggregateMacSettingsIndicators from "./AggregateMacSettingsIndicators"; import ProfileStatusAggregate from "./ProfileStatusAggregate";
import TurnOnMdmMessage from "../components/TurnOnMdmMessage"; import TurnOnMdmMessage from "../components/TurnOnMdmMessage";
const baseClass = "os-settings"; const baseClass = "os-settings";
@ -40,9 +39,10 @@ const OSSettings = ({
data: aggregateProfileStatusData, data: aggregateProfileStatusData,
refetch: refetchAggregateProfileStatus, refetch: refetchAggregateProfileStatus,
isLoading: isLoadingAggregateProfileStatus, isLoading: isLoadingAggregateProfileStatus,
} = useQuery<ProfileSummaryResponse>( } = useQuery(
["aggregateProfileStatuses", teamId], ["aggregateProfileStatuses", teamId],
() => mdmAPI.getAggregateProfileStatuses(teamId), () =>
mdmAPI.getAggregateProfileStatuses(teamId, config?.mdm_enabled ?? false),
{ {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: false, retry: false,
@ -50,7 +50,10 @@ const OSSettings = ({
); );
// MDM is not on so show messaging for user to enable it. // MDM is not on so show messaging for user to enable it.
if (!config?.mdm.enabled_and_configured) { if (
!config?.mdm.enabled_and_configured &&
!config?.mdm.windows_enabled_and_configured
) {
return <TurnOnMdmMessage router={router} />; return <TurnOnMdmMessage router={router} />;
} }
@ -67,7 +70,7 @@ const OSSettings = ({
<p className={`${baseClass}__description`}> <p className={`${baseClass}__description`}>
Remotely enforce settings on macOS hosts assigned to this team. Remotely enforce settings on macOS hosts assigned to this team.
</p> </p>
<AggregateMacSettingsIndicators <ProfileStatusAggregate
isLoading={isLoadingAggregateProfileStatus} isLoading={isLoadingAggregateProfileStatus}
teamId={teamId} teamId={teamId}
aggregateProfileStatusData={aggregateProfileStatusData} aggregateProfileStatusData={aggregateProfileStatusData}

View File

@ -0,0 +1,95 @@
import React from "react";
import paths from "router/paths";
import { buildQueryStringFromParams } from "utilities/url";
import { MdmProfileStatus } from "interfaces/mdm";
import { ProfileStatusSummaryResponse } from "services/entities/mdm";
import Spinner from "components/Spinner";
import StatusIndicatorWithIcon, {
IndicatorStatus,
} from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon";
import AGGREGATE_STATUS_DISPLAY_OPTIONS from "./ProfileStatusAggregateOptions";
const baseClass = "profile-status-aggregate";
interface IProfileStatusCountProps {
statusIcon: IndicatorStatus;
statusValue: MdmProfileStatus;
title: string;
teamId: number;
hostCount: number;
tooltipText: string;
}
const ProfileStatusCount = ({
statusIcon,
statusValue,
teamId,
title,
hostCount,
tooltipText,
}: IProfileStatusCountProps) => {
const generateFilterHostsByStatusLink = () => {
return `${paths.MANAGE_HOSTS}?${buildQueryStringFromParams({
team_id: teamId,
macos_settings: statusValue,
})}`;
};
return (
<div className={`${baseClass}__profile-status-count`}>
<StatusIndicatorWithIcon
status={statusIcon}
value={title}
tooltip={{ tooltipText, position: "top" }}
layout="vertical"
valueClassName={`${baseClass}__status-indicator-value`}
/>
<a href={generateFilterHostsByStatusLink()}>{hostCount} hosts</a>
</div>
);
};
interface ProfileStatusAggregateProps {
isLoading: boolean;
teamId: number;
aggregateProfileStatusData?: ProfileStatusSummaryResponse;
}
const ProfileStatusAggregate = ({
isLoading,
teamId,
aggregateProfileStatusData,
}: ProfileStatusAggregateProps) => {
if (!aggregateProfileStatusData) return null;
if (isLoading) {
return (
<div className={baseClass}>
<Spinner className={`${baseClass}__loading-spinner`} centered={false} />
</div>
);
}
const indicators = AGGREGATE_STATUS_DISPLAY_OPTIONS.map((status) => {
const { value, text, iconName, tooltipText } = status;
const count = aggregateProfileStatusData[value];
return (
<ProfileStatusCount
statusIcon={iconName}
statusValue={value}
teamId={teamId}
title={text}
hostCount={count}
tooltipText={tooltipText}
/>
);
});
return <div className={baseClass}>{indicators}</div>;
};
export default ProfileStatusAggregate;

View File

@ -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;

View File

@ -1,4 +1,4 @@
.aggregate-mac-settings-indicators { .profile-status-aggregate {
display: flex; display: flex;
height: 94px; height: 94px;
border-top: 1px solid #e2e4ea; border-top: 1px solid #e2e4ea;
@ -10,7 +10,7 @@
margin: auto; margin: auto;
} }
.aggregate-mac-settings-indicator { &__profile-status-count {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
@ -29,13 +29,17 @@
font-weight: $regular; font-weight: $regular;
} }
.settings-indicator { .profile-status-indicator {
flex-direction: column; flex-direction: column;
} }
} }
.aggregate-mac-settings-indicator:last-child { &__profile-status-count:last-child {
border-top-right-radius: 6px; border-top-right-radius: 6px;
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;
} }
&__status-indicator-value {
font-weight: $bold;
}
} }

View File

@ -0,0 +1 @@
export { default } from "./ProfileStatusAggregate";

View File

@ -31,7 +31,7 @@ const DiskEncryption = ({
const defaultShowDiskEncryption = currentTeamId const defaultShowDiskEncryption = currentTeamId
? false ? false
: config?.mdm.macos_settings.enable_disk_encryption ?? false; : config?.mdm.enable_disk_encryption ?? false;
const [isLoadingTeam, setIsLoadingTeam] = useState(true); const [isLoadingTeam, setIsLoadingTeam] = useState(true);
@ -67,8 +67,7 @@ const DiskEncryption = ({
enabled: currentTeamId !== 0, enabled: currentTeamId !== 0,
select: (res) => res.team, select: (res) => res.team,
onSuccess: (res) => { onSuccess: (res) => {
const enableDiskEncryption = const enableDiskEncryption = res.mdm?.enable_disk_encryption ?? false;
res.mdm?.macos_settings.enable_disk_encryption ?? false;
setDiskEncryptionEnabled(enableDiskEncryption); setDiskEncryptionEnabled(enableDiskEncryption);
setShowAggregate(enableDiskEncryption); setShowAggregate(enableDiskEncryption);
setIsLoadingTeam(false); setIsLoadingTeam(false);
@ -100,6 +99,19 @@ const DiskEncryption = ({
setIsLoadingTeam(false); 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 ( return (
<div className={baseClass}> <div className={baseClass}>
<h2>Disk encryption</h2> <h2>Disk encryption</h2>
@ -124,8 +136,7 @@ const DiskEncryption = ({
On On
</Checkbox> </Checkbox>
<p> <p>
Apple calls this FileVault. If turned on, hosts&apos; disk {createDescriptionText()}
encryption keys will be stored in Fleet.{" "}
<CustomLink <CustomLink
text="Learn more" text="Learn more"
url="https://fleetdm.com/docs/using-fleet/mdm-disk-encryption" url="https://fleetdm.com/docs/using-fleet/mdm-disk-encryption"

View File

@ -2,6 +2,7 @@
h2 { h2 {
margin-top: 0; margin-top: 0;
padding-bottom: $pad-small; padding-bottom: $pad-small;
margin-bottom: $pad-xxlarge;
font-size: $medium; font-size: $medium;
font-weight: $regular; font-weight: $regular;
color: $core-fleet-black; color: $core-fleet-black;

View File

@ -1,7 +1,8 @@
import React from "react"; import React, { useContext } from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import mdmAPI, { IFileVaultSummaryResponse } from "services/entities/mdm"; import { AppContext } from "context/app";
import mdmAPI, { IDiskEncryptionSummaryResponse } from "services/entities/mdm";
import TableContainer from "components/TableContainer"; import TableContainer from "components/TableContainer";
import EmptyTable from "components/EmptyTable"; import EmptyTable from "components/EmptyTable";
@ -18,25 +19,30 @@ interface IDiskEncryptionTableProps {
currentTeamId?: number; currentTeamId?: number;
} }
const DEFAULT_SORT_HEADER = "hosts";
const DEFAULT_SORT_DIRECTION = "asc";
const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => { const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
const { config } = useContext(AppContext);
const { const {
data: diskEncryptionStatusData, data: diskEncryptionStatusData,
error: diskEncryptionStatusError, error: diskEncryptionStatusError,
} = useQuery<IFileVaultSummaryResponse, Error, IFileVaultSummaryResponse>( } = useQuery<IDiskEncryptionSummaryResponse, Error>(
["disk-encryption-summary", currentTeamId], ["disk-encryption-summary", currentTeamId],
() => mdmAPI.getDiskEncryptionAggregate(currentTeamId), () => mdmAPI.getDiskEncryptionSummary(currentTeamId),
{ {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: false, retry: false,
} }
); );
const tableHeaders = generateTableHeaders(); // 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 tableData = generateTableData(diskEncryptionStatusData, currentTeamId); const windowsFeatureFlagEnabled = config?.mdm_enabled ?? false;
const tableHeaders = generateTableHeaders(windowsFeatureFlagEnabled);
const tableData = generateTableData(
windowsFeatureFlagEnabled,
diskEncryptionStatusData,
currentTeamId
);
if (diskEncryptionStatusError) { if (diskEncryptionStatusError) {
return <DataError />; return <DataError />;
@ -53,8 +59,7 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
isLoading={false} isLoading={false}
showMarkAllPages={false} showMarkAllPages={false}
isAllPagesSelected={false} isAllPagesSelected={false}
defaultSortHeader={DEFAULT_SORT_HEADER} manualSortBy
defaultSortDirection={DEFAULT_SORT_DIRECTION}
disableTableHeader disableTableHeader
disablePagination disablePagination
disableCount disableCount

View File

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import { FileVaultProfileStatus } from "interfaces/mdm"; import { DiskEncryptionStatus } from "interfaces/mdm";
import { IFileVaultSummaryResponse } from "services/entities/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 TextCell from "components/TableContainer/DataTable/TextCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
@ -12,7 +16,7 @@ import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndica
interface IStatusCellValue { interface IStatusCellValue {
displayName: string; displayName: string;
statusName: IndicatorStatus; statusName: IndicatorStatus;
value: FileVaultProfileStatus; value: DiskEncryptionStatus;
tooltip?: string | JSX.Element; tooltip?: string | JSX.Element;
} }
@ -28,6 +32,7 @@ interface ICellProps {
}; };
row: { row: {
original: { original: {
includeWindows: boolean;
status: IStatusCellValue; status: IStatusCellValue;
teamId: number; teamId: number;
}; };
@ -72,15 +77,53 @@ const defaultTableHeaders: IDataColumn[] = [
}, },
}, },
{ {
title: "Hosts", title: "macOS hosts",
Header: (cellProps: IHeaderProps) => ( Header: (cellProps: IHeaderProps) => (
<HeaderCell <HeaderCell
value={cellProps.column.title} value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc} isSortedDesc={cellProps.column.isSortedDesc}
disableSortBy={false} disableSortBy
/> />
), ),
accessor: "hosts", disableSortBy: true,
accessor: "macosHosts",
Cell: ({
cell: { value: aggregateCount },
row: { original },
}: ICellProps) => {
return (
<div className="disk-encryption-table__aggregate-table-data">
<TextCell value={aggregateCount} formatter={(val) => <>{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 && (
<ViewAllHostsLink
className="view-hosts-link"
queryParams={{
[DISK_ENCRYPTION_QUERY_PARAM_NAME]: original.status.value,
team_id: original.teamId,
}}
/>
)}
</div>
);
},
},
];
const windowsTableHeader: IDataColumn[] = [
{
title: "Windows hosts",
Header: (cellProps: IHeaderProps) => (
<HeaderCell
value={cellProps.column.title}
isSortedDesc={cellProps.column.isSortedDesc}
disableSortBy
/>
),
disableSortBy: true,
accessor: "windowsHosts",
Cell: ({ Cell: ({
cell: { value: aggregateCount }, cell: { value: aggregateCount },
row: { original }, row: { original },
@ -91,7 +134,7 @@ const defaultTableHeaders: IDataColumn[] = [
<ViewAllHostsLink <ViewAllHostsLink
className="view-hosts-link" className="view-hosts-link"
queryParams={{ queryParams={{
macos_settings_disk_encryption: original.status.value, [DISK_ENCRYPTION_QUERY_PARAM_NAME]: original.status.value,
team_id: original.teamId, team_id: original.teamId,
}} }}
/> />
@ -101,15 +144,17 @@ const defaultTableHeaders: IDataColumn[] = [
}, },
]; ];
type StatusNames = keyof IFileVaultSummaryResponse; // TODO: WINDOWS FEATURE FLAG: return all headers when windows feature flag is removed.
export const generateTableHeaders = (
type StatusEntry = [StatusNames, number]; includeWindows: boolean
): IDataColumn[] => {
export const generateTableHeaders = (): IDataColumn[] => { return includeWindows
? [...defaultTableHeaders, ...windowsTableHeader]
: defaultTableHeaders;
return defaultTableHeaders; return defaultTableHeaders;
}; };
const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = { const STATUS_CELL_VALUES: Record<DiskEncryptionStatus, IStatusCellValue> = {
verified: { verified: {
displayName: "Verified", displayName: "Verified",
statusName: "success", statusName: "success",
@ -122,8 +167,8 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
statusName: "successPartial", statusName: "successPartial",
value: "verifying", value: "verifying",
tooltip: tooltip:
"These hosts acknowledged the MDM command to install disk encryption profile. " + "These hosts acknowledged the MDM command to turn on disk encryption. Fleet is verifying with " +
"Fleet is verifying with osquery and retrieving the disk encryption key. This may take up to one hour.", "osquery and retrieving the disk encryption key. This may take up to one hour.",
}, },
action_required: { action_required: {
displayName: "Action required (pending)", displayName: "Action required (pending)",
@ -141,7 +186,7 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
statusName: "pendingPartial", statusName: "pendingPartial",
value: "enforcing", value: "enforcing",
tooltip: 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: { failed: {
displayName: "Failed", displayName: "Failed",
@ -153,21 +198,41 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
statusName: "pendingPartial", statusName: "pendingPartial",
value: "removing_enforcement", value: "removing_enforcement",
tooltip: 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 = ( 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 currentTeamId?: number
) => { ) => {
if (!data) return []; if (!data) return [];
const entries = Object.entries(data) as StatusEntry[];
return entries.map(([status, numHosts]) => ({ const rowFromStatusEntry = (
// eslint-disable-next-line object-shorthand status: DiskEncryptionStatus,
statusAggregate: IDiskEncryptionStatusAggregate
) => ({
includeWindows,
status: STATUS_CELL_VALUES[status], status: STATUS_CELL_VALUES[status],
hosts: numHosts, macosHosts: statusAggregate.macos,
windowsHosts: statusAggregate.windows,
teamId: currentTeamId, teamId: currentTeamId,
})); });
return STATUS_ORDER.map((status) => rowFromStatusEntry(status, data[status]));
}; };

View File

@ -1,7 +1,4 @@
.disk-encryption-table { .disk-encryption-table {
padding: $pad-xxlarge;
border: 1px solid $ui-fleet-black-10;
border-radius: $border-radius;
margin-bottom: $pad-xxlarge; margin-bottom: $pad-xxlarge;
.data-table-block .data-table tbody td .w250 { .data-table-block .data-table tbody td .w250 {

View File

@ -30,7 +30,7 @@ const TurnOnMdmMessage = ({ router }: ITurnOnMdmMessageProps) => {
return ( return (
<EmptyTable <EmptyTable
header="Manage your macOS hosts" header="Manage your hosts"
info={"Turn on MDM to change settings on your hosts."} info={"Turn on MDM to change settings on your hosts."}
primaryButton={renderConnectButton()} primaryButton={renderConnectButton()}
/> />

View File

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import Icon from "components/Icon"; import Icon from "components/Icon";
import { DISK_ENCRYPTION_QUERY_PARAM_NAME } from "services/entities/hosts";
export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [ export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [
"query", "query",
@ -17,7 +18,7 @@ export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [
"os_version", "os_version",
"munki_issue_id", "munki_issue_id",
"low_disk_space", "low_disk_space",
"macos_settings_disk_encryption", DISK_ENCRYPTION_QUERY_PARAM_NAME,
"bootstrap_package", "bootstrap_package",
] as const; ] as const;

View File

@ -18,6 +18,7 @@ import labelsAPI, { ILabelsResponse } from "services/entities/labels";
import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams"; import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
import globalPoliciesAPI from "services/entities/global_policies"; import globalPoliciesAPI from "services/entities/global_policies";
import hostsAPI, { import hostsAPI, {
DISK_ENCRYPTION_QUERY_PARAM_NAME,
ILoadHostsQueryKey, ILoadHostsQueryKey,
ILoadHostsResponse, ILoadHostsResponse,
ISortOption, ISortOption,
@ -49,7 +50,7 @@ import { IOperatingSystemVersion } from "interfaces/operating_system";
import { IPolicy, IStoredPolicyResponse } from "interfaces/policy"; import { IPolicy, IStoredPolicyResponse } from "interfaces/policy";
import { ITeam } from "interfaces/team"; import { ITeam } from "interfaces/team";
import { IEmptyTableProps } from "interfaces/empty_table"; import { IEmptyTableProps } from "interfaces/empty_table";
import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm"; import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm";
import sortUtils from "utilities/sort"; import sortUtils from "utilities/sort";
import { import {
@ -232,8 +233,8 @@ const ManageHostsPage = ({
? parseInt(queryParams.low_disk_space, 10) ? parseInt(queryParams.low_disk_space, 10)
: undefined; : undefined;
const missingHosts = queryParams?.status === "missing"; const missingHosts = queryParams?.status === "missing";
const diskEncryptionStatus: FileVaultProfileStatus | undefined = const diskEncryptionStatus: DiskEncryptionStatus | undefined =
queryParams?.macos_settings_disk_encryption; queryParams?.[DISK_ENCRYPTION_QUERY_PARAM_NAME];
const bootstrapPackageStatus: BootstrapPackageStatus | undefined = const bootstrapPackageStatus: BootstrapPackageStatus | undefined =
queryParams?.bootstrap_package; queryParams?.bootstrap_package;
@ -558,7 +559,7 @@ const ManageHostsPage = ({
}; };
const handleChangeDiskEncryptionStatusFilter = ( const handleChangeDiskEncryptionStatusFilter = (
newStatus: FileVaultProfileStatus newStatus: DiskEncryptionStatus
) => { ) => {
handleResetPageIndex(); handleResetPageIndex();
@ -569,7 +570,7 @@ const ManageHostsPage = ({
routeParams, routeParams,
queryParams: { queryParams: {
...queryParams, ...queryParams,
macos_settings_disk_encryption: newStatus, [DISK_ENCRYPTION_QUERY_PARAM_NAME]: newStatus,
page: 0, // resets page index page: 0, // resets page index
}, },
}) })
@ -768,7 +769,7 @@ const ManageHostsPage = ({
newQueryParams.os_version = osVersion; newQueryParams.os_version = osVersion;
} else if (diskEncryptionStatus && isPremiumTier) { } else if (diskEncryptionStatus && isPremiumTier) {
// Premium feature only // Premium feature only
newQueryParams.macos_settings_disk_encryption = diskEncryptionStatus; newQueryParams[DISK_ENCRYPTION_QUERY_PARAM_NAME] = diskEncryptionStatus;
} else if (bootstrapPackageStatus && isPremiumTier) { } else if (bootstrapPackageStatus && isPremiumTier) {
newQueryParams.bootstrap_package = bootstrapPackageStatus; newQueryParams.bootstrap_package = bootstrapPackageStatus;
} }

View File

@ -4,7 +4,7 @@ import { IDropdownOption } from "interfaces/dropdownOption";
// @ts-ignore // @ts-ignore
import Dropdown from "components/forms/fields/Dropdown"; import Dropdown from "components/forms/fields/Dropdown";
import { FileVaultProfileStatus } from "interfaces/mdm"; import { DiskEncryptionStatus } from "interfaces/mdm";
const baseClass = "disk-encryption-status-filter"; const baseClass = "disk-encryption-status-filter";
@ -42,8 +42,8 @@ const DISK_ENCRYPTION_STATUS_OPTIONS: IDropdownOption[] = [
]; ];
interface IDiskEncryptionStatusFilterProps { interface IDiskEncryptionStatusFilterProps {
diskEncryptionStatus: FileVaultProfileStatus; diskEncryptionStatus: DiskEncryptionStatus;
onChange: (value: FileVaultProfileStatus) => void; onChange: (value: DiskEncryptionStatus) => void;
} }
const DiskEncryptionStatusFilter = ({ const DiskEncryptionStatusFilter = ({

View File

@ -7,7 +7,7 @@ import {
IOperatingSystemVersion, IOperatingSystemVersion,
} from "interfaces/operating_system"; } from "interfaces/operating_system";
import { import {
FileVaultProfileStatus, DiskEncryptionStatus,
BootstrapPackageStatus, BootstrapPackageStatus,
IMdmSolution, IMdmSolution,
MDM_ENROLLMENT_STATUS, MDM_ENROLLMENT_STATUS,
@ -15,7 +15,10 @@ import {
import { IMunkiIssuesAggregate } from "interfaces/macadmins"; import { IMunkiIssuesAggregate } from "interfaces/macadmins";
import { ISoftware } from "interfaces/software"; import { ISoftware } from "interfaces/software";
import { IPolicy } from "interfaces/policy"; import { IPolicy } from "interfaces/policy";
import { MacSettingsStatusQueryParam } from "services/entities/hosts"; import {
DISK_ENCRYPTION_QUERY_PARAM_NAME,
MacSettingsStatusQueryParam,
} from "services/entities/hosts";
import { import {
PLATFORM_LABEL_DISPLAY_NAMES, PLATFORM_LABEL_DISPLAY_NAMES,
@ -60,7 +63,7 @@ interface IHostsFilterBlockProps {
osVersions?: IOperatingSystemVersion[]; osVersions?: IOperatingSystemVersion[];
softwareDetails: ISoftware | null; softwareDetails: ISoftware | null;
mdmSolutionDetails: IMdmSolution | null; mdmSolutionDetails: IMdmSolution | null;
diskEncryptionStatus?: FileVaultProfileStatus; diskEncryptionStatus?: DiskEncryptionStatus;
bootstrapPackageStatus?: BootstrapPackageStatus; bootstrapPackageStatus?: BootstrapPackageStatus;
}; };
selectedLabel?: ILabel; selectedLabel?: ILabel;
@ -68,9 +71,7 @@ interface IHostsFilterBlockProps {
handleClearRouteParam: () => void; handleClearRouteParam: () => void;
handleClearFilter: (omitParams: string[]) => void; handleClearFilter: (omitParams: string[]) => void;
onChangePoliciesFilter: (response: PolicyResponse) => void; onChangePoliciesFilter: (response: PolicyResponse) => void;
onChangeDiskEncryptionStatusFilter: ( onChangeDiskEncryptionStatusFilter: (response: DiskEncryptionStatus) => void;
response: FileVaultProfileStatus
) => void;
onChangeBootstrapPackageStatusFilter: ( onChangeBootstrapPackageStatusFilter: (
response: BootstrapPackageStatus response: BootstrapPackageStatus
) => void; ) => void;
@ -376,8 +377,8 @@ const HostsFilterBlock = ({
onChange={onChangeDiskEncryptionStatusFilter} onChange={onChangeDiskEncryptionStatusFilter}
/> />
<FilterPill <FilterPill
label="macOS settings: Disk encryption" label="OS settings: Disk encryption"
onClear={() => handleClearFilter(["macos_settings_disk_encryption"])} onClear={() => handleClearFilter([DISK_ENCRYPTION_QUERY_PARAM_NAME])}
/> />
</> </>
); );

View File

@ -417,6 +417,7 @@ const DeviceUserPage = ({
showRefetchSpinner={showRefetchSpinner} showRefetchSpinner={showRefetchSpinner}
onRefetchHost={onRefetchHost} onRefetchHost={onRefetchHost}
renderActionButtons={renderActionButtons} renderActionButtons={renderActionButtons}
osSettings={host?.mdm.os_settings}
deviceUser deviceUser
/> />
<TabsWrapper> <TabsWrapper>
@ -489,6 +490,7 @@ const DeviceUserPage = ({
)} )}
{showMacSettingsModal && ( {showMacSettingsModal && (
<MacSettingsModal <MacSettingsModal
platform={host?.platform}
hostMDMData={host?.mdm} hostMDMData={host?.mdm}
onClose={toggleMacSettingsModal} onClose={toggleMacSettingsModal}
/> />

View File

@ -65,6 +65,7 @@ import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
import MacSettingsModal from "../MacSettingsModal"; import MacSettingsModal from "../MacSettingsModal";
import BootstrapPackageModal from "./modals/BootstrapPackageModal"; import BootstrapPackageModal from "./modals/BootstrapPackageModal";
import SelectQueryModal from "./modals/SelectQueryModal"; import SelectQueryModal from "./modals/SelectQueryModal";
import { isSupportedPlatform } from "./modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal";
const baseClass = "host-details"; const baseClass = "host-details";
@ -720,6 +721,7 @@ const HostDetailsPage = ({
showRefetchSpinner={showRefetchSpinner} showRefetchSpinner={showRefetchSpinner}
onRefetchHost={onRefetchHost} onRefetchHost={onRefetchHost}
renderActionButtons={renderActionButtons} renderActionButtons={renderActionButtons}
osSettings={host?.mdm.os_settings}
/> />
<TabsWrapper> <TabsWrapper>
<Tabs <Tabs
@ -845,6 +847,7 @@ const HostDetailsPage = ({
)} )}
{showMacSettingsModal && ( {showMacSettingsModal && (
<MacSettingsModal <MacSettingsModal
platform={host?.platform}
hostMDMData={host?.mdm} hostMDMData={host?.mdm}
onClose={toggleMacSettingsModal} onClose={toggleMacSettingsModal}
/> />
@ -852,8 +855,11 @@ const HostDetailsPage = ({
{showUnenrollMdmModal && !!host && ( {showUnenrollMdmModal && !!host && (
<UnenrollMdmModal hostId={host.id} onClose={toggleUnenrollMdmModal} /> <UnenrollMdmModal hostId={host.id} onClose={toggleUnenrollMdmModal} />
)} )}
{showDiskEncryptionModal && host && ( {showDiskEncryptionModal &&
host &&
isSupportedPlatform(host.platform) && (
<DiskEncryptionKeyModal <DiskEncryptionKeyModal
platform={host.platform}
hostId={host.id} hostId={host.id}
onCancel={() => setShowDiskEncryptionModal(false)} onCancel={() => setShowDiskEncryptionModal(false)}
/> />

View File

@ -9,15 +9,32 @@ import CustomLink from "components/CustomLink";
import Button from "components/buttons/Button"; import Button from "components/buttons/Button";
import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent"; import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent";
import DataError from "components/DataError"; import DataError from "components/DataError";
import { SupportedPlatform } from "interfaces/platform";
const baseClass = "disk-encryption-key-modal"; 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 { interface IDiskEncryptionKeyModal {
platform: ModalSupportedPlatform;
hostId: number; hostId: number;
onCancel: () => void; onCancel: () => void;
} }
const DiskEncryptionKeyModal = ({ const DiskEncryptionKeyModal = ({
platform,
hostId, hostId,
onCancel, onCancel,
}: IDiskEncryptionKeyModal) => { }: IDiskEncryptionKeyModal) => {
@ -33,6 +50,18 @@ const DiskEncryptionKeyModal = ({
select: (data) => data.encryption_key.key, 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 ( return (
<Modal title="Disk encryption key" onExit={onCancel} className={baseClass}> <Modal title="Disk encryption key" onExit={onCancel} className={baseClass}>
{encryptionKeyError ? ( {encryptionKeyError ? (
@ -40,15 +69,12 @@ const DiskEncryptionKeyModal = ({
) : ( ) : (
<> <>
<InputFieldHiddenContent value={encrpytionKey ?? ""} /> <InputFieldHiddenContent value={encrpytionKey ?? ""} />
<p>{descriptionText}</p>
<p> <p>
The disk encryption key refers to the FileVault recovery key for {recoveryText}{" "}
macOS.
</p>
<p>
Use this key to log in to the host if you forgot the password.{" "}
<CustomLink <CustomLink
text="View recovery instructions" text="View recovery instructions"
url="https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#reset-a-macos-hosts-password-using-the-disk-encryption-key" url={recoveryUrl}
newTab newTab
/> />
</p> </p>

View File

@ -1 +0,0 @@
export { default } from "./MacSettingsIndicator";

View File

@ -7,20 +7,28 @@ import MacSettingsTable from "./MacSettingsTable";
import { generateTableData } from "./MacSettingsTable/MacSettingsTableConfig"; import { generateTableData } from "./MacSettingsTable/MacSettingsTableConfig";
interface IMacSettingsModalProps { interface IMacSettingsModalProps {
hostMDMData?: Pick<IHostMdmData, "profiles" | "macos_settings">; platform?: string;
hostMDMData?: IHostMdmData;
onClose: () => void; onClose: () => void;
} }
const baseClass = "mac-settings-modal"; const baseClass = "mac-settings-modal";
const MacSettingsModal = ({ hostMDMData, onClose }: IMacSettingsModalProps) => { const MacSettingsModal = ({
const memoizedTableData = useMemo(() => generateTableData(hostMDMData), [ platform,
hostMDMData, hostMDMData,
]); onClose,
}: IMacSettingsModalProps) => {
const memoizedTableData = useMemo(
() => generateTableData(hostMDMData, platform),
[hostMDMData, platform]
);
if (!platform) return null;
return ( return (
<Modal <Modal
title="macOS settings" title="OS settings"
onExit={onClose} onExit={onClose}
className={baseClass} className={baseClass}
width="large" width="large"

View File

@ -10,7 +10,10 @@ import {
MacMdmProfileOperationType, MacMdmProfileOperationType,
} from "interfaces/mdm"; } from "interfaces/mdm";
import { MacSettingsTableStatusValue } from "../MacSettingsTableConfig"; import {
isMdmProfileStatus,
MacSettingsTableStatusValue,
} from "../MacSettingsTableConfig";
import TooltipContent, { import TooltipContent, {
TooltipInnerContentFunc, TooltipInnerContentFunc,
TooltipInnerContentOption, TooltipInnerContentOption,
@ -41,8 +44,8 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
iconName: "pending-partial", iconName: "pending-partial",
tooltip: (innerProps) => tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile innerProps.isDiskEncryptionProfile
? "The host will receive the MDM command to install the disk encryption profile when the " + ? "The hosts will receive the MDM command to turn on disk encryption " +
"host comes online." "when the hosts come online."
: "The host will receive the MDM command to install the configuration profile when the " + : "The host will receive the MDM command to install the configuration profile when the " +
"host comes online.", "host comes online.",
}, },
@ -56,8 +59,8 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
iconName: "success", iconName: "success",
tooltip: (innerProps) => tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile innerProps.isDiskEncryptionProfile
? "The host turned disk encryption on and " + ? "The host turned disk encryption on and sent the key to Fleet. " +
"sent their key to Fleet. Fleet verified with osquery." "Fleet verified with osquery."
: "The host installed the configuration profile. Fleet verified with osquery.", : "The host installed the configuration profile. Fleet verified with osquery.",
}, },
verifying: { verifying: {
@ -65,8 +68,9 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
iconName: "success-partial", iconName: "success-partial",
tooltip: (innerProps) => tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile innerProps.isDiskEncryptionProfile
? "The host acknowledged the MDM command to install disk encryption profile. Fleet is " + ? "The host acknowledged the MDM command to turn on disk encryption. " +
"verifying with osquery and retrieving the disk encryption key. This may take up to one hour." "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 " + : "The host acknowledged the MDM command to install the configuration profile. Fleet is " +
"verifying with osquery.", "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 { interface IMacSettingStatusCellProps {
status: MacSettingsTableStatusValue; status: MacSettingsTableStatusValue;
operationType: MacMdmProfileOperationType; operationType: MacMdmProfileOperationType | null;
profileName: string; profileName: string;
} }
@ -108,8 +144,18 @@ const MacSettingStatusCell = ({
status, status,
operationType, operationType,
profileName = "", profileName = "",
}: IMacSettingStatusCellProps): JSX.Element => { }: IMacSettingStatusCellProps) => {
const diplayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status]; 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 const isDeviceUser = window.location.pathname
.toLowerCase() .toLowerCase()
@ -118,8 +164,8 @@ const MacSettingStatusCell = ({
const isDiskEncryptionProfile = const isDiskEncryptionProfile =
profileName === FLEET_FILEVAULT_PROFILE_DISPLAY_NAME; profileName === FLEET_FILEVAULT_PROFILE_DISPLAY_NAME;
if (diplayOption) { if (displayOption) {
const { statusText, iconName, tooltip } = diplayOption; const { statusText, iconName, tooltip } = displayOption;
const tooltipId = uniqueId(); const tooltipId = uniqueId();
return ( return (
<span className={baseClass}> <span className={baseClass}>

View File

@ -5,20 +5,27 @@ import { IHostMdmData } from "interfaces/host";
import { import {
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME, FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
// FLEET_FILEVAULT_PROFILE_IDENTIFIER, // FLEET_FILEVAULT_PROFILE_IDENTIFIER,
IHostMacMdmProfile, IHostMdmProfile,
MdmProfileStatus, MdmProfileStatus,
isWindowsDiskEncryptionStatus,
} from "interfaces/mdm"; } from "interfaces/mdm";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell"; import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell";
import MacSettingStatusCell from "./MacSettingStatusCell"; import MacSettingStatusCell from "./MacSettingStatusCell";
import { generateWinDiskEncryptionProfile } from "../../helpers";
export interface IMacSettingsTableRow export interface IMacSettingsTableRow extends Omit<IHostMdmProfile, "status"> {
extends Omit<IHostMacMdmProfile, "status"> {
status: MacSettingsTableStatusValue; status: MacSettingsTableStatusValue;
} }
export type MacSettingsTableStatusValue = MdmProfileStatus | "action_required"; export type MacSettingsTableStatusValue = MdmProfileStatus | "action_required";
export const isMdmProfileStatus = (
status: string
): status is MdmProfileStatus => {
return status !== "action_required";
};
interface IHeaderProps { interface IHeaderProps {
column: { column: {
title: string; title: string;
@ -92,20 +99,41 @@ const tableHeaders: IDataColumn[] = [
]; ];
export const generateTableData = ( export const generateTableData = (
hostMDMData?: Pick<IHostMdmData, "profiles" | "macos_settings"> hostMDMData?: IHostMdmData,
platform?: string
) => { ) => {
if (!platform) return [];
let rows: IMacSettingsTableRow[] = []; let rows: IMacSettingsTableRow[] = [];
if (!hostMDMData) { if (!hostMDMData) {
return rows; 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; const { profiles, macos_settings } = hostMDMData;
if (!profiles) { if (!profiles) {
return rows; return rows;
} }
rows = profiles;
if (macos_settings?.disk_encryption === "action_required") { if (
platform === "darwin" &&
macos_settings?.disk_encryption === "action_required"
) {
rows = profiles.map((p) => { rows = profiles.map((p) => {
// TODO: this is a brittle check for the filevault profile // TODO: this is a brittle check for the filevault profile
// it would be better to match on the identifier but it is not // it would be better to match on the identifier but it is not

View File

@ -1,12 +1,15 @@
import React from "react"; import React from "react";
import { fireEvent, render, screen } from "@testing-library/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", () => { it("Renders the text and icon", () => {
const indicatorText = "test text"; const indicatorText = "test text";
render( render(
<MacSettingsIndicator indicatorText={indicatorText} iconName="success" /> <ProfileStatusIndicator
indicatorText={indicatorText}
iconName="success"
/>
); );
const renderedIndicatorText = screen.getByText(indicatorText); const renderedIndicatorText = screen.getByText(indicatorText);
const renderedIcon = screen.getByTestId("success-icon"); const renderedIcon = screen.getByTestId("success-icon");
@ -19,7 +22,7 @@ describe("MacSettingsIndicator", () => {
const indicatorText = "test text"; const indicatorText = "test text";
const tooltipText = "test tooltip text"; const tooltipText = "test tooltip text";
render( render(
<MacSettingsIndicator <ProfileStatusIndicator
indicatorText={indicatorText} indicatorText={indicatorText}
iconName="success" iconName="success"
tooltip={{ tooltipText }} tooltip={{ tooltipText }}
@ -42,7 +45,7 @@ describe("MacSettingsIndicator", () => {
document.body.appendChild(newDiv); document.body.appendChild(newDiv);
}; };
render( render(
<MacSettingsIndicator <ProfileStatusIndicator
indicatorText={indicatorText} indicatorText={indicatorText}
iconName="success" iconName="success"
onClick={() => { onClick={() => {

View File

@ -4,9 +4,9 @@ import { IconNames } from "components/icons";
import Icon from "components/Icon"; import Icon from "components/Icon";
import Button from "components/buttons/Button"; import Button from "components/buttons/Button";
const baseClass = "settings-indicator"; const baseClass = "profile-status-indicator";
export interface IMacSettingsIndicator { export interface IProfileStatusIndicatorProps {
indicatorText: string; indicatorText: string;
iconName: IconNames; iconName: IconNames;
onClick?: () => void; onClick?: () => void;
@ -16,12 +16,12 @@ export interface IMacSettingsIndicator {
}; };
} }
const MacSettingsIndicator = ({ const ProfileStatusIndicator = ({
indicatorText, indicatorText,
iconName, iconName,
onClick, onClick,
tooltip, tooltip,
}: IMacSettingsIndicator): JSX.Element => { }: IProfileStatusIndicatorProps) => {
const getIndicatorTextWrapped = () => { const getIndicatorTextWrapped = () => {
if (onClick && tooltip?.tooltipText) { if (onClick && tooltip?.tooltipText) {
return ( return (
@ -103,4 +103,4 @@ const MacSettingsIndicator = ({
); );
}; };
export default MacSettingsIndicator; export default ProfileStatusIndicator;

View File

@ -1,4 +1,4 @@
.settings-indicator { .profile-status-indicator {
display: flex; display: flex;
gap: 4px; gap: 4px;

View File

@ -0,0 +1 @@
export { default } from "./ProfileStatusIndicator";

View File

@ -1,7 +1,12 @@
import React from "react"; import React from "react";
import ReactTooltip from "react-tooltip"; 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 getHostStatusTooltipText from "pages/hosts/helpers";
import TooltipWrapper from "components/TooltipWrapper"; import TooltipWrapper from "components/TooltipWrapper";
@ -9,6 +14,7 @@ import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon"; import Icon from "components/Icon/Icon";
import DiskSpaceGraph from "components/DiskSpaceGraph"; import DiskSpaceGraph from "components/DiskSpaceGraph";
import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import { import {
getHostDiskEncryptionTooltipMessage, getHostDiskEncryptionTooltipMessage,
humanHostMemory, humanHostMemory,
@ -16,10 +22,11 @@ import {
} from "utilities/helpers"; } from "utilities/helpers";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import StatusIndicator from "components/StatusIndicator"; import StatusIndicator from "components/StatusIndicator";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import MacSettingsIndicator from "./MacSettingsIndicator"; import MacSettingsIndicator from "./MacSettingsIndicator";
import HostSummaryIndicator from "./HostSummaryIndicator"; import HostSummaryIndicator from "./HostSummaryIndicator";
import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPackageIndicator"; import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPackageIndicator";
import { generateWinDiskEncryptionProfile } from "../../helpers";
const baseClass = "host-summary"; const baseClass = "host-summary";
@ -38,7 +45,7 @@ interface IHostSummaryProps {
toggleOSPolicyModal?: () => void; toggleOSPolicyModal?: () => void;
toggleMacSettingsModal?: () => void; toggleMacSettingsModal?: () => void;
toggleBootstrapPackageModal?: () => void; toggleBootstrapPackageModal?: () => void;
hostMdmProfiles?: IHostMacMdmProfile[]; hostMdmProfiles?: IHostMdmProfile[];
mdmName?: string; mdmName?: string;
showRefetchSpinner: boolean; showRefetchSpinner: boolean;
onRefetchHost: ( onRefetchHost: (
@ -46,6 +53,7 @@ interface IHostSummaryProps {
) => void; ) => void;
renderActionButtons: () => JSX.Element | null; renderActionButtons: () => JSX.Element | null;
deviceUser?: boolean; deviceUser?: boolean;
osSettings?: IOSSettings;
} }
const HostSummary = ({ const HostSummary = ({
@ -64,8 +72,9 @@ const HostSummary = ({
onRefetchHost, onRefetchHost,
renderActionButtons, renderActionButtons,
deviceUser, deviceUser,
osSettings,
}: IHostSummaryProps): JSX.Element => { }: IHostSummaryProps): JSX.Element => {
const { status, id, platform } = titleData; const { status, platform } = titleData;
const renderRefetch = () => { const renderRefetch = () => {
const isOnline = titleData.status === "online"; const isOnline = titleData.status === "online";
@ -179,6 +188,22 @@ const HostSummary = ({
}; };
const renderSummary = () => { 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 ( return (
<div className="info-flex"> <div className="info-flex">
<div className="info-flex__item info-flex__item--title"> <div className="info-flex__item info-flex__item--title">
@ -198,12 +223,15 @@ const HostSummary = ({
{isPremiumTier && renderHostTeam()} {isPremiumTier && renderHostTeam()}
{platform === "darwin" && {/* Rendering of OS Settings data */}
{(platform === "darwin" || platform === "windows") &&
isPremiumTier && 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 &&
hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced
<HostSummaryIndicator title="macOS settings"> <HostSummaryIndicator title="OS settings">
<MacSettingsIndicator <MacSettingsIndicator
profiles={hostMdmProfiles} profiles={hostMdmProfiles}
onClick={toggleMacSettingsModal} onClick={toggleMacSettingsModal}

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import ReactTooltip from "react-tooltip"; import ReactTooltip from "react-tooltip";
import { IHostMacMdmProfile } from "interfaces/mdm"; import { IHostMdmProfile } from "interfaces/mdm";
import Icon from "components/Icon"; import Icon from "components/Icon";
import Button from "components/buttons/Button"; import Button from "components/buttons/Button";
@ -24,23 +24,23 @@ const STATUS_DISPLAY_OPTIONS: StatusDisplayOptions = {
Verified: { Verified: {
iconName: "success", iconName: "success",
tooltipText: tooltipText:
"The host installed all configuration profiles. Fleet verified with osquery.", "The host applied all OS settings. Fleet verified with osquery.",
}, },
Verifying: { Verifying: {
iconName: "success-partial", iconName: "success-partial",
tooltipText: tooltipText:
"The hosts acknowledged all MDM commands to install configuration profiles. Fleet is verifying " + "The host acknowledged all MDM commands to apply OS settings. " +
"the profiles are installed with osquery.", "Fleet is verifying the OS settings are applied with osquery.",
}, },
Pending: { Pending: {
iconName: "pending-partial", iconName: "pending-partial",
tooltipText: tooltipText:
"The host will receive MDM commands to install configuration profiles when the host comes online.", "The host will receive MDM command to apply OS settings when the host comes online.",
}, },
Failed: { Failed: {
iconName: "error", iconName: "error",
tooltipText: tooltipText:
"Host failed to install configuration profiles. Click to view error(s).", "The host failed to apply the latest OS settings. Click to view error(s).",
}, },
}; };
@ -52,7 +52,7 @@ const STATUS_DISPLAY_OPTIONS: StatusDisplayOptions = {
* Finally if all profiles have a status of "verified", the status will be displayed as "Verified". * Finally if all profiles have a status of "verified", the status will be displayed as "Verified".
*/ */
const getMacProfileStatus = ( const getMacProfileStatus = (
hostMacSettings: IHostMacMdmProfile[] hostMacSettings: IHostMdmProfile[]
): MacProfileStatus => { ): MacProfileStatus => {
const statuses = hostMacSettings.map((setting) => setting.status); const statuses = hostMacSettings.map((setting) => setting.status);
if (statuses.includes("failed")) { if (statuses.includes("failed")) {
@ -68,7 +68,7 @@ const getMacProfileStatus = (
}; };
interface IMacSettingsIndicatorProps { interface IMacSettingsIndicatorProps {
profiles: IHostMacMdmProfile[]; profiles: IHostMdmProfile[];
onClick?: () => void; onClick?: () => void;
} }
const MacSettingsIndicator = ({ const MacSettingsIndicator = ({

View File

@ -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,
};
};

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services"; import sendRequest from "services";
import endpoints from "utilities/endpoints"; import endpoints from "utilities/endpoints";
import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm"; import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm";
import { HostStatus } from "interfaces/host"; import { HostStatus } from "interfaces/host";
import { import {
buildQueryStringFromParams, buildQueryStringFromParams,
@ -43,7 +43,7 @@ export interface IHostCountLoadOptions {
osId?: number; osId?: number;
osName?: string; osName?: string;
osVersion?: string; osVersion?: string;
diskEncryptionStatus?: FileVaultProfileStatus; diskEncryptionStatus?: DiskEncryptionStatus;
bootstrapPackageStatus?: BootstrapPackageStatus; bootstrapPackageStatus?: BootstrapPackageStatus;
} }

View File

@ -11,7 +11,7 @@ import {
import { SelectedPlatform } from "interfaces/platform"; import { SelectedPlatform } from "interfaces/platform";
import { ISoftware } from "interfaces/software"; import { ISoftware } from "interfaces/software";
import { import {
FileVaultProfileStatus, DiskEncryptionStatus,
BootstrapPackageStatus, BootstrapPackageStatus,
IMdmSolution, IMdmSolution,
} from "interfaces/mdm"; } from "interfaces/mdm";
@ -29,6 +29,11 @@ export interface ILoadHostsResponse {
mobile_device_management_solution: IMdmSolution; 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 { export interface ILoadHostsQueryKey extends ILoadHostsOptions {
scope: "hosts"; scope: "hosts";
} }
@ -57,7 +62,7 @@ export interface ILoadHostsOptions {
device_mapping?: boolean; device_mapping?: boolean;
columns?: string; columns?: string;
visibleColumns?: string; visibleColumns?: string;
diskEncryptionStatus?: FileVaultProfileStatus; diskEncryptionStatus?: DiskEncryptionStatus;
bootstrapPackageStatus?: BootstrapPackageStatus; bootstrapPackageStatus?: BootstrapPackageStatus;
} }
@ -83,7 +88,7 @@ export interface IExportHostsOptions {
device_mapping?: boolean; device_mapping?: boolean;
columns?: string; columns?: string;
visibleColumns?: string; visibleColumns?: string;
diskEncryptionStatus?: FileVaultProfileStatus; diskEncryptionStatus?: DiskEncryptionStatus;
} }
export interface IActionByFilter { export interface IActionByFilter {

View File

@ -1,19 +1,61 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* 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 { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
import sendRequest from "services"; import sendRequest from "services";
import endpoints from "utilities/endpoints"; import endpoints from "utilities/endpoints";
import { buildQueryStringFromParams } from "utilities/url"; import { buildQueryStringFromParams } from "utilities/url";
export type IFileVaultSummaryResponse = Record<FileVaultProfileStatus, number>;
export interface IEulaMetadataResponse { export interface IEulaMetadataResponse {
name: string; name: string;
token: string; token: string;
created_at: string; created_at: string;
} }
export default { export type ProfileStatusSummaryResponse = Record<MdmProfileStatus, number>;
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) => { downloadDeviceUserEnrollmentProfile: (token: string) => {
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints; const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
return sendRequest("GET", DEVICE_USER_MDM_ENROLLMENT_PROFILE(token)); return sendRequest("GET", DEVICE_USER_MDM_ENROLLMENT_PROFILE(token));
@ -72,24 +114,51 @@ export default {
return sendRequest("DELETE", MDM_PROFILE(profileId)); 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 = `${ const path = `${
endpoints.MDM_PROFILES_AGGREGATE_STATUSES endpoints.MDM_PROFILES_AGGREGATE_STATUSES
}?${buildQueryStringFromParams({ team_id: teamId })}`; }?${buildQueryStringFromParams({ team_id: teamId })}`;
return sendRequest("GET", path); return sendRequest("GET", path);
}, },
getDiskEncryptionAggregate: (teamId?: number) => { getDiskEncryptionSummary: (teamId?: number) => {
let { MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: path } = endpoints; let { MDM_DISK_ENCRYPTION_SUMMARY: path } = endpoints;
if (teamId) { if (teamId) {
path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`; path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
} }
return sendRequest("GET", path); return sendRequest("GET", path);
}, },
// TODO: API INTEGRATION: change when API is implemented that works for windows
// disk encryption too.
updateAppleMdmSettings: (enableDiskEncryption: boolean, teamId?: number) => { updateAppleMdmSettings: (enableDiskEncryption: boolean, teamId?: number) => {
const { const {
MDM_UPDATE_APPLE_SETTINGS: teamsEndpoint, MDM_UPDATE_APPLE_SETTINGS: teamsEndpoint,
@ -98,7 +167,9 @@ export default {
if (teamId === 0) { if (teamId === 0) {
return sendRequest("PATCH", noTeamsEndpoint, { return sendRequest("PATCH", noTeamsEndpoint, {
mdm: { mdm: {
// TODO: API INTEGRATION: remove macos_settings when API change is merged in.
macos_settings: { enable_disk_encryption: enableDiskEncryption }, macos_settings: { enable_disk_encryption: enableDiskEncryption },
// enable_disk_encryption: enableDiskEncryption,
}, },
}); });
} }
@ -179,3 +250,5 @@ export default {
}); });
}, },
}; };
export default mdmService;

View File

@ -51,7 +51,7 @@ export default {
MDM_PROFILE: (id: number) => `/${API_VERSION}/fleet/mdm/apple/profiles/${id}`, MDM_PROFILE: (id: number) => `/${API_VERSION}/fleet/mdm/apple/profiles/${id}`,
MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`, MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`,
MDM_PROFILES_AGGREGATE_STATUSES: `/${API_VERSION}/fleet/mdm/apple/profiles/summary`, 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_SSO: `/${API_VERSION}/fleet/mdm/sso`,
MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => { MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => {
const query = new URLSearchParams({ token }); const query = new URLSearchParams({ token });

View File

@ -1,6 +1,10 @@
import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm";
import { isEmpty, reduce, omitBy, Dictionary } from "lodash"; 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; type QueryValues = string | number | boolean | undefined | null;
export type QueryParams = Record<string, QueryValues>; export type QueryParams = Record<string, QueryValues>;
@ -24,7 +28,7 @@ interface IMutuallyExclusiveHostParams {
osId?: number; osId?: number;
osName?: string; osName?: string;
osVersion?: string; osVersion?: string;
diskEncryptionStatus?: FileVaultProfileStatus; diskEncryptionStatus?: DiskEncryptionStatus;
bootstrapPackageStatus?: BootstrapPackageStatus; bootstrapPackageStatus?: BootstrapPackageStatus;
} }
@ -123,7 +127,7 @@ export const reconcileMutuallyExclusiveHostParams = ({
case !!lowDiskSpaceHosts: case !!lowDiskSpaceHosts:
return { low_disk_space: lowDiskSpaceHosts }; return { low_disk_space: lowDiskSpaceHosts };
case !!diskEncryptionStatus: case !!diskEncryptionStatus:
return { macos_settings_disk_encryption: diskEncryptionStatus }; return { [DISK_ENCRYPTION_QUERY_PARAM_NAME]: diskEncryptionStatus };
case !!bootstrapPackageStatus: case !!bootstrapPackageStatus:
return { bootstrap_package: bootstrapPackageStatus }; return { bootstrap_package: bootstrapPackageStatus };
default: default:

1
go.mod
View File

@ -272,6 +272,7 @@ require (
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // 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/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/slack-go/slack v0.9.4 // indirect github.com/slack-go/slack v0.9.4 // indirect

2
go.sum
View File

@ -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/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/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-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/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 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=

View File

@ -0,0 +1 @@
* Adding support to manage Bitlocker operations through Orbit notifications

View File

@ -622,6 +622,7 @@ func main() {
const ( const (
renewEnrollmentProfileCommandFrequency = time.Hour renewEnrollmentProfileCommandFrequency = time.Hour
windowsMDMEnrollmentCommandFrequency = time.Hour windowsMDMEnrollmentCommandFrequency = time.Hour
windowsMDMBitlockerCommandFrequency = time.Hour
) )
configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL) configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL)
configFetcher = update.ApplyRunScriptsConfigFetcherMiddleware(configFetcher, c.Bool("enable-scripts"), orbitClient) configFetcher = update.ApplyRunScriptsConfigFetcherMiddleware(configFetcher, c.Bool("enable-scripts"), orbitClient)
@ -638,6 +639,7 @@ func main() {
configFetcher = update.ApplySwiftDialogDownloaderMiddleware(configFetcher, updateRunner) configFetcher = update.ApplySwiftDialogDownloaderMiddleware(configFetcher, updateRunner)
case "windows": case "windows":
configFetcher = update.ApplyWindowsMDMEnrollmentFetcherMiddleware(configFetcher, windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient) configFetcher = update.ApplyWindowsMDMEnrollmentFetcherMiddleware(configFetcher, windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient)
configFetcher = update.ApplyWindowsMDMBitlockerFetcherMiddleware(configFetcher, windowsMDMBitlockerCommandFrequency, orbitClient)
} }
const orbitFlagsUpdateInterval = 30 * time.Second const orbitFlagsUpdateInterval = 30 * time.Second

View File

@ -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
}

View File

@ -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
}

View File

@ -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 = "<none>"
// VolumeTypeDefault indicates the default behavior.
VolumeTypeDefault DiscoveryVolumeType = "<default>"
// 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
}

View File

@ -9,3 +9,7 @@ func RunWindowsMDMEnrollment(args WindowsMDMEnrollmentArgs) error {
func RunWindowsMDMUnenrollment(args WindowsMDMEnrollmentArgs) error { func RunWindowsMDMUnenrollment(args WindowsMDMEnrollmentArgs) error {
return nil return nil
} }
func IsRunningOnWindowsServer() (bool, error) {
return false, nil
}

View File

@ -174,3 +174,17 @@ func generateWindowsMDMAccessTokenPayload(args WindowsMDMEnrollmentArgs) ([]byte
pld.Payload.OrbitNodeKey = args.OrbitNodeKey pld.Payload.OrbitNodeKey = args.OrbitNodeKey
return json.Marshal(pld) 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
}

View File

@ -7,6 +7,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/fleetdm/fleet/v4/orbit/pkg/bitlocker"
"github.com/fleetdm/fleet/v4/orbit/pkg/profiles" "github.com/fleetdm/fleet/v4/orbit/pkg/profiles"
"github.com/fleetdm/fleet/v4/orbit/pkg/scripts" "github.com/fleetdm/fleet/v4/orbit/pkg/scripts"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
@ -397,3 +398,119 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
} }
return cfg, err 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()
}

View File

@ -573,3 +573,67 @@ func TestRunScripts(t *testing.T) {
require.Contains(t, logBuf.String(), "running scripts [c] succeeded") 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
})
}

View File

@ -53,3 +53,42 @@ func (s *String) UnmarshalJSON(data []byte) error {
s.Valid = true s.Valid = true
return nil 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
}

View File

@ -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))
})
}
})
}

55
pkg/rawjson/rawjson.go Normal file
View File

@ -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
}

104
pkg/rawjson/rawjson_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -213,3 +213,18 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet
} }
return secrets, nil 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
}

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
@ -30,6 +31,7 @@ func TestAppConfig(t *testing.T) {
{"AggregateEnrollSecretPerTeam", testAggregateEnrollSecretPerTeam}, {"AggregateEnrollSecretPerTeam", testAggregateEnrollSecretPerTeam},
{"Defaults", testAppConfigDefaults}, {"Defaults", testAppConfigDefaults},
{"Backwards Compatibility", testAppConfigBackwardsCompatibility}, {"Backwards Compatibility", testAppConfigBackwardsCompatibility},
{"GetConfigEnableDiskEncryption", testGetConfigEnableDiskEncryption},
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { 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) secrets, err = ds.GetEnrollSecrets(context.Background(), nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, secrets, 2) require.Len(t, secrets, 2)
} }
func testAppConfigEnrollSecretUniqueness(t *testing.T, ds *Datastore) { 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"}, {TeamID: ptr.Uint(3), Secret: "team_3_secret_1"},
}, agg) }, 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)
}

View File

@ -2082,7 +2082,7 @@ func (ds *Datastore) GetMDMIdPAccount(ctx context.Context, uuid string) (*fleet.
return &acct, nil return &acct, nil
} }
func subqueryDiskEncryptionVerifying() (string, []interface{}) { func subqueryFileVaultVerifying() (string, []interface{}) {
sql := ` sql := `
SELECT SELECT
1 FROM host_mdm_apple_profiles hmap 1 FROM host_mdm_apple_profiles hmap
@ -2100,7 +2100,7 @@ func subqueryDiskEncryptionVerifying() (string, []interface{}) {
return sql, args return sql, args
} }
func subqueryDiskEncryptionVerified() (string, []interface{}) { func subqueryFileVaultVerified() (string, []interface{}) {
sql := ` sql := `
SELECT SELECT
1 FROM host_mdm_apple_profiles hmap 1 FROM host_mdm_apple_profiles hmap
@ -2118,7 +2118,7 @@ func subqueryDiskEncryptionVerified() (string, []interface{}) {
return sql, args return sql, args
} }
func subqueryDiskEncryptionActionRequired() (string, []interface{}) { func subqueryFileVaultActionRequired() (string, []interface{}) {
sql := ` sql := `
SELECT SELECT
1 FROM host_mdm_apple_profiles hmap 1 FROM host_mdm_apple_profiles hmap
@ -2138,7 +2138,7 @@ func subqueryDiskEncryptionActionRequired() (string, []interface{}) {
return sql, args return sql, args
} }
func subqueryDiskEncryptionEnforcing() (string, []interface{}) { func subqueryFileVaultEnforcing() (string, []interface{}) {
sql := ` sql := `
SELECT SELECT
1 FROM host_mdm_apple_profiles hmap 1 FROM host_mdm_apple_profiles hmap
@ -2168,7 +2168,7 @@ func subqueryDiskEncryptionEnforcing() (string, []interface{}) {
return sql, args return sql, args
} }
func subqueryDiskEncryptionFailed() (string, []interface{}) { func subqueryFileVaultFailed() (string, []interface{}) {
sql := ` sql := `
SELECT SELECT
1 FROM host_mdm_apple_profiles hmap 1 FROM host_mdm_apple_profiles hmap
@ -2180,7 +2180,7 @@ func subqueryDiskEncryptionFailed() (string, []interface{}) {
return sql, args return sql, args
} }
func subqueryDiskEncryptionRemovingEnforcement() (string, []interface{}) { func subqueryFileVaultRemovingEnforcement() (string, []interface{}) {
sql := ` sql := `
SELECT SELECT
1 FROM host_mdm_apple_profiles hmap 1 FROM host_mdm_apple_profiles hmap
@ -2224,20 +2224,20 @@ FROM
hosts h hosts h
LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id
WHERE WHERE
%s` h.platform = 'darwin' AND %s`
var args []interface{} var args []interface{}
subqueryVerified, subqueryVerifiedArgs := subqueryDiskEncryptionVerified() subqueryVerified, subqueryVerifiedArgs := subqueryFileVaultVerified()
args = append(args, subqueryVerifiedArgs...) args = append(args, subqueryVerifiedArgs...)
subqueryVerifying, subqueryVerifyingArgs := subqueryDiskEncryptionVerifying() subqueryVerifying, subqueryVerifyingArgs := subqueryFileVaultVerifying()
args = append(args, subqueryVerifyingArgs...) args = append(args, subqueryVerifyingArgs...)
subqueryActionRequired, subqueryActionRequiredArgs := subqueryDiskEncryptionActionRequired() subqueryActionRequired, subqueryActionRequiredArgs := subqueryFileVaultActionRequired()
args = append(args, subqueryActionRequiredArgs...) args = append(args, subqueryActionRequiredArgs...)
subqueryEnforcing, subqueryEnforcingArgs := subqueryDiskEncryptionEnforcing() subqueryEnforcing, subqueryEnforcingArgs := subqueryFileVaultEnforcing()
args = append(args, subqueryEnforcingArgs...) args = append(args, subqueryEnforcingArgs...)
subqueryFailed, subqueryFailedArgs := subqueryDiskEncryptionFailed() subqueryFailed, subqueryFailedArgs := subqueryFileVaultFailed()
args = append(args, subqueryFailedArgs...) args = append(args, subqueryFailedArgs...)
subqueryRemovingEnforcement, subqueryRemovingEnforcementArgs := subqueryDiskEncryptionRemovingEnforcement() subqueryRemovingEnforcement, subqueryRemovingEnforcementArgs := subqueryFileVaultRemovingEnforcement()
args = append(args, subqueryRemovingEnforcementArgs...) args = append(args, subqueryRemovingEnforcementArgs...)
teamFilter := "h.team_id IS NULL" teamFilter := "h.team_id IS NULL"

View File

@ -782,7 +782,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
var hostID uint var hostID uint
err = sqlx.GetContext(context.Background(), ds.reader(context.Background()), &hostID, `SELECT id FROM hosts WHERE uuid = ?`, testUUID) err = sqlx.GetContext(context.Background(), ds.reader(context.Background()), &hostID, `SELECT id FROM hosts WHERE uuid = ?`, testUUID)
require.NoError(t, err) require.NoError(t, err)
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf") err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf", "", nil)
require.NoError(t, err) require.NoError(t, err)
key, err := ds.GetHostDiskEncryptionKey(ctx, hostID) key, err := ds.GetHostDiskEncryptionKey(ctx, hostID)
@ -1474,7 +1474,7 @@ func upsertHostCPs(
func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) { func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) {
ctx := context.Background() 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{} expectedIDs := []uint{}
for _, h := range expected { for _, h := range expected {
expectedIDs = append(expectedIDs, h.ID) 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.Verifying)
require.Equal(t, uint(0), res.Verified) 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) require.NoError(t, err)
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, nil) res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, nil)
require.NoError(t, err) 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(0), res.Verifying)
require.Equal(t, uint(1), res.Verified) // hosts[0] now has filevault fully enforced and verified 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) require.NoError(t, err)
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, false, time.Now().Add(1*time.Hour)) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, false, time.Now().Add(1*time.Hour))
require.NoError(t, err) require.NoError(t, err)
@ -1619,10 +1619,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
require.Equal(t, uint(1), res.Verified) require.Equal(t, uint(1), res.Verified)
// check that list hosts by status matches summary // check that list hosts by status matches summary
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:])) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, hosts[1:2])) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[1:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, hosts[0:1])) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, hosts[0:1]))
// create a team // create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"}) 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.Verifying)
require.Equal(t, uint(0), res.Verified) 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) require.NoError(t, err)
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour)) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour))
require.NoError(t, err) require.NoError(t, err)
@ -1675,10 +1675,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
// check that list hosts by status matches summary // check that list hosts by status matches summary
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{}))
upsertHostCPs(hosts[9:10], append(teamCPs, fvTeam), fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t) upsertHostCPs(hosts[9:10], append(teamCPs, fvTeam), fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t)
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &team.ID) 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) require.Equal(t, uint(0), res.Verified)
// check that list hosts by status matches summary // check that list hosts by status matches summary
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{}))
// set decryptable back to true for hosts[9] // set decryptable back to true for hosts[9]
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour)) 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 require.Equal(t, uint(1), res.Verified) // hosts[9] goes back to verified
// check that list hosts by status matches summary // check that list hosts by status matches summary
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, hosts[9:10]))
} }
func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) { func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
ctx := context.Background() 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{} expectedIDs := []uint{}
for _, h := range expected { for _, h := range expected {
expectedIDs = append(expectedIDs, h.ID) 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}) gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID})
gotIDs := []uint{} gotIDs := []uint{}
for _, h := range gotHosts { 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) 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 var hosts []*fleet.Host
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1", 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.Failed)
require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verifying)
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
// all hosts pending install of all profiles // all hosts pending install of all profiles
upsertHostCPs(hosts, noTeamCPs, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryPending, ctx, ds, t) 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.Failed)
require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verifying)
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
// hosts[0] and hosts[1] failed one profile // hosts[0] and hosts[1] failed one profile
upsertHostCPs(hosts[0:2], noTeamCPs[0:1], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryFailed, ctx, ds, t) 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(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.Verifying)
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:])) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts[2:])) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
// hosts[0:3] installed a third profile // hosts[0:3] installed a third profile
upsertHostCPs(hosts[0:3], noTeamCPs[2:3], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) 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(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.Verifying) // no change, host must apply all profiles count as latest
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:])) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts[2:])) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
// hosts[6] deletes all its profiles // hosts[6] deletes all its profiles
tx, err := ds.writer(ctx).BeginTxx(ctx, nil) 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(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.Verifying) // no change, host must apply all profiles count as latest
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, 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) // 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) 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(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.Verifying) // no change, host must apply all profiles count as latest
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
// hosts[9] installed all profiles // hosts[9] installed all profiles
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) 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(2), res.Failed) // no change
require.Equal(t, uint(1), res.Verifying) // add one host that has installed all profiles require.Equal(t, uint(1), res.Verifying) // add one host that has installed all profiles
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
// create a team // create a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "rocket"}) 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.Failed) // no profiles yet
require.Equal(t, uint(0), res.Verifying) // no profiles yet require.Equal(t, uint(0), res.Verifying) // no profiles yet
require.Equal(t, uint(0), res.Verified) // 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.OSSettingsPending, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
// transfer hosts[9] to new team // transfer hosts[9] to new team
err = ds.AddHostsToTeam(ctx, &tm.ID, []uint{hosts[9].ID}) 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(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(2), res.Failed) // no change
require.Equal(t, uint(0), res.Verifying) // hosts[9] was transferred so this is now zero 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.OSSettingsPending, nil, pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, 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 res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &tm.ID) // get summary for new team
require.NoError(t, err) 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.Failed)
require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verifying)
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
// create somes config profiles for the new team // create somes config profiles for the new team
var teamCPs []*fleet.MDMAppleConfigProfile 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.Failed)
require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verifying)
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
// hosts[9] successfully removed old profiles // hosts[9] successfully removed old profiles
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeRemove, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t) 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(0), res.Failed)
require.Equal(t, uint(1), res.Verifying) // hosts[9] is verifying all new profiles require.Equal(t, uint(1), res.Verifying) // hosts[9] is verifying all new profiles
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
// verify one profile on hosts[9] // verify one profile on hosts[9]
upsertHostCPs(hosts[9:10], teamCPs[0:1], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t) 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(0), res.Failed)
require.Equal(t, uint(1), res.Verifying) // hosts[9] is still verifying other profiles require.Equal(t, uint(1), res.Verifying) // hosts[9] is still verifying other profiles
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
// verify the other profiles on hosts[9] // verify the other profiles on hosts[9]
upsertHostCPs(hosts[9:10], teamCPs[1:], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t) 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.Failed)
require.Equal(t, uint(0), res.Verifying) require.Equal(t, uint(0), res.Verifying)
require.Equal(t, uint(1), res.Verified) // hosts[9] is all verified 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.OSSettingsPending, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, hosts[9:10])) require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, hosts[9:10]))
// confirm no changes in summary for profiles with no team // confirm no changes in summary for profiles with no team
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, ptr.Uint(0)) // team id zero represents 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(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.Verifying) // hosts[9] transferred to new team so is not counted under no team
require.Equal(t, uint(0), res.Verified) require.Equal(t, uint(0), res.Verified)
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts)) require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2])) require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{})) require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
} }
func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) { 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) { 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) require.NoError(t, err)
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hostId}, decryptable, threshold) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hostId}, decryptable, threshold)
require.NoError(t, err) require.NoError(t, err)

View File

@ -887,7 +887,11 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt
} }
leftJoinFailingPolicies := !useHostPaginationOptim 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{} hosts := []*fleet.Host{}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, sql, params...); err != nil { 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? // 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) opt.OrderKey = defaultHostColumnTableAlias(opt.OrderKey)
deviceMappingJoin := `LEFT JOIN ( 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 = filterHostsByMDM(sql, opt, params)
sql, params = filterHostsByMacOSSettingsStatus(sql, opt, params) sql, params = filterHostsByMacOSSettingsStatus(sql, opt, params)
sql, params = filterHostsByMacOSDiskEncryptionStatus(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 = filterHostsByMDMBootstrapPackageStatus(sql, opt, params)
sql, params = filterHostsByOS(sql, opt, params) sql, params = filterHostsByOS(sql, opt, params)
sql, params, _ = hostSearchLike(sql, params, opt.MatchQuery, hostSearchColumns...) sql, params, _ = hostSearchLike(sql, params, opt.MatchQuery, hostSearchColumns...)
sql, params = appendListOptionsWithCursorToSQL(sql, params, &opt.ListOptions) 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{}) { 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 subquery string
var subqueryParams []interface{} var subqueryParams []interface{}
switch opt.MacOSSettingsFilter { switch opt.MacOSSettingsFilter {
case fleet.MacOSSettingsFailed: case fleet.OSSettingsFailed:
subquery, subqueryParams = subqueryHostsMacOSSettingsStatusFailing() subquery, subqueryParams = subqueryHostsMacOSSettingsStatusFailing()
case fleet.MacOSSettingsPending: case fleet.OSSettingsPending:
subquery, subqueryParams = subqueryHostsMacOSSettingsStatusPending() subquery, subqueryParams = subqueryHostsMacOSSettingsStatusPending()
case fleet.MacOSSettingsVerifying: case fleet.OSSettingsVerifying:
subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying() subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying()
case fleet.MacOSSettingsVerified: case fleet.OSSettingsVerified:
subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified() subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified()
} }
if subquery != "" { if subquery != "" {
@ -1140,22 +1152,131 @@ func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOption
var subqueryParams []interface{} var subqueryParams []interface{}
switch opt.MacOSSettingsDiskEncryptionFilter { switch opt.MacOSSettingsDiskEncryptionFilter {
case fleet.DiskEncryptionVerified: case fleet.DiskEncryptionVerified:
subquery, subqueryParams = subqueryDiskEncryptionVerified() subquery, subqueryParams = subqueryFileVaultVerified()
case fleet.DiskEncryptionVerifying: case fleet.DiskEncryptionVerifying:
subquery, subqueryParams = subqueryDiskEncryptionVerifying() subquery, subqueryParams = subqueryFileVaultVerifying()
case fleet.DiskEncryptionActionRequired: case fleet.DiskEncryptionActionRequired:
subquery, subqueryParams = subqueryDiskEncryptionActionRequired() subquery, subqueryParams = subqueryFileVaultActionRequired()
case fleet.DiskEncryptionEnforcing: case fleet.DiskEncryptionEnforcing:
subquery, subqueryParams = subqueryDiskEncryptionEnforcing() subquery, subqueryParams = subqueryFileVaultEnforcing()
case fleet.DiskEncryptionFailed: case fleet.DiskEncryptionFailed:
subquery, subqueryParams = subqueryDiskEncryptionFailed() subquery, subqueryParams = subqueryFileVaultFailed()
case fleet.DiskEncryptionRemovingEnforcement: case fleet.DiskEncryptionRemovingEnforcement:
subquery, subqueryParams = subqueryDiskEncryptionRemovingEnforcement() subquery, subqueryParams = subqueryFileVaultRemovingEnforcement()
} }
return sql + fmt.Sprintf(` AND EXISTS (%s)`, subquery), append(params, subqueryParams...) 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{}) { func filterHostsByMDMBootstrapPackageStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
if opt.MDMBootstrapPackageFilter == nil || !opt.MDMBootstrapPackageFilter.IsValid() { if opt.MDMBootstrapPackageFilter == nil || !opt.MDMBootstrapPackageFilter.IsValid() {
return sql, params return sql, params
@ -1210,7 +1331,11 @@ func (ds *Datastore) CountHosts(ctx context.Context, filter fleet.TeamFilter, op
leftJoinFailingPolicies := false leftJoinFailingPolicies := false
var params []interface{} 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 var count int
if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, sql, params...); err != nil { if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, sql, params...); err != nil {
@ -1749,6 +1874,7 @@ type hostWithMDMInfo struct {
IsServer *bool `db:"is_server"` IsServer *bool `db:"is_server"`
MDMID *uint `db:"mdm_id"` MDMID *uint `db:"mdm_id"`
Name *string `db:"name"` Name *string `db:"name"`
EncryptionKeyAvailable *bool `db:"encryption_key_available"`
} }
// LoadHostByOrbitNodeKey loads the whole host identified by the node key. // 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(hm.is_server, false) AS is_server,
COALESCE(mdms.name, ?) AS name, COALESCE(mdms.name, ?) AS name,
COALESCE(hdek.reset_requested, false) AS disk_encryption_reset_requested, 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, 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 t.name as team_name
FROM FROM
hosts h hosts h
@ -1824,6 +1952,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
host_disk_encryption_keys hdek host_disk_encryption_keys hdek
ON ON
hdek.host_id = h.id hdek.host_id = h.id
LEFT OUTER JOIN
host_disks hd
ON
hd.host_id = h.id
LEFT OUTER JOIN LEFT OUTER JOIN
teams t teams t
ON ON
@ -1846,6 +1978,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
MDMID: hostWithMDM.MDMID, MDMID: hostWithMDM.MDMID,
Name: *hostWithMDM.Name, Name: *hostWithMDM.Name,
} }
host.MDM = fleet.MDMHostData{
EncryptionKeyAvailable: *hostWithMDM.EncryptionKeyAvailable,
}
} }
return &host, nil return &host, nil
case errors.Is(err, sql.ErrNoRows): 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, ` _, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_disk_encryption_keys (host_id, base64_encrypted) INSERT INTO host_disk_encryption_keys
VALUES (?, ?) (host_id, base64_encrypted, client_error, decryptable)
ON DUPLICATE KEY UPDATE VALUES
/* if the key has changed, NULLify this value so it can be calculated again */ (?, ?, ?, ?)
decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, NULL), ON DUPLICATE KEY UPDATE
base64_encrypted = VALUES(base64_encrypted) /* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */
`, hostID, encryptedBase64Key) 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 return err
} }
func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { 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 var keys []fleet.HostDiskEncryptionKey
err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, ` err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, `
SELECT SELECT
@ -3035,7 +3182,8 @@ func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fle
FROM FROM
host_disk_encryption_keys host_disk_encryption_keys
WHERE WHERE
decryptable IS NULL decryptable IS NULL AND
base64_encrypted != ''
`) `)
return keys, err return keys, err
} }

View File

@ -21,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet" "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/ptr"
"github.com/fleetdm/fleet/v4/server/test" "github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid" "github.com/google/uuid"
@ -111,7 +112,7 @@ func TestHosts(t *testing.T) {
{"HostsListBySoftwareChangedAt", testHostsListBySoftwareChangedAt}, {"HostsListBySoftwareChangedAt", testHostsListBySoftwareChangedAt},
{"HostsListByOperatingSystemID", testHostsListByOperatingSystemID}, {"HostsListByOperatingSystemID", testHostsListByOperatingSystemID},
{"HostsListByOSNameAndVersion", testHostsListByOSNameAndVersion}, {"HostsListByOSNameAndVersion", testHostsListByOSNameAndVersion},
{"HostsListByDiskEncryptionStatus", testHostsListDiskEncryptionStatus}, {"HostsListByDiskEncryptionStatus", testHostsListMacOSSettingsDiskEncryptionStatus},
{"HostsListFailingPolicies", printReadsInTest(testHostsListFailingPolicies)}, {"HostsListFailingPolicies", printReadsInTest(testHostsListFailingPolicies)},
{"HostsExpiration", testHostsExpiration}, {"HostsExpiration", testHostsExpiration},
{"HostsAllPackStats", testHostsAllPackStats}, {"HostsAllPackStats", testHostsAllPackStats},
@ -722,8 +723,13 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
var hosts []*fleet.Host var hosts []*fleet.Host
for i := 0; i < 10; i++ { 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", 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) hosts = append(hosts, h)
} }
userFilter := fleet.TeamFilter{User: test.UserAdmin} userFilter := fleet.TeamFilter{User: test.UserAdmin}
@ -763,12 +769,12 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
Checksum: []byte("csum"), 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: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
// macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero // 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: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 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.MacOSSettingsVerifying}, 0) // no team listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{ {
@ -781,12 +787,39 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
Checksum: []byte("csum"), 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: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
// macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero // 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: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 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.MacOSSettingsVerifying}, 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) { 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() ctx := context.Background()
// seed hosts // seed hosts
@ -5740,7 +5773,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0") err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0")
require.NoError(t, err) require.NoError(t, err)
// set an encryption key // 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) require.NoError(t, err)
// set an mdm profile // set an mdm profile
prof, err := ds.NewMDMAppleConfigProfile(context.Background(), *configProfileForTest(t, "N1", "I1", "U1")) 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.NoError(t, err)
require.Equal(t, hFleet.ID, loadFleet.ID) require.Equal(t, hFleet.ID, loadFleet.ID)
require.False(t, loadFleet.MDMInfo.IsServer) require.False(t, loadFleet.MDMInfo.IsServer)
// fill in disk encryption information
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true))
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hFleet.ID, "test-key", "", nil)
require.NoError(t, err)
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hFleet.ID}, true, time.Now())
require.NoError(t, err)
loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
require.NoError(t, err)
require.True(t, loadFleet.MDM.EncryptionKeyAvailable)
require.NoError(t, err)
require.NotNil(t, loadFleet.DiskEncryptionEnabled)
require.True(t, *loadFleet.DiskEncryptionEnabled)
} }
func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected *bool) { func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedDecryptable *bool) {
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error { got, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
var actual *bool
row := tx.QueryRowxContext(
context.Background(),
"SELECT decryptable FROM host_disk_encryption_keys WHERE host_id = ?",
hostID,
)
err := row.Scan(&actual)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, actual) require.Equal(t, expectedKey, got.Base64Encrypted)
return nil require.Equal(t, expectedDecryptable, got.Decryptable)
})
} }
func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) { 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", PrimaryMac: "30-65-EC-6F-C4-59",
}) })
require.NoError(t, err) require.NoError(t, err)
host3, err := ds.NewHost(context.Background(), &fleet.Host{
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA") 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) 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) require.NoError(t, err)
checkEncryptionKey := func(hostID uint, expected string) { err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB", "", nil)
actual, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, actual.Base64Encrypted)
}
h, err := ds.Host(context.Background(), host.ID) h, err := ds.Host(context.Background(), host.ID)
require.NoError(t, err) require.NoError(t, err)
checkEncryptionKey(h.ID, "AAA") checkEncryptionKeyStatus(t, ds, h.ID, "AAA", nil)
h, err = ds.Host(context.Background(), host2.ID) h, err = ds.Host(context.Background(), host2.ID)
require.NoError(t, err) 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) require.NoError(t, err)
h, err = ds.Host(context.Background(), host2.ID) h, err = ds.Host(context.Background(), host2.ID)
require.NoError(t, err) 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 // setting the encryption key to an existing value doesn't change its
// encryption status // encryption status
err = ds.SetHostsDiskEncryptionKeyStatus(context.Background(), []uint{host.ID}, true, time.Now().Add(time.Hour)) err = ds.SetHostsDiskEncryptionKeyStatus(context.Background(), []uint{host.ID}, true, time.Now().Add(time.Hour))
require.NoError(t, err) 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 // 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) 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 // 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) require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host.ID, nil) checkEncryptionKeyStatus(t, ds, host.ID, "XZY", nil)
// set the key with an initial decrypted status of true
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(true))
require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true))
// same key, provided decrypted status is ignored (stored one is kept)
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(false))
require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true))
// client error, key is removed and decrypted status is nulled
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "fail", nil)
require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host3.ID, "", nil)
// new key, provided decrypted status is applied
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "def", "", ptr.Bool(true))
require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host3.ID, "def", ptr.Bool(true))
// different key, provided decrypted status is applied
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "ghi", "", ptr.Bool(false))
require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host3.ID, "ghi", ptr.Bool(false))
} }
func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) { 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", PrimaryMac: "30-65-EC-6F-C4-58",
}) })
require.NoError(t, err) require.NoError(t, err)
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY") err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil)
require.NoError(t, err) require.NoError(t, err)
host2, err := ds.NewHost(context.Background(), &fleet.Host{ host2, err := ds.NewHost(context.Background(), &fleet.Host{
@ -6709,7 +6777,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
}) })
require.NoError(t, err) require.NoError(t, err)
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY") err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil)
require.NoError(t, err) require.NoError(t, err)
threshold := time.Now().Add(time.Hour) threshold := time.Now().Add(time.Hour)
@ -6717,31 +6785,31 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
// empty set // empty set
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{}, false, threshold) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{}, false, threshold)
require.NoError(t, err) require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host.ID, nil) checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil)
checkEncryptionKeyStatus(t, ds, host2.ID, nil) checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
// keys that changed after the provided threshold are not updated // 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)) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold.Add(-24*time.Hour))
require.NoError(t, err) require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host.ID, nil) checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil)
checkEncryptionKeyStatus(t, ds, host2.ID, nil) checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
// single host // single host
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, threshold) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, threshold)
require.NoError(t, err) require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true)) checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true))
checkEncryptionKeyStatus(t, ds, host2.ID, nil) checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
// multiple hosts // multiple hosts
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold)
require.NoError(t, err) require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true)) checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true))
checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(true)) checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(true))
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold)
require.NoError(t, err) require.NoError(t, err)
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(false)) checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(false))
checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(false)) checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(false))
} }
func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) { func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
@ -6773,9 +6841,9 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
}) })
require.NoError(t, err) require.NoError(t, err)
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY") err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil)
require.NoError(t, err) require.NoError(t, err)
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY") err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil)
require.NoError(t, err) require.NoError(t, err)
keys, err := ds.GetUnverifiedDiskEncryptionKeys(ctx) keys, err := ds.GetUnverifiedDiskEncryptionKeys(ctx)
@ -6794,6 +6862,17 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx) keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, keys, 1) 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) err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold)
require.NoError(t, err) require.NoError(t, err)
@ -6992,7 +7071,7 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) {
require.Equal(t, -1, *got.MDM.TestGetRawDecryptable()) require.Equal(t, -1, *got.MDM.TestGetRawDecryptable())
// create the encryption key row, but unknown decryptable // 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) require.NoError(t, err)
got, err = ds.Host(ctx, host.ID) got, err = ds.Host(ctx, host.ID)

View File

@ -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 := 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{} 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 { if err != nil {
return nil, ctxerr.Wrap(ctx, err, "selecting label query executions") 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. // 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} params := []interface{}{lid}
if opt.ListOptions.OrderKey == "display_name" { 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 = filterHostsByMacOSSettingsStatus(query, opt, params)
query, params = filterHostsByMacOSDiskEncryptionStatus(query, opt, params) query, params = filterHostsByMacOSDiskEncryptionStatus(query, opt, params)
query, params = filterHostsByMDMBootstrapPackageStatus(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 = searchLike(query, params, opt.MatchQuery, hostSearchColumns...)
query, params = appendListOptionsWithCursorToSQL(query, params, &opt.ListOptions) 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) { func (ds *Datastore) CountHostsInLabel(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) (int, error) {
query := `SELECT count(*) FROM label_membership lm query := `SELECT count(*) FROM label_membership lm
JOIN hosts h ON (lm.host_id = h.id) JOIN hosts h ON (lm.host_id = h.id)
LEFT JOIN host_seen_times hst ON (h.id=hst.host_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 query += hostMDMJoin
if opt.LowDiskSpaceFilter != nil { query, params, err := ds.applyHostLabelFilters(ctx, filter, lid, query, opt)
query += ` LEFT JOIN host_disks hd ON (h.id=hd.host_id) ` if err != nil {
return 0, err
} }
query, params := ds.applyHostLabelFilters(filter, lid, query, opt)
var count int var count int
if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, query, params...); err != nil { if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, query, params...); err != nil {
return 0, ctxerr.Wrap(ctx, err, "count hosts") return 0, ctxerr.Wrap(ctx, err, "count hosts")

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test" "github.com/fleetdm/fleet/v4/server/test"
@ -66,6 +67,7 @@ func TestLabels(t *testing.T) {
{"ListHostsInLabelFailingPolicies", testListHostsInLabelFailingPolicies}, {"ListHostsInLabelFailingPolicies", testListHostsInLabelFailingPolicies},
{"ListHostsInLabelDiskEncryptionStatus", testListHostsInLabelDiskEncryptionStatus}, {"ListHostsInLabelDiskEncryptionStatus", testListHostsInLabelDiskEncryptionStatus},
{"HostMemberOfAllLabels", testHostMemberOfAllLabels}, {"HostMemberOfAllLabels", testHostMemberOfAllLabels},
{"ListHostsInLabelOSSettings", testLabelsListHostsInLabelOSSettings},
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
@ -497,12 +499,12 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
Checksum: []byte("csum"), 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: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
// macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero // 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: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{ 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"), 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: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
// macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero // 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: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 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{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2 listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2
} }
func testLabelsBuiltIn(t *testing.T, db *Datastore) { 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})
})
}

View File

@ -3,13 +3,16 @@ package mysql
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/kit/log/level"
"github.com/jmoiron/sqlx" "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) { func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceHWID string) (*fleet.MDMWindowsEnrolledDevice, error) {
stmt := `SELECT stmt := `SELECT
mdm_device_id, mdm_device_id,
@ -36,6 +39,33 @@ func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceH
return &winMDMDevice, nil 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 // MDMWindowsInsertEnrolledDevice inserts a new MDMWindowsEnrolledDevice in the database
func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error { func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error {
stmt := ` stmt := `
@ -74,7 +104,8 @@ func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device
return nil 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 { func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error {
stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?" 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")) 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
}

View File

@ -3,9 +3,15 @@ package mysql
import ( import (
"context" // nolint:gosec // used only to hash for efficient comparisons "context" // nolint:gosec // used only to hash for efficient comparisons
"testing" "testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/fleet" "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/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -66,4 +72,387 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) {
err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID) err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID)
require.ErrorAs(t, err, &nfe) 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})
})
})
} }

View File

@ -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
}

View File

@ -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)
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More