Skip to content

Commit

Permalink
SSH - 18588 - Updated dependencies | Updated SDK to the latest version (
Browse files Browse the repository at this point in the history
  • Loading branch information
igorski-r7 authored Jan 27, 2025
1 parent 7c593e9 commit 7ac9e7e
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 107 deletions.
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.
[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.

## 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
File renamed without changes.
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

0 comments on commit 7ac9e7e

Please sign in to comment.