mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
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:
parent
cc547ba02c
commit
f0d77ab3db
1
changes/12927-disk-encryption-settings
Normal file
1
changes/12927-disk-encryption-settings
Normal file
@ -0,0 +1 @@
|
||||
* Deprecate `mdm.macos_settings.enable_disk_encryption` in favor of `mdm.enable_disk_encryption`
|
4
changes/12932-bitlocker-api-updates
Normal file
4
changes/12932-bitlocker-api-updates
Normal 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.
|
1
changes/12933-bitlocker-host-details-api
Normal file
1
changes/12933-bitlocker-host-details-api
Normal file
@ -0,0 +1 @@
|
||||
- Added `mdm.os_settings` to `GET /api/v1/hosts/{id}` response.
|
@ -0,0 +1 @@
|
||||
- change Controls/Disk Encryption and host details page to include windows bitlocker information.
|
1
changes/issue-13954-orbit-disk-encryption-key
Normal file
1
changes/issue-13954-orbit-disk-encryption-key
Normal file
@ -0,0 +1 @@
|
||||
* Added the `POST /api/fleet/orbit/disk_encryption_key` endpoint for Windows hosts to report the bitlocker encryption key.
|
1
changes/issue-14007-support-get-windows-encryption-key
Normal file
1
changes/issue-14007-support-get-windows-encryption-key
Normal file
@ -0,0 +1 @@
|
||||
* Added support to return the decrypted disk encryption key of a Windows host.
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/policies"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
@ -838,7 +839,7 @@ func verifyDiskEncryptionKeys(
|
||||
if key.UpdatedAt.After(latest) {
|
||||
latest = key.UpdatedAt
|
||||
}
|
||||
if _, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey); err != nil {
|
||||
if _, err := mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey); err != nil {
|
||||
undecryptable = append(undecryptable, key.HostID)
|
||||
continue
|
||||
}
|
||||
|
@ -1044,13 +1044,13 @@ spec:
|
||||
foo: qux
|
||||
name: Team1
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_updates:
|
||||
minimum_version: 10.10.10
|
||||
deadline: 1992-03-01
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- %s
|
||||
enable_disk_encryption: false
|
||||
secrets:
|
||||
- secret: BBB
|
||||
`, mobileConfigPath))
|
||||
@ -1062,9 +1062,9 @@ spec:
|
||||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
assert.JSONEq(t, string(json.RawMessage(`{"config":{"views":{"foo":"qux"}}}`)), string(*savedTeam.Config.AgentOptions))
|
||||
assert.Equal(t, fleet.TeamMDM{
|
||||
EnableDiskEncryption: false,
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
EnableDiskEncryption: false,
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
@ -1097,9 +1097,9 @@ spec:
|
||||
require.True(t, ds.NewJobFuncInvoked)
|
||||
// all left untouched, only setup assistant added
|
||||
assert.Equal(t, fleet.TeamMDM{
|
||||
EnableDiskEncryption: false,
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
EnableDiskEncryption: false,
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
@ -1129,9 +1129,9 @@ spec:
|
||||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
// all left untouched, only bootstrap package added
|
||||
assert.Equal(t, fleet.TeamMDM{
|
||||
EnableDiskEncryption: false,
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
EnableDiskEncryption: false,
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
@ -2886,7 +2886,7 @@ spec:
|
||||
macos_settings:
|
||||
enable_disk_encryption: true
|
||||
`,
|
||||
wantErr: `Couldn't update macos_settings because MDM features aren't turned on in Fleet.`,
|
||||
wantErr: `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on`,
|
||||
},
|
||||
{
|
||||
desc: "app config macos_settings.enable_disk_encryption false",
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fleetdm/fleet/v4/pkg/rawjson"
|
||||
"github.com/fleetdm/fleet/v4/pkg/secure"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
@ -167,12 +168,15 @@ func (eacp enrichedAppConfigPresenter) MarshalJSON() ([]byte, error) {
|
||||
*fleet.VulnerabilitiesConfig
|
||||
}
|
||||
|
||||
return json.Marshal(&struct {
|
||||
fleet.EnrichedAppConfig
|
||||
enrichedJSON, err := json.Marshal(fleet.EnrichedAppConfig(eacp))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extraFieldsJSON, err := json.Marshal(&struct {
|
||||
UpdateInterval UpdateIntervalConfigPresenter `json:"update_interval,omitempty"`
|
||||
Vulnerabilities VulnerabilitiesConfigPresenter `json:"vulnerabilities,omitempty"`
|
||||
}{
|
||||
EnrichedAppConfig: fleet.EnrichedAppConfig(eacp),
|
||||
UpdateInterval: UpdateIntervalConfigPresenter{
|
||||
eacp.UpdateInterval.OSQueryDetail.String(),
|
||||
eacp.UpdateInterval.OSQueryPolicy.String(),
|
||||
@ -184,6 +188,13 @@ func (eacp enrichedAppConfigPresenter) MarshalJSON() ([]byte, error) {
|
||||
eacp.Vulnerabilities,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we need to marshal and combine both groups separately because
|
||||
// enrichedAppConfig has a custom marshaler.
|
||||
return rawjson.CombineRoots(enrichedJSON, extraFieldsJSON)
|
||||
}
|
||||
|
||||
func printConfig(c *cli.Context, config interface{}) error {
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -168,15 +167,15 @@ func TestGetTeams(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsText.txt"))
|
||||
b, err := os.ReadFile(filepath.Join("testdata", "expectedGetTeamsText.txt"))
|
||||
require.NoError(t, err)
|
||||
expectedText := string(b)
|
||||
|
||||
b, err = ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsYaml.yml"))
|
||||
b, err = os.ReadFile(filepath.Join("testdata", "expectedGetTeamsYaml.yml"))
|
||||
require.NoError(t, err)
|
||||
expectedYaml := string(b)
|
||||
|
||||
b, err = ioutil.ReadFile(filepath.Join("testdata", "expectedGetTeamsJson.json"))
|
||||
b, err = os.ReadFile(filepath.Join("testdata", "expectedGetTeamsJson.json"))
|
||||
require.NoError(t, err)
|
||||
// must read each JSON value separately and compact it
|
||||
var buf bytes.Buffer
|
||||
@ -206,8 +205,8 @@ func TestGetTeams(t *testing.T) {
|
||||
errBuffer.Reset()
|
||||
actualJSON, err := runWithErrWriter([]string{"get", "teams", "--json"}, &errBuffer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedJson, actualJSON.String())
|
||||
require.Equal(t, errBuffer.String() == expiredBanner.String(), tt.shouldHaveExpiredBanner)
|
||||
require.Equal(t, expectedJson, actualJSON.String())
|
||||
|
||||
errBuffer.Reset()
|
||||
actualYaml, err := runWithErrWriter([]string{"get", "teams", "--yaml"}, &errBuffer)
|
||||
@ -433,7 +432,7 @@ func TestGetHosts(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expected, err := ioutil.ReadFile(filepath.Join("testdata", tt.goldenFile))
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", tt.goldenFile))
|
||||
require.NoError(t, err)
|
||||
expectedResults := tt.scanner(string(expected))
|
||||
actualResult := tt.scanner(runAppForTest(t, tt.args))
|
||||
@ -536,7 +535,7 @@ func TestGetHostsMDM(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.goldenFile != "" {
|
||||
expected, err := ioutil.ReadFile(filepath.Join("testdata", tt.goldenFile))
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", tt.goldenFile))
|
||||
require.NoError(t, err)
|
||||
if ext := filepath.Ext(tt.goldenFile); ext == ".json" {
|
||||
// the output of --json is not a json array, but a list of
|
||||
|
@ -85,6 +85,7 @@
|
||||
"enabled_and_configured": false,
|
||||
"apple_bm_default_team": "",
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
@ -95,8 +96,7 @@
|
||||
"webhook_url": ""
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null,
|
||||
"enable_disk_encryption": false
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
|
@ -19,6 +19,7 @@ spec:
|
||||
enabled_and_configured: false
|
||||
apple_bm_default_team: ""
|
||||
windows_enabled_and_configured: false
|
||||
enable_disk_encryption: false
|
||||
macos_migration:
|
||||
enable: false
|
||||
mode: ""
|
||||
@ -28,7 +29,6 @@ spec:
|
||||
deadline: null
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
|
@ -43,6 +43,7 @@
|
||||
"apple_bm_enabled_and_configured": false,
|
||||
"enabled_and_configured": false,
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
@ -53,8 +54,7 @@
|
||||
"webhook_url": ""
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null,
|
||||
"enable_disk_encryption": false
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
|
@ -19,6 +19,7 @@ spec:
|
||||
apple_bm_terms_expired: false
|
||||
enabled_and_configured: false
|
||||
windows_enabled_and_configured: false
|
||||
enable_disk_encryption: false
|
||||
macos_migration:
|
||||
enable: false
|
||||
mode: ""
|
||||
@ -28,7 +29,6 @@ spec:
|
||||
deadline: null
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
|
@ -24,13 +24,13 @@
|
||||
"enable_software_inventory": true
|
||||
},
|
||||
"mdm": {
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": null,
|
||||
"deadline": null
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null,
|
||||
"enable_disk_encryption": false
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
@ -84,13 +84,13 @@
|
||||
}
|
||||
},
|
||||
"mdm": {
|
||||
"enable_disk_encryption": false,
|
||||
"macos_updates": {
|
||||
"minimum_version": "12.3.1",
|
||||
"deadline": "2021-12-14"
|
||||
},
|
||||
"macos_settings": {
|
||||
"custom_settings": null,
|
||||
"enable_disk_encryption": false
|
||||
"custom_settings": null
|
||||
},
|
||||
"macos_setup": {
|
||||
"bootstrap_package": null,
|
||||
|
@ -7,12 +7,12 @@ spec:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_updates:
|
||||
minimum_version: null
|
||||
deadline: null
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
@ -36,12 +36,12 @@ spec:
|
||||
enable_host_users: false
|
||||
enable_software_inventory: false
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_updates:
|
||||
minimum_version: "12.3.1"
|
||||
deadline: "2021-12-14"
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package:
|
||||
enable_end_user_authentication: false
|
||||
|
@ -19,13 +19,13 @@ spec:
|
||||
apple_bm_terms_expired: false
|
||||
enabled_and_configured: true
|
||||
windows_enabled_and_configured: false
|
||||
enable_disk_encryption: false
|
||||
macos_migration:
|
||||
enable: false
|
||||
mode: ""
|
||||
webhook_url: ""
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
|
@ -19,13 +19,13 @@ spec:
|
||||
apple_bm_terms_expired: false
|
||||
enabled_and_configured: true
|
||||
windows_enabled_and_configured: false
|
||||
enable_disk_encryption: false
|
||||
macos_migration:
|
||||
enable: false
|
||||
mode: ""
|
||||
webhook_url: ""
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
enable_end_user_authentication: false
|
||||
|
@ -7,9 +7,9 @@ spec:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
@ -27,9 +27,9 @@ spec:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: null
|
||||
macos_setup_assistant: null
|
||||
|
@ -7,9 +7,9 @@ spec:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
enable_end_user_authentication: false
|
||||
@ -27,9 +27,9 @@ spec:
|
||||
enable_host_users: false
|
||||
enable_software_inventory: false
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: %s
|
||||
macos_setup_assistant: %s
|
||||
|
@ -7,9 +7,9 @@ spec:
|
||||
enable_host_users: false
|
||||
enable_software_inventory: false
|
||||
mdm:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings: null
|
||||
enable_disk_encryption: false
|
||||
macos_setup:
|
||||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
|
@ -533,6 +533,7 @@ The MDM endpoints exist to support the related command-line interface sub-comman
|
||||
- [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment)
|
||||
- [Preassign profiles to devices](#preassign-profiles-to-devices)
|
||||
- [Match preassigned profiles](#match-preassigned-profiles)
|
||||
- [Get FileVault statistics](#get-filevault-statistics)
|
||||
|
||||
### Generate Apple DEP Key Pair
|
||||
|
||||
@ -701,6 +702,44 @@ This endpoint stores a profile to be assigned to a host at some point in the fut
|
||||
|
||||
`Status: 204`
|
||||
|
||||
### Get FileVault statistics
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
Get aggregate status counts of disk encryption enforced on macOS hosts.
|
||||
|
||||
The summary can optionally be filtered by team id.
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple/filevault/summary`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ------------------------- | ------ | ----- | ------------------------------------------------------------------------- |
|
||||
| team_id | string | query | _Available in Fleet Premium_ The team id to filter the summary. |
|
||||
|
||||
#### Example
|
||||
|
||||
Get aggregate status counts of Apple disk encryption profiles applying to macOS hosts enrolled to Fleet's MDM that are not assigned to any team.
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple/filevault/summary`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
{
|
||||
"verified": 123,
|
||||
"verifying": 123,
|
||||
"action_required": 123,
|
||||
"enforcing": 123,
|
||||
"failed": 123,
|
||||
"removing_enforcement": 123
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Match preassigned profiles
|
||||
|
||||
_Available in Fleet Premium_
|
||||
@ -2291,7 +2330,9 @@ Gets all information required by Fleet Desktop, this includes things like the nu
|
||||
{
|
||||
"failing_policies_count": 3,
|
||||
"notifications": {
|
||||
"needs_mdm_migration": true
|
||||
"needs_mdm_migration": true,
|
||||
"renew_enrollment_profile": false,
|
||||
"enforce_bitlocker_encryption": false,
|
||||
},
|
||||
"config": {
|
||||
"org_info": {
|
||||
@ -2313,6 +2354,7 @@ In regards to the `notifications` key:
|
||||
|
||||
- `needs_mdm_migration` means that the device fits all the requirements to allow the user to initiate an MDM migration to Fleet.
|
||||
- `renew_enrollment_profile` means that the device is currently unmanaged from MDM but should be DEP enrolled into Fleet.
|
||||
- `enforce_bitlocker_encryption` applies only to Windows devices and means that it should encrypt the disk and report the encryption key back to Fleet.
|
||||
|
||||
|
||||
#### Get device's policies
|
||||
|
@ -93,6 +93,7 @@ If you also have Fleetd running on hosts, it will need access to these API endpo
|
||||
* `/api/fleet/orbit/ping`
|
||||
* `/api/fleet/orbit/scripts/request`
|
||||
* `/api/fleet/orbit/scripts/result`
|
||||
* `/api/fleet/orbit/disk_encryption_key`
|
||||
* `/api/osquery/log`
|
||||
|
||||
<meta name="description" value="Find commonly asked questions and answers about contributing to Fleet as part of our community.">
|
||||
|
@ -1829,14 +1829,14 @@ the `software` table.
|
||||
| page | integer | query | Page number of the results to fetch. |
|
||||
| per_page | integer | query | Results per page. |
|
||||
| order_key | string | query | What to order results by. Can be any column in the hosts table. |
|
||||
| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. **Note:** Use `page` instead of `after`. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). |
|
||||
| additional_info_filters | string | query | A comma-delimited list of fields to include in each host's additional information object. See [Fleet Configuration Options](https://fleetdm.com/docs/using-fleet/fleetctl-cli#fleet-configuration-options) for an example configuration with hosts' additional information. Use `*` to get all stored fields. |
|
||||
| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. **Note:** Use `page` instead of `after` |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). |
|
||||
| additional_info_filters | string | query | A comma-delimited list of fields to include in each host's additional information object. See [Fleet Configuration Options](https://fleetdm.com/docs/using-fleet/fleetctl-cli#fleet-configuration-options) for an example configuration with hosts' additional information. Use '*' to get all stored fields. |
|
||||
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
|
||||
| policy_id | integer | query | The ID of the policy to filter hosts by. |
|
||||
| policy_response | string | query | Valid options are `passing` or `failing`. `policy_id` must also be specified with `policy_response`. |
|
||||
| policy_response | string | query | Valid options are 'passing' or 'failing'. `policy_id` must also be specified with `policy_response`. |
|
||||
| software_id | integer | query | The ID of the software to filter hosts by. |
|
||||
| os_id | integer | query | The ID of the operating system to filter hosts by. |
|
||||
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
|
||||
@ -1849,8 +1849,11 @@ the `software` table.
|
||||
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
|
||||
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
|
||||
| disable_failing_policies| boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. |
|
||||
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of `verified`, `verifying`, `action_required`, `enforcing`, `failed`, or `removing_enforcement`. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. |
|
||||
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. |
|
||||
| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
|
||||
|
||||
If `additional_info_filters` is not specified, no `additional` information will be returned.
|
||||
|
||||
@ -1858,9 +1861,9 @@ If `software_id` is specified, an additional top-level key `"software"` is retur
|
||||
|
||||
If `mdm_id` is specified, an additional top-level key `"mobile_device_management_solution"` is returned with the information corresponding to the `mdm_id`.
|
||||
|
||||
If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results.
|
||||
If `mdm_id`, `mdm_name`, `mdm_enrollment_status`, `os_settings`, or `os_settings_disk_encryption` is specified, then Windows Servers are excluded from the results.
|
||||
|
||||
If `munki_issue_id` is specified, an additional top-level key `"munki_issue"` is returned with the information corresponding to the `munki_issue_id`.
|
||||
If `munki_issue_id` is specified, an additional top-level key `munki_issue` is returned with the information corresponding to the `munki_issue_id`.
|
||||
|
||||
If `after` is being used with `created_at` or `updated_at`, the table must be specified in `order_key`. Those columns become `h.created_at` and `h.updated_at`.
|
||||
|
||||
@ -1988,13 +1991,13 @@ Response payload with the `munki_issue_id` filter provided:
|
||||
| Name | Type | In | Description |
|
||||
| ----------------------- | ------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| order_key | string | query | What to order results by. Can be any column in the hosts table. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. |
|
||||
| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an '@', no space, etc.). |
|
||||
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
|
||||
| policy_id | integer | query | The ID of the policy to filter hosts by. |
|
||||
| policy_response | string | query | Valid options are `passing` or `failing`. `policy_id` must also be specified with `policy_response`. |
|
||||
| policy_response | string | query | Valid options are 'passing' or 'failing'. `policy_id` must also be specified with `policy_response`. |
|
||||
| software_id | integer | query | The ID of the software to filter hosts by. |
|
||||
| os_id | integer | query | The ID of the operating system to filter hosts by. |
|
||||
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
|
||||
@ -2006,8 +2009,10 @@ Response payload with the `munki_issue_id` filter provided:
|
||||
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
|
||||
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
|
||||
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of `verified`, `verifying`, `action_required`, `enforcing`, `failed`, or `removing_enforcement`. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
|
||||
If `additional_info_filters` is not specified, no `additional` information will be returned.
|
||||
|
||||
@ -2555,6 +2560,9 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id
|
||||
"bootstrap_package_status": "installed",
|
||||
"detail": ""
|
||||
},
|
||||
"os_settings": {
|
||||
"disk_encryption": null
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": 999,
|
||||
@ -2743,6 +2751,9 @@ This is the API route used by the **My device** page in Fleet desktop to display
|
||||
"detail": "",
|
||||
"bootstrap_package_name": "test.pkg"
|
||||
},
|
||||
"os_settings": {
|
||||
"disk_encryption": null
|
||||
},
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": 999,
|
||||
@ -3291,12 +3302,12 @@ requested by a web browser.
|
||||
| format | string | query | **Required**, must be "csv" (only supported format for now). |
|
||||
| columns | string | query | Comma-delimited list of columns to include in the report (returns all columns if none is specified). |
|
||||
| order_key | string | query | What to order results by. Can be any column in the hosts table. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, `ipv4` and the hosts' email addresses (only searched if the query looks like an email address, i.e. contains an `@`, no space, etc.). |
|
||||
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
|
||||
| policy_id | integer | query | The ID of the policy to filter hosts by. |
|
||||
| policy_response | string | query | Valid options are `passing` or `failing`. `policy_id` must also be specified with `policy_response`. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** |
|
||||
| policy_response | string | query | Valid options are 'passing' or 'failing'. `policy_id` must also be specified with `policy_response`. **Note: If `policy_id` is specified _without_ including `policy_response`, this will also return hosts where the policy is not configured to run or failed to run.** |
|
||||
| software_id | integer | query | The ID of the software to filter hosts by. |
|
||||
| os_id | integer | query | The ID of the operating system to filter hosts by. |
|
||||
| os_name | string | query | The name of the operating system to filter hosts by. `os_version` must also be specified with `os_name` |
|
||||
@ -3308,7 +3319,7 @@ requested by a web browser.
|
||||
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
|
||||
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
|
||||
| label_id | integer | query | A valid label ID. Can only be used in combination with `order_key`, `order_direction`, `status`, `query` and `team_id`. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| disable_failing_policies | boolean | query | If `true`, hosts will return failing policies as 0 (returned as the `issues` column) regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. |
|
||||
|
||||
If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results.
|
||||
@ -3330,7 +3341,7 @@ created_at,updated_at,id,detail_updated_at,label_updated_at,policy_updated_at,la
|
||||
|
||||
### Get host's disk encryption key
|
||||
|
||||
Requires the [macadmins osquery extension](https://github.com/macadmins/osquery-extension) which comes bundled
|
||||
For macOS, requires the [macadmins osquery extension](https://github.com/macadmins/osquery-extension) which comes bundled
|
||||
in [Fleet's osquery installers](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
|
||||
|
||||
Requires Fleet's MDM properly [enabled and configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup).
|
||||
@ -3724,9 +3735,9 @@ Returns a list of the hosts that belong to the specified label.
|
||||
| page | integer | query | Page number of the results to fetch. |
|
||||
| per_page | integer | query | Results per page. |
|
||||
| order_key | string | query | What to order results by. Can be any column in the hosts table. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
|
||||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include 'asc' and 'desc'. Default is 'asc'. |
|
||||
| after | string | query | The value to get results after. This needs `order_key` defined, as that's the column that would be used. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. |
|
||||
| status | string | query | Indicates the status of the hosts to return. Can either be 'new', 'online', 'offline', 'mia' or 'missing'. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `hostname`, `machine_serial`, `uuid`, and `ipv4`. |
|
||||
| team_id | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts in the specified team. |
|
||||
| disable_failing_policies | boolean | query | If "true", hosts will return failing policies as 0 regardless of whether there are any that failed for the host. This is meant to be used when increased performance is needed in exchange for the extra information. |
|
||||
@ -3735,10 +3746,12 @@ Returns a list of the hosts that belong to the specified label.
|
||||
| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Can be one of 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. |
|
||||
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| low_disk_space | integer | query | _Available in Fleet Premium_ Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
|
||||
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of `verified`, `verifying`, `action_required`, `enforcing`, `failed`, or `removing_enforcement`. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of `installed`, `pending`, or `failed`. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| macos_settings_disk_encryption | string | query | Filters the hosts by the status of the macOS disk encryption MDM profile on the host. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. |
|
||||
| bootstrap_package | string | query | _Available in Fleet Premium_ Filters the hosts by the status of the MDM bootstrap package on the host. Can be one of 'installed', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| os_settings | string | query | Filters the hosts by the status of the operating system settings applied to the hosts. Can be one of 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
| os_settings_disk_encryption | string | query | Filters the hosts by the status of the disk encryption setting applied to the hosts. Can be one of 'verified', 'verifying', 'action_required', 'enforcing', 'failed', or 'removing_enforcement'. **Note: If this filter is used in Fleet Premium without a team id filter, the results include only hosts that are not assigned to any team.** |
|
||||
|
||||
If `mdm_id`, `mdm_name` or `mdm_enrollment_status` is specified, then Windows Servers are excluded from the results.
|
||||
If `mdm_id`, `mdm_name`, `mdm_enrollment_status`, `os_settings`, or `os_settings_disk_encryption` is specified, then Windows Servers are excluded from the results.
|
||||
|
||||
#### Example
|
||||
|
||||
@ -4090,11 +4103,11 @@ _Available in Fleet Premium_
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
Get aggregate status counts of disk encryption enforced on hosts.
|
||||
Get aggregate status counts of disk encryption enforced on macOS and Windows hosts.
|
||||
|
||||
The summary can optionally be filtered by team id.
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple/filevault/summary`
|
||||
`GET /api/v1/fleet/mdm/disk_encryption/summary`
|
||||
|
||||
#### Parameters
|
||||
|
||||
@ -4104,9 +4117,9 @@ The summary can optionally be filtered by team id.
|
||||
|
||||
#### Example
|
||||
|
||||
Get aggregate status counts of Apple disk encryption profiles applying to macOS hosts enrolled to Fleet's MDM that are not assigned to any team.
|
||||
Get aggregate disk encryption status counts of macOS and Windows hosts enrolled to Fleet's MDM that are not assigned to any team.
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple/filevault/summary`
|
||||
`GET /api/v1/fleet/mdm/disk_encryption/summary`
|
||||
|
||||
##### Default response
|
||||
|
||||
@ -4114,12 +4127,12 @@ Get aggregate status counts of Apple disk encryption profiles applying to macOS
|
||||
|
||||
```json
|
||||
{
|
||||
"verified": 123,
|
||||
"verifying": 123,
|
||||
"action_required": 123,
|
||||
"enforcing": 123,
|
||||
"failed": 123,
|
||||
"removing_enforcement": 123
|
||||
"verified": {"macos": 123, "windows": 123},
|
||||
"verifying": {"macos": 123, "windows": 0},
|
||||
"action_required": {"macos": 123, "windows": 0},
|
||||
"enforcing": {"macos": 123, "windows": 123},
|
||||
"failed": {"macos": 123, "windows": 123},
|
||||
"removing_enforcement": {"macos": 123, "windows": 0},
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -75,7 +75,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
|
||||
| View Apple mobile device management (MDM) certificate information | | | | ✅ | |
|
||||
| View Apple business manager (BM) information | | | | ✅ | |
|
||||
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ | |
|
||||
| View disk encryption key for macOS hosts | ✅ | ✅ | ✅ | ✅ | |
|
||||
| View disk encryption key for macOS and Windows hosts | ✅ | ✅ | ✅ | ✅ | |
|
||||
| Create edit and delete configuration profiles for macOS hosts | | | ✅ | ✅ | ✅ |
|
||||
| Execute MDM commands on macOS and Windows hosts*** | | | ✅ | ✅ | |
|
||||
| View results of MDM commands executed on macOS and Windows hosts*** | ✅ | ✅ | ✅ | ✅ | |
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
@ -890,12 +891,7 @@ func (svc *Service) getOrCreatePreassignTeam(ctx context.Context, groups []strin
|
||||
}
|
||||
|
||||
payload.MDM = &fleet.TeamPayloadMDM{
|
||||
MacOSSettings: &fleet.MacOSSettings{
|
||||
// teams created by the match endpoint have disk encryption
|
||||
// enabled by default.
|
||||
// TODO: maybe make this configurable?
|
||||
EnableDiskEncryption: true,
|
||||
},
|
||||
EnableDiskEncryption: optjson.SetBool(true),
|
||||
MacOSSetup: &fleet.MacOSSetup{
|
||||
MacOSSetupAssistant: ac.MDM.MacOSSetup.MacOSSetupAssistant,
|
||||
// NOTE: BootstrapPackage is currently ignored by svc.ModifyTeam and gets set
|
||||
@ -968,3 +964,51 @@ func teamNameFromPreassignGroups(groups []string) string {
|
||||
|
||||
return strings.Join(groups, " - ")
|
||||
}
|
||||
|
||||
func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*fleet.MDMDiskEncryptionSummary, error) {
|
||||
// TODO: Consider adding a new generic OSSetting type or Windows-specific type for authz checks
|
||||
// like this.
|
||||
if err := svc.authz.Authorize(ctx, fleet.MDMAppleConfigProfile{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
var macOS fleet.MDMAppleFileVaultSummary
|
||||
if m, err := svc.ds.GetMDMAppleFileVaultSummary(ctx, teamID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting filevault summary")
|
||||
} else if m != nil {
|
||||
macOS = *m
|
||||
}
|
||||
|
||||
var windows fleet.MDMWindowsBitLockerSummary
|
||||
if w, err := svc.ds.GetMDMWindowsBitLockerSummary(ctx, teamID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "getting bitlocker summary")
|
||||
} else if w != nil {
|
||||
windows = *w
|
||||
}
|
||||
|
||||
return &fleet.MDMDiskEncryptionSummary{
|
||||
Verified: fleet.MDMPlatformsCounts{
|
||||
MacOS: macOS.Verified,
|
||||
Windows: windows.Verified,
|
||||
},
|
||||
Verifying: fleet.MDMPlatformsCounts{
|
||||
MacOS: macOS.Verifying,
|
||||
Windows: windows.Verifying,
|
||||
},
|
||||
ActionRequired: fleet.MDMPlatformsCounts{
|
||||
MacOS: macOS.ActionRequired,
|
||||
Windows: windows.ActionRequired,
|
||||
},
|
||||
Enforcing: fleet.MDMPlatformsCounts{
|
||||
MacOS: macOS.Enforcing,
|
||||
Windows: windows.Enforcing,
|
||||
},
|
||||
Failed: fleet.MDMPlatformsCounts{
|
||||
MacOS: macOS.Failed,
|
||||
Windows: windows.Failed,
|
||||
},
|
||||
RemovingEnforcement: fleet.MDMPlatformsCounts{
|
||||
MacOS: macOS.RemovingEnforcement,
|
||||
Windows: windows.RemovingEnforcement,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
@ -150,13 +150,13 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
}
|
||||
}
|
||||
|
||||
if payload.MDM.MacOSSettings != nil {
|
||||
if !appCfg.MDM.EnabledAndConfigured && payload.MDM.MacOSSettings.EnableDiskEncryption {
|
||||
if payload.MDM.EnableDiskEncryption.Valid {
|
||||
macOSDiskEncryptionUpdated = team.Config.MDM.EnableDiskEncryption != payload.MDM.EnableDiskEncryption.Value
|
||||
if macOSDiskEncryptionUpdated && !appCfg.MDM.EnabledAndConfigured {
|
||||
return nil, fleet.NewInvalidArgumentError("macos_settings.enable_disk_encryption",
|
||||
`Couldn't update macos_settings because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`)
|
||||
}
|
||||
macOSDiskEncryptionUpdated = team.Config.MDM.MacOSSettings.EnableDiskEncryption != payload.MDM.MacOSSettings.EnableDiskEncryption
|
||||
team.Config.MDM.MacOSSettings.EnableDiskEncryption = payload.MDM.MacOSSettings.EnableDiskEncryption
|
||||
team.Config.MDM.EnableDiskEncryption = payload.MDM.EnableDiskEncryption.Value
|
||||
}
|
||||
|
||||
if payload.MDM.MacOSSetup != nil {
|
||||
@ -225,7 +225,7 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
|
||||
}
|
||||
if macOSDiskEncryptionUpdated {
|
||||
var act fleet.ActivityDetails
|
||||
if team.Config.MDM.MacOSSettings.EnableDiskEncryption {
|
||||
if team.Config.MDM.EnableDiskEncryption {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
@ -802,6 +802,17 @@ func (svc *Service) createTeamFromSpec(
|
||||
`Couldn't update macos_setup because MDM features aren't turned on in Fleet. Use fleetctl generate mdm-apple and then fleet serve with mdm configuration to turn on MDM features.`))
|
||||
}
|
||||
}
|
||||
enableDiskEncryption := spec.MDM.EnableDiskEncryption.Value
|
||||
if !spec.MDM.EnableDiskEncryption.Valid {
|
||||
if de := macOSSettings.DeprecatedEnableDiskEncryption; de != nil {
|
||||
enableDiskEncryption = *de
|
||||
}
|
||||
}
|
||||
|
||||
if enableDiskEncryption && !defaults.MDM.AtLeastOnePlatformEnabledAndConfigured() {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm",
|
||||
`Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`))
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return &fleet.Team{Name: spec.Name}, nil
|
||||
@ -813,6 +824,7 @@ func (svc *Service) createTeamFromSpec(
|
||||
AgentOptions: agentOptions,
|
||||
Features: features,
|
||||
MDM: fleet.TeamMDM{
|
||||
EnableDiskEncryption: enableDiskEncryption,
|
||||
MacOSUpdates: spec.MDM.MacOSUpdates,
|
||||
MacOSSettings: macOSSettings,
|
||||
MacOSSetup: macOSSetup,
|
||||
@ -824,7 +836,7 @@ func (svc *Service) createTeamFromSpec(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if macOSSettings.EnableDiskEncryption {
|
||||
if enableDiskEncryption && defaults.MDM.EnabledAndConfigured {
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
}
|
||||
@ -871,11 +883,23 @@ func (svc *Service) editTeamFromSpec(
|
||||
team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates
|
||||
}
|
||||
|
||||
oldMacOSDiskEncryption := team.Config.MDM.MacOSSettings.EnableDiskEncryption
|
||||
oldMacOSDiskEncryption := team.Config.MDM.EnableDiskEncryption
|
||||
if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
newMacOSDiskEncryption := team.Config.MDM.MacOSSettings.EnableDiskEncryption
|
||||
|
||||
// 1. if the spec has the new setting, use that
|
||||
// 2. else if the spec has the deprecated setting, use that
|
||||
// 3. otherwise, leave the setting untouched
|
||||
if spec.MDM.EnableDiskEncryption.Valid {
|
||||
team.Config.MDM.EnableDiskEncryption = spec.MDM.EnableDiskEncryption.Value
|
||||
} else if de := team.Config.MDM.MacOSSettings.DeprecatedEnableDiskEncryption; de != nil {
|
||||
team.Config.MDM.EnableDiskEncryption = *de
|
||||
}
|
||||
if team.Config.MDM.EnableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm",
|
||||
`Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`))
|
||||
}
|
||||
|
||||
oldMacOSSetup := team.Config.MDM.MacOSSetup
|
||||
if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set || spec.MDM.MacOSSetup.BootstrapPackage.Set {
|
||||
@ -925,9 +949,9 @@ func (svc *Service) editTeamFromSpec(
|
||||
return err
|
||||
}
|
||||
}
|
||||
if oldMacOSDiskEncryption != newMacOSDiskEncryption {
|
||||
if appCfg.MDM.EnabledAndConfigured && oldMacOSDiskEncryption != team.Config.MDM.EnableDiskEncryption {
|
||||
var act fleet.ActivityDetails
|
||||
if team.Config.MDM.MacOSSettings.EnableDiskEncryption {
|
||||
if team.Config.MDM.EnableDiskEncryption {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
@ -982,7 +1006,7 @@ func (svc *Service) applyTeamMacOSSettings(ctx context.Context, spec *fleet.Team
|
||||
}
|
||||
|
||||
if (setFields["custom_settings"] && len(applyUpon.CustomSettings) > 0) ||
|
||||
(setFields["enable_disk_encryption"] && applyUpon.EnableDiskEncryption) {
|
||||
(setFields["enable_disk_encryption"] && *applyUpon.DeprecatedEnableDiskEncryption) {
|
||||
field := "custom_settings"
|
||||
if !setFields["custom_settings"] {
|
||||
field = "enable_disk_encryption"
|
||||
@ -1016,8 +1040,8 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) {
|
||||
func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Team, payload fleet.MDMAppleSettingsPayload) error {
|
||||
var didUpdate, didUpdateMacOSDiskEncryption bool
|
||||
if payload.EnableDiskEncryption != nil {
|
||||
if tm.Config.MDM.MacOSSettings.EnableDiskEncryption != *payload.EnableDiskEncryption {
|
||||
tm.Config.MDM.MacOSSettings.EnableDiskEncryption = *payload.EnableDiskEncryption
|
||||
if tm.Config.MDM.EnableDiskEncryption != *payload.EnableDiskEncryption {
|
||||
tm.Config.MDM.EnableDiskEncryption = *payload.EnableDiskEncryption
|
||||
didUpdate = true
|
||||
didUpdateMacOSDiskEncryption = true
|
||||
}
|
||||
@ -1029,7 +1053,7 @@ func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Te
|
||||
}
|
||||
if didUpdateMacOSDiskEncryption {
|
||||
var act fleet.ActivityDetails
|
||||
if tm.Config.MDM.MacOSSettings.EnableDiskEncryption {
|
||||
if tm.Config.MDM.EnableDiskEncryption {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name}
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
|
@ -125,6 +125,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
||||
},
|
||||
fleet_desktop: { transparency_url: "https://fleetdm.com/transparency" },
|
||||
mdm: {
|
||||
enable_disk_encryption: false,
|
||||
windows_enabled_and_configured: true,
|
||||
apple_bm_default_team: "Apples",
|
||||
apple_bm_enabled_and_configured: true,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IHost } from "interfaces/host";
|
||||
import { IHostMacMdmProfile } from "interfaces/mdm";
|
||||
import { IHostMdmProfile } from "interfaces/mdm";
|
||||
|
||||
const DEFAULT_HOST_PROFILE_MOCK: IHostMacMdmProfile = {
|
||||
const DEFAULT_HOST_PROFILE_MOCK: IHostMdmProfile = {
|
||||
profile_id: 1,
|
||||
name: "Test Profile",
|
||||
operation_type: "install",
|
||||
@ -10,8 +10,8 @@ const DEFAULT_HOST_PROFILE_MOCK: IHostMacMdmProfile = {
|
||||
};
|
||||
|
||||
export const createMockHostMacMdmProfile = (
|
||||
overrides?: Partial<IHostMacMdmProfile>
|
||||
): IHostMacMdmProfile => {
|
||||
overrides?: Partial<IHostMdmProfile>
|
||||
): IHostMdmProfile => {
|
||||
return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
@ -53,6 +53,11 @@ const DEFAULT_HOST_MOCK: IHost = {
|
||||
enrollment_status: "Off",
|
||||
server_url: "https://www.example.com/1",
|
||||
profiles: [],
|
||||
os_settings: {
|
||||
disk_encryption: {
|
||||
status: null,
|
||||
},
|
||||
},
|
||||
macos_settings: {
|
||||
disk_encryption: null,
|
||||
action_required: null,
|
||||
|
@ -36,6 +36,11 @@ const DEFAULT_HOST_MDM_DATA: IHostMdmData = {
|
||||
name: "MDM Solution",
|
||||
id: 1,
|
||||
profiles: [],
|
||||
os_settings: {
|
||||
disk_encryption: {
|
||||
status: "verified",
|
||||
},
|
||||
},
|
||||
macos_settings: {
|
||||
disk_encryption: null,
|
||||
action_required: null,
|
||||
|
@ -23,7 +23,10 @@ interface IStatusIndicatorWithIconProps {
|
||||
tooltipText: string | JSX.Element;
|
||||
position?: "top" | "bottom";
|
||||
};
|
||||
layout?: "horizontal" | "vertical";
|
||||
className?: string;
|
||||
/** Classname to add to the value text */
|
||||
valueClassName?: string;
|
||||
}
|
||||
|
||||
const statusIconNameMapping: Record<IndicatorStatus, IconNames> = {
|
||||
@ -38,13 +41,18 @@ const StatusIndicatorWithIcon = ({
|
||||
status,
|
||||
value,
|
||||
tooltip,
|
||||
layout = "horizontal",
|
||||
className,
|
||||
valueClassName,
|
||||
}: IStatusIndicatorWithIconProps) => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
const id = `status-${uniqueId()}`;
|
||||
|
||||
const valueClasses = classnames(`${baseClass}__value`, valueClassName, {
|
||||
[`${baseClass}__value-vertical`]: layout === "vertical",
|
||||
});
|
||||
const valueContent = (
|
||||
<span className={`${baseClass}__value`}>
|
||||
<span className={valueClasses}>
|
||||
<Icon name={statusIconNameMapping[status]} />
|
||||
<span>{value}</span>
|
||||
</span>
|
||||
|
@ -1,4 +1,5 @@
|
||||
.status-indicator-with-icon {
|
||||
// default layout is horizontal
|
||||
&__value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -8,4 +9,10 @@
|
||||
margin-right: $pad-xsmall;
|
||||
}
|
||||
}
|
||||
|
||||
// overrides for different layout
|
||||
&__value-vertical {
|
||||
flex-direction: column;
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
}
|
||||
|
@ -1,95 +1,11 @@
|
||||
/* Config interface is a flattened version of the fleet/config API response */
|
||||
|
||||
import {
|
||||
IWebhookHostStatus,
|
||||
IWebhookFailingPolicies,
|
||||
IWebhookSoftwareVulnerabilities,
|
||||
} from "interfaces/webhook";
|
||||
import PropTypes from "prop-types";
|
||||
import { IIntegrations } from "./integration";
|
||||
|
||||
export default PropTypes.shape({
|
||||
org_name: PropTypes.string,
|
||||
org_logo_url: PropTypes.string,
|
||||
contact_url: PropTypes.string,
|
||||
server_url: PropTypes.string,
|
||||
live_query_disabled: PropTypes.bool,
|
||||
enable_analytics: PropTypes.bool,
|
||||
enable_smtp: PropTypes.bool,
|
||||
configured: PropTypes.bool,
|
||||
sender_address: PropTypes.string,
|
||||
server: PropTypes.string,
|
||||
port: PropTypes.number,
|
||||
authentication_type: PropTypes.string,
|
||||
user_name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
enable_ssl_tls: PropTypes.bool,
|
||||
authentication_method: PropTypes.string,
|
||||
domain: PropTypes.string,
|
||||
verify_sll_certs: PropTypes.bool,
|
||||
enable_start_tls: PropTypes.bool,
|
||||
entity_id: PropTypes.string,
|
||||
idp_image_url: PropTypes.string,
|
||||
metadata: PropTypes.string,
|
||||
metadata_url: PropTypes.string,
|
||||
idp_name: PropTypes.string,
|
||||
enable_sso: PropTypes.bool,
|
||||
enable_sso_idp_login: PropTypes.bool,
|
||||
enable_jit_provisioning: PropTypes.bool,
|
||||
host_expiry_enabled: PropTypes.bool,
|
||||
host_expiry_window: PropTypes.number,
|
||||
agent_options: PropTypes.string,
|
||||
tier: PropTypes.string,
|
||||
organization: PropTypes.string,
|
||||
device_count: PropTypes.number,
|
||||
expiration: PropTypes.string,
|
||||
mdm: PropTypes.shape({
|
||||
enabled_and_configured: PropTypes.bool,
|
||||
apple_bm_terms_expired: PropTypes.bool,
|
||||
apple_bm_enabled_and_configured: PropTypes.bool,
|
||||
windows_enabled_and_configured: PropTypes.bool,
|
||||
macos_updates: PropTypes.shape({
|
||||
minimum_version: PropTypes.string,
|
||||
deadline: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
note: PropTypes.string,
|
||||
// vulnerability_settings: PropTypes.any, TODO
|
||||
enable_host_status_webhook: PropTypes.bool,
|
||||
destination_url: PropTypes.string,
|
||||
host_percentage: PropTypes.number,
|
||||
days_count: PropTypes.number,
|
||||
logging: PropTypes.shape({
|
||||
debug: PropTypes.bool,
|
||||
json: PropTypes.bool,
|
||||
result: PropTypes.shape({
|
||||
plugin: PropTypes.string,
|
||||
config: PropTypes.shape({
|
||||
status_log_file: PropTypes.string,
|
||||
result_log_file: PropTypes.string,
|
||||
enable_log_rotation: PropTypes.bool,
|
||||
enable_log_compression: PropTypes.bool,
|
||||
}),
|
||||
}),
|
||||
status: PropTypes.shape({
|
||||
plugin: PropTypes.string,
|
||||
config: PropTypes.shape({
|
||||
status_log_file: PropTypes.string,
|
||||
result_log_file: PropTypes.string,
|
||||
enable_log_rotation: PropTypes.bool,
|
||||
enable_log_compression: PropTypes.bool,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
email: PropTypes.shape({
|
||||
backend: PropTypes.string,
|
||||
config: PropTypes.shape({
|
||||
region: PropTypes.string,
|
||||
source_arn: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export interface ILicense {
|
||||
tier: string;
|
||||
device_count: number;
|
||||
@ -113,6 +29,7 @@ export interface IMacOsMigrationSettings {
|
||||
}
|
||||
|
||||
export interface IMdmConfig {
|
||||
enable_disk_encryption: boolean;
|
||||
enabled_and_configured: boolean;
|
||||
apple_bm_default_team?: string;
|
||||
apple_bm_terms_expired: boolean;
|
||||
@ -285,7 +202,10 @@ export interface IConfig {
|
||||
};
|
||||
};
|
||||
mdm: IMdmConfig;
|
||||
mdm_enabled?: boolean; // TODO: remove when windows MDM is released. Only used for windows MDM dev currently.
|
||||
/** This is the flag that determines if the windwos mdm feature flag is enabled.
|
||||
TODO: WINDOWS FEATURE FLAG: remove when windows MDM is released. Only used for windows MDM dev currently.
|
||||
*/
|
||||
mdm_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IWebhookSettings {
|
||||
|
@ -8,9 +8,10 @@ import hostQueryResult from "./campaign";
|
||||
import queryStatsInterface, { IQueryStats } from "./query_stats";
|
||||
import { ILicense, IDeviceGlobalConfig } from "./config";
|
||||
import {
|
||||
IHostMacMdmProfile,
|
||||
IHostMdmProfile,
|
||||
MdmEnrollmentStatus,
|
||||
BootstrapPackageStatus,
|
||||
DiskEncryptionStatus,
|
||||
} from "./mdm";
|
||||
|
||||
export default PropTypes.shape({
|
||||
@ -90,18 +91,16 @@ export interface IMunkiData {
|
||||
version: string;
|
||||
}
|
||||
|
||||
type MacDiskEncryptionState =
|
||||
| "applied"
|
||||
| "action_required"
|
||||
| "enforcing"
|
||||
| "failed"
|
||||
| "removing_enforcement"
|
||||
| null;
|
||||
|
||||
type MacDiskEncryptionActionRequired = "log_out" | "rotate_key" | null;
|
||||
|
||||
export interface IOSSettings {
|
||||
disk_encryption: {
|
||||
status: DiskEncryptionStatus | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface IMdmMacOsSettings {
|
||||
disk_encryption: MacDiskEncryptionState | null;
|
||||
disk_encryption: DiskEncryptionStatus | null;
|
||||
action_required: MacDiskEncryptionActionRequired | null;
|
||||
}
|
||||
|
||||
@ -117,7 +116,8 @@ export interface IHostMdmData {
|
||||
name?: string;
|
||||
server_url: string | null;
|
||||
id?: number;
|
||||
profiles: IHostMacMdmProfile[] | null;
|
||||
profiles: IHostMdmProfile[] | null;
|
||||
os_settings?: IOSSettings;
|
||||
macos_settings?: IMdmMacOsSettings;
|
||||
macos_setup?: IMdmMacOsSetup;
|
||||
}
|
||||
@ -210,7 +210,7 @@ export interface IHost {
|
||||
osquery_version: string;
|
||||
os_version: string;
|
||||
build: string;
|
||||
platform_like: string;
|
||||
platform_like: string; // TODO: replace with more specific union type
|
||||
code_name: string;
|
||||
uptime: number;
|
||||
memory: number;
|
||||
|
@ -22,8 +22,6 @@ export const MDM_ENROLLMENT_STATUS = {
|
||||
|
||||
export type MdmEnrollmentStatus = keyof typeof MDM_ENROLLMENT_STATUS;
|
||||
|
||||
export type ProfileSummaryResponse = Record<MdmProfileStatus, number>;
|
||||
|
||||
export interface IMdmStatusCardData {
|
||||
status: MdmEnrollmentStatus;
|
||||
hosts: number;
|
||||
@ -74,16 +72,15 @@ export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
|
||||
|
||||
export type MacMdmProfileOperationType = "remove" | "install";
|
||||
|
||||
export interface IHostMacMdmProfile {
|
||||
export interface IHostMdmProfile {
|
||||
profile_id: number;
|
||||
name: string;
|
||||
// identifier?: string; // TODO: add when API is updated to return this
|
||||
operation_type: MacMdmProfileOperationType;
|
||||
operation_type: MacMdmProfileOperationType | null;
|
||||
status: MdmProfileStatus;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export type FileVaultProfileStatus =
|
||||
export type DiskEncryptionStatus =
|
||||
| "verified"
|
||||
| "verifying"
|
||||
| "action_required"
|
||||
@ -91,9 +88,18 @@ export type FileVaultProfileStatus =
|
||||
| "failed"
|
||||
| "removing_enforcement";
|
||||
|
||||
// // TODO: update when list profiles API returns identifier
|
||||
// export const FLEET_FILEVAULT_PROFILE_IDENTIFIER =
|
||||
// "com.fleetdm.fleet.mdm.filevault";
|
||||
/** Currently windows disk enxryption status will only be one of these four
|
||||
values. In the future we may add more. */
|
||||
export type IWindowsDiskEncryptionStatus = Extract<
|
||||
DiskEncryptionStatus,
|
||||
"verified" | "verifying" | "enforcing" | "failed"
|
||||
>;
|
||||
|
||||
export const isWindowsDiskEncryptionStatus = (
|
||||
status: DiskEncryptionStatus
|
||||
): status is IWindowsDiskEncryptionStatus => {
|
||||
return !["action_required", "removing_enforcement"].includes(status);
|
||||
};
|
||||
|
||||
export const FLEET_FILEVAULT_PROFILE_DISPLAY_NAME = "Disk encryption";
|
||||
|
||||
|
@ -44,6 +44,7 @@ export interface ITeam extends ITeamSummary {
|
||||
secrets?: IEnrollSecret[];
|
||||
role?: UserRole; // role value is included when the team is in the context of a user
|
||||
mdm?: {
|
||||
enable_disk_encryption: boolean;
|
||||
macos_updates: {
|
||||
minimum_version: string;
|
||||
deadline: string;
|
||||
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from "./AggregateMacSettingsIndicators";
|
@ -4,12 +4,11 @@ import { useQuery } from "react-query";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import SideNav from "pages/admin/components/SideNav";
|
||||
import { ProfileSummaryResponse } from "interfaces/mdm";
|
||||
import { API_NO_TEAM_ID, APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import OS_SETTINGS_NAV_ITEMS from "./OSSettingsNavItems";
|
||||
import AggregateMacSettingsIndicators from "./AggregateMacSettingsIndicators";
|
||||
import ProfileStatusAggregate from "./ProfileStatusAggregate";
|
||||
import TurnOnMdmMessage from "../components/TurnOnMdmMessage";
|
||||
|
||||
const baseClass = "os-settings";
|
||||
@ -40,9 +39,10 @@ const OSSettings = ({
|
||||
data: aggregateProfileStatusData,
|
||||
refetch: refetchAggregateProfileStatus,
|
||||
isLoading: isLoadingAggregateProfileStatus,
|
||||
} = useQuery<ProfileSummaryResponse>(
|
||||
} = useQuery(
|
||||
["aggregateProfileStatuses", teamId],
|
||||
() => mdmAPI.getAggregateProfileStatuses(teamId),
|
||||
() =>
|
||||
mdmAPI.getAggregateProfileStatuses(teamId, config?.mdm_enabled ?? false),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
@ -50,7 +50,10 @@ const OSSettings = ({
|
||||
);
|
||||
|
||||
// MDM is not on so show messaging for user to enable it.
|
||||
if (!config?.mdm.enabled_and_configured) {
|
||||
if (
|
||||
!config?.mdm.enabled_and_configured &&
|
||||
!config?.mdm.windows_enabled_and_configured
|
||||
) {
|
||||
return <TurnOnMdmMessage router={router} />;
|
||||
}
|
||||
|
||||
@ -67,7 +70,7 @@ const OSSettings = ({
|
||||
<p className={`${baseClass}__description`}>
|
||||
Remotely enforce settings on macOS hosts assigned to this team.
|
||||
</p>
|
||||
<AggregateMacSettingsIndicators
|
||||
<ProfileStatusAggregate
|
||||
isLoading={isLoadingAggregateProfileStatus}
|
||||
teamId={teamId}
|
||||
aggregateProfileStatusData={aggregateProfileStatusData}
|
||||
|
@ -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;
|
@ -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;
|
@ -1,4 +1,4 @@
|
||||
.aggregate-mac-settings-indicators {
|
||||
.profile-status-aggregate {
|
||||
display: flex;
|
||||
height: 94px;
|
||||
border-top: 1px solid #e2e4ea;
|
||||
@ -10,7 +10,7 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.aggregate-mac-settings-indicator {
|
||||
&__profile-status-count {
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
@ -29,13 +29,17 @@
|
||||
font-weight: $regular;
|
||||
}
|
||||
|
||||
.settings-indicator {
|
||||
.profile-status-indicator {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.aggregate-mac-settings-indicator:last-child {
|
||||
&__profile-status-count:last-child {
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
&__status-indicator-value {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from "./ProfileStatusAggregate";
|
@ -31,7 +31,7 @@ const DiskEncryption = ({
|
||||
|
||||
const defaultShowDiskEncryption = currentTeamId
|
||||
? false
|
||||
: config?.mdm.macos_settings.enable_disk_encryption ?? false;
|
||||
: config?.mdm.enable_disk_encryption ?? false;
|
||||
|
||||
const [isLoadingTeam, setIsLoadingTeam] = useState(true);
|
||||
|
||||
@ -67,8 +67,7 @@ const DiskEncryption = ({
|
||||
enabled: currentTeamId !== 0,
|
||||
select: (res) => res.team,
|
||||
onSuccess: (res) => {
|
||||
const enableDiskEncryption =
|
||||
res.mdm?.macos_settings.enable_disk_encryption ?? false;
|
||||
const enableDiskEncryption = res.mdm?.enable_disk_encryption ?? false;
|
||||
setDiskEncryptionEnabled(enableDiskEncryption);
|
||||
setShowAggregate(enableDiskEncryption);
|
||||
setIsLoadingTeam(false);
|
||||
@ -100,6 +99,19 @@ const DiskEncryption = ({
|
||||
setIsLoadingTeam(false);
|
||||
}
|
||||
|
||||
const createDescriptionText = () => {
|
||||
// table is showing disk encryption status.
|
||||
if (showAggregate) {
|
||||
return "If turned on, hosts' disk encryption keys will be stored in Fleet. ";
|
||||
}
|
||||
|
||||
const isWindowsFeatureFlagEnabled = config?.mdm_enabled ?? false;
|
||||
const dynamicText = isWindowsFeatureFlagEnabled
|
||||
? " and “BitLocker” on Windows"
|
||||
: "";
|
||||
return `Also known as “FileVault” on macOS${dynamicText}. If turned on, hosts' disk encryption keys will be stored in Fleet. `;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>Disk encryption</h2>
|
||||
@ -124,8 +136,7 @@ const DiskEncryption = ({
|
||||
On
|
||||
</Checkbox>
|
||||
<p>
|
||||
Apple calls this “FileVault.” If turned on, hosts' disk
|
||||
encryption keys will be stored in Fleet.{" "}
|
||||
{createDescriptionText()}
|
||||
<CustomLink
|
||||
text="Learn more"
|
||||
url="https://fleetdm.com/docs/using-fleet/mdm-disk-encryption"
|
||||
|
@ -2,6 +2,7 @@
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
padding-bottom: $pad-small;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
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 EmptyTable from "components/EmptyTable";
|
||||
@ -18,25 +19,30 @@ interface IDiskEncryptionTableProps {
|
||||
currentTeamId?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_HEADER = "hosts";
|
||||
const DEFAULT_SORT_DIRECTION = "asc";
|
||||
|
||||
const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const {
|
||||
data: diskEncryptionStatusData,
|
||||
error: diskEncryptionStatusError,
|
||||
} = useQuery<IFileVaultSummaryResponse, Error, IFileVaultSummaryResponse>(
|
||||
} = useQuery<IDiskEncryptionSummaryResponse, Error>(
|
||||
["disk-encryption-summary", currentTeamId],
|
||||
() => mdmAPI.getDiskEncryptionAggregate(currentTeamId),
|
||||
() => mdmAPI.getDiskEncryptionSummary(currentTeamId),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
}
|
||||
);
|
||||
|
||||
const tableHeaders = generateTableHeaders();
|
||||
|
||||
const tableData = generateTableData(diskEncryptionStatusData, currentTeamId);
|
||||
// TODO: WINDOWS FEATURE FLAG: remove this when windows feature flag is removed.
|
||||
// this is used to conditianlly show "View all hosts" link in table cells.
|
||||
const windowsFeatureFlagEnabled = config?.mdm_enabled ?? false;
|
||||
const tableHeaders = generateTableHeaders(windowsFeatureFlagEnabled);
|
||||
const tableData = generateTableData(
|
||||
windowsFeatureFlagEnabled,
|
||||
diskEncryptionStatusData,
|
||||
currentTeamId
|
||||
);
|
||||
|
||||
if (diskEncryptionStatusError) {
|
||||
return <DataError />;
|
||||
@ -53,8 +59,7 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
|
||||
isLoading={false}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
defaultSortHeader={DEFAULT_SORT_HEADER}
|
||||
defaultSortDirection={DEFAULT_SORT_DIRECTION}
|
||||
manualSortBy
|
||||
disableTableHeader
|
||||
disablePagination
|
||||
disableCount
|
||||
|
@ -1,7 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
import { FileVaultProfileStatus } from "interfaces/mdm";
|
||||
import { IFileVaultSummaryResponse } from "services/entities/mdm";
|
||||
import { DiskEncryptionStatus } from "interfaces/mdm";
|
||||
import {
|
||||
IDiskEncryptionStatusAggregate,
|
||||
IDiskEncryptionSummaryResponse,
|
||||
} from "services/entities/mdm";
|
||||
import { DISK_ENCRYPTION_QUERY_PARAM_NAME } from "services/entities/hosts";
|
||||
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
@ -12,7 +16,7 @@ import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndica
|
||||
interface IStatusCellValue {
|
||||
displayName: string;
|
||||
statusName: IndicatorStatus;
|
||||
value: FileVaultProfileStatus;
|
||||
value: DiskEncryptionStatus;
|
||||
tooltip?: string | JSX.Element;
|
||||
}
|
||||
|
||||
@ -28,6 +32,7 @@ interface ICellProps {
|
||||
};
|
||||
row: {
|
||||
original: {
|
||||
includeWindows: boolean;
|
||||
status: IStatusCellValue;
|
||||
teamId: number;
|
||||
};
|
||||
@ -72,15 +77,53 @@ const defaultTableHeaders: IDataColumn[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Hosts",
|
||||
title: "macOS hosts",
|
||||
Header: (cellProps: IHeaderProps) => (
|
||||
<HeaderCell
|
||||
value={cellProps.column.title}
|
||||
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: { value: aggregateCount },
|
||||
row: { original },
|
||||
@ -91,7 +134,7 @@ const defaultTableHeaders: IDataColumn[] = [
|
||||
<ViewAllHostsLink
|
||||
className="view-hosts-link"
|
||||
queryParams={{
|
||||
macos_settings_disk_encryption: original.status.value,
|
||||
[DISK_ENCRYPTION_QUERY_PARAM_NAME]: original.status.value,
|
||||
team_id: original.teamId,
|
||||
}}
|
||||
/>
|
||||
@ -101,15 +144,17 @@ const defaultTableHeaders: IDataColumn[] = [
|
||||
},
|
||||
];
|
||||
|
||||
type StatusNames = keyof IFileVaultSummaryResponse;
|
||||
|
||||
type StatusEntry = [StatusNames, number];
|
||||
|
||||
export const generateTableHeaders = (): IDataColumn[] => {
|
||||
// TODO: WINDOWS FEATURE FLAG: return all headers when windows feature flag is removed.
|
||||
export const generateTableHeaders = (
|
||||
includeWindows: boolean
|
||||
): IDataColumn[] => {
|
||||
return includeWindows
|
||||
? [...defaultTableHeaders, ...windowsTableHeader]
|
||||
: defaultTableHeaders;
|
||||
return defaultTableHeaders;
|
||||
};
|
||||
|
||||
const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
|
||||
const STATUS_CELL_VALUES: Record<DiskEncryptionStatus, IStatusCellValue> = {
|
||||
verified: {
|
||||
displayName: "Verified",
|
||||
statusName: "success",
|
||||
@ -122,8 +167,8 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
|
||||
statusName: "successPartial",
|
||||
value: "verifying",
|
||||
tooltip:
|
||||
"These hosts acknowledged the MDM command to install disk encryption profile. " +
|
||||
"Fleet is verifying with osquery and retrieving the disk encryption key. This may take up to one hour.",
|
||||
"These hosts acknowledged the MDM command to turn on disk encryption. Fleet is verifying with " +
|
||||
"osquery and retrieving the disk encryption key. This may take up to one hour.",
|
||||
},
|
||||
action_required: {
|
||||
displayName: "Action required (pending)",
|
||||
@ -141,7 +186,7 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
|
||||
statusName: "pendingPartial",
|
||||
value: "enforcing",
|
||||
tooltip:
|
||||
"These hosts will receive the MDM command to install the disk encryption profile when the hosts come online.",
|
||||
"These hosts will receive the MDM command to turn on disk encryption when the hosts come online.",
|
||||
},
|
||||
failed: {
|
||||
displayName: "Failed",
|
||||
@ -153,21 +198,41 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
|
||||
statusName: "pendingPartial",
|
||||
value: "removing_enforcement",
|
||||
tooltip:
|
||||
"These hosts will receive the MDM command to remove the disk encryption profile when the hosts come online.",
|
||||
"These hosts will receive the MDM command to turn off disk encryption when the hosts come online.",
|
||||
},
|
||||
};
|
||||
|
||||
type StatusEntry = [DiskEncryptionStatus, IDiskEncryptionStatusAggregate];
|
||||
|
||||
// Order of the status column. We want the order to always be the same.
|
||||
const STATUS_ORDER = [
|
||||
"verified",
|
||||
"verifying",
|
||||
"failed",
|
||||
"action_required",
|
||||
"enforcing",
|
||||
"removing_enforcement",
|
||||
] as const;
|
||||
|
||||
export const generateTableData = (
|
||||
data?: IFileVaultSummaryResponse,
|
||||
// TODO: WINDOWS FEATURE FLAG: remove includeWindows when windows feature flag is removed.
|
||||
// This is used to conditionally show "View all hosts" link in table cells.
|
||||
includeWindows: boolean,
|
||||
data?: IDiskEncryptionSummaryResponse,
|
||||
currentTeamId?: number
|
||||
) => {
|
||||
if (!data) return [];
|
||||
const entries = Object.entries(data) as StatusEntry[];
|
||||
|
||||
return entries.map(([status, numHosts]) => ({
|
||||
// eslint-disable-next-line object-shorthand
|
||||
const rowFromStatusEntry = (
|
||||
status: DiskEncryptionStatus,
|
||||
statusAggregate: IDiskEncryptionStatusAggregate
|
||||
) => ({
|
||||
includeWindows,
|
||||
status: STATUS_CELL_VALUES[status],
|
||||
hosts: numHosts,
|
||||
macosHosts: statusAggregate.macos,
|
||||
windowsHosts: statusAggregate.windows,
|
||||
teamId: currentTeamId,
|
||||
}));
|
||||
});
|
||||
|
||||
return STATUS_ORDER.map((status) => rowFromStatusEntry(status, data[status]));
|
||||
};
|
||||
|
@ -1,7 +1,4 @@
|
||||
.disk-encryption-table {
|
||||
padding: $pad-xxlarge;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $pad-xxlarge;
|
||||
|
||||
.data-table-block .data-table tbody td .w250 {
|
||||
|
@ -30,7 +30,7 @@ const TurnOnMdmMessage = ({ router }: ITurnOnMdmMessageProps) => {
|
||||
|
||||
return (
|
||||
<EmptyTable
|
||||
header="Manage your macOS hosts"
|
||||
header="Manage your hosts"
|
||||
info={"Turn on MDM to change settings on your hosts."}
|
||||
primaryButton={renderConnectButton()}
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import { DISK_ENCRYPTION_QUERY_PARAM_NAME } from "services/entities/hosts";
|
||||
|
||||
export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [
|
||||
"query",
|
||||
@ -17,7 +18,7 @@ export const MANAGE_HOSTS_PAGE_FILTER_KEYS = [
|
||||
"os_version",
|
||||
"munki_issue_id",
|
||||
"low_disk_space",
|
||||
"macos_settings_disk_encryption",
|
||||
DISK_ENCRYPTION_QUERY_PARAM_NAME,
|
||||
"bootstrap_package",
|
||||
] as const;
|
||||
|
||||
|
@ -18,6 +18,7 @@ import labelsAPI, { ILabelsResponse } from "services/entities/labels";
|
||||
import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
|
||||
import globalPoliciesAPI from "services/entities/global_policies";
|
||||
import hostsAPI, {
|
||||
DISK_ENCRYPTION_QUERY_PARAM_NAME,
|
||||
ILoadHostsQueryKey,
|
||||
ILoadHostsResponse,
|
||||
ISortOption,
|
||||
@ -49,7 +50,7 @@ import { IOperatingSystemVersion } from "interfaces/operating_system";
|
||||
import { IPolicy, IStoredPolicyResponse } from "interfaces/policy";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IEmptyTableProps } from "interfaces/empty_table";
|
||||
import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
|
||||
import sortUtils from "utilities/sort";
|
||||
import {
|
||||
@ -232,8 +233,8 @@ const ManageHostsPage = ({
|
||||
? parseInt(queryParams.low_disk_space, 10)
|
||||
: undefined;
|
||||
const missingHosts = queryParams?.status === "missing";
|
||||
const diskEncryptionStatus: FileVaultProfileStatus | undefined =
|
||||
queryParams?.macos_settings_disk_encryption;
|
||||
const diskEncryptionStatus: DiskEncryptionStatus | undefined =
|
||||
queryParams?.[DISK_ENCRYPTION_QUERY_PARAM_NAME];
|
||||
const bootstrapPackageStatus: BootstrapPackageStatus | undefined =
|
||||
queryParams?.bootstrap_package;
|
||||
|
||||
@ -558,7 +559,7 @@ const ManageHostsPage = ({
|
||||
};
|
||||
|
||||
const handleChangeDiskEncryptionStatusFilter = (
|
||||
newStatus: FileVaultProfileStatus
|
||||
newStatus: DiskEncryptionStatus
|
||||
) => {
|
||||
handleResetPageIndex();
|
||||
|
||||
@ -569,7 +570,7 @@ const ManageHostsPage = ({
|
||||
routeParams,
|
||||
queryParams: {
|
||||
...queryParams,
|
||||
macos_settings_disk_encryption: newStatus,
|
||||
[DISK_ENCRYPTION_QUERY_PARAM_NAME]: newStatus,
|
||||
page: 0, // resets page index
|
||||
},
|
||||
})
|
||||
@ -768,7 +769,7 @@ const ManageHostsPage = ({
|
||||
newQueryParams.os_version = osVersion;
|
||||
} else if (diskEncryptionStatus && isPremiumTier) {
|
||||
// Premium feature only
|
||||
newQueryParams.macos_settings_disk_encryption = diskEncryptionStatus;
|
||||
newQueryParams[DISK_ENCRYPTION_QUERY_PARAM_NAME] = diskEncryptionStatus;
|
||||
} else if (bootstrapPackageStatus && isPremiumTier) {
|
||||
newQueryParams.bootstrap_package = bootstrapPackageStatus;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { IDropdownOption } from "interfaces/dropdownOption";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { FileVaultProfileStatus } from "interfaces/mdm";
|
||||
import { DiskEncryptionStatus } from "interfaces/mdm";
|
||||
|
||||
const baseClass = "disk-encryption-status-filter";
|
||||
|
||||
@ -42,8 +42,8 @@ const DISK_ENCRYPTION_STATUS_OPTIONS: IDropdownOption[] = [
|
||||
];
|
||||
|
||||
interface IDiskEncryptionStatusFilterProps {
|
||||
diskEncryptionStatus: FileVaultProfileStatus;
|
||||
onChange: (value: FileVaultProfileStatus) => void;
|
||||
diskEncryptionStatus: DiskEncryptionStatus;
|
||||
onChange: (value: DiskEncryptionStatus) => void;
|
||||
}
|
||||
|
||||
const DiskEncryptionStatusFilter = ({
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
IOperatingSystemVersion,
|
||||
} from "interfaces/operating_system";
|
||||
import {
|
||||
FileVaultProfileStatus,
|
||||
DiskEncryptionStatus,
|
||||
BootstrapPackageStatus,
|
||||
IMdmSolution,
|
||||
MDM_ENROLLMENT_STATUS,
|
||||
@ -15,7 +15,10 @@ import {
|
||||
import { IMunkiIssuesAggregate } from "interfaces/macadmins";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import { IPolicy } from "interfaces/policy";
|
||||
import { MacSettingsStatusQueryParam } from "services/entities/hosts";
|
||||
import {
|
||||
DISK_ENCRYPTION_QUERY_PARAM_NAME,
|
||||
MacSettingsStatusQueryParam,
|
||||
} from "services/entities/hosts";
|
||||
|
||||
import {
|
||||
PLATFORM_LABEL_DISPLAY_NAMES,
|
||||
@ -60,7 +63,7 @@ interface IHostsFilterBlockProps {
|
||||
osVersions?: IOperatingSystemVersion[];
|
||||
softwareDetails: ISoftware | null;
|
||||
mdmSolutionDetails: IMdmSolution | null;
|
||||
diskEncryptionStatus?: FileVaultProfileStatus;
|
||||
diskEncryptionStatus?: DiskEncryptionStatus;
|
||||
bootstrapPackageStatus?: BootstrapPackageStatus;
|
||||
};
|
||||
selectedLabel?: ILabel;
|
||||
@ -68,9 +71,7 @@ interface IHostsFilterBlockProps {
|
||||
handleClearRouteParam: () => void;
|
||||
handleClearFilter: (omitParams: string[]) => void;
|
||||
onChangePoliciesFilter: (response: PolicyResponse) => void;
|
||||
onChangeDiskEncryptionStatusFilter: (
|
||||
response: FileVaultProfileStatus
|
||||
) => void;
|
||||
onChangeDiskEncryptionStatusFilter: (response: DiskEncryptionStatus) => void;
|
||||
onChangeBootstrapPackageStatusFilter: (
|
||||
response: BootstrapPackageStatus
|
||||
) => void;
|
||||
@ -376,8 +377,8 @@ const HostsFilterBlock = ({
|
||||
onChange={onChangeDiskEncryptionStatusFilter}
|
||||
/>
|
||||
<FilterPill
|
||||
label="macOS settings: Disk encryption"
|
||||
onClear={() => handleClearFilter(["macos_settings_disk_encryption"])}
|
||||
label="OS settings: Disk encryption"
|
||||
onClear={() => handleClearFilter([DISK_ENCRYPTION_QUERY_PARAM_NAME])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -417,6 +417,7 @@ const DeviceUserPage = ({
|
||||
showRefetchSpinner={showRefetchSpinner}
|
||||
onRefetchHost={onRefetchHost}
|
||||
renderActionButtons={renderActionButtons}
|
||||
osSettings={host?.mdm.os_settings}
|
||||
deviceUser
|
||||
/>
|
||||
<TabsWrapper>
|
||||
@ -489,6 +490,7 @@ const DeviceUserPage = ({
|
||||
)}
|
||||
{showMacSettingsModal && (
|
||||
<MacSettingsModal
|
||||
platform={host?.platform}
|
||||
hostMDMData={host?.mdm}
|
||||
onClose={toggleMacSettingsModal}
|
||||
/>
|
||||
|
@ -65,6 +65,7 @@ import HostActionDropdown from "./HostActionsDropdown/HostActionsDropdown";
|
||||
import MacSettingsModal from "../MacSettingsModal";
|
||||
import BootstrapPackageModal from "./modals/BootstrapPackageModal";
|
||||
import SelectQueryModal from "./modals/SelectQueryModal";
|
||||
import { isSupportedPlatform } from "./modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal";
|
||||
|
||||
const baseClass = "host-details";
|
||||
|
||||
@ -720,6 +721,7 @@ const HostDetailsPage = ({
|
||||
showRefetchSpinner={showRefetchSpinner}
|
||||
onRefetchHost={onRefetchHost}
|
||||
renderActionButtons={renderActionButtons}
|
||||
osSettings={host?.mdm.os_settings}
|
||||
/>
|
||||
<TabsWrapper>
|
||||
<Tabs
|
||||
@ -845,6 +847,7 @@ const HostDetailsPage = ({
|
||||
)}
|
||||
{showMacSettingsModal && (
|
||||
<MacSettingsModal
|
||||
platform={host?.platform}
|
||||
hostMDMData={host?.mdm}
|
||||
onClose={toggleMacSettingsModal}
|
||||
/>
|
||||
@ -852,8 +855,11 @@ const HostDetailsPage = ({
|
||||
{showUnenrollMdmModal && !!host && (
|
||||
<UnenrollMdmModal hostId={host.id} onClose={toggleUnenrollMdmModal} />
|
||||
)}
|
||||
{showDiskEncryptionModal && host && (
|
||||
{showDiskEncryptionModal &&
|
||||
host &&
|
||||
isSupportedPlatform(host.platform) && (
|
||||
<DiskEncryptionKeyModal
|
||||
platform={host.platform}
|
||||
hostId={host.id}
|
||||
onCancel={() => setShowDiskEncryptionModal(false)}
|
||||
/>
|
||||
|
@ -9,15 +9,32 @@ import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent";
|
||||
import DataError from "components/DataError";
|
||||
import { SupportedPlatform } from "interfaces/platform";
|
||||
|
||||
const baseClass = "disk-encryption-key-modal";
|
||||
|
||||
// currently these are the only supported platforms for the disk encryption
|
||||
// key modal.
|
||||
export type ModalSupportedPlatform = Extract<
|
||||
SupportedPlatform,
|
||||
"darwin" | "windows"
|
||||
>;
|
||||
|
||||
// Checks to see if the platform is supported by the modal.
|
||||
export const isSupportedPlatform = (
|
||||
platform: string
|
||||
): platform is ModalSupportedPlatform => {
|
||||
return ["darwin", "windows"].includes(platform);
|
||||
};
|
||||
|
||||
interface IDiskEncryptionKeyModal {
|
||||
platform: ModalSupportedPlatform;
|
||||
hostId: number;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const DiskEncryptionKeyModal = ({
|
||||
platform,
|
||||
hostId,
|
||||
onCancel,
|
||||
}: IDiskEncryptionKeyModal) => {
|
||||
@ -33,6 +50,18 @@ const DiskEncryptionKeyModal = ({
|
||||
select: (data) => data.encryption_key.key,
|
||||
});
|
||||
|
||||
const isMacOS = platform === "darwin";
|
||||
const descriptionText = isMacOS
|
||||
? "The disk encryption key refers to the FileVault recovery key for macOS."
|
||||
: "The disk encryption key refers to the BitLocker recovery key for Windows.";
|
||||
|
||||
const recoveryText = isMacOS
|
||||
? "Use this key to log in to the host if you forgot the password."
|
||||
: "Use this key to unlock the encrypted drive.";
|
||||
const recoveryUrl = isMacOS
|
||||
? "https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#reset-a-macos-hosts-password-using-the-disk-encryption-key"
|
||||
: "https://fleetdm.com/docs/using-fleet/mdm-disk-encryption#unlock-a-windows-hosts-drive-using-the-disk-encryption-key";
|
||||
|
||||
return (
|
||||
<Modal title="Disk encryption key" onExit={onCancel} className={baseClass}>
|
||||
{encryptionKeyError ? (
|
||||
@ -40,15 +69,12 @@ const DiskEncryptionKeyModal = ({
|
||||
) : (
|
||||
<>
|
||||
<InputFieldHiddenContent value={encrpytionKey ?? ""} />
|
||||
<p>{descriptionText}</p>
|
||||
<p>
|
||||
The disk encryption key refers to the FileVault recovery key for
|
||||
macOS.
|
||||
</p>
|
||||
<p>
|
||||
Use this key to log in to the host if you forgot the password.{" "}
|
||||
{recoveryText}{" "}
|
||||
<CustomLink
|
||||
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
|
||||
/>
|
||||
</p>
|
||||
|
@ -1 +0,0 @@
|
||||
export { default } from "./MacSettingsIndicator";
|
@ -7,20 +7,28 @@ import MacSettingsTable from "./MacSettingsTable";
|
||||
import { generateTableData } from "./MacSettingsTable/MacSettingsTableConfig";
|
||||
|
||||
interface IMacSettingsModalProps {
|
||||
hostMDMData?: Pick<IHostMdmData, "profiles" | "macos_settings">;
|
||||
platform?: string;
|
||||
hostMDMData?: IHostMdmData;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "mac-settings-modal";
|
||||
|
||||
const MacSettingsModal = ({ hostMDMData, onClose }: IMacSettingsModalProps) => {
|
||||
const memoizedTableData = useMemo(() => generateTableData(hostMDMData), [
|
||||
const MacSettingsModal = ({
|
||||
platform,
|
||||
hostMDMData,
|
||||
]);
|
||||
onClose,
|
||||
}: IMacSettingsModalProps) => {
|
||||
const memoizedTableData = useMemo(
|
||||
() => generateTableData(hostMDMData, platform),
|
||||
[hostMDMData, platform]
|
||||
);
|
||||
|
||||
if (!platform) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="macOS settings"
|
||||
title="OS settings"
|
||||
onExit={onClose}
|
||||
className={baseClass}
|
||||
width="large"
|
||||
|
@ -10,7 +10,10 @@ import {
|
||||
MacMdmProfileOperationType,
|
||||
} from "interfaces/mdm";
|
||||
|
||||
import { MacSettingsTableStatusValue } from "../MacSettingsTableConfig";
|
||||
import {
|
||||
isMdmProfileStatus,
|
||||
MacSettingsTableStatusValue,
|
||||
} from "../MacSettingsTableConfig";
|
||||
import TooltipContent, {
|
||||
TooltipInnerContentFunc,
|
||||
TooltipInnerContentOption,
|
||||
@ -41,8 +44,8 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
||||
iconName: "pending-partial",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host will receive the MDM command to install the disk encryption profile when the " +
|
||||
"host comes online."
|
||||
? "The hosts will receive the MDM command to turn on disk encryption " +
|
||||
"when the hosts come online."
|
||||
: "The host will receive the MDM command to install the configuration profile when the " +
|
||||
"host comes online.",
|
||||
},
|
||||
@ -56,8 +59,8 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
||||
iconName: "success",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host turned disk encryption on and " +
|
||||
"sent their key to Fleet. Fleet verified with osquery."
|
||||
? "The host turned disk encryption on and sent the key to Fleet. " +
|
||||
"Fleet verified with osquery."
|
||||
: "The host installed the configuration profile. Fleet verified with osquery.",
|
||||
},
|
||||
verifying: {
|
||||
@ -65,8 +68,9 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
||||
iconName: "success-partial",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
? "The host acknowledged the MDM command to install disk encryption profile. Fleet is " +
|
||||
"verifying with osquery and retrieving the disk encryption key. This may take up to one hour."
|
||||
? "The host acknowledged the MDM command to turn on disk encryption. " +
|
||||
"Fleet is verifying with osquery and retrieving the disk encryption key. " +
|
||||
"This may take up to one hour."
|
||||
: "The host acknowledged the MDM command to install the configuration profile. Fleet is " +
|
||||
"verifying with osquery.",
|
||||
},
|
||||
@ -98,9 +102,41 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
type WindowsDiskEncryptionDisplayConfig = Omit<
|
||||
OperationTypeOption,
|
||||
"action_required"
|
||||
>;
|
||||
|
||||
const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDisplayConfig = {
|
||||
verified: {
|
||||
statusText: "Verified",
|
||||
iconName: "success",
|
||||
tooltip: () =>
|
||||
"The host turned disk encryption on and sent the key to Fleet. Fleet verified with osquery.",
|
||||
},
|
||||
verifying: {
|
||||
statusText: "Verifying",
|
||||
iconName: "success-partial",
|
||||
tooltip: () =>
|
||||
"The host acknowledged the MDM command to turn on disk encryption. Fleet is verifying with osquery and retrieving " +
|
||||
"the disk encryption key. This may take up to one hour.",
|
||||
},
|
||||
pending: {
|
||||
statusText: "Enforcing (pending)",
|
||||
iconName: "pending-partial",
|
||||
tooltip: () =>
|
||||
"The host will receive the MDM command to turn on disk encryption when the host comes online.",
|
||||
},
|
||||
failed: {
|
||||
statusText: "Failed",
|
||||
iconName: "error",
|
||||
tooltip: null,
|
||||
},
|
||||
};
|
||||
|
||||
interface IMacSettingStatusCellProps {
|
||||
status: MacSettingsTableStatusValue;
|
||||
operationType: MacMdmProfileOperationType;
|
||||
operationType: MacMdmProfileOperationType | null;
|
||||
profileName: string;
|
||||
}
|
||||
|
||||
@ -108,8 +144,18 @@ const MacSettingStatusCell = ({
|
||||
status,
|
||||
operationType,
|
||||
profileName = "",
|
||||
}: IMacSettingStatusCellProps): JSX.Element => {
|
||||
const diplayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status];
|
||||
}: IMacSettingStatusCellProps) => {
|
||||
let displayOption: ProfileDisplayOption = null;
|
||||
|
||||
// windows hosts do not have an operation type at the moment and their display options are
|
||||
// different than mac hosts.
|
||||
if (!operationType && isMdmProfileStatus(status)) {
|
||||
displayOption = WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG[status];
|
||||
}
|
||||
|
||||
if (operationType) {
|
||||
displayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status];
|
||||
}
|
||||
|
||||
const isDeviceUser = window.location.pathname
|
||||
.toLowerCase()
|
||||
@ -118,8 +164,8 @@ const MacSettingStatusCell = ({
|
||||
const isDiskEncryptionProfile =
|
||||
profileName === FLEET_FILEVAULT_PROFILE_DISPLAY_NAME;
|
||||
|
||||
if (diplayOption) {
|
||||
const { statusText, iconName, tooltip } = diplayOption;
|
||||
if (displayOption) {
|
||||
const { statusText, iconName, tooltip } = displayOption;
|
||||
const tooltipId = uniqueId();
|
||||
return (
|
||||
<span className={baseClass}>
|
||||
|
@ -5,20 +5,27 @@ import { IHostMdmData } from "interfaces/host";
|
||||
import {
|
||||
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
|
||||
// FLEET_FILEVAULT_PROFILE_IDENTIFIER,
|
||||
IHostMacMdmProfile,
|
||||
IHostMdmProfile,
|
||||
MdmProfileStatus,
|
||||
isWindowsDiskEncryptionStatus,
|
||||
} from "interfaces/mdm";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import TruncatedTextCell from "components/TableContainer/DataTable/TruncatedTextCell";
|
||||
import MacSettingStatusCell from "./MacSettingStatusCell";
|
||||
import { generateWinDiskEncryptionProfile } from "../../helpers";
|
||||
|
||||
export interface IMacSettingsTableRow
|
||||
extends Omit<IHostMacMdmProfile, "status"> {
|
||||
export interface IMacSettingsTableRow extends Omit<IHostMdmProfile, "status"> {
|
||||
status: MacSettingsTableStatusValue;
|
||||
}
|
||||
|
||||
export type MacSettingsTableStatusValue = MdmProfileStatus | "action_required";
|
||||
|
||||
export const isMdmProfileStatus = (
|
||||
status: string
|
||||
): status is MdmProfileStatus => {
|
||||
return status !== "action_required";
|
||||
};
|
||||
|
||||
interface IHeaderProps {
|
||||
column: {
|
||||
title: string;
|
||||
@ -92,20 +99,41 @@ const tableHeaders: IDataColumn[] = [
|
||||
];
|
||||
|
||||
export const generateTableData = (
|
||||
hostMDMData?: Pick<IHostMdmData, "profiles" | "macos_settings">
|
||||
hostMDMData?: IHostMdmData,
|
||||
platform?: string
|
||||
) => {
|
||||
if (!platform) return [];
|
||||
|
||||
let rows: IMacSettingsTableRow[] = [];
|
||||
if (!hostMDMData) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
if (
|
||||
platform === "windows" &&
|
||||
hostMDMData.os_settings?.disk_encryption.status &&
|
||||
isWindowsDiskEncryptionStatus(
|
||||
hostMDMData.os_settings.disk_encryption.status
|
||||
)
|
||||
) {
|
||||
rows.push(
|
||||
generateWinDiskEncryptionProfile(
|
||||
hostMDMData.os_settings.disk_encryption.status
|
||||
)
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
const { profiles, macos_settings } = hostMDMData;
|
||||
|
||||
if (!profiles) {
|
||||
return rows;
|
||||
}
|
||||
rows = profiles;
|
||||
|
||||
if (macos_settings?.disk_encryption === "action_required") {
|
||||
if (
|
||||
platform === "darwin" &&
|
||||
macos_settings?.disk_encryption === "action_required"
|
||||
) {
|
||||
rows = profiles.map((p) => {
|
||||
// TODO: this is a brittle check for the filevault profile
|
||||
// it would be better to match on the identifier but it is not
|
||||
|
@ -1,12 +1,15 @@
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import MacSettingsIndicator from "./MacSettingsIndicator";
|
||||
import ProfileStatusIndicator from "./ProfileStatusIndicator";
|
||||
|
||||
describe("MacSettingsIndicator", () => {
|
||||
describe("ProfileStatusIndicator component", () => {
|
||||
it("Renders the text and icon", () => {
|
||||
const indicatorText = "test text";
|
||||
render(
|
||||
<MacSettingsIndicator indicatorText={indicatorText} iconName="success" />
|
||||
<ProfileStatusIndicator
|
||||
indicatorText={indicatorText}
|
||||
iconName="success"
|
||||
/>
|
||||
);
|
||||
const renderedIndicatorText = screen.getByText(indicatorText);
|
||||
const renderedIcon = screen.getByTestId("success-icon");
|
||||
@ -19,7 +22,7 @@ describe("MacSettingsIndicator", () => {
|
||||
const indicatorText = "test text";
|
||||
const tooltipText = "test tooltip text";
|
||||
render(
|
||||
<MacSettingsIndicator
|
||||
<ProfileStatusIndicator
|
||||
indicatorText={indicatorText}
|
||||
iconName="success"
|
||||
tooltip={{ tooltipText }}
|
||||
@ -42,7 +45,7 @@ describe("MacSettingsIndicator", () => {
|
||||
document.body.appendChild(newDiv);
|
||||
};
|
||||
render(
|
||||
<MacSettingsIndicator
|
||||
<ProfileStatusIndicator
|
||||
indicatorText={indicatorText}
|
||||
iconName="success"
|
||||
onClick={() => {
|
@ -4,9 +4,9 @@ import { IconNames } from "components/icons";
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
const baseClass = "settings-indicator";
|
||||
const baseClass = "profile-status-indicator";
|
||||
|
||||
export interface IMacSettingsIndicator {
|
||||
export interface IProfileStatusIndicatorProps {
|
||||
indicatorText: string;
|
||||
iconName: IconNames;
|
||||
onClick?: () => void;
|
||||
@ -16,12 +16,12 @@ export interface IMacSettingsIndicator {
|
||||
};
|
||||
}
|
||||
|
||||
const MacSettingsIndicator = ({
|
||||
const ProfileStatusIndicator = ({
|
||||
indicatorText,
|
||||
iconName,
|
||||
onClick,
|
||||
tooltip,
|
||||
}: IMacSettingsIndicator): JSX.Element => {
|
||||
}: IProfileStatusIndicatorProps) => {
|
||||
const getIndicatorTextWrapped = () => {
|
||||
if (onClick && tooltip?.tooltipText) {
|
||||
return (
|
||||
@ -103,4 +103,4 @@ const MacSettingsIndicator = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MacSettingsIndicator;
|
||||
export default ProfileStatusIndicator;
|
@ -1,4 +1,4 @@
|
||||
.settings-indicator {
|
||||
.profile-status-indicator {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from "./ProfileStatusIndicator";
|
@ -1,7 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { IHostMacMdmProfile, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
|
||||
import {
|
||||
IHostMdmProfile,
|
||||
BootstrapPackageStatus,
|
||||
isWindowsDiskEncryptionStatus,
|
||||
} from "interfaces/mdm";
|
||||
import { IOSSettings } from "interfaces/host";
|
||||
import getHostStatusTooltipText from "pages/hosts/helpers";
|
||||
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
@ -9,6 +14,7 @@ import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import DiskSpaceGraph from "components/DiskSpaceGraph";
|
||||
import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip";
|
||||
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
|
||||
import {
|
||||
getHostDiskEncryptionTooltipMessage,
|
||||
humanHostMemory,
|
||||
@ -16,10 +22,11 @@ import {
|
||||
} from "utilities/helpers";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import StatusIndicator from "components/StatusIndicator";
|
||||
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
|
||||
|
||||
import MacSettingsIndicator from "./MacSettingsIndicator";
|
||||
import HostSummaryIndicator from "./HostSummaryIndicator";
|
||||
import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPackageIndicator";
|
||||
import { generateWinDiskEncryptionProfile } from "../../helpers";
|
||||
|
||||
const baseClass = "host-summary";
|
||||
|
||||
@ -38,7 +45,7 @@ interface IHostSummaryProps {
|
||||
toggleOSPolicyModal?: () => void;
|
||||
toggleMacSettingsModal?: () => void;
|
||||
toggleBootstrapPackageModal?: () => void;
|
||||
hostMdmProfiles?: IHostMacMdmProfile[];
|
||||
hostMdmProfiles?: IHostMdmProfile[];
|
||||
mdmName?: string;
|
||||
showRefetchSpinner: boolean;
|
||||
onRefetchHost: (
|
||||
@ -46,6 +53,7 @@ interface IHostSummaryProps {
|
||||
) => void;
|
||||
renderActionButtons: () => JSX.Element | null;
|
||||
deviceUser?: boolean;
|
||||
osSettings?: IOSSettings;
|
||||
}
|
||||
|
||||
const HostSummary = ({
|
||||
@ -64,8 +72,9 @@ const HostSummary = ({
|
||||
onRefetchHost,
|
||||
renderActionButtons,
|
||||
deviceUser,
|
||||
osSettings,
|
||||
}: IHostSummaryProps): JSX.Element => {
|
||||
const { status, id, platform } = titleData;
|
||||
const { status, platform } = titleData;
|
||||
|
||||
const renderRefetch = () => {
|
||||
const isOnline = titleData.status === "online";
|
||||
@ -179,6 +188,22 @@ const HostSummary = ({
|
||||
};
|
||||
|
||||
const renderSummary = () => {
|
||||
// for windows hosts we have to manually add a profile for disk encryption
|
||||
// as this is not currently included in the `profiles` value from the API
|
||||
// response for windows hosts.
|
||||
if (
|
||||
platform === "windows" &&
|
||||
osSettings?.disk_encryption?.status &&
|
||||
isWindowsDiskEncryptionStatus(osSettings.disk_encryption.status)
|
||||
) {
|
||||
const winDiskEncryptionProfile: IHostMdmProfile = generateWinDiskEncryptionProfile(
|
||||
osSettings.disk_encryption.status
|
||||
);
|
||||
hostMdmProfiles = hostMdmProfiles
|
||||
? [...hostMdmProfiles, winDiskEncryptionProfile]
|
||||
: [winDiskEncryptionProfile];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="info-flex">
|
||||
<div className="info-flex__item info-flex__item--title">
|
||||
@ -198,12 +223,15 @@ const HostSummary = ({
|
||||
|
||||
{isPremiumTier && renderHostTeam()}
|
||||
|
||||
{platform === "darwin" &&
|
||||
{/* Rendering of OS Settings data */}
|
||||
{(platform === "darwin" || platform === "windows") &&
|
||||
isPremiumTier &&
|
||||
mdmName === "Fleet" && // show if 1 - host is enrolled in Fleet MDM, and
|
||||
// TODO: API INTEGRATION: change this when we figure out why the API is
|
||||
// returning "Fleet" or "FleetDM" for the MDM name.
|
||||
mdmName?.includes("Fleet") && // show if 1 - host is enrolled in Fleet MDM, and
|
||||
hostMdmProfiles &&
|
||||
hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced
|
||||
<HostSummaryIndicator title="macOS settings">
|
||||
<HostSummaryIndicator title="OS settings">
|
||||
<MacSettingsIndicator
|
||||
profiles={hostMdmProfiles}
|
||||
onClick={toggleMacSettingsModal}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
|
||||
import { IHostMacMdmProfile } from "interfaces/mdm";
|
||||
import { IHostMdmProfile } from "interfaces/mdm";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
@ -24,23 +24,23 @@ const STATUS_DISPLAY_OPTIONS: StatusDisplayOptions = {
|
||||
Verified: {
|
||||
iconName: "success",
|
||||
tooltipText:
|
||||
"The host installed all configuration profiles. Fleet verified with osquery.",
|
||||
"The host applied all OS settings. Fleet verified with osquery.",
|
||||
},
|
||||
Verifying: {
|
||||
iconName: "success-partial",
|
||||
tooltipText:
|
||||
"The hosts acknowledged all MDM commands to install configuration profiles. Fleet is verifying " +
|
||||
"the profiles are installed with osquery.",
|
||||
"The host acknowledged all MDM commands to apply OS settings. " +
|
||||
"Fleet is verifying the OS settings are applied with osquery.",
|
||||
},
|
||||
Pending: {
|
||||
iconName: "pending-partial",
|
||||
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: {
|
||||
iconName: "error",
|
||||
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".
|
||||
*/
|
||||
const getMacProfileStatus = (
|
||||
hostMacSettings: IHostMacMdmProfile[]
|
||||
hostMacSettings: IHostMdmProfile[]
|
||||
): MacProfileStatus => {
|
||||
const statuses = hostMacSettings.map((setting) => setting.status);
|
||||
if (statuses.includes("failed")) {
|
||||
@ -68,7 +68,7 @@ const getMacProfileStatus = (
|
||||
};
|
||||
|
||||
interface IMacSettingsIndicatorProps {
|
||||
profiles: IHostMacMdmProfile[];
|
||||
profiles: IHostMdmProfile[];
|
||||
onClick?: () => void;
|
||||
}
|
||||
const MacSettingsIndicator = ({
|
||||
|
33
frontend/pages/hosts/details/helpers.ts
Normal file
33
frontend/pages/hosts/details/helpers.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
import { HostStatus } from "interfaces/host";
|
||||
import {
|
||||
buildQueryStringFromParams,
|
||||
@ -43,7 +43,7 @@ export interface IHostCountLoadOptions {
|
||||
osId?: number;
|
||||
osName?: string;
|
||||
osVersion?: string;
|
||||
diskEncryptionStatus?: FileVaultProfileStatus;
|
||||
diskEncryptionStatus?: DiskEncryptionStatus;
|
||||
bootstrapPackageStatus?: BootstrapPackageStatus;
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
import { SelectedPlatform } from "interfaces/platform";
|
||||
import { ISoftware } from "interfaces/software";
|
||||
import {
|
||||
FileVaultProfileStatus,
|
||||
DiskEncryptionStatus,
|
||||
BootstrapPackageStatus,
|
||||
IMdmSolution,
|
||||
} from "interfaces/mdm";
|
||||
@ -29,6 +29,11 @@ export interface ILoadHostsResponse {
|
||||
mobile_device_management_solution: IMdmSolution;
|
||||
}
|
||||
|
||||
// the source of truth for the filter option names.
|
||||
// there are used on many other pages but we define them here.
|
||||
// TODO: add other filter options here.
|
||||
export const DISK_ENCRYPTION_QUERY_PARAM_NAME = "os_settings_disk_encryption";
|
||||
|
||||
export interface ILoadHostsQueryKey extends ILoadHostsOptions {
|
||||
scope: "hosts";
|
||||
}
|
||||
@ -57,7 +62,7 @@ export interface ILoadHostsOptions {
|
||||
device_mapping?: boolean;
|
||||
columns?: string;
|
||||
visibleColumns?: string;
|
||||
diskEncryptionStatus?: FileVaultProfileStatus;
|
||||
diskEncryptionStatus?: DiskEncryptionStatus;
|
||||
bootstrapPackageStatus?: BootstrapPackageStatus;
|
||||
}
|
||||
|
||||
@ -83,7 +88,7 @@ export interface IExportHostsOptions {
|
||||
device_mapping?: boolean;
|
||||
columns?: string;
|
||||
visibleColumns?: string;
|
||||
diskEncryptionStatus?: FileVaultProfileStatus;
|
||||
diskEncryptionStatus?: DiskEncryptionStatus;
|
||||
}
|
||||
|
||||
export interface IActionByFilter {
|
||||
|
@ -1,19 +1,61 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { FileVaultProfileStatus } from "interfaces/mdm";
|
||||
import { DiskEncryptionStatus, MdmProfileStatus } from "interfaces/mdm";
|
||||
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
export type IFileVaultSummaryResponse = Record<FileVaultProfileStatus, number>;
|
||||
|
||||
export interface IEulaMetadataResponse {
|
||||
name: string;
|
||||
token: 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) => {
|
||||
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
|
||||
return sendRequest("GET", DEVICE_USER_MDM_ENROLLMENT_PROFILE(token));
|
||||
@ -72,24 +114,51 @@ export default {
|
||||
return sendRequest("DELETE", MDM_PROFILE(profileId));
|
||||
},
|
||||
|
||||
getAggregateProfileStatuses: (teamId = APP_CONTEXT_NO_TEAM_ID) => {
|
||||
// TODO: API INTEGRATION: we need to rework this when we create API call that
|
||||
// will return the aggregate statuses for windows included in the response.
|
||||
// Currently to get windows data included we will need to make a separate call.
|
||||
// We will likely change this to go back to single "getProfileStatusSummary" API call.
|
||||
getAggregateProfileStatuses: async (
|
||||
teamId = APP_CONTEXT_NO_TEAM_ID,
|
||||
// TODO: WINDOWS FEATURE FLAG: remove when we windows feature is released.
|
||||
includeWindows: boolean
|
||||
) => {
|
||||
// if we are not including windows we can just call the existing profile summary API
|
||||
if (!includeWindows) {
|
||||
return mdmService.getProfileStatusSummary(teamId);
|
||||
}
|
||||
|
||||
// otherwise we have to make two calls and combine the results.
|
||||
return mdmService
|
||||
.getAggregateProfileStatusesWithWindows(teamId)
|
||||
.then((res) => generateCombinedProfileStatusSummary(...res));
|
||||
},
|
||||
|
||||
getAggregateProfileStatusesWithWindows: async (teamId: number) => {
|
||||
return Promise.all([
|
||||
mdmService.getProfileStatusSummary(teamId),
|
||||
mdmService.getDiskEncryptionSummary(teamId),
|
||||
]);
|
||||
},
|
||||
|
||||
getProfileStatusSummary: (teamId = APP_CONTEXT_NO_TEAM_ID) => {
|
||||
const path = `${
|
||||
endpoints.MDM_PROFILES_AGGREGATE_STATUSES
|
||||
}?${buildQueryStringFromParams({ team_id: teamId })}`;
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
|
||||
getDiskEncryptionAggregate: (teamId?: number) => {
|
||||
let { MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: path } = endpoints;
|
||||
getDiskEncryptionSummary: (teamId?: number) => {
|
||||
let { MDM_DISK_ENCRYPTION_SUMMARY: path } = endpoints;
|
||||
|
||||
if (teamId) {
|
||||
path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
|
||||
}
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
|
||||
// TODO: API INTEGRATION: change when API is implemented that works for windows
|
||||
// disk encryption too.
|
||||
updateAppleMdmSettings: (enableDiskEncryption: boolean, teamId?: number) => {
|
||||
const {
|
||||
MDM_UPDATE_APPLE_SETTINGS: teamsEndpoint,
|
||||
@ -98,7 +167,9 @@ export default {
|
||||
if (teamId === 0) {
|
||||
return sendRequest("PATCH", noTeamsEndpoint, {
|
||||
mdm: {
|
||||
// TODO: API INTEGRATION: remove macos_settings when API change is merged in.
|
||||
macos_settings: { enable_disk_encryption: enableDiskEncryption },
|
||||
// enable_disk_encryption: enableDiskEncryption,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -179,3 +250,5 @@ export default {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default mdmService;
|
||||
|
@ -51,7 +51,7 @@ export default {
|
||||
MDM_PROFILE: (id: number) => `/${API_VERSION}/fleet/mdm/apple/profiles/${id}`,
|
||||
MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`,
|
||||
MDM_PROFILES_AGGREGATE_STATUSES: `/${API_VERSION}/fleet/mdm/apple/profiles/summary`,
|
||||
MDM_APPLE_DISK_ENCRYPTION_AGGREGATE: `/${API_VERSION}/fleet/mdm/apple/filevault/summary`,
|
||||
MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`,
|
||||
MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`,
|
||||
MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => {
|
||||
const query = new URLSearchParams({ token });
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { FileVaultProfileStatus, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
import { isEmpty, reduce, omitBy, Dictionary } from "lodash";
|
||||
import { MacSettingsStatusQueryParam } from "services/entities/hosts";
|
||||
|
||||
import { DiskEncryptionStatus, BootstrapPackageStatus } from "interfaces/mdm";
|
||||
import {
|
||||
DISK_ENCRYPTION_QUERY_PARAM_NAME,
|
||||
MacSettingsStatusQueryParam,
|
||||
} from "services/entities/hosts";
|
||||
|
||||
type QueryValues = string | number | boolean | undefined | null;
|
||||
export type QueryParams = Record<string, QueryValues>;
|
||||
@ -24,7 +28,7 @@ interface IMutuallyExclusiveHostParams {
|
||||
osId?: number;
|
||||
osName?: string;
|
||||
osVersion?: string;
|
||||
diskEncryptionStatus?: FileVaultProfileStatus;
|
||||
diskEncryptionStatus?: DiskEncryptionStatus;
|
||||
bootstrapPackageStatus?: BootstrapPackageStatus;
|
||||
}
|
||||
|
||||
@ -123,7 +127,7 @@ export const reconcileMutuallyExclusiveHostParams = ({
|
||||
case !!lowDiskSpaceHosts:
|
||||
return { low_disk_space: lowDiskSpaceHosts };
|
||||
case !!diskEncryptionStatus:
|
||||
return { macos_settings_disk_encryption: diskEncryptionStatus };
|
||||
return { [DISK_ENCRYPTION_QUERY_PARAM_NAME]: diskEncryptionStatus };
|
||||
case !!bootstrapPackageStatus:
|
||||
return { bootstrap_package: bootstrapPackageStatus };
|
||||
default:
|
||||
|
1
go.mod
1
go.mod
@ -272,6 +272,7 @@ require (
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/slack-go/slack v0.9.4 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -1080,6 +1080,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e/go.mod h1:9Tc1SKnfACJb9N7cw2eyuI6xzy845G7uZONBsi5uPEA=
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc=
|
||||
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
|
||||
|
1
orbit/changes/12842-orbit-bitlocker-management
Normal file
1
orbit/changes/12842-orbit-bitlocker-management
Normal file
@ -0,0 +1 @@
|
||||
* Adding support to manage Bitlocker operations through Orbit notifications
|
@ -622,6 +622,7 @@ func main() {
|
||||
const (
|
||||
renewEnrollmentProfileCommandFrequency = time.Hour
|
||||
windowsMDMEnrollmentCommandFrequency = time.Hour
|
||||
windowsMDMBitlockerCommandFrequency = time.Hour
|
||||
)
|
||||
configFetcher := update.ApplyRenewEnrollmentProfileConfigFetcherMiddleware(orbitClient, renewEnrollmentProfileCommandFrequency, fleetURL)
|
||||
configFetcher = update.ApplyRunScriptsConfigFetcherMiddleware(configFetcher, c.Bool("enable-scripts"), orbitClient)
|
||||
@ -638,6 +639,7 @@ func main() {
|
||||
configFetcher = update.ApplySwiftDialogDownloaderMiddleware(configFetcher, updateRunner)
|
||||
case "windows":
|
||||
configFetcher = update.ApplyWindowsMDMEnrollmentFetcherMiddleware(configFetcher, windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient)
|
||||
configFetcher = update.ApplyWindowsMDMBitlockerFetcherMiddleware(configFetcher, windowsMDMBitlockerCommandFrequency, orbitClient)
|
||||
}
|
||||
|
||||
const orbitFlagsUpdateInterval = 30 * time.Second
|
||||
|
17
orbit/pkg/bitlocker/bitlocker_management.go
Normal file
17
orbit/pkg/bitlocker/bitlocker_management.go
Normal 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
|
||||
}
|
19
orbit/pkg/bitlocker/bitlocker_management_notwindows.go
Normal file
19
orbit/pkg/bitlocker/bitlocker_management_notwindows.go
Normal 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
|
||||
}
|
573
orbit/pkg/bitlocker/bitlocker_management_windows.go
Normal file
573
orbit/pkg/bitlocker/bitlocker_management_windows.go
Normal 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
|
||||
}
|
@ -9,3 +9,7 @@ func RunWindowsMDMEnrollment(args WindowsMDMEnrollmentArgs) error {
|
||||
func RunWindowsMDMUnenrollment(args WindowsMDMEnrollmentArgs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsRunningOnWindowsServer() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
@ -174,3 +174,17 @@ func generateWindowsMDMAccessTokenPayload(args WindowsMDMEnrollmentArgs) ([]byte
|
||||
pld.Payload.OrbitNodeKey = args.OrbitNodeKey
|
||||
return json.Marshal(pld)
|
||||
}
|
||||
|
||||
// IsRunningOnWindowsServer determines if the process is running on a Windows server. Exported so it can be used across packages.
|
||||
func IsRunningOnWindowsServer() (bool, error) {
|
||||
installType, err := readInstallationType()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.ToLower(installType) == "server" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/bitlocker"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/profiles"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/scripts"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
@ -397,3 +398,119 @@ func (h *runScriptsConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
type DiskEncryptionKeySetter interface {
|
||||
SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error
|
||||
}
|
||||
|
||||
type execEncryptVolumeFunc func(string) (string, error)
|
||||
|
||||
type windowsMDMBitlockerConfigFetcher struct {
|
||||
// Fetcher is the OrbitConfigFetcher that will be wrapped. It is responsible
|
||||
// for actually returning the orbit configuration or an error.
|
||||
Fetcher OrbitConfigFetcher
|
||||
|
||||
// Frequency is the minimum amount of time that must pass between two
|
||||
// executions of the windows MDM enrollment attempt.
|
||||
Frequency time.Duration
|
||||
|
||||
// Bitlocker Operation Results
|
||||
EncryptionResult DiskEncryptionKeySetter
|
||||
|
||||
// tracks last time the enrollment command was executed
|
||||
lastEnrollRun time.Time
|
||||
|
||||
// ensures only one script execution runs at a time
|
||||
mu sync.Mutex
|
||||
|
||||
// for tests, to be able to mock API commands. If nil, will use
|
||||
// EncryptVolume
|
||||
execEncryptVolumeFn execEncryptVolumeFunc
|
||||
}
|
||||
|
||||
func ApplyWindowsMDMBitlockerFetcherMiddleware(
|
||||
fetcher OrbitConfigFetcher,
|
||||
frequency time.Duration,
|
||||
encryptionResult DiskEncryptionKeySetter,
|
||||
) OrbitConfigFetcher {
|
||||
return &windowsMDMBitlockerConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: frequency,
|
||||
EncryptionResult: encryptionResult,
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
|
||||
// server set the "EnforceBitLockerEncryption" flag to true, executes the command
|
||||
// to attempt BitlockerEncryption (or not, if the device is a Windows Server).
|
||||
func (w *windowsMDMBitlockerConfigFetcher) GetConfig() (*fleet.OrbitConfig, error) {
|
||||
cfg, err := w.Fetcher.GetConfig()
|
||||
if err == nil && cfg.Notifications.EnforceBitLockerEncryption {
|
||||
if w.mu.TryLock() {
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.attemptBitlockerEncryption(cfg.Notifications)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (w *windowsMDMBitlockerConfigFetcher) attemptBitlockerEncryption(notifs fleet.OrbitConfigNotifications) {
|
||||
// do not trigger Bitlocker encryption if running on a Windwos server
|
||||
isWindowsServer, err := IsRunningOnWindowsServer()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("checking if the host is a Windows server")
|
||||
return
|
||||
}
|
||||
|
||||
if isWindowsServer {
|
||||
log.Debug().Msg("device is a Windows Server, encryption is not going to be performed")
|
||||
return
|
||||
}
|
||||
|
||||
if time.Since(w.lastEnrollRun) <= w.Frequency {
|
||||
log.Debug().Msg("skipped encryption process, last run was too recent")
|
||||
return
|
||||
}
|
||||
|
||||
// Performing Bitlocker encryption operation against C: volume
|
||||
|
||||
// We are supporting only C: volume for now
|
||||
targetVolume := "C:"
|
||||
|
||||
// Performing actual encryption
|
||||
|
||||
// Getting Bitlocker encryption mock operation function if any
|
||||
fn := w.execEncryptVolumeFn
|
||||
if fn == nil {
|
||||
// Otherwise, using the real one
|
||||
fn = bitlocker.EncryptVolume
|
||||
}
|
||||
recoveryKey, err := fn(targetVolume)
|
||||
|
||||
// Getting Bitlocker encryption operation error message if any
|
||||
bitlockerError := ""
|
||||
if err != nil {
|
||||
bitlockerError = err.Error()
|
||||
}
|
||||
|
||||
// Update Fleet Server with encryption result
|
||||
payload := fleet.OrbitHostDiskEncryptionKeyPayload{
|
||||
EncryptionKey: []byte(recoveryKey),
|
||||
ClientError: bitlockerError,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to encrypt the volume")
|
||||
return
|
||||
}
|
||||
|
||||
err = w.EncryptionResult.SetOrUpdateDiskEncryptionKey(payload)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to send encryption result to Fleet Server")
|
||||
return
|
||||
}
|
||||
|
||||
w.lastEnrollRun = time.Now()
|
||||
}
|
||||
|
@ -573,3 +573,67 @@ func TestRunScripts(t *testing.T) {
|
||||
require.Contains(t, logBuf.String(), "running scripts [c] succeeded")
|
||||
})
|
||||
}
|
||||
|
||||
type mockDiskEncryptionKeySetter struct{}
|
||||
|
||||
func (m mockDiskEncryptionKeySetter) SetOrUpdateDiskEncryptionKey(diskEncryptionStatus fleet.OrbitHostDiskEncryptionKeyPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBitlockerOperations(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
oldLog := log.Logger
|
||||
log.Logger = log.Output(&logBuf)
|
||||
t.Cleanup(func() { log.Logger = oldLog })
|
||||
|
||||
var (
|
||||
shouldEncrypt = true
|
||||
shouldReturnError = false
|
||||
)
|
||||
|
||||
fetcher := &dummyConfigFetcher{
|
||||
cfg: &fleet.OrbitConfig{
|
||||
Notifications: fleet.OrbitConfigNotifications{
|
||||
EnforceBitLockerEncryption: shouldEncrypt,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
enrollFetcher := &windowsMDMBitlockerConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: time.Hour, // doesn't matter for this test
|
||||
EncryptionResult: mockDiskEncryptionKeySetter{},
|
||||
execEncryptVolumeFn: func(string) (string, error) {
|
||||
if shouldReturnError {
|
||||
return "", errors.New("error")
|
||||
}
|
||||
|
||||
return "123456", nil
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("bitlocker encryption is performed", func(t *testing.T) {
|
||||
shouldEncrypt = true
|
||||
shouldReturnError = false
|
||||
cfg, err := enrollFetcher.GetConfig()
|
||||
require.NoError(t, err) // the dummy fetcher never returns an error
|
||||
require.Equal(t, fetcher.cfg, cfg) // the bitlocker wrapper properly returns the expected config
|
||||
})
|
||||
|
||||
t.Run("bitlocker encryption is not performed", func(t *testing.T) {
|
||||
shouldEncrypt = false
|
||||
shouldReturnError = false
|
||||
cfg, err := enrollFetcher.GetConfig()
|
||||
require.NoError(t, err) // the dummy fetcher never returns an error
|
||||
require.Equal(t, fetcher.cfg, cfg) // the bitlocker wrapper properly returns the expected config
|
||||
})
|
||||
|
||||
t.Run("bitlocker encryption returns an error", func(t *testing.T) {
|
||||
shouldEncrypt = true
|
||||
shouldReturnError = true
|
||||
cfg, err := enrollFetcher.GetConfig()
|
||||
require.NoError(t, err) // the dummy fetcher never returns an error
|
||||
require.Equal(t, fetcher.cfg, cfg) // the bitlocker wrapper properly returns the expected config
|
||||
})
|
||||
}
|
||||
|
@ -53,3 +53,42 @@ func (s *String) UnmarshalJSON(data []byte) error {
|
||||
s.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bool represents an optional boolean value.
|
||||
type Bool struct {
|
||||
Set bool
|
||||
Valid bool
|
||||
Value bool
|
||||
}
|
||||
|
||||
func SetBool(b bool) Bool {
|
||||
return Bool{Set: true, Valid: true, Value: b}
|
||||
}
|
||||
|
||||
func (b Bool) MarshalJSON() ([]byte, error) {
|
||||
if !b.Valid {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return json.Marshal(b.Value)
|
||||
}
|
||||
|
||||
func (b *Bool) UnmarshalJSON(data []byte) error {
|
||||
// If this method was called, the value was set.
|
||||
b.Set = true
|
||||
b.Valid = false
|
||||
|
||||
if bytes.Equal(data, []byte("null")) {
|
||||
// The key was set to null, blank the value
|
||||
b.Value = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// The key isn't set to null
|
||||
var v bool
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
b.Value = v
|
||||
b.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
@ -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
55
pkg/rawjson/rawjson.go
Normal 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
104
pkg/rawjson/rawjson_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -213,3 +213,18 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) {
|
||||
if teamID != nil && *teamID > 0 {
|
||||
tc, err := ds.TeamMDMConfig(ctx, *teamID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return tc.EnableDiskEncryption, nil
|
||||
}
|
||||
ac, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ac.MDM.EnableDiskEncryption.Value, nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
@ -30,6 +31,7 @@ func TestAppConfig(t *testing.T) {
|
||||
{"AggregateEnrollSecretPerTeam", testAggregateEnrollSecretPerTeam},
|
||||
{"Defaults", testAppConfigDefaults},
|
||||
{"Backwards Compatibility", testAppConfigBackwardsCompatibility},
|
||||
{"GetConfigEnableDiskEncryption", testGetConfigEnableDiskEncryption},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@ -309,7 +311,6 @@ func testAppConfigEnrollSecretRoundtrip(t *testing.T, ds *Datastore) {
|
||||
secrets, err = ds.GetEnrollSecrets(context.Background(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, secrets, 2)
|
||||
|
||||
}
|
||||
|
||||
func testAppConfigEnrollSecretUniqueness(t *testing.T, ds *Datastore) {
|
||||
@ -431,3 +432,48 @@ func testAggregateEnrollSecretPerTeam(t *testing.T, ds *Datastore) {
|
||||
{TeamID: ptr.Uint(3), Secret: "team_3_secret_1"},
|
||||
}, agg)
|
||||
}
|
||||
|
||||
func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
defer TruncateTables(t, ds)
|
||||
|
||||
ac, err := ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ac.MDM.EnableDiskEncryption.Value)
|
||||
|
||||
enabled, err := ds.getConfigEnableDiskEncryption(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, enabled)
|
||||
|
||||
// Enable disk encryption for no team
|
||||
ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
|
||||
err = ds.SaveAppConfig(ctx, ac)
|
||||
require.NoError(t, err)
|
||||
ac, err = ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ac.MDM.EnableDiskEncryption.Value)
|
||||
|
||||
enabled, err = ds.getConfigEnableDiskEncryption(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, enabled)
|
||||
|
||||
// Create team
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
tm, err := ds.Team(ctx, team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tm)
|
||||
require.False(t, tm.Config.MDM.EnableDiskEncryption)
|
||||
|
||||
enabled, err = ds.getConfigEnableDiskEncryption(ctx, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, enabled)
|
||||
|
||||
// Enable disk encryption for the team
|
||||
tm.Config.MDM.EnableDiskEncryption = true
|
||||
tm, err = ds.SaveTeam(ctx, tm)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tm)
|
||||
require.True(t, tm.Config.MDM.EnableDiskEncryption)
|
||||
}
|
||||
|
@ -2082,7 +2082,7 @@ func (ds *Datastore) GetMDMIdPAccount(ctx context.Context, uuid string) (*fleet.
|
||||
return &acct, nil
|
||||
}
|
||||
|
||||
func subqueryDiskEncryptionVerifying() (string, []interface{}) {
|
||||
func subqueryFileVaultVerifying() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
1 FROM host_mdm_apple_profiles hmap
|
||||
@ -2100,7 +2100,7 @@ func subqueryDiskEncryptionVerifying() (string, []interface{}) {
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func subqueryDiskEncryptionVerified() (string, []interface{}) {
|
||||
func subqueryFileVaultVerified() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
1 FROM host_mdm_apple_profiles hmap
|
||||
@ -2118,7 +2118,7 @@ func subqueryDiskEncryptionVerified() (string, []interface{}) {
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func subqueryDiskEncryptionActionRequired() (string, []interface{}) {
|
||||
func subqueryFileVaultActionRequired() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
1 FROM host_mdm_apple_profiles hmap
|
||||
@ -2138,7 +2138,7 @@ func subqueryDiskEncryptionActionRequired() (string, []interface{}) {
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func subqueryDiskEncryptionEnforcing() (string, []interface{}) {
|
||||
func subqueryFileVaultEnforcing() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
1 FROM host_mdm_apple_profiles hmap
|
||||
@ -2168,7 +2168,7 @@ func subqueryDiskEncryptionEnforcing() (string, []interface{}) {
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func subqueryDiskEncryptionFailed() (string, []interface{}) {
|
||||
func subqueryFileVaultFailed() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
1 FROM host_mdm_apple_profiles hmap
|
||||
@ -2180,7 +2180,7 @@ func subqueryDiskEncryptionFailed() (string, []interface{}) {
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func subqueryDiskEncryptionRemovingEnforcement() (string, []interface{}) {
|
||||
func subqueryFileVaultRemovingEnforcement() (string, []interface{}) {
|
||||
sql := `
|
||||
SELECT
|
||||
1 FROM host_mdm_apple_profiles hmap
|
||||
@ -2224,20 +2224,20 @@ FROM
|
||||
hosts h
|
||||
LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id
|
||||
WHERE
|
||||
%s`
|
||||
h.platform = 'darwin' AND %s`
|
||||
|
||||
var args []interface{}
|
||||
subqueryVerified, subqueryVerifiedArgs := subqueryDiskEncryptionVerified()
|
||||
subqueryVerified, subqueryVerifiedArgs := subqueryFileVaultVerified()
|
||||
args = append(args, subqueryVerifiedArgs...)
|
||||
subqueryVerifying, subqueryVerifyingArgs := subqueryDiskEncryptionVerifying()
|
||||
subqueryVerifying, subqueryVerifyingArgs := subqueryFileVaultVerifying()
|
||||
args = append(args, subqueryVerifyingArgs...)
|
||||
subqueryActionRequired, subqueryActionRequiredArgs := subqueryDiskEncryptionActionRequired()
|
||||
subqueryActionRequired, subqueryActionRequiredArgs := subqueryFileVaultActionRequired()
|
||||
args = append(args, subqueryActionRequiredArgs...)
|
||||
subqueryEnforcing, subqueryEnforcingArgs := subqueryDiskEncryptionEnforcing()
|
||||
subqueryEnforcing, subqueryEnforcingArgs := subqueryFileVaultEnforcing()
|
||||
args = append(args, subqueryEnforcingArgs...)
|
||||
subqueryFailed, subqueryFailedArgs := subqueryDiskEncryptionFailed()
|
||||
subqueryFailed, subqueryFailedArgs := subqueryFileVaultFailed()
|
||||
args = append(args, subqueryFailedArgs...)
|
||||
subqueryRemovingEnforcement, subqueryRemovingEnforcementArgs := subqueryDiskEncryptionRemovingEnforcement()
|
||||
subqueryRemovingEnforcement, subqueryRemovingEnforcementArgs := subqueryFileVaultRemovingEnforcement()
|
||||
args = append(args, subqueryRemovingEnforcementArgs...)
|
||||
|
||||
teamFilter := "h.team_id IS NULL"
|
||||
|
@ -782,7 +782,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) {
|
||||
var hostID uint
|
||||
err = sqlx.GetContext(context.Background(), ds.reader(context.Background()), &hostID, `SELECT id FROM hosts WHERE uuid = ?`, testUUID)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostID, "asdf", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
key, err := ds.GetHostDiskEncryptionKey(ctx, hostID)
|
||||
@ -1474,7 +1474,7 @@ func upsertHostCPs(
|
||||
func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
checkListHosts := func(status fleet.MacOSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
||||
checkListHosts := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
||||
expectedIDs := []uint{}
|
||||
for _, h := range expected {
|
||||
expectedIDs = append(expectedIDs, h.ID)
|
||||
@ -1556,7 +1556,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "foo")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "foo", "", nil)
|
||||
require.NoError(t, err)
|
||||
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
@ -1596,7 +1596,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(1), res.Verified) // hosts[0] now has filevault fully enforced and verified
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1].ID, "bar")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[1].ID, "bar", "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[1].ID}, false, time.Now().Add(1*time.Hour))
|
||||
require.NoError(t, err)
|
||||
@ -1619,10 +1619,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(1), res.Verified)
|
||||
|
||||
// check that list hosts by status matches summary
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, hosts[1:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, hosts[0:1]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[1:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, hosts[0:1]))
|
||||
|
||||
// create a team
|
||||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
||||
@ -1662,7 +1662,7 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9].ID, "baz")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[9].ID, "baz", "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour))
|
||||
require.NoError(t, err)
|
||||
@ -1675,10 +1675,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
|
||||
// check that list hosts by status matches summary
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{}))
|
||||
|
||||
upsertHostCPs(hosts[9:10], append(teamCPs, fvTeam), fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t)
|
||||
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &team.ID)
|
||||
@ -1701,10 +1701,10 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
|
||||
// check that list hosts by status matches summary
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, []*fleet.Host{}))
|
||||
|
||||
// set decryptable back to true for hosts[9]
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hosts[9].ID}, true, time.Now().Add(1*time.Hour))
|
||||
@ -1718,21 +1718,22 @@ func testAggregateMacOSSettingsStatusWithFileVault(t *testing.T, ds *Datastore)
|
||||
require.Equal(t, uint(1), res.Verified) // hosts[9] goes back to verified
|
||||
|
||||
// check that list hosts by status matches summary
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &team.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &team.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &team.ID, hosts[9:10]))
|
||||
}
|
||||
|
||||
func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
checkListHosts := func(status fleet.MacOSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
||||
checkFilterHostsByMacOSSettings := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
||||
expectedIDs := []uint{}
|
||||
for _, h := range expected {
|
||||
expectedIDs = append(expectedIDs, h.ID)
|
||||
}
|
||||
|
||||
// check that list hosts by macos settings status matches summary
|
||||
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{MacOSSettingsFilter: status, TeamFilter: teamID})
|
||||
gotIDs := []uint{}
|
||||
for _, h := range gotHosts {
|
||||
@ -1742,6 +1743,26 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs)
|
||||
}
|
||||
|
||||
// check that list hosts by os settings status matches summary
|
||||
checkFilterHostsByOSSettings := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
||||
expectedIDs := []uint{}
|
||||
for _, h := range expected {
|
||||
expectedIDs = append(expectedIDs, h.ID)
|
||||
}
|
||||
|
||||
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String("admin")}}, fleet.HostListOptions{OSSettingsFilter: status, TeamFilter: teamID})
|
||||
gotIDs := []uint{}
|
||||
for _, h := range gotHosts {
|
||||
gotIDs = append(gotIDs, h.ID)
|
||||
}
|
||||
|
||||
return assert.NoError(t, err) && assert.Len(t, gotHosts, len(expected)) && assert.ElementsMatch(t, expectedIDs, gotIDs)
|
||||
}
|
||||
|
||||
checkListHosts := func(status fleet.OSSettingsStatus, teamID *uint, expected []*fleet.Host) bool {
|
||||
return checkFilterHostsByMacOSSettings(status, teamID, expected) && checkFilterHostsByOSSettings(status, teamID, expected)
|
||||
}
|
||||
|
||||
var hosts []*fleet.Host
|
||||
for i := 0; i < 10; i++ {
|
||||
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
|
||||
@ -1766,14 +1787,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// all hosts pending install of all profiles
|
||||
upsertHostCPs(hosts, noTeamCPs, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryPending, ctx, ds, t)
|
||||
@ -1784,14 +1805,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// hosts[0] and hosts[1] failed one profile
|
||||
upsertHostCPs(hosts[0:2], noTeamCPs[0:1], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryFailed, ctx, ds, t)
|
||||
@ -1810,14 +1831,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(2), res.Failed) // only count one failure per host (hosts[0] failed two profiles but only counts once)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// hosts[0:3] installed a third profile
|
||||
upsertHostCPs(hosts[0:3], noTeamCPs[2:3], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t)
|
||||
@ -1828,14 +1849,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(2), res.Failed) // no change
|
||||
require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), hosts[2:]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// hosts[6] deletes all its profiles
|
||||
tx, err := ds.writer(ctx).BeginTxx(ctx, nil)
|
||||
@ -1850,14 +1871,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(2), res.Failed) // no change
|
||||
require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// hosts[9] installed all profiles but one is with status nil (pending)
|
||||
upsertHostCPs(hosts[9:10], noTeamCPs[:9], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t)
|
||||
@ -1870,14 +1891,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(2), res.Failed) // no change
|
||||
require.Equal(t, uint(0), res.Verifying) // no change, host must apply all profiles count as latest
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// hosts[9] installed all profiles
|
||||
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t)
|
||||
@ -1889,14 +1910,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(2), res.Failed) // no change
|
||||
require.Equal(t, uint(1), res.Verifying) // add one host that has installed all profiles
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
// create a team
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "rocket"})
|
||||
@ -1908,10 +1929,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed) // no profiles yet
|
||||
require.Equal(t, uint(0), res.Verifying) // no profiles yet
|
||||
require.Equal(t, uint(0), res.Verified) // no profiles yet
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
|
||||
// transfer hosts[9] to new team
|
||||
err = ds.AddHostsToTeam(ctx, &tm.ID, []uint{hosts[9].ID})
|
||||
@ -1926,14 +1947,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(len(hosts)-4), res.Pending) // hosts[9] is still not pending, transferred to team
|
||||
require.Equal(t, uint(2), res.Failed) // no change
|
||||
require.Equal(t, uint(0), res.Verifying) // hosts[9] was transferred so this is now zero
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
|
||||
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, &tm.ID) // get summary for new team
|
||||
require.NoError(t, err)
|
||||
@ -1942,10 +1963,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
|
||||
// create somes config profiles for the new team
|
||||
var teamCPs []*fleet.MDMAppleConfigProfile
|
||||
@ -1964,10 +1985,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
|
||||
// hosts[9] successfully removed old profiles
|
||||
upsertHostCPs(hosts[9:10], noTeamCPs, fleet.MDMAppleOperationTypeRemove, &fleet.MDMAppleDeliveryVerifying, ctx, ds, t)
|
||||
@ -1978,10 +1999,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(1), res.Verifying) // hosts[9] is verifying all new profiles
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
|
||||
// verify one profile on hosts[9]
|
||||
upsertHostCPs(hosts[9:10], teamCPs[0:1], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t)
|
||||
@ -1992,10 +2013,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(1), res.Verifying) // hosts[9] is still verifying other profiles
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, []*fleet.Host{}))
|
||||
|
||||
// verify the other profiles on hosts[9]
|
||||
upsertHostCPs(hosts[9:10], teamCPs[1:], fleet.MDMAppleOperationTypeInstall, &fleet.MDMAppleDeliveryVerified, ctx, ds, t)
|
||||
@ -2006,10 +2027,10 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(0), res.Failed)
|
||||
require.Equal(t, uint(0), res.Verifying)
|
||||
require.Equal(t, uint(1), res.Verified) // hosts[9] is all verified
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, &tm.ID, hosts[9:10]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, &tm.ID, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, &tm.ID, hosts[9:10]))
|
||||
|
||||
// confirm no changes in summary for profiles with no team
|
||||
res, err = ds.GetMDMAppleHostsProfilesSummary(ctx, ptr.Uint(0)) // team id zero represents no team
|
||||
@ -2020,14 +2041,14 @@ func testMDMAppleHostsProfilesStatus(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, uint(2), res.Failed) // two failed hosts
|
||||
require.Equal(t, uint(0), res.Verifying) // hosts[9] transferred to new team so is not counted under no team
|
||||
require.Equal(t, uint(0), res.Verified)
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.MacOSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, nil, pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, nil, hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, nil, []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsPending, ptr.Uint(0), pendingHosts))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsFailed, ptr.Uint(0), hosts[0:2]))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerifying, ptr.Uint(0), []*fleet.Host{}))
|
||||
require.True(t, checkListHosts(fleet.OSSettingsVerified, ptr.Uint(0), []*fleet.Host{}))
|
||||
}
|
||||
|
||||
func testMDMAppleIdPAccount(t *testing.T, ds *Datastore) {
|
||||
@ -2166,7 +2187,7 @@ func testDeleteMDMAppleProfilesForHost(t *testing.T, ds *Datastore) {
|
||||
}
|
||||
|
||||
func createDiskEncryptionRecord(ctx context.Context, ds *Datastore, t *testing.T, hostId uint, key string, decryptable bool, threshold time.Time) {
|
||||
err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostId, key)
|
||||
err := ds.SetOrUpdateHostDiskEncryptionKey(ctx, hostId, key, "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hostId}, decryptable, threshold)
|
||||
require.NoError(t, err)
|
||||
|
@ -887,7 +887,11 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt
|
||||
}
|
||||
|
||||
leftJoinFailingPolicies := !useHostPaginationOptim
|
||||
sql, params = ds.applyHostFilters(opt, sql, filter, params, leftJoinFailingPolicies)
|
||||
|
||||
sql, params, err := ds.applyHostFilters(ctx, opt, sql, filter, params, leftJoinFailingPolicies)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list hosts: apply host filters")
|
||||
}
|
||||
|
||||
hosts := []*fleet.Host{}
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, sql, params...); err != nil {
|
||||
@ -906,7 +910,7 @@ func (ds *Datastore) ListHosts(ctx context.Context, filter fleet.TeamFilter, opt
|
||||
}
|
||||
|
||||
// TODO(Sarah): Do we need to reconcile mutually exclusive filters?
|
||||
func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, filter fleet.TeamFilter, params []interface{}, leftJoinFailingPolicies bool) (string, []interface{}) {
|
||||
func (ds *Datastore) applyHostFilters(ctx context.Context, opt fleet.HostListOptions, sql string, filter fleet.TeamFilter, params []interface{}, leftJoinFailingPolicies bool) (string, []interface{}, error) {
|
||||
opt.OrderKey = defaultHostColumnTableAlias(opt.OrderKey)
|
||||
|
||||
deviceMappingJoin := `LEFT JOIN (
|
||||
@ -1004,12 +1008,20 @@ func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, fil
|
||||
sql, params = filterHostsByMDM(sql, opt, params)
|
||||
sql, params = filterHostsByMacOSSettingsStatus(sql, opt, params)
|
||||
sql, params = filterHostsByMacOSDiskEncryptionStatus(sql, opt, params)
|
||||
if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
|
||||
return "", nil, err
|
||||
} else if opt.OSSettingsFilter.IsValid() {
|
||||
sql, params = ds.filterHostsByOSSettingsStatus(sql, opt, params, enableDiskEncryption)
|
||||
} else if opt.OSSettingsDiskEncryptionFilter.IsValid() {
|
||||
sql, params = ds.filterHostsByOSSettingsDiskEncryptionStatus(sql, opt, params, enableDiskEncryption)
|
||||
}
|
||||
|
||||
sql, params = filterHostsByMDMBootstrapPackageStatus(sql, opt, params)
|
||||
sql, params = filterHostsByOS(sql, opt, params)
|
||||
sql, params, _ = hostSearchLike(sql, params, opt.MatchQuery, hostSearchColumns...)
|
||||
sql, params = appendListOptionsWithCursorToSQL(sql, params, &opt.ListOptions)
|
||||
|
||||
return sql, params
|
||||
return sql, params, nil
|
||||
}
|
||||
|
||||
func filterHostsByTeam(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
|
||||
@ -1115,13 +1127,13 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par
|
||||
var subquery string
|
||||
var subqueryParams []interface{}
|
||||
switch opt.MacOSSettingsFilter {
|
||||
case fleet.MacOSSettingsFailed:
|
||||
case fleet.OSSettingsFailed:
|
||||
subquery, subqueryParams = subqueryHostsMacOSSettingsStatusFailing()
|
||||
case fleet.MacOSSettingsPending:
|
||||
case fleet.OSSettingsPending:
|
||||
subquery, subqueryParams = subqueryHostsMacOSSettingsStatusPending()
|
||||
case fleet.MacOSSettingsVerifying:
|
||||
case fleet.OSSettingsVerifying:
|
||||
subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying()
|
||||
case fleet.MacOSSettingsVerified:
|
||||
case fleet.OSSettingsVerified:
|
||||
subquery, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified()
|
||||
}
|
||||
if subquery != "" {
|
||||
@ -1140,22 +1152,131 @@ func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOption
|
||||
var subqueryParams []interface{}
|
||||
switch opt.MacOSSettingsDiskEncryptionFilter {
|
||||
case fleet.DiskEncryptionVerified:
|
||||
subquery, subqueryParams = subqueryDiskEncryptionVerified()
|
||||
subquery, subqueryParams = subqueryFileVaultVerified()
|
||||
case fleet.DiskEncryptionVerifying:
|
||||
subquery, subqueryParams = subqueryDiskEncryptionVerifying()
|
||||
subquery, subqueryParams = subqueryFileVaultVerifying()
|
||||
case fleet.DiskEncryptionActionRequired:
|
||||
subquery, subqueryParams = subqueryDiskEncryptionActionRequired()
|
||||
subquery, subqueryParams = subqueryFileVaultActionRequired()
|
||||
case fleet.DiskEncryptionEnforcing:
|
||||
subquery, subqueryParams = subqueryDiskEncryptionEnforcing()
|
||||
subquery, subqueryParams = subqueryFileVaultEnforcing()
|
||||
case fleet.DiskEncryptionFailed:
|
||||
subquery, subqueryParams = subqueryDiskEncryptionFailed()
|
||||
subquery, subqueryParams = subqueryFileVaultFailed()
|
||||
case fleet.DiskEncryptionRemovingEnforcement:
|
||||
subquery, subqueryParams = subqueryDiskEncryptionRemovingEnforcement()
|
||||
subquery, subqueryParams = subqueryFileVaultRemovingEnforcement()
|
||||
}
|
||||
|
||||
return sql + fmt.Sprintf(` AND EXISTS (%s)`, subquery), append(params, subqueryParams...)
|
||||
}
|
||||
|
||||
func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostListOptions, params []interface{}, isDiskEncryptionEnabled bool) (string, []interface{}) {
|
||||
if !opt.OSSettingsFilter.IsValid() {
|
||||
return sql, params
|
||||
}
|
||||
|
||||
sqlFmt := ` AND h.platform IN('windows', 'darwin')`
|
||||
if opt.TeamFilter == nil {
|
||||
// macOS settings filter is not compatible with the "all teams" option so append the "no
|
||||
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
|
||||
sqlFmt += ` AND h.team_id IS NULL`
|
||||
}
|
||||
sqlFmt += ` AND ((h.platform = 'windows' AND (%s)) OR (h.platform = 'darwin' AND (%s)))`
|
||||
|
||||
var subqueryMacOS string
|
||||
var subqueryParams []interface{}
|
||||
whereWindows := "FALSE"
|
||||
whereMacOS := "FALSE"
|
||||
|
||||
switch opt.OSSettingsFilter {
|
||||
case fleet.OSSettingsFailed:
|
||||
subqueryMacOS, subqueryParams = subqueryHostsMacOSSettingsStatusFailing()
|
||||
if isDiskEncryptionEnabled {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionFailed)
|
||||
}
|
||||
case fleet.OSSettingsPending:
|
||||
subqueryMacOS, subqueryParams = subqueryHostsMacOSSettingsStatusPending()
|
||||
if isDiskEncryptionEnabled {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing)
|
||||
}
|
||||
case fleet.OSSettingsVerifying:
|
||||
subqueryMacOS, subqueryParams = subqueryHostsMacOSSetttingsStatusVerifying()
|
||||
if isDiskEncryptionEnabled {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying)
|
||||
}
|
||||
case fleet.OSSettingsVerified:
|
||||
subqueryMacOS, subqueryParams = subqueryHostsMacOSSetttingsStatusVerified()
|
||||
if isDiskEncryptionEnabled {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerified)
|
||||
}
|
||||
}
|
||||
|
||||
if subqueryMacOS != "" {
|
||||
whereMacOS = "EXISTS (" + subqueryMacOS + ")"
|
||||
}
|
||||
|
||||
return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...)
|
||||
}
|
||||
|
||||
func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}, enableDiskEncryption bool) (string, []interface{}) {
|
||||
if !opt.OSSettingsDiskEncryptionFilter.IsValid() {
|
||||
return sql, params
|
||||
}
|
||||
|
||||
sqlFmt := " AND h.platform IN('windows', 'darwin')"
|
||||
// TODO: Should we add no team filter here? It isn't included for the FileVault filter but is
|
||||
// for the general macOS settings filter.
|
||||
if opt.TeamFilter == nil {
|
||||
// macOS settings filter is not compatible with the "all teams" option so append the "no
|
||||
// team" filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0)
|
||||
sqlFmt += ` AND h.team_id IS NULL`
|
||||
}
|
||||
sqlFmt += ` AND ((h.platform = 'windows' AND %s) OR (h.platform = 'darwin' AND %s))`
|
||||
|
||||
var subqueryMacOS string
|
||||
var subqueryParams []interface{}
|
||||
whereWindows := "FALSE"
|
||||
whereMacOS := "FALSE"
|
||||
|
||||
switch opt.OSSettingsDiskEncryptionFilter {
|
||||
case fleet.DiskEncryptionVerified:
|
||||
if enableDiskEncryption {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerified)
|
||||
}
|
||||
subqueryMacOS, subqueryParams = subqueryFileVaultVerified()
|
||||
|
||||
case fleet.DiskEncryptionVerifying:
|
||||
if enableDiskEncryption {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying)
|
||||
}
|
||||
subqueryMacOS, subqueryParams = subqueryFileVaultVerifying()
|
||||
|
||||
case fleet.DiskEncryptionActionRequired:
|
||||
// Windows hosts cannot be action required status in the current implementation.
|
||||
subqueryMacOS, subqueryParams = subqueryFileVaultActionRequired()
|
||||
|
||||
case fleet.DiskEncryptionEnforcing:
|
||||
if enableDiskEncryption {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing)
|
||||
}
|
||||
subqueryMacOS, subqueryParams = subqueryFileVaultEnforcing()
|
||||
|
||||
case fleet.DiskEncryptionFailed:
|
||||
if enableDiskEncryption {
|
||||
whereWindows = ds.whereBitLockerStatus(fleet.DiskEncryptionFailed)
|
||||
}
|
||||
subqueryMacOS, subqueryParams = subqueryFileVaultFailed()
|
||||
|
||||
case fleet.DiskEncryptionRemovingEnforcement:
|
||||
// Windows hosts cannot be removing enforcement status in the current implementation.
|
||||
subqueryMacOS, subqueryParams = subqueryFileVaultRemovingEnforcement()
|
||||
}
|
||||
|
||||
if subqueryMacOS != "" {
|
||||
whereMacOS = "EXISTS (" + subqueryMacOS + ")"
|
||||
}
|
||||
|
||||
return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...)
|
||||
}
|
||||
|
||||
func filterHostsByMDMBootstrapPackageStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
|
||||
if opt.MDMBootstrapPackageFilter == nil || !opt.MDMBootstrapPackageFilter.IsValid() {
|
||||
return sql, params
|
||||
@ -1210,7 +1331,11 @@ func (ds *Datastore) CountHosts(ctx context.Context, filter fleet.TeamFilter, op
|
||||
leftJoinFailingPolicies := false
|
||||
|
||||
var params []interface{}
|
||||
sql, params = ds.applyHostFilters(opt, sql, filter, params, leftJoinFailingPolicies)
|
||||
|
||||
sql, params, err := ds.applyHostFilters(ctx, opt, sql, filter, params, leftJoinFailingPolicies)
|
||||
if err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, err, "count hosts: apply host filters")
|
||||
}
|
||||
|
||||
var count int
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, sql, params...); err != nil {
|
||||
@ -1749,6 +1874,7 @@ type hostWithMDMInfo struct {
|
||||
IsServer *bool `db:"is_server"`
|
||||
MDMID *uint `db:"mdm_id"`
|
||||
Name *string `db:"name"`
|
||||
EncryptionKeyAvailable *bool `db:"encryption_key_available"`
|
||||
}
|
||||
|
||||
// LoadHostByOrbitNodeKey loads the whole host identified by the node key.
|
||||
@ -1804,7 +1930,9 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
|
||||
COALESCE(hm.is_server, false) AS is_server,
|
||||
COALESCE(mdms.name, ?) AS name,
|
||||
COALESCE(hdek.reset_requested, false) AS disk_encryption_reset_requested,
|
||||
COALESCE(hdek.decryptable, false) as encryption_key_available,
|
||||
IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
|
||||
hd.encrypted as disk_encryption_enabled,
|
||||
t.name as team_name
|
||||
FROM
|
||||
hosts h
|
||||
@ -1824,6 +1952,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
|
||||
host_disk_encryption_keys hdek
|
||||
ON
|
||||
hdek.host_id = h.id
|
||||
LEFT OUTER JOIN
|
||||
host_disks hd
|
||||
ON
|
||||
hd.host_id = h.id
|
||||
LEFT OUTER JOIN
|
||||
teams t
|
||||
ON
|
||||
@ -1846,6 +1978,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
|
||||
MDMID: hostWithMDM.MDMID,
|
||||
Name: *hostWithMDM.Name,
|
||||
}
|
||||
|
||||
host.MDM = fleet.MDMHostData{
|
||||
EncryptionKeyAvailable: *hostWithMDM.EncryptionKeyAvailable,
|
||||
}
|
||||
}
|
||||
return &host, nil
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
@ -3013,19 +3149,30 @@ func (ds *Datastore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID
|
||||
)
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string) error {
|
||||
func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error {
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, `
|
||||
INSERT INTO host_disk_encryption_keys (host_id, base64_encrypted)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
/* if the key has changed, NULLify this value so it can be calculated again */
|
||||
decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, NULL),
|
||||
base64_encrypted = VALUES(base64_encrypted)
|
||||
`, hostID, encryptedBase64Key)
|
||||
INSERT INTO host_disk_encryption_keys
|
||||
(host_id, base64_encrypted, client_error, decryptable)
|
||||
VALUES
|
||||
(?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
/* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */
|
||||
decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, VALUES(decryptable)),
|
||||
base64_encrypted = VALUES(base64_encrypted),
|
||||
client_error = VALUES(client_error)
|
||||
`, hostID, encryptedBase64Key, clientError, decryptable)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
|
||||
// NOTE(mna): currently we only verify encryption keys for macOS,
|
||||
// Windows/bitlocker uses a different approach where orbit sends the
|
||||
// encryption key and we encrypt it server-side with the WSTEP certificate,
|
||||
// so it is always decryptable once received.
|
||||
//
|
||||
// To avoid sending Windows-related keys to verify as part of this call, we
|
||||
// only return rows that have a non-empty encryption key (for Windows, the
|
||||
// key is blanked if an error occurred trying to retrieve it on the host).
|
||||
var keys []fleet.HostDiskEncryptionKey
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, `
|
||||
SELECT
|
||||
@ -3035,7 +3182,8 @@ func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fle
|
||||
FROM
|
||||
host_disk_encryption_keys
|
||||
WHERE
|
||||
decryptable IS NULL
|
||||
decryptable IS NULL AND
|
||||
base64_encrypted != ''
|
||||
`)
|
||||
return keys, err
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
@ -111,7 +112,7 @@ func TestHosts(t *testing.T) {
|
||||
{"HostsListBySoftwareChangedAt", testHostsListBySoftwareChangedAt},
|
||||
{"HostsListByOperatingSystemID", testHostsListByOperatingSystemID},
|
||||
{"HostsListByOSNameAndVersion", testHostsListByOSNameAndVersion},
|
||||
{"HostsListByDiskEncryptionStatus", testHostsListDiskEncryptionStatus},
|
||||
{"HostsListByDiskEncryptionStatus", testHostsListMacOSSettingsDiskEncryptionStatus},
|
||||
{"HostsListFailingPolicies", printReadsInTest(testHostsListFailingPolicies)},
|
||||
{"HostsExpiration", testHostsExpiration},
|
||||
{"HostsAllPackStats", testHostsAllPackStats},
|
||||
@ -722,8 +723,13 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
|
||||
|
||||
var hosts []*fleet.Host
|
||||
for i := 0; i < 10; i++ {
|
||||
var opts []test.NewHostOption
|
||||
switch i {
|
||||
case 5, 6:
|
||||
opts = append(opts, test.WithPlatform("windows"))
|
||||
}
|
||||
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
|
||||
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now())
|
||||
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...)
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
userFilter := fleet.TeamFilter{User: test.UserAdmin}
|
||||
@ -763,12 +769,12 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
|
||||
Checksum: []byte("csum"),
|
||||
},
|
||||
}))
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
|
||||
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
@ -781,12 +787,39 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
|
||||
Checksum: []byte("csum"),
|
||||
},
|
||||
}))
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
|
||||
// test team filter in combination with os settings filter
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerifying}, 1)
|
||||
|
||||
// test team filter in combination with os settings disk encryptionfilter
|
||||
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
ProfileID: 1,
|
||||
ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier,
|
||||
HostUUID: hosts[8].UUID, // hosts[8] is assgined to no team
|
||||
CommandUUID: "command-uuid-3",
|
||||
OperationType: fleet.MDMAppleOperationTypeInstall,
|
||||
Status: &fleet.MDMAppleDeliveryPending,
|
||||
Checksum: []byte("disk-encryption-csum"),
|
||||
},
|
||||
}))
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // wrong team
|
||||
// os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8]
|
||||
}
|
||||
|
||||
func testHostsListFilterAdditional(t *testing.T, ds *Datastore) {
|
||||
@ -2920,7 +2953,7 @@ func testHostsListByOSNameAndVersion(t *testing.T, ds *Datastore) {
|
||||
}
|
||||
}
|
||||
|
||||
func testHostsListDiskEncryptionStatus(t *testing.T, ds *Datastore) {
|
||||
func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
// seed hosts
|
||||
@ -5740,7 +5773,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
|
||||
err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0")
|
||||
require.NoError(t, err)
|
||||
// set an encryption key
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
// set an mdm profile
|
||||
prof, err := ds.NewMDMAppleConfigProfile(context.Background(), *configProfileForTest(t, "N1", "I1", "U1"))
|
||||
@ -6586,23 +6619,26 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, hFleet.ID, loadFleet.ID)
|
||||
require.False(t, loadFleet.MDMInfo.IsServer)
|
||||
|
||||
// fill in disk encryption information
|
||||
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true))
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hFleet.ID, "test-key", "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hFleet.ID}, true, time.Now())
|
||||
require.NoError(t, err)
|
||||
loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
|
||||
require.NoError(t, err)
|
||||
require.True(t, loadFleet.MDM.EncryptionKeyAvailable)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadFleet.DiskEncryptionEnabled)
|
||||
require.True(t, *loadFleet.DiskEncryptionEnabled)
|
||||
}
|
||||
|
||||
func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected *bool) {
|
||||
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
||||
var actual *bool
|
||||
|
||||
row := tx.QueryRowxContext(
|
||||
context.Background(),
|
||||
"SELECT decryptable FROM host_disk_encryption_keys WHERE host_id = ?",
|
||||
hostID,
|
||||
)
|
||||
|
||||
err := row.Scan(&actual)
|
||||
func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedDecryptable *bool) {
|
||||
got, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual)
|
||||
return nil
|
||||
})
|
||||
require.Equal(t, expectedKey, got.Base64Encrypted)
|
||||
require.Equal(t, expectedDecryptable, got.Decryptable)
|
||||
}
|
||||
|
||||
func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
|
||||
@ -6632,49 +6668,81 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
|
||||
PrimaryMac: "30-65-EC-6F-C4-59",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA")
|
||||
host3, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String("3"),
|
||||
UUID: "3",
|
||||
OsqueryHostID: ptr.String("3"),
|
||||
Hostname: "foo.local3",
|
||||
PrimaryIP: "192.168.1.3",
|
||||
PrimaryMac: "30-65-EC-6F-C4-60",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkEncryptionKey := func(hostID uint, expected string) {
|
||||
actual, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB", "", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual.Base64Encrypted)
|
||||
}
|
||||
|
||||
h, err := ds.Host(context.Background(), host.ID)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKey(h.ID, "AAA")
|
||||
checkEncryptionKeyStatus(t, ds, h.ID, "AAA", nil)
|
||||
|
||||
h, err = ds.Host(context.Background(), host2.ID)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKey(h.ID, "BBB")
|
||||
checkEncryptionKeyStatus(t, ds, h.ID, "BBB", nil)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err = ds.Host(context.Background(), host2.ID)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKey(h.ID, "CCC")
|
||||
checkEncryptionKeyStatus(t, ds, h.ID, "CCC", nil)
|
||||
|
||||
// setting the encryption key to an existing value doesn't change its
|
||||
// encryption status
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(context.Background(), []uint{host.ID}, true, time.Now().Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true))
|
||||
|
||||
// same key doesn't change encryption status
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true))
|
||||
|
||||
// different key resets encryption status
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY", "", nil)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "XZY", nil)
|
||||
|
||||
// set the key with an initial decrypted status of true
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(true))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true))
|
||||
|
||||
// same key, provided decrypted status is ignored (stored one is kept)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(false))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true))
|
||||
|
||||
// client error, key is removed and decrypted status is nulled
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "fail", nil)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "", nil)
|
||||
|
||||
// new key, provided decrypted status is applied
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "def", "", ptr.Bool(true))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "def", ptr.Bool(true))
|
||||
|
||||
// different key, provided decrypted status is applied
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "ghi", "", ptr.Bool(false))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "ghi", ptr.Bool(false))
|
||||
}
|
||||
|
||||
func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
||||
@ -6692,7 +6760,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
||||
PrimaryMac: "30-65-EC-6F-C4-58",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
host2, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
@ -6709,7 +6777,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
threshold := time.Now().Add(time.Hour)
|
||||
@ -6717,31 +6785,31 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
||||
// empty set
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{}, false, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
|
||||
|
||||
// keys that changed after the provided threshold are not updated
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold.Add(-24*time.Hour))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
|
||||
|
||||
// single host
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
|
||||
|
||||
// multiple hosts
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(true))
|
||||
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(false))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(false))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(false))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(false))
|
||||
}
|
||||
|
||||
func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
|
||||
@ -6773,9 +6841,9 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, err := ds.GetUnverifiedDiskEncryptionKeys(ctx)
|
||||
@ -6794,6 +6862,17 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
|
||||
keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
require.Equal(t, host2.ID, keys[0].HostID)
|
||||
|
||||
// update key of host 1 to empty with a client error, should not be reported
|
||||
// by GetUnverifiedDiskEncryptionKeys
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "failed", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
require.Equal(t, host2.ID, keys[0].HostID)
|
||||
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold)
|
||||
require.NoError(t, err)
|
||||
@ -6992,7 +7071,7 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, -1, *got.MDM.TestGetRawDecryptable())
|
||||
|
||||
// create the encryption key row, but unknown decryptable
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err = ds.Host(ctx, host.ID)
|
||||
|
@ -552,10 +552,13 @@ func (ds *Datastore) ListHostsInLabel(ctx context.Context, filter fleet.TeamFilt
|
||||
|
||||
query := fmt.Sprintf(queryFmt, hostMDMSelect, failingPoliciesSelect, deviceMappingSelect, hostMDMJoin, failingPoliciesJoin, deviceMappingJoin)
|
||||
|
||||
query, params := ds.applyHostLabelFilters(filter, lid, query, opt)
|
||||
query, params, err := ds.applyHostLabelFilters(ctx, filter, lid, query, opt)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "applying label query filters")
|
||||
}
|
||||
|
||||
hosts := []*fleet.Host{}
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, query, params...)
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &hosts, query, params...)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "selecting label query executions")
|
||||
}
|
||||
@ -563,7 +566,7 @@ func (ds *Datastore) ListHostsInLabel(ctx context.Context, filter fleet.TeamFilt
|
||||
}
|
||||
|
||||
// NOTE: the hosts table must be aliased to `h` in the query passed to this function.
|
||||
func (ds *Datastore) applyHostLabelFilters(filter fleet.TeamFilter, lid uint, query string, opt fleet.HostListOptions) (string, []interface{}) {
|
||||
func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.TeamFilter, lid uint, query string, opt fleet.HostListOptions) (string, []interface{}, error) {
|
||||
params := []interface{}{lid}
|
||||
|
||||
if opt.ListOptions.OrderKey == "display_name" {
|
||||
@ -582,26 +585,33 @@ func (ds *Datastore) applyHostLabelFilters(filter fleet.TeamFilter, lid uint, qu
|
||||
query, params = filterHostsByMacOSSettingsStatus(query, opt, params)
|
||||
query, params = filterHostsByMacOSDiskEncryptionStatus(query, opt, params)
|
||||
query, params = filterHostsByMDMBootstrapPackageStatus(query, opt, params)
|
||||
if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
|
||||
return "", nil, err
|
||||
} else if opt.OSSettingsFilter.IsValid() {
|
||||
query, params = ds.filterHostsByOSSettingsStatus(query, opt, params, enableDiskEncryption)
|
||||
} else if opt.OSSettingsDiskEncryptionFilter.IsValid() {
|
||||
query, params = ds.filterHostsByOSSettingsDiskEncryptionStatus(query, opt, params, enableDiskEncryption)
|
||||
}
|
||||
query, params = searchLike(query, params, opt.MatchQuery, hostSearchColumns...)
|
||||
|
||||
query, params = appendListOptionsWithCursorToSQL(query, params, &opt.ListOptions)
|
||||
return query, params
|
||||
return query, params, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) CountHostsInLabel(ctx context.Context, filter fleet.TeamFilter, lid uint, opt fleet.HostListOptions) (int, error) {
|
||||
query := `SELECT count(*) FROM label_membership lm
|
||||
JOIN hosts h ON (lm.host_id = h.id)
|
||||
LEFT JOIN host_seen_times hst ON (h.id=hst.host_id)
|
||||
LEFT JOIN host_disks hd ON (h.id=hd.host_id)
|
||||
`
|
||||
|
||||
query += hostMDMJoin
|
||||
|
||||
if opt.LowDiskSpaceFilter != nil {
|
||||
query += ` LEFT JOIN host_disks hd ON (h.id=hd.host_id) `
|
||||
query, params, err := ds.applyHostLabelFilters(ctx, filter, lid, query, opt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
query, params := ds.applyHostLabelFilters(filter, lid, query, opt)
|
||||
|
||||
var count int
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, query, params...); err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, err, "count hosts")
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
@ -66,6 +67,7 @@ func TestLabels(t *testing.T) {
|
||||
{"ListHostsInLabelFailingPolicies", testListHostsInLabelFailingPolicies},
|
||||
{"ListHostsInLabelDiskEncryptionStatus", testListHostsInLabelDiskEncryptionStatus},
|
||||
{"HostMemberOfAllLabels", testHostMemberOfAllLabels},
|
||||
{"ListHostsInLabelOSSettings", testLabelsListHostsInLabelOSSettings},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@ -497,12 +499,12 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
|
||||
Checksum: []byte("csum"),
|
||||
},
|
||||
}))
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h1
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h1
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
|
||||
require.NoError(t, db.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
@ -515,12 +517,12 @@ func testLabelsListHostsInLabelAndTeamFilter(deferred bool, t *testing.T, db *Da
|
||||
Checksum: []byte("csum"),
|
||||
},
|
||||
}))
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h1
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h1
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // h2
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2
|
||||
listHostsInLabelCheckCount(t, db, userFilter, l1.ID, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // h2
|
||||
}
|
||||
|
||||
func testLabelsBuiltIn(t *testing.T, db *Datastore) {
|
||||
@ -1329,3 +1331,97 @@ func testHostMemberOfAllLabels(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) {
|
||||
h1, err := db.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("1"),
|
||||
NodeKey: ptr.String("1"),
|
||||
UUID: "1",
|
||||
Hostname: "foo.local",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
h2, err := db.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("2"),
|
||||
NodeKey: ptr.String("2"),
|
||||
UUID: "2",
|
||||
Hostname: "bar.local",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
h3, err := db.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("3"),
|
||||
NodeKey: ptr.String("3"),
|
||||
UUID: "3",
|
||||
Hostname: "baz.local",
|
||||
Platform: "centos",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
l1 := &fleet.LabelSpec{
|
||||
ID: 1,
|
||||
Name: "label foo",
|
||||
Query: "query1",
|
||||
}
|
||||
err = db.ApplyLabelSpecs(context.Background(), []*fleet.LabelSpec{l1})
|
||||
require.Nil(t, err)
|
||||
|
||||
filter := fleet.TeamFilter{User: test.UserAdmin}
|
||||
// add all hosts to label
|
||||
for _, h := range []*fleet.Host{h1, h2, h3} {
|
||||
require.NoError(t, db.RecordLabelQueryExecutions(context.Background(), h, map[uint]*bool{l1.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
}
|
||||
|
||||
// turn on disk encryption
|
||||
ac, err := db.AppConfig(context.Background())
|
||||
require.NoError(t, err)
|
||||
ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
|
||||
require.NoError(t, db.SaveAppConfig(context.Background(), ac))
|
||||
|
||||
// add two hosts to MDM to enforce disk encryption, fleet doesn't enforce settings on centos so h3 is not included
|
||||
for _, h := range []*fleet.Host{h1, h2} {
|
||||
require.NoError(t, db.SetOrUpdateMDMData(context.Background(), h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet))
|
||||
}
|
||||
// add disk encryption key for h1
|
||||
require.NoError(t, db.SetOrUpdateHostDiskEncryptionKey(context.Background(), h1.ID, "test-key", "", ptr.Bool(true)))
|
||||
// add disk encryption for h1
|
||||
require.NoError(t, db.SetOrUpdateHostDisksEncryption(context.Background(), h1.ID, true))
|
||||
|
||||
checkHosts := func(t *testing.T, gotHosts []*fleet.Host, expectedIDs []uint) {
|
||||
require.Len(t, gotHosts, len(expectedIDs))
|
||||
for _, h := range gotHosts {
|
||||
require.Contains(t, expectedIDs, h.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// baseline no filter
|
||||
hosts := listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{}, 3)
|
||||
checkHosts(t, hosts, []uint{h1.ID, h2.ID, h3.ID})
|
||||
|
||||
t.Run("os_settings", func(t *testing.T) {
|
||||
hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1)
|
||||
checkHosts(t, hosts, []uint{h1.ID})
|
||||
hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1)
|
||||
checkHosts(t, hosts, []uint{h2.ID})
|
||||
})
|
||||
|
||||
t.Run("os_settings_disk_encryption", func(t *testing.T) {
|
||||
hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerified}, 1)
|
||||
checkHosts(t, hosts, []uint{h1.ID})
|
||||
hosts = listHostsInLabelCheckCount(t, db, filter, l1.ID, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsPending}, 1)
|
||||
checkHosts(t, hosts, []uint{h2.ID})
|
||||
})
|
||||
}
|
||||
|
@ -3,13 +3,16 @@ package mysql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/go-kit/kit/log/level"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// MDMWindowsGetEnrolledDevice receives a Windows MDM device id and returns the device information.
|
||||
// MDMWindowsGetEnrolledDevice receives a Windows MDM HW Device id and returns the device information.
|
||||
func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceHWID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
stmt := `SELECT
|
||||
mdm_device_id,
|
||||
@ -36,6 +39,33 @@ func (ds *Datastore) MDMWindowsGetEnrolledDevice(ctx context.Context, mdmDeviceH
|
||||
return &winMDMDevice, nil
|
||||
}
|
||||
|
||||
// MDMWindowsGetEnrolledDeviceWithDeviceID receives a Windows MDM device id and returns the device information.
|
||||
func (ds *Datastore) MDMWindowsGetEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
stmt := `SELECT
|
||||
mdm_device_id,
|
||||
mdm_hardware_id,
|
||||
device_state,
|
||||
device_type,
|
||||
device_name,
|
||||
enroll_type,
|
||||
enroll_user_id,
|
||||
enroll_proto_version,
|
||||
enroll_client_version,
|
||||
not_in_oobe,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM mdm_windows_enrollments WHERE mdm_device_id = ?`
|
||||
|
||||
var winMDMDevice fleet.MDMWindowsEnrolledDevice
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &winMDMDevice, stmt, mdmDeviceID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ctxerr.Wrap(ctx, notFound("MDMWindowsGetEnrolledDeviceWithDeviceID").WithMessage(mdmDeviceID))
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get MDMWindowsGetEnrolledDeviceWithDeviceID")
|
||||
}
|
||||
return &winMDMDevice, nil
|
||||
}
|
||||
|
||||
// MDMWindowsInsertEnrolledDevice inserts a new MDMWindowsEnrolledDevice in the database
|
||||
func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device *fleet.MDMWindowsEnrolledDevice) error {
|
||||
stmt := `
|
||||
@ -74,7 +104,8 @@ func (ds *Datastore) MDMWindowsInsertEnrolledDevice(ctx context.Context, device
|
||||
return nil
|
||||
}
|
||||
|
||||
// MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database using the device id.
|
||||
// MDMWindowsDeleteEnrolledDevice deletes a give MDMWindowsEnrolledDevice entry from the database
|
||||
// using the HW Device ID.
|
||||
func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDeviceHWID string) error {
|
||||
stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_hardware_id = ?"
|
||||
|
||||
@ -90,3 +121,202 @@ func (ds *Datastore) MDMWindowsDeleteEnrolledDevice(ctx context.Context, mdmDevi
|
||||
|
||||
return ctxerr.Wrap(ctx, notFound("MDMWindowsEnrolledDevice"))
|
||||
}
|
||||
|
||||
// MDMWindowsDeleteEnrolledDeviceWithDeviceID deletes a give MDMWindowsEnrolledDevice entry from the database using the device id.
|
||||
func (ds *Datastore) MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx context.Context, mdmDeviceID string) error {
|
||||
stmt := "DELETE FROM mdm_windows_enrollments WHERE mdm_device_id = ?"
|
||||
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, mdmDeviceID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete MDMWindowsDeleteEnrolledDeviceWithDeviceID")
|
||||
}
|
||||
|
||||
deleted, _ := res.RowsAffected()
|
||||
if deleted == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ctxerr.Wrap(ctx, notFound("MDMWindowsDeleteEnrolledDeviceWithDeviceID"))
|
||||
}
|
||||
|
||||
// whereBitLockerStatus returns a string suitable for inclusion within a SQL WHERE clause to filter by
|
||||
// the given status. The caller is responsible for ensuring the status is valid. In the case of an invalid
|
||||
// status, the function will return the string "FALSE". The caller should also ensure that the query in
|
||||
// which this is used joins the following tables with the specified aliases:
|
||||
// - host_disk_encryption_keys: hdek
|
||||
// - host_mdm: hmdm
|
||||
// - host_disks: hd
|
||||
func (ds *Datastore) whereBitLockerStatus(status fleet.DiskEncryptionStatus) string {
|
||||
const (
|
||||
whereNotServer = `(hmdm.is_server IS NOT NULL AND hmdm.is_server = 0)`
|
||||
whereKeyAvailable = `(hdek.base64_encrypted IS NOT NULL AND hdek.base64_encrypted != '' AND hdek.decryptable IS NOT NULL AND hdek.decryptable = 1)`
|
||||
whereEncrypted = `(hd.encrypted IS NOT NULL AND hd.encrypted = 1)`
|
||||
whereHostDisksUpdated = `(hd.updated_at IS NOT NULL AND hdek.updated_at IS NOT NULL AND hd.updated_at >= hdek.updated_at)`
|
||||
whereClientError = `(hdek.client_error IS NOT NULL AND hdek.client_error != '')`
|
||||
withinGracePeriod = `(hdek.updated_at IS NOT NULL AND hdek.updated_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR))`
|
||||
)
|
||||
|
||||
// TODO: what if windows sends us a key for an already encrypted volumne? could it get stuck
|
||||
// in pending or verifying? should we modify SetOrUpdateHostDiskEncryption to ensure that we
|
||||
// increment the updated_at timestamp on the host_disks table for all encrypted volumes
|
||||
// host_disks if the hdek timestamp is newer? What about SetOrUpdateHostDiskEncryptionKey?
|
||||
|
||||
switch status {
|
||||
case fleet.DiskEncryptionVerified:
|
||||
return whereNotServer + `
|
||||
AND NOT ` + whereClientError + `
|
||||
AND ` + whereKeyAvailable + `
|
||||
AND ` + whereEncrypted + `
|
||||
AND ` + whereHostDisksUpdated
|
||||
|
||||
case fleet.DiskEncryptionVerifying:
|
||||
// Possible verifying scenarios:
|
||||
// - we have the key and host_disks already encrypted before the key but hasn't been updated yet
|
||||
// - we have the key and host_disks reported unencrypted during the 1-hour grace period after key was updated
|
||||
return whereNotServer + `
|
||||
AND NOT ` + whereClientError + `
|
||||
AND ` + whereKeyAvailable + `
|
||||
AND (
|
||||
(` + whereEncrypted + ` AND NOT ` + whereHostDisksUpdated + `)
|
||||
OR (NOT ` + whereEncrypted + ` AND ` + whereHostDisksUpdated + ` AND ` + withinGracePeriod + `)
|
||||
)`
|
||||
|
||||
case fleet.DiskEncryptionEnforcing:
|
||||
// Possible enforcing scenarios:
|
||||
// - we don't have the key
|
||||
// - we have the key and host_disks reported unencrypted before the key was updated or outside the 1-hour grace period after key was updated
|
||||
return whereNotServer + `
|
||||
AND NOT ` + whereClientError + `
|
||||
AND (
|
||||
NOT ` + whereKeyAvailable + `
|
||||
OR (` + whereKeyAvailable + `
|
||||
AND (NOT ` + whereEncrypted + `
|
||||
AND (NOT ` + whereHostDisksUpdated + ` OR NOT ` + withinGracePeriod + `)
|
||||
)
|
||||
)
|
||||
)`
|
||||
|
||||
case fleet.DiskEncryptionFailed:
|
||||
return whereNotServer + ` AND ` + whereClientError
|
||||
|
||||
default:
|
||||
level.Debug(ds.logger).Log("msg", "unknown bitlocker status", "status", status)
|
||||
return "FALSE"
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) {
|
||||
enabled, err := ds.getConfigEnableDiskEncryption(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !enabled {
|
||||
return &fleet.MDMWindowsBitLockerSummary{}, nil
|
||||
}
|
||||
|
||||
// Note action_required and removing_enforcement are not applicable to Windows hosts
|
||||
sqlFmt := `
|
||||
SELECT
|
||||
COUNT(if((%s), 1, NULL)) AS verified,
|
||||
COUNT(if((%s), 1, NULL)) AS verifying,
|
||||
0 AS action_required,
|
||||
COUNT(if((%s), 1, NULL)) AS enforcing,
|
||||
COUNT(if((%s), 1, NULL)) AS failed,
|
||||
0 AS removing_enforcement
|
||||
FROM
|
||||
hosts h
|
||||
LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id
|
||||
LEFT JOIN host_mdm hmdm ON h.id = hmdm.host_id
|
||||
LEFT JOIN host_disks hd ON h.id = hd.host_id
|
||||
WHERE
|
||||
h.platform = 'windows' AND hmdm.is_server = 0 AND %s`
|
||||
|
||||
var args []interface{}
|
||||
teamFilter := "h.team_id IS NULL"
|
||||
if teamID != nil && *teamID > 0 {
|
||||
teamFilter = "h.team_id = ?"
|
||||
args = append(args, *teamID)
|
||||
}
|
||||
|
||||
var res fleet.MDMWindowsBitLockerSummary
|
||||
stmt := fmt.Sprintf(
|
||||
sqlFmt,
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionVerified),
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying),
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing),
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionFailed),
|
||||
teamFilter,
|
||||
)
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fleet.Host) (*fleet.DiskEncryptionStatus, error) {
|
||||
if host == nil {
|
||||
return nil, errors.New("host cannot be nil")
|
||||
}
|
||||
|
||||
if host.Platform != "windows" {
|
||||
// Generally, the caller should have already checked this, but just in case we log and
|
||||
// return nil
|
||||
level.Debug(ds.logger).Log("msg", "cannot get bitlocker status for non-windows host", "host_id", host.ID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if host.MDMInfo != nil && host.MDMInfo.IsServer {
|
||||
// It is currently expected that server hosts do not have a bitlocker status so we can skip
|
||||
// the query and return nil. We log for potential debugging in case this changes in the future.
|
||||
level.Debug(ds.logger).Log("msg", "no bitlocker status for server host", "host_id", host.ID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
enabled, err := ds.getConfigEnableDiskEncryption(ctx, host.TeamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Note action_required and removing_enforcement are not applicable to Windows hosts
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
CASE
|
||||
WHEN (%s) THEN '%s'
|
||||
WHEN (%s) THEN '%s'
|
||||
WHEN (%s) THEN '%s'
|
||||
WHEN (%s) THEN '%s'
|
||||
END AS status
|
||||
FROM
|
||||
host_mdm hmdm
|
||||
LEFT JOIN host_disk_encryption_keys hdek ON hmdm.host_id = hdek.host_id
|
||||
LEFT JOIN host_disks hd ON hmdm.host_id = hd.host_id
|
||||
WHERE
|
||||
hmdm.host_id = ?`,
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionVerified),
|
||||
fleet.DiskEncryptionVerified,
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionVerifying),
|
||||
fleet.DiskEncryptionVerifying,
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionEnforcing),
|
||||
fleet.DiskEncryptionEnforcing,
|
||||
ds.whereBitLockerStatus(fleet.DiskEncryptionFailed),
|
||||
fleet.DiskEncryptionFailed,
|
||||
)
|
||||
|
||||
var des fleet.DiskEncryptionStatus
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &des, stmt, host.ID); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// At this point we know disk encryption is enabled so if we don't have a record for the
|
||||
// host then we treat it as enforcing and log for potential debugging
|
||||
level.Debug(ds.logger).Log("msg", "no bitlocker status found for host", "host_id", host.ID)
|
||||
des = fleet.DiskEncryptionEnforcing
|
||||
return &des, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &des, nil
|
||||
}
|
||||
|
@ -3,9 +3,15 @@ package mysql
|
||||
import (
|
||||
"context" // nolint:gosec // used only to hash for efficient comparisons
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -66,4 +72,387 @@ func testMDMWindowsEnrolledDevice(t *testing.T, ds *Datastore) {
|
||||
|
||||
err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
|
||||
// Test using device ID instead of hardware ID
|
||||
err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.MDMWindowsInsertEnrolledDevice(ctx, enrolledDevice)
|
||||
require.ErrorAs(t, err, &ae)
|
||||
|
||||
gotEnrolledDevice, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, gotEnrolledDevice.CreatedAt)
|
||||
require.Equal(t, enrolledDevice.MDMDeviceID, gotEnrolledDevice.MDMDeviceID)
|
||||
require.Equal(t, enrolledDevice.MDMHardwareID, gotEnrolledDevice.MDMHardwareID)
|
||||
|
||||
err = ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, enrolledDevice.MDMDeviceID)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
|
||||
err = ds.MDMWindowsDeleteEnrolledDevice(ctx, enrolledDevice.MDMHardwareID)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
}
|
||||
|
||||
func TestMDMWindowsDiskEncryption(t *testing.T) {
|
||||
ds := CreateMySQLDS(t)
|
||||
ctx := context.Background()
|
||||
|
||||
checkBitLockerSummary := func(t *testing.T, teamID *uint, expected fleet.MDMWindowsBitLockerSummary) {
|
||||
bls, err := ds.GetMDMWindowsBitLockerSummary(ctx, teamID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, bls)
|
||||
require.Equal(t, expected, *bls)
|
||||
}
|
||||
|
||||
checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) {
|
||||
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, gotHosts, len(expectedIDs))
|
||||
for _, h := range gotHosts {
|
||||
require.Contains(t, expectedIDs, h.ID)
|
||||
}
|
||||
}
|
||||
|
||||
checkListHostsFilterDiskEncryption := func(t *testing.T, teamID *uint, status fleet.DiskEncryptionStatus, expectedIDs []uint) {
|
||||
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsDiskEncryptionFilter: status})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, gotHosts, len(expectedIDs), "status: %s", status)
|
||||
for _, h := range gotHosts {
|
||||
require.Contains(t, expectedIDs, h.ID)
|
||||
}
|
||||
}
|
||||
|
||||
checkHostBitLockerStatus := func(t *testing.T, expected fleet.DiskEncryptionStatus, hostIDs []uint) {
|
||||
for _, id := range hostIDs {
|
||||
h, err := ds.Host(ctx, id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, h)
|
||||
bls, err := ds.GetMDMWindowsBitLockerStatus(ctx, h)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, bls)
|
||||
require.Equal(t, expected, *bls)
|
||||
}
|
||||
}
|
||||
|
||||
type hostIDsByStatus map[fleet.DiskEncryptionStatus][]uint
|
||||
|
||||
checkExpected := func(t *testing.T, teamID *uint, expected hostIDsByStatus) {
|
||||
for _, status := range []fleet.DiskEncryptionStatus{
|
||||
fleet.DiskEncryptionVerified,
|
||||
fleet.DiskEncryptionVerifying,
|
||||
fleet.DiskEncryptionFailed,
|
||||
fleet.DiskEncryptionEnforcing,
|
||||
fleet.DiskEncryptionRemovingEnforcement,
|
||||
fleet.DiskEncryptionActionRequired,
|
||||
} {
|
||||
hostIDs, ok := expected[status]
|
||||
if !ok {
|
||||
hostIDs = []uint{}
|
||||
}
|
||||
checkListHostsFilterDiskEncryption(t, teamID, status, hostIDs)
|
||||
checkHostBitLockerStatus(t, status, hostIDs)
|
||||
}
|
||||
|
||||
checkBitLockerSummary(t, teamID, fleet.MDMWindowsBitLockerSummary{
|
||||
Verified: uint(len(expected[fleet.DiskEncryptionVerified])),
|
||||
Verifying: uint(len(expected[fleet.DiskEncryptionVerifying])),
|
||||
Failed: uint(len(expected[fleet.DiskEncryptionFailed])),
|
||||
Enforcing: uint(len(expected[fleet.DiskEncryptionEnforcing])),
|
||||
RemovingEnforcement: uint(len(expected[fleet.DiskEncryptionRemovingEnforcement])),
|
||||
ActionRequired: uint(len(expected[fleet.DiskEncryptionActionRequired])),
|
||||
})
|
||||
|
||||
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, expected[fleet.DiskEncryptionVerified])
|
||||
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, expected[fleet.DiskEncryptionVerifying])
|
||||
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, expected[fleet.DiskEncryptionFailed])
|
||||
var expectedPending []uint
|
||||
expectedPending = append(expectedPending, expected[fleet.DiskEncryptionEnforcing]...)
|
||||
expectedPending = append(expectedPending, expected[fleet.DiskEncryptionRemovingEnforcement]...)
|
||||
expectedPending = append(expectedPending, expected[fleet.DiskEncryptionActionRequired]...)
|
||||
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, expectedPending)
|
||||
}
|
||||
|
||||
updateHostDisks := func(t *testing.T, hostID uint, encrypted bool, updated_at time.Time) {
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `UPDATE host_disks SET encrypted = ?, updated_at = ? where host_id = ?`
|
||||
_, err := q.ExecContext(ctx, stmt, encrypted, updated_at, hostID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
setKeyUpdatedAt := func(t *testing.T, hostID uint, keyUpdatedAt time.Time) {
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
stmt := `UPDATE host_disk_encryption_keys SET updated_at = ? where host_id = ?`
|
||||
_, err := q.ExecContext(ctx, stmt, keyUpdatedAt, hostID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Create some hosts
|
||||
var hosts []*fleet.Host
|
||||
for i := 0; i < 10; i++ {
|
||||
p := "windows"
|
||||
if i >= 5 {
|
||||
p = "darwin"
|
||||
}
|
||||
u := uuid.New().String()
|
||||
h, err := ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: &u,
|
||||
UUID: u,
|
||||
Hostname: u,
|
||||
Platform: p,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, h)
|
||||
hosts = append(hosts, h)
|
||||
|
||||
require.NoError(t, ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "https://example.com", false, fleet.WellKnownMDMFleet))
|
||||
}
|
||||
|
||||
t.Run("Disk encryption disabled", func(t *testing.T) {
|
||||
ac, err := ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ac.MDM.EnableDiskEncryption.Value)
|
||||
|
||||
checkExpected(t, nil, hostIDsByStatus{}) // no hosts are counted because disk encryption is not enabled
|
||||
})
|
||||
|
||||
t.Run("Disk encryption enabled", func(t *testing.T) {
|
||||
ac, err := ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
|
||||
require.NoError(t, ds.SaveAppConfig(ctx, ac))
|
||||
ac, err = ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ac.MDM.EnableDiskEncryption.Value)
|
||||
|
||||
t.Run("Bitlocker enforcing status", func(t *testing.T) {
|
||||
// all windows hosts are counted as enforcing because they have not reported any disk encryption status yet
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
|
||||
})
|
||||
|
||||
require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true)))
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
// status is still pending because hosts_disks hasn't been updated yet
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
|
||||
})
|
||||
|
||||
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
hostDisksEncrypted bool
|
||||
reportedAfterKey bool
|
||||
expectedWithinGracePeriod fleet.DiskEncryptionStatus
|
||||
expectedOutsideGracePeriod fleet.DiskEncryptionStatus
|
||||
}{
|
||||
{
|
||||
name: "encrypted reported after key",
|
||||
hostDisksEncrypted: true,
|
||||
reportedAfterKey: true,
|
||||
expectedWithinGracePeriod: fleet.DiskEncryptionVerified,
|
||||
expectedOutsideGracePeriod: fleet.DiskEncryptionVerified,
|
||||
},
|
||||
{
|
||||
name: "encrypted reported before key",
|
||||
hostDisksEncrypted: true,
|
||||
reportedAfterKey: false,
|
||||
expectedWithinGracePeriod: fleet.DiskEncryptionVerifying,
|
||||
expectedOutsideGracePeriod: fleet.DiskEncryptionVerifying,
|
||||
},
|
||||
{
|
||||
name: "not encrypted reported before key",
|
||||
hostDisksEncrypted: false,
|
||||
reportedAfterKey: false,
|
||||
expectedWithinGracePeriod: fleet.DiskEncryptionEnforcing,
|
||||
expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing,
|
||||
},
|
||||
{
|
||||
name: "not encrypted reported after key",
|
||||
hostDisksEncrypted: false,
|
||||
reportedAfterKey: true,
|
||||
expectedWithinGracePeriod: fleet.DiskEncryptionVerifying,
|
||||
expectedOutsideGracePeriod: fleet.DiskEncryptionEnforcing,
|
||||
},
|
||||
}
|
||||
|
||||
testHostID := hosts[0].ID
|
||||
otherWindowsHostIDs := []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var keyUpdatedAt, hostDisksUpdatedAt time.Time
|
||||
|
||||
t.Run("within grace period", func(t *testing.T) {
|
||||
expected := make(hostIDsByStatus)
|
||||
if c.expectedWithinGracePeriod == fleet.DiskEncryptionEnforcing {
|
||||
expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...)
|
||||
} else {
|
||||
expected[c.expectedWithinGracePeriod] = []uint{testHostID}
|
||||
expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs
|
||||
}
|
||||
|
||||
keyUpdatedAt = time.Now().Add(-10 * time.Minute)
|
||||
setKeyUpdatedAt(t, testHostID, keyUpdatedAt)
|
||||
|
||||
if c.reportedAfterKey {
|
||||
hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute)
|
||||
} else {
|
||||
hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute)
|
||||
}
|
||||
updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt)
|
||||
|
||||
checkExpected(t, nil, expected)
|
||||
})
|
||||
|
||||
t.Run("outside grace period", func(t *testing.T) {
|
||||
expected := make(hostIDsByStatus)
|
||||
if c.expectedOutsideGracePeriod == fleet.DiskEncryptionEnforcing {
|
||||
expected[fleet.DiskEncryptionEnforcing] = append([]uint{testHostID}, otherWindowsHostIDs...)
|
||||
} else {
|
||||
expected[c.expectedOutsideGracePeriod] = []uint{testHostID}
|
||||
expected[fleet.DiskEncryptionEnforcing] = otherWindowsHostIDs
|
||||
}
|
||||
|
||||
keyUpdatedAt = time.Now().Add(-2 * time.Hour)
|
||||
setKeyUpdatedAt(t, testHostID, keyUpdatedAt)
|
||||
|
||||
if c.reportedAfterKey {
|
||||
hostDisksUpdatedAt = keyUpdatedAt.Add(5 * time.Minute)
|
||||
} else {
|
||||
hostDisksUpdatedAt = keyUpdatedAt.Add(-5 * time.Minute)
|
||||
}
|
||||
updateHostDisks(t, testHostID, c.hostDisksEncrypted, hostDisksUpdatedAt)
|
||||
|
||||
checkExpected(t, nil, expected)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ensure hosts[0] is set to verified for the rest of the tests
|
||||
require.NoError(t, ds.SetOrUpdateHostDiskEncryptionKey(ctx, hosts[0].ID, "test-key", "", ptr.Bool(true)))
|
||||
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hosts[0].ID, true))
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
|
||||
})
|
||||
|
||||
t.Run("BitLocker failed status", func(t *testing.T) {
|
||||
// TODO: Update test to use methods to set windows disk encryption when they are implemented
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
`INSERT INTO host_disk_encryption_keys (host_id, decryptable, client_error) VALUES (?, ?, ?)`,
|
||||
hosts[1].ID,
|
||||
false,
|
||||
"test-error")
|
||||
return err
|
||||
})
|
||||
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
|
||||
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID, hosts[3].ID, hosts[4].ID},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("BitLocker team filtering", func(t *testing.T) {
|
||||
// Test team filtering
|
||||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"})
|
||||
require.NoError(t, err)
|
||||
|
||||
tm, err := ds.Team(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tm)
|
||||
require.False(t, tm.Config.MDM.EnableDiskEncryption) // disk encryption is not enabled for team
|
||||
|
||||
// Transfer hosts[2] to the team
|
||||
require.NoError(t, ds.AddHostsToTeam(ctx, &team.ID, []uint{hosts[2].ID}))
|
||||
|
||||
// Check the summary for the team
|
||||
checkExpected(t, &team.ID, hostIDsByStatus{}) // disk encryption is not enabled for team so hosts[2] is not counted
|
||||
|
||||
// Check the summary for no team
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
|
||||
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID}, // hosts[2] is no longer included in the no team summary
|
||||
})
|
||||
|
||||
// Enable disk encryption for the team
|
||||
tm.Config.MDM.EnableDiskEncryption = true
|
||||
tm, err = ds.SaveTeam(ctx, tm)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tm)
|
||||
require.True(t, tm.Config.MDM.EnableDiskEncryption)
|
||||
|
||||
// Check the summary for the team
|
||||
checkExpected(t, &team.ID, hostIDsByStatus{
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[2].ID}, // disk encryption is enabled for team so hosts[2] is counted
|
||||
})
|
||||
|
||||
// Check the summary for no team (should be unchanged)
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
|
||||
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[3].ID, hosts[4].ID},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("BitLocker Windows server excluded", func(t *testing.T) {
|
||||
require.NoError(t, ds.SetOrUpdateMDMData(ctx,
|
||||
hosts[3].ID,
|
||||
true, // set is_server to true for hosts[3]
|
||||
true, "https://example.com", false, fleet.WellKnownMDMFleet))
|
||||
|
||||
// Check Windows servers not counted
|
||||
checkExpected(t, nil, hostIDsByStatus{
|
||||
fleet.DiskEncryptionVerified: []uint{hosts[0].ID},
|
||||
fleet.DiskEncryptionFailed: []uint{hosts[1].ID},
|
||||
fleet.DiskEncryptionEnforcing: []uint{hosts[4].ID}, // hosts[3] is not counted
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("OS settings filters include Windows and macOS hosts", func(t *testing.T) {
|
||||
// Make macOS host fail disk encryption
|
||||
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
HostUUID: hosts[5].UUID,
|
||||
ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier,
|
||||
ProfileName: "Disk encryption",
|
||||
ProfileID: 1,
|
||||
CommandUUID: uuid.New().String(),
|
||||
OperationType: fleet.MDMAppleOperationTypeInstall,
|
||||
Status: &fleet.MDMAppleDeliveryFailed,
|
||||
Checksum: []byte("checksum"),
|
||||
},
|
||||
}))
|
||||
|
||||
// Check that BitLocker summary does not include macOS hosts
|
||||
checkBitLockerSummary(t, nil, fleet.MDMWindowsBitLockerSummary{
|
||||
Verified: 1,
|
||||
Verifying: 0,
|
||||
Failed: 1,
|
||||
Enforcing: 1,
|
||||
RemovingEnforcement: 0,
|
||||
ActionRequired: 0,
|
||||
})
|
||||
|
||||
// Check that filtered lists do include macOS hosts
|
||||
checkListHostsFilterDiskEncryption(t, nil, fleet.DiskEncryptionFailed, []uint{hosts[1].ID, hosts[5].ID})
|
||||
checkListHostsFilterOSSettings(t, nil, fleet.OSSettingsFailed, []uint{hosts[1].ID, hosts[5].ID})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user