blob: 13400c18afaf0ca1493f8a9b2e685f1db271f9ef [file]
import base64
import json
import os
from base64 import b64encode
from unittest import mock
from unittest.mock import MagicMock
import pytest
from app.objects.c_operation import Operation
from app.objects.secondclass.c_link import Link
from app.service.interfaces.i_event_svc import EventServiceInterface
from app.service.interfaces.i_planning_svc import PlanningServiceInterface
from app.utility.base_service import BaseService
from app.objects.c_source import Source
from app.objects.c_planner import Planner
from app.objects.c_objective import Objective
from app.objects.secondclass.c_result import Result
from app.objects.secondclass.c_fact import Fact
from app.utility.base_object import BaseObject
from app.utility.base_world import BaseWorld
LINK1_DECIDE_TIME = MOCK_LINK_FINISH_TIME = '2021-01-01T08:00:00Z'
LINK1_COLLECT_TIME = '2021-01-01T08:01:00Z'
LINK1_FINISH_TIME = '2021-01-01T08:02:00Z'
LINK2_DECIDE_TIME = OP_START_TIME = '2021-01-01T09:00:00Z'
LINK2_COLLECT_TIME = '2021-01-01T09:01:00Z'
LINK2_FINISH_TIME = '2021-01-01T09:02:00Z'
@pytest.fixture
def operation_agent(agent):
return agent(sleep_min=30, sleep_max=60, watchdog=0, platform='windows', host='WORKSTATION',
username='testagent', architecture='amd64', group='red', location=r'C:\Users\Public\test.exe',
pid=1234, ppid=123, executors=['psh'], privilege='User', exe_name='test.exe', contact='unknown',
paw='testpaw')
@pytest.fixture
def untrusted_operation_agent(operation_agent):
agent = operation_agent
agent.trusted = False
return agent
@pytest.fixture
def op_agent_creation_time(operation_agent):
return operation_agent.created.strftime(BaseObject.TIME_FORMAT)
@pytest.fixture
def operation_adversary(adversary):
return adversary(adversary_id='123', name='test adversary', description='test adversary desc')
@pytest.fixture
def operation_link():
def _generate_link(command, plaintext_command, paw, ability, executor, pid=0, decide=None, collect=None, finish=None, **kwargs):
generated_link = Link(command, plaintext_command, paw, ability, executor, **kwargs)
generated_link.pid = pid
if decide:
generated_link.decide = decide
if collect:
generated_link.collect = collect
if finish:
generated_link.finish = finish
return generated_link
return _generate_link
@pytest.fixture
def encoded_command():
def _encode_command(command_str):
return b64encode(command_str.encode('utf-8')).decode()
return _encode_command
@pytest.fixture
def setup_op_config():
BaseWorld.apply_config(name='main', config={'exfil_dir': '/tmp/caldera',
'reports_dir': '/tmp'})
@pytest.fixture
def op_for_event_logs(operation_agent, operation_adversary, executor, ability, operation_link, encoded_command,
parse_datestring):
op = Operation(name='test', agents=[operation_agent], adversary=operation_adversary)
op.set_start_details()
command_1 = 'whoami'
command_2 = 'hostname'
executor_1 = executor(name='psh', platform='windows', command=command_1)
executor_2 = executor(name='psh', platform='windows', command=command_2)
ability_1 = ability(ability_id='123', tactic='test tactic', technique_id='T0000', technique_name='test technique',
name='test ability', description='test ability desc', executors=[executor_1])
ability_2 = ability(ability_id='456', tactic='test tactic', technique_id='T0000', technique_name='test technique',
name='test ability 2', description='test ability 2 desc', executors=[executor_2])
link_1 = operation_link(ability=ability_1, paw=operation_agent.paw, executor=executor_1,
command=encoded_command(command_1), plaintext_command=encoded_command(command_1), status=0, host=operation_agent.host, pid=789,
decide=parse_datestring(LINK1_DECIDE_TIME),
collect=parse_datestring(LINK1_COLLECT_TIME),
finish=LINK1_FINISH_TIME)
link_2 = operation_link(ability=ability_2, paw=operation_agent.paw, executor=executor_2,
command=encoded_command(command_2), plaintext_command=encoded_command(command_2), status=0, host=operation_agent.host, pid=7890,
decide=parse_datestring(LINK2_DECIDE_TIME),
collect=parse_datestring(LINK2_COLLECT_TIME),
finish=LINK2_FINISH_TIME)
discarded_link = operation_link(ability=ability_2, paw=operation_agent.paw, executor=executor_2,
command=encoded_command(command_2), plaintext_command=encoded_command(command_2), status=-2, host=operation_agent.host, pid=7891,
decide=parse_datestring('2021-01-01T10:00:00Z'))
op.chain = [link_1, link_2, discarded_link]
return op
@pytest.fixture
def event_log_op_start_time(op_for_event_logs):
return op_for_event_logs.start.strftime(BaseObject.TIME_FORMAT)
@pytest.fixture
def fake_event_svc(event_loop):
class FakeEventService(BaseService, EventServiceInterface):
def __init__(self):
self.fired = {}
def reset(self):
self.fired = {}
async def observe_event(self, callback, exchange=None, queue=None):
pass
async def fire_event(self, exchange=None, queue=None, timestamp=True, **callback_kwargs):
self.fired[exchange, queue] = callback_kwargs
service = FakeEventService()
service.add_service('event_svc', service)
yield service
BaseService.remove_service('event_svc')
@pytest.fixture
def fake_planning_svc(event_loop, make_test_link, test_agent):
class FakePlanningService(BaseService, PlanningServiceInterface):
def __init__(self):
self.fired = {}
async def get_cleanup_links(self, operation, agent):
cleanup_link = make_test_link(135, link_paw=test_agent.paw, link_cleanup=1, link_status=0)
return [cleanup_link]
def get_links(self, operation, buckets, agent, trim):
pass
def generate_and_trim_links(self, agent, operation, abilities, trim):
pass
def sort_links(self, links):
pass
service = FakePlanningService()
service.add_service('planning_svc', service)
yield service
BaseService.remove_service('planning_svc')
@pytest.fixture
def test_ability(ability, executor):
return ability(ability_id='123', executors=[executor(name='psh', platform='windows')])
@pytest.fixture
def make_test_link(test_ability):
def _make_link(link_id, link_paw='123456', link_status=-3, link_cleanup=0):
return Link(command='', paw=link_paw, ability=test_ability, id=link_id, executor=next(test_ability.executors),
status=link_status, cleanup=link_cleanup)
return _make_link
@pytest.fixture
def make_test_result():
def _make_result(link_id):
result = dict(
id=link_id,
output=str(base64.b64encode('10.10.10.10'.encode('utf-8')).decode('utf-8')),
pid=0,
status=0
)
return Result(**result)
return _make_result
@pytest.fixture
def op_with_learning_parser(ability, adversary):
op = Operation(id='12345', name='testA', agents=[], adversary=adversary, use_learning_parsers=True)
return op
@pytest.fixture
def op_without_learning_parser(ability, adversary):
op = Operation(id='54321', name='testB', agents=[], adversary=adversary, use_learning_parsers=False)
return op
@pytest.fixture
def custom_agent(test_agent, test_executor):
def _make_agent(platform='windows', trusted=True, executor_name='psh'):
test_executor.name = executor_name
test_agent.platform = platform
test_agent.executors = [test_executor.name]
test_agent.trusted = trusted
return test_agent
return _make_agent
@pytest.fixture
def op_with_learning_and_seeded(ability, adversary, operation_agent, parse_datestring):
sc = Source(id='3124', name='test', facts=[Fact(trait='domain.user.name', value='bob'), Fact(trait='domain.user.name', value='jane', score=0)])
op = Operation(id='6789', name='testC', agents=[], adversary=adversary, source=sc, use_learning_parsers=True)
# patch operation to make it 'realistic'
op.start = parse_datestring(OP_START_TIME)
op.adversary = op.adversary()
op.planner = Planner(planner_id='12345', name='test_planner',
module='not.an.actual.planner', params=None)
op.objective = Objective(id='6428', name='not_an_objective')
t_operation_agent = operation_agent
t_operation_agent.paw = '123456'
op.agents = [t_operation_agent]
return op
class TestOperation:
def test_ran_ability_id(self, ability, adversary):
op = Operation(name='test', agents=[], adversary=adversary)
mock_link = MagicMock(spec=Link, ability=ability(ability_id='123'), finish=MOCK_LINK_FINISH_TIME)
op.chain = [mock_link]
assert op.ran_ability_id('123')
def test_event_logs(self, event_loop, op_for_event_logs, operation_agent, file_svc, data_svc, event_log_op_start_time,
op_agent_creation_time, fire_event_mock):
event_loop.run_until_complete(data_svc.remove('agents', match=dict(unique=operation_agent.unique)))
event_loop.run_until_complete(data_svc.store(operation_agent))
want_agent_metadata = dict(
paw='testpaw',
group='red',
architecture='amd64',
username='testagent',
location=r'C:\Users\Public\test.exe',
pid=1234,
ppid=123,
privilege='User',
host='WORKSTATION',
contact='unknown',
created=op_agent_creation_time,
)
want_operation_metadata = dict(
operation_name='test',
operation_start=event_log_op_start_time,
operation_adversary='test adversary',
)
want_attack_metadata = dict(
tactic='test tactic',
technique_name='test technique',
technique_id='T0000',
)
want = [
dict(
command='whoami',
plaintext_command='whoami',
delegated_timestamp=LINK1_DECIDE_TIME,
collected_timestamp=LINK1_COLLECT_TIME,
finished_timestamp=LINK1_FINISH_TIME,
status=0,
platform='windows',
executor='psh',
pid=789,
agent_metadata=want_agent_metadata,
ability_metadata=dict(
ability_id='123',
ability_name='test ability',
ability_description='test ability desc',
),
operation_metadata=want_operation_metadata,
attack_metadata=want_attack_metadata,
),
dict(
command='hostname',
plaintext_command='hostname',
delegated_timestamp=LINK2_DECIDE_TIME,
collected_timestamp=LINK2_COLLECT_TIME,
finished_timestamp=LINK2_FINISH_TIME,
status=0,
platform='windows',
executor='psh',
pid=7890,
agent_metadata=want_agent_metadata,
ability_metadata=dict(
ability_id='456',
ability_name='test ability 2',
ability_description='test ability 2 desc',
),
operation_metadata=want_operation_metadata,
attack_metadata=want_attack_metadata,
),
]
event_logs = event_loop.run_until_complete(op_for_event_logs.event_logs(file_svc, data_svc))
assert event_logs == want
@pytest.mark.usefixtures(
"setup_op_config"
)
def test_writing_event_logs_to_disk(self, event_loop, op_for_event_logs, operation_agent, file_svc, data_svc,
event_log_op_start_time, op_agent_creation_time, fire_event_mock):
event_loop.run_until_complete(data_svc.remove('agents', match=dict(unique=operation_agent.unique)))
event_loop.run_until_complete(data_svc.store(operation_agent))
want_agent_metadata = dict(
paw='testpaw',
group='red',
architecture='amd64',
username='testagent',
location=r'C:\Users\Public\test.exe',
pid=1234,
ppid=123,
privilege='User',
host='WORKSTATION',
contact='unknown',
created=op_agent_creation_time,
)
want_operation_metadata = dict(
operation_name='test',
operation_start=event_log_op_start_time,
operation_adversary='test adversary',
)
want_attack_metadata = dict(
tactic='test tactic',
technique_name='test technique',
technique_id='T0000',
)
want = [
dict(
command='whoami',
plaintext_command='whoami',
delegated_timestamp=LINK1_DECIDE_TIME,
collected_timestamp=LINK1_COLLECT_TIME,
finished_timestamp=LINK1_FINISH_TIME,
status=0,
platform='windows',
executor='psh',
pid=789,
agent_metadata=want_agent_metadata,
ability_metadata=dict(
ability_id='123',
ability_name='test ability',
ability_description='test ability desc',
),
operation_metadata=want_operation_metadata,
attack_metadata=want_attack_metadata,
),
dict(
command='hostname',
plaintext_command='hostname',
delegated_timestamp=LINK2_DECIDE_TIME,
collected_timestamp=LINK2_COLLECT_TIME,
finished_timestamp=LINK2_FINISH_TIME,
status=0,
platform='windows',
executor='psh',
pid=7890,
agent_metadata=want_agent_metadata,
ability_metadata=dict(
ability_id='456',
ability_name='test ability 2',
ability_description='test ability 2 desc',
),
operation_metadata=want_operation_metadata,
attack_metadata=want_attack_metadata,
),
]
event_loop.run_until_complete(op_for_event_logs.write_event_logs_to_disk(file_svc, data_svc))
target_path = f'/tmp/event_logs/operation_{op_for_event_logs.id}.json'
assert os.path.isfile(target_path)
try:
with open(target_path, 'rb') as log_file:
recorded_log = json.load(log_file)
assert recorded_log == want
finally:
os.remove(target_path)
@mock.patch.object(Operation, '_emit_state_change_event')
def test_no_state_change_event_on_instantiation(self, mock_emit_state_change_method, fake_event_svc, adversary):
Operation(name='test', agents=[], adversary=adversary)
mock_emit_state_change_method.assert_not_called()
@mock.patch.object(Operation, '_emit_state_change_event')
def test_no_state_change_event_fired_when_setting_same_state(self, mock_emit_state_change_method, fake_event_svc,
adversary):
initial_state = 'running'
op = Operation(name='test', agents=[], adversary=adversary, state=initial_state)
op.state = initial_state
mock_emit_state_change_method.assert_not_called()
@mock.patch.object(Operation, '_emit_state_change_event')
def test_state_change_event_fired_on_state_change(self, mock_emit_state_change_method, fake_event_svc, adversary):
op = Operation(name='test', agents=[], adversary=adversary, state='running')
op.state = 'finished'
mock_emit_state_change_method.assert_called_with(from_state='running', to_state='finished')
def test_emit_state_change_event(self, event_loop, fake_event_svc, adversary):
op = Operation(name='test', agents=[], adversary=adversary, state='running')
fake_event_svc.reset()
event_loop.run_until_complete(
op._emit_state_change_event(
from_state='running',
to_state='finished'
)
)
expected_key = (Operation.EVENT_EXCHANGE, Operation.EVENT_QUEUE_STATE_CHANGED)
assert expected_key in fake_event_svc.fired
event_kwargs = fake_event_svc.fired[expected_key]
assert event_kwargs['op'] == op.id
assert event_kwargs['from_state'] == 'running'
assert event_kwargs['to_state'] == 'finished'
def test_with_learning_parser(self, event_loop, app_svc, file_svc, contact_svc, data_svc, learning_svc, event_svc, op_with_learning_parser,
make_test_link, make_test_result, knowledge_svc, fire_event_mock):
test_link = make_test_link(1234)
op_with_learning_parser.add_link(test_link)
test_result = make_test_result(test_link.id)
event_loop.run_until_complete(data_svc.store(op_with_learning_parser))
event_loop.run_until_complete(contact_svc._save(test_result))
assert len(test_link.facts) == 1
fact = test_link.facts[0]
assert fact.trait == 'host.ip.address'
assert fact.value == '10.10.10.10'
knowledge_data = event_loop.run_until_complete(op_with_learning_parser.all_facts())
assert len(knowledge_data) == 1
assert knowledge_data[0].trait == 'host.ip.address'
assert knowledge_data[0].value == '10.10.10.10'
def test_without_learning_parser(self, event_loop, app_svc, contact_svc, data_svc, learning_svc, event_svc,
op_without_learning_parser, make_test_link, make_test_result):
test_link = make_test_link(5678)
op_without_learning_parser.add_link(test_link)
test_result = make_test_result(test_link.id)
event_loop.run_until_complete(data_svc.store(op_without_learning_parser))
event_loop.run_until_complete(contact_svc._save(test_result))
assert len(test_link.facts) == 0
def test_facts(self, event_loop, app_svc, contact_svc, file_svc, data_svc, learning_svc, fire_event_mock,
op_with_learning_and_seeded, make_test_link, make_test_result, knowledge_svc):
event_loop.run_until_complete(data_svc.store(op_with_learning_and_seeded.source))
test_link = make_test_link(9876)
op_with_learning_and_seeded.add_link(test_link)
test_result = make_test_result(test_link.id)
event_loop.run_until_complete(data_svc.store(op_with_learning_and_seeded))
event_loop.run_until_complete(op_with_learning_and_seeded._init_source()) # need to call this manually (no 'run')
event_loop.run_until_complete(contact_svc._save(test_result))
assert len(test_link.facts) == 1
fact = test_link.facts[0]
assert fact.trait == 'host.ip.address'
assert fact.value == '10.10.10.10'
knowledge_data = event_loop.run_until_complete(op_with_learning_and_seeded.all_facts())
assert len(knowledge_data) == 2
origin_set = [x.source for x in knowledge_data]
assert op_with_learning_and_seeded.id in origin_set
assert op_with_learning_and_seeded.source.id in origin_set
report = event_loop.run_until_complete(op_with_learning_and_seeded.report(file_svc, data_svc))
assert len(report['facts']) == 2
async def test_wait_for_links_completion_ignorable_link(self, make_test_link, operation_agent):
test_agent = operation_agent
test_link = make_test_link(9876, test_agent.paw, Link().states['DISCARD'])
op = Operation(name='test', agents=[test_agent], state='running')
op.add_link(test_link)
assert not op.ignored_links
assert test_link in op.chain
await op.wait_for_links_completion([test_link.id])
assert test_link.id in op.ignored_links
assert len(op.ignored_links) == 1
assert test_link in op.chain
async def test_wait_for_links_completion_non_ignorable_link(self, make_test_link, untrusted_operation_agent, mocker,
async_return):
test_agent = untrusted_operation_agent
test_link = make_test_link(9876, test_agent.paw)
op = Operation(name='test', agents=[test_agent], state='running')
op.add_link(test_link)
assert not op.ignored_links
assert test_link in op.chain
with mocker.patch('asyncio.sleep') as mock_sleep:
mock_sleep.return_value = async_return(None)
await op.wait_for_links_completion([test_link.id])
assert not op.ignored_links
assert test_link in op.chain
def test_update_untrusted_agents_with_trusted(self, operation_agent, ability, adversary):
operation_agent.trusted = True
op = Operation(name='test', agents=[operation_agent], adversary=adversary)
op.update_untrusted_agents(operation_agent)
assert not op.untrusted_agents
def test_update_untrusted_agents_with_untrusted(self, operation_agent, ability, adversary):
operation_agent.trusted = False
op = Operation(name='test', agents=[operation_agent], adversary=adversary)
op.update_untrusted_agents(operation_agent)
assert operation_agent.paw in op.untrusted_agents
def test_update_untrusted_agents_with_trusted_no_operation_agents(self, operation_agent, ability, adversary):
operation_agent.trusted = True
op = Operation(name='test', agents=[], adversary=adversary)
op.update_untrusted_agents(operation_agent)
assert not op.untrusted_agents
def test_update_untrusted_agents_with_untrusted_no_operation_agents(self, operation_agent, ability, adversary):
operation_agent.trusted = False
op = Operation(name='test', agents=[], adversary=adversary)
op.update_untrusted_agents(operation_agent)
assert not op.untrusted_agents
def test_check_reason_skipped_unknown_platform(self, test_agent, test_ability):
test_agent.platform = 'unknown'
op = Operation(name='test', agents=[test_agent], state='running')
reason = op._check_reason_skipped(agent=test_agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=test_agent.executors, agent_ran={})
assert reason['reason'] == 'Platform not available'
assert reason['reason_id'] == Operation.Reason.PLATFORM.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_valid_executor(self, test_agent, test_ability):
test_agent.platform = 'darwin'
op = Operation(name='test', agents=[test_agent], state='running')
reason = op._check_reason_skipped(agent=test_agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=[], agent_ran={})
assert reason['reason'] == 'Mismatched ability platform and executor'
assert reason['reason_id'] == Operation.Reason.EXECUTOR.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_privilege(self, custom_agent, test_ability, mocker, test_executor):
test_executor.name = 'psh'
agent = custom_agent()
test_ability.privilege = 'Elevated'
op = Operation(name='test', agents=[agent], state='running')
reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=agent.executors, agent_ran={})
assert reason['reason'] == 'Ability privilege not fulfilled'
assert reason['reason_id'] == Operation.Reason.PRIVILEGE.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_fact_dependency(self, custom_agent, test_ability, mocker, test_executor, fact):
test_executor.name = 'psh'
agent = custom_agent()
op = Operation(name='test', agents=[agent], state='running')
with mocker.patch('app.objects.c_ability.Ability.find_executors') as mock_find_executors:
mock_find_executors.return_value = [test_executor]
with mocker.patch('re.findall') as mock_findall:
mock_findall.return_value = [fact('test.fact.attribute')]
reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=agent.executors, agent_ran={})
assert reason['reason'] == 'Fact dependency not fulfilled'
assert reason['reason_id'] == Operation.Reason.FACT_DEPENDENCY.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_link_ignored(self, custom_agent, test_ability, mocker, active_link):
agent = custom_agent()
op = Operation(name='test', agents=[agent], state='running')
test_link = Link.load(active_link)
op.chain = [test_link]
op.ignored_links = [test_link.id]
reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=agent.executors, agent_ran={})
assert reason['reason'] == 'Link ignored - highly visible or discarded link'
assert reason['reason_id'] == Operation.Reason.LINK_IGNORED.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_untrusted(self, custom_agent, test_ability, mocker):
agent = custom_agent(trusted=False)
op = Operation(name='test', agents=[agent], state='running')
reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=agent.executors, agent_ran={})
assert reason['reason'] == 'Agent not trusted'
assert reason['reason_id'] == Operation.Reason.UNTRUSTED.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_op_running(self, custom_agent, test_ability, mocker):
agent = custom_agent()
op = Operation(name='test', agents=[agent], state='running')
reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=agent.executors, agent_ran={})
assert reason['reason'] == 'Operation not completed'
assert reason['reason_id'] == Operation.Reason.OP_RUNNING.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_check_reason_skipped_other(self, custom_agent, test_ability, mocker):
agent = custom_agent()
op = Operation(name='test', agents=[agent], state='finished')
reason = op._check_reason_skipped(agent=agent, ability=test_ability, op_facts=[], state=op.state,
agent_executors=agent.executors, agent_ran={})
assert reason['reason'] == 'Other'
assert reason['reason_id'] == Operation.Reason.OTHER.value
assert reason['ability_id'] == test_ability.ability_id
assert reason['ability_name'] == test_ability.name
async def test_add_ignored_link(self, make_test_link, operation_agent):
test_agent = operation_agent
test_link = make_test_link(9876, test_agent.paw, Link().states['DISCARD'])
op = Operation(name='test', agents=[test_agent], state='running')
op.add_ignored_link(test_link.id)
assert op.ignored_links
assert test_link.id in op.ignored_links
assert len(op.ignored_links) == 1
async def test_operation_cleanup_status(self, fake_planning_svc, operation_agent):
services = {'planning_svc': fake_planning_svc}
op = Operation(name='test with cleanup', agents=[operation_agent], state='running')
assert op.state == 'running'
assert await op.is_closeable()
await op._cleanup_operation(services)
assert op.state == 'cleanup'