diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46bf107edfa2..007448c1c8c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,64 @@
+## Fleet 4.60.1 (Dec 03, 2024)
+
+### Bug fixes
+
+- Fixed a bug where breaking occurred with gitops user role running `fleetctl gitops` command when MDM was enabled.
+
+## Fleet 4.60.0 (Nov 27, 2024)
+
+### Endpoint operations
+- Added support for labels_include_any to gitops.
+- Added major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation).
+- Added activity item for `fleetd` enrollment with host serial and display name.
+- Added capability for Fleet to serve YARA rules to agents over HTTPS authenticated via node key (requires osquery 5.14+).
+- Added a query to allow users to turn on/off automations while being transparent of the current log destination.
+- Updated UI to allow users to view scripts (from both the scripts page and host details page) without downloading them.
+- Updated activity feed to generate an activity when activity automations are enabled, edited, or disabled.
+- Cancelled pending script executions when a script is edited or deleted.
+
+### Device management (MDM)
+- Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy.
+- Added info banner for cloud customers to help with their windows autoenrollment setup.
+- Added DB support for "include any" label profile deployment.
+- Added support for "include any" label/profile relationships to the profile reconciliation machinery.
+- Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint.
+- Added indicator of how fresh a software title's host and version counts are on the title's details page.
+- Added UI for allowing users to install custom profiles on hosts that include any of the defined labels.
+- Added UI features supporting disk encryption for Ubuntu and Fedora Linux.
+- Added support for deb packages compressed with zstd.
+
+### Vulnerability management
+- Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`).
+
+### Bug fixes and improvements
+- Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required.
+- Improved performance of adding and removing profiles to large teams by an order of magnitude.
+- Disabled accessibility via keyboard for forms that are disabled via a slider.
+- Updated software batch endpoint status code from 200 (OK) to 202 (Accepted).
+- Updated a package used for testing (msw) to improve security.
+- Updated to reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04.
+- Updated GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there are more than 1 ABM tokens in Fleet.
+- Dismissed error flash on the my device page when navigating to another URL.
+- Modified the Fleet setup experience feature to not run if there is no software or script configured for the setup experience.
+- Set a more accurate minimum height for the Add hosts > ChromeOS > Policy for extension field, avoiding a scrollbar.
+- Added UI prompt for user to reenter the password if SCEP/NDES url or username has changed.
+- Updated ABM public key to download as as PEM format instead of CRT.
+- Fixed issue with uploading macOS software packages that do not have a top level `Distribution.xml`, but do have a top level `PackageInfo.xml`. For example, Okta Verify.app.
+- Fixed some cases where Fleet Maintained Apps generated incorrect uninstall scripts.
+- Fixed a bug where a device that was removed from ABM and then added back wouldn't properly re-enroll in Fleet MDM.
+- Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera.
+- Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica.
+- Fixed the MDM configuration profiles deployment when based on excluded labels.
+- Fixed gitops path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere).
+- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM IdP integration was enabled.
+- Fixed a bug where users would be allowed to attempt an install of an App Store app on a host that was not MDM enrolled.
+
+## Fleet 4.59.1 (Nov 18, 2024)
+
+### Bug fixes
+
+* Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint.
+
## Fleet 4.59.0 (Nov 12, 2024)
### Endpoint operations
diff --git a/changes/14899-yara-rules b/changes/14899-yara-rules
deleted file mode 100644
index 2c92188cfc02..000000000000
--- a/changes/14899-yara-rules
+++ /dev/null
@@ -1 +0,0 @@
-* Added capability for Fleet to serve yara rules to agents over HTTPS authenticated via node key (requires osquery 5.14+).
\ No newline at end of file
diff --git a/changes/20595-improve-memory-usage-software-installers b/changes/20595-improve-memory-usage-software-installers
deleted file mode 100644
index 7e15f3b935de..000000000000
--- a/changes/20595-improve-memory-usage-software-installers
+++ /dev/null
@@ -1 +0,0 @@
-* Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required.
diff --git a/changes/21633-windows-auto-enrollment-info-banner b/changes/21633-windows-auto-enrollment-info-banner
deleted file mode 100644
index 86cdfafdaf8f..000000000000
--- a/changes/21633-windows-auto-enrollment-info-banner
+++ /dev/null
@@ -1 +0,0 @@
-- add info banner for cloud customers to help with their windows autoenrollment setup
diff --git a/changes/21709-activities-automation-activity b/changes/21709-activities-automation-activity
deleted file mode 100644
index bc47a6e27330..000000000000
--- a/changes/21709-activities-automation-activity
+++ /dev/null
@@ -1 +0,0 @@
-* Generate an activity when activity automations are enabled, edited, or disabled.
diff --git a/changes/21888-dequeue-pending-scripts b/changes/21888-dequeue-pending-scripts
deleted file mode 100644
index 3852ee09c3e4..000000000000
--- a/changes/21888-dequeue-pending-scripts
+++ /dev/null
@@ -1 +0,0 @@
-* Cancelled pending script executions when a script is edited or deleted.
diff --git a/changes/22162-exclude-labels-fix-default-behavior b/changes/22162-exclude-labels-fix-default-behavior
deleted file mode 100644
index 41524c8c0399..000000000000
--- a/changes/22162-exclude-labels-fix-default-behavior
+++ /dev/null
@@ -1 +0,0 @@
-* Fixed the MDM configuration profiles deployment when based on excluded labels - prior to this fix, hosts were considered "not a member" of the label by default, even if they had not yet returned results for the excluded labels. The fix checks the label's creation time vs the host's last reported label results timestamp to prevent deploying a configuration profile if it does not yet know if the host is a member or not of those labels.
diff --git a/changes/22187-gitops-software-relative-paths b/changes/22187-gitops-software-relative-paths
deleted file mode 100644
index 8f1ce8f480ca..000000000000
--- a/changes/22187-gitops-software-relative-paths
+++ /dev/null
@@ -1 +0,0 @@
-* GitOps: Fixed path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere).
\ No newline at end of file
diff --git a/changes/22224-query-log-destinations b/changes/22224-query-log-destinations
deleted file mode 100644
index b6172a331b0e..000000000000
--- a/changes/22224-query-log-destinations
+++ /dev/null
@@ -1 +0,0 @@
-- Creating a query allow users to turn on/off automations while being transparent of the current log destination
diff --git a/changes/22269-software-title-updated-at b/changes/22269-software-title-updated-at
deleted file mode 100644
index dfc3f127697d..000000000000
--- a/changes/22269-software-title-updated-at
+++ /dev/null
@@ -1 +0,0 @@
-* Added indicator of how fresh a software title's host and version counts are on the title's details page
diff --git a/changes/22359-gitops-mult-abm b/changes/22359-gitops-mult-abm
deleted file mode 100644
index b7a7801edbf2..000000000000
--- a/changes/22359-gitops-mult-abm
+++ /dev/null
@@ -1,2 +0,0 @@
-- Updates GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there
- are more than 1 ABM tokens in Fleet.
\ No newline at end of file
diff --git a/changes/22437-linux-lock-black-screen b/changes/22437-linux-lock-black-screen
deleted file mode 100644
index edfd4dc8d477..000000000000
--- a/changes/22437-linux-lock-black-screen
+++ /dev/null
@@ -1 +0,0 @@
-- Reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04
diff --git a/changes/22446-scripts-modal b/changes/22446-scripts-modal
deleted file mode 100644
index 1e06aea93108..000000000000
--- a/changes/22446-scripts-modal
+++ /dev/null
@@ -1 +0,0 @@
-- Users can view scripts in the UI (from both the scripts page and host details page) without downloading them
diff --git a/changes/22575-ui-for-include-any-labels b/changes/22575-ui-for-include-any-labels
deleted file mode 100644
index 5f66f8396b79..000000000000
--- a/changes/22575-ui-for-include-any-labels
+++ /dev/null
@@ -1,2 +0,0 @@
-- add UI for allowing users to install custom profiles on hosts that include any of the defined
-labels
diff --git a/changes/22576-labels-include-any-gitops b/changes/22576-labels-include-any-gitops
deleted file mode 100644
index 228171c7d161..000000000000
--- a/changes/22576-labels-include-any-gitops
+++ /dev/null
@@ -1 +0,0 @@
-- Add support for labels_include_any to gitops
diff --git a/changes/22578-db-schema b/changes/22578-db-schema
deleted file mode 100644
index 281c14a6b909..000000000000
--- a/changes/22578-db-schema
+++ /dev/null
@@ -1 +0,0 @@
-- Adds DB support for "include any" label profile deployment
\ No newline at end of file
diff --git a/changes/22581-cron-updates b/changes/22581-cron-updates
deleted file mode 100644
index f228460a0406..000000000000
--- a/changes/22581-cron-updates
+++ /dev/null
@@ -1 +0,0 @@
-- Adds support for "include any" label/profile relationships to the profile reconciliation machinery.
\ No newline at end of file
diff --git a/changes/22606-keyboard-accessiblity b/changes/22606-keyboard-accessiblity
deleted file mode 100644
index 6f863e248a7a..000000000000
--- a/changes/22606-keyboard-accessiblity
+++ /dev/null
@@ -1 +0,0 @@
-- Fleet UI: Major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation)
\ No newline at end of file
diff --git a/changes/22773-fma-uninstall-fix b/changes/22773-fma-uninstall-fix
deleted file mode 100644
index 74c4390533b5..000000000000
--- a/changes/22773-fma-uninstall-fix
+++ /dev/null
@@ -1 +0,0 @@
-- Fix some cases where Fleet Maintained Apps generated incorrect uninstall scripts
diff --git a/changes/22891-zstd-deb-packages b/changes/22891-zstd-deb-packages
deleted file mode 100644
index f523dd62720d..000000000000
--- a/changes/22891-zstd-deb-packages
+++ /dev/null
@@ -1 +0,0 @@
-- Add support for deb packages compressed with zstd
diff --git a/changes/22985-disable-forms-keyboard-access b/changes/22985-disable-forms-keyboard-access
deleted file mode 100644
index 2e90b69dc543..000000000000
--- a/changes/22985-disable-forms-keyboard-access
+++ /dev/null
@@ -1 +0,0 @@
-- Fleet UI: Disable accessibility via keyboard for forms that are disabled via a slider
diff --git a/changes/23016-add-chrome-host-text-area-height b/changes/23016-add-chrome-host-text-area-height
deleted file mode 100644
index 7616f4bfa0a2..000000000000
--- a/changes/23016-add-chrome-host-text-area-height
+++ /dev/null
@@ -1,2 +0,0 @@
-* Set a more elegant minimum height for the Add hosts > ChromeOS > Policy for extension field,
-avoiding a scrollbar.
diff --git a/changes/23078-allow-skipping-vuln-details b/changes/23078-allow-skipping-vuln-details
deleted file mode 100644
index 7a299339769b..000000000000
--- a/changes/23078-allow-skipping-vuln-details
+++ /dev/null
@@ -1 +0,0 @@
-* Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`)
\ No newline at end of file
diff --git a/changes/23128-update-mock-service-worker-package-for-secutiy b/changes/23128-update-mock-service-worker-package-for-secutiy
deleted file mode 100644
index aa9a3e47af24..000000000000
--- a/changes/23128-update-mock-service-worker-package-for-secutiy
+++ /dev/null
@@ -1 +0,0 @@
-- update a package used for testing (msw) to improve security
diff --git a/changes/23213-okta-verify b/changes/23213-okta-verify
deleted file mode 100644
index 6fd38a9e476b..000000000000
--- a/changes/23213-okta-verify
+++ /dev/null
@@ -1 +0,0 @@
-Fixed issue with uploading macOS software packages that do not have a top level Distribution.xml, but do have a top level PackageInfo.xml. For example, Okta Verify.app
diff --git a/changes/23247-vpp-app-install b/changes/23247-vpp-app-install
deleted file mode 100644
index 97a62eb9df09..000000000000
--- a/changes/23247-vpp-app-install
+++ /dev/null
@@ -1,2 +0,0 @@
-- Fixes a bug where users would be allowed to attempt an install of an App Store app on a host that
- was not MDM enrolled.
\ No newline at end of file
diff --git a/changes/23492-software-batch-status-code b/changes/23492-software-batch-status-code
deleted file mode 100644
index 9ab51770d9a4..000000000000
--- a/changes/23492-software-batch-status-code
+++ /dev/null
@@ -1 +0,0 @@
-* Updated software batch endpoint status code from 200 (OK) to 202 (Accepted)
\ No newline at end of file
diff --git a/changes/23525-ndes-errors b/changes/23525-ndes-errors
deleted file mode 100644
index 409723e8095c..000000000000
--- a/changes/23525-ndes-errors
+++ /dev/null
@@ -1 +0,0 @@
-Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy.
diff --git a/changes/23597-fix-create-update-label-returns-outdated-info b/changes/23597-fix-create-update-label-returns-outdated-info
deleted file mode 100644
index 3a5e26e5aa8c..000000000000
--- a/changes/23597-fix-create-update-label-returns-outdated-info
+++ /dev/null
@@ -1 +0,0 @@
-* Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica.
diff --git a/changes/23651-reenter-password b/changes/23651-reenter-password
deleted file mode 100644
index b3fc7df44d87..000000000000
--- a/changes/23651-reenter-password
+++ /dev/null
@@ -1 +0,0 @@
-- Fleet UI: Prompt user to reenter the password if SCEP/NDES url or username has changed
diff --git a/changes/23669-dismiss-error-flash-on-url-change-dup b/changes/23669-dismiss-error-flash-on-url-change-dup
deleted file mode 100644
index 125774f81fe3..000000000000
--- a/changes/23669-dismiss-error-flash-on-url-change-dup
+++ /dev/null
@@ -1 +0,0 @@
-* Dismiss error flash on the my device page when navigating to another URL.
\ No newline at end of file
diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml
index 0a7be2bfdb94..d8da013055b0 100644
--- a/charts/fleet/Chart.yaml
+++ b/charts/fleet/Chart.yaml
@@ -4,11 +4,11 @@ name: fleet
keywords:
- fleet
- osquery
-version: v6.2.1
+version: v6.2.3
home: https://github.com/fleetdm/fleet
sources:
- https://github.com/fleetdm/fleet.git
-appVersion: v4.59.0
+appVersion: v4.60.1
dependencies:
- name: mysql
condition: mysql.enabled
diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml
index da975de66ad3..e8570a7d600a 100644
--- a/charts/fleet/values.yaml
+++ b/charts/fleet/values.yaml
@@ -3,7 +3,7 @@
hostName: fleet.localhost
replicas: 3 # The number of Fleet instances to deploy
imageRepository: fleetdm/fleet
-imageTag: v4.59.0 # Version of Fleet to deploy
+imageTag: v4.60.1 # Version of Fleet to deploy
podAnnotations: {} # Additional annotations to add to the Fleet pod
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
resources:
diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go
index 4c836bebdccd..2f7bf65f68c8 100644
--- a/cmd/fleet/serve.go
+++ b/cmd/fleet/serve.go
@@ -189,6 +189,7 @@ the way that the Fleet server works.
if config.MysqlReadReplica.Address != "" {
opts = append(opts, mysql.Replica(&config.MysqlReadReplica))
}
+ // NOTE this will disable OTEL/APM interceptor
if dev && os.Getenv("FLEET_DEV_ENABLE_SQL_INTERCEPTOR") != "" {
opts = append(opts, mysql.WithInterceptor(&devSQLInterceptor{
logger: kitlog.With(logger, "component", "sql-interceptor"),
@@ -1027,6 +1028,9 @@ the way that the Fleet server works.
"get_frontend",
service.ServeFrontend(config.Server.URLPrefix, config.Server.SandboxEnabled, httpLogger),
)
+
+ frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler)
+
apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore)
setupRequired, err := svc.SetupRequired(baseCtx)
diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go
index ba5ba641f754..3c2bbee35640 100644
--- a/cmd/fleetctl/apply_test.go
+++ b/cmd/fleetctl/apply_test.go
@@ -3781,7 +3781,9 @@ spec:
macos_settings:
enable_disk_encryption: true
`,
- wantErr: `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on`,
+
+ // Since Linux disk encryption does not use MDM, we allow enabling it even without MDM enabled and configured
+ wantOutput: `[+] applied fleet config`,
},
{
desc: "app config macos_settings.enable_disk_encryption false",
diff --git a/cmd/fleetctl/gitops.go b/cmd/fleetctl/gitops.go
index 30efe0a513a7..5389065913ac 100644
--- a/cmd/fleetctl/gitops.go
+++ b/cmd/fleetctl/gitops.go
@@ -299,12 +299,12 @@ func checkABMTeamAssignments(config *spec.GitOps, fleetClient *service.Client) (
return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
}
- abmToks, err := fleetClient.ListABMTokens()
+ abmToks, err := fleetClient.CountABMTokens()
if err != nil {
return nil, false, false, err
}
- if hasLegacyConfig && len(abmToks) > 1 {
+ if hasLegacyConfig && abmToks > 1 {
return nil, false, false, errors.New(fleet.AppleABMDefaultTeamDeprecatedMessage)
}
diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go
index 407351c154b2..72d999823003 100644
--- a/cmd/fleetctl/gitops_test.go
+++ b/cmd/fleetctl/gitops_test.go
@@ -1217,6 +1217,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
+ ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
+ return 0, nil
+ }
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
@@ -1815,6 +1818,9 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
+ ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
+ return 0, nil
+ }
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes()
require.NoError(t, err)
@@ -2854,6 +2860,9 @@ software:
}
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
}
+ ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
+ return len(tt.tokens), nil
+ }
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
var res []*fleet.TeamSummary
@@ -3177,6 +3186,9 @@ software:
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}, {OrganizationName: "Foo Inc."}}, nil
}
+ ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
+ return 1, nil
+ }
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
var res []*fleet.TeamSummary
diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go
index 5c73a3ce89cc..6b306da0128f 100644
--- a/cmd/osquery-perf/agent.go
+++ b/cmd/osquery-perf/agent.go
@@ -18,6 +18,7 @@ import (
"net/http"
_ "net/http/pprof"
"os"
+ "sort"
"strconv"
"strings"
"sync"
@@ -1412,6 +1413,7 @@ func (a *agent) orbitEnroll() error {
EnrollSecret: a.EnrollSecret,
HardwareUUID: a.UUID,
HardwareSerial: a.SerialNumber,
+ Hostname: a.CachedString("hostname"),
}
jsonBytes, err := json.Marshal(params)
if err != nil {
@@ -2097,8 +2099,9 @@ func (a *agent) runLiveQuery(query string) (results []map[string]string, status
}
}
-func (a *agent) processQuery(name, query string) (
- handled bool, results []map[string]string, status *fleet.OsqueryStatus, message *string, stats *fleet.Stats,
+func (a *agent) processQuery(name, query string, cachedResults *cachedResults) (
+ handled bool, results []map[string]string,
+ status *fleet.OsqueryStatus, message *string, stats *fleet.Stats,
) {
const (
hostPolicyQueryPrefix = "fleet_policy_query_"
@@ -2164,6 +2167,33 @@ func (a *agent) processQuery(name, query string) (
}
if ss == fleet.StatusOK {
results = a.softwareMacOS()
+ cachedResults.software = results
+ }
+ return true, results, &ss, nil, nil
+ case name == hostDetailQueryPrefix+"software_macos_codesign":
+ // Given queries run in lexicographic order software_macos already run and
+ // cachedResults.software should have its results.
+ ss := fleet.StatusOK
+ if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
+ ss = fleet.OsqueryStatus(1)
+ }
+ if ss == fleet.StatusOK {
+ if len(cachedResults.software) > 0 {
+ for _, s := range cachedResults.software {
+ if s["source"] != "apps" {
+ continue
+ }
+ installedPath := s["installed_path"]
+ teamIdentifier := s["name"] // use name to be fixed (more realistic than changing often).
+ if len(teamIdentifier) > 10 {
+ teamIdentifier = teamIdentifier[:10]
+ }
+ results = append(results, map[string]string{
+ "path": installedPath,
+ "team_identifier": teamIdentifier,
+ })
+ }
+ }
}
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_windows":
@@ -2254,6 +2284,10 @@ func (a *agent) processQuery(name, query string) (
}
}
+type cachedResults struct {
+ software []map[string]string
+}
+
func (a *agent) DistributedWrite(queries map[string]string) error {
r := service.SubmitDistributedQueryResultsRequest{
Results: make(fleet.OsqueryDistributedQueryResults),
@@ -2262,8 +2296,21 @@ func (a *agent) DistributedWrite(queries map[string]string) error {
Stats: make(map[string]*fleet.Stats),
}
r.NodeKey = a.nodeKey
- for name, query := range queries {
- handled, results, status, message, stats := a.processQuery(name, query)
+
+ cachedResults := cachedResults{}
+
+ // Sort queries to be executed by lexicographic name order (for result processing
+ // to be more predictable). This aligns to how osquery executes the queries.
+ queryNames := make([]string, 0, len(queries))
+ for name := range queries {
+ queryNames = append(queryNames, name)
+ }
+ sort.Strings(queryNames)
+
+ for _, name := range queryNames {
+ query := queries[name]
+
+ handled, results, status, message, stats := a.processQuery(name, query, &cachedResults)
if !handled {
// If osquery-perf does not handle the incoming query,
// always return status OK and the default query result.
diff --git a/docs/Contributing/Audit-logs.md b/docs/Contributing/Audit-logs.md
index 85f22fe2192f..26c8a02f3dc5 100644
--- a/docs/Contributing/Audit-logs.md
+++ b/docs/Contributing/Audit-logs.md
@@ -521,6 +521,23 @@ This activity contains the following fields:
}
```
+## fleet_enrolled
+
+Generated when a host is enrolled to Fleet (Fleet's agent fleetd is installed).
+
+This activity contains the following fields:
+- "host_serial": Serial number of the host.
+- "host_display_name": Display name of the host.
+
+#### Example
+
+```json
+{
+ "host_serial": "B04FL3ALPT21",
+ "host_display_name": "WIN-DESKTOP-JGS78KJ7C"
+}
+```
+
## mdm_enrolled
Generated when a host is enrolled in Fleet's MDM.
diff --git a/docs/Contributing/Understanding-host-vitals.md b/docs/Contributing/Understanding-host-vitals.md
index c1c6d2b0c9d1..aa3670ee63be 100644
--- a/docs/Contributing/Understanding-host-vitals.md
+++ b/docs/Contributing/Understanding-host-vitals.md
@@ -480,7 +480,6 @@ SELECT
version AS version,
identifier AS extension_id,
browser_type AS browser,
- 'Browser plugin (Chrome)' AS type,
'chrome_extensions' AS source,
'' AS vendor,
'' AS installed_path
@@ -500,7 +499,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups)
SELECT
name AS name,
version AS version,
- 'Package (deb)' AS type,
'' AS extension_id,
'' AS browser,
'deb_packages' AS source,
@@ -514,7 +512,6 @@ UNION
SELECT
package AS name,
version AS version,
- 'Package (Portage)' AS type,
'' AS extension_id,
'' AS browser,
'portage_packages' AS source,
@@ -527,7 +524,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (RPM)' AS type,
'' AS extension_id,
'' AS browser,
'rpm_packages' AS source,
@@ -540,7 +536,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (NPM)' AS type,
'' AS extension_id,
'' AS browser,
'npm_packages' AS source,
@@ -553,7 +548,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (Chrome)' AS type,
identifier AS extension_id,
browser_type AS browser,
'chrome_extensions' AS source,
@@ -566,7 +560,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (Firefox)' AS type,
identifier AS extension_id,
'firefox' AS browser,
'firefox_addons' AS source,
@@ -579,7 +572,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (Python)' AS type,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
@@ -603,7 +595,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups)
SELECT
name AS name,
COALESCE(NULLIF(bundle_short_version, ''), bundle_version) AS version,
- 'Application (macOS)' AS type,
bundle_identifier AS bundle_identifier,
'' AS extension_id,
'' AS browser,
@@ -616,7 +607,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (Python)' AS type,
'' AS bundle_identifier,
'' AS extension_id,
'' AS browser,
@@ -629,7 +619,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (Chrome)' AS type,
'' AS bundle_identifier,
identifier AS extension_id,
browser_type AS browser,
@@ -642,7 +631,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (Firefox)' AS type,
'' AS bundle_identifier,
identifier AS extension_id,
'firefox' AS browser,
@@ -655,7 +643,6 @@ UNION
SELECT
name As name,
version AS version,
- 'Browser plugin (Safari)' AS type,
'' AS bundle_identifier,
'' AS extension_id,
'' AS browser,
@@ -668,7 +655,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (Homebrew)' AS type,
'' AS bundle_identifier,
'' AS extension_id,
'' AS browser,
@@ -679,9 +665,27 @@ SELECT
FROM homebrew_packages;
```
+## software_macos_codesign
+
+- Description: A software override query[^1] to append codesign information to macOS software entries. Requires `fleetd`
+
+- Platforms: darwin
+
+- Discovery query:
+```sql
+SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'codesign'
+```
+
+- Query:
+```sql
+SELECT a.path, c.team_identifier
+ FROM apps a
+ JOIN codesign c ON a.path = c.path
+```
+
## software_macos_firefox
-- Description: A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd`
+- Description: A software override query[^1] to differentiate between Firefox and Firefox ESR on macOS. Requires `fleetd`
- Platforms: darwin
@@ -709,7 +713,6 @@ WITH app_paths AS (
ELSE 'Firefox.app'
END AS name,
COALESCE(NULLIF(apps.bundle_short_version, ''), apps.bundle_version) AS version,
- 'Application (macOS)' AS type,
apps.bundle_identifier AS bundle_identifier,
'' AS extension_id,
'' AS browser,
@@ -740,7 +743,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups)
SELECT
name,
version,
- 'IDE extension (VS Code)' AS type,
'' AS bundle_identifier,
uuid AS extension_id,
'' AS browser,
@@ -764,7 +766,6 @@ WITH cached_users AS (WITH cached_groups AS (select * from groups)
SELECT
name AS name,
version AS version,
- 'Program (Windows)' AS type,
'' AS extension_id,
'' AS browser,
'programs' AS source,
@@ -775,7 +776,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (Python)' AS type,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
@@ -786,7 +786,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (IE)' AS type,
'' AS extension_id,
'' AS browser,
'ie_extensions' AS source,
@@ -797,7 +796,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (Chrome)' AS type,
identifier AS extension_id,
browser_type AS browser,
'chrome_extensions' AS source,
@@ -808,7 +806,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Browser plugin (Firefox)' AS type,
identifier AS extension_id,
'firefox' AS browser,
'firefox_addons' AS source,
@@ -819,7 +816,6 @@ UNION
SELECT
name AS name,
version AS version,
- 'Package (Chocolatey)' AS type,
'' AS extension_id,
'' AS browser,
'chocolatey_packages' AS source,
diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go
index 7c3b580e92b3..77a6c7ce46fe 100644
--- a/ee/server/service/devices.go
+++ b/ee/server/service/devices.go
@@ -166,3 +166,57 @@ func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSu
return sum, nil
}
+
+func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error {
+ if svc.ds.IsHostPendingEscrow(ctx, host.ID) {
+ return nil
+ }
+
+ if err := svc.validateReadyForLinuxEscrow(ctx, host); err != nil {
+ _ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error())
+ return err
+ }
+
+ return svc.ds.QueueEscrow(ctx, host.ID)
+}
+
+func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet.Host) error {
+ if !host.IsLUKSSupported() {
+ return &fleet.BadRequestError{Message: "Fleet does not yet support creating LUKS disk encryption keys on this platform."}
+ }
+
+ ac, err := svc.ds.AppConfig(ctx)
+ if err != nil {
+ return err
+ }
+
+ if host.TeamID == nil {
+ if !ac.MDM.EnableDiskEncryption.Value {
+ return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team."}
+ }
+ } else {
+ tc, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID)
+ if err != nil {
+ return err
+ }
+ if !tc.EnableDiskEncryption {
+ return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team."}
+ }
+ }
+
+ if host.DiskEncryptionEnabled == nil || !*host.DiskEncryptionEnabled {
+ return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please encrypt your disk first."}
+ }
+
+ // We have to pull Orbit info because the auth context doesn't fill in host.OrbitVersion
+ orbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
+ if err != nil {
+ return err
+ }
+
+ if orbitInfo == nil || !fleet.IsAtLeastVersion(orbitInfo.Version, fleet.MinOrbitLUKSVersion) {
+ return &fleet.BadRequestError{Message: "Your version of fleetd does not support creating disk encryption keys on Linux. Please upgrade fleetd, then click Refetch, then try again."}
+ }
+
+ return svc.ds.AssertHasNoEncryptionKeyStored(ctx, host.ID)
+}
diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go
index 354ae1e32250..2de82b708025 100644
--- a/ee/server/service/mdm.go
+++ b/ee/server/service/mdm.go
@@ -1013,10 +1013,16 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin
windows = *w
}
+ linux, err := svc.ds.GetLinuxDiskEncryptionSummary(ctx, teamID)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting linux disk encryption summary")
+ }
+
return &fleet.MDMDiskEncryptionSummary{
Verified: fleet.MDMPlatformsCounts{
MacOS: macOS.Verified,
Windows: windows.Verified,
+ Linux: linux.Verified,
},
Verifying: fleet.MDMPlatformsCounts{
MacOS: macOS.Verifying,
@@ -1025,6 +1031,7 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin
ActionRequired: fleet.MDMPlatformsCounts{
MacOS: macOS.ActionRequired,
Windows: windows.ActionRequired,
+ Linux: linux.ActionRequired,
},
Enforcing: fleet.MDMPlatformsCounts{
MacOS: macOS.Enforcing,
@@ -1033,6 +1040,7 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin
Failed: fleet.MDMPlatformsCounts{
MacOS: macOS.Failed,
Windows: windows.Failed,
+ Linux: linux.Failed,
},
RemovingEnforcement: fleet.MDMPlatformsCounts{
MacOS: macOS.RemovingEnforcement,
@@ -1266,6 +1274,22 @@ func (svc *Service) ListABMTokens(ctx context.Context) ([]*fleet.ABMToken, error
return tokens, nil
}
+func (svc *Service) CountABMTokens(ctx context.Context) (int, error) {
+ // Authorizing using the more general AppConfig object because:
+ // - this service method returns a count, which is not sensitive information
+ // - gitops role, which needs this info, is not authorized for AppleBM access (as of 2024/12/02)
+ if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
+ return 0, err
+ }
+
+ tokens, err := svc.ds.GetABMTokenCount(ctx)
+ if err != nil {
+ return 0, ctxerr.Wrap(ctx, err, "count ABM tokens")
+ }
+
+ return tokens, nil
+}
+
func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOSTeamID, iOSTeamID, iPadOSTeamID *uint) (*fleet.ABMToken, error) {
if err := svc.authz.Authorize(ctx, &fleet.AppleBM{}, fleet.ActionWrite); err != nil {
return nil, err
diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go
index 9cb9aa46f0cf..71272b08af65 100644
--- a/ee/server/service/mdm_external_test.go
+++ b/ee/server/service/mdm_external_test.go
@@ -45,6 +45,9 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser
AppleSCEPCertBytes: eeservice.TestCert,
AppleSCEPKeyBytes: eeservice.TestKey,
},
+ Server: config.ServerConfig{
+ PrivateKey: "foo",
+ },
}
depStorage := &nanodep_mock.Storage{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/ee/server/service/mdm_test.go b/ee/server/service/mdm_test.go
index 21cc3d21d939..5162f20f4dcb 100644
--- a/ee/server/service/mdm_test.go
+++ b/ee/server/service/mdm_test.go
@@ -6,12 +6,15 @@ import (
"strings"
"testing"
+ "github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
+ "github.com/fleetdm/fleet/v4/server/test"
"github.com/jmoiron/sqlx"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -148,3 +151,46 @@ b1xn1jGQd/o0xFf9ojpDNy6vNojidQGHh6E3h0GYvxbnQmVNq5U=
// prevent static analysis tools from raising issues due to detection of
// private key in code.
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
+
+func TestCountABMTokensAuth(t *testing.T) {
+ t.Parallel()
+ ds := new(mock.Store)
+ ctx := context.Background()
+ authorizer, err := authz.NewAuthorizer()
+ require.NoError(t, err)
+ svc := Service{ds: ds, authz: authorizer}
+
+ ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
+ return 5, nil
+ }
+
+ t.Run("CountABMTokens", func(t *testing.T) {
+ cases := []struct {
+ desc string
+ user *fleet.User
+ shoudFailWithAuth bool
+ }{
+ {"no role", test.UserNoRoles, true},
+ {"gitops can read", test.UserGitOps, false},
+ {"maintainer can read", test.UserMaintainer, false},
+ {"observer can read", test.UserObserver, false},
+ {"observer+ can read", test.UserObserverPlus, false},
+ {"admin can read", test.UserAdmin, false},
+ {"tm1 gitops cannot read", test.UserTeamGitOpsTeam1, true},
+ {"tm1 maintainer can read", test.UserTeamMaintainerTeam1, false},
+ {"tm1 observer can read", test.UserTeamObserverTeam1, false},
+ {"tm1 observer+ can read", test.UserTeamObserverPlusTeam1, false},
+ {"tm1 admin can read", test.UserTeamAdminTeam1, false},
+ }
+ for _, c := range cases {
+ t.Run(c.desc, func(t *testing.T) {
+ ctx = test.UserContext(ctx, c.user)
+ count, err := svc.CountABMTokens(ctx)
+ checkAuthErr(t, c.shoudFailWithAuth, err)
+ if !c.shoudFailWithAuth {
+ assert.EqualValues(t, 5, count)
+ }
+ })
+ }
+ })
+}
diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go
index 34d3b1f76c2c..eded9b4788ac 100644
--- a/ee/server/service/teams.go
+++ b/ee/server/service/teams.go
@@ -1046,13 +1046,9 @@ func (svc *Service) createTeamFromSpec(
}
invalid := &fleet.InvalidArgumentError{}
- if enableDiskEncryption && !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() {
- invalid.Append(
- "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 enableDiskEncryption && svc.config.Server.PrivateKey == "" {
+ return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
}
-
validateTeamCustomSettings(invalid, "macos", macOSSettings.CustomSettings)
validateTeamCustomSettings(invalid, "windows", spec.MDM.WindowsSettings.CustomSettings.Value)
@@ -1210,11 +1206,10 @@ func (svc *Service) editTeamFromSpec(
team.Config.MDM.EnableDiskEncryption = *de
}
didUpdateDiskEncryption := team.Config.MDM.EnableDiskEncryption != oldEnableDiskEncryption
- if !appCfg.MDM.AtLeastOnePlatformEnabledAndConfigured() && didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption {
- 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.`))
- }
+ if didUpdateDiskEncryption && team.Config.MDM.EnableDiskEncryption && svc.config.Server.PrivateKey == "" {
+ return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
+ }
if !team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Valid {
team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
}
@@ -1521,12 +1516,15 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) {
}
func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.Team, enable *bool) error {
- var didUpdate, didUpdateMacOSDiskEncryption bool
+ var didUpdate bool
if enable != nil {
if tm.Config.MDM.EnableDiskEncryption != *enable {
+ if *enable && svc.config.Server.PrivateKey == "" {
+ return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
+ }
+
tm.Config.MDM.EnableDiskEncryption = *enable
didUpdate = true
- didUpdateMacOSDiskEncryption = true
}
}
@@ -1539,13 +1537,7 @@ func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.T
if err != nil {
return err
}
-
- // macOS-specific stuff. For legacy reasons we check if apple is configured
- // via `appCfg.MDM.EnabledAndConfigured`
- //
- // TODO: is there a missing bitlocker activity feed item? (see same TODO on
- // other methods that deal with disk encryption)
- if appCfg.MDM.EnabledAndConfigured && didUpdateMacOSDiskEncryption {
+ if appCfg.MDM.EnabledAndConfigured {
var act fleet.ActivityDetails
if tm.Config.MDM.EnableDiskEncryption {
act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name}
diff --git a/frontend/components/InfoBanner/InfoBanner.tsx b/frontend/components/InfoBanner/InfoBanner.tsx
index 1a78e0a486d3..bd49e3d20645 100644
--- a/frontend/components/InfoBanner/InfoBanner.tsx
+++ b/frontend/components/InfoBanner/InfoBanner.tsx
@@ -15,10 +15,11 @@ export interface IInfoBannerProps {
/** default 4px */
borderRadius?: "large" | "xlarge";
pageLevel?: boolean;
- /** cta and link are mutually exclusive */
+ /** Add this element to the end of the banner message. Mutually exclusive with `link`. */
cta?: JSX.Element;
/** closable and link are mutually exclusive */
closable?: boolean;
+ /** Makes the entire banner clickable */
link?: string;
icon?: IconNames;
}
diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss
index e12ba664092f..ef41106731ab 100644
--- a/frontend/components/Modal/_styles.scss
+++ b/frontend/components/Modal/_styles.scss
@@ -25,8 +25,8 @@
&__content-wrapper {
margin-top: $pad-large;
font-size: $x-small;
- max-height: 800px;
- overflow-y: auto;
+ // New pattern of max height modals pushed to 4.61 with PR #24019
+ overflow: visible;
.input-field {
width: 100%;
diff --git a/frontend/components/SectionHeader/SectionHeader.tsx b/frontend/components/SectionHeader/SectionHeader.tsx
index f540ae821cce..c3ebe5e76888 100644
--- a/frontend/components/SectionHeader/SectionHeader.tsx
+++ b/frontend/components/SectionHeader/SectionHeader.tsx
@@ -7,24 +7,32 @@ interface ISectionHeaderProps {
title: string;
subTitle?: React.ReactNode;
details?: JSX.Element;
- className?: string;
+ wrapperCustomClass?: string;
+ alignLeftHeaderVertically?: boolean;
+ greySubtitle?: boolean;
}
const SectionHeader = ({
title,
subTitle,
details,
- className,
+ wrapperCustomClass,
+ alignLeftHeaderVertically,
+ greySubtitle,
}: ISectionHeaderProps) => {
- const classNames = classnames(baseClass, className);
+ const wrapperClassnames = classnames(baseClass, wrapperCustomClass);
+ const leftHeaderClassnames = classnames(`${baseClass}__left-header`, {
+ [`${baseClass}__left-header--vertical`]: alignLeftHeaderVertically,
+ });
+ const subTitleClassnames = classnames(`${baseClass}__sub-title`, {
+ [`${baseClass}__sub-title--grey`]: greySubtitle,
+ });
return (
-
-
+
+
{title}
- {subTitle && (
-
{subTitle}
- )}
+ {subTitle &&
{subTitle}
}
{details &&
{details}
}
diff --git a/frontend/components/SectionHeader/_styles.scss b/frontend/components/SectionHeader/_styles.scss
index c9f1b6412ecb..943ea5c9e163 100644
--- a/frontend/components/SectionHeader/_styles.scss
+++ b/frontend/components/SectionHeader/_styles.scss
@@ -7,6 +7,15 @@
display: flex;
align-items: center;
gap: $pad-small;
+ &--vertical {
+ flex-direction: column;
+ }
+ }
+
+ &__sub-title {
+ &--grey {
+ @include grey-text;
+ }
}
h2 {
diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx
index 3e134954ca11..e38b1c1b13f3 100644
--- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx
+++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx
@@ -113,10 +113,6 @@ const TeamsDropdown = ({
};
const customStyles: StylesConfig = {
- container: (provided) => ({
- ...provided,
- width: "80px",
- }),
control: (provided, state) => ({
...provided,
display: "flex",
diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss
index c86dc61bf62c..dc7a763f7c3e 100644
--- a/frontend/components/forms/fields/Dropdown/_styles.scss
+++ b/frontend/components/forms/fields/Dropdown/_styles.scss
@@ -225,6 +225,10 @@
animation: fade-in 150ms ease-out;
}
+ .Select-menu {
+ max-height: 190px;
+ }
+
.Select-noresults {
font-size: $x-small;
}
diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts
index 0d7b3acb28ea..d6fcafc8c7ce 100644
--- a/frontend/interfaces/activity.ts
+++ b/frontend/interfaces/activity.ts
@@ -33,6 +33,7 @@ export enum ActivityType {
UserDeletedGlobalRole = "deleted_user_global_role",
UserChangedTeamRole = "changed_user_team_role",
UserDeletedTeamRole = "deleted_user_team_role",
+ FleetEnrolled = "fleet_enrolled",
MdmEnrolled = "mdm_enrolled",
MdmUnenrolled = "mdm_unenrolled",
EditedMacosMinVersion = "edited_macos_min_version",
diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts
index 07fe2cffb97c..fba6866e3344 100644
--- a/frontend/interfaces/mdm.ts
+++ b/frontend/interfaces/mdm.ts
@@ -1,5 +1,4 @@
import { IConfigServerSettings } from "./config";
-import { ITeamSummary } from "./team";
export interface IMdmApple {
common_name: string;
@@ -93,7 +92,7 @@ export interface IMdmSummaryResponse {
mobile_device_management_solution: IMdmSummaryMdmSolution[] | null;
}
-export type ProfilePlatform = "darwin" | "windows" | "ios" | "ipados";
+export type ProfilePlatform = "darwin" | "windows" | "ios" | "ipados" | "linux";
export interface IProfileLabel {
name: string;
@@ -129,10 +128,11 @@ export interface IHostMdmProfile {
name: string;
operation_type: ProfileOperationType | null;
platform: ProfilePlatform;
- status: MdmProfileStatus | MdmDDMProfileStatus;
+ status: MdmProfileStatus | MdmDDMProfileStatus | LinuxDiskEncryptionStatus;
detail: string;
}
+// TODO - move disk encryption related types to dedicated file
export type DiskEncryptionStatus =
| "verified"
| "verifying"
@@ -143,14 +143,14 @@ export type DiskEncryptionStatus =
/** Currently windows disk enxryption status will only be one of these four
values. In the future we may add more. */
-export type IWindowsDiskEncryptionStatus = Extract<
+export type WindowsDiskEncryptionStatus = Extract<
DiskEncryptionStatus,
"verified" | "verifying" | "enforcing" | "failed"
>;
export const isWindowsDiskEncryptionStatus = (
status: DiskEncryptionStatus
-): status is IWindowsDiskEncryptionStatus => {
+): status is WindowsDiskEncryptionStatus => {
switch (status) {
case "verified":
case "verifying":
@@ -162,6 +162,16 @@ export const isWindowsDiskEncryptionStatus = (
}
};
+export type LinuxDiskEncryptionStatus = Extract<
+ DiskEncryptionStatus,
+ "verified" | "failed" | "action_required"
+>;
+
+export const isLinuxDiskEncryptionStatus = (
+ status: DiskEncryptionStatus
+): status is LinuxDiskEncryptionStatus =>
+ ["verified", "failed", "action_required"].includes(status);
+
export const FLEET_FILEVAULT_PROFILE_DISPLAY_NAME = "Disk encryption";
export interface IMdmSSOReponse {
diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts
index 2be1f412d4b3..1f33a1639fdf 100644
--- a/frontend/interfaces/platform.ts
+++ b/frontend/interfaces/platform.ts
@@ -64,9 +64,9 @@ export const MACADMINS_EXTENSION_TABLES: Record = {
*/
export const HOST_LINUX_PLATFORMS = [
"linux",
- "ubuntu",
+ "ubuntu", // covers Kubuntu
"debian",
- "rhel",
+ "rhel", // covers Fedora
"centos",
"sles",
"kali",
@@ -111,3 +111,55 @@ export const isAppleDevice = (platform: string) => {
export const isIPadOrIPhone = (platform: string | HostPlatform) =>
["ios", "ipados"].includes(platform);
+
+export const DISK_ENCRYPTION_SUPPORTED_LINUX_PLATFORMS = [
+ "ubuntu", // covers Kubuntu
+ "rhel", // *included here to support Fedora systems. Necessary to cross-check with `os_versions` as well to confrim host is Fedora and not another, non-support rhel-like platform.
+] as const;
+
+export const isDiskEncryptionSupportedLinuxPlatform = (
+ platform: HostPlatform,
+ os_version: string
+) => {
+ const isFedora =
+ platform === "rhel" && os_version.toLowerCase().includes("fedora");
+ return isFedora || platform === "ubuntu";
+};
+
+const DISK_ENCRYPTION_SUPPORTED_PLATFORMS = [
+ "darwin",
+ "windows",
+ "chrome",
+ ...DISK_ENCRYPTION_SUPPORTED_LINUX_PLATFORMS,
+] as const;
+
+export type DiskEncryptionSupportedPlatform = typeof DISK_ENCRYPTION_SUPPORTED_PLATFORMS[number];
+
+export const platformSupportsDiskEncryption = (
+ platform: HostPlatform,
+ /** os_version necessary to differentiate Fedora from other rhel-like platforms */
+ os_version?: string
+) => {
+ if (platform === "rhel") {
+ return !!os_version && os_version.toLowerCase().includes("fedora");
+ }
+ return DISK_ENCRYPTION_SUPPORTED_PLATFORMS.includes(
+ platform as DiskEncryptionSupportedPlatform
+ );
+};
+
+const OS_SETTINGS_DISPLAY_PLATFORMS = [
+ ...DISK_ENCRYPTION_SUPPORTED_PLATFORMS,
+ "ios",
+ "ipados",
+];
+
+export const isOsSettingsDisplayPlatform = (
+ platform: HostPlatform,
+ os_version: string
+) => {
+ if (platform === "rhel") {
+ return !!os_version && os_version.toLowerCase().includes("fedora");
+ }
+ return OS_SETTINGS_DISPLAY_PLATFORMS.includes(platform);
+};
diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
index 245f7aaddde7..b162a22fcc9f 100644
--- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
+++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
@@ -279,6 +279,14 @@ const TAGGED_TEMPLATES = {
>
);
},
+ fleetEnrolled: (activity: IActivity) => {
+ const hostDisplayName = activity.details?.host_display_name ? (
+ {activity.details.host_display_name}
+ ) : (
+ "A host"
+ );
+ return <>{hostDisplayName} enrolled in Fleet.>;
+ },
mdmEnrolled: (activity: IActivity) => {
if (activity.details?.mdm_platform === "microsoft") {
return (
@@ -1167,6 +1175,9 @@ const getDetail = (
case ActivityType.UserDeletedTeamRole: {
return TAGGED_TEMPLATES.userDeletedTeamRole(activity);
}
+ case ActivityType.FleetEnrolled: {
+ return TAGGED_TEMPLATES.fleetEnrolled(activity);
+ }
case ActivityType.MdmEnrolled: {
return TAGGED_TEMPLATES.mdmEnrolled(activity);
}
@@ -1374,6 +1385,14 @@ const ActivityItem = ({
? addGravatarUrlToResource({ email: actor_email })
: { gravatar_url: DEFAULT_GRAVATAR_LINK };
+ if (
+ !activity.actor_email &&
+ !activity.actor_full_name &&
+ !activity.actor_id
+ ) {
+ activity.actor_full_name = "Fleet";
+ }
+
const activityCreatedAt = new Date(activity.created_at);
const indicatePremiumFeature =
isSandboxMode && PREMIUM_ACTIVITIES.has(activity.type);
diff --git a/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx
index f6cf8c435f67..dff1dc41dffc 100644
--- a/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx
+++ b/frontend/pages/ManageControlsPage/OSSettings/OSSettings.tsx
@@ -9,7 +9,6 @@ import mdmAPI from "services/entities/mdm";
import OS_SETTINGS_NAV_ITEMS from "./OSSettingsNavItems";
import ProfileStatusAggregate from "./ProfileStatusAggregate";
-import TurnOnMdmMessage from "../../../components/TurnOnMdmMessage";
const baseClass = "os-settings";
@@ -29,7 +28,7 @@ const OSSettings = ({
params,
}: IOSSettingsProps) => {
const { section } = params;
- const { config, currentTeam } = useContext(AppContext);
+ const { currentTeam } = useContext(AppContext);
// TODO: consider using useTeamIdParam hook here instead in the future
const teamId =
@@ -51,14 +50,6 @@ const OSSettings = ({
}
);
- // MDM is not on so show messaging for user to enable it.
- if (
- !config?.mdm.enabled_and_configured &&
- !config?.mdm.windows_enabled_and_configured
- ) {
- return ;
- }
-
const DEFAULT_SETTINGS_SECTION = OS_SETTINGS_NAV_ITEMS[0];
const currentFormSection =
diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx
index ede9ac75b1ff..b0e31bf3b201 100644
--- a/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx
+++ b/frontend/pages/ManageControlsPage/OSSettings/cards/CustomSettings/CustomSettings.tsx
@@ -14,6 +14,7 @@ import CustomLink from "components/CustomLink";
import SectionHeader from "components/SectionHeader";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
+import TurnOnMdmMessage from "components/TurnOnMdmMessage";
import Pagination from "pages/ManageControlsPage/components/Pagination";
@@ -46,7 +47,11 @@ const CustomSettings = ({
onMutation,
}: ICustomSettingsProps) => {
const { renderFlash } = useContext(NotificationContext);
- const { isPremiumTier } = useContext(AppContext);
+ const { config, isPremiumTier } = useContext(AppContext);
+
+ const mdmEnabled =
+ config?.mdm.enabled_and_configured ||
+ config?.mdm.windows_enabled_and_configured;
const [showAddProfileModal, setShowAddProfileModal] = useState(false);
const [
@@ -78,6 +83,7 @@ const CustomSettings = ({
per_page: PROFILES_PER_PAGE,
}),
{
+ enabled: mdmEnabled,
refetchOnWindowFocus: false,
}
);
@@ -185,7 +191,14 @@ const CustomSettings = ({
url="https://fleetdm.com/learn-more-about/custom-os-settings"
/>
- <>{renderProfileList()}>
+ {!mdmEnabled ? (
+
+ ) : (
+ renderProfileList()
+ )}
{showAddProfileModal && (
{
try {
- await mdmAPI.updateAppleMdmSettings(diskEncryptionEnabled, currentTeamId);
+ await diskEncryptionAPI.updateDiskEncryption(
+ diskEncryptionEnabled,
+ currentTeamId
+ );
renderFlash(
"success",
"Successfully updated disk encryption enforcement!"
@@ -91,11 +99,24 @@ const DiskEncryption = ({
if (currentTeamId === 0) {
getUpdatedAppConfig();
}
- } catch {
- renderFlash(
- "error",
- "Could not update the disk encryption enforcement. Please try again."
- );
+ } catch (e) {
+ if (getErrorReason(e).includes("Missing required private key")) {
+ const link =
+ "https://fleetdm.com/learn-more-about/fleet-server-private-key";
+ renderFlash(
+ "error",
+ <>
+ Could't enable disk encryption. Missing required private key.
+ Learn how to configure the private key here:{" "}
+ {link}
+ >
+ );
+ } else {
+ renderFlash(
+ "error",
+ "Could not update the disk encryption enforcement. Please try again."
+ );
+ }
}
};
@@ -103,18 +124,62 @@ 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 getTipContent = (platform: "windows" | "macOS" | "linux") => {
+ if (platform === "linux") {
+ return (
+ <>
+ For Ubuntu and Fedora Linux.
+
+ Currently, full disk encryption must be turned on{" "}
+
+ during OS
+
+ setup
+
+ . If disk encryption is off, the end user must re-install
+
+ their operating system.
+ >
+ );
}
-
- return `Also known as “FileVault” on macOS and “BitLocker” on Windows. If turned on, hosts' disk encryption keys will be stored in Fleet. `;
+ const [AppleOrWindows, DEMethod] =
+ platform === "windows"
+ ? ["Windows", "BitLocker"]
+ : ["Apple", "FileVault"];
+ return (
+ <>
+ {AppleOrWindows} MDM must be turned on in{" "}
+
+ Settings > Integrations >{" "}
+ Mobile Device Management (MDM)
+ {" "}
+ to enforce disk encryption via {DEMethod}.
+ >
+ );
};
+ const subTitle = (
+ <>
+ Disk encryption is available on{" "}
+ macOS
+ ,{" "}
+
+ Windows
+
+ , and{" "}
+ Linux{" "}
+ hosts.
+ >
+ );
+
return (
-
+
{!isPremiumTier ? (
- {createDescriptionText()}
+ If turned on, hosts' disk encryption keys will be stored in
+ Fleet{" "}
);
};
diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx
index cd3f28c01bc8..0341ad8f00d2 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx
+++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tests.tsx
@@ -6,7 +6,8 @@ import DeviceUserBanners from "./DeviceUserBanners";
describe("Device User Banners", () => {
const turnOnMdmExpcetedText = /Mobile device management \(MDM\) is off\./;
- const resetKeyDiskEncryptExpcetedText = /Disk encryption: Log out of your device or restart it to safeguard your data in case your device is lost or stolen\./;
+ const resetNonLinuxDiskEncryptKeyExpectedText = /Disk encryption: Log out of your device or restart it to safeguard your data in case your device is lost or stolen\./;
+ const createNewLinuxDiskEncryptKeyExpectedText = /Disk encryption: Create a new disk encryption key\. This lets your organization help you unlock your device if you forget your passphrase\./;
it("renders the turn on mdm banner correctly", () => {
render(
@@ -14,52 +15,121 @@ describe("Device User Banners", () => {
hostPlatform="darwin"
mdmEnrollmentStatus="Off"
mdmEnabledAndConfigured
- mdmConnectedToFleet
- diskEncryptionStatus={null}
+ connectedToFleetMdm
+ macDiskEncryptionStatus={null}
diskEncryptionActionRequired={null}
onTurnOnMdm={noop}
+ onTriggerEscrowLinuxKey={noop}
/>
);
expect(screen.getByText(turnOnMdmExpcetedText)).toBeInTheDocument();
});
- it("renders the reset key for disk encryption banner correctly", () => {
+ it("renders the reset key for non-linux disk encryption banner correctly", () => {
render(
);
expect(
- screen.getByText(resetKeyDiskEncryptExpcetedText)
+ screen.getByText(resetNonLinuxDiskEncryptKeyExpectedText)
).toBeInTheDocument();
});
+ it("renders the create new linux disk encryption key banner correctly for Ubuntu", () => {
+ render(
+
+ );
+ expect(
+ screen.getByText(createNewLinuxDiskEncryptKeyExpectedText)
+ ).toBeInTheDocument();
+ });
+ it("renders the create new linux disk encryption key banner correctly for Fedora", () => {
+ render(
+
+ );
+ expect(
+ screen.getByText(createNewLinuxDiskEncryptKeyExpectedText)
+ ).toBeInTheDocument();
+ });
+
+ it("renders no banner correctly for a mac that is verifying its disk encryption", () => {
+ render(
+
+ );
- it("renders no banner correctly", () => {
+ expect(screen.queryByText(turnOnMdmExpcetedText)).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText)
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText)
+ ).not.toBeInTheDocument();
+ });
+ it("renders no banner correctly for a mac without MDM set up", () => {
// setup so mdm is not enabled and configured.
render(
);
expect(screen.queryByText(turnOnMdmExpcetedText)).not.toBeInTheDocument();
expect(
- screen.queryByText(resetKeyDiskEncryptExpcetedText)
+ screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText)
).not.toBeInTheDocument();
expect(
- screen.queryByText(resetKeyDiskEncryptExpcetedText)
+ screen.queryByText(resetNonLinuxDiskEncryptKeyExpectedText)
).not.toBeInTheDocument();
});
});
diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx
index 286101a026ff..c3455f7be55b 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx
+++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/DeviceUserBanners.tsx
@@ -2,42 +2,45 @@ import React from "react";
import InfoBanner from "components/InfoBanner";
import Button from "components/buttons/Button";
-import { DiskEncryptionStatus, MdmEnrollmentStatus } from "interfaces/mdm";
import { MacDiskEncryptionActionRequired } from "interfaces/host";
+import { IHostBannersBaseProps } from "pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners";
+import CustomLink from "components/CustomLink";
+import { isDiskEncryptionSupportedLinuxPlatform } from "interfaces/platform";
const baseClass = "device-user-banners";
-interface IDeviceUserBannersProps {
- hostPlatform: string;
- mdmEnrollmentStatus: MdmEnrollmentStatus | null;
+interface IDeviceUserBannersProps extends IHostBannersBaseProps {
mdmEnabledAndConfigured: boolean;
- mdmConnectedToFleet: boolean;
- diskEncryptionStatus: DiskEncryptionStatus | null;
diskEncryptionActionRequired: MacDiskEncryptionActionRequired | null;
onTurnOnMdm: () => void;
+ onTriggerEscrowLinuxKey: () => void;
}
const DeviceUserBanners = ({
hostPlatform,
+ hostOsVersion,
mdmEnrollmentStatus,
mdmEnabledAndConfigured,
- mdmConnectedToFleet,
- diskEncryptionStatus,
+ connectedToFleetMdm,
+ macDiskEncryptionStatus,
diskEncryptionActionRequired,
onTurnOnMdm,
+ diskEncryptionOSSetting,
+ diskIsEncrypted,
+ diskEncryptionKeyAvailable,
+ onTriggerEscrowLinuxKey,
}: IDeviceUserBannersProps) => {
const isMdmUnenrolled =
mdmEnrollmentStatus === "Off" || mdmEnrollmentStatus === null;
- const diskEncryptionBannersEnabled =
- mdmEnabledAndConfigured && mdmConnectedToFleet;
+ const mdmEnabledAndConnected = mdmEnabledAndConfigured && connectedToFleetMdm;
- const showTurnOnMdmBanner =
+ const showTurnOnAppleMdmBanner =
hostPlatform === "darwin" && isMdmUnenrolled && mdmEnabledAndConfigured;
- const showDiskEncryptionKeyResetRequired =
- diskEncryptionBannersEnabled &&
- diskEncryptionStatus === "action_required" &&
+ const showMacDiskEncryptionKeyResetRequired =
+ mdmEnabledAndConnected &&
+ macDiskEncryptionStatus === "action_required" &&
diskEncryptionActionRequired === "rotate_key";
const turnOnMdmButton = (
@@ -47,7 +50,7 @@ const DeviceUserBanners = ({
);
const renderBanner = () => {
- if (showTurnOnMdmBanner) {
+ if (showTurnOnAppleMdmBanner) {
return (
Mobile device management (MDM) is off. MDM allows your organization to
@@ -58,7 +61,7 @@ const DeviceUserBanners = ({
);
}
- if (showDiskEncryptionKeyResetRequired) {
+ if (showMacDiskEncryptionKeyResetRequired) {
return (
Disk encryption: Log out of your device or restart it to safeguard
@@ -68,6 +71,60 @@ const DeviceUserBanners = ({
);
}
+ // setting applies to a supported Linux host
+ if (
+ hostPlatform &&
+ isDiskEncryptionSupportedLinuxPlatform(
+ hostPlatform,
+ hostOsVersion ?? ""
+ ) &&
+ diskEncryptionOSSetting?.status
+ ) {
+ // host not in compliance with setting
+ if (!diskIsEncrypted) {
+ // banner 1
+ return (
+
+ }
+ color="yellow"
+ >
+ Disk encryption: Follow the instructions in the guide to encrypt
+ your device. This lets your organization help you unlock your device
+ if you forget your password.
+
+ );
+ }
+ // host disk is encrypted, so in compliance with the setting
+ if (!diskEncryptionKeyAvailable) {
+ // key is not escrowed: banner 3
+ return (
+
+ Create key
+
+ }
+ color="yellow"
+ >
+ Disk encryption: Create a new disk encryption key. This lets your
+ organization help you unlock your device if you forget your
+ passphrase.
+
+ );
+ }
+ }
+
return null;
};
diff --git a/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss
new file mode 100644
index 000000000000..a09bdf15e086
--- /dev/null
+++ b/frontend/pages/hosts/details/DeviceUserPage/components/DeviceUserBanners/_styles.scss
@@ -0,0 +1,6 @@
+.device-user-banners {
+ .create-key-button {
+ color: $core-fleet-black;
+ font-weight: $bold;
+ }
+}
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 8ef7adccff14..8947a18cf43b 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -93,7 +93,6 @@ import OSSettingsModal from "../OSSettingsModal";
import BootstrapPackageModal from "./modals/BootstrapPackageModal";
import ScriptModalGroup from "./modals/ScriptModalGroup";
import SelectQueryModal from "./modals/SelectQueryModal";
-import { isSupportedPlatform } from "./modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal";
import HostDetailsBanners from "./components/HostDetailsBanners";
import { IShowActivityDetailsData } from "../cards/Activity/Activity";
import LockModal from "./modals/LockModal";
@@ -740,11 +739,6 @@ const HostDetailsPage = ({
}
};
- // const hostDeviceStatusUIState = getHostDeviceStatusUIState(
- // host.mdm.device_status,
- // host.mdm.pending_action
- // );
-
const renderActionDropdown = () => {
if (!host) {
return null;
@@ -851,10 +845,13 @@ const HostDetailsPage = ({
<>
)}
- {showDiskEncryptionModal &&
- host &&
- isSupportedPlatform(host.platform) && (
- setShowDiskEncryptionModal(false)}
- />
- )}
+ {showDiskEncryptionModal && host && (
+ setShowDiskEncryptionModal(false)}
+ />
+ )}
{showBootstrapPackageModal &&
bootstrapPackageData.details &&
bootstrapPackageData.name && (
diff --git a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx
index 656d3f2debb8..0e4eb8b851a4 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/components/HostDetailsBanners/HostDetailsBanners.tsx
@@ -1,28 +1,50 @@
import React, { useContext } from "react";
import { AppContext } from "context/app";
+import { hasLicenseExpired } from "utilities/helpers";
+
import { DiskEncryptionStatus, MdmEnrollmentStatus } from "interfaces/mdm";
-import { hasLicenseExpired, willExpireWithinXDays } from "utilities/helpers";
+import { IOSSettings } from "interfaces/host";
+import {
+ HostPlatform,
+ isDiskEncryptionSupportedLinuxPlatform,
+ platformSupportsDiskEncryption,
+} from "interfaces/platform";
+
import InfoBanner from "components/InfoBanner";
+import CustomLink from "components/CustomLink";
+import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
+import { isDiskEncryptionProfile } from "pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers";
const baseClass = "host-details-banners";
-interface IHostDetailsBannersProps {
- hostMdmEnrollmentStatus?: MdmEnrollmentStatus | null;
- hostPlatform?: string;
- diskEncryptionStatus: DiskEncryptionStatus | null | undefined;
+export interface IHostBannersBaseProps {
+ macDiskEncryptionStatus: DiskEncryptionStatus | null | undefined;
+ mdmEnrollmentStatus: MdmEnrollmentStatus | null;
connectedToFleetMdm?: boolean;
+ hostPlatform?: HostPlatform;
+ // used to identify Fedora hosts, whose platform is "rhel"
+ hostOsVersion?: string;
+ /** Disk encryption setting status and detail, if any, that apply to this host (via a team or the "no team" team) */
+ diskEncryptionOSSetting?: IOSSettings["disk_encryption"];
+ /** Whether or not this host's disk is encrypted */
+ diskIsEncrypted?: boolean;
+ /** Whether or not Fleet has escrowed the host's disk encryption key */
+ diskEncryptionKeyAvailable?: boolean;
}
-
/**
* Handles the displaying of banners on the host details page
*/
const HostDetailsBanners = ({
- hostMdmEnrollmentStatus,
+ mdmEnrollmentStatus,
hostPlatform,
+ hostOsVersion,
connectedToFleetMdm,
- diskEncryptionStatus,
-}: IHostDetailsBannersProps) => {
+ macDiskEncryptionStatus,
+ diskEncryptionOSSetting,
+ diskIsEncrypted,
+ diskEncryptionKeyAvailable,
+}: IHostBannersBaseProps) => {
const {
config,
isPremiumTier,
@@ -53,8 +75,7 @@ const HostDetailsBanners = ({
willVppExpire ||
isFleetLicenseExpired);
- const isMdmUnenrolled =
- hostMdmEnrollmentStatus === "Off" || !hostMdmEnrollmentStatus;
+ const isMdmUnenrolled = mdmEnrollmentStatus === "Off" || !mdmEnrollmentStatus;
const showTurnOnMdmInfoBanner =
!showingAppWideBanner &&
@@ -62,30 +83,78 @@ const HostDetailsBanners = ({
isMdmUnenrolled &&
config?.mdm.enabled_and_configured;
- const showDiskEncryptionUserActionRequired =
+ const showMacDiskEncryptionUserActionRequired =
!showingAppWideBanner &&
config?.mdm.enabled_and_configured &&
connectedToFleetMdm &&
- diskEncryptionStatus === "action_required";
+ macDiskEncryptionStatus === "action_required";
- if (showTurnOnMdmInfoBanner || showDiskEncryptionUserActionRequired) {
+ if (showTurnOnMdmInfoBanner) {
return (
- {showTurnOnMdmInfoBanner && (
-
- To enforce settings, OS updates, disk encryption, and more, ask the
- end user to follow the Turn on MDM instructions on
- their My device page.
+
+ To enforce settings, OS updates, disk encryption, and more, ask the
+ end user to follow the Turn on MDM instructions on
+ their My device page.
+
+
+
+ Disk encryption: Requires action from the end user. Ask the end user
+ to log out of their device or restart it.
+
+
+ );
+ }
+ // setting applies
+ if (
+ hostPlatform &&
+ platformSupportsDiskEncryption(hostPlatform, hostOsVersion) &&
+ diskEncryptionOSSetting?.status
+ ) {
+ if (
+ !diskIsEncrypted &&
+ isDiskEncryptionSupportedLinuxPlatform(hostPlatform, hostOsVersion ?? "")
+ ) {
+ // linux host not in compliance with setting
+ return (
+
+
+ }
+ >
+ Disk encryption: Disk encryption is off. Currently, to turn on{" "}
+ full disk encryption, the end user has to re-install their
+ operating system.
- )}
- {showDiskEncryptionUserActionRequired && (
+
+ );
+ }
+ if (!diskEncryptionKeyAvailable) {
+ // disk is encrypted, but Fleet doesn't yet have a disk
+ // encryption key escrowed (possible for Linux hosts)
+ return (
+
- Disk encryption: Requires action from the end user. Ask the end user
- to log out of their device or restart it.
+ Disk encryption: Requires action from the end user. Ask the user to
+ follow Disk encryption instructions on their My device{" "}
+ page.
- )}
-
- );
+
+ );
+ }
}
return null;
};
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
index 69e3763d4617..bb87157940a3 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/DiskEncryptionKeyModal/DiskEncryptionKeyModal.tsx
@@ -8,26 +8,14 @@ import Modal from "components/Modal";
import Button from "components/buttons/Button";
import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent";
import DataError from "components/DataError";
-import { QueryablePlatform } from "interfaces/platform";
+import CustomLink from "components/CustomLink";
+import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
+import { HostPlatform } 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<
- QueryablePlatform,
- "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;
+ platform: HostPlatform;
hostId: number;
onCancel: () => void;
}
@@ -37,7 +25,7 @@ const DiskEncryptionKeyModal = ({
hostId,
onCancel,
}: IDiskEncryptionKeyModal) => {
- const { data: encrpytionKey, error: encryptionKeyError } = useQuery<
+ const { data: encryptionKey, error: encryptionKeyError } = useQuery<
IHostEncrpytionKeyResponse,
unknown,
string
@@ -49,14 +37,10 @@ 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 recoveryText =
+ platform === "darwin"
+ ? "Use this key to log in to the host if you forgot the password."
+ : "Use this key to unlock the encrypted drive.";
return (
) : (
<>
-
-
{descriptionText}
-
{recoveryText}
+
+
+ {recoveryText}{" "}
+
+
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx
index f86226a967c6..f73601f9319e 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx
@@ -29,8 +29,7 @@ const OSSettingsModal = ({
onClose,
onProfileResent,
}: IOSSettingsModalProps) => {
- // the caller should ensure that hostMDMData is not undefined and that platform is "windows" or
- // "darwin", otherwise we will allow an empty modal will be rendered.
+ // the caller should ensure that hostMDMData is not undefined and that platform is supported otherwise we will allow an empty modal will be rendered.
// https://fleetdm.com/handbook/company/why-this-way#why-make-it-obvious-when-stuff-breaks
const memoizedTableData = useMemo(
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx
index d3e515908ce1..4fe684de77d6 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/OSSettingStatusCell.tsx
@@ -4,7 +4,11 @@ import { uniqueId } from "lodash";
import Icon from "components/Icon";
import TextCell from "components/TableContainer/DataTable/TextCell";
-import { ProfileOperationType } from "interfaces/mdm";
+import {
+ LinuxDiskEncryptionStatus,
+ ProfileOperationType,
+ ProfilePlatform,
+} from "interfaces/mdm";
import { COLORS } from "styles/var/colors";
import {
@@ -14,6 +18,7 @@ import {
import TooltipContent from "./components/Tooltip/TooltipContent";
import {
isDiskEncryptionProfile,
+ LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG,
PROFILE_DISPLAY_CONFIG,
ProfileDisplayOption,
WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG,
@@ -25,22 +30,27 @@ interface IOSSettingStatusCellProps {
status: OsSettingsTableStatusValue;
operationType: ProfileOperationType | null;
profileName: string;
+ hostPlatform?: ProfilePlatform;
}
const OSSettingStatusCell = ({
status,
operationType,
profileName = "",
+ hostPlatform,
}: IOSSettingStatusCellProps) => {
let displayOption: ProfileDisplayOption = null;
+ if (hostPlatform === "linux") {
+ displayOption =
+ LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG[status as LinuxDiskEncryptionStatus];
+ }
+
// windows hosts do not have an operation type at the moment and their display options are
// different than mac hosts.
- if (!operationType && isMdmProfileStatus(status)) {
+ else if (!operationType && isMdmProfileStatus(status)) {
displayOption = WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG[status];
- }
-
- if (operationType) {
+ } else if (operationType) {
displayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status];
}
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts
index 588e4e8d2954..60996eab61ee 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingStatusCell/helpers.ts
@@ -135,3 +135,27 @@ export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDispla
tooltip: null,
},
};
+
+type LinuxDiskEncryptionDisplayConfig = Omit<
+ OperationTypeOption,
+ "success" | "pending" | "acknowledged" | "verifying"
+>;
+
+export const LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG: LinuxDiskEncryptionDisplayConfig = {
+ verified: {
+ statusText: "Verified",
+ iconName: "success",
+ tooltip: () =>
+ "The host turned disk encryption on and sent the key to Fleet. Fleet verified.",
+ },
+ failed: {
+ statusText: "Failed",
+ iconName: "error",
+ tooltip: null,
+ },
+ action_required: {
+ statusText: "Action required (pending)",
+ iconName: "pending-outline",
+ tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc,
+ },
+};
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx
index 8f6d2650c93a..0d9b4017c87f 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx
@@ -9,12 +9,17 @@ import {
IHostMdmProfile,
MdmDDMProfileStatus,
MdmProfileStatus,
+ isLinuxDiskEncryptionStatus,
isWindowsDiskEncryptionStatus,
} from "interfaces/mdm";
+import { isDiskEncryptionSupportedLinuxPlatform } from "interfaces/platform";
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
import OSSettingStatusCell from "./OSSettingStatusCell";
-import { generateWinDiskEncryptionProfile } from "../../helpers";
+import {
+ generateLinuxDiskEncryptionSetting,
+ generateWinDiskEncryptionSetting,
+} from "../../helpers";
import OSSettingsErrorCell from "./OSSettingsErrorCell";
export const isMdmProfileStatus = (
@@ -69,6 +74,7 @@ const generateTableConfig = (
status={cellProps.row.original.status}
operationType={cellProps.row.original.operation_type}
profileName={cellProps.row.original.name}
+ hostPlatform={cellProps.row.original.platform}
/>
);
},
@@ -101,7 +107,33 @@ const makeWindowsRows = ({ profiles, os_settings }: IHostMdmData) => {
isWindowsDiskEncryptionStatus(os_settings.disk_encryption.status)
) {
rows.push(
- generateWinDiskEncryptionProfile(
+ generateWinDiskEncryptionSetting(
+ os_settings.disk_encryption.status,
+ os_settings.disk_encryption.detail
+ )
+ );
+ }
+
+ if (rows.length === 0 && !profiles) {
+ return null;
+ }
+
+ return rows;
+};
+
+const makeLinuxRows = ({ profiles, os_settings }: IHostMdmData) => {
+ const rows: IHostMdmProfileWithAddedStatus[] = [];
+
+ if (profiles) {
+ rows.push(...profiles);
+ }
+
+ if (
+ os_settings?.disk_encryption?.status &&
+ isLinuxDiskEncryptionStatus(os_settings.disk_encryption.status)
+ ) {
+ rows.push(
+ generateLinuxDiskEncryptionSetting(
os_settings.disk_encryption.status,
os_settings.disk_encryption.detail
)
@@ -145,6 +177,10 @@ export const generateTableData = (
return makeWindowsRows(hostMDMData);
case "darwin":
return makeDarwinRows(hostMDMData);
+ case "ubuntu":
+ return makeLinuxRows(hostMDMData);
+ case "rhel":
+ return makeLinuxRows(hostMDMData);
case "ios":
return hostMDMData.profiles;
case "ipados":
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss
index dc330199451c..9469111a6ddb 100644
--- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss
+++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss
@@ -1,18 +1,22 @@
.os-settings-table {
-
// stylings for the table cells. This was the explicit width we want
// for these cells in the table. Total width of the table cell will be
// 240px including the padding.
- .data-table-block .data-table tbody td {
- .os-settings-name-cell {
- width: 135px;
- max-width: none;
- }
- .os-settings-status-cell {
- width: 200px;
+ .data-table-block .data-table {
+ &__wrapper {
+ width: initial;
}
- .os-settings-error-cell {
- width: 237px;
+ tbody td {
+ .os-settings-name-cell {
+ width: 135px;
+ max-width: none;
+ }
+ .os-settings-status-cell {
+ width: 200px;
+ }
+ .os-settings-error-cell {
+ width: 237px;
+ }
}
}
diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx
index 047050ba783b..bbf233f5d94e 100644
--- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx
+++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tests.tsx
@@ -74,39 +74,39 @@ describe("Host Summary section", () => {
});
});
- it("omit fleet desktop from tooltip if no fleet desktop version", async () => {
- const render = createCustomRenderer({
- context: {
- app: {
- isPremiumTier: true,
- isGlobalAdmin: true,
- currentUser: createMockUser(),
- },
- },
- });
- const summaryData = createMockHostSummary({
- fleet_desktop_version: null,
- });
- const orbitVersion = summaryData.orbit_version as string;
- const osqueryVersion = summaryData.osquery_version as string;
+ // it("omit fleet desktop from tooltip if no fleet desktop version", async () => {
+ // const render = createCustomRenderer({
+ // context: {
+ // app: {
+ // isPremiumTier: true,
+ // isGlobalAdmin: true,
+ // currentUser: createMockUser(),
+ // },
+ // },
+ // });
+ // const summaryData = createMockHostSummary({
+ // fleet_desktop_version: null,
+ // });
+ // const orbitVersion = summaryData.orbit_version as string;
+ // const osqueryVersion = summaryData.osquery_version as string;
- const { user } = render(
- null}
- />
- );
+ // const { user } = render(
+ // null}
+ // />
+ // );
- expect(screen.getByText("Agent")).toBeInTheDocument();
- await user.hover(screen.getByText(orbitVersion));
+ // expect(screen.getByText("Agent")).toBeInTheDocument();
+ // await user.hover(screen.getByText(orbitVersion));
- expect(
- screen.getByText(new RegExp(osqueryVersion, "i"))
- ).toBeInTheDocument();
- expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument();
- });
+ // expect(
+ // screen.getByText(new RegExp(osqueryVersion, "i"))
+ // ).toBeInTheDocument();
+ // expect(screen.queryByText(/Fleet desktop:/i)).not.toBeInTheDocument();
+ // });
it("for Chromebooks, render Agent header with osquery_version that is the fleetd chrome version and no tooltip", async () => {
const render = createCustomRenderer({
diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
index 63caddf9e0fa..1c9c8e52ad34 100644
--- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
+++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx
@@ -6,9 +6,17 @@ import {
IHostMdmProfile,
BootstrapPackageStatus,
isWindowsDiskEncryptionStatus,
+ isLinuxDiskEncryptionStatus,
} from "interfaces/mdm";
import { IOSSettings, IHostMaintenanceWindow } from "interfaces/host";
import { IAppleDeviceUpdates } from "interfaces/config";
+import {
+ DiskEncryptionSupportedPlatform,
+ isDiskEncryptionSupportedLinuxPlatform,
+ isOsSettingsDisplayPlatform,
+ platformSupportsDiskEncryption,
+} from "interfaces/platform";
+
import getHostStatusTooltipText from "pages/hosts/helpers";
import TooltipWrapper from "components/TooltipWrapper";
@@ -37,7 +45,8 @@ import BootstrapPackageIndicator from "./BootstrapPackageIndicator/BootstrapPack
import {
HostMdmDeviceStatusUIState,
- generateWinDiskEncryptionProfile,
+ generateLinuxDiskEncryptionSetting,
+ generateWinDiskEncryptionSetting,
} from "../../helpers";
import { DEVICE_STATUS_TAGS, REFETCH_TOOLTIP_MESSAGES } from "./helpers";
@@ -118,8 +127,7 @@ interface IHostSummaryProps {
isPremiumTier?: boolean;
toggleOSSettingsModal?: () => void;
toggleBootstrapPackageModal?: () => void;
- hostMdmProfiles?: IHostMdmProfile[];
- isConnectedToFleetMdm?: boolean;
+ hostSettings?: IHostMdmProfile[];
showRefetchSpinner: boolean;
onRefetchHost: (
evt: React.MouseEvent
@@ -131,7 +139,7 @@ interface IHostSummaryProps {
hostMdmDeviceStatus?: HostMdmDeviceStatusUIState;
}
-const MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES = {
+const DISK_ENCRYPTION_MESSAGES = {
darwin: {
enabled: (
<>
@@ -155,20 +163,28 @@ const MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES = {
),
disabled: "The disk is unencrypted.",
},
+ linux: {
+ enabled: "The disk is encrypted.",
+ unknown: "The disk may be encrypted.",
+ },
};
const getHostDiskEncryptionTooltipMessage = (
- platform: "darwin" | "windows" | "chrome", // TODO: improve this type
+ platform: DiskEncryptionSupportedPlatform, // TODO: improve this type
diskEncryptionEnabled = false
) => {
if (platform === "chrome") {
return "Fleet does not check for disk encryption on Chromebooks, as they are encrypted by default.";
}
- if (!["windows", "darwin"].includes(platform)) {
- return "Disk encryption is enabled.";
+ if (platform === "rhel" || platform === "ubuntu") {
+ return DISK_ENCRYPTION_MESSAGES.linux[
+ diskEncryptionEnabled ? "enabled" : "unknown"
+ ];
}
- return MAC_WINDOWS_DISK_ENCRYPTION_MESSAGES[platform][
+
+ // mac or windows
+ return DISK_ENCRYPTION_MESSAGES[platform][
diskEncryptionEnabled ? "enabled" : "disabled"
];
};
@@ -179,8 +195,7 @@ const HostSummary = ({
isPremiumTier,
toggleOSSettingsModal,
toggleBootstrapPackageModal,
- hostMdmProfiles,
- isConnectedToFleetMdm,
+ hostSettings,
showRefetchSpinner,
onRefetchHost,
renderActionDropdown,
@@ -192,6 +207,7 @@ const HostSummary = ({
const {
status,
platform,
+ os_version,
disk_encryption_enabled: diskEncryptionEnabled,
} = summaryData;
@@ -281,8 +297,7 @@ const HostSummary = ({
);
};
const renderDiskEncryptionSummary = () => {
- // TODO: improve this typing, platforms!
- if (!["darwin", "windows", "chrome"].includes(platform)) {
+ if (!platformSupportsDiskEncryption(platform, os_version)) {
return <>>;
}
const tooltipMessage = getHostDiskEncryptionTooltipMessage(
@@ -301,6 +316,11 @@ const HostSummary = ({
case diskEncryptionEnabled === false:
statusText = "Off";
break;
+ case (diskEncryptionEnabled === null ||
+ diskEncryptionEnabled === undefined) &&
+ platformSupportsDiskEncryption(platform, os_version):
+ statusText = "Unknown";
+ break;
default:
// something unexpected happened on the way to this component, display whatever we got or
// "Unknown" to draw attention to the issue.
@@ -441,21 +461,35 @@ const HostSummary = ({
};
const renderSummary = () => {
- // for windows hosts we have to manually add a profile for disk encryption
+ // for windows and linux 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.
+ // response for windows and linux hosts.
if (
platform === "windows" &&
osSettings?.disk_encryption?.status &&
isWindowsDiskEncryptionStatus(osSettings.disk_encryption.status)
) {
- const winDiskEncryptionProfile: IHostMdmProfile = generateWinDiskEncryptionProfile(
+ const winDiskEncryptionSetting: IHostMdmProfile = generateWinDiskEncryptionSetting(
+ osSettings.disk_encryption.status,
+ osSettings.disk_encryption.detail
+ );
+ hostSettings = hostSettings
+ ? [...hostSettings, winDiskEncryptionSetting]
+ : [winDiskEncryptionSetting];
+ }
+
+ if (
+ isDiskEncryptionSupportedLinuxPlatform(platform, os_version) &&
+ osSettings?.disk_encryption?.status &&
+ isLinuxDiskEncryptionStatus(osSettings.disk_encryption.status)
+ ) {
+ const linuxDiskEncryptionSetting: IHostMdmProfile = generateLinuxDiskEncryptionSetting(
osSettings.disk_encryption.status,
osSettings.disk_encryption.detail
);
- hostMdmProfiles = hostMdmProfiles
- ? [...hostMdmProfiles, winDiskEncryptionProfile]
- : [winDiskEncryptionProfile];
+ hostSettings = hostSettings
+ ? [...hostSettings, linuxDiskEncryptionSetting]
+ : [linuxDiskEncryptionSetting];
}
return (
@@ -484,19 +518,15 @@ const HostSummary = ({
renderIssues()}
{isPremiumTier && renderHostTeam()}
{/* Rendering of OS Settings data */}
- {(platform === "darwin" ||
- platform === "windows" ||
- platform === "ios" ||
- platform === "ipados") &&
+ {isOsSettingsDisplayPlatform(platform, os_version) &&
isPremiumTier &&
- isConnectedToFleetMdm && // show if 1 - host is enrolled in Fleet MDM, and
- hostMdmProfiles &&
- hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced
+ hostSettings &&
+ hostSettings.length > 0 && (
}
diff --git a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx
index 60cc9c069b6d..3a145e4f0e89 100644
--- a/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx
+++ b/frontend/pages/hosts/details/cards/HostSummary/OSSettingsIndicator/OSSettingsIndicator.tsx
@@ -47,7 +47,7 @@ const countHostProfilesByStatus = (
(acc, { status }) => {
if (status === "failed") {
acc.failed += 1;
- } else if (status === "pending") {
+ } else if (status === "pending" || status === "action_required") {
acc.pending += 1;
} else if (status === "verifying") {
acc.verifying += 1;
diff --git a/frontend/pages/hosts/details/helpers.ts b/frontend/pages/hosts/details/helpers.ts
index 52a0efd6ddaf..5c78a68bc9ad 100644
--- a/frontend/pages/hosts/details/helpers.ts
+++ b/frontend/pages/hosts/details/helpers.ts
@@ -2,12 +2,13 @@
import { HostMdmDeviceStatus, HostMdmPendingAction } from "interfaces/host";
import {
IHostMdmProfile,
- IWindowsDiskEncryptionStatus,
+ WindowsDiskEncryptionStatus,
MdmProfileStatus,
+ LinuxDiskEncryptionStatus,
} from "interfaces/mdm";
-const convertWinDiskEncryptionStatusToProfileStatus = (
- diskEncryptionStatus: IWindowsDiskEncryptionStatus
+const convertWinDiskEncryptionStatusToSettingStatus = (
+ diskEncryptionStatus: WindowsDiskEncryptionStatus
): MdmProfileStatus => {
return diskEncryptionStatus === "enforcing"
? "pending"
@@ -15,20 +16,40 @@ const convertWinDiskEncryptionStatusToProfileStatus = (
};
/**
- * Manually generates a profile for the windows disk encryption status. We need
+ * Manually generates a setting 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,
+export const generateWinDiskEncryptionSetting = (
+ diskEncryptionStatus: WindowsDiskEncryptionStatus,
detail: string
): IHostMdmProfile => {
return {
profile_uuid: "0", // This s the only type of profile that can have this value
platform: "windows",
name: "Disk Encryption",
- status: convertWinDiskEncryptionStatusToProfileStatus(diskEncryptionStatus),
+ status: convertWinDiskEncryptionStatusToSettingStatus(diskEncryptionStatus),
+ detail,
+ operation_type: null,
+ };
+};
+
+/**
+ * Manually generates a setting for the linux disk encryption status. We need
+ * this as we don't have a linux disk encryption setting in the `profiles`
+ * attribute coming back from the GET /hosts/:id API response.
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const generateLinuxDiskEncryptionSetting = (
+ diskEncryptionStatus: LinuxDiskEncryptionStatus,
+ detail: string
+): IHostMdmProfile => {
+ return {
+ profile_uuid: "0", // This s the only type of profile that can have this value
+ platform: "linux",
+ name: "Disk Encryption",
+ status: diskEncryptionStatus,
detail,
operation_type: null,
};
diff --git a/frontend/services/entities/disk_encryption.ts b/frontend/services/entities/disk_encryption.ts
new file mode 100644
index 000000000000..50a0eb63ef7d
--- /dev/null
+++ b/frontend/services/entities/disk_encryption.ts
@@ -0,0 +1,60 @@
+import sendRequest from "services";
+
+import endpoints from "utilities/endpoints";
+import { buildQueryStringFromParams } from "utilities/url";
+
+// TODO - move disk encryption types like this to dedicated file
+import { DiskEncryptionStatus } from "interfaces/mdm";
+import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
+
+export interface IDiskEncryptionStatusAggregate {
+ macos: number;
+ windows: number;
+ linux: number;
+}
+
+export type IDiskEncryptionSummaryResponse = Record<
+ DiskEncryptionStatus,
+ IDiskEncryptionStatusAggregate
+>;
+
+const diskEncryptionService = {
+ getDiskEncryptionSummary: (teamId?: number) => {
+ let { MDM_DISK_ENCRYPTION_SUMMARY: path } = endpoints;
+
+ if (teamId) {
+ path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
+ }
+ return sendRequest("GET", path);
+ },
+ updateDiskEncryption: (enableDiskEncryption: boolean, teamId?: number) => {
+ // TODO - use same endpoint for both once issue with new endpoint for no team is resolved
+ const {
+ UPDATE_DISK_ENCRYPTION: teamsEndpoint,
+ CONFIG: noTeamsEndpoint,
+ } = endpoints;
+ if (teamId === 0) {
+ return sendRequest("PATCH", noTeamsEndpoint, {
+ mdm: {
+ enable_disk_encryption: enableDiskEncryption,
+ },
+ });
+ }
+ return sendRequest("POST", teamsEndpoint, {
+ enable_disk_encryption: enableDiskEncryption,
+ // TODO - it would be good to be able to use an API_CONTEXT_NO_TEAM_ID here, but that is
+ // currently set to 0, which should actually be undefined since the server expects teamId ==
+ // nil for no teams, not 0.
+ team_id: teamId === APP_CONTEXT_NO_TEAM_ID ? undefined : teamId,
+ });
+ },
+ triggerLinuxDiskEncryptionKeyEscrow: (token: string) => {
+ const { DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW } = endpoints;
+ return sendRequest(
+ "POST",
+ DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW(token)
+ );
+ },
+};
+
+export default diskEncryptionService;
diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts
index 50ed3bd428a8..aab412772ae7 100644
--- a/frontend/services/entities/mdm.ts
+++ b/frontend/services/entities/mdm.ts
@@ -1,5 +1,4 @@
import {
- DiskEncryptionStatus,
IHostMdmProfile,
IMdmCommandResult,
IMdmProfile,
@@ -21,16 +20,6 @@ export interface IEulaMetadataResponse {
export type ProfileStatusSummaryResponse = Record;
-export interface IDiskEncryptionStatusAggregate {
- macos: number;
- windows: number;
-}
-
-export type IDiskEncryptionSummaryResponse = Record<
- DiskEncryptionStatus,
- IDiskEncryptionStatusAggregate
->;
-
export interface IGetProfilesApiParams {
page?: number;
per_page?: number;
@@ -179,7 +168,7 @@ const mdmService = {
},
getProfilesStatusSummary: (teamId: number) => {
- let { MDM_PROFILES_STATUS_SUMMARY: path } = endpoints;
+ let { PROFILES_STATUS_SUMMARY: path } = endpoints;
if (teamId) {
path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
@@ -188,37 +177,6 @@ const mdmService = {
return sendRequest("GET", path);
},
- 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,
- CONFIG: noTeamsEndpoint,
- } = endpoints;
- 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,
- },
- });
- }
- return sendRequest("PATCH", teamsEndpoint, {
- enable_disk_encryption: enableDiskEncryption,
- team_id: teamId,
- });
- },
-
initiateMDMAppleSSO: () => {
const { MDM_APPLE_SSO } = endpoints;
return sendRequest("POST", MDM_APPLE_SSO, {});
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index bb6b3dea65b9..a1acd94adee5 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -34,6 +34,9 @@ export default {
DEVICE_USER_MDM_ENROLLMENT_PROFILE: (token: string): string => {
return `/${API_VERSION}/fleet/device/${token}/mdm/apple/manual_enrollment_profile`;
},
+ DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW: (token: string): string => {
+ return `/${API_VERSION}/fleet/device/${token}/mdm/linux/trigger_escrow`;
+ },
// Host endpoints
HOST_SUMMARY: `/${API_VERSION}/fleet/host_summary`,
@@ -111,7 +114,7 @@ export default {
MDM_PROFILE: (id: string) => `/${API_VERSION}/fleet/mdm/profiles/${id}`,
MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`,
- MDM_PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/mdm/profiles/summary`,
+ PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/configuration_profiles/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) => {
@@ -138,6 +141,9 @@ export default {
ME: `/${API_VERSION}/fleet/me`,
+ // Disk encryption endpoints
+ UPDATE_DISK_ENCRYPTION: `/${API_VERSION}/fleet/disk_encryption`,
+
// Setup experiece endpoints
MDM_SETUP_EXPERIENCE: `/${API_VERSION}/fleet/setup_experience`,
MDM_SETUP_EXPERIENCE_SOFTWARE: `/${API_VERSION}/fleet/setup_experience/software`,
diff --git a/go.mod b/go.mod
index 822b953698bc..aa464c81f768 100644
--- a/go.mod
+++ b/go.mod
@@ -67,6 +67,7 @@ require (
github.com/jmoiron/sqlx v1.3.5
github.com/josephspurrier/goversioninfo v1.4.0
github.com/kevinburke/go-bindata v3.24.0+incompatible
+ github.com/klauspost/compress v1.17.9
github.com/kolide/launcher v1.0.12
github.com/lib/pq v1.10.9
github.com/macadmins/osquery-extension v1.2.1
@@ -92,11 +93,12 @@ require (
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/rs/zerolog v1.32.0
github.com/russellhaering/goxmldsig v1.2.0
- github.com/saferwall/pe v1.5.2
+ github.com/saferwall/pe v1.5.5
github.com/sassoftware/relic/v8 v8.0.1
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9
github.com/sethvargo/go-password v0.3.0
github.com/shirou/gopsutil/v3 v3.24.3
+ github.com/siderolabs/go-blockdevice/v2 v2.0.3
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/smallstep/pkcs7 v0.0.0-20240723090913-5e2c6a136dfa
github.com/smallstep/scep v0.0.0-20240214080410-892e41795b99
@@ -181,6 +183,7 @@ require (
github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect
github.com/antchfx/xpath v1.2.2 // indirect
github.com/apache/thrift v0.18.1 // indirect
+ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/atc0005/go-teams-notify/v2 v2.6.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
@@ -266,7 +269,6 @@ require (
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
- github.com/klauspost/compress v1.17.8 // indirect
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
@@ -292,9 +294,11 @@ require (
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect
github.com/secure-systems-lab/go-securesystemslib v0.5.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
+ github.com/siderolabs/go-cmd v0.1.1 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/slack-go/slack v0.9.4 // indirect
github.com/spf13/afero v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index 36ba36ea7db0..1b90fec76b5b 100644
--- a/go.sum
+++ b/go.sum
@@ -233,6 +233,8 @@ github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
+github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -460,6 +462,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8=
github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
+github.com/freddierice/go-losetup/v2 v2.0.1 h1:wPDx/Elu9nDV8y/CvIbEDz5Xi5Zo80y4h7MKbi3XaAI=
+github.com/freddierice/go-losetup/v2 v2.0.1/go.mod h1:TEyBrvlOelsPEhfWD5rutNXDmUszBXuFnwT1kIQF4J8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -817,8 +821,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab h1:KVR7cs+oPyy85i+8t1ZaNSy1bymCy5FuWyt51pdrXu4=
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY=
github.com/kolide/launcher v1.0.12 h1:f2uT1kKYGIbj/WVsHDc10f7MIiwu8MpmgwaGaT7D09k=
@@ -1034,14 +1038,16 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
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/saferwall/pe v1.5.2 h1:h5lLtLsyxGHQ9dN6cd8EfeLEBEo5gdqJpkuw4o4vTMY=
-github.com/saferwall/pe v1.5.2/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s=
+github.com/saferwall/pe v1.5.5 h1:GGbzKjXDm7i+1K6riOgtgblyTdRmTbr3r11IzjovAK8=
+github.com/saferwall/pe v1.5.5/go.mod h1:mJx+PuptmNpoPFBNhWs/uDMFL/kTHVZIkg0d4OUJFbQ=
github.com/sassoftware/relic/v8 v8.0.1 h1:uYUoaoTQMs67up8/46NgrSxSftgfY4VWBusDVg56k7I=
github.com/sassoftware/relic/v8 v8.0.1/go.mod h1:s/MwugRcovgYcNJNOyvLfqRHDX7iArHtFtUR9kEodz8=
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/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
+github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc=
+github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA=
github.com/secure-systems-lab/go-securesystemslib v0.5.0 h1:oTiNu0QnulMQgN/hLK124wJD/r2f9ZhIUuKIeBsCBT8=
github.com/secure-systems-lab/go-securesystemslib v0.5.0/go.mod h1:uoCqUC0Ap7jrBSEanxT+SdACYJTVplRXWLkGMuDjXqk=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -1057,6 +1063,12 @@ github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/siderolabs/gen v0.5.0 h1:Afdjx+zuZDf53eH5DB+E+T2JeCwBXGinV66A6osLgQI=
+github.com/siderolabs/gen v0.5.0/go.mod h1:1GUMBNliW98Xeq8GPQeVMYqQE09LFItE8enR3wgMh3Q=
+github.com/siderolabs/go-blockdevice/v2 v2.0.3 h1:IEgDqd3H3gPphahrdvfAzU8RmD4r5eQdWC+vgFQQoEg=
+github.com/siderolabs/go-blockdevice/v2 v2.0.3/go.mod h1:74htzCV913UzaLZ4H+NBXkwWlYnBJIq5m/379ZEcu8w=
+github.com/siderolabs/go-cmd v0.1.1 h1:nTouZUSxLeiiEe7hFexSVvaTsY/3O8k1s08BxPRrsps=
+github.com/siderolabs/go-cmd v0.1.1/go.mod h1:6hY0JG34LxEEwYE8aH2iIHkHX/ir12VRLqfwAf2yJIY=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf
index f81ed35ca06c..896e7474e727 100644
--- a/infrastructure/dogfood/terraform/aws/variables.tf
+++ b/infrastructure/dogfood/terraform/aws/variables.tf
@@ -56,7 +56,7 @@ variable "database_name" {
variable "fleet_image" {
description = "the name of the container image to run"
- default = "fleetdm/fleet:v4.59.0"
+ default = "fleetdm/fleet:v4.60.1"
}
variable "software_inventory" {
diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf
index 6e690ee8a54a..eb0391b392d4 100644
--- a/infrastructure/dogfood/terraform/gcp/variables.tf
+++ b/infrastructure/dogfood/terraform/gcp/variables.tf
@@ -68,7 +68,7 @@ variable "redis_mem" {
}
variable "image" {
- default = "fleetdm/fleet:v4.59.0"
+ default = "fleetdm/fleet:v4.60.1"
}
variable "software_installers_bucket_name" {
diff --git a/infrastructure/guardduty/.terraform.lock.hcl b/infrastructure/guardduty/.terraform.lock.hcl
index c58c51094906..1f3b9a6b8471 100644
--- a/infrastructure/guardduty/.terraform.lock.hcl
+++ b/infrastructure/guardduty/.terraform.lock.hcl
@@ -2,8 +2,8 @@
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
- version = "4.59.0"
- constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.0"
+ version = "4.60.1"
+ constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.1"
hashes = [
"h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=",
"zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4",
diff --git a/infrastructure/guardduty/main.tf b/infrastructure/guardduty/main.tf
index b20182929594..a68123626f23 100644
--- a/infrastructure/guardduty/main.tf
+++ b/infrastructure/guardduty/main.tf
@@ -2,7 +2,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = "~> 4.59.0"
+ version = "~> 4.60.1"
}
}
backend "s3" {
diff --git a/infrastructure/infrastructure/cloudtrail/main.tf b/infrastructure/infrastructure/cloudtrail/main.tf
index cce55a6999df..a8232723e7c5 100644
--- a/infrastructure/infrastructure/cloudtrail/main.tf
+++ b/infrastructure/infrastructure/cloudtrail/main.tf
@@ -2,7 +2,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = "~> 4.59.0"
+ version = "~> 4.60.1"
}
}
backend "s3" {
diff --git a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl
index ec59d9ece644..3bf60fe7cc31 100644
--- a/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl
+++ b/infrastructure/infrastructure/elastic-agent/.terraform.lock.hcl
@@ -2,8 +2,8 @@
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
- version = "4.59.0"
- constraints = ">= 3.63.0, ~> 4.59.0"
+ version = "4.60.1"
+ constraints = ">= 3.63.0, ~> 4.60.1"
hashes = [
"h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=",
"zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4",
diff --git a/infrastructure/infrastructure/elastic-agent/main.tf b/infrastructure/infrastructure/elastic-agent/main.tf
index ae7ef8d89026..383fd562cfb2 100644
--- a/infrastructure/infrastructure/elastic-agent/main.tf
+++ b/infrastructure/infrastructure/elastic-agent/main.tf
@@ -20,7 +20,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = "~> 4.59.0"
+ version = "~> 4.60.1"
}
}
backend "s3" {
diff --git a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl
index c58c51094906..1f3b9a6b8471 100644
--- a/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl
+++ b/infrastructure/infrastructure/guardduty-alerts/.terraform.lock.hcl
@@ -2,8 +2,8 @@
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/aws" {
- version = "4.59.0"
- constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.0"
+ version = "4.60.1"
+ constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.1"
hashes = [
"h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=",
"zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4",
diff --git a/infrastructure/infrastructure/guardduty-alerts/main.tf b/infrastructure/infrastructure/guardduty-alerts/main.tf
index 1e1ee8abe2c3..d39ad9e1817b 100644
--- a/infrastructure/infrastructure/guardduty-alerts/main.tf
+++ b/infrastructure/infrastructure/guardduty-alerts/main.tf
@@ -15,7 +15,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = "~> 4.59.0"
+ version = "~> 4.60.1"
}
}
backend "s3" {
diff --git a/infrastructure/infrastructure/spend_alerts/main.tf b/infrastructure/infrastructure/spend_alerts/main.tf
index 1754cfdf8cdf..203822163486 100644
--- a/infrastructure/infrastructure/spend_alerts/main.tf
+++ b/infrastructure/infrastructure/spend_alerts/main.tf
@@ -2,7 +2,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = "~> 4.59.0"
+ version = "~> 4.60.1"
}
}
backend "s3" {
diff --git a/orbit/changes/22047-linux-key-escrow b/orbit/changes/22047-linux-key-escrow
new file mode 100644
index 000000000000..d8a3daa001a2
--- /dev/null
+++ b/orbit/changes/22047-linux-key-escrow
@@ -0,0 +1 @@
+* added functionality to support linux disk encryption key escrow including end user prompts and LUKS key management
\ No newline at end of file
diff --git a/orbit/changes/22810-fleetd-enroll-activity b/orbit/changes/22810-fleetd-enroll-activity
new file mode 100644
index 000000000000..2b99a1a8608e
--- /dev/null
+++ b/orbit/changes/22810-fleetd-enroll-activity
@@ -0,0 +1,2 @@
+Added computer_name and hardware_model for fleetd enrollment.
+Added serial number for fleetd enrollment for Windows hosts (already present for macOS and Linux).
diff --git a/orbit/changes/add-codesign-table b/orbit/changes/add-codesign-table
new file mode 100644
index 000000000000..49b38025d636
--- /dev/null
+++ b/orbit/changes/add-codesign-table
@@ -0,0 +1 @@
+* Added `codesign` table to provide the "Team identifier" of macOS applications.
diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go
index da467570bad9..1be45d7cfb5a 100644
--- a/orbit/cmd/orbit/orbit.go
+++ b/orbit/cmd/orbit/orbit.go
@@ -26,6 +26,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/installer"
"github.com/fleetdm/fleet/v4/orbit/pkg/keystore"
"github.com/fleetdm/fleet/v4/orbit/pkg/logging"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/luks"
"github.com/fleetdm/fleet/v4/orbit/pkg/osquery"
"github.com/fleetdm/fleet/v4/orbit/pkg/osservice"
"github.com/fleetdm/fleet/v4/orbit/pkg/platform"
@@ -38,6 +39,7 @@ import (
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
"github.com/fleetdm/fleet/v4/orbit/pkg/update/filestore"
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/zenity"
"github.com/fleetdm/fleet/v4/pkg/certificate"
"github.com/fleetdm/fleet/v4/pkg/file"
retrypkg "github.com/fleetdm/fleet/v4/pkg/retry"
@@ -694,6 +696,8 @@ func main() {
HardwareUUID: osqueryHostInfo.HardwareUUID,
Hostname: osqueryHostInfo.Hostname,
Platform: osqueryHostInfo.Platform,
+ ComputerName: osqueryHostInfo.ComputerName,
+ HardwareModel: osqueryHostInfo.HardwareModel,
}
if runtime.GOOS == "darwin" {
@@ -737,13 +741,6 @@ func main() {
orbitHostInfo.OsqueryIdentifier = osqueryHostInfo.InstanceID
}
- // The hardware serial was not sent when Windows MDM was implemented,
- // thus we clear its value here to not break any existing enroll functionality
- // on the server.
- if runtime.GOOS == "windows" {
- orbitHostInfo.HardwareSerial = ""
- }
-
var (
options []osquery.Option
// optionsAfterFlagfile is populated with options that will be set after the '--flagfile' argument
@@ -940,6 +937,8 @@ func main() {
case "windows":
orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMEnrollmentFetcherMiddleware(windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient))
orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMBitlockerFetcherMiddleware(windowsMDMBitlockerCommandFrequency, orbitClient))
+ case "linux":
+ orbitClient.RegisterConfigReceiver(luks.New(orbitClient, zenity.New()))
}
flagUpdateReceiver := update.NewFlagReceiver(orbitClient.TriggerOrbitRestart, update.FlagUpdateOptions{
@@ -1697,6 +1696,10 @@ type osqueryHostInfo struct {
HardwareSerial string `json:"hardware_serial"`
// Hostname is the device's hostname (extracted from `system_info` osquery table).
Hostname string `json:"hostname"`
+ // ComputerName is the friendly computer name (optional) (extracted from `system_info` osquery table).
+ ComputerName string `json:"computer_name"`
+ // HardwareModel is the device's hardware model (extracted from `system_info` osquery table).
+ HardwareModel string `json:"hardware_model"`
// Platform is the device's platform as defined by osquery (extracted from `os_version` osquery table).
Platform string `json:"platform"`
// InstanceID is the osquery's randomly generated instance ID
@@ -1714,7 +1717,18 @@ func getHostInfo(osqueryPath string, osqueryDBPath string) (*osqueryHostInfo, er
if err := os.MkdirAll(filepath.Dir(osqueryDBPath), constant.DefaultDirMode); err != nil {
return nil, err
}
- const systemQuery = "SELECT si.uuid, si.hardware_serial, si.hostname, os.platform, os.version as os_version, oi.instance_id, oi.version as osquery_version FROM system_info si, os_version os, osquery_info oi"
+ const systemQuery = `
+ SELECT
+ si.uuid,
+ si.hardware_serial,
+ si.hostname,
+ si.computer_name,
+ si.hardware_model,
+ os.platform,
+ os.version as os_version,
+ oi.instance_id,
+ oi.version as osquery_version
+ FROM system_info si, os_version os, osquery_info oi`
args := []string{
"-S",
"--database_path", osqueryDBPath,
diff --git a/orbit/pkg/dialog/dialog.go b/orbit/pkg/dialog/dialog.go
new file mode 100644
index 000000000000..362c77b0b691
--- /dev/null
+++ b/orbit/pkg/dialog/dialog.go
@@ -0,0 +1,66 @@
+package dialog
+
+import (
+ "context"
+ "errors"
+ "time"
+)
+
+var (
+ // ErrCanceled is returned when the dialog is canceled by the cancel button.
+ ErrCanceled = errors.New("dialog canceled")
+ // ErrTimeout is returned when the dialog is automatically closed due to a timeout.
+ ErrTimeout = errors.New("dialog timed out")
+ // ErrUnknown is returned when an unknown error occurs.
+ ErrUnknown = errors.New("unknown error")
+)
+
+// Dialog represents a UI dialog that can be displayed to the end user
+// on a host
+type Dialog interface {
+ // ShowEntry displays a dialog that accepts end user input. It returns the entered
+ // text or errors ErrCanceled, ErrTimeout, or ErrUnknown.
+ ShowEntry(ctx context.Context, opts EntryOptions) ([]byte, error)
+ // ShowInfo displays a dialog that displays information. It returns an error if the dialog
+ // could not be displayed.
+ ShowInfo(ctx context.Context, opts InfoOptions) error
+ // Progress displays a dialog that shows progress. It waits until the
+ // context is cancelled.
+ ShowProgress(ctx context.Context, opts ProgressOptions) error
+}
+
+// EntryOptions represents options for a dialog that accepts end user input.
+type EntryOptions struct {
+ // Title sets the title of the dialog.
+ Title string
+
+ // Text sets the text of the dialog.
+ Text string
+
+ // HideText hides the text entered by the user.
+ HideText bool
+
+ // TimeOut sets the time in seconds before the dialog is automatically closed.
+ TimeOut time.Duration
+}
+
+// InfoOptions represents options for a dialog that displays information.
+type InfoOptions struct {
+ // Title sets the title of the dialog.
+ Title string
+
+ // Text sets the text of the dialog.
+ Text string
+
+ // Timeout sets the time in seconds before the dialog is automatically closed.
+ TimeOut time.Duration
+}
+
+// ProgressOptions represents options for a dialog that shows progress.
+type ProgressOptions struct {
+ // Title sets the title of the dialog.
+ Title string
+
+ // Text sets the text of the dialog.
+ Text string
+}
diff --git a/orbit/pkg/execuser/execuser.go b/orbit/pkg/execuser/execuser.go
index 5dc188ea99d8..e598bdc2aaaa 100644
--- a/orbit/pkg/execuser/execuser.go
+++ b/orbit/pkg/execuser/execuser.go
@@ -19,10 +19,6 @@ func WithEnv(name, value string) Option {
}
// WithArg sets command line arguments for the application.
-//
-// TODO: for now CLI arguments are only used by the darwin
-// implementation, just because it's the only platform that needs
-// them.
func WithArg(name, value string) Option {
return func(a *eopts) {
a.args = append(a.args, [2]string{name, value})
@@ -40,3 +36,16 @@ func Run(path string, opts ...Option) (lastLogs string, err error) {
}
return run(path, o)
}
+
+// RunWithOutput runs an application as the current login user and returns its output.
+// It assumes the caller is running with high privileges (root on UNIX).
+//
+// It blocks until the child process exits.
+// Non ExitError errors return with a -1 exitCode.
+func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, err error) {
+ var o eopts
+ for _, fn := range opts {
+ fn(&o)
+ }
+ return runWithOutput(path, o)
+}
diff --git a/orbit/pkg/execuser/execuser_darwin.go b/orbit/pkg/execuser/execuser_darwin.go
index 7902b2c761b2..6641b40604f7 100644
--- a/orbit/pkg/execuser/execuser_darwin.go
+++ b/orbit/pkg/execuser/execuser_darwin.go
@@ -1,6 +1,7 @@
package execuser
import (
+ "errors"
"fmt"
"io"
"os"
@@ -8,6 +9,8 @@ import (
)
// run uses macOS open command to start application as the current login user.
+// Note that the child process spawns a new process in user space and thus it is not
+// effective to add a context to this function to cancel the child process.
func run(path string, opts eopts) (lastLogs string, err error) {
info, err := os.Stat(path)
if err != nil {
@@ -47,3 +50,7 @@ func run(path string, opts eopts) (lastLogs string, err error) {
}
return tw.String(), nil
}
+
+func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
+ return nil, 0, errors.New("not implemented")
+}
diff --git a/orbit/pkg/execuser/execuser_linux.go b/orbit/pkg/execuser/execuser_linux.go
index 3ed91d7a62ec..5ce487c23c6a 100644
--- a/orbit/pkg/execuser/execuser_linux.go
+++ b/orbit/pkg/execuser/execuser_linux.go
@@ -18,9 +18,71 @@ import (
// run uses sudo to run the given path as login user.
func run(path string, opts eopts) (lastLogs string, err error) {
+ args, err := getUserAndDisplayArgs(path, opts)
+ if err != nil {
+ return "", fmt.Errorf("get args: %w", err)
+ }
+
+ args = append(args,
+ // Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH.
+ //
+ // Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to
+ // keep this to support older versions of Fleet Desktop.
+ fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")),
+ path,
+ )
+
+ if len(opts.args) > 0 {
+ for _, arg := range opts.args {
+ args = append(args, arg[0], arg[1])
+ }
+ }
+
+ cmd := exec.Command("sudo", args...)
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ log.Printf("cmd=%s", cmd.String())
+
+ if err := cmd.Start(); err != nil {
+ return "", fmt.Errorf("open path %q: %w", path, err)
+ }
+ return "", nil
+}
+
+// run uses sudo to run the given path as login user and waits for the process to finish.
+func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
+ args, err := getUserAndDisplayArgs(path, opts)
+ if err != nil {
+ return nil, -1, fmt.Errorf("get args: %w", err)
+ }
+
+ args = append(args, path)
+
+ if len(opts.args) > 0 {
+ for _, arg := range opts.args {
+ args = append(args, arg[0], arg[1])
+ }
+ }
+
+ cmd := exec.Command("sudo", args...)
+ log.Printf("cmd=%s", cmd.String())
+
+ output, err = cmd.Output()
+ if err != nil {
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ exitCode = exitErr.ExitCode()
+ return output, exitCode, fmt.Errorf("%q exited with code %d: %w", path, exitCode, err)
+ }
+ return output, -1, fmt.Errorf("%q error: %w", path, err)
+ }
+
+ return output, exitCode, nil
+}
+
+func getUserAndDisplayArgs(path string, opts eopts) ([]string, error) {
user, err := getLoginUID()
if err != nil {
- return "", fmt.Errorf("get user: %w", err)
+ return nil, fmt.Errorf("get user: %w", err)
}
// TODO(lucas): Default to display :0 if user DISPLAY environment variable
@@ -68,23 +130,9 @@ func run(path string, opts eopts) (lastLogs string, err error) {
// This is required for Ubuntu 18, and not required for Ubuntu 21/22
// (because it's already part of the user).
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id),
- // Append the packaged libayatana-appindicator3 libraries path to LD_LIBRARY_PATH.
- //
- // Fleet Desktop doesn't use libayatana-appindicator3 since 1.18.3, but we need to
- // keep this to support older versions of Fleet Desktop.
- fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")),
- path,
)
- cmd := exec.Command("sudo", args...)
- cmd.Stderr = os.Stderr
- cmd.Stdout = os.Stdout
- log.Printf("cmd=%s", cmd.String())
-
- if err := cmd.Start(); err != nil {
- return "", fmt.Errorf("open path %q: %w", path, err)
- }
- return "", nil
+ return args, nil
}
type user struct {
diff --git a/orbit/pkg/execuser/execuser_windows.go b/orbit/pkg/execuser/execuser_windows.go
index 90e274b7a32d..9cf7e9d33855 100644
--- a/orbit/pkg/execuser/execuser_windows.go
+++ b/orbit/pkg/execuser/execuser_windows.go
@@ -117,6 +117,10 @@ func run(path string, opts eopts) (lastLogs string, err error) {
return "", startProcessAsCurrentUser(path, "", "")
}
+func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
+ return nil, 0, errors.New("not implemented")
+}
+
// getCurrentUserSessionId will attempt to resolve
// the session ID of the user currently active on
// the system.
diff --git a/orbit/pkg/luks/luks.go b/orbit/pkg/luks/luks.go
new file mode 100644
index 000000000000..6376b24eaa83
--- /dev/null
+++ b/orbit/pkg/luks/luks.go
@@ -0,0 +1,37 @@
+package luks
+
+import (
+ "github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
+)
+
+type KeyEscrower interface {
+ SendLinuxKeyEscrowResponse(LuksResponse) error
+}
+
+type LuksRunner struct {
+ escrower KeyEscrower
+ notifier dialog.Dialog
+}
+
+type LuksResponse struct {
+ // Passphrase is a newly created passphrase generated by fleetd for securing the LUKS volume.
+ // This passphrase will be securely escrowed to the server.
+ Passphrase string
+
+ // KeySlot specifies the LUKS key slot where this new passphrase was created.
+ // It is currently not used, but may be useful in the future for passphrase rotation.
+ KeySlot *uint
+
+ // Salt is the salt used to generate the LUKS key.
+ Salt string
+
+ // Err is the error message that occurred during the escrow process.
+ Err string
+}
+
+func New(escrower KeyEscrower, notifier dialog.Dialog) *LuksRunner {
+ return &LuksRunner{
+ escrower: escrower,
+ notifier: notifier,
+ }
+}
diff --git a/orbit/pkg/luks/luks_linux.go b/orbit/pkg/luks/luks_linux.go
new file mode 100644
index 000000000000..c45cb74ebffd
--- /dev/null
+++ b/orbit/pkg/luks/luks_linux.go
@@ -0,0 +1,332 @@
+//go:build linux
+
+package luks
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math/big"
+ "os/exec"
+ "regexp"
+ "time"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/lvm"
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/rs/zerolog/log"
+ "github.com/siderolabs/go-blockdevice/v2/encryption"
+ luksdevice "github.com/siderolabs/go-blockdevice/v2/encryption/luks"
+)
+
+const (
+ entryDialogTitle = "Enter disk encryption passphrase"
+ entryDialogText = "Passphrase:"
+ retryEntryDialogText = "Passphrase incorrect. Please try again."
+ infoTitle = "Disk encryption"
+ infoFailedText = "Failed to escrow key. Please try again later."
+ infoSuccessText = "Success! Now, return to your browser window and follow the instructions to verify disk encryption."
+ timeoutMessage = "Please visit Fleet Desktop > My device and click Create key"
+ maxKeySlots = 8
+ userKeySlot = 0 // Key slot 0 is assumed to be the location of the user's passphrase
+)
+
+var ErrKeySlotFull = regexp.MustCompile(`Key slot \d+ is full`)
+
+func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error {
+ ctx := context.Background()
+
+ if !oc.Notifications.RunDiskEncryptionEscrow {
+ return nil
+ }
+
+ devicePath, err := lvm.FindRootDisk()
+ if err != nil {
+ return fmt.Errorf("Failed to find LUKS Root Partition: %w", err)
+ }
+
+ var response LuksResponse
+ key, keyslot, err := lr.getEscrowKey(ctx, devicePath)
+ if err != nil {
+ response.Err = err.Error()
+ }
+
+ if len(key) == 0 && err == nil {
+ // dialog was canceled or timed out
+ return nil
+ }
+
+ response.Passphrase = string(key)
+ response.KeySlot = keyslot
+
+ if keyslot != nil {
+ salt, err := getSaltforKeySlot(ctx, devicePath, *keyslot)
+ if err != nil {
+ if err := removeKeySlot(ctx, devicePath, *keyslot); err != nil {
+ log.Error().Err(err).Msgf("failed to remove key slot %d", *keyslot)
+ }
+ return fmt.Errorf("Failed to get salt for key slot: %w", err)
+ }
+ response.Salt = salt
+ }
+
+ if err := lr.escrower.SendLinuxKeyEscrowResponse(response); err != nil {
+ // If sending the response fails, remove the key slot
+ if keyslot != nil {
+ if err := removeKeySlot(ctx, devicePath, *keyslot); err != nil {
+ log.Error().Err(err).Msg("failed to remove key slot")
+ }
+ }
+
+ // Show error in dialog
+ if err := lr.infoPrompt(ctx, infoTitle, infoFailedText); err != nil {
+ log.Info().Err(err).Msg("failed to show failed escrow key dialog")
+ }
+
+ return fmt.Errorf("escrower escrowKey err: %w", err)
+ }
+
+ if response.Err != "" {
+ if err := lr.infoPrompt(ctx, infoTitle, response.Err); err != nil {
+ log.Info().Err(err).Msg("failed to show response error dialog")
+ }
+ return fmt.Errorf("error getting linux escrow key: %s", response.Err)
+ }
+
+ // Show success dialog
+ if err := lr.infoPrompt(ctx, infoTitle, infoSuccessText); err != nil {
+ log.Info().Err(err).Msg("failed to show success escrow key dialog")
+ }
+
+ return nil
+}
+
+func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]byte, *uint, error) {
+ // AESXTSPlain64Cipher is the default cipher used by ubuntu/kubuntu/fedora
+ device := luksdevice.New(luksdevice.AESXTSPlain64Cipher)
+
+ // Prompt user for existing LUKS passphrase
+ passphrase, err := lr.entryPrompt(ctx, entryDialogTitle, entryDialogText)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err)
+ }
+
+ if len(passphrase) == 0 {
+ log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out")
+ return nil, nil, nil
+ }
+
+ err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
+ Title: infoTitle,
+ Text: "Validating passphrase...",
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("failed to show progress dialog")
+ }
+
+ // Validate the passphrase
+ for {
+ valid, err := lr.passphraseIsValid(ctx, device, devicePath, passphrase, userKeySlot)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Failed validating passphrase: %w", err)
+ }
+
+ if valid {
+ break
+ }
+
+ passphrase, err = lr.entryPrompt(ctx, entryDialogTitle, retryEntryDialogText)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err)
+ }
+
+ if len(passphrase) == 0 {
+ log.Debug().Msg("Passphrase is empty, no password supplied, dialog was canceled, or timed out")
+ return nil, nil, nil
+ }
+
+ err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
+ Title: infoTitle,
+ Text: "Validating passphrase...",
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("failed to show progress dialog after retry")
+ }
+ }
+
+ err = lr.notifier.ShowProgress(ctx, dialog.ProgressOptions{
+ Title: infoTitle,
+ Text: "Key escrow in progress...",
+ })
+ if err != nil {
+ log.Error().Err(err).Msg("failed to show progress dialog")
+ }
+
+ escrowPassphrase, err := generateRandomPassphrase()
+ if err != nil {
+ return nil, nil, fmt.Errorf("Failed to generate random passphrase: %w", err)
+ }
+
+ // Create a new key slot and error if all key slots are full
+ // Start at slot 1 as keySlot 0 is assumed to be the location of
+ // the user's passphrase
+ var keySlot uint = userKeySlot + 1
+ for {
+ if keySlot == maxKeySlots {
+ return nil, nil, errors.New("all LUKS key slots are full")
+ }
+
+ userKey := encryption.NewKey(userKeySlot, passphrase)
+ escrowKey := encryption.NewKey(int(keySlot), escrowPassphrase) // #nosec G115
+
+ if err := device.AddKey(ctx, devicePath, userKey, escrowKey); err != nil {
+ if ErrKeySlotFull.MatchString(err.Error()) {
+ keySlot++
+ continue
+ }
+ return nil, nil, fmt.Errorf("Failed to add key: %w", err)
+ }
+
+ break
+ }
+
+ valid, err := lr.passphraseIsValid(ctx, device, devicePath, escrowPassphrase, keySlot)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Error while validating escrow passphrase: %w", err)
+ }
+
+ if !valid {
+ return nil, nil, errors.New("Failed to validate escrow passphrase")
+ }
+
+ return escrowPassphrase, &keySlot, nil
+}
+
+func (lr *LuksRunner) passphraseIsValid(ctx context.Context, device *luksdevice.LUKS, devicePath string, passphrase []byte, keyslot uint) (bool, error) {
+ if len(passphrase) == 0 {
+ return false, nil
+ }
+
+ valid, err := device.CheckKey(ctx, devicePath, encryption.NewKey(int(keyslot), passphrase)) // #nosec G115
+ if err != nil {
+ return false, fmt.Errorf("Error validating passphrase: %w", err)
+ }
+
+ return valid, nil
+}
+
+// generateRandomPassphrase generates a random passphrase with 32 characters
+// in the format XXXX-XXXX-XXXX-XXXX where X is a random character from the
+// set [0-9A-Za-z].
+func generateRandomPassphrase() ([]byte, error) {
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ const length = 35 // 32 characters + 3 dashes
+ passphrase := make([]byte, length)
+
+ for i := 0; i < length; i++ {
+ // Insert dashes at positions 8, 17, and 26
+ if i == 8 || i == 17 || i == 26 {
+ passphrase[i] = '-'
+ continue
+ }
+
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
+ if err != nil {
+ return nil, err
+ }
+ passphrase[i] = chars[num.Int64()]
+ }
+
+ return passphrase, nil
+}
+
+func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]byte, error) {
+ passphrase, err := lr.notifier.ShowEntry(ctx, dialog.EntryOptions{
+ Title: title,
+ Text: text,
+ HideText: true,
+ TimeOut: 1 * time.Minute,
+ })
+ if err != nil {
+ switch {
+ case errors.Is(err, dialog.ErrCanceled):
+ log.Debug().Msg("end user canceled key escrow dialog")
+ return nil, nil
+ case errors.Is(err, dialog.ErrTimeout):
+ log.Debug().Msg("key escrow dialog timed out")
+ err := lr.infoPrompt(ctx, infoTitle, timeoutMessage)
+ if err != nil {
+ log.Info().Err(err).Msg("failed to show timeout dialog")
+ }
+ return nil, nil
+ case errors.Is(err, dialog.ErrUnknown):
+ return nil, err
+ default:
+ return nil, err
+ }
+ }
+
+ return passphrase, nil
+}
+
+func (lr *LuksRunner) infoPrompt(ctx context.Context, title, text string) error {
+ err := lr.notifier.ShowInfo(ctx, dialog.InfoOptions{
+ Title: title,
+ Text: text,
+ TimeOut: 1 * time.Minute,
+ })
+ if err != nil {
+ switch {
+ case errors.Is(err, dialog.ErrTimeout):
+ log.Debug().Msg("successPrompt timed out")
+ return nil
+ default:
+ return err
+ }
+ }
+
+ return nil
+}
+
+type LuksDump struct {
+ Keyslots map[string]Keyslot `json:"keyslots"`
+}
+
+type Keyslot struct {
+ KDF KDF `json:"kdf"`
+}
+
+type KDF struct {
+ Salt string `json:"salt"`
+}
+
+func getSaltforKeySlot(ctx context.Context, devicePath string, keySlot uint) (string, error) {
+ cmd := exec.CommandContext(ctx, "cryptsetup", "luksDump", "--dump-json-metadata", devicePath)
+ output, err := cmd.Output()
+ if err != nil {
+ return "", fmt.Errorf("Failed to run cryptsetup luksDump: %w", err)
+ }
+
+ var dump LuksDump
+ if err := json.Unmarshal(output, &dump); err != nil {
+ return "", fmt.Errorf("Failed to unmarshal luksDump output: %w", err)
+ }
+
+ slot, ok := dump.Keyslots[fmt.Sprintf("%d", keySlot)]
+ if !ok {
+ return "", errors.New("key slot not found")
+ }
+
+ return slot.KDF.Salt, nil
+}
+
+func removeKeySlot(ctx context.Context, devicePath string, keySlot uint) error {
+ cmd := exec.CommandContext(ctx, "cryptsetup", "luksKillSlot", devicePath, fmt.Sprintf("%d", keySlot)) // #nosec G204
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("Failed to run cryptsetup luksKillSlot: %w", err)
+ }
+
+ return nil
+}
diff --git a/orbit/pkg/luks/luks_stub.go b/orbit/pkg/luks/luks_stub.go
new file mode 100644
index 000000000000..4358df26c744
--- /dev/null
+++ b/orbit/pkg/luks/luks_stub.go
@@ -0,0 +1,13 @@
+//go:build !linux
+// +build !linux
+
+package luks
+
+import (
+ "github.com/fleetdm/fleet/v4/server/fleet"
+)
+
+// Run is a placeholder method for non-Linux builds.
+func (lr *LuksRunner) Run(oc *fleet.OrbitConfig) error {
+ return nil
+}
diff --git a/orbit/pkg/lvm/lvm.go b/orbit/pkg/lvm/lvm.go
new file mode 100644
index 000000000000..c662d80d78fd
--- /dev/null
+++ b/orbit/pkg/lvm/lvm.go
@@ -0,0 +1,104 @@
+package lvm
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os/exec"
+)
+
+type BlockDevice struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Mountpoints []string `json:"mountpoints"`
+ Children []BlockDevice `json:"children,omitempty"`
+}
+
+// FindRootDisk finds the physical partition that
+// contains the root filesystem mounted at "/" on a
+// LVM Linux volume.
+func FindRootDisk() (string, error) {
+ cmd := exec.Command("lsblk", "--json")
+ var out bytes.Buffer
+ cmd.Stdout = &out
+
+ if err := cmd.Run(); err != nil {
+ return "", fmt.Errorf("failed to run lsblk: %w", err)
+ }
+
+ return rootDiskFromJson(out)
+}
+
+func rootDiskFromJson(input bytes.Buffer) (string, error) {
+ var data struct {
+ Blockdevices []BlockDevice `json:"blockdevices"`
+ }
+
+ if err := json.Unmarshal(input.Bytes(), &data); err != nil {
+ return "", fmt.Errorf("failed to unmarshal JSON: %w", err)
+ }
+
+ // Find the root partition mounted at "/"
+ rootPartition := findRootPartition(data.Blockdevices)
+ if rootPartition == nil {
+ return "", errors.New("root partition not found")
+ }
+
+ // Trace up to the nearest parent partition of type "part" (partition)
+ physicalPartition := findParentPartitionOfTypePart(data.Blockdevices, rootPartition)
+ if physicalPartition == nil {
+ return "", errors.New("physical partition of type 'part' not found")
+ }
+
+ return fmt.Sprintf("/dev/%s", physicalPartition.Name), nil
+}
+
+// findRootPartition recursively searches for the partition
+// mounted at "/" within the device tree.
+func findRootPartition(devices []BlockDevice) *BlockDevice {
+ for _, device := range devices {
+ if result := searchForRoot(device); result != nil {
+ return result
+ }
+ }
+ return nil
+}
+
+// searchForRoot recursively checks each device and its children
+// to find the one mounted at "/".
+func searchForRoot(device BlockDevice) *BlockDevice {
+ for _, mountpoint := range device.Mountpoints {
+ if mountpoint == "/" {
+ return &device
+ }
+ }
+ for _, child := range device.Children {
+ if result := searchForRoot(child); result != nil {
+ return result
+ }
+ }
+ return nil
+}
+
+// findParentPartitionOfTypePart traverses upwards from the given device to find the nearest "part" type parent.
+func findParentPartitionOfTypePart(devices []BlockDevice, target *BlockDevice) *BlockDevice {
+ var queue []BlockDevice
+ queue = append(queue, devices...)
+
+ for len(queue) > 0 {
+ current := queue[0]
+ queue = queue[1:]
+
+ for _, child := range current.Children {
+ if child.Name == target.Name {
+ if current.Type == "part" {
+ return ¤t
+ }
+ return findParentPartitionOfTypePart(devices, ¤t)
+ }
+ queue = append(queue, child)
+ }
+ }
+ return nil
+}
diff --git a/orbit/pkg/lvm/lvm_test.go b/orbit/pkg/lvm/lvm_test.go
new file mode 100644
index 000000000000..73058caf09c8
--- /dev/null
+++ b/orbit/pkg/lvm/lvm_test.go
@@ -0,0 +1,344 @@
+package lvm
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// sample from real LUKS encrypted Ubuntu disk
+var testJsonUbuntu = `{
+ "blockdevices": [
+ {
+ "name": "loop0",
+ "maj:min": "7:0",
+ "rm": false,
+ "size": "4K",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/bare/5"
+ ]
+ },{
+ "name": "loop1",
+ "maj:min": "7:1",
+ "rm": false,
+ "size": "74.3M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/core22/1564"
+ ]
+ },{
+ "name": "loop2",
+ "maj:min": "7:2",
+ "rm": false,
+ "size": "73.9M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/core22/1663"
+ ]
+ },{
+ "name": "loop3",
+ "maj:min": "7:3",
+ "rm": false,
+ "size": "269.8M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/firefox/4793"
+ ]
+ },{
+ "name": "loop4",
+ "maj:min": "7:4",
+ "rm": false,
+ "size": "10.7M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/firmware-updater/127"
+ ]
+ },{
+ "name": "loop5",
+ "maj:min": "7:5",
+ "rm": false,
+ "size": "11.1M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/firmware-updater/147"
+ ]
+ },{
+ "name": "loop6",
+ "maj:min": "7:6",
+ "rm": false,
+ "size": "505.1M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/gnome-42-2204/176"
+ ]
+ },{
+ "name": "loop7",
+ "maj:min": "7:7",
+ "rm": false,
+ "size": "91.7M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/gtk-common-themes/1535"
+ ]
+ },{
+ "name": "loop8",
+ "maj:min": "7:8",
+ "rm": false,
+ "size": "10.7M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/snap-store/1218"
+ ]
+ },{
+ "name": "loop9",
+ "maj:min": "7:9",
+ "rm": false,
+ "size": "10.5M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/snap-store/1173"
+ ]
+ },{
+ "name": "loop10",
+ "maj:min": "7:10",
+ "rm": false,
+ "size": "38.8M",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/snapd/21759"
+ ]
+ },{
+ "name": "loop11",
+ "maj:min": "7:11",
+ "rm": false,
+ "size": "500K",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/snapd-desktop-integration/178"
+ ]
+ },{
+ "name": "loop12",
+ "maj:min": "7:12",
+ "rm": false,
+ "size": "568K",
+ "ro": true,
+ "type": "loop",
+ "mountpoints": [
+ "/snap/snapd-desktop-integration/253"
+ ]
+ },{
+ "name": "nvme0n1",
+ "maj:min": "259:0",
+ "rm": false,
+ "size": "476.9G",
+ "ro": false,
+ "type": "disk",
+ "mountpoints": [
+ null
+ ],
+ "children": [
+ {
+ "name": "nvme0n1p1",
+ "maj:min": "259:1",
+ "rm": false,
+ "size": "1G",
+ "ro": false,
+ "type": "part",
+ "mountpoints": [
+ "/boot/efi"
+ ]
+ },{
+ "name": "nvme0n1p2",
+ "maj:min": "259:2",
+ "rm": false,
+ "size": "2G",
+ "ro": false,
+ "type": "part",
+ "mountpoints": [
+ "/boot"
+ ]
+ },{
+ "name": "nvme0n1p3",
+ "maj:min": "259:3",
+ "rm": false,
+ "size": "473.9G",
+ "ro": false,
+ "type": "part",
+ "mountpoints": [
+ null
+ ],
+ "children": [
+ {
+ "name": "dm_crypt-0",
+ "maj:min": "252:0",
+ "rm": false,
+ "size": "473.9G",
+ "ro": false,
+ "type": "crypt",
+ "mountpoints": [
+ null
+ ],
+ "children": [
+ {
+ "name": "ubuntu--vg-ubuntu--lv",
+ "maj:min": "252:1",
+ "rm": false,
+ "size": "473.9G",
+ "ro": false,
+ "type": "lvm",
+ "mountpoints": [
+ "/"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}`
+
+var testJsonFedora = `{
+ "blockdevices": [
+ {
+ "name": "sr0",
+ "maj:min": "11:0",
+ "rm": true,
+ "size": "2.1G",
+ "ro": false,
+ "type": "rom",
+ "mountpoints": [
+ "/run/media/luk/Fedora-WS-Live-40-1-14"
+ ]
+ },{
+ "name": "zram0",
+ "maj:min": "252:0",
+ "rm": false,
+ "size": "1.9G",
+ "ro": false,
+ "type": "disk",
+ "mountpoints": [
+ "[SWAP]"
+ ]
+ },{
+ "name": "nvme0n1",
+ "maj:min": "259:0",
+ "rm": false,
+ "size": "20G",
+ "ro": false,
+ "type": "disk",
+ "mountpoints": [
+ null
+ ],
+ "children": [
+ {
+ "name": "nvme0n1p1",
+ "maj:min": "259:1",
+ "rm": false,
+ "size": "600M",
+ "ro": false,
+ "type": "part",
+ "mountpoints": [
+ "/boot/efi"
+ ]
+ },{
+ "name": "nvme0n1p2",
+ "maj:min": "259:2",
+ "rm": false,
+ "size": "1G",
+ "ro": false,
+ "type": "part",
+ "mountpoints": [
+ "/boot"
+ ]
+ },{
+ "name": "nvme0n1p3",
+ "maj:min": "259:3",
+ "rm": false,
+ "size": "18.4G",
+ "ro": false,
+ "type": "part",
+ "mountpoints": [
+ null
+ ],
+ "children": [
+ {
+ "name": "luks-21fc9b67-752e-42fb-83bb-8c92864382e9",
+ "maj:min": "253:0",
+ "rm": false,
+ "size": "18.4G",
+ "ro": false,
+ "type": "crypt",
+ "mountpoints": [
+ "/home", "/"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}`
+
+func TestFindRootDisk(t *testing.T) {
+ var input bytes.Buffer
+ _, err := input.WriteString(testJsonUbuntu)
+ assert.NoError(t, err)
+
+ output, err := rootDiskFromJson(input)
+ assert.NoError(t, err)
+ assert.Equal(t, "/dev/nvme0n1p3", output)
+
+ input = bytes.Buffer{}
+ _, err = input.WriteString(testJsonFedora)
+ assert.NoError(t, err)
+
+ output, err = rootDiskFromJson(input)
+ assert.NoError(t, err)
+ assert.Equal(t, "/dev/nvme0n1p3", output)
+}
+
+func TestErrorNoMountPoint(t *testing.T) {
+ var input bytes.Buffer
+ _, err := input.WriteString(`{"blockdevices": [{"name": "nvme0n1", "mountpoints": [null]}]}`)
+ assert.NoError(t, err)
+
+ output, err := rootDiskFromJson(input)
+ assert.Error(t, err)
+ assert.Empty(t, output)
+}
+
+func TestErrorNoRootPartition(t *testing.T) {
+ var input bytes.Buffer
+ _, err := input.WriteString(`{"blockdevices": [{"name": "nvme0n1", "mountpoints": ["/boot"]}]}`)
+ assert.NoError(t, err)
+
+ output, err := rootDiskFromJson(input)
+ assert.Error(t, err)
+ assert.Empty(t, output)
+}
+
+func TestErrorInvalidJson(t *testing.T) {
+ var input bytes.Buffer
+ _, err := input.WriteString(`{`)
+ assert.NoError(t, err)
+
+ output, err := rootDiskFromJson(input)
+ assert.Error(t, err)
+ assert.Empty(t, output)
+}
diff --git a/orbit/pkg/table/codesign/codesign_darwin.go b/orbit/pkg/table/codesign/codesign_darwin.go
new file mode 100644
index 000000000000..e1e8b26c9abd
--- /dev/null
+++ b/orbit/pkg/table/codesign/codesign_darwin.go
@@ -0,0 +1,100 @@
+//go:build darwin
+// +build darwin
+
+// Package codesign implements an extension osquery table
+// to get signature information of macOS applications.
+package codesign
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "errors"
+ "os/exec"
+ "strings"
+
+ "github.com/osquery/osquery-go/plugin/table"
+ "github.com/rs/zerolog/log"
+)
+
+// Columns is the schema of the table.
+func Columns() []table.ColumnDefinition {
+ return []table.ColumnDefinition{
+ // path is the absolute path to the app bundle.
+ // It's required and only supports the equality operator.
+ table.TextColumn("path"),
+ // team_identifier is the "Team ID", aka "Signature ID", "Developer ID".
+ // The value is "" if the app doesn't have a team identifier set.
+ // (this is the case for example for builtin Apple apps).
+ //
+ // See https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/.
+ table.TextColumn("team_identifier"),
+ }
+}
+
+// Generate is called to return the results for the table at query time.
+//
+// Constraints for generating can be retrieved from the queryContext.
+func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
+ constraints, ok := queryContext.Constraints["path"]
+ if !ok || len(constraints.Constraints) == 0 {
+ return nil, errors.New("missing path")
+ }
+
+ var paths []string
+ for _, constraint := range constraints.Constraints {
+ if constraint.Operator != table.OperatorEquals {
+ return nil, errors.New("only supported operator for 'path' is '='")
+ }
+ paths = append(paths, constraint.Expression)
+ }
+
+ var rows []map[string]string
+ for _, path := range paths {
+ row := map[string]string{
+ "path": path,
+ "team_identifier": "",
+ }
+ output, err := exec.CommandContext(ctx, "/usr/bin/codesign",
+ // `codesign --display` does not perform any verification of executables/resources,
+ // it just parses and displays signature information read from the `Contents` folder.
+ "--display",
+ // If we don't set verbose it only prints the executable path.
+ "--verbose",
+ path,
+ ).CombinedOutput() // using CombinedOutput because output is in stderr and stdout is empty.
+ if err != nil {
+ // Logging as debug to prevent non signed apps to generate a lot of logged errors.
+ log.Debug().Err(err).Str("output", string(output)).Str("path", path).Msg("codesign --display failed")
+ rows = append(rows, row)
+ continue
+ }
+ info := parseCodesignOutput(output)
+ row["team_identifier"] = info.teamIdentifier
+ rows = append(rows, row)
+ }
+
+ return rows, nil
+}
+
+type parsedInfo struct {
+ teamIdentifier string
+}
+
+func parseCodesignOutput(output []byte) parsedInfo {
+ const teamIdentifierPrefix = "TeamIdentifier="
+
+ scanner := bufio.NewScanner(bytes.NewReader(output))
+ var info parsedInfo
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, teamIdentifierPrefix) {
+ info.teamIdentifier = strings.TrimSpace(strings.TrimPrefix(line, teamIdentifierPrefix))
+ // "not set" is usually displayed on Apple builtin apps.
+ if info.teamIdentifier == "not set" {
+ info.teamIdentifier = ""
+ }
+ }
+ }
+ return info
+}
diff --git a/orbit/pkg/table/extension_darwin.go b/orbit/pkg/table/extension_darwin.go
index 18bdc6884f5f..59f0b240077f 100644
--- a/orbit/pkg/table/extension_darwin.go
+++ b/orbit/pkg/table/extension_darwin.go
@@ -6,6 +6,7 @@ import (
"context"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/table/codesign"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/csrutil_info"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/dataflattentable"
"github.com/fleetdm/fleet/v4/orbit/pkg/table/diskutil/apfs"
@@ -92,6 +93,8 @@ func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) {
// Table for parsing Apple Property List files, which are typically stored in ~/Library/Preferences/
dataflattentable.TablePlugin(log.Logger, dataflattentable.PlistType), // table name is "parse_plist"
+
+ table.NewPlugin("codesign", codesign.Columns(), codesign.Generate),
}
// append platform specific tables
diff --git a/orbit/pkg/zenity/zenity.go b/orbit/pkg/zenity/zenity.go
new file mode 100644
index 000000000000..2d2989f9d804
--- /dev/null
+++ b/orbit/pkg/zenity/zenity.go
@@ -0,0 +1,161 @@
+package zenity
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/platform"
+ "github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
+ "github.com/rs/zerolog/log"
+)
+
+const zenityProcessName = "zenity"
+
+type Zenity struct {
+ // cmdWithOutput can be set in tests to mock execution of the dialog.
+ cmdWithOutput func(ctx context.Context, args ...string) ([]byte, int, error)
+ // cmdWithWait can be set in tests to mock execution of the dialog.
+ cmdWithWait func(ctx context.Context, args ...string) error
+ // killZenityFunc can be set in tests to mock killing the zenity process.
+ killZenityFunc func()
+}
+
+// New creates a new Zenity dialog instance for zenity v4 on Linux.
+// Zenity implements the Dialog interface.
+func New() *Zenity {
+ return &Zenity{
+ cmdWithOutput: execCmdWithOutput,
+ cmdWithWait: execCmdWithWait,
+ killZenityFunc: killZenityProcesses,
+ }
+}
+
+// ShowEntry displays an dialog that accepts end user input. It returns the entered
+// text or errors ErrCanceled, ErrTimeout, or ErrUnknown.
+func (z *Zenity) ShowEntry(ctx context.Context, opts dialog.EntryOptions) ([]byte, error) {
+ z.killZenityFunc()
+
+ args := []string{"--entry"}
+ if opts.Title != "" {
+ args = append(args, fmt.Sprintf("--title=%s", opts.Title))
+ }
+ if opts.Text != "" {
+ args = append(args, fmt.Sprintf("--text=%s", opts.Text))
+ }
+ if opts.HideText {
+ args = append(args, "--hide-text")
+ }
+ if opts.TimeOut > 0 {
+ args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds())))
+ }
+
+ output, statusCode, err := z.cmdWithOutput(ctx, args...)
+ if err != nil {
+ switch statusCode {
+ case 1:
+ return nil, dialog.ErrCanceled
+ case 5:
+ return nil, dialog.ErrTimeout
+ default:
+ return nil, ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
+ }
+ }
+
+ return output, nil
+}
+
+// ShowInfo displays an information dialog. It returns errors ErrTimeout or ErrUnknown.
+func (z *Zenity) ShowInfo(ctx context.Context, opts dialog.InfoOptions) error {
+ z.killZenityFunc()
+
+ args := []string{"--info"}
+ if opts.Title != "" {
+ args = append(args, fmt.Sprintf("--title=%s", opts.Title))
+ }
+ if opts.Text != "" {
+ args = append(args, fmt.Sprintf("--text=%s", opts.Text))
+ }
+ if opts.TimeOut > 0 {
+ args = append(args, fmt.Sprintf("--timeout=%d", int(opts.TimeOut.Seconds())))
+ }
+
+ _, statusCode, err := z.cmdWithOutput(ctx, args...)
+ if err != nil {
+ switch statusCode {
+ case 5:
+ return ctxerr.Wrap(ctx, dialog.ErrTimeout)
+ default:
+ return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
+ }
+ }
+
+ return nil
+}
+
+// ShowProgress starts a Zenity progress dialog with the given options.
+// This function is designed to block until the provided context is canceled.
+// It is intended to be used within a separate goroutine to avoid blocking
+// the main execution flow.
+//
+// If the context is already canceled, the function will return immediately.
+//
+// Use this function for cases where a progress dialog is needed to run
+// alongside other operations, with explicit cancellation or termination.
+func (z *Zenity) ShowProgress(ctx context.Context, opts dialog.ProgressOptions) error {
+ z.killZenityFunc()
+
+ args := []string{"--progress"}
+ if opts.Title != "" {
+ args = append(args, fmt.Sprintf("--title=%s", opts.Title))
+ }
+ if opts.Text != "" {
+ args = append(args, fmt.Sprintf("--text=%s", opts.Text))
+ }
+
+ // --pulsate shows a pulsating progress bar
+ args = append(args, "--pulsate")
+
+ // --no-cancel disables the cancel button
+ args = append(args, "--no-cancel")
+
+ err := z.cmdWithWait(ctx, args...)
+ if err != nil {
+ return ctxerr.Wrap(ctx, dialog.ErrUnknown, err.Error())
+ }
+
+ return nil
+}
+
+func execCmdWithOutput(ctx context.Context, args ...string) ([]byte, int, error) {
+ var opts []execuser.Option
+ for _, arg := range args {
+ opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
+ }
+
+ output, exitCode, err := execuser.RunWithOutput(zenityProcessName, opts...)
+
+ // Trim the newline from zenity output
+ output = bytes.TrimSuffix(output, []byte("\n"))
+
+ return output, exitCode, err
+}
+
+func execCmdWithWait(ctx context.Context, args ...string) error {
+ var opts []execuser.Option
+ for _, arg := range args {
+ opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
+ }
+
+ _, err := execuser.Run(zenityProcessName, opts...)
+ return err
+}
+
+func killZenityProcesses() {
+ _, err := platform.KillAllProcessByName(zenityProcessName)
+ if err != nil {
+ log.Warn().Err(err).Msg("failed to kill zenity process")
+ }
+}
diff --git a/orbit/pkg/zenity/zenity_test.go b/orbit/pkg/zenity/zenity_test.go
new file mode 100644
index 000000000000..f7b2337f8cc2
--- /dev/null
+++ b/orbit/pkg/zenity/zenity_test.go
@@ -0,0 +1,275 @@
+package zenity
+
+import (
+ "context"
+ "os/exec"
+ "testing"
+ "time"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
+ "github.com/stretchr/testify/require"
+ "github.com/tj/assert"
+)
+
+type mockExecCmd struct {
+ output []byte
+ exitCode int
+ capturedArgs []string
+ waitDuration time.Duration
+}
+
+// MockCommandContext simulates exec.CommandContext and captures arguments
+func (m *mockExecCmd) runWithOutput(ctx context.Context, args ...string) ([]byte, int, error) {
+ m.capturedArgs = append(m.capturedArgs, args...)
+
+ if m.exitCode != 0 {
+ return nil, m.exitCode, &exec.ExitError{}
+ }
+
+ return m.output, m.exitCode, nil
+}
+
+func (m *mockExecCmd) runWithWait(ctx context.Context, args ...string) error {
+ m.capturedArgs = append(m.capturedArgs, args...)
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(m.waitDuration):
+
+ }
+
+ return nil
+}
+
+func TestShowEntryArgs(t *testing.T) {
+ ctx := context.Background()
+
+ testCases := []struct {
+ name string
+ opts dialog.EntryOptions
+ expectedArgs []string
+ }{
+ {
+ name: "Basic Entry",
+ opts: dialog.EntryOptions{
+ Title: "A Title",
+ Text: "Some text",
+ },
+ expectedArgs: []string{"--entry", "--title=A Title", "--text=Some text"},
+ },
+ {
+ name: "All Options",
+ opts: dialog.EntryOptions{
+ Title: "Another Title",
+ Text: "Some more text",
+ HideText: true,
+ TimeOut: 1 * time.Minute,
+ },
+ expectedArgs: []string{"--entry", "--title=Another Title", "--text=Some more text", "--hide-text", "--timeout=60"},
+ },
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ mock := &mockExecCmd{
+ output: []byte("some output"),
+ }
+ z := &Zenity{
+ cmdWithOutput: mock.runWithOutput,
+ killZenityFunc: func() {},
+ }
+ output, err := z.ShowEntry(ctx, tt.opts)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedArgs, mock.capturedArgs)
+ assert.Equal(t, []byte("some output"), output)
+ })
+ }
+}
+
+func TestShowEntryError(t *testing.T) {
+ ctx := context.Background()
+
+ testcases := []struct {
+ name string
+ exitCode int
+ expectedErr error
+ }{
+ {
+ name: "Dialog Cancelled",
+ exitCode: 1,
+ expectedErr: dialog.ErrCanceled,
+ },
+ {
+ name: "Dialog Timed Out",
+ exitCode: 5,
+ expectedErr: dialog.ErrTimeout,
+ },
+ {
+ name: "Unknown Error",
+ exitCode: 99,
+ expectedErr: dialog.ErrUnknown,
+ },
+ }
+
+ for _, tt := range testcases {
+ t.Run(tt.name, func(t *testing.T) {
+ mock := &mockExecCmd{
+ exitCode: tt.exitCode,
+ }
+ z := &Zenity{
+ cmdWithOutput: mock.runWithOutput,
+ killZenityFunc: func() {},
+ }
+ output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
+ require.ErrorIs(t, err, tt.expectedErr)
+ assert.Nil(t, output)
+ })
+ }
+}
+
+func TestShowEntrySuccess(t *testing.T) {
+ ctx := context.Background()
+
+ mock := &mockExecCmd{
+ output: []byte("some output"),
+ }
+ z := &Zenity{
+ cmdWithOutput: mock.runWithOutput,
+ killZenityFunc: func() {},
+ }
+ output, err := z.ShowEntry(ctx, dialog.EntryOptions{})
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("some output"), output)
+}
+
+func TestShowInfoArgs(t *testing.T) {
+ ctx := context.Background()
+
+ testCases := []struct {
+ name string
+ opts dialog.InfoOptions
+ expectedArgs []string
+ }{
+ {
+ name: "Basic Entry",
+ opts: dialog.InfoOptions{},
+ expectedArgs: []string{"--info"},
+ },
+ {
+ name: "All Options",
+ opts: dialog.InfoOptions{
+ Title: "Another Title",
+ Text: "Some more text",
+ TimeOut: 1 * time.Minute,
+ },
+ expectedArgs: []string{"--info", "--title=Another Title", "--text=Some more text", "--timeout=60"},
+ },
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ mock := &mockExecCmd{}
+ z := &Zenity{
+ cmdWithOutput: mock.runWithOutput,
+ killZenityFunc: func() {},
+ }
+ err := z.ShowInfo(ctx, tt.opts)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedArgs, mock.capturedArgs)
+ })
+ }
+}
+
+func TestShowInfoError(t *testing.T) {
+ ctx := context.Background()
+
+ testcases := []struct {
+ name string
+ exitCode int
+ expectedErr error
+ }{
+ {
+ name: "Dialog Timed Out",
+ exitCode: 5,
+ expectedErr: dialog.ErrTimeout,
+ },
+ {
+ name: "Unknown Error",
+ exitCode: 99,
+ expectedErr: dialog.ErrUnknown,
+ },
+ }
+
+ for _, tt := range testcases {
+ t.Run(tt.name, func(t *testing.T) {
+ mock := &mockExecCmd{
+ exitCode: tt.exitCode,
+ }
+ z := &Zenity{
+ cmdWithOutput: mock.runWithOutput,
+ killZenityFunc: func() {},
+ }
+ err := z.ShowInfo(ctx, dialog.InfoOptions{})
+ require.ErrorIs(t, err, tt.expectedErr)
+ })
+ }
+}
+
+func TestProgressArgs(t *testing.T) {
+ ctx := context.Background()
+
+ testCases := []struct {
+ name string
+ opts dialog.ProgressOptions
+ expectedArgs []string
+ }{
+ {
+ name: "Basic Entry",
+ opts: dialog.ProgressOptions{
+ Title: "A Title",
+ Text: "Some text",
+ },
+ expectedArgs: []string{"--progress", "--title=A Title", "--text=Some text", "--pulsate", "--no-cancel"},
+ },
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ mock := &mockExecCmd{}
+ z := &Zenity{
+ cmdWithWait: mock.runWithWait,
+ killZenityFunc: func() {},
+ }
+ err := z.ShowProgress(ctx, tt.opts)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedArgs, mock.capturedArgs)
+ })
+ }
+}
+
+func TestProgressKillOnCancel(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+
+ mock := &mockExecCmd{
+ waitDuration: 5 * time.Second,
+ }
+ z := &Zenity{
+ cmdWithWait: mock.runWithWait,
+ killZenityFunc: func() {},
+ }
+
+ done := make(chan struct{})
+ start := time.Now()
+
+ go func() {
+ _ = z.ShowProgress(ctx, dialog.ProgressOptions{})
+ close(done)
+ }()
+
+ time.Sleep(100 * time.Millisecond)
+ cancel()
+ <-done
+
+ assert.True(t, time.Since(start) < 5*time.Second)
+}
diff --git a/pkg/file/pe.go b/pkg/file/pe.go
index 27e872221d00..4516b5a25e15 100644
--- a/pkg/file/pe.go
+++ b/pkg/file/pe.go
@@ -48,21 +48,51 @@ func ExtractPEMetadata(tfr *fleet.TempFileReader) (*InstallerMetadata, error) {
return nil, fmt.Errorf("error parsing PE file: %w", err)
}
- v, err := pep.ParseVersionResources()
+ resources, err := pep.ParseVersionResourcesForEntries()
if err != nil {
return nil, fmt.Errorf("error parsing PE version resources: %w", err)
}
- name := strings.TrimSpace(v["ProductName"])
+ var name, version, sfxName, sfxVersion string
+
+ for _, e := range resources {
+ productName, ok := e["ProductName"]
+ if !ok {
+ productName = e["productname"] // used by Opera SFX (self-extracting archive)
+ }
+ productVersion := strings.TrimSpace(e["ProductVersion"])
+ if productName != "" {
+ productName = strings.TrimSpace(productName)
+ if productName == "7-Zip" {
+ // This may be a 7-Zip self-extracting archive.
+ sfxName = productName
+ sfxVersion = productVersion
+ continue
+ }
+ name = productName
+ }
+ if productVersion != "" {
+ version = productVersion
+ }
+ }
+ if name == "" && sfxName != "" {
+ // If we didn't find a ProductName, we may be
+ // dealing with an archive executable (e.g., if we're dealing with the 7-Zip executable itself rather than Opera)
+ name = sfxName
+ if sfxVersion != "" {
+ version = sfxVersion
+ }
+ }
+
return applySpecialCases(&InstallerMetadata{
Name: name,
- Version: strings.TrimSpace(v["ProductVersion"]),
+ Version: version,
PackageIDs: []string{name},
SHASum: h.Sum(nil),
- }, v), nil
+ }, resources), nil
}
-var exeSpecialCases = map[string]func(*InstallerMetadata, map[string]string) *InstallerMetadata{
- "Notion": func(meta *InstallerMetadata, resources map[string]string) *InstallerMetadata {
+var exeSpecialCases = map[string]func(*InstallerMetadata, []map[string]string) *InstallerMetadata{
+ "Notion": func(meta *InstallerMetadata, _ []map[string]string) *InstallerMetadata {
if meta.Version != "" {
meta.Name = meta.Name + " " + meta.Version
}
@@ -82,7 +112,7 @@ var exeSpecialCases = map[string]func(*InstallerMetadata, map[string]string) *In
// least for the most popular apps that use unusual naming.
//
// See https://github.com/fleetdm/fleet/issues/20440#issuecomment-2260500661
-func applySpecialCases(meta *InstallerMetadata, resources map[string]string) *InstallerMetadata {
+func applySpecialCases(meta *InstallerMetadata, resources []map[string]string) *InstallerMetadata {
if fn := exeSpecialCases[meta.Name]; fn != nil {
return fn(meta, resources)
}
diff --git a/schema/osquery_fleet_schema.json b/schema/osquery_fleet_schema.json
index b538b1a00295..4624549bf730 100644
--- a/schema/osquery_fleet_schema.json
+++ b/schema/osquery_fleet_schema.json
@@ -4171,6 +4171,31 @@
"url": "https://fleetdm.com/tables/cis_audit",
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/cis_audit.yml"
},
+ {
+ "name": "codesign",
+ "platforms": [
+ "darwin"
+ ],
+ "description": "Retrieves codesign information of a given .app path. It doesn't perform (expensive) verification, it just parses the signature from the 'Contents' folder using the \"codesign --display\" command.",
+ "columns": [
+ {
+ "name": "path",
+ "type": "text",
+ "required": true,
+ "description": "Path is the absolute path to the app folder."
+ },
+ {
+ "name": "team_identifier",
+ "type": "text",
+ "required": false,
+ "description": "Unique 10-character string generated by Apple that's assigned to a developer account to sign packages. This value is empty on unsigned applications and built-in Apple applications."
+ }
+ ],
+ "notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
+ "evented": false,
+ "url": "https://fleetdm.com/tables/codesign",
+ "fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/codesign.yml"
+ },
{
"name": "connected_displays",
"description": "Provides information about the connected displays of the machine.",
diff --git a/schema/tables/codesign.yml b/schema/tables/codesign.yml
new file mode 100644
index 000000000000..532da9bc4e8d
--- /dev/null
+++ b/schema/tables/codesign.yml
@@ -0,0 +1,15 @@
+name: codesign
+platforms:
+ - darwin
+description: Retrieves codesign information of a given .app path. It doesn't perform (expensive) verification, it just parses the signature from the 'Contents' folder using the "codesign --display" command.
+columns:
+ - name: path
+ type: text
+ required: true
+ description: Path is the absolute path to the app folder.
+ - name: team_identifier
+ type: text
+ required: false
+ description: Unique 10-character string generated by Apple that's assigned to a developer account to sign packages. This value is empty on unsigned applications and built-in Apple applications.
+notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
+evented: false
diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go
index 8f8d708eb908..5de25d4d0006 100644
--- a/server/datastore/mysql/app_configs.go
+++ b/server/datastore/mysql/app_configs.go
@@ -274,7 +274,7 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet
return secrets, nil
}
-func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) {
+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 {
diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go
index dc0d4b1c9d9d..46df41010be8 100644
--- a/server/datastore/mysql/app_configs_test.go
+++ b/server/datastore/mysql/app_configs_test.go
@@ -449,7 +449,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.False(t, ac.MDM.EnableDiskEncryption.Value)
- enabled, err := ds.getConfigEnableDiskEncryption(ctx, nil)
+ enabled, err := ds.GetConfigEnableDiskEncryption(ctx, nil)
require.NoError(t, err)
require.False(t, enabled)
@@ -461,7 +461,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.True(t, ac.MDM.EnableDiskEncryption.Value)
- enabled, err = ds.getConfigEnableDiskEncryption(ctx, nil)
+ enabled, err = ds.GetConfigEnableDiskEncryption(ctx, nil)
require.NoError(t, err)
require.True(t, enabled)
@@ -474,7 +474,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) {
require.NotNil(t, tm)
require.False(t, tm.Config.MDM.EnableDiskEncryption)
- enabled, err = ds.getConfigEnableDiskEncryption(ctx, &team1.ID)
+ enabled, err = ds.GetConfigEnableDiskEncryption(ctx, &team1.ID)
require.NoError(t, err)
require.False(t, enabled)
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index 5188e3e9b139..9c8e522988a5 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -1940,12 +1940,16 @@ func (ds *Datastore) bulkDeleteMDMAppleHostsConfigProfilesDB(ctx context.Context
return nil
}
+// NOTE If onlyProfileUUIDs is provided (not nil), only profiles with
+// those UUIDs will be update instead of rebuilding all pending
+// profiles for hosts
func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
ctx context.Context,
tx sqlx.ExtContext,
- uuids []string,
+ hostUUIDs []string,
+ onlyProfileUUIDs []string,
) (updatedDB bool, err error) {
- if len(uuids) == 0 {
+ if len(hostUUIDs) == 0 {
return false, nil
}
@@ -1972,6 +1976,11 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
// considers "remove" operations that have NULL status, which it would
// update to make its status to NULL).
+ profileHostIn := "h.uuid IN (?)"
+ if len(onlyProfileUUIDs) > 0 {
+ profileHostIn = "mae.profile_uuid IN (?) AND " + profileHostIn
+ }
+
toInstallStmt := fmt.Sprintf(`
SELECT
ds.profile_uuid as profile_uuid,
@@ -1990,7 +1999,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
-- profiles in A and B but with operation type "remove"
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) )
-`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
+`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, profileHostIn, profileHostIn, profileHostIn, profileHostIn))
// batches of 10K hosts because h.uuid appears three times in the
// query, and the max number of prepared statements is 65K, this was
@@ -2000,19 +2009,35 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
if ds.testSelectMDMProfilesBatchSize > 0 {
selectProfilesBatchSize = ds.testSelectMDMProfilesBatchSize
}
- selectProfilesTotalBatches := int(math.Ceil(float64(len(uuids)) / float64(selectProfilesBatchSize)))
+ selectProfilesTotalBatches := int(math.Ceil(float64(len(hostUUIDs)) / float64(selectProfilesBatchSize)))
var wantedProfiles []*fleet.MDMAppleProfilePayload
for i := 0; i < selectProfilesTotalBatches; i++ {
start := i * selectProfilesBatchSize
end := start + selectProfilesBatchSize
- if end > len(uuids) {
- end = len(uuids)
+ if end > len(hostUUIDs) {
+ end = len(hostUUIDs)
}
- batchUUIDs := uuids[start:end]
+ batchUUIDs := hostUUIDs[start:end]
+
+ var stmt string
+ var args []any
+ var err error
+
+ if len(onlyProfileUUIDs) > 0 {
+ stmt, args, err = sqlx.In(
+ toInstallStmt,
+ onlyProfileUUIDs, batchUUIDs,
+ onlyProfileUUIDs, batchUUIDs,
+ onlyProfileUUIDs, batchUUIDs,
+ onlyProfileUUIDs, batchUUIDs,
+ fleet.MDMOperationTypeRemove,
+ )
+ } else {
+ stmt, args, err = sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
+ }
- stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
if err != nil {
return false, ctxerr.Wrapf(ctx, err, "building statement to select profiles to install, batch %d of %d", i,
selectProfilesTotalBatches)
@@ -2030,6 +2055,11 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
// Exclude macOS only profiles from iPhones/iPads.
wantedProfiles = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(wantedProfiles)
+ narrowByProfiles := ""
+ if len(onlyProfileUUIDs) > 0 {
+ narrowByProfiles = "AND hmap.profile_uuid IN (?)"
+ }
+
toRemoveStmt := fmt.Sprintf(`
SELECT
hmap.profile_uuid as profile_uuid,
@@ -2045,7 +2075,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
RIGHT JOIN host_mdm_apple_profiles hmap
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
WHERE
- hmap.host_uuid IN (?) AND
+ hmap.host_uuid IN (?) %s AND
-- profiles that are in B but not in A
ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND
-- except "remove" operations in any state
@@ -2059,19 +2089,36 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
mcpl.apple_profile_uuid = hmap.profile_uuid AND
mcpl.label_id IS NULL
)
-`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)", "h.uuid IN (?)"))
+`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, profileHostIn, profileHostIn, profileHostIn, profileHostIn), narrowByProfiles)
var currentProfiles []*fleet.MDMAppleProfilePayload
for i := 0; i < selectProfilesTotalBatches; i++ {
start := i * selectProfilesBatchSize
end := start + selectProfilesBatchSize
- if end > len(uuids) {
- end = len(uuids)
+ if end > len(hostUUIDs) {
+ end = len(hostUUIDs)
}
- batchUUIDs := uuids[start:end]
+ batchUUIDs := hostUUIDs[start:end]
+
+ var stmt string
+ var args []any
+ var err error
+
+ if len(onlyProfileUUIDs) > 0 {
+ stmt, args, err = sqlx.In(
+ toRemoveStmt,
+ onlyProfileUUIDs, batchUUIDs,
+ onlyProfileUUIDs, batchUUIDs,
+ onlyProfileUUIDs, batchUUIDs,
+ onlyProfileUUIDs, batchUUIDs,
+ batchUUIDs, onlyProfileUUIDs,
+ fleet.MDMOperationTypeRemove,
+ )
+ } else {
+ stmt, args, err = sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
+ }
- stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "building profiles to remove statement")
}
@@ -2431,7 +2478,7 @@ func generateDesiredStateQuery(entityType string) string {
COUNT(*) as ${countEntityLabelsColumn},
COUNT(mel.label_id) as count_non_broken_labels,
COUNT(lm.label_id) as count_host_labels,
- -- this helps avoid the case where the host is not a member of a label
+ -- this helps avoid the case where the host is not a member of a label
-- just because it hasn't reported results for that label yet.
SUM(CASE WHEN lbl.created_at IS NOT NULL AND h.label_updated_at >= lbl.created_at THEN 1 ELSE 0 END) as count_host_updated_after_labels
FROM
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index 3cf3e99694c4..83bb3e23c587 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -6545,6 +6545,14 @@ func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) {
tm3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team3"})
require.NoError(t, err)
+ toks, err := ds.ListABMTokens(ctx)
+ require.NoError(t, err)
+ require.Empty(t, toks)
+
+ tokCount, err := ds.GetABMTokenCount(ctx)
+ require.NoError(t, err)
+ assert.EqualValues(t, 0, tokCount)
+
// create a token with an empty name and no team set, and another that will be unused
encTok := uuid.NewString()
@@ -6555,10 +6563,14 @@ func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.NotEmpty(t, t2.ID)
- toks, err := ds.ListABMTokens(ctx)
+ toks, err = ds.ListABMTokens(ctx)
require.NoError(t, err)
require.Len(t, toks, 2)
+ tokCount, err = ds.GetABMTokenCount(ctx)
+ require.NoError(t, err)
+ assert.EqualValues(t, 2, tokCount)
+
// get that token
tok, err = ds.GetABMTokenByOrgName(ctx, "")
require.NoError(t, err)
@@ -6654,6 +6666,10 @@ func testMDMAppleGetAndUpdateABMToken(t *testing.T, ds *Datastore) {
require.Equal(t, uint(0), expTok.MacOSTeam.ID)
require.Equal(t, tm2.Name, expTok.IOSTeamName)
require.Equal(t, tm3.Name, expTok.IPadOSTeamName)
+
+ tokCount, err = ds.GetABMTokenCount(ctx)
+ require.NoError(t, err)
+ assert.EqualValues(t, 1, tokCount)
}
func testMDMAppleABMTokensTermsExpired(t *testing.T, ds *Datastore) {
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 4a9af648f668..659aa12091c2 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -1218,7 +1218,7 @@ func (ds *Datastore) applyHostFilters(
return "", nil, ctxerr.Wrap(ctx, err, "building query to filter macOS settings status")
}
sqlStmt, whereParams = filterHostsByMacOSDiskEncryptionStatus(sqlStmt, opt, whereParams)
- if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
+ if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", nil, ctxerr.Wrap(
ctx, &fleet.BadRequestError{
@@ -1404,21 +1404,39 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
// or are servers. Similar logic could be applied to macOS hosts but is not included in this
// current implementation.
- sqlFmt := ` AND h.platform IN('windows', 'darwin', 'ios', 'ipados') AND (ne.id IS NOT NULL OR mwe.host_uuid IS NOT NULL) AND hmdm.enrolled = 1`
+ // TODO once testLabelsListHostsInLabelOSSettings enrolls hosts into the correct MDM, switch to this:
+ /*sqlFmt := ` AND (
+ (h.platform = 'windows' AND mwe.host_uuid IS NOT NULL AND hmdm.enrolled = 1) -- windows
+ OR (h.platform IN ('darwin', 'ios', 'ipados') AND ne.id IS NOT NULL AND hmdm.enrolled = 1) -- apple
+ OR (h.platform = 'ubuntu' OR h.os_version LIKE 'Fedora%%') -- linux
+ )`*/
+
+ sqlFmt := ` AND (
+ (h.platform IN('windows', 'darwin', 'ios', 'ipados') AND (ne.id IS NOT NULL OR mwe.host_uuid IS NOT NULL) AND hmdm.enrolled = 1)
+ OR (h.platform = 'ubuntu' OR h.os_version LIKE 'Fedora%%')
+ )`
+
if opt.TeamFilter == nil {
// OS 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`
}
- var whereMacOS, whereWindows string
+ var whereMacOS, whereWindows, whereLinux string
sqlFmt += `
-AND ((h.platform = 'windows' AND (%s))
-OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s)))`
+AND (
+ (h.platform = 'windows' AND (%s))
+ OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s))
+ OR ((h.os_version LIKE 'Fedora%%' OR h.platform = 'ubuntu') AND (%s))
+)`
// construct the WHERE for macOS
whereMacOS = fmt.Sprintf(`(%s) = ?`, sqlCaseMDMAppleStatus())
paramsMacOS := []any{opt.OSSettingsFilter}
+ // construct the WHERE for linux
+ whereLinux = fmt.Sprintf(`(%s) = ?`, sqlCaseLinuxOSSettingsStatus())
+ paramsLinux := []any{opt.OSSettingsFilter}
+
// construct the WHERE for windows
whereWindows = `hmdm.is_server = 0`
paramsWindows := []any{}
@@ -1520,8 +1538,9 @@ OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (
paramsWindows = append(paramsWindows, opt.OSSettingsFilter)
params = append(params, paramsWindows...)
params = append(params, paramsMacOS...)
+ params = append(params, paramsLinux...)
- return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), params, nil
+ return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS, whereLinux), params, nil
}
func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}, enableDiskEncryption bool) (string, []interface{}) {
@@ -1529,13 +1548,13 @@ func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt
return sql, params
}
- sqlFmt := " AND h.platform IN('windows', 'darwin')"
+ sqlFmt := " AND h.platform IN('windows', 'darwin', 'ubuntu', 'rhel')"
if opt.TeamFilter == nil {
// OS 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))`
+ sqlFmt += ` AND ((h.platform = 'windows' AND %s) OR (h.platform = 'darwin' AND %s) OR ((h.platform = 'ubuntu' OR h.os_version LIKE 'Fedora%%') AND %s))`
var subqueryMacOS string
var subqueryParams []interface{}
@@ -1580,7 +1599,10 @@ func (ds *Datastore) filterHostsByOSSettingsDiskEncryptionStatus(sql string, opt
whereMacOS = "EXISTS (" + subqueryMacOS + ")"
}
- return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS), append(params, subqueryParams...)
+ whereLinux := fmt.Sprintf(`(%s) = ?`, sqlCaseLinuxDiskEncryptionStatus())
+ subqueryParams = append(subqueryParams, opt.OSSettingsDiskEncryptionFilter)
+
+ return sql + fmt.Sprintf(sqlFmt, whereWindows, whereMacOS, whereLinux), append(params, subqueryParams...)
}
func filterHostsByMDMBootstrapPackageStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
@@ -1904,9 +1926,20 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
}
// NOTE: allow an empty serial, currently it is empty for Windows.
- var host fleet.Host
+ host := fleet.Host{
+ ComputerName: hostInfo.ComputerName,
+ Hostname: hostInfo.Hostname,
+ HardwareModel: hostInfo.HardwareModel,
+ HardwareSerial: hostInfo.HardwareSerial,
+ }
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
- enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier, hostInfo.HardwareUUID, hostInfo.HardwareSerial)
+ serialToMatch := hostInfo.HardwareSerial
+ if hostInfo.Platform == "windows" {
+ // For Windows, don't match by serial number to retain legacy functionality.
+ serialToMatch = ""
+ }
+ enrolledHostInfo, err := matchHostDuringEnrollment(ctx, tx, orbitEnroll, isMDMEnabled, hostInfo.OsqueryIdentifier,
+ hostInfo.HardwareUUID, serialToMatch)
// If the osquery identifier that osqueryd will use was not sent by Orbit, then use the hardware UUID as identifier
// (using the hardware UUID is Orbit's default behavior).
@@ -1936,6 +1969,8 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
uuid = COALESCE(NULLIF(uuid, ''), ?),
osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?),
hardware_serial = COALESCE(NULLIF(hardware_serial, ''), ?),
+ computer_name = COALESCE(NULLIF(computer_name, ''), ?),
+ hardware_model = COALESCE(NULLIF(hardware_model, ''), ?),
team_id = ?
WHERE id = ?`
_, err := tx.ExecContext(ctx, sqlUpdate,
@@ -1943,6 +1978,8 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
hostInfo.HardwareUUID,
osqueryIdentifier,
hostInfo.HardwareSerial,
+ hostInfo.ComputerName,
+ hostInfo.HardwareModel,
teamID,
enrolledHostInfo.ID,
)
@@ -1977,8 +2014,10 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
orbit_node_key,
hardware_serial,
hostname,
+ computer_name,
+ hardware_model,
platform
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
`
result, err := tx.ExecContext(ctx, sqlInsert,
zeroTime,
@@ -1992,6 +2031,8 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
orbitNodeKey,
hostInfo.HardwareSerial,
hostInfo.Hostname,
+ hostInfo.ComputerName,
+ hostInfo.HardwareModel,
hostInfo.Platform,
)
if err != nil {
@@ -1999,9 +2040,9 @@ func (ds *Datastore) EnrollOrbit(ctx context.Context, isMDMEnabled bool, hostInf
}
hostID, _ := result.LastInsertId()
const sqlHostDisplayName = `
- INSERT INTO host_display_names (host_id, display_name) VALUES (?, '')
+ INSERT INTO host_display_names (host_id, display_name) VALUES (?, ?)
`
- _, err = tx.ExecContext(ctx, sqlHostDisplayName, hostID)
+ _, err = tx.ExecContext(ctx, sqlHostDisplayName, hostID, host.DisplayName())
if err != nil {
return ctxerr.Wrap(ctx, err, "insert host_display_names")
}
@@ -2421,6 +2462,7 @@ func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken st
COALESCE(hd.gigs_disk_space_available, 0) as gigs_disk_space_available,
COALESCE(hd.percent_disk_space_available, 0) as percent_disk_space_available,
COALESCE(hd.gigs_total_disk_space, 0) as gigs_total_disk_space,
+ hd.encrypted as disk_encryption_enabled,
IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet
FROM
host_device_auth hda
@@ -3800,6 +3842,65 @@ ON DUPLICATE KEY UPDATE
return err
}
+func (ds *Datastore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error {
+ if encryptedBase64Passphrase == "" || encryptedBase64Salt == "" { // should have been caught at service level
+ return errors.New("passphrase and salt must be set")
+ }
+
+ _, err := ds.writer(ctx).ExecContext(ctx, `
+INSERT INTO host_disk_encryption_keys
+ (host_id, base64_encrypted, base64_encrypted_salt, key_slot, client_error, decryptable)
+VALUES
+ (?, ?, ?, ?, '', TRUE)
+ON DUPLICATE KEY UPDATE
+ decryptable = TRUE,
+ base64_encrypted = VALUES(base64_encrypted),
+ base64_encrypted_salt = VALUES(base64_encrypted_salt),
+ key_slot = VALUES(key_slot),
+ client_error = ''
+`, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot)
+ return err
+}
+
+func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool {
+ var pendingEscrowCount uint
+ _ = sqlx.GetContext(ctx, ds.reader(ctx), &pendingEscrowCount, `
+ SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND reset_requested = TRUE`, hostID)
+ return pendingEscrowCount > 0
+}
+
+func (ds *Datastore) ClearPendingEscrow(ctx context.Context, hostID uint) error {
+ _, err := ds.writer(ctx).ExecContext(ctx, `UPDATE host_disk_encryption_keys SET reset_requested = FALSE WHERE host_id = ?`, hostID)
+ return err
+}
+
+func (ds *Datastore) ReportEscrowError(ctx context.Context, hostID uint, errorMessage string) error {
+ _, err := ds.writer(ctx).ExecContext(ctx, `
+INSERT INTO host_disk_encryption_keys
+ (host_id, base64_encrypted, client_error) VALUES (?, '', ?) ON DUPLICATE KEY UPDATE client_error = VALUES(client_error)
+`, hostID, errorMessage)
+ return err
+}
+
+func (ds *Datastore) QueueEscrow(ctx context.Context, hostID uint) error {
+ _, err := ds.writer(ctx).ExecContext(ctx, `
+INSERT INTO host_disk_encryption_keys
+ (host_id, base64_encrypted, reset_requested) VALUES (?, '', TRUE) ON DUPLICATE KEY UPDATE reset_requested = TRUE
+`, hostID)
+ return err
+}
+
+func (ds *Datastore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error {
+ var hasKeyCount uint
+ err := sqlx.GetContext(ctx, ds.reader(ctx), &hasKeyCount, `
+ SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND base64_encrypted != ''`, hostID)
+ if hasKeyCount > 0 {
+ return &fleet.BadRequestError{Message: "Key has already been escrowed for this host"}
+ }
+
+ 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
@@ -3849,7 +3950,7 @@ func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint)
var key fleet.HostDiskEncryptionKey
err := sqlx.GetContext(ctx, ds.reader(ctx), &key, `
SELECT
- host_id, base64_encrypted, decryptable, updated_at
+ host_id, base64_encrypted, decryptable, updated_at, client_error
FROM
host_disk_encryption_keys
WHERE host_id = ?`, hostID)
@@ -4473,6 +4574,7 @@ func (ds *Datastore) HostLite(ctx context.Context, id uint) (*fleet.Host, error)
"hardware_model",
"computer_name",
"platform",
+ "os_version",
"team_id",
"distributed_interval",
"logger_tls_period",
@@ -5155,6 +5257,42 @@ func (ds *Datastore) GetMatchingHostSerials(ctx context.Context, serials []strin
return result, nil
}
+func (ds *Datastore) GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) {
+ result := map[string]struct{}{}
+ if len(serials) == 0 {
+ return result, nil
+ }
+
+ stmt := `
+SELECT
+ hardware_serial
+FROM
+ hosts h
+ JOIN host_dep_assignments hdep ON hdep.host_id = h.id
+WHERE
+ h.hardware_serial IN (?) AND hdep.deleted_at IS NOT NULL;
+ `
+
+ var args []interface{}
+ for _, serial := range serials {
+ args = append(args, serial)
+ }
+ stmt, args, err := sqlx.In(stmt, args)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "building IN statement for matching hosts")
+ }
+ var matchingSerials []string
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &matchingSerials, stmt, args...); err != nil {
+ return nil, err
+ }
+
+ for _, serial := range matchingSerials {
+ result[serial] = struct{}{}
+ }
+
+ return result, nil
+}
+
func (ds *Datastore) GetHostHealth(ctx context.Context, id uint) (*fleet.HostHealth, error) {
sqlStmt := `
SELECT h.os_version, h.updated_at, h.platform, h.team_id, hd.encrypted as disk_encryption_enabled FROM hosts h
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index bb82622b6a28..51e12566a68e 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -155,6 +155,7 @@ func TestHosts(t *testing.T) {
{"SetOrUpdateHostDiskEncryptionKeys", testHostsSetOrUpdateHostDisksEncryptionKey},
{"SetHostsDiskEncryptionKeyStatus", testHostsSetDiskEncryptionKeyStatus},
{"GetUnverifiedDiskEncryptionKeys", testHostsGetUnverifiedDiskEncryptionKeys},
+ {"LUKS", testLUKSDatastoreFunctions},
{"EnrollOrbit", testHostsEnrollOrbit},
{"EnrollUpdatesMissingInfo", testHostsEnrollUpdatesMissingInfo},
{"EncryptionKeyRawDecryption", testHostsEncryptionKeyRawDecryption},
@@ -170,6 +171,7 @@ func TestHosts(t *testing.T) {
{"UpdateHostIssues", testUpdateHostIssues},
{"ListUpcomingHostMaintenanceWindows", testListUpcomingHostMaintenanceWindows},
{"GetHostEmails", testGetHostEmails},
+ {"TestGetMatchingHostSerialsMarkedDeleted", testGetMatchingHostSerialsMarkedDeleted},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -788,6 +790,7 @@ func testHostsDelete(t *testing.T, ds *Datastore) {
}
func listHostsCheckCount(t *testing.T, ds *Datastore, filter fleet.TeamFilter, opt fleet.HostListOptions, expectedCount int) []*fleet.Host {
+ t.Helper()
hosts, err := ds.ListHosts(context.Background(), filter, opt)
require.NoError(t, err)
count, err := ds.CountHosts(context.Background(), filter, opt)
@@ -807,28 +810,35 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
require.NoError(t, err)
var hosts []*fleet.Host
- for i := 0; i < 10; i++ {
+ for i := 0; i < 20; i++ {
var opts []test.NewHostOption
switch i {
- case 5, 6:
+ case 0:
opts = append(opts, test.WithPlatform("windows"))
+ case 1, 2:
+ opts = append(opts, test.WithPlatform("ubuntu")) // supported for linux encryption
+ case 3, 4, 5:
+ opts = append(opts, test.WithOSVersion("Fedora 33")) // supported for linux encryption
+ case 6, 7, 8, 9:
+ opts = append(opts, test.WithPlatform("foo")) // not supported for linux encryption
}
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(), opts...)
+ fmt.Sprintf("%d", i), fmt.Sprintf("%d", i), time.Now(), opts...) // default macos platform
hosts = append(hosts, h)
nanoEnrollAndSetHostMDMData(t, ds, h, false)
}
+
userFilter := fleet.TeamFilter{User: test.UserAdmin}
- // confirm intial state
+ // confirm initial state
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID}, 0)
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0)
- // assign three hosts to team 1
- require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID}))
+ // assign three macos hosts to team 1
+ require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[10].ID, hosts[11].ID, hosts[12].ID}))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)-3)
@@ -836,7 +846,7 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team2.ID}, 0)
// assign four hosts to team 2
- require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{hosts[3].ID, hosts[4].ID, hosts[5].ID, hosts[6].ID}))
+ require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{hosts[13].ID, hosts[14].ID, hosts[15].ID, hosts[16].ID}))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil}, len(hosts))
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero}, len(hosts)-7)
@@ -849,7 +859,7 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
{
ProfileUUID: profUUID,
ProfileIdentifier: "identifier",
- HostUUID: hosts[0].UUID, // hosts[0] is assgined to team 1
+ HostUUID: hosts[10].UUID, // hosts[10] is assgined to team 1
CommandUUID: "command-uuid-1",
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryVerifying,
@@ -867,46 +877,78 @@ func testHostListOptionsTeamFilter(t *testing.T, ds *Datastore) {
{
ProfileUUID: profUUID,
ProfileIdentifier: "identifier",
- HostUUID: hosts[9].UUID, // hosts[9] is assgined to no team
+ HostUUID: hosts[19].UUID, // hosts[19] is assgined to no team
CommandUUID: "command-uuid-2",
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryVerifying,
Checksum: []byte("csum"),
},
}))
- listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[10]
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.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]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{MacOSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19]
- // test team filter in combination with os settings filter
- listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[0]
+ // OS Settings Filters
+
+ // team 1
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[10]
+
+ // team 2
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{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsFilter: fleet.OSSettingsVerifying}, 1) // hosts[19]
listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsFilter: fleet.OSSettingsVerifying}, 1)
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsPending}, 5) // pending supported linux hosts
+
+ require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1].ID, "key1", "morton", 1)) // set host 1 to verified
+ require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed
+
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsVerified}, 1) // hosts[1]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsFailed}, 1) // hosts[2]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsFilter: fleet.OSSettingsPending}, 3) // still-pending supported linux hosts
+
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1)
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionFailed}, 1)
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionActionRequired}, 3)
// test team filter in combination with os settings disk encryptionfilter
require.NoError(t, ds.BulkUpsertMDMAppleHostProfiles(context.Background(), []*fleet.MDMAppleBulkUpsertHostProfilePayload{
{
ProfileUUID: profUUID,
ProfileIdentifier: mobileconfig.FleetFileVaultPayloadIdentifier,
- HostUUID: hosts[8].UUID, // hosts[8] is assgined to no team
+ HostUUID: hosts[18].UUID, // hosts[18] is assgined to no team
CommandUUID: "command-uuid-3",
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
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: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 0) // hosts[10]
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]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterZero, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[18]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterNil, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[18]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing}, 1) // hosts[18]
+
+ // move linux hosts to team 1 (un-escrows keys)
+ require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID, hosts[5].ID}))
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsPending}, 5) // pending supported linux hosts
+
+ require.NoError(t, ds.SaveLUKSData(context.Background(), hosts[1].ID, "key1", "mutton", 2)) // set host 1 to verified
+ require.NoError(t, ds.ReportEscrowError(context.Background(), hosts[2].ID, "error")) // set host 2 to failed
+
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsVerified}, 1) // hosts[1]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsFailed}, 1) // hosts[2]
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsFilter: fleet.OSSettingsPending}, 3) // still-pending supported linux hosts
+
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionVerified}, 1)
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionFailed}, 1)
+ listHostsCheckCount(t, ds, userFilter, fleet.HostListOptions{TeamFilter: &team1.ID, OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionActionRequired}, 3)
// Bad team filter
_, err = ds.ListHosts(context.Background(), userFilter, fleet.HostListOptions{TeamFilter: teamIDFilterBad})
@@ -6192,6 +6234,17 @@ func testHostsLoadHostByDeviceAuthToken(t *testing.T, ds *Datastore) {
require.Equal(t, hSimple.ID, loadSimple.ID)
require.True(t, loadSimple.IsOsqueryEnrolled())
+ // make sure disk encryption state is reflected
+ require.Nil(t, loadSimple.DiskEncryptionEnabled)
+ require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hSimple.ID, false))
+ loadSimple, err = ds.LoadHostByDeviceAuthToken(ctx, "simple", time.Second*3)
+ require.NoError(t, err)
+ require.False(t, *loadSimple.DiskEncryptionEnabled)
+ require.NoError(t, ds.SetOrUpdateHostDisksEncryption(ctx, hSimple.ID, true))
+ loadSimple, err = ds.LoadHostByDeviceAuthToken(ctx, "simple", time.Second*3)
+ require.NoError(t, err)
+ require.True(t, *loadSimple.DiskEncryptionEnabled)
+
// create a host that will be pending enrollment in Fleet MDM
hFleet := createHostWithDeviceToken("fleet")
err = ds.SetOrUpdateMDMData(ctx, hFleet.ID, false, false, "https://fleetdm.com", true, fleet.WellKnownMDMFleet, "")
@@ -7806,6 +7859,101 @@ func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected
require.Equal(t, expectedDecryptable, got.Decryptable)
}
+func testLUKSDatastoreFunctions(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ host1, err := ds.NewHost(ctx, &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String("1"),
+ UUID: "1",
+ OsqueryHostID: ptr.String("1"),
+ Hostname: "foo.local",
+ PrimaryIP: "192.168.1.1",
+ PrimaryMac: "30-65-EC-6F-C4-58",
+ })
+ require.NoError(t, err)
+ host2, err := ds.NewHost(ctx, &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String("2"),
+ UUID: "2",
+ OsqueryHostID: ptr.String("2"),
+ Hostname: "foo.local2",
+ PrimaryIP: "192.168.1.2",
+ PrimaryMac: "30-65-EC-6F-C4-59",
+ })
+ require.NoError(t, err)
+ host3, err := ds.NewHost(ctx, &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)
+
+ // queue shows as pending
+ require.False(t, ds.IsHostPendingEscrow(ctx, host1.ID))
+ err = ds.QueueEscrow(ctx, host1.ID)
+ require.NoError(t, err)
+ require.False(t, ds.IsHostPendingEscrow(ctx, host2.ID))
+ require.True(t, ds.IsHostPendingEscrow(ctx, host1.ID))
+
+ // clear removes pending
+ err = ds.QueueEscrow(ctx, host2.ID)
+ require.NoError(t, err)
+ err = ds.ClearPendingEscrow(ctx, host1.ID)
+ require.NoError(t, err)
+ require.False(t, ds.IsHostPendingEscrow(ctx, host1.ID))
+ require.True(t, ds.IsHostPendingEscrow(ctx, host2.ID))
+
+ // report escrow error does not remove pending
+ err = ds.ReportEscrowError(ctx, host2.ID, "this broke")
+ require.NoError(t, err)
+ require.True(t, ds.IsHostPendingEscrow(ctx, host2.ID))
+ // TODO confirm error was persisted
+
+ // assert no key stored on hosts with varying no-key-stored states
+ require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
+ require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
+ require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID))
+
+ // no change when blank key or salt attempted to save
+ err = ds.SaveLUKSData(ctx, host1.ID, "", "", 0)
+ require.Error(t, err)
+ require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
+ err = ds.SaveLUKSData(ctx, host1.ID, "foo", "", 0)
+ require.Error(t, err)
+ require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
+
+ // persists with passphrase and salt set
+ err = ds.SaveLUKSData(ctx, host2.ID, "bazqux", "fuzzmuffin", 0)
+ require.NoError(t, err)
+ require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
+ require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
+ key, err := ds.GetHostDiskEncryptionKey(ctx, host2.ID)
+ require.NoError(t, err)
+ require.Equal(t, "bazqux", key.Base64Encrypted)
+
+ // persists when host hasn't had anything queued
+ err = ds.SaveLUKSData(ctx, host3.ID, "newstuff", "fuzzball", 1)
+ require.NoError(t, err)
+ require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID))
+ key, err = ds.GetHostDiskEncryptionKey(ctx, host3.ID)
+ require.NoError(t, err)
+ require.Equal(t, "newstuff", key.Base64Encrypted)
+}
+
func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
host, err := ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
@@ -8060,6 +8208,11 @@ func testHostsGetUnverifiedDiskEncryptionKeys(t *testing.T, ds *Datastore) {
func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
ctx := context.Background()
+ const (
+ computerName = "My computer"
+ hardwareModel = "CMP-1000"
+ )
+
createHost := func(osqueryID, serial string) *fleet.Host {
dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
var osqueryIDPtr *string
@@ -8074,6 +8227,8 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
DetailUpdatedAt: dbZeroTime,
OsqueryHostID: osqueryIDPtr,
RefetchRequested: true,
+ ComputerName: computerName,
+ HardwareModel: hardwareModel,
})
require.NoError(t, err)
return h
@@ -8111,10 +8266,19 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
h, err = ds.EnrollOrbit(ctx, true, fleet.OrbitHostInfo{
HardwareUUID: *hBoth.OsqueryHostID,
HardwareSerial: hBoth.HardwareSerial,
+ ComputerName: hBoth.ComputerName,
+ HardwareModel: hBoth.HardwareModel,
}, uuid.New().String(), nil)
require.NoError(t, err)
require.Equal(t, hBoth.ID, h.ID)
- require.Empty(t, h.HardwareSerial) // this is just to prove that it was loaded based on osquery_host_id, the serial was not set in the lookup
+ assert.Equal(t, hBoth.HardwareSerial, h.HardwareSerial)
+ assert.Equal(t, hBoth.ComputerName, h.ComputerName)
+ assert.Equal(t, hBoth.HardwareModel, h.HardwareModel)
+ h, err = ds.Host(ctx, h.ID)
+ require.NoError(t, err)
+ assert.Equal(t, hBoth.HardwareSerial, h.HardwareSerial)
+ assert.Equal(t, hBoth.ComputerName, h.ComputerName)
+ assert.Equal(t, hBoth.HardwareModel, h.HardwareModel)
// enroll with osquery id from hBoth and serial from hSerialNoOsquery (should
// use the osquery match)
@@ -8124,14 +8288,17 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
}, uuid.New().String(), nil)
require.NoError(t, err)
require.Equal(t, hBoth.ID, h.ID)
- require.Empty(t, h.HardwareSerial)
+ assert.Equal(t, hSerialNoOsquery.HardwareSerial, h.HardwareSerial)
// enroll with no match, will create a new one
+ newSerial := uuid.NewString()
h, err = ds.EnrollOrbit(ctx, true, fleet.OrbitHostInfo{
HardwareUUID: uuid.New().String(),
- HardwareSerial: uuid.New().String(),
+ HardwareSerial: newSerial,
Hostname: "foo2",
Platform: "darwin",
+ ComputerName: "New computer",
+ HardwareModel: "ABC-3000",
}, uuid.New().String(), nil)
require.NoError(t, err)
require.Greater(t, h.ID, hBoth.ID)
@@ -8140,6 +8307,9 @@ func testHostsEnrollOrbit(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, "foo2", h.Hostname)
require.Equal(t, "darwin", h.Platform)
+ assert.Equal(t, "New computer", h.ComputerName)
+ assert.Equal(t, "ABC-3000", h.HardwareModel)
+ assert.Equal(t, newSerial, h.HardwareSerial)
// simulate a "corrupt database" where two hosts have the same serial and
// enroll by serial should always use the same (the smaller ID)
@@ -9751,3 +9921,83 @@ func testGetHostEmails(t *testing.T, ds *Datastore) {
require.NoError(t, err)
assert.ElementsMatch(t, []string{"foo@example.com", "bar@example.com"}, emails)
}
+
+func testGetMatchingHostSerialsMarkedDeleted(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+ serials := []string{"foo", "bar", "baz"}
+ team, err := ds.NewTeam(context.Background(), &fleet.Team{
+ Name: "team1",
+ })
+ require.NoError(t, err)
+ abmTok, err := ds.InsertABMToken(ctx, &fleet.ABMToken{OrganizationName: t.Name(), EncryptedToken: []byte("token")})
+ require.NoError(t, err)
+ var hosts []fleet.Host
+ for i, serial := range serials {
+ var tmID *uint
+ if serial == "bar" {
+ tmID = &team.ID
+ }
+ h, err := ds.NewHost(ctx, &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String(fmt.Sprint(i)),
+ UUID: fmt.Sprint(i),
+ OsqueryHostID: ptr.String(fmt.Sprint(i)),
+ Hostname: "foo.local",
+ PrimaryIP: "192.168.1.1",
+ PrimaryMac: "30-65-EC-6F-C4-58",
+ HardwareSerial: serial,
+ TeamID: tmID,
+ ID: uint(i),
+ })
+ require.NoError(t, err)
+ require.NotNil(t, h)
+
+ // Only "foo" and "baz" are
+ if i%2 == 0 {
+ hosts = append(hosts, *h)
+ }
+ }
+
+ require.NoError(t, ds.UpsertMDMAppleHostDEPAssignments(ctx, hosts, abmTok.ID))
+ require.NoError(t, ds.DeleteHostDEPAssignments(ctx, abmTok.ID, serials))
+
+ cases := []struct {
+ name string
+ in []string
+ want map[string]struct{}
+ err string
+ }{
+ {"no serials provided", []string{}, map[string]struct{}{}, ""},
+ {"no matching serials", []string{"oof", "rab", "bar"}, map[string]struct{}{}, ""},
+ {
+ "partial matches",
+ []string{"foo", "rab", "bar"},
+ map[string]struct{}{"foo": {}},
+ "",
+ },
+ {
+ "all matching",
+ []string{"foo", "baz"},
+ map[string]struct{}{
+ "foo": {},
+ "baz": {},
+ },
+ "",
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ds.GetMatchingHostSerialsMarkedDeleted(ctx, tt.in)
+ if tt.err == "" {
+ require.NoError(t, err)
+ } else {
+ require.ErrorContains(t, err, tt.err)
+ }
+ require.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go
index 21c4e0eb9402..4111c81b6ee0 100644
--- a/server/datastore/mysql/labels.go
+++ b/server/datastore/mysql/labels.go
@@ -662,7 +662,7 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea
}
query, whereParams = filterHostsByMacOSDiskEncryptionStatus(query, opt, whereParams)
query, whereParams = filterHostsByMDMBootstrapPackageStatus(query, opt, whereParams)
- if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
+ if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil {
return "", nil, err
} else if opt.OSSettingsFilter.IsValid() {
query, whereParams, err = ds.filterHostsByOSSettingsStatus(query, opt, whereParams, enableDiskEncryption)
diff --git a/server/datastore/mysql/labels_test.go b/server/datastore/mysql/labels_test.go
index 52805cecc6a5..8db192ddbaea 100644
--- a/server/datastore/mysql/labels_test.go
+++ b/server/datastore/mysql/labels_test.go
@@ -1568,14 +1568,14 @@ func testLabelsListHostsInLabelOSSettings(t *testing.T, db *Datastore) {
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) {
+ t.Run("os_settings_disk_encryption", 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) {
+ t.Run("os_settings", 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)
diff --git a/server/datastore/mysql/linux_mdm.go b/server/datastore/mysql/linux_mdm.go
new file mode 100644
index 000000000000..126cbc0a39a8
--- /dev/null
+++ b/server/datastore/mysql/linux_mdm.go
@@ -0,0 +1,103 @@
+package mysql
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/jmoiron/sqlx"
+)
+
+func (ds *Datastore) GetLinuxDiskEncryptionSummary(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) {
+ var args []interface{}
+ var teamFilter string
+ if teamID != nil {
+ teamFilter = "AND h.team_id = ?"
+ args = append(args, *teamID)
+ } else {
+ teamFilter = "AND h.team_id IS NULL"
+ }
+
+ stmt := fmt.Sprintf(`SELECT
+ CASE WHEN hdek.base64_encrypted IS NOT NULL
+ AND hdek.base64_encrypted != ''
+ AND hdek.client_error = '' THEN
+ 'verified'
+ WHEN hdek.client_error IS NOT NULL
+ AND hdek.client_error != '' THEN
+ 'failed'
+ WHEN hdek.base64_encrypted IS NULL
+ OR (hdek.base64_encrypted = ''
+ AND hdek.client_error = '') THEN
+ 'action_required'
+ END AS status,
+ COUNT(h.id) AS host_count
+ FROM
+ hosts h
+ LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id
+ WHERE
+ (h.os_version LIKE '%%fedora%%'
+ OR h.platform LIKE 'ubuntu')
+ %s
+ GROUP BY
+ status`, teamFilter)
+
+ type countRow struct {
+ Status string `db:"status"`
+ HostCount uint `db:"host_count"`
+ }
+
+ var counts []countRow
+ summary := fleet.MDMLinuxDiskEncryptionSummary{}
+
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &counts, stmt, args...); err != nil {
+ return summary, err
+ }
+
+ for _, count := range counts {
+ switch count.Status {
+ case "verified":
+ summary.Verified = count.HostCount
+ case "action_required":
+ summary.ActionRequired = count.HostCount
+ case "failed":
+ summary.Failed = count.HostCount
+ }
+ }
+
+ return summary, nil
+}
+
+func sqlCaseLinuxOSSettingsStatus() string {
+ return `
+ CASE WHEN
+ hdek.base64_encrypted IS NOT NULL
+ AND hdek.base64_encrypted != ''
+ AND hdek.client_error = '' THEN
+ '` + string(fleet.OSSettingsVerified) + `'
+ WHEN hdek.client_error IS NOT NULL
+ AND hdek.client_error != '' THEN
+ '` + string(fleet.OSSettingsFailed) + `'
+ WHEN hdek.base64_encrypted IS NULL
+ OR (hdek.base64_encrypted = ''
+ AND hdek.client_error = '') THEN
+ '` + string(fleet.OSSettingsPending) + `'
+ END`
+}
+
+func sqlCaseLinuxDiskEncryptionStatus() string {
+ return `
+ CASE WHEN
+ hdek.base64_encrypted IS NOT NULL
+ AND hdek.base64_encrypted != ''
+ AND hdek.client_error = '' THEN
+ '` + string(fleet.DiskEncryptionVerified) + `'
+ WHEN hdek.client_error IS NOT NULL
+ AND hdek.client_error != '' THEN
+ '` + string(fleet.DiskEncryptionFailed) + `'
+ WHEN hdek.base64_encrypted IS NULL
+ OR (hdek.base64_encrypted = ''
+ AND hdek.client_error = '') THEN
+ '` + string(fleet.DiskEncryptionActionRequired) + `'
+ END`
+}
diff --git a/server/datastore/mysql/linux_mdm_test.go b/server/datastore/mysql/linux_mdm_test.go
new file mode 100644
index 000000000000..d0cecb405bea
--- /dev/null
+++ b/server/datastore/mysql/linux_mdm_test.go
@@ -0,0 +1,146 @@
+package mysql
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/fleetdm/fleet/v4/server/test"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLinuxDiskEncryptionSummary(t *testing.T) {
+ ds := CreateMySQLDS(t)
+ ctx := context.Background()
+
+ // 5 new ubuntu hosts
+ var ubuntuHosts []*fleet.Host
+ for i := 0; i < 5; i++ {
+ 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(), test.WithPlatform("ubuntu"))
+ ubuntuHosts = append(ubuntuHosts, h)
+ }
+
+ // 5 new fedora hosts
+ var fedoraHosts []*fleet.Host
+ for i := 5; i < 10; i++ {
+ 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(),
+ test.WithOSVersion("Fedora Linux 38.0.0"), test.WithPlatform("rhel"))
+ fedoraHosts = append(fedoraHosts, h)
+ }
+
+ // 5 macos hosts
+ var macosHosts []*fleet.Host
+ for i := 10; i < 15; i++ {
+ 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(), test.WithPlatform("darwin"))
+ macosHosts = append(macosHosts, h)
+ }
+
+ // no teams tests =====
+ summary, err := ds.GetLinuxDiskEncryptionSummary(ctx, nil)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(0), summary.Verified)
+ require.Equal(t, uint(10), summary.ActionRequired)
+ require.Equal(t, uint(0), summary.Failed)
+
+ // Add disk encryption keys
+
+ // ubuntu
+ err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0].ID, "base64_encrypted", "", nil)
+ require.NoError(t, err)
+ // fedora
+ err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, fedoraHosts[0].ID, "base64_encrypted", "", nil)
+ require.NoError(t, err)
+ // macos
+ err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, macosHosts[0].ID, "base64_encrypted", "", nil)
+ require.NoError(t, err)
+
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(2), summary.Verified)
+ require.Equal(t, uint(8), summary.ActionRequired)
+ require.Equal(t, uint(0), summary.Failed)
+
+ // update ubuntu with key and client error
+ err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[0].ID, "base64_encrypted", "client error", nil)
+ require.NoError(t, err)
+
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(1), summary.Verified)
+ require.Equal(t, uint(8), summary.ActionRequired)
+ require.Equal(t, uint(1), summary.Failed)
+
+ // add ubuntu with no key and client error
+ err = ds.SetOrUpdateHostDiskEncryptionKey(ctx, ubuntuHosts[1].ID, "", "client error", nil)
+ require.NoError(t, err)
+
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(1), summary.Verified)
+ require.Equal(t, uint(7), summary.ActionRequired)
+ require.Equal(t, uint(2), summary.Failed)
+
+ // move verified fedora host to team will remove existing key
+ team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
+ require.NoError(t, err)
+
+ err = ds.AddHostsToTeam(ctx, &team.ID, []uint{fedoraHosts[0].ID})
+ require.NoError(t, err)
+
+ // team summary
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, &team.ID)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(0), summary.Verified)
+ require.Equal(t, uint(1), summary.ActionRequired)
+ require.Equal(t, uint(0), summary.Failed)
+
+ // no team summary
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(0), summary.Verified)
+ require.Equal(t, uint(7), summary.ActionRequired)
+ require.Equal(t, uint(2), summary.Failed)
+
+ // move all hosts to team
+ for _, h := range ubuntuHosts {
+ err = ds.AddHostsToTeam(ctx, &team.ID, []uint{h.ID})
+ require.NoError(t, err)
+ }
+
+ for _, h := range fedoraHosts {
+ err = ds.AddHostsToTeam(ctx, &team.ID, []uint{h.ID})
+ require.NoError(t, err)
+ }
+
+ for _, h := range macosHosts {
+ err = ds.AddHostsToTeam(ctx, &team.ID, []uint{h.ID})
+ require.NoError(t, err)
+ }
+
+ // team summary
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, &team.ID)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(0), summary.Verified)
+ require.Equal(t, uint(10), summary.ActionRequired)
+ require.Equal(t, uint(0), summary.Failed)
+
+ // no team summary
+ summary, err = ds.GetLinuxDiskEncryptionSummary(ctx, nil)
+ require.NoError(t, err)
+
+ require.Equal(t, uint(0), summary.Verified)
+ require.Equal(t, uint(0), summary.ActionRequired)
+ require.Equal(t, uint(0), summary.Failed)
+}
diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go
index 66f90c06f6af..5943b7ba8a4e 100644
--- a/server/datastore/mysql/mdm.go
+++ b/server/datastore/mysql/mdm.go
@@ -461,28 +461,36 @@ func (ds *Datastore) bulkSetPendingMDMHostProfilesDB(
}
case len(macProfUUIDs) > 0:
- // TODO: if a very large number (~65K) of profile UUIDs was provided, could
+ // TODO: if a very large number (~65K/2) of profile UUIDs was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `
SELECT DISTINCT h.uuid, h.platform
FROM hosts h
JOIN mdm_apple_configuration_profiles macp
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
+LEFT JOIN host_mdm_apple_profiles hmap
+ ON h.uuid = hmap.host_uuid
WHERE
- macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')`
- args = append(args, macProfUUIDs)
+ macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')
+OR
+ hmap.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')`
+ args = append(args, macProfUUIDs, macProfUUIDs)
case len(winProfUUIDs) > 0:
- // TODO: if a very large number (~65K) of profile IDs was provided, could
+ // TODO: if a very large number (~65K/2) of profile IDs was provided, could
// result in too many placeholders (not an immediate concern).
uuidStmt = `
SELECT DISTINCT h.uuid, h.platform
FROM hosts h
JOIN mdm_windows_configuration_profiles mawp
ON h.team_id = mawp.team_id OR (h.team_id IS NULL AND mawp.team_id = 0)
+LEFT JOIN host_mdm_windows_profiles hmwp
+ ON h.uuid = hmwp.host_uuid
WHERE
- mawp.profile_uuid IN (?) AND h.platform = 'windows'`
- args = append(args, winProfUUIDs)
+ mawp.profile_uuid IN (?) AND h.platform = 'windows'
+OR
+ hmwp.profile_uuid IN (?) AND h.platform = 'windows'`
+ args = append(args, winProfUUIDs, winProfUUIDs)
}
@@ -515,12 +523,12 @@ WHERE
}
}
- updates.AppleConfigProfile, err = ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts)
+ updates.AppleConfigProfile, err = ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts, profileUUIDs)
if err != nil {
return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
}
- updates.WindowsConfigProfile, err = ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts)
+ updates.WindowsConfigProfile, err = ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts, profileUUIDs)
if err != nil {
return updates, ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
}
@@ -834,7 +842,7 @@ FROM
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
WHERE
- macp.team_id = ? AND
+ macp.team_id = ? AND
NOT EXISTS (
SELECT
1
@@ -865,16 +873,16 @@ FROM
mdm_apple_configuration_profiles
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
- JOIN mdm_configuration_profile_labels mcpl
+ JOIN mdm_configuration_profile_labels mcpl
ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 0
- LEFT OUTER JOIN label_membership lm
+ LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
WHERE
macp.team_id = ?
GROUP BY
identifier
HAVING
- count_profile_labels > 0 AND
+ count_profile_labels > 0 AND
count_host_labels = count_profile_labels
UNION
@@ -897,9 +905,9 @@ FROM
mdm_apple_configuration_profiles
GROUP BY checksum
) cs ON macp.checksum = cs.checksum
- JOIN mdm_configuration_profile_labels mcpl
+ JOIN mdm_configuration_profile_labels mcpl
ON mcpl.apple_profile_uuid = macp.profile_uuid AND mcpl.exclude = 1
- LEFT OUTER JOIN label_membership lm
+ LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = ?
WHERE
macp.team_id = ?
@@ -907,7 +915,7 @@ GROUP BY
identifier
HAVING
-- considers only the profiles with labels, without any broken label, and with the host not in any label
- count_profile_labels > 0 AND
+ count_profile_labels > 0 AND
count_profile_labels = count_non_broken_labels AND
count_host_labels = 0
`
diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go
index 524888cf8d23..e846ef8af79e 100644
--- a/server/datastore/mysql/microsoft_mdm.go
+++ b/server/datastore/mysql/microsoft_mdm.go
@@ -585,7 +585,7 @@ AND (
}
func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) {
- enabled, err := ds.getConfigEnableDiskEncryption(ctx, teamID)
+ enabled, err := ds.GetConfigEnableDiskEncryption(ctx, teamID)
if err != nil {
return nil, err
}
@@ -655,7 +655,7 @@ func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fle
return nil, nil
}
- enabled, err := ds.getConfigEnableDiskEncryption(ctx, host.TeamID)
+ enabled, err := ds.GetConfigEnableDiskEncryption(ctx, host.TeamID)
if err != nil {
return nil, err
}
@@ -887,7 +887,7 @@ func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}, e
}
func (ds *Datastore) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
- includeBitLocker, err := ds.getConfigEnableDiskEncryption(ctx, teamID)
+ includeBitLocker, err := ds.GetConfigEnableDiskEncryption(ctx, teamID)
if err != nil {
return nil, err
}
@@ -1267,7 +1267,7 @@ func (ds *Datastore) ListMDMWindowsProfilesToInstall(ctx context.Context) ([]*fl
// be without and use the reader replica?
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
var err error
- result, err = listMDMWindowsProfilesToInstallDB(ctx, tx, nil)
+ result, err = listMDMWindowsProfilesToInstallDB(ctx, tx, nil, nil)
return err
})
return result, err
@@ -1277,6 +1277,7 @@ func listMDMWindowsProfilesToInstallDB(
ctx context.Context,
tx sqlx.ExtContext,
hostUUIDs []string,
+ onlyProfileUUIDs []string,
) ([]*fleet.MDMWindowsProfilePayload, error) {
// The query below is a set difference between:
//
@@ -1318,14 +1319,29 @@ func listMDMWindowsProfilesToInstallDB(
hostFilter := "TRUE"
if len(hostUUIDs) > 0 {
- hostFilter = "h.uuid IN (?)"
+ if len(onlyProfileUUIDs) > 0 {
+ hostFilter = "mwcp.profile_uuid IN (?) AND h.uuid IN (?)"
+ } else {
+ hostFilter = "h.uuid IN (?)"
+ }
}
var err error
args := []any{fleet.MDMOperationTypeInstall}
query = fmt.Sprintf(query, hostFilter, hostFilter, hostFilter, hostFilter)
if len(hostUUIDs) > 0 {
- query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
+ if len(onlyProfileUUIDs) > 0 {
+ query, args, err = sqlx.In(
+ query,
+ onlyProfileUUIDs, hostUUIDs,
+ onlyProfileUUIDs, hostUUIDs,
+ onlyProfileUUIDs, hostUUIDs,
+ onlyProfileUUIDs, hostUUIDs,
+ fleet.MDMOperationTypeInstall,
+ )
+ } else {
+ query, args, err = sqlx.In(query, hostUUIDs, hostUUIDs, hostUUIDs, hostUUIDs, fleet.MDMOperationTypeInstall)
+ }
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In")
}
@@ -1340,7 +1356,7 @@ func (ds *Datastore) ListMDMWindowsProfilesToRemove(ctx context.Context) ([]*fle
var result []*fleet.MDMWindowsProfilePayload
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
var err error
- result, err = listMDMWindowsProfilesToRemoveDB(ctx, tx, nil)
+ result, err = listMDMWindowsProfilesToRemoveDB(ctx, tx, nil, nil)
return err
})
@@ -1351,6 +1367,7 @@ func listMDMWindowsProfilesToRemoveDB(
ctx context.Context,
tx sqlx.ExtContext,
hostUUIDs []string,
+ onlyProfileUUIDs []string,
) ([]*fleet.MDMWindowsProfilePayload, error) {
// The query below is a set difference between:
//
@@ -1374,7 +1391,11 @@ func listMDMWindowsProfilesToRemoveDB(
hostFilter := "TRUE"
if len(hostUUIDs) > 0 {
- hostFilter = "hmwp.host_uuid IN (?)"
+ if len(onlyProfileUUIDs) > 0 {
+ hostFilter = "hmwp.profile_uuid IN (?) AND hmwp.host_uuid IN (?)"
+ } else {
+ hostFilter = "hmwp.host_uuid IN (?)"
+ }
}
query := fmt.Sprintf(`
@@ -1408,7 +1429,11 @@ func listMDMWindowsProfilesToRemoveDB(
var err error
var args []any
if len(hostUUIDs) > 0 {
- query, args, err = sqlx.In(query, hostUUIDs)
+ if len(onlyProfileUUIDs) > 0 {
+ query, args, err = sqlx.In(query, onlyProfileUUIDs, hostUUIDs)
+ } else {
+ query, args, err = sqlx.In(query, hostUUIDs)
+ }
if err != nil {
return nil, err
}
@@ -1917,18 +1942,19 @@ ON DUPLICATE KEY UPDATE
func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB(
ctx context.Context,
tx sqlx.ExtContext,
- uuids []string,
+ hostUUIDs []string,
+ onlyProfileUUIDs []string,
) (updatedDB bool, err error) {
- if len(uuids) == 0 {
+ if len(hostUUIDs) == 0 {
return false, nil
}
- profilesToInstall, err := listMDMWindowsProfilesToInstallDB(ctx, tx, uuids)
+ profilesToInstall, err := listMDMWindowsProfilesToInstallDB(ctx, tx, hostUUIDs, onlyProfileUUIDs)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "list profiles to install")
}
- profilesToRemove, err := listMDMWindowsProfilesToRemoveDB(ctx, tx, uuids)
+ profilesToRemove, err := listMDMWindowsProfilesToRemoveDB(ctx, tx, hostUUIDs, onlyProfileUUIDs)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "list profiles to remove")
}
diff --git a/server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go b/server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go
new file mode 100644
index 000000000000..71fa8847e243
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20241110152839_AddTeamIdentifierToHostSoftwareInstalledPaths.go
@@ -0,0 +1,23 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20241110152839, Down_20241110152839)
+}
+
+func Up_20241110152839(tx *sql.Tx) error {
+ if _, err := tx.Exec(`
+ ALTER TABLE host_software_installed_paths ADD COLUMN team_identifier VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''`,
+ ); err != nil {
+ return fmt.Errorf("failed to add team_identifier to host_software_installed_paths table: %w", err)
+ }
+ return nil
+}
+
+func Down_20241110152839(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20241025141856_AddYaraRulesTable.go b/server/datastore/mysql/migrations/tables/20241110152840_AddYaraRulesTable.go
similarity index 74%
rename from server/datastore/mysql/migrations/tables/20241025141856_AddYaraRulesTable.go
rename to server/datastore/mysql/migrations/tables/20241110152840_AddYaraRulesTable.go
index d5f043dc9416..e005c84d679e 100644
--- a/server/datastore/mysql/migrations/tables/20241025141856_AddYaraRulesTable.go
+++ b/server/datastore/mysql/migrations/tables/20241110152840_AddYaraRulesTable.go
@@ -6,10 +6,10 @@ import (
)
func init() {
- MigrationClient.AddMigration(Up_20241016155452, Down_20241016155452)
+ MigrationClient.AddMigration(Up_20241110152840, Down_20241110152840)
}
-func Up_20241016155452(tx *sql.Tx) error {
+func Up_20241110152840(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE yara_rules (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -24,6 +24,6 @@ CREATE TABLE yara_rules (
return nil
}
-func Down_20241016155452(tx *sql.Tx) error {
+func Down_20241110152840(tx *sql.Tx) error {
return nil
}
diff --git a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels.go
similarity index 87%
rename from server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go
rename to server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels.go
index ada5cb8f641a..f95946084d31 100644
--- a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels.go
+++ b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels.go
@@ -6,10 +6,10 @@ import (
)
func init() {
- MigrationClient.AddMigration(Up_20241030102721, Down_20241030102721)
+ MigrationClient.AddMigration(Up_20241110152841, Down_20241110152841)
}
-func Up_20241030102721(tx *sql.Tx) error {
+func Up_20241110152841(tx *sql.Tx) error {
// Add columns
_, err := tx.Exec(`ALTER TABLE mdm_configuration_profile_labels ADD COLUMN require_all BOOL NOT NULL DEFAULT false`)
if err != nil {
@@ -36,6 +36,6 @@ func Up_20241030102721(tx *sql.Tx) error {
return nil
}
-func Down_20241030102721(tx *sql.Tx) error {
+func Down_20241110152841(tx *sql.Tx) error {
return nil
}
diff --git a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels_test.go
similarity index 98%
rename from server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go
rename to server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels_test.go
index 4a0a9b97fc00..448d375652bf 100644
--- a/server/datastore/mysql/migrations/tables/20241030102721_AddAllLabelsToMDMProfileLabels_test.go
+++ b/server/datastore/mysql/migrations/tables/20241110152841_AddAllLabelsToMDMProfileLabels_test.go
@@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestUp_20241030102721(t *testing.T) {
+func TestUp_20241110152841(t *testing.T) {
db := applyUpToPrev(t)
// insert 2 profiles and 2 declarations
diff --git a/server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go b/server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go
new file mode 100644
index 000000000000..7a790a454099
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20241116233322_AddLuksDataToHostDiskEncryptionKeys.go
@@ -0,0 +1,25 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20241116233322, Down_20241116233322)
+}
+
+func Up_20241116233322(tx *sql.Tx) error {
+ _, err := tx.Exec(`ALTER TABLE host_disk_encryption_keys
+ ADD COLUMN base64_encrypted_salt VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' AFTER base64_encrypted,
+ ADD COLUMN key_slot TINYINT UNSIGNED DEFAULT NULL AFTER base64_encrypted_salt`)
+ if err != nil {
+ return fmt.Errorf("failed to add base64_encrypted_salt and key_slot columns to host_disk_encryption_keys: %w", err)
+ }
+
+ return nil
+}
+
+func Down_20241116233322(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index cbcc985cb204..45718cbd1d7c 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -303,6 +303,8 @@ CREATE TABLE `host_device_auth` (
CREATE TABLE `host_disk_encryption_keys` (
`host_id` int unsigned NOT NULL,
`base64_encrypted` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `base64_encrypted_salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
+ `key_slot` tinyint unsigned DEFAULT NULL,
`decryptable` tinyint(1) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -587,9 +589,10 @@ CREATE TABLE `host_software_installed_paths` (
`host_id` int unsigned NOT NULL,
`software_id` bigint unsigned NOT NULL,
`installed_path` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `team_identifier` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `host_id_software_id_idx` (`host_id`,`software_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
@@ -1101,9 +1104,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=329 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=331 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241025141856,1,'2020-01-01 01:01:01'),(328,20241030102721,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `mobile_device_management_solutions` (
diff --git a/server/datastore/mysql/setup_experience.go b/server/datastore/mysql/setup_experience.go
index 328cbb12aec0..33ffa264f087 100644
--- a/server/datastore/mysql/setup_experience.go
+++ b/server/datastore/mysql/setup_experience.go
@@ -109,8 +109,11 @@ WHERE global_or_team_id = ?`
}
totalInsertions += uint(inserts) // nolint: gosec
- if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil {
- return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true")
+ // Only run setup experience on hosts that have something configured.
+ if totalInsertions > 0 {
+ if err := setHostAwaitingConfiguration(ctx, tx, hostUUID, true); err != nil {
+ return ctxerr.Wrap(ctx, err, "setting host awaiting configuration to true")
+ }
}
return nil
@@ -503,7 +506,7 @@ WHERE host_uuid = ?
if err := sqlx.GetContext(ctx, ds.reader(ctx), &awaitingConfiguration, stmt, hostUUID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
- return false, nil
+ return false, notFound("HostAwaitingConfiguration")
}
return false, ctxerr.Wrap(ctx, err, "getting host awaiting configuration")
diff --git a/server/datastore/mysql/setup_experience_test.go b/server/datastore/mysql/setup_experience_test.go
index fa48a4107c96..1cbece2af264 100644
--- a/server/datastore/mysql/setup_experience_test.go
+++ b/server/datastore/mysql/setup_experience_test.go
@@ -39,20 +39,22 @@ func TestSetupExperience(t *testing.T) {
}
}
+// TODO(JVE): this test could probably be simplified and most of the ad-hoc SQL removed.
func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
ctx := context.Background()
test.CreateInsertGlobalVPPToken(t, ds)
+ // Create some teams
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
require.NoError(t, err)
-
team3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team3"})
require.NoError(t, err)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
+ // Create some software installers and add them to setup experience
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
@@ -96,6 +98,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
return err
})
+ // Create some VPP apps and add them to setup experience
app1 := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"}
vpp1, err := ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
require.NoError(t, err)
@@ -109,33 +112,17 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
return err
})
- var script1ID, script2ID int64
- ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- res, err := insertScriptContents(ctx, q, "SCRIPT 1")
- if err != nil {
- return err
- }
- id1, _ := res.LastInsertId()
- res, err = insertScriptContents(ctx, q, "SCRIPT 2")
- if err != nil {
- return err
- }
- id2, _ := res.LastInsertId()
-
- res, err = q.ExecContext(ctx, "INSERT INTO setup_experience_scripts (team_id, global_or_team_id, name, script_content_id) VALUES (?, ?, ?, ?)", team1.ID, team1.ID, "script1", id1)
- if err != nil {
- return err
- }
- script1ID, _ = res.LastInsertId()
+ // Create some scripts and add them to setup experience
+ err = ds.SetSetupExperienceScript(ctx, &fleet.Script{Name: "script1", ScriptContents: "SCRIPT 1", TeamID: &team1.ID})
+ require.NoError(t, err)
+ err = ds.SetSetupExperienceScript(ctx, &fleet.Script{Name: "script2", ScriptContents: "SCRIPT 2", TeamID: &team2.ID})
+ require.NoError(t, err)
- res, err = q.ExecContext(ctx, "INSERT INTO setup_experience_scripts (team_id, global_or_team_id, name, script_content_id) VALUES (?, ?, ?, ?)", team2.ID, team2.ID, "script2", id2)
- if err != nil {
- return err
- }
- script2ID, _ = res.LastInsertId()
+ script1, err := ds.GetSetupExperienceScript(ctx, &team1.ID)
+ require.NoError(t, err)
- return nil
- })
+ script2, err := ds.GetSetupExperienceScript(ctx, &team2.ID)
+ require.NoError(t, err)
hostTeam1 := "123"
hostTeam2 := "456"
@@ -144,14 +131,26 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
anythingEnqueued, err := ds.EnqueueSetupExperienceItems(ctx, hostTeam1, team1.ID)
require.NoError(t, err)
require.True(t, anythingEnqueued)
+ awaitingConfig, err := ds.GetHostAwaitingConfiguration(ctx, hostTeam1)
+ require.NoError(t, err)
+ require.True(t, awaitingConfig)
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam2, team2.ID)
require.NoError(t, err)
require.True(t, anythingEnqueued)
+ awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam2)
+ require.NoError(t, err)
+ require.True(t, awaitingConfig)
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam3, team3.ID)
require.NoError(t, err)
require.False(t, anythingEnqueued)
+ // Nothing is configured for setup experience in team 3, so we do not set
+ // host_mdm_apple_awaiting_configuration.
+ awaitingConfig, err = ds.GetHostAwaitingConfiguration(ctx, hostTeam3)
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+ require.False(t, awaitingConfig)
seRows := []setupExperienceInsertTestRows{}
@@ -190,13 +189,13 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
HostUUID: hostTeam1,
Name: "script1",
Status: "pending",
- ScriptID: nullableUint(uint(script1ID)), // nolint: gosec
+ ScriptID: nullableUint(script1.ID),
},
{
HostUUID: hostTeam2,
Name: "script2",
Status: "pending",
- ScriptID: nullableUint(uint(script2ID)), // nolint: gosec
+ ScriptID: nullableUint(script2.ID),
},
} {
var found bool
@@ -211,35 +210,28 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
}
}
- for _, row := range seRows {
- if row.HostUUID == hostTeam3 {
- t.Error("team 3 shouldn't have any any entries")
- }
- }
-
- ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
- _, err := q.ExecContext(ctx, "DELETE FROM setup_experience_scripts WHERE global_or_team_id = ?", team2.ID)
- if err != nil {
- return err
+ require.Condition(t, func() (success bool) {
+ for _, row := range seRows {
+ if row.HostUUID == hostTeam3 {
+ return false
+ }
}
- _, err = q.ExecContext(ctx, "UPDATE software_installers SET install_during_setup = false WHERE global_or_team_id = ?", team2.ID)
- if err != nil {
- return err
- }
+ return true
+ })
- _, err = q.ExecContext(ctx, "UPDATE vpp_apps_teams SET install_during_setup = false WHERE global_or_team_id = ?", team2.ID)
- if err != nil {
- return err
- }
+ // Remove team2's setup experience items
+ err = ds.DeleteSetupExperienceScript(ctx, &team2.ID)
+ require.NoError(t, err)
- return nil
- })
+ err = ds.SetSetupExperienceSoftwareTitles(ctx, team2.ID, []uint{})
+ require.NoError(t, err)
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam1, team1.ID)
require.NoError(t, err)
require.True(t, anythingEnqueued)
+ // team2 now has nothing enqueued
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, hostTeam2, team2.ID)
require.NoError(t, err)
require.False(t, anythingEnqueued)
@@ -271,7 +263,7 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
HostUUID: hostTeam1,
Name: "script1",
Status: "pending",
- ScriptID: nullableUint(uint(script1ID)), // nolint: gosec
+ ScriptID: nullableUint(script1.ID),
},
} {
var found bool
@@ -908,4 +900,10 @@ func testHostInSetupExperience(t *testing.T, ds *Datastore) {
inSetupExperience, err = ds.GetHostAwaitingConfiguration(ctx, "abc")
require.NoError(t, err)
require.False(t, inSetupExperience)
+
+ // host without a record in the table returns not found
+ inSetupExperience, err = ds.GetHostAwaitingConfiguration(ctx, "404")
+ require.Error(t, err)
+ require.True(t, fleet.IsNotFound(err))
+ require.False(t, inSetupExperience)
}
diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go
index d6cd45f962f8..d89bf398cf37 100644
--- a/server/datastore/mysql/software.go
+++ b/server/datastore/mysql/software.go
@@ -91,7 +91,7 @@ func (ds *Datastore) getHostSoftwareInstalledPaths(
error,
) {
stmt := `
- SELECT t.id, t.host_id, t.software_id, t.installed_path
+ SELECT t.id, t.host_id, t.software_id, t.installed_path, t.team_identifier
FROM host_software_installed_paths t
WHERE t.host_id = ?
`
@@ -145,7 +145,10 @@ func hostSoftwareInstalledPathsDelta(
continue
}
- key := fmt.Sprintf("%s%s%s", r.InstalledPath, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
+ key := fmt.Sprintf(
+ "%s%s%s%s%s",
+ r.InstalledPath, fleet.SoftwareFieldSeparator, r.TeamIdentifier, fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
+ )
iSPathLookup[key] = r
// Anything stored but not reported should be deleted
@@ -155,8 +158,8 @@ func hostSoftwareInstalledPathsDelta(
}
for key := range reported {
- parts := strings.SplitN(key, fleet.SoftwareFieldSeparator, 2)
- iSPath, unqStr := parts[0], parts[1]
+ parts := strings.SplitN(key, fleet.SoftwareFieldSeparator, 3)
+ installedPath, teamIdentifier, unqStr := parts[0], parts[1], parts[2]
// Shouldn't be possible ... everything 'reported' should be in the the software table
// because this executes after 'ds.UpdateHostSoftware'
@@ -172,9 +175,10 @@ func hostSoftwareInstalledPathsDelta(
}
toInsert = append(toInsert, fleet.HostSoftwareInstalledPath{
- HostID: hostID,
- SoftwareID: s.ID,
- InstalledPath: iSPath,
+ HostID: hostID,
+ SoftwareID: s.ID,
+ InstalledPath: installedPath,
+ TeamIdentifier: teamIdentifier,
})
}
@@ -211,7 +215,7 @@ func insertHostSoftwareInstalledPaths(
return nil
}
- stmt := "INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES %s"
+ stmt := "INSERT INTO host_software_installed_paths (host_id, software_id, installed_path, team_identifier) VALUES %s"
batchSize := 500
for i := 0; i < len(toInsert); i += batchSize {
@@ -223,10 +227,10 @@ func insertHostSoftwareInstalledPaths(
var args []interface{}
for _, v := range batch {
- args = append(args, v.HostID, v.SoftwareID, v.InstalledPath)
+ args = append(args, v.HostID, v.SoftwareID, v.InstalledPath, v.TeamIdentifier)
}
- placeHolders := strings.TrimSuffix(strings.Repeat("(?, ?, ?), ", len(batch)), ", ")
+ placeHolders := strings.TrimSuffix(strings.Repeat("(?, ?, ?, ?), ", len(batch)), ", ")
stmt := fmt.Sprintf(stmt, placeHolders)
_, err := tx.ExecContext(ctx, stmt, args...)
@@ -639,7 +643,19 @@ func (ds *Datastore) insertNewInstalledHostSoftwareDB(
)
// INSERT IGNORE is used to avoid duplicate key errors, which may occur since our previous read came from the replica.
stmt := fmt.Sprintf(
- "INSERT IGNORE INTO software (name, version, source, `release`, vendor, arch, bundle_identifier, extension_id, browser, title_id, checksum) VALUES %s",
+ `INSERT IGNORE INTO software (
+ name,
+ version,
+ source,
+ `+"`release`"+`,
+ vendor,
+ arch,
+ bundle_identifier,
+ extension_id,
+ browser,
+ title_id,
+ checksum
+ ) VALUES %s`,
values,
)
args := make([]interface{}, 0, totalToProcess*numberOfArgsPerSoftware)
@@ -1228,16 +1244,22 @@ func (ds *Datastore) LoadHostSoftware(ctx context.Context, host *fleet.Host, inc
return err
}
- lookup := make(map[uint][]string)
+ installedPathsList := make(map[uint][]string)
+ pathSignatureInformation := make(map[uint][]fleet.PathSignatureInformation)
for _, ip := range installedPaths {
- lookup[ip.SoftwareID] = append(lookup[ip.SoftwareID], ip.InstalledPath)
+ installedPathsList[ip.SoftwareID] = append(installedPathsList[ip.SoftwareID], ip.InstalledPath)
+ pathSignatureInformation[ip.SoftwareID] = append(pathSignatureInformation[ip.SoftwareID], fleet.PathSignatureInformation{
+ InstalledPath: ip.InstalledPath,
+ TeamIdentifier: ip.TeamIdentifier,
+ })
}
host.Software = make([]fleet.HostSoftwareEntry, 0, len(software))
for _, s := range software {
host.Software = append(host.Software, fleet.HostSoftwareEntry{
- Software: s,
- InstalledPaths: lookup[s.ID],
+ Software: s,
+ InstalledPaths: installedPathsList[s.ID],
+ PathSignatureInformation: pathSignatureInformation[s.ID],
})
}
return nil
@@ -1283,7 +1305,7 @@ func (ds *Datastore) AllSoftwareIterator(
var args []interface{}
stmt := `SELECT
- s.id, s.name, s.version, s.source, s.bundle_identifier, s.release, s.arch, s.vendor, s.browser, s.extension_id, s.title_id ,
+ s.id, s.name, s.version, s.source, s.bundle_identifier, s.release, s.arch, s.vendor, s.browser, s.extension_id, s.title_id,
COALESCE(sc.cpe, '') AS generated_cpe
FROM software s
LEFT JOIN software_cpe sc ON (s.id=sc.software_id)`
@@ -2524,6 +2546,8 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
st.id as software_title_id,
s.id as software_id,
s.version,
+ s.bundle_identifier,
+ s.source,
hs.last_opened_at
FROM
software s
@@ -2588,7 +2612,8 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
const pathsStmt = `
SELECT
hsip.software_id,
- hsip.installed_path
+ hsip.installed_path,
+ hsip.team_identifier
FROM
host_software_installed_paths hsip
WHERE
@@ -2598,8 +2623,9 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
software_id, installed_path
`
type installedPath struct {
- SoftwareID uint `db:"software_id"`
- InstalledPath string `db:"installed_path"`
+ SoftwareID uint `db:"software_id"`
+ InstalledPath string `db:"installed_path"`
+ TeamIdentifier string `db:"team_identifier"`
}
var installedPaths []installedPath
stmt, args, err = sqlx.In(pathsStmt, host.ID, softwareIDs)
@@ -2614,6 +2640,12 @@ INNER JOIN software_cve scve ON scve.software_id = s.id
for _, path := range installedPaths {
ver := bySoftwareID[path.SoftwareID]
ver.InstalledPaths = append(ver.InstalledPaths, path.InstalledPath)
+ if ver.Source == "apps" {
+ ver.SignatureInformation = append(ver.SignatureInformation, fleet.PathSignatureInformation{
+ InstalledPath: path.InstalledPath,
+ TeamIdentifier: path.TeamIdentifier,
+ })
+ }
}
}
}
diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go
index 4e99fe4b6a3b..a613ba82cbe4 100644
--- a/server/datastore/mysql/software_test.go
+++ b/server/datastore/mysql/software_test.go
@@ -58,10 +58,10 @@ func TestSoftware(t *testing.T) {
{"DeleteSoftwareCPEs", testDeleteSoftwareCPEs},
{"SoftwareByIDNoDuplicatedVulns", testSoftwareByIDNoDuplicatedVulns},
{"SoftwareByIDIncludesCVEPublishedDate", testSoftwareByIDIncludesCVEPublishedDate},
- {"getHostSoftwareInstalledPaths", testGetHostSoftwareInstalledPaths},
- {"hostSoftwareInstalledPathsDelta", testHostSoftwareInstalledPathsDelta},
- {"deleteHostSoftwareInstalledPaths", testDeleteHostSoftwareInstalledPaths},
- {"insertHostSoftwareInstalledPaths", testInsertHostSoftwareInstalledPaths},
+ {"GetHostSoftwareInstalledPaths", testGetHostSoftwareInstalledPaths},
+ {"HostSoftwareInstalledPathsDelta", testHostSoftwareInstalledPathsDelta},
+ {"DeleteHostSoftwareInstalledPaths", testDeleteHostSoftwareInstalledPaths},
+ {"InsertHostSoftwareInstalledPaths", testInsertHostSoftwareInstalledPaths},
{"VerifySoftwareChecksum", testVerifySoftwareChecksum},
{"ListHostSoftware", testListHostSoftware},
{"ListIOSHostSoftware", testListIOSHostSoftware},
@@ -1342,7 +1342,7 @@ func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) {
// Insert paths for software1
s1Paths := map[string]struct{}{}
for _, s := range software1 {
- key := fmt.Sprintf("%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, s.ToUniqueStr())
+ key := fmt.Sprintf("%s%s%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr())
s1Paths[key] = struct{}{}
}
require.NoError(t, ds.UpdateHostSoftwareInstalledPaths(context.Background(), host1.ID, s1Paths, mutationResults))
@@ -1353,7 +1353,7 @@ func insertVulnSoftwareForTest(t *testing.T, ds *Datastore) {
// Insert paths for software2
s2Paths := map[string]struct{}{}
for _, s := range software2 {
- key := fmt.Sprintf("%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, s.ToUniqueStr())
+ key := fmt.Sprintf("%s%s%s%s%s", fmt.Sprintf("/some/path/%s", s.Name), fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr())
s2Paths[key] = struct{}{}
}
require.NoError(t, ds.UpdateHostSoftwareInstalledPaths(context.Background(), host2.ID, s2Paths, mutationResults))
@@ -2733,9 +2733,9 @@ func testHostSoftwareInstalledPathsDelta(t *testing.T, ds *Datastore) {
t.Run("host has no software but some paths were reported", func(t *testing.T) {
reported := make(map[string]struct{})
- reported[fmt.Sprintf("/some/path/%d%s%s", software[0].ID, fleet.SoftwareFieldSeparator, software[0].ToUniqueStr())] = struct{}{}
- reported[fmt.Sprintf("/some/path/%d%s%s", software[1].ID+1, fleet.SoftwareFieldSeparator, software[1].ToUniqueStr())] = struct{}{}
- reported[fmt.Sprintf("/some/path/%d%s%s", software[2].ID, fleet.SoftwareFieldSeparator, software[2].ToUniqueStr())] = struct{}{}
+ reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[0].ID, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[0].ToUniqueStr())] = struct{}{}
+ reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[1].ID+1, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[1].ToUniqueStr())] = struct{}{}
+ reported[fmt.Sprintf("/some/path/%d%s%s%s%s", software[2].ID, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, software[2].ToUniqueStr())] = struct{}{}
var stored []fleet.HostSoftwareInstalledPath
_, _, err := hostSoftwareInstalledPathsDelta(host.ID, reported, stored, nil)
@@ -2744,7 +2744,7 @@ func testHostSoftwareInstalledPathsDelta(t *testing.T, ds *Datastore) {
t.Run("we have some deltas", func(t *testing.T) {
getKey := func(s fleet.Software, change uint) string {
- return fmt.Sprintf("/some/path/%d%s%s", s.ID+change, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
+ return fmt.Sprintf("/some/path/%d%s%s%s%s", s.ID+change, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr())
}
reported := make(map[string]struct{})
reported[getKey(software[0], 0)] = struct{}{}
@@ -3308,7 +3308,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
installPaths := make([]string, 0, len(software))
for _, s := range software {
path := fmt.Sprintf("/some/path/%s", s.Name)
- key := fmt.Sprintf("%s%s%s", path, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
+ key := fmt.Sprintf("%s%s%s%s%s", path, fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, s.ToUniqueStr())
swPaths[key] = struct{}{}
installPaths = append(installPaths, path)
}
diff --git a/server/fleet/activities.go b/server/fleet/activities.go
index f62689adaaa8..751218ac6eb5 100644
--- a/server/fleet/activities.go
+++ b/server/fleet/activities.go
@@ -50,6 +50,7 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeChangedUserTeamRole{},
ActivityTypeDeletedUserTeamRole{},
+ ActivityTypeFleetEnrolled{},
ActivityTypeMDMEnrolled{},
ActivityTypeMDMUnenrolled{},
@@ -795,6 +796,25 @@ func (a ActivityTypeDeletedUserTeamRole) Documentation() (activity string, detai
}`
}
+type ActivityTypeFleetEnrolled struct {
+ HostSerial string `json:"host_serial"`
+ HostDisplayName string `json:"host_display_name"`
+}
+
+func (a ActivityTypeFleetEnrolled) ActivityName() string {
+ return "fleet_enrolled"
+}
+
+func (a ActivityTypeFleetEnrolled) Documentation() (activity string, details string, detailsExample string) {
+ return `Generated when a host is enrolled to Fleet (Fleet's agent fleetd is installed).`,
+ `This activity contains the following fields:
+- "host_serial": Serial number of the host.
+- "host_display_name": Display name of the host.`, `{
+ "host_serial": "B04FL3ALPT21",
+ "host_display_name": "WIN-DESKTOP-JGS78KJ7C"
+}`
+}
+
type ActivityTypeMDMEnrolled struct {
HostSerial string `json:"host_serial"`
HostDisplayName string `json:"host_display_name"`
diff --git a/server/fleet/capabilities.go b/server/fleet/capabilities.go
index 7da6f3620522..d8abea773166 100644
--- a/server/fleet/capabilities.go
+++ b/server/fleet/capabilities.go
@@ -80,6 +80,9 @@ const (
CapabilityEndUserEmail Capability = "end_user_email"
// CapabilityEscrowBuddy allows to use Escrow Buddy to rotate FileVault keys
CapabilityEscrowBuddy Capability = "escrow_buddy"
+ // CapabilityLinuxDiskEncryptionEscrow denotes the ability of the server to escrow Ubuntu and Fedora disk
+ // encryption LUKS passphrases
+ CapabilityLinuxDiskEncryptionEscrow Capability = "linux_disk_encryption_escrow"
// CapabilitySetupExperience denotes the ability of the server to support
// installing software and running a script during macOS ADE enrollment, and
// the ability of the client to show the corresponding UI to support that
@@ -89,11 +92,12 @@ const (
func GetServerOrbitCapabilities() CapabilityMap {
return CapabilityMap{
- CapabilityOrbitEndpoints: {},
- CapabilityTokenRotation: {},
- CapabilityEndUserEmail: {},
- CapabilityEscrowBuddy: {},
- CapabilitySetupExperience: {},
+ CapabilityOrbitEndpoints: {},
+ CapabilityTokenRotation: {},
+ CapabilityEndUserEmail: {},
+ CapabilityEscrowBuddy: {},
+ CapabilityLinuxDiskEncryptionEscrow: {},
+ CapabilitySetupExperience: {},
}
}
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index c05e5ac4fcd5..2a536c8a8dde 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -842,9 +842,12 @@ type Datastore interface {
// UpdateHostSoftwareInstalledPaths looks at all software for 'hostID' and based on the contents of
// 'reported', either inserts or deletes the corresponding entries in the
// 'host_software_installed_paths' table. 'reported' is a set of
- // 'software.ToUniqueStr()--installed_path' strings. 'mutationResults' contains the software inventory of
+ // 'installed_path\0team_identifier\0software.ToUniqueStr()' strings. 'mutationResults' contains the software inventory of
// the host (pre-mutations) and the mutations performed after calling 'UpdateHostSoftware',
// it is used as DB optimization.
+ //
+ // TODO(lucas): We should amend UpdateHostSoftwareInstalledPaths to just accept raw information
+ // otherwise the caller has to assemble the reported set the same way in all places where it's used.
UpdateHostSoftwareInstalledPaths(ctx context.Context, hostID uint, reported map[string]struct{}, mutationResults *UpdateHostSoftwareDBResult) error
// UpdateHost updates a host.
@@ -895,19 +898,30 @@ type Datastore interface {
// GetHostEmails returns the emails associated with the provided host for a given source, such as "google_chrome_profiles"
GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error)
SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error
+
+ GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error)
SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error
// SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for
// a host
SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error
+ // SaveLUKSData sets base64'd encrypted LUKS passphrase, key slot, and salt data for a host that has successfully
+ // escrowed LUKS data
+ SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error
+
// GetUnverifiedDiskEncryptionKeys returns all the encryption keys that
// are collected but their decryptable status is not known yet (ie:
// we're able to decrypt the key using a private key in the server)
GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]HostDiskEncryptionKey, error)
// SetHostsDiskEncryptionKeyStatus sets the encryptable status for the set
// of encription keys provided
- SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error
+ SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error
// GetHostDiskEncryptionKey returns the encryption key information for a given host
GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*HostDiskEncryptionKey, error)
+ IsHostPendingEscrow(ctx context.Context, hostID uint) bool
+ ClearPendingEscrow(ctx context.Context, hostID uint) error
+ ReportEscrowError(ctx context.Context, hostID uint, err string) error
+ QueueEscrow(ctx context.Context, hostID uint) error
+ AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error
// GetHostCertAssociationsToExpire retrieves host certificate
// associations that are close to expire and don't have a renewal in
@@ -1298,6 +1312,11 @@ type Datastore interface {
// a map that only contains the serials that have a matching row in the `hosts` table.
GetMatchingHostSerials(ctx context.Context, serials []string) (map[string]*Host, error)
+ // GetMatchingHostSerialsMarkedDeleted takes a list of device serial numbers and returns a map
+ // of only the ones that were found in the `hosts` table AND have a row in
+ // `host_dep_assignments` that is marked as deleted.
+ GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error)
+
// DeleteHostDEPAssignmentsFromAnotherABM makes as deleted any DEP entry that matches one of the provided serials only if the entry is NOT associated to the provided ABM token.
DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error
@@ -1496,6 +1515,14 @@ type Datastore interface {
// GetHostMDMProfileInstallStatus returns the status of the profile for the host.
GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID string, profileUUID string) (MDMDeliveryStatus, error)
+ ///////////////////////////////////////////////////////////////////////////////
+ // Linux MDM
+
+ // GetLinuxDiskEncryptionSummary summarizes the current state of Linux disk encryption on
+ // each Linux host in the specified team (or, if no team is specified, each host that is not assigned
+ // to any team).
+ GetLinuxDiskEncryptionSummary(ctx context.Context, teamID *uint) (MDMLinuxDiskEncryptionSummary, error)
+
///////////////////////////////////////////////////////////////////////////////
// MDM Commands
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index 3ff287c9f10d..95ce9ee268e8 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -322,7 +322,7 @@ type Host struct {
// DiskEncryptionEnabled is only returned by GET /host/{id} and so is not
// exportable as CSV (which is the result of List Hosts endpoint). It is
- // a *bool because for Linux we set it to NULL and omit it from the JSON
+ // a *bool because for some Linux we set it to NULL and omit it from the JSON
// response if the host does not have disk encryption enabled. It is also
// omitted if we don't have encryption information yet.
DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled" csv:"-"`
@@ -682,6 +682,12 @@ func (h *Host) IsDEPAssignedToFleet() bool {
return h.DEPAssignedToFleet != nil && *h.DEPAssignedToFleet
}
+// IsLUKSSupported returns true if the host's platform is Linux and running
+// one of the supported OS versions.
+func (h *Host) IsLUKSSupported() bool {
+ return h.Platform == "ubuntu" || strings.Contains(h.OSVersion, "Fedora") // fedora h.Platform reports as "rhel"
+}
+
// IsEligibleForWindowsMDMUnenrollment returns true if the host must be
// unenrolled from Fleet's Windows MDM (if it MDM was disabled).
func (h *Host) IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM bool) bool {
@@ -1173,6 +1179,7 @@ type HostDiskEncryptionKey struct {
Decryptable *bool `json:"-" db:"decryptable"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
DecryptedValue string `json:"key" db:"-"`
+ ClientError string `json:"-" db:"client_error"`
}
// HostSoftwareInstalledPath represents where in the file system a software on a host was installed
@@ -1185,6 +1192,9 @@ type HostSoftwareInstalledPath struct {
SoftwareID uint `db:"software_id"`
// InstalledPath is the file system path where the software is installed
InstalledPath string `db:"installed_path"`
+ // TeamIdentifier (not to be confused with Fleet's team IDs) is the Apple's "Team ID" (aka "Developer ID"
+ // or "Signing ID") of signed applications, see https://developer.apple.com/help/account/manage-your-team/locate-your-team-id.
+ TeamIdentifier string `db:"team_identifier"`
}
// HostMacOSProfile represents a macOS profile installed on a host as reported by the macos_profiles
diff --git a/server/fleet/linux_mdm.go b/server/fleet/linux_mdm.go
new file mode 100644
index 000000000000..7a9a5544e105
--- /dev/null
+++ b/server/fleet/linux_mdm.go
@@ -0,0 +1,7 @@
+package fleet
+
+type MDMLinuxDiskEncryptionSummary struct {
+ Verified uint `json:"verified"`
+ ActionRequired uint `json:"action_required"`
+ Failed uint `json:"failed"`
+}
diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go
index 7f717eeb14ba..4d3e09f68ec8 100644
--- a/server/fleet/mdm.go
+++ b/server/fleet/mdm.go
@@ -298,6 +298,7 @@ type MDMCommandFilters struct {
type MDMPlatformsCounts struct {
MacOS uint `db:"macos" json:"macos"`
Windows uint `db:"windows" json:"windows"`
+ Linux uint `db:"linux" json:"linux"`
}
type MDMDiskEncryptionSummary struct {
@@ -309,8 +310,8 @@ type MDMDiskEncryptionSummary struct {
RemovingEnforcement MDMPlatformsCounts `db:"removing_enforcement" json:"removing_enforcement"`
}
-// MDMProfilesSummary reports the number of hosts being managed with MDM configuration
-// profiles. Each host may be counted in only one of four mutually-exclusive categories:
+// MDMProfilesSummary reports the number of hosts being managed with configuration
+// profiles and/or disk encryption. Each host may be counted in only one of four mutually-exclusive categories:
// Failed, Pending, Verifying, or Verified.
type MDMProfilesSummary struct {
// Verified includes each host where Fleet has verified the installation of all of the
diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go
index 1d340b1e7b34..6c06a963e263 100644
--- a/server/fleet/orbit.go
+++ b/server/fleet/orbit.go
@@ -40,6 +40,9 @@ type OrbitConfigNotifications struct {
// RunSetupExperience indicates whether or not Orbit should run the Fleet setup experience
// during macOS Setup Assistant.
RunSetupExperience bool `json:"run_setup_experience,omitempty"`
+
+ // RunDiskEncryptionEscrow tells Orbit to prompt the end user to escrow disk encryption data
+ RunDiskEncryptionEscrow bool `json:"run_disk_encryption_escrow,omitempty"`
}
type OrbitConfig struct {
@@ -91,6 +94,10 @@ type OrbitHostInfo struct {
//
// If not set, then the HardwareUUID is used/set as the osquery identifier.
OsqueryIdentifier string
+ // ComputerName is the device's friendly name (optional).
+ ComputerName string
+ // HardwareModel is the device's hardware model. For example: Standard PC (Q35 + ICH9, 2009)
+ HardwareModel string
}
// ExtensionInfo holds the data of a osquery extension to apply to an Orbit client.
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 285ddac74d7a..db7fa3b11040 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -394,6 +394,7 @@ type Service interface {
GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error)
HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error)
+ EscrowLUKSData(ctx context.Context, passphrase string, salt string, keySlot *uint, clientError string) error
// AddLabelsToHost adds the given label names to the host's label membership.
//
@@ -850,6 +851,9 @@ type Service interface {
// ListABMTokens lists all the ABM tokens in Fleet.
ListABMTokens(ctx context.Context) ([]*ABMToken, error)
+ // CountABMTokens counts the ABM tokens in Fleet.
+ CountABMTokens(ctx context.Context) (int, error)
+
// UpdateABMTokenTeams updates the default macOS, iOS, and iPadOS team IDs for a given ABM token.
UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOSTeamID, iOSTeamID, iPadOSTeamID *uint) (*ABMToken, error)
@@ -964,6 +968,8 @@ type Service interface {
GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error)
+ TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *Host) error
+
// CheckMDMAppleEnrollmentWithMinimumOSVersion checks if the minimum OS version is met for a MDM enrollment
CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx context.Context, m *MDMAppleMachineInfo) (*MDMAppleSoftwareUpdateRequired, error)
@@ -1049,11 +1055,23 @@ type Service interface {
assumeEnabled *bool,
) error
+ ///////////////////////////////////////////////////////////////////////////////
+ // Linux MDM
+
+ // LinuxHostDiskEncryptionStatus returns the current disk encryption status of the specified Linux host
+ // Returns empty status if the host is not a supported Linux host
+ LinuxHostDiskEncryptionStatus(ctx context.Context, host Host) (HostMDMDiskEncryption, error)
+
+ // GetMDMLinuxProfilesSummary summarizes the current status of Linux disk encryption for
+ // the provided team (or hosts without a team if teamId is nil), or returns zeroes if disk
+ // encryption is not enforced on the selected team
+ GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (MDMProfilesSummary, error)
+
///////////////////////////////////////////////////////////////////////////////
// Common MDM
- // GetMDMDiskEncryptionSummary returns the current disk encryption status of all macOS and
- // Windows hosts in the specified team (or, if no team is specified, each host that is not
+ // GetMDMDiskEncryptionSummary returns the current disk encryption status of all macOS, Windows, and
+ // Linux hosts in the specified team (or, if no team is specified, each host that is not
// assigned to any team).
GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uint) (*MDMDiskEncryptionSummary, error)
@@ -1169,4 +1187,6 @@ const (
BatchSetSoftwareInstallersStatusCompleted = "completed"
// BatchSetSoftwareInstallerStatusFailed is the value returned for a failed BatchSetSoftwareInstallers operation.
BatchSetSoftwareInstallersStatusFailed = "failed"
+ // MinOrbitLUKSVersion is the earliest version of Orbit that can escrow LUKS passphrases
+ MinOrbitLUKSVersion = "1.36.0"
)
diff --git a/server/fleet/software.go b/server/fleet/software.go
index 487c1a50e9f6..04b40771312c 100644
--- a/server/fleet/software.go
+++ b/server/fleet/software.go
@@ -28,6 +28,10 @@ const (
SoftwareReleaseMaxLength = 64
SoftwareVendorMaxLength = 114
SoftwareArchMaxLength = 16
+
+ // SoftwareTeamIdentifierMaxLength is the max length for Apple's Team ID,
+ // see https://developer.apple.com/help/account/manage-your-team/locate-your-team-id
+ SoftwareTeamIdentifierMaxLength = 10
)
type Vulnerabilities []CVE
@@ -271,7 +275,13 @@ type HostSoftwareEntry struct {
Software
// Where this software was installed on the host, value is derived from the
// host_software_installed_paths table.
- InstalledPaths []string `json:"installed_paths"`
+ InstalledPaths []string `json:"installed_paths"`
+ PathSignatureInformation []PathSignatureInformation `json:"signature_information"`
+}
+
+type PathSignatureInformation struct {
+ InstalledPath string `json:"installed_path"`
+ TeamIdentifier string `json:"team_identifier"`
}
// HostSoftware is the set of software installed on a specific host
@@ -383,9 +393,10 @@ func ParseSoftwareLastOpenedAtRowValue(value string) (time.Time, error) {
//
// All fields are trimmed to fit on Fleet's database.
// The vendor field is currently trimmed by removing the extra characters and adding `...` at the end.
-func SoftwareFromOsqueryRow(name, version, source, vendor, installedPath, release, arch, bundleIdentifier, extensionId, browser, lastOpenedAt string) (
- *Software, error,
-) {
+func SoftwareFromOsqueryRow(
+ name, version, source, vendor, installedPath, release, arch,
+ bundleIdentifier, extensionId, browser, lastOpenedAt string,
+) (*Software, error) {
if name == "" {
return nil, errors.New("host reported software with empty name")
}
diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go
index aae130902a1c..835762c1549e 100644
--- a/server/fleet/software_installer.go
+++ b/server/fleet/software_installer.go
@@ -480,15 +480,18 @@ type HostSoftwareUninstall struct {
UninstalledAt time.Time `json:"uninstalled_at"`
}
-// HostSoftwareInstalledVersion represents a version of software installed on a
-// host.
+// HostSoftwareInstalledVersion represents a version of software installed on a host.
type HostSoftwareInstalledVersion struct {
- SoftwareID uint `json:"-" db:"software_id"`
- SoftwareTitleID uint `json:"-" db:"software_title_id"`
- Version string `json:"version" db:"version"`
- LastOpenedAt *time.Time `json:"last_opened_at" db:"last_opened_at"`
- Vulnerabilities []string `json:"vulnerabilities" db:"vulnerabilities"`
- InstalledPaths []string `json:"installed_paths" db:"installed_paths"`
+ SoftwareID uint `json:"-" db:"software_id"`
+ SoftwareTitleID uint `json:"-" db:"software_title_id"`
+ Source string `json:"-" db:"source"`
+ Version string `json:"version" db:"version"`
+ BundleIdentifier string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
+ LastOpenedAt *time.Time `json:"last_opened_at" db:"last_opened_at"`
+
+ Vulnerabilities []string `json:"vulnerabilities" db:"vulnerabilities"`
+ InstalledPaths []string `json:"installed_paths"`
+ SignatureInformation []PathSignatureInformation `json:"signature_information,omitempty"`
}
// HostSoftwareInstallResultPayload is the payload provided by fleetd to record
diff --git a/server/fleet/utils.go b/server/fleet/utils.go
index ddab26e8e797..fb286b56a764 100644
--- a/server/fleet/utils.go
+++ b/server/fleet/utils.go
@@ -6,6 +6,7 @@ import (
"io"
"strings"
+ "github.com/Masterminds/semver"
"github.com/fatih/color"
"golang.org/x/text/unicode/norm"
)
@@ -65,3 +66,28 @@ func Preprocess(input string) string {
// Normalize Unicode characters.
return norm.NFC.String(input)
}
+
+// CompareVersions returns an integer comparing two versions according to semantic version
+// precedence. The result will be 0 if a == b, -1 if a < b, or +1 if a > b.
+// An invalid semantic version string is considered less than a valid one. All invalid semantic
+// version strings compare equal to each other.
+func CompareVersions(a string, b string) int {
+ verA, errA := semver.NewVersion(a)
+ verB, errB := semver.NewVersion(b)
+ switch {
+ case errA != nil && errB != nil:
+ return 0
+ case errA != nil:
+ return -1
+ case errB != nil:
+ return 1
+ default:
+ return verA.Compare(verB)
+ }
+}
+
+// IsAtLeastVersion returns whether currentVersion is at least minimumVersion, using semantics
+// of CompareVersions for version validity
+func IsAtLeastVersion(currentVersion string, minimumVersion string) bool {
+ return CompareVersions(currentVersion, minimumVersion) >= 0
+}
diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go
index 49e1125f247a..5ea71f3aa284 100644
--- a/server/mdm/apple/apple_mdm.go
+++ b/server/mdm/apple/apple_mdm.go
@@ -660,6 +660,18 @@ func (d *DEPService) processDeviceResponse(
for _, device := range addedDevicesSlice {
addedSerials = append(addedSerials, device.SerialNumber)
}
+
+ // Check if any of the "added" or "modified" hosts are hosts that we've recently removed from
+ // Fleet in ABM. A host in this state will have a row in `host_dep_assignments` where the
+ // `deleted_at ` col is NOT NULL. Down below we skip assigning the profile to devices that we
+ // think are still enrolled; doing this check here allows us to avoid skipping devices that
+ // _seem_ like they're still enrolled but were actually removed and should get the profile.
+ // See https://github.com/fleetdm/fleet/issues/23200 for more context.
+ existingDeletedSerials, err := d.ds.GetMatchingHostSerialsMarkedDeleted(ctx, addedSerials)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "get matching deleted host serials")
+ }
+
err = d.ds.DeleteHostDEPAssignmentsFromAnotherABM(ctx, abmTokenID, addedSerials)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting dep assignments from another abm")
@@ -682,7 +694,7 @@ func (d *DEPService) processDeviceResponse(
}
level.Debug(kitlog.With(d.logger)).Log("msg", "devices to assign DEP profiles", "to_add", len(addedDevicesSlice), "to_remove",
- deletedSerials, "to_modify", modifiedSerials)
+ strings.Join(deletedSerials, ", "), "to_modify", strings.Join(modifiedSerials, ", "))
// at this point, the hosts rows are created for the devices, with the
// correct team_id, so we know what team-specific profile needs to be applied.
@@ -754,7 +766,8 @@ func (d *DEPService) processDeviceResponse(
for profUUID, devices := range profileToDevices {
var serials []string
for _, device := range devices {
- if device.ProfileUUID == profUUID {
+ _, ok := existingDeletedSerials[device.SerialNumber]
+ if device.ProfileUUID == profUUID && !ok {
skippedSerials = append(skippedSerials, device.SerialNumber)
continue
}
diff --git a/server/mdm/apple/gdmf/api.go b/server/mdm/apple/gdmf/api.go
index ee8c671814cd..d969c2580081 100644
--- a/server/mdm/apple/gdmf/api.go
+++ b/server/mdm/apple/gdmf/api.go
@@ -15,7 +15,6 @@ import (
"github.com/cenkalti/backoff"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
- apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
)
const baseURL = "https://gdmf.apple.com/v2/pmv"
@@ -91,7 +90,7 @@ func GetLatestOSVersion(device fleet.MDMAppleMachineInfo) (*Asset, error) {
latestIdx = i // first match found, update the index
continue
}
- if apple_mdm.CompareVersions(assetSet[latestIdx].ProductVersion, s.ProductVersion) < 0 {
+ if fleet.CompareVersions(assetSet[latestIdx].ProductVersion, s.ProductVersion) < 0 {
latestIdx = i // found a later version, update the index
}
}
diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go
index 88267f953fa3..200e1d4f39ad 100644
--- a/server/mdm/apple/util.go
+++ b/server/mdm/apple/util.go
@@ -124,22 +124,3 @@ func IsLessThanVersion(current string, target string) (bool, error) {
return cv.LessThan(tv), nil
}
-
-// CompareVersions returns an integer comparing two versions according to semantic version
-// precedence. The result will be 0 if a == b, -1 if a < b, or +1 if a > b.
-// An invalid semantic version string is considered less than a valid one. All invalid semantic
-// version strings compare equal to each other.
-func CompareVersions(a string, b string) int {
- verA, errA := semver.NewVersion(a)
- verB, errB := semver.NewVersion(b)
- switch {
- case errA != nil && errB != nil:
- return 0
- case errA != nil:
- return -1
- case errB != nil:
- return 1
- default:
- return verA.Compare(verB)
- }
-}
diff --git a/server/mdm/lifecycle/lifecycle.go b/server/mdm/lifecycle/lifecycle.go
index 33658a23678d..fd96454274f3 100644
--- a/server/mdm/lifecycle/lifecycle.go
+++ b/server/mdm/lifecycle/lifecycle.go
@@ -32,13 +32,14 @@ const (
// Not all options are required for all actions, each individual action should
// validate that it receives the required information.
type HostOptions struct {
- Action HostAction
- Platform string
- UUID string
- HardwareSerial string
- HardwareModel string
- EnrollReference string
- Host *fleet.Host
+ Action HostAction
+ Platform string
+ UUID string
+ HardwareSerial string
+ HardwareModel string
+ EnrollReference string
+ Host *fleet.Host
+ HasSetupExperienceItems bool
}
// HostLifecycle manages MDM host lifecycle actions
@@ -174,6 +175,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro
opts.Platform,
tmID,
opts.EnrollReference,
+ !opts.HasSetupExperienceItems,
)
return ctxerr.Wrap(ctx, err, "queue DEP post-enroll task")
}
@@ -189,6 +191,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro
opts.Platform,
tmID,
opts.EnrollReference,
+ false,
); err != nil {
return ctxerr.Wrap(ctx, err, "queue manual post-enroll task")
}
diff --git a/server/mdm/maintainedapps/apps.json b/server/mdm/maintainedapps/apps.json
index ce8eaf816e00..f832c158582b 100644
--- a/server/mdm/maintainedapps/apps.json
+++ b/server/mdm/maintainedapps/apps.json
@@ -30,7 +30,8 @@
{
"identifier": "cloudflare-warp",
"bundle_identifier": "com.cloudflare.1dot1dot1dot1.macos",
- "installer_format": "pkg"
+ "installer_format": "pkg",
+ "post_uninstall_scripts": ["/Applications/Cloudflare\\ WARP.app/Contents/Resources/uninstall.sh"]
},
{
"identifier": "docker",
diff --git a/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh
index 838490f85ee1..d118bf83e573 100644
--- a/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh
+++ b/server/mdm/maintainedapps/testdata/scripts/cloudflare-warp_uninstall.golden.sh
@@ -113,6 +113,7 @@ remove_launchctl_service 'com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp'
remove_launchctl_service 'com.cloudflare.1dot1dot1dot1.macos.warp.daemon'
quit_application 'com.cloudflare.1dot1dot1dot1.macos'
sudo pkgutil --forget 'com.cloudflare.1dot1dot1dot1.macos'
+/Applications/Cloudflare\ WARP.app/Contents/Resources/uninstall.sh
trash $LOGGED_IN_USER '~/Library/Application Scripts/com.cloudflare.1dot1dot1dot1.macos.loginlauncherapp'
trash $LOGGED_IN_USER '~/Library/Application Support/com.cloudflare.1dot1dot1dot1.macos'
trash $LOGGED_IN_USER '~/Library/Caches/com.cloudflare.1dot1dot1dot1.macos'
diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go
index 5aaae483d8e6..cf800dcfa690 100644
--- a/server/mdm/mdm.go
+++ b/server/mdm/mdm.go
@@ -3,8 +3,13 @@ package mdm
import (
"bytes"
"crypto"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
"crypto/x509"
"encoding/base64"
+ "fmt"
+ "io"
"github.com/smallstep/pkcs7"
)
@@ -81,6 +86,55 @@ func GuessProfileExtension(profile []byte) string {
}
}
+func EncryptAndEncode(plainText string, symmetricKey string) (string, error) {
+ block, err := aes.NewCipher([]byte(symmetricKey))
+ if err != nil {
+ return "", fmt.Errorf("create new cipher: %w", err)
+ }
+
+ aesGCM, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", fmt.Errorf("create new gcm: %w", err)
+ }
+
+ nonce := make([]byte, aesGCM.NonceSize())
+ if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
+ return "", fmt.Errorf("generate nonce: %w", err)
+ }
+
+ return base64.StdEncoding.EncodeToString(aesGCM.Seal(nonce, nonce, []byte(plainText), nil)), nil
+}
+
+func DecodeAndDecrypt(base64CipherText string, symmetricKey string) (string, error) {
+ encrypted, err := base64.StdEncoding.DecodeString(base64CipherText)
+ if err != nil {
+ return "", fmt.Errorf("base64 decode: %w", err)
+ }
+
+ block, err := aes.NewCipher([]byte(symmetricKey))
+ if err != nil {
+ return "", fmt.Errorf("create new cipher: %w", err)
+ }
+
+ aesGCM, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", fmt.Errorf("create new gcm: %w", err)
+ }
+
+ // Get the nonce size
+ nonceSize := aesGCM.NonceSize()
+
+ // Extract the nonce from the encrypted data
+ nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
+
+ decrypted, err := aesGCM.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return "", fmt.Errorf("decrypting: %w", err)
+ }
+
+ return string(decrypted), nil
+}
+
const (
// FleetdConfigProfileName is the value for the PayloadDisplayName used by
// fleetd to read configuration values from the system.
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index 48cc6d60766a..7c1f909f0ba6 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -635,16 +635,30 @@ type GetHostEmailsFunc func(ctx context.Context, hostUUID string, source string)
type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error
+type GetConfigEnableDiskEncryptionFunc func(ctx context.Context, teamID *uint) (bool, error)
+
type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error
type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error
+type SaveLUKSDataFunc func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error
+
type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error)
-type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error
+type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error
type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error)
+type IsHostPendingEscrowFunc func(ctx context.Context, hostID uint) bool
+
+type ClearPendingEscrowFunc func(ctx context.Context, hostID uint) error
+
+type ReportEscrowErrorFunc func(ctx context.Context, hostID uint, err string) error
+
+type QueueEscrowFunc func(ctx context.Context, hostID uint) error
+
+type AssertHasNoEncryptionKeyStoredFunc func(ctx context.Context, hostID uint) error
+
type GetHostCertAssociationsToExpireFunc func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error)
type SetCommandForPendingSCEPRenewalFunc func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error
@@ -859,6 +873,8 @@ type GetMDMAppleDefaultSetupAssistantFunc func(ctx context.Context, teamID *uint
type GetMatchingHostSerialsFunc func(ctx context.Context, serials []string) (map[string]*fleet.Host, error)
+type GetMatchingHostSerialsMarkedDeletedFunc func(ctx context.Context, serials []string) (map[string]struct{}, error)
+
type DeleteHostDEPAssignmentsFromAnotherABMFunc func(ctx context.Context, abmTokenID uint, serials []string) error
type DeleteHostDEPAssignmentsFunc func(ctx context.Context, abmTokenID uint, serials []string) error
@@ -973,6 +989,8 @@ type ResendHostMDMProfileFunc func(ctx context.Context, hostUUID string, profile
type GetHostMDMProfileInstallStatusFunc func(ctx context.Context, hostUUID string, profileUUID string) (fleet.MDMDeliveryStatus, error)
+type GetLinuxDiskEncryptionSummaryFunc func(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error)
+
type GetMDMCommandPlatformFunc func(ctx context.Context, commandUUID string) (string, error)
type ListMDMCommandsFunc func(ctx context.Context, tmFilter fleet.TeamFilter, listOpts *fleet.MDMCommandListOptions) ([]*fleet.MDMCommand, error)
@@ -2069,12 +2087,18 @@ type DataStore struct {
SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFunc
SetOrUpdateHostDisksSpaceFuncInvoked bool
+ GetConfigEnableDiskEncryptionFunc GetConfigEnableDiskEncryptionFunc
+ GetConfigEnableDiskEncryptionFuncInvoked bool
+
SetOrUpdateHostDisksEncryptionFunc SetOrUpdateHostDisksEncryptionFunc
SetOrUpdateHostDisksEncryptionFuncInvoked bool
SetOrUpdateHostDiskEncryptionKeyFunc SetOrUpdateHostDiskEncryptionKeyFunc
SetOrUpdateHostDiskEncryptionKeyFuncInvoked bool
+ SaveLUKSDataFunc SaveLUKSDataFunc
+ SaveLUKSDataFuncInvoked bool
+
GetUnverifiedDiskEncryptionKeysFunc GetUnverifiedDiskEncryptionKeysFunc
GetUnverifiedDiskEncryptionKeysFuncInvoked bool
@@ -2084,6 +2108,21 @@ type DataStore struct {
GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFunc
GetHostDiskEncryptionKeyFuncInvoked bool
+ IsHostPendingEscrowFunc IsHostPendingEscrowFunc
+ IsHostPendingEscrowFuncInvoked bool
+
+ ClearPendingEscrowFunc ClearPendingEscrowFunc
+ ClearPendingEscrowFuncInvoked bool
+
+ ReportEscrowErrorFunc ReportEscrowErrorFunc
+ ReportEscrowErrorFuncInvoked bool
+
+ QueueEscrowFunc QueueEscrowFunc
+ QueueEscrowFuncInvoked bool
+
+ AssertHasNoEncryptionKeyStoredFunc AssertHasNoEncryptionKeyStoredFunc
+ AssertHasNoEncryptionKeyStoredFuncInvoked bool
+
GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFunc
GetHostCertAssociationsToExpireFuncInvoked bool
@@ -2405,6 +2444,9 @@ type DataStore struct {
GetMatchingHostSerialsFunc GetMatchingHostSerialsFunc
GetMatchingHostSerialsFuncInvoked bool
+ GetMatchingHostSerialsMarkedDeletedFunc GetMatchingHostSerialsMarkedDeletedFunc
+ GetMatchingHostSerialsMarkedDeletedFuncInvoked bool
+
DeleteHostDEPAssignmentsFromAnotherABMFunc DeleteHostDEPAssignmentsFromAnotherABMFunc
DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked bool
@@ -2576,6 +2618,9 @@ type DataStore struct {
GetHostMDMProfileInstallStatusFunc GetHostMDMProfileInstallStatusFunc
GetHostMDMProfileInstallStatusFuncInvoked bool
+ GetLinuxDiskEncryptionSummaryFunc GetLinuxDiskEncryptionSummaryFunc
+ GetLinuxDiskEncryptionSummaryFuncInvoked bool
+
GetMDMCommandPlatformFunc GetMDMCommandPlatformFunc
GetMDMCommandPlatformFuncInvoked bool
@@ -4989,6 +5034,13 @@ func (s *DataStore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint,
return s.SetOrUpdateHostDisksSpaceFunc(ctx, hostID, gigsAvailable, percentAvailable, gigsTotal)
}
+func (s *DataStore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) {
+ s.mu.Lock()
+ s.GetConfigEnableDiskEncryptionFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetConfigEnableDiskEncryptionFunc(ctx, teamID)
+}
+
func (s *DataStore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error {
s.mu.Lock()
s.SetOrUpdateHostDisksEncryptionFuncInvoked = true
@@ -5003,6 +5055,13 @@ func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID
return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key, clientError, decryptable)
}
+func (s *DataStore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64Salt string, keySlot uint) error {
+ s.mu.Lock()
+ s.SaveLUKSDataFuncInvoked = true
+ s.mu.Unlock()
+ return s.SaveLUKSDataFunc(ctx, hostID, encryptedBase64Passphrase, encryptedBase64Salt, keySlot)
+}
+
func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
s.mu.Lock()
s.GetUnverifiedDiskEncryptionKeysFuncInvoked = true
@@ -5010,11 +5069,11 @@ func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]flee
return s.GetUnverifiedDiskEncryptionKeysFunc(ctx)
}
-func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error {
+func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error {
s.mu.Lock()
s.SetHostsDiskEncryptionKeyStatusFuncInvoked = true
s.mu.Unlock()
- return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, encryptable, threshold)
+ return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, decryptable, threshold)
}
func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
@@ -5024,6 +5083,41 @@ func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (
return s.GetHostDiskEncryptionKeyFunc(ctx, hostID)
}
+func (s *DataStore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool {
+ s.mu.Lock()
+ s.IsHostPendingEscrowFuncInvoked = true
+ s.mu.Unlock()
+ return s.IsHostPendingEscrowFunc(ctx, hostID)
+}
+
+func (s *DataStore) ClearPendingEscrow(ctx context.Context, hostID uint) error {
+ s.mu.Lock()
+ s.ClearPendingEscrowFuncInvoked = true
+ s.mu.Unlock()
+ return s.ClearPendingEscrowFunc(ctx, hostID)
+}
+
+func (s *DataStore) ReportEscrowError(ctx context.Context, hostID uint, err string) error {
+ s.mu.Lock()
+ s.ReportEscrowErrorFuncInvoked = true
+ s.mu.Unlock()
+ return s.ReportEscrowErrorFunc(ctx, hostID, err)
+}
+
+func (s *DataStore) QueueEscrow(ctx context.Context, hostID uint) error {
+ s.mu.Lock()
+ s.QueueEscrowFuncInvoked = true
+ s.mu.Unlock()
+ return s.QueueEscrowFunc(ctx, hostID)
+}
+
+func (s *DataStore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error {
+ s.mu.Lock()
+ s.AssertHasNoEncryptionKeyStoredFuncInvoked = true
+ s.mu.Unlock()
+ return s.AssertHasNoEncryptionKeyStoredFunc(ctx, hostID)
+}
+
func (s *DataStore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
s.mu.Lock()
s.GetHostCertAssociationsToExpireFuncInvoked = true
@@ -5773,6 +5867,13 @@ func (s *DataStore) GetMatchingHostSerials(ctx context.Context, serials []string
return s.GetMatchingHostSerialsFunc(ctx, serials)
}
+func (s *DataStore) GetMatchingHostSerialsMarkedDeleted(ctx context.Context, serials []string) (map[string]struct{}, error) {
+ s.mu.Lock()
+ s.GetMatchingHostSerialsMarkedDeletedFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetMatchingHostSerialsMarkedDeletedFunc(ctx, serials)
+}
+
func (s *DataStore) DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error {
s.mu.Lock()
s.DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked = true
@@ -6172,6 +6273,13 @@ func (s *DataStore) GetHostMDMProfileInstallStatus(ctx context.Context, hostUUID
return s.GetHostMDMProfileInstallStatusFunc(ctx, hostUUID, profileUUID)
}
+func (s *DataStore) GetLinuxDiskEncryptionSummary(ctx context.Context, teamID *uint) (fleet.MDMLinuxDiskEncryptionSummary, error) {
+ s.mu.Lock()
+ s.GetLinuxDiskEncryptionSummaryFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetLinuxDiskEncryptionSummaryFunc(ctx, teamID)
+}
+
func (s *DataStore) GetMDMCommandPlatform(ctx context.Context, commandUUID string) (string, error) {
s.mu.Lock()
s.GetMDMCommandPlatformFuncInvoked = true
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index 561850901271..030c2c650202 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -416,6 +416,9 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
// 1. To get the JSON value from the database
// 2. To update fields with the incoming values
if newAppConfig.MDM.EnableDiskEncryption.Valid {
+ if newAppConfig.MDM.EnableDiskEncryption.Value && svc.config.Server.PrivateKey == "" {
+ return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
+ }
appConfig.MDM.EnableDiskEncryption = newAppConfig.MDM.EnableDiskEncryption
} else if appConfig.MDM.EnableDiskEncryption.Set && !appConfig.MDM.EnableDiskEncryption.Valid {
appConfig.MDM.EnableDiskEncryption = oldAppConfig.MDM.EnableDiskEncryption
@@ -1130,15 +1133,6 @@ func (svc *Service) validateMDM(
return nil
}
}
-
- // if either macOS or Windows MDM is enabled, this setting can be set.
- if !mdm.AtLeastOnePlatformEnabledAndConfigured() {
- if mdm.EnableDiskEncryption.Valid && mdm.EnableDiskEncryption.Value && mdm.EnableDiskEncryption.Value != oldMdm.EnableDiskEncryption.Value {
- invalid.Append("mdm.enable_disk_encryption",
- `Couldn't edit enable_disk_encryption. Neither macOS MDM nor Windows is turned on. Visit https://fleetdm.com/docs/using-fleet to learn how to turn on MDM.`)
- }
- }
-
return nil
}
diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go
index 1e601997c45e..734ef43f1c18 100644
--- a/server/service/appconfig_test.go
+++ b/server/service/appconfig_test.go
@@ -1208,6 +1208,59 @@ func TestMDMAppleConfig(t *testing.T) {
}
}
+func TestDiskEncryptionSetting(t *testing.T) {
+ ds := new(mock.Store)
+
+ admin := &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}
+ t.Run("enableDiskEncryptionWithNoPrivateKey", func(t *testing.T) {
+ testConfig = config.TestConfig()
+ testConfig.Server.PrivateKey = ""
+ svc, ctx := newTestServiceWithConfig(t, ds, testConfig, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
+ ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
+
+ dsAppConfig := &fleet.AppConfig{
+ OrgInfo: fleet.OrgInfo{OrgName: "Test"},
+ ServerSettings: fleet.ServerSettings{ServerURL: "https://example.org"},
+ MDM: fleet.MDM{},
+ }
+
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return dsAppConfig, nil
+ }
+
+ ds.SaveAppConfigFunc = func(ctx context.Context, conf *fleet.AppConfig) error {
+ *dsAppConfig = *conf
+ return nil
+ }
+ ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
+ return nil, sql.ErrNoRows
+ }
+ ds.NewMDMAppleEnrollmentProfileFunc = func(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) {
+ return &fleet.MDMAppleEnrollmentProfile{}, nil
+ }
+ ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) {
+ raw := json.RawMessage("{}")
+ return &fleet.MDMAppleEnrollmentProfile{DEPProfile: &raw}, nil
+ }
+ ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
+ return job, nil
+ }
+
+ ac, err := svc.AppConfigObfuscated(ctx)
+ require.NoError(t, err)
+ require.Equal(t, dsAppConfig.MDM, ac.MDM)
+
+ raw, err := json.Marshal(fleet.MDM{
+ EnableDiskEncryption: optjson.SetBool(true),
+ })
+ require.NoError(t, err)
+ raw = []byte(`{"mdm":` + string(raw) + `}`)
+ _, err = svc.ModifyAppConfig(ctx, raw, fleet.ApplySpecOptions{})
+ require.Error(t, err)
+ require.ErrorContains(t, err, "Missing required private key")
+ })
+}
+
func TestModifyAppConfigSMTPSSOAgentOptions(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index aa6fcaf05a7a..da9938611d06 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -847,7 +847,7 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID
}
// cannot use the profile ID as it is now deleted
- if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
+ if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{profileUUID}, nil); err != nil {
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
}
@@ -1526,7 +1526,13 @@ func (svc *Service) needsOSUpdateForDEPEnrollment(ctx context.Context, m fleet.M
return false, nil
}
- return apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value)
+ needsUpdate, err := apple_mdm.IsLessThanVersion(m.OSVersion, settings.MinimumVersion.Value)
+ if err != nil {
+ level.Info(svc.logger).Log("msg", "checking os updates settings, cannot compare versions", "serial", m.Serial, "current_version", m.OSVersion, "minimum_version", settings.MinimumVersion.Value)
+ return false, nil
+ }
+
+ return needsUpdate, nil
}
func (svc *Service) getAppleSoftwareUpdateRequiredForDEPEnrollment(m fleet.MDMAppleMachineInfo) (*fleet.MDMAppleSoftwareUpdateRequired, error) {
@@ -2137,12 +2143,15 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable
return err
}
- var didUpdate, didUpdateMacOSDiskEncryption bool
+ var didUpdate bool
if enabled != nil {
if ac.MDM.EnableDiskEncryption.Value != *enabled {
+ if *enabled && svc.config.Server.PrivateKey == "" {
+ return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
+ }
+
ac.MDM.EnableDiskEncryption = optjson.SetBool(*enabled)
didUpdate = true
- didUpdateMacOSDiskEncryption = true
}
}
@@ -2150,7 +2159,7 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable
if err := svc.ds.SaveAppConfig(ctx, ac); err != nil {
return err
}
- if didUpdateMacOSDiskEncryption {
+ if ac.MDM.EnabledAndConfigured { // if macOS MDM is configured, set up FileVault escrow
var act fleet.ActivityDetails
if ac.MDM.EnableDiskEncryption.Value {
act = fleet.ActivityTypeEnabledMacosDiskEncryption{}
@@ -2760,20 +2769,21 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm.
return ctxerr.Wrap(r.Context, err, "cleaning SCEP refs")
}
+ var hasSetupExpItems bool
if m.AwaitingConfiguration {
// Enqueue setup experience items and mark the host as being in setup experience
- _, err := svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID)
+ hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID)
if err != nil {
return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks")
}
-
}
return svc.mdmLifecycle.Do(r.Context, mdmlifecycle.HostOptions{
- Action: mdmlifecycle.HostActionTurnOn,
- Platform: info.Platform,
- UUID: r.ID,
- EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey],
+ Action: mdmlifecycle.HostActionTurnOn,
+ Platform: info.Platform,
+ UUID: r.ID,
+ EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey],
+ HasSetupExperienceItems: hasSetupExpItems,
})
}
@@ -4452,6 +4462,35 @@ func (svc *Service) ListABMTokens(ctx context.Context) ([]*fleet.ABMToken, error
return nil, fleet.ErrMissingLicense
}
+// //////////////////////////////////////////////////////////////////////////////
+// Count ABM tokens endpoint
+// //////////////////////////////////////////////////////////////////////////////
+
+type countABMTokensResponse struct {
+ Err error `json:"error,omitempty"`
+ Count int `json:"count"`
+}
+
+func (r countABMTokensResponse) error() error { return r.Err }
+
+func countABMTokensEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (errorer, error) {
+ tokenCount, err := svc.CountABMTokens(ctx)
+ if err != nil {
+ return &countABMTokensResponse{Err: err}, nil
+ }
+
+ return &countABMTokensResponse{Count: tokenCount}, nil
+}
+
+func (svc *Service) CountABMTokens(ctx context.Context) (int, error) {
+ // Automatic enrollment (ABM/ADE/DEP) is a feature that requires a license.
+ // skipauth: No authorization check needed due to implementation returning
+ // only license error.
+ svc.authz.SkipAuthorization(ctx)
+
+ return 0, fleet.ErrMissingLicense
+}
+
////////////////////////////////////////////////////////////////////////////////
// Update ABM token teams endpoint
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index 60ae723f9a7a..9f07c02629d6 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -1939,7 +1939,7 @@ func TestUpdateMDMAppleSettings(t *testing.T) {
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
nil,
- ErrMissingLicense.Error(),
+ fleet.ErrMissingLicense.Error(),
},
{
"global admin premium",
@@ -1960,7 +1960,7 @@ func TestUpdateMDMAppleSettings(t *testing.T) {
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
false,
nil,
- ErrMissingLicense.Error(),
+ fleet.ErrMissingLicense.Error(),
},
{
"global maintainer premium",
@@ -2037,7 +2037,7 @@ func TestUpdateMDMAppleSettings(t *testing.T) {
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
false,
ptr.Uint(1),
- ErrMissingLicense.Error(),
+ fleet.ErrMissingLicense.Error(),
},
}
@@ -4093,7 +4093,7 @@ func TestCheckMDMAppleEnrollmentWithMinimumOSVersion(t *testing.T) {
SoftwareUpdateDeviceID: "J516sAP",
},
updateRequired: nil,
- err: "invalid current version",
+ err: "", // no error, allow enrollment to proceed without software update
},
}
diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go
index c41915b91a01..e849eb4b0fd1 100644
--- a/server/service/client_mdm.go
+++ b/server/service/client_mdm.go
@@ -40,11 +40,11 @@ func (c *Client) GetAppleBM() (*fleet.AppleBM, error) {
return responseBody.AppleBM, err
}
-func (c *Client) ListABMTokens() ([]*fleet.ABMToken, error) {
- verb, path := "GET", "/api/latest/fleet/abm_tokens"
- var responseBody listABMTokensResponse
+func (c *Client) CountABMTokens() (int, error) {
+ verb, path := "GET", "/api/latest/fleet/abm_tokens/count"
+ var responseBody countABMTokensResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "")
- return responseBody.Tokens, err
+ return responseBody.Count, err
}
// RequestAppleCSR requests a signed CSR from the Fleet server and returns the
diff --git a/server/service/devices.go b/server/service/devices.go
index 3ae57851b102..187e168bf48a 100644
--- a/server/service/devices.go
+++ b/server/service/devices.go
@@ -609,6 +609,43 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos
return fleet.ErrMissingLicense
}
+////////////////////////////////////////////////////////////////////////////////
+// Trigger linux key escrow
+////////////////////////////////////////////////////////////////////////////////
+
+type triggerLinuxDiskEncryptionEscrowRequest struct {
+ Token string `url:"token"`
+}
+
+func (r *triggerLinuxDiskEncryptionEscrowRequest) deviceAuthToken() string {
+ return r.Token
+}
+
+type triggerLinuxDiskEncryptionEscrowResponse struct {
+ Err error `json:"error,omitempty"`
+}
+
+func (r triggerLinuxDiskEncryptionEscrowResponse) error() error { return r.Err }
+
+func (r triggerLinuxDiskEncryptionEscrowResponse) Status() int { return http.StatusNoContent }
+
+func triggerLinuxDiskEncryptionEscrowEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
+ host, ok := hostctx.FromContext(ctx)
+ if !ok {
+ err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
+ return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil
+ }
+
+ if err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host); err != nil {
+ return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil
+ }
+ return triggerLinuxDiskEncryptionEscrowResponse{}, nil
+}
+
+func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error {
+ return fleet.ErrMissingLicense
+}
+
////////////////////////////////////////////////////////////////////////////////
// Get Current Device's Software
////////////////////////////////////////////////////////////////////////////////
diff --git a/server/service/devices_test.go b/server/service/devices_test.go
index 1100683be4fb..53d9644931ac 100644
--- a/server/service/devices_test.go
+++ b/server/service/devices_test.go
@@ -3,10 +3,12 @@ package service
import (
"context"
"database/sql"
+ "errors"
"fmt"
"testing"
"time"
+ "github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
@@ -475,3 +477,116 @@ func TestGetFleetDesktopSummary(t *testing.T) {
})
}
+
+func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) {
+ t.Run("unavailable in Fleet Free", func(t *testing.T) {
+ ds := new(mock.Store)
+ svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{SkipCreateTestUsers: true})
+ err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, &fleet.Host{ID: 1})
+ require.ErrorIs(t, err, fleet.ErrMissingLicense)
+ })
+
+ t.Run("no-op on already pending", func(t *testing.T) {
+ ds := new(mock.Store)
+ svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
+ ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool {
+ return true
+ }
+
+ err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, &fleet.Host{ID: 1})
+ require.NoError(t, err)
+ require.True(t, ds.IsHostPendingEscrowFuncInvoked)
+ })
+
+ t.Run("validation failures", func(t *testing.T) {
+ ds := new(mock.Store)
+ svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
+ ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool {
+ return false
+ }
+ var reportedErrors []string
+ host := &fleet.Host{ID: 1, Platform: "rhel", OSVersion: "Red Hat Enterprise Linux 9.0.0"}
+ ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error {
+ require.Equal(t, hostID, host.ID)
+ reportedErrors = append(reportedErrors, err)
+ return nil
+ }
+
+ // invalid platform
+ err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "Fleet does not yet support creating LUKS disk encryption keys on this platform.")
+ require.True(t, ds.IsHostPendingEscrowFuncInvoked)
+
+ // valid platform, no-team, encryption not enabled
+ host.OSVersion = "Fedora 32.0.0"
+ appConfig := &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(false)}}
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return appConfig, nil
+ }
+ err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "Disk encryption is not enabled for hosts not assigned to a team.")
+
+ // valid platform, team, encryption not enabled
+ host.TeamID = ptr.Uint(1)
+ teamConfig := &fleet.TeamMDM{}
+ ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) {
+ require.Equal(t, uint(1), teamID)
+ return teamConfig, nil
+ }
+ err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "Disk encryption is not enabled for this host's team.")
+
+ // valid platform, team, host disk is not encrypted or unknown encryption state
+ teamConfig = &fleet.TeamMDM{EnableDiskEncryption: true}
+ err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "Host's disk is not encrypted. Please encrypt your disk first.")
+ host.DiskEncryptionEnabled = ptr.Bool(false)
+ err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "Host's disk is not encrypted. Please encrypt your disk first.")
+
+ // No Fleet Desktop
+ host.DiskEncryptionEnabled = ptr.Bool(true)
+ orbitInfo := &fleet.HostOrbitInfo{Version: "1.35.1"}
+ ds.GetHostOrbitInfoFunc = func(ctx context.Context, id uint) (*fleet.HostOrbitInfo, error) {
+ return orbitInfo, nil
+ }
+ err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "Your version of fleetd does not support creating disk encryption keys on Linux. Please upgrade fleetd, then click Refetch, then try again.")
+
+ // Encryption key is already escrowed
+ orbitInfo.Version = fleet.MinOrbitLUKSVersion
+ ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error {
+ return errors.New("encryption key is already escrowed")
+ }
+ err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.ErrorContains(t, err, "encryption key is already escrowed")
+
+ require.Len(t, reportedErrors, 7)
+ })
+
+ t.Run("validation success", func(t *testing.T) {
+ ds := new(mock.Store)
+ svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true})
+ ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool {
+ return false
+ }
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
+ return &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}}, nil
+ }
+ ds.GetHostOrbitInfoFunc = func(ctx context.Context, id uint) (*fleet.HostOrbitInfo, error) {
+ return &fleet.HostOrbitInfo{Version: "1.36.0", DesktopVersion: ptr.String("42")}, nil
+ }
+ ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error {
+ return nil
+ }
+ host := &fleet.Host{ID: 1, Platform: "ubuntu", DiskEncryptionEnabled: ptr.Bool(true), OrbitVersion: ptr.String(fleet.MinOrbitLUKSVersion)}
+ ds.QueueEscrowFunc = func(ctx context.Context, hostID uint) error {
+ require.Equal(t, uint(1), hostID)
+ return nil
+ }
+
+ err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host)
+ require.NoError(t, err)
+ require.True(t, ds.QueueEscrowFuncInvoked)
+ })
+}
diff --git a/server/service/handler.go b/server/service/handler.go
index 4f916f576ba2..5228e901d8a2 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"net/http"
@@ -696,18 +697,18 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// Deprecated: GET /mdm/disk_encryption/summary is now deprecated, replaced by the
// GET /disk_encryption endpoint.
- mdmAnyMW.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
- mdmAnyMW.GET("/api/_version_/fleet/disk_encryption", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
+ ue.GET("/api/_version_/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
+ ue.GET("/api/_version_/fleet/disk_encryption", getMDMDiskEncryptionSummaryEndpoint, getMDMDiskEncryptionSummaryRequest{})
// Deprecated: GET /mdm/hosts/:id/encryption_key is now deprecated, replaced by
// GET /hosts/:id/encryption_key.
- mdmAnyMW.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
- mdmAnyMW.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
+ ue.GET("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
+ ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
// Deprecated: GET /mdm/profiles/summary is now deprecated, replaced by the
// GET /configuration_profiles/summary endpoint.
- mdmAnyMW.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
- mdmAnyMW.GET("/api/_version_/fleet/configuration_profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
+ ue.GET("/api/_version_/fleet/mdm/profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
+ ue.GET("/api/_version_/fleet/configuration_profiles/summary", getMDMProfilesSummaryEndpoint, getMDMProfilesSummaryRequest{})
// Deprecated: GET /mdm/profiles/:profile_uuid is now deprecated, replaced by
// GET /configuration_profiles/:profile_uuid.
@@ -734,7 +735,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// Deprecated: PATCH /mdm/apple/settings is deprecated, replaced by POST /disk_encryption.
// It was only used to set disk encryption.
mdmAnyMW.PATCH("/api/_version_/fleet/mdm/apple/settings", updateMDMAppleSettingsEndpoint, updateMDMAppleSettingsRequest{})
- mdmAnyMW.POST("/api/_version_/fleet/disk_encryption", updateMDMDiskEncryptionEndpoint, updateMDMDiskEncryptionRequest{})
+ ue.POST("/api/_version_/fleet/disk_encryption", updateDiskEncryptionEndpoint, updateDiskEncryptionRequest{})
// the following set of mdm endpoints must always be accessible (even
// if MDM is not configured) as it bootstraps the setup of MDM
@@ -749,6 +750,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.POST("/api/_version_/fleet/abm_tokens", uploadABMTokenEndpoint, uploadABMTokenRequest{})
ue.DELETE("/api/_version_/fleet/abm_tokens/{id:[0-9]+}", deleteABMTokenEndpoint, deleteABMTokenRequest{})
ue.GET("/api/_version_/fleet/abm_tokens", listABMTokensEndpoint, nil)
+ ue.GET("/api/_version_/fleet/abm_tokens/count", countABMTokensEndpoint, nil)
ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/teams", updateABMTokenTeamsEndpoint, updateABMTokenTeamsRequest{})
ue.PATCH("/api/_version_/fleet/abm_tokens/{id:[0-9]+}/renew", renewABMTokenEndpoint, renewABMTokenRequest{})
@@ -837,6 +839,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
errorLimiter.Limit("post_device_migrate_mdm", desktopQuota),
).POST("/api/_version_/fleet/device/{token}/migrate_mdm", migrateMDMDeviceEndpoint, deviceMigrateMDMRequest{})
+ de.WithCustomMiddleware(
+ errorLimiter.Limit("post_device_trigger_linux_escrow", desktopQuota),
+ ).POST("/api/_version_/fleet/device/{token}/mdm/linux/trigger_escrow", triggerLinuxDiskEncryptionEscrowEndpoint, triggerLinuxDiskEncryptionEscrowRequest{})
+
// host-authenticated endpoints
he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
@@ -879,6 +885,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
oeWindowsMDM := oe.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM())
oeWindowsMDM.POST("/api/fleet/orbit/disk_encryption_key", postOrbitDiskEncryptionKeyEndpoint, orbitPostDiskEncryptionKeyRequest{})
+ oe.POST("/api/fleet/orbit/luks_data", postOrbitLUKSEndpoint, orbitPostLUKSRequest{})
+
// unauthenticated endpoints - most of those are either login-related,
// invite-related or host-enrolling. So they typically do some kind of
// one-time authentication by verifying that a valid secret token is provided
@@ -1225,3 +1233,44 @@ func registerMDM(
mux.Handle(apple_mdm.MDMPath, mdmHandler)
return nil
}
+
+func WithMDMEnrollmentMiddleware(svc fleet.Service, logger kitlog.Logger, next http.Handler) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/mdm/sso" {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ // if x-apple-aspen-deviceinfo custom header is present, we need to check for minimum os version
+ di := r.Header.Get("x-apple-aspen-deviceinfo")
+ if di != "" {
+ parsed, err := apple_mdm.ParseDeviceinfo(di, false) // FIXME: use verify=true when we have better parsing for various Apple certs (https://github.com/fleetdm/fleet/issues/20879)
+ if err != nil {
+ // just log the error and continue to next
+ level.Error(logger).Log("msg", "parsing x-apple-aspen-deviceinfo", "err", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(r.Context(), parsed)
+ if err != nil {
+ // just log the error and continue to next
+ level.Error(logger).Log("msg", "checking minimum os version for mdm", "err", err)
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ if sur != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ if err := json.NewEncoder(w).Encode(sur); err != nil {
+ level.Error(logger).Log("msg", "failed to encode software update required", "err", err)
+ http.Redirect(w, r, r.URL.String()+"?error=true", http.StatusSeeOther)
+ }
+ return
+ }
+ }
+
+ next.ServeHTTP(w, r)
+ }
+}
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 33e32c438036..4af625c2029f 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -1242,6 +1242,20 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
}
host.MDM.Profiles = &profiles
+ if host.IsLUKSSupported() {
+ status, err := svc.LinuxHostDiskEncryptionStatus(ctx, *host)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "get host disk encryption status")
+ }
+ host.MDM.OSSettings = &fleet.HostMDMOSSettings{
+ DiskEncryption: status,
+ }
+
+ if status.Status != nil && *status.Status == fleet.DiskEncryptionVerified {
+ host.MDM.EncryptionKeyAvailable = true
+ }
+ }
+
var macOSSetup *fleet.HostMDMMacOSSetup
if ac.MDM.EnabledAndConfigured && license.IsPremium(ctx) {
macOSSetup, err = svc.ds.GetHostMDMMacOSSetup(ctx, host.ID)
@@ -2200,12 +2214,51 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host
return nil, err
}
- // The middleware checks that either Apple or Windows MDM are configured and
- // enabled, but here we must check if the specific one is enabled for that
- // particular host's platform.
+ var key *fleet.HostDiskEncryptionKey
+ if host.IsLUKSSupported() {
+ if svc.config.Server.PrivateKey == "" {
+ return nil, ctxerr.Wrap(ctx, errors.New("private key is unavailable"), "getting host encryption key")
+ }
+
+ key, err = svc.ds.GetHostDiskEncryptionKey(ctx, id)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
+ }
+ if key.Base64Encrypted == "" {
+ return nil, ctxerr.Wrap(ctx, newNotFoundError(), "host encryption key is not set")
+ }
+
+ decryptedKey, err := mdm.DecodeAndDecrypt(key.Base64Encrypted, svc.config.Server.PrivateKey)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key")
+ }
+ key.DecryptedValue = decryptedKey
+ } else {
+ key, err = svc.decryptForMDMPlatform(ctx, host)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ err = svc.NewActivity(
+ ctx,
+ authz.UserFromContext(ctx),
+ fleet.ActivityTypeReadHostDiskEncryptionKey{
+ HostID: host.ID,
+ HostDisplayName: host.DisplayName(),
+ },
+ )
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity")
+ }
+
+ return key, nil
+}
+
+func (svc *Service) decryptForMDMPlatform(ctx context.Context, host *fleet.Host) (*fleet.HostDiskEncryptionKey, error) {
+ // Here we must check if the appropriate MDM is enabled for that particular host's platform.
var decryptCert *tls.Certificate
- switch host.FleetPlatform() {
- case "windows":
+ if host.FleetPlatform() == "windows" {
if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil {
return nil, err
}
@@ -2216,8 +2269,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host
return nil, ctxerr.Wrap(ctx, err, "getting Microsoft WSTEP certificate to decrypt key")
}
decryptCert = cert
-
- default:
+ } else {
if err := svc.VerifyMDMAppleConfigured(ctx); err != nil {
return nil, err
}
@@ -2230,7 +2282,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host
decryptCert = cert
}
- key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id)
+ key, err := svc.ds.GetHostDiskEncryptionKey(ctx, host.ID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
}
@@ -2242,20 +2294,8 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key")
}
- key.DecryptedValue = string(decryptedKey)
-
- err = svc.NewActivity(
- ctx,
- authz.UserFromContext(ctx),
- fleet.ActivityTypeReadHostDiskEncryptionKey{
- HostID: host.ID,
- HostDisplayName: host.DisplayName(),
- },
- )
- if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity")
- }
+ key.DecryptedValue = string(decryptedKey)
return key, nil
}
diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go
index 5b0837cf60c2..035a55248672 100644
--- a/server/service/hosts_test.go
+++ b/server/service/hosts_test.go
@@ -19,6 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"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/mdm/apple/mobileconfig"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
@@ -399,6 +400,10 @@ func TestHostDetailsOSSettings(t *testing.T) {
return &fleet.HostLockWipeStatus{}, nil
}
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
+ return &fleet.HostDiskEncryptionKey{}, nil
+ }
+
type testCase struct {
name string
host *fleet.Host
@@ -1315,7 +1320,8 @@ func TestHostEncryptionKey(t *testing.T) {
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
- _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
+ _ sqlx.QueryerContext,
+ ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM},
@@ -1368,7 +1374,8 @@ func TestHostEncryptionKey(t *testing.T) {
return nil, keyErr
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
- _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
+ _ sqlx.QueryerContext,
+ ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM},
@@ -1429,7 +1436,8 @@ func TestHostEncryptionKey(t *testing.T) {
return nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
- _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
+ _ sqlx.QueryerContext,
+ ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: testCertPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: testKeyPEM},
@@ -1448,6 +1456,73 @@ func TestHostEncryptionKey(t *testing.T) {
})
}
})
+
+ t.Run("Linux encryption", func(t *testing.T) {
+ ds := new(mock.Store)
+ host := &fleet.Host{ID: 1, Platform: "ubuntu"}
+ symmetricKey := "this_is_a_32_byte_symmetric_key!"
+ passphrase := "this_is_a_passphrase"
+ base64EncryptedKey, err := mdm.EncryptAndEncode(passphrase, symmetricKey)
+ require.NoError(t, err)
+
+ ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
+ return host, nil
+ }
+
+ ds.NewActivityFunc = func(
+ ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
+ ) error {
+ return nil
+ }
+ ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { // needed for new activity
+ return &fleet.AppConfig{}, nil
+ }
+
+ // error when no server private key
+ fleetCfg.Server.PrivateKey = ""
+ svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
+ ctx = test.UserContext(ctx, test.UserAdmin)
+ key, err := svc.HostEncryptionKey(ctx, 1)
+ require.Error(t, err, "private key is unavailable")
+ require.Nil(t, key)
+
+ // error when key is not set
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ return &fleet.HostDiskEncryptionKey{}, nil
+ }
+ fleetCfg.Server.PrivateKey = symmetricKey
+ svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
+ ctx = test.UserContext(ctx, test.UserAdmin)
+ key, err = svc.HostEncryptionKey(ctx, 1)
+ require.Error(t, err, "host encryption key is not set")
+ require.Nil(t, key)
+
+ // error when key is not set
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ return &fleet.HostDiskEncryptionKey{
+ Base64Encrypted: "thisIsWrong",
+ Decryptable: ptr.Bool(true),
+ }, nil
+ }
+ svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
+ ctx = test.UserContext(ctx, test.UserAdmin)
+ key, err = svc.HostEncryptionKey(ctx, 1)
+ require.Error(t, err, "decrypt host encryption key")
+ require.Nil(t, key)
+
+ // happy path
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ return &fleet.HostDiskEncryptionKey{
+ Base64Encrypted: base64EncryptedKey,
+ Decryptable: ptr.Bool(true),
+ }, nil
+ }
+ svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
+ ctx = test.UserContext(ctx, test.UserAdmin)
+ key, err = svc.HostEncryptionKey(ctx, 1)
+ require.NoError(t, err)
+ require.Equal(t, passphrase, key.DecryptedValue)
+ })
}
func TestHostMDMProfileDetail(t *testing.T) {
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index 2e08b4ef9dc3..0dfab1f02f1c 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -6223,10 +6223,8 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Fleet MDM is not configured")
- // update MDM disk encryption, the endpoint returns an error if MDM is not enabled
- res = s.Do("POST", "/api/latest/fleet/disk_encryption", fleet.MDMAppleSettingsPayload{}, fleet.ErrMDMNotConfigured.StatusCode())
- errMsg = extractServerErrorText(res.Body)
- require.Contains(t, errMsg, fleet.ErrMDMNotConfigured.Error())
+ // update MDM disk encryption
+ _ = s.Do("POST", "/api/latest/fleet/disk_encryption", fleet.MDMAppleSettingsPayload{}, http.StatusPaymentRequired)
// device migrate mdm endpoint returns an error if not premium
createHostAndDeviceToken(t, s.ds, "some-token")
@@ -8640,6 +8638,11 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() {
require.Equal(t, hostLin.ID, getHostResp.Host.ID)
require.True(t, *getHostResp.Host.DiskEncryptionEnabled)
+ // should succeed as we no longer require MDM to access this endpoint, as Linux encryption doesn't require MDM
+ var profiles getMDMProfilesSummaryResponse
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles)
+ s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles)
+
// set unencrypted for all hosts
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostWin.ID, false))
require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostMac.ID, false))
@@ -8655,7 +8658,7 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() {
require.Equal(t, hostMac.ID, getHostResp.Host.ID)
require.False(t, *getHostResp.Host.DiskEncryptionEnabled)
- // Linux does not return false, it omits the field when false
+ // Linux may omit the field when false
getHostResp = getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostLin.ID), nil, http.StatusOK, &getHostResp)
require.Equal(t, hostLin.ID, getHostResp.Host.ID)
@@ -12292,3 +12295,108 @@ func (s *integrationTestSuite) TestHostWithNoPoliciesClearsPolicyCounts() {
require.Len(t, listHostsResp.Hosts, 1)
require.Equal(t, uint64(0), listHostsResp.Hosts[0].FailingPoliciesCount)
}
+
+func (s *integrationTestSuite) TestHostSoftwareWithTeamIdentifier() {
+ t := s.T()
+ ctx := context.Background()
+
+ host, err := s.ds.NewHost(ctx, &fleet.Host{
+ NodeKey: ptr.String(t.Name()),
+ OsqueryHostID: ptr.String(t.Name()),
+ UUID: t.Name(),
+ Hostname: t.Name() + "foo.local",
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+
+ safariApp := fleet.Software{
+ Name: "Safari.app",
+ BundleIdentifier: "com.apple.safari",
+ Version: "18.1",
+ Source: "apps",
+ }
+ googleChromeApp := fleet.Software{
+ Name: "Google Chrome.app",
+ BundleIdentifier: "com.google.Chrome",
+ Version: "130.0.6723.117",
+ Source: "apps",
+ }
+ ghCli := fleet.Software{
+ Name: "gh",
+ Source: "homebrew_packages",
+ }
+
+ // Update the host's software.
+ software := []fleet.Software{
+ safariApp, googleChromeApp, ghCli,
+ }
+ hostSoftware, err := s.ds.UpdateHostSoftware(context.Background(), host.ID, software)
+ require.NoError(t, err)
+ require.Len(t, hostSoftware.CurrInstalled(), 3)
+
+ // Update the host's software installed paths for the software above.
+ // Google Chrome.app will have two installed paths one with team identifier set
+ // the other one set to empty.
+ swPaths := map[string]struct{}{}
+ for _, s := range software {
+ pathItems := [][2]string{{fmt.Sprintf("/some/path/%s", s.Name), ""}}
+ if s.Name == "Google Chrome.app" {
+ pathItems = [][2]string{
+ {fmt.Sprintf("/some/path/%s", s.Name), "EQHXZ8M8AV"},
+ {fmt.Sprintf("/some/other/path/%s", s.Name), ""},
+ }
+ }
+ for _, pathItem := range pathItems {
+ path := pathItem[0]
+ teamIdentifier := pathItem[1]
+ key := fmt.Sprintf(
+ "%s%s%s%s%s",
+ path, fleet.SoftwareFieldSeparator, teamIdentifier, fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
+ )
+ swPaths[key] = struct{}{}
+ }
+ }
+ err = s.ds.UpdateHostSoftwareInstalledPaths(ctx, host.ID, swPaths, hostSoftware)
+ require.NoError(t, err)
+
+ hostsCountTs := time.Now().UTC()
+ err = s.ds.SyncHostsSoftware(context.Background(), hostsCountTs)
+ require.NoError(t, err)
+
+ getHostSoftwareResp := getHostSoftwareResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID),
+ nil, http.StatusOK, &getHostSoftwareResp,
+ "per_page", "5", "page", "0", "order_key", "name", "order_direction", "desc",
+ )
+ require.Len(t, getHostSoftwareResp.Software, 3)
+ require.Equal(t, "Safari.app", getHostSoftwareResp.Software[0].Name)
+ require.Len(t, getHostSoftwareResp.Software[0].InstalledVersions, 1)
+ require.Len(t, getHostSoftwareResp.Software[0].InstalledVersions[0].InstalledPaths, 1)
+ require.Equal(t, "/some/path/Safari.app", getHostSoftwareResp.Software[0].InstalledVersions[0].InstalledPaths[0])
+ require.Len(t, getHostSoftwareResp.Software[0].InstalledVersions[0].SignatureInformation, 1)
+ require.Equal(t, "/some/path/Safari.app", getHostSoftwareResp.Software[0].InstalledVersions[0].SignatureInformation[0].InstalledPath)
+ require.Empty(t, getHostSoftwareResp.Software[0].InstalledVersions[0].SignatureInformation[0].TeamIdentifier)
+
+ require.Equal(t, "Google Chrome.app", getHostSoftwareResp.Software[1].Name)
+ require.Len(t, getHostSoftwareResp.Software[1].InstalledVersions, 1)
+ require.Len(t, getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths, 2)
+ sort.Slice(getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths, func(i, j int) bool {
+ return getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[i] < getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[j]
+ })
+ require.Equal(t, "/some/other/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[0])
+ require.Equal(t, "/some/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].InstalledPaths[1])
+ require.Len(t, getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation, 2)
+ sort.Slice(getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation, func(i, j int) bool {
+ return getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[i].InstalledPath < getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[j].InstalledPath
+ })
+ require.Equal(t, "/some/other/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[0].InstalledPath)
+ require.Equal(t, "", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[0].TeamIdentifier)
+ require.Equal(t, "/some/path/Google Chrome.app", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[1].InstalledPath)
+ require.Equal(t, "EQHXZ8M8AV", getHostSoftwareResp.Software[1].InstalledVersions[0].SignatureInformation[1].TeamIdentifier)
+
+ require.Equal(t, "gh", getHostSoftwareResp.Software[2].Name)
+ require.Len(t, getHostSoftwareResp.Software[2].InstalledVersions, 1)
+ require.Len(t, getHostSoftwareResp.Software[2].InstalledVersions[0].InstalledPaths, 1)
+ require.Equal(t, "/some/path/gh", getHostSoftwareResp.Software[2].InstalledVersions[0].InstalledPaths[0])
+ require.Nil(t, getHostSoftwareResp.Software[2].InstalledVersions[0].SignatureInformation)
+}
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index 649d0b5912d0..93bac5241052 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -2881,6 +2881,168 @@ func (s *integrationEnterpriseTestSuite) TestAppleOSUpdatesTeamConfig() {
}, http.StatusUnprocessableEntity, &tmResp)
}
+func (s *integrationEnterpriseTestSuite) TestLinuxDiskEncryption() {
+ t := s.T()
+
+ // create a Linux host
+ noTeamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"),
+ OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"),
+ UUID: t.Name() + "3",
+ Hostname: t.Name() + "foo3.local",
+ PrimaryIP: "192.168.1.3",
+ PrimaryMac: "30-65-EC-6F-C4-60",
+ Platform: "ubuntu",
+ OSVersion: "Ubuntu 22.04",
+ })
+ require.NoError(t, err)
+ orbitKey := setOrbitEnrollment(t, noTeamHost, s.ds)
+ noTeamHost.OrbitNodeKey = &orbitKey
+
+ team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "A team"})
+ require.NoError(t, err)
+ teamID := ptr.Uint(team.ID)
+ teamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
+ OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
+ UUID: t.Name() + "2",
+ Hostname: t.Name() + "foo2.local",
+ PrimaryIP: "192.168.1.2",
+ PrimaryMac: "30-65-EC-6F-C4-59",
+ Platform: "rhel",
+ OSVersion: "Fedora 38.0", // this check is why HostLite now includes os_version in the data it's selecting
+ TeamID: teamID,
+ })
+ require.NoError(t, err)
+ teamOrbitKey := setOrbitEnrollment(t, teamHost, s.ds)
+ teamHost.OrbitNodeKey = &teamOrbitKey
+
+ // NO TEAM //
+
+ // config profiles endpoint should work but show all zeroes
+ var profileSummary getMDMProfilesSummaryResponse
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary)
+ require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
+
+ // set encrypted for host
+ require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), noTeamHost.ID, true))
+
+ // should still show zeroes
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary)
+ require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
+
+ // turn on disk encryption enforcement
+ s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent)
+
+ // should show the Linux host as pending
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary)
+ require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary)
+
+ // encryption summary should succeed (Linux encryption doesn't require MDM)
+ var summary getMDMDiskEncryptionSummaryResponse
+ s.DoJSON("GET", "/api/latest/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary)
+ s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary)
+ // disk is encrypted but key hasn't been escrowed yet
+ require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
+
+ // trigger escrow process from device
+ token := "much_valid"
+ mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
+ _, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, noTeamHost.ID, token)
+ return err
+ })
+ // should fail because default Orbit version is too old
+ res := s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusBadRequest)
+ res.Body.Close()
+
+ // should succeed now that Orbit version isn't too old
+ require.NoError(t, s.ds.SetOrUpdateHostOrbitInfo(context.Background(), noTeamHost.ID, fleet.MinOrbitLUKSVersion, sql.NullString{}, sql.NullBool{}))
+ res = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusNoContent)
+ res.Body.Close()
+
+ // confirm that Orbit endpoint shows notification flag
+ var orbitResponse orbitGetConfigResponse
+ s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &orbitResponse)
+ require.True(t, orbitResponse.Notifications.RunDiskEncryptionEscrow)
+
+ // confirm that second Orbit pull doesn't show notification flag
+ var secondOrbitResponse orbitGetConfigResponse
+ s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &secondOrbitResponse)
+ require.False(t, secondOrbitResponse.Notifications.RunDiskEncryptionEscrow)
+
+ // set an error first; the successful write should overwrite that
+ s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{
+ OrbitNodeKey: *noTeamHost.OrbitNodeKey,
+ ClientError: "Houston, we had a problem",
+ }, http.StatusNoContent)
+
+ // upload LUKS data
+ keySlot := ptr.Uint(1)
+ s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{
+ OrbitNodeKey: *noTeamHost.OrbitNodeKey,
+ Passphrase: "whale makes pail rise",
+ Salt: "the team i like lost",
+ KeySlot: keySlot,
+ }, http.StatusNoContent)
+
+ // confirm verified
+ s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary)
+ require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
+
+ // get passphrase back
+ var keyResponse getHostEncryptionKeyResponse
+ s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/mdm/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse)
+ s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse)
+ require.Equal(t, "whale makes pail rise", keyResponse.EncryptionKey.DecryptedValue)
+
+ // TEAM //
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary)
+ require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
+
+ // set encrypted for host
+ require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), teamHost.ID, true))
+
+ // should still show zeroes
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary)
+ require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary)
+
+ // turn on disk encryption enforcement for team
+ s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{TeamID: teamID, EnableDiskEncryption: true}, http.StatusNoContent)
+
+ // should show the Linux host as pending
+ s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary)
+ require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary)
+
+ // encryption summary should show host as action required
+ s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary)
+ require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
+
+ // upload LUKS data (no error, and no trigger, first this time)
+ keySlot = ptr.Uint(3)
+ s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{
+ OrbitNodeKey: *teamHost.OrbitNodeKey,
+ Passphrase: "the mome raths outgrabe",
+ Salt: "jabberwocky, but salty",
+ KeySlot: keySlot,
+ }, http.StatusNoContent)
+
+ // confirm verified
+ s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary)
+ require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary)
+
+ // get passphrase back
+ s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, teamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse)
+ require.Equal(t, "the mome raths outgrabe", keyResponse.EncryptionKey.DecryptedValue)
+}
+
func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() {
t := s.T()
ctx := context.Background()
diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go
index 50ec5112cc17..0cb740680d8b 100644
--- a/server/service/integration_mdm_dep_test.go
+++ b/server/service/integration_mdm_dep_test.go
@@ -1,7 +1,13 @@
package service
import (
+ "bytes"
"context"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -15,13 +21,16 @@ import (
"time"
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
+ "github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
+ apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
+ "github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/worker"
kitlog "github.com/go-kit/log"
@@ -31,6 +40,7 @@ import (
micromdm "github.com/micromdm/micromdm/mdm/mdm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "go.mozilla.org/pkcs7"
)
type profileAssignmentReq struct {
@@ -111,18 +121,33 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceGlobal() {
s.enableABM("fleet_ade_test")
+ // add a setup experience script to run for no team
+ extraArgs := make(map[string][]string)
+ body, headers := generateNewScriptMultipartRequest(t,
+ "script.sh", []byte(`echo "hello"`), s.token, extraArgs)
+ s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers)
+
// test manual and automatic release with the new setup experience flow
for _, enableReleaseManually := range []bool{false, true} {
- t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
+ t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false)
})
}
// test manual and automatic release with the old worker flow
for _, enableReleaseManually := range []bool{false, true} {
- t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
+ t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", true)
})
}
+
+ // remove the setup experience script, run the new setup experience flow when
+ // there is no setup experience item to process (so it is bypassed)
+ s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK)
+ for _, enableReleaseManually := range []bool{false, true} {
+ t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) {
+ s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false)
+ })
+ }
}
func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() {
@@ -207,18 +232,35 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() {
// enable FileVault
s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", json.RawMessage([]byte(fmt.Sprintf(`{"enable_disk_encryption":true,"team_id":%d}`, tm.ID))), http.StatusNoContent)
+ // add a setup experience script to run for this team
+ extraArgs := map[string][]string{
+ "team_id": {fmt.Sprintf("%d", tm.ID)},
+ }
+ body, headers := generateNewScriptMultipartRequest(t,
+ "script.sh", []byte(`echo "hello"`), s.token, extraArgs)
+ s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers)
+
// test manual and automatic release with the new setup experience flow
for _, enableReleaseManually := range []bool{false, true} {
- t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
+ t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false)
})
}
// test manual and automatic release with the old worker flow
for _, enableReleaseManually := range []bool{false, true} {
- t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
+ t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", true)
})
}
+
+ // remove the setup experience script, run the new setup experience flow when
+ // there is no setup experience item to process (so it is bypassed)
+ s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK, "team_id", fmt.Sprint(tm.ID))
+ for _, enableReleaseManually := range []bool{false, true} {
+ t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) {
+ s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false)
+ })
+ }
}
func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() {
@@ -288,6 +330,11 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() {
func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, device godep.Device, enableReleaseManually bool, teamID *uint, customProfileIdent string, useOldFleetdFlow bool) {
ctx := context.Background()
+ var isIphone bool
+ if device.DeviceFamily == "iPhone" {
+ isIphone = true
+ }
+
// set the enable release device manually option
payload := map[string]any{
"enable_release_device_manually": enableReleaseManually,
@@ -361,15 +408,22 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
// enroll the host
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
- var isIphone bool
- if device.DeviceFamily == "iPhone" {
+ if isIphone {
mdmDevice.Model = "iPhone 14,6"
- isIphone = true
}
mdmDevice.SerialNumber = device.SerialNumber
err := mdmDevice.Enroll()
require.NoError(t, err)
+ // check if it has setup experience items or not
+ hasSetupExpItems := true
+ _, err = s.ds.GetHostAwaitingConfiguration(ctx, mdmDevice.UUID)
+ if fleet.IsNotFound(err) {
+ hasSetupExpItems = false
+ } else if err != nil {
+ require.NoError(t, err)
+ }
+
// run the worker to process the DEP enroll request
s.runWorker()
// run the cron to assign configuration profiles
@@ -527,8 +581,13 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
b, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(b, &orbitConfigResp))
- // should be notified of the setup experience flow
- require.True(t, orbitConfigResp.Notifications.RunSetupExperience)
+ if hasSetupExpItems {
+ // should be notified of the setup experience flow
+ require.True(t, orbitConfigResp.Notifications.RunSetupExperience)
+ } else {
+ // should bypass the setup experience flow
+ require.False(t, orbitConfigResp.Notifications.RunSetupExperience)
+ }
if enableReleaseManually {
// get the worker's pending job from the future, there should not be any
@@ -539,7 +598,7 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
return
}
- if useOldFleetdFlow {
+ if useOldFleetdFlow || !hasSetupExpItems {
// there should be a Release Device pending job
pending, err := s.ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute))
require.NoError(t, err)
@@ -576,6 +635,12 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
require.NoError(t, err)
require.Len(t, pending, 0)
+ // mark the setup experience script as done
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx, `UPDATE setup_experience_status_results SET status = 'success' WHERE host_uuid = ?`, mdmDevice.UUID)
+ return err
+ })
+
// call the /status endpoint to automatically release the host
var statusResp getOrbitSetupExperienceStatusResponse
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
@@ -679,18 +744,19 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
}
type hostDEPRow struct {
- HostID uint `db:"host_id"`
- ProfileUUID string `db:"profile_uuid"`
- AssignProfileResponse string `db:"assign_profile_response"`
- ResponseUpdatedAt time.Time `db:"response_updated_at"`
- RetryJobID uint `db:"retry_job_id"`
+ HostID uint `db:"host_id"`
+ ProfileUUID string `db:"profile_uuid"`
+ AssignProfileResponse string `db:"assign_profile_response"`
+ ResponseUpdatedAt time.Time `db:"response_updated_at"`
+ RetryJobID uint `db:"retry_job_id"`
+ DeletedAt *time.Time `db:"deleted_at"`
}
checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow {
bySerial := make(map[string]hostDEPRow, len(deviceSerials))
for _, deviceSerial := range deviceSerials {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
var dest hostDEPRow
- err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial)
+ err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id, deleted_at FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial)
require.NoError(t, err)
require.Equal(t, string(expectedStatus), dest.AssignProfileResponse)
bySerial[deviceSerial] = dest
@@ -1046,14 +1112,22 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
deletedSerial = devices[1].SerialNumber
devices = []godep.Device{
{SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now()},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified",
- OpDate: time.Now().Add(time.Second)},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(2 * time.Second)},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added",
- OpDate: time.Now().Add(3 * time.Second)},
- {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(4 * time.Second)},
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified",
+ OpDate: time.Now().Add(time.Second),
+ },
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(2 * time.Second),
+ },
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(3 * time.Second),
+ },
+ {
+ SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(4 * time.Second),
+ },
{SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()},
{SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now().Add(time.Second)},
@@ -1436,7 +1510,8 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
RetryJobID uint `db:"retry_job_id"`
}
checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string,
- expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow {
+ expectedStatus fleet.DEPAssignProfileResponseStatus,
+ ) map[string]hostDEPRow {
bySerial := make(map[string]hostDEPRow, len(deviceSerials))
for _, deviceSerial := range deviceSerials {
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
@@ -1627,14 +1702,18 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
devices = []godep.Device{
{SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "added", OpDate: time.Now()},
{SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", OpDate: time.Now()},
- {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
defaultOrgDevices = []godep.Device{
{SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "added", OpDate: time.Now()},
{SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", OpDate: time.Now()},
- {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
// trigger a profile sync
@@ -1663,13 +1742,17 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
// Delete the devices
devices = []godep.Device{
{SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "modified", OpDate: time.Now()},
- {SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
defaultOrgDevices = []godep.Device{
{SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "modified", OpDate: time.Now()},
- {SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted",
- OpDate: time.Now().Add(time.Microsecond)},
+ {
+ SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(time.Microsecond),
+ },
}
// trigger a profile sync
@@ -1694,7 +1777,6 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1],
fleet.DEPAssignProfileResponseSuccess)
checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess)
-
}
func (s *integrationMDMTestSuite) TestDeprecatedDefaultAppleBMTeam() {
@@ -2342,3 +2424,660 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo
require.Equal(t, 1, deviceConfiguredCount)
require.Equal(t, 0, otherCount)
}
+
+func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM() {
+ t := s.T()
+ s.enableABM(t.Name())
+ ctx := context.Background()
+
+ checkPostEnrollmentCommands := func(mdmDevice *mdmtest.TestAppleMDMClient, shouldReceive bool) {
+ // run the worker to process the DEP enroll request
+ s.runWorker()
+ // run the worker to assign configuration profiles
+ s.awaitTriggerProfileSchedule(t)
+
+ var fleetdCmd, installProfileCmd *micromdm.CommandPayload
+ cmd, err := mdmDevice.Idle()
+ require.NoError(t, err)
+ for cmd != nil {
+ var fullCmd micromdm.CommandPayload
+ require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
+ if fullCmd.Command.RequestType == "InstallEnterpriseApplication" &&
+ fullCmd.Command.InstallEnterpriseApplication.ManifestURL != nil &&
+ strings.Contains(*fullCmd.Command.InstallEnterpriseApplication.ManifestURL, fleetdbase.GetPKGManifestURL()) {
+ fleetdCmd = &fullCmd
+ } else if cmd.Command.RequestType == "InstallProfile" {
+ installProfileCmd = &fullCmd
+ }
+ cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ }
+
+ if shouldReceive {
+ // received request to install fleetd
+ require.NotNil(t, fleetdCmd, "host didn't get a command to install fleetd")
+ require.NotNil(t, fleetdCmd.Command, "host didn't get a command to install fleetd")
+
+ // received request to install the global configuration profile
+ require.NotNil(t, installProfileCmd, "host didn't get a command to install profiles")
+ require.NotNil(t, installProfileCmd.Command, "host didn't get a command to install profiles")
+ } else {
+ require.Nil(t, fleetdCmd, "host got a command to install fleetd")
+ require.Nil(t, installProfileCmd, "host got a command to install profiles")
+ }
+ }
+
+ devices := []godep.Device{
+ {SerialNumber: uuid.New().String(), Model: "MacBook Pro", OS: "osx", OpType: "added"},
+ }
+
+ profileAssignmentReqs := []profileAssignmentReq{}
+
+ type hostDEPRow struct {
+ HostID uint `db:"host_id"`
+ ProfileUUID string `db:"profile_uuid"`
+ AssignProfileResponse string `db:"assign_profile_response"`
+ ResponseUpdatedAt time.Time `db:"response_updated_at"`
+ RetryJobID uint `db:"retry_job_id"`
+ DeletedAt *time.Time `db:"deleted_at"`
+ }
+ checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string, expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow {
+ bySerial := make(map[string]hostDEPRow, len(deviceSerials))
+ for _, deviceSerial := range deviceSerials {
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ var dest hostDEPRow
+ err := sqlx.GetContext(ctx, q, &dest, "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id, deleted_at FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)", expectedProfileUUID, deviceSerial)
+ require.NoError(t, err)
+ require.Equal(t, string(expectedStatus), dest.AssignProfileResponse)
+ bySerial[deviceSerial] = dest
+ return nil
+ })
+ }
+ return bySerial
+ }
+
+ s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ encoder := json.NewEncoder(w)
+ switch r.URL.Path {
+ case "/session":
+ err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
+ require.NoError(t, err)
+ case "/profile":
+ err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
+ require.NoError(t, err)
+ case "/server/devices":
+ // This endpoint is used to get an initial list of
+ // devices, return a single device
+ err := encoder.Encode(godep.DeviceResponse{Devices: devices[:1]})
+ require.NoError(t, err)
+ case "/devices/sync":
+ // This endpoint is polled over time to sync devices from
+ // ABM, send a repeated serial and a new one
+ err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"})
+ require.NoError(t, err)
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ profileAssignmentReqs = append(profileAssignmentReqs, prof)
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ resp.Devices = make(map[string]string, len(prof.Devices))
+ for _, device := range prof.Devices {
+ resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
+ }
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
+ default:
+ _, _ = w.Write([]byte(`{}`))
+ }
+ }))
+
+ s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) {
+ return map[string]*push.Response{}, nil
+ }
+
+ // Enroll the host via ADE
+ depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
+ mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
+ mdmDevice.SerialNumber = devices[0].SerialNumber
+ err := mdmDevice.Enroll()
+ require.NoError(t, err)
+
+ // Simulate an osquery enrollment too
+ // set an enroll secret
+ var applyResp applyEnrollSecretSpecResponse
+ s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
+ Spec: &fleet.EnrollSecretSpec{
+ Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
+ },
+ }, http.StatusOK, &applyResp)
+
+ // simulate a matching host enrolling via osquery
+ j, err := json.Marshal(&enrollAgentRequest{
+ EnrollSecret: t.Name(),
+ HostIdentifier: mdmDevice.UUID,
+ })
+ require.NoError(t, err)
+ var enrollResp enrollAgentResponse
+ hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK)
+ defer hres.Body.Close()
+ require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp))
+ require.NotEmpty(t, enrollResp.NodeKey)
+
+ listHostsRes := listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ require.Len(t, listHostsRes.Hosts, 1)
+ h := listHostsRes.Hosts[0]
+
+ s.runDEPSchedule()
+
+ // make sure the host gets post enrollment requests
+ checkPostEnrollmentCommands(mdmDevice, true)
+
+ var hostResp getHostResponse
+ s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp)
+ // 1 profile with fleetd configuration + 1 root CA config
+ require.Len(t, *hostResp.Host.MDM.Profiles, 2)
+
+ // Turn MDM off in the host
+ s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusOK)
+
+ // profiles are removed and the host is no longer enrolled
+ hostResp = getHostResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp)
+ require.Nil(t, hostResp.Host.MDM.Profiles)
+ require.Equal(t, "", hostResp.Host.MDM.Name)
+
+ err = mdmDevice.Checkout()
+ require.NoError(t, err)
+
+ // Simulate the device getting unassigned from Fleet in ABM
+ devices = []godep.Device{
+ {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()},
+ }
+
+ t.Log("RUN AFTER DELETED")
+ s.runDEPSchedule()
+
+ a := checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess)
+ require.NotZero(t, a[mdmDevice.SerialNumber].DeletedAt)
+
+ // Now we add the device back into ABM
+ profileAssignmentReqs = []profileAssignmentReq{}
+
+ devices = []godep.Device{
+ // In https://github.com/fleetdm/fleet/issues/23200, we saw a profileUUID being sent back on
+ // the godep.Device in the response from ABM. We're not 100% sure why, but the fact that
+ // this field is set was the source of the bug, which is why we're including it here.
+ {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now(), ProfileUUID: a[mdmDevice.SerialNumber].ProfileUUID},
+ }
+
+ t.Log("RUN AFTER RE-ADDED")
+ s.runDEPSchedule()
+
+ a = checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess)
+ require.Nil(t, a[mdmDevice.SerialNumber].DeletedAt)
+
+ err = mdmDevice.Enroll()
+ require.NoError(t, err)
+
+ // make sure the host gets post enrollment requests
+ checkPostEnrollmentCommands(mdmDevice, true)
+}
+
+func (s *integrationMDMTestSuite) TestEnforceMiniumOSVersion() {
+ t := s.T()
+ s.enableABM(t.Name())
+
+ latestMacOSVersion := "14.6.1" // this is the latest version in our test data (see ../mdm/apple/gdmf/testdata/gdmf.json)
+ latestMacOSBuild := "23G93" // this is the latest version in our test data (see ../mdm/apple/gdmf/testdata/gdmf.json)
+ deadline := "2023-12-31"
+ scepChallenge := "scepcha/> team_identifier
+ for _, codesignResult := range codesignResults {
+ codesignInformation[codesignResult["path"]] = codesignResult["team_identifier"]
+ }
+ if len(codesignInformation) == 0 {
+ return mainSoftwareResults
+ }
+
+ for _, result := range mainSoftwareResults {
+ codesignInfo := codesignInformation[result["installed_path"]]
+ if codesignInfo == "" {
+ // No codesign information for this application.
+ continue
+ }
+ result["team_identifier"] = codesignInfo
+ }
+
+ return mainSoftwareResults
+ },
+ },
}
var usersQuery = DetailQuery{
@@ -1546,7 +1564,18 @@ func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Ho
// NOTE: osquery is sometimes incorrectly returning the value "null" for some install paths.
// Thus, we explicitly ignore such value here.
strings.ToLower(installedPath) != "null" {
- key := fmt.Sprintf("%s%s%s", installedPath, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
+ truncateString := func(str string, length int) string {
+ runes := []rune(str)
+ if len(runes) > length {
+ return string(runes[:length])
+ }
+ return str
+ }
+ teamIdentifier := truncateString(row["team_identifier"], fleet.SoftwareTeamIdentifierMaxLength)
+ key := fmt.Sprintf(
+ "%s%s%s%s%s",
+ installedPath, fleet.SoftwareFieldSeparator, teamIdentifier, fleet.SoftwareFieldSeparator, s.ToUniqueStr(),
+ )
sPaths[key] = struct{}{}
}
}
diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go
index 08ee059ac1d8..b15590a574f2 100644
--- a/server/service/osquery_utils/queries_test.go
+++ b/server/service/osquery_utils/queries_test.go
@@ -307,7 +307,7 @@ func TestGetDetailQueries(t *testing.T) {
queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true})
qs = baseQueries
qs = append(qs, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions",
- "software_chrome", "scheduled_query_stats", "software_macos_firefox")
+ "software_chrome", "scheduled_query_stats", "software_macos_firefox", "software_macos_codesign")
require.Len(t, queriesWithUsersAndSoftware, len(qs))
sortedKeysCompare(t, queriesWithUsersAndSoftware, qs)
@@ -1338,7 +1338,7 @@ func TestDirectIngestSoftware(t *testing.T) {
require.True(t, ds.UpdateHostSoftwareFuncInvoked)
require.Len(t, calledWith, 1)
- require.Contains(t, strings.Join(maps.Keys(calledWith), " "), fmt.Sprintf("%s%s%s", data[1]["installed_path"], fleet.SoftwareFieldSeparator, data[1]["name"]))
+ require.Contains(t, strings.Join(maps.Keys(calledWith), " "), fmt.Sprintf("%s%s%s%s%s", data[1]["installed_path"], fleet.SoftwareFieldSeparator, "", fleet.SoftwareFieldSeparator, data[1]["name"]))
ds.UpdateHostSoftwareInstalledPathsFuncInvoked = false
})
diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go
index 99a0851bdc1d..3ef8acdcecc4 100644
--- a/server/service/testing_utils.go
+++ b/server/service/testing_utils.go
@@ -338,6 +338,7 @@ type TestServerOpts struct {
BootstrapPackageStore fleet.MDMBootstrapPackageStore
KeyValueStore fleet.KeyValueStore
EnableSCEPProxy bool
+ WithDEPWebview bool
}
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
@@ -413,6 +414,14 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ
}
}
+ if len(opts) > 0 && opts[0].WithDEPWebview {
+ frontendHandler := WithMDMEnrollmentMiddleware(svc, logger, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // do nothing and return 200
+ w.WriteHeader(http.StatusOK)
+ }))
+ rootMux.Handle("/", frontendHandler)
+ }
+
apiHandler := MakeHandler(svc, cfg, logger, limitStore, WithLoginRateLimit(throttled.PerMin(1000)))
rootMux.Handle("/api/", apiHandler)
var errHandler *errorstore.Handler
@@ -684,18 +693,13 @@ func mdmConfigurationRequiredEndpoints() []struct {
{"GET", "/api/latest/fleet/mdm/apple/profiles/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/apple/profiles/1", false, false},
{"GET", "/api/latest/fleet/mdm/apple/profiles/summary", false, false},
- {"GET", "/api/latest/fleet/mdm/profiles/summary", false, false},
- {"GET", "/api/latest/fleet/configuration_profiles/summary", false, false},
{"PATCH", "/api/latest/fleet/mdm/hosts/1/unenroll", false, false},
{"DELETE", "/api/latest/fleet/hosts/1/mdm", false, false},
- {"GET", "/api/latest/fleet/mdm/hosts/1/encryption_key", false, false},
- {"GET", "/api/latest/fleet/hosts/1/encryption_key", false, false},
{"GET", "/api/latest/fleet/mdm/hosts/1/profiles", false, true},
{"GET", "/api/latest/fleet/hosts/1/configuration_profiles", false, true},
{"POST", "/api/latest/fleet/mdm/hosts/1/lock", false, false},
{"POST", "/api/latest/fleet/mdm/hosts/1/wipe", false, false},
{"PATCH", "/api/latest/fleet/mdm/apple/settings", false, false},
- {"POST", "/api/latest/fleet/disk_encryption", false, false},
{"GET", "/api/latest/fleet/mdm/apple", false, false},
{"GET", "/api/latest/fleet/apns", false, false},
{"GET", apple_mdm.EnrollPath + "?token=test", false, false},
@@ -725,8 +729,6 @@ func mdmConfigurationRequiredEndpoints() []struct {
{"GET", "/api/latest/fleet/mdm/commands", false, false},
{"GET", "/api/latest/fleet/commands", false, false},
{"POST", "/api/fleet/orbit/disk_encryption_key", false, false},
- {"GET", "/api/latest/fleet/mdm/disk_encryption/summary", false, true},
- {"GET", "/api/latest/fleet/disk_encryption", false, true},
{"GET", "/api/latest/fleet/mdm/profiles/1", false, false},
{"GET", "/api/latest/fleet/configuration_profiles/1", false, false},
{"DELETE", "/api/latest/fleet/mdm/profiles/1", false, false},
diff --git a/server/test/new_objects.go b/server/test/new_objects.go
index 8a285783e5e8..f56496faea86 100644
--- a/server/test/new_objects.go
+++ b/server/test/new_objects.go
@@ -217,6 +217,12 @@ func WithPlatform(s string) NewHostOption {
}
}
+func WithOSVersion(s string) NewHostOption {
+ return func(h *fleet.Host) {
+ h.OSVersion = s
+ }
+}
+
func WithTeamID(teamID uint) NewHostOption {
return func(h *fleet.Host) {
h.TeamID = &teamID
diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go
index 01ac59ea79f9..235a3a7333d7 100644
--- a/server/worker/apple_mdm.go
+++ b/server/worker/apple_mdm.go
@@ -50,12 +50,13 @@ func (a *AppleMDM) Name() string {
// appleMDMArgs is the payload for the Apple MDM job.
type appleMDMArgs struct {
- Task AppleMDMTask `json:"task"`
- HostUUID string `json:"host_uuid"`
- TeamID *uint `json:"team_id,omitempty"`
- EnrollReference string `json:"enroll_reference,omitempty"`
- EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
- Platform string `json:"platform,omitempty"`
+ Task AppleMDMTask `json:"task"`
+ HostUUID string `json:"host_uuid"`
+ TeamID *uint `json:"team_id,omitempty"`
+ EnrollReference string `json:"enroll_reference,omitempty"`
+ EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
+ Platform string `json:"platform,omitempty"`
+ UseWorkerDeviceRelease bool `json:"use_worker_device_release,omitempty"`
}
// Run executes the apple_mdm job.
@@ -163,9 +164,10 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
}
}
- // proceed to release the device only if it is not a macos, as those are
- // released via the setup experience flow.
- if !isMacOS(args.Platform) {
+ // proceed to release the device if it is not a macos, as those are released
+ // via the setup experience flow, or if we were told to use the worker based
+ // release.
+ if !isMacOS(args.Platform) || args.UseWorkerDeviceRelease {
var manualRelease bool
if args.TeamID == nil {
ac, err := a.Datastore.AppConfig(ctx)
@@ -187,7 +189,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
// be final and same for MDM profiles of that host; it means the DEP
// enrollment process is done and the device can be released.
if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask,
- args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil {
+ args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, false, awaitCmdUUIDs...); err != nil {
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
}
}
@@ -198,10 +200,11 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
// This job is deprecated for macos because releasing devices is now done via
// the orbit endpoint /setup_experience/status that is polled by a swift dialog
-// UI window during the setup process, and automatically releases the device
-// once all pending setup tasks are done. However, it must remain implemented
-// for iOS and iPadOS and in case there are such jobs to process after a Fleet
-// migration to a new version.
+// UI window during the setup process (unless there are no setup experience
+// items, in which case this worker job is used), and automatically releases
+// the device once all pending setup tasks are done. However, it must remain
+// implemented for iOS and iPadOS and in case there are such jobs to process
+// after a Fleet migration to a new version.
func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error {
// Edge cases:
// - if the device goes offline for a long time, should we go ahead and
@@ -355,6 +358,7 @@ func QueueAppleMDMJob(
platform string,
teamID *uint,
enrollReference string,
+ useWorkerDeviceRelease bool,
enrollmentCommandUUIDs ...string,
) error {
attrs := []interface{}{
@@ -373,12 +377,13 @@ func QueueAppleMDMJob(
level.Info(logger).Log(attrs...)
args := &appleMDMArgs{
- Task: task,
- HostUUID: hostUUID,
- TeamID: teamID,
- EnrollReference: enrollReference,
- EnrollmentCommands: enrollmentCommandUUIDs,
- Platform: platform,
+ Task: task,
+ HostUUID: hostUUID,
+ TeamID: teamID,
+ EnrollReference: enrollReference,
+ EnrollmentCommands: enrollmentCommandUUIDs,
+ Platform: platform,
+ UseWorkerDeviceRelease: useWorkerDeviceRelease,
}
// the release device task is always added with a delay
diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go
index 8b497379aba0..f27aa32bf15a 100644
--- a/server/worker/apple_mdm_test.go
+++ b/server/worker/apple_mdm_test.go
@@ -141,7 +141,7 @@ func TestAppleMDM(t *testing.T) {
// create a host and enqueue the job
h := createEnrolledHost(t, 1, nil, true)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should mark the job as done
@@ -171,7 +171,7 @@ func TestAppleMDM(t *testing.T) {
// create a host and enqueue the job
h := createEnrolledHost(t, 1, nil, true)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should mark the job as failed
@@ -204,7 +204,7 @@ func TestAppleMDM(t *testing.T) {
w.Register(mdmWorker)
// use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support.
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -239,7 +239,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -281,7 +281,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -330,7 +330,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -380,7 +380,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -418,7 +418,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -461,7 +461,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID)
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID, false)
require.NoError(t, err)
// run the worker, should succeed
@@ -514,7 +514,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID)
+ err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID, false)
require.NoError(t, err)
// run the worker, should succeed
@@ -548,7 +548,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
- err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "")
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@@ -564,4 +564,40 @@ func TestAppleMDM(t *testing.T) {
require.Empty(t, jobs)
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
})
+
+ t.Run("use worker for automatic release", func(t *testing.T) {
+ mysql.SetTestABMAssets(t, ds, testOrgName)
+ defer mysql.TruncateTables(t, ds)
+
+ h := createEnrolledHost(t, 1, nil, true)
+
+ mdmWorker := &AppleMDM{
+ Datastore: ds,
+ Log: nopLog,
+ Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
+ }
+ w := NewWorker(ds, nopLog)
+ w.Register(mdmWorker)
+
+ err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true)
+ require.NoError(t, err)
+
+ // run the worker, should succeed
+ err = w.ProcessJobs(ctx)
+ require.NoError(t, err)
+
+ // ensure the job's not_before allows it to be returned if it were to run
+ // again
+ time.Sleep(time.Second)
+
+ require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
+
+ // the release device job got enqueued
+ jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().Add(time.Minute)) // release job is always added with a delay
+ require.NoError(t, err)
+ require.Len(t, jobs, 1)
+ require.Equal(t, fleet.JobStateQueued, jobs[0].State)
+ require.Equal(t, appleMDMJobName, jobs[0].Name)
+ require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
+ })
}
diff --git a/terraform/addons/ses/README.md b/terraform/addons/ses/README.md
index 7b549d31ccf7..bf74e252eabf 100644
--- a/terraform/addons/ses/README.md
+++ b/terraform/addons/ses/README.md
@@ -9,7 +9,7 @@ No requirements.
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | 4.60.0 |
+| [aws](#provider\_aws) | 4.60.1 |
## Modules
diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf
index 17947300da25..b1c9aec6bd79 100644
--- a/terraform/addons/vuln-processing/variables.tf
+++ b/terraform/addons/vuln-processing/variables.tf
@@ -24,7 +24,7 @@ variable "fleet_config" {
vuln_processing_cpu = optional(number, 2048)
vuln_data_stream_mem = optional(number, 1024)
vuln_data_stream_cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.59.0")
+ image = optional(string, "fleetdm/fleet:v4.60.1")
family = optional(string, "fleet-vuln-processing")
sidecars = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
@@ -82,7 +82,7 @@ variable "fleet_config" {
vuln_processing_cpu = 2048
vuln_data_stream_mem = 1024
vuln_data_stream_cpu = 512
- image = "fleetdm/fleet:v4.59.0"
+ image = "fleetdm/fleet:v4.60.1"
family = "fleet-vuln-processing"
sidecars = []
extra_environment_variables = {}
diff --git a/terraform/byo-vpc/byo-db/README.md b/terraform/byo-vpc/byo-db/README.md
index 60d144448934..3783008815e2 100644
--- a/terraform/byo-vpc/byo-db/README.md
+++ b/terraform/byo-vpc/byo-db/README.md
@@ -6,7 +6,7 @@ No requirements.
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | 4.60.0 |
+| [aws](#provider\_aws) | 4.60.1 |
## Modules
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
index 049841c1f735..ddebead667f6 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
@@ -16,7 +16,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.59.0")
+ image = optional(string, "fleetdm/fleet:v4.60.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -119,7 +119,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.59.0"
+ image = "fleetdm/fleet:v4.60.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf
index 20040d516caf..476f6f755837 100644
--- a/terraform/byo-vpc/byo-db/variables.tf
+++ b/terraform/byo-vpc/byo-db/variables.tf
@@ -77,7 +77,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.59.0")
+ image = optional(string, "fleetdm/fleet:v4.60.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -205,7 +205,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.59.0"
+ image = "fleetdm/fleet:v4.60.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf
index 8b0eefb3bea0..4acb7e7be87d 100644
--- a/terraform/byo-vpc/example/main.tf
+++ b/terraform/byo-vpc/example/main.tf
@@ -17,7 +17,7 @@ provider "aws" {
}
locals {
- fleet_image = "fleetdm/fleet:v4.59.0"
+ fleet_image = "fleetdm/fleet:v4.60.1"
domain_name = "example.com"
}
diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf
index 593d3a390ffc..e1684a63a425 100644
--- a/terraform/byo-vpc/variables.tf
+++ b/terraform/byo-vpc/variables.tf
@@ -170,7 +170,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.59.0")
+ image = optional(string, "fleetdm/fleet:v4.60.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -298,7 +298,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.59.0"
+ image = "fleetdm/fleet:v4.60.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/terraform/example/main.tf b/terraform/example/main.tf
index 8b92f669bece..b0922c2906d3 100644
--- a/terraform/example/main.tf
+++ b/terraform/example/main.tf
@@ -63,8 +63,8 @@ module "fleet" {
fleet_config = {
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
- # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.59.0"
- image = "fleetdm/fleet:v4.59.0" # override default to deploy the image you desire
+ # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.60.1"
+ image = "fleetdm/fleet:v4.60.1" # override default to deploy the image you desire
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
# memory and cpu.
autoscaling = {
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 34c6d7a1f58c..892ba29be866 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -218,7 +218,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.59.0")
+ image = optional(string, "fleetdm/fleet:v4.60.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -346,7 +346,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.59.0"
+ image = "fleetdm/fleet:v4.60.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/tools/dialog/main.go b/tools/dialog/main.go
new file mode 100644
index 000000000000..23e46da66c30
--- /dev/null
+++ b/tools/dialog/main.go
@@ -0,0 +1,55 @@
+package main
+
+// This is a tool to test the zenity package on Linux
+// It will show an entry dialog, a progress dialog, and an info dialog
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/zenity"
+)
+
+func main() {
+ prompt := zenity.New()
+ ctx := context.Background()
+
+ output, err := prompt.ShowEntry(ctx, dialog.EntryOptions{
+ Title: "Zenity Test Entry Title",
+ Text: "Zenity Test Entry Text",
+ HideText: true,
+ TimeOut: 10 * time.Second,
+ })
+ if err != nil {
+ fmt.Println("Err ShowEntry")
+ panic(err)
+ }
+
+ ctx, cancelProgress := context.WithCancel(context.Background())
+
+ go func() {
+ err := prompt.ShowProgress(ctx, dialog.ProgressOptions{
+ Title: "Zenity Test Progress Title",
+ Text: "Zenity Test Progress Text",
+ })
+ if err != nil {
+ fmt.Println("Err ShowProgress")
+ panic(err)
+ }
+ }()
+
+ time.Sleep(2 * time.Second)
+ cancelProgress()
+
+ err = prompt.ShowInfo(ctx, dialog.InfoOptions{
+ Title: "Zenity Test Info Title",
+ Text: "Result: " + string(output),
+ TimeOut: 10 * time.Second,
+ })
+ if err != nil {
+ fmt.Println("Err ShowInfo")
+ panic(err)
+ }
+}
diff --git a/tools/fleetctl-docker/Dockerfile b/tools/fleetctl-docker/Dockerfile
index 6b82ed628541..ca678cedf7fa 100644
--- a/tools/fleetctl-docker/Dockerfile
+++ b/tools/fleetctl-docker/Dockerfile
@@ -2,7 +2,7 @@ FROM rust:latest@sha256:56418f03475cf7b107f87d7fabe99ce9a4a9f9904daafa99be7c50d9
ARG transporter_url=https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/resources/download/public/Transporter__Linux/bin
-RUN cargo install --version 0.16.0 apple-codesign \
+RUN cargo install --locked --version 0.16.0 apple-codesign \
&& curl -sSf $transporter_url -o transporter_install.sh \
&& sh transporter_install.sh --target transporter --accept --noexec
diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json
index 4832a3f837ca..9ca552068b9d 100644
--- a/tools/fleetctl-npm/package.json
+++ b/tools/fleetctl-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "fleetctl",
- "version": "v4.59.0",
+ "version": "v4.60.1",
"description": "Installer for the fleetctl CLI tool",
"bin": {
"fleetctl": "./run.js"
diff --git a/tools/luks/luks/main.go b/tools/luks/luks/main.go
new file mode 100644
index 000000000000..f20c28f4e289
--- /dev/null
+++ b/tools/luks/luks/main.go
@@ -0,0 +1,72 @@
+//go:build linux
+
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/lvm"
+ "github.com/fleetdm/fleet/v4/orbit/pkg/zenity"
+ "github.com/siderolabs/go-blockdevice/v2/encryption"
+ "github.com/siderolabs/go-blockdevice/v2/encryption/luks"
+)
+
+func main() {
+ devicePath, err := lvm.FindRootDisk()
+ if err != nil {
+ fmt.Println("devicepath err:", err)
+ panic(err)
+ }
+
+ prompt := zenity.New()
+
+ // Prompt existing passphrase from the user.
+ currentPassphrase, err := prompt.ShowEntry(context.Background(), dialog.EntryOptions{
+ Title: "Enter Existing LUKS Passphrase",
+ Text: "Enter your existing LUKS passphrase:",
+ HideText: true,
+ })
+ if err != nil {
+ fmt.Println("Err ShowEntry")
+ panic(err)
+ }
+
+ const escrowPassPhrase = "fleet123"
+
+ device := luks.New(luks.AESXTSPlain64Cipher)
+
+ keySlot := 1
+ for {
+ if keySlot == 8 {
+ panic(errors.New("all LUKS key slots are full"))
+ }
+
+ userKey := encryption.NewKey(0, currentPassphrase)
+ escrowKey := encryption.NewKey(keySlot, []byte(escrowPassPhrase))
+
+ if err := device.AddKey(context.Background(), devicePath, userKey, escrowKey); err != nil {
+ if errors.Is(err, encryption.ErrEncryptionKeyRejected) {
+ currentPassphrase, err = prompt.ShowEntry(context.Background(), dialog.EntryOptions{
+ Title: "Enter Existing LUKS Passphrase",
+ Text: "Bad password. Enter your existing LUKS passphrase:",
+ HideText: true,
+ })
+ if err != nil {
+ fmt.Println("Err Retry ShowEntry")
+ panic(err)
+ }
+ continue
+ }
+
+ keySlot++
+ continue
+ }
+
+ break
+ }
+
+ fmt.Println("Key escrowed successfully.")
+}
diff --git a/tools/luks/lvm/main.go b/tools/luks/lvm/main.go
new file mode 100644
index 000000000000..de6cbe143195
--- /dev/null
+++ b/tools/luks/lvm/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/lvm"
+)
+
+func main() {
+ disk, err := lvm.FindRootDisk()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println("Root Partition:", disk)
+}
diff --git a/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh b/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh
index bc11878a9377..dbafc8bc01cc 100755
--- a/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh
+++ b/tools/mdm/apple/macos-vm-auto-enroll/macos-vm-auto-enroll.sh
@@ -65,6 +65,10 @@ echo "Deleting old fleet package"
echo "Creating fleet package..."
./build/fleetctl package --type=pkg --enable-scripts --fleet-desktop --disable-open-folder --fleet-url="$FLEET_URL" --enroll-secret="$FLEET_ENROLL_SECRET"
+if [ ! -f fleet-osquery.pkg ]; then
+ echo "package not generated"
+ exit 1
+fi
if tart list | grep $vm_name >/dev/null 2>&1; then
echo 'Enrollment test VM exists, deleting...'
diff --git a/tools/telemetry/README.md b/tools/telemetry/README.md
index fda2e3790dde..940c8f267c70 100644
--- a/tools/telemetry/README.md
+++ b/tools/telemetry/README.md
@@ -17,6 +17,6 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" \
--logging_tracing_enabled=true \
--logging_tracing_type=opentelemetry \
--dev --logging_debug
-```
+```
-Afterward, you can navigate to http://localhost:16686/ to access the Jaeger UI.
+Afterwards, you can navigate to http://localhost:16686/ to access the Jaeger UI.