Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
elegantmoose committed Apr 14, 2023
1 parent 8128c0c commit 3438ffc
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 37 deletions.
15 changes: 13 additions & 2 deletions app/api/v2/handlers/ability_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
from app.api.v2.handlers.base_object_api import BaseObjectApi
from app.api.v2.managers.ability_api_manager import AbilityApiManager
from app.api.v2.schemas.base_schemas import BaseGetAllQuerySchema, BaseGetOneQuerySchema
from app.objects.c_ability import Ability, AbilitySchema
from app.objects.c_ability import Ability, AbilitySchema, GetAbilityByFactSchema


class AbilityApi(BaseObjectApi):
def __init__(self, services):
super().__init__(description='ability', obj_class=Ability, schema=AbilitySchema, ram_key='abilities',
id_property='ability_id', auth_svc=services['auth_svc'])
self._api_manager = AbilityApiManager(data_svc=services['data_svc'], file_svc=services['file_svc'])
self._api_manager = AbilityApiManager(data_svc=services['data_svc'], file_svc=services['file_svc'], planning_svc=services['planning_svc'])

def add_routes(self, app: web.Application):
router = app.router
router.add_get('/abilities', self.get_abilities)
router.add_get('/abilities/{ability_id}', self.get_ability_by_id)
router.add_post('/abilities/search/facts', self.get_abilities_by_facts)
router.add_post('/abilities', self.create_ability)
router.add_put('/abilities/{ability_id}', self.create_or_update_ability)
router.add_patch('/abilities/{ability_id}', self.update_ability)
Expand Down Expand Up @@ -47,6 +48,16 @@ async def get_ability_by_id(self, request: web.Request):
ability = await self.get_object(request)
return web.json_response(ability)

@aiohttp_apispec.docs(tags=['abilities'], summary='Get abilities by required/produced facts.',
description='Searches for abilities that require or produce the supplied fact names.')
@aiohttp_apispec.querystring_schema(GetAbilityByFactSchema)
@aiohttp_apispec.response_schema(AbilitySchema(many=True, partial=True),
description='Returns a list of abilities.')
async def get_abilities_by_facts(self, request: web.Request):
fact_names = await request.json()
abilities = await self._api_manager.get_abilities_by_facts(**fact_names)
return web.json_response(abilities)

@aiohttp_apispec.docs(tags=['abilities'], summary='Creates a new ability.',
description='Creates a new adversary based on the `AbilitySchema`. '
'"name", "tactic", "technique_name", "technique_id" and "executors" are all required fields.')
Expand Down
13 changes: 7 additions & 6 deletions app/api/v2/handlers/adversary_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
from app.api.v2.handlers.base_object_api import BaseObjectApi
from app.api.v2.managers.adversary_api_manager import AdversaryApiManager
from app.api.v2.schemas.base_schemas import BaseGetAllQuerySchema, BaseGetOneQuerySchema
from app.objects.c_adversary import Adversary, AdversarySchema
from app.objects.c_adversary import Adversary, AdversarySchema, AdversaryFactAnalysisRequestSchema, AdversaryFactAnalysisResponseSchema


class AdversaryApi(BaseObjectApi):
def __init__(self, services):
super().__init__(description='adversary', obj_class=Adversary, schema=AdversarySchema, ram_key='adversaries',
id_property='adversary_id', auth_svc=services['auth_svc'])
self._api_manager = AdversaryApiManager(data_svc=services['data_svc'], file_svc=services['file_svc'])
self._api_manager = AdversaryApiManager(data_svc=services['data_svc'], file_svc=services['file_svc'], planning_svc=services['planning_svc'])

def add_routes(self, app: web.Application):
router = app.router
Expand All @@ -24,6 +24,7 @@ def add_routes(self, app: web.Application):
router.add_patch(adversaries_by_id_path, self.update_adversary)
router.add_put(adversaries_by_id_path, self.create_or_update_adversary)
router.add_delete(adversaries_by_id_path, self.delete_adversary)
router.add_post('/adveraries/fact_analysis', self.get_adversary_fact_analysis)

@aiohttp_apispec.docs(tags=['adversaries'],
summary='Retrieve all adversaries',
Expand Down Expand Up @@ -120,11 +121,11 @@ async def delete_adversary(self, request: web.Request):

@aiohttp_apispec.docs(tags=['adversaries'], summary='Gets fact analysis for an adversary.',
description='Gets fact analysis for an adversary.')
@aiohttp_apispec.request_schema(AdversarySchema(partial=True))
@aiohttp_apispec.response_schema(description='The response is the fact and requirement analysis for the adversary')
@aiohttp_apispec.request_schema(AdversaryFactAnalysisRequestSchema())
@aiohttp_apispec.response_schema(AdversaryFactAnalysisResponseSchema())
async def get_adversary_fact_analysis(self, request: web.Request):
data = await request.json()
analysis = await self._api_manager.fact_analysis(data)
analysis = await self._api_manager.fact_analysis(self.ram_key, **data)
return web.json_response(analysis)

async def create_on_disk_object(self, request: web.Request):
Expand All @@ -136,7 +137,7 @@ async def create_on_disk_object(self, request: web.Request):
self.obj_class)
return obj

async def _parse_common_data_from_request(self, request) -> tuple(dict, dict, str, dict, dict):
async def _parse_common_data_from_request(self, request) -> (dict, dict, str, dict, dict):
data = {}
raw_body = await request.read()
if raw_body:
Expand Down
6 changes: 5 additions & 1 deletion app/api/v2/managers/ability_api_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@


class AbilityApiManager(BaseApiManager):
def __init__(self, data_svc, file_svc):
def __init__(self, data_svc, file_svc, planning_svc):
super().__init__(data_svc=data_svc, file_svc=file_svc)
self._planning_svc = planning_svc

async def create_on_disk_object(self, data: dict, access: dict, ram_key: str, id_property: str, obj_class: type):
self._validate_ability_data(create=True, data=data)
Expand Down Expand Up @@ -50,6 +51,9 @@ async def update_on_disk_object(self, obj: Any, data: dict, ram_key: str, id_pro
await self._save_and_reload_object(file_path, existing_obj_data, obj_class, obj.access)
return next(self.find_objects(ram_key, {id_property: obj_id}))

async def get_abilities_by_facts(self, required: list[str] = None, produced: list[str] = None):
return await self._planning_svc.get_abilities_by_facts(required_facts=required, produced_facts=produced)

def _validate_ability_data(self, create: bool, data: dict):
# Correct ability_id key for ability file saving.
data['id'] = data.pop('ability_id', '')
Expand Down
16 changes: 12 additions & 4 deletions app/api/v2/managers/adversary_api_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@


class AdversaryApiManager(BaseApiManager):
def __init__(self, data_svc, file_svc):
def __init__(self, data_svc, file_svc, planning_svc):
super().__init__(data_svc=data_svc, file_svc=file_svc)
self._planning_svc = planning_svc

async def verify_adversary(self, adversary: Adversary):
adversary.verify(log=self.log, abilities=self._data_svc.ram['abilities'],
objectives=self._data_svc.ram['objectives'])
return adversary

async def fact_analysis(self, data: dict):
fact_reqs = await self._planning_svc.adversary_fact_requirements(**data)
# TODO: ability/ATT&CK suggestions for missing facts
async def fact_analysis(self, ram_key, adversary_id: str = None, atomic_ordering: list = None):
fa = dict(errors=[])
if adversary_id is not None:
adversary = self.find_object(ram_key, dict(adversary_id=adversary_id))
elif atomic_ordering is not None:
adversary = Adversary(name='prototype', atomic_ordering=atomic_ordering, description='Not full adversary. Only has atomic ordering field.')
else:
fa['errors'].append('Could not find adversary, or "atomic_ordering" field not supplied.')
return
return await self._planning_svc.adversary_fact_requirements(adversary)
7 changes: 7 additions & 0 deletions app/objects/c_ability.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
from app.utility.base_world import AccessSchema


class GetAbilityByFactSchema(ma.Schema):

required = ma.fields.List(ma.fields.String(), required=False)
produced = ma.fields.List(ma.fields.String(), required=False)


class AbilitySchema(ma.Schema):

ability_id = ma.fields.String()
tactic = ma.fields.String(missing=None)
technique_name = ma.fields.String(missing=None)
Expand Down
11 changes: 11 additions & 0 deletions app/objects/c_adversary.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
DEFAULT_OBJECTIVE_ID = '495a9828-cab1-44dd-a0ca-66e58177d8cc'


class AdversaryFactAnalysisRequestSchema(ma.Schema):

adversary_id = ma.fields.String(required=False)
atomic_ordering = ma.fields.List(ma.fields.String(), required=False)


class AdversaryFactAnalysisResponseSchema(AdversaryFactAnalysisRequestSchema):
missing_requirements = ma.fields.List(ma.fields.Dict)
errors = ma.fields.List(ma.fields.String)


class AdversarySchema(ma.Schema):

adversary_id = ma.fields.String()
Expand Down
104 changes: 80 additions & 24 deletions app/service/planning_svc.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,44 +265,100 @@ async def update_stopping_condition_met(self, planner, operation):
planner.stopping_condition_met = await self.check_stopping_conditions(planner.stopping_conditions,
operation)

async def adversary_fact_requirements(self, adversary_id=None, atomic_ordering=None):
async def adversary_fact_requirements(self, adversary):
""" """
fr = dict(errors=[])
facts_produced = []
required_facts = []
if atomic_ordering is None:
adversary = await self.get_service('data_svc') \
.locate('adversary', match=dict(adversary_id=adversary_id))
if not adversary:
fr['errors'].append(f'Adversary (ID: {adversary_id}) not found')
return fr
atomic_ordering = adversary.atomic_ordering

for ability_id in atomic_ordering:
missing_fact_reqs = dict(missing_requirements=[], errors=[])
all_produced_facts = []
fullfillment_abilities = []
all_missing_facts = set([])
NON_FACT_VARS = set(['server', 'group', ])

for i, ability_id in enumerate(adversary.atomic_ordering):
ability = await self.get_service('data_svc') \
.locate('ability', match=dict(ability_id=ability_id))
.locate('abilities', match=dict(ability_id=ability_id))
if ability is None:
fr['errors'].append('Ability (ID: {ability_id}) not found')
missing_fact_reqs['errors'].append('Ability (ID: {ability_id}) not found')
continue

self.log.error(f'-----Ability: {ability[0].name}')

# record produced and required facts
executors = ability.get_exectuors()
executors = ability[0].get_executors()
for _platform, name_executor in executors.items():
for _name, executor in name_executor.items():
required_facts = set([])

# record required facts
for fact_ in re.findall(BasePlanningService.re_variable, executor.test):
required_facts.add(fact_)
self.log.error(f'--------Required Fact: {fact_}')

# record produced facts
for parser in executor.parsers:
for parserconfig in parser.parserconfigs:
for fact_type in ['source', 'target', 'edge']:
fact_ = getattr(parserconfig, fact_type, False)
if fact_:
facts_produced.append(fact_)
# record required facts
for fact_ in re.findall(BasePlanningService.re_variable, executor.test):
required_facts.append(fact_)

# Required facts gap
# HERE
return None
all_produced_facts.add(fact_)
self.log.error(f'--------Produced Fact: {fact_}')

required_fact_diff = (required_facts - all_produced_facts) - NON_FACT_VARS
if required_fact_diff:
all_missing_facts.update(required_fact_diff)
missing_fact_reqs['detailed_missing_requirements'].append(dict(
step=i,
ability=ability.display,
missing_required_facts=required_fact_diff,
fullfillment_abilities=set([]),
fullfillment_tactics=set([])
))

fullfillment_abilities = await self.get_abilities_by_facts(produced_facts=all_missing_facts)
for fa in fullfillment_abilities:
fact, ability = fa
for missing_req in missing_fact_reqs['detailed_missing_requirements']:
if fact in missing_req['missing_required_facts']:
missing_req['fulfillment_abilities'].add(ability.display),
missing_req['fulfillment_tactics'].add(dict(tactic=ability['tactic'], technique=ability['technique'], technique_id=ability['technique_id']))

return missing_fact_reqs

async def get_abilities_by_facts(self, required_facts: list[str] = None, produced_facts: list[str] = None) -> list[tuple]:
"""
Args:
required_facts: required fact names (NOTE: Not Fact objects but names, i.e. Fact.name)
produced_facts: produced fact names (NOTE: Not Fact objects but names, i.e. Fact.name)
"""
abilities = dict(required={}, produced={})
if required_facts is None:
required_facts = []
if produced_facts is None:
produced_facts = []
if not (required_facts or produced_facts):
return abilities

for ability_ in await self.get_service('data_svc').locate('abilities', match=dict()):
executors = ability_.get_executors()
for _platform, name_executor in executors.items():
for _name, executor in name_executor.items():
# required facts
if getattr(executor.test, False):
for fact_ in re.findall(BasePlanningService.re_variable, executor.test):
if fact_ in required_facts:
if fact_ not in abilities['required']:
abilities['required'][fact_] = set([])
abilities['required'][fact_].add(ability_)

# produced facts
for parser in executor.parsers:
for parserconfig in parser.parserconfigs:
for fact_type in ['source', 'target', 'edge']:
fact_ = getattr(parserconfig, fact_type, False)
if fact_ in produced_facts:
if fact_ not in abilities['produced']:
abilities['produced'][fact_] = set([])
abilities['produced'][fact_].add(ability_)
return abilities

@staticmethod
async def sort_links(links):
Expand Down

0 comments on commit 3438ffc

Please sign in to comment.