Skip to content

Commit 1a19420

Browse files
uruwhyclenk
andauthored
Addressing some unit test warnings (#3222)
* fix asyncmock and atomic planner test * adjust TCP contact * address some warnings * rework ftp contact * remove unused imports * switch to start method to fix error in unit tests * use non-breaking pyasn1 version to address unit test warning * handle task cancelation * different method of suppressing exception * more logging * use testing dir for ftp contact unit tests * close TCP sessions during contact close * style fix * refactor * handle possible null server/tasks * address marshmallow deprecated meta.ordered message --------- Co-authored-by: Chris Lenk <clenk@users.noreply.github.com>
1 parent 0f2fca5 commit 1a19420

File tree

9 files changed

+75
-54
lines changed

9 files changed

+75
-54
lines changed

app/api/v2/schemas/caldera_info_schemas.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,3 @@ class CalderaInfoSchema(schema.Schema):
99
version = fields.String()
1010
access = fields.String()
1111
plugins = fields.List(fields.Nested(Plugin.display_schema))
12-
13-
class Meta:
14-
ordered = True

app/api/v2/schemas/error_schemas.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ class JsonHttpErrorSchema(schema.Schema):
66
error = fields.String(required=True)
77
details = fields.Dict()
88

9-
class Meta:
10-
ordered = True
11-
129
@classmethod
1310
def make_dict(cls, error, details=None):
1411
obj = {'error': error}

app/contacts/contact_ftp.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import re
44
import asyncio
55
import aioftp
6-
import sys
76

87
from app.utility.base_world import BaseWorld
98

@@ -39,18 +38,14 @@ def __init__(self, services):
3938
self.user = self.get_config('app.contact.ftp.user')
4039
self.pword = self.get_config('app.contact.ftp.pword')
4140
self.server = None
42-
self.task = None
4341

4442
async def start(self):
4543
self.set_up_server()
46-
if sys.version_info >= (3, 7):
47-
self.task = asyncio.create_task(self.ftp_server_python_new())
48-
else:
49-
self.task = asyncio.create_task(self.ftp_server_python_old())
50-
await self.task
44+
await self.ftp_server()
5145

5246
async def stop(self):
53-
self.task.cancel()
47+
if self.server:
48+
await self.server.close()
5449

5550
def set_up_server(self):
5651
user = self.setup_ftp_users()
@@ -89,12 +84,9 @@ def setup_ftp_users(self):
8984
),
9085
)
9186

92-
async def ftp_server_python_old(self):
87+
async def ftp_server(self):
9388
await self.server.start(host=self.host, port=self.port)
9489

95-
async def ftp_server_python_new(self):
96-
await self.server.run(host=self.host, port=self.port)
97-
9890
def check_config(self):
9991
if not self.get_config(FTP_HOST_PROPERTY):
10092
self.set_config('main', FTP_HOST_PROPERTY, FTP_HOST_DEFAULT)

app/contacts/contact_tcp.py

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,72 @@ def __init__(self, services):
1717
self.log = self.create_logger('contact_tcp')
1818
self.contact_svc = services.get('contact_svc')
1919
self.tcp_handler = TcpSessionHandler(services, self.log)
20+
self.server_task = None
21+
self.op_loop_task = None
22+
self.server = None
2023

2124
async def start(self):
2225
loop = asyncio.get_event_loop()
2326
tcp = self.get_config('app.contact.tcp')
24-
loop.create_task(asyncio.start_server(self.tcp_handler.accept, *tcp.split(':')))
25-
loop.create_task(self.operation_loop())
27+
self.server_task = loop.create_task(self.start_server(*tcp.split(':')))
28+
self.op_loop_task = loop.create_task(self.operation_loop())
29+
30+
async def stop(self):
31+
tasks_to_stop = [t for t in (self.server_task, self.op_loop_task) if t is not None]
32+
for t in tasks_to_stop:
33+
if t:
34+
t.cancel()
35+
if tasks_to_stop:
36+
_ = await asyncio.gather(*tasks_to_stop, return_exceptions=True)
37+
38+
async def start_server(self, host, port):
39+
try:
40+
self.server = await asyncio.start_server(self.tcp_handler.accept, host, port)
41+
async with self.server:
42+
await self.server.serve_forever()
43+
except asyncio.CancelledError:
44+
self.log.debug('Canceling TCP contact server task.')
45+
if self.server:
46+
self.log.debug('Closing TCP contact server.')
47+
self.server.close()
48+
await self.server.wait_closed()
49+
self.log.debug('Closed TCP contact server.')
50+
raise
2651

2752
async def operation_loop(self):
28-
while True:
29-
await self.tcp_handler.refresh()
30-
for session in self.tcp_handler.sessions:
31-
_, instructions = await self.contact_svc.handle_heartbeat(paw=session.paw)
32-
for instruction in instructions:
33-
try:
34-
self.log.debug('TCP instruction: %s' % instruction.id)
35-
status, _, response, agent_reported_time = await self.tcp_handler.send(
36-
session.id,
37-
self.decode_bytes(instruction.command),
38-
timeout=instruction.timeout
39-
)
40-
beacon = dict(paw=session.paw,
41-
results=[dict(id=instruction.id, output=self.encode_string(response), status=status, agent_reported_time=agent_reported_time)])
42-
await self.contact_svc.handle_heartbeat(**beacon)
43-
await asyncio.sleep(instruction.sleep)
44-
except Exception as e:
45-
self.log.debug('[-] operation exception: %s' % e)
46-
await asyncio.sleep(20)
53+
try:
54+
while True:
55+
await self.tcp_handler.refresh()
56+
await self.handle_sessions()
57+
await asyncio.sleep(20)
58+
except asyncio.CancelledError:
59+
self.log.debug('Canceling TCP contact operation loop task.')
60+
for sess in self.tcp_handler.sessions:
61+
self.log.debug(f'Closing session {sess.id}.')
62+
sess.writer.close()
63+
await sess.writer.wait_closed()
64+
self.log.debug('Closed TCP contact sessions.')
65+
raise
66+
67+
async def handle_sessions(self):
68+
for session in self.tcp_handler.sessions:
69+
_, instructions = await self.contact_svc.handle_heartbeat(paw=session.paw)
70+
for instruction in instructions:
71+
try:
72+
self.log.debug('TCP instruction: %s' % instruction.id)
73+
status, _, response, agent_reported_time = await self.tcp_handler.send(
74+
session.id,
75+
self.decode_bytes(instruction.command),
76+
timeout=instruction.timeout
77+
)
78+
beacon = dict(paw=session.paw,
79+
results=[dict(id=instruction.id, output=self.encode_string(response), status=status, agent_reported_time=agent_reported_time)])
80+
await self.contact_svc.handle_heartbeat(**beacon)
81+
await asyncio.sleep(instruction.sleep)
82+
except asyncio.CancelledError:
83+
raise
84+
except Exception as e:
85+
self.log.debug('[-] operation exception: %s' % e)
4786

4887

4988
class TcpSessionHandler(BaseWorld):
@@ -68,6 +107,7 @@ async def refresh(self):
68107
index += 1
69108

70109
async def accept(self, reader, writer):
110+
self.log.debug('Accepting connection.')
71111
try:
72112
profile = await self._handshake(reader)
73113
except Exception as e:

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ marshmallow==3.26.2
1414
dirhash==0.2.1
1515
marshmallow-enum==1.5.1
1616
ldap3==2.9.1
17+
pyasn1~=0.5.1
1718
reportlab==4.0.4 # debrief
1819
rich==13.7.0
1920
lxml==6.0.2 # debrief

tests/api/v2/test_knowledge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def base_world():
4242

4343

4444
@pytest.fixture
45-
async def knowledge_webapp(event_loop, base_world, data_svc):
45+
async def knowledge_webapp(base_world, data_svc):
4646
app_svc = AppService(web.Application())
4747
app_svc.add_service('auth_svc', AuthService())
4848
app_svc.add_service('knowledge_svc', KnowledgeService())

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def agent_config():
332332

333333

334334
@pytest.fixture
335-
async def api_v2_client(event_loop, aiohttp_client, contact_svc):
335+
async def api_v2_client(aiohttp_client, contact_svc):
336336
def make_app(svcs):
337337
warnings.filterwarnings(
338338
"ignore",

tests/contacts/test_contact_ftp.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import os
3+
import shutil
34

45
from app.contacts import contact_ftp
56
from app.utility.base_world import BaseWorld
@@ -26,7 +27,7 @@ def base_world():
2627
BaseWorld.apply_config(name='main', config={'app.contact.ftp.host': '0.0.0.0',
2728
'app.contact.ftp.port': '2222',
2829
'app.contact.ftp.pword': 'caldera',
29-
'app.contact.ftp.server.dir': 'ftp_dir',
30+
'app.contact.ftp.server.dir': 'ftp_dir_testing',
3031
'app.contact.ftp.user': 'caldera_user',
3132
'plugins': ['sandcat', 'stockpile'],
3233
'crypt_salt': 'BLAH',
@@ -64,7 +65,7 @@ def test_server_setup(ftp_c2):
6465
assert ftp_c2.description == 'Accept agent beacons through ftp'
6566
assert ftp_c2.host == '0.0.0.0'
6667
assert ftp_c2.port == '2222'
67-
assert ftp_c2.directory == 'ftp_dir'
68+
assert ftp_c2.directory == 'ftp_dir_testing'
6869
assert ftp_c2.user == 'caldera_user'
6970
assert ftp_c2.pword == 'caldera'
7071
assert ftp_c2.server is None
@@ -80,6 +81,6 @@ def test_my_server_setup(ftp_c2_my_server):
8081
assert ftp_c2_my_server.port == '2222'
8182
assert ftp_c2_my_server.login == 'caldera_user'
8283
assert ftp_c2_my_server.pword == 'caldera'
83-
assert ftp_c2_my_server.ftp_server_dir == os.path.join(os.getcwd(), 'ftp_dir')
84+
assert ftp_c2_my_server.ftp_server_dir == os.path.join(os.getcwd(), 'ftp_dir_testing')
8485
assert os.path.exists(ftp_c2_my_server.ftp_server_dir)
85-
os.rmdir(ftp_c2_my_server.ftp_server_dir)
86+
shutil.rmtree(ftp_c2_my_server.ftp_server_dir)

tests/services/test_data_svc.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import asyncio
21
import glob
32
import json
43
import yaml
@@ -81,12 +80,6 @@ def strip_payload_yaml(path):
8180
return PAYLOAD_CONFIG_YAMLS.get(path, [])
8281

8382

84-
def async_mock_return(to_return):
85-
mock_future = asyncio.Future()
86-
mock_future.set_result(to_return)
87-
return mock_future
88-
89-
9083
class TestDataService:
9184
mock_payload_config = dict()
9285

@@ -174,8 +167,8 @@ def test_no_autogen_cleanup_cmds(self, event_loop, data_svc):
174167
assert not executor.cleanup
175168

176169
@mock.patch.object(BaseWorld, 'strip_yml', wraps=strip_payload_yaml)
177-
@mock.patch.object(DataService, '_apply_special_payload_hooks', return_value=async_mock_return(None))
178-
@mock.patch.object(DataService, '_apply_special_extension_hooks', return_value=async_mock_return(None))
170+
@mock.patch.object(DataService, '_apply_special_payload_hooks', return_value=None)
171+
@mock.patch.object(DataService, '_apply_special_extension_hooks', return_value=None)
179172
def test_load_payloads(self, mock_ext_hooks, mock_payload_hooks, mock_strip_yml, event_loop, data_svc):
180173
def _mock_apply_payload_config(config=None, **_):
181174
TestDataService.mock_payload_config = config

0 commit comments

Comments
 (0)