diff --git a/ssh_shell_command_executor_using_sshj/pom.xml b/ssh_shell_command_executor_using_sshj/pom.xml
new file mode 100644
index 00000000..08d3b809
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/pom.xml
@@ -0,0 +1,139 @@
+
+
+ 4.0.0
+ com.testsigma.addons
+ ssh_shell_command_executor_using_sshj
+ 1.0.1
+ jar
+
+
+ UTF-8
+ 11
+ 11
+ 1.2.24_cloud
+ 5.8.0-M1
+ 1.0.0
+ 3.2.1
+ 1.18.30
+
+
+
+
+
+ com.testsigma
+ testsigma-java-sdk
+ ${testsigma.sdk.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ true
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.jupiter.version}
+ test
+
+
+ org.testng
+ testng
+ 6.14.3
+
+
+
+ org.seleniumhq.selenium
+ selenium-java
+ 4.33.0
+
+
+
+ io.appium
+ java-client
+ 9.4.0
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.13.0
+
+
+
+
+
+ com.hierynomus
+ sshj
+ 0.38.0
+
+
+ org.apache.commons
+ commons-lang3
+ 3.14.0
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.12.3
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.13
+
+
+ org.apache.poi
+ poi
+ 5.2.4
+
+
+
+
+ ssh_shell_command_executor_using_sshj
+
+
+
+ true
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ ${maven.source.plugin.version}
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+
+
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/android/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/android/SSHCommandExecutionSSHJ.java
new file mode 100644
index 00000000..6b8933be
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/android/SSHCommandExecutionSSHJ.java
@@ -0,0 +1,149 @@
+package com.testsigma.addons.android;
+
+import com.testsigma.addons.util.ImageComparisonUtils;
+import com.testsigma.addons.util.StringToImageConverter;
+import com.testsigma.sdk.ApplicationType;
+import com.testsigma.sdk.AndroidAction;
+import com.testsigma.sdk.Result;
+import com.testsigma.sdk.annotation.Action;
+import com.testsigma.sdk.annotation.RunTimeData;
+import com.testsigma.sdk.annotation.TestData;
+import com.testsigma.sdk.annotation.TestStepResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.connection.channel.direct.Session;
+import net.schmizz.sshj.connection.channel.direct.Session.Command;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.openqa.selenium.NoSuchElementException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name,"
+ + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ,"
+ + " Command Separator: Command-Separator , Store Output Variable: Variable-Name",
+ description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). "
+ + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.",
+ applicationType = ApplicationType.ANDROID,
+ displayName = "SSHJ: Connect to SSH server and execute commands",
+ useCustomScreenshot = true)
+public class SSHCommandExecutionSSHJ extends AndroidAction {
+
+ @TestStepResult
+ private com.testsigma.sdk.TestStepResult testStepResult;
+
+ @TestData(reference = "Host-Name")
+ private com.testsigma.sdk.TestData hostName;
+ @TestData(reference = "Port-Number")
+ private com.testsigma.sdk.TestData portNumber;
+ @TestData(reference = "User-Name")
+ private com.testsigma.sdk.TestData userName;
+ @TestData(reference = "User-Password")
+ private com.testsigma.sdk.TestData userPassword;
+ @TestData(reference = "Terminal-Commands")
+ private com.testsigma.sdk.TestData commands;
+ @TestData(reference = "Command-Separator")
+ private com.testsigma.sdk.TestData commandSeparator;
+ @TestData(reference = "Variable-Name")
+ private com.testsigma.sdk.TestData storeVariable;
+
+ @RunTimeData
+ private com.testsigma.sdk.RunTimeData runTimeData;
+
+ @Override
+ public Result execute() throws NoSuchElementException {
+ logger.info("Initiating execution (SSHJ) - Android");
+
+ String user = userName.getValue().toString();
+ String host = hostName.getValue().toString();
+ String password = userPassword.getValue().toString();
+ int port = Integer.parseInt(portNumber.getValue().toString());
+ String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty()
+ ? commandSeparator.getValue().toString()
+ : "&&";
+ String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&");
+
+ String wrappedCommand = wrapInLoginShell(allCommands);
+
+ SSHClient ssh = null;
+ Session session = null;
+
+ try {
+ ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
+ ssh.authPassword(user, password);
+ logger.info("SSH session connected (SSHJ).");
+
+ session = ssh.startSession();
+ Command cmd = session.exec(wrappedCommand);
+ StringBuilder output = new StringBuilder();
+ try (Reader reader = new InputStreamReader(cmd.getInputStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = reader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ cmd.join(30, TimeUnit.SECONDS);
+ if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) {
+ try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = errReader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ }
+
+ runTimeData.setKey(storeVariable.getValue().toString());
+ runTimeData.setValue(output.toString());
+
+ File screenshotFile = null;
+ try {
+ screenshotFile = StringToImageConverter.convertToFile(output);
+ String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null;
+ if (s3Url != null && !s3Url.isEmpty()) {
+ ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger);
+ boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath());
+ if (!uploadResult) {
+ logger.debug("Error uploading custom screenshot to S3; step result may not show the output image.");
+ } else {
+ logger.debug("Custom screenshot (command output) uploaded successfully.");
+ }
+ }
+ } finally {
+ if (screenshotFile != null && screenshotFile.exists()) {
+ screenshotFile.deleteOnExit();
+ }
+ }
+
+ setSuccessMessage("Output is: " + output.toString());
+ return Result.SUCCESS;
+ } catch (Exception e) {
+ String errorStack = ExceptionUtils.getStackTrace(e);
+ logger.info("Error occurred while executing the command (SSHJ): " + errorStack);
+ setErrorMessage("Error occurred while executing the command: " + e.getMessage());
+ return Result.FAILED;
+ } finally {
+ try {
+ if (session != null) session.close();
+ if (ssh != null) ssh.disconnect();
+ } catch (IOException e) {
+ logger.info("Error closing SSHJ resources: " + e.getMessage());
+ }
+ }
+ }
+
+ private static String wrapInLoginShell(String command) {
+ String escaped = command.replace("'", "'\\''");
+ return "bash -l -c '" + escaped + "'";
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/ios/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/ios/SSHCommandExecutionSSHJ.java
new file mode 100644
index 00000000..5afa03ed
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/ios/SSHCommandExecutionSSHJ.java
@@ -0,0 +1,149 @@
+package com.testsigma.addons.ios;
+
+import com.testsigma.addons.util.ImageComparisonUtils;
+import com.testsigma.addons.util.StringToImageConverter;
+import com.testsigma.sdk.ApplicationType;
+import com.testsigma.sdk.IOSAction;
+import com.testsigma.sdk.Result;
+import com.testsigma.sdk.annotation.Action;
+import com.testsigma.sdk.annotation.RunTimeData;
+import com.testsigma.sdk.annotation.TestData;
+import com.testsigma.sdk.annotation.TestStepResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.connection.channel.direct.Session;
+import net.schmizz.sshj.connection.channel.direct.Session.Command;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.openqa.selenium.NoSuchElementException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name,"
+ + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ,"
+ + " Command Separator: Command-Separator , Store Output Variable: Variable-Name",
+ description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). "
+ + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.",
+ applicationType = ApplicationType.IOS,
+ displayName = "SSHJ: Connect to SSH server and execute commands",
+ useCustomScreenshot = true)
+public class SSHCommandExecutionSSHJ extends IOSAction {
+
+ @TestStepResult
+ private com.testsigma.sdk.TestStepResult testStepResult;
+
+ @TestData(reference = "Host-Name")
+ private com.testsigma.sdk.TestData hostName;
+ @TestData(reference = "Port-Number")
+ private com.testsigma.sdk.TestData portNumber;
+ @TestData(reference = "User-Name")
+ private com.testsigma.sdk.TestData userName;
+ @TestData(reference = "User-Password")
+ private com.testsigma.sdk.TestData userPassword;
+ @TestData(reference = "Terminal-Commands")
+ private com.testsigma.sdk.TestData commands;
+ @TestData(reference = "Command-Separator")
+ private com.testsigma.sdk.TestData commandSeparator;
+ @TestData(reference = "Variable-Name")
+ private com.testsigma.sdk.TestData storeVariable;
+
+ @RunTimeData
+ private com.testsigma.sdk.RunTimeData runTimeData;
+
+ @Override
+ public Result execute() throws NoSuchElementException {
+ logger.info("Initiating execution (SSHJ) - iOS");
+
+ String user = userName.getValue().toString();
+ String host = hostName.getValue().toString();
+ String password = userPassword.getValue().toString();
+ int port = Integer.parseInt(portNumber.getValue().toString());
+ String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty()
+ ? commandSeparator.getValue().toString()
+ : "&&";
+ String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&");
+
+ String wrappedCommand = wrapInLoginShell(allCommands);
+
+ SSHClient ssh = null;
+ Session session = null;
+
+ try {
+ ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
+ ssh.authPassword(user, password);
+ logger.info("SSH session connected (SSHJ).");
+
+ session = ssh.startSession();
+ Command cmd = session.exec(wrappedCommand);
+ StringBuilder output = new StringBuilder();
+ try (Reader reader = new InputStreamReader(cmd.getInputStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = reader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ cmd.join(30, TimeUnit.SECONDS);
+ if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) {
+ try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = errReader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ }
+
+ runTimeData.setKey(storeVariable.getValue().toString());
+ runTimeData.setValue(output.toString());
+
+ File screenshotFile = null;
+ try {
+ screenshotFile = StringToImageConverter.convertToFile(output);
+ String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null;
+ if (s3Url != null && !s3Url.isEmpty()) {
+ ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger);
+ boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath());
+ if (!uploadResult) {
+ logger.debug("Error uploading custom screenshot to S3; step result may not show the output image.");
+ } else {
+ logger.debug("Custom screenshot (command output) uploaded successfully.");
+ }
+ }
+ } finally {
+ if (screenshotFile != null && screenshotFile.exists()) {
+ screenshotFile.deleteOnExit();
+ }
+ }
+
+ setSuccessMessage("Output is: " + output.toString());
+ return Result.SUCCESS;
+ } catch (Exception e) {
+ String errorStack = ExceptionUtils.getStackTrace(e);
+ logger.info("Error occurred while executing the command (SSHJ): " + errorStack);
+ setErrorMessage("Error occurred while executing the command: " + e.getMessage());
+ return Result.FAILED;
+ } finally {
+ try {
+ if (session != null) session.close();
+ if (ssh != null) ssh.disconnect();
+ } catch (IOException e) {
+ logger.info("Error closing SSHJ resources: " + e.getMessage());
+ }
+ }
+ }
+
+ private static String wrapInLoginShell(String command) {
+ String escaped = command.replace("'", "'\\''");
+ return "bash -l -c '" + escaped + "'";
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/mobileWeb/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/mobileWeb/SSHCommandExecutionSSHJ.java
new file mode 100644
index 00000000..79670de0
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/mobileWeb/SSHCommandExecutionSSHJ.java
@@ -0,0 +1,149 @@
+package com.testsigma.addons.mobileWeb;
+
+import com.testsigma.addons.util.ImageComparisonUtils;
+import com.testsigma.addons.util.StringToImageConverter;
+import com.testsigma.sdk.ApplicationType;
+import com.testsigma.sdk.Result;
+import com.testsigma.sdk.WebAction;
+import com.testsigma.sdk.annotation.Action;
+import com.testsigma.sdk.annotation.RunTimeData;
+import com.testsigma.sdk.annotation.TestData;
+import com.testsigma.sdk.annotation.TestStepResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.connection.channel.direct.Session;
+import net.schmizz.sshj.connection.channel.direct.Session.Command;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.openqa.selenium.NoSuchElementException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name,"
+ + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ,"
+ + " Command Separator: Command-Separator , Store Output Variable: Variable-Name",
+ description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). "
+ + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.",
+ applicationType = ApplicationType.MOBILE_WEB,
+ displayName = "SSHJ: Connect to SSH server and execute commands",
+ useCustomScreenshot = true)
+public class SSHCommandExecutionSSHJ extends WebAction {
+
+ @TestStepResult
+ private com.testsigma.sdk.TestStepResult testStepResult;
+
+ @TestData(reference = "Host-Name")
+ private com.testsigma.sdk.TestData hostName;
+ @TestData(reference = "Port-Number")
+ private com.testsigma.sdk.TestData portNumber;
+ @TestData(reference = "User-Name")
+ private com.testsigma.sdk.TestData userName;
+ @TestData(reference = "User-Password")
+ private com.testsigma.sdk.TestData userPassword;
+ @TestData(reference = "Terminal-Commands")
+ private com.testsigma.sdk.TestData commands;
+ @TestData(reference = "Command-Separator")
+ private com.testsigma.sdk.TestData commandSeparator;
+ @TestData(reference = "Variable-Name")
+ private com.testsigma.sdk.TestData storeVariable;
+
+ @RunTimeData
+ private com.testsigma.sdk.RunTimeData runTimeData;
+
+ @Override
+ public Result execute() throws NoSuchElementException {
+ logger.info("Initiating execution (SSHJ) - Mobile Web");
+
+ String user = userName.getValue().toString();
+ String host = hostName.getValue().toString();
+ String password = userPassword.getValue().toString();
+ int port = Integer.parseInt(portNumber.getValue().toString());
+ String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty()
+ ? commandSeparator.getValue().toString()
+ : "&&";
+ String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&");
+
+ String wrappedCommand = wrapInLoginShell(allCommands);
+
+ SSHClient ssh = null;
+ Session session = null;
+
+ try {
+ ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
+ ssh.authPassword(user, password);
+ logger.info("SSH session connected (SSHJ).");
+
+ session = ssh.startSession();
+ Command cmd = session.exec(wrappedCommand);
+ StringBuilder output = new StringBuilder();
+ try (Reader reader = new InputStreamReader(cmd.getInputStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = reader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ cmd.join(30, TimeUnit.SECONDS);
+ if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) {
+ try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = errReader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ }
+
+ runTimeData.setKey(storeVariable.getValue().toString());
+ runTimeData.setValue(output.toString());
+
+ File screenshotFile = null;
+ try {
+ screenshotFile = StringToImageConverter.convertToFile(output);
+ String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null;
+ if (s3Url != null && !s3Url.isEmpty()) {
+ ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger);
+ boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath());
+ if (!uploadResult) {
+ logger.debug("Error uploading custom screenshot to S3; step result may not show the output image.");
+ } else {
+ logger.debug("Custom screenshot (command output) uploaded successfully.");
+ }
+ }
+ } finally {
+ if (screenshotFile != null && screenshotFile.exists()) {
+ screenshotFile.deleteOnExit();
+ }
+ }
+
+ setSuccessMessage("Output is: " + output.toString());
+ return Result.SUCCESS;
+ } catch (Exception e) {
+ String errorStack = ExceptionUtils.getStackTrace(e);
+ logger.info("Error occurred while executing the command (SSHJ): " + errorStack);
+ setErrorMessage("Error occurred while executing the command: " + e.getMessage());
+ return Result.FAILED;
+ } finally {
+ try {
+ if (session != null) session.close();
+ if (ssh != null) ssh.disconnect();
+ } catch (IOException e) {
+ logger.info("Error closing SSHJ resources: " + e.getMessage());
+ }
+ }
+ }
+
+ private static String wrapInLoginShell(String command) {
+ String escaped = command.replace("'", "'\\''");
+ return "bash -l -c '" + escaped + "'";
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/salesforce/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/salesforce/SSHCommandExecutionSSHJ.java
new file mode 100644
index 00000000..32712c8c
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/salesforce/SSHCommandExecutionSSHJ.java
@@ -0,0 +1,150 @@
+package com.testsigma.addons.salesforce;
+
+import com.testsigma.addons.util.ImageComparisonUtils;
+import com.testsigma.addons.util.StringToImageConverter;
+import com.testsigma.sdk.ApplicationType;
+import com.testsigma.sdk.Result;
+import com.testsigma.sdk.WebAction;
+import com.testsigma.sdk.annotation.Action;
+import com.testsigma.sdk.annotation.RunTimeData;
+import com.testsigma.sdk.annotation.TestData;
+import com.testsigma.sdk.annotation.TestStepResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.connection.channel.direct.Session;
+import net.schmizz.sshj.connection.channel.direct.Session.Command;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.openqa.selenium.NoSuchElementException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name,"
+ + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ,"
+ + " Command Separator: Command-Separator , Store Output Variable: Variable-Name",
+ description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). "
+ + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable. "
+ + "For use in Salesforce test plans (applicationType WEB).",
+ applicationType = ApplicationType.WEB,
+ displayName = "SSHJ: Connect to SSH server and execute commands (Salesforce)",
+ useCustomScreenshot = true)
+public class SSHCommandExecutionSSHJ extends WebAction {
+
+ @TestStepResult
+ private com.testsigma.sdk.TestStepResult testStepResult;
+
+ @TestData(reference = "Host-Name")
+ private com.testsigma.sdk.TestData hostName;
+ @TestData(reference = "Port-Number")
+ private com.testsigma.sdk.TestData portNumber;
+ @TestData(reference = "User-Name")
+ private com.testsigma.sdk.TestData userName;
+ @TestData(reference = "User-Password")
+ private com.testsigma.sdk.TestData userPassword;
+ @TestData(reference = "Terminal-Commands")
+ private com.testsigma.sdk.TestData commands;
+ @TestData(reference = "Command-Separator")
+ private com.testsigma.sdk.TestData commandSeparator;
+ @TestData(reference = "Variable-Name")
+ private com.testsigma.sdk.TestData storeVariable;
+
+ @RunTimeData
+ private com.testsigma.sdk.RunTimeData runTimeData;
+
+ @Override
+ public Result execute() throws NoSuchElementException {
+ logger.info("Initiating execution (SSHJ) - Salesforce");
+
+ String user = userName.getValue().toString();
+ String host = hostName.getValue().toString();
+ String password = userPassword.getValue().toString();
+ int port = Integer.parseInt(portNumber.getValue().toString());
+ String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty()
+ ? commandSeparator.getValue().toString()
+ : "&&";
+ String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&");
+
+ String wrappedCommand = wrapInLoginShell(allCommands);
+
+ SSHClient ssh = null;
+ Session session = null;
+
+ try {
+ ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
+ ssh.authPassword(user, password);
+ logger.info("SSH session connected (SSHJ).");
+
+ session = ssh.startSession();
+ Command cmd = session.exec(wrappedCommand);
+ StringBuilder output = new StringBuilder();
+ try (Reader reader = new InputStreamReader(cmd.getInputStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = reader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ cmd.join(30, TimeUnit.SECONDS);
+ if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) {
+ try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = errReader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ }
+
+ runTimeData.setKey(storeVariable.getValue().toString());
+ runTimeData.setValue(output.toString());
+
+ File screenshotFile = null;
+ try {
+ screenshotFile = StringToImageConverter.convertToFile(output);
+ String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null;
+ if (s3Url != null && !s3Url.isEmpty()) {
+ ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger);
+ boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath());
+ if (!uploadResult) {
+ logger.debug("Error uploading custom screenshot to S3; step result may not show the output image.");
+ } else {
+ logger.debug("Custom screenshot (command output) uploaded successfully.");
+ }
+ }
+ } finally {
+ if (screenshotFile != null && screenshotFile.exists()) {
+ screenshotFile.deleteOnExit();
+ }
+ }
+
+ setSuccessMessage("Output is: " + output.toString());
+ return Result.SUCCESS;
+ } catch (Exception e) {
+ String errorStack = ExceptionUtils.getStackTrace(e);
+ logger.info("Error occurred while executing the command (SSHJ): " + errorStack);
+ setErrorMessage("Error occurred while executing the command: " + e.getMessage());
+ return Result.FAILED;
+ } finally {
+ try {
+ if (session != null) session.close();
+ if (ssh != null) ssh.disconnect();
+ } catch (IOException e) {
+ logger.info("Error closing SSHJ resources: " + e.getMessage());
+ }
+ }
+ }
+
+ private static String wrapInLoginShell(String command) {
+ String escaped = command.replace("'", "'\\''");
+ return "bash -l -c '" + escaped + "'";
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java
new file mode 100644
index 00000000..8e7e4f9f
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/ImageComparisonUtils.java
@@ -0,0 +1,56 @@
+package com.testsigma.addons.util;
+
+import com.testsigma.sdk.Logger;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.EntityBuilder;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.openqa.selenium.WebDriver;
+
+import java.io.File;
+
+public class ImageComparisonUtils {
+ WebDriver driver;
+ Logger logger;
+
+ public ImageComparisonUtils(WebDriver driver, Logger logger) {
+ this.driver = driver;
+ this.logger = logger;
+ }
+
+ RequestConfig config = RequestConfig.custom()
+ .setSocketTimeout(10 * 60 * 1000)
+ .setConnectionRequestTimeout(60 * 1000)
+ .setConnectTimeout(60 * 1000)
+ .build();
+
+ public boolean uploadFile(String s3SignedURL, String localPath) {
+ logger.debug("s3SignedURL - " + s3SignedURL);
+ logger.debug("localPath - " + localPath);
+ boolean localUrlExists = new File(localPath).exists();
+ if (localUrlExists) {
+ logger.info(String.format("Uploading test asset to storage, presigned-URL:%s, localFilePath:%s", s3SignedURL, localPath));
+ try (CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(config).build()) {
+ HttpPut httpPut = new HttpPut(s3SignedURL);
+
+ File file = new File(localPath);
+ HttpEntity entity = EntityBuilder.create().setFile(file).build();
+ httpPut.setEntity(entity);
+ HttpResponse response = httpclient.execute(httpPut);
+ logger.info("Upload completed");
+ return true;
+ } catch (Exception e) {
+ logger.info("Exception while uploading custom screenshot to s3: " + ExceptionUtils.getStackTrace(e));
+ return false;
+ }
+ } else {
+ logger.info("Local path does not exist");
+ return false;
+ }
+ }
+
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/StringToImageConverter.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/StringToImageConverter.java
new file mode 100644
index 00000000..bde8eeee
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/util/StringToImageConverter.java
@@ -0,0 +1,153 @@
+package com.testsigma.addons.util;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts a string into an image with white background and the text drawn on it.
+ * Font size is chosen based on the input text length (shorter text = larger font).
+ */
+public final class StringToImageConverter {
+
+ private static final int MIN_FONT_SIZE = 12;
+ private static final int MAX_FONT_SIZE = 72;
+ private static final int PADDING = 40;
+ private static final int MIN_IMAGE_WIDTH = 200;
+ private static final int MAX_IMAGE_WIDTH = 1200;
+ private static final String FONT_NAME = Font.SANS_SERIF;
+
+ private StringToImageConverter() {
+ }
+
+ /**
+ * Converts the given text into a BufferedImage with white background and black text.
+ * Font size is derived from text length.
+ *
+ * @param text the string to render (can be null or empty; empty yields a small placeholder image)
+ * @return BufferedImage with white background and text
+ */
+ public static BufferedImage convert(String text) {
+ String safeText = text == null ? "" : text;
+ int fontSize = computeFontSize(safeText.length());
+ Font font = new Font(FONT_NAME, Font.PLAIN, fontSize);
+
+ FontMetrics fm = getFontMetrics(font);
+ List lines = wrapLines(safeText, fm, MAX_IMAGE_WIDTH - 2 * PADDING);
+ int lineHeight = fm.getHeight();
+ int ascent = fm.getAscent();
+ int imageWidth = Math.max(MIN_IMAGE_WIDTH, computeTextWidth(lines, fm) + 2 * PADDING);
+ int imageHeight = Math.max(40, lines.size() * lineHeight + 2 * PADDING);
+
+ BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g = image.createGraphics();
+ try {
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ g.setColor(Color.WHITE);
+ g.fillRect(0, 0, imageWidth, imageHeight);
+ g.setColor(Color.BLACK);
+ g.setFont(font);
+
+ int y = PADDING + ascent;
+ for (String line : lines) {
+ g.drawString(line, PADDING, y);
+ y += lineHeight;
+ }
+ } finally {
+ g.dispose();
+ }
+ return image;
+ }
+
+ /**
+ * Converts the given text to an image and writes it to a temporary PNG file.
+ *
+ * @param text the string to render (can be CharSequence e.g. StringBuilder to avoid extra copy)
+ * @return temporary File containing the PNG image
+ * @throws IOException if writing the file fails
+ */
+ public static File convertToFile(CharSequence text) throws IOException {
+ BufferedImage image = convert(text == null ? null : text.toString());
+ File tempFile = File.createTempFile("testsigma-text-image-", ".png");
+ ImageIO.write(image, "png", tempFile);
+ return tempFile;
+ }
+
+ private static int computeFontSize(int textLength) {
+ if (textLength <= 0) return MAX_FONT_SIZE;
+ if (textLength <= 15) return MAX_FONT_SIZE;
+ if (textLength <= 50) return 48;
+ if (textLength <= 150) return 32;
+ if (textLength <= 400) return 24;
+ if (textLength <= 800) return 18;
+ return Math.max(MIN_FONT_SIZE, 14);
+ }
+
+ private static FontMetrics getFontMetrics(Font font) {
+ BufferedImage dummy = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g = dummy.createGraphics();
+ g.setFont(font);
+ FontMetrics fm = g.getFontMetrics();
+ g.dispose();
+ return fm;
+ }
+
+ private static List wrapLines(String text, FontMetrics fm, int maxWidth) {
+ List lines = new ArrayList<>();
+ if (text.isEmpty()) {
+ lines.add(" ");
+ return lines;
+ }
+ String[] paragraphs = text.split("\\n", -1);
+ for (String para : paragraphs) {
+ if (para.isEmpty()) {
+ lines.add(" ");
+ continue;
+ }
+ StringBuilder line = new StringBuilder();
+ for (String word : para.split(" ", -1)) {
+ String candidate = line.length() == 0 ? word : line + " " + word;
+ if (fm.stringWidth(candidate) <= maxWidth) {
+ line.setLength(0);
+ line.append(candidate);
+ } else {
+ if (line.length() > 0) {
+ lines.add(line.toString());
+ line.setLength(0);
+ }
+ if (fm.stringWidth(word) <= maxWidth) {
+ line.append(word);
+ } else {
+ for (int i = 0; i < word.length(); ) {
+ int fit = 0;
+ int maxFit = word.length() - i;
+ while (fit < maxFit && fm.stringWidth(word.substring(i, i + fit + 1)) <= maxWidth) {
+ fit++;
+ }
+ if (fit == 0) fit = 1;
+ lines.add(word.substring(i, i + fit));
+ i += fit;
+ }
+ }
+ }
+ }
+ if (line.length() > 0) {
+ lines.add(line.toString());
+ }
+ }
+ return lines.isEmpty() ? List.of(" ") : lines;
+ }
+
+ private static int computeTextWidth(List lines, FontMetrics fm) {
+ int max = 0;
+ for (String line : lines) {
+ max = Math.max(max, fm.stringWidth(line));
+ }
+ return max;
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/web/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/web/SSHCommandExecutionSSHJ.java
new file mode 100644
index 00000000..ee16a69d
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/web/SSHCommandExecutionSSHJ.java
@@ -0,0 +1,154 @@
+package com.testsigma.addons.web;
+
+import com.testsigma.addons.util.ImageComparisonUtils;
+import com.testsigma.addons.util.StringToImageConverter;
+import com.testsigma.sdk.ApplicationType;
+import com.testsigma.sdk.Result;
+import com.testsigma.sdk.WebAction;
+import com.testsigma.sdk.annotation.Action;
+import com.testsigma.sdk.annotation.RunTimeData;
+import com.testsigma.sdk.annotation.TestData;
+import com.testsigma.sdk.annotation.TestStepResult;
+import lombok.Data;
+
+import java.io.File;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.connection.channel.direct.Session;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import net.schmizz.sshj.connection.channel.direct.Session.Command;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.openqa.selenium.NoSuchElementException;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name," +
+ " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ," +
+ " Command Separator: Command-Separator , Store Output Variable: Variable-Name",
+ description = "Executes commands on an SSH server using the SSHJ library (alternative implementation)",
+ applicationType = ApplicationType.WEB,
+ useCustomScreenshot = true)
+public class SSHCommandExecutionSSHJ extends WebAction {
+
+ @TestStepResult
+ private com.testsigma.sdk.TestStepResult testStepResult;
+
+ @TestData(reference = "Host-Name")
+ private com.testsigma.sdk.TestData hostName;
+ @TestData(reference = "Port-Number")
+ private com.testsigma.sdk.TestData portNumber;
+ @TestData(reference = "User-Name")
+ private com.testsigma.sdk.TestData userName;
+ @TestData(reference = "User-Password")
+ private com.testsigma.sdk.TestData userPassword;
+ @TestData(reference = "Terminal-Commands")
+ private com.testsigma.sdk.TestData commands;
+ @TestData(reference = "Command-Separator")
+ private com.testsigma.sdk.TestData commandSeparator;
+ @TestData(reference = "Variable-Name")
+ private com.testsigma.sdk.TestData storeVariable;
+
+ @RunTimeData
+ private com.testsigma.sdk.RunTimeData runTimeData;
+
+ @Override
+ public com.testsigma.sdk.Result execute() throws NoSuchElementException {
+ logger.info("Initiating execution (SSHJ)");
+
+ String user = userName.getValue().toString();
+ String host = hostName.getValue().toString();
+ String password = userPassword.getValue().toString();
+ int port = Integer.parseInt(portNumber.getValue().toString());
+ String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty()
+ ? commandSeparator.getValue().toString()
+ : "&&";
+ String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&");
+
+ // Run in a login shell so PATH and env from /etc/profile, ~/.profile are loaded.
+ // Otherwise exec() uses a minimal shell (e.g. sh) and commands like "smsd" are not found.
+ String wrappedCommand = wrapInLoginShell(allCommands);
+
+ SSHClient ssh = null;
+ Session session = null;
+
+ try {
+ ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
+ ssh.authPassword(user, password);
+ logger.info("SSH session connected (SSHJ).");
+
+ session = ssh.startSession();
+ Command cmd = session.exec(wrappedCommand);
+ StringBuilder output = new StringBuilder();
+ try (Reader reader = new InputStreamReader(cmd.getInputStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = reader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ cmd.join(30, TimeUnit.SECONDS);
+ if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) {
+ try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = errReader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ }
+
+ runTimeData.setKey(storeVariable.getValue().toString());
+ runTimeData.setValue(output.toString());
+
+ // Display output as custom screenshot in step result (same as DisplayTestDataAsStepResultScreenShot)
+ File screenshotFile = null;
+ try {
+ screenshotFile = StringToImageConverter.convertToFile(output);
+ String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null;
+ if (s3Url != null && !s3Url.isEmpty()) {
+ ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(driver, logger);
+ boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath());
+ if (!uploadResult) {
+ logger.debug("Error uploading custom screenshot to S3; step result may not show the output image.");
+ } else {
+ logger.debug("Custom screenshot (command output) uploaded successfully.");
+ }
+ }
+ } finally {
+ if (screenshotFile != null && screenshotFile.exists()) {
+ screenshotFile.deleteOnExit();
+ }
+ }
+
+ setSuccessMessage("Output is: " + output.toString());
+ return Result.SUCCESS;
+ } catch (Exception e) {
+ String errorStack = ExceptionUtils.getStackTrace(e);
+ logger.info("Error occurred while executing the command (SSHJ): " + errorStack);
+ setErrorMessage("Error occurred while executing the command: " + e.getMessage());
+ return Result.FAILED;
+ } finally {
+ try {
+ if (session != null) session.close();
+ if (ssh != null) ssh.disconnect();
+ } catch (IOException e) {
+ logger.info("Error closing SSHJ resources: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Wraps the command in a login shell (bash -l -c '...') so that profile scripts
+ * are sourced and PATH includes /usr/local/bin, custom paths, etc. Without this,
+ * session.exec() runs in a minimal non-login shell where commands like "smsd" may not be found.
+ */
+ private static String wrapInLoginShell(String command) {
+ String escaped = command.replace("'", "'\\''");
+ return "bash -l -c '" + escaped + "'";
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/windowsAdvanced/SSHCommandExecutionSSHJ.java b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/windowsAdvanced/SSHCommandExecutionSSHJ.java
new file mode 100644
index 00000000..8207975a
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/java/com/testsigma/addons/windowsAdvanced/SSHCommandExecutionSSHJ.java
@@ -0,0 +1,152 @@
+package com.testsigma.addons.windowsAdvanced;
+
+import com.testsigma.addons.util.ImageComparisonUtils;
+import com.testsigma.addons.util.StringToImageConverter;
+import com.testsigma.sdk.Result;
+import com.testsigma.sdk.WindowsAdvancedAction;
+import com.testsigma.sdk.annotation.Action;
+import com.testsigma.sdk.annotation.RunTimeData;
+import com.testsigma.sdk.annotation.TestData;
+import com.testsigma.sdk.annotation.TestStepResult;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.connection.channel.direct.Session;
+import net.schmizz.sshj.connection.channel.direct.Session.Command;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Action(actionText = "SSHJ: Connect to SSH server and execute commands, SSH server details: Host: Host-Name,"
+ + " Port: Port-Number, UserName: User-Name, Password: User-Password, Commands: Terminal-Commands ,"
+ + " Command Separator: Command-Separator , Store Output Variable: Variable-Name",
+ description = "Executes commands on an SSH server using the SSHJ library (alternative implementation). "
+ + "This action connects via SSH, runs the given commands in a login shell, and stores the output in a variable.",
+ applicationType = com.testsigma.sdk.ApplicationType.WINDOWS_ADVANCED,
+ displayName = "SSHJ: Connect to SSH server and execute commands",
+ useCustomScreenshot = true)
+public class SSHCommandExecutionSSHJ extends WindowsAdvancedAction {
+
+ @TestStepResult
+ private com.testsigma.sdk.TestStepResult testStepResult;
+
+ @TestData(reference = "Host-Name")
+ private com.testsigma.sdk.TestData hostName;
+ @TestData(reference = "Port-Number")
+ private com.testsigma.sdk.TestData portNumber;
+ @TestData(reference = "User-Name")
+ private com.testsigma.sdk.TestData userName;
+ @TestData(reference = "User-Password")
+ private com.testsigma.sdk.TestData userPassword;
+ @TestData(reference = "Terminal-Commands")
+ private com.testsigma.sdk.TestData commands;
+ @TestData(reference = "Command-Separator")
+ private com.testsigma.sdk.TestData commandSeparator;
+ @TestData(reference = "Variable-Name")
+ private com.testsigma.sdk.TestData storeVariable;
+
+ @RunTimeData
+ private com.testsigma.sdk.RunTimeData runTimeData;
+
+ @Override
+ protected Result execute() {
+ logger.info("Initiating execution (SSHJ) - Windows Advanced");
+
+ String user = userName.getValue().toString();
+ String host = hostName.getValue().toString();
+ String password = userPassword.getValue().toString();
+ int port = Integer.parseInt(portNumber.getValue().toString());
+ String commandSeparatorStr = commandSeparator != null && !commandSeparator.getValue().toString().isEmpty()
+ ? commandSeparator.getValue().toString()
+ : "&&";
+ String allCommands = commands.getValue().toString().replace(commandSeparatorStr, "&&");
+
+ String wrappedCommand = wrapInLoginShell(allCommands);
+
+ SSHClient ssh = null;
+ Session session = null;
+
+ try {
+ ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
+ ssh.authPassword(user, password);
+ logger.info("SSH session connected (SSHJ).");
+
+ session = ssh.startSession();
+ Command cmd = session.exec(wrappedCommand);
+ StringBuilder output = new StringBuilder();
+ try (Reader reader = new InputStreamReader(cmd.getInputStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = reader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ cmd.join(30, TimeUnit.SECONDS);
+ if (cmd.getExitStatus() != null && cmd.getExitStatus() != 0) {
+ try (Reader errReader = new InputStreamReader(cmd.getErrorStream())) {
+ char[] buf = new char[1024];
+ int n;
+ while ((n = errReader.read(buf)) != -1) {
+ output.append(buf, 0, n);
+ }
+ }
+ }
+
+ runTimeData.setKey(storeVariable.getValue().toString());
+ runTimeData.setValue(output.toString());
+
+ // Display output as custom screenshot in step result (same as web action)
+ File screenshotFile = null;
+ try {
+ screenshotFile = StringToImageConverter.convertToFile(output);
+ String s3Url = testStepResult != null ? testStepResult.getScreenshotUrl() : null;
+ if (s3Url != null && !s3Url.isEmpty()) {
+ ImageComparisonUtils imageComparisonUtils = new ImageComparisonUtils(null, logger);
+ boolean uploadResult = imageComparisonUtils.uploadFile(s3Url, screenshotFile.getAbsolutePath());
+ if (!uploadResult) {
+ logger.debug("Error uploading custom screenshot to S3; step result may not show the output image.");
+ } else {
+ logger.debug("Custom screenshot (command output) uploaded successfully.");
+ }
+ }
+ } finally {
+ if (screenshotFile != null && screenshotFile.exists()) {
+ screenshotFile.deleteOnExit();
+ }
+ }
+
+ setSuccessMessage("Output is: " + output.toString());
+ return Result.SUCCESS;
+ } catch (Exception e) {
+ String errorStack = ExceptionUtils.getStackTrace(e);
+ logger.info("Error occurred while executing the command (SSHJ): " + errorStack);
+ setErrorMessage("Error occurred while executing the command: " + e.getMessage());
+ return Result.FAILED;
+ } finally {
+ try {
+ if (session != null) session.close();
+ if (ssh != null) ssh.disconnect();
+ } catch (IOException e) {
+ logger.info("Error closing SSHJ resources: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Wraps the command in a login shell (bash -l -c '...') so that profile scripts
+ * are sourced and PATH includes /usr/local/bin, custom paths, etc.
+ */
+ private static String wrapInLoginShell(String command) {
+ String escaped = command.replace("'", "'\\''");
+ return "bash -l -c '" + escaped + "'";
+ }
+}
diff --git a/ssh_shell_command_executor_using_sshj/src/main/resources/testsigma-sdk.properties b/ssh_shell_command_executor_using_sshj/src/main/resources/testsigma-sdk.properties
new file mode 100644
index 00000000..99da96ff
--- /dev/null
+++ b/ssh_shell_command_executor_using_sshj/src/main/resources/testsigma-sdk.properties
@@ -0,0 +1 @@
+testsigma-sdk.api.key=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIyNDBkMjhiNS0xNmUwLThlNmYtOWQ0ZS05MjYxMGNiZTcyYzciLCJ1bmlxdWVJZCI6IjU5NzgiLCJpZGVudGl0eUFjY291bnRVVUlkIjoiNDMifQ.eX8QdtK6wAKQJ2rI3Em8VbztUA2m06VFbSLJ35vsNsiljS7yrqGjIZ3TjxdXL3dvscC-ZcU21ugKIjqyWmstkQ
\ No newline at end of file