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 tempfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from io import StringIO

import paramiko
import urllib3
from CommonServerPython import *  # noqa # pylint: disable=unused-wildcard-import
from paramiko import AutoAddPolicy, SSHClient, Transport, transport
from paramiko.ssh_exception import NoValidConnectionsError
from scp import SCPClient, SCPException

from CommonServerUserPython import *  # noqa

# Disable insecure warnings
urllib3.disable_warnings()  # pylint: disable=no-member
DEFAULT_TIMEOUT = 10.0
DEFAULT_PORT = 22
""" HELPER FUNCTIONS """


def perform_copy_command(
    ssh_client: SSHClient, file_path: str, destination_path: str, copy_to_remote: bool, socket_timeout: float
) -> Union[str, bytes]:
    """
    Function to perform copy to or copy from remote machine.
    This helper function was separated from command functions mainly for easier mocking in tests.
    Args:
        ssh_client (SSHClient): SSH client to perform copy from or to.
        file_path (str):
        - Copy to remote machine from Cortex XSOAR - the Cortex XSOAR file path.
        - Copy to Cortex XSOAR from remote machine - the remote machine file path.
        destination_path (str):
        - Copy to remote machine from Cortex XSOAR - the remote machine file path to contain the copied data.
        - Copy to Cortex XSOAR from remote machine - Temp file name to save the file, before extracting its data.
        copy_to_remote (bool): Whether a request to copy to remote was made.
        socket_timeout(float): Socket timeout.

    Returns:
        (str): Empty str if command requested was copy to.
        (str): Str containing the copied file from remote machine data.
    Raises:
        (Exception):  if unexpected behaviour occurred.
    """
    try:
        with SCPClient(ssh_client.get_transport(), socket_timeout=socket_timeout) as scp_client:
            if copy_to_remote:
                scp_client.put(file_path, destination_path)
                return ""
            else:
                with tempfile.TemporaryDirectory() as temp_dir:
                    scp_client.get(file_path, f"{temp_dir}/{destination_path}")
                    with open(f"{temp_dir}/{destination_path}", "rb") as f:
                        return f.read()
    except (FileNotFoundError, SCPException) as e:
        if "No such file or directory" in str(e):
            raise DemistoException(
                f"Could not find the given path {file_path} in the local machine.\nPlease verify the path is correct."
            ) from e
        if "Not a directory" in str(e):
            raise DemistoException(
                f"Given destination path: {destination_path} does not exist in remote machine.\n"
                "Please verify destination path is valid."
            ) from e
        if "No such file or directory" in str(e):
            raise DemistoException(
                f"Could not find the given path {file_path} in the remote machine.\nPlease verify the path is correct."
            ) from e
        raise e


def get_available_ciphers() -> Set[str]:
    """
    Gets a set of the available ciphers supported by server.
    Returns:
        (Set[str]): Set of supported ciphers.
    """
    tmp_socket = socket.socket()
    opts = transport.Transport(tmp_socket).get_security_options()
    tmp_socket.close()
    return set(opts.ciphers)


def get_available_key_algorithms() -> Set[str]:
    """
    Gets a set of the available key algorithms supported by server.
    Returns:
        (Set[str]): Set of supported ciphers.
    """
    tmp_socket = socket.socket()
    opts = transport.Transport(tmp_socket).get_security_options()
    tmp_socket.close()
    return set(opts.kex)


def create_paramiko_ssh_client(
    host_name: str,
    user_name: str,
    password: str,
    ciphers: Set[str],
    key_algorithms: Set[str],
    private_key: str = "",
    port: int = DEFAULT_PORT,
) -> SSHClient:
    """
    Creates the Paramiko SSH client.

    Args:
        host_name (str): Hostname of the machine to create the SSH for.
        user_name (str): User to create the SSH session with the given host.
        password (str): Password of the given user.
        ciphers (Set[str]): Set of ciphers to be used, if given.
        key_algorithms (Set[str]): Set of key algorithms to be used, if given.
        private_key (str): The SSH certificate (should be PEM file based certificate only).
        port (int): The port to connect to.

    Returns:
        (SSHClient): Paramiko SSH client if connection was successful, exception otherwise.
    """
    if ciphers:
        # Getting available ciphers from server, in order to print an appropriate error message upon no cipher match.
        available_ciphers = get_available_ciphers()
        if not ciphers.intersection(available_ciphers):
            raise DemistoException(
                f"Given ciphers are not available in server.\nCiphers available in server are: {available_ciphers}"
            )
        Transport._preferred_ciphers = (*ciphers,)  # type: ignore
    if key_algorithms:
        available_key_args = get_available_key_algorithms()
        if not key_algorithms.intersection(available_key_args):
            raise DemistoException(
                f"Given key algorithms are not available in server.\n"
                f"Key algorithms available in server are: {available_key_args}"
            )
        Transport._preferred_kex = (*key_algorithms,)  # type: ignore
    client = SSHClient()
    client.set_missing_host_key_policy(AutoAddPolicy())
    try:
        rsa_private_key = None
        if private_key:
            # authenticating with private key only works for certificates which are based on PEM files.
            # (RSA private keys)
            rsa_private_key = paramiko.RSAKey.from_private_key(StringIO(private_key))  # type: ignore # [assignment]
        client.connect(hostname=host_name, username=user_name, password=password, port=port, pkey=rsa_private_key)
    except NoValidConnectionsError as e:
        raise DemistoException(f"Unable to connect to port {port} on {host_name}") from e
    return client


def find_nonexistent_systems(given_systems: List[str], given_hosts: List[str]):
    investigation = demisto.investigation()
    if not investigation:
        return None

    systems = investigation.get("systems")
    investigation_id = investigation.get("id")
    demisto.debug(f"Available systems on investigation {investigation_id} are {systems}.")
    if not systems:
        return None

    systems_names = [system_properties.get("name") for system_properties in systems]
    not_found_systems = []
    for given_system in given_systems:
        if given_system not in systems_names:
            not_found_systems.append(given_system)

    systems_hosts = [system_properties.get("host") for system_properties in systems]
    not_found_hosts = []
    for given_host in given_hosts:
        if given_host not in systems_hosts:
            not_found_hosts.append(given_host)

    if not not_found_hosts and not not_found_systems:
        return None

    return (
        f'{f"Systems {not_found_systems}" if not_found_systems else ""}'
        f'{" and " if not_found_systems and not_found_hosts else ""}'
        f'{f"Hosts {not_found_hosts}" if not_found_hosts else ""} '
        f'not found on investigation {investigation_id}. '
        f'Available systems by name are {systems_names}, and by host are {systems_hosts}.'
    )


def create_clients(
    host_name: str,
    user: str,
    password: str,
    ciphers: Set[str],
    key_algorithms: Set[str],
    certificate: str,
    systems: List[str],
    hosts: List[str],
    port: int = DEFAULT_PORT,
) -> List[SSHClient]:
    clients = [
        create_paramiko_ssh_client(system, user, password, ciphers, key_algorithms, certificate, port) for system in systems
    ]
    clients.extend(
        [create_paramiko_ssh_client(host, user, password, ciphers, key_algorithms, certificate, port) for host in hosts]
    )

    if not clients and host_name:
        client = create_paramiko_ssh_client(host_name, user, password, ciphers, key_algorithms, certificate, port)
        clients.append(client)

    return clients


def close_clients(clients: List[SSHClient]):
    for client in clients:
        client.close()


""" COMMAND FUNCTIONS """


def execute_shell_command(ssh_client: SSHClient, args: Dict[str, Any]) -> CommandResults:
    """
    Executes shell command.
    Args:
        ssh_client (SSHClient): SSH client to perform the command with.
        args (Dict[str, Any]): Cortex XSOAR arguments.

    Returns:
        (CommandResults).
    """
    command: str = args.get("cmd", "")
    timeout: Optional[int] = arg_to_number(args.get("timeout"))
    # exec_command returns a tuple of stdin, stdout, stderr. No need to parse stdin because it does not contain data.
    _, stdout, std_err = ssh_client.exec_command(command, timeout=timeout)
    stdout_str: str = stdout.read().decode()
    std_error_str: str = std_err.read().decode()
    if stdout_str or std_error_str:
        outputs: Optional[List[Dict]] = [
            {"output": stdout_str, "error": std_error_str, "command": command, "success": not std_error_str}
        ]
        readable_output = tableToMarkdown(f"Command {command} Outputs", outputs, removeNull=True)
    else:
        outputs = None
        readable_output = f"### Command {command} was executed successfully without any outputs."
    return CommandResults(outputs_prefix="RemoteAccess.Command", outputs=outputs, readable_output=readable_output)


def copy_to_command(ssh_client: SSHClient, args: Dict[str, Any]) -> CommandResults:
    """
    Executes a safe copy from Cortex XSOAR to remote machine.
    Args:
        ssh_client (SSHClient): SSH client to perform the command with.
        args (Dict[str, Any]): Cortex XSOAR arguments.

    Returns:
        (CommandResults).
    """
    dest_dir_arg = args.get("dest-dir", "")
    destination_path_arg = args.get("destination_path", "")
    if dest_dir_arg and destination_path_arg:
        raise DemistoException('Please provide at most one of "dest-dir" argument or "destination_path", not both.')

    # Support `entry` argument to maintain BC:
    entry: str = args.get("entry", "")
    entry_id: str = args.get("entry_id", entry)
    if not entry_id:
        raise DemistoException('No entry ID path given. Please provide one of the "entry_id" (recommended) or "entry" inputs.')

    if timeout := args.get("timeout"):
        timeout = float(timeout)
    else:
        timeout = DEFAULT_TIMEOUT
    file_path_data = demisto.getFilePath(entry_id)
    if not (file_path := file_path_data.get("path", "")):
        raise DemistoException("Could not find given entry ID path. Please assure given entry ID is correct.")
    file_name = file_path_data.get("name", "")

    if dest_dir_arg:
        destination_path = os.path.join(dest_dir_arg, file_name)
        destination_dir = dest_dir_arg
    elif destination_path_arg:
        destination_path = destination_path_arg
        destination_dir = os.path.split(destination_path)[0]
    else:
        destination_path = file_name
        destination_dir = ""

    # Create all folders to destination_path in the remote machine
    if destination_dir:
        try:
            execute_shell_command(ssh_client, args={"cmd": f"mkdir -p {destination_dir}"})
        except Exception as e:
            # ignore the error of creating the dir, as sometime is already exist and the error are due to permission
            # otherwise the next operation will fail.
            demisto.debug(f"Ignoring the error: {e!s}, occurred when run the command: mkdir -p {destination_dir}")

    perform_copy_command(ssh_client, file_path, destination_path, copy_to_remote=True, socket_timeout=timeout)
    return CommandResults(readable_output=f"### The file corresponding to entry ID: {entry_id} was copied to remote host.")


def copy_from_command(ssh_client: SSHClient, args: Dict[str, Any]) -> Dict:
    """
    Executes a safe copy from remote machine to Cortex XSOAR machine.
    Args:
        ssh_client (SSHClient): SSH client to perform the command with.
        args (Dict[str, Any]): Cortex XSOAR arguments.

    Returns:
        (Dict): FileResult data.
    """
    if timeout := args.get("timeout"):
        timeout = float(timeout)
    else:
        timeout = DEFAULT_TIMEOUT
    # Support `file` argument to maintain BC:
    file: str = args.get("file", "")
    file_path: str = args.get("file_path", file)
    file_name: str = args.get("file_name", os.path.basename(file_path))
    remote_file_data = perform_copy_command(ssh_client, file_path, file_name, copy_to_remote=False, socket_timeout=timeout)

    return fileResult(file_name, remote_file_data)


""" MAIN FUNCTION """


def main() -> None:
    params = demisto.params()
    args = demisto.args()
    command = demisto.command()

    credentials: Dict[str, Any] = params.get("credentials") or {}
    user: str = credentials.get("identifier", "")
    password: str = credentials.get("password", "")
    certificate: str = (credentials.get("credentials") or {}).get("sshkey", "")

    host_name: str = params.get("hostname", "")
    ciphers: Set[str] = set(argToList(params.get("ciphers")))
    key_algorithms: Set[str] = set(argToList(params.get("key_algorithms")))

    demisto.debug(f"Command being called is {demisto.command()}")
    if (
        (password_param := params.get("additional_password", {}).get("password"))
        and command != "test-module"
        and password_param != args.get("additional_password")
    ):
        raise DemistoException(
            "Additional password to use the module have been supplied.\n"
            'Please supply "additional_password" argument that matches the "Additional Password"'
            " parameter value."
        )

    systems = argToList(args.get("system"))
    hosts = argToList(args.get("host"))
    port = args.get("port") or DEFAULT_PORT

    clients = []
    try:
        if nonexistent_systems_result := find_nonexistent_systems(systems, hosts):
            raise DemistoException(nonexistent_systems_result)
        clients = create_clients(host_name, user, password, ciphers, key_algorithms, certificate, systems, hosts, port)

        commands = {
            "ssh": execute_shell_command,
            "copy-to": copy_to_command,
            "copy-from": copy_from_command,
        }

        if command == "test-module":
            return_results("ok")
        elif command in commands:
            if not clients:
                raise DemistoException("Command can't be executed because no hostname, system, or host was provided.")

            results = []
            with ThreadPoolExecutor(max_workers=len(clients)) as executor:
                future_results = [executor.submit(commands[command], ssh_client=client, args=args) for client in clients]
                for future in as_completed(future_results):
                    results.append(future.result())

            return_results(results)

        else:
            raise NotImplementedError(f"""Command '{command}' is not implemented.""")

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}")

    finally:
        close_clients(clients)


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()