Merge branch 'main' into feat-fleet-app-library

This commit is contained in:
Gabriel Hernandez 2024-09-25 10:51:09 +01:00
commit 825cec3dbd
27 changed files with 322 additions and 424 deletions

View file

@ -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 Fleets 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.
<meta name="articleTitle" value="Automatic installation of software on hosts">
<meta name="authorFullName" value="Sharon Katz">
<meta name="authorGitHubUsername" value="sharon-fdm">
<meta name="category" value="guides">
<meta name="publishedOn" value="2024-09-23">
<meta name="articleImageUrl" value="../website/assets/images/articles/automatic-software-install-in-fleet-731x738@2x.png">
<meta name="description" value="A guide to workflows using automatic software installation in Fleet.">

View file

@ -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 | | | ✅ | ✅ | ✅ |

View file

@ -0,0 +1 @@
- UI: Remove redundant built in label filter pills

View file

@ -0,0 +1 @@
- Improved performance of SQL queries used to determine MDM profile status for Apple hosts.

View file

@ -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 = ({
<InputField
label="Minimum version"
tooltip={getMinimumVersionTooltip()}
helpText="Version number only (e.g., “13.0.1” not “Ventura 13” or “13.0.1 (22A400)”)"
helpText={
<>
Use only versions available from Apple.{" "}
<CustomLink
text="Learn more"
newTab
url="https://fleetdm.com/learn-more-about/available-os-update-versions"
/>
</>
}
value={minOsVersion}
error={minOsVersionError}
onChange={handleMinVersionChange}

View file

@ -1430,16 +1430,11 @@ const ManageHostsPage = ({
? selectedLabel
: undefined;
const statusDropdownClassnames = classNames(
`${baseClass}__status_dropdown`,
{ [`${baseClass}__status-dropdown-sandbox`]: isSandboxMode }
);
return (
<div className={`${baseClass}__filter-dropdowns`}>
<Dropdown
value={status || ""}
className={statusDropdownClassnames}
className={`${baseClass}__status_dropdown`}
options={getHostSelectStatuses(isSandboxMode)}
searchable={false}
onChange={handleStatusDropdownChange}

View file

@ -121,13 +121,6 @@
line-height: 38px;
}
}
.manage-hosts__status-dropdown-sandbox {
width: auto;
.Select-control {
width: 182px;
}
}
}
}
}
@ -183,12 +176,6 @@
.manage-hosts__status_dropdown {
width: auto;
}
.manage-hosts__status-dropdown-sandbox {
.Select-control {
width: 100%;
}
}
}
}
}
@ -246,7 +233,7 @@
.Select-menu-outer {
width: 364px;
max-height: 325px;
max-height: min-content;
.Select-menu {
max-height: none;

View file

@ -24,6 +24,7 @@ import {
import {
PLATFORM_LABEL_DISPLAY_NAMES,
PLATFORM_TYPE_ICONS,
isPlatformLabelNameFromAPI,
PolicyResponse,
} from "utilities/constants";
@ -41,6 +42,8 @@ import BootstrapPackageStatusFilter from "../BootstrapPackageStatusFilter/Bootst
const baseClass = "hosts-filter-block";
type PlatformLabelNameFromAPI = keyof typeof PLATFORM_TYPE_ICONS;
interface IHostsFilterBlockProps {
/**
* An object of params the the HostFilterBlock uses to render the correct
@ -145,6 +148,16 @@ const HostsFilterBlock = ({
PLATFORM_LABEL_DISPLAY_NAMES[display_text]) ||
display_text;
// Hide built-in labels supported in label dropdown
if (
label_type === "builtin" &&
Object.keys(PLATFORM_TYPE_ICONS).includes(
display_text as PlatformLabelNameFromAPI
)
) {
return <></>;
}
return (
<>
<FilterPill

View file

@ -158,7 +158,7 @@
min-width: 220px;
&__single-value {
max-width: 135px !important; // Must override default styling of .css-qc6sy-singleValue
max-width: 210px !important; // Must override default styling of .css-qc6sy-singleValue
}
}
}

View file

@ -13,7 +13,7 @@
&__item-topline {
display: flex;
flex-direction: row;
height: 64px;
height: 66px;
align-items: center;
gap: 16px;
overflow: hidden;

View file

@ -9,7 +9,7 @@
.Select {
.Select-menu-outer {
width: 364px;
max-height: 310px;
max-height: min-content;
.Select-menu {
max-height: none;

View file

@ -63,7 +63,7 @@
&__platform-dropdown {
.Select-menu-outer {
width: 364px;
max-height: 380px;
max-height: min-content;
.Select-menu {
max-height: none;

View file

@ -10,8 +10,7 @@ This handbook page details processes specific to working [with](#contact-us) and
| Chief Revenue Officer (CRO) | [Alex Mitchell](https://www.linkedin.com/in/alexandercmitchell/) _([@alexmitchelliii](https://github.com/alexmitchelliii))_
| Solutions Consulting (SC) | [Dave Herder](https://www.linkedin.com/in/daveherder/) _([@dherder](https://github.com/dherder))_ <br> [Zach Wasserman](https://www.linkedin.com/in/zacharywasserman/) _([@zwass](https://github.com/zwass))_ <br> [Allen Houchins](https://www.linkedin.com/in/allenhouchins/) _([@allenhouchins](https://github.com/allenhouchins))_ <br> [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))_ <br> [Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@anthonysnyder8](https://github.com/AnthonySnyder8))_ <br> [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))_ <br> [Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@anthonysnyder8](https://github.com/AnthonySnyder8))_ <br> [Paul Tardif](https://www.linkedin.com/in/paul-t-750833/) _([@phtardif1](https://github.com/phtardif1))_ <br> [Kendra McKeever](https://www.linkedin.com/in/kendramckeever/) _([@KendraAtFleet](https://github.com/KendraAtFleet))_
## Contact us

View file

@ -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:

View file

@ -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:

View file

@ -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)
}

View file

@ -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"})
}

View file

@ -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
}

View file

@ -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`

View file

@ -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)

View file

@ -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,
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -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',