RemoteAccess v2
This integration transfers files between Cortex XSOAR and a remote machine and executes commands on the remote machine.
Utilities · Remote Access
Details
| ID | RemoteAccess v2 |
|---|---|
| Provider | Axis Communications |
| Category | Utilities |
| From Version | 6.0.0 |
| Docker Image | demisto/netmiko:1.0.0.9067966 |
| Supported Modules | Agentix 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:
- Edit the /etc/ssh/sshd_config file.
- set
PermitRootLogintoyes - set
PasswordAuthenticationtoyes
- 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 Addresscredentials— Useradditional_password—ciphers— Cipherskey_algorithms— Key Algorithms
Commands (3)
-
copy-fromCopies the given file from the remote machine to Cortex XSOAR.
-
copy-toCopies the given file from Cortex XSOAR to the remote machine.
-
sshRun 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