From 6eed2cf084cce422aa1051f45fd2bd9a7a1c78bf Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 5 Mar 2026 16:35:43 +0100 Subject: [PATCH 1/4] fix indentation --- docs/source/configuration/remote_config.rst | 49 +++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/docs/source/configuration/remote_config.rst b/docs/source/configuration/remote_config.rst index 87031b84..e5394afc 100644 --- a/docs/source/configuration/remote_config.rst +++ b/docs/source/configuration/remote_config.rst @@ -10,33 +10,34 @@ Like other executors, the configuration uses unique identifiers (names) for each .. code-block:: yaml -remote_config: - remote_server: - url: "https://10.0.0.5:5000" - username: admin - password: securepassword - cafile: "/path/to/cert.pem" - another_server: - url: "https://10.0.0.6:5000" - username: user - password: anotherpassword - cafile: "/path/to/another_cert.pem" + remote_config: + remote_server: + url: "https://10.0.0.5:5000" + username: admin + password: securepassword + cafile: "/path/to/cert.pem" + another_server: + url: "https://10.0.0.6:5000" + username: user + password: anotherpassword + cafile: "/path/to/another_cert.pem" + .. code-block:: yaml -commands: -# Executed on 'another_server' -- type: remote - connection: another_server - cmd: execute_command - remote_command: - type: shell - cmd: "whoami" - - # Executed on 'remote_server' (defaults to first remote_config entry)) - - type: remote - cmd: execute_playbook - playbook_yaml_path: path/to/playbook.yml + commands: + # Executed on 'another_server' + - type: remote + connection: another_server + cmd: execute_command + remote_command: + type: shell + cmd: "whoami" + + # Executed on 'remote_server' (defaults to first remote_config entry)) + - type: remote + cmd: execute_playbook + playbook_yaml_path: path/to/playbook.yml .. confval:: url From 1b150795a77fb9165acabcc6c11194d9d406a212 Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 5 Mar 2026 16:39:20 +0100 Subject: [PATCH 2/4] remove remote server from attackmate docs --- docs/source/playbook/commands/remote.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/playbook/commands/remote.rst diff --git a/docs/source/playbook/commands/remote.rst b/docs/source/playbook/commands/remote.rst new file mode 100644 index 00000000..e69de29b From d17dfb9dd3904fe0e80562573c2c9beaac4967d7 Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 5 Mar 2026 16:40:32 +0100 Subject: [PATCH 3/4] add docs for remote command --- docs/source/playbook/commands/index.rst | 1 + docs/source/playbook/commands/remote.rst | 7 ++++ docs/source/remote.rst | 50 ------------------------ 3 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 docs/source/remote.rst diff --git a/docs/source/playbook/commands/index.rst b/docs/source/playbook/commands/index.rst index 2f977025..9ab32c3f 100644 --- a/docs/source/playbook/commands/index.rst +++ b/docs/source/playbook/commands/index.rst @@ -229,6 +229,7 @@ The next pages will describe all possible commands in detail. msf-session payload regex + remote setvar shell sftp diff --git a/docs/source/playbook/commands/remote.rst b/docs/source/playbook/commands/remote.rst index e69de29b..5ac5b989 100644 --- a/docs/source/playbook/commands/remote.rst +++ b/docs/source/playbook/commands/remote.rst @@ -0,0 +1,7 @@ +===== +remote +===== + +This command execute playbooks or commands on a remote attackmate instance. +The connection to the remote instance is defined in the ``remote_config`` section of the configuration file. +If no connection is specified, the first entry in the ``remote_config`` section will be used as default. diff --git a/docs/source/remote.rst b/docs/source/remote.rst deleted file mode 100644 index dc57721c..00000000 --- a/docs/source/remote.rst +++ /dev/null @@ -1,50 +0,0 @@ - -Remote Execution -================= -AttackMate can be used for remote command execution. -This section explains hoe the server handles authentication, logging, and AttackMate instances. - -Authentication --------------- - -The AttackMate API server secures its endpoints using **token-based authentication**. - -* **Login Endpoint**: Clients must first authenticate by sending a username and password to the `/login` endpoint (e.g., `https://localhost:8445/login`). This endpoint is defined in `main.py` and uses FastAPI's `OAuth2PasswordRequestForm` for standard form encoding. -* **Token Generation**: Upon successful authentication, the server generates an access token and returns it to the client. -* **Token Storage and Usage**: - * The provided token is received and then stored globally (`CURRENT_TOKEN`) and optionally in an environment variable (`ATTACKMATE_API_TOKEN`). - * For subsequent requests to protected endpoints (like `/command` or `/playbooks`), the client include this token in the `X-Auth-Token` header. - * The `update_token_from_response` function in `client.py` also suggests a mechanism for token renewal, where the server might return a new token in a response. -* **SSL/TLS**: The `main.py` server is configured to run with HTTPS on port `8443`, requiring `key.pem` and `cert.pem` files for SSL/TLS encryption. Clients should specify the path to the server's public certificate. - -Logging -------- - -The AttackMate API server provides the following logging features: - -* **Centralized Logging**: `main.py` initializes several loggers (`attackmate_api`, `playbook`, `output`, `json`) at startup. -* **Instance-Specific Logging**: For remote command and playbook executions, AttackMate implements instance-specific logging. - * The `instance_logging` context manager (from `log_utils.py`) creates dedicated log files for each AttackMate instance based on its `instance_id`. - * These logs are stored in a `attackmate_server_logs` directory (relative to the project root, or a configurable absolute path like `/var/log/attackmate_instances`). - * Each remote command or playbook execution within an instance generates new timestamped log files (e.g., `20240715_123456_my_instance_output.log`). - * This ensures that logs from different concurrent AttackMate instances remain separate. -* **Log Levels**: Log levels can be adjusted for debugging, providing more verbose output when needed. - -AttackMate Instances --------------------- - -The AttackMate API server manages AttackMate instances to handle multiple execution contexts. - -* **Default Instance**: On startup, `main.py` initializes a single default AttackMate instance named `default_context`. This instance handles request to process entire playbooks. -* **Instance Management**: - * The server can create and manage multiple AttackMate instances, each identified by a unique `instance_id`. -* **Instance Lifecycle**: - * Instances are stored in a global dictionary (`state.INSTANCES`). - * When the API server starts (`lifespan` function in `main.py`), it loads a global AttackMate configuration and creates the `default_context` instance. - * When the API server shuts down, it iterates through all active instances, performing cleanup operations like `clean_session_stores()` and `kill_or_wait_processes()` to ensure resources are properly released. -* **Command and Playbook Execution**: - * The API routes (`commands`, `playbooks` routers included in `main.py`) receive client requests. - * These requests specify an `instance_id` (or implicitly use `default_context`). - * The API then dispatches the command or playbook to the designated AttackMate instance for execution. This allows for isolated execution environments, where variables and session data for one instance do not interfere with another. -* **State Management**: - * Each AttackMate instance maintains its own internal state, including a `VariableStore`. From 9a66acf6ace55ab62b449238bf29402babf0dfb0 Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 5 Mar 2026 16:47:20 +0100 Subject: [PATCH 4/4] rename var to playbook_path --- docs/source/configuration/remote_config.rst | 2 +- docs/source/playbook/commands/remote.rst | 126 +++++++++++++++++- .../executors/remote/remoteexecutor.py | 8 +- src/attackmate/schemas/remote.py | 6 +- test/units/test_remoteexecutor.py | 8 +- 5 files changed, 135 insertions(+), 15 deletions(-) diff --git a/docs/source/configuration/remote_config.rst b/docs/source/configuration/remote_config.rst index e5394afc..c032c7d2 100644 --- a/docs/source/configuration/remote_config.rst +++ b/docs/source/configuration/remote_config.rst @@ -37,7 +37,7 @@ Like other executors, the configuration uses unique identifiers (names) for each # Executed on 'remote_server' (defaults to first remote_config entry)) - type: remote cmd: execute_playbook - playbook_yaml_path: path/to/playbook.yml + playbook_path: path/to/playbook.yml .. confval:: url diff --git a/docs/source/playbook/commands/remote.rst b/docs/source/playbook/commands/remote.rst index 5ac5b989..5a8dc4da 100644 --- a/docs/source/playbook/commands/remote.rst +++ b/docs/source/playbook/commands/remote.rst @@ -1,7 +1,127 @@ -===== +====== remote -===== +====== -This command execute playbooks or commands on a remote attackmate instance. +This command executes playbooks or commands on a remote AttackMate instance. The connection to the remote instance is defined in the ``remote_config`` section of the configuration file. If no connection is specified, the first entry in the ``remote_config`` section will be used as default. + +Configuration +============= + +Remote connections are defined under the ``remote_config`` key in the AttackMate configuration file. +Each entry requires at minimum a ``url`` a ``username``, ``password``, and ``cafile`` +for TLS certificate verification. + +.. code-block:: yaml + + remote_config: + my-remote-instance: + url: https://192.42.0.254:8443 + username: admin + password: secret + cafile: /path/to/ca.pem + +Commands +======== + +Execute Command +--------------- + +Executes a single AttackMate command on the remote instance. + +.. code-block:: yaml + + commands: + - type: remote + cmd: execute_command + connection: my-remote-instance + remote_command: + type: shell + cmd: whoami + +Execute Playbook +---------------- + +Sends a local playbook YAML file to the remote instance and executes it there. + +.. code-block:: yaml + + commands: + - type: remote + cmd: execute_playbook + connection: my-remote-instance + playbook_path: /path/to/playbook.yaml + +Options +======= + +.. confval:: cmd + + The remote operation to perform. Must be one of the following: + + - ``execute_command`` — Execute a single AttackMate command on the remote instance. + Requires ``remote_command`` to be set. + - ``execute_playbook`` — Execute a full playbook YAML file on the remote instance. + Requires ``playbook_path`` to be set. + + :type: str (``execute_command`` | ``execute_playbook``) + +.. confval:: connection + + The name of the remote connection to use, as defined in the ``remote_config`` section + of the configuration file. If omitted, the first entry in ``remote_config`` is used + as the default connection. + + :type: str + :default: first entry in ``remote_config`` + +.. confval:: playbook_path + + Path to a local YAML playbook file that will be read and sent to the remote AttackMate + instance for execution. Required when ``cmd`` is ``execute_playbook``. + + :type: str + +.. confval:: remote_command + + An inline AttackMate command definition that will be executed on the remote instance. + This supports any command type that the remote AttackMate instance is configured to handle + (e.g., ``shell``, ``sliver``, etc., EXCEPT another remote_command). Required when ``cmd`` is ``execute_command``. + + :type: RemotelyExecutableCommand + +.. warning:: + + Parameters such as ``background`` and ``only_if`` defined at the top-level ``remote`` command + apply to the **local** execution of the remote command, not to the command executed on the + remote instance. + +Examples +======== + +Execute a shell command on a remote instance +-------------------------------------------- + +.. code-block:: yaml + + vars: + $REMOTE_HOST: my-remote-instance + + commands: + - type: remote + cmd: execute_command + connection: $REMOTE_HOST + remote_command: + type: shell + cmd: id + +Execute a playbook on a remote instance using the default connection +-------------------------------------------------------------------- + +.. code-block:: yaml + + commands: + - type: remote + cmd: execute_playbook + playbook_path: /homeuser/playbooks/recon.yaml diff --git a/src/attackmate/executors/remote/remoteexecutor.py b/src/attackmate/executors/remote/remoteexecutor.py index 6ea1e97e..5ad3f766 100644 --- a/src/attackmate/executors/remote/remoteexecutor.py +++ b/src/attackmate/executors/remote/remoteexecutor.py @@ -148,15 +148,15 @@ def _dispatch_remote_command( debug = getattr(command, 'debug', False) self.logger.debug(f"Dispatching command '{command.cmd}' with debug={debug}") - if command.cmd == 'execute_playbook' and command.playbook_yaml_path: + if command.cmd == 'execute_playbook' and command.playbook_path: try: - with open(command.playbook_yaml_path, 'r', encoding='utf-8') as f: + with open(command.playbook_path, 'r', encoding='utf-8') as f: yaml_content = f.read() response = client.execute_remote_playbook_yaml(yaml_content, debug=debug) except FileNotFoundError as e: - raise ExecException(f'Playbook file not found: {command.playbook_yaml_path}') from e + raise ExecException(f'Playbook file not found: {command.playbook_path}') from e except IOError as e: - raise ExecException(f'Failed to read playbook file {command.playbook_yaml_path}: {e}') from e + raise ExecException(f'Failed to read playbook file {command.playbook_path}: {e}') from e elif command.cmd == 'execute_command': response = client.execute_remote_command(command.remote_command, debug=debug) diff --git a/src/attackmate/schemas/remote.py b/src/attackmate/schemas/remote.py index 5a3251dd..794b8328 100644 --- a/src/attackmate/schemas/remote.py +++ b/src/attackmate/schemas/remote.py @@ -13,7 +13,7 @@ class AttackMateRemoteCommand(BaseCommand): type: Literal['remote'] cmd: Literal['execute_command', 'execute_playbook'] connection: Optional[str] = None - playbook_yaml_path: Optional[str] = None + playbook_path: Optional[str] = None remote_command: Optional[RemotelyExecutableCommand] = None # Common command parameters (like background, only_if) from BaseCommand @@ -23,6 +23,6 @@ class AttackMateRemoteCommand(BaseCommand): def check_remote_command(self) -> 'AttackMateRemoteCommand': if self.cmd == 'execute_command' and not self.remote_command: raise ValueError("remote_command must be provided when cmd is 'execute_command'") - if self.cmd == 'execute_playbook' and not self.playbook_yaml_path: - raise ValueError("playbook_yaml_path must be provided when cmd is 'execute_playbook_yaml'") + if self.cmd == 'execute_playbook' and not self.playbook_path: + raise ValueError("playbook_path must be provided when cmd is 'execute_playbook_yaml'") return self diff --git a/test/units/test_remoteexecutor.py b/test/units/test_remoteexecutor.py index 5b31d11f..a088517d 100644 --- a/test/units/test_remoteexecutor.py +++ b/test/units/test_remoteexecutor.py @@ -50,7 +50,7 @@ def mock_remote_command(): command.type = 'remote' command.remote_command = MagicMock() command.remote_command.model_dump = MagicMock(return_value={'cmd': 'test'}) - command.playbook_yaml_path = None + command.playbook_path = None command.debug = False return command @@ -227,7 +227,7 @@ def test_dispatch_execute_playbook(self, setup_executor, mock_remote_command, te mock_client = MagicMock() mock_client.execute_remote_playbook_yaml = MagicMock(return_value={'success': True}) mock_remote_command.cmd = 'execute_playbook' - mock_remote_command.playbook_yaml_path = temp_yaml_file + mock_remote_command.playbook_path = temp_yaml_file response = executor._dispatch_remote_command(mock_client, mock_remote_command) @@ -242,7 +242,7 @@ def test_dispatch_execute_playbook_file_not_found(self, setup_executor, mock_rem executor = setup_executor mock_client = MagicMock() mock_remote_command.cmd = 'execute_playbook' - mock_remote_command.playbook_yaml_path = '/nonexistent/file.yaml' + mock_remote_command.playbook_path = '/nonexistent/file.yaml' with pytest.raises(ExecException, match='Playbook file not found'): executor._dispatch_remote_command(mock_client, mock_remote_command) @@ -252,7 +252,7 @@ def test_dispatch_execute_playbook_read_error(self, setup_executor, mock_remote_ executor = setup_executor mock_client = MagicMock() mock_remote_command.cmd = 'execute_playbook' - mock_remote_command.playbook_yaml_path = '/path/to/file.yaml' + mock_remote_command.playbook_path = '/path/to/file.yaml' with patch('builtins.open', side_effect=IOError('Permission denied')): with pytest.raises(ExecException, match='Failed to read playbook file'):