mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +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
136 changed files with 5367 additions and 930 deletions
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,
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
|
|
@ -1097,9 +1097,9 @@ spec:
|
|||
require.True(t, ds.NewJobFuncInvoked)
|
||||
// all left untouched, only setup assistant added
|
||||
assert.Equal(t, fleet.TeamMDM{
|
||||
EnableDiskEncryption: false,
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
EnableDiskEncryption: false,
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
|
|
@ -1129,9 +1129,9 @@ spec:
|
|||
require.Equal(t, "[+] applied 1 teams\n", runAppForTest(t, []string{"apply", "-f", name}))
|
||||
// all left untouched, only bootstrap package added
|
||||
assert.Equal(t, fleet.TeamMDM{
|
||||
EnableDiskEncryption: false,
|
||||
MacOSSettings: fleet.MacOSSettings{
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
EnableDiskEncryption: false,
|
||||
CustomSettings: []string{mobileConfigPath},
|
||||
},
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: optjson.SetString("10.10.10"),
|
||||
|
|
@ -2886,7 +2886,7 @@ spec:
|
|||
macos_settings:
|
||||
enable_disk_encryption: true
|
||||
`,
|
||||
wantErr: `Couldn't update macos_settings because MDM features aren't turned on in Fleet.`,
|
||||
wantErr: `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on`,
|
||||
},
|
||||
{
|
||||
desc: "app config macos_settings.enable_disk_encryption false",
|
||||
|
|
|
|||
|
|
@ -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,23 +4103,23 @@ _Available in Fleet Premium_
|
|||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
Get aggregate status counts of disk encryption enforced on hosts.
|
||||
Get aggregate status counts of disk encryption enforced on macOS and Windows hosts.
|
||||
|
||||
The summary can optionally be filtered by team id.
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple/filevault/summary`
|
||||
`GET /api/v1/fleet/mdm/disk_encryption/summary`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ------------------------- | ------ | ----- | ------------------------------------------------------------------------- |
|
||||
| team_id | string | query | _Available in Fleet Premium_ The team id to filter the summary. |
|
||||
| team_id | string | query | _Available in Fleet Premium_ The team id to filter the summary. |
|
||||
|
||||
#### Example
|
||||
|
||||
Get aggregate status counts of Apple disk encryption profiles applying to macOS hosts enrolled to Fleet's MDM that are not assigned to any team.
|
||||
Get aggregate disk encryption status counts of macOS and Windows hosts enrolled to Fleet's MDM that are not assigned to any team.
|
||||
|
||||
`GET /api/v1/fleet/mdm/apple/filevault/summary`
|
||||
`GET /api/v1/fleet/mdm/disk_encryption/summary`
|
||||
|
||||
##### Default response
|
||||
|
||||
|
|
@ -4114,12 +4127,12 @@ Get aggregate status counts of Apple disk encryption profiles applying to macOS
|
|||
|
||||
```json
|
||||
{
|
||||
"verified": 123,
|
||||
"verifying": 123,
|
||||
"action_required": 123,
|
||||
"enforcing": 123,
|
||||
"failed": 123,
|
||||
"removing_enforcement": 123
|
||||
"verified": {"macos": 123, "windows": 123},
|
||||
"verifying": {"macos": 123, "windows": 0},
|
||||
"action_required": {"macos": 123, "windows": 0},
|
||||
"enforcing": {"macos": 123, "windows": 123},
|
||||
"failed": {"macos": 123, "windows": 123},
|
||||
"removing_enforcement": {"macos": 123, "windows": 0},
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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,9 +824,10 @@ func (svc *Service) createTeamFromSpec(
|
|||
AgentOptions: agentOptions,
|
||||
Features: features,
|
||||
MDM: fleet.TeamMDM{
|
||||
MacOSUpdates: spec.MDM.MacOSUpdates,
|
||||
MacOSSettings: macOSSettings,
|
||||
MacOSSetup: macOSSetup,
|
||||
EnableDiskEncryption: enableDiskEncryption,
|
||||
MacOSUpdates: spec.MDM.MacOSUpdates,
|
||||
MacOSSettings: macOSSettings,
|
||||
MacOSSetup: macOSSetup,
|
||||
},
|
||||
},
|
||||
Secrets: secrets,
|
||||
|
|
@ -824,7 +836,7 @@ func (svc *Service) createTeamFromSpec(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if macOSSettings.EnableDiskEncryption {
|
||||
if enableDiskEncryption && defaults.MDM.EnabledAndConfigured {
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
}
|
||||
|
|
@ -871,11 +883,23 @@ func (svc *Service) editTeamFromSpec(
|
|||
team.Config.MDM.MacOSUpdates = spec.MDM.MacOSUpdates
|
||||
}
|
||||
|
||||
oldMacOSDiskEncryption := team.Config.MDM.MacOSSettings.EnableDiskEncryption
|
||||
oldMacOSDiskEncryption := team.Config.MDM.EnableDiskEncryption
|
||||
if err := svc.applyTeamMacOSSettings(ctx, spec, &team.Config.MDM.MacOSSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
newMacOSDiskEncryption := team.Config.MDM.MacOSSettings.EnableDiskEncryption
|
||||
|
||||
// 1. if the spec has the new setting, use that
|
||||
// 2. else if the spec has the deprecated setting, use that
|
||||
// 3. otherwise, leave the setting untouched
|
||||
if spec.MDM.EnableDiskEncryption.Valid {
|
||||
team.Config.MDM.EnableDiskEncryption = spec.MDM.EnableDiskEncryption.Value
|
||||
} else if de := team.Config.MDM.MacOSSettings.DeprecatedEnableDiskEncryption; de != nil {
|
||||
team.Config.MDM.EnableDiskEncryption = *de
|
||||
}
|
||||
if team.Config.MDM.EnableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() {
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("mdm",
|
||||
`Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`))
|
||||
}
|
||||
|
||||
oldMacOSSetup := team.Config.MDM.MacOSSetup
|
||||
if spec.MDM.MacOSSetup.MacOSSetupAssistant.Set || spec.MDM.MacOSSetup.BootstrapPackage.Set {
|
||||
|
|
@ -925,9 +949,9 @@ func (svc *Service) editTeamFromSpec(
|
|||
return err
|
||||
}
|
||||
}
|
||||
if oldMacOSDiskEncryption != newMacOSDiskEncryption {
|
||||
if appCfg.MDM.EnabledAndConfigured && oldMacOSDiskEncryption != team.Config.MDM.EnableDiskEncryption {
|
||||
var act fleet.ActivityDetails
|
||||
if team.Config.MDM.MacOSSettings.EnableDiskEncryption {
|
||||
if team.Config.MDM.EnableDiskEncryption {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
|
|
@ -982,7 +1006,7 @@ func (svc *Service) applyTeamMacOSSettings(ctx context.Context, spec *fleet.Team
|
|||
}
|
||||
|
||||
if (setFields["custom_settings"] && len(applyUpon.CustomSettings) > 0) ||
|
||||
(setFields["enable_disk_encryption"] && applyUpon.EnableDiskEncryption) {
|
||||
(setFields["enable_disk_encryption"] && *applyUpon.DeprecatedEnableDiskEncryption) {
|
||||
field := "custom_settings"
|
||||
if !setFields["custom_settings"] {
|
||||
field = "enable_disk_encryption"
|
||||
|
|
@ -1016,8 +1040,8 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) {
|
|||
func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Team, payload fleet.MDMAppleSettingsPayload) error {
|
||||
var didUpdate, didUpdateMacOSDiskEncryption bool
|
||||
if payload.EnableDiskEncryption != nil {
|
||||
if tm.Config.MDM.MacOSSettings.EnableDiskEncryption != *payload.EnableDiskEncryption {
|
||||
tm.Config.MDM.MacOSSettings.EnableDiskEncryption = *payload.EnableDiskEncryption
|
||||
if tm.Config.MDM.EnableDiskEncryption != *payload.EnableDiskEncryption {
|
||||
tm.Config.MDM.EnableDiskEncryption = *payload.EnableDiskEncryption
|
||||
didUpdate = true
|
||||
didUpdateMacOSDiskEncryption = true
|
||||
}
|
||||
|
|
@ -1029,7 +1053,7 @@ func (svc *Service) updateTeamMDMAppleSettings(ctx context.Context, tm *fleet.Te
|
|||
}
|
||||
if didUpdateMacOSDiskEncryption {
|
||||
var act fleet.ActivityDetails
|
||||
if tm.Config.MDM.MacOSSettings.EnableDiskEncryption {
|
||||
if tm.Config.MDM.EnableDiskEncryption {
|
||||
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name}
|
||||
if err := svc.MDMAppleEnableFileVaultAndEscrow(ctx, &tm.ID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "enable team filevault and escrow")
|
||||
|
|
|
|||
|
|
@ -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,12 +855,15 @@ const HostDetailsPage = ({
|
|||
{showUnenrollMdmModal && !!host && (
|
||||
<UnenrollMdmModal hostId={host.id} onClose={toggleUnenrollMdmModal} />
|
||||
)}
|
||||
{showDiskEncryptionModal && host && (
|
||||
<DiskEncryptionKeyModal
|
||||
hostId={host.id}
|
||||
onCancel={() => setShowDiskEncryptionModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showDiskEncryptionModal &&
|
||||
host &&
|
||||
isSupportedPlatform(host.platform) && (
|
||||
<DiskEncryptionKeyModal
|
||||
platform={host.platform}
|
||||
hostId={host.id}
|
||||
onCancel={() => setShowDiskEncryptionModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showBootstrapPackageModal &&
|
||||
bootstrapPackageData.details &&
|
||||
bootstrapPackageData.name && (
|
||||
|
|
|
|||
|
|
@ -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), [
|
||||
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 {
|
||||
|
|
@ -1742,13 +1867,14 @@ func (ds *Datastore) LoadHostByNodeKey(ctx context.Context, nodeKey string) (*fl
|
|||
|
||||
type hostWithMDMInfo struct {
|
||||
fleet.Host
|
||||
HostID *uint `db:"host_id"`
|
||||
Enrolled *bool `db:"enrolled"`
|
||||
ServerURL *string `db:"server_url"`
|
||||
InstalledFromDep *bool `db:"installed_from_dep"`
|
||||
IsServer *bool `db:"is_server"`
|
||||
MDMID *uint `db:"mdm_id"`
|
||||
Name *string `db:"name"`
|
||||
HostID *uint `db:"host_id"`
|
||||
Enrolled *bool `db:"enrolled"`
|
||||
ServerURL *string `db:"server_url"`
|
||||
InstalledFromDep *bool `db:"installed_from_dep"`
|
||||
IsServer *bool `db:"is_server"`
|
||||
MDMID *uint `db:"mdm_id"`
|
||||
Name *string `db:"name"`
|
||||
EncryptionKeyAvailable *bool `db:"encryption_key_available"`
|
||||
}
|
||||
|
||||
// LoadHostByOrbitNodeKey loads the whole host identified by the node key.
|
||||
|
|
@ -1804,7 +1930,9 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
|
|||
COALESCE(hm.is_server, false) AS is_server,
|
||||
COALESCE(mdms.name, ?) AS name,
|
||||
COALESCE(hdek.reset_requested, false) AS disk_encryption_reset_requested,
|
||||
COALESCE(hdek.decryptable, false) as encryption_key_available,
|
||||
IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
|
||||
hd.encrypted as disk_encryption_enabled,
|
||||
t.name as team_name
|
||||
FROM
|
||||
hosts h
|
||||
|
|
@ -1824,6 +1952,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
|
|||
host_disk_encryption_keys hdek
|
||||
ON
|
||||
hdek.host_id = h.id
|
||||
LEFT OUTER JOIN
|
||||
host_disks hd
|
||||
ON
|
||||
hd.host_id = h.id
|
||||
LEFT OUTER JOIN
|
||||
teams t
|
||||
ON
|
||||
|
|
@ -1846,6 +1978,10 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
|
|||
MDMID: hostWithMDM.MDMID,
|
||||
Name: *hostWithMDM.Name,
|
||||
}
|
||||
|
||||
host.MDM = fleet.MDMHostData{
|
||||
EncryptionKeyAvailable: *hostWithMDM.EncryptionKeyAvailable,
|
||||
}
|
||||
}
|
||||
return &host, nil
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
|
|
@ -3013,19 +3149,30 @@ func (ds *Datastore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID
|
|||
)
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key string) error {
|
||||
func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error {
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, `
|
||||
INSERT INTO host_disk_encryption_keys (host_id, base64_encrypted)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
/* if the key has changed, NULLify this value so it can be calculated again */
|
||||
decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, NULL),
|
||||
base64_encrypted = VALUES(base64_encrypted)
|
||||
`, hostID, encryptedBase64Key)
|
||||
INSERT INTO host_disk_encryption_keys
|
||||
(host_id, base64_encrypted, client_error, decryptable)
|
||||
VALUES
|
||||
(?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
/* if the key has changed, set decrypted to its initial value so it can be calculated again if necessary (if null) */
|
||||
decryptable = IF(base64_encrypted = VALUES(base64_encrypted), decryptable, VALUES(decryptable)),
|
||||
base64_encrypted = VALUES(base64_encrypted),
|
||||
client_error = VALUES(client_error)
|
||||
`, hostID, encryptedBase64Key, clientError, decryptable)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
|
||||
// NOTE(mna): currently we only verify encryption keys for macOS,
|
||||
// Windows/bitlocker uses a different approach where orbit sends the
|
||||
// encryption key and we encrypt it server-side with the WSTEP certificate,
|
||||
// so it is always decryptable once received.
|
||||
//
|
||||
// To avoid sending Windows-related keys to verify as part of this call, we
|
||||
// only return rows that have a non-empty encryption key (for Windows, the
|
||||
// key is blanked if an error occurred trying to retrieve it on the host).
|
||||
var keys []fleet.HostDiskEncryptionKey
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &keys, `
|
||||
SELECT
|
||||
|
|
@ -3035,7 +3182,8 @@ func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fle
|
|||
FROM
|
||||
host_disk_encryption_keys
|
||||
WHERE
|
||||
decryptable IS NULL
|
||||
decryptable IS NULL AND
|
||||
base64_encrypted != ''
|
||||
`)
|
||||
return keys, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -111,7 +112,7 @@ func TestHosts(t *testing.T) {
|
|||
{"HostsListBySoftwareChangedAt", testHostsListBySoftwareChangedAt},
|
||||
{"HostsListByOperatingSystemID", testHostsListByOperatingSystemID},
|
||||
{"HostsListByOSNameAndVersion", testHostsListByOSNameAndVersion},
|
||||
{"HostsListByDiskEncryptionStatus", testHostsListDiskEncryptionStatus},
|
||||
{"HostsListByDiskEncryptionStatus", testHostsListMacOSSettingsDiskEncryptionStatus},
|
||||
{"HostsListFailingPolicies", printReadsInTest(testHostsListFailingPolicies)},
|
||||
{"HostsExpiration", testHostsExpiration},
|
||||
{"HostsAllPackStats", testHostsAllPackStats},
|
||||
|
|
@ -722,8 +723,13 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
|
|||
|
||||
var hosts []*fleet.Host
|
||||
for i := 0; i < 10; i++ {
|
||||
var opts []test.NewHostOption
|
||||
switch i {
|
||||
case 5, 6:
|
||||
opts = append(opts, test.WithPlatform("windows"))
|
||||
}
|
||||
h := test.NewHost(t, ds, fmt.Sprintf("foo.local.%d", i), "1.1.1.1",
|
||||
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now())
|
||||
fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...)
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
userFilter := fleet.TeamFilter{User: test.UserAdmin}
|
||||
|
|
@ -763,12 +769,12 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
|
|||
Checksum: []byte("csum"),
|
||||
},
|
||||
}))
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// macos settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // no team
|
||||
|
||||
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
|
|
@ -781,12 +787,39 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
|
|||
Checksum: []byte("csum"),
|
||||
},
|
||||
}))
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 0) // wrong team
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// macos settings filter does not support "all teams" so both teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.MacOSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
|
||||
// test team filter in combination with os settings filter
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 0) // wrong team
|
||||
// os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[9]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerifying}, 1)
|
||||
|
||||
// test team filter in combination with os settings disk encryptionfilter
|
||||
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
|
||||
{
|
||||
ProfileID: 1,
|
||||
ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier,
|
||||
HostUUID: hosts[8].UUID, // hosts[8] is assgined to no team
|
||||
CommandUUID: "command-uuid-3",
|
||||
OperationType: fleet.MDMAppleOperationTypeInstall,
|
||||
Status: &fleet.MDMAppleDeliveryPending,
|
||||
Checksum: []byte("disk-encryption-csum"),
|
||||
},
|
||||
}))
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // hosts[0]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // wrong team
|
||||
// os settings filter does not support "all teams" so teamIDFilterNil acts the same as teamIDFilterZero
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8]
|
||||
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[8]
|
||||
}
|
||||
|
||||
func testHostsListFilterAdditional(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -2920,7 +2953,7 @@ func testHostsListByOSNameAndVersion(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
}
|
||||
|
||||
func testHostsListDiskEncryptionStatus(t *testing.T, ds *Datastore) {
|
||||
func testHostsListMacOSSettingsDiskEncryptionStatus(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
// seed hosts
|
||||
|
|
@ -5740,7 +5773,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
|
|||
err = ds.SetOrUpdateHostOrbitInfo(context.Background(), host.ID, "1.1.0")
|
||||
require.NoError(t, err)
|
||||
// set an encryption key
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
// set an mdm profile
|
||||
prof, err := ds.NewMDMAppleConfigProfile(context.Background(), *configProfileForTest(t, "N1", "I1", "U1"))
|
||||
|
|
@ -6586,23 +6619,26 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.Equal(t, hFleet.ID, loadFleet.ID)
|
||||
require.False(t, loadFleet.MDMInfo.IsServer)
|
||||
|
||||
// fill in disk encryption information
|
||||
require.NoError(t, ds.SetOrUpdateHostDisksEncryption(context.Background(), hFleet.ID, true))
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, hFleet.ID, "test-key", "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{hFleet.ID}, true, time.Now())
|
||||
require.NoError(t, err)
|
||||
loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
|
||||
require.NoError(t, err)
|
||||
require.True(t, loadFleet.MDM.EncryptionKeyAvailable)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, loadFleet.DiskEncryptionEnabled)
|
||||
require.True(t, *loadFleet.DiskEncryptionEnabled)
|
||||
}
|
||||
|
||||
func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected *bool) {
|
||||
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
||||
var actual *bool
|
||||
|
||||
row := tx.QueryRowxContext(
|
||||
context.Background(),
|
||||
"SELECT decryptable FROM host_disk_encryption_keys WHERE host_id = ?",
|
||||
hostID,
|
||||
)
|
||||
|
||||
err := row.Scan(&actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual)
|
||||
return nil
|
||||
})
|
||||
func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expectedKey string, expectedDecryptable *bool) {
|
||||
got, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedKey, got.Base64Encrypted)
|
||||
require.Equal(t, expectedDecryptable, got.Decryptable)
|
||||
}
|
||||
|
||||
func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -6632,49 +6668,81 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
|
|||
PrimaryMac: "30-65-EC-6F-C4-59",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA")
|
||||
host3, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String("3"),
|
||||
UUID: "3",
|
||||
OsqueryHostID: ptr.String("3"),
|
||||
Hostname: "foo.local3",
|
||||
PrimaryIP: "192.168.1.3",
|
||||
PrimaryMac: "30-65-EC-6F-C4-60",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkEncryptionKey := func(hostID uint, expected string) {
|
||||
actual, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, actual.Base64Encrypted)
|
||||
}
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "BBB", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err := ds.Host(context.Background(), host.ID)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKey(h.ID, "AAA")
|
||||
checkEncryptionKeyStatus(t, ds, h.ID, "AAA", nil)
|
||||
|
||||
h, err = ds.Host(context.Background(), host2.ID)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKey(h.ID, "BBB")
|
||||
checkEncryptionKeyStatus(t, ds, h.ID, "BBB", nil)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host2.ID, "CCC", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
h, err = ds.Host(context.Background(), host2.ID)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKey(h.ID, "CCC")
|
||||
checkEncryptionKeyStatus(t, ds, h.ID, "CCC", nil)
|
||||
|
||||
// setting the encryption key to an existing value doesn't change its
|
||||
// encryption status
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(context.Background(), []uint{host.ID}, true, time.Now().Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true))
|
||||
|
||||
// same key doesn't change encryption status
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "AAA", "", nil)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "AAA", ptr.Bool(true))
|
||||
|
||||
// different key resets encryption status
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host.ID, "XZY", "", nil)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "XZY", nil)
|
||||
|
||||
// set the key with an initial decrypted status of true
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(true))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true))
|
||||
|
||||
// same key, provided decrypted status is ignored (stored one is kept)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "abc", "", ptr.Bool(false))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "abc", ptr.Bool(true))
|
||||
|
||||
// client error, key is removed and decrypted status is nulled
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "", "fail", nil)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "", nil)
|
||||
|
||||
// new key, provided decrypted status is applied
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "def", "", ptr.Bool(true))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "def", ptr.Bool(true))
|
||||
|
||||
// different key, provided decrypted status is applied
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(context.Background(), host3.ID, "ghi", "", ptr.Bool(false))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host3.ID, "ghi", ptr.Bool(false))
|
||||
}
|
||||
|
||||
func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -6692,7 +6760,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
|||
PrimaryMac: "30-65-EC-6F-C4-58",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
host2, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
|
|
@ -6709,7 +6777,7 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
threshold := time.Now().Add(time.Hour)
|
||||
|
|
@ -6717,31 +6785,31 @@ func testHostsSetDiskEncryptionKeyStatus(t *testing.T, ds *Datastore) {
|
|||
// empty set
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{}, false, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
|
||||
|
||||
// keys that changed after the provided threshold are not updated
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold.Add(-24*time.Hour))
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", nil)
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
|
||||
|
||||
// single host
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, nil)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", nil)
|
||||
|
||||
// multiple hosts
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, true, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(true))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(true))
|
||||
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold)
|
||||
require.NoError(t, err)
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, ptr.Bool(false))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, ptr.Bool(false))
|
||||
checkEncryptionKeyStatus(t, ds, host.ID, "TESTKEY", ptr.Bool(false))
|
||||
checkEncryptionKeyStatus(t, ds, host2.ID, "TESTKEY", ptr.Bool(false))
|
||||
}
|
||||
|
||||
func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -6773,9 +6841,9 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host2.ID, "TESTKEY", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, err := ds.GetUnverifiedDiskEncryptionKeys(ctx)
|
||||
|
|
@ -6794,6 +6862,17 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
|
|||
keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
require.Equal(t, host2.ID, keys[0].HostID)
|
||||
|
||||
// update key of host 1 to empty with a client error, should not be reported
|
||||
// by GetUnverifiedDiskEncryptionKeys
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "", "failed", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys, err = ds.GetUnverifiedDiskEncryptionKeys(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
require.Equal(t, host2.ID, keys[0].HostID)
|
||||
|
||||
err = ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID, host2.ID}, false, threshold)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -6992,7 +7071,7 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, -1, *got.MDM.TestGetRawDecryptable())
|
||||
|
||||
// create the encryption key row, but unknown decryptable
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc")
|
||||
err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, "abc", "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err = ds.Host(ctx, host.ID)
|
||||
|
|
|
|||
|
|
@ -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 a new issue