From 5c78bd92fb8c93e3f6b217046b0a23d79a39fb45 Mon Sep 17 00:00:00 2001 From: yansh97 Date: Sat, 4 Oct 2025 10:52:34 +0800 Subject: [PATCH] feat(client): Support body placeholder for SSH endpoints (#1286) * feat(ssh): Add BODY placeholder support for SSH endpoints - Modify ExecuteSSHCommand to capture stdout output - Update SSH endpoint handling to use needsToReadBody() mechanism - Add comprehensive test cases for SSH BODY functionality - Support basic body content, pattern matching, JSONPath, and functions - Maintain backward compatibility with existing SSH endpoints * docs: Add SSH BODY placeholder examples to README - Add [BODY] placeholder to supported SSH placeholders list - Add comprehensive examples showing various SSH BODY conditions - Include pattern matching, length checks, JSONPath expressions - Demonstrate function wrappers (len, has, any) usage * Revert "docs: Add SSH BODY placeholder examples to README" This reverts commit ae93e386833fb71885cab637fc46eb31500cb7c4. * docs: Add [BODY] placeholder to SSH supported placeholders list * test: remove SSH BODY placeholder test cases * Update client/client.go * Update client/client.go * docs: Add minimal SSH BODY example --------- Co-authored-by: TwiN --- README.md | 4 +++- client/client.go | 19 ++++++++++++------- config/endpoint/endpoint.go | 7 ++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4321e9ee..179923a4 100644 --- a/README.md +++ b/README.md @@ -3031,12 +3031,13 @@ endpoints: password: "password" body: | { - "command": "uptime" + "command": "echo '{\"memory\": {\"used\": 512}}'" } interval: 1m conditions: - "[CONNECTED] == true" - "[STATUS] == 0" + - "[BODY].memory.used > 500" ``` you can also use no authentication to monitor the endpoint by not specifying the username @@ -3059,6 +3060,7 @@ endpoints: The following placeholders are supported for endpoints of type SSH: - `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise - `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success) +- `[BODY]` resolves to the stdout output of the command executed on the remote server - `[IP]` resolves to the IP address of the server - `[RESPONSE_TIME]` resolves to the time it took to establish the connection and execute the command diff --git a/client/client.go b/client/client.go index 6fd49f95..81df0d2a 100644 --- a/client/client.go +++ b/client/client.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "context" "crypto/tls" "crypto/x509" @@ -301,7 +302,7 @@ func CheckSSHBanner(address string, cfg *Config) (bool, int, error) { } // ExecuteSSHCommand executes a command to an address using the SSH protocol. -func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) { +func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, []byte, error) { type Body struct { Command string `json:"command"` } @@ -309,26 +310,30 @@ func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool var b Body body = parseLocalAddressPlaceholder(body, sshClient.Conn.LocalAddr()) if err := json.Unmarshal([]byte(body), &b); err != nil { - return false, 0, err + return false, 0, nil, err } sess, err := sshClient.NewSession() if err != nil { - return false, 0, err + return false, 0, nil, err } + // Capture stdout + var stdout bytes.Buffer + sess.Stdout = &stdout err = sess.Start(b.Command) if err != nil { - return false, 0, err + return false, 0, nil, err } defer sess.Close() err = sess.Wait() + output := stdout.Bytes() if err == nil { - return true, 0, nil + return true, 0, output, nil } var exitErr *ssh.ExitError if ok := errors.As(err, &exitErr); !ok { - return false, 0, err + return false, 0, nil, err } - return true, exitErr.ExitStatus(), nil + return true, exitErr.ExitStatus(), output, nil } // Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged diff --git a/config/endpoint/endpoint.go b/config/endpoint/endpoint.go index d0e2c151..1faf3bf1 100644 --- a/config/endpoint/endpoint.go +++ b/config/endpoint/endpoint.go @@ -514,11 +514,16 @@ func (e *Endpoint) call(result *Result) { result.AddError(err.Error()) return } - result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, e.getParsedBody(), e.ClientConfig) + var output []byte + result.Success, result.HTTPStatus, output, err = client.ExecuteSSHCommand(cli, e.getParsedBody(), e.ClientConfig) if err != nil { result.AddError(err.Error()) return } + // Only store the output in result.Body if there's a condition that uses the BodyPlaceholder + if e.needsToReadBody() { + result.Body = output + } result.Duration = time.Since(startTime) } else { response, err = client.GetHTTPClient(e.ClientConfig).Do(request)