From cdf2a0c47c313ecbe62d7f9eea06e43af20e13e5 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Tue, 28 May 2024 19:17:14 -0300 Subject: [PATCH] iPhone/iPad support (#19221) #18119 - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [x] Added/updated tests - [X] Manual QA for all new/changed functionality --------- Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Co-authored-by: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Co-authored-by: Jacob Shandling --- changes/18119-iphone-ipad-support | 1 + cmd/fleet/cron.go | 63 ++++ cmd/fleet/serve.go | 9 + cmd/fleetctl/mdm.go | 17 +- frontend/__mocks__/hostMock.ts | 17 + frontend/components/App/App.tsx | 2 +- .../ManageSoftwareAutomationsModal.tsx | 7 +- frontend/pages/errors/Fleet404/Fleet404.tsx | 7 +- frontend/pages/errors/Fleet500/Fleet500.tsx | 7 +- .../hosts/ManageHostsPage/HostTableConfig.tsx | 6 +- .../details/DeviceUserPage/DeviceUserPage.tsx | 8 +- .../HostActionsDropdown.tests.tsx | 52 +++ .../HostActionsDropdown.tsx | 4 + .../HostDetailsPage/HostDetailsPage.tsx | 100 +++--- .../details/HostDetailsPage/_styles.scss | 12 + .../pages/hosts/details/cards/About/About.tsx | 33 +- .../cards/HostSummary/HostSummary.tests.tsx | 96 +++++- .../details/cards/HostSummary/HostSummary.tsx | 79 +++-- .../details/cards/HostSummary/helpers.tsx | 4 +- .../details/cards/Policies/HostPolicies.tsx | 94 +++--- .../details/cards/Queries/HostQueries.tsx | 80 +++-- .../{Software.tsx => HostSoftware.tsx} | 92 +++-- .../hosts/details/cards/Software/index.ts | 2 +- .../QueryDetailsPage/QueryDetailsPage.tsx | 10 +- .../components/QueryResults/QueryResults.tsx | 9 +- .../services/mock_service/mocks/config.ts | 1 + .../services/mock_service/mocks/responses.ts | 4 +- frontend/utilities/constants.tsx | 1 + server/datastore/mysql/apple_mdm.go | 129 +++++-- server/datastore/mysql/apple_mdm_test.go | 317 ++++++++++++++++-- server/datastore/mysql/hosts.go | 13 +- server/datastore/mysql/hosts_test.go | 2 + server/datastore/mysql/mdm.go | 22 +- server/datastore/mysql/scripts.go | 2 +- server/fleet/apple_mdm.go | 5 +- server/fleet/cron_schedules.go | 21 +- server/fleet/datastore.go | 4 + server/fleet/hosts.go | 23 +- server/fleet/mdm.go | 43 +++ server/fleet/mdm_test.go | 144 ++++++++ server/mdm/lifecycle/lifecycle.go | 7 +- server/mdm/mdm.go | 2 +- server/mdm/nanomdm/mdm/checkin.go | 7 + server/mock/datastore_mock.go | 12 + server/service/appconfig.go | 2 +- server/service/apple_mdm.go | 72 +++- server/service/apple_mdm_test.go | 81 ++++- server/service/client_mdm.go | 2 +- server/service/hosts.go | 8 +- server/service/integration_core_test.go | 1 - server/service/mdm.go | 8 +- server/worker/apple_mdm.go | 44 ++- server/worker/apple_mdm_test.go | 23 +- 53 files changed, 1433 insertions(+), 378 deletions(-) create mode 100644 changes/18119-iphone-ipad-support rename frontend/pages/hosts/details/cards/Software/{Software.tsx => HostSoftware.tsx} (79%) diff --git a/changes/18119-iphone-ipad-support b/changes/18119-iphone-ipad-support new file mode 100644 index 0000000000..89d958a3af --- /dev/null +++ b/changes/18119-iphone-ipad-support @@ -0,0 +1 @@ +* Added MDM support for iPhone/iPad. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index 4b8d65930c..39e409c311 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -35,6 +35,7 @@ import ( "github.com/fleetdm/fleet/v4/server/worker" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/google/uuid" "github.com/hashicorp/go-multierror" ) @@ -1173,3 +1174,65 @@ func stringSliceToUintSlice(s []string, logger kitlog.Logger) []uint { } return result } + +// newIPhoneIPadRefetcher will enqueue DeviceInformation commands on iOS/iPadOS devices +// to refetch their host details. +// +// See https://developer.apple.com/documentation/devicemanagement/get_device_information. +// +// We will refetch iPhones/iPads every 1 hour (to match the default +// detail interval of all (osquery-capable) hosts in Fleet). +func newIPhoneIPadRefetcher( + ctx context.Context, + instanceID string, + periodicity time.Duration, + ds fleet.Datastore, + commander *apple_mdm.MDMAppleCommander, + logger kitlog.Logger, +) (*schedule.Schedule, error) { + const name = string(fleet.CronAppleMDMIPhoneIPadRefetcher) + logger = kitlog.With(logger, "cron", name, "component", "iphone-ipad-refetcher") + s := schedule.New( + ctx, name, instanceID, periodicity, ds, ds, + schedule.WithLogger(logger), + schedule.WithJob("cron_iphone_ipad_refetcher", func(ctx context.Context) error { + start := time.Now() + uuids, err := ds.ListIOSAndIPadOSToRefetch(ctx, 1*time.Hour) + if err != nil { + return ctxerr.Wrap(ctx, err, "list ios and ipad devices to refetch") + } + if len(uuids) == 0 { + return nil + } + logger.Log("msg", "sending commands to refetch", "count", len(uuids), "lookup-duration", time.Since(start)) + commandUUID := fleet.RefetchCommandUUIDPrefix + uuid.NewString() + if err := commander.EnqueueCommand(ctx, uuids, fmt.Sprintf(` + + + + Command + + Queries + + DeviceName + DeviceCapacity + AvailableDeviceCapacity + OSVersion + WiFiMAC + ProductName + + RequestType + DeviceInformation + + CommandUUID + %s + +`, commandUUID)); err != nil { + return ctxerr.Wrap(ctx, err, "send DeviceInformation commands to ios and ipados devices") + } + return nil + }), + ) + + return s, nil +} diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index 34d765c9b8..355a892441 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -798,6 +798,15 @@ the way that the Fleet server works. } } + if license.IsPremium() && appCfg.MDM.EnabledAndConfigured { + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService, config.MDM) + return newIPhoneIPadRefetcher(ctx, instanceID, 10*time.Minute, ds, commander, logger) + }); err != nil { + initFatal(err, "failed to register apple_mdm_iphone_ipad_refetcher schedule") + } + } + if license.IsPremium() && config.Activity.EnableAuditLog { if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { return newActivitiesStreamingSchedule(ctx, instanceID, ds, logger, auditLogger) diff --git a/cmd/fleetctl/mdm.go b/cmd/fleetctl/mdm.go index ce5841dab6..b21231dd9f 100644 --- a/cmd/fleetctl/mdm.go +++ b/cmd/fleetctl/mdm.go @@ -90,7 +90,7 @@ func mdmRunCommand() *cli.Command { var ( hostUUIDs []string notFoundCount int - platform string + mdmPlatform string // "darwin" or "windows" ) for _, ident := range hostIdents { host, err := client.HostByIdentifier(ident) @@ -110,10 +110,11 @@ func mdmRunCommand() *cli.Command { return err } - if host.Platform != platform && platform != "" { + mdmHostPlatform := fleet.MDMPlatform(host.Platform) + if mdmHostPlatform != mdmPlatform && mdmPlatform != "" { return errors.New(`Command can't run on hosts with different platforms. Make sure the hosts specified in the "hosts" flag are either all macOS or all Windows hosts.`) } - platform = host.Platform + mdmPlatform = mdmHostPlatform // TODO(mna): this "On" check is brittle, but looks like it's the only // enrollment indication we have right now... @@ -134,15 +135,15 @@ func mdmRunCommand() *cli.Command { return errors.New("One or more targeted hosts don't exist. Make sure you provide a valid hostname, UUID, osquery host ID, or node key.") } - result, err := client.RunMDMCommand(hostUUIDs, payload, platform) + result, err := client.RunMDMCommand(hostUUIDs, payload, mdmPlatform) if err != nil { - if errors.Is(err, service.ErrMissingLicense) && platform == "windows" { + if errors.Is(err, service.ErrMissingLicense) && mdmPlatform == "windows" { return errors.New(fleet.WindowsMDMRequiresPremiumCmdMessage) } var sce kithttp.StatusCoder if errors.As(err, &sce) { - if sce.StatusCode() == http.StatusUnsupportedMediaType && platform == "darwin" { + if sce.StatusCode() == http.StatusUnsupportedMediaType && mdmPlatform == "darwin" { return fmt.Errorf("The payload isn't valid. Please provide a valid MDM command in the form of a plist-encoded XML file: %w", err) } // this condition needs to be repeated here: maybe the user has @@ -229,7 +230,7 @@ func mdmUnlockCommand() *cli.Command { return fmt.Errorf("Failed to unlock host: %w", err) } - if host.Platform == "darwin" { + if fleet.MDMPlatform(host.Platform) == "darwin" { fmt.Fprintf(c.App.Writer, ` Use this 6 digit PIN to unlock the host: @@ -329,7 +330,7 @@ func hostMdmActionSetup(c *cli.Context, hostIdent string, actionType string) (cl } // check mdm is on for the host - if host.Platform == "windows" || host.Platform == "darwin" { + if fleet.MDMSupported(host.Platform) { if host.MDM.EnrollmentStatus == nil || !strings.HasPrefix(*host.MDM.EnrollmentStatus, "On") || host.MDM.Name != fleet.WellKnownMDMFleet { return nil, nil, fmt.Errorf("Can't %s the host because it doesn't have MDM turned on.", actionType) diff --git a/frontend/__mocks__/hostMock.ts b/frontend/__mocks__/hostMock.ts index fce7dec502..276c99b520 100644 --- a/frontend/__mocks__/hostMock.ts +++ b/frontend/__mocks__/hostMock.ts @@ -112,6 +112,23 @@ const createMockHost = (overrides?: Partial): IHost => { export const createMockHostResponse = { host: createMockHost() }; +export const createMockIosHostResponse = { + host: createMockHost({ + hostname: "Test device (iPhone)", + display_name: "Test device (iPhone)", + team_id: 2, + team_name: "Mobile", + platform: "ios", + os_version: "iOS 14.7.1", + hardware_serial: "C8QH6T96DPNA", + created_at: "2024-01-01T12:00:00Z", + updated_at: "2024-05-02T12:00:00Z", + detail_updated_at: "2024-05-02T12:00:00Z", + last_restarted_at: "2024-04-02T12:00:00Z", + last_enrolled_at: "2024-01-02T12:00:00Z", + }), +}; + export const createMockHostSummary = (overrides?: Partial) => { return normalizeEmptyValues( pick(createMockHost(overrides), HOST_SUMMARY_DATA) diff --git a/frontend/components/App/App.tsx b/frontend/components/App/App.tsx index 136480369a..1152060eaa 100644 --- a/frontend/components/App/App.tsx +++ b/frontend/components/App/App.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useContext, useEffect, useState } from "react"; +import React, { FC, useContext, useEffect, useState } from "react"; import { AxiosResponse } from "axios"; import { QueryClient, diff --git a/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx b/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx index 2869dd799e..475e3dfc08 100644 --- a/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx +++ b/frontend/pages/SoftwarePage/components/ManageSoftwareAutomationsModal/ManageSoftwareAutomationsModal.tsx @@ -16,6 +16,7 @@ import { CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS, } from "interfaces/config"; import configAPI from "services/entities/config"; +import { SUPPORT_LINK } from "utilities/constants"; import ReactTooltip from "react-tooltip"; // @ts-ignore @@ -477,11 +478,7 @@ const ManageAutomationsModal = ({

Vulnerability automations currently run for software vulnerabilities. Interested in automations for OS vulnerabilities?{" "} - +

diff --git a/frontend/pages/errors/Fleet404/Fleet404.tsx b/frontend/pages/errors/Fleet404/Fleet404.tsx index b05d346369..1c95cca7c7 100644 --- a/frontend/pages/errors/Fleet404/Fleet404.tsx +++ b/frontend/pages/errors/Fleet404/Fleet404.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router"; import PATHS from "router/paths"; +import { SUPPORT_LINK } from "utilities/constants"; import Button from "components/buttons/Button"; // @ts-ignore @@ -38,11 +39,7 @@ const Fleet404 = () => ( The page you are looking for has either moved, or doesn't exist.

- +
- {renderActionButtons()} + {renderActionDropdown()}
{renderSummary()} diff --git a/frontend/pages/hosts/details/cards/HostSummary/helpers.tsx b/frontend/pages/hosts/details/cards/HostSummary/helpers.tsx index 668feffc6c..7828578395 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/helpers.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/helpers.tsx @@ -30,13 +30,13 @@ export const DEVICE_STATUS_TAGS: DeviceStatusTagConfig = { unlocking: { title: "UNLOCK PENDING", tagType: "warning", - generateTooltip: (platform) => + generateTooltip: () => "Host will unlock when it comes online. If the host is online, it will unlock the next time it checks in to Fleet.", }, locking: { title: "LOCK PENDING", tagType: "warning", - generateTooltip: (platform) => + generateTooltip: () => "Host will lock when it comes online. If the host is online, it will lock the next time it checks in to Fleet.", }, wiped: { diff --git a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx index 05dba4ad7d..67df181e3c 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx @@ -1,9 +1,11 @@ import React from "react"; import { IHostPolicy } from "interfaces/policy"; +import { SUPPORT_LINK } from "utilities/constants"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; import Card from "components/Card"; +import CustomLink from "components/CustomLink"; import { generatePolicyTableHeaders, @@ -18,6 +20,7 @@ interface IPoliciesProps { isLoading: boolean; deviceUser?: boolean; togglePolicyDetailsModal: (policy: IHostPolicy) => void; + hostPlatform: string; } const Policies = ({ @@ -25,16 +28,34 @@ const Policies = ({ isLoading, deviceUser, togglePolicyDetailsModal, + hostPlatform, }: IPoliciesProps): JSX.Element => { - if (policies.length === 0) { - return ( - -

Policies

+ const tableHeaders = generatePolicyTableHeaders(togglePolicyDetailsModal); + if (deviceUser) { + // Remove view all hosts link + tableHeaders.pop(); + } + const failingResponses: IHostPolicy[] = + policies.filter((policy: IHostPolicy) => policy.response === "fail") || []; + + const renderHostPolicies = () => { + if (hostPlatform === "ios" || hostPlatform === "ipados") { + return ( + Policies are not supported for this host} + info={ + <> + Interested in detecting device health issues on{" "} + {hostPlatform === "ios" ? "iPhones" : "iPads"}?{" "} + + + } + /> + ); + } + + if (policies.length === 0) { + return ( @@ -50,17 +71,30 @@ const Policies = ({ } /> -
- ); - } + ); + } - const tableHeaders = generatePolicyTableHeaders(togglePolicyDetailsModal); - if (deviceUser) { - // Remove view all hosts link - tableHeaders.pop(); - } - const failingResponses: IHostPolicy[] = - policies.filter((policy: IHostPolicy) => policy.response === "fail") || []; + return ( + <> + {failingResponses?.length > 0 && ( + + )} + <>} + showMarkAllPages={false} + isAllPagesSelected={false} + disablePagination + disableCount + disableMultiRowSelect + /> + + ); + }; return (

Policies

- - {policies.length > 0 && ( - <> - {failingResponses?.length > 0 && ( - - )} - <>} - showMarkAllPages={false} - isAllPagesSelected={false} - disablePagination - disableCount - disableMultiRowSelect - /> - - )} + {renderHostPolicies()}
); }; diff --git a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx index 8b3d4e96d9..885419acfa 100644 --- a/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx +++ b/frontend/pages/hosts/details/cards/Queries/HostQueries.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo } from "react"; import { IQueryStats } from "interfaces/query_stats"; +import { SUPPORT_LINK } from "utilities/constants"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; import CustomLink from "components/CustomLink"; @@ -19,7 +20,7 @@ const baseClass = "host-queries-card"; interface IHostQueriesProps { hostId: number; schedule?: IQueryStats[]; - isChromeOSHost: boolean; + hostPlatform: string; queryReportsDisabled?: boolean; router: InjectedRouter; } @@ -30,15 +31,16 @@ interface IHostQueriesRowProps extends Row { should_link_to_hqr?: boolean; }; } + const HostQueries = ({ hostId, schedule, - isChromeOSHost, + hostPlatform, queryReportsDisabled, router, }: IHostQueriesProps): JSX.Element => { const renderEmptyQueriesTab = () => { - if (isChromeOSHost) { + if (hostPlatform === "chrome") { return ( ); } + + if (hostPlatform === "ios" || hostPlatform === "ipados") { + return ( + + Interested in querying{" "} + {hostPlatform === "ios" ? "iPhones" : "iPads"}?{" "} + + + } + /> + ); + } + return ( { + if ( + !schedule || + !schedule.length || + hostPlatform === "chrome" || + hostPlatform === "ios" || + hostPlatform === "ipados" + ) { + return renderEmptyQueriesTab(); + } + + return ( +
+ null} + resultsTitle="queries" + defaultSortHeader="query_name" + defaultSortDirection="asc" + showMarkAllPages={false} + isAllPagesSelected={false} + emptyComponent={() => <>} + disablePagination + disableCount + disableMultiRowSelect + isLoading={false} // loading state handled at parent level + onSelectSingleRow={onSelectSingleRow} + /> +
+ ); + }; + return (

Queries

- {!schedule || !schedule.length || isChromeOSHost ? ( - renderEmptyQueriesTab() - ) : ( -
- null} - resultsTitle="queries" - defaultSortHeader="query_name" - defaultSortDirection="asc" - showMarkAllPages={false} - isAllPagesSelected={false} - emptyComponent={() => <>} - disablePagination - disableCount - disableMultiRowSelect - isLoading={false} // loading state handled at parent level - onSelectSingleRow={onSelectSingleRow} - /> -
- )} + {renderHostQueries()}
); }; diff --git a/frontend/pages/hosts/details/cards/Software/Software.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx similarity index 79% rename from frontend/pages/hosts/details/cards/Software/Software.tsx rename to frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index df562a3dda..a1aeb122ea 100644 --- a/frontend/pages/hosts/details/cards/Software/Software.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -14,13 +14,15 @@ import deviceAPI, { } from "services/entities/device_user"; import { getErrorReason } from "interfaces/errors"; import { IHostSoftware, ISoftware } from "interfaces/software"; -import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; +import { DEFAULT_USE_QUERY_OPTIONS, SUPPORT_LINK } from "utilities/constants"; import { NotificationContext } from "context/notification"; import { AppContext } from "context/app"; import Card from "components/Card/Card"; import DataError from "components/DataError"; import Spinner from "components/Spinner"; +import EmptyTable from "components/EmptyTable"; +import CustomLink from "components/CustomLink"; import { generateSoftwareTableHeaders as generateHostSoftwareTableConfig } from "./HostSoftwareTableConfig"; import { generateSoftwareTableHeaders as generateDeviceSoftwareTableConfig } from "./DeviceSoftwareTableConfig"; @@ -32,15 +34,15 @@ export interface ITableSoftware extends Omit { vulnerabilities: string[]; // for client-side search purposes, we only want an array of cve strings } -interface ISoftwareCardProps { +interface IHostSoftwareProps { /** This is the host id or the device token */ id: number | string; isFleetdHost: boolean; router: InjectedRouter; queryParams: ReturnType; pathname: string; - /** Team id for the host */ - teamId: number; + hostTeamId: number; + hostPlatform: string; onShowSoftwareDetails?: (software: IHostSoftware) => void; isSoftwareEnabled?: boolean; isMyDevicePage?: boolean; @@ -75,17 +77,18 @@ export const parseHostSoftwareQueryParams = (queryParams: { }; }; -const SoftwareCard = ({ +const HostSoftware = ({ id, isFleetdHost, router, queryParams, pathname, - teamId = 0, + hostTeamId = 0, + hostPlatform, onShowSoftwareDetails, isSoftwareEnabled = false, isMyDevicePage = false, -}: ISoftwareCardProps) => { +}: IHostSoftwareProps) => { const { renderFlash } = useContext(NotificationContext); const { isGlobalAdmin, @@ -98,6 +101,8 @@ const SoftwareCard = ({ number | null >(null); + const isIosOrIpadOs = hostPlatform === "ipados" || hostPlatform === "ios"; + const { data: hostSoftwareRes, isLoading: hostSoftwareLoading, @@ -122,7 +127,7 @@ const SoftwareCard = ({ }, { ...DEFAULT_USE_QUERY_OPTIONS, - enabled: isSoftwareEnabled && !isMyDevicePage, // if disabled, we'll always show a generic "No software detected" message + enabled: isSoftwareEnabled && !isMyDevicePage && !isIosOrIpadOs, // if disabled, we'll always show a generic "No software detected" message keepPreviousData: true, staleTime: 7000, } @@ -217,7 +222,7 @@ const SoftwareCard = ({ installingSoftwareId, canInstall: canInstallSoftware, onSelectAction, - teamId, + teamId: hostTeamId, isFleetdHost, }); }, [ @@ -226,7 +231,7 @@ const SoftwareCard = ({ installingSoftwareId, canInstallSoftware, onSelectAction, - teamId, + hostTeamId, isFleetdHost, ]); @@ -238,6 +243,48 @@ const SoftwareCard = ({ const data = isMyDevicePage ? deviceSoftwareRes : hostSoftwareRes; + const renderHostSoftware = () => { + if (isLoading) { + return ; + } + + if (isIosOrIpadOs) { + return ( + + Interested in viewing software for{" "} + {hostPlatform === "ios" ? "iPhones" : "iPads"}?{" "} + + + } + /> + ); + } + + return ( + <> + {isError && } + {!isError && ( + + )} + + ); + }; + return (

Software

- {isLoading ? ( - - ) : ( - <> - {isError && } - {!isError && ( - - )} - - )} + {renderHostSoftware()}
); }; -export default React.memo(SoftwareCard); +export default React.memo(HostSoftware); diff --git a/frontend/pages/hosts/details/cards/Software/index.ts b/frontend/pages/hosts/details/cards/Software/index.ts index d7a37903c0..465d5f68e0 100644 --- a/frontend/pages/hosts/details/cards/Software/index.ts +++ b/frontend/pages/hosts/details/cards/Software/index.ts @@ -1 +1 @@ -export { default } from "./Software"; +export { default } from "./HostSoftware"; diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx index 9811794433..712ae3a0f5 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx @@ -21,7 +21,7 @@ import { isGlobalObserver, isTeamObserver, } from "utilities/permissions/permissions"; -import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants"; +import { DOCUMENT_TITLE_SUFFIX, SUPPORT_LINK } from "utilities/constants"; import { buildQueryStringFromParams } from "utilities/url"; import useTeamIdParam from "hooks/useTeamIdParam"; @@ -332,13 +332,7 @@ const QueryDetailsPage = ({ const renderClippedBanner = () => ( - } + cta={} >
Report clipped. A sample of this query's results is included diff --git a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx index c27ab54685..43fed46962 100644 --- a/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx +++ b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx @@ -11,6 +11,7 @@ import { generateCSVQueryResults, } from "utilities/generate_csv"; import { getTableColumnsFromSql } from "utilities/helpers"; +import { SUPPORT_LINK } from "utilities/constants"; import { ICampaign, ICampaignError } from "interfaces/campaign"; import { ITarget } from "interfaces/target"; @@ -263,13 +264,7 @@ const QueryResults = ({ {isQueryClipped && ( - } + cta={} >
Results clipped. A sample of this query's results and diff --git a/frontend/services/mock_service/mocks/config.ts b/frontend/services/mock_service/mocks/config.ts index 5d5c41029d..6a13a49aec 100644 --- a/frontend/services/mock_service/mocks/config.ts +++ b/frontend/services/mock_service/mocks/config.ts @@ -23,6 +23,7 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = { // expensive data operations "targets?query={*}": RESPONSES.hosts, // "SchedulableQueries" to be used in developing frontend for #7765 + "hosts/12345": RESPONSES.hostDetailsiOS, queries: RESPONSES.globalQueries, "queries/1": RESPONSES.globalQuery1, "queries/2": RESPONSES.globalQuery2, diff --git a/frontend/services/mock_service/mocks/responses.ts b/frontend/services/mock_service/mocks/responses.ts index 709abc3f8b..265b6dd494 100644 --- a/frontend/services/mock_service/mocks/responses.ts +++ b/frontend/services/mock_service/mocks/responses.ts @@ -4,6 +4,7 @@ * Also please check the README for how to use the mock service :) */ +import { createMockIosHostResponse } from "__mocks__/hostMock"; import { createMockPoliciesResponse } from "__mocks__/policyMock"; const count = { @@ -10593,7 +10594,7 @@ const globalQuery6 = { query: globalQueries.queries[6] }; const teamQuery1 = { query: teamQueries.queries[0] }; const teamQuery2 = { query: teamQueries.queries[1] }; const teamPolicy1 = createMockPoliciesResponse(); - +const hostDetailsiOS = createMockIosHostResponse; const aiAutofillPolicy = { description: "The firewall is not enabled, exposing the laptop to potential security threats such as unauthorized access, data breaches, and malware attacks.", @@ -10618,4 +10619,5 @@ export default { teamQuery2, aiAutofillPolicy, teamPolicy1, + hostDetailsiOS, }; diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index 74a06ee3cd..3c5235c61c 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -387,6 +387,7 @@ export const HOST_ABOUT_DATA = [ "batteries", "detail_updated_at", "last_restarted_at", + "platform", ]; export const HOST_OSQUERY_DATA = [ diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index be5ecd2148..621c763999 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -707,7 +707,7 @@ WHERE func (ds *Datastore) MDMAppleUpsertHost(ctx context.Context, mdmHost *fleet.Host) error { appCfg, err := ds.AppConfig(ctx) if err != nil { - return ctxerr.Wrap(ctx, err, "ingest mdm apple host get app config") + return ctxerr.Wrap(ctx, err, "mdm apple upsert host get app config") } return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { return ingestMDMAppleDeviceFromCheckinDB(ctx, tx, mdmHost, ds.logger, appCfg) @@ -743,6 +743,20 @@ func ingestMDMAppleDeviceFromCheckinDB( } } +func mdmHostEnrollFields(mdmHost *fleet.Host) (refetchRequested bool, lastEnrolledAt time.Time) { + supportsOsquery := mdmHost.SupportsOsquery() + // 2000-01-01 00:00:00 is what Fleet considers the zero/"Never" time. + lastEnrolledAt, err := time.Parse("2006-01-02 15:04:05", "2000-01-01 00:00:00") + if err != nil { + panic(err) + } + if !supportsOsquery { + // Given the device does not have osquery, we set the last_enrolled_at as the MDM enroll time. + lastEnrolledAt = time.Now() + } + return supportsOsquery, lastEnrolledAt +} + func updateMDMAppleHostDB( ctx context.Context, tx sqlx.ExtContext, @@ -750,6 +764,8 @@ func updateMDMAppleHostDB( mdmHost *fleet.Host, appCfg *fleet.AppConfig, ) error { + refetchRequested, lastEnrolledAt := mdmHostEnrollFields(mdmHost) + updateStmt := ` UPDATE hosts SET hardware_serial = ?, @@ -757,6 +773,7 @@ func updateMDMAppleHostDB( hardware_model = ?, platform = ?, refetch_requested = ?, + last_enrolled_at = ?, osquery_host_id = COALESCE(NULLIF(osquery_host_id, ''), ?) WHERE id = ?` @@ -766,8 +783,9 @@ func updateMDMAppleHostDB( mdmHost.HardwareSerial, mdmHost.UUID, mdmHost.HardwareModel, - "darwin", - 1, + mdmHost.Platform, + refetchRequested, + lastEnrolledAt, // Set osquery_host_id to the device UUID only if it is not already set. mdmHost.UUID, hostID, @@ -794,6 +812,7 @@ func insertMDMAppleHostDB( logger log.Logger, appCfg *fleet.AppConfig, ) error { + refetchRequested, lastEnrolledAt := mdmHostEnrollFields(mdmHost) insertStmt := ` INSERT INTO hosts ( hardware_serial, @@ -812,11 +831,11 @@ func insertMDMAppleHostDB( mdmHost.HardwareSerial, mdmHost.UUID, mdmHost.HardwareModel, - "darwin", - "2000-01-01 00:00:00", + mdmHost.Platform, + lastEnrolledAt, "2000-01-01 00:00:00", mdmHost.UUID, - 1, + refetchRequested, ) if err != nil { return ctxerr.Wrap(ctx, err, "insert mdm apple host") @@ -896,18 +915,18 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic SELECT us.hardware_serial, COALESCE(GROUP_CONCAT(DISTINCT us.hardware_model), ''), - 'darwin' AS platform, + us.platform, '2000-01-01 00:00:00' AS last_enrolled_at, '2000-01-01 00:00:00' AS detail_updated_at, NULL AS osquery_host_id, - 1 AS refetch_requested, + IF(us.platform = 'ios' OR us.platform = 'ipados', 0, 1) AS refetch_requested, ? AS team_id FROM (%s) us LEFT JOIN hosts h ON us.hardware_serial = h.hardware_serial WHERE h.id IS NULL GROUP BY - us.hardware_serial)`, + us.hardware_serial, us.platform)`, us, ) @@ -933,6 +952,7 @@ func (ds *Datastore) IngestMDMAppleDevicesFromDEPSync(ctx context.Context, devic err = sqlx.SelectContext(ctx, tx, &hostsWithMDMInfo, fmt.Sprintf(` SELECT h.id, + h.platform, h.hardware_model, h.hardware_serial, COALESCE(hmdm.enrolled, 0) as enrolled @@ -1096,25 +1116,42 @@ func upsertMDMAppleHostLabelMembershipDB(ctx context.Context, tx sqlx.ExtContext // now because it may still be some time before osquery is running on these // devices. Because these are Apple devices, we're adding them to the "All // Hosts" and "macOS" labels. - labelIDs := []uint{} - err := sqlx.SelectContext(ctx, tx, &labelIDs, `SELECT id FROM labels WHERE label_type = 1 AND (name = 'All Hosts' OR name = 'macOS')`) + labels := []struct { + ID uint `db:"id"` + Name string `db:"name"` + }{} + err := sqlx.SelectContext(ctx, tx, &labels, `SELECT id, name FROM labels WHERE label_type = 1 AND (name = 'All Hosts' OR name = 'macOS')`) switch { case err != nil: return ctxerr.Wrap(ctx, err, "get builtin labels") - case len(labelIDs) != 2: + case len(labels) != 2: // Builtin labels can get deleted so it is important that we check that // they still exist before we continue. - level.Error(logger).Log("err", fmt.Sprintf("expected 2 builtin labels but got %d", len(labelIDs))) + level.Error(logger).Log("err", fmt.Sprintf("expected 2 builtin labels but got %d", len(labels))) return nil default: // continue } + // Put "All Hosts" label first (we don't want to make assumptions around ids of builtin labels). + labelIDs := make([]uint, 0, 2) + if labels[0].Name == "All Hosts" { + labelIDs = append(labelIDs, labels[0].ID, labels[1].ID) + } else { + labelIDs = append(labelIDs, labels[1].ID, labels[0].ID) + } + parts := []string{} args := []interface{}{} for _, h := range hosts { - parts = append(parts, "(?,?),(?,?)") - args = append(args, h.ID, labelIDs[0], h.ID, labelIDs[1]) + // iOS/iPadOS devices only get the "All Hosts" label. + if h.Platform == "ios" || h.Platform == "ipados" { + parts = append(parts, "(?,?)") + args = append(args, h.ID, labelIDs[0]) + } else { // macOS devices get both labels, "All Hosts" and "macOS". + parts = append(parts, "(?,?),(?,?)") + args = append(args, h.ID, labelIDs[0], h.ID, labelIDs[1]) + } } _, err = tx.ExecContext(ctx, fmt.Sprintf(` INSERT INTO label_membership (host_id, label_id) VALUES %s @@ -1131,6 +1168,8 @@ func upsertMDMAppleHostLabelMembershipDB(ctx context.Context, tx sqlx.ExtContext func (ds *Datastore) deleteMDMOSCustomSettingsForHost(ctx context.Context, tx sqlx.ExtContext, uuid, platform string) error { tableMap := map[string][]string{ "darwin": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"}, + "ios": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"}, + "ipados": {"host_mdm_apple_profiles", "host_mdm_apple_declarations"}, "windows": {"host_mdm_windows_profiles"}, } @@ -1162,8 +1201,8 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error { return ctxerr.Wrap(ctx, err, "getting host info from UUID") } - if host.Platform != "darwin" && host.Platform != "windows" { - return ctxerr.Errorf(ctx, "unsupported host platform: %s", host.Platform) + if !fleet.MDMSupported(host.Platform) { + return ctxerr.Errorf(ctx, "unsupported host platform: %q", host.Platform) } // NOTE: set installed_from_dep = 0 so DEP host will not be @@ -1192,6 +1231,11 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error { // NOTE: intentionally keeping disk encryption keys and bootstrap // package information. + // iPhones and iPads have no osquery thus we don't need to refetch. + if host.Platform == "ios" || host.Platform == "ipados" { + return nil + } + // request a refetch to update any eventually consistent stale information. err = updateHostRefetchRequestedDB(ctx, tx, host.ID, true) return ctxerr.Wrap(ctx, err, "setting host refetch requested") @@ -1201,11 +1245,19 @@ func (ds *Datastore) MDMTurnOff(ctx context.Context, uuid string) error { func unionSelectDevices(devices []godep.Device) (stmt string, args []interface{}) { for i, d := range devices { if i == 0 { - stmt = "SELECT ? hardware_serial, ? hardware_model" + stmt = "SELECT ? hardware_serial, ? hardware_model, ? platform" } else { - stmt += " UNION SELECT ?, ?" + stmt += " UNION SELECT ?, ?, ?" } - args = append(args, d.SerialNumber, d.Model) + // Map Apple's device family to Fleet's hosts.platform field. + platform := "darwin" + switch d.DeviceFamily { + case "iPhone": + platform = "ios" + case "iPad": + platform = "ipados" + } + args = append(args, d.SerialNumber, d.Model, platform) } return stmt, args @@ -1590,6 +1642,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( SELECT ds.profile_uuid as profile_uuid, ds.host_uuid as host_uuid, + ds.host_platform as host_platform, ds.profile_identifier as profile_identifier, ds.profile_name as profile_name, ds.checksum as checksum @@ -1619,6 +1672,9 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB( return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute") } + // Exclude macOS only profiles from iPhones/iPads. + wantedProfiles = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(wantedProfiles) + toRemoveStmt := fmt.Sprintf(` SELECT hmap.profile_uuid as profile_uuid, @@ -1817,6 +1873,7 @@ func generateDesiredStateQuery(entityType string) string { SELECT mae.%[1]s_uuid, h.uuid as host_uuid, + h.platform as host_platform, mae.identifier as %[1]s_identifier, mae.name as %[1]s_name, mae.checksum as checksum, @@ -1829,7 +1886,7 @@ func generateDesiredStateQuery(entityType string) string { JOIN nano_enrollments ne ON ne.device_id = h.uuid WHERE - h.platform = 'darwin' AND + (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND ne.enabled = 1 AND ne.type = 'Device' AND NOT EXISTS ( @@ -1845,6 +1902,7 @@ func generateDesiredStateQuery(entityType string) string { SELECT mae.%[1]s_uuid, h.uuid as host_uuid, + h.platform as host_platform, mae.identifier as %[1]s_identifier, mae.name as %[1]s_name, mae.checksum as checksum, @@ -1861,12 +1919,12 @@ func generateDesiredStateQuery(entityType string) string { LEFT OUTER JOIN label_membership lm ON lm.label_id = mel.label_id AND lm.host_id = h.id WHERE - h.platform = 'darwin' AND + (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND ne.enabled = 1 AND ne.type = 'Device' AND ( %[3]s ) GROUP BY - mae.%[1]s_uuid, h.uuid, mae.identifier, mae.name, mae.checksum + mae.%[1]s_uuid, h.uuid, h.platform, mae.identifier, mae.name, mae.checksum HAVING count_%[1]s_labels > 0 AND count_host_labels = count_%[1]s_labels @@ -1978,6 +2036,7 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee SELECT ds.profile_uuid, ds.host_uuid, + ds.host_platform, ds.profile_identifier, ds.profile_name, ds.checksum @@ -2466,7 +2525,8 @@ SELECT COUNT(id) as count FROM hosts h -GROUP BY status, platform, team_id HAVING platform = 'darwin' AND status IN (?, ?, ?, ?) AND %s` +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) @@ -3415,8 +3475,8 @@ func (ds *Datastore) MDMResetEnrollment(ctx context.Context, hostUUID string) er return ctxerr.Wrap(ctx, err, "getting host info from UUID") } - if host.Platform != "darwin" && host.Platform != "windows" { - return ctxerr.Errorf(ctx, "unsupported host platform: %s", host.Platform) + if !fleet.MDMSupported(host.Platform) { + return ctxerr.Errorf(ctx, "unsupported host platform: %q", host.Platform) } // Deleting profiles from this table will cause all profiles to @@ -4116,3 +4176,20 @@ VALUES return nil } + +// ListIOSAndIPadOSToRefetch returns the UUIDs of iPhones/iPads that should be refetched +// (their details haven't been updated in the given `interval`). +func (ds *Datastore) ListIOSAndIPadOSToRefetch(ctx context.Context, interval time.Duration) (uuids []string, err error) { + var deviceUUIDs []string + hostsStmt := fmt.Sprintf(` +SELECT h.uuid FROM hosts h +JOIN host_mdm hmdm ON hmdm.host_id = h.id +WHERE (h.platform = 'ios' OR h.platform = 'ipados') +AND hmdm.enrolled +AND TIMESTAMPDIFF(SECOND, h.detail_updated_at, NOW()) > ?;`) + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &deviceUUIDs, hostsStmt, interval.Seconds()); err != nil { + return nil, err + } + + return deviceUUIDs, nil +} diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 43ec3a7812..d1547644f7 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -1,6 +1,7 @@ package mysql import ( + "bytes" "context" "crypto/md5" // nolint:gosec // used only to hash for efficient comparisons "crypto/sha256" @@ -8,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "testing" "time" @@ -74,6 +76,10 @@ func TestMDMApple(t *testing.T) { {"MDMAppleSetPendingDeclarationsAs", testMDMAppleSetPendingDeclarationsAs}, {"SetOrUpdateMDMAppleDeclaration", testSetOrUpdateMDMAppleDDMDeclaration}, {"DEPAssignmentUpdates", testMDMAppleDEPAssignmentUpdates}, + {"ListIOSAndIPadOSToRefetch", testListIOSAndIPadOSToRefetch}, + {"MDMAppleUpsertHostIOSiPadOS", testMDMAppleUpsertHostIOSIPadOS}, + {"IngestMDMAppleDevicesFromDEPSyncIOSIPadOS", testIngestMDMAppleDevicesFromDEPSyncIOSIPadOS}, + {"MDMAppleProfilesOnIOSIPadOS", testMDMAppleProfilesOnIOSIPadOS}, } for _, c := range cases { @@ -787,6 +793,7 @@ func testIngestMDMNonDarwinHostAlreadyExistsInFleet(t *testing.T, ds *Datastore) err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, + Platform: "darwin", }) require.NoError(t, err) @@ -904,6 +911,7 @@ func testUpdateHostTablesOnMDMUnenroll(t *testing.T, ds *Datastore) { err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ UUID: testUUID, HardwareSerial: testSerial, + Platform: "darwin", }) require.NoError(t, err) @@ -1393,9 +1401,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ - {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"}, + {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, }, profiles) // add another host, it belongs to a team @@ -1416,9 +1424,9 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ - {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"}, + {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, }, profiles) // assign profiles to team 1 @@ -1440,11 +1448,11 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ - {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2"}, - {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2"}, + {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"}, + {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"}, }, profiles) // add another global host @@ -1463,14 +1471,14 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ - {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2"}, - {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2"}, - {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3"}, - {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3"}, - {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3"}, + {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"}, + {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-2", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[0].ProfileUUID, ProfileIdentifier: globalPfs[0].Identifier, ProfileName: globalPfs[0].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[1].ProfileUUID, ProfileIdentifier: globalPfs[1].Identifier, ProfileName: globalPfs[1].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"}, + {ProfileUUID: globalPfs[2].ProfileUUID, ProfileIdentifier: globalPfs[2].Identifier, ProfileName: globalPfs[2].Name, HostUUID: "test-uuid-3", HostPlatform: "darwin"}, }, profiles) // cron runs and updates the status @@ -1597,8 +1605,8 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { profiles, err = ds.ListMDMAppleProfilesToInstall(ctx) require.NoError(t, err) matchProfiles([]*fleet.MDMAppleProfilePayload{ - {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1"}, - {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1"}, + {ProfileUUID: teamPfs[0].ProfileUUID, ProfileIdentifier: teamPfs[0].Identifier, ProfileName: teamPfs[0].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, + {ProfileUUID: teamPfs[1].ProfileUUID, ProfileIdentifier: teamPfs[1].Identifier, ProfileName: teamPfs[1].Name, HostUUID: "test-uuid-1", HostPlatform: "darwin"}, }, profiles) // profiles to be removed includes host1's old profiles @@ -5497,3 +5505,270 @@ func createRawAppleCmd(reqType, cmdUUID string) string { `, reqType, cmdUUID) } + +func testListIOSAndIPadOSToRefetch(t *testing.T, ds *Datastore) { + ctx := context.Background() + + refetchInterval := 1 * time.Hour + hostCount := 0 + newHost := func(platform string) *fleet.Host { + h, err := ds.NewHost(ctx, &fleet.Host{ + Hostname: fmt.Sprintf("foobar%d", hostCount), + OsqueryHostID: ptr.String(fmt.Sprintf("foobar-%d", hostCount)), + NodeKey: ptr.String(fmt.Sprintf("foobar-%d", hostCount)), + UUID: fmt.Sprintf("foobar-%d", hostCount), + Platform: platform, + HardwareSerial: fmt.Sprintf("foobar-%d", hostCount), + }) + require.NoError(t, err) + hostCount++ + return h + } + + // Test with no hosts. + uuids, err := ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) + require.NoError(t, err) + require.Empty(t, uuids) + + // Create a placeholder macOS host. + _ = newHost("darwin") + + // Mock results incoming from depsync.Syncer + depDevices := []godep.Device{ + {SerialNumber: "iOS0_SERIAL", DeviceFamily: "iPhone", OpType: "added"}, + {SerialNumber: "iPadOS0_SERIAL", DeviceFamily: "iPad", OpType: "added"}, + } + n, _, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices) + require.NoError(t, err) + require.Equal(t, int64(2), n) + + // Hosts are not enrolled yet (e.g. DEP enrolled) + uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) + require.NoError(t, err) + require.Empty(t, uuids) + + // Now simulate the initial MDM checkin of the devices. + err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: "iOS0_UUID", + HardwareSerial: "iOS0_SERIAL", + HardwareModel: "iPhone14,6", + Platform: "ios", + OsqueryHostID: ptr.String("iOS0_OSQUERY_HOST_ID"), + }) + require.NoError(t, err) + iOS0, err := ds.HostByIdentifier(ctx, "iOS0_SERIAL") + require.NoError(t, err) + nanoEnroll(t, ds, iOS0, false) + err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: "iPadOS0_UUID", + HardwareSerial: "iPadOS0_SERIAL", + HardwareModel: "iPad13,18", + Platform: "ipados", + OsqueryHostID: ptr.String("iPadOS0_OSQUERY_HOST_ID"), + }) + require.NoError(t, err) + iPadOS0, err := ds.HostByIdentifier(ctx, "iPadOS0_SERIAL") + require.NoError(t, err) + nanoEnroll(t, ds, iPadOS0, false) + + // Test with hosts but empty state in nanomdm command tables. + uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) + require.NoError(t, err) + require.Len(t, uuids, 2) + sort.Slice(uuids, func(i, j int) bool { + return uuids[i] < uuids[j] + }) + require.Equal(t, uuids, []string{"iOS0_UUID", "iPadOS0_UUID"}) + + // Set iOS detail_updated_at as 30 minutes in the past. + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 30 MINUTE) WHERE id = ?`, iOS0.ID) + return err + }) + + // iOS device should not be returned because it was refetched recently + uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) + require.NoError(t, err) + require.Len(t, uuids, 1) + require.Equal(t, uuids[0], "iPadOS0_UUID") + + // Set iPadOS detail_updated_at as 30 minutes in the past. + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 30 MINUTE) WHERE id = ?`, iPadOS0.ID) + return err + }) + + // Both devices are up-to-date thus none should be returned. + uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) + require.NoError(t, err) + require.Empty(t, uuids) + + // Set iOS detail_updated_at as 2 hours in the past. + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, iOS0.ID) + return err + }) + + // iOS device be returned because it is out of date. + uuids, err = ds.ListIOSAndIPadOSToRefetch(ctx, refetchInterval) + require.NoError(t, err) + require.Len(t, uuids, 1) + require.Equal(t, uuids[0], "iOS0_UUID") +} + +func testMDMAppleUpsertHostIOSIPadOS(t *testing.T, ds *Datastore) { + ctx := context.Background() + createBuiltinLabels(t, ds) + + for i, platform := range []string{"ios", "ipados"} { + // Upsert first to test insertMDMAppleHostDB. + err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: fmt.Sprintf("test-uuid-%d", i), + HardwareSerial: fmt.Sprintf("test-serial-%d", i), + HardwareModel: "test-hw-model", + Platform: platform, + }) + require.NoError(t, err) + h, err := ds.HostByIdentifier(ctx, fmt.Sprintf("test-uuid-%d", i)) + require.NoError(t, err) + require.Equal(t, false, h.RefetchRequested) + require.Less(t, time.Since(h.LastEnrolledAt), 1*time.Hour) // check it's not in the date in the 2000 we use as "Never". + require.Equal(t, "test-hw-model", h.HardwareModel) + + labels, err := ds.ListLabelsForHost(ctx, h.ID) + require.NoError(t, err) + require.Len(t, labels, 1) + require.Equal(t, "All Hosts", labels[0].Name) + + // Insert again to test updateMDMAppleHostDB. + err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: fmt.Sprintf("test-uuid-%d", i), + HardwareSerial: fmt.Sprintf("test-serial-%d", i), + HardwareModel: "test-hw-model-2", + Platform: platform, + }) + require.NoError(t, err) + h, err = ds.HostByIdentifier(ctx, fmt.Sprintf("test-uuid-%d", i)) + require.NoError(t, err) + require.Equal(t, false, h.RefetchRequested) + require.Less(t, time.Since(h.LastEnrolledAt), 1*time.Hour) // check it's not in the date in the 2000 we use as "Never". + require.Equal(t, "test-hw-model-2", h.HardwareModel) + + labels, err = ds.ListLabelsForHost(ctx, h.ID) + require.NoError(t, err) + require.Len(t, labels, 1) + require.Equal(t, "All Hosts", labels[0].Name) + } + + err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: "test-uuid-2", + HardwareSerial: "test-serial-2", + HardwareModel: "test-hw-model", + Platform: "darwin", + }) + require.NoError(t, err) + h, err := ds.HostByIdentifier(ctx, "test-uuid-2") + require.NoError(t, err) + require.Equal(t, true, h.RefetchRequested) + require.Less(t, 1*time.Hour, time.Since(h.LastEnrolledAt)) // check it's in the date in the 2000 we use as "Never". + labels, err := ds.ListLabelsForHost(ctx, h.ID) + require.NoError(t, err) + require.Len(t, labels, 2) + require.Equal(t, "All Hosts", labels[0].Name) + require.Equal(t, "macOS", labels[1].Name) +} + +func testIngestMDMAppleDevicesFromDEPSyncIOSIPadOS(t *testing.T, ds *Datastore) { + ctx := context.Background() + + // Mock results incoming from depsync.Syncer + depDevices := []godep.Device{ + {SerialNumber: "iOS0_SERIAL", DeviceFamily: "iPhone", OpType: "added"}, + {SerialNumber: "iPadOS0_SERIAL", DeviceFamily: "iPad", OpType: "added"}, + } + + n, _, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, depDevices) + require.NoError(t, err) + require.Equal(t, int64(2), n) + + hosts, err := ds.ListHosts(ctx, fleet.TeamFilter{ + User: &fleet.User{ + GlobalRole: ptr.String(fleet.RoleAdmin), + }, + }, fleet.HostListOptions{}) + require.NoError(t, err) + require.Len(t, hosts, 2) + require.Equal(t, "ios", hosts[0].Platform) + require.Equal(t, false, hosts[0].RefetchRequested) + require.Equal(t, "ipados", hosts[1].Platform) + require.Equal(t, false, hosts[1].RefetchRequested) +} + +func testMDMAppleProfilesOnIOSIPadOS(t *testing.T, ds *Datastore) { + ctx := context.Background() + + // Add the Fleetd configuration and profile that are only for macOS. + params := mobileconfig.FleetdProfileOptions{ + EnrollSecret: t.Name(), + ServerURL: "https://example.com", + PayloadType: mobileconfig.FleetdConfigPayloadIdentifier, + PayloadName: fleetmdm.FleetdConfigProfileName, + } + var contents bytes.Buffer + err := mobileconfig.FleetdProfileTemplate.Execute(&contents, params) + require.NoError(t, err) + fleetdConfigProfile, err := fleet.NewMDMAppleConfigProfile(contents.Bytes(), nil) + require.NoError(t, err) + _, err = ds.NewMDMAppleConfigProfile(ctx, *fleetdConfigProfile) + require.NoError(t, err) + + // For the FileVault profile we re-use the FleetdProfileTemplate + // (because fileVaultProfileTemplate is not exported) + var contents2 bytes.Buffer + params.PayloadName = fleetmdm.FleetFileVaultProfileName + params.PayloadType = mobileconfig.FleetFileVaultPayloadIdentifier + err = mobileconfig.FleetdProfileTemplate.Execute(&contents2, params) + require.NoError(t, err) + fileVaultProfile, err := fleet.NewMDMAppleConfigProfile(contents2.Bytes(), nil) + require.NoError(t, err) + _, err = ds.NewMDMAppleConfigProfile(ctx, *fileVaultProfile) + require.NoError(t, err) + + err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: "iOS0_UUID", + HardwareSerial: "iOS0_SERIAL", + HardwareModel: "iPhone14,6", + Platform: "ios", + OsqueryHostID: ptr.String("iOS0_OSQUERY_HOST_ID"), + }) + require.NoError(t, err) + iOS0, err := ds.HostByIdentifier(ctx, "iOS0_UUID") + require.NoError(t, err) + nanoEnroll(t, ds, iOS0, false) + err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ + UUID: "iPadOS0_UUID", + HardwareSerial: "iPadOS0_SERIAL", + HardwareModel: "iPad13,18", + Platform: "ipados", + OsqueryHostID: ptr.String("iPadOS0_OSQUERY_HOST_ID"), + }) + require.NoError(t, err) + iPadOS0, err := ds.HostByIdentifier(ctx, "iPadOS0_UUID") + require.NoError(t, err) + nanoEnroll(t, ds, iPadOS0, false) + + someProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("a", "a", 0)) + require.NoError(t, err) + + err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil) + require.NoError(t, err) + + profiles, err := ds.GetHostMDMAppleProfiles(ctx, "iOS0_UUID") + require.NoError(t, err) + require.Len(t, profiles, 1) + require.Equal(t, someProfile.Name, profiles[0].Name) + profiles, err = ds.GetHostMDMAppleProfiles(ctx, "iPadOS0_UUID") + require.NoError(t, err) + require.Len(t, profiles, 1) + require.Equal(t, someProfile.Name, profiles[0].Name) +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index b9c64045cd..09248b1324 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1188,7 +1188,7 @@ func filterHostsByMDM(sql string, opt fleet.HostListOptions, params []interface{ } } if opt.MDMNameFilter != nil || opt.MDMIDFilter != nil || opt.MDMEnrollmentStatusFilter != "" { - sql += ` AND NOT COALESCE(hmdm.is_server, false) AND h.platform IN('darwin', 'windows')` + sql += ` AND NOT COALESCE(hmdm.is_server, false) AND h.platform IN ('darwin', 'windows', 'ios', 'ipados')` } return sql, params } @@ -1297,7 +1297,7 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis // or are servers. Similar logic could be applied to macOS hosts but is not included in this // current implementation. - sqlFmt := ` AND h.platform IN('windows', 'darwin')` + sqlFmt := ` AND h.platform IN('windows', 'darwin', 'ios', 'ipados')` if opt.TeamFilter == nil { // OS settings filter is not compatible with the "all teams" option so append the "no team" // filter here (note that filterHostsByTeam applies the "no team" filter if TeamFilter == 0) @@ -1306,7 +1306,7 @@ func (ds *Datastore) filterHostsByOSSettingsStatus(sql string, opt fleet.HostLis var whereMacOS, whereWindows string sqlFmt += ` AND ((h.platform = 'windows' AND (%s)) -OR (h.platform = 'darwin' AND (%s)))` +OR ((h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados') AND (%s)))` whereMacOS, paramsMacOS, err := subqueryOSSettingsStatusMac() if err != nil { @@ -1741,8 +1741,8 @@ func matchHostDuringEnrollment(ctx context.Context, q sqlx.QueryerContext, enrol if query.Len() > 0 { _, _ = query.WriteString(" UNION ") } - _, _ = query.WriteString(`(SELECT id, last_enrolled_at, 2 priority FROM hosts WHERE hardware_serial = ? AND platform = ? ORDER BY id LIMIT 1)`) - args = append(args, serial, "darwin") + _, _ = query.WriteString(`(SELECT id, last_enrolled_at, 2 priority FROM hosts WHERE hardware_serial = ? AND (platform = 'darwin' OR platform = 'ios' OR platform = 'ipados') ORDER BY id LIMIT 1)`) + args = append(args, serial) } if err := sqlx.SelectContext(ctx, q, &rows, query.String(), args...); err != nil { @@ -3814,7 +3814,8 @@ func (ds *Datastore) GetHostMDMCheckinInfo(ctx context.Context, hostUUID string) COALESCE(h.team_id, 0) as team_id, hda.host_id IS NOT NULL AND hda.deleted_at IS NULL as dep_assigned_to_fleet, h.node_key IS NOT NULL as osquery_enrolled, - ncaa.renew_command_uuid IS NOT NULL as scep_renewal_in_progress + ncaa.renew_command_uuid IS NOT NULL as scep_renewal_in_progress, + h.platform FROM hosts h LEFT JOIN diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index a829e8ee89..26f8dc41c8 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -7391,6 +7391,7 @@ func testHostsGetHostMDMCheckinInfo(t *testing.T, ds *Datastore) { PrimaryMac: "30-65-EC-6F-C4-58", HardwareSerial: "123456789", TeamID: &tm.ID, + Platform: "darwin", }) require.NoError(t, err) err = ds.SetOrUpdateMDMData(ctx, host.ID, false, true, "https://fleetdm.com", true, fleet.WellKnownMDMFleet, "") @@ -7403,6 +7404,7 @@ func testHostsGetHostMDMCheckinInfo(t *testing.T, ds *Datastore) { require.EqualValues(t, tm.ID, info.TeamID) require.False(t, info.DEPAssignedToFleet) require.True(t, info.OsqueryEnrolled) + require.Equal(t, "darwin", info.Platform) err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*host}) require.NoError(t, err) diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index aff32bdd18..c8c137fd43 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -425,7 +425,7 @@ FROM hosts h JOIN mdm_apple_configuration_profiles macp ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0) WHERE - macp.profile_uuid IN (?) AND h.platform = 'darwin'` + macp.profile_uuid IN (?) AND (h.platform = 'darwin' OR h.platform = 'ios' OR h.platform = 'ipados')` args = append(args, macProfUUIDs) case len(winProfUUIDs) > 0: @@ -454,12 +454,12 @@ WHERE } } - var macHosts []string + var appleHosts []string var winHosts []string for _, h := range hosts { switch h.Platform { - case "darwin": - macHosts = append(macHosts, h.UUID) + case "darwin", "ios", "ipados": + appleHosts = append(appleHosts, h.UUID) case "windows": winHosts = append(winHosts, h.UUID) default: @@ -471,7 +471,7 @@ WHERE } } - if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, macHosts); err != nil { + if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts); err != nil { return ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles") } @@ -537,7 +537,7 @@ WHERE var stmt string switch host.Platform { - case "darwin": + case "darwin", "ios", "ipados": stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier") case "windows": stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name") @@ -577,7 +577,7 @@ WHERE var stmt string switch host.Platform { - case "darwin": + case "darwin", "ios", "ipados": stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier") case "windows": stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name") @@ -630,7 +630,7 @@ WHERE var stmt string switch host.Platform { - case "darwin": + case "darwin", "ios", "ipados": stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier") case "windows": stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name") @@ -667,7 +667,7 @@ func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Conte } switch host.Platform { - case "darwin": + case "darwin", "ios", "ipados": return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID, host.ID) case "windows": return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID, host.ID) @@ -823,7 +823,7 @@ WHERE var stmt string switch host.Platform { - case "darwin": + case "darwin", "ios", "ipados": stmt = darwinStmt case "windows": stmt = windowsStmt @@ -860,7 +860,7 @@ WHERE var stmt string switch host.Platform { - case "darwin": + case "darwin", "ios", "ipados": stmt = darwinStmt case "windows": stmt = windowsStmt diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go index 4ecf682f15..775a0c4948 100644 --- a/server/datastore/mysql/scripts.go +++ b/server/datastore/mysql/scripts.go @@ -706,7 +706,7 @@ func (ds *Datastore) GetHostLockWipeStatus(ctx context.Context, host *fleet.Host } switch fleetPlatform { - case "darwin": + case "darwin", "ios", "ipados": if mdmActions.UnlockPIN != nil { status.UnlockPIN = *mdmActions.UnlockPIN } diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index ff4a041494..5691650265 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -255,7 +255,7 @@ type HostMDMAppleProfile struct { } // ToHostMDMProfile converts the HostMDMAppleProfile to a HostMDMProfile. -func (p HostMDMAppleProfile) ToHostMDMProfile() HostMDMProfile { +func (p HostMDMAppleProfile) ToHostMDMProfile(platform string) HostMDMProfile { return HostMDMProfile{ HostUUID: p.HostUUID, ProfileUUID: p.ProfileUUID, @@ -264,7 +264,7 @@ func (p HostMDMAppleProfile) ToHostMDMProfile() HostMDMProfile { Status: p.Status, OperationType: p.OperationType, Detail: p.Detail, - Platform: "darwin", + Platform: platform, } } @@ -292,6 +292,7 @@ type MDMAppleProfilePayload struct { ProfileIdentifier string `db:"profile_identifier"` ProfileName string `db:"profile_name"` HostUUID string `db:"host_uuid"` + HostPlatform string `db:"host_platform"` Checksum []byte `db:"checksum"` Status *MDMDeliveryStatus `db:"status" json:"status"` OperationType MDMOperationType `db:"operation_type"` diff --git a/server/fleet/cron_schedules.go b/server/fleet/cron_schedules.go index 6b16734fd4..f6d7173ebe 100644 --- a/server/fleet/cron_schedules.go +++ b/server/fleet/cron_schedules.go @@ -12,16 +12,17 @@ type CronScheduleName string // List of recognized cron schedule names. const ( - CronAppleMDMDEPProfileAssigner CronScheduleName = "apple_mdm_dep_profile_assigner" - CronCleanupsThenAggregation CronScheduleName = "cleanups_then_aggregation" - CronFrequentCleanups CronScheduleName = "frequent_cleanups" - CronUsageStatistics CronScheduleName = "usage_statistics" - CronVulnerabilities CronScheduleName = "vulnerabilities" - CronAutomations CronScheduleName = "automations" - CronWorkerIntegrations CronScheduleName = "integrations" - CronActivitiesStreaming CronScheduleName = "activities_streaming" - CronMDMAppleProfileManager CronScheduleName = "mdm_apple_profile_manager" - CronCalendar CronScheduleName = "calendar" + CronAppleMDMDEPProfileAssigner CronScheduleName = "apple_mdm_dep_profile_assigner" + CronCleanupsThenAggregation CronScheduleName = "cleanups_then_aggregation" + CronFrequentCleanups CronScheduleName = "frequent_cleanups" + CronUsageStatistics CronScheduleName = "usage_statistics" + CronVulnerabilities CronScheduleName = "vulnerabilities" + CronAutomations CronScheduleName = "automations" + CronWorkerIntegrations CronScheduleName = "integrations" + CronActivitiesStreaming CronScheduleName = "activities_streaming" + CronMDMAppleProfileManager CronScheduleName = "mdm_apple_profile_manager" + CronAppleMDMIPhoneIPadRefetcher CronScheduleName = "apple_mdm_iphone_ipad_refetcher" + CronCalendar CronScheduleName = "calendar" ) type CronSchedulesService interface { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index c32f9b8b92..0f8cf591e1 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -325,6 +325,10 @@ type Datastore interface { GetHostMDM(ctx context.Context, hostID uint) (*HostMDM, error) GetHostMDMCheckinInfo(ctx context.Context, hostUUID string) (*HostMDMCheckinInfo, error) + // ListIOSAndIPadOSToRefetch returns the UUIDs of iPhones/iPads that should be refetched (their details haven't been + // updated in the given `interval`). + ListIOSAndIPadOSToRefetch(ctx context.Context, refetchInterval time.Duration) (uuids []string, err error) + AggregatedMunkiVersion(ctx context.Context, teamID *uint) ([]AggregatedMunkiVersion, time.Time, error) AggregatedMunkiIssues(ctx context.Context, teamID *uint) ([]AggregatedMunkiIssue, time.Time, error) AggregatedMDMStatus(ctx context.Context, teamID *uint, platform string) (AggregatedMDMStatus, time.Time, error) diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 1c3bad6e6e..05b879786a 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -840,6 +840,11 @@ func (h *Host) FleetPlatform() string { return PlatformFromHost(h.Platform) } +// SupportsOsquery returns whether the device runs osquery. +func (h *Host) SupportsOsquery() bool { + return h.Platform != "ios" && h.Platform != "ipados" +} + // HostLinuxOSs are the possible linux values for Host.Platform. var HostLinuxOSs = []string{ "linux", "ubuntu", "debian", "rhel", "centos", "sles", "kali", "gentoo", "amzn", "pop", "arch", "linuxmint", "void", "nixos", "endeavouros", "manjaro", "opensuse-leap", "opensuse-tumbleweed", @@ -879,7 +884,9 @@ func PlatformFromHost(hostPlatform string) string { // TODO remove this once that customer migrates to Fleetd for Chrome hostPlatform == "CrOS", // Fleet now supports Chrome via fleetd - hostPlatform == "chrome": + hostPlatform == "chrome", + hostPlatform == "ios", + hostPlatform == "ipados": return hostPlatform default: return "" @@ -1238,13 +1245,15 @@ type EnrollHostLimiter interface { } type HostMDMCheckinInfo struct { - HardwareSerial string `json:"hardware_serial" db:"hardware_serial"` - InstalledFromDEP bool `json:"installed_from_dep" db:"installed_from_dep"` - DisplayName string `json:"display_name" db:"display_name"` - TeamID uint `json:"team_id" db:"team_id"` - DEPAssignedToFleet bool `json:"dep_assigned_to_fleet" db:"dep_assigned_to_fleet"` - OsqueryEnrolled bool `json:"osquery_enrolled" db:"osquery_enrolled"` + HardwareSerial string `json:"hardware_serial" db:"hardware_serial"` + InstalledFromDEP bool `json:"installed_from_dep" db:"installed_from_dep"` + DisplayName string `json:"display_name" db:"display_name"` + TeamID uint `json:"team_id" db:"team_id"` + DEPAssignedToFleet bool `json:"dep_assigned_to_fleet" db:"dep_assigned_to_fleet"` + OsqueryEnrolled bool `json:"osquery_enrolled" db:"osquery_enrolled"` + SCEPRenewalInProgress bool `json:"-" db:"scep_renewal_in_progress"` + Platform string `json:"-" db:"platform"` } type HostDiskEncryptionKey struct { diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 1d1f3c078c..c1ef630e7d 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -7,6 +7,8 @@ import ( "fmt" "net/url" "time" + + mdm_types "github.com/fleetdm/fleet/v4/server/mdm" ) const ( @@ -169,6 +171,8 @@ type CommandEnqueueResult struct { // FailedUUIDs is the list of host UUIDs that failed to receive the command. FailedUUIDs []string `json:"failed_uuids,omitempty"` // Platform is the platform of the hosts targeted by the command. + // Current possible values are "darwin" or "windows". + // Here "darwin" means "Apple" devices (iOS/iPadOS/macOS). Platform string `json:"platform"` } @@ -532,3 +536,42 @@ func MDMProfileSpecsMatch(a, b []MDMProfileSpec) bool { return len(pathLabelCounts) == 0 } + +// MDMPlatform returns "darwin" or "windows" as MDM platforms +// derived from a host's platform (hosts.platform field). +// +// Note that "darwin" as MDM platform means Apple (we keep it as "darwin" +// to keep backwards compatibility throughout the app). +func MDMPlatform(hostPlatform string) string { + switch hostPlatform { + case "darwin", "ios", "ipados": + return "darwin" + case "windows": + return "windows" + } + return "" +} + +// MDMSupported returns whether MDM is supported for a given host platform. +func MDMSupported(hostPlatform string) bool { + return MDMPlatform(hostPlatform) != "" +} + +// FilterMacOSOnlyProfilesFromIOSIPadOS will filter out profiles that are only for macOS devices +// if the profile target's platform is ios/ipados. +func FilterMacOSOnlyProfilesFromIOSIPadOS(profiles []*MDMAppleProfilePayload) []*MDMAppleProfilePayload { + i := 0 + for _, profilePayload := range profiles { + if (profilePayload.HostPlatform == "ios" || profilePayload.HostPlatform == "ipados") && + (profilePayload.ProfileName == mdm_types.FleetdConfigProfileName || + profilePayload.ProfileName == mdm_types.FleetFileVaultProfileName) { + continue + } + profiles[i] = profilePayload + i++ + } + return profiles[:i] +} + +// RefetchCommandUUIDPrefix is the prefix used for MDM commands used to refetch information from iOS/iPadOS devices. +const RefetchCommandUUIDPrefix = "REFETCH-" diff --git a/server/fleet/mdm_test.go b/server/fleet/mdm_test.go index 184649514b..e35099f551 100644 --- a/server/fleet/mdm_test.go +++ b/server/fleet/mdm_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/fleetdm/fleet/v4/server/fleet" + fleetmdm "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" @@ -324,3 +325,146 @@ func TestMDMProfileSpecsMatch(t *testing.T) { }) } } + +func TestFilterMacOSOnlyProfilesFromIOSIPadOS(t *testing.T) { + for _, tc := range []struct { + profiles []*fleet.MDMAppleProfilePayload + expectedProfiles []*fleet.MDMAppleProfilePayload + }{ + { + profiles: []*fleet.MDMAppleProfilePayload{}, + expectedProfiles: []*fleet.MDMAppleProfilePayload{}, + }, + { + profiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "darwin", + }, + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ios", + }, + { + ProfileName: "SomeProfile", + HostPlatform: "darwin", + }, + { + ProfileName: fleetmdm.FleetdConfigProfileName, + HostPlatform: "ipados", + }, + { + ProfileName: fleetmdm.FleetdConfigProfileName, + HostPlatform: "ios", + }, + { + ProfileName: "SomeProfile2", + HostPlatform: "ios", + }, + { + ProfileName: "SomeProfile3", + HostPlatform: "ipados", + }, + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ipados", + }, + }, + expectedProfiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "darwin", + }, + { + ProfileName: "SomeProfile", + HostPlatform: "darwin", + }, + { + ProfileName: "SomeProfile2", + HostPlatform: "ios", + }, + { + ProfileName: "SomeProfile3", + HostPlatform: "ipados", + }, + }, + }, + { + profiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "darwin", + }, + { + ProfileName: "SomeProfile", + HostPlatform: "ios", + }, + }, + expectedProfiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "darwin", + }, + { + ProfileName: "SomeProfile", + HostPlatform: "ios", + }, + }, + }, + { + profiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ios", + }, + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ipados", + }, + }, + expectedProfiles: []*fleet.MDMAppleProfilePayload{}, + }, + { + profiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ios", + }, + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ipados", + }, + }, + expectedProfiles: []*fleet.MDMAppleProfilePayload{}, + }, + { + profiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ios", + }, + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "darwin", + }, + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "ipados", + }, + }, + expectedProfiles: []*fleet.MDMAppleProfilePayload{ + { + ProfileName: fleetmdm.FleetFileVaultProfileName, + HostPlatform: "darwin", + }, + }, + }, + } { + actualProfiles := fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(tc.profiles) + require.Equal(t, len(actualProfiles), len(tc.expectedProfiles)) + for i := 0; i < len(actualProfiles); i++ { + require.Equal(t, *actualProfiles[i], *tc.expectedProfiles[i]) + } + + } +} diff --git a/server/mdm/lifecycle/lifecycle.go b/server/mdm/lifecycle/lifecycle.go index 2fd7ece7f4..5587e71f4b 100644 --- a/server/mdm/lifecycle/lifecycle.go +++ b/server/mdm/lifecycle/lifecycle.go @@ -58,9 +58,9 @@ func New(ds fleet.Datastore, logger kitlog.Logger) *HostLifecycle { // Do executes the provided HostAction based on the platform requested func (t *HostLifecycle) Do(ctx context.Context, opts HostOptions) error { switch opts.Platform { - case "darwin": + case "darwin", "ios", "ipados": err := t.doDarwin(ctx, opts) - return ctxerr.Wrapf(ctx, err, "running darwin lifecycle action %s", opts.Action) + return ctxerr.Wrapf(ctx, err, "running apple lifecycle action %s", opts.Action) case "windows": err := t.doWindows(ctx, opts) return ctxerr.Wrapf(ctx, err, "running windows lifecycle action %s", opts.Action) @@ -124,6 +124,7 @@ func (t *HostLifecycle) resetDarwin(ctx context.Context, opts HostOptions) error UUID: opts.UUID, HardwareSerial: opts.HardwareSerial, HardwareModel: opts.HardwareModel, + Platform: opts.Platform, } if err := t.ds.MDMAppleUpsertHost(ctx, host); err != nil { return ctxerr.Wrap(ctx, err, "upserting mdm host") @@ -170,6 +171,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro t.logger, worker.AppleMDMPostDEPEnrollmentTask, opts.UUID, + opts.Platform, tmID, opts.EnrollReference, ) @@ -184,6 +186,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro t.logger, worker.AppleMDMPostManualEnrollmentTask, opts.UUID, + opts.Platform, tmID, opts.EnrollReference, ); err != nil { diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 8439070714..041aeb960d 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -38,7 +38,7 @@ func prefixMatches(val []byte, prefix string) bool { // GetRawProfilePlatform identifies the platform type of a profile bytes by // examining its initial content: // -// - Returns "darwin" if the profile starts with " + + + + CommandUUID + REFETCH-fd23f8ac-1c50-41c7-a5bb-f13633c9ea97 + QueryResponses + + AvailableDeviceCapacity + 51.260395520000003 + DeviceCapacity + 64 + DeviceName + Work iPad + OSVersion + 17.5.1 + ProductName + iPad13,18 + WiFiMAC + ff:ff:ff:ff:ff:ff + + Status + Acknowledged + UDID + FFFFFFFF-FFFFFFFFFFFFFFFF + +`), + }, + ) + require.NoError(t, err) + + require.True(t, ds.UpdateHostFuncInvoked) + require.True(t, ds.HostByIdentifierFuncInvoked) + require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked) +} diff --git a/server/service/client_mdm.go b/server/service/client_mdm.go index 4eb82d0968..a61ef3fd6e 100644 --- a/server/service/client_mdm.go +++ b/server/service/client_mdm.go @@ -287,7 +287,7 @@ func (c *Client) RunMDMCommand(hostUUIDs []string, rawCmd []byte, forPlatform st case "windows": prepareFn = c.prepareWindowsMDMCommand default: - return nil, fmt.Errorf("Invalid platform %q. You can only run MDM commands on Windows or macOS hosts.", forPlatform) + return nil, fmt.Errorf("Invalid platform %q. You can only run MDM commands on Windows or Apple hosts.", forPlatform) } rawCmd, err := prepareFn(rawCmd) diff --git a/server/service/hosts.go b/server/service/hosts.go index 62dbab643c..dacd56ddc2 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -293,7 +293,7 @@ func (svc *Service) DeleteHosts(ctx context.Context, ids []uint, filter *map[str mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger) for _, host := range hosts { - if host.Platform == "darwin" || host.Platform == "windows" { + if fleet.MDMSupported(host.Platform) { err := mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{ Action: mdmlifecycle.HostActionDelete, Host: host, @@ -749,7 +749,7 @@ func (svc *Service) DeleteHost(ctx context.Context, id uint) error { return ctxerr.Wrap(ctx, err, "delete host") } - if host.Platform == "windows" || host.Platform == "darwin" { + if fleet.MDMSupported(host.Platform) { mdmLifecycle := mdmlifecycle.New(svc.ds, svc.logger) err = mdmLifecycle.Do(ctx, mdmlifecycle.HostOptions{ Action: mdmlifecycle.HostActionDelete, @@ -1104,7 +1104,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f profiles = append(profiles, p.ToHostMDMProfile()) } - case "darwin": + case "darwin", "ios", "ipados": if ac.MDM.EnabledAndConfigured { profs, err := svc.ds.GetHostMDMAppleProfiles(ctx, host.UUID) if err != nil { @@ -1120,7 +1120,7 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f p.Status = host.MDM.ProfileStatusFromDiskEncryptionState(p.Status) } p.Detail = fleet.HostMDMProfileDetail(p.Detail).Message() - profiles = append(profiles, p.ToHostMDMProfile()) + profiles = append(profiles, p.ToHostMDMProfile(host.Platform)) } } } diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 7be3faed47..5347ca84bd 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6951,7 +6951,6 @@ func (s *integrationTestSuite) TestListSoftwareAndSoftwareDetails() { assertVersionsResp(versResp, nil, time.Time{}, "", expectedVulnVersionsCount) // /software/versions filtered by name, version, cve (`/software` is deprecated) - // TODO(jacob) use `assertVersionsResp` versionsResp := listSoftwareVersionsResponse{} s.DoJSON("GET", "/api/latest/fleet/software/versions", nil, http.StatusOK, &versionsResp, "query", sws[0].Name) assertVersionsResp(versionsResp, []fleet.Software{sws[0]}, hostsCountTs, "", 1, 1) diff --git a/server/service/mdm.go b/server/service/mdm.go index adbd2c0c13..dc81199de4 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -496,8 +496,8 @@ func (svc *Service) RunMDMCommand(ctx context.Context, rawBase64Cmd string, host for platform := range platforms { commandPlatform = platform } - if commandPlatform != "windows" && commandPlatform != "darwin" { - err := fleet.NewInvalidArgumentError("host_uuids", "Invalid platform. You can only run MDM commands on Windows or macOS hosts.") + if !fleet.MDMSupported(commandPlatform) { + err := fleet.NewInvalidArgumentError("host_uuids", "Invalid platform. You can only run MDM commands on Windows or Apple hosts.") return nil, ctxerr.Wrap(ctx, err, "check host platform") } @@ -2038,7 +2038,7 @@ func (svc *Service) ResendHostMDMProfile(ctx context.Context, hostID uint, profi if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest), "check apple mdm enabled") } - if host.Platform != "darwin" { + if host.Platform != "darwin" && host.Platform != "ios" && host.Platform != "ipados" { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", "Profile is not compatible with host platform."), "check host platform") } prof, err := svc.ds.GetMDMAppleConfigProfile(ctx, profileUUID) @@ -2052,7 +2052,7 @@ func (svc *Service) ResendHostMDMProfile(ctx context.Context, hostID uint, profi if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest), "check apple mdm enabled") } - if host.Platform != "darwin" { + if host.Platform != "darwin" && host.Platform != "ios" && host.Platform != "ipados" { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("HostMDMProfile", "Profile is not compatible with host platform."), "check host platform") } decl, err := svc.ds.GetMDMAppleDeclaration(ctx, profileUUID) diff --git a/server/worker/apple_mdm.go b/server/worker/apple_mdm.go index 0537af05e8..35555a9ea5 100644 --- a/server/worker/apple_mdm.go +++ b/server/worker/apple_mdm.go @@ -50,6 +50,7 @@ type appleMDMArgs struct { TeamID *uint `json:"team_id,omitempty"` EnrollReference string `json:"enroll_reference,omitempty"` EnrollmentCommands []string `json:"enrollment_commands,omitempty"` + Platform string `json:"platform,omitempty"` } // Run executes the apple_mdm job. @@ -83,9 +84,17 @@ func (a *AppleMDM) Run(ctx context.Context, argsJSON json.RawMessage) error { } } +func isMacOS(platform string) bool { + // For backwards compatibility, we assume empty platform in job arguments is macOS. + return platform == "" || + platform == "darwin" +} + func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArgs) error { - if _, err := a.installFleetd(ctx, args.HostUUID); err != nil { - return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") + if isMacOS(args.Platform) { + if _, err := a.installFleetd(ctx, args.HostUUID); err != nil { + return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") + } } return nil @@ -94,18 +103,20 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error { var awaitCmdUUIDs []string - fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID) - if err != nil { - return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") - } - awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID) + if isMacOS(args.Platform) { + fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID) + if err != nil { + return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") + } + awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID) - bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID) - if err != nil { - return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") - } - if bootstrapCmdUUID != "" { - awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID) + bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID) + if err != nil { + return ctxerr.Wrap(ctx, err, "installing post-enrollment packages") + } + if bootstrapCmdUUID != "" { + awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID) + } } if ref := args.EnrollReference; ref != "" { @@ -166,7 +177,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) // be final and same for MDM profiles of that host; it means the DEP // enrollment process is done and the device can be released. if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask, - args.HostUUID, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil { + args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil { return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job") } } @@ -323,6 +334,7 @@ func QueueAppleMDMJob( logger kitlog.Logger, task AppleMDMTask, hostUUID string, + platform string, teamID *uint, enrollReference string, enrollmentCommandUUIDs ...string, @@ -331,13 +343,14 @@ func QueueAppleMDMJob( "enabled", "true", appleMDMJobName, task, "host_uuid", hostUUID, + "platform", platform, "with_enroll_reference", enrollReference != "", } if teamID != nil { attrs = append(attrs, "team_id", *teamID) } if len(enrollmentCommandUUIDs) > 0 { - attrs = append(attrs, "enrollment_commands", enrollmentCommandUUIDs) + attrs = append(attrs, "enrollment_commands", fmt.Sprintf("%v", enrollmentCommandUUIDs)) } level.Info(logger).Log(attrs...) @@ -347,6 +360,7 @@ func QueueAppleMDMJob( TeamID: teamID, EnrollReference: enrollReference, EnrollmentCommands: enrollmentCommandUUIDs, + Platform: platform, } // the release device task is always added with a delay diff --git a/server/worker/apple_mdm_test.go b/server/worker/apple_mdm_test.go index fd42b97027..f1809be57a 100644 --- a/server/worker/apple_mdm_test.go +++ b/server/worker/apple_mdm_test.go @@ -130,7 +130,7 @@ func TestAppleMDM(t *testing.T) { // create a host and enqueue the job h := createEnrolledHost(t, 1, nil, true) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") require.NoError(t, err) // run the worker, should mark the job as done @@ -159,7 +159,7 @@ func TestAppleMDM(t *testing.T) { // create a host and enqueue the job h := createEnrolledHost(t, 1, nil, true) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "") require.NoError(t, err) // run the worker, should mark the job as failed @@ -190,7 +190,8 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "") + // use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support. + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "") require.NoError(t, err) // run the worker, should succeed @@ -227,7 +228,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") require.NoError(t, err) // run the worker, should succeed @@ -268,7 +269,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "") require.NoError(t, err) // run the worker, should succeed @@ -319,7 +320,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "") require.NoError(t, err) // run the worker, should succeed @@ -371,7 +372,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, "") + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "") require.NoError(t, err) // run the worker, should succeed @@ -408,7 +409,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, "abcd") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd") require.NoError(t, err) // run the worker, should succeed @@ -450,7 +451,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, nil, idpAcc.UUID) + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID) require.NoError(t, err) // run the worker, should succeed @@ -505,7 +506,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, &tm.ID, idpAcc.UUID) + err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID) require.NoError(t, err) // run the worker, should succeed @@ -541,7 +542,7 @@ func TestAppleMDM(t *testing.T) { w := NewWorker(ds, nopLog) w.Register(mdmWorker) - err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, nil, "") + err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "") require.NoError(t, err) // run the worker, should succeed