diff --git a/.github/workflows/dogfood-gitops.yml b/.github/workflows/dogfood-gitops.yml index 8b2e3217c0..487cdac1ad 100644 --- a/.github/workflows/dogfood-gitops.yml +++ b/.github/workflows/dogfood-gitops.yml @@ -69,6 +69,7 @@ jobs: DOGFOOD_GLOBAL_ENROLL_SECRET: ${{ secrets.DOGFOOD_GLOBAL_ENROLL_SECRET }} DOGFOOD_SSO_ISSUER_URI: ${{ secrets.DOGFOOD_SSO_ISSUER_URI }} DOGFOOD_SSO_METADATA: ${{ secrets.DOGFOOD_SSO_METADATA }} + DOGFOOD_MDM_SSO_METADATA_URL: ${{ secrets.DOGFOOD_MDM_SSO_METADATA_URL }} DOGFOOD_FAILING_POLICIES_WEBHOOK_URL: ${{ secrets.DOGFOOD_FAILING_POLICIES_WEBHOOK_URL }} DOGFOOD_VULNERABILITIES_WEBHOOK_URL: ${{ secrets.DOGFOOD_VULNERABILITIES_WEBHOOK_URL }} DOGFOOD_WORKSTATIONS_ENROLL_SECRET: ${{ secrets.DOGFOOD_WORKSTATIONS_ENROLL_SECRET }} diff --git a/articles/discovering-chrome-ai-using-fleet.md b/articles/discovering-chrome-ai-using-fleet.md index 55c90b4767..72b73627d6 100644 --- a/articles/discovering-chrome-ai-using-fleet.md +++ b/articles/discovering-chrome-ai-using-fleet.md @@ -4,15 +4,15 @@ # Discovering AI in Chrome with Fleet -Staying ahead of technological innovations is crucial for both individuals and organizations. Google Chrome, one of the most widely used web browsers, continually evolves to incorporate advanced features, including artificial intelligence (AI). This article will guide you through detecting if AI capabilities have been enabled in Chrome using Fleet. +Staying ahead of technological innovations is crucial for individuals and organizations. Google Chrome, one of the most widely used web browsers, continually evolves to incorporate new features, including artificial intelligence (AI). This article will guide you through detecting if AI capabilities have been enabled in Chrome using Fleet. ## Introduction to Chrome AI innovations -Google Chrome has integrated AI to enhance user experience by providing intelligent suggestions, improving search results, and offering in-browser assistance. To explore these innovations further, visit the [Chrome AI Innovations page](https://www.google.com/chrome/ai-innovations/). Detecting whether AI features are enabled in your Chrome browser can help you understand and leverage these advancements effectively. +Google Chrome has integrated AI to enhance user experience by providing intelligent suggestions, improving search results, and offering in-browser assistance. Visit the [Chrome AI Innovations page](https://www.google.com/chrome/ai-innovations/) for more infomration. ## Using Fleet to detect AI features in Chrome -Fleet, a comprehensive device management and security tool, allows you to monitor various aspects of your devices, including software configurations and enabled features. Using Fleet, you can detect whether AI features are enabled in Chrome by querying device settings, specifically in the "Preferences" JSON file. +Fleet, a comprehensive device management and security tool, allows you to monitor various aspects of your devices, including software configurations and enabled features. Using Fleet, you can detect whether AI features are enabled in Chrome by querying device settings, specifically in the Chrome "Preferences" JSON file. ### Step 1: Understanding Chrome's preferences JSON file @@ -24,33 +24,31 @@ Chrome stores user settings and configurations in a JSON file at the following p ### Step 2: Identifying AI-related settings -Within this file, AI-related features are stored in the `optimization_guide` section. The `tab_organization_setting_state` field will tell you if AI-based tab management features are enabled: +AI-related features are stored in the `optimization_guide` section of the preferences. The `tab_organization_setting_state` field will tell you if AI-based tab management features are enabled: -- Enabled: - The setting will return `1`. +`> jq` is a lightweight and powerful command-line tool for parsing, filtering, and manipulating JSON data. It allows you to extract specific information from JSON files efficiently. In this case, we use `jq` to locate and read the value of the `tab_organization_setting_state` key within Chrome's preference file which will help us understand how to craft our Fleet query for reporting the state of this setting. + +- If enabled, the setting will return `1`. ![Chrome settings UI with Chrome AI enabled](../website/assets/images/articles/discovering-chrome-ai-using-fleet-1-1472x370@2x.png) ``` -% jq '.optimization_guide.tab_organization_setting_state' /Users/brock/Library/Application\ Support/Google/Chrome/Default/Preferences +% jq '.optimization_guide.tab_organization_setting_state' /Users//Library/Application\ Support/Google/Chrome/Default/Preferences 1 ``` -- Disabled: - The setting will return `2`. +- If disabled, the setting will return `2`. ![Chrome settings UI with Chrome AI disabled](../website/assets/images/articles/discovering-chrome-ai-using-fleet-2-1474x276@2x.png) ``` -% jq '.optimization_guide.tab_organization_setting_state' /Users/brock/Library/Application\ Support/Google/Chrome/Default/Preferences +% jq '.optimization_guide.tab_organization_setting_state' /Users//Library/Application\ Support/Google/Chrome/Default/Preferences 2 ``` -`> jq` is a lightweight and powerful command-line tool for parsing, filtering, and manipulating JSON data. It allows you to extract specific information from JSON files efficiently. In this case, we use `jq` to locate and read the value of the `tab_organization_setting_state` key within Chrome's preference file. - ### Step 3: Query the JSON file with Fleet -To query the JSON file and detect AI features, you can use the following command: +To query the JSON file and detect AI features using Fleet, you can use the following SQL query: ``` SELECT fullkey,path FROM parse_json WHERE path LIKE '/Users/%/Library/Application Support/Google/Chrome/Default/Preferences' AND fullkey='optimization_guide/tab_organization_setting_state'; diff --git a/articles/macos-mdm-setup.md b/articles/macos-mdm-setup.md index 28252b62d1..bc91ee6a72 100644 --- a/articles/macos-mdm-setup.md +++ b/articles/macos-mdm-setup.md @@ -31,7 +31,7 @@ banner at the top of page reminding you to renew your token. To renew an ABM token: 1. Navigate to the **Settings > Integrations > Mobile device management (MDM)** page. -2. Under "Automatic enrollment", click "Edit", and then fin +2. Under "Automatic enrollment", click "Edit", and then find the token that you want to renew. Token status is indicated in the "Renew date" column: tokens less than 30 days from expiring will have a yellow indicator, and expired tokens will have a red indicator. Click the "Actions" dropdown for the token and then click "Renew". Follow the instructions in the modal to download a new token from Apple Business Manager and then upload the new token to Fleet. After connecting Fleet to ABM, set Fleet to be the MDM for all Macs: diff --git a/articles/teams.md b/articles/teams.md index 1b0c8f1cbb..ea688bc08b 100644 --- a/articles/teams.md +++ b/articles/teams.md @@ -2,7 +2,7 @@ _Available in Fleet Premium_ -In Fleet, you can group hosts together in a "team" in Fleet. This way, you can apply queries, policies, scripts, and more that are tailored to the hosts' risk/compliance needs. +In Fleet, you can group hosts together in a "team" in Fleet. This way, you can apply queries, policies, scripts, and more that are tailored to a host's risk/compliance needs. A host can only belong to one team. diff --git a/changes/17558-validation-errs b/changes/17558-validation-errs new file mode 100644 index 0000000000..115c9bf14e --- /dev/null +++ b/changes/17558-validation-errs @@ -0,0 +1,2 @@ +- Adds validation of Setup Assistant profiles on profile upload, giving users immediate feedback on +the validity of the profile. \ No newline at end of file diff --git a/changes/20828-better-appid-error b/changes/20828-better-appid-error new file mode 100644 index 0000000000..540c8fcbfa --- /dev/null +++ b/changes/20828-better-appid-error @@ -0,0 +1 @@ +- Improve clarity of gitops VPP app ID type errors diff --git a/changes/21019-ota-enrollment b/changes/21019-ota-enrollment new file mode 100644 index 0000000000..b43db060a7 --- /dev/null +++ b/changes/21019-ota-enrollment @@ -0,0 +1 @@ +* Implement protocol support for OTA enrollment and automatic team assignment for hosts. diff --git a/changes/21402-improve-windows-mdm-enabled-error-message b/changes/21402-improve-windows-mdm-enabled-error-message new file mode 100644 index 0000000000..36dc6082f6 --- /dev/null +++ b/changes/21402-improve-windows-mdm-enabled-error-message @@ -0,0 +1 @@ +- Improve gitops error message about enabling windows MDM diff --git a/changes/21866-startup-expired-abm-cert b/changes/21866-startup-expired-abm-cert new file mode 100644 index 0000000000..f9e74bb641 --- /dev/null +++ b/changes/21866-startup-expired-abm-cert @@ -0,0 +1,2 @@ +- Fixed issue where Fleet server could start when expired ABM cerfificate was provided as server + config options. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index c8773ba765..19dfd798aa 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -499,7 +499,18 @@ the way that the Fleet server works. mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger) } - // validate Apple APNs/SCEP config + checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) { + _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names) + if err != nil { + if fleet.IsNotFound(err) || errors.Is(err, mysql.ErrPartialResult) { + return false, nil + } + return false, err + } + return true, nil + } + + // reconcile Apple Business Manager configuration environment variables with the database if config.MDM.IsAppleAPNsSet() || config.MDM.IsAppleSCEPSet() { if !config.MDM.IsAppleAPNsSet() { initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), "validate Apple MDM") @@ -508,40 +519,63 @@ the way that the Fleet server works. } if len(config.Server.PrivateKey) == 0 { - initFatal(errors.New("inserting APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + initFatal(errors.New("inserting MDM APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } + // parse the APNs and SCEP assets from the config _, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs() if err != nil { - initFatal(err, "validate Apple APNs certificate and key") + initFatal(err, "parse Apple APNs certificate and key from config") } - _, appleSCEPCertPEM, appleSCEPKeyPEM, err := config.MDM.AppleSCEP() if err != nil { - initFatal(err, "validate Apple SCEP certificate and key") + initFatal(err, "load Apple SCEP certificate and key from config") } - err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ - {Name: fleet.MDMAssetAPNSCert, Value: apnsCertPEM}, - {Name: fleet.MDMAssetAPNSKey, Value: apnsKeyPEM}, - {Name: fleet.MDMAssetCACert, Value: appleSCEPCertPEM}, - {Name: fleet.MDMAssetCAKey, Value: appleSCEPKeyPEM}, - }) - if err != nil { - // duplicate key errors mean that we already - // have a value for those keys in the - // database, fail to initalize on other - // cases. - if !mysql.IsDuplicate(err) { - initFatal(err, "inserting MDM APNs and SCEP assets") - } + // first we'll check if the APNs and SCEP assets are already in the database and + // only insert config values if they're not already present in the database + toInsert := make([]fleet.MDMConfigAsset, 0, 4) - level.Warn(logger).Log("msg", "Your server already has stored SCEP and APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + // check DB for APNs assets + found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetAPNSCert, fleet.MDMAssetAPNSKey}) + switch { + case err != nil: + initFatal(err, "reading APNs assets from database") + case !found: + toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetAPNSCert, Value: apnsCertPEM}, fleet.MDMConfigAsset{Name: fleet.MDMAssetAPNSKey, Value: apnsKeyPEM}) + default: + level.Warn(logger).Log("msg", "Your server already has stored APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + } + + // check DB for SCEP assets + found, err = checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey}) + switch { + case err != nil: + initFatal(err, "reading SCEP assets from database") + case !found: + toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetCACert, Value: appleSCEPCertPEM}, fleet.MDMConfigAsset{Name: fleet.MDMAssetCAKey, Value: appleSCEPKeyPEM}) + default: + level.Warn(logger).Log("msg", "Your server already has stored SCEP certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + } + + if len(toInsert) > 0 { + if len(config.Server.PrivateKey) == 0 { + initFatal(errors.New("inserting APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + if err := ds.InsertMDMConfigAssets(context.Background(), toInsert); err != nil { + if mysql.IsDuplicate(err) { + // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case + level.Debug(logger).Log("msg", "unexpected duplicate key error inserting MDM APNs and SCEP assets") + } else { + initFatal(err, "inserting MDM APNs and SCEP assets") + } + } } } - // validate Apple BM config + // reconcile Apple Business Manager configuration environment variables with the database if config.MDM.IsAppleBMSet() { + // TODO: Confirm whether we should have any fatal license errors if !license.IsPremium() { initFatal(errors.New("Apple Business Manager configuration is only available in Fleet Premium"), "validate Apple BM") } @@ -552,36 +586,38 @@ the way that the Fleet server works. appleBM, err := config.MDM.AppleBM() if err != nil { - initFatal(err, "validate Apple BM token, certificate and key") + initFatal(err, "parse Apple BM token, certificate and key from config") } - err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{ - {Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, - {Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM}, - }) - if err != nil { - // duplicate key errors mean that we already - // have a value for those keys in the - // database, fail to initalize on other - // cases. - if !mysql.IsDuplicate(err) { - initFatal(err, "inserting MDM ABM assets") - } + toInsert := make([]fleet.MDMConfigAsset, 0, 4) + found, err := checkMDMAssets([]fleet.MDMAssetName{fleet.MDMAssetABMKey, fleet.MDMAssetABMCert}) + switch { + case err != nil: + initFatal(err, "reading ABM assets from database") + case !found: + toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM}) + default: level.Warn(logger).Log("msg", "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.") - } else { - // insert the ABM token without any metdata, - // it'll be picked by the - // apple_mdm_dep_profile_assigner cron and - // backfilled - tok := &fleet.ABMToken{ - EncryptedToken: appleBM.EncryptedToken, - // 2000-01-01 is our "zero value" for time - RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), - } - _, err = ds.InsertABMToken(context.Background(), tok) - if err != nil { - initFatal(err, "save ABM token") + } + + if len(toInsert) > 0 { + err := ds.InsertMDMConfigAssets(context.Background(), toInsert) + switch { + case err != nil && mysql.IsDuplicate(err): + // we already checked for existing assets so we should never have a duplicate key error here; we'll add a debug log just in case + level.Debug(logger).Log("msg", "unexpected duplicate key error inserting ABM assets") + case err != nil: + initFatal(err, "inserting ABM assets") + default: + // insert the ABM token without any metdata; it'll be picked by the + // apple_mdm_dep_profile_assigner cron and backfilled + if _, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{ + EncryptedToken: appleBM.EncryptedToken, + RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), // 2000-01-01 is our "zero value" for time + }); err != nil { + initFatal(err, "save ABM token") + } } } } @@ -591,17 +627,6 @@ the way that the Fleet server works. initFatal(err, "loading app config") } - checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) { - _, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names) - if err != nil { - if fleet.IsNotFound(err) || errors.Is(err, mysql.ErrPartialResult) { - return false, nil - } - return false, err - } - return true, nil - } - appCfg.MDM.EnabledAndConfigured = false appCfg.MDM.AppleBMEnabledAndConfigured = false if len(config.Server.PrivateKey) > 0 { @@ -612,7 +637,7 @@ the way that the Fleet server works. fleet.MDMAssetAPNSCert, }) if err != nil { - initFatal(err, "validating MDM assets from database") + initFatal(err, "loading MDM assets from database") } var appleBMCerts bool @@ -621,14 +646,14 @@ the way that the Fleet server works. fleet.MDMAssetABMKey, }) if err != nil { - initFatal(err, "validating MDM ABM assets from database") + initFatal(err, "loading MDM ABM assets from database") } if appleBMCerts { // the ABM certs are there, check if a token exists and if so, apple // BM is enabled and configured. count, err := ds.GetABMTokenCount(context.Background()) if err != nil { - initFatal(err, "validating MDM ABM token from database") + initFatal(err, "loading MDM ABM token from database") } appCfg.MDM.AppleBMEnabledAndConfigured = count > 0 } diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 675f1242aa..e472566f3e 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -30,9 +30,9 @@ import ( "github.com/go-kit/log" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/smallstep/pkcs7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.mozilla.org/pkcs7" ) // safeStore is a wrapper around mock.Store to allow for concurrent calling to diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go index 1010e8ad1e..f35b39dc84 100644 --- a/cmd/fleetctl/apply_test.go +++ b/cmd/fleetctl/apply_test.go @@ -23,9 +23,11 @@ import ( "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/fleet" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki" "github.com/fleetdm/fleet/v4/server/mock" mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm" + nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service" "github.com/google/uuid" @@ -1171,6 +1173,9 @@ func TestApplyAsGitOps(t *testing.T) { testCertPEM := tokenpki.PEMCertificate(testCert.Raw) testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey) fleetCfg := config.TestConfig() + // Mock Apple DEP API + depStorage := SetupMockDEPStorageAndMockDEPServer(t) + config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, "../../server/service/testdata") _, ds := runServerWithMockedDS(t, &service.TestServerOpts{ @@ -1178,6 +1183,7 @@ func TestApplyAsGitOps(t *testing.T) { MDMStorage: enqueuer, MDMPusher: mockPusher{}, FleetConfig: &fleetCfg, + DEPStorage: depStorage, }) gitOps := &fleet.User{ @@ -1296,6 +1302,20 @@ func TestApplyAsGitOps(t *testing.T) { return nil } + ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) { + return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil + } + ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) { + return 0, nil + } + + ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) { + return []string{"foobar"}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{{ID: 1}}, nil + } + // Apply global config. name := writeTmpYml(t, `--- apiVersion: v1 @@ -1632,6 +1652,34 @@ spec: assert.Equal(t, "select * from app_schemes;", appliedQueries[0].Query) } +func SetupMockDEPStorageAndMockDEPServer(t *testing.T) *nanodep_mock.Storage { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.Contains(r.URL.Path, "/server/devices"): + _, err := w.Write([]byte("{}")) + require.NoError(t, err) + case strings.Contains(r.URL.Path, "/session"): + _, err := w.Write([]byte(`{"auth_session_token": "yoo"}`)) + require.NoError(t, err) + case strings.Contains(r.URL.Path, "/profile"): + _, err := w.Write([]byte(`{"profile_uuid": "profile123"}`)) + require.NoError(t, err) + } + })) + depStorage := &nanodep_mock.Storage{} + depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) { + return &nanodep_client.Config{ + BaseURL: ts.URL, + }, nil + } + depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) { + return &nanodep_client.OAuth1Tokens{}, nil + } + t.Cleanup(func() { ts.Close() }) + + return depStorage +} + func TestApplyEnrollSecrets(t *testing.T) { _, ds := runServerWithMockedDS(t) @@ -1885,7 +1933,8 @@ func TestApplyMacosSetup(t *testing.T) { tier = fleet.TierPremium } license := &fleet.LicenseInfo{Tier: tier, Expiration: time.Now().Add(24 * time.Hour)} - _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license}) + depStorage := SetupMockDEPStorageAndMockDEPServer(t) + _, ds := runServerWithMockedDS(t, &service.TestServerOpts{License: license, DEPStorage: depStorage}) tm1 := &fleet.Team{ID: 1, Name: "tm1"} teamsByName := map[string]*fleet.Team{ @@ -2027,6 +2076,21 @@ func TestApplyMacosSetup(t *testing.T) { ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) { return nil, nil } + + ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) { + return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil + } + ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) { + return 0, nil + } + + ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) { + return []string{"foobar"}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{{ID: 1}}, nil + } + return ds } diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go index a512d3a6a5..58fb94b1c6 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -1262,7 +1262,7 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) { {"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"}, {"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"}, {"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"}, - {"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwarePackage.packages.self_service of type bool"}, + {"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"}, } for _, c := range cases { t.Run(filepath.Base(c.file), func(t *testing.T) { @@ -1311,7 +1311,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) { {"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"}, {"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"}, {"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"}, - {"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwarePackage.packages.self_service of type bool"}, + {"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"}, } for _, c := range cases { t.Run(filepath.Base(c.file), func(t *testing.T) { @@ -1383,6 +1383,7 @@ func TestGitOpsTeamVPPApps(t *testing.T) { {"testdata/gitops/team_vpp_valid_empty.yml", "", time.Now().Add(-24 * time.Hour)}, {"testdata/gitops/team_vpp_valid_app.yml", "VPP token expired", time.Now().Add(-24 * time.Hour)}, {"testdata/gitops/team_vpp_invalid_app.yml", "app not available on vpp account", time.Now().Add(24 * time.Hour)}, + {"testdata/gitops/team_vpp_incorrect_type.yml", "\"app_store_apps.app_store_id\" must be a string, found number", time.Now().Add(24 * time.Hour)}, {"testdata/gitops/team_vpp_empty_adamid.yml", "software app store id required", time.Now().Add(24 * time.Hour)}, } diff --git a/cmd/fleetctl/testdata/gitops/team_vpp_incorrect_type.yml b/cmd/fleetctl/testdata/gitops/team_vpp_incorrect_type.yml new file mode 100644 index 0000000000..74e7b17806 --- /dev/null +++ b/cmd/fleetctl/testdata/gitops/team_vpp_incorrect_type.yml @@ -0,0 +1,17 @@ +name: "${TEST_TEAM_NAME}" +team_settings: + secrets: + - secret: "ABC" + features: + enable_host_users: true + enable_software_inventory: true + host_expiry_settings: + host_expiry_enabled: true + host_expiry_window: 30 +agent_options: +controls: +policies: +queries: +software: + app_store_apps: + - app_store_id: 1 diff --git a/docs/Configuration/fleet-server-configuration.md b/docs/Configuration/fleet-server-configuration.md index 145f887ebd..176d9026e4 100644 --- a/docs/Configuration/fleet-server-configuration.md +++ b/docs/Configuration/fleet-server-configuration.md @@ -12,7 +12,8 @@ You can specify configuration options in the following formats: 2. Environment variables 3. Command-line flags -All duration-based settings accept valid time units of `s`, `m`, `h`. +- All duration-based settings accept valid time units of `s`, `m`, `h`. +- Command-line flags can also be piped in via stdin. #### MySQL diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md index a23d84b0b2..be73c1ff29 100644 --- a/docs/Configuration/yaml-files.md +++ b/docs/Configuration/yaml-files.md @@ -273,14 +273,16 @@ Use `labels_include_all` to only apply (scope) profiles to hosts that have all t #### macos_setup -The `macos_setup` section lets you control the [end user migration workflow](https://fleetdm.com/docs/using-fleet/mdm-migration-guide#end-user-workflow) for macOS hosts that automatically enrolled to your old MDM solution. +The `macos_setup` section lets you control the out-of-the-box macOS [setup experience](https://fleetdm.com/guides/macos-setup-experience) for hosts that use Automated Device Enrollment (ADE). - `bootstrap_package` is the URL to a bootstap package. Fleet will download the bootstrap package (default: `""`). - `enable_end_user_authentication` specifies whether or not to require end user authentication when the user first sets up their macOS host. -- `macos_setup_assistant` is a path to a custom automatic enrollment (DEP) profile (.json). +- `macos_setup_assistant` is a path to a custom automatic enrollment (ADE) profile (.json). #### macos_migration +The `macos_migration` section lets you control the [end user migration workflow](https://fleetdm.com/docs/using-fleet/mdm-migration-guide#end-user-workflow) for macOS hosts that enrolled to your old MDM solution. + - `enable` specifies whether or not to enable end user migration workflow (default: `false`) - `mode` specifies whether the end user initiates migration (`voluntary`) or they're nudged every 15-20 minutes to migrate (`forced`) (default: `""`). - `webhook_url` is the URL that Fleet sends a webhook to when the end user selects **Start**. Receive this webhook using your automation tool (ex. Tines) to unenroll your end users from your old MDM solution. @@ -585,6 +587,19 @@ org_settings: Can only be configured for all teams (`org_settings`). +##### end_user_authentication + +The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula). + +Once the IdP settings are configured, you can use the [`controls.macos_setup.enable_end_user_authentication`](#macos_setup) key to control the end user experience during ADE. + +- `idp_name` is the human-friendly name for the identity provider that will provide single sign-on authentication (default: `""`). +- `entity_id` is the entity ID: a Uniform Resource Identifier (URI) that you use to identify Fleet when configuring the identity provider. It must exactly match the Entity ID field used in identity provider configuration (default: `""`). +- `metadata` is the metadata (in XML format) provided by the identity provider. (default: `""`) +- `metadata_url` is the URL that references the identity provider metadata. Only one of `metadata` or `metadata_url` is required (default: `""`). + +Can only be configured for all teams (`org_settings`). + diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index da63c0ccb5..e31703b6fb 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -8511,6 +8511,9 @@ Get a list of all software. | vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | | available_for_install | boolean | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. | | self_service | boolean | query | If `true` or `1`, only lists self-service software. Default is `false`. | +| min_cvss_score | integer | query | _Available in Fleet Premium_. Filters to include only software with vulnerabilities that have a CVSS version 3.x base score higher than the specified value. | +| max_cvss_score | integer | query | _Available in Fleet Premium_. Filters to only include software with vulnerabilities that have a CVSS version 3.x base score lower than what's specified. | +| exploit | boolean | query | _Available in Fleet Premium_. If `true`, filters to only include software with vulnerabilities that have been actively exploited in the wild (`cisa_known_exploit: true`). Default is `false`. | #### Example @@ -8630,6 +8633,9 @@ Get a list of all software versions. | query | string | query | Search query keywords. Searchable fields include `name`, `version`, and `cve`. | | team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". | | vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. | +| min_cvss_score | integer | query | _Available in Fleet Premium_. Filters to include only software with vulnerabilities that have a CVSS version 3.x base score higher than the specified value. | +| max_cvss_score | integer | query | _Available in Fleet Premium_. Filters to only include software with vulnerabilities that have a CVSS version 3.x base score lower than what's specified. | +| exploit | boolean | query | _Available in Fleet Premium_. If `true`, filters to only include software with vulnerabilities that have been actively exploited in the wild (`cisa_known_exploit: true`). Default is `false`. | #### Example @@ -9285,7 +9291,6 @@ Retrieves a list of all CVEs affecting software and/or OS versions. | exploit | boolean | query | _Available in Fleet Premium_. If `true`, filters to only include vulnerabilities that have been actively exploited in the wild (`cisa_known_exploit: true`). Otherwise, includes vulnerabilities with any `cisa_known_exploit` value. | - ##### Default response `Status: 200` @@ -9320,12 +9325,14 @@ Retrieves a list of all CVEs affecting software and/or OS versions. Retrieve details about a vulnerability and its affected software and OS versions. +If no vulnerable OS versions or software were found, but Fleet is aware of the vulnerability, a 204 status code is returned. + #### Parameters -| Name | Type | In | Description | -| --- | --- | --- | --- | -| cve | string | path | The cve to get information about (including "cve-" prefix, case-insensitive). | -| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | +| Name | Type | In | Description | +|---------|---------|-------|------------------------------------------------------------------------------------------------------------------------------| +| cve | string | path | The cve to get information about (format must be CVE-YYYY-<4 or more digits>, case-insensitive). | +| team_id | integer | query | _Available in Fleet Premium_. Filters response data to the specified team. Use `0` to filter by hosts assigned to "No team". | `GET /api/v1/fleet/vulnerabilities/:cve` diff --git a/ee/server/service/mdm.go b/ee/server/service/mdm.go index 9d94428790..f6db29af5c 100644 --- a/ee/server/service/mdm.go +++ b/ee/server/service/mdm.go @@ -510,8 +510,10 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst // enabled (either globally or for a specific team, if provided) var endUserAuthEnabled bool var teamName *string + var tm *fleet.Team if asst.TeamID != nil { - tm, err := svc.ds.Team(ctx, *asst.TeamID) + var err error + tm, err = svc.ds.Team(ctx, *asst.TeamID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "get team") } @@ -540,6 +542,12 @@ func (svc *Service) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profile", `Couldn't edit macos_setup_assistant. The profile can't include "await_device_configured" option.`)) } + // Validate the profile with Apple's API. Don't save the profile if it isn't valid. + err := svc.depService.ValidateSetupAssistant(ctx, tm, asst, "") + if err != nil { + return nil, fleet.NewInvalidArgumentError("profile", err.Error()) + } + // must read the existing setup assistant first to detect if it did change // (so that the changed activity is not created if the same assistant was // uploaded). diff --git a/ee/server/service/mdm_external_test.go b/ee/server/service/mdm_external_test.go index c6324f84fe..1d92d1ce3b 100644 --- a/ee/server/service/mdm_external_test.go +++ b/ee/server/service/mdm_external_test.go @@ -4,6 +4,9 @@ import ( "context" "encoding/json" "errors" + "net/http" + "net/http/httptest" + "strings" "testing" "time" @@ -18,7 +21,7 @@ import ( "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" - nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage" + nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client" "github.com/fleetdm/fleet/v4/server/mock" nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/fleetdm/fleet/v4/server/ptr" @@ -30,7 +33,7 @@ import ( "github.com/stretchr/testify/require" ) -func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, context.Context) { +func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Service, context.Context) { ds := new(mock.Store) lic := &fleet.LicenseInfo{Tier: fleet.TierPremium} ctx := license.NewContext(context.Background(), lic) @@ -42,7 +45,29 @@ func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, contex AppleSCEPKeyBytes: eeservice.TestKey, }, } - var depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{} + depStorage := &nanodep_mock.Storage{} + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.Contains(r.URL.Path, "/server/devices"): + _, err := w.Write([]byte("{}")) + require.NoError(t, err) + case strings.Contains(r.URL.Path, "/session"): + _, err := w.Write([]byte(`{"auth_session_token": "yoo"}`)) + require.NoError(t, err) + case strings.Contains(r.URL.Path, "/profile"): + _, err := w.Write([]byte(`{"profile_uuid": "profile123"}`)) + require.NoError(t, err) + } + })) + depStorage.RetrieveConfigFunc = func(context.Context, string) (*nanodep_client.Config, error) { + return &nanodep_client.Config{ + BaseURL: ts.URL, + }, nil + } + depStorage.RetrieveAuthTokensFunc = func(ctx context.Context, name string) (*nanodep_client.OAuth1Tokens, error) { + return &nanodep_client.OAuth1Tokens{}, nil + } + t.Cleanup(func() { ts.Close() }) freeSvc, err := service.NewService( ctx, @@ -92,7 +117,7 @@ func setupMockDatastorePremiumService() (*mock.Store, *eeservice.Service, contex } func TestGetOrCreatePreassignTeam(t *testing.T) { - ds, svc, ctx := setupMockDatastorePremiumService() + ds, svc, ctx := setupMockDatastorePremiumService(t) ssoSettings := fleet.SSOProviderSettings{ EntityID: "foo", @@ -219,6 +244,19 @@ func TestGetOrCreatePreassignTeam(t *testing.T) { fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM}, }, nil } + + ds.GetMDMAppleEnrollmentProfileByTypeFunc = func(ctx context.Context, typ fleet.MDMAppleEnrollmentType) (*fleet.MDMAppleEnrollmentProfile, error) { + return &fleet.MDMAppleEnrollmentProfile{Token: "foobar"}, nil + } + ds.GetABMTokenOrgNamesAssociatedWithTeamFunc = func(ctx context.Context, teamID *uint) ([]string, error) { + return []string{"foobar"}, nil + } + ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) { + return []*fleet.ABMToken{{ID: 1}}, nil + } + ds.CountABMTokensWithTermsExpiredFunc = func(ctx context.Context) (int, error) { + return 0, nil + } } authzCtx := &authz_ctx.AuthorizationContext{} diff --git a/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx b/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx index 40d991e2f8..7cfba0f4d2 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx @@ -6,7 +6,9 @@ import { AppContext } from "context/app"; import InputField from "components/forms/fields/InputField"; const generateUrl = (serverUrl: string, enrollSecret: string) => { - return `${serverUrl}/enroll?enroll_secret=${enrollSecret}`; + return `${serverUrl}/enroll?enroll_secret=${encodeURIComponent( + enrollSecret + )}`; }; const baseClass = "ios-ipados-panel"; diff --git a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx index 4d67aea8b6..1935b38b11 100644 --- a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx @@ -42,9 +42,10 @@ const DeleteScriptModal = ({ > <>

- This action will cancel script{" "} - {scriptName} from - running on macOS hosts on which the script hasn't run yet. + The script{" "} + {scriptName} will + run on pending hosts. After the scripts runs, it's output and + exit code will appear in the activity feed.