From afc4cc5d23197699172923948fa1ab1e128023c0 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Fri, 22 Nov 2024 16:52:03 +0000 Subject: [PATCH 1/5] add UI for new windows mdm page and automatic migration (#24068) relates to #22896 Implements the UI for the windows automatic migration. **new windows mdm page layout with automatic migration checkbox** ![image](https://github.com/user-attachments/assets/2909d6d2-e802-4dec-9c78-0b8f6a4466c0) - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality --- changes/22896-ui-windows-automatic-migration | 1 + frontend/__mocks__/configMock.ts | 1 + .../forms/fields/Checkbox/_styles.scss | 1 + frontend/interfaces/config.ts | 1 + .../pages/DashboardPage/cards/MDM/MDM.tsx | 6 +- .../WindowsMdmPage/WindowsMdmPage.tsx | 111 +++++++++--------- .../MdmSettings/WindowsMdmPage/__styles.scss | 9 +- frontend/utilities/constants.tsx | 5 +- 8 files changed, 71 insertions(+), 64 deletions(-) create mode 100644 changes/22896-ui-windows-automatic-migration diff --git a/changes/22896-ui-windows-automatic-migration b/changes/22896-ui-windows-automatic-migration new file mode 100644 index 000000000000..ae0234123bfd --- /dev/null +++ b/changes/22896-ui-windows-automatic-migration @@ -0,0 +1 @@ +- add UI changes for windows mdm page and allow for automatic migration for windows hosts. diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts index c589f0538629..d1f0a644cd7b 100644 --- a/frontend/__mocks__/configMock.ts +++ b/frontend/__mocks__/configMock.ts @@ -39,6 +39,7 @@ const DEFAULT_CONFIG_MDM_MOCK: IMdmConfig = { deadline_days: null, grace_period_days: null, }, + windows_migration_enabled: false, end_user_authentication: { entity_id: "", issuer_uri: "", diff --git a/frontend/components/forms/fields/Checkbox/_styles.scss b/frontend/components/forms/fields/Checkbox/_styles.scss index 8e8d02342220..6d311ead479f 100644 --- a/frontend/components/forms/fields/Checkbox/_styles.scss +++ b/frontend/components/forms/fields/Checkbox/_styles.scss @@ -68,6 +68,7 @@ } .checkbox-unchecked-state { stroke: $ui-fleet-black-25; + fill: $ui-fleet-black-25; } } } diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index 0de36a5d369b..12d61f3c85e5 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -57,6 +57,7 @@ export interface IMdmConfig { apple_bm_terms_expired: boolean; apple_bm_enabled_and_configured: boolean; windows_enabled_and_configured: boolean; + windows_migration_enabled: boolean; end_user_authentication: IEndUserAuthentication; macos_updates: IAppleDeviceUpdates; ios_updates: IAppleDeviceUpdates; diff --git a/frontend/pages/DashboardPage/cards/MDM/MDM.tsx b/frontend/pages/DashboardPage/cards/MDM/MDM.tsx index 583460c1287f..6824ebe5ae64 100644 --- a/frontend/pages/DashboardPage/cards/MDM/MDM.tsx +++ b/frontend/pages/DashboardPage/cards/MDM/MDM.tsx @@ -2,11 +2,7 @@ import React, { useMemo, useState } from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { Row } from "react-table"; -import { - IMdmStatusCardData, - IMdmSolution, - IMdmSummaryMdmSolution, -} from "interfaces/mdm"; +import { IMdmStatusCardData, IMdmSummaryMdmSolution } from "interfaces/mdm"; import TabsWrapper from "components/TabsWrapper"; import TableContainer from "components/TableContainer"; diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx index 405cf3b4a65a..e7b5b814a4ac 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useContext, useState } from "react"; import { InjectedRouter } from "react-router"; import { isAxiosError } from "axios"; @@ -11,18 +11,22 @@ import { AppContext } from "context/app"; import MainContent from "components/MainContent/MainContent"; import Button from "components/buttons/Button"; import BackLink from "components/BackLink/BackLink"; +import Slider from "components/forms/fields/Slider"; +import Checkbox from "components/forms/fields/Checkbox"; const baseClass = "windows-mdm-page"; interface ISetWindowsMdmOptions { - enable: boolean; + enableMdm: boolean; + enableAutoMigration: boolean; successMessage: string; errorMessage: string; router: InjectedRouter; } const useSetWindowsMdm = ({ - enable, + enableMdm, + enableAutoMigration, successMessage, errorMessage, router, @@ -35,13 +39,14 @@ const useSetWindowsMdm = ({ try { const updatedConfig = await configAPI.updateMDMConfig( { - windows_enabled_and_configured: enable, + windows_enabled_and_configured: enableMdm, + windows_migration_enabled: enableAutoMigration, }, true ); setConfig(updatedConfig); } catch (e) { - if (enable && isAxiosError(e) && e.response?.status === 422) { + if (enableMdm && isAxiosError(e) && e.response?.status === 422) { flashErrMsg = getErrorReason(e, { nameEquals: "mdm.windows_enabled_and_configured", @@ -62,62 +67,44 @@ const useSetWindowsMdm = ({ return turnOnWindowsMdm; }; -interface IWindowsMdmOnContentProps { +interface IWindowsMdmPageProps { router: InjectedRouter; } -const WindowsMdmOnContent = ({ router }: IWindowsMdmOnContentProps) => { - const turnOnWindowsMdm = useSetWindowsMdm({ - enable: true, - successMessage: "Windows MDM turned on (servers excluded).", - errorMessage: "Unable to turn on Windows MDM. Please try again.", - router, - }); +const WindowsMdmPage = ({ router }: IWindowsMdmPageProps) => { + const { config } = useContext(AppContext); - return ( - <> -

Turn on Windows MDM

-

This will turn MDM on for Windows hosts with fleetd.

-

Hosts connected to another MDM solution won't be migrated.

-

MDM won't be turned on for Windows servers.

- - + const [mdmOn, setMdmOn] = useState( + config?.mdm?.windows_enabled_and_configured ?? false + ); + const [autoMigration, setAutoMigration] = useState( + config?.mdm?.windows_migration_enabled ?? false ); -}; - -interface IWindowsMdmOffContentProps { - router: InjectedRouter; -} -const WindowsMdmOffContent = ({ router }: IWindowsMdmOffContentProps) => { - const turnOffWindowsMdm = useSetWindowsMdm({ - enable: false, - successMessage: "Windows MDM turned off.", - errorMessage: "Unable to turn off Windows MDM. Please try again.", + const updateWindowsMdm = useSetWindowsMdm({ + enableMdm: mdmOn, + enableAutoMigration: autoMigration, + successMessage: "Windows MDM settings successfully updated.", + errorMessage: "Unable to update Windows MDM. Please try again.", router, }); - return ( - <> -

Turn off Windows MDM

-

- MDM will no longer be turned on for Windows hosts that enroll to Fleet. -

-

Hosts with MDM already turned on will not have MDM removed.

- - - ); -}; + const onChangeMdmOn = () => { + setMdmOn(!mdmOn); + mdmOn && setAutoMigration(false); + }; -interface IWindowsMdmPageProps { - router: InjectedRouter; -} + const onChangeAutoMigration = () => { + setAutoMigration(!autoMigration); + }; -const WindowsMdmPage = ({ router }: IWindowsMdmPageProps) => { - const { config } = useContext(AppContext); + const onSaveMdm = () => { + updateWindowsMdm(); + }; - const isWindowsMdmEnabled = - config?.mdm?.windows_enabled_and_configured ?? false; + const descriptionText = mdmOn + ? "Turns on MDM for Windows hosts that enroll to Fleet (excluding servers)." + : "Hosts with MDM already turned on will not have MDM removed."; return ( @@ -127,11 +114,27 @@ const WindowsMdmPage = ({ router }: IWindowsMdmPageProps) => { path={PATHS.ADMIN_INTEGRATIONS_MDM} className={`${baseClass}__back-to-mdm`} /> - {isWindowsMdmEnabled ? ( - - ) : ( - - )} +

Manage Windows MDM

+
+ +

{descriptionText}

+ + Automatically migrate hosts connected to another MDM solution + + + +
); diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss index ecf4b4c49c76..64d127974611 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/__styles.scss @@ -5,12 +5,13 @@ } h1 { - margin-bottom: $pad-xxlarge; + margin-bottom: $pad-large; font-size: $x-large; } - p { - font-size: $x-small; - margin: 0 0 $pad-large; + form { + display: flex; + flex-direction: column; + gap: $pad-large; } } diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index d7e2497655a4..3c5820df3562 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -340,7 +340,10 @@ export const MDM_STATUS_TOOLTIP: Record = { ), "On (manual)": ( - MDM was turned on manually. End users can turn MDM off. + + MDM was turned on manually (macOS), or hosts were automatically migrated + with fleetd (Windows). End users can turn MDM off. + ), Off: undefined, // no tooltip specified Pending: ( From c4404d9d68c192b2f0bc7c4bc0521c4fd5286d2d Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Tue, 26 Nov 2024 11:52:56 -0500 Subject: [PATCH 2/5] Windows MDM Migration: API, CLI and activities (#24141) --- ...2897-add-windows-migration-enabled-setting | 1 + cmd/fleetctl/gitops_test.go | 25 ++++++ .../expectedGetConfigAppConfigJson.json | 1 + .../expectedGetConfigAppConfigYaml.yml | 1 + ...ectedGetConfigIncludeServerConfigJson.json | 1 + ...pectedGetConfigIncludeServerConfigYaml.yml | 1 + ...l_config_windows_migration_false_false.yml | 75 +++++++++++++++++ ...al_config_windows_migration_false_true.yml | 75 +++++++++++++++++ ...al_config_windows_migration_true_false.yml | 75 +++++++++++++++++ ...bal_config_windows_migration_true_true.yml | 75 +++++++++++++++++ .../macosSetupExpectedAppConfigEmpty.yml | 1 + .../macosSetupExpectedAppConfigSet.yml | 1 + docs/Contributing/Audit-logs.md | 12 +++ pkg/spec/gitops.go | 3 +- pkg/spec/gitops_test.go | 2 + pkg/spec/testdata/controls.yml | 1 + pkg/spec/testdata/global_config_no_paths.yml | 1 + pkg/spec/testdata/team_config_no_paths.yml | 1 + ...ddAppConfigWindowsMigrationEnabledField.go | 54 ++++++++++++ ...ConfigWindowsMigrationEnabledField_test.go | 33 ++++++++ server/datastore/mysql/schema.sql | 6 +- server/fleet/activities.go | 24 ++++++ server/fleet/app.go | 9 +- server/service/appconfig.go | 27 ++++++ server/service/client.go | 5 ++ server/service/integration_core_test.go | 6 ++ server/service/integration_enterprise_test.go | 10 +++ server/service/integration_mdm_test.go | 82 +++++++++++++++++++ .../generated_files/appconfig.txt | 1 + 29 files changed, 601 insertions(+), 8 deletions(-) create mode 100644 changes/22897-add-windows-migration-enabled-setting create mode 100644 cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_false.yml create mode 100644 cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_true.yml create mode 100644 cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_false.yml create mode 100644 cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_true.yml create mode 100644 server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField.go create mode 100644 server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField_test.go diff --git a/changes/22897-add-windows-migration-enabled-setting b/changes/22897-add-windows-migration-enabled-setting new file mode 100644 index 000000000000..15866a98c7af --- /dev/null +++ b/changes/22897-add-windows-migration-enabled-setting @@ -0,0 +1 @@ +* Added support for the new `windows_migration_enabled` setting (can be set via `fleetctl`, the `PATCH /api/latest/fleet/config` API endpoint and the UI). Requires a premium license. diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index 407351c154b2..7abc95dc6376 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -3219,6 +3219,31 @@ software: } } +func TestGitOpsWindowsMigration(t *testing.T) { + cases := []struct { + file string + wantErr string + }{ + // booleans are Windows MDM enabled and Windows migration enabled + {"testdata/gitops/global_config_windows_migration_true_true.yml", ""}, + {"testdata/gitops/global_config_windows_migration_false_true.yml", "Windows MDM is not enabled"}, + {"testdata/gitops/global_config_windows_migration_true_false.yml", ""}, + {"testdata/gitops/global_config_windows_migration_false_false.yml", ""}, + } + for _, c := range cases { + t.Run(filepath.Base(c.file), func(t *testing.T) { + setupFullGitOpsPremiumServer(t) + + _, err := runAppNoChecks([]string{"gitops", "-f", c.file}) + if c.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, c.wantErr) + } + }) + } +} + type memKeyValueStore struct { m sync.Map } diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json index c3779641775a..1cbede5abf54 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json @@ -118,6 +118,7 @@ "deadline_days": 7, "grace_period_days": 3 }, + "windows_migration_enabled": false, "macos_migration": { "enable": false, "mode": "", diff --git a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml index ff6fbaa22eae..042be511914c 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml @@ -27,6 +27,7 @@ spec: volume_purchasing_program: null windows_enabled_and_configured: false enable_disk_encryption: false + windows_migration_enabled: false macos_migration: enable: false mode: "" diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json index dea76a995b16..f8c421065b52 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigJson.json @@ -70,6 +70,7 @@ "deadline_days": 7, "grace_period_days": 3 }, + "windows_migration_enabled": false, "macos_migration": { "enable": false, "mode": "", diff --git a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml index f6b8136407cd..5f6e877f163b 100644 --- a/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml +++ b/cmd/fleetctl/testdata/expectedGetConfigIncludeServerConfigYaml.yml @@ -27,6 +27,7 @@ spec: enabled_and_configured: false windows_enabled_and_configured: false enable_disk_encryption: false + windows_migration_enabled: false macos_migration: enable: false mode: "" diff --git a/cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_false.yml b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_false.yml new file mode 100644 index 000000000000..645be877d3a3 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_false.yml @@ -0,0 +1,75 @@ +controls: + macos_settings: + windows_settings: + scripts: + enable_disk_encryption: false + macos_migration: + enable: false + mode: "" + webhook_url: "" + macos_setup: + bootstrap_package: null + enable_end_user_authentication: false + macos_setup_assistant: null + macos_updates: + deadline: null + minimum_version: null + windows_enabled_and_configured: false + windows_migration_enabled: false + windows_updates: + deadline_days: null + grace_period_days: null +queries: +policies: +agent_options: + command_line_flags: + distributed_denylist_duration: 0 + config: + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + options: + disable_distributed: false + distributed_interval: 10 + distributed_plugin: tls + distributed_tls_max_attempts: 3 + logger_tls_endpoint: /api/v1/osquery/log + pack_delimiter: / +org_settings: + server_settings: + deferred_save_host: false + enable_analytics: true + live_query_disabled: false + query_report_cap: 2000 + query_reports_disabled: false + scripts_disabled: false + server_url: $FLEET_SERVER_URL + ai_features_disabled: true + org_info: + contact_url: https://fleetdm.com/company/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: $ORG_NAME + smtp_settings: + sso_settings: + integrations: + mdm: + end_user_authentication: + webhook_settings: + fleet_desktop: # Applies to Fleet Premium only + transparency_url: https://fleetdm.com/transparency + host_expiry_settings: # Applies to all teams + host_expiry_enabled: false + activity_expiry_settings: + activity_expiry_enabled: true + activity_expiry_window: 60 + features: # Features added to all teams + enable_host_users: true + enable_software_inventory: true + vulnerability_settings: + databases_path: "" + secrets: # These secrets are used to enroll hosts to the "All teams" team + - secret: SampleSecret123 + - secret: ABC +software: diff --git a/cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_true.yml b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_true.yml new file mode 100644 index 000000000000..99cdf07c3918 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_false_true.yml @@ -0,0 +1,75 @@ +controls: + macos_settings: + windows_settings: + scripts: + enable_disk_encryption: false + macos_migration: + enable: false + mode: "" + webhook_url: "" + macos_setup: + bootstrap_package: null + enable_end_user_authentication: false + macos_setup_assistant: null + macos_updates: + deadline: null + minimum_version: null + windows_enabled_and_configured: false + windows_migration_enabled: true + windows_updates: + deadline_days: null + grace_period_days: null +queries: +policies: +agent_options: + command_line_flags: + distributed_denylist_duration: 0 + config: + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + options: + disable_distributed: false + distributed_interval: 10 + distributed_plugin: tls + distributed_tls_max_attempts: 3 + logger_tls_endpoint: /api/v1/osquery/log + pack_delimiter: / +org_settings: + server_settings: + deferred_save_host: false + enable_analytics: true + live_query_disabled: false + query_report_cap: 2000 + query_reports_disabled: false + scripts_disabled: false + server_url: $FLEET_SERVER_URL + ai_features_disabled: true + org_info: + contact_url: https://fleetdm.com/company/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: $ORG_NAME + smtp_settings: + sso_settings: + integrations: + mdm: + end_user_authentication: + webhook_settings: + fleet_desktop: # Applies to Fleet Premium only + transparency_url: https://fleetdm.com/transparency + host_expiry_settings: # Applies to all teams + host_expiry_enabled: false + activity_expiry_settings: + activity_expiry_enabled: true + activity_expiry_window: 60 + features: # Features added to all teams + enable_host_users: true + enable_software_inventory: true + vulnerability_settings: + databases_path: "" + secrets: # These secrets are used to enroll hosts to the "All teams" team + - secret: SampleSecret123 + - secret: ABC +software: diff --git a/cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_false.yml b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_false.yml new file mode 100644 index 000000000000..c934e124a205 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_false.yml @@ -0,0 +1,75 @@ +controls: + macos_settings: + windows_settings: + scripts: + enable_disk_encryption: false + macos_migration: + enable: false + mode: "" + webhook_url: "" + macos_setup: + bootstrap_package: null + enable_end_user_authentication: false + macos_setup_assistant: null + macos_updates: + deadline: null + minimum_version: null + windows_enabled_and_configured: true + windows_migration_enabled: false + windows_updates: + deadline_days: null + grace_period_days: null +queries: +policies: +agent_options: + command_line_flags: + distributed_denylist_duration: 0 + config: + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + options: + disable_distributed: false + distributed_interval: 10 + distributed_plugin: tls + distributed_tls_max_attempts: 3 + logger_tls_endpoint: /api/v1/osquery/log + pack_delimiter: / +org_settings: + server_settings: + deferred_save_host: false + enable_analytics: true + live_query_disabled: false + query_report_cap: 2000 + query_reports_disabled: false + scripts_disabled: false + server_url: $FLEET_SERVER_URL + ai_features_disabled: true + org_info: + contact_url: https://fleetdm.com/company/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: $ORG_NAME + smtp_settings: + sso_settings: + integrations: + mdm: + end_user_authentication: + webhook_settings: + fleet_desktop: # Applies to Fleet Premium only + transparency_url: https://fleetdm.com/transparency + host_expiry_settings: # Applies to all teams + host_expiry_enabled: false + activity_expiry_settings: + activity_expiry_enabled: true + activity_expiry_window: 60 + features: # Features added to all teams + enable_host_users: true + enable_software_inventory: true + vulnerability_settings: + databases_path: "" + secrets: # These secrets are used to enroll hosts to the "All teams" team + - secret: SampleSecret123 + - secret: ABC +software: diff --git a/cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_true.yml b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_true.yml new file mode 100644 index 000000000000..f6ab917ecf92 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/global_config_windows_migration_true_true.yml @@ -0,0 +1,75 @@ +controls: + macos_settings: + windows_settings: + scripts: + enable_disk_encryption: false + macos_migration: + enable: false + mode: "" + webhook_url: "" + macos_setup: + bootstrap_package: null + enable_end_user_authentication: false + macos_setup_assistant: null + macos_updates: + deadline: null + minimum_version: null + windows_enabled_and_configured: true + windows_migration_enabled: true + windows_updates: + deadline_days: null + grace_period_days: null +queries: +policies: +agent_options: + command_line_flags: + distributed_denylist_duration: 0 + config: + decorators: + load: + - SELECT uuid AS host_uuid FROM system_info; + - SELECT hostname AS hostname FROM system_info; + options: + disable_distributed: false + distributed_interval: 10 + distributed_plugin: tls + distributed_tls_max_attempts: 3 + logger_tls_endpoint: /api/v1/osquery/log + pack_delimiter: / +org_settings: + server_settings: + deferred_save_host: false + enable_analytics: true + live_query_disabled: false + query_report_cap: 2000 + query_reports_disabled: false + scripts_disabled: false + server_url: $FLEET_SERVER_URL + ai_features_disabled: true + org_info: + contact_url: https://fleetdm.com/company/contact + org_logo_url: "" + org_logo_url_light_background: "" + org_name: $ORG_NAME + smtp_settings: + sso_settings: + integrations: + mdm: + end_user_authentication: + webhook_settings: + fleet_desktop: # Applies to Fleet Premium only + transparency_url: https://fleetdm.com/transparency + host_expiry_settings: # Applies to all teams + host_expiry_enabled: false + activity_expiry_settings: + activity_expiry_enabled: true + activity_expiry_window: 60 + features: # Features added to all teams + enable_host_users: true + enable_software_inventory: true + vulnerability_settings: + databases_path: "" + secrets: # These secrets are used to enroll hosts to the "All teams" team + - secret: SampleSecret123 + - secret: ABC +software: diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml index 49c129df695b..50250bc34eaa 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigEmpty.yml @@ -27,6 +27,7 @@ spec: enabled_and_configured: true windows_enabled_and_configured: false enable_disk_encryption: false + windows_migration_enabled: false macos_migration: enable: false mode: "" diff --git a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml index 27e6a2a5459e..b74e3c2d8f5e 100644 --- a/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml +++ b/cmd/fleetctl/testdata/macosSetupExpectedAppConfigSet.yml @@ -27,6 +27,7 @@ spec: enabled_and_configured: true windows_enabled_and_configured: false enable_disk_encryption: false + windows_migration_enabled: false macos_migration: enable: false mode: "" diff --git a/docs/Contributing/Audit-logs.md b/docs/Contributing/Audit-logs.md index 26c8a02f3dc5..0650b6179864 100644 --- a/docs/Contributing/Audit-logs.md +++ b/docs/Contributing/Audit-logs.md @@ -894,6 +894,18 @@ Generated when a user turns off MDM features for all Windows hosts. This activity does not contain any detail fields. +## enabled_windows_mdm_migration + +Generated when a user enables automatic MDM migration for Windows hosts, if Windows MDM is turned on. + +This activity does not contain any detail fields. + +## disabled_windows_mdm_migration + +Generated when a user disables automatic MDM migration for Windows hosts, if Windows MDM is turned on. + +This activity does not contain any detail fields. + ## ran_script Generated when a script is sent to be run for a host. diff --git a/pkg/spec/gitops.go b/pkg/spec/gitops.go index c26c9b500f3a..bdfdf1ceec3c 100644 --- a/pkg/spec/gitops.go +++ b/pkg/spec/gitops.go @@ -33,6 +33,7 @@ type Controls struct { WindowsUpdates interface{} `json:"windows_updates"` WindowsSettings interface{} `json:"windows_settings"` WindowsEnabledAndConfigured interface{} `json:"windows_enabled_and_configured"` + WindowsMigrationEnabled interface{} `json:"windows_migration_enabled"` EnableDiskEncryption interface{} `json:"enable_disk_encryption"` @@ -46,7 +47,7 @@ func (c Controls) Set() bool { c.IPadOSUpdates != nil || c.MacOSSettings != nil || c.MacOSSetup != nil || c.MacOSMigration != nil || c.WindowsUpdates != nil || c.WindowsSettings != nil || c.WindowsEnabledAndConfigured != nil || - c.EnableDiskEncryption != nil || len(c.Scripts) > 0 + c.WindowsMigrationEnabled != nil || c.EnableDiskEncryption != nil || len(c.Scripts) > 0 } type Policy struct { diff --git a/pkg/spec/gitops_test.go b/pkg/spec/gitops_test.go index 8ca334ce3620..bdc5423f0a40 100644 --- a/pkg/spec/gitops_test.go +++ b/pkg/spec/gitops_test.go @@ -216,6 +216,8 @@ func TestValidGitOpsYaml(t *testing.T) { assert.True(t, ok, "ipados_updates not found") _, ok = gitops.Controls.WindowsEnabledAndConfigured.(bool) assert.True(t, ok, "windows_enabled_and_configured not found") + _, ok = gitops.Controls.WindowsMigrationEnabled.(bool) + assert.True(t, ok, "windows_migration_enabled not found") _, ok = gitops.Controls.WindowsUpdates.(map[string]interface{}) assert.True(t, ok, "windows_updates not found") diff --git a/pkg/spec/testdata/controls.yml b/pkg/spec/testdata/controls.yml index 5da356792168..27fe44dc03b4 100644 --- a/pkg/spec/testdata/controls.yml +++ b/pkg/spec/testdata/controls.yml @@ -25,6 +25,7 @@ ipados_updates: deadline: null minimum_version: null windows_enabled_and_configured: true +windows_migration_enabled: false windows_updates: deadline_days: null grace_period_days: null diff --git a/pkg/spec/testdata/global_config_no_paths.yml b/pkg/spec/testdata/global_config_no_paths.yml index c8d68f946237..36ce19ca3e49 100644 --- a/pkg/spec/testdata/global_config_no_paths.yml +++ b/pkg/spec/testdata/global_config_no_paths.yml @@ -27,6 +27,7 @@ controls: # Controls added to "No team" deadline: null minimum_version: null windows_enabled_and_configured: true + windows_migration_enabled: false windows_updates: deadline_days: null grace_period_days: null diff --git a/pkg/spec/testdata/team_config_no_paths.yml b/pkg/spec/testdata/team_config_no_paths.yml index e660d5eccce7..2ff83e4730fa 100644 --- a/pkg/spec/testdata/team_config_no_paths.yml +++ b/pkg/spec/testdata/team_config_no_paths.yml @@ -60,6 +60,7 @@ controls: mode: "" webhook_url: "" windows_enabled_and_configured: true + windows_migration_enabled: false queries: - name: Scheduled query stats description: Collect osquery performance stats directly from osquery diff --git a/server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField.go b/server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField.go new file mode 100644 index 000000000000..1765d00c6220 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField.go @@ -0,0 +1,54 @@ +package tables + +import ( + "database/sql" + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +func init() { + MigrationClient.AddMigration(Up_20241125150614, Down_20241125150614) +} + +func Up_20241125150614(tx *sql.Tx) error { + var raw json.RawMessage + var id uint + row := tx.QueryRow(`SELECT id, json_value FROM app_config_json LIMIT 1;`) + if err := row.Scan(&id, &raw); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil + } + return fmt.Errorf("select app_config_json: %w", err) + } + + var config map[string]interface{} + if err := json.Unmarshal(raw, &config); err != nil { + return fmt.Errorf("unmarshal appconfig: %w", err) + } + + mdm, ok := config["mdm"] + if !ok { + return errors.New("missing mdm section") + } + mdmMap, ok := mdm.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid type for mdm: %T", mdm) + } + mdmMap["windows_migration_enabled"] = false + + b, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("marshal updated appconfig: %w", err) + } + if _, err := tx.Exec(`UPDATE app_config_json SET json_value = ? WHERE id = ?;`, b, id); err != nil { + return fmt.Errorf("update app_config_json: %w", err) + } + + return nil +} + +func Down_20241125150614(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField_test.go b/server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField_test.go new file mode 100644 index 000000000000..743f82de1db5 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241125150614_AddAppConfigWindowsMigrationEnabledField_test.go @@ -0,0 +1,33 @@ +package tables + +import ( + "encoding/json" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestUp_20241125150614(t *testing.T) { + db := applyUpToPrev(t) + + // Apply current migration. + applyNext(t, db) + + var appCfg json.RawMessage + err := sqlx.Get(db, &appCfg, `SELECT json_value FROM app_config_json LIMIT 1;`) + require.NoError(t, err) + + var config map[string]interface{} + err = json.Unmarshal(appCfg, &config) + require.NoError(t, err) + + mdm, ok := config["mdm"] + require.True(t, ok) + mdmMap, ok := mdm.(map[string]interface{}) + require.True(t, ok) + + _, ok = mdmMap["windows_enabled_and_configured"].(bool) + require.True(t, ok) + require.False(t, mdmMap["windows_migration_enabled"].(bool)) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 038a0de892c1..450848024da6 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -64,7 +64,7 @@ CREATE TABLE `app_config_json` ( PRIMARY KEY (`id`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); +INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `calendar_events` ( @@ -1101,9 +1101,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=332 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=333 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'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,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'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/activities.go b/server/fleet/activities.go index 751218ac6eb5..1298476dde93 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -79,6 +79,8 @@ var ActivityDetailsList = []ActivityDetails{ ActivityTypeEnabledWindowsMDM{}, ActivityTypeDisabledWindowsMDM{}, + ActivityTypeEnabledWindowsMDMMigration{}, + ActivityTypeDisabledWindowsMDMMigration{}, ActivityTypeRanScript{}, ActivityTypeAddedScript{}, @@ -1236,6 +1238,28 @@ func (a ActivityTypeDisabledWindowsMDM) Documentation() (activity, details, deta `This activity does not contain any detail fields.`, `` } +type ActivityTypeEnabledWindowsMDMMigration struct{} + +func (a ActivityTypeEnabledWindowsMDMMigration) ActivityName() string { + return "enabled_windows_mdm_migration" +} + +func (a ActivityTypeEnabledWindowsMDMMigration) Documentation() (activity, details, detailsExample string) { + return `Generated when a user enables automatic MDM migration for Windows hosts, if Windows MDM is turned on.`, + `This activity does not contain any detail fields.`, `` +} + +type ActivityTypeDisabledWindowsMDMMigration struct{} + +func (a ActivityTypeDisabledWindowsMDMMigration) ActivityName() string { + return "disabled_windows_mdm_migration" +} + +func (a ActivityTypeDisabledWindowsMDMMigration) Documentation() (activity, details, detailsExample string) { + return `Generated when a user disables automatic MDM migration for Windows hosts, if Windows MDM is turned on.`, + `This activity does not contain any detail fields.`, `` +} + type ActivityTypeRanScript struct { HostID uint `json:"host_id"` HostDisplayName string `json:"host_display_name"` diff --git a/server/fleet/app.go b/server/fleet/app.go index 5622c438a90d..662095832ee1 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -189,10 +189,11 @@ type MDM struct { // WindowsUpdates defines the OS update settings for Windows devices. WindowsUpdates WindowsUpdates `json:"windows_updates"` - MacOSSettings MacOSSettings `json:"macos_settings"` - MacOSSetup MacOSSetup `json:"macos_setup"` - MacOSMigration MacOSMigration `json:"macos_migration"` - EndUserAuthentication MDMEndUserAuthentication `json:"end_user_authentication"` + MacOSSettings MacOSSettings `json:"macos_settings"` + MacOSSetup MacOSSetup `json:"macos_setup"` + MacOSMigration MacOSMigration `json:"macos_migration"` + WindowsMigrationEnabled bool `json:"windows_migration_enabled"` + EndUserAuthentication MDMEndUserAuthentication `json:"end_user_authentication"` // WindowsEnabledAndConfigured indicates if Fleet MDM is enabled for Windows. // There is no other configuration required for Windows other than enabling diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 030c2c650202..e0ee0317e67f 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -337,6 +337,15 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle return nil, ctxerr.Wrap(ctx, err) } + // if turning off Windows MDM and Windows Migration is not explicitly set to + // on in the same update, set it to off (otherwise, if it is explicitly set + // to true, return an error that it can't be done when MDM is off, this is + // addressed in validateMDM). + if oldAppConfig.MDM.WindowsEnabledAndConfigured != appConfig.MDM.WindowsEnabledAndConfigured && + !appConfig.MDM.WindowsEnabledAndConfigured && !newAppConfig.MDM.WindowsMigrationEnabled { + appConfig.MDM.WindowsMigrationEnabled = false + } + type ndesStatusType string const ( ndesStatusAdded ndesStatusType = "added" @@ -869,6 +878,18 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle } } + if appConfig.MDM.WindowsEnabledAndConfigured && oldAppConfig.MDM.WindowsMigrationEnabled != appConfig.MDM.WindowsMigrationEnabled { + var act fleet.ActivityDetails + if appConfig.MDM.WindowsMigrationEnabled { + act = fleet.ActivityTypeEnabledWindowsMDMMigration{} + } else { + act = fleet.ActivityTypeDisabledWindowsMDMMigration{} + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), act); err != nil { + return nil, ctxerr.Wrapf(ctx, err, "create activity %s", act.ActivityName()) + } + } + return obfuscatedAppConfig, nil } @@ -958,6 +979,9 @@ func (svc *Service) validateMDM( if mdm.MacOSSetup.EnableEndUserAuthentication && oldMdm.MacOSSetup.EnableEndUserAuthentication != mdm.MacOSSetup.EnableEndUserAuthentication && !license.IsPremium() { invalid.Append("macos_setup.enable_end_user_authentication", ErrMissingLicense.Error()) } + if mdm.WindowsMigrationEnabled && !license.IsPremium() { + invalid.Append("windows_migration_enabled", ErrMissingLicense.Error()) + } // we want to use `oldMdm` here as this boolean is set by the fleet // server at startup and can't be modified by the user @@ -1133,6 +1157,9 @@ func (svc *Service) validateMDM( return nil } } + if !mdm.WindowsEnabledAndConfigured && mdm.WindowsMigrationEnabled { + invalid.Append("mdm.windows_migration_enabled", "Couldn't enable Windows MDM migration, Windows MDM is not enabled.") + } return nil } diff --git a/server/service/client.go b/server/service/client.go index 2a3b1a6c5d64..935ca0743433 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -1512,6 +1512,11 @@ func (c *Client) DoGitOps( } else { mdmAppConfig["windows_enabled_and_configured"] = false } + // Put in default values for windows_migration_enabled + mdmAppConfig["windows_migration_enabled"] = config.Controls.WindowsMigrationEnabled + if config.Controls.WindowsMigrationEnabled == nil { + mdmAppConfig["windows_migration_enabled"] = false + } if windowsEnabledAndConfiguredAssumption, ok := mdmAppConfig["windows_enabled_and_configured"].(bool); ok { teamAssumptions = &fleet.TeamSpecsDryRunAssumptions{ WindowsEnabledAndConfigured: optjson.SetBool(windowsEnabledAndConfiguredAssumption), diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 0dfab1f02f1c..73171dfb3c81 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -6314,6 +6314,12 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() { }`), http.StatusUnprocessableEntity) errMsg = extractServerErrorText(res.Body) require.Contains(t, errMsg, "missing or invalid license") + + res = s.Do("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "windows_migration_enabled": true } + }`), http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "missing or invalid license") } func (s *integrationTestSuite) TestScriptsEndpointsWithoutLicense() { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 93bac5241052..dda4f93bf85b 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -15353,3 +15353,13 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { require.NoError(t, err) require.Equal(t, req.PostInstallScript, string(postinstall)) } + +func (s *integrationEnterpriseTestSuite) TestWindowsMigrateMDMNotEnabled() { + t := s.T() + + res := s.Do("PATCH", "/api/v1/fleet/config", json.RawMessage(`{ + "mdm": { "windows_migration_enabled": true } + }`), http.StatusUnprocessableEntity) + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Windows MDM is not enabled") +} diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index d8d2f6390dcd..14b26700d0dd 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -11896,6 +11896,88 @@ func (s *integrationMDMTestSuite) TestSetupExperience() { require.True(t, awaitingConfig) } +func (s *integrationMDMTestSuite) TestWindowsMigrationEnabled() { + t := s.T() + + var acResp appConfigResponse + s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + require.False(t, acResp.MDM.WindowsMigrationEnabled) + + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_migration_enabled": true + } + }`), http.StatusOK, &acResp) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + require.True(t, acResp.MDM.WindowsMigrationEnabled) + s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", 0) + + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_migration_enabled": false + } + }`), http.StatusOK, &acResp) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + require.False(t, acResp.MDM.WindowsMigrationEnabled) + s.lastActivityMatches(fleet.ActivityTypeDisabledWindowsMDMMigration{}.ActivityName(), "", 0) + + // set migrations back to true to see if they turn false when turning MDM off + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_migration_enabled": true + } + }`), http.StatusOK, &acResp) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + require.True(t, acResp.MDM.WindowsMigrationEnabled) + lastEnabledID := s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", 0) + + // not providing any mdm update should leave the current values + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": {} + }`), http.StatusOK, &acResp) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + require.True(t, acResp.MDM.WindowsMigrationEnabled) + // no new activity was created + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", lastEnabledID) + + // set to true again does not generate a new activity, was already true + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_migration_enabled": true + } + }`), http.StatusOK, &acResp) + require.True(t, acResp.MDM.WindowsEnabledAndConfigured) + require.True(t, acResp.MDM.WindowsMigrationEnabled) + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", lastEnabledID) + + res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_enabled_and_configured": false, + "windows_migration_enabled": true + } + }`), http.StatusUnprocessableEntity) + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Windows MDM is not enabled") + + // turn off Windows MDM and try to enable migrations in a distinct call + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_enabled_and_configured": false + } + }`), http.StatusOK, &acResp) + require.False(t, acResp.MDM.WindowsEnabledAndConfigured) + require.False(t, acResp.MDM.WindowsMigrationEnabled) + + res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { + "windows_migration_enabled": true + } + }`), http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "Windows MDM is not enabled") +} + func (s *integrationMDMTestSuite) TestHostsCantTurnMDMOff() { t := s.T() iOSHost, _ := s.createAppleMobileHostThenEnrollMDM("ios") diff --git a/tools/cloner-check/generated_files/appconfig.txt b/tools/cloner-check/generated_files/appconfig.txt index 2454027fa9d2..6c48aadd230a 100644 --- a/tools/cloner-check/generated_files/appconfig.txt +++ b/tools/cloner-check/generated_files/appconfig.txt @@ -158,6 +158,7 @@ github.com/fleetdm/fleet/v4/server/fleet/MDM MacOSMigration fleet.MacOSMigration github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration Enable bool github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration Mode fleet.MacOSMigrationMode string github.com/fleetdm/fleet/v4/server/fleet/MacOSMigration WebhookURL string +github.com/fleetdm/fleet/v4/server/fleet/MDM WindowsMigrationEnabled bool github.com/fleetdm/fleet/v4/server/fleet/MDM EndUserAuthentication fleet.MDMEndUserAuthentication github.com/fleetdm/fleet/v4/server/fleet/MDMEndUserAuthentication SSOProviderSettings fleet.SSOProviderSettings github.com/fleetdm/fleet/v4/server/fleet/MDM WindowsEnabledAndConfigured bool From c27c859b3aa48eb81f0cf0befa70df67ef32a802 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Mon, 2 Dec 2024 09:14:10 -0500 Subject: [PATCH 3/5] Windows MDM migration: implement fleetd notification and migration (#24185) --- ee/server/service/devices.go | 4 +- .../22898-support-windows-mdm-migration | 1 + orbit/pkg/update/execwinapi_windows.go | 3 +- orbit/pkg/update/notifications.go | 32 +++-- orbit/pkg/update/notifications_test.go | 30 +++-- server/fleet/hosts.go | 2 +- server/fleet/mdm.go | 5 + server/fleet/orbit.go | 7 +- server/service/integration_mdm_test.go | 117 ++++++++++++++---- server/service/microsoft_mdm.go | 14 ++- server/service/orbit.go | 15 ++- server/service/osquery.go | 3 +- server/service/osquery_test.go | 10 +- server/service/osquery_utils/queries.go | 14 ++- tools/mdm/windows/poc-mdm-server/README.md | 17 ++- 15 files changed, 209 insertions(+), 65 deletions(-) create mode 100644 orbit/changes/22898-support-windows-mdm-migration diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 77a6c7ce46fe..b0752ef0ec89 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -17,8 +17,6 @@ func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([ return svc.ds.ListPoliciesForHost(ctx, host) } -const refetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute - // TriggerMigrateMDMDevice triggers the webhook associated with the MDM // migration to Fleet configuration. It is located in the ee package instead of // the server/webhooks one because it is a Fleet Premium only feature and for @@ -88,7 +86,7 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos // if the webhook was successfully triggered, we update the host to // constantly run the query to check if it has been unenrolled from its // existing third-party MDM. - refetchUntil := svc.clock.Now().Add(refetchMDMUnenrollCriticalQueryDuration) + refetchUntil := svc.clock.Now().Add(fleet.RefetchMDMUnenrollCriticalQueryDuration) host.RefetchCriticalQueriesUntil = &refetchUntil if err := svc.ds.UpdateHostRefetchCriticalQueriesUntil(ctx, host.ID, &refetchUntil); err != nil { return ctxerr.Wrap(ctx, err, "save host with refetch critical queries timestamp") diff --git a/orbit/changes/22898-support-windows-mdm-migration b/orbit/changes/22898-support-windows-mdm-migration new file mode 100644 index 000000000000..e4e0401d2ba4 --- /dev/null +++ b/orbit/changes/22898-support-windows-mdm-migration @@ -0,0 +1 @@ +* Added support to migrate the MDM provider of Windows devices to Fleet. diff --git a/orbit/pkg/update/execwinapi_windows.go b/orbit/pkg/update/execwinapi_windows.go index 3c0988a0fcff..d07e50192f37 100644 --- a/orbit/pkg/update/execwinapi_windows.go +++ b/orbit/pkg/update/execwinapi_windows.go @@ -175,7 +175,8 @@ func generateWindowsMDMAccessTokenPayload(args WindowsMDMEnrollmentArgs) ([]byte return json.Marshal(pld) } -// IsRunningOnWindowsServer determines if the process is running on a Windows server. Exported so it can be used across packages. +// IsRunningOnWindowsServer determines if the process is running on a Windows +// server. Exported so it can be used across packages. func IsRunningOnWindowsServer() (bool, error) { installType, err := readInstallationType() if err != nil { diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go index cd5f25645834..8e1b1003a6bf 100644 --- a/orbit/pkg/update/notifications.go +++ b/orbit/pkg/update/notifications.go @@ -165,14 +165,22 @@ func ApplyWindowsMDMEnrollmentFetcherMiddleware( var errIsWindowsServer = errors.New("device is a Windows Server") -// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet -// server set the "needs windows enrollment" flag to true, executes the command -// to enroll into Windows MDM (or not, if the device is a Windows Server). +// Run checks if the fleet server set the "needs windows {un}enrollment" flag +// to true, and executes the command to {un}enroll into Windows MDM (or not, if +// the device is a Windows Server). It also unenrolls the device if the flag +// "needs MDM migration" is set to true, so that the device can then be +// enrolled in Fleet MDM. func (w *windowsMDMEnrollmentConfigReceiver) Run(cfg *fleet.OrbitConfig) error { - if cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment { + switch { + case cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment: w.attemptEnrollment(cfg.Notifications) - } else if cfg.Notifications.NeedsProgrammaticWindowsMDMUnenrollment { - w.attemptUnenrollment() + case cfg.Notifications.NeedsProgrammaticWindowsMDMUnenrollment, + cfg.Notifications.NeedsMDMMigration: + label := "unenroll" + if cfg.Notifications.NeedsMDMMigration { + label = "migrate" + } + w.attemptUnenrollment(label) } return nil } @@ -227,18 +235,18 @@ func (w *windowsMDMEnrollmentConfigReceiver) attemptEnrollment(notifs fleet.Orbi } } -func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment() { +func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment(actionLabel string) { if w.mu.TryLock() { defer w.mu.Unlock() // do not unenroll Windows Servers, and do not attempt unenrollment if the // last run is not at least Frequency ago. if w.isWindowsServer { - log.Debug().Msg("skipped calling UnregisterDeviceWithManagement to unenroll Windows device, device is a server") + log.Debug().Msgf("skipped calling UnregisterDeviceWithManagement to %s Windows device, device is a server", actionLabel) return } if time.Since(w.lastUnenrollRun) <= w.Frequency { - log.Debug().Msg("skipped calling UnregisterDeviceWithManagement to unenroll Windows device, last run was too recent") + log.Debug().Msgf("skipped calling UnregisterDeviceWithManagement to %s Windows device, last run was too recent", actionLabel) return } @@ -252,15 +260,15 @@ func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment() { if err := fn(args); err != nil { if errors.Is(err, errIsWindowsServer) { w.isWindowsServer = true - log.Info().Msg("device is a Windows Server, skipping unenrollment") + log.Info().Msgf("device is a Windows Server, skipping %s", actionLabel) } else { - log.Info().Err(err).Msg("calling UnregisterDeviceWithManagement to unenroll Windows device failed") + log.Info().Err(err).Msgf("calling UnregisterDeviceWithManagement to %s Windows device failed", actionLabel) } return } w.lastUnenrollRun = time.Now() - log.Info().Msg("successfully called UnregisterDeviceWithManagement to unenroll Windows device") + log.Info().Msgf("successfully called UnregisterDeviceWithManagement to %s Windows device", actionLabel) } } diff --git a/orbit/pkg/update/notifications_test.go b/orbit/pkg/update/notifications_test.go index 1455d8fc7e5b..a16b9dfbf15d 100644 --- a/orbit/pkg/update/notifications_test.go +++ b/orbit/pkg/update/notifications_test.go @@ -191,21 +191,27 @@ func TestWindowsMDMEnrollment(t *testing.T) { desc string enrollFlag *bool unenrollFlag *bool + migrateFlag *bool discoveryURL string apiErr error wantAPICalled bool wantLog string }{ - {"enroll=false", ptr.Bool(false), nil, "", nil, false, ""}, - {"enroll=true,discovery=''", ptr.Bool(true), nil, "", nil, false, "discovery endpoint is empty"}, - {"enroll=true,discovery!='',success", ptr.Bool(true), nil, "http://example.com", nil, true, "successfully called RegisterDeviceWithManagement"}, - {"enroll=true,discovery!='',fail", ptr.Bool(true), nil, "http://example.com", io.ErrUnexpectedEOF, true, "enroll Windows device failed"}, - {"enroll=true,discovery!='',server", ptr.Bool(true), nil, "http://example.com", errIsWindowsServer, true, "device is a Windows Server, skipping enrollment"}, - - {"unenroll=false", nil, ptr.Bool(false), "", nil, false, ""}, - {"unenroll=true,success", nil, ptr.Bool(true), "", nil, true, "successfully called UnregisterDeviceWithManagement"}, - {"unenroll=true,fail", nil, ptr.Bool(true), "", io.ErrUnexpectedEOF, true, "unenroll Windows device failed"}, - {"unenroll=true,server", nil, ptr.Bool(true), "", errIsWindowsServer, true, "device is a Windows Server, skipping unenrollment"}, + {"enroll=false", ptr.Bool(false), nil, nil, "", nil, false, ""}, + {"enroll=true,discovery=''", ptr.Bool(true), nil, nil, "", nil, false, "discovery endpoint is empty"}, + {"enroll=true,discovery!='',success", ptr.Bool(true), nil, nil, "http://example.com", nil, true, "successfully called RegisterDeviceWithManagement"}, + {"enroll=true,discovery!='',fail", ptr.Bool(true), nil, nil, "http://example.com", io.ErrUnexpectedEOF, true, "enroll Windows device failed"}, + {"enroll=true,discovery!='',server", ptr.Bool(true), nil, nil, "http://example.com", errIsWindowsServer, true, "device is a Windows Server, skipping enrollment"}, + + {"unenroll=false", nil, ptr.Bool(false), nil, "", nil, false, ""}, + {"unenroll=true,success", nil, ptr.Bool(true), nil, "", nil, true, "successfully called UnregisterDeviceWithManagement to unenroll"}, + {"unenroll=true,fail", nil, ptr.Bool(true), nil, "", io.ErrUnexpectedEOF, true, "unenroll Windows device failed"}, + {"unenroll=true,server", nil, ptr.Bool(true), nil, "", errIsWindowsServer, true, "device is a Windows Server, skipping unenroll"}, + + {"migrate=false", nil, nil, ptr.Bool(false), "", nil, false, ""}, + {"migrate=true,success", nil, nil, ptr.Bool(true), "", nil, true, "successfully called UnregisterDeviceWithManagement to migrate"}, + {"migrate=true,fail", nil, nil, ptr.Bool(true), "", io.ErrUnexpectedEOF, true, "migrate Windows device failed"}, + {"migrate=true,server", nil, nil, ptr.Bool(true), "", errIsWindowsServer, true, "device is a Windows Server, skipping migrate"}, } for _, c := range cases { @@ -215,12 +221,14 @@ func TestWindowsMDMEnrollment(t *testing.T) { var ( enroll = c.enrollFlag != nil && *c.enrollFlag unenroll = c.unenrollFlag != nil && *c.unenrollFlag + migrate = c.migrateFlag != nil && *c.migrateFlag isUnenroll = c.unenrollFlag != nil ) testConfig := &fleet.OrbitConfig{Notifications: fleet.OrbitConfigNotifications{ NeedsProgrammaticWindowsMDMEnrollment: enroll, NeedsProgrammaticWindowsMDMUnenrollment: unenroll, + NeedsMDMMigration: migrate, WindowsMDMDiscoveryEndpoint: c.discoveryURL, }} @@ -241,7 +249,7 @@ func TestWindowsMDMEnrollment(t *testing.T) { err := enrollReceiver.Run(testConfig) require.NoError(t, err) // the dummy receiver never returns an error - if isUnenroll { + if isUnenroll || migrate { require.Equal(t, c.wantAPICalled, unenrollGotCalled) require.False(t, enrollGotCalled) } else { diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 95ce9ee268e8..d55e4ff3ceb0 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -344,7 +344,7 @@ type Host struct { // is that the latter is a one-time request, while this one is a persistent // until the timestamp expires. The initial use-case is to check for a host // to be unenrolled from its old MDM solution, in the "migrate to Fleet MDM" - // workflow. + // workflow (both Apple and Windows). // // In the future, if we want to use it for more than one use-case, we could // add a "reason" field with well-known labels so we know what condition(s) diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 4d3e09f68ec8..b441067398b8 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -18,6 +18,11 @@ const ( MDMAppleDeclarationUUIDPrefix = "d" MDMAppleProfileUUIDPrefix = "a" MDMWindowsProfileUUIDPrefix = "w" + + // RefetchMDMUnenrollCriticalQueryDuration is the duration to set the + // RefetchCriticalQueriesUntil field when migrating a device from a + // third-party MDM solution to Fleet. + RefetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute ) type AppleMDM struct { diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index 6c06a963e263..357af033a6c5 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -8,7 +8,12 @@ import "encoding/json" type OrbitConfigNotifications struct { RenewEnrollmentProfile bool `json:"renew_enrollment_profile,omitempty"` RotateDiskEncryptionKey bool `json:"rotate_disk_encryption_key,omitempty"` - NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"` + + // NeedsMDMMigration is set to true if MDM is enabled for the host's + // platform, MDM migration is enabled for that platform, and the host is + // eligible for such a migration (e.g. it is enrolled in a third-party MDM + // solution). + NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"` // NeedsProgrammaticWindowsMDMEnrollment is sent as true if Windows MDM is // enabled and the device should be enrolled as far as the server knows (e.g. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 14b26700d0dd..4a4230cd31c0 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -5926,7 +5926,6 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { err = s.ds.SaveAppConfig(context.Background(), appConf) require.NoError(s.T(), err) - // the feature flag is enabled for the MDM test suite var acResp appConfigResponse s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) assert.False(t, acResp.MDM.WindowsEnabledAndConfigured) @@ -5937,55 +5936,102 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"}) require.NoError(t, err) + // enable Windows MDM + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "windows_enabled_and_configured": true } + }`), http.StatusOK, &acResp) + assert.True(t, acResp.MDM.WindowsEnabledAndConfigured) + assert.False(t, acResp.MDM.WindowsMigrationEnabled) + s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDM{}.ActivityName(), `{}`, 0) + // create some hosts - a Windows workstation in each team and no-team, // Windows server in no team, Windows workstation enrolled in a 3rd-party in // team 2, Windows workstation already enrolled in Fleet in no team, and a // macOS host in no team. metadataHosts := []struct { - os string - suffix string - isServer bool - teamID *uint - enrolledName string - shouldEnroll bool + os string + suffix string + isServer bool + teamID *uint + enrolledName string + shouldEnroll bool + shouldMigrate bool }{ - {"windows", "win-no-team", false, nil, "", true}, - {"windows", "win-team-1", false, &tm1.ID, "", true}, - {"windows", "win-team-2", false, &tm2.ID, "", true}, - {"windows", "win-server", true, nil, "", false}, // is a server - {"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false}, // is enrolled in 3rd-party - {"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false}, // is already Fleet-enrolled - {"darwin", "macos-no-team", false, nil, "", false}, // is not Windows + {"windows", "win-no-team", false, nil, "", true, false}, + {"windows", "win-team-1", false, &tm1.ID, "", true, false}, + {"windows", "win-team-2", false, &tm2.ID, "", true, false}, + {"windows", "win-server", true, nil, "", false, false}, // is a server + {"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false, true}, // is enrolled in 3rd-party + {"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false, false}, // is already Fleet-enrolled + {"darwin", "macos-no-team", false, nil, "", false, false}, // is not Windows + {"windows", "win-server-third-party", true, nil, fleet.WellKnownMDMSimpleMDM, false, false}, // is enrolled in 3rd-party, but is a server } hostsBySuffix := make(map[string]*fleet.Host, len(metadataHosts)) for _, meta := range metadataHosts { - h := createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds) - createDeviceTokenForHost(t, s.ds, h.ID, meta.suffix) - err := s.ds.SetOrUpdateMDMData(ctx, h.ID, meta.isServer, meta.enrolledName != "", "https://example.com", false, meta.enrolledName, "") - require.NoError(t, err) + var host *fleet.Host + if meta.os == "windows" && meta.enrolledName == fleet.WellKnownMDMFleet { + // special-case to create a properly MDM-enrolled into Fleet host + host = createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds) + mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *host.OrbitNodeKey) + err := mdmDevice.Enroll() + require.NoError(t, err) + err = s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, mdmDevice.DeviceID) + require.NoError(t, err) + err = s.ds.SetOrUpdateMDMData(ctx, host.ID, meta.isServer, true, s.server.URL, false, fleet.WellKnownMDMFleet, "") + require.NoError(t, err) + } else { + host = createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds) + createDeviceTokenForHost(t, s.ds, host.ID, meta.suffix) + + serverURL := "https://example.com" + err := s.ds.SetOrUpdateMDMData(ctx, host.ID, meta.isServer, meta.enrolledName != "", serverURL, false, meta.enrolledName, "") + require.NoError(t, err) + } + if meta.teamID != nil { - err = s.ds.AddHostsToTeam(ctx, meta.teamID, []uint{h.ID}) + err = s.ds.AddHostsToTeam(ctx, meta.teamID, []uint{host.ID}) require.NoError(t, err) } - hostsBySuffix[meta.suffix] = h + hostsBySuffix[meta.suffix] = host } - // enable Windows MDM + // get the orbit config for each host, verify that only the expected ones + // receive the "needs enrollment to Windows MDM" notification. + for _, meta := range metadataHosts { + var resp orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", + json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)), + http.StatusOK, &resp) + require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + require.False(t, resp.Notifications.NeedsMDMMigration) + if meta.shouldEnroll { + require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath) + } else { + require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) + } + } + + // enable Windows MDM migration acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ - "mdm": { "windows_enabled_and_configured": true } + "mdm": { "windows_migration_enabled": true } }`), http.StatusOK, &acResp) assert.True(t, acResp.MDM.WindowsEnabledAndConfigured) - s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDM{}.ActivityName(), `{}`, 0) + assert.True(t, acResp.MDM.WindowsMigrationEnabled) + s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), `{}`, 0) // get the orbit config for each host, verify that only the expected ones - // receive the "needs enrollment to Windows MDM" notification. + // receive the "needs enrollment to Windows MDM" and "needs migration" notifications. + // They still get enrollment notifications as we have not proceeded with enrollment. for _, meta := range metadataHosts { var resp orbitGetConfigResponse s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)), http.StatusOK, &resp) require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.Equal(t, meta.shouldMigrate, resp.Notifications.NeedsMDMMigration) require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) if meta.shouldEnroll { require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath) @@ -5994,7 +6040,7 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { } } - // turn on MDM for a host + // turn on MDM for another host orbitHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t) // disable Microsoft MDM @@ -6002,9 +6048,10 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { "mdm": { "windows_enabled_and_configured": false } }`), http.StatusOK, &acResp) assert.False(t, acResp.MDM.WindowsEnabledAndConfigured) + assert.False(t, acResp.MDM.WindowsMigrationEnabled) s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledWindowsMDM{}.ActivityName(), `{}`, 0) - // get the orbit config for win-no-team should return true for the + // get the orbit config for that MDM-enrolled host returns true for the // unenrollment notification var resp orbitGetConfigResponse s.DoJSON("POST", "/api/fleet/orbit/config", @@ -6012,7 +6059,25 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { http.StatusOK, &resp) require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.False(t, resp.Notifications.NeedsMDMMigration) require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) + + // get the orbit config for each host, only the fleet-enrolled ones get the unenrollment, + // and none get enrollment/migration (because MDM is now off). + for _, meta := range metadataHosts { + var resp orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", + json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)), + http.StatusOK, &resp) + require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.False(t, resp.Notifications.NeedsMDMMigration) + if meta.enrolledName == fleet.WellKnownMDMFleet { + require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + } else { + require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + } + require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) + } } func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 78fde975ffb8..4d79d45ce83f 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -615,14 +615,22 @@ func NewCertStoreProvisioningData(enrollmentType string, identityFingerprint str return certStore } -// IsEligibleForWindowsMDMEnrollment returns true if the host can be enrolled +// isEligibleForWindowsMDMEnrollment returns true if the host can be enrolled // in Fleet's Windows MDM (if it was enabled). -func IsEligibleForWindowsMDMEnrollment(host *fleet.Host, mdmInfo *fleet.HostMDM) bool { +func isEligibleForWindowsMDMEnrollment(host *fleet.Host, mdmInfo *fleet.HostMDM) bool { return host.FleetPlatform() == "windows" && host.IsOsqueryEnrolled() && (mdmInfo == nil || (!mdmInfo.IsServer && !mdmInfo.Enrolled)) } +// isEligibleForWindowsMDMMigration returns true if the host can be migrated to +// Fleet's Windows MDM (if it was enabled). +func isEligibleForWindowsMDMMigration(host *fleet.Host, mdmInfo *fleet.HostMDM) bool { + return host.FleetPlatform() == "windows" && + host.IsOsqueryEnrolled() && + (mdmInfo != nil && !mdmInfo.IsServer && mdmInfo.Enrolled && mdmInfo.Name != fleet.WellKnownMDMFleet) +} + // NewApplicationProvisioningData returns a new ApplicationProvisioningData Characteristic // The Application Provisioning configuration is used for bootstrapping a device with an OMA DM account // The paramenters here maps to the W7 application CSP @@ -976,7 +984,7 @@ func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *flee } // This ensures that only hosts that are eligible for Windows enrollment can be enrolled - if !IsEligibleForWindowsMDMEnrollment(host, mdmInfo) { + if !isEligibleForWindowsMDMEnrollment(host, mdmInfo) { return "", "", errors.New("host is not elegible for Windows MDM enrollment") } diff --git a/server/service/orbit.go b/server/service/orbit.go index df75ce21c5f0..1be699ad47eb 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -268,13 +268,26 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro // set the host's orbit notifications for Windows MDM if appConfig.MDM.WindowsEnabledAndConfigured { - if IsEligibleForWindowsMDMEnrollment(host, mdmInfo) { + if isEligibleForWindowsMDMEnrollment(host, mdmInfo) { discoURL, err := microsoft_mdm.ResolveWindowsMDMDiscovery(appConfig.ServerSettings.ServerURL) if err != nil { return fleet.OrbitConfig{}, err } notifs.WindowsMDMDiscoveryEndpoint = discoURL notifs.NeedsProgrammaticWindowsMDMEnrollment = true + } else if appConfig.MDM.WindowsMigrationEnabled && isEligibleForWindowsMDMMigration(host, mdmInfo) { + notifs.NeedsMDMMigration = true + + // Set the host to refetch the "critical queries" quickly for some time, + // to improve ingestion time of the unenroll and make the host eligible to + // enroll into Fleet faster. + if host.RefetchCriticalQueriesUntil == nil { + refetchUntil := svc.clock.Now().Add(fleet.RefetchMDMUnenrollCriticalQueryDuration) + host.RefetchCriticalQueriesUntil = &refetchUntil + if err := svc.ds.UpdateHostRefetchCriticalQueriesUntil(ctx, host.ID, &refetchUntil); err != nil { + return fleet.OrbitConfig{}, err + } + } } } if !appConfig.MDM.WindowsEnabledAndConfigured { diff --git a/server/service/osquery.go b/server/service/osquery.go index 21555df37270..ce88aa625003 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -691,7 +691,8 @@ const alwaysTrueQuery = "SELECT 1" // list of detail queries that are returned when only the critical queries // should be returned (due to RefetchCriticalQueriesUntil timestamp being set). var criticalDetailQueries = map[string]bool{ - "mdm": true, + "mdm": true, + "mdm_windows": true, } // detailQueriesForHost returns the map of detail+additional queries that should be executed by diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 5be2974a12ae..d0f37e1e9071 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -179,7 +179,7 @@ func TestGetClientConfig(t *testing.T) { // Check scheduled queries are loaded properly conf, err = svc.GetClientConfig(ctx3) require.NoError(t, err) - assert.JSONEq(t, `{ + assert.JSONEq(t, `{ "pack_by_label": { "queries":{ "time":{"query":"select * from time","interval":30,"removed":false} @@ -208,7 +208,7 @@ func TestGetClientConfig(t *testing.T) { "version": "" } } - } + } }`, string(conf["packs"].(json.RawMessage)), ) @@ -1165,8 +1165,12 @@ func TestHostDetailQueries(t *testing.T) { host.RefetchCriticalQueriesUntil = ptr.Time(mockClock.Now().Add(1 * time.Minute)) queries, discovery, err = svc.detailQueriesForHost(ctx, &host) require.NoError(t, err) - require.Equal(t, len(criticalDetailQueries), len(queries), distQueriesMapKeys(queries)) + // host is darwin so it gets only the darwin critical query + require.Equal(t, 1, len(queries), distQueriesMapKeys(queries)) for name := range criticalDetailQueries { + if strings.HasSuffix(name, "_windows") { + continue + } assert.Contains(t, queries, hostDetailQueryPrefix+name) } verifyDiscovery(t, queries, discovery) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index da62dfef17a6..8f7600c16c5f 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -1885,6 +1885,11 @@ func directIngestMDMWindows(ctx context.Context, logger log.Logger, host *fleet. return nil } + if host.RefetchCriticalQueriesUntil != nil { + level.Debug(logger).Log("msg", "ingesting Windows mdm data during refetch critical queries window", "host_id", host.ID, + "data", fmt.Sprintf("%+v", rows)) + } + data := rows[0] var enrolled bool var automatic bool @@ -1900,13 +1905,20 @@ func directIngestMDMWindows(ctx context.Context, logger log.Logger, host *fleet. } isServer := strings.Contains(strings.ToLower(data["installation_type"]), "server") + mdmSolutionName := deduceMDMNameWindows(data) + if !enrolled && mdmSolutionName != fleet.WellKnownMDMFleet && host.RefetchCriticalQueriesUntil != nil { + // the host was unenrolled from a non-Fleet MDM solution, and the refetch + // critical queries timestamp was set, so clear it. + host.RefetchCriticalQueriesUntil = nil + } + return ds.SetOrUpdateMDMData(ctx, host.ID, isServer, enrolled, serverURL, automatic, - deduceMDMNameWindows(data), + mdmSolutionName, "", ) } diff --git a/tools/mdm/windows/poc-mdm-server/README.md b/tools/mdm/windows/poc-mdm-server/README.md index 74760b2de26d..eebc49061ad4 100644 --- a/tools/mdm/windows/poc-mdm-server/README.md +++ b/tools/mdm/windows/poc-mdm-server/README.md @@ -23,6 +23,17 @@ This code is MIT licensed and it was forked from [here](https://github.com/oscar ## Usage On the server side, you just need to run the project using the already provided cert and keys. The certificate is in `.pfx` file format, so you need to extract the certificate and key first, see https://stackoverflow.com/a/59120388/1094941. +The "Import password" is "testpassword", and the names of the output files matter, on Linux something like this works (assuming you are in the certs/ directory): + +``` +# for the cert +$ openssl pkcs12 -in dev_cert_mdmwindows_com.pfx -clcerts -nokeys -out dev_cert_mdmwindows_com_cert.pem + +# for the key +$ openssl pkcs12 -in dev_cert_mdmwindows_com.pfx -out dev_cert_mdmwindows_com.key -nocerts -nodes +``` + +Note that an asn1 error might occur when running the server, if that's the case you need to patch your local Go toolchain by running `$ go run ./patch/patch.go` (`GOROOT` env var must be set to point to your `go env GOROOT` directory). It may require `sudo` depending on where your `go` installation is (due to https://github.com/golang/go/issues/14017). Next go to the project folder and run. @@ -30,7 +41,9 @@ Next go to the project folder and run. go run . ``` -On the Windows client side, you need to import a custom CA certificate to the certificate store, and populate the `hosts` file before running the Windows Enrollment. The certificate to import is on the certs directory and it is called `dev_cert_mdmwindows_com.pfx`. You need to copy this certificate to the client machine and run the powershell command below. This is required because the project uses a local dev https endpoint. +Note that the server binds to the standard and usually firewall-protected `443` port, so you may need to configure your firewall to allow connections to it for the duration of your test. + +On the Windows client side, you need to import the custom CA certificate to the certificate store, and populate the `hosts` file before running the Windows Enrollment. The certificate to import is on the certs directory and it is called `dev_cert_mdmwindows_com.pfx`. You need to copy this certificate to the client machine and run the powershell command below (in the console, not in a powershell terminal). This is required because the project uses a local dev https endpoint. 1) Import certificate to Trusted CAs repository (be sure to update the path to the pfx certificate) @@ -42,6 +55,8 @@ On the Windows client side, you need to import a custom CA certificate to the ce echo autodiscovery.mdmwindows.com >> %SystemRoot%\System32\drivers\etc\hosts echo enterpriseenrollment.mdmwindows.com >> %SystemRoot%\System32\drivers\etc\hosts +To enroll the device into this MDM server, go to `Settings > Accounts > Access work or school` and click the connect button, enter the email provided to the server when you ran `go run .` (default: `demo@mdmwindows.com`) and it should automatically detect the server and proceed with enrollment. This is why the server must run on port `:443`, because it uses automatic discovery and will not attempt a custom port. + ## Protocol Details Below is the raw https exchange of the MS-MDE and MS-MDM protocols when run using the -verbose mode: From 1677a0ae29862f9fc5f9ddab42739bd22151d306 Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Mon, 2 Dec 2024 12:24:28 -0500 Subject: [PATCH 4/5] Windows Migration: leave checkbox disabled if non premium (#24272) --- .../cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx index e7b5b814a4ac..27dbdddc1516 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage.tsx @@ -72,7 +72,7 @@ interface IWindowsMdmPageProps { } const WindowsMdmPage = ({ router }: IWindowsMdmPageProps) => { - const { config } = useContext(AppContext); + const { config, isPremiumTier } = useContext(AppContext); const [mdmOn, setMdmOn] = useState( config?.mdm?.windows_enabled_and_configured ?? false @@ -124,9 +124,12 @@ const WindowsMdmPage = ({ router }: IWindowsMdmPageProps) => { />

{descriptionText}

Automatically migrate hosts connected to another MDM solution From 4d732113e0ccf3a4e8f0628c840ff67915511b5d Mon Sep 17 00:00:00 2001 From: Martin Angers Date: Mon, 2 Dec 2024 15:30:51 -0500 Subject: [PATCH 5/5] Windows Migration: support the new activities in the UI (#24279) --- frontend/interfaces/activity.ts | 2 ++ .../ActivityItem/ActivityItem.tsx | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index d6fcafc8c7ce..14618ad9e2cf 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -69,6 +69,8 @@ export enum ActivityType { TransferredHosts = "transferred_hosts", EnabledWindowsMdm = "enabled_windows_mdm", DisabledWindowsMdm = "disabled_windows_mdm", + EnabledWindowsMdmMigration = "enabled_windows_mdm_migration", + DisabledWindowsMdmMigration = "disabled_windows_mdm_migration", RanScript = "ran_script", AddedScript = "added_script", DeletedScript = "deleted_script", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index b162a22fcc9f..5e724059bf7e 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -37,6 +37,8 @@ const PREMIUM_ACTIVITIES = new Set([ "enabled_macos_setup_end_user_auth", "disabled_macos_setup_end_user_auth", "tranferred_hosts", + "enabled_windows_mdm_migration", + "disabled_windows_mdm_migration", ]); const getProfileMessageSuffix = ( @@ -663,6 +665,24 @@ const TAGGED_TEMPLATES = { disabledWindowsMdm: () => { return <> told Fleet to turn off Windows MDM features.; }, + enabledWindowsMdmMigration: () => { + return ( + <> + {" "} + told Fleet to automatically migrate Windows hosts connected to another + MDM solution. + + ); + }, + disabledWindowsMdmMigration: () => { + return ( + <> + {" "} + told Fleet to stop migrating Windows hosts connected to another MDM + solution. + + ); + }, // TODO: Combine ranScript template with host details page templates // frontend/pages/hosts/details/cards/Activity/PastActivity/PastActivity.tsx and // frontend/pages/hosts/details/cards/Activity/UpcomingActivity/UpcomingActivity.tsx @@ -1262,6 +1282,12 @@ const getDetail = ( case ActivityType.DisabledWindowsMdm: { return TAGGED_TEMPLATES.disabledWindowsMdm(); } + case ActivityType.EnabledWindowsMdmMigration: { + return TAGGED_TEMPLATES.enabledWindowsMdmMigration(); + } + case ActivityType.DisabledWindowsMdmMigration: { + return TAGGED_TEMPLATES.disabledWindowsMdmMigration(); + } case ActivityType.RanScript: { return TAGGED_TEMPLATES.ranScript(activity, onDetailsClick); }