blob: 3fe3e7554decb5ed87c8332a976d20f1a3f8dfdd [file]
import asyncio
import os.path
import jinja2
import pytest
import random
import string
import uuid
import yaml
import aiohttp_apispec
import warnings
from datetime import datetime, timezone
from base64 import b64encode
from unittest import mock
from unittest.mock import AsyncMock
from aiohttp_apispec import validation_middleware
from aiohttp import web
import aiohttp_jinja2
from pathlib import Path
from app.api.v2.handlers.agent_api import AgentApi
from app.api.v2.handlers.ability_api import AbilityApi
from app.api.v2.handlers.objective_api import ObjectiveApi
from app.api.v2.handlers.adversary_api import AdversaryApi
from app.api.v2.handlers.operation_api import OperationApi
from app.api.v2.handlers.contact_api import ContactApi
from app.api.v2.handlers.obfuscator_api import ObfuscatorApi
from app.api.v2.handlers.plugins_api import PluginApi
from app.api.v2.handlers.fact_source_api import FactSourceApi
from app.api.v2.handlers.fact_api import FactApi
from app.api.v2.handlers.planner_api import PlannerApi
from app.api.v2.handlers.health_api import HealthApi
from app.api.v2.handlers.schedule_api import ScheduleApi
from app.api.v2.handlers.payload_api import PayloadApi
from app.objects.c_obfuscator import Obfuscator
from app.objects.c_objective import Objective
from app.objects.c_planner import PlannerSchema
from app.objects.c_source import SourceSchema, Source
from app.service.app_svc import AppService
from app.service.auth_svc import AuthService
from app.service.data_svc import DataService
from app.service.contact_svc import ContactService
from app.service.event_svc import EventService
from app.service.file_svc import FileSvc
from app.service.learning_svc import LearningService
from app.service.planning_svc import PlanningService
from app.service.rest_svc import RestService
from app.service.knowledge_svc import KnowledgeService
from app.objects.c_adversary import Adversary, AdversarySchema
from app.objects.c_ability import Ability, AbilitySchema
from app.objects.c_operation import Operation, OperationSchema
from app.objects.c_plugin import Plugin
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.objects.secondclass.c_fact import Fact
from app.objects.secondclass.c_relationship import Relationship
from app.objects.secondclass.c_rule import Rule
from app.utility.base_object import BaseObject
from app.utility.base_service import BaseService
from app.utility.base_world import BaseWorld
from app.api.v2.responses import json_request_validation_middleware
from app.api.v2.security import authentication_required_middleware_factory
from app.api.v2.responses import apispec_request_validation_middleware
from app.api.rest_api import RestApi
from app import version
DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_DIR = os.path.join(DIR, '..', 'conf')
@pytest.fixture(scope='session')
def init_base_world():
with open(os.path.join(CONFIG_DIR, 'default.yml')) as c:
BaseWorld.apply_config('main', yaml.load(c, Loader=yaml.FullLoader))
BaseWorld.apply_config('agents', BaseWorld.strip_yml(os.path.join(CONFIG_DIR, 'agents.yml'))[0])
BaseWorld.apply_config('payloads', BaseWorld.strip_yml(os.path.join(CONFIG_DIR, 'payloads.yml'))[0])
@pytest.fixture
async def app_svc():
# async def _init_app_svc():
# return AppService(None)
# def _app_svc(event_loop):
# return event_loop.run_until_complete(_init_app_svc())
# return _app_svc
return AppService(None)
@pytest.fixture(scope='class')
def data_svc():
return DataService()
@pytest.fixture(scope='class')
def knowledge_svc():
return KnowledgeService()
@pytest.fixture(scope='class')
def file_svc():
return FileSvc()
@pytest.fixture
async def contact_svc():
contact_svc = ContactService()
yield contact_svc
await contact_svc.deregister_contacts()
@pytest.fixture
def event_svc(contact_svc, init_base_world):
return EventService()
@pytest.fixture
async def rest_svc():
"""
The REST service requires the test's loop in order to be initialized in the same Thread
as the test. This mitigates the issue where the service's calls to `asyncio.get_event_loop`
would result in a RuntimeError indicating that there is no currentevent loop in the main
thread.
"""
async def _init_rest_svc():
return RestService()
def _rest_svc(event_loop):
return event_loop.run_until_complete(_init_rest_svc())
return _rest_svc
# return RestService()
@pytest.fixture(scope='class')
def planning_svc():
return PlanningService()
@pytest.fixture(scope='class')
def learning_svc():
return LearningService()
@pytest.fixture(scope='class')
def services(app_svc):
return app_svc.get_services()
@pytest.fixture(scope='class')
def mocker():
return mock
@pytest.fixture
def adversary():
def _generate_adversary(adversary_id=None, name=None, description=None, phases=None):
if not adversary_id:
adversary_id = uuid.uuid4()
if not name:
name = ''.join(random.choice(string.ascii_uppercase) for _ in range(10))
if not description:
description = "description"
if not phases:
phases = dict()
return Adversary(adversary_id=adversary_id, name=name, description=description, atomic_ordering=phases)
return _generate_adversary
@pytest.fixture
def executor():
def _generate_executor(name='psh', platform='windows', *args, **kwargs):
return Executor(name, platform, *args, **kwargs)
return _generate_executor
@pytest.fixture
def ability():
def _generate_ability(ability_id=None, *args, **kwargs):
if not ability_id:
ability_id = random.randint(0, 999999)
return Ability(*args, ability_id=ability_id, **kwargs)
return _generate_ability
@pytest.fixture
def operation():
def _generate_operation(name, agents, adversary, *args, **kwargs):
return Operation(*args, name=name, agents=agents, adversary=adversary, **kwargs)
return _generate_operation
@pytest.fixture
def demo_operation(event_loop, data_svc, operation, adversary):
tadversary = event_loop.run_until_complete(data_svc.store(adversary()))
return operation(name='my first op', agents=[], adversary=tadversary)
@pytest.fixture
def obfuscator(event_loop, data_svc):
event_loop.run_until_complete(data_svc.store(
Obfuscator(name='plain-text',
description='Does no obfuscation to any command, instead running it in plain text',
module='app.obfuscators.plain_text')
)
)
@pytest.fixture
def agent():
def _generate_agent(sleep_min, sleep_max, watchdog, *args, **kwargs):
return Agent(*args, sleep_min=sleep_min, sleep_max=sleep_max, watchdog=watchdog, **kwargs)
return _generate_agent
@pytest.fixture
def link():
def _generate_link(command, paw, ability, executor, *args, **kwargs):
return Link.load(dict(*args, ability=ability, executor=executor, command=command, paw=paw, **kwargs))
return _generate_link
@pytest.fixture
def fact():
def _generate_fact(trait, *args, **kwargs):
return Fact(*args, trait=trait, **kwargs)
return _generate_fact
@pytest.fixture
def rule():
def _generate_rule(action, trait, *args, **kwargs):
return Rule(*args, action=action, trait=trait, **kwargs)
return _generate_rule
@pytest.fixture
def relationship():
def _generate_relationship(source, edge, target, *args, **kwargs):
return Relationship(*args, source=source, edge=edge, target=target, **kwargs)
return _generate_relationship
@pytest.fixture
def demo_plugin():
def _generate_plugin(enabled=False, gui=False, data_dir=None, access=None):
name = ''.join(random.choice(string.ascii_lowercase) for _ in range(10))
desc = 'this is a good description'
address = '/plugin/%s/gui' % name if gui else None
return Plugin(name=name, description=desc, address=address, enabled=enabled, data_dir=data_dir, access=access)
return _generate_plugin
@pytest.fixture
def agent_profile():
def _agent_profile(paw=None, group='red', platform='linux', executors=None, privilege='Elevated'):
if not executors:
executors = ['sh']
return dict(
server='http://127.0.0.1:8888',
username='username',
group=group,
host='hostname',
platform=platform,
architecture='x86_64',
location='/path/to/agent',
pid=random.randint(2, 32768),
ppid=random.randint(2, 32768),
executors=executors,
privilege=privilege,
exe_name='agent-exe-name',
paw=paw
)
return _agent_profile
@pytest.fixture
def app_config():
return {
'app.contact.dns.domain': 'mycaldera.caldera',
'app.contact.dns.socket': '0.0.0.0:8853',
'app.contact.html': '/weather',
'app.contact.http': '0.0.0.0:8888',
'app.contact.tcp': '0.0.0.0:7010',
'app.contact.tunnel.ssh.socket': '0.0.0.0:8022',
'app.contact.udp': '0.0.0.0:7013',
'app.contact.websocket': '0.0.0.0:7012',
'plugins': [
'stockpile',
'atomic'
],
'host': '0.0.0.0',
'auth.login.handler.module': 'default',
'users': {
'red': {
'red': 'password-foo'
},
'blue': {
'blue': 'password-bar'
}
}
}
@pytest.fixture
def agent_config():
return {
'sleep_min': '30',
'sleep_max': '60',
'untrusted_timer': '90',
'watchdog': '0',
'implant_name': 'splunkd',
'deadman_abilities': [
'this-is-a-fake-ability'
],
'bootstrap_abilities': [
'this-is-another-fake-ability'
]
}
@pytest.fixture
async def api_v2_client(aiohttp_client, contact_svc):
def make_app(svcs):
warnings.filterwarnings(
"ignore",
message="Multiple schemas resolved to the name"
)
app = web.Application(
middlewares=[
authentication_required_middleware_factory(svcs['auth_svc']),
json_request_validation_middleware
]
)
AgentApi(svcs).add_routes(app)
AbilityApi(svcs).add_routes(app)
OperationApi(svcs).add_routes(app)
AdversaryApi(svcs).add_routes(app)
ContactApi(svcs).add_routes(app)
ObjectiveApi(svcs).add_routes(app)
ObfuscatorApi(svcs).add_routes(app)
PluginApi(svcs).add_routes(app)
FactApi(svcs).add_routes(app)
FactSourceApi(svcs).add_routes(app)
PlannerApi(svcs).add_routes(app)
HealthApi(svcs).add_routes(app)
ScheduleApi(svcs).add_routes(app)
PayloadApi(svcs).add_routes(app)
return app
async def initialize():
with open(Path(__file__).parents[1] / 'conf' / 'default.yml', 'r') as fle:
BaseWorld.apply_config('main', yaml.safe_load(fle))
with open(Path(__file__).parents[1] / 'conf' / 'payloads.yml', 'r') as fle:
BaseWorld.apply_config('payloads', yaml.safe_load(fle))
with open(Path(__file__).parents[1] / 'conf' / 'agents.yml', 'r') as fle:
BaseWorld.apply_config('agents', yaml.safe_load(fle))
app_svc = AppService(web.Application(client_max_size=5120 ** 2))
_ = DataService()
_ = RestService()
_ = PlanningService()
_ = KnowledgeService()
_ = LearningService()
auth_svc = AuthService()
_ = FileSvc()
_ = EventService()
services = app_svc.get_services()
os.chdir(str(Path(__file__).parents[1]))
_ = await RestApi(services).enable()
await app_svc.register_contacts()
await auth_svc.apply(app_svc.application, auth_svc.get_config('users'))
await auth_svc.set_login_handlers(services)
app_svc.register_subapp('/api/v2', make_app(svcs=services))
aiohttp_apispec.setup_aiohttp_apispec(
app=app_svc.application,
title='Caldera',
version=version.get_version(),
swagger_path='/api/docs',
url='/api/docs/swagger.json',
static_path='/static/swagger'
)
app_svc.application.middlewares.append(apispec_request_validation_middleware)
app_svc.application.middlewares.append(validation_middleware)
templates = ['plugins/%s/templates' % p.lower() for p in app_svc.get_config('plugins')]
templates.append('plugins/magma/dist')
templates.append("templates")
aiohttp_jinja2.setup(app_svc.application, loader=jinja2.FileSystemLoader(templates))
return app_svc
app_svc = await initialize()
app = app_svc.application
yield await aiohttp_client(app)
await app_svc._destroy_plugins()
@pytest.fixture
def api_cookies(event_loop, api_v2_client):
async def get_cookie():
r = await api_v2_client.post('/enter', allow_redirects=False, data=dict(username='admin', password='admin'))
return r.cookies
return event_loop.run_until_complete(get_cookie())
@pytest.fixture
def async_return():
def _async_return(return_param):
f = asyncio.Future()
f.set_result(return_param)
return f
return _async_return
@pytest.fixture
def mock_time():
return datetime(2021, 1, 1, tzinfo=timezone.utc)
@pytest.fixture
def parse_datestring():
def _parse_datestring(datestring):
return datetime.strptime(datestring, BaseObject.TIME_FORMAT)
return _parse_datestring
@pytest.fixture
def test_adversary(event_loop):
expected_adversary = {'name': 'ad-hoc',
'description': 'an empty adversary profile',
'adversary_id': 'ad-hoc',
'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc',
'tags': [],
'has_repeatable_abilities': False}
test_adversary = AdversarySchema().load(expected_adversary)
event_loop.run_until_complete(BaseService.get_service('data_svc').store(test_adversary))
return test_adversary
@pytest.fixture
def test_planner(event_loop):
expected_planner = {'name': 'test planner',
'description': 'test planner',
'module': 'test',
'stopping_conditions': [],
'params': {},
'allow_repeatable_abilities': False,
'ignore_enforcement_modules': [],
'id': '123'}
test_planner = PlannerSchema().load(expected_planner)
event_loop.run_until_complete(BaseService.get_service('data_svc').store(test_planner))
return test_planner
@pytest.fixture
def test_source(event_loop):
test_fact = Fact(trait='remote.host.fqdn', value='dc')
test_source = Source(id='123', name='test', facts=[test_fact], adjustments=[])
event_loop.run_until_complete(BaseService.get_service('data_svc').store(test_source))
return test_source
@pytest.fixture
def test_source_existing_relationships(event_loop):
test_fact_1 = Fact(trait='test_1', value='1')
test_fact_2 = Fact(trait='test_2', value='2')
test_relationship = Relationship(source=test_fact_1, edge='test_edge', target=test_fact_2)
test_source = Source(id='123', name='test', facts=[test_fact_1, test_fact_2], adjustments=[],
relationships=[test_relationship])
event_loop.run_until_complete(BaseService.get_service('data_svc').store(test_source))
return test_source
@pytest.fixture
def test_operation(test_adversary, test_planner, test_source):
expected_operation = {'name': '123',
'adversary': AdversarySchema().dump(test_adversary),
'state': 'paused',
'id': '123',
'group': 'red',
'autonomous': 0,
'planner': PlannerSchema().dump(test_planner),
'source': SourceSchema().dump(test_source),
'jitter': '2/8',
'visibility': 50,
'auto_close': False,
'obfuscator': 'plain-text',
'use_learning_parsers': False}
return expected_operation
@pytest.fixture
def test_agent(event_loop):
agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['sh'], platform='linux')
event_loop.run_until_complete(BaseService.get_service('data_svc').store(agent))
return agent
@pytest.fixture
def test_executor(test_agent):
return ExecutorSchema().load(dict(timeout=60, platform=test_agent.platform, name='sh', command='ls'))
@pytest.fixture
def test_ability(test_executor, event_loop):
ability = AbilitySchema().load(dict(ability_id='123',
tactic='discovery',
technique_id='auto-generated',
technique_name='auto-generated',
name='Manual Command',
description='test ability',
executors=[ExecutorSchema().dump(test_executor)]))
event_loop.run_until_complete(BaseService.get_service('data_svc').store(ability))
return ability
@pytest.fixture
def expected_link_output():
return 'test_dir'
@pytest.fixture
def active_link(test_executor, test_agent, test_ability):
return {
'command': str(b64encode(test_executor.command.encode()), 'utf-8'),
'paw': test_agent.paw,
'ability': test_ability,
'executor': test_executor,
'score': 0,
'jitter': 0,
'cleanup': 0,
'pin': 0,
'host': test_agent.host,
'deadman': False,
'used': [],
'id': '456',
'relationships': []
}
@pytest.fixture
def finished_link(test_executor, test_agent, test_ability):
return {
'command': str(b64encode(test_executor.command.encode()), 'utf-8'),
'paw': test_agent.paw,
'ability': test_ability,
'executor': test_executor,
'host': test_agent.host,
'deadman': False,
'used': [],
'id': '789',
'relationships': [],
'status': 0,
'output': 'test_dir'
}
@pytest.fixture
def finished_operation_payload(test_operation):
op_id = "00000000-0000-0000-0000-000000000000"
test_operation['id'] = op_id
test_operation['name'] = op_id
test_operation['state'] = 'finished'
return test_operation
@pytest.fixture
def setup_finished_operation(event_loop, finished_operation_payload):
finished_operation = OperationSchema().load(finished_operation_payload)
event_loop.run_until_complete(BaseService.get_service('data_svc').store(finished_operation))
@pytest.fixture
def setup_operations_api_test(event_loop, api_v2_client, test_operation, test_agent, test_ability,
active_link, finished_link, expected_link_output):
test_operation = OperationSchema().load(test_operation)
test_operation.agents.append(test_agent)
test_operation.set_start_details()
test_link = Link.load(active_link)
test_link.host = test_agent.host
finished_link = Link.load(finished_link)
finished_link.output = expected_link_output
finished_link.host = test_agent.host
test_operation.chain.append(test_link)
test_operation.chain.append(finished_link)
test_objective = Objective(id='123', name='test objective', description='test', goals=[])
test_operation.objective = test_objective
event_loop.run_until_complete(BaseService.get_service('data_svc').store(test_operation))
@pytest.fixture
def setup_empty_operation(event_loop, test_operation):
test_operation = OperationSchema().load(test_operation)
test_operation.set_start_details()
test_objective = Objective(id='123', name='test objective', description='test', goals=[])
test_operation.objective = test_objective
event_loop.run_until_complete(BaseService.get_service('data_svc').store(test_operation))
@pytest.fixture()
def fire_event_mock(event_svc):
"""A mock for event_svc.fire_event()
fire_event() wont work in tests as underlying Application
is a stub so mock call here
"""
event_svc.fire_event = AsyncMock(return_value=None)