diff --git a/articles/automatic-software-install-in-fleet.md b/articles/automatic-software-install-in-fleet.md new file mode 100644 index 0000000000..9b0ba6e65a --- /dev/null +++ b/articles/automatic-software-install-in-fleet.md @@ -0,0 +1,80 @@ +# Automatic policy-based installation of software on hosts + +![Top Image](../website/assets/images/articles/automatic-software-install-top-image.png) + +Fleet [v4.57.0](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.57.0) introduces the ability to automatically and remotely install software on hosts based on predefined policy failures. This guide will walk you through the process of configuring fleet for automatic installation of software on hosts using uploaded installation images and based on programmed policies. You'll learn how to configure and use this feature, as well as understand how the underlying mechanism works. + +Fleet allows its users to upload trusted software installation files to be installed and used on hosts. This installation could be conditioned on a failure of a specific Fleet Policy. + +## Prerequisites + +* Fleet premium with Admin permissions. +* Fleet [v4.57.0](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.57.0) or greater. + +## Step-by-step instructions + +1. **Adding software**: Add any software to be available for installation. Follow the [deploying software](https://fleetdm.com/guides/deploy-security-agents) document with instructions how to do it. Note that all installation steps (pre-install query, install script, and post-install script) will be executed as configured, regardless of the policy that triggers the installation. + + +![Add software](../website/assets/images/articles/automatic-software-install-add-software.png) + +Current supported software deployment formats: +- macOS: .pkg +- Windows: .msi, .exe +- Linux: .deb + +Coming soon: +- VPP for iOS and iPadOS + +2. **Add a policy**: In Fleet, add a policy that failure to pass will trigger the required installation. Go to Policies tab --> Press the top right "Add policy" button. --> Click "create your own policy" --> Enter your policy SQL --> Save --> Fill in details in the Save modal and Save. + +``` +SELECT 1 FROM apps WHERE name = 'Adobe Acrobat Reader.app' AND version_compare(bundle_short_version, '23.001.20687') >= 0; +``` + +Note: In order to know the exact application name to put in the query (e.g. "Adobe Acrobat Reader.app" in the query above) you can manually install it on a canary/test host and then query SELECT * from apps; + + +3. **Manage automation**: Open Manage Automations: Policies Tab --> top right "Manage automations" --> "Install software". + +![Manage policies](../website/assets/images/articles/automatic-software-install-policies-manage.png) + +4. **Select policy**: Select (click the check box of) your newly created policy. To the right of it select from the + drop-down list the software you would like to be installed upon failure of this policy. + +![Install software modal](../website/assets/images/articles/automatic-software-install-install-software.png) + +Upon failure of the selected policy, the selected software installation will be triggered. + +## How does it work? + +* After configuring Fleet to auto-install a specific software the rest will be done automatically. +* The policy check mechanism runs on a typical 1 hour cadence on all online hosts. +* Fleet will send install requests to the hosts on the first policy failure (first "No" result for the host) or if a policy goes from "Yes" to "No". On this iteration it will not send a install request if a policy is already failing and continues to fail ("No" -> "No"). See the following flowchart for details. + +![Flowchart](../website/assets/images/articles/automatic-software-install-workflow.png) +*Detailed flowchart* + +## Using the REST API for self-service software packages + +Fleet provides a REST API for managing software packages, including self-service software packages. Learn more about Fleet's [REST API](https://fleetdm.com/docs/rest-api/rest-api#add-team-policy). + +## Managing self-service software packages with GitOps + +To manage self-service software packages using Fleet's best practice GitOps, check out the `software` key in the [GitOps reference documentation](https://fleetdm.com/docs/configuration/yaml-files#policies). + +## Conclusion + +Software deployment can be time-consuming and risky. This guide presents Fleet's ability to mass deploy software to your fleet in a simple and safe way. Starting with uploading a trusted installer and ending with deploying it to the proper set of machines answering the exact policy defined by you. + +Leveraging Fleet’s ability to install and upgrade software on your hosts, you can streamline the process of controlling your hosts, replacing old versions of software and having the up-to-date info on what's installed on your fleet. + +By automating software deployment, you can gain greater control over what's installed on your machines and have better oversight of version upgrades, ensuring old software with known issues is replaced. + + + + + + + + diff --git a/articles/role-based-access.md b/articles/role-based-access.md index 95fc712c52..fcdae9a8d4 100644 --- a/articles/role-based-access.md +++ b/articles/role-based-access.md @@ -79,9 +79,10 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines. | Edit agent options for hosts assigned to teams\* | | | | ✅ | ✅ | | Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | | ✅ | ✅ | | | Retrieve contents from file carving | | | | ✅ | | -| View Apple mobile device management (MDM) certificate information | | | | ✅ | | -| View Apple business manager (BM) information | | | | ✅ | | -| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | | ✅ | | +| Create Apple Push Certificates service (APNs) certificate signing request (CSR) | | | | ✅ | | +| View, edit, and delete APNs certificate | | | | ✅ | | +| View, edit, and delete Apple Business Manager (ABM) connections | | | | ✅ | | +| View, edit, and delete Volume Purchasing Program (VPP) connections | | | | ✅ | | | View disk encryption key for macOS and Windows hosts | ✅ | ✅ | ✅ | ✅ | | | Edit OS updates for macOS, Windows, iOS, and iPadOS hosts | | | ✅ | ✅ | ✅ | | Create, edit, resend and delete configuration profiles for macOS and Windows hosts | | | ✅ | ✅ | ✅ | diff --git a/changes/21343-hide-redundant-built-in-label-pills b/changes/21343-hide-redundant-built-in-label-pills new file mode 100644 index 0000000000..92baea5ba5 --- /dev/null +++ b/changes/21343-hide-redundant-built-in-label-pills @@ -0,0 +1 @@ +- UI: Remove redundant built in label filter pills diff --git a/changes/22122-mdm-apple-status-queries b/changes/22122-mdm-apple-status-queries new file mode 100644 index 0000000000..2ea893d31f --- /dev/null +++ b/changes/22122-mdm-apple-status-queries @@ -0,0 +1 @@ +- Improved performance of SQL queries used to determine MDM profile status for Apple hosts. \ No newline at end of file diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/AppleOSTargetForm/AppleOSTargetForm.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/AppleOSTargetForm/AppleOSTargetForm.tsx index 755b59d093..fdf5343282 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/AppleOSTargetForm/AppleOSTargetForm.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/AppleOSTargetForm/AppleOSTargetForm.tsx @@ -5,12 +5,13 @@ import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import { NotificationContext } from "context/notification"; import configAPI from "services/entities/config"; import teamsAPI from "services/entities/teams"; +import { ApplePlatform } from "interfaces/platform"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import Button from "components/buttons/Button"; import validatePresence from "components/forms/validators/validate_presence"; -import { ApplePlatform } from "interfaces/platform"; +import CustomLink from "components/CustomLink"; const baseClass = "apple-os-target-form"; @@ -197,7 +198,16 @@ const AppleOSTargetForm = ({ + 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',