From a06d6a5dfa257142c1bfd2e9fb5b4b3b22ea2d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Alvarez=20Pi=C3=B1eiro?= <95703246+emilioalvap@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:03:00 +0200 Subject: [PATCH] [Heartbeat] Add base64 encoding option to inline monitors (#45100) * Add base64 encoding option to inline monitors * Fix linter * Fix linter * Remove unused Decode() method * Fix linter * Add changelog (cherry picked from commit 2266395d99a80086a82f0fce1da9df89b53c3345) --- CHANGELOG.next.asciidoc | 1 + .../monitors/browser/source/inline.go | 24 ++++++++- .../monitors/browser/source/local.go | 4 ++ .../monitors/browser/source/project.go | 11 ++-- .../monitors/browser/source/source.go | 1 + .../monitors/browser/source/zipurl.go | 4 ++ .../heartbeat/monitors/browser/sourcejob.go | 6 ++- .../monitors/browser/sourcejob_test.go | 50 +++++++++++++++++++ .../monitors/browser/synthexec/synthexec.go | 32 ++---------- .../browser/synthexec/synthexec_test.go | 27 +++++++++- 10 files changed, 126 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index a0d6239609fa..81110c52d44f 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -262,6 +262,7 @@ otherwise no tag is added. {issue}42208[42208] {pull}42403[42403] - Added maintenance windows support for Heartbeat. {pull}41508[41508] - Add missing dependencies to ubi9-minimal distro. {pull}44556[44556] +- Add base64 encoding option to inline monitors. {pull}45100[45100] *Metricbeat* diff --git a/x-pack/heartbeat/monitors/browser/source/inline.go b/x-pack/heartbeat/monitors/browser/source/inline.go index cc3ac4f78b54..74dac89ee961 100644 --- a/x-pack/heartbeat/monitors/browser/source/inline.go +++ b/x-pack/heartbeat/monitors/browser/source/inline.go @@ -6,12 +6,14 @@ package source import ( + "encoding/base64" "fmt" "regexp" ) type InlineSource struct { - Script string `config:"script"` + Script string `config:"script"` + Encoding string `config:"encoding"` BaseSource } @@ -22,6 +24,10 @@ func (s *InlineSource) Validate() error { return ErrNoInlineScript } + if s.Encoding != "" && s.Encoding != "base64" { + return fmt.Errorf("unsupported encoding: %v", s.Encoding) + } + return nil } @@ -36,3 +42,19 @@ func (s *InlineSource) Workdir() string { func (s *InlineSource) Close() error { return nil } + +func (s *InlineSource) Decode() error { + // Don't decode if flag is missing + if s.Encoding != "base64" { + return nil + } + + decoded, err := base64.StdEncoding.DecodeString(s.Script) + if err != nil { + return fmt.Errorf("error decoding from base64: %w", err) + } + + s.Script = string(decoded) + + return nil +} diff --git a/x-pack/heartbeat/monitors/browser/source/local.go b/x-pack/heartbeat/monitors/browser/source/local.go index 7455655d4990..3efb403b8189 100644 --- a/x-pack/heartbeat/monitors/browser/source/local.go +++ b/x-pack/heartbeat/monitors/browser/source/local.go @@ -32,3 +32,7 @@ func (l *LocalSource) Workdir() string { func (l *LocalSource) Close() error { return ecserr.NewUnsupportedMonitorTypeError(ErrLocalUnsupportedType) } + +func (l *LocalSource) Decode() error { + return nil +} diff --git a/x-pack/heartbeat/monitors/browser/source/project.go b/x-pack/heartbeat/monitors/browser/source/project.go index 7c11de311345..26fbbf20935c 100644 --- a/x-pack/heartbeat/monitors/browser/source/project.go +++ b/x-pack/heartbeat/monitors/browser/source/project.go @@ -11,7 +11,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -55,7 +54,7 @@ func (p *ProjectSource) Fetch() error { return err } - tf, err := ioutil.TempFile(os.TempDir(), "elastic-synthetics-zip-") + tf, err := os.CreateTemp(os.TempDir(), "elastic-synthetics-zip-") if err != nil { return fmt.Errorf("could not create tmpfile for project monitor source: %w", err) } @@ -67,7 +66,7 @@ func (p *ProjectSource) Fetch() error { return err } - p.TargetDirectory, err = ioutil.TempDir(os.TempDir(), "elastic-synthetics-unzip-") + p.TargetDirectory, err = os.MkdirTemp(os.TempDir(), "elastic-synthetics-unzip-") if err != nil { return fmt.Errorf("could not make temp dir for unzipping project source: %w", err) } @@ -132,7 +131,7 @@ func setupProjectDir(workdir string) error { if err != nil { return err } - err = ioutil.WriteFile(filepath.Join(workdir, "package.json"), pkgJsonContent, defaultMod) + err = os.WriteFile(filepath.Join(workdir, "package.json"), pkgJsonContent, defaultMod) if err != nil { return err } @@ -173,3 +172,7 @@ func runSimpleCommand(cmd *exec.Cmd, dir string) error { logp.L().Infof("Ran %s (%d) got '%s': (%s) as (%d/%d)", cmd, cmd.ProcessState.ExitCode(), string(output), err, syscall.Getuid(), syscall.Geteuid()) return err } + +func (p *ProjectSource) Decode() error { + return nil +} diff --git a/x-pack/heartbeat/monitors/browser/source/source.go b/x-pack/heartbeat/monitors/browser/source/source.go index 21be17b5621f..6501fffe414c 100644 --- a/x-pack/heartbeat/monitors/browser/source/source.go +++ b/x-pack/heartbeat/monitors/browser/source/source.go @@ -54,6 +54,7 @@ type ISource interface { Fetch() error Workdir() string Close() error + Decode() error } type BaseSource struct { diff --git a/x-pack/heartbeat/monitors/browser/source/zipurl.go b/x-pack/heartbeat/monitors/browser/source/zipurl.go index 748b5a8acbf1..148b6af084b2 100644 --- a/x-pack/heartbeat/monitors/browser/source/zipurl.go +++ b/x-pack/heartbeat/monitors/browser/source/zipurl.go @@ -32,3 +32,7 @@ func (z *ZipURLSource) Workdir() string { func (z *ZipURLSource) Close() error { return ecserr.NewUnsupportedMonitorTypeError(ErrZipURLUnsupportedType) } + +func (z *ZipURLSource) Decode() error { + return nil +} diff --git a/x-pack/heartbeat/monitors/browser/sourcejob.go b/x-pack/heartbeat/monitors/browser/sourcejob.go index 697e51abf51a..e191d5d2131c 100644 --- a/x-pack/heartbeat/monitors/browser/sourcejob.go +++ b/x-pack/heartbeat/monitors/browser/sourcejob.go @@ -44,6 +44,10 @@ func NewSourceJob(rawCfg *config.C) (*SourceJob, error) { if err != nil { return nil, ErrBadConfig(err) } + err = s.browserCfg.Source.Active().Decode() + if err != nil { + return nil, ErrBadConfig(err) + } return s, nil } @@ -164,7 +168,7 @@ func (sj *SourceJob) jobs() []jobs.Job { var j jobs.Job isScript := sj.browserCfg.Source.Inline != nil - ctx := context.WithValue(sj.ctx, synthexec.SynthexecTimeout, sj.browserCfg.Timeout+30*time.Second) + ctx := context.WithValue(sj.ctx, synthexec.SynthexecTimeoutKey, sj.browserCfg.Timeout+30*time.Second) sFields := sj.StdFields() if isScript { diff --git a/x-pack/heartbeat/monitors/browser/sourcejob_test.go b/x-pack/heartbeat/monitors/browser/sourcejob_test.go index 0e6127d354a7..23567b7f12de 100644 --- a/x-pack/heartbeat/monitors/browser/sourcejob_test.go +++ b/x-pack/heartbeat/monitors/browser/sourcejob_test.go @@ -6,6 +6,7 @@ package browser import ( + "encoding/base64" "encoding/json" "fmt" "path" @@ -355,3 +356,52 @@ func TestFilterDevFlags(t *testing.T) { }) } } + +func TestSourceDecoding(t *testing.T) { + script := "a script" + encoded := base64.StdEncoding.EncodeToString([]byte(script)) + timeout := 30 + cfg := conf.MustNewConfigFrom(mapstr.M{ + "name": "My Name", + "id": "myId", + "source": mapstr.M{ + "inline": mapstr.M{ + "script": encoded, + "encoding": "base64", + }, + }, + "timeout": timeout, + }) + s, e := NewSourceJob(cfg) + require.NoError(t, e) + require.NotNil(t, s) + require.Equal(t, script, s.browserCfg.Source.Inline.Script) + require.Equal(t, "", s.Workdir()) + + e = s.Close() + require.NoError(t, e) +} + +func TestDisabledSourceDecoding(t *testing.T) { + script := "a script" + encoded := base64.StdEncoding.EncodeToString([]byte(script)) + timeout := 30 + cfg := conf.MustNewConfigFrom(mapstr.M{ + "name": "My Name", + "id": "myId", + "source": mapstr.M{ + "inline": mapstr.M{ + "script": encoded, + }, + }, + "timeout": timeout, + }) + s, e := NewSourceJob(cfg) + require.NoError(t, e) + require.NotNil(t, s) + require.Equal(t, encoded, s.browserCfg.Source.Inline.Script) + require.Equal(t, "", s.Workdir()) + + e = s.Close() + require.NoError(t, e) +} diff --git a/x-pack/heartbeat/monitors/browser/synthexec/synthexec.go b/x-pack/heartbeat/monitors/browser/synthexec/synthexec.go index 3c0139557f5f..0f88e859c59d 100644 --- a/x-pack/heartbeat/monitors/browser/synthexec/synthexec.go +++ b/x-pack/heartbeat/monitors/browser/synthexec/synthexec.go @@ -15,7 +15,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "runtime" "strings" "sync" @@ -43,7 +42,9 @@ type FilterJourneyConfig struct { // where these are unsupported var platformCmdMutate func(*SynthCmd) = func(*SynthCmd) {} -var SynthexecTimeout struct{} +type SynthexecTimeout string + +var SynthexecTimeoutKey = SynthexecTimeout("synthexec_timeout") // ProjectJob will run a single journey by name from the given project. func ProjectJob(ctx context.Context, projectPath string, params mapstr.M, filterJourneys FilterJourneyConfig, fields stdfields.StdMonitorFields, extraArgs ...string) (jobs.Job, error) { @@ -249,7 +250,7 @@ func runCmd( } // Get timeout from parent ctx - timeout, _ := ctx.Value(SynthexecTimeout).(time.Duration) + timeout, _ := ctx.Value(SynthexecTimeoutKey).(time.Duration) ctx, cancel := context.WithTimeout(ctx, timeout) go func() { <-ctx.Done() @@ -278,7 +279,7 @@ func runCmd( logp.L().Warn("Error executing command '%s' (%d): %s", cmd, cmd.ProcessState.ExitCode(), err) if errors.Is(ctx.Err(), context.DeadlineExceeded) { - timeout, _ := ctx.Value(SynthexecTimeout).(time.Duration) + timeout, _ := ctx.Value(SynthexecTimeoutKey).(time.Duration) cmdError = ECSErrToSynthError(ecserr.NewCmdTimeoutStatusErr(timeout, cmd.String())) } else { cmdError = ECSErrToSynthError(ecserr.NewBadCmdStatusErr(cmd.ProcessState.ExitCode(), cmd.String())) @@ -345,29 +346,6 @@ func lineToSynthEventFactory(typ string) func(bytes []byte, text string) (res *S } } -var emptyStringRegexp = regexp.MustCompile(`^\s*$`) - -// jsonToSynthEvent can take a line from the scanner and transform it into a *SynthEvent. Will return -// nil res on empty lines. -func jsonToSynthEvent(bytes []byte, text string) (res *SynthEvent, err error) { - // Skip empty lines - if emptyStringRegexp.Match(bytes) { - return nil, nil - } - - res = &SynthEvent{} - err = json.Unmarshal(bytes, res) - - if err != nil { - return nil, err - } - - if res.Type == "" { - return nil, fmt.Errorf("unmarshal succeeded, but no type found for: %s", text) - } - return res, err -} - // getNpmRoot gets the closest ancestor path that contains package.json. func getNpmRoot(path string) (string, error) { return getNpmRootIn(path, path) diff --git a/x-pack/heartbeat/monitors/browser/synthexec/synthexec_test.go b/x-pack/heartbeat/monitors/browser/synthexec/synthexec_test.go index 28158c24a1e3..cb33ff3f9f21 100644 --- a/x-pack/heartbeat/monitors/browser/synthexec/synthexec_test.go +++ b/x-pack/heartbeat/monitors/browser/synthexec/synthexec_test.go @@ -8,10 +8,12 @@ package synthexec import ( "context" + "encoding/json" "fmt" "os" "os/exec" "path/filepath" + "regexp" "runtime" "testing" "time" @@ -98,6 +100,29 @@ func TestJsonToSynthEvent(t *testing.T) { } } +var emptyStringRegexp = regexp.MustCompile(`^\s*$`) + +// jsonToSynthEvent can take a line from the scanner and transform it into a *SynthEvent. Will return +// nil res on empty lines. +func jsonToSynthEvent(bytes []byte, text string) (res *SynthEvent, err error) { + // Skip empty lines + if emptyStringRegexp.Match(bytes) { + return nil, nil + } + + res = &SynthEvent{} + err = json.Unmarshal(bytes, res) + + if err != nil { + return nil, err + } + + if res.Type == "" { + return nil, fmt.Errorf("unmarshal succeeded, but no type found for: %s", text) + } + return res, err +} + func goCmd(args ...string) *exec.Cmd { goBinary := "go" // relative by default // GET the GOROOT if defined, this helps in scenarios where @@ -182,7 +207,7 @@ func runAndCollect(t *testing.T, cmd *exec.Cmd, stdinStr string, cmdTimeout time cwd, err := os.Getwd() require.NoError(t, err) cmd.Dir = filepath.Join(cwd, "testcmd") - ctx := context.WithValue(context.TODO(), SynthexecTimeout, cmdTimeout) + ctx := context.WithValue(context.TODO(), SynthexecTimeoutKey, cmdTimeout) mpx, err := runCmd(ctx, &SynthCmd{cmd}, &stdinStr, nil, FilterJourneyConfig{}) require.NoError(t, err)