diff --git a/app/objects/c_operation.py b/app/objects/c_operation.py index 013af3732..11b2de1cd 100644 --- a/app/objects/c_operation.py +++ b/app/objects/c_operation.py @@ -241,6 +241,11 @@ async def report(self, file_svc, data_svc, output=False, redacted=False): except Exception: logging.error('Error saving operation report (%s)' % self.name, exc_info=True) + async def event_logs(self, file_svc, data_svc, output=False): + # Ignore discarded / high visibility links that did not actually run. + return [await self._convert_link_to_event_log(step, file_svc, data_svc, output=output) for step in self.chain + if not step.can_ignore()] + async def run(self, services): # load objective obj = await services.get('data_svc').locate('objectives', match=dict(id=self.adversary.objective)) @@ -259,6 +264,23 @@ async def run(self, services): """ PRIVATE """ + async def _convert_link_to_event_log(self, link, file_svc, data_svc, output=False): + event_dict = dict(command=link.command, + delegated_timestamp=link.decide.strftime('%Y-%m-%d %H:%M:%S'), + collected_timestamp=link.collect.strftime('%Y-%m-%d %H:%M:%S') if link.collect else None, + finished_timestamp=link.finish, + status=link.status, + platform=link.ability.platform, + executor=link.ability.executor, + pid=link.pid, + agent_metadata=await self._get_agent_info_for_event_log(link.paw, data_svc), + ability_metadata=self._get_ability_metadata_for_event_log(link.ability), + operation_metadata=self._get_operation_metadata_for_event_log(), + attack_metadata=self._get_attack_metadata_for_event_log(link.ability)) + if output and link.output: + event_dict['output'] = self.decode_bytes(file_svc.read_result_file(link.unique)) + return event_dict + async def _cleanup_operation(self, services): cleanup_count = 0 for member in self.agents: @@ -335,6 +357,43 @@ def _check_reason_skipped(self, agent, ability, op_facts, state, agent_executors return dict(reason='Agent untrusted', reason_id=self.Reason.UNTRUSTED.value, ability_id=ability.ability_id, ability_name=ability.name) + def _get_operation_metadata_for_event_log(self): + return dict(operation_name=self.name, + operation_start=self.start.strftime('%Y-%m-%d %H:%M:%S'), + operation_adversary=self.adversary.name) + + @staticmethod + def _get_ability_metadata_for_event_log(ability): + return dict(ability_id=ability.ability_id, + ability_name=ability.name, + ability_description=ability.description) + + @staticmethod + def _get_attack_metadata_for_event_log(ability): + return dict(tactic=ability.tactic, + technique_name=ability.technique_name, + technique_id=ability.technique_id) + + @staticmethod + async def _get_agent_info_for_event_log(agent_paw, data_svc): + agent_search_results = await data_svc.locate('agents', match=dict(paw=agent_paw)) + if not agent_search_results: + return {} + else: + # We expect only one agent per paw. + agent = agent_search_results[0] + return dict(paw=agent.paw, + group=agent.group, + architecture=agent.architecture, + username=agent.username, + location=agent.location, + pid=agent.pid, + ppid=agent.ppid, + privilege=agent.privilege, + host=agent.host, + contact=agent.contact, + created=agent.created.strftime('%Y-%m-%d %H:%M:%S')) + class Reason(Enum): PLATFORM = 0 EXECUTOR = 1 diff --git a/app/service/rest_svc.py b/app/service/rest_svc.py index 0a0af8b73..6059ad251 100644 --- a/app/service/rest_svc.py +++ b/app/service/rest_svc.py @@ -130,8 +130,16 @@ async def display_result(self, data): async def display_operation_report(self, data): op_id = data.pop('op_id') op = (await self.get_service('data_svc').locate('operations', match=dict(id=int(op_id))))[0] - return await op.report(file_svc=self.get_service('file_svc'), data_svc=self.get_service('data_svc'), - output=data.get('agent_output')) + report_format = data.pop('format', 'full-report') + if report_format == 'full-report': + generator_func = op.report + elif report_format == 'event-logs': + generator_func = op.event_logs + else: + self.log.error('Unsupported operation report format requested: %s' % report_format) + return '' + return await generator_func(file_svc=self.get_service('file_svc'), data_svc=self.get_service('data_svc'), + output=data.get('agent_output')) async def download_contact_report(self, contact): return dict(contacts=self.get_service('contact_svc').report.get(contact.get('contact'), dict())) diff --git a/templates/operations.html b/templates/operations.html index 413cc9613..3df7a4e42 100644 --- a/templates/operations.html +++ b/templates/operations.html @@ -32,7 +32,8 @@

Operations

- + +
@@ -346,7 +347,8 @@

function checkOpBtns(){ validateFormState(($('#operation-list').val()), '#opDelete'); - validateFormState(($('#operation-list').val()), '#reportBtn'); + validateFormState(($('#operation-list').val()), '#fullReportBtn'); + validateFormState(($('#operation-list').val()), '#eventLogBtn'); } function deleteOperation(){ @@ -770,15 +772,27 @@

restRequest('POST', {'index':'result','link_id':lnk}, loadResults); } - function downloadOperationReport() { + function downloadOperationFullReport() { + downloadOperationInfo('full-report'); + } + + function downloadOperationEventLogs() { + downloadOperationInfo('event-logs'); + } + + function downloadOperationInfo(format) { let selectedOperationId = $('#operation-list option:selected').attr('value'); let agentOutput = $('#agent-output').prop("checked"); - let postData = selectedOperationId ? {'index':'operation_report', 'op_id': selectedOperationId, 'agent_output': Number(agentOutput)} : null; + let postData = selectedOperationId ? { + 'index':'operation_report', + 'op_id': selectedOperationId, + 'agent_output': Number(agentOutput), + 'format': format, + } : null; let selectedOpName = $('#operation-list option:selected').text(); downloadReport('/api/rest', selectedOpName, postData); } - function resetMoreModal() { let modal = $('#more-modal'); modal.hide(); diff --git a/tests/objects/test_operation.py b/tests/objects/test_operation.py index 046ef8ac2..771d7186e 100644 --- a/tests/objects/test_operation.py +++ b/tests/objects/test_operation.py @@ -1,13 +1,148 @@ +import pytest + +from base64 import b64encode +from datetime import datetime from unittest.mock import MagicMock from app.objects.c_operation import Operation from app.objects.secondclass.c_link import Link -class TestOperation: +@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='C:\\Users\\Public\\test.exe', + pid=1234, ppid=123, executors=['psh'], privilege='User', exe_name='test.exe', contact='unknown', + paw='testpaw') + + +@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, paw, ability, pid=0, decide=None, collect=None, finish=None, **kwargs): + generated_link = Link(command, paw, ability, **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 op_for_event_logs(operation_agent, operation_adversary, ability, operation_link, encoded_command): + op = Operation(name='test', agents=[operation_agent], adversary=operation_adversary) + op.set_start_details() + encoded_command_1 = encoded_command('whoami') + encoded_command_2 = encoded_command('hostname') + ability_1 = ability(ability_id='123', tactic='test tactic', technique_id='T0000', technique='test technique', + name='test ability', description='test ability desc', executor='psh', platform='windows', + test=encoded_command_1) + ability_2 = ability(ability_id='456', tactic='test tactic', technique_id='T0000', technique='test technique', + name='test ability 2', description='test ability 2 desc', executor='psh', + platform='windows', test=encoded_command_2) + link_1 = operation_link(ability=ability_1, paw=operation_agent.paw, + command=encoded_command_1, status=0, host=operation_agent.host, pid=789, + decide=datetime.strptime('2021-01-01 08:00:00', '%Y-%m-%d %H:%M:%S'), + collect=datetime.strptime('2021-01-01 08:01:00', '%Y-%m-%d %H:%M:%S'), + finish='2021-01-01 08:02:00') + link_2 = operation_link(ability=ability_2, paw=operation_agent.paw, + command=encoded_command_2, status=0, host=operation_agent.host, pid=7890, + decide=datetime.strptime('2021-01-01 09:00:00', '%Y-%m-%d %H:%M:%S'), + collect=datetime.strptime('2021-01-01 09:01:00', '%Y-%m-%d %H:%M:%S'), + finish='2021-01-01 09:02:00') + discarded_link = operation_link(ability=ability_2, paw=operation_agent.paw, + command=encoded_command_2, status=-2, host=operation_agent.host, pid=7891, + decide=datetime.strptime('2021-01-01 10:00:00', '%Y-%m-%d %H:%M:%S')) + op.chain = [link_1, link_2, discarded_link] + 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='2021-01-01 08:00:00') op.chain = [mock_link] assert op.ran_ability_id('123') + + def test_event_logs(self, loop, op_for_event_logs, operation_agent, file_svc, data_svc): + loop.run_until_complete(data_svc.store(operation_agent)) + start_time = op_for_event_logs.start.strftime('%Y-%m-%d %H:%M:%S') + agent_creation_time = operation_agent.created.strftime('%Y-%m-%d %H:%M:%S') + want_agent_metadata = dict( + paw='testpaw', + group='red', + architecture='amd64', + username='testagent', + location='C:\\Users\\Public\\test.exe', + pid=1234, + ppid=123, + privilege='User', + host='WORKSTATION', + contact='unknown', + created=agent_creation_time, + ) + want_operation_metadata = dict( + operation_name='test', + operation_start=start_time, + operation_adversary='test adversary', + ) + want_attack_metadata = dict( + tactic='test tactic', + technique_name='test technique', + technique_id='T0000', + ) + want = [ + dict( + command='d2hvYW1p', + delegated_timestamp='2021-01-01 08:00:00', + collected_timestamp='2021-01-01 08:01:00', + finished_timestamp='2021-01-01 08:02:00', + 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='aG9zdG5hbWU=', + delegated_timestamp='2021-01-01 09:00:00', + collected_timestamp='2021-01-01 09:01:00', + finished_timestamp='2021-01-01 09:02:00', + 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 = loop.run_until_complete(op_for_event_logs.event_logs(file_svc, data_svc)) + assert event_logs == want