From 379ffd18f49460378227e4861a23760801398c9e Mon Sep 17 00:00:00 2001 From: uruwhy <58484522+uruwhy@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:40:17 +0000 Subject: [PATCH 1/6] add agent status and agent kill via API --- app/api/v2/handlers/agent_api.py | 11 ++++++++- app/api/v2/managers/agent_api_manager.py | 10 +++++++- app/objects/c_agent.py | 30 ++++++++++++++++++++++++ tests/api/v2/handlers/test_agents_api.py | 23 ++++++++++++++++++ tests/conftest.py | 2 ++ tests/services/test_rest_svc.py | 4 ++-- tests/web_server/test_core_endpoints.py | 2 ++ 7 files changed, 78 insertions(+), 4 deletions(-) diff --git a/app/api/v2/handlers/agent_api.py b/app/api/v2/handlers/agent_api.py index 8ef885f38..cda77e288 100644 --- a/app/api/v2/handlers/agent_api.py +++ b/app/api/v2/handlers/agent_api.py @@ -19,6 +19,7 @@ def add_routes(self, app: web.Application): router.add_get('/agents', self.get_agents) router.add_get('/agents/{paw}', self.get_agent_by_id) router.add_post('/agents', self.create_agent) + router.add_post('/agents/kill/{paw}', self.kill_agent) router.add_patch('/agents/{paw}', self.update_agent) router.add_put('/agents/{paw}', self.create_or_update_agent) router.add_delete('/agents/{paw}', self.delete_agent) @@ -58,11 +59,19 @@ async def get_agent_by_id(self, request: web.Request): @aiohttp_apispec.docs(tags=['agents'], summary="Create a new agent", description="Creates a new agent using the format from 'AgentSchema'.") - @aiohttp_apispec.request_schema(AgentSchema) + @aiohttp_apispec.request_schema(AgentSchema(partial=True)) @aiohttp_apispec.response_schema(AgentSchema, description="Returns a single agent in 'AgentSchema' format") async def create_agent(self, request: web.Request): agent = await self.create_object(request) return web.json_response(agent.display) + + @aiohttp_apispec.docs(tags=['agents'], + summary="Kills an agent", + description="Marks an agent to stop after the next beacon.") + async def kill_agent(self, request: web.Request): + target_paw = request.match_info.get('paw') + kill_resp = await self._api_manager.kill_agent(target_paw) + return web.json_response(kill_resp) @aiohttp_apispec.docs(tags=['agents'], summary="Update an Agent", diff --git a/app/api/v2/managers/agent_api_manager.py b/app/api/v2/managers/agent_api_manager.py index c5ba93afa..000a12e1b 100644 --- a/app/api/v2/managers/agent_api_manager.py +++ b/app/api/v2/managers/agent_api_manager.py @@ -1,5 +1,5 @@ from app.api.v2.managers.base_api_manager import BaseApiManager - +from app.api.v2.responses import JsonHttpNotFound class AgentApiManager(BaseApiManager): def __init__(self, data_svc, file_svc): @@ -25,3 +25,11 @@ async def get_deploy_commands(self, ability_id: str = None): app_config.update({f'agents.{k}': v for k, v in self.get_config(name='agents').items()}) return dict(abilities=raw_abilities, app_config=app_config) + + async def kill_agent(self, target_paw: str): + agents = await self._data_svc.locate('agents', {'paw': target_paw}) + if not agents: + raise JsonHttpNotFound(f'Agent {target_paw} not found.') + target = agents[0] + await target.kill() + return {'response': 'Ok'} \ No newline at end of file diff --git a/app/objects/c_agent.py b/app/objects/c_agent.py index 6c087bdd1..ea771f774 100644 --- a/app/objects/c_agent.py +++ b/app/objects/c_agent.py @@ -48,6 +48,8 @@ class AgentFieldsSchema(ma.Schema): links = ma.fields.List(ma.fields.Nested(LinkSchema), dump_only=True) pending_contact = ma.fields.String() + status = ma.fields.String(dump_only=True) + @ma.pre_load def remove_nulls(self, in_data, **_): return {k: v for k, v in in_data.items() if v is not None} @@ -58,6 +60,7 @@ def remove_properties(self, data, **_): data.pop('created', None) data.pop('last_seen', None) data.pop('links', None) + data.pop('status', None) return data @@ -84,6 +87,24 @@ def unique(self): @property def display_name(self): return '{}${}'.format(self.host, self.username) + + @property + def status(self): + now = datetime.now(timezone.utc) + if self._marked_for_stop: + # If agent hasn't received the stop instruction yet in a beacon response, it's still pending stop + if self._stop_delivered: + return 'dead' + else: + return 'pending stop' + else: + # If agent hasn't beaconed in since max beacon time + untrusted timer, mark as dead + untrusted_buffer = int(self.get_config(name='agents', prop='untrusted_timer')) + time_diff = (now - self.last_seen).total_seconds() + if time_diff > int(self.sleep_max) + untrusted_buffer: + return 'dead' + else: + return 'alive' @classmethod def is_global_variable(cls, variable): @@ -139,6 +160,8 @@ def __init__(self, sleep_min=30, sleep_max=60, watchdog=0, platform='unknown', s self.upstream_dest = self.server self._executor_change_to_assign = None self.log = self.create_logger('agent') + self._marked_for_stop = False + self._stop_delivered = False def store(self, ram): existing = self.retrieve(ram['agents'], self.unique) @@ -212,6 +235,10 @@ async def heartbeat_modification(self, **kwargs): if not self._executor_change_to_assign: # Don't update executors if we're waiting to assign an executor change to the agent. self.update('executors', kwargs.get('executors')) + + # Check if agent has been marked to stop + if self._marked_for_stop and not self._stop_delivered: + self._stop_delivered = True async def gui_modification(self, **kwargs): loaded = AgentFieldsSchema(only=('group', 'trusted', 'sleep_min', 'sleep_max', 'watchdog', 'pending_contact')).load(kwargs) @@ -223,6 +250,9 @@ async def kill(self): self.update('sleep_min', 60 * 2) self.update('sleep_max', 60 * 2) + self._marked_for_stop = True + self._stop_delivered = False + def replace(self, encoded_cmd, file_svc): decoded_cmd = b64decode(encoded_cmd).decode('utf-8', errors='ignore').replace('\n', '') decoded_cmd = decoded_cmd.replace(self.RESERVED['server'], self.server) diff --git a/tests/api/v2/handlers/test_agents_api.py b/tests/api/v2/handlers/test_agents_api.py index d25683b78..a338ad079 100644 --- a/tests/api/v2/handlers/test_agents_api.py +++ b/tests/api/v2/handlers/test_agents_api.py @@ -162,6 +162,29 @@ async def test_create_agent(self, api_v2_client, api_cookies, new_agent_payload, async def test_unauthorized_create_agent(self, api_v2_client, new_agent_payload): resp = await api_v2_client.post('/api/v2/agents', json=new_agent_payload) assert resp.status == HTTPStatus.UNAUTHORIZED + + async def test_kill_agent(self, api_v2_client, api_cookies, test_agent, mocker, mock_time): + assert test_agent.watchdog == 0 and test_agent.sleep_min == 2 and test_agent.sleep_max == 8 + assert not (test_agent._marked_for_stop or test_agent._stop_delivered) + with mocker.patch('app.objects.c_agent.datetime') as mock_datetime: + mock_datetime.return_value = mock_datetime + mock_datetime.now.return_value = mock_time + assert test_agent.status == 'alive' + + resp = await api_v2_client.post('/api/v2/agents/kill/123', cookies=api_cookies) + assert resp.status == HTTPStatus.OK + assert {'response': 'Ok'} == await resp.json() + assert test_agent.status == 'pending stop' + assert test_agent._marked_for_stop and not test_agent._stop_delivered + assert test_agent.watchdog == 1 and test_agent.sleep_min == 120 and test_agent.sleep_max == 120 + + async def test_unauthorized_kill_agent(self, api_v2_client): + resp = await api_v2_client.post('/api/v2/agents/kill/123') + assert resp.status == HTTPStatus.UNAUTHORIZED + + async def test_nonexistent_kill_agent(self, api_v2_client, api_cookies): + resp = await api_v2_client.post('/api/v2/agents/kill/999', cookies=api_cookies) + assert resp.status == HTTPStatus.NOT_FOUND async def test_update_agent(self, api_v2_client, api_cookies, test_agent, updated_agent_fields_payload, expected_updated_agent_dump): diff --git a/tests/conftest.py b/tests/conftest.py index 28fb32c66..3fe3e7554 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -366,6 +366,8 @@ async def initialize(): BaseWorld.apply_config('main', yaml.safe_load(fle)) with open(Path(__file__).parents[1] / 'conf' / 'payloads.yml', 'r') as fle: BaseWorld.apply_config('payloads', yaml.safe_load(fle)) + with open(Path(__file__).parents[1] / 'conf' / 'agents.yml', 'r') as fle: + BaseWorld.apply_config('agents', yaml.safe_load(fle)) app_svc = AppService(web.Application(client_max_size=5120 ** 2)) _ = DataService() diff --git a/tests/services/test_rest_svc.py b/tests/services/test_rest_svc.py index 22437cb17..db2b75739 100644 --- a/tests/services/test_rest_svc.py +++ b/tests/services/test_rest_svc.py @@ -88,7 +88,7 @@ def test_delete_operation(self, event_loop, rest_svc, data_svc): 'privilege': 'User', 'proxy_receivers': {}, 'proxy_chain': [], 'origin_link_id': '', 'deadman_enabled': False, 'available_contacts': ['unknown'], 'pending_contact': 'unknown', - 'host_ip_addrs': [], 'upstream_dest': '://None:None'}], + 'host_ip_addrs': [], 'upstream_dest': '://None:None', 'status': 'alive'}], 'visibility': 50, 'autonomous': 1, 'chain': [], 'auto_close': False, 'obfuscator': 'plain-text', 'use_learning_parsers': False, 'group': '', @@ -163,7 +163,7 @@ def test_create_operation(self, event_loop, rest_svc, data_svc): 'display_name': 'unknown$unknown', 'group': 'red', 'location': 'unknown', 'privilege': 'User', 'proxy_receivers': {}, 'proxy_chain': [], 'origin_link_id': '', 'deadman_enabled': False, 'available_contacts': ['unknown'], 'pending_contact': 'unknown', - 'host_ip_addrs': [], 'upstream_dest': '://None:None'}], + 'host_ip_addrs': [], 'upstream_dest': '://None:None', 'status': 'alive'}], 'visibility': 50, 'autonomous': 1, 'chain': [], 'auto_close': False, 'objective': '', 'obfuscator': 'plain-text', 'use_learning_parsers': False} internal_rest_svc = rest_svc(event_loop) diff --git a/tests/web_server/test_core_endpoints.py b/tests/web_server/test_core_endpoints.py index 33798dae8..f77656ee7 100644 --- a/tests/web_server/test_core_endpoints.py +++ b/tests/web_server/test_core_endpoints.py @@ -25,6 +25,8 @@ async def initialize(): BaseWorld.apply_config('main', yaml.safe_load(fle)) with open(Path(__file__).parents[2] / 'conf' / 'payloads.yml', 'r') as fle: BaseWorld.apply_config('payloads', yaml.safe_load(fle)) + with open(Path(__file__).parents[2] / 'conf' / 'agents.yml', 'r') as fle: + BaseWorld.apply_config('agents', yaml.safe_load(fle)) app_svc = AppService(web.Application()) _ = DataService() From 78efe603f83950065376d8417cc5cc1cf2b300ff Mon Sep 17 00:00:00 2001 From: uruwhy <58484522+uruwhy@users.noreply.github.com> Date: Thu, 29 Jan 2026 00:38:13 +0000 Subject: [PATCH 2/6] agent status unit test, style fixes --- app/api/v2/handlers/agent_api.py | 2 +- app/api/v2/managers/agent_api_manager.py | 3 +- app/objects/c_agent.py | 20 +++++-------- tests/api/v2/handlers/test_agents_api.py | 23 +++++++------- tests/objects/test_agent.py | 38 ++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/app/api/v2/handlers/agent_api.py b/app/api/v2/handlers/agent_api.py index cda77e288..9b027fef6 100644 --- a/app/api/v2/handlers/agent_api.py +++ b/app/api/v2/handlers/agent_api.py @@ -64,7 +64,7 @@ async def get_agent_by_id(self, request: web.Request): async def create_agent(self, request: web.Request): agent = await self.create_object(request) return web.json_response(agent.display) - + @aiohttp_apispec.docs(tags=['agents'], summary="Kills an agent", description="Marks an agent to stop after the next beacon.") diff --git a/app/api/v2/managers/agent_api_manager.py b/app/api/v2/managers/agent_api_manager.py index 000a12e1b..b8b145ba7 100644 --- a/app/api/v2/managers/agent_api_manager.py +++ b/app/api/v2/managers/agent_api_manager.py @@ -1,6 +1,7 @@ from app.api.v2.managers.base_api_manager import BaseApiManager from app.api.v2.responses import JsonHttpNotFound + class AgentApiManager(BaseApiManager): def __init__(self, data_svc, file_svc): super().__init__(data_svc=data_svc, file_svc=file_svc) @@ -32,4 +33,4 @@ async def kill_agent(self, target_paw: str): raise JsonHttpNotFound(f'Agent {target_paw} not found.') target = agents[0] await target.kill() - return {'response': 'Ok'} \ No newline at end of file + return {'response': 'Ok'} diff --git a/app/objects/c_agent.py b/app/objects/c_agent.py index ea771f774..9a66dc649 100644 --- a/app/objects/c_agent.py +++ b/app/objects/c_agent.py @@ -87,24 +87,20 @@ def unique(self): @property def display_name(self): return '{}${}'.format(self.host, self.username) - + @property def status(self): now = datetime.now(timezone.utc) + untrusted_buffer = int(self.get_config(name='agents', prop='untrusted_timer')) + time_diff = (now - self.last_seen).total_seconds() + expired = time_diff > int(self.sleep_max) + untrusted_buffer if self._marked_for_stop: # If agent hasn't received the stop instruction yet in a beacon response, it's still pending stop - if self._stop_delivered: - return 'dead' - else: - return 'pending stop' + # Otherwise, if agent has received the stop instruction or takes too long to beacon back, mark as dead + return 'dead' if self._stop_delivered or expired else 'pending stop' else: # If agent hasn't beaconed in since max beacon time + untrusted timer, mark as dead - untrusted_buffer = int(self.get_config(name='agents', prop='untrusted_timer')) - time_diff = (now - self.last_seen).total_seconds() - if time_diff > int(self.sleep_max) + untrusted_buffer: - return 'dead' - else: - return 'alive' + return 'dead' if expired else 'alive' @classmethod def is_global_variable(cls, variable): @@ -235,7 +231,7 @@ async def heartbeat_modification(self, **kwargs): if not self._executor_change_to_assign: # Don't update executors if we're waiting to assign an executor change to the agent. self.update('executors', kwargs.get('executors')) - + # Check if agent has been marked to stop if self._marked_for_stop and not self._stop_delivered: self._stop_delivered = True diff --git a/tests/api/v2/handlers/test_agents_api.py b/tests/api/v2/handlers/test_agents_api.py index a338ad079..2d8542bf2 100644 --- a/tests/api/v2/handlers/test_agents_api.py +++ b/tests/api/v2/handlers/test_agents_api.py @@ -162,26 +162,27 @@ async def test_create_agent(self, api_v2_client, api_cookies, new_agent_payload, async def test_unauthorized_create_agent(self, api_v2_client, new_agent_payload): resp = await api_v2_client.post('/api/v2/agents', json=new_agent_payload) assert resp.status == HTTPStatus.UNAUTHORIZED - + async def test_kill_agent(self, api_v2_client, api_cookies, test_agent, mocker, mock_time): - assert test_agent.watchdog == 0 and test_agent.sleep_min == 2 and test_agent.sleep_max == 8 - assert not (test_agent._marked_for_stop or test_agent._stop_delivered) with mocker.patch('app.objects.c_agent.datetime') as mock_datetime: mock_datetime.return_value = mock_datetime mock_datetime.now.return_value = mock_time + + assert test_agent.watchdog == 0 and test_agent.sleep_min == 2 and test_agent.sleep_max == 8 + assert not (test_agent._marked_for_stop or test_agent._stop_delivered) assert test_agent.status == 'alive' - resp = await api_v2_client.post('/api/v2/agents/kill/123', cookies=api_cookies) - assert resp.status == HTTPStatus.OK - assert {'response': 'Ok'} == await resp.json() - assert test_agent.status == 'pending stop' - assert test_agent._marked_for_stop and not test_agent._stop_delivered - assert test_agent.watchdog == 1 and test_agent.sleep_min == 120 and test_agent.sleep_max == 120 - + resp = await api_v2_client.post('/api/v2/agents/kill/123', cookies=api_cookies) + assert resp.status == HTTPStatus.OK + assert {'response': 'Ok'} == await resp.json() + assert test_agent.status == 'pending stop' + assert test_agent._marked_for_stop and not test_agent._stop_delivered + assert test_agent.watchdog == 1 and test_agent.sleep_min == 120 and test_agent.sleep_max == 120 + async def test_unauthorized_kill_agent(self, api_v2_client): resp = await api_v2_client.post('/api/v2/agents/kill/123') assert resp.status == HTTPStatus.UNAUTHORIZED - + async def test_nonexistent_kill_agent(self, api_v2_client, api_cookies): resp = await api_v2_client.post('/api/v2/agents/kill/999', cookies=api_cookies) assert resp.status == HTTPStatus.NOT_FOUND diff --git a/tests/objects/test_agent.py b/tests/objects/test_agent.py index a414e4585..78e4cea8a 100644 --- a/tests/objects/test_agent.py +++ b/tests/objects/test_agent.py @@ -1,9 +1,11 @@ from base64 import b64decode +from datetime import timedelta from app.objects.c_ability import Ability from app.objects.c_agent import Agent from app.objects.secondclass.c_executor import Executor from app.objects.secondclass.c_fact import Fact +from app.utility.base_world import BaseWorld class TestAgent: @@ -126,6 +128,42 @@ def test_heartbeat_modification_during_pending_executor_removal(self, event_loop event_loop.run_until_complete(agent.heartbeat_modification(executors=original_executors)) assert agent.executors == ['cmd'] + def test_status_and_kill(self, event_loop, mocker, mock_time): + BaseWorld.set_config(name='agents', prop='untrusted_timer', value=30) + agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows') + assert agent.status == 'alive' + event_loop.run_until_complete(agent.kill()) + assert agent.status == 'pending stop' + assert agent.watchdog == 1 and agent.sleep_min == 120 and agent.sleep_max == 120 + event_loop.run_until_complete(agent.heartbeat_modification()) + assert agent.status == 'dead' + + with mocker.patch('app.objects.c_agent.datetime') as mock_datetime: + mock_datetime.return_value = mock_datetime + mock_datetime.now.return_value = mock_time + second_agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows') + event_loop.run_until_complete(second_agent.kill()) + + mock_datetime.now.return_value = mock_time + timedelta(0, 30) + assert second_agent.status == 'pending stop' + mock_datetime.now.return_value = mock_time + timedelta(0, 140) + assert second_agent.status == 'pending stop' + mock_datetime.now.return_value = mock_time + timedelta(0, 151) + assert second_agent.status == 'dead' + + def test_status_and_timeout(self, event_loop, mocker, mock_time): + BaseWorld.set_config(name='agents', prop='untrusted_timer', value=30) + with mocker.patch('app.objects.c_agent.datetime') as mock_datetime: + mock_datetime.return_value = mock_datetime + mock_datetime.now.return_value = mock_time + agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows') + assert agent.status == 'alive' + + mock_datetime.now.return_value = mock_time + timedelta(0, 30) + assert agent.status == 'alive' + mock_datetime.now.return_value = mock_time + timedelta(0, 39) + assert agent.status == 'dead' + def test_store_new_agent(self, data_svc): agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd', 'test'], platform='windows') stored_agent = agent.store(data_svc.ram) From 1604cefe008aa8e01b91a93891ad331726090833 Mon Sep 17 00:00:00 2001 From: uruwhy <58484522+uruwhy@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:23:23 +0000 Subject: [PATCH 3/6] change pending kill status wording --- app/objects/c_agent.py | 2 +- tests/objects/test_agent.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/objects/c_agent.py b/app/objects/c_agent.py index 9a66dc649..6e1bab5d7 100644 --- a/app/objects/c_agent.py +++ b/app/objects/c_agent.py @@ -97,7 +97,7 @@ def status(self): if self._marked_for_stop: # If agent hasn't received the stop instruction yet in a beacon response, it's still pending stop # Otherwise, if agent has received the stop instruction or takes too long to beacon back, mark as dead - return 'dead' if self._stop_delivered or expired else 'pending stop' + return 'dead' if self._stop_delivered or expired else 'pending kill' else: # If agent hasn't beaconed in since max beacon time + untrusted timer, mark as dead return 'dead' if expired else 'alive' diff --git a/tests/objects/test_agent.py b/tests/objects/test_agent.py index 78e4cea8a..191dee87f 100644 --- a/tests/objects/test_agent.py +++ b/tests/objects/test_agent.py @@ -133,7 +133,7 @@ def test_status_and_kill(self, event_loop, mocker, mock_time): agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows') assert agent.status == 'alive' event_loop.run_until_complete(agent.kill()) - assert agent.status == 'pending stop' + assert agent.status == 'pending kill' assert agent.watchdog == 1 and agent.sleep_min == 120 and agent.sleep_max == 120 event_loop.run_until_complete(agent.heartbeat_modification()) assert agent.status == 'dead' @@ -145,9 +145,9 @@ def test_status_and_kill(self, event_loop, mocker, mock_time): event_loop.run_until_complete(second_agent.kill()) mock_datetime.now.return_value = mock_time + timedelta(0, 30) - assert second_agent.status == 'pending stop' + assert second_agent.status == 'pending kill' mock_datetime.now.return_value = mock_time + timedelta(0, 140) - assert second_agent.status == 'pending stop' + assert second_agent.status == 'pending kill' mock_datetime.now.return_value = mock_time + timedelta(0, 151) assert second_agent.status == 'dead' From 03698bd7685b5348a9513255ddcfb1332648813b Mon Sep 17 00:00:00 2001 From: uruwhy <58484522+uruwhy@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:43:32 +0000 Subject: [PATCH 4/6] change sleep time for killing agent --- app/objects/c_agent.py | 4 ++-- tests/api/v2/handlers/test_agents_api.py | 4 ++-- tests/objects/test_agent.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/objects/c_agent.py b/app/objects/c_agent.py index 6e1bab5d7..1fe8457bb 100644 --- a/app/objects/c_agent.py +++ b/app/objects/c_agent.py @@ -243,8 +243,8 @@ async def gui_modification(self, **kwargs): async def kill(self): self.update('watchdog', 1) - self.update('sleep_min', 60 * 2) - self.update('sleep_max', 60 * 2) + self.update('sleep_min', 3) + self.update('sleep_max', 3) self._marked_for_stop = True self._stop_delivered = False diff --git a/tests/api/v2/handlers/test_agents_api.py b/tests/api/v2/handlers/test_agents_api.py index 2d8542bf2..5bb47d656 100644 --- a/tests/api/v2/handlers/test_agents_api.py +++ b/tests/api/v2/handlers/test_agents_api.py @@ -175,9 +175,9 @@ async def test_kill_agent(self, api_v2_client, api_cookies, test_agent, mocker, resp = await api_v2_client.post('/api/v2/agents/kill/123', cookies=api_cookies) assert resp.status == HTTPStatus.OK assert {'response': 'Ok'} == await resp.json() - assert test_agent.status == 'pending stop' + assert test_agent.status == 'pending kill' assert test_agent._marked_for_stop and not test_agent._stop_delivered - assert test_agent.watchdog == 1 and test_agent.sleep_min == 120 and test_agent.sleep_max == 120 + assert test_agent.watchdog == 1 and test_agent.sleep_min == 3 and test_agent.sleep_max == 3 async def test_unauthorized_kill_agent(self, api_v2_client): resp = await api_v2_client.post('/api/v2/agents/kill/123') diff --git a/tests/objects/test_agent.py b/tests/objects/test_agent.py index 191dee87f..5014a80ee 100644 --- a/tests/objects/test_agent.py +++ b/tests/objects/test_agent.py @@ -134,7 +134,7 @@ def test_status_and_kill(self, event_loop, mocker, mock_time): assert agent.status == 'alive' event_loop.run_until_complete(agent.kill()) assert agent.status == 'pending kill' - assert agent.watchdog == 1 and agent.sleep_min == 120 and agent.sleep_max == 120 + assert agent.watchdog == 1 and agent.sleep_min == 3 and agent.sleep_max == 3 event_loop.run_until_complete(agent.heartbeat_modification()) assert agent.status == 'dead' From 90565b8fefc41cfad05949ef84f3e90e1a079538 Mon Sep 17 00:00:00 2001 From: uruwhy <58484522+uruwhy@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:59:49 +0000 Subject: [PATCH 5/6] update unit tests with new sleep times --- tests/objects/test_agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/objects/test_agent.py b/tests/objects/test_agent.py index 5014a80ee..220ffd52d 100644 --- a/tests/objects/test_agent.py +++ b/tests/objects/test_agent.py @@ -144,11 +144,11 @@ def test_status_and_kill(self, event_loop, mocker, mock_time): second_agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows') event_loop.run_until_complete(second_agent.kill()) - mock_datetime.now.return_value = mock_time + timedelta(0, 30) + mock_datetime.now.return_value = mock_time + timedelta(0, 10) assert second_agent.status == 'pending kill' - mock_datetime.now.return_value = mock_time + timedelta(0, 140) + mock_datetime.now.return_value = mock_time + timedelta(0, 32) assert second_agent.status == 'pending kill' - mock_datetime.now.return_value = mock_time + timedelta(0, 151) + mock_datetime.now.return_value = mock_time + timedelta(0, 34) assert second_agent.status == 'dead' def test_status_and_timeout(self, event_loop, mocker, mock_time): From b59e719d6567d09c4da9b5d977c5d79f6bac0dbd Mon Sep 17 00:00:00 2001 From: Daniel Matthews <58484522+uruwhy@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:00:50 -0500 Subject: [PATCH 6/6] undo unintended edit --- app/api/v2/handlers/agent_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/v2/handlers/agent_api.py b/app/api/v2/handlers/agent_api.py index 9b027fef6..4b3d8d30d 100644 --- a/app/api/v2/handlers/agent_api.py +++ b/app/api/v2/handlers/agent_api.py @@ -59,7 +59,7 @@ async def get_agent_by_id(self, request: web.Request): @aiohttp_apispec.docs(tags=['agents'], summary="Create a new agent", description="Creates a new agent using the format from 'AgentSchema'.") - @aiohttp_apispec.request_schema(AgentSchema(partial=True)) + @aiohttp_apispec.request_schema(AgentSchema) @aiohttp_apispec.response_schema(AgentSchema, description="Returns a single agent in 'AgentSchema' format") async def create_agent(self, request: web.Request): agent = await self.create_object(request)