diff --git a/README.md b/README.md index 3e607978b..01814d772 100644 --- a/README.md +++ b/README.md @@ -2971,6 +2971,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 3172a8d1c..1d57eea4c 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,35 @@ 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() + + // Get the output + 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 d0e2c1517..1faf3bf15 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) diff --git a/config/endpoint/endpoint_test.go b/config/endpoint/endpoint_test.go index 080b7313d..4a54739d9 100644 --- a/config/endpoint/endpoint_test.go +++ b/config/endpoint/endpoint_test.go @@ -834,6 +834,93 @@ func TestIntegrationEvaluateHealthForSSH(t *testing.T) { conditions: []Condition{Condition("[STATUS] == 1")}, success: false, }, + { + name: "ssh-body-basic", + endpoint: Endpoint{ + Name: "ssh-body-basic", + URL: "ssh://localhost", + SSHConfig: &ssh.Config{ + Username: "scenario", + Password: "scenario", + }, + Body: "{ \"command\": \"echo 'test-output'\" }", + }, + conditions: []Condition{ + Condition("[STATUS] == 0"), + Condition("[BODY] == test-output"), + }, + success: true, + }, + { + name: "ssh-body-pattern", + endpoint: Endpoint{ + Name: "ssh-body-pattern", + URL: "ssh://localhost", + SSHConfig: &ssh.Config{ + Username: "scenario", + Password: "scenario", + }, + Body: "{ \"command\": \"echo 'test-pattern-match'\" }", + }, + conditions: []Condition{ + Condition("[STATUS] == 0"), + Condition("[BODY] == pat(*pattern*)"), + }, + success: true, + }, + { + name: "ssh-body-json-path", + endpoint: Endpoint{ + Name: "ssh-body-json", + URL: "ssh://localhost", + SSHConfig: &ssh.Config{ + Username: "scenario", + Password: "scenario", + }, + Body: "{ \"command\": \"echo '{\\\"status\\\": \\\"healthy\\\", \\\"memory\\\": {\\\"used\\\": 512}}'\" }", + }, + conditions: []Condition{ + Condition("[STATUS] == 0"), + Condition("[BODY].status == healthy"), + Condition("[BODY].memory.used > 500"), + }, + success: true, + }, + { + name: "ssh-body-functions", + endpoint: Endpoint{ + Name: "ssh-body-functions", + URL: "ssh://localhost", + SSHConfig: &ssh.Config{ + Username: "scenario", + Password: "scenario", + }, + Body: "{ \"command\": \"echo -n '12345'\" }", + }, + conditions: []Condition{ + Condition("[STATUS] == 0"), + Condition("len([BODY]) == 5"), + Condition("has([BODY]) == true"), + }, + success: true, + }, + { + name: "ssh-command-fail-with-body", + endpoint: Endpoint{ + Name: "ssh-command-fail-body", + URL: "ssh://localhost", + SSHConfig: &ssh.Config{ + Username: "scenario", + Password: "scenario", + }, + Body: "{ \"command\": \"echo 'error message'; exit 1\" }", + }, + conditions: []Condition{ + Condition("[STATUS] == 1"), + Condition("[BODY] == pat(*error*)"), + }, + success: true, + }, } for _, scenario := range scenarios {