Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SOAR-18588] SSH - Updated dependencies | Updated SDK to the latest version #3062

Merged
merged 1 commit into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions plugins/ssh/.CHECKSUM
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"spec": "995ad61b7f831d806b2191f6eee722cb",
"manifest": "90a7f8d75087e034f3a72948cc72667b",
"setup": "5dce286de98dbb6fc18aeea69725e879",
"spec": "9247db0545cd8d1474eabb488c08997c",
"manifest": "58de38307d29215935d4e06a1a0943c7",
"setup": "b0941213ed47967cbec2b3e8a68bf900",
"schemas": [
{
"identifier": "run/schema.py",
Expand Down
4 changes: 2 additions & 2 deletions plugins/ssh/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 rapid7/insightconnect-python-3-slim-plugin:6.1.0
FROM --platform=linux/amd64 rapid7/insightconnect-python-3-slim-plugin:6.2.3

LABEL organization=rapid7
LABEL sdk=python
Expand All @@ -12,7 +12,7 @@ RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

ADD . /python/src

RUN python setup.py build && python setup.py install
RUN pip install .

# User to run plugin code. The two supported users are: root, nobody
USER nobody
Expand Down
4 changes: 2 additions & 2 deletions plugins/ssh/bin/komand_ssh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ from sys import argv

Name = "SSH"
Vendor = "rapid7"
Version = "4.0.2"
Description = "The SSH protocol is a method for secure remote login from one computer to another"
Version = "4.0.3"
Description = "[Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell) (SSH) is a cryptographic network protocol for operating network services securely over an unsecured network. This plugin uses the [paramiko](http://www.paramiko.org/) to connect to a remote host via the library. The SSH plugin allows you to run commands on a remote host"


def main():
Expand Down
34 changes: 16 additions & 18 deletions plugins/ssh/help.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Description

[Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell) (SSH) is a cryptographic network protocol for operating network services securely over an unsecured network.
This plugin uses the [paramiko](http://www.paramiko.org/) to connect to a remote host via the library. The SSH plugin allows you to run commands on a remote host.
igorski-r7 marked this conversation as resolved.
Show resolved Hide resolved
[Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell) (SSH) is a cryptographic network protocol for operating network services securely over an unsecured network. This plugin uses the [paramiko](http://www.paramiko.org/) to connect to a remote host via the library. The SSH plugin allows you to run commands on a remote host

# Key Features

Expand All @@ -14,7 +13,7 @@ This plugin uses the [paramiko](http://www.paramiko.org/) to connect to a remote

# Supported Product Versions

* SSH 2024-09-09
* SSH 2025-01-15

# Documentation

Expand Down Expand Up @@ -48,19 +47,6 @@ Example input:
}
```

The `key` field takes a base64 encoded RSA private key which must contain a newline character after the BEGIN marker and before the END marker:
E.g.

```
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7g4h53s=
...
-----END RSA PRIVATE KEY-----
```

You can easily encode a private key file and copy a key to your clipboard on MacOS with the following command: `base64 < .ssh/id_rsa | pbcopy`.
This can then be pasted into the Connection's `key` input field.

igorski-r7 marked this conversation as resolved.
Show resolved Hide resolved
## Technical Details

### Actions
Expand Down Expand Up @@ -122,11 +108,23 @@ Example output:


## Troubleshooting

*This plugin does not contain a troubleshooting.*

* The `key` field in connection setup takes a base64 encoded RSA private key which must contain a newline character after the BEGIN marker and before the END marker:
E.g.

```
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7g4h53s=
...
-----END RSA PRIVATE KEY-----
```

You can easily encode a private key file and copy a key to your clipboard on MacOS with the following command: `base64 < .ssh/id_rsa | pbcopy`.
This can then be pasted into the Connection's `key` input field

# Version History

* 4.0.3 - Updated dependencies | Updated SDK to the latest version
* 4.0.2 - Initial updates for fedramp compliance | Updated SDK to the latest version
* 4.0.1 - Update from komand to insight-plugin-runtime
* 4.0.0 - Upgrade the plugin runtime to `komand/python-3-37-plugin` and run as least-privileged user | Change the SSH key credential type to `credential_secret_key` to skip PEM validation in the product UI
Expand Down
7 changes: 4 additions & 3 deletions plugins/ssh/komand_ssh/actions/run/action.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import insightconnect_plugin_runtime
from .schema import RunInput, RunOutput, Input, Output

from .schema import Input, Output, RunInput, RunOutput

# Custom imports below

Expand All @@ -12,8 +13,8 @@ def __init__(self):

def run(self, params={}):
# START INPUT BINDING - DO NOT REMOVE - ANY INPUTS BELOW WILL UPDATE WITH YOUR PLUGIN SPEC AFTER REGENERATION
command = params.get(Input.COMMAND)
host = params.get(Input.HOST)
host = params.get(Input.HOST, "")
command = params.get(Input.COMMAND, "")
# END INPUT BINDING - DO NOT REMOVE

results = {}
Expand Down
76 changes: 36 additions & 40 deletions plugins/ssh/komand_ssh/connection/connection.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,55 @@
import insightconnect_plugin_runtime
from insightconnect_plugin_runtime.exceptions import PluginException
from typing import Any, Dict

from .schema import ConnectionSchema, Input
import insightconnect_plugin_runtime

# Custom imports below
import base64
import paramiko
import io
from typing import Dict, Any
from insightconnect_plugin_runtime.exceptions import PluginException

from komand_ssh.util.policies import CustomMissingKeyPolicy
from komand_ssh.util.strategies import ConnectUsingPasswordStrategy, ConnectUsingRSAKeyStrategy

from .schema import ConnectionSchema, Input


class Connection(insightconnect_plugin_runtime.Connection):
def __init__(self):
super(self.__class__, self).__init__(input=ConnectionSchema())
self.host = None
self.port = None
self.username = None
self.password = None
self.use_key = None
self.key = None

def connect_key(self, params: Dict[str, Any]) -> paramiko.SSHClient:
self.logger.info("Connecting via key...")
key = base64.b64decode(params.get(Input.KEY, {}).get("secretKey")).strip().decode("utf-8")
fd = io.StringIO(key)
rsa_key = paramiko.RSAKey.from_private_key(fd, password=params.get(Input.PASSWORD, {}).get("secretKey"))
def connect(self, params={}) -> None:
self.logger.info("Connecting...")
self.host = params.get(Input.HOST, "").strip()
self.port = params.get(Input.PORT, 22)
self.username = params.get(Input.USERNAME, "").strip()
self.password = params.get(Input.PASSWORD, {}).get("secretKey", "").strip()
self.use_key = params.get(Input.USE_KEY, False)
self.key = params.get(Input.KEY, {}).get("secretKey", "").strip()

def client(self, host: str = None) -> paramiko.SSHClient:
# Create fresh client instance
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # noqa B507
ssh_client.load_system_host_keys()
ssh_client.set_missing_host_key_policy(CustomMissingKeyPolicy())

ssh_client.connect(
params.get(Input.HOST), params.get(Input.PORT), username=params.get(Input.USERNAME), pkey=rsa_key
)
return ssh_client
# Update host only if entered and different from host in connection
if host and host != self.host:
self.host = host

def connect_password(self, params: Dict[str, Any]) -> paramiko.SSHClient:
self.logger.info("Connecting via password")
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # noqa B507
ssh_client.load_system_host_keys()
ssh_client.connect(
params.get(Input.HOST),
params.get(Input.PORT),
params.get(Input.USERNAME),
params.get(Input.PASSWORD, {}).get("secretKey"),
)
return ssh_client
# Select connection strategy
connection_strategy = ConnectUsingRSAKeyStrategy if self.use_key else ConnectUsingPasswordStrategy

def client(self, host: str = None) -> paramiko.SSHClient:
if host:
self.parameters["host"] = host
if self.parameters.get(Input.USE_KEY):
return self.connect_key(self.parameters)
else:
return self.connect_password(self.parameters)

def connect(self, params={}) -> None:
self.logger.info("Connecting...")
self.host = params.get(Input.HOST)
# Return SSH client
try:
return connection_strategy(ssh_client, self.logger).connect(
self.host, self.port, self.username, self.password, self.key
)
except Exception as error:
raise PluginException(preset=PluginException.Preset.UNKNOWN, data=error)

def test(self) -> Dict[str, Any]:
try:
Expand Down
1 change: 1 addition & 0 deletions plugins/ssh/komand_ssh/util/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT_ENCODING = "UTF-8"
6 changes: 6 additions & 0 deletions plugins/ssh/komand_ssh/util/policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from paramiko import MissingHostKeyPolicy, PKey, SSHClient


class CustomMissingKeyPolicy(MissingHostKeyPolicy):
def missing_host_key(self, client: SSHClient, hostname: str, key: PKey) -> None:
client.get_host_keys().add(hostname, key.get_name(), key)
34 changes: 34 additions & 0 deletions plugins/ssh/komand_ssh/util/strategies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from abc import ABC, abstractmethod
from base64 import b64decode
from io import StringIO
from logging import Logger

from paramiko import RSAKey, SSHClient

from .constants import DEFAULT_ENCODING


class SSHConnectionStrategy(ABC):
def __init__(self, client: SSHClient, logger: Logger) -> None:
self.client = client
self.logger = logger

@abstractmethod
def connect(self, host: str, port: int, username: str, password: str, key: str = None) -> SSHClient:
pass


class ConnectUsingPasswordStrategy(SSHConnectionStrategy):
def connect(self, host: str, port: int, username: str, password: str, key: str = None) -> SSHClient:
self.logger.info("Connecting to SSH server via password...")
self.client.connect(host, port, username, password)
return self.client


class ConnectUsingRSAKeyStrategy(SSHConnectionStrategy):
def connect(self, host: str, port: int, username: str, password: str, key: str = None) -> SSHClient:
self.logger.info("Connecting to SSH server via RSA key...")
key = b64decode(key).decode(DEFAULT_ENCODING)
rsa_key = RSAKey.from_private_key(StringIO(key), password=password)
self.client.connect(host, port, username, password, pkey=rsa_key)
return self.client
11 changes: 7 additions & 4 deletions plugins/ssh/plugin.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ extension: plugin
products: [insightconnect]
name: ssh
title: SSH
description: The SSH protocol is a method for secure remote login from one computer to another
version: 4.0.2
description: "[Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell) (SSH) is a cryptographic network protocol for operating network services securely over an unsecured network. This plugin uses the [paramiko](http://www.paramiko.org/) to connect to a remote host via the library. The SSH plugin allows you to run commands on a remote host"
version: 4.0.3
connection_version: 4
supported_versions: ["SSH 2024-09-09"]
supported_versions: ["SSH 2025-01-15"]
vendor: rapid7
support: community
status: []
Expand All @@ -24,14 +24,17 @@ hub_tags:
fedramp_ready: true
sdk:
type: slim
version: 6.1.0
version: 6.2.3
user: nobody
key_features:
- Run remote commands with SSH
requirements:
- Credentials for the target remote host
- Address and port for the target remote host
troubleshooting:
- "The `key` field in connection setup takes a base64 encoded RSA private key which must contain a newline character after the BEGIN marker and before the END marker:\nE.g.\n\n```\n-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7g4h53s=\n...\n-----END RSA PRIVATE KEY-----\n```\n\nYou can easily encode a private key file and copy a key to your clipboard on MacOS with the following command: `base64 < .ssh/id_rsa | pbcopy`.\nThis can then be pasted into the Connection's `key` input field"
version_history:
- "4.0.3 - Updated dependencies | Updated SDK to the latest version"
- "4.0.2 - Initial updates for fedramp compliance | Updated SDK to the latest version"
- "4.0.1 - Update from komand to insight-plugin-runtime"
- "4.0.0 - Upgrade the plugin runtime to `komand/python-3-37-plugin` and run as least-privileged user | Change the SSH key credential type to `credential_secret_key` to skip PEM validation in the product UI"
Expand Down
2 changes: 1 addition & 1 deletion plugins/ssh/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# List third-party dependencies here, separated by newlines.
# All dependencies must be version-pinned, eg. requests==1.2.0
# See: https://pip.pypa.io/en/stable/user_guide/#requirements-files
paramiko==3.4.1
paramiko==3.5.0
4 changes: 2 additions & 2 deletions plugins/ssh/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@


setup(name="ssh-rapid7-plugin",
version="4.0.2",
description="The SSH protocol is a method for secure remote login from one computer to another",
version="4.0.3",
description="[Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell) (SSH) is a cryptographic network protocol for operating network services securely over an unsecured network. This plugin uses the [paramiko](http://www.paramiko.org/) to connect to a remote host via the library. The SSH plugin allows you to run commands on a remote host",
author="rapid7",
author_email="",
url="",
Expand Down
31 changes: 12 additions & 19 deletions plugins/ssh/unit_test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,25 @@
sys.path.append(os.path.abspath("../"))

from unittest import TestCase
from unittest.mock import patch
from unittest.mock import MagicMock, patch

from komand_ssh.actions.run import Run
from komand_ssh.actions.run.schema import Output
from komand_ssh.actions.run.schema import Input, Output

from util import Util

STUB_PARAMETERS = {Input.HOST: "example.com", Input.COMMAND: "ls -l"}


class TestRun(TestCase):
def setUp(self):
self.action = Run()
self.params = {
"host": "example.com",
"command": "ls -l",
}
self.action.connection = Util.default_connector()

def mock_execute_command(self):
file1 = open("./ssh/unit_test/results", "r")
return file1, file1, file1

@patch("paramiko.SSHClient.set_missing_host_key_policy", return_value=None)
@patch("paramiko.SSHClient.load_system_host_keys", return_value=None)
self.action = Util.default_connector(Run())

@patch("paramiko.SSHClient.connect", return_value=None)
@patch("paramiko.SSHClient.exec_command", side_effect=mock_execute_command)
def test_run(self, mock_key_policy, mock_host_keys, mock_connect, mock_exec):
@patch("paramiko.SSHClient.exec_command", side_effect=Util.mock_execute_command)
def test_run(self, mock_connect: MagicMock, mock_exec: MagicMock) -> None:
response = self.action.run(STUB_PARAMETERS)
expected = {Output.RESULTS: {"stdout": "/home/vagrant", "stderr": "", "all_output": "/home/vagrant"}}
actual = self.action.run(self.params)
self.assertEqual(actual, expected)
self.assertEqual(response, expected)
mock_connect.assert_called()
mock_exec.assert_called()
Loading
Loading