+ Use only versions available from Apple.{" "}
+
+ >
+ }
value={minOsVersion}
error={minOsVersionError}
onChange={handleMinVersionChange}
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index 6464f4d59b..7c95b74cfc 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -1430,16 +1430,11 @@ const ManageHostsPage = ({
? selectedLabel
: undefined;
- const statusDropdownClassnames = classNames(
- `${baseClass}__status_dropdown`,
- { [`${baseClass}__status-dropdown-sandbox`]: isSandboxMode }
- );
-
return (
>;
+ }
+
return (
<>
[Zach Wasserman](https://www.linkedin.com/in/zacharywasserman/) _([@zwass](https://github.com/zwass))_
[Allen Houchins](https://www.linkedin.com/in/allenhouchins/) _([@allenhouchins](https://github.com/allenhouchins))_
[Harrison Ravazzolo](https://www.linkedin.com/in/harrison-ravazzolo/) _([@harrisonravazzolo](https://github.com/harrisonravazzolo))_
| Channel Sales | [Tom Ostertag](https://www.linkedin.com/in/tom-ostertag-77212791/) _([@tomostertag](https://github.com/TomOstertag))_
-| Sr. Account Executive | [Kendra McKeever](https://www.linkedin.com/in/kendramckeever/) _([@KendraAtFleet](https://github.com/KendraAtFleet))_
-| Account Executive (AE) | [Patricia Ambrus](https://www.linkedin.com/in/pambrus/) _([@ambrusps](https://github.com/ambrusps))_
[Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@anthonysnyder8](https://github.com/AnthonySnyder8))_
[Paul Tardif](https://www.linkedin.com/in/paul-t-750833/) _([@phtardif1](https://github.com/phtardif1))_
+| Account Executive (AE) | [Patricia Ambrus](https://www.linkedin.com/in/pambrus/) _([@ambrusps](https://github.com/ambrusps))_
[Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@anthonysnyder8](https://github.com/AnthonySnyder8))_
[Paul Tardif](https://www.linkedin.com/in/paul-t-750833/) _([@phtardif1](https://github.com/phtardif1))_
[Kendra McKeever](https://www.linkedin.com/in/kendramckeever/) _([@KendraAtFleet](https://github.com/KendraAtFleet))_
## Contact us
diff --git a/it-and-security/default.yml b/it-and-security/default.yml
index 52baadb564..9b60ffb92c 100644
--- a/it-and-security/default.yml
+++ b/it-and-security/default.yml
@@ -1,27 +1,5 @@
agent_options:
path: ./lib/agent-options.yml
-controls:
- enable_disk_encryption: true
- macos_migration:
- enable: true
- mode: voluntary
- webhook_url: $DOGFOOD_MACOS_MIGRATION_WEBHOOK_URL
- macos_settings:
- custom_settings: null
- macos_setup:
- bootstrap_package: ""
- enable_end_user_authentication: false
- macos_setup_assistant: null
- macos_updates:
- deadline: "2023-06-13"
- minimum_version: 13.4.1
- windows_enabled_and_configured: true
- windows_settings:
- custom_settings: []
- windows_updates:
- deadline_days: 3
- grace_period_days: 2
- scripts: []
org_settings:
features:
enable_host_users: true
@@ -90,4 +68,3 @@ org_settings:
policies:
queries:
- path: ./lib/collect-fleetd-update-channels.queries.yml
-software:
diff --git a/it-and-security/teams/no-team.yml b/it-and-security/teams/no-team.yml
new file mode 100644
index 0000000000..ef6baf9e40
--- /dev/null
+++ b/it-and-security/teams/no-team.yml
@@ -0,0 +1,25 @@
+name: No team
+policies:
+controls:
+ enable_disk_encryption: true
+ macos_migration:
+ enable: true
+ mode: voluntary
+ webhook_url: $DOGFOOD_MACOS_MIGRATION_WEBHOOK_URL
+ macos_settings:
+ custom_settings: null
+ macos_setup:
+ bootstrap_package: ""
+ enable_end_user_authentication: false
+ macos_setup_assistant: null
+ macos_updates:
+ deadline: "2023-06-13"
+ minimum_version: 13.4.1
+ windows_enabled_and_configured: true
+ windows_settings:
+ custom_settings: []
+ windows_updates:
+ deadline_days: 3
+ grace_period_days: 2
+ scripts: []
+software:
diff --git a/orbit/pkg/update/escrow_buddy.go b/orbit/pkg/update/escrow_buddy.go
index e1f6fdf116..b226da9a24 100644
--- a/orbit/pkg/update/escrow_buddy.go
+++ b/orbit/pkg/update/escrow_buddy.go
@@ -5,8 +5,9 @@ import (
"sync"
"time"
- "github.com/fleetdm/fleet/v4/server/fleet"
"github.com/rs/zerolog/log"
+
+ "github.com/fleetdm/fleet/v4/server/fleet"
)
// EscrowBuddyRunner sets up [Escrow Buddy][1] to rotate FileVault keys on
@@ -86,6 +87,13 @@ func (e *EscrowBuddyRunner) Run(cfg *fleet.OrbitConfig) error {
}
}
+ // Some macOS updates and upgrades reset the authorization database to its default state
+ // which will deactivate Escrow Buddy and prevent FileVault key generation upon next login.
+ log.Debug().Msg("EscrowBuddyRunner: re-enable Escrow Buddy in the authorization database")
+ if err := e.setAuthDBSetup(); err != nil {
+ return fmt.Errorf("failed to re-enable Escrow Buddy in the authorization database, err: %w", err)
+ }
+
log.Debug().Msg("EscrowBuddyRunner: enabling disk encryption rotation")
if err := e.setGenerateNewKeyTo(true); err != nil {
return fmt.Errorf("enabling disk encryption rotation: %w", err)
@@ -118,3 +126,13 @@ func (e *EscrowBuddyRunner) setGenerateNewKeyTo(enabled bool) error {
}
return fn("sh", "-c", cmd)
}
+
+func (e *EscrowBuddyRunner) setAuthDBSetup() error {
+ log.Debug().Msg("ready to re-enable Escrow Buddy in the authorization database")
+ cmd := "/Library/Security/SecurityAgentPlugins/Escrow\\ Buddy.bundle/Contents/Resources/AuthDBSetup.sh"
+ fn := e.runCmdFunc
+ if fn == nil {
+ fn = runCmdCollectErr
+ }
+ return fn("sh", "-c", cmd)
+}
diff --git a/orbit/pkg/update/escrow_buddy_test.go b/orbit/pkg/update/escrow_buddy_test.go
index 0ed61883b0..ccd3093834 100644
--- a/orbit/pkg/update/escrow_buddy_test.go
+++ b/orbit/pkg/update/escrow_buddy_test.go
@@ -65,9 +65,11 @@ func (s *escrowBuddyTestSuite) TestEscrowBuddyRotatesKey() {
err = r.Run(cfg)
require.NoError(t, err)
- require.Len(t, cmdCalls, 1)
+ require.Len(t, cmdCalls, 2)
require.Equal(t, cmdCalls[0]["cmd"], "sh")
- require.Equal(t, cmdCalls[0]["args"], []string{"-c", "defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool true"})
+ require.Equal(t, cmdCalls[0]["args"], []string{"-c", "/Library/Security/SecurityAgentPlugins/Escrow\\ Buddy.bundle/Contents/Resources/AuthDBSetup.sh"})
+ require.Equal(t, cmdCalls[1]["cmd"], "sh")
+ require.Equal(t, cmdCalls[1]["args"], []string{"-c", "defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool true"})
targets = runner.updater.opt.Targets
require.Len(t, targets, 1)
@@ -77,10 +79,12 @@ func (s *escrowBuddyTestSuite) TestEscrowBuddyRotatesKey() {
time.Sleep(3 * time.Millisecond)
cfg.Notifications.RotateDiskEncryptionKey = false
+ cmdCalls = []map[string]any{}
err = r.Run(cfg)
require.NoError(t, err)
- require.Len(t, cmdCalls, 2)
- require.Equal(t, cmdCalls[1]["cmd"], "sh")
- require.Equal(t, cmdCalls[1]["args"], []string{"-c", "defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool false"})
+ // only one call to set the GenerateNewKey to false
+ require.Len(t, cmdCalls, 1)
+ require.Equal(t, cmdCalls[0]["cmd"], "sh")
+ require.Equal(t, cmdCalls[0]["args"], []string{"-c", "defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool false"})
}
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index b4ae8c5ac0..610a25db52 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -2535,377 +2535,150 @@ func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, prof
return err
}
-const (
- appleMDMFailedProfilesStmt = `
- h.uuid = hmap.host_uuid AND
- hmap.status = :failed`
-
- appleMDMPendingProfilesStmt = `
- h.uuid = hmap.host_uuid AND
- (
- hmap.status IS NULL OR
- hmap.status = :pending OR
+// sqlCaseMDMAppleStatus returns a SQL snippet that can be used to determine the status of a host
+// based on the status of its profiles and declarations and filevault status. It should be used in
+// conjunction with sqlJoinMDMAppleProfilesStatus and sqlJoinMDMAppleDeclarationsStatus. It assumes the
+// hosts table to be aliased as 'h' and the host_disk_encryption_keys table to be aliased as 'hdek'.
+func sqlCaseMDMAppleStatus() string {
+ // NOTE: To make this snippet reusable, we're not using sqlx.Named here because it would
+ // complicate usage in other queries (e.g., list hosts).
+ var (
+ failed = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryFailed))
+ pending = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryPending))
+ verifying = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryVerifying))
+ verified = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryVerified))
+ )
+ return `
+ CASE WHEN (prof_failed
+ OR decl_failed
+ OR fv_failed) THEN
+ ` + failed + `
+ WHEN (prof_pending
+ OR decl_pending
-- special case for filevault, it's pending if the profile is
-- pending OR the profile is verified or verifying but we still
-- don't have an encryption key.
- (
- hmap.profile_identifier = :filevault AND
- hmap.status IN (:verifying, :verified) AND
- hmap.operation_type = :install AND
- NOT EXISTS (
- SELECT 1
- FROM host_disk_encryption_keys hdek
- WHERE h.id = hdek.host_id AND
- (hdek.decryptable = 1 OR hdek.decryptable IS NULL)
- )
- )
- )`
-
- appleMDMVerifyingProfilesStmt = `
- h.uuid = hmap.host_uuid AND
- hmap.operation_type = :install AND
- (
- -- all profiles except filevault that are 'verifying'
- (
- hmap.profile_identifier != :filevault AND
- hmap.status = :verifying
- )
- OR
- -- special cases for filevault
- (
- hmap.profile_identifier = :filevault AND
- (
- -- filevault profile is verified, but we didn't verify the encryption key
- (
- hmap.status = :verified AND
- EXISTS (
- SELECT 1
- FROM host_disk_encryption_keys AS hdek
- WHERE h.id = hdek.host_id AND
- hdek.decryptable IS NULL
- )
- )
- OR
- -- filevault profile is verifying, and we already have an encryption key, in any state
- (
- hmap.status = :verifying AND
- EXISTS (
- SELECT 1
- FROM host_disk_encryption_keys AS hdek
- WHERE h.id = hdek.host_id AND
- hdek.decryptable = 1 OR hdek.decryptable IS NULL
- )
- )
- )
- )
- )`
-
- appleVerifiedProfilesStmt = `
- h.uuid = hmap.host_uuid AND
- hmap.operation_type = :install AND
- hmap.status = :verified AND
- (
- hmap.profile_identifier != :filevault OR
- EXISTS (
- SELECT 1
- FROM host_disk_encryption_keys hdek
- WHERE h.id = hdek.host_id AND
- hdek.decryptable = 1
- )
- )`
-)
-
-// subqueryAppleProfileStatus builds the right subquery that can be used to
-// filter hosts based on their profile status.
-//
-// The subquery mechanism works by finding profiles for hosts that:
-// - match with the provided status
-// - match any status that supercedes the provided status (eg: failed supercedes verifying)
-//
-// Hosts will be considered to be in the given status only if the profiles
-// match the given status and zero profiles match any superceding status.
-func subqueryAppleProfileStatus(status fleet.MDMDeliveryStatus) (string, []any, error) {
- var condition string
- var excludeConditions string
- switch status {
- case fleet.MDMDeliveryFailed:
- condition = appleMDMFailedProfilesStmt
- excludeConditions = "FALSE"
- case fleet.MDMDeliveryPending:
- condition = appleMDMPendingProfilesStmt
- excludeConditions = appleMDMFailedProfilesStmt
- case fleet.MDMDeliveryVerifying:
- condition = appleMDMVerifyingProfilesStmt
- excludeConditions = fmt.Sprintf("(%s) OR (%s)", appleMDMPendingProfilesStmt, appleMDMFailedProfilesStmt)
- case fleet.MDMDeliveryVerified:
- condition = appleVerifiedProfilesStmt
- excludeConditions = fmt.Sprintf("(%s) OR (%s) OR (%s)", appleMDMPendingProfilesStmt, appleMDMFailedProfilesStmt, appleMDMVerifyingProfilesStmt)
- default:
- return "", nil, fmt.Errorf("invalid status: %s", status)
- }
-
- sql := fmt.Sprintf(`
- SELECT 1
- FROM host_mdm_apple_profiles hmap
- WHERE %s AND
- NOT EXISTS (
- SELECT 1
- FROM host_mdm_apple_profiles hmap
- WHERE %s
- )`, condition, excludeConditions)
-
- arg := map[string]any{
- "install": fleet.MDMOperationTypeInstall,
- "verifying": fleet.MDMDeliveryVerifying,
- "failed": fleet.MDMDeliveryFailed,
- "verified": fleet.MDMDeliveryVerified,
- "pending": fleet.MDMDeliveryPending,
- "filevault": mobileconfig.FleetFileVaultPayloadIdentifier,
- }
- query, args, err := sqlx.Named(sql, arg)
- if err != nil {
- return "", nil, fmt.Errorf("subqueryAppleProfileStatus %s: %w", status, err)
- }
-
- return query, args, nil
+ OR(fv_pending
+ OR((fv_verifying
+ OR fv_verified)
+ AND (hdek.base64_encrypted IS NULL OR (hdek.decryptable IS NOT NULL AND hdek.decryptable != 1))))) THEN
+ ` + pending + `
+ WHEN (prof_verifying
+ OR decl_verifying
+ -- special case when fv profile is verifying, and we already have an encryption key, in any state, we treat as verifying
+ OR(fv_verifying
+ AND hdek.base64_encrypted IS NOT NULL AND (hdek.decryptable IS NULL OR hdek.decryptable = 1))
+ -- special case when fv profile is verified, but we didn't verify the encryption key, we treat as verifying
+ OR(fv_verified
+ AND hdek.base64_encrypted IS NOT NULL AND hdek.decryptable IS NULL)) THEN
+ ` + verifying + `
+ WHEN (prof_verified
+ OR decl_verified
+ OR(fv_verified
+ AND hdek.base64_encrypted IS NOT NULL AND hdek.decryptable = 1)) THEN
+ ` + verified + `
+ END
+`
}
-// subqueryAppleDeclarationStatus builds out the subquery for declaration status
-func subqueryAppleDeclarationStatus() (string, []any, error) {
- const declNamedStmt = `
- CASE WHEN EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d1
- WHERE
- h.uuid = d1.host_uuid
- AND d1.operation_type = :install
- AND d1.status = :failed
- AND d1.declaration_name NOT IN (:reserved_names)) THEN
- 'declarations_failed'
- WHEN EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d2
- WHERE
- h.uuid = d2.host_uuid
- AND d2.operation_type = :install
- AND(d2.status IS NULL
- OR d2.status = :pending)
- AND d2.declaration_name NOT IN (:reserved_names)
- AND NOT EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d3
- WHERE
- h.uuid = d3.host_uuid
- AND d3.operation_type = :install
- AND d3.status = :failed
- AND d3.declaration_name NOT IN (:reserved_names))) THEN
- 'declarations_pending'
- WHEN EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d4
- WHERE
- h.uuid = d4.host_uuid
- AND d4.operation_type = :install
- AND d4.status = :verifying
- AND d4.declaration_name NOT IN (:reserved_names)
- AND NOT EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d5
- WHERE (h.uuid = d5.host_uuid
- AND d5.operation_type = :install
- AND d5.declaration_name NOT IN (:reserved_names)
- AND(d5.status IS NULL
- OR d5.status IN(:pending, :failed))))) THEN
- 'declarations_verifying'
- WHEN EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d6
- WHERE
- h.uuid = d6.host_uuid
- AND d6.operation_type = :install
- AND d6.status = :verified
- AND d6.declaration_name NOT IN (:reserved_names)
- AND NOT EXISTS (
- SELECT
- 1
- FROM
- host_mdm_apple_declarations d7
- WHERE (h.uuid = d7.host_uuid
- AND d7.operation_type = :install
- AND d7.declaration_name NOT IN (:reserved_names)
- AND(d7.status IS NULL
- OR d7.status IN(:pending, :failed, :verifying))))) THEN
- 'declarations_verified'
- ELSE
- ''
- END`
-
- arg := map[string]any{
- "install": fleet.MDMOperationTypeInstall,
- "verifying": fleet.MDMDeliveryVerifying,
- "failed": fleet.MDMDeliveryFailed,
- "verified": fleet.MDMDeliveryVerified,
- "pending": fleet.MDMDeliveryPending,
- "reserved_names": fleetmdm.ListFleetReservedMacOSDeclarationNames(),
- }
- query, args, err := sqlx.Named(declNamedStmt, arg)
- if err != nil {
- return "", nil, fmt.Errorf("subqueryAppleDeclarationStatus: %w", err)
- }
- query, args, err = sqlx.In(query, args...)
- if err != nil {
- return "", nil, fmt.Errorf("subqueryAppleDeclarationStatus resolve IN: %w", err)
- }
-
- return query, args, nil
-}
-
-func subqueryOSSettingsStatusMac() (string, []any, error) {
- var profArgs []any
- profFailed, profFailedArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryFailed)
- if err != nil {
- return "", nil, err
- }
- profArgs = append(profArgs, profFailedArgs...)
-
- profPending, profPendingArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryPending)
- if err != nil {
- return "", nil, err
- }
- profArgs = append(profArgs, profPendingArgs...)
-
- profVerifying, profVerifyingArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryVerifying)
- if err != nil {
- return "", nil, err
- }
- profArgs = append(profArgs, profVerifyingArgs...)
-
- profVerified, profVerifiedArgs, err := subqueryAppleProfileStatus(fleet.MDMDeliveryVerified)
- if err != nil {
- return "", nil, err
- }
- profArgs = append(profArgs, profVerifiedArgs...)
-
- profStmt := fmt.Sprintf(`
- CASE WHEN EXISTS (%s) THEN
- 'profiles_failed'
- WHEN EXISTS (%s) THEN
- 'profiles_pending'
- WHEN EXISTS (%s) THEN
- 'profiles_verifying'
- WHEN EXISTS (%s) THEN
- 'profiles_verified'
- ELSE
- ''
- END`,
- profFailed,
- profPending,
- profVerifying,
- profVerified,
+// sqlJoinMDMAppleProfilesStatus returns a SQL snippet that can be used to join a table derived from
+// host_mdm_apple_profiles (grouped by host_uuid and status) and the hosts table. For each host_uuid,
+// it derives a boolean value for each status category. The value will be 1 if the host has any
+// profile in the given status category. Separate columns are used for status of the filevault profile
+// vs. all other profiles. The snippet assumes the hosts table to be aliased as 'h'.
+func sqlJoinMDMAppleProfilesStatus() string {
+ // NOTE: To make this snippet reusable, we're not using sqlx.Named here because it would
+ // complicate usage in other queries (e.g., list hosts).
+ var (
+ failed = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryFailed))
+ pending = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryPending))
+ verifying = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryVerifying))
+ verified = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryVerified))
+ install = fmt.Sprintf("'%s'", string(fleet.MDMOperationTypeInstall))
+ filevault = fmt.Sprintf("'%s'", mobileconfig.FleetFileVaultPayloadIdentifier)
)
+ return `
+ LEFT JOIN (
+ -- profile statuses grouped by host uuid, boolean value will be 1 if host has any profile with the given status
+ -- filevault profiles are treated separately
+ SELECT
+ host_uuid,
+ MAX( IF((status IS NULL OR status = ` + pending + `) AND profile_identifier != ` + filevault + `, 1, 0)) AS prof_pending,
+ MAX( IF(status = ` + failed + ` AND profile_identifier != ` + filevault + `, 1, 0)) AS prof_failed,
+ MAX( IF(status = ` + verifying + ` AND profile_identifier != ` + filevault + ` AND operation_type = ` + install + `, 1, 0)) AS prof_verifying,
+ MAX( IF(status = ` + verified + ` AND profile_identifier != ` + filevault + ` AND operation_type = ` + install + `, 1, 0)) AS prof_verified,
+ MAX( IF((status IS NULL OR status = ` + pending + `) AND profile_identifier = ` + filevault + `, 1, 0)) AS fv_pending,
+ MAX( IF(status = ` + failed + ` AND profile_identifier = ` + filevault + `, 1, 0)) AS fv_failed,
+ MAX( IF(status = ` + verifying + ` AND profile_identifier = ` + filevault + ` AND operation_type = ` + install + `, 1, 0)) AS fv_verifying,
+ MAX( IF(status = ` + verified + ` AND profile_identifier = ` + filevault + ` AND operation_type = ` + install + `, 1, 0)) AS fv_verified
+ FROM
+ host_mdm_apple_profiles
+ GROUP BY
+ host_uuid) hmap ON h.uuid = hmap.host_uuid
+`
+}
- declStmt, declArgs, err := subqueryAppleDeclarationStatus()
- if err != nil {
- return "", nil, err
- }
-
- stmt := fmt.Sprintf(`
- CASE (%s)
- WHEN 'profiles_failed' THEN
- 'failed'
- WHEN 'profiles_pending' THEN (
- CASE (%s)
- WHEN 'declarations_failed' THEN
- 'failed'
- ELSE
- 'pending'
- END)
- WHEN 'profiles_verifying' THEN (
- CASE (%s)
- WHEN 'declarations_failed' THEN
- 'failed'
- WHEN 'declarations_pending' THEN
- 'pending'
- ELSE
- 'verifying'
- END)
- WHEN 'profiles_verified' THEN (
- CASE (%s)
- WHEN 'declarations_failed' THEN
- 'failed'
- WHEN 'declarations_pending' THEN
- 'pending'
- WHEN 'declarations_verifying' THEN
- 'verifying'
- ELSE
- 'verified'
- END)
- ELSE
- REPLACE((%s), 'declarations_', '')
- END`, profStmt, declStmt, declStmt, declStmt, declStmt)
-
- args := append(profArgs, declArgs...)
- args = append(args, declArgs...)
- args = append(args, declArgs...)
- args = append(args, declArgs...)
-
- // FIXME(roberto): we found issues in MySQL 5.7.17 (only that version,
- // which we must support for now) with prepared statements on this
- // query. The results returned by the DB were always different what
- // expected unless the arguments are inlined in the query.
- //
- // We decided to do this given:
- //
- // - The time constraints we were given to develop DDM
- // - The fact that all the variables in this query are really strings managed by us
- // - The imminent deprecation of MySQL 5.7
- return fmt.Sprintf(strings.Replace(stmt, "?", "'%s'", -1), args...), []any{}, nil
+// sqlJoinMDMAppleDeclarationsStatus returns a SQL snippet that can be used to join a table derived from
+// host_mdm_apple_declarations (grouped by host_uuid and status) and the hosts table. For each host_uuid,
+// it derives a boolean value for each status category. The value will be 1 if the host has any
+// declaration in the given status category. The snippet assumes the hosts table to be aliased as 'h'.
+func sqlJoinMDMAppleDeclarationsStatus() string {
+ // NOTE: To make this snippet reusable, we're not using sqlx.Named here because it would
+ // complicate usage in other queries (e.g., list hosts).
+ var (
+ failed = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryFailed))
+ pending = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryPending))
+ verifying = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryVerifying))
+ verified = fmt.Sprintf("'%s'", string(fleet.MDMDeliveryVerified))
+ install = fmt.Sprintf("'%s'", string(fleet.MDMOperationTypeInstall))
+ reservedDeclNames = fmt.Sprintf("'%s', '%s', '%s'", fleetmdm.FleetMacOSUpdatesProfileName, fleetmdm.FleetIOSUpdatesProfileName, fleetmdm.FleetIPadOSUpdatesProfileName)
+ )
+ return `
+ LEFT JOIN (
+ -- declaration statuses grouped by host uuid, boolean value will be 1 if host has any declaration with the given status
+ SELECT
+ host_uuid,
+ MAX( IF((status IS NULL OR status = ` + pending + `), 1, 0)) AS decl_pending,
+ MAX( IF(status = ` + failed + `, 1, 0)) AS decl_failed,
+ MAX( IF(status = ` + verifying + ` , 1, 0)) AS decl_verifying,
+ MAX( IF(status = ` + verified + ` , 1, 0)) AS decl_verified
+ FROM
+ host_mdm_apple_declarations
+ WHERE
+ operation_type = ` + install + ` AND declaration_name NOT IN(` + reservedDeclNames + `)
+ GROUP BY
+ host_uuid) hmad ON h.uuid = hmad.host_uuid
+`
}
func (ds *Datastore) GetMDMAppleProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) {
- subquery, args, err := subqueryOSSettingsStatusMac()
- if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "building os settings subquery")
- }
-
- sqlFmt := `
+ stmt := `
SELECT
- %s as status,
- COUNT(id) as count
+ COUNT(id) AS count,
+ %s AS status
FROM
- hosts h
-WHERE platform = 'darwin' OR platform = 'ios' OR platform = 'ipados'
-GROUP BY status, team_id HAVING status IN (?, ?, ?, ?) AND %s`
-
- args = append(args, fleet.MDMDeliveryFailed, fleet.MDMDeliveryPending, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified)
+ hosts h
+ %s
+ %s
+ LEFT JOIN host_disk_encryption_keys hdek ON h.id = hdek.host_id
+WHERE
+ platform IN('darwin', 'ios', 'ipad_os') AND %s
+GROUP BY
+ status HAVING status IS NOT NULL`
teamFilter := "team_id IS NULL"
if teamID != nil && *teamID > 0 {
- teamFilter = "team_id = ?"
- args = append(args, *teamID)
+ teamFilter = fmt.Sprintf("team_id = %d", *teamID)
}
- stmt := fmt.Sprintf(sqlFmt, subquery, teamFilter)
+ stmt = fmt.Sprintf(stmt, sqlCaseMDMAppleStatus(), sqlJoinMDMAppleProfilesStatus(), sqlJoinMDMAppleDeclarationsStatus(), teamFilter)
var dest []struct {
Count uint `db:"count"`
Status string `db:"status"`
}
- err = sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, args...)
- if err != nil {
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt); err != nil {
return nil, err
}
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 0b3a0e4983..ee23749de8 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -1114,6 +1114,14 @@ func (ds *Datastore) applyHostFilters(
whereParams = append(whereParams, microsoft_mdm.MDMDeviceStateEnrolled)
}
+ mdmAppleProfilesStatusJoin := ""
+ mdmAppleDeclarationsStatusJoin := ""
+ if opt.OSSettingsFilter.IsValid() ||
+ opt.MacOSSettingsFilter.IsValid() {
+ mdmAppleProfilesStatusJoin = sqlJoinMDMAppleProfilesStatus()
+ mdmAppleDeclarationsStatusJoin = sqlJoinMDMAppleDeclarationsStatus()
+ }
+
sqlStmt += fmt.Sprintf(
`FROM hosts h
LEFT JOIN host_seen_times hst ON (h.id = hst.host_id)
@@ -1128,6 +1136,8 @@ func (ds *Datastore) applyHostFilters(
%s
%s
%s
+ %s
+ %s
%s
WHERE TRUE AND %s AND %s AND %s AND %s
`,
@@ -1142,6 +1152,8 @@ func (ds *Datastore) applyHostFilters(
munkiJoin,
displayNameJoin,
connectedToFleetJoin,
+ mdmAppleProfilesStatusJoin,
+ mdmAppleDeclarationsStatusJoin,
// Conditions
ds.whereFilterHostsByTeams(filter, "h"),
@@ -1304,15 +1316,9 @@ func filterHostsByMacOSSettingsStatus(sql string, opt fleet.HostListOptions, par
whereStatus += ` AND h.team_id IS NULL`
}
- subqueryStatus, paramsStatus, err := subqueryOSSettingsStatusMac()
- if err != nil {
- return "", nil, err
- }
+ whereStatus += fmt.Sprintf(` AND %s = ?`, sqlCaseMDMAppleStatus())
- whereStatus += fmt.Sprintf(` AND %s = ?`, subqueryStatus)
- paramsStatus = append(paramsStatus, opt.MacOSSettingsFilter)
-
- return sql + whereStatus, append(params, paramsStatus...), nil
+ return sql + whereStatus, append(params, opt.MacOSSettingsFilter), nil
}
func filterHostsByMacOSDiskEncryptionStatus(sql string, opt fleet.HostListOptions, params []interface{}) (string, []interface{}) {
@@ -1364,13 +1370,9 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis
AND ((h.platform = 'windows' AND (%s))
OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s)))`
- whereMacOS, paramsMacOS, err := subqueryOSSettingsStatusMac()
- if err != nil {
- return "", nil, err
- }
- whereMacOS += ` = ?`
- // ensure the host has MDM turned on
- paramsMacOS = append(paramsMacOS, opt.OSSettingsFilter)
+ // construct the WHERE for macOS
+ whereMacOS = fmt.Sprintf(`(%s) = ?`, sqlCaseMDMAppleStatus())
+ paramsMacOS := []any{opt.OSSettingsFilter}
// construct the WHERE for windows
whereWindows = `hmdm.is_server = 0`
diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go
index d604b286a4..5ac777be39 100644
--- a/server/datastore/mysql/labels.go
+++ b/server/datastore/mysql/labels.go
@@ -638,6 +638,12 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea
joinParams = append(joinParams, microsoft_mdm.MDMDeviceStateEnrolled)
}
+ if opt.OSSettingsFilter.IsValid() ||
+ opt.MacOSSettingsFilter.IsValid() {
+ query += sqlJoinMDMAppleProfilesStatus()
+ query += sqlJoinMDMAppleDeclarationsStatus()
+ }
+
query += fmt.Sprintf(` WHERE lm.label_id = ? AND %s `, ds.whereFilterHostsByTeams(filter, "h"))
whereParams = append(whereParams, lid)
diff --git a/server/vulnerabilities/nvd/cve_test.go b/server/vulnerabilities/nvd/cve_test.go
index 691f3e321a..f9f8b12562 100644
--- a/server/vulnerabilities/nvd/cve_test.go
+++ b/server/vulnerabilities/nvd/cve_test.go
@@ -343,7 +343,7 @@ func TestTranslateCPEToCVE(t *testing.T) {
},
"cpe:2.3:a:python:python:3.9.6:*:*:*:*:windows:*:*": {
includedCVEs: []cve{
- {ID: "CVE-2024-4030", resolvedInVersion: "3.12.4"},
+ {ID: "CVE-2024-4030", resolvedInVersion: "3.9.20"},
},
continuesToUpdate: true,
},
diff --git a/website/assets/images/articles/automatic-software-install-add-software.png b/website/assets/images/articles/automatic-software-install-add-software.png
new file mode 100644
index 0000000000..4fdd54fe64
Binary files /dev/null and b/website/assets/images/articles/automatic-software-install-add-software.png differ
diff --git a/website/assets/images/articles/automatic-software-install-install-software.png b/website/assets/images/articles/automatic-software-install-install-software.png
new file mode 100644
index 0000000000..5e0aaef0b1
Binary files /dev/null and b/website/assets/images/articles/automatic-software-install-install-software.png differ
diff --git a/website/assets/images/articles/automatic-software-install-policies-manage.png b/website/assets/images/articles/automatic-software-install-policies-manage.png
new file mode 100644
index 0000000000..862c98eb15
Binary files /dev/null and b/website/assets/images/articles/automatic-software-install-policies-manage.png differ
diff --git a/website/assets/images/articles/automatic-software-install-top-image.png b/website/assets/images/articles/automatic-software-install-top-image.png
new file mode 100644
index 0000000000..ed188acd17
Binary files /dev/null and b/website/assets/images/articles/automatic-software-install-top-image.png differ
diff --git a/website/assets/images/articles/automatic-software-install-workflow.png b/website/assets/images/articles/automatic-software-install-workflow.png
new file mode 100644
index 0000000000..10dd582e13
Binary files /dev/null and b/website/assets/images/articles/automatic-software-install-workflow.png differ
diff --git a/website/config/routes.js b/website/config/routes.js
index 28fdbbf106..496eaf7597 100644
--- a/website/config/routes.js
+++ b/website/config/routes.js
@@ -353,6 +353,11 @@ module.exports.routes = {
'GET /device-management/fleet-user-stories-wayfair': '/success-stories/fleet-user-stories-wayfair',
'GET /handbook/security': '/handbook/digital-experience/security',
'GET /handbook/security/security-policies':'/handbook/digital-experience/security-policies#information-security-policy-and-acceptable-use-policy',// « reasoning: https://github.com/fleetdm/fleet/pull/9624
+ 'GET /handbook/business-operations/security-policies':'/handbook/digital-experience/security-policies',
+ 'GET /handbook/business-operations/application-security': '/handbook/digital-experience/application-security',
+ 'GET /handbook/business-operations/security-audits': '/handbook/digital-experience/security-audits',
+ 'GET /handbook/business-operations/security': '/handbook/digital-experience/security',
+ 'GET /handbook/business-operations/vendor-questionnaires': '/handbook/digital-experience/vendor-questionnaires',
'GET /handbook/handbook': '/handbook/company/handbook',
'GET /handbook/company/development-groups': '/handbook/company/product-groups',
'GET /docs/using-fleet/mdm-macos-settings': '/docs/using-fleet/mdm-custom-macos-settings',
@@ -565,6 +570,7 @@ module.exports.routes = {
'GET /learn-more-about/apple-business-manager-teams-api': 'https://github.com/fleetdm/fleet/blob/main/docs/Contributing/API-for-contributors.md#update-abm-tokens-teams',
'GET /learn-more-about/apple-business-manager-gitops': '/docs/using-fleet/gitops#apple-business-manager',
'GET /learn-more-about/s3-bootstrap-package': '/docs/configuration/fleet-server-configuration#s-3-software-installers-bucket',
+ 'GET /learn-more-about/policy-automation-install-software': '/guides/automatic-software-install-in-fleet',
'GET /learn-more-about/exe-install-scripts': '/guides/exe-install-scripts',
'GET /learn-more-about/install-scripts': '/guides/deploy-software-packages#install-script',
'GET /learn-more-about/uninstall-scripts': '/guides/deploy-software-packages#uninstall-script',