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