From 28ca463d13a6585b27140b9c2ce436ce25f93568 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Mon, 8 Jul 2024 18:05:29 -0300 Subject: [PATCH] iOS/iPadOS as platforms/labels (#20126) #19963 - [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) - [X] Added/updated tests - [X] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [X] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [X] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [X] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [X] Manual QA for all new/changed functionality --- # API changes for dashboard UI changes ## Main dashboard page `GET /api/latest/fleet/host_summary?low_disk_space=32` (see `ios`/`ipados` platforms and `iOS`/`iPadOS` labels) ```json { "totals_hosts_count": 9, "online_count": 0, "offline_count": 9, "mia_count": 0, "missing_30_days_count": 0, "new_count": 0, "all_linux_count": 2, "low_disk_space_count": 3, "builtin_labels": [ { "id": 1, "name": "macOS 14+ (Sonoma+)", "description": "macOS hosts with version 14 and above", "label_type": "builtin" }, { "id": 7, "name": "All Hosts", "description": "All hosts which have enrolled in Fleet", "label_type": "builtin" }, { "id": 8, "name": "macOS", "description": "All macOS hosts", "label_type": "builtin" }, { "id": 9, "name": "Ubuntu Linux", "description": "All Ubuntu hosts", "label_type": "builtin" }, { "id": 10, "name": "CentOS Linux", "description": "All CentOS hosts", "label_type": "builtin" }, { "id": 11, "name": "MS Windows", "description": "All Windows hosts", "label_type": "builtin" }, { "id": 12, "name": "Red Hat Linux", "description": "All Red Hat Enterprise Linux hosts", "label_type": "builtin" }, { "id": 13, "name": "All Linux", "description": "All Linux distributions", "label_type": "builtin" }, { "id": 14, "name": "chrome", "description": "All Chrome hosts", "label_type": "builtin" }, { "id": 15, "name": "iOS", "description": "All iOS hosts", "label_type": "builtin" }, { "id": 16, "name": "iPadOS", "description": "All iPadOS hosts", "label_type": "builtin" } ], "platforms": [ { "platform": "darwin", "hosts_count": 3 }, { "platform": "ios", "hosts_count": 1 }, { "platform": "ipados", "hosts_count": 1 }, { "platform": "rhel", "hosts_count": 1 }, { "platform": "ubuntu", "hosts_count": 1 }, { "platform": "windows", "hosts_count": 2 } ] } ``` ## After selecting a platform `GET /api/latest/fleet/host_summary?platform=ios&low_disk_space=100` (similar with `ipados`) ```json { "totals_hosts_count": 1, "online_count": 0, "offline_count": 1, "mia_count": 0, "missing_30_days_count": 0, "new_count": 0, "all_linux_count": 0, "low_disk_space_count": 1, "builtin_labels": [ { "id": 1, "name": "macOS 14+ (Sonoma+)", "description": "macOS hosts with version 14 and above", "label_type": "builtin" }, { "id": 7, "name": "All Hosts", "description": "All hosts which have enrolled in Fleet", "label_type": "builtin" }, { "id": 8, "name": "macOS", "description": "All macOS hosts", "label_type": "builtin" }, { "id": 9, "name": "Ubuntu Linux", "description": "All Ubuntu hosts", "label_type": "builtin" }, { "id": 10, "name": "CentOS Linux", "description": "All CentOS hosts", "label_type": "builtin" }, { "id": 11, "name": "MS Windows", "description": "All Windows hosts", "label_type": "builtin" }, { "id": 12, "name": "Red Hat Linux", "description": "All Red Hat Enterprise Linux hosts", "label_type": "builtin" }, { "id": 13, "name": "All Linux", "description": "All Linux distributions", "label_type": "builtin" }, { "id": 14, "name": "chrome", "description": "All Chrome hosts", "label_type": "builtin" }, { "id": 15, "name": "iOS", "description": "All iOS hosts", "label_type": "builtin" }, { "id": 16, "name": "iPadOS", "description": "All iPadOS hosts", "label_type": "builtin" } ], "platforms": [ { "platform": "ios", "hosts_count": 1 } ] } ``` ### To populate list of MDM solutions of a selected platform `GET /api/latest/fleet/hosts/summary/mdm\?platform=ios` (similar with `ipados`) ```json { "counts_updated_at": "2024-06-27T21:56:45Z", "mobile_device_management_enrollment_status": { "enrolled_manual_hosts_count": 0, "enrolled_automated_hosts_count": 1, "pending_hosts_count": 0, "unenrolled_hosts_count": 0, "hosts_count": 1 }, "mobile_device_management_solution": [ { "id": 1, "name": "Fleet", "server_url": "https://lucas-fleet.ngrok.app/mdm/apple/mdm", "hosts_count": 1 } ] } ``` ### To populate OS versions of a selected platform `GET /api/latest/fleet/os_versions?platform=ipados` (similar with `ios`) ```json { "meta": { "has_next_results": false, "has_previous_results": false }, "count": 1, "counts_updated_at": "2024-06-27T21:36:12Z", "os_versions": [ { "os_version_id": 7, "hosts_count": 1, "name": "iPadOS 17.5.1", "name_only": "iPadOS", "version": "17.5.1", "platform": "ipados", "vulnerabilities": [] } ] } ``` ## Filtering hosts by the two new `iOS`/`iPadOS` labels Works the same as with other labels. --- changes/19963-ios-ipados-as-platforms | 3 + docker-compose.yml | 29 +-- server/datastore/mysql/apple_mdm.go | 67 +++++-- server/datastore/mysql/apple_mdm_test.go | 184 +++++++++++++++++- server/datastore/mysql/hosts.go | 8 +- server/datastore/mysql/hosts_test.go | 88 ++++++++- server/datastore/mysql/mdm.go | 1 - ...7134036_CreateIOSAndIPADOSBuiltinLabels.go | 114 +++++++++++ ...36_CreateIOSAndIPADOSBuiltinLabels_test.go | 100 ++++++++++ server/datastore/mysql/schema.sql | 8 +- server/fleet/labels.go | 4 + server/mdm/apple/profile_verifier.go | 5 +- server/service/apple_mdm.go | 20 +- server/service/apple_mdm_test.go | 8 + server/test/new_objects.go | 14 ++ tools/mdm/apple/applebmapi/main.go | 7 + 16 files changed, 605 insertions(+), 55 deletions(-) create mode 100644 changes/19963-ios-ipados-as-platforms create mode 100644 server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels.go create mode 100644 server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels_test.go diff --git a/changes/19963-ios-ipados-as-platforms b/changes/19963-ios-ipados-as-platforms new file mode 100644 index 0000000000..a06c8c321a --- /dev/null +++ b/changes/19963-ios-ipados-as-platforms @@ -0,0 +1,3 @@ +- Added iOS/iPadOS builtin manual labels. IMPORTANT: Before migrating to this version, make sure to delete any labels with name "iOS" or "iPadOS". +- Added aggregation of iOS/iPadOS OS versions. +- Added change to custom profiles for iOS/iPadOS to go from 'pending' straight to 'verified' (skip 'verifying'). diff --git a/docker-compose.yml b/docker-compose.yml index bb7e3066a3..5b8b7b48ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,12 +5,11 @@ services: # officially supported). # To run in macOS M1, set FLEET_MYSQL_IMAGE=arm64v8/mysql:oracle FLEET_MYSQL_PLATFORM=linux/arm64/v8 mysql: - image: ${FLEET_MYSQL_IMAGE:-mysql:5.7} + image: ${FLEET_MYSQL_IMAGE:-mysql:5.7.21} platform: ${FLEET_MYSQL_PLATFORM:-linux/x86_64} volumes: - mysql-persistent-volume:/tmp - command: - [ + command: [ "mysqld", "--datadir=/tmp/mysqldata", # These 3 keys run MySQL with GTID consistency enforced to avoid issues with production deployments that use it. @@ -18,7 +17,7 @@ services: "--log-bin=bin.log", "--server-id=master-01", # Required for storage of Apple MDM bootstrap packages. - "--max_allowed_packet=536870912" + "--max_allowed_packet=536870912", ] environment: &mysql-default-environment MYSQL_ROOT_PASSWORD: toor @@ -31,11 +30,10 @@ services: - "3306:3306" mysql_test: - image: ${FLEET_MYSQL_IMAGE:-mysql:5.7} + image: ${FLEET_MYSQL_IMAGE:-mysql:5.7.21} platform: ${FLEET_MYSQL_PLATFORM:-linux/x86_64} # innodb-file-per-table=OFF gives ~20% speedup for test runs. - command: - [ + command: [ "mysqld", "--datadir=/tmpfs", "--slow_query_log=1", @@ -48,7 +46,7 @@ services: "--log-bin=bin.log", "--server-id=1", # Required for storage of Apple MDM bootstrap packages. - "--max_allowed_packet=536870912" + "--max_allowed_packet=536870912", ] environment: *mysql-default-environment ports: @@ -58,11 +56,10 @@ services: - /tmpfs mysql_replica_test: - image: ${FLEET_MYSQL_IMAGE:-mysql:5.7} + image: ${FLEET_MYSQL_IMAGE:-mysql:5.7.21} platform: ${FLEET_MYSQL_PLATFORM:-linux/x86_64} # innodb-file-per-table=OFF gives ~20% speedup for test runs. - command: - [ + command: [ "mysqld", "--datadir=/tmpfs", "--slow_query_log=1", @@ -75,7 +72,7 @@ services: "--log-bin=bin.log", "--server-id=2", # Required for storage of Apple MDM bootstrap packages. - "--max_allowed_packet=536870912" + "--max_allowed_packet=536870912", ] environment: *mysql-default-environment ports: @@ -100,11 +97,7 @@ services: - "1026:1025" volumes: - ./tools/mailpit/auth.txt:/auth.txt - command: - [ - "--smtp-auth-file=/auth.txt", - "--smtp-auth-allow-insecure=true" - ] + command: ["--smtp-auth-file=/auth.txt", "--smtp-auth-allow-insecure=true"] # SMTP server with TLS smtp4dev_test: @@ -183,5 +176,3 @@ services: volumes: mysql-persistent-volume: data-minio: - - diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index bdeb1a99f4..1ec94188b1 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -1143,38 +1143,53 @@ func upsertMDMAppleHostLabelMembershipDB(ctx context.Context, tx sqlx.ExtContext 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')`) + err := sqlx.SelectContext(ctx, tx, &labels, `SELECT id, name FROM labels WHERE label_type = 1 AND (name = 'All Hosts' OR name = 'macOS' OR name = 'iOS' OR name = 'iPadOS')`) switch { case err != nil: return ctxerr.Wrap(ctx, err, "get builtin labels") - case len(labels) != 2: + case len(labels) != 4: // 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(labels))) + level.Error(logger).Log("err", fmt.Sprintf("expected 4 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) + // We cannot assume IDs on labels, thus we look by name. + var ( + allHostsLabelID uint + macOSLabelID uint + iOSLabelID uint + iPadOSLabelID uint + ) + for _, label := range labels { + switch label.Name { + case "All Hosts": + allHostsLabelID = label.ID + case "macOS": + macOSLabelID = label.ID + case "iOS": + iOSLabelID = label.ID + case "iPadOS": + iPadOSLabelID = label.ID + } } parts := []string{} args := []interface{}{} for _, h := range hosts { - // 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]) + var osLabelID uint + switch h.Platform { + case "ios": + osLabelID = iOSLabelID + case "ipados": + osLabelID = iPadOSLabelID + default: // at this point, assume "darwin" + osLabelID = macOSLabelID } + parts = append(parts, "(?,?),(?,?)") + args = append(args, h.ID, allHostsLabelID, h.ID, osLabelID) } _, err = tx.ExecContext(ctx, fmt.Sprintf(` INSERT INTO label_membership (host_id, label_id) VALUES %s @@ -2269,11 +2284,29 @@ func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, prof detail = fmt.Sprintf("Failed to remove: %s", detail) } + // Check whether we want to set a install operation as 'verifying' for an iOS/iPadOS device. + var isIOSIPadOSInstallVerifiying bool + if profile.OperationType == fleet.MDMOperationTypeInstall && profile.Status != nil && *profile.Status == fleet.MDMDeliveryVerifying { + if err := ds.writer(ctx).GetContext(ctx, &isIOSIPadOSInstallVerifiying, ` + SELECT platform = 'ios' OR platform = 'ipados' FROM hosts WHERE uuid = ?`, + profile.HostUUID, + ); err != nil { + return err + } + } + + status := profile.Status + if isIOSIPadOSInstallVerifiying { + // iOS/iPadOS devices do not have osquery, + // thus they go from 'pending' straight to 'verified' + status = &fleet.MDMDeliveryVerified + } + _, err := ds.writer(ctx).ExecContext(ctx, ` UPDATE host_mdm_apple_profiles SET status = ?, operation_type = ?, detail = ? WHERE host_uuid = ? AND command_uuid = ? - `, profile.Status, profile.OperationType, detail, profile.HostUUID, profile.CommandUUID) + `, status, profile.OperationType, detail, profile.HostUUID, profile.CommandUUID) return err } diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 08b4ae68ff..7bf73fb2ab 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -46,6 +46,7 @@ func TestMDMApple(t *testing.T) { {"TestDeleteMDMAppleConfigProfileByTeamAndIdentifier", testDeleteMDMAppleConfigProfileByTeamAndIdentifier}, {"TestListMDMAppleConfigProfiles", testListMDMAppleConfigProfiles}, {"TestHostDetailsMDMProfiles", testHostDetailsMDMProfiles}, + {"TestHostDetailsMDMProfilesIOSIPadOS", testHostDetailsMDMProfilesIOSIPadOS}, {"TestBatchSetMDMAppleProfiles", testBatchSetMDMAppleProfiles}, {"TestMDMAppleProfileManagement", testMDMAppleProfileManagement}, {"TestMDMAppleProfileManagementBatch2", testMDMAppleProfileManagementBatch2}, @@ -1730,14 +1731,22 @@ func testGetMDMAppleProfilesContents(t *testing.T, ds *Datastore) { // createBuiltinLabels creates entries for "All Hosts" and "macOS" labels, which are assumed to be // extant for MDM flows func createBuiltinLabels(t *testing.T, ds *Datastore) { + // Labels are deleted when truncating tables in between tests. + // We need to delete the iOS/iPadOS labels because these two are created on a table migration, + // and also we want to keep their indexes higher than "All Hosts" and "macOS" (to not break existing tests). _, err := ds.writer(context.Background()).Exec(` + DELETE FROM labels WHERE name = 'iOS' OR name = 'iPadOS'`, + ) + require.NoError(t, err) + + _, err = ds.writer(context.Background()).Exec(` INSERT INTO labels ( name, description, query, platform, label_type - ) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`, + ) VALUES (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)`, "All Hosts", "", "", @@ -1748,6 +1757,16 @@ func createBuiltinLabels(t *testing.T, ds *Datastore) { "", "", fleet.LabelTypeBuiltIn, + "iOS", + "", + "", + "", + fleet.LabelTypeBuiltIn, + "iPadOS", + "", + "", + "", + fleet.LabelTypeBuiltIn, ) require.NoError(t, err) } @@ -5735,8 +5754,16 @@ func testMDMAppleUpsertHostIOSIPadOS(t *testing.T, ds *Datastore) { labels, err := ds.ListLabelsForHost(ctx, h.ID) require.NoError(t, err) - require.Len(t, labels, 1) + require.Len(t, labels, 2) + sort.Slice(labels, func(i, j int) bool { + return labels[i].ID < labels[j].ID + }) require.Equal(t, "All Hosts", labels[0].Name) + if i == 0 { + require.Equal(t, "iOS", labels[1].Name) + } else { + require.Equal(t, "iPadOS", labels[1].Name) + } // Insert again to test updateMDMAppleHostDB. err = ds.MDMAppleUpsertHost(ctx, &fleet.Host{ @@ -5754,8 +5781,16 @@ func testMDMAppleUpsertHostIOSIPadOS(t *testing.T, ds *Datastore) { labels, err = ds.ListLabelsForHost(ctx, h.ID) require.NoError(t, err) - require.Len(t, labels, 1) + require.Len(t, labels, 2) + sort.Slice(labels, func(i, j int) bool { + return labels[i].ID < labels[j].ID + }) require.Equal(t, "All Hosts", labels[0].Name) + if i == 0 { + require.Equal(t, "iOS", labels[1].Name) + } else { + require.Equal(t, "iPadOS", labels[1].Name) + } } err := ds.MDMAppleUpsertHost(ctx, &fleet.Host{ @@ -5870,3 +5905,146 @@ func testMDMAppleProfilesOnIOSIPadOS(t *testing.T, ds *Datastore) { require.Len(t, profiles, 1) require.Equal(t, someProfile.Name, profiles[0].Name) } + +func testHostDetailsMDMProfilesIOSIPadOS(t *testing.T, ds *Datastore) { + ctx := context.Background() + + p0, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{ + Name: "Name0", + Identifier: "Identifier0", + Mobileconfig: []byte("profile0-bytes"), + }) + require.NoError(t, err) + + profiles, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) + require.NoError(t, err) + require.Len(t, profiles, 1) + + iOS, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("host0-osquery-id"), + NodeKey: ptr.String("host0-node-key"), + UUID: "host0-test-mdm-profiles", + Hostname: "hostname0", + Platform: "ios", + }) + require.NoError(t, err) + iPadOS, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("host0-osquery-id-2"), + NodeKey: ptr.String("host0-node-key-2"), + UUID: "host0-test-mdm-profiles-2", + Hostname: "hostname0-2", + Platform: "ipados", + }) + require.NoError(t, err) + + gotHost, err := ds.Host(ctx, iOS.ID) + require.NoError(t, err) + require.Nil(t, gotHost.MDM.Profiles) + gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, iOS.UUID) + require.NoError(t, err) + require.Nil(t, gotProfs) + gotHost, err = ds.Host(ctx, iPadOS.ID) + require.NoError(t, err) + require.Nil(t, gotHost.MDM.Profiles) + gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, iPadOS.UUID) + require.NoError(t, err) + require.Nil(t, gotProfs) + + expectedProfilesIOS := map[string]fleet.HostMDMAppleProfile{ + p0.ProfileUUID: { + HostUUID: iOS.UUID, + Name: p0.Name, + ProfileUUID: p0.ProfileUUID, + CommandUUID: "cmd0-uuid", + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + Detail: "", + }, + } + expectedProfilesIPadOS := map[string]fleet.HostMDMAppleProfile{ + p0.ProfileUUID: { + HostUUID: iPadOS.UUID, + Name: p0.Name, + ProfileUUID: p0.ProfileUUID, + CommandUUID: "cmd0-uuid", + Status: &fleet.MDMDeliveryPending, + OperationType: fleet.MDMOperationTypeInstall, + Detail: "", + }, + } + + var args []interface{} + for _, p := range expectedProfilesIOS { + args = append(args, p.HostUUID, p.ProfileUUID, p.CommandUUID, *p.Status, p.OperationType, p.Detail, p.Name) + } + for _, p := range expectedProfilesIPadOS { + args = append(args, p.HostUUID, p.ProfileUUID, p.CommandUUID, *p.Status, p.OperationType, p.Detail, p.Name) + } + + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, ` + INSERT INTO host_mdm_apple_profiles ( + host_uuid, profile_uuid, command_uuid, status, operation_type, detail, profile_name) + VALUES (?,?,?,?,?,?,?),(?,?,?,?,?,?,?) + `, args..., + ) + if err != nil { + return err + } + return nil + }) + + for _, tc := range []struct { + host *fleet.Host + expectedProfiles map[string]fleet.HostMDMAppleProfile + }{ + { + host: iOS, + expectedProfiles: expectedProfilesIOS, + }, + { + host: iPadOS, + expectedProfiles: expectedProfilesIPadOS, + }, + } { + gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, tc.host.UUID) + require.NoError(t, err) + require.Len(t, gotProfs, 1) + for _, gp := range gotProfs { + ep, ok := expectedProfilesIOS[gp.ProfileUUID] + require.True(t, ok) + require.Equal(t, ep.Name, gp.Name) + require.Equal(t, *ep.Status, *gp.Status) + require.Equal(t, ep.OperationType, gp.OperationType) + require.Equal(t, ep.Detail, gp.Detail) + } + + // mark pending profile to 'verifying', which should instead set it as 'verified'. + installPendingProfile := expectedProfilesIOS[p0.ProfileUUID] + err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{ + HostUUID: installPendingProfile.HostUUID, + CommandUUID: installPendingProfile.CommandUUID, + ProfileUUID: installPendingProfile.ProfileUUID, + Name: installPendingProfile.Name, + Status: &fleet.MDMDeliveryVerifying, + OperationType: fleet.MDMOperationTypeInstall, + Detail: "", + }) + require.NoError(t, err) + + // Check that the profile is the 'verified' state. + gotProfs, err = ds.GetHostMDMAppleProfiles(ctx, iOS.UUID) + require.NoError(t, err) + require.Len(t, gotProfs, 1) + require.NotNil(t, gotProfs[0].Status) + require.Equal(t, fleet.MDMDeliveryVerified, *gotProfs[0].Status) + } +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 633c222d6a..4fb210cfae 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -26,8 +26,10 @@ import ( // Since many hosts may have issues, we need to batch the inserts of host issues. // This is a variable, so it can be adjusted during unit testing. -var hostIssuesInsertBatchSize = 10000 -var hostIssuesUpdateFailingPoliciesBatchSize = 10000 +var ( + hostIssuesInsertBatchSize = 10000 + hostIssuesUpdateFailingPoliciesBatchSize = 10000 +) // A large number of hosts could be changing teams at once, so we need to batch this operation to prevent excessive locks var addHostsToTeamBatchSize = 10000 @@ -4082,7 +4084,7 @@ func (ds *Datastore) AggregatedMDMSolutions(ctx context.Context, teamID *uint, p func (ds *Datastore) GenerateAggregatedMunkiAndMDM(ctx context.Context) error { var ( - platforms = []string{"", "darwin", "windows"} + platforms = []string{"", "darwin", "windows", "ios", "ipados"} teamIDs []uint ) diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 8c552448f7..5725940831 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -5724,15 +5724,21 @@ func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) { h2 := test.NewHost(t, ds, "h2"+t.Name(), "192.168.1.11", "2", "2", time.Now(), test.WithPlatform("darwin")) h3 := test.NewHost(t, ds, "h3"+t.Name(), "192.168.1.11", "3", "3", time.Now(), test.WithPlatform("darwin")) h4 := test.NewHost(t, ds, "h4"+t.Name(), "192.168.1.11", "4", "4", time.Now(), test.WithPlatform("windows")) + h5 := test.NewHost(t, ds, "h5"+t.Name(), "192.168.1.12", "5", "5", time.Now(), test.WithPlatform("ios")) + h6 := test.NewHost(t, ds, "h6"+t.Name(), "192.168.1.12", "6", "6", time.Now(), test.WithPlatform("ipados")) require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{h1.ID})) require.NoError(t, ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{h2.ID})) require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{h3.ID})) require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{h4.ID})) + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team1.ID, []uint{h6.ID})) require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h1.ID, false, true, "https://simplemdm.com", false, fleet.WellKnownMDMSimpleMDM, "")) require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h2.ID, false, true, "url", false, "", "")) + require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h5.ID, false, true, "https://fleet.example.com", true, fleet.WellKnownMDMFleet, "")) + require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h6.ID, false, true, "https://fleet.example.com", true, fleet.WellKnownMDMFleet, "")) + // Add a server, this will be ignored in lists and aggregated data. require.NoError(t, ds.SetOrUpdateMDMData(context.Background(), h4.ID, true, true, "https://simplemdm.com", false, fleet.WellKnownMDMSimpleMDM, "")) @@ -5782,20 +5788,39 @@ func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) { }) require.True(t, updatedAt.After(firstUpdatedAt)) + status, _, err = ds.AggregatedMDMStatus(context.Background(), nil, "") + require.NoError(t, err) + assert.Equal(t, 11, status.HostsCount) + assert.Equal(t, 1, status.UnenrolledHostsCount) + assert.Equal(t, 5, status.EnrolledManualHostsCount) + assert.Equal(t, 3, status.EnrolledAutomatedHostsCount) + status, _, err = ds.AggregatedMDMStatus(context.Background(), &team1.ID, "") require.NoError(t, err) - assert.Equal(t, 1, status.HostsCount) + assert.Equal(t, 2, status.HostsCount) assert.Equal(t, 0, status.UnenrolledHostsCount) assert.Equal(t, 1, status.EnrolledManualHostsCount) - assert.Equal(t, 0, status.EnrolledAutomatedHostsCount) + assert.Equal(t, 1, status.EnrolledAutomatedHostsCount) + + solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), nil, "") + require.True(t, updatedAt.After(firstUpdatedAt)) + require.NoError(t, err) + require.Len(t, solutions, 5) + // Check the new MDM solution used by the iOS/iPadOS + assert.Equal(t, "https://fleet.example.com", solutions[4].ServerURL) + assert.Equal(t, fleet.WellKnownMDMFleet, solutions[4].Name) + assert.Equal(t, 2, solutions[4].HostsCount) solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), &team1.ID, "") require.True(t, updatedAt.After(firstUpdatedAt)) require.NoError(t, err) - require.Len(t, solutions, 1) + require.Len(t, solutions, 2) assert.Equal(t, "https://simplemdm.com", solutions[0].ServerURL) assert.Equal(t, fleet.WellKnownMDMSimpleMDM, solutions[0].Name) assert.Equal(t, 1, solutions[0].HostsCount) + assert.Equal(t, "https://fleet.example.com", solutions[1].ServerURL) + assert.Equal(t, fleet.WellKnownMDMFleet, solutions[1].Name) + assert.Equal(t, 1, solutions[1].HostsCount) status, _, err = ds.AggregatedMDMStatus(context.Background(), &team1.ID, "darwin") require.NoError(t, err) @@ -5816,6 +5841,34 @@ func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) { assert.Equal(t, 1, status.EnrolledManualHostsCount) assert.Equal(t, 0, status.EnrolledAutomatedHostsCount) + status, _, err = ds.AggregatedMDMStatus(context.Background(), &team1.ID, "ios") + require.NoError(t, err) + assert.Equal(t, 0, status.HostsCount) + assert.Equal(t, 0, status.UnenrolledHostsCount) + assert.Equal(t, 0, status.EnrolledManualHostsCount) + assert.Equal(t, 0, status.EnrolledAutomatedHostsCount) + + status, _, err = ds.AggregatedMDMStatus(context.Background(), nil, "ios") + require.NoError(t, err) + assert.Equal(t, 1, status.HostsCount) + assert.Equal(t, 0, status.UnenrolledHostsCount) + assert.Equal(t, 0, status.EnrolledManualHostsCount) + assert.Equal(t, 1, status.EnrolledAutomatedHostsCount) + + status, _, err = ds.AggregatedMDMStatus(context.Background(), &team1.ID, "ipados") + require.NoError(t, err) + assert.Equal(t, 1, status.HostsCount) + assert.Equal(t, 0, status.UnenrolledHostsCount) + assert.Equal(t, 0, status.EnrolledManualHostsCount) + assert.Equal(t, 1, status.EnrolledAutomatedHostsCount) + + status, _, err = ds.AggregatedMDMStatus(context.Background(), nil, "ipados") + require.NoError(t, err) + assert.Equal(t, 1, status.HostsCount) + assert.Equal(t, 0, status.UnenrolledHostsCount) + assert.Equal(t, 0, status.EnrolledManualHostsCount) + assert.Equal(t, 1, status.EnrolledAutomatedHostsCount) + solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), &team1.ID, "windows") require.True(t, updatedAt.After(firstUpdatedAt)) require.NoError(t, err) @@ -5823,6 +5876,35 @@ func testAggregatedHostMDMAndMunki(t *testing.T, ds *Datastore) { assert.Equal(t, "https://simplemdm.com", solutions[0].ServerURL) assert.Equal(t, fleet.WellKnownMDMSimpleMDM, solutions[0].Name) assert.Equal(t, 1, solutions[0].HostsCount) + + solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), nil, "ios") + require.True(t, updatedAt.After(firstUpdatedAt)) + require.NoError(t, err) + require.Len(t, solutions, 1) + assert.Equal(t, "https://fleet.example.com", solutions[0].ServerURL) + assert.Equal(t, fleet.WellKnownMDMFleet, solutions[0].Name) + assert.Equal(t, 1, solutions[0].HostsCount) + + solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), &team1.ID, "ios") + require.True(t, updatedAt.After(firstUpdatedAt)) + require.NoError(t, err) + require.Len(t, solutions, 0) + + solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), nil, "ipados") + require.True(t, updatedAt.After(firstUpdatedAt)) + require.NoError(t, err) + require.Len(t, solutions, 1) + assert.Equal(t, "https://fleet.example.com", solutions[0].ServerURL) + assert.Equal(t, fleet.WellKnownMDMFleet, solutions[0].Name) + assert.Equal(t, 1, solutions[0].HostsCount) + + solutions, updatedAt, err = ds.AggregatedMDMSolutions(context.Background(), &team1.ID, "ipados") + require.True(t, updatedAt.After(firstUpdatedAt)) + require.NoError(t, err) + require.Len(t, solutions, 1) + assert.Equal(t, "https://fleet.example.com", solutions[0].ServerURL) + assert.Equal(t, fleet.WellKnownMDMFleet, solutions[0].Name) + assert.Equal(t, 1, solutions[0].HostsCount) } func testHostsLite(t *testing.T, ds *Datastore) { diff --git a/server/datastore/mysql/mdm.go b/server/datastore/mysql/mdm.go index 44eae46c7b..252eaccf58 100644 --- a/server/datastore/mysql/mdm.go +++ b/server/datastore/mysql/mdm.go @@ -1342,7 +1342,6 @@ func (ds *Datastore) AreHostsConnectedToFleetMDM(ctx context.Context, hosts []*f } return res, nil - } func (ds *Datastore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) { diff --git a/server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels.go b/server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels.go new file mode 100644 index 0000000000..70c8dcb129 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels.go @@ -0,0 +1,114 @@ +package tables + +import ( + "database/sql" + "fmt" + "time" + + "github.com/VividCortex/mysqlerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/go-sql-driver/mysql" +) + +func init() { + MigrationClient.AddMigration(Up_20240707134036, Down_20240707134036) +} + +func Up_20240707134036(tx *sql.Tx) error { + // Create new builtin+manual labels for iOS/iPadOS + iOSLabelID, iPadOSLabelID, err := createBuiltinManualIOSAndIPadOSLabels(tx) + if err != nil { + return fmt.Errorf("failed to create iOS/iPadOS labels: %w", err) + } + + // Add label membership to existing iOS/iPadOS devices. + if _, err := tx.Exec(` + INSERT INTO label_membership (host_id, label_id) + SELECT id AS host_id, IF(platform = 'ios', ?, ?) AS label_id + FROM hosts WHERE platform = 'ios' OR platform = 'ipados';`, + iOSLabelID, iPadOSLabelID, + ); err != nil { + return fmt.Errorf("failed to insert label membership: %w", err) + } + + // Move existing iOS/iPadOS profiles from "Verifying" to "Verified" + // (there's no osquery in these devices). + if _, err := tx.Exec(` + UPDATE host_mdm_apple_profiles hmap + JOIN hosts h ON hmap.host_uuid = h.uuid AND + (h.platform = 'ios' OR h.platform = 'ipados') AND hmap.status = 'verifying' + SET hmap.status = 'verified';`, + ); err != nil { + return fmt.Errorf("failed to update host_mdm_apple_profiles: %w", err) + } + + return nil +} + +func createBuiltinManualIOSAndIPadOSLabels(tx *sql.Tx) (iOSLabelID uint, iPadOSLabelID uint, err error) { + // hard-coded timestamps are used so that schema.sql is stable + stableTS := time.Date(2024, 6, 28, 0, 0, 0, 0, time.UTC) + for _, label := range []struct { + name string + description string + platform string + }{ + { + fleet.BuiltinLabelIOS, + "All iOS hosts", + "ios", + }, + { + fleet.BuiltinLabelIPadOS, + "All iPadOS hosts", + "ipados", + }, + } { + res, err := tx.Exec(` + INSERT INTO labels ( + name, + description, + query, + platform, + label_type, + label_membership_type, + created_at, + updated_at + ) VALUES (?, ?, '', ?, ?, ?, ?, ?);`, + label.name, + label.description, + label.platform, + fleet.LabelTypeBuiltIn, + fleet.LabelMembershipTypeManual, + stableTS, + stableTS, + ) + if err != nil { + if driverErr, ok := err.(*mysql.MySQLError); ok { + if driverErr.Number == mysqlerr.ER_DUP_ENTRY { + // All label names need to be unique across built-in and regular. + // Thus we return an error and instruct the user how to solve the issue. + // + // NOTE(lucas): This is using the same approach we used when creating the Sonoma builtin label. + return 0, 0, fmt.Errorf( + "label with the name %q already exists, please rename it before applying this migration: %w", + label.name, + err, + ) + } + } + return 0, 0, fmt.Errorf("failed to insert label: %w", err) + } + labelID, _ := res.LastInsertId() + if label.name == fleet.BuiltinLabelIOS { + iOSLabelID = uint(labelID) + } else { + iPadOSLabelID = uint(labelID) + } + } + return iOSLabelID, iPadOSLabelID, nil +} + +func Down_20240707134036(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels_test.go b/server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels_test.go new file mode 100644 index 0000000000..c24ed8e1f9 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20240707134036_CreateIOSAndIPADOSBuiltinLabels_test.go @@ -0,0 +1,100 @@ +package tables + +import ( + "fmt" + "sort" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUp_20240707134036(t *testing.T) { + db := applyUpToPrev(t) + + // Insert existing hosts before migration. + hostID := 1 + newHost := func(platform, uuid string) uint { + id := fmt.Sprintf("%d", hostID) + hostID++ + return uint(execNoErrLastID(t, db, + `INSERT INTO hosts (osquery_host_id, node_key, uuid, platform) VALUES (?, ?, ?, ?);`, + id, id, uuid, platform, + )) + } + iOSID := newHost("ios", "iOS_UUID") + iPadOSID := newHost("ipados", "iPadOS_UUID") + newHost("darwin", "macOS_UUID") + + // Insert existing profiles and host profiles before migration. + stmt := ` +INSERT INTO + mdm_apple_configuration_profiles (team_id, identifier, name, mobileconfig, checksum, profile_uuid) +VALUES (?, ?, ?, ?, '', ?)` + + _, err := db.Exec(stmt, 0, "profileID0", "TestPayloadName0", `= 14;','darwin',1,0); +INSERT INTO `labels` VALUES (1,'2024-04-03 00:00:00','2024-04-03 00:00:00','macOS 14+ (Sonoma+)','macOS hosts with version 14 and above','select 1 from os_version where platform = \'darwin\' and major >= 14;','darwin',1,0),(2,'2024-06-28 00:00:00','2024-06-28 00:00:00','iOS','All iOS hosts','','ios',1,1),(3,'2024-06-28 00:00:00','2024-06-28 00:00:00','iPadOS','All iPadOS hosts','','ipados',1,1); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `locks` ( @@ -945,9 +945,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=279 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=280 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/labels.go b/server/fleet/labels.go index a40c53049e..001e507930 100644 --- a/server/fleet/labels.go +++ b/server/fleet/labels.go @@ -156,6 +156,8 @@ const ( BuiltinLabelNameAllLinux = "All Linux" BuiltinLabelNameChrome = "chrome" BuiltinLabelMacOS14Plus = "macOS 14+ (Sonoma+)" + BuiltinLabelIOS = "iOS" + BuiltinLabelIPadOS = "iPadOS" ) // ReservedLabelNames returns a map of label name strings @@ -171,5 +173,7 @@ func ReservedLabelNames() map[string]struct{} { BuiltinLabelNameAllLinux: {}, BuiltinLabelNameChrome: {}, BuiltinLabelMacOS14Plus: {}, + BuiltinLabelIOS: {}, + BuiltinLabelIPadOS: {}, } } diff --git a/server/mdm/apple/profile_verifier.go b/server/mdm/apple/profile_verifier.go index b482749892..d8428f54e8 100644 --- a/server/mdm/apple/profile_verifier.go +++ b/server/mdm/apple/profile_verifier.go @@ -99,8 +99,11 @@ func VerifyHostMDMProfiles(ctx context.Context, ds fleet.ProfileVerificationStor // the MDM protocol and updates the verification status in the datastore. It is intended to be // called by the Fleet MDM checkin and command service install profile request handler. func HandleHostMDMProfileInstallResult(ctx context.Context, ds fleet.ProfileVerificationStore, hostUUID string, cmdUUID string, status *fleet.MDMDeliveryStatus, detail string) error { - host := &fleet.Host{UUID: hostUUID, Platform: "darwin"} if status != nil && *status == fleet.MDMDeliveryFailed { + // Here we set the host.Platform to "darwin" but it applies to iOS/iPadOS too. + // The logic in GetHostMDMProfileRetryCountByCommandUUID and UpdateHostMDMProfilesVerification + // is the exact same when platform is "darwin", "ios" or "ipados". + host := &fleet.Host{UUID: hostUUID, Platform: "darwin"} m, err := ds.GetHostMDMProfileRetryCountByCommandUUID(ctx, host, cmdUUID) if err != nil { return err diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 8677c0fdad..e8db6fe331 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2737,13 +2737,18 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ host.Hostname = deviceName host.GigsDiskSpaceAvailable = availableDeviceCapacity host.GigsTotalDiskSpace = deviceCapacity - var osVersionPrefix string + var ( + osVersionPrefix string + platform string + ) if strings.HasPrefix(productName, "iPhone") { - osVersionPrefix = "iOS " + osVersionPrefix = "iOS" + platform = "ios" } else { // iPad - osVersionPrefix = "iPadOS " + osVersionPrefix = "iPadOS" + platform = "ipados" } - host.OSVersion = osVersionPrefix + osVersion + host.OSVersion = osVersionPrefix + " " + osVersion host.PrimaryMac = wifiMac host.HardwareModel = productName host.DetailUpdatedAt = time.Now() @@ -2753,6 +2758,13 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ if err := svc.ds.SetOrUpdateHostDisksSpace(r.Context, host.ID, availableDeviceCapacity, 100*availableDeviceCapacity/deviceCapacity, deviceCapacity); err != nil { return nil, ctxerr.Wrap(r.Context, err, "failed to update host storage") } + if err := svc.ds.UpdateHostOperatingSystem(r.Context, host.ID, fleet.OperatingSystem{ + Name: osVersionPrefix, + Version: osVersion, + Platform: platform, + }); err != nil { + return nil, ctxerr.Wrap(r.Context, err, "failed to update host operating system") + } return nil, nil } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 554ef26204..93c8606af5 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -3237,6 +3237,13 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) { require.NotZero(t, 64, int64(gigsTotal)) return nil } + ds.UpdateHostOperatingSystemFunc = func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error { + require.Equal(t, hostID, hostID) + require.Equal(t, "iPadOS", hostOS.Name) + require.Equal(t, "17.5.1", hostOS.Version) + require.Equal(t, "ipados", hostOS.Platform) + return nil + } _, err := svc.CommandAndReportResults( &mdm.Request{Context: ctx}, @@ -3277,4 +3284,5 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) { require.True(t, ds.UpdateHostFuncInvoked) require.True(t, ds.HostByIdentifierFuncInvoked) require.True(t, ds.SetOrUpdateHostDisksSpaceFuncInvoked) + require.True(t, ds.UpdateHostOperatingSystemFuncInvoked) } diff --git a/server/test/new_objects.go b/server/test/new_objects.go index cf92fb798d..099b7a0e57 100644 --- a/server/test/new_objects.go +++ b/server/test/new_objects.go @@ -162,6 +162,20 @@ func AddBuiltinLabels(t *testing.T, ds fleet.Datastore) { LabelType: fleet.LabelTypeBuiltIn, LabelMembershipType: fleet.LabelMembershipTypeDynamic, }, + { + Name: "iOS", + Platform: "ios", + Query: "", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeManual, + }, + { + Name: "iPadOS", + Platform: "ipados", + Query: "", + LabelType: fleet.LabelTypeBuiltIn, + LabelMembershipType: fleet.LabelMembershipTypeManual, + }, } names := fleet.ReservedLabelNames() diff --git a/tools/mdm/apple/applebmapi/main.go b/tools/mdm/apple/applebmapi/main.go index 419bb48ed8..04cbc198d8 100644 --- a/tools/mdm/apple/applebmapi/main.go +++ b/tools/mdm/apple/applebmapi/main.go @@ -38,6 +38,13 @@ func main() { log.Fatal("only one of -profile-uuid or -serial-number must be provided") } + if len(*serverPrivateKey) > 32 { + // We truncate to 32 bytes because AES-256 requires a 32 byte (256 bit) PK, but some + // infra setups generate keys that are longer than 32 bytes. + truncatedServerPrivateKey := (*serverPrivateKey)[:32] + serverPrivateKey = &truncatedServerPrivateKey + } + cfg := config.MysqlConfig{ Protocol: "tcp", Address: *mysqlAddr,