Skip to content

Commit 24b3be6

Browse files
authored
Merge branch 'master' into docker-compose
2 parents 2ac36d0 + fc70ba9 commit 24b3be6

File tree

9 files changed

+119
-5
lines changed

9 files changed

+119
-5
lines changed

app/api/rest_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ def __init__(self, services):
2929
asyncio.get_event_loop().create_task(AdvancedPack(services).enable())
3030

3131
async def enable(self):
32-
self.app_svc.application.router.add_static('/assets', 'plugins/magma/dist/assets/', append_version=True)
32+
# check if plugin path is present
33+
if os.path.exists("plugins/magma/dist/assets") and (len(os.listdir("plugins/magma/dist/assets")) > 0):
34+
self.app_svc.application.router.add_static('/assets', 'plugins/magma/dist/assets/', append_version=True)
3335
# TODO: only serve static files in legacy plugin mode
3436
self.app_svc.application.router.add_static('/gui', 'static/', append_version=True)
3537
# unauthorized GUI endpoints

app/api/v2/handlers/agent_api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def add_routes(self, app: web.Application):
1919
router.add_get('/agents', self.get_agents)
2020
router.add_get('/agents/{paw}', self.get_agent_by_id)
2121
router.add_post('/agents', self.create_agent)
22+
router.add_post('/agents/kill/{paw}', self.kill_agent)
2223
router.add_patch('/agents/{paw}', self.update_agent)
2324
router.add_put('/agents/{paw}', self.create_or_update_agent)
2425
router.add_delete('/agents/{paw}', self.delete_agent)
@@ -64,6 +65,14 @@ async def create_agent(self, request: web.Request):
6465
agent = await self.create_object(request)
6566
return web.json_response(agent.display)
6667

68+
@aiohttp_apispec.docs(tags=['agents'],
69+
summary="Kills an agent",
70+
description="Marks an agent to stop after the next beacon.")
71+
async def kill_agent(self, request: web.Request):
72+
target_paw = request.match_info.get('paw')
73+
kill_resp = await self._api_manager.kill_agent(target_paw)
74+
return web.json_response(kill_resp)
75+
6776
@aiohttp_apispec.docs(tags=['agents'],
6877
summary="Update an Agent",
6978
description="Update the attributes of a specific Agent using its ID (paw). Use the paw "

app/api/v2/managers/agent_api_manager.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from app.api.v2.managers.base_api_manager import BaseApiManager
2+
from app.api.v2.responses import JsonHttpNotFound
23

34

45
class AgentApiManager(BaseApiManager):
@@ -25,3 +26,11 @@ async def get_deploy_commands(self, ability_id: str = None):
2526
app_config.update({f'agents.{k}': v for k, v in self.get_config(name='agents').items()})
2627

2728
return dict(abilities=raw_abilities, app_config=app_config)
29+
30+
async def kill_agent(self, target_paw: str):
31+
agents = await self._data_svc.locate('agents', {'paw': target_paw})
32+
if not agents:
33+
raise JsonHttpNotFound(f'Agent {target_paw} not found.')
34+
target = agents[0]
35+
await target.kill()
36+
return {'response': 'Ok'}

app/objects/c_agent.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class AgentFieldsSchema(ma.Schema):
4848
links = ma.fields.List(ma.fields.Nested(LinkSchema), dump_only=True)
4949
pending_contact = ma.fields.String()
5050

51+
status = ma.fields.String(dump_only=True)
52+
5153
@ma.pre_load
5254
def remove_nulls(self, in_data, **_):
5355
return {k: v for k, v in in_data.items() if v is not None}
@@ -58,6 +60,7 @@ def remove_properties(self, data, **_):
5860
data.pop('created', None)
5961
data.pop('last_seen', None)
6062
data.pop('links', None)
63+
data.pop('status', None)
6164
return data
6265

6366

@@ -85,6 +88,20 @@ def unique(self):
8588
def display_name(self):
8689
return '{}${}'.format(self.host, self.username)
8790

91+
@property
92+
def status(self):
93+
now = datetime.now(timezone.utc)
94+
untrusted_buffer = int(self.get_config(name='agents', prop='untrusted_timer'))
95+
time_diff = (now - self.last_seen).total_seconds()
96+
expired = time_diff > int(self.sleep_max) + untrusted_buffer
97+
if self._marked_for_stop:
98+
# If agent hasn't received the stop instruction yet in a beacon response, it's still pending stop
99+
# Otherwise, if agent has received the stop instruction or takes too long to beacon back, mark as dead
100+
return 'dead' if self._stop_delivered or expired else 'pending kill'
101+
else:
102+
# If agent hasn't beaconed in since max beacon time + untrusted timer, mark as dead
103+
return 'dead' if expired else 'alive'
104+
88105
@classmethod
89106
def is_global_variable(cls, variable):
90107
if variable.startswith('payload:'):
@@ -139,6 +156,8 @@ def __init__(self, sleep_min=30, sleep_max=60, watchdog=0, platform='unknown', s
139156
self.upstream_dest = self.server
140157
self._executor_change_to_assign = None
141158
self.log = self.create_logger('agent')
159+
self._marked_for_stop = False
160+
self._stop_delivered = False
142161

143162
def store(self, ram):
144163
existing = self.retrieve(ram['agents'], self.unique)
@@ -213,15 +232,22 @@ async def heartbeat_modification(self, **kwargs):
213232
# Don't update executors if we're waiting to assign an executor change to the agent.
214233
self.update('executors', kwargs.get('executors'))
215234

235+
# Check if agent has been marked to stop
236+
if self._marked_for_stop and not self._stop_delivered:
237+
self._stop_delivered = True
238+
216239
async def gui_modification(self, **kwargs):
217240
loaded = AgentFieldsSchema(only=('group', 'trusted', 'sleep_min', 'sleep_max', 'watchdog', 'pending_contact')).load(kwargs)
218241
for k, v in loaded.items():
219242
self.update(k, v)
220243

221244
async def kill(self):
222245
self.update('watchdog', 1)
223-
self.update('sleep_min', 60 * 2)
224-
self.update('sleep_max', 60 * 2)
246+
self.update('sleep_min', 3)
247+
self.update('sleep_max', 3)
248+
249+
self._marked_for_stop = True
250+
self._stop_delivered = False
225251

226252
def replace(self, encoded_cmd, file_svc):
227253
decoded_cmd = b64decode(encoded_cmd).decode('utf-8', errors='ignore').replace('\n', '')

tests/api/v2/handlers/test_agents_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,30 @@ async def test_unauthorized_create_agent(self, api_v2_client, new_agent_payload)
163163
resp = await api_v2_client.post('/api/v2/agents', json=new_agent_payload)
164164
assert resp.status == HTTPStatus.UNAUTHORIZED
165165

166+
async def test_kill_agent(self, api_v2_client, api_cookies, test_agent, mocker, mock_time):
167+
with mocker.patch('app.objects.c_agent.datetime') as mock_datetime:
168+
mock_datetime.return_value = mock_datetime
169+
mock_datetime.now.return_value = mock_time
170+
171+
assert test_agent.watchdog == 0 and test_agent.sleep_min == 2 and test_agent.sleep_max == 8
172+
assert not (test_agent._marked_for_stop or test_agent._stop_delivered)
173+
assert test_agent.status == 'alive'
174+
175+
resp = await api_v2_client.post('/api/v2/agents/kill/123', cookies=api_cookies)
176+
assert resp.status == HTTPStatus.OK
177+
assert {'response': 'Ok'} == await resp.json()
178+
assert test_agent.status == 'pending kill'
179+
assert test_agent._marked_for_stop and not test_agent._stop_delivered
180+
assert test_agent.watchdog == 1 and test_agent.sleep_min == 3 and test_agent.sleep_max == 3
181+
182+
async def test_unauthorized_kill_agent(self, api_v2_client):
183+
resp = await api_v2_client.post('/api/v2/agents/kill/123')
184+
assert resp.status == HTTPStatus.UNAUTHORIZED
185+
186+
async def test_nonexistent_kill_agent(self, api_v2_client, api_cookies):
187+
resp = await api_v2_client.post('/api/v2/agents/kill/999', cookies=api_cookies)
188+
assert resp.status == HTTPStatus.NOT_FOUND
189+
166190
async def test_update_agent(self, api_v2_client, api_cookies, test_agent, updated_agent_fields_payload,
167191
expected_updated_agent_dump):
168192
resp = await api_v2_client.patch('/api/v2/agents/123', cookies=api_cookies, json=updated_agent_fields_payload)

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ async def initialize():
366366
BaseWorld.apply_config('main', yaml.safe_load(fle))
367367
with open(Path(__file__).parents[1] / 'conf' / 'payloads.yml', 'r') as fle:
368368
BaseWorld.apply_config('payloads', yaml.safe_load(fle))
369+
with open(Path(__file__).parents[1] / 'conf' / 'agents.yml', 'r') as fle:
370+
BaseWorld.apply_config('agents', yaml.safe_load(fle))
369371

370372
app_svc = AppService(web.Application(client_max_size=5120 ** 2))
371373
_ = DataService()

tests/objects/test_agent.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from base64 import b64decode
2+
from datetime import timedelta
23

34
from app.objects.c_ability import Ability
45
from app.objects.c_agent import Agent
56
from app.objects.secondclass.c_executor import Executor
67
from app.objects.secondclass.c_fact import Fact
8+
from app.utility.base_world import BaseWorld
79

810

911
class TestAgent:
@@ -126,6 +128,42 @@ def test_heartbeat_modification_during_pending_executor_removal(self, event_loop
126128
event_loop.run_until_complete(agent.heartbeat_modification(executors=original_executors))
127129
assert agent.executors == ['cmd']
128130

131+
def test_status_and_kill(self, event_loop, mocker, mock_time):
132+
BaseWorld.set_config(name='agents', prop='untrusted_timer', value=30)
133+
agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows')
134+
assert agent.status == 'alive'
135+
event_loop.run_until_complete(agent.kill())
136+
assert agent.status == 'pending kill'
137+
assert agent.watchdog == 1 and agent.sleep_min == 3 and agent.sleep_max == 3
138+
event_loop.run_until_complete(agent.heartbeat_modification())
139+
assert agent.status == 'dead'
140+
141+
with mocker.patch('app.objects.c_agent.datetime') as mock_datetime:
142+
mock_datetime.return_value = mock_datetime
143+
mock_datetime.now.return_value = mock_time
144+
second_agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows')
145+
event_loop.run_until_complete(second_agent.kill())
146+
147+
mock_datetime.now.return_value = mock_time + timedelta(0, 10)
148+
assert second_agent.status == 'pending kill'
149+
mock_datetime.now.return_value = mock_time + timedelta(0, 32)
150+
assert second_agent.status == 'pending kill'
151+
mock_datetime.now.return_value = mock_time + timedelta(0, 34)
152+
assert second_agent.status == 'dead'
153+
154+
def test_status_and_timeout(self, event_loop, mocker, mock_time):
155+
BaseWorld.set_config(name='agents', prop='untrusted_timer', value=30)
156+
with mocker.patch('app.objects.c_agent.datetime') as mock_datetime:
157+
mock_datetime.return_value = mock_datetime
158+
mock_datetime.now.return_value = mock_time
159+
agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd'], platform='windows')
160+
assert agent.status == 'alive'
161+
162+
mock_datetime.now.return_value = mock_time + timedelta(0, 30)
163+
assert agent.status == 'alive'
164+
mock_datetime.now.return_value = mock_time + timedelta(0, 39)
165+
assert agent.status == 'dead'
166+
129167
def test_store_new_agent(self, data_svc):
130168
agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['cmd', 'test'], platform='windows')
131169
stored_agent = agent.store(data_svc.ram)

tests/services/test_rest_svc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_delete_operation(self, event_loop, rest_svc, data_svc):
8888
'privilege': 'User', 'proxy_receivers': {}, 'proxy_chain': [],
8989
'origin_link_id': '', 'deadman_enabled': False,
9090
'available_contacts': ['unknown'], 'pending_contact': 'unknown',
91-
'host_ip_addrs': [], 'upstream_dest': '://None:None'}],
91+
'host_ip_addrs': [], 'upstream_dest': '://None:None', 'status': 'alive'}],
9292
'visibility': 50, 'autonomous': 1, 'chain': [], 'auto_close': False,
9393
'obfuscator': 'plain-text', 'use_learning_parsers': False,
9494
'group': '',
@@ -163,7 +163,7 @@ def test_create_operation(self, event_loop, rest_svc, data_svc):
163163
'display_name': 'unknown$unknown', 'group': 'red', 'location': 'unknown', 'privilege': 'User',
164164
'proxy_receivers': {}, 'proxy_chain': [], 'origin_link_id': '',
165165
'deadman_enabled': False, 'available_contacts': ['unknown'], 'pending_contact': 'unknown',
166-
'host_ip_addrs': [], 'upstream_dest': '://None:None'}],
166+
'host_ip_addrs': [], 'upstream_dest': '://None:None', 'status': 'alive'}],
167167
'visibility': 50, 'autonomous': 1, 'chain': [], 'auto_close': False, 'objective': '',
168168
'obfuscator': 'plain-text', 'use_learning_parsers': False}
169169
internal_rest_svc = rest_svc(event_loop)

tests/web_server/test_core_endpoints.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ async def initialize():
2525
BaseWorld.apply_config('main', yaml.safe_load(fle))
2626
with open(Path(__file__).parents[2] / 'conf' / 'payloads.yml', 'r') as fle:
2727
BaseWorld.apply_config('payloads', yaml.safe_load(fle))
28+
with open(Path(__file__).parents[2] / 'conf' / 'agents.yml', 'r') as fle:
29+
BaseWorld.apply_config('agents', yaml.safe_load(fle))
2830

2931
app_svc = AppService(web.Application())
3032
_ = DataService()
@@ -63,6 +65,8 @@ async def sample_agent(aiohttp_client):
6365

6466

6567
async def test_home(aiohttp_client):
68+
if not (len(os.listdir("plugins/magma/dist/assets")) > 0):
69+
pytest.xfail("Magma plugin not present, expecting failure")
6670
resp = await aiohttp_client.get('/')
6771
assert resp.status == HTTPStatus.OK
6872
assert resp.content_type == 'text/html'

0 commit comments

Comments
 (0)