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

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 2, 2021
1 parent f4b645a commit b458a73
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 b458a73

Please sign in to comment.