From addaaa3f8c9166cc6e00513bf4347ebcfa7def58 Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Tue, 17 Dec 2024 16:10:48 -0600 Subject: [PATCH 1/5] Move release article image into the /articles folder (#24848) --- .../{ => articles}/fleet-4.61.0-1600x900@2x.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename website/assets/images/{ => articles}/fleet-4.61.0-1600x900@2x.png (100%) diff --git a/website/assets/images/fleet-4.61.0-1600x900@2x.png b/website/assets/images/articles/fleet-4.61.0-1600x900@2x.png similarity index 100% rename from website/assets/images/fleet-4.61.0-1600x900@2x.png rename to website/assets/images/articles/fleet-4.61.0-1600x900@2x.png From c9bdae8fb3eb0a39c1b6e6a0fce631d58715465b Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:14:12 -0500 Subject: [PATCH 2/5] Embedded secrets validation (#24624) #24549 Co-authored-by: Victor Lyuboslavsky --- changes/24549-validate-script-profle-secrets | 1 + cmd/fleetctl/testing_utils.go | 3 + ee/server/service/setup_experience.go | 4 + pkg/spec/spec.go | 6 +- server/datastore/mysql/secret_variables.go | 77 +++++++++++++++ .../datastore/mysql/secret_variables_test.go | 96 +++++++++++++++++++ server/fleet/datastore.go | 8 ++ server/fleet/fleet_vars_test.go | 6 +- server/fleet/secrets.go | 23 ++++- server/mock/datastore_mock.go | 24 +++++ server/service/apple_mdm.go | 39 +++++++- server/service/apple_mdm_test.go | 6 ++ server/service/integration_enterprise_test.go | 17 ++++ server/service/integration_mdm_dep_test.go | 5 + .../service/integration_mdm_profiles_test.go | 60 +++++++++++- server/service/mdm.go | 33 ++++++- server/service/mdm_test.go | 21 ++++ server/service/scripts.go | 18 ++++ server/service/scripts_test.go | 9 ++ server/service/setup_experience_test.go | 6 ++ 20 files changed, 448 insertions(+), 14 deletions(-) create mode 100644 changes/24549-validate-script-profle-secrets diff --git a/changes/24549-validate-script-profle-secrets b/changes/24549-validate-script-profle-secrets new file mode 100644 index 000000000000..fdf7ea4a416e --- /dev/null +++ b/changes/24549-validate-script-profle-secrets @@ -0,0 +1 @@ +- Validate fleet secrets embedded into scripts and profiles on ingestion diff --git a/cmd/fleetctl/testing_utils.go b/cmd/fleetctl/testing_utils.go index 0c4507b359e3..19bb8bdaeff3 100644 --- a/cmd/fleetctl/testing_utils.go +++ b/cmd/fleetctl/testing_utils.go @@ -152,6 +152,9 @@ func runServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*http ds.ApplyYaraRulesFunc = func(context.Context, []fleet.YaraRule) error { return nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } var cachedDS fleet.Datastore if len(opts) > 0 && opts[0].NoCacheDatastore { diff --git a/ee/server/service/setup_experience.go b/ee/server/service/setup_experience.go index 7bece3103e03..c11b1ac05f92 100644 --- a/ee/server/service/setup_experience.go +++ b/ee/server/service/setup_experience.go @@ -76,6 +76,10 @@ func (svc *Service) SetSetupExperienceScript(ctx context.Context, teamID *uint, ScriptContents: string(b), } + if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{script.ScriptContents}); err != nil { + return fleet.NewInvalidArgumentError("script", err.Error()) + } + // setup experience is only supported for macOS currently so we need to override the file // extension check in the general script validation if filepath.Ext(script.Name) != ".sh" { diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 08eed07042bc..ce7eb4b81744 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -186,10 +186,10 @@ func expandEnv(s string, failOnSecret bool) (string, error) { case strings.HasPrefix(env, fleet.ServerVarPrefix): // Don't expand fleet vars -- they will be expanded on the server return "", false - case strings.HasPrefix(env, fleet.FLEET_SECRET_PREFIX): + case strings.HasPrefix(env, fleet.ServerSecretPrefix): if failOnSecret { err = multierror.Append(err, fmt.Errorf("environment variables with %q prefix are only allowed in profiles and scripts: %q", - fleet.FLEET_SECRET_PREFIX, env)) + fleet.ServerSecretPrefix, env)) } return "", false } @@ -231,7 +231,7 @@ func LookupEnvSecrets(s string, secretsMap map[string]string) error { } var err *multierror.Error _ = fleet.MaybeExpand(s, func(env string) (string, bool) { - if strings.HasPrefix(env, fleet.FLEET_SECRET_PREFIX) { + if strings.HasPrefix(env, fleet.ServerSecretPrefix) { // lookup the secret and save it, but don't replace v, ok := os.LookupEnv(env) if !ok { diff --git a/server/datastore/mysql/secret_variables.go b/server/datastore/mysql/secret_variables.go index fa8d1f6fca41..50ff7955d215 100644 --- a/server/datastore/mysql/secret_variables.go +++ b/server/datastore/mysql/secret_variables.go @@ -68,3 +68,80 @@ func (ds *Datastore) GetSecretVariables(ctx context.Context, names []string) ([] return secretVariables, nil } + +func (ds *Datastore) ExpandEmbeddedSecrets(ctx context.Context, document string) (string, error) { + embeddedSecrets := fleet.ContainsPrefixVars(document, fleet.ServerSecretPrefix) + + secrets, err := ds.GetSecretVariables(ctx, embeddedSecrets) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "expanding embedded secrets") + } + + secretMap := make(map[string]string, len(secrets)) + + for _, secret := range secrets { + secretMap[secret.Name] = secret.Value + } + + missingSecrets := []string{} + + for _, wantSecret := range embeddedSecrets { + if _, ok := secretMap[wantSecret]; !ok { + missingSecrets = append(missingSecrets, wantSecret) + } + } + + if len(missingSecrets) > 0 { + return "", fleet.MissingSecretsError{MissingSecrets: missingSecrets} + } + + expanded := fleet.MaybeExpand(document, func(s string) (string, bool) { + if !strings.HasPrefix(s, fleet.ServerSecretPrefix) { + return "", false + } + val, ok := secretMap[strings.TrimPrefix(s, fleet.ServerSecretPrefix)] + return val, ok + }) + + return expanded, nil +} + +func (ds *Datastore) ValidateEmbeddedSecrets(ctx context.Context, documents []string) error { + wantSecrets := make(map[string]struct{}) + haveSecrets := make(map[string]struct{}) + + for _, document := range documents { + vars := fleet.ContainsPrefixVars(document, fleet.ServerSecretPrefix) + for _, v := range vars { + wantSecrets[v] = struct{}{} + } + } + + wantSecretsList := make([]string, 0, len(wantSecrets)) + for wantSecret := range wantSecrets { + wantSecretsList = append(wantSecretsList, wantSecret) + } + + dbSecrets, err := ds.GetSecretVariables(ctx, wantSecretsList) + if err != nil { + return ctxerr.Wrap(ctx, err, "validating document embedded secrets") + } + + for _, dbSecret := range dbSecrets { + haveSecrets[dbSecret.Name] = struct{}{} + } + + missingSecrets := []string{} + + for wantSecret := range wantSecrets { + if _, ok := haveSecrets[wantSecret]; !ok { + missingSecrets = append(missingSecrets, wantSecret) + } + } + + if len(missingSecrets) > 0 { + return &fleet.MissingSecretsError{MissingSecrets: missingSecrets} + } + + return nil +} diff --git a/server/datastore/mysql/secret_variables_test.go b/server/datastore/mysql/secret_variables_test.go index 72eaa5ab3030..8936442b58f0 100644 --- a/server/datastore/mysql/secret_variables_test.go +++ b/server/datastore/mysql/secret_variables_test.go @@ -17,6 +17,8 @@ func TestSecretVariables(t *testing.T) { fn func(t *testing.T, ds *Datastore) }{ {"UpsertSecretVariables", testUpsertSecretVariables}, + {"ValidateEmbeddedSecrets", testValidateEmbeddedSecrets}, + {"ExpandEmbeddedSecrets", testExpandEmbeddedSecrets}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -70,3 +72,97 @@ func testUpsertSecretVariables(t *testing.T, ds *Datastore) { assert.Equal(t, secretMap[results[0].Name], results[0].Value) } + +func testValidateEmbeddedSecrets(t *testing.T, ds *Datastore) { + noSecrets := ` +This document contains to fleet secrets. +$FLEET_VAR_XX $HOSTNAME ${SOMETHING_ELSE} +` + + validSecret := ` +This document contains a valid ${FLEET_SECRET_VALID}. +Another $FLEET_SECRET_ALSO_VALID. +` + + invalidSecret := ` +This document contains a secret not stored in the database. +Hello doc${FLEET_SECRET_INVALID}. $FLEET_SECRET_ALSO_INVALID +` + ctx := context.Background() + secretMap := map[string]string{ + "VALID": "testValue1", + "ALSO_VALID": "testValue2", + } + + secrets := make([]fleet.SecretVariable, 0, len(secretMap)) + for name, value := range secretMap { + secrets = append(secrets, fleet.SecretVariable{Name: name, Value: value}) + } + + err := ds.UpsertSecretVariables(ctx, secrets) + require.NoError(t, err) + + err = ds.ValidateEmbeddedSecrets(ctx, []string{noSecrets}) + require.NoError(t, err) + + err = ds.ValidateEmbeddedSecrets(ctx, []string{validSecret}) + require.NoError(t, err) + + err = ds.ValidateEmbeddedSecrets(ctx, []string{noSecrets, validSecret}) + require.NoError(t, err) + + err = ds.ValidateEmbeddedSecrets(ctx, []string{invalidSecret}) + require.ErrorContains(t, err, "$FLEET_SECRET_INVALID") + require.ErrorContains(t, err, "$FLEET_SECRET_ALSO_INVALID") + + err = ds.ValidateEmbeddedSecrets(ctx, []string{noSecrets, validSecret, invalidSecret}) + require.ErrorContains(t, err, "$FLEET_SECRET_INVALID") + require.ErrorContains(t, err, "$FLEET_SECRET_ALSO_INVALID") +} + +func testExpandEmbeddedSecrets(t *testing.T, ds *Datastore) { + noSecrets := ` +This document contains to fleet secrets. +$FLEET_VAR_XX $HOSTNAME ${SOMETHING_ELSE} +` + + validSecret := ` +This document contains a valid ${FLEET_SECRET_VALID}. +Another $FLEET_SECRET_ALSO_VALID. +` + validSecretExpanded := ` +This document contains a valid testValue1. +Another testValue2. +` + + invalidSecret := ` +This document contains a secret not stored in the database. +Hello doc${FLEET_SECRET_INVALID}. $FLEET_SECRET_ALSO_INVALID +` + + ctx := context.Background() + secretMap := map[string]string{ + "VALID": "testValue1", + "ALSO_VALID": "testValue2", + } + + secrets := make([]fleet.SecretVariable, 0, len(secretMap)) + for name, value := range secretMap { + secrets = append(secrets, fleet.SecretVariable{Name: name, Value: value}) + } + + err := ds.UpsertSecretVariables(ctx, secrets) + require.NoError(t, err) + + expanded, err := ds.ExpandEmbeddedSecrets(ctx, noSecrets) + require.NoError(t, err) + require.Equal(t, noSecrets, expanded) + + expanded, err = ds.ExpandEmbeddedSecrets(ctx, validSecret) + require.NoError(t, err) + require.Equal(t, validSecretExpanded, expanded) + + _, err = ds.ExpandEmbeddedSecrets(ctx, invalidSecret) + require.ErrorContains(t, err, "$FLEET_SECRET_INVALID") + require.ErrorContains(t, err, "$FLEET_SECRET_ALSO_INVALID") +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 8d93dfd1fd6b..a12608785ca0 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1883,6 +1883,14 @@ type Datastore interface { // GetSecretVariables retrieves secret variables from the database. GetSecretVariables(ctx context.Context, names []string) ([]SecretVariable, error) + + // ValidateEmbeddedSecrets parses fleet secrets from a list of + // documents and checks that they exist in the database. + ValidateEmbeddedSecrets(ctx context.Context, documents []string) error + + // ExpandEmbeddedSecrets expands the fleet secrets in a + // document using the secrets stored in the datastore. + ExpandEmbeddedSecrets(ctx context.Context, document string) (string, error) } // MDMAppleStore wraps nanomdm's storage and adds methods to deal with diff --git a/server/fleet/fleet_vars_test.go b/server/fleet/fleet_vars_test.go index 0876799ec22c..992ba7eb4e7b 100644 --- a/server/fleet/fleet_vars_test.go +++ b/server/fleet/fleet_vars_test.go @@ -16,7 +16,7 @@ echo words${FLEET_SECRET_BAR}words $FLEET_SECRET_BAZ ${FLEET_SECRET_QUX} ` - secrets := ContainsPrefixVars(script, FLEET_SECRET_PREFIX) + secrets := ContainsPrefixVars(script, ServerSecretPrefix) require.Contains(t, secrets, "FOO") require.Contains(t, secrets, "BAR") require.Contains(t, secrets, "BAZ") @@ -39,8 +39,8 @@ We want to remember BREAD and alsoSHORTCAKEare important. } mapper := func(s string) (string, bool) { - if strings.HasPrefix(s, FLEET_SECRET_PREFIX) { - return mapping[strings.TrimPrefix(s, FLEET_SECRET_PREFIX)], true + if strings.HasPrefix(s, ServerSecretPrefix) { + return mapping[strings.TrimPrefix(s, ServerSecretPrefix)], true } return "", false } diff --git a/server/fleet/secrets.go b/server/fleet/secrets.go index 8f5b5d5c0bd8..0688a825d0f1 100644 --- a/server/fleet/secrets.go +++ b/server/fleet/secrets.go @@ -1,3 +1,24 @@ package fleet -const FLEET_SECRET_PREFIX = "FLEET_SECRET_" +import ( + "fmt" + "strings" +) + +const ServerSecretPrefix = "FLEET_SECRET_" + +type MissingSecretsError struct { + MissingSecrets []string +} + +func (e MissingSecretsError) Error() string { + secretVars := make([]string, 0, len(e.MissingSecrets)) + for _, secret := range e.MissingSecrets { + secretVars = append(secretVars, fmt.Sprintf("\"$%s%s\"", ServerSecretPrefix, secret)) + } + plural := "" + if len(secretVars) > 1 { + plural = "s" + } + return fmt.Sprintf("Couldn't add. Variable%s %s missing", plural, strings.Join(secretVars, ", ")) +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 2bbfba33b72a..89eeb21e3882 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1177,6 +1177,10 @@ type UpsertSecretVariablesFunc func(ctx context.Context, secretVariables []fleet type GetSecretVariablesFunc func(ctx context.Context, names []string) ([]fleet.SecretVariable, error) +type ValidateEmbeddedSecretsFunc func(ctx context.Context, documents []string) error + +type ExpandEmbeddedSecretsFunc func(ctx context.Context, document string) (string, error) + type DataStore struct { HealthCheckFunc HealthCheckFunc HealthCheckFuncInvoked bool @@ -2912,6 +2916,12 @@ type DataStore struct { GetSecretVariablesFunc GetSecretVariablesFunc GetSecretVariablesFuncInvoked bool + ValidateEmbeddedSecretsFunc ValidateEmbeddedSecretsFunc + ValidateEmbeddedSecretsFuncInvoked bool + + ExpandEmbeddedSecretsFunc ExpandEmbeddedSecretsFunc + ExpandEmbeddedSecretsFuncInvoked bool + mu sync.Mutex } @@ -6960,3 +6970,17 @@ func (s *DataStore) GetSecretVariables(ctx context.Context, names []string) ([]f s.mu.Unlock() return s.GetSecretVariablesFunc(ctx, names) } + +func (s *DataStore) ValidateEmbeddedSecrets(ctx context.Context, documents []string) error { + s.mu.Lock() + s.ValidateEmbeddedSecretsFuncInvoked = true + s.mu.Unlock() + return s.ValidateEmbeddedSecretsFunc(ctx, documents) +} + +func (s *DataStore) ExpandEmbeddedSecrets(ctx context.Context, document string) (string, error) { + s.mu.Lock() + s.ExpandEmbeddedSecretsFuncInvoked = true + s.mu.Unlock() + return s.ExpandEmbeddedSecretsFunc(ctx, document) +} diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 56dc433539d8..60095583958f 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -380,12 +380,24 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r }) } - cp, err := fleet.NewMDMAppleConfigProfile(b, &teamID) + if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{string(b)}); err != nil { + return nil, fleet.NewInvalidArgumentError("profile", err.Error()) + } + + // Expand secrets in profile for validation + expanded, err := svc.ds.ExpandEmbeddedSecrets(ctx, string(b)) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "expanding secrets in profile for parsing") + } + + cp, err := fleet.NewMDMAppleConfigProfile([]byte(expanded), &teamID) if err != nil { return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{ Message: fmt.Sprintf("failed to parse config profile: %s", err.Error()), }) } + // Save the original unexpanded profile + cp.Mobileconfig = b if err := cp.ValidateUserProvided(); err != nil { return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{Message: err.Error()}) @@ -405,6 +417,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r default: // TODO what happens if mode is not set?s } + err = validateConfigProfileFleetVariables(string(cp.Mobileconfig)) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating fleet variables") @@ -503,6 +516,10 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i return nil, err } + if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{string(data)}); err != nil { + return nil, fleet.NewInvalidArgumentError("profile", err.Error()) + } + if err := validateDeclarationFleetVariables(string(data)); err != nil { return nil, err } @@ -1968,12 +1985,21 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%d]", i), "maximum configuration profile file size is 1 MB"), ) } - mdmProf, err := fleet.NewMDMAppleConfigProfile(prof, tmID) + // Expand profile for validation + expanded, err := svc.ds.ExpandEmbeddedSecrets(ctx, string(prof)) + if err != nil { + return ctxerr.Wrap(ctx, + fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%d]", i), err.Error()), + "missing fleet secrets") + } + mdmProf, err := fleet.NewMDMAppleConfigProfile([]byte(expanded), tmID) if err != nil { return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError(fmt.Sprintf("profiles[%d]", i), err.Error()), "invalid mobileconfig profile") } + // Store original unexpanded profile + mdmProf.Mobileconfig = prof if err := mdmProf.ValidateUserProvided(); err != nil { return ctxerr.Wrap(ctx, @@ -1997,6 +2023,15 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm profs = append(profs, mdmProf) } + profStrings := make([]string, 0, len(profs)) + for _, prof := range profs { + profStrings = append(profStrings, string(prof.Mobileconfig)) + } + + if err := svc.ds.ValidateEmbeddedSecrets(ctx, profStrings); err != nil { + return fleet.NewInvalidArgumentError("profiles", err.Error()) + } + if !skipBulkPending { // check for duplicates with existing profiles, skipBulkPending signals that the caller // is responsible for ensuring that the profiles names are unique (e.g., MDMAppleMatchPreassignment) diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 940c32d1fee6..b60c72d8517b 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -209,6 +209,12 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi ds.MDMDeleteEULAFunc = func(ctx context.Context, token string) error { return nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } + ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { + return document, nil + } apnsCert, apnsKey, err := mysql.GenerateTestCertBytes() require.NoError(t, err) crt, key, err := apple_mdm.NewSCEPCACertKey() diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 9b7e5b693c4a..dd050dad14e9 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -6192,6 +6192,11 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() { err := s.ds.MarkHostsSeen(ctx, []uint{host.ID}, time.Now()) require.NoError(t, err) + // make sure invalid secrets aren't allowed + res = s.Do("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo $FLEET_SECRET_INVALID"}, http.StatusUnprocessableEntity) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, `$FLEET_SECRET_INVALID`) + // create a valid script execution request s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusAccepted, &runResp) require.Equal(t, host.ID, runResp.HostID) @@ -7038,6 +7043,13 @@ func (s *integrationEnterpriseTestSuite) TestSavedScripts() { errMsg := extractServerErrorText(res.Body) require.Contains(t, errMsg, "no file headers for script") + // contains invalid fleet secret + body, headers = generateNewScriptMultipartRequest(t, + "secrets.sh", []byte(`echo "$FLEET_SECRET_INVALID"`), s.token, nil) + res = s.DoRawWithHeaders("POST", "/api/latest/fleet/scripts", body.Bytes(), http.StatusUnprocessableEntity, headers) + errMsg = extractServerErrorText(res.Body) + require.Contains(t, errMsg, "$FLEET_SECRET_INVALID") + // file name is not .sh body, headers = generateNewScriptMultipartRequest(t, "not_sh.txt", []byte(`echo "hello"`), s.token, nil) @@ -7952,6 +7964,11 @@ func (s *integrationEnterpriseTestSuite) TestBatchApplyScriptsEndpoints() { {Name: "", ScriptContents: []byte("foo")}, }}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID)) + // invalid secret + s.Do("POST", "/api/v1/fleet/scripts/batch", batchSetScriptsRequest{Scripts: []fleet.ScriptPayload{ + {Name: "N2.sh", ScriptContents: []byte("echo $FLEET_SECRET_INVALID")}, + }}, http.StatusUnprocessableEntity, "team_id", fmt.Sprint(tm.ID)) + // successfully apply a scripts for the team saveAndCheckScripts(tm, []fleet.ScriptPayload{ {Name: "N1.sh", ScriptContents: []byte("foo")}, diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index c63a6a2916c3..d0616ec3419d 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -1846,6 +1846,11 @@ func (s *integrationMDMTestSuite) TestSetupExperienceScript() { err = json.NewDecoder(res.Body).Decode(&newScriptResp) require.NoError(t, err) + // test script secret validation + body, headers = generateNewScriptMultipartRequest(t, + "script.sh", []byte(`echo "$FLEET_SECRET_INVALID"`), s.token, map[string][]string{}) + s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusUnprocessableEntity, headers) + // get team script metadata var getScriptResp getSetupExperienceScriptResponse s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/setup_experience/script?team_id=%d", tm.ID), nil, http.StatusOK, &getScriptResp) diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go index f9e73e1eee5f..bdb95c14c073 100644 --- a/server/service/integration_mdm_profiles_test.go +++ b/server/service/integration_mdm_profiles_test.go @@ -80,6 +80,32 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() { // add global profiles s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent) + // invalid secrets + var invalidSecretsProfile = []byte(` + + + + + PayloadContent + + PayloadDisplayName + $FLEET_SECRET_INVALID + PayloadIdentifier + N3 + PayloadType + Configuration + PayloadUUID + 601E0B42-0989-4FAD-A61B-18656BA3670E + PayloadVersion + 1 + + +`) + + res := s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{invalidSecretsProfile}}, http.StatusUnprocessableEntity) + errMsg := extractServerErrorText(res.Body) + require.Contains(t, errMsg, "$FLEET_SECRET_INVALID") + // create a new team tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "batch_set_mdm_profiles"}) require.NoError(t, err) @@ -224,8 +250,8 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() { s.checkMDMProfilesSummaries(t, &tm.ID, fleet.MDMProfilesSummary{Verifying: 1}, nil) // can't resend profile while verifying - res := s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusConflict) - errMsg := extractServerErrorText(res.Body) + res = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, mcUUID), nil, http.StatusConflict) + errMsg = extractServerErrorText(res.Body) require.Contains(t, errMsg, "Couldn’t resend. Configuration profiles with “pending” or “verifying” status can’t be resent.") // set the profile to pending, can't resend @@ -4628,6 +4654,36 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() { getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID) _ = s.DoRawWithHeaders("GET", getPath, nil, http.StatusNotFound, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)}) + // fail to create new profile (no team), invalid fleet secret + testProfiles["badSecrets"] = fleet.MDMAppleConfigProfile{ + Name: "badSecrets", + Identifier: "badSecrets.One", + Mobileconfig: mobileconfig.Mobileconfig(` + + + + PayloadContent + + PayloadDisplayName + badSecrets + PayloadIdentifier + badSecrets.One + PayloadType + Configuration + PayloadUUID + $FLEET_SECRET_INVALID.35E2029E-A0C2-4754-B709-4CAAB1B8D3CB + PayloadVersion + 1 + + +`), + } + + body, headers = generateNewReq("badSecrets", nil) + newResp = s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusUnprocessableEntity, headers) + errMsg := extractServerErrorText(newResp.Body) + require.Contains(t, errMsg, "$FLEET_SECRET_INVALID") + // trying to add/delete profiles with identifiers managed by Fleet fails for p := range mobileconfig.FleetPayloadIdentifiers() { generateTestProfile("TestNoTeam", p) diff --git a/server/service/mdm.go b/server/service/mdm.go index 44759b8dc657..ffc9c3d7e626 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -1425,6 +1425,10 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, cp.LabelsIncludeAll = labelMap } + if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{string(cp.SyncML)}); err != nil { + return nil, ctxerr.Wrap(ctx, err, "validating fleet secrets") + } + err = validateWindowsProfileFleetVariables(string(cp.SyncML)) if err != nil { return nil, ctxerr.Wrap(ctx, err, "validating Windows profile") @@ -1632,13 +1636,18 @@ func (svc *Service) BatchSetMDMProfiles( return ctxerr.Wrap(ctx, err, "validating cross-platform profile names") } - err = validateFleetVariables(ctx, appleProfiles, windowsProfiles, appleDecls) + if dryRun { + return nil + } + + err = svc.validateFleetSecrets(ctx, appleProfiles, windowsProfiles, appleDecls) if err != nil { return err } - if dryRun { - return nil + err = validateFleetVariables(ctx, appleProfiles, windowsProfiles, appleDecls) + if err != nil { + return err } var profUpdates fleet.MDMProfilesUpdates @@ -1706,6 +1715,7 @@ func validateFleetVariables(ctx context.Context, appleProfiles []*fleet.MDMApple windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration, ) error { var err error + for _, p := range appleProfiles { err = validateConfigProfileFleetVariables(string(p.Mobileconfig)) if err != nil { @@ -1727,6 +1737,23 @@ func validateFleetVariables(ctx context.Context, appleProfiles []*fleet.MDMApple return nil } +func (svc *Service) validateFleetSecrets(ctx context.Context, appleProfiles []*fleet.MDMAppleConfigProfile, windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration) error { + allProfiles := make([]string, 0, len(appleProfiles)+len(appleDecls)+len(windowsProfiles)) + for _, p := range appleProfiles { + allProfiles = append(allProfiles, string(p.Mobileconfig)) + } + for _, p := range appleDecls { + allProfiles = append(allProfiles, string(p.RawJSON)) + } + for _, p := range windowsProfiles { + allProfiles = append(allProfiles, string(p.SyncML)) + } + if err := svc.ds.ValidateEmbeddedSecrets(ctx, allProfiles); err != nil { + return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("profiles", err.Error())) + } + return nil +} + func (svc *Service) validateCrossPlatformProfileNames(ctx context.Context, appleProfiles []*fleet.MDMAppleConfigProfile, windowsProfiles []*fleet.MDMWindowsConfigProfile, appleDecls []*fleet.MDMAppleDeclaration) error { // map all profile names to check for duplicates, regardless of platform; key is name, value is one of // ".mobileconfig" or ".json" or ".xml" diff --git a/server/service/mdm_test.go b/server/service/mdm_test.go index ef0821cecf59..f89451e210e4 100644 --- a/server/service/mdm_test.go +++ b/server/service/mdm_test.go @@ -1110,6 +1110,9 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) { ) (updates fleet.MDMProfilesUpdates, err error) { return fleet.MDMProfilesUpdates{}, nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } checkShouldFail := func(t *testing.T, err error, shouldFail bool) { if !shouldFail { @@ -1186,6 +1189,12 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) { ) (updates fleet.MDMProfilesUpdates, err error) { return fleet.MDMProfilesUpdates{}, nil } + ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { + return document, nil + } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } cases := []struct { desc string @@ -1282,6 +1291,12 @@ func TestMDMBatchSetProfiles(t *testing.T) { ) (updates fleet.MDMProfilesUpdates, err error) { return fleet.MDMProfilesUpdates{}, nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } + ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { + return document, nil + } testCases := []struct { name string @@ -2090,6 +2105,12 @@ func TestBatchSetMDMProfilesLabels(t *testing.T) { } return m, nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } + ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { + return document, nil + } profiles := []fleet.MDMProfileBatchPayload{ // macOS diff --git a/server/service/scripts.go b/server/service/scripts.go index 71ed203d325c..c55d18be1238 100644 --- a/server/service/scripts.go +++ b/server/service/scripts.go @@ -175,6 +175,13 @@ func (svc *Service) RunHostScript(ctx context.Context, request *fleet.HostScript } } + if request.ScriptContents != "" { + if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{request.ScriptContents}); err != nil { + svc.authz.SkipAuthorization(ctx) + return nil, fleet.NewInvalidArgumentError("script", err.Error()) + } + } + if request.ScriptName != "" { scriptID, err := svc.GetScriptIDByName(ctx, request.ScriptName, &request.TeamID) if err != nil { @@ -510,6 +517,11 @@ func (svc *Service) NewScript(ctx context.Context, teamID *uint, name string, r Name: name, ScriptContents: file.Dos2UnixNewlines(string(b)), } + + if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{script.ScriptContents}); err != nil { + return nil, fleet.NewInvalidArgumentError("script", err.Error()) + } + if err := script.ValidateNewScript(); err != nil { return nil, fleet.NewInvalidArgumentError("script", err.Error()) } @@ -849,6 +861,7 @@ func (svc *Service) BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeT // any duplicate name in the provided set results in an error scripts := make([]*fleet.Script, 0, len(payloads)) byName := make(map[string]bool, len(payloads)) + scriptContents := []string{} for i, p := range payloads { script := &fleet.Script{ ScriptContents: string(p.ScriptContents), @@ -867,6 +880,7 @@ func (svc *Service) BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeT "duplicate script by name") } byName[script.Name] = true + scriptContents = append(scriptContents, script.ScriptContents) scripts = append(scripts, script) } @@ -874,6 +888,10 @@ func (svc *Service) BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeT return nil, nil } + if err := svc.ds.ValidateEmbeddedSecrets(ctx, scriptContents); err != nil { + return nil, fleet.NewInvalidArgumentError("script", err.Error()) + } + scriptResponses, err := svc.ds.BatchSetScripts(ctx, teamID, scripts) if err != nil { return nil, ctxerr.Wrap(ctx, err, "batch saving scripts") diff --git a/server/service/scripts_test.go b/server/service/scripts_test.go index d1957ea0e797..4f3169fd8ba1 100644 --- a/server/service/scripts_test.go +++ b/server/service/scripts_test.go @@ -60,6 +60,9 @@ func TestHostRunScript(t *testing.T) { return []byte("echo"), nil } ds.IsExecutionPendingForHostFunc = func(ctx context.Context, hostID, scriptID uint) (bool, error) { return false, nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } t.Run("authorization checks", func(t *testing.T) { testCases := []struct { @@ -539,6 +542,12 @@ func TestSavedScripts(t *testing.T) { ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: 0}, nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } + ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { + return document, nil + } testCases := []struct { name string diff --git a/server/service/setup_experience_test.go b/server/service/setup_experience_test.go index af1ca985ae2a..7696775c155b 100644 --- a/server/service/setup_experience_test.go +++ b/server/service/setup_experience_test.go @@ -57,6 +57,12 @@ func TestSetupExperienceAuth(t *testing.T) { ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) { return &fleet.Team{ID: id}, nil } + ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { + return nil + } + ds.ExpandEmbeddedSecretsFunc = func(ctx context.Context, document string) (string, error) { + return document, nil + } testCases := []struct { name string From 6b83ae30e2383c428f00ab97adefe7141b37ff86 Mon Sep 17 00:00:00 2001 From: Drew Baker <89049099+Drew-P-drawers@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:31:26 -0500 Subject: [PATCH 3/5] Embed sprint demos 4.61.0 video (#24849) Adding Sprint demos - 4.61.0 from YouTube to embed in the article --- articles/fleet-4.61.0.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/articles/fleet-4.61.0.md b/articles/fleet-4.61.0.md index 313415784bc4..65505d82fd47 100644 --- a/articles/fleet-4.61.0.md +++ b/articles/fleet-4.61.0.md @@ -1,5 +1,9 @@ # Fleet 4.61.0 | Auto-install software, email two-factor authentication (2FA), automatic Windows migration +
+ +
+ Fleet 4.61.0 is live. Check out the full [changelog](https://github.com/fleetdm/fleet/releases/tag/fleet-v4.61.0) or continue reading to get the highlights. For upgrade instructions, see our [upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in the Fleet docs. @@ -98,4 +102,4 @@ Visit our [Upgrade guide](https://fleetdm.com/docs/deploying/upgrading-fleet) in - \ No newline at end of file + From 82ec1d8e163f54404d4a6f96ceac614b8d3774c2 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 17 Dec 2024 17:08:25 -0600 Subject: [PATCH 4/5] Website: update article links on /testimonials page (#24850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #24746 Changes: - Replaced the hard-coded links to articles on the /testimonials page with server-side rendered generated links to articles. These links are now built using the website's markdown articles configuration. - Added support for a new meta tag on articles: `showOnTestimonialsPageWithEmoji` If provided and set to one of the four supported emoji (🥀, 🔌, 🚪, or 🪟), a link to the article will be added to the /testimonials page. Example: ``. - Updated the build-static-content script to throw an error if an article has an invalid `showOnTestimonialsPageWithEmoji` meta tag value. - Updated recent case study articles to have a `showOnTestimonialsPageWithEmoji` meta tag. @Drew-P-drawers When this PR is merged, you can add links to the new case studies articles to the /testimonials page with a `showOnTestimonialsPageWithEmoji` meta tag. The definitions for each of the supported emoji are in this [google doc](https://docs.google.com/document/d/1-KWQa3uMIJzeitzDRmzT3SnUoFCfcFCb6K2lyVt-Gy0/edit?tab=t.0#heading=h.oskipmb8530l) --- .../consolidate-multiple-tools-with-fleet.md | 1 + .../foursquare-quickly-migrates-to-fleet.md | 1 + ...simplifies-device-management-with-fleet.md | 1 + ...social-media-platform-switches-to-fleet.md | 2 ++ ...nhances-server-observability-with-fleet.md | 1 + ...nsitions-to-fleet-for-endpoint-security.md | 1 + ...cation-platform-chooses-fleet-for-linux.md | 1 + website/api/controllers/view-testimonials.js | 18 +++++++++++++++- website/scripts/build-static-content.js | 10 +++++++-- website/views/pages/testimonials.ejs | 21 +++---------------- 10 files changed, 36 insertions(+), 21 deletions(-) diff --git a/articles/consolidate-multiple-tools-with-fleet.md b/articles/consolidate-multiple-tools-with-fleet.md index 9de6c4cad0ea..273e9b5c11c2 100644 --- a/articles/consolidate-multiple-tools-with-fleet.md +++ b/articles/consolidate-multiple-tools-with-fleet.md @@ -88,3 +88,4 @@ To learn more about how Fleet can support your organization, visit [fleetdm.com/ + diff --git a/articles/foursquare-quickly-migrates-to-fleet.md b/articles/foursquare-quickly-migrates-to-fleet.md index 0fc128180e4d..d926941ef5fe 100644 --- a/articles/foursquare-quickly-migrates-to-fleet.md +++ b/articles/foursquare-quickly-migrates-to-fleet.md @@ -87,3 +87,4 @@ Foursquare’s migration to Fleet for device management highlights its commitmen + diff --git a/articles/global-cloud-platform-simplifies-device-management-with-fleet.md b/articles/global-cloud-platform-simplifies-device-management-with-fleet.md index 79eee619d26c..46f05087e373 100644 --- a/articles/global-cloud-platform-simplifies-device-management-with-fleet.md +++ b/articles/global-cloud-platform-simplifies-device-management-with-fleet.md @@ -95,3 +95,4 @@ I love Fleet. + diff --git a/articles/global-social-media-platform-switches-to-fleet.md b/articles/global-social-media-platform-switches-to-fleet.md index 6829aa7e53d4..4c2def730997 100644 --- a/articles/global-social-media-platform-switches-to-fleet.md +++ b/articles/global-social-media-platform-switches-to-fleet.md @@ -77,3 +77,5 @@ Transitioning to Fleet provided the platform with a strategic solution that addr + + diff --git a/articles/large-gaming-company-enhances-server-observability-with-fleet.md b/articles/large-gaming-company-enhances-server-observability-with-fleet.md index 7eec305bc3fa..581671e3696e 100644 --- a/articles/large-gaming-company-enhances-server-observability-with-fleet.md +++ b/articles/large-gaming-company-enhances-server-observability-with-fleet.md @@ -78,3 +78,4 @@ By adopting Fleet for server observability, they've successfully addressed scala + diff --git a/articles/vehicle-manufacturer-transitions-to-fleet-for-endpoint-security.md b/articles/vehicle-manufacturer-transitions-to-fleet-for-endpoint-security.md index 94aa329bd3d6..887b0c027506 100644 --- a/articles/vehicle-manufacturer-transitions-to-fleet-for-endpoint-security.md +++ b/articles/vehicle-manufacturer-transitions-to-fleet-for-endpoint-security.md @@ -82,3 +82,4 @@ The decision to purchase Fleet was driven by the need for a more reliable, compr + diff --git a/articles/worldwide-security-and-authentication-platform-chooses-fleet-for-linux.md b/articles/worldwide-security-and-authentication-platform-chooses-fleet-for-linux.md index d2b83d2788e2..a3409ea00677 100644 --- a/articles/worldwide-security-and-authentication-platform-chooses-fleet-for-linux.md +++ b/articles/worldwide-security-and-authentication-platform-chooses-fleet-for-linux.md @@ -87,3 +87,4 @@ To learn more about how Fleet can support your organization, visit [fleetdm.com/ + diff --git a/website/api/controllers/view-testimonials.js b/website/api/controllers/view-testimonials.js index 7021cef3825e..105844863d00 100644 --- a/website/api/controllers/view-testimonials.js +++ b/website/api/controllers/view-testimonials.js @@ -21,7 +21,7 @@ module.exports = { if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.testimonials) || !sails.config.builtStaticContent.compiledPagePartialsAppPath) { throw {badConfig: 'builtStaticContent.testimonials'}; } - // Get testimonials for the component. + // Get testimonials for the page contents let testimonials = _.clone(sails.config.builtStaticContent.testimonials); // Filter the testimonials by product category @@ -97,11 +97,27 @@ module.exports = { return testimonial.youtubeVideoUrl; }); + // Get articles with a showOnTestimonialsPageWithEmoji meta tag to display on this page. + let articles = sails.config.builtStaticContent.markdownPages.filter((page)=>{ + if(_.startsWith(page.htmlId, 'articles')) { + return page; + } + }); + let articlesForThisPage = _.filter(articles, (article)=>{ + return article.meta.showOnTestimonialsPageWithEmoji; + }); + // Sort the articles by their publish date. + articlesForThisPage = _.sortBy(articlesForThisPage, 'meta.publishedOn'); + + + + return { testimonialsForMdm, testimonialsForSoftwareManagement, testimonialsForObservability, testimonialsWithVideoLinks, + articlesForThisPage, }; } diff --git a/website/scripts/build-static-content.js b/website/scripts/build-static-content.js index 951b922532e4..104264662a4f 100644 --- a/website/scripts/build-static-content.js +++ b/website/scripts/build-static-content.js @@ -511,18 +511,24 @@ module.exports = { if (!isExternal) { // If the image is hosted on fleetdm.com, we'll modify the meta value to reference the file directly in the `website/assets/` folder embeddedMetadata.articleImageUrl = embeddedMetadata.articleImageUrl.replace(/https?:\/\//, '').replace(/^fleetdm\.com/, ''); } else { // If the value is a link to an image that will not be hosted on fleetdm.com, we'll throw an error. - throw new Error(`Failed compiling markdown content: An article page has an invalid a articleImageUrl meta tag () at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, change the value of the meta tag to be an image that will be hosted on fleetdm.com`); + throw new Error(`Failed compiling markdown content: An article page has an invalid articleImageUrl meta tag () at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, change the value of the meta tag to be an image that will be hosted on fleetdm.com`); } } else if(inWebsiteAssetFolder) { // If the `articleImageUrl` value is a relative link to the `website/assets/` folder, we'll modify the value to link directly to that folder. embeddedMetadata.articleImageUrl = embeddedMetadata.articleImageUrl.replace(/^\.\.\/website\/assets/g, ''); } else { // If the value is not a url and the relative link does not go to the 'website/assets/' folder, we'll throw an error. - throw new Error(`Failed compiling markdown content: An article page has an invalid a articleImageUrl meta tag () at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, change the value of the meta tag to be a URL or repo relative link to an image in the 'website/assets/images' folder`); + throw new Error(`Failed compiling markdown content: An article page has an invalid articleImageUrl meta tag () at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, change the value of the meta tag to be a URL or repo relative link to an image in the 'website/assets/images' folder`); } } if(embeddedMetadata.description && embeddedMetadata.description.length > 150) { // Throwing an error if the article's description meta tag value is over 150 characters long throw new Error(`Failed compiling markdown content: An article page has an invalid description meta tag () at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, make sure the value of the meta description is less than 150 characters long.`); } + if(embeddedMetadata.showOnTestimonialsPageWithEmoji){ + // Throw an error if a showOnTestimonialsPageWithEmoji value is not one of: 🥀, 🔌, 🚪, or 🪟. + if(!['🥀', '🔌', '🚪', '🪟'].includes(embeddedMetadata.showOnTestimonialsPageWithEmoji)){ + throw new Error(`Failed compiling markdown content: An article page has an invalid showOnTestimonialsPageWithEmoji meta tag () at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, change the value of the meta tag to be one of 🥀, 🔌, 🚪, or 🪟 and try running this script again.`); + } + } // For article pages, we'll attach the category to the `rootRelativeUrlPath`. // If the article is categorized as 'product' we'll replace the category with 'use-cases', or if it is categorized as 'success story' we'll replace it with 'device-management' rootRelativeUrlPath = ( diff --git a/website/views/pages/testimonials.ejs b/website/views/pages/testimonials.ejs index d4dd1c715386..761174aaac6c 100644 --- a/website/views/pages/testimonials.ejs +++ b/website/views/pages/testimonials.ejs @@ -117,26 +117,11 @@

Real-world stories of why the community and customers love Fleet.

-
+ <%for(let article of articlesForThisPage) {%>
- 🥀 Leading financial company consolidates multiple tools with Fleet + <%= article.meta.showOnTestimonialsPageWithEmoji %> <%= article.meta.articleTitle %>
-
- 🪟 Global edge cloud platform simplifies device management with Fleet -
-
- 🚪 Worldwide security and authentication platform chooses Fleet for Linux management -
-
- 🔌 Large gaming company enhances server observability with Fleet -
-
- 🚪 Vehicle manufacturer transitions to Fleet for endpoint security -
-
- 🚪 Foursquare quickly migrates to Fleet for Device Management -
-
+ <% } %>
Share your story From 223b25fe9874fc86819857e2cbe29f2bd99ce06b Mon Sep 17 00:00:00 2001 From: Drew Baker <89049099+Drew-P-drawers@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:13:08 -0500 Subject: [PATCH 5/5] Update testimonials.ejs (#24852) Typo fix --- website/views/pages/testimonials.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/views/pages/testimonials.ejs b/website/views/pages/testimonials.ejs index 761174aaac6c..eedc1e98505d 100644 --- a/website/views/pages/testimonials.ejs +++ b/website/views/pages/testimonials.ejs @@ -113,7 +113,7 @@
-

Case strudies

+

Case studies

Real-world stories of why the community and customers love Fleet.