Skip to content

Commit

Permalink
Users can now select between full operation report and event-based op…
Browse files Browse the repository at this point in the history
…eration logs (#2045)

Proper awaiting

Ignore discarded and high visibility links when generating event logs

Adding tests for event log report generation
  • Loading branch information
uruwhy committed Mar 9, 2021
1 parent 52150f2 commit feb27aa
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 8 deletions.
59 changes: 59 additions & 0 deletions app/objects/c_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions app/service/rest_svc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
24 changes: 19 additions & 5 deletions templates/operations.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ <h4>Operations</h4>
</tr>
</table>
</center>
<button id="reportBtn" type="button" class="button-notready atomic-button" onclick="downloadOperationReport()">Download report</button>
<button id="fullReportBtn" type="button" class="button-notready atomic-button" onclick="downloadOperationFullReport()">Download full report</button>
<button id="eventLogBtn" type="button" class="button-notready atomic-button" onclick="downloadOperationEventLogs()">Download event logs</button>
<hr>
<button id="opDelete" type="button" class="button-notready atomic-button" onclick="deleteOperation()">Delete</button>
</div>
Expand Down Expand Up @@ -346,7 +347,8 @@ <h1 id="time-tactic" class="member-title"></h1>

function checkOpBtns(){
validateFormState(($('#operation-list').val()), '#opDelete');
validateFormState(($('#operation-list').val()), '#reportBtn');
validateFormState(($('#operation-list').val()), '#fullReportBtn');
validateFormState(($('#operation-list').val()), '#eventLogBtn');
}

function deleteOperation(){
Expand Down Expand Up @@ -770,15 +772,27 @@ <h1 id="time-tactic" class="member-title"></h1>
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();
Expand Down
137 changes: 136 additions & 1 deletion tests/objects/test_operation.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit feb27aa

Please sign in to comment.