RemoteAccess v2

This integration transfers files between Cortex XSOAR and a remote machine and executes commands on the remote machine.

Utilities · Remote Access

Details

IDRemoteAccess v2
ProviderAxis Communications
CategoryUtilities
From Version6.0.0
Docker Imagedemisto/netmiko:1.0.0.9067966
Supported ModulesAgentix XSIAM EDR Cortex Cloud Attack Surface Management Cloud Runtime Security Cloud Posture Security Exposure Management

README

This integration transfers files between Cortex XSOAR and a remote machine and executes commands on the remote machine.

Some changes have been made that might affect your existing content.
If you are upgrading from a previous version of this integration, see Breaking Changes.

Note: This integration was integrated and tested on a remote machine with Centos-7 operating system. It does not work with Windows operation system.

Configure RemoteAccess v2 in Cortex

Parameter Description Required
Default Hostname or IP Address If not provided, “host” or “system” should be provided in the command’s arguments. False
User For example, “root”. False
Password The password of the remote machine. False
sshKey The private RSA key used to authenticate with the remote machine should be configured in the Credentials tab under Settings > Integrations > Credentials. See here for more information. False
Additional Password Requires an additional password as an argument to run any command of this module. False
Ciphers A comma-separated list of ciphers to use. If none of the specified ciphers are agreed to by the server, an error message specifying the supported ciphers is returned. False
Key Algorithms A comma-separated list of key algorithms to use. If none of the specified key algorithms are agreed to by the server, an error message specifying the supported key algorithms is returned. False

Configure SSH From Remote

For login using root:

  1. Edit the /etc/ssh/sshd_config file.
  • set PermitRootLogin to yes
  • set PasswordAuthentication to yes
  1. Restart the sshd server: service sshd restart

Configure the instance with SSH certificate

Currently, the only type of certificate that is supported is RSA private keys (.PEM) files.
In case access is required to an instance in the cloud, use the PEM file provided by the cloud provider.

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

ssh


Run the specified command on the remote system with SSH.

Base Command

ssh

Input

Argument Name Description Required
cmd Command to run on the remote machine. To combine commands and run them in a single SSH session, concatenate them using semicolons (;) e.g., ‘echo 1;echo 2’. Required
additional_password Password required to match the Additional Password parameter if it was supplied to run the command. Optional
timeout Timeout for command in seconds. Optional
system System to run the command on. Optional
host Host name to run the command on. Optional
port Port to run the command on. Optional

Context Output

Path Type Description
RemoteAccess.Command.output String Standard output of the specified command.
RemoteAccess.Command.error String Standard error output of the specified command.
RemoteAccess.Command.success Boolean Whether the operation was successful.
RemoteAccess.Command.command String Command that was run.

Command Example

!ssh command="echo test"

Context Example

{
    "RemoteAccess": {
        "Command": [
            {
                "command": "echo test",
                "error": "",
                "output": "test\n",
                "success": true
            }
        ]
    }
}

Human Readable Output

Command echo test Outputs

command output success
echo test test
true

copy-to


Copies the given file from Cortex XSOAR to the remote machine.

Base Command

copy-to

Input

Argument Name Description Required
entry_id Entry ID of the file to be copied from Cortex XSOAR to the remote machine. Optional
destination_path Destination of the path of the copied file in the remote machine. Defaults to the entry_id file name if not specified. Optional
additional_password Password. Required to match the Additional Password parameter if it was supplied in order to run the command. Optional
timeout Timeout for command in seconds. Default is 10.0 seconds. Optional
dest-dir Destination of the directory to copy the file to in the remote machine. The file name of the entry_id will be used as the file name in the destination directory. Creates the destination directory in the remote machine if it does not exist. Optional
entry This input is deprecated. Please use the entry_id input instead. Optional
system System to run the command on. Optional
host Host name to run the command on. Optional
port Port to run the command on. Optional

Context Output

There is no context output for this command.

Command Example

!copy-to entry_id=104@49493d71-eef6-4bb4-8075-4be38d9bc340 destination_path="test/cortex_copied_file"

Human Readable Output

The file corresponding to entry ID: 104@49493d71-eef6-4bb4-8075-4be38d9bc340 was copied to remote host

copy-from


Copies the given file from the remote machine to Cortex XSOAR.

Base Command

copy-from

Input

Argument Name Description Required
file_path Path of the file in the remote machine to be copied to Cortex XSOAR. Optional
file_name Name of the file to be copied to Cortex XSOAR. Defaults to the file name in file_path if not specified. For example, if file_path is “a/b/c.txt”, the file name will be c.txt. Optional
additional_password Password required to match the Additional Password parameter if it was supplied to run the command. Optional
timeout Timeout for command, in seconds. Default is 10.0 seconds. Optional
file This input is deprecated. Please use the file_path input instead. Optional
system System to run the command on. Optional
host Host name to run the command on. Optional
port Port to run the command on. Optional

Context Output

Path Type Description
File.Name String The full file name (including the file extension).
File.EntryID String The ID for locating the file in the War Room.
File.Size Number The size of the file in bytes.
File.MD5 String The MD5 hash of the file.
File.SHA1 String The SHA1 hash of the file.
File.SHA256 String The SHA256 hash of the file.
File.SHA512 String The SHA512 hash of the file.
File.Extension String The file extension. For example: “xls”.
File.Type String The file type, as determined by libmagic (same as displayed in file entries).

Command Example

!copy-from file_path="test/remote_file.txt" file_name="CopiedRemoteFile"

Context Example

{
    "File": {
        "EntryID": "165@49493d71-eef6-4bb4-8075-4be38d9bc340",
        "Info": "text/plain",
        "MD5": "c5253b90e791d18439a84511c382616b",
        "Name": "CopiedRemoteFile",
        "SHA1": "98c94e6e64b7a52576870fc07a0da5f33243c505",
        "SHA256": "bf98cd7cda320c300218397d9ee1df263415aac7f0f41c8f57dee7944e68fba0",
        "SHA512": "6f2199d786a13c7b8cd6d268166a26f4423d4aa1e4ba59565e9130da01555d83d190870dade6098fe7992425e0d2d9128841b92f419dbf4193a6112c4cf7264f",
        "SSDeep": "3:9bLbEin:6i",
        "Size": 16,
        "Type": "ASCII text, with no line terminators"
    }
}

Human Readable Output

Breaking changes from the previous version of this integration - RemoteAccess v2

  • Removed the Interactive terminal mode instance parameter.
  • Removed the Terminal Type instance parameter.

Commands

Arguments

The following argument names were changed, added, or removed in this version

Remote Access Command Name Old Command Argument Name New Command Name
copy-to fileID Argument was removed
copy-from Argument did not exist file_name

Outputs

The following outputs were removed in this version

Remote Access Command Name Old Command Outputs Remote Access v2 Command Name New Command Outputs
ssh Command outputs were:
- command
- stdout
- stderr
- remote machine IP
- success status
ssh Outputs:
- stdout
- stderr

Configuration parameters

  • hostname — Default Hostname or IP Address
  • credentials — User
  • additional_password
  • ciphers — Ciphers
  • key_algorithms — Key Algorithms

Commands (3)

  • copy-from

    Copies the given file from the remote machine to Cortex XSOAR.

  • copy-to

    Copies the given file from Cortex XSOAR to the remote machine.

  • ssh

    Run the specified command on the remote system with SSH.

import json

import demistomock as demisto
import pytest
from paramiko import RSAKey
from paramiko.ssh_exception import SSHException
from RemoteAccessv2 import CommandResults, DemistoException


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


@pytest.mark.parametrize(
    "cipher_arg, key_arg, server_ciphers, server_key_algorithms",
    [
        (set(), set(), set(), set()),
        (set(), {"diffie-hellman-group14-sha256"}, set(), {"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha128"}),
        ({"aes128-cbc"}, set(), {"aes128-cbc"}, set()),
        (
            {"aes128-cbc"},
            {"diffie-hellman-group14-sha256"},
            {"aes128-cbc"},
            {"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha128"},
        ),
    ],
)
def test_create_paramiko_ssh_client_valid(mocker, cipher_arg, key_arg, server_ciphers, server_key_algorithms):
    """
    Given:
    - Parameters to create SSH client.

    When:
    - Given parameters are valid.
    Cases:
    - Case a: No ciphers and no key algorithms to be used have been given by the user.
    - Case b: No ciphers to be used, but key algorithm to be used which is supported by server where given by the user.
    - Case c: Cipher to be used that is supported by server, but no key algorithm were given by the user.
    - Case d: Both cipher and key algorithm have been requested by the user, both are supported by server.


    Then:
    - Ensure SSH client is created.
    """
    from RemoteAccessv2 import create_paramiko_ssh_client

    mocker.patch("paramiko.SSHClient.connect")
    if server_ciphers:
        mocker.patch("RemoteAccessv2.get_available_ciphers", return_value=server_ciphers)
    if server_key_algorithms:
        mocker.patch("RemoteAccessv2.get_available_key_algorithms", return_value=server_key_algorithms)
    create_paramiko_ssh_client("host", "user", "password", cipher_arg, key_arg)


def test_create_paramiko_ssh_client_with_valid_ssh_certificate(mocker):
    """ "
    Given:
    - valid SSH certificate.

    When:
    - trying to connect to an SSH client.

    Then:
    - Ensure that creating SSH connection was successful.
    """
    from RemoteAccessv2 import create_paramiko_ssh_client

    ssh_connect_mock = mocker.patch("paramiko.SSHClient.connect")
    valid_private_key = "1234\n1234"

    mocker.patch("paramiko.RSAKey.from_private_key", return_value=RSAKey(key=valid_private_key))

    create_paramiko_ssh_client("host", "user", None, set(), set(), private_key=valid_private_key)
    assert type(ssh_connect_mock.call_args.kwargs.get("pkey")) is RSAKey
    assert not ssh_connect_mock.call_args.kwargs.get("password")


@pytest.mark.parametrize(
    "invalid_private_key", ["1234\n1234", "-----BEGIN RSA PRIVATE KEY-----\n1234-----END RSA PRIVATE KEY-----"]
)
def test_create_paramiko_ssh_client_with_invalid_ssh_certificate(invalid_private_key):
    """
    Given:
    - invalid SSH certificate structure.

    When:
    - trying to connect to an SSH client.

    Then:
    - Ensure that SSHException is raised.
    """
    from RemoteAccessv2 import create_paramiko_ssh_client

    with pytest.raises(SSHException):
        create_paramiko_ssh_client("host", "user", "password", set(), set(), private_key=invalid_private_key)


@pytest.mark.parametrize(
    "cipher_arg, key_arg, server_ciphers, server_key_algorithms",
    [
        ({"unsupported cipher"}, set(), {"aes128-cbc"}, set()),
        (set(), {"unsupported key"}, set(), {"diffie-hellman-group14-sha256"}),
    ],
)
def test_create_paramiko_ssh_client_invalid(mocker, cipher_arg, key_arg, server_ciphers, server_key_algorithms):
    """
    Given:
    - Parameters to create SSH client.

    When:
    - Given ciphers do not match the server ciphers.
    Cases:
    Case a: Cipher which is not supported by server was given by the user.
    Case b: Key algorithm which is not supported by server was given by the user.

    Then:
    - Ensure DemistoException is thrown as expected.
    """
    from RemoteAccessv2 import create_paramiko_ssh_client

    server_ciphers = {"diffie-hellman-group14-sha512"}
    expected_err_msg: str = ""
    if server_ciphers:
        expected_err_msg = f"Given ciphers are not available in server.\nCiphers available in server are: {server_ciphers}"
        mocker.patch("RemoteAccessv2.get_available_ciphers", return_value=server_ciphers)
    if server_key_algorithms:
        expected_err_msg = (
            f"Given key algorithms are not available in server.\n"
            f"Key algorithms available in server are: {server_key_algorithms}"
        )
        mocker.patch("RemoteAccessv2.get_available_key_algorithms", return_value=server_key_algorithms)
    with pytest.raises(DemistoException, match=expected_err_msg):
        create_paramiko_ssh_client("host", "user", "password", cipher_arg, key_arg)


@pytest.mark.parametrize(
    "command, mock_std_output, mock_std_error, success",
    [
        ("echo lol", "lol", "", True),
        ("cat invalid_path_file", "", "cat: lol: No such file or directory", False),
        ("non-known-command", "", "command not found: non-known-command", False),
    ],
)
def test_execute_shell_command(mocker, command, mock_std_output, mock_std_error, success):
    """
    Given:
    - Cortex XSOAR arguments.

    When:
    - Executing shell command.

    Then:
    - Ensure expected output and error are returned.
    """
    from paramiko import SSHClient
    from RemoteAccessv2 import CommandResults, execute_shell_command, tempfile

    mock_client: SSHClient = SSHClient()
    with tempfile.TemporaryDirectory() as temp_dir:
        with open(f"{temp_dir}/std_out", "wb") as f_output, open(f"{temp_dir}/std_err", "wb") as f_err:
            f_output.write(mock_std_output.encode("utf-8"))
            f_err.write(mock_std_error.encode("utf-8"))

        with open(f"{temp_dir}/std_out", "rb") as f_output, open(f"{temp_dir}/std_err", "rb") as f_err:
            mocker.patch.object(mock_client, "exec_command", return_value=(None, f_output, f_err))
            results: CommandResults = execute_shell_command(mock_client, {"cmd": command})
    assert results.outputs_prefix == "RemoteAccess.Command"
    assert results.outputs == [{"output": mock_std_output, "error": mock_std_error, "success": success, "command": command}]


def test_copy_to_command_valid(mocker):
    """
    Given:
    - Cortex XSOAR arguments

    When:
    - Calling the copy-to command with the entry_id argument.
    - Calling the copy-to command with both the entry_id and entry argument.

    Then:
    - Ensure expected readable output is returned upon successful copy.
    """
    from paramiko import SSHClient
    from RemoteAccessv2 import copy_to_command

    mock_client: SSHClient = SSHClient()
    mocker.patch.object(demisto, "getFilePath", return_value={"path": "test", "name": "file-name.txt"})
    mocker.patch("RemoteAccessv2.perform_copy_command", return_value="")
    results: CommandResults = copy_to_command(mock_client, {"entry_id": 123})
    assert results.readable_output == "### The file corresponding to entry ID: 123 was copied to remote host."

    # When both entry and entry_id are given, validate that entry_id is used
    results: CommandResults = copy_to_command(mock_client, {"entry": 123, "entry_id": 456})
    assert results.readable_output == "### The file corresponding to entry ID: 456 was copied to remote host."


def test_copy_to_command_failed_to_mkdir(mocker):
    """
    Given:
    - Cortex XSOAR arguments

    When:
    - Calling the copy-to command but failing to run the mkdir.

    Then:
    - Ensure the error info printed to debug but the command finished successfully.
    """
    from paramiko import SSHClient
    from RemoteAccessv2 import copy_to_command

    mock_client: SSHClient = SSHClient()
    mocker.patch.object(demisto, "getFilePath", return_value={"path": "test", "name": "file-name.txt"})
    mocker.patch.object(demisto, "debug")
    mocker.patch("RemoteAccessv2.perform_copy_command", return_value="")
    mocker.patch("RemoteAccessv2.execute_shell_command", side_effect=Exception("permission error"))

    results: CommandResults = copy_to_command(mock_client, {"entry": 123, "entry_id": 456, "dest-dir": "test_dir"})

    assert "permission error, occurred when run the command: mkdir -p test_dir" in demisto.debug.call_args[0][0]
    assert results.readable_output == "### The file corresponding to entry ID: 456 was copied to remote host."


def test_copy_to_command_invalid_entry_id(mocker):
    """
    Given:
    - Cortex XSOAR arguments

    When:
    - Calling the copy-to command with invalid entry ID input.

    Then:
    - Ensure DemistoException is thrown with expected error message.
    """
    import demistomock as demisto
    from paramiko import SSHClient
    from RemoteAccessv2 import copy_to_command

    mock_client: SSHClient = SSHClient()
    mocker.patch.object(demisto, "getFilePath", return_value={})
    with pytest.raises(DemistoException, match="Could not find given entry ID path. Please assure given entry ID is correct."):
        copy_to_command(mock_client, {"entry_id": 123})


def test_copy_to_command_invalid_arguments():
    """
    Given:
    - Cortex XSOAR arguments

    When:
    - Calling the copy-to command with both dest-dir and destination_path arguments.

    Then:
    - Ensure DemistoException is thrown with expected error message.
    """
    from paramiko import SSHClient
    from RemoteAccessv2 import copy_to_command

    mock_client: SSHClient = SSHClient()
    with pytest.raises(
        DemistoException, match='Please provide at most one of "dest-dir" argument or "destination_path", not both.'
    ):
        copy_to_command(mock_client, {"entry_id": 123, "dest-dir": "A", "destination_path": "B/file.txt"})


@pytest.mark.parametrize("file_name, expected_file_name", ([None, "mock_path.txt"], ["Name", "Name"]))
def test_copy_from_command_valid(mocker, file_name, expected_file_name):
    """
    Given:
    - Cortex XSOAR arguments

    When:
    - Calling the copy-to command.
    Case a: Calling the command without specified file name.
    Case b: Calling the command with specified file name.

    Then:
    - Ensure expected fileResult object is returned.
    """
    import RemoteAccessv2
    from paramiko import SSHClient
    from RemoteAccessv2 import main

    mock_client: SSHClient = SSHClient()
    mocker_results = mocker.patch.object(RemoteAccessv2, "fileResult")
    mocker.patch.object(demisto, "command", return_value="copy-from")
    mocker.patch.object(RemoteAccessv2, "create_paramiko_ssh_client", return_value=mock_client)
    mocker.patch.object(RemoteAccessv2, "return_results")
    args = {"file_path": "mock_path.txt", "host": "host"}
    if file_name:
        args["file_name"] = file_name
    mocker.patch.object(demisto, "args", return_value=args)
    mocker.patch("RemoteAccessv2.perform_copy_command", return_value="RemoteFileData")
    main()
    assert mocker_results.call_args[0][0] == expected_file_name
    assert mocker_results.call_args[0][1] == "RemoteFileData"


@pytest.mark.parametrize("args", [({}), ({"additional_password": "1243"})])
def test_invalid_password_for_command(mocker, args):
    """ "
    Given:
    - Cortex XSOAR params, having additional_password parameter fulfilled.
    - Cortex XSOAR arguments.

    When:
    - Executing a command.
    Cases:
    Case a: No additional_password was given in the arguments.
    Case a: Incorrect additional_password was given in the arguments.

    Then:
    - Ensure DemistoException is thrown with the expected error message.
    """
    from RemoteAccessv2 import main

    mocker.patch.object(demisto, "params", return_value={"additional_password": {"password": "1234"}})
    mocker.patch.object(demisto, "args", return_value=args)
    with pytest.raises(
        DemistoException,
        match="Additional password to use the module have been supplied.\n"
        'Please supply "additional_password" argument that matches '
        'the "Additional Password" parameter value.',
    ):
        main()


def test_failed_authentication(mocker):
    """
    Given:
    - Cortex XSOAR arguments.
    - Cortex XSOAR command for Remote Access integration.

    When:
    - No credentials are given.

    Then:
    - Ensure error is returned.
    """
    import RemoteAccessv2
    from RemoteAccessv2 import main

    mocker.patch.object(RemoteAccessv2, "return_error")
    mocker.patch.object(demisto, "error")
    main()
    assert RemoteAccessv2.return_error.called


investigation_data = {
    "id": 46510,
    "systems": [
        {
            "ciphers": None,
            "credentials": "ssh - test",
            "engineId": "",
            "host": "11.234.1.17",
            "integrationinstanceid": "",
            "issharedagent": False,
            "name": "i-0fb7ben3de5e85283",
            "os": "linux",
            "servicesID": "",
            "terminalOptions": {
                "Echo": 0,
                "Terminal": False,
                "TerminalHeight": 0,
                "TerminalType": "",
                "TerminalWidth": 0,
                "TyISpeed": 0,
                "TyOSpeed": 0,
            },
            "user": "ubuntu",
        }
    ],
}

system_found = (["i-0fb7ben3de5e85283"], [], investigation_data, None)
host_found = ([], ["11.234.1.17"], investigation_data, None)
system_and_host_found = (["i-0fb7ben3de5e85283"], ["11.234.1.17"], investigation_data, None)
host_not_found_no_system = (
    [],
    ["11.234.1.1"],
    investigation_data,
    "Hosts ['11.234.1.1'] not found on investigation 46510. Available systems by "
    "name are ['i-0fb7ben3de5e85283'], and by host are ['11.234.1.17'].",
)
host_not_found_with_system = (
    ["i-0fb7ben3de5e85283"],
    ["11.234.1.1"],
    investigation_data,
    "Hosts ['11.234.1.1'] not found on investigation 46510. Available systems by "
    "name are ['i-0fb7ben3de5e85283'], and by host are ['11.234.1.17'].",
)
system_not_found_no_host = (
    ["not-0fb7ben3de5e85283"],
    [],
    investigation_data,
    "Systems ['not-0fb7ben3de5e85283'] not found on investigation 46510. Available systems by "
    "name are ['i-0fb7ben3de5e85283'], and by host are ['11.234.1.17'].",
)
system_not_found_with_host = (
    ["not-0fb7ben3de5e85283"],
    ["11.234.1.17"],
    investigation_data,
    "Systems ['not-0fb7ben3de5e85283'] not found on investigation 46510. Available systems by "
    "name are ['i-0fb7ben3de5e85283'], and by host are ['11.234.1.17'].",
)
system_and_host_not_found = (
    ["not-0fb7ben3de5e85283"],
    ["11.234.1.1"],
    investigation_data,
    "Systems ['not-0fb7ben3de5e85283'] and Hosts ['11.234.1.1'] not found on investigation 46510. "
    "Available systems by name are ['i-0fb7ben3de5e85283'], and by host are ['11.234.1.17'].",
)
no_investigation_data = (["i-0fb7ben3de5e85283"], ["11.234.1.17"], None, None)
no_system_data = (["i-0fb7ben3de5e85283"], ["11.234.1.17"], {"id": 46510, "systems": []}, None)


@pytest.mark.parametrize(
    "given_systems, given_hosts, investigation, expected_message",
    [
        system_found,
        host_found,
        system_and_host_found,
        host_not_found_no_system,
        host_not_found_with_system,
        system_not_found_no_host,
        system_not_found_with_host,
        system_and_host_not_found,
        no_investigation_data,
        no_system_data,
    ],
)
def test_find_nonexistent_systems(mocker, given_systems, given_hosts, investigation, expected_message):
    """
    Given:
        A system/host argument is provided.

    When:
        Running a command.

    Then:
        The system/host are found or not in the investigation data and a relevant message is returned.
    """
    from RemoteAccessv2 import find_nonexistent_systems

    mocker.patch.object(demisto, "investigation", return_value=investigation)

    assert find_nonexistent_systems(given_systems, given_hosts) == expected_message