Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Operation layer JSON #37

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 182 additions & 5 deletions app/compass_svc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@

from aiohttp import web
from aiohttp_jinja2 import template
from collections import defaultdict
from enum import Enum

from app.service.auth_svc import for_all_public_methods, check_authorization


class _HeatmapColors(Enum):
SUCCESS = '#44AA99' # Green
PARTIAL_SUCCESS = '#FFB000' # Orange
FAILURE = '#CC3311' # Red
NOT_RUN = '#555555' # Dark grey


@for_all_public_methods(check_authorization)
class CompassService:

def __init__(self, services):
self.services = services
self.auth_svc = self.services.get('auth_svc')
Expand All @@ -19,10 +27,14 @@ def __init__(self, services):
@template('compass.html')
async def splash(self, request):
adversaries = [a.display for a in await self.data_svc.locate('adversaries')]
return dict(adversaries=sorted(adversaries, key=lambda a: a['name']))
operations = [o.display for o in await self.data_svc.locate('operations')]
return dict(
adversaries=sorted(adversaries, key=lambda a: a['name']),
operations=operations,
)

@staticmethod
def _get_layer_boilerplate(name, description):
def _get_adversary_layer_boilerplate(name, description):
return dict(
version='3.0',
name=name,
Expand All @@ -44,14 +56,77 @@ def _get_layer_boilerplate(name, description):
)
)

@staticmethod
def _get_operation_layer_boilerplate(name, description):
return dict(
name=name,
versions=dict(
attack="8",
navigator="4.0",
layer="4.0",
),
domain="enterprise-attack",
description=description,
filters={
"platforms": [
"Linux",
"macOS",
"Windows",
"Office 365",
"Azure AD",
"AWS",
"GCP",
"Azure",
"SaaS",
"Network"
]
},
sorting=0,
hideDisabled=False,
techniques=[],
gradient=dict(
colors=[
_HeatmapColors.NOT_RUN.value,
_HeatmapColors.FAILURE.value,
_HeatmapColors.PARTIAL_SUCCESS.value,
_HeatmapColors.SUCCESS.value,
],
minValue=0,
maxValue=3
),
legendItems=[
{
"label": "All ran procedures succeeded",
"color": _HeatmapColors.SUCCESS.value
},
{
"label": "None of the ran procedures succeeded",
"color": _HeatmapColors.FAILURE.value
},
{
"label": "Some of the ran procedures succeeded",
"color": _HeatmapColors.PARTIAL_SUCCESS.value
},
{
"label": "All procedures skipped",
"color": _HeatmapColors.NOT_RUN.value
}
],
metadata=[],
showTacticRowBackground=False,
tacticRowBackground="#dddddd",
selectTechniquesAcrossTactics=True,
selectSubtechniquesWithParent=False,
)

async def _get_all_abilities(self):
return 'All-Abilities', 'full set of techniques available', [ability.display for ability in await self.services.get('data_svc').locate('abilities')]

async def _get_adversary_abilities(self, request_body):
adversary = (await self.rest_svc.display_objects(object_name='adversaries', data=dict(adversary_id=request_body.get('adversary_id'))))[0]
return adversary['name'], adversary['description'], adversary['atomic_ordering']

async def generate_layer(self, request):
async def generate_adversary_layer(self, request):
request_body = json.loads(await request.read())

ability_functions = dict(
Expand All @@ -60,7 +135,7 @@ async def generate_layer(self, request):
)
display_name, description, abilities = await ability_functions[request_body['index']](request_body)

layer = self._get_layer_boilerplate(name=display_name, description=description)
layer = self._get_adversary_layer_boilerplate(name=display_name, description=description)
for ability in abilities:
technique = dict(
techniqueID=ability['technique_id'],
Expand All @@ -75,6 +150,108 @@ async def generate_layer(self, request):

return web.json_response(layer)

async def generate_operation_layer(self, request):
request_body = json.loads(await request.read())
operation = (await self.data_svc.locate('operations', match=dict(id=int(request_body.get('id')))))[0]
display_name = operation.name
description = 'Operation {name} was conducted using adversary profile {adv_profile}'.format(
name=operation.name,
adv_profile=operation.adversary.name
)
layer = self._get_operation_layer_boilerplate(name=display_name, description=description)
success_counter = defaultdict(int)
technique_counter = defaultdict(int)
no_success_counter = defaultdict(int)
skipped_counter = defaultdict(int)
technique_tactic_map = dict()
technique_dicts = dict() # Map technique ID to corresponding dict object.
to_process = [] # list of (ability, status) tuples

# Get links from operation chain
for link in operation.chain:
to_process.append((link.ability, link.status))

# Get automatically skipped links
skipped_abilities = await operation.get_skipped_abilities_by_agent(self.data_svc)
for skipped_by_agent in skipped_abilities:
for _, skipped in skipped_by_agent.items():
for skipped_info in skipped:
skipped_reason = skipped_info.get('reason_id')
ability_id = skipped_info.get('ability_id')
if skipped_reason is not None:
status = 10 + int(skipped_reason) # skipped_reason can be in range [0, 5]
ability = (await self.data_svc.locate('abilities', match=dict(ability_id=ability_id)))[0]
if ability:
to_process.append((ability, status))

# Count success, failures, no-runs for links.
for (ability, status) in to_process:
technique_id = ability.technique_id
technique_counter[technique_id] += 1
technique_tactic_map[technique_id] = ability.tactic
if status == 0:
success_counter[technique_id] += 1
elif status in (1, 124, -3, -4):
# Did not succeed if status was failure, timeout, untrusted, or collected (in the
# case of collected/untrusted/timeout, the command may have run successfully, but
# we don't know for sure due to lack of timely response from the agent).
no_success_counter[technique_id] += 1
else:
# Ability either queued, skipped, manually discarded, or visibility threshold was surpassed.
skipped_counter[technique_id] += 1

for technique_id, num_procedures in technique_counter.items():
# case 1: all ran procedures succeeded
# case 2: all ran procedures failed
# case 3: none of the procedures ran
# case 4: some procedures ran, but not all of them succeeded

# Default case: none of the procedures for this technique were run.
score = 0
if skipped_counter[technique_id] < num_procedures:
if success_counter[technique_id] == 0:
# None of the procedures that ran for this technique succeeded
score = 1
elif no_success_counter[technique_id] == 0:
# All of the procedures that ran for this technique succeeded.
score = 3
else:
# Some of the procedures that ran failed
score = 2
technique_dicts[technique_id] = dict(
techniqueID=technique_id,
tactic=technique_tactic_map[technique_id],
score=score,
color='',
comment='',
enabled=True,
metadata=[],
showSubtechniques=False,
)

for technique_id, num_procedures in technique_counter.items():
# Check if we need to expand the parent technique.
if '.' in technique_id:
parent_id = technique_id.split('.')[0]

# Check if the parent technique was already processed
if parent_id in technique_dicts:
technique_dicts.get(parent_id)['showSubtechniques'] = True
else:
technique_dicts[parent_id] = dict(
techniqueID=parent_id,
tactic=technique_tactic_map[technique_id],
color='',
comment='',
enabled=True,
metadata=[],
showSubtechniques=True,
)

for _, technique_dict in technique_dicts.items():
layer['techniques'].append(technique_dict)
return web.json_response(layer)

@staticmethod
def _extract_techniques(request_body):
techniques = request_body.get('techniques')
Expand Down
3 changes: 2 additions & 1 deletion hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ async def enable(services):
app = services.get('app_svc').application
compass_svc = CompassService(services)
app.router.add_static('/compass', 'plugins/compass/static/', append_version=True)
app.router.add_route('POST', '/plugin/compass/layer', compass_svc.generate_layer)
app.router.add_route('POST', '/plugin/compass/adversarylayer', compass_svc.generate_adversary_layer)
app.router.add_route('POST', '/plugin/compass/operationlayer', compass_svc.generate_operation_layer)
app.router.add_route('POST', '/plugin/compass/adversary', compass_svc.create_adversary_from_layer)
app.router.add_route('GET', '/plugin/compass/gui', compass_svc.splash)
23 changes: 21 additions & 2 deletions static/js/compass.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function generateLayer() {
function generateAdversaryLayer() {
function downloadObjectAsJson(data){
let exportName = 'layer';
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
Expand All @@ -12,7 +12,26 @@ function generateLayer() {

let selectionAdversaryID = $('#layer-selection-adversary option:selected').attr('value');
let postData = selectionAdversaryID ? {'index':'adversary', 'adversary_id': selectionAdversaryID} : {'index': 'all'};
restRequest('POST', postData, downloadObjectAsJson, '/plugin/compass/layer');
restRequest('POST', postData, downloadObjectAsJson, '/plugin/compass/adversarylayer');
}

function generateOperationLayer() {
function downloadObjectAsJson(data){
let exportName = 'operation_layer';
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
let downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", exportName + ".json");
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}

let selectionOperationID = $('#layer-selection-operation option:selected').attr('value');
if (selectionOperationID) {
let postData = {'index':'operation', 'id': selectionOperationID};
restRequest('POST', postData, downloadObjectAsJson, '/plugin/compass/operationlayer');
}
}

function uploadAdversaryLayerButtonFileUpload() {
Expand Down
19 changes: 16 additions & 3 deletions templates/compass.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h2 style="margin-top:-50px">find your way</h2>
upload the layer file to generate an adversary to use in an operation</p>
</div>
<div class="column section-border" style="flex:37%;text-align:left;padding:15px;">
<h2>Generate Layer</h2>
<h2>Generate Adversary Layer</h2>
<div id="layerSelectionAdversary">
<select id="layer-selection-adversary" style="margin:0 0 0 0">
<option value="" selected>Select an Adversary (All)</option>
Expand All @@ -19,8 +19,21 @@ <h2>Generate Layer</h2>
{% endfor %}}
</select>
</div>
<button id="generateLayer" type="button" class="button-success"
style="" onclick="generateLayer()">Generate Layer</button>
<button id="generateAdversaryLayer" type="button" class="button-success"
style="" onclick="generateAdversaryLayer()">Generate Layer</button>
</div>
<div class="column section-border" style="flex:37%;text-align:left;padding:15px;">
<h2>Generate Operation Layer</h2>
<div id="layerSelectionOperation">
<select id="layer-selection-operation" style="margin:0 0 0 0">
<option value="" selected>Select an Operation</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.id }}-{{ op.name }}</option>
{% endfor %}}
</select>
</div>
<button id="generateOperationLayer" type="button" class="button-success"
style="" onclick="generateOperationLayer()">Generate Layer</button>
</div>
<div class="column" style="flex:37%;text-align:left;padding:15px;">
<h2>Generate Adversary</h2>
Expand Down