blob: 7754bf977ec53cc84b30e20703a3155b486ecb1a [file]
import asyncio
import uuid
from marshmallow.schema import SchemaMeta
from typing import Any
from app.api.v2.managers.base_api_manager import BaseApiManager
from app.api.v2.responses import JsonHttpNotFound, JsonHttpForbidden, JsonHttpBadRequest
from app.objects.c_adversary import Adversary, AdversarySchema
from app.objects.c_operation import Operation, OperationSchema
from app.objects.c_planner import PlannerSchema
from app.objects.c_source import SourceSchema
from app.objects.c_ability import AbilitySchema
from app.objects.c_agent import Agent
from app.objects.secondclass.c_executor import Executor, ExecutorSchema
from app.objects.secondclass.c_link import Link
from app.utility.base_world import BaseWorld
class OperationApiManager(BaseApiManager):
def __init__(self, services):
super().__init__(data_svc=services['data_svc'], file_svc=services['file_svc'])
self.services = services
self.knowledge_svc = services['knowledge_svc']
async def get_operation_report(self, operation_id: str, access: dict, output: bool):
operation = await self.get_operation_object(operation_id, access)
report = await operation.report(file_svc=self._file_svc, data_svc=self._data_svc, output=output)
return report
async def get_operation_event_logs(self, operation_id: str, access: dict, output: bool):
operation = await self.get_operation_object(operation_id, access)
event_logs = await operation.event_logs(file_svc=self._file_svc, data_svc=self._data_svc, output=output)
return event_logs
async def create_object_from_schema(self, schema: SchemaMeta, data: dict,
access: BaseWorld.Access, existing_operation: Operation = None):
if data.get('state'):
await self.validate_operation_state(data, existing_operation)
operation = await self.setup_operation(data, access)
operation.set_start_details()
operation.store(self._data_svc.ram)
asyncio.get_event_loop().create_task(operation.run(self.services))
return operation
async def find_and_update_object(self, ram_key: str, data: dict, search: dict = None):
for obj in self.find_objects(ram_key, search):
new_obj = await self.update_object(obj, data)
return new_obj
async def update_object(self, obj: Any, data: dict):
await self.validate_operation_state(data, obj)
return super().update_object(obj, data)
async def get_operation_links(self, operation_id: str, access: dict):
operation = await self.get_operation_object(operation_id, access)
links = [link.display for link in operation.chain]
return links
async def get_operation_link(self, operation_id: str, link_id: str, access: dict):
operation = await self.get_operation_object(operation_id, access)
link = self.search_operation_for_link(operation, link_id)
return link.display
async def get_operation_link_result(self, operation_id: str, link_id: str, access: dict):
operation = await self.get_operation_object(operation_id, access)
link = self.search_operation_for_link(operation, link_id)
try:
result = self.services['file_svc'].read_result_file('%s' % link_id)
return dict(link=link.display, result=result)
except FileNotFoundError:
return dict(link=link.display, result='')
async def update_operation_link(self, operation_id: str, link_id: str, link_data: dict, access: BaseWorld.Access):
operation = await self.get_operation_object(operation_id, access)
link = self.search_operation_for_link(operation, link_id)
if link.access not in access['access']:
raise JsonHttpForbidden(f'Cannot update link {link_id} due to insufficient permissions.')
if link.is_finished() or link.can_ignore():
raise JsonHttpForbidden(f'Cannot update a finished link: {link_id}')
if link_data.get('command'):
command_str = link_data.get('command')
link.executor.command = command_str
link.ability = self.build_ability(link_data.get('ability', {}), link.executor)
link.command = self._encode_string(command_str)
if link_data.get('status'):
link_status = link_data['status']
if not link.is_valid_status(link_status):
raise JsonHttpBadRequest(f'Cannot update link {link_id} due to invalid link status.')
link.status = link_status
if link.can_ignore():
operation.add_ignored_link(link.id)
return link.display
async def create_potential_link(self, operation_id: str, data: dict, access: BaseWorld.Access):
self.validate_link_data(data)
operation = await self.get_operation_object(operation_id, access)
agent = await self.get_agent(operation, data)
if data['executor']['name'] not in agent.executors:
raise JsonHttpBadRequest(f'Agent {agent.paw} missing specified executor')
encoded_command = self._encode_string(agent.replace(self._encode_string(data['executor']['command']),
file_svc=self.services['file_svc']))
executor = self.build_executor(data=data.pop('executor', {}), agent=agent)
ability = self.build_ability(data=data.pop('ability', {}), executor=executor)
await self._call_ability_plugin_hooks(ability, executor)
link = Link.load(dict(command=encoded_command, plaintext_command=encoded_command, paw=agent.paw, ability=ability, executor=executor,
status=operation.link_status(), score=data.get('score', 0), jitter=data.get('jitter', 0),
cleanup=data.get('cleanup', 0), pin=data.get('pin', 0),
host=agent.host, deadman=data.get('deadman', False), used=data.get('used', []),
relationships=data.get('relationships', [])))
link.apply_id(agent.host)
await operation.apply(link)
return link.display
async def get_potential_links(self, operation_id: str, access: dict, paw: str = None):
operation = await self.get_operation_object(operation_id, access)
if operation.finish:
return []
if paw:
agents = [a for a in operation.agents if a.paw == paw]
if not agents:
raise JsonHttpNotFound(f'Agent not found: {paw}')
else:
agents = operation.agents
potential_abilities = await self.services['rest_svc'].build_potential_abilities(operation)
operation.potential_links = await self.services['rest_svc'].build_potential_links(operation, agents,
potential_abilities)
potential_links = [potential_link.display for potential_link in operation.potential_links]
return potential_links
async def get_operation_object(self, operation_id: str, access: dict):
try:
operation = (await self._data_svc.locate('operations', {'id': operation_id}))[0]
except IndexError:
raise JsonHttpNotFound(f'Operation not found: {operation_id}')
if operation.match(access):
return operation
raise JsonHttpForbidden(f'Cannot view operation due to insufficient permissions: {operation_id}')
async def setup_operation(self, data: dict, access: BaseWorld.Access):
"""Applies default settings to an operation if data is missing."""
planner_id = data.pop('planner', {}).get('id', '')
data['planner'] = await self._construct_and_dump_planner(planner_id)
adversary_id = data.pop('adversary', {}).get('adversary_id', '')
data['adversary'] = await self._construct_and_dump_adversary(adversary_id)
fact_source_id = data.pop('source', {}).get('id', '')
data['source'] = await self._construct_and_dump_source(fact_source_id)
operation = OperationSchema().load(data)
await operation.update_operation_agents(self.services)
allowed = self._get_allowed_from_access(access)
operation.access = allowed
return operation
async def _construct_and_dump_planner(self, planner_id: str):
planner = await self.services['data_svc'].locate('planners', match=dict(planner_id=planner_id))
if not planner:
planner = await self.services['data_svc'].locate('planners', match=dict(name='atomic'))
return PlannerSchema().dump(planner[0])
async def _construct_and_dump_adversary(self, adversary_id: str):
adv = await self.services['data_svc'].locate('adversaries', match=dict(adversary_id=adversary_id))
if not adv:
adv = Adversary.load(dict(adversary_id='ad-hoc', name='ad-hoc', description='an empty adversary profile',
atomic_ordering=[]))
else:
adv = adv[0]
return AdversarySchema().dump(adv)
async def _construct_and_dump_source(self, source_id: str):
source = await self.services['data_svc'].locate('sources', match=dict(id=source_id))
if not source:
source = (await self.services['data_svc'].locate('sources', match=dict(name='basic')))
return SourceSchema().dump(source[0])
async def _call_ability_plugin_hooks(self, ability, executor):
"""Calls any plugin hooks (at runtime) that exist for the ability and executor."""
if (hasattr(executor, 'HOOKS') and executor.HOOKS and
hasattr(executor, 'language') and executor.language and
executor.language in executor.HOOKS):
await executor.HOOKS[executor.language](ability, executor)
async def validate_operation_state(self, data: dict, existing: Operation = None):
if not existing:
if data.get('state') in Operation.get_finished_states():
raise JsonHttpBadRequest('Cannot create a finished operation.')
elif data.get('state') not in Operation.get_states():
raise JsonHttpBadRequest('state must be one of {}'.format(Operation.get_states()))
else:
if await existing.is_finished():
raise JsonHttpBadRequest('This operation has already finished.')
elif 'state' in data and data.get('state') not in Operation.get_states():
raise JsonHttpBadRequest('state must be one of {}'.format(Operation.get_states()))
def search_operation_for_link(self, operation: Operation, link_id: str):
for link in operation.chain:
if link.id == link_id:
return link
raise JsonHttpNotFound(f'Link {link_id} was not found in Operation {operation.id}')
def validate_link_data(self, link_data: dict):
if not link_data.get('executor'):
raise JsonHttpBadRequest('\'executor\' is a required field for link creation.')
if not link_data['executor'].get('name'):
raise JsonHttpBadRequest('\'name\' is a required field for link executor.')
if not link_data['executor'].get('command'):
raise JsonHttpBadRequest('\'command\' is a required field for link executor.')
if not link_data.get('paw'):
raise JsonHttpBadRequest('\'paw\' is a required field for link creation.')
async def get_agent(self, operation: Operation, data: dict):
paw = data['paw']
try:
agent = [a for a in operation.agents if a.paw == paw][0]
except IndexError:
raise JsonHttpNotFound(f'Agent {data["paw"]} was not found.')
return agent
def get_agents(self, operation: dict):
agents = {}
chain = operation.get('chain', [])
for link in chain:
paw = link.get('paw')
if paw and paw not in agents:
tmp_agent = self.find_object('agents', {'paw': paw}).display
tmp_agent['links'] = []
agents[paw] = tmp_agent
agents[paw]['links'].append(link)
return agents
async def get_hosts(self, operation: dict):
hosts = {}
chain = operation.get('chain', [])
for link in chain:
host = link.get('host')
if not host:
continue
if host not in hosts:
tmp_agent = self.find_object('agents', {'host': host}).display
tmp_host = {
'host': tmp_agent.get('host'),
'host_ip_addrs': tmp_agent.get('host_ip_addrs'),
'platform': tmp_agent.get('platform'),
'reachable_hosts': await self.get_reachable_hosts(agent=tmp_agent)
}
hosts[host] = tmp_host
return hosts
async def get_reachable_hosts(self, agent: dict = None, operation: dict = None):
"""
NOTE: When agent is supplied, only hosts discovered by agent
are retrieved.
"""
trait_names = BaseWorld.get_config('reachable_host_traits') or []
paws = ()
if agent is not None:
paws = paws + (agent.get('paw'),)
else:
for agent in operation.get('host_group', []):
paw = agent.get('paw')
if paw:
paws = paws + (paw,)
hosts = []
for trait in trait_names:
fqdns = await self.services['knowledge_svc'].get_facts({
'trait': trait,
'collected_by': paws,
})
for name in fqdns:
hosts.append(name.value)
return hosts
def build_executor(self, data: dict, agent: Agent):
if not data.get('timeout'):
data['timeout'] = 60
data['platform'] = agent.platform
executor = ExecutorSchema().load(data)
return executor
def build_ability(self, data: dict, executor: Executor):
if not data.get('ability_id'):
data['ability_id'] = str(uuid.uuid4())
if not data.get('tactic'):
data['tactic'] = 'auto-generated'
if not data.get('technique_id'):
data['technique_id'] = 'auto-generated'
if not data.get('technique_name'):
data['technique_name'] = 'auto-generated'
if not data.get('name'):
data['name'] = 'Manual Command'
if not data.get('description'):
data['description'] = 'Manual command ability'
data['executors'] = [ExecutorSchema().dump(executor)]
ability = AbilitySchema().load(data)
return ability