From 2c773064922c53d476989e4431a60185aa5e981d Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 11 Jun 2026 10:12:18 +0200 Subject: [PATCH 1/6] add jitter fields to config --- src/attackmate/schemas/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/attackmate/schemas/config.py b/src/attackmate/schemas/config.py index 4c785c96d..33ba7455d 100644 --- a/src/attackmate/schemas/config.py +++ b/src/attackmate/schemas/config.py @@ -17,6 +17,9 @@ class MsfConfig(BaseModel): class CommandConfig(BaseModel): loop_sleep: int = 5 command_delay: float = 0 + command_delay_jitter: bool = False + command_delay_jitter_min: float = 0.5 + command_delay_jitter_max: float = 2.0 class BettercapConfig(BaseModel): From f9cba7f4dead6dd3e59d4df702efcb8b8732a5de Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 11 Jun 2026 10:13:13 +0200 Subject: [PATCH 2/6] add calculation of command delay jitter --- src/attackmate/attackmate.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/attackmate/attackmate.py b/src/attackmate/attackmate.py index 8c900dae5..7b9e16dd1 100644 --- a/src/attackmate/attackmate.py +++ b/src/attackmate/attackmate.py @@ -14,6 +14,7 @@ :ref:`integration` for documentation on scripted usage. """ +import random import time from typing import Dict, Optional import logging @@ -151,13 +152,20 @@ async def _run_commands(self, commands: Commands): The delay between commands is controlled by ``cmd_config.command_delay`` in the :class:`Config`. Defaults to ``0`` if not set. """ - delay = self.pyconfig.cmd_config.command_delay or 0 - self.logger.info(f'Delay before commands: {delay} seconds') + cfg = self.pyconfig.cmd_config + base_delay = cfg.command_delay or 0 for command in commands: command_type = 'ssh' if command.type == 'sftp' else command.type executor = self._get_executor(command_type) if executor: if command.type not in ('sleep', 'debug', 'setvar'): + if cfg.command_delay_jitter: + offset = random.uniform(cfg.command_delay_jitter_min, cfg.command_delay_jitter_max) + sign = random.choice([-1, 1]) + delay = max(0.0, base_delay + sign * offset) + else: + delay = base_delay + self.logger.info(f'Delay before command: {delay:.3f} seconds') time.sleep(delay) await executor.run(command, is_api_instance=self.is_api_instance) From a546b94b07f340d8f2c69e5e0503d9f3f3b963d0 Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 11 Jun 2026 10:13:58 +0200 Subject: [PATCH 3/6] add jitter docstring --- src/attackmate/attackmate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/attackmate/attackmate.py b/src/attackmate/attackmate.py index 7b9e16dd1..52d6f96bb 100644 --- a/src/attackmate/attackmate.py +++ b/src/attackmate/attackmate.py @@ -151,6 +151,12 @@ async def _run_commands(self, commands: Commands): .. note:: The delay between commands is controlled by ``cmd_config.command_delay`` in the :class:`Config`. Defaults to ``0`` if not set. + + When ``cmd_config.command_delay_jitter`` is ``True``, each delay is + randomized as ``command_delay ± random(jitter_min, jitter_max)``, + clamped to a minimum of ``0``. The jitter bounds are set via + ``command_delay_jitter_min`` (default ``0.5``) and + ``command_delay_jitter_max`` (default ``2.0``). """ cfg = self.pyconfig.cmd_config base_delay = cfg.command_delay or 0 From a470ae42505d5241823391e37e0512ba8189c1db Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 11 Jun 2026 10:14:19 +0200 Subject: [PATCH 4/6] add tests for jitter behaviour --- test/units/test_commanddelay.py | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/units/test_commanddelay.py b/test/units/test_commanddelay.py index 2295402c6..e08fdb620 100644 --- a/test/units/test_commanddelay.py +++ b/test/units/test_commanddelay.py @@ -1,4 +1,5 @@ import time +from unittest.mock import patch import pytest from attackmate.attackmate import AttackMate from attackmate.schemas.config import Config, CommandConfig @@ -93,3 +94,65 @@ async def test_delay_is_not_applied_for_exempt_commands(): assert elapsed_time < 0.1, ( f'Execution with exempt commands took too long: {elapsed_time:.4f}s.' ) + + +@pytest.mark.asyncio +async def test_jitter_off_behavior_unchanged(): + """ + With jitter disabled (default), behavior is identical to fixed command_delay. + """ + delay = 0.1 + playbook = Playbook(commands=[ShellCommand(type='shell', cmd='echo 1')]) + config = Config(cmd_config=CommandConfig(command_delay=delay, command_delay_jitter=False)) + attackmate_instance = AttackMate(playbook=playbook, config=config) + + with patch('attackmate.attackmate.time.sleep') as mock_sleep: + await attackmate_instance._run_commands(attackmate_instance.playbook.commands) + mock_sleep.assert_called_once_with(delay) + + +@pytest.mark.asyncio +async def test_jitter_on_computes_correct_delay(): + """ + With jitter enabled, actual delay = command_delay + sign * uniform(jitter_min, jitter_max), + clamped to >= 0. + """ + playbook = Playbook(commands=[ShellCommand(type='shell', cmd='echo 1')]) + config = Config(cmd_config=CommandConfig( + command_delay=1.0, + command_delay_jitter=True, + command_delay_jitter_min=0.5, + command_delay_jitter_max=2.0, + )) + attackmate_instance = AttackMate(playbook=playbook, config=config) + + with patch('attackmate.attackmate.random.uniform', return_value=0.8) as mock_uniform, \ + patch('attackmate.attackmate.random.choice', return_value=1) as mock_choice, \ + patch('attackmate.attackmate.time.sleep') as mock_sleep: + await attackmate_instance._run_commands(attackmate_instance.playbook.commands) + mock_uniform.assert_called_once_with(0.5, 2.0) + mock_choice.assert_called_once_with([-1, 1]) + # 1.0 + 1 * 0.8 = 1.8, clamped to >= 0 → 1.8 + mock_sleep.assert_called_once_with(1.8) + + +@pytest.mark.asyncio +async def test_jitter_clamped_to_zero(): + """ + When jitter produces a negative effective delay, it is clamped to 0. + """ + playbook = Playbook(commands=[ShellCommand(type='shell', cmd='echo 1')]) + config = Config(cmd_config=CommandConfig( + command_delay=0.0, + command_delay_jitter=True, + command_delay_jitter_min=0.5, + command_delay_jitter_max=2.0, + )) + attackmate_instance = AttackMate(playbook=playbook, config=config) + + with patch('attackmate.attackmate.random.uniform', return_value=1.5), \ + patch('attackmate.attackmate.random.choice', return_value=-1), \ + patch('attackmate.attackmate.time.sleep') as mock_sleep: + await attackmate_instance._run_commands(attackmate_instance.playbook.commands) + # 0.0 + (-1) * 1.5 = -1.5, clamped → 0.0 + mock_sleep.assert_called_once_with(0.0) From 19f24a0eb59a69a8406339d0e1a1de5745a01d24 Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 11 Jun 2026 10:16:57 +0200 Subject: [PATCH 5/6] add jitter to docs --- docs/source/configuration/command_config.rst | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/source/configuration/command_config.rst b/docs/source/configuration/command_config.rst index a0fb8d0ff..8091cf34d 100644 --- a/docs/source/configuration/command_config.rst +++ b/docs/source/configuration/command_config.rst @@ -10,6 +10,9 @@ These are settings for **all** commands. cmd_config: loop_sleep: 5 command_delay: 0 + command_delay_jitter: false + command_delay_jitter_min: 0.5 + command_delay_jitter_max: 2.0 .. confval:: loop_sleep @@ -27,3 +30,29 @@ These are settings for **all** commands. :type: float :default: 0 + +.. confval:: command_delay_jitter + + When ``true``, randomizes the per-command delay. The effective delay is calculated as + ``command_delay ± random(command_delay_jitter_min, command_delay_jitter_max)``, + clamped to a minimum of ``0``. Has no effect when ``command_delay_jitter`` + is ``false``. + + :type: bool + :default: false + +.. confval:: command_delay_jitter_min + + Lower bound of the jitter offset in seconds. Only used when + :confval:`command_delay_jitter` is ``true``. + + :type: float + :default: 0.5 + +.. confval:: command_delay_jitter_max + + Upper bound of the jitter offset in seconds. Only used when + :confval:`command_delay_jitter` is ``true``. + + :type: float + :default: 2.0 From 4c27a78b85cef5a27201e8d488957177d13d7157 Mon Sep 17 00:00:00 2001 From: thorinaboenke Date: Thu, 11 Jun 2026 10:18:53 +0200 Subject: [PATCH 6/6] add jitter to config in index.rst --- docs/source/configuration/index.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/configuration/index.rst b/docs/source/configuration/index.rst index 7f20365a2..ebdb3617b 100644 --- a/docs/source/configuration/index.rst +++ b/docs/source/configuration/index.rst @@ -26,7 +26,10 @@ sliver, metasploit and remote attackmate server: cmd_config: loop_sleep: 5 - command_delay: 0 + command_delay: 1 + command_delay_jitter: true + command_delay_jitter_min: 0.5 + command_delay_jitter_max: 2.0 bettercap_config: default: