├── data ├── abilities │ └── .gitkeep ├── payloads │ └── .gitkeep ├── results │ └── .gitkeep ├── sources │ └── .gitkeep ├── adversaries │ └── .gitkeep └── objectives │ └── .gitkeep ├── tests ├── objects │ ├── __init__.py │ ├── test_fact.py │ ├── test_agent.py │ ├── test_objective.py │ └── test_ability.py ├── parsers │ ├── __init__.py │ └── test_parsers.py ├── services │ ├── __init__.py │ ├── test_file_svc.py │ ├── test_learning_svc.py │ ├── test_contact_svc.py │ └── test_data_svc.py ├── __init__.py ├── contacts │ └── test_contact_gist.py ├── utility │ └── test_base_world.py ├── web_server │ └── test_core_endpoints.py └── conftest.py ├── VERSION.txt ├── conf ├── payloads.yml ├── agents.yml └── default.yml ├── static ├── img │ ├── duk.png │ ├── x.png │ ├── facts.png │ ├── group.png │ ├── linux.png │ ├── plus.png │ ├── back-red.png │ ├── compass.png │ ├── contact.png │ ├── darwin.png │ ├── errors.png │ ├── executor.png │ ├── expand.png │ ├── favicon.png │ ├── hacker.png │ ├── payload.png │ ├── planner.png │ ├── planners.png │ ├── recycle.png │ ├── success.png │ ├── switch.png │ ├── weather.jpg │ ├── windows.png │ ├── back-blue.png │ ├── back-grey.jpg │ ├── operation.png │ ├── obfuscation.png │ └── additional-fields.png ├── jquery │ └── images │ │ ├── ui-icons_444444_256x240.png │ │ ├── ui-icons_555555_256x240.png │ │ ├── ui-icons_777620_256x240.png │ │ ├── ui-icons_777777_256x240.png │ │ ├── ui-icons_cc0000_256x240.png │ │ └── ui-icons_ffffff_256x240.png ├── css │ ├── blue.css │ ├── red.css │ ├── login.css │ ├── navigate.css │ ├── modal.css │ ├── multi-select.css │ └── timeline.css └── js │ ├── ability.js │ └── shared.js ├── requirements-dev.txt ├── .coveragerc ├── .codecov.yml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── greetings.yml │ ├── stale.yml │ └── codeql-analysis.yml └── pull_request_template.md ├── .flake8 ├── app ├── objects │ ├── interfaces │ │ └── i_object.py │ ├── secondclass │ │ ├── c_result.py │ │ ├── c_requirement.py │ │ ├── c_variation.py │ │ ├── c_rule.py │ │ ├── c_visibility.py │ │ ├── c_instruction.py │ │ ├── c_parser.py │ │ ├── c_relationship.py │ │ ├── c_parserconfig.py │ │ ├── c_goal.py │ │ └── c_fact.py │ ├── c_schedule.py │ ├── c_obfuscator.py │ ├── c_objective.py │ ├── c_planner.py │ ├── c_source.py │ ├── c_adversary.py │ └── c_plugin.py ├── learning │ ├── p_path.py │ └── p_ip.py ├── utility │ ├── base_service.py │ ├── base_obfuscator.py │ ├── payload_encoder.py │ ├── config_generator.py │ ├── base_parser.py │ ├── rule_set.py │ ├── base_object.py │ ├── file_decryptor.py │ └── base_world.py ├── service │ ├── interfaces │ │ ├── i_contact_svc.py │ │ ├── i_learning_svc.py │ │ ├── i_event_svc.py │ │ ├── i_auth_svc.py │ │ ├── i_planning_svc.py │ │ ├── i_app_svc.py │ │ ├── i_data_svc.py │ │ ├── i_rest_svc.py │ │ └── i_file_svc.py │ ├── learning_svc.py │ └── event_svc.py ├── contacts │ ├── handles │ │ └── h_beacon.py │ ├── contact_websocket.py │ ├── contact_http.py │ ├── contact_html.py │ ├── contact_udp.py │ └── contact_tcp.py └── api │ └── packs │ ├── advanced.py │ └── campaign.py ├── .pre-commit-config.yaml ├── .dockerignore ├── .travis.yml ├── requirements.txt ├── docker-compose.yml ├── .gitignore ├── Dockerfile ├── release.sh ├── templates ├── obfuscators.html ├── contacts.html ├── login.html ├── planners.html ├── configurations.html ├── BLUE.html └── RED.html ├── .gitmodules ├── tox.ini ├── CONTRIBUTING.md ├── server.py └── README.md /data/abilities/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/payloads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/results/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/sources/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/adversaries/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/objectives/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 2.9.0-796cbdc26559a50869d042e0a442f1c7 2 | -------------------------------------------------------------------------------- /conf/payloads.yml: -------------------------------------------------------------------------------- 1 | special_payloads: {} 2 | standard_payloads: {} 3 | extensions: {} -------------------------------------------------------------------------------- /static/img/duk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/duk.png -------------------------------------------------------------------------------- /static/img/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/x.png -------------------------------------------------------------------------------- /static/img/facts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/facts.png -------------------------------------------------------------------------------- /static/img/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/group.png -------------------------------------------------------------------------------- /static/img/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/linux.png -------------------------------------------------------------------------------- /static/img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/plus.png -------------------------------------------------------------------------------- /static/img/back-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/back-red.png -------------------------------------------------------------------------------- /static/img/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/compass.png -------------------------------------------------------------------------------- /static/img/contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/contact.png -------------------------------------------------------------------------------- /static/img/darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/darwin.png -------------------------------------------------------------------------------- /static/img/errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/errors.png -------------------------------------------------------------------------------- /static/img/executor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/executor.png -------------------------------------------------------------------------------- /static/img/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/expand.png -------------------------------------------------------------------------------- /static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/favicon.png -------------------------------------------------------------------------------- /static/img/hacker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/hacker.png -------------------------------------------------------------------------------- /static/img/payload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/payload.png -------------------------------------------------------------------------------- /static/img/planner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/planner.png -------------------------------------------------------------------------------- /static/img/planners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/planners.png -------------------------------------------------------------------------------- /static/img/recycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/recycle.png -------------------------------------------------------------------------------- /static/img/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/success.png -------------------------------------------------------------------------------- /static/img/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/switch.png -------------------------------------------------------------------------------- /static/img/weather.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/weather.jpg -------------------------------------------------------------------------------- /static/img/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/windows.png -------------------------------------------------------------------------------- /static/img/back-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/back-blue.png -------------------------------------------------------------------------------- /static/img/back-grey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/back-grey.jpg -------------------------------------------------------------------------------- /static/img/operation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/operation.png -------------------------------------------------------------------------------- /static/img/obfuscation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/obfuscation.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | tox 2 | pytest 3 | pytest-aiohttp==0.3.0 4 | coverage 5 | pre-commit 6 | safety 7 | bandit 8 | -------------------------------------------------------------------------------- /static/img/additional-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/img/additional-fields.png -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | . 4 | omit = 5 | *tests* 6 | *.tox* 7 | *venv* 8 | [html] 9 | directory = _htmlcov -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 5% 6 | patch: off 7 | -------------------------------------------------------------------------------- /static/jquery/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/jquery/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /static/jquery/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/jquery/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /static/jquery/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/jquery/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /static/jquery/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/jquery/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /static/jquery/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/jquery/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /static/jquery/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/caldera/master/static/jquery/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /static/css/blue.css: -------------------------------------------------------------------------------- 1 | :root { 2 | background-color: black; 3 | --navbar-color: #191970; 4 | } 5 | 6 | body { 7 | background-image: url('/gui/img/back-blue.png'); 8 | } -------------------------------------------------------------------------------- /static/css/red.css: -------------------------------------------------------------------------------- 1 | :root { 2 | background-color: black; 3 | --navbar-color: #750b20; 4 | } 5 | 6 | body { 7 | background-image: url('/gui/img/back-red.png'); 8 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Documentation 3 | url: https://caldera.readthedocs.io/en/latest/ 4 | about: Your question may be answered in the documentation 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | 4 | class AsyncMock(MagicMock): 5 | async def __call__(self, *args, **kwargs): 6 | return super(AsyncMock, self).__call__(*args, **kwargs) 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 180 3 | exclude = 4 | .svn, 5 | CVS, 6 | .bzr, 7 | .hg, 8 | .git, 9 | __pycache__, 10 | .tox, 11 | venv, 12 | .venv 13 | 14 | per-file-ignores = 15 | app/service/file_svc.py:B305 -------------------------------------------------------------------------------- /app/objects/interfaces/i_object.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class FirstClassObjectInterface(abc.ABC): 5 | 6 | @property 7 | @abc.abstractmethod 8 | def unique(self): 9 | pass 10 | 11 | @abc.abstractmethod 12 | def store(self, ram): 13 | pass 14 | -------------------------------------------------------------------------------- /conf/agents.yml: -------------------------------------------------------------------------------- 1 | bootstrap_abilities: 2 | - 43b3754c-def4-4699-a673-1d85648fda6a 3 | implant_name: splunkd 4 | sleep_max: 60 5 | sleep_min: 30 6 | untrusted_timer: 90 7 | watchdog: 0 8 | deployments: 9 | - 2f34977d-9558-4c12-abad-349716777c6b #54ndc47 10 | - 356d1722-7784-40c4-822b-0cf864b0b36d #Manx 11 | - 0ab383be-b819-41bf-91b9-1bd4404d83bf #Ragdoll -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://gitlab.com/pycqa/flake8 3 | rev: 3.7.7 4 | hooks: 5 | - id: flake8 6 | additional_dependencies: [flake8-bugbear] 7 | - repo: https://github.com/PyCQA/bandit 8 | rev: 1.6.2 9 | hooks: 10 | - id: bandit 11 | entry: bandit -ll --exclude=tests/ --skip=B322 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # common 2 | **/README.md 3 | **/CONTRIBUTING.md 4 | docker-compose.yml 5 | Dockerfile 6 | 7 | # git 8 | **/.git 9 | **/.gitattributes 10 | **/.gitignore 11 | **/.gitmodules 12 | **/.github 13 | 14 | # dev 15 | tests/ 16 | **/.codecov.yml 17 | **/.coveragerc 18 | **/.flake8 19 | **/.pre-commit-config.yaml 20 | **/.travis.yml 21 | **/release.sh 22 | **/requirements-dev.txt 23 | **/tox.ini 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U00002753 Question" 3 | about: Support questions 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: 3.6 6 | env: TOXENV=py36,style,coverage-ci,safety 7 | 8 | - python: 3.7 9 | env: TOXENV=py37,style,coverage-ci,safety 10 | 11 | - python: 3.8 12 | env: TOXENV=py38,style,coverage-ci,safety 13 | 14 | install: 15 | - pip install --upgrade virtualenv 16 | - pip install tox 17 | 18 | script: 19 | - tox 20 | -------------------------------------------------------------------------------- /app/learning/p_path.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from app.objects.secondclass.c_fact import Fact 4 | 5 | 6 | class Parser: 7 | 8 | def __init__(self): 9 | self.trait = 'host.file.path' 10 | 11 | def parse(self, blob): 12 | for p in re.findall(r'(\/.*?\.[\w:]+[^\s]+)', blob): 13 | yield Fact.load(dict(trait=self.trait, value=p)) 14 | for p in re.findall(r'(C:\\.*?\.[\w:]+)', blob): 15 | yield Fact.load(dict(trait=self.trait, value=p)) 16 | -------------------------------------------------------------------------------- /app/utility/base_service.py: -------------------------------------------------------------------------------- 1 | from app.utility.base_world import BaseWorld 2 | 3 | 4 | class BaseService(BaseWorld): 5 | 6 | _services = dict() 7 | 8 | def add_service(self, name, svc): 9 | self.__class__._services[name] = svc 10 | return self.create_logger(name) 11 | 12 | @classmethod 13 | def get_service(cls, name): 14 | return cls._services.get(name) 15 | 16 | @classmethod 17 | def get_services(cls): 18 | return cls._services 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp-jinja2==1.2.0 2 | aiohttp==3.6.2 3 | aiohttp_session==2.9.0 4 | aiohttp-security==0.4.0 5 | jinja2==2.10.3 6 | pyyaml>=5.1 7 | cryptography>=3.2 8 | websockets==8.1 9 | Sphinx==3.0.4 10 | sphinx_rtd_theme==0.4.3 11 | recommonmark==0.6.0 12 | marshmallow==3.5.1 13 | dirhash==0.2.0 14 | docker==4.2.0 15 | donut-shellcode==0.9.2 16 | marshmallow-enum==1.5.1 17 | ldap3==2.8.1 18 | lxml~=4.6.2 # debrief 19 | reportlab==3.5.49 # debrief 20 | svglib==1.0.1 # debrief 21 | -------------------------------------------------------------------------------- /static/css/login.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | *:focus { 5 | outline: none; 6 | } 7 | .login { 8 | border: 1px solid var(--secondary-background); 9 | margin: 200px auto; 10 | width: 300px; 11 | } 12 | .login-screen { 13 | background-color: #FFF; 14 | padding: 20px; 15 | border-radius: 5px 16 | } 17 | .app-title { 18 | text-align: center; 19 | color: #777; 20 | } 21 | .login-form { 22 | text-align: center; 23 | } 24 | .control-group { 25 | margin-bottom: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | caldera: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | TZ: "UTC" #TZ sets timezone for ubuntu setup 10 | WIN_BUILD: "false" #WIN_BUILD is used to enable windows build in sandcat plugin 11 | image: caldera:latest 12 | ports: 13 | - "8888:8888" 14 | - "7010:7010" 15 | - "7011:7011/udp" 16 | - "7012:7012" 17 | volumes: 18 | - ./:/usr/src/app 19 | command: --fresh --insecure 20 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Looks like your first issue -- we aim to respond to issues as quickly as possible. In the meantime, check out our documentation here: http://caldera.readthedocs.io/' 13 | pr-message: 'Wohoo! Your first PR -- thanks for contributing!' 14 | -------------------------------------------------------------------------------- /app/service/interfaces/i_contact_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class ContactServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def register(self, contact): 8 | pass 9 | 10 | @abc.abstractmethod 11 | def handle_heartbeat(self): 12 | """ 13 | Accept all components of an agent profile and save a new agent or register an updated heartbeat. 14 | :return: the agent object, instructions to execute 15 | """ 16 | pass 17 | 18 | @abc.abstractmethod 19 | def build_filename(self): 20 | pass 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.crt 3 | *.exe 4 | *.pyd 5 | *.DS_Store 6 | *.spec 7 | *.pstat 8 | *.tokens 9 | *__pycache__* 10 | .history/* 11 | .idea/* 12 | logs/* 13 | !logs/.gitkeep 14 | core.db 15 | .venv/ 16 | venv/ 17 | calderaenv/ 18 | .env 19 | conf/*.yml 20 | !conf/default.yml 21 | data/object_store 22 | data/results/* 23 | !data/results/.gitkeep 24 | data/payloads/* 25 | !data/payloads/.gitkeep 26 | data/facts/*yml 27 | !data/facts/.gitkeep 28 | data/sources/* 29 | !data/sources/.gitkeep 30 | data/objectives/* 31 | !data/objectives/.gitkeep 32 | .tox/ 33 | 34 | # coverage reports 35 | htmlcov/ 36 | .coverage 37 | .coverage.* 38 | *,cover 39 | _*/ 40 | -------------------------------------------------------------------------------- /app/service/interfaces/i_learning_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class LearningServiceInterface(abc.ABC): 5 | 6 | @staticmethod 7 | @abc.abstractmethod 8 | def add_parsers(directory): 9 | pass 10 | 11 | @abc.abstractmethod 12 | def build_model(self): 13 | """ 14 | The model is a static set of all variables used inside all ability commands 15 | This can be used to determine which facts - when found together - are more likely to be used together 16 | :return: 17 | """ 18 | pass 19 | 20 | @abc.abstractmethod 21 | def learn(self, facts, link, blob): 22 | pass 23 | -------------------------------------------------------------------------------- /app/contacts/handles/h_beacon.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | class Handle: 5 | 6 | def __init__(self, tag): 7 | self.tag = tag 8 | 9 | @staticmethod 10 | async def run(message, services, caller): 11 | callback = message.pop('callback', None) 12 | message['executors'] = [e for e in message.get('executors', '').split(',') if e] 13 | message['contact'] = 'udp' 14 | await services.get('contact_svc').handle_heartbeat(**message) 15 | 16 | if callback: 17 | sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) 18 | sock.sendto('roger'.encode(), (caller, int(callback))) 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | ARG TZ="UTC" 4 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ 5 | echo $TZ > /etc/timezone 6 | 7 | WORKDIR /usr/src/app 8 | 9 | RUN apt-get update && \ 10 | apt-get -y install python3 python3-pip golang git 11 | 12 | #WIN_BUILD is used to enable windows build in sandcat plugin 13 | ARG WIN_BUILD=false 14 | RUN if [ "$WIN_BUILD" = "true" ] ; then apt-get -y install mingw-w64; fi 15 | 16 | ADD requirements.txt . 17 | 18 | RUN pip3 install --no-cache-dir -r requirements.txt 19 | 20 | ADD . . 21 | 22 | EXPOSE 8888 23 | EXPOSE 7010 24 | EXPOSE 7011/udp 25 | EXPOSE 7012 26 | 27 | ENTRYPOINT ["python3", "server.py"] 28 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_result.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class ResultSchema(ma.Schema): 7 | id = ma.fields.String() 8 | output = ma.fields.String() 9 | pid = ma.fields.String() 10 | status = ma.fields.String() 11 | 12 | @ma.post_load 13 | def build_result(self, data, **_): 14 | return Result(**data) 15 | 16 | 17 | class Result(BaseObject): 18 | 19 | schema = ResultSchema() 20 | 21 | def __init__(self, id, output, pid=0, status=0): 22 | super().__init__() 23 | self.id = id 24 | self.output = output 25 | self.pid = pid 26 | self.status = status 27 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function update_version(){ 4 | git reset --hard origin/master && git checkout master && git pull 5 | newHash=$(dirhash . -a md5 -i "/plugins/ .*/ _*/" -m "*.py *.html *.js *.go") 6 | echo "${1}-${newHash}" > VERSION.txt 7 | } 8 | 9 | read -p "[+] Enter a new version: " newVersion 10 | 11 | for d in plugins/* ; do 12 | read -p "[+] Release $d (y/n)?" CONT 13 | if [ "$CONT" = "n" ]; then 14 | echo "[!] Skipping ${d}..." 15 | continue 16 | fi 17 | 18 | cd $d 19 | update_version $newVersion 20 | cd - > /dev/null 21 | done 22 | 23 | for d in plugins/* ; do 24 | git add $d 25 | done 26 | update_version $newVersion 27 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_requirement.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class RequirementSchema(ma.Schema): 7 | 8 | module = ma.fields.String() 9 | relationship_match = ma.fields.List(ma.fields.Dict()) 10 | 11 | @ma.post_load() 12 | def build_requirement(self, data, **_): 13 | return Requirement(**data) 14 | 15 | 16 | class Requirement(BaseObject): 17 | 18 | schema = RequirementSchema() 19 | 20 | @property 21 | def unique(self): 22 | return self.module 23 | 24 | def __init__(self, module, relationship_match): 25 | super().__init__() 26 | self.module = module 27 | self.relationship_match = relationship_match 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: wbooth 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Mac, Windows, Kali] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 2.8.0] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /app/service/interfaces/i_event_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class EventServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def observe_event(self, event, callback): 8 | 9 | """ 10 | Register an event handler 11 | :param event: The event topic and (optional) subtopic, separated by a '/' 12 | :param callback: The function that will handle the event 13 | :return: None 14 | """ 15 | pass 16 | 17 | @abc.abstractmethod 18 | def fire_event(self, event, **callback_kwargs): 19 | """ 20 | Fire an event 21 | :param event: The event topic and (optional) subtopic, separated by a '/' 22 | :param callback_kwargs: Any additional parameters to pass to the event handler 23 | :return: None 24 | """ 25 | pass 26 | -------------------------------------------------------------------------------- /tests/objects/test_fact.py: -------------------------------------------------------------------------------- 1 | from app.objects.secondclass.c_fact import Fact 2 | 3 | 4 | class TestFact: 5 | 6 | def test_escaped_cmd(self): 7 | test_fact = Fact('test', 'test value| &') 8 | assert test_fact.escaped('cmd') == 'test^ value^|^ ^&' 9 | assert test_fact.escaped('cmd') != 'test value| &' 10 | 11 | def test_escaped_sh(self): 12 | test_fact = Fact('test', 'test value| &') 13 | test_dupe = test_fact.escaped('sh').replace('\\', '*') 14 | assert test_dupe == 'test* value*|* *&' 15 | assert test_fact.escaped('sh') != 'test value| &' 16 | 17 | def test_escaped_psh(self): 18 | test_fact = Fact('test', 'test value| &') 19 | assert test_fact.escaped('psh') == 'test` value`|` `&' 20 | assert test_fact.escaped('psh') != 'test value| &' 21 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_variation.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class VariationSchema(ma.Schema): 7 | 8 | description = ma.fields.String() 9 | command = ma.fields.String() 10 | 11 | @ma.post_load 12 | def build_variation(self, data, **_): 13 | return Variation(**data) 14 | 15 | 16 | class Variation(BaseObject): 17 | 18 | schema = VariationSchema() 19 | 20 | @property 21 | def command(self): 22 | return self.replace_app_props(self._command) 23 | 24 | @property 25 | def raw_command(self): 26 | return self.decode_bytes(self._command) 27 | 28 | def __init__(self, description, command): 29 | super().__init__() 30 | self.description = description 31 | self._command = self.encode_string(command) 32 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-label: 'no-issue-activity' 17 | stale-pr-label: 'no-pr-activity' 18 | stale-pr-message: 'This issue is stale because it has been open 20 days with no activity. Remove stale label or comment or this will be closed in 5 days' 19 | stale-issue-message: 'This issue is stale because it has been open 20 days with no activity. Remove stale label or comment or this will be closed in 5 days' 20 | exempt-issue-labels: 'feature,keep' 21 | days-before-stale: 20 22 | days-before-close: 5 23 | -------------------------------------------------------------------------------- /app/learning/p_ip.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ipaddress import ip_address 4 | 5 | from app.objects.secondclass.c_fact import Fact 6 | 7 | 8 | class Parser: 9 | 10 | def __init__(self): 11 | self.trait = 'host.ip.address' 12 | 13 | def parse(self, blob): 14 | for ip in re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', blob): 15 | if self._is_valid_ip(ip): 16 | yield Fact.load(dict(trait=self.trait, value=ip)) 17 | 18 | @staticmethod 19 | def _is_valid_ip(raw_ip): 20 | try: 21 | # The following hardcoded addresses are not used to bind to an interface. 22 | if raw_ip in ['0.0.0.0', '127.0.0.1']: # nosec 23 | return False 24 | ip_address(raw_ip) 25 | except BaseException: 26 | return False 27 | return True 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | (insert summary) 4 | 5 | ## Type of change 6 | 7 | Please delete options that are not relevant. 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | ## How Has This Been Tested? 15 | 16 | Please describe the tests that you ran to verify your changes. 17 | 18 | 19 | ## Checklist: 20 | 21 | - [ ] My code follows the style guidelines of this project 22 | - [ ] I have performed a self-review of my own code 23 | - [ ] I have made corresponding changes to the documentation 24 | - [ ] I have added tests that prove my fix is effective or that my feature works 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 New Feature Request" 3 | about: Propose a new feature 4 | title: '' 5 | labels: feature 6 | assignees: 'wbooth' 7 | 8 | --- 9 | 10 | **What problem are you trying to solve? Please describe.** 11 | > Eg. I'm always frustrated when [...] 12 | 13 | 14 | **The ideal solution: What should the feature should do?** 15 | > a clear and concise description 16 | 17 | 18 | **What category of feature is this?** 19 | 20 | - [ ] UI/UX 21 | - [ ] API 22 | - [ ] Other 23 | 24 | **If you have code or pseudo-code please provide:** 25 | 26 | 27 | ```python 28 | 29 | ``` 30 | 31 | - [ ] Willing to submit a pull request to implement this feature? 32 | 33 | **Additional context** 34 | Add any other context or screenshots about the feature request here. 35 | 36 | Thank you for your contribution! 37 | -------------------------------------------------------------------------------- /app/utility/base_obfuscator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import hashlib 3 | 4 | from app.utility.base_world import BaseWorld 5 | 6 | 7 | class BaseObfuscator(BaseWorld): 8 | 9 | def __init__(self, agent): 10 | self.agent = agent 11 | 12 | def run(self, link, **kwargs): 13 | agent = self.__getattribute__('agent') 14 | supported_platforms = self.__getattribute__('supported_platforms') 15 | try: 16 | if agent.platform in supported_platforms and link.ability.executor in agent.executors: 17 | link.command_hash = hashlib.sha256(str.encode(link.command)).hexdigest() 18 | o = self.__getattribute__(link.ability.executor) 19 | return o(link, **kwargs) 20 | except Exception: 21 | logging.error('Failed to run BaseObfuscator, returning default decoded bytes') 22 | 23 | return self.decode_bytes(link.command) 24 | -------------------------------------------------------------------------------- /tests/parsers/test_parsers.py: -------------------------------------------------------------------------------- 1 | from app.learning.p_ip import Parser as p_ip 2 | from app.learning.p_path import Parser as p_path 3 | 4 | 5 | class TestFile: 6 | 7 | def test_ip(self): 8 | parser = p_ip() 9 | results = list(parser.parse('this ip 10.1.2.3 and 5.3.4.6 will be parsed, but 0.0.0.0, 1.2.3 and 127.0.0.1 will not')) 10 | assert len(results) == 2 11 | assert '10.1.2.3' in [r.value for r in results] 12 | assert '5.3.4.6' in [r.value for r in results] 13 | 14 | def test_path(self): 15 | parser = p_path() 16 | results = list(parser.parse('first /some/file/path/one.txt for linux and ' 17 | '/Users/tricky/rsc.io_quote_v1.5.1.txt')) 18 | assert len(results) == 2 19 | assert '/some/file/path/one.txt' in [r.value for r in results] 20 | assert '/Users/tricky/rsc.io_quote_v1.5.1.txt' in [r.value for r in results] 21 | -------------------------------------------------------------------------------- /conf/default.yml: -------------------------------------------------------------------------------- 1 | ability_refresh: 60 2 | api_key_blue: BLUEADMIN123 3 | api_key_red: ADMIN123 4 | app.contact.gist: API_KEY 5 | app.contact.html: /weather 6 | app.contact.http: http://0.0.0.0:8888 7 | app.contact.tcp: 0.0.0.0:7010 8 | app.contact.udp: 0.0.0.0:7011 9 | app.contact.websocket: 0.0.0.0:7012 10 | crypt_salt: REPLACE_WITH_RANDOM_VALUE 11 | encryption_key: ADMIN123 12 | exfil_dir: /tmp 13 | host: 0.0.0.0 14 | plugins: 15 | - access 16 | - atomic 17 | - compass 18 | - debrief 19 | - fieldmanual 20 | - gameboard 21 | - manx 22 | - response 23 | - sandcat 24 | - stockpile 25 | - training 26 | port: 8888 27 | reports_dir: /tmp 28 | requirements: 29 | go: 30 | command: go version 31 | type: installed_program 32 | version: 1.11 33 | python: 34 | attr: version 35 | module: sys 36 | type: python_module 37 | version: 3.6.1 38 | users: 39 | blue: 40 | blue: admin 41 | red: 42 | admin: admin 43 | red: admin 44 | -------------------------------------------------------------------------------- /tests/contacts/test_contact_gist.py: -------------------------------------------------------------------------------- 1 | from app.contacts.contact_gist import Contact 2 | from app.utility.base_world import BaseWorld 3 | 4 | 5 | class TestContactGist: 6 | 7 | def test_retrieve_config(self, loop, app_svc): 8 | BaseWorld.apply_config(name='main', config={'app.contact.gist': 'arandomkeythatisusedtoconnecttogithubapi', 9 | 'plugins': ['sandcat', 'stockpile'], 10 | 'crypt_salt': 'BLAH', 11 | 'api_key': 'ADMIN123', 12 | 'encryption_key': 'ADMIN123', 13 | 'exfil_dir': '/tmp'}) 14 | gist_c2 = Contact(app_svc(loop).get_services()) 15 | loop.run_until_complete(gist_c2.start()) 16 | assert gist_c2.retrieve_config() == 'arandomkeythatisusedtoconnecttogithubapi' 17 | -------------------------------------------------------------------------------- /app/objects/c_schedule.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.objects.interfaces.i_object import FirstClassObjectInterface 4 | from app.utility.base_object import BaseObject 5 | 6 | 7 | class ScheduleSchema(ma.Schema): 8 | 9 | name = ma.fields.String() 10 | schedule = ma.fields.Time() 11 | task = ma.fields.Function(lambda obj: obj.task.display) 12 | 13 | 14 | class Schedule(FirstClassObjectInterface, BaseObject): 15 | schema = ScheduleSchema() 16 | 17 | @property 18 | def unique(self): 19 | return self.hash('%s' % self.name) 20 | 21 | def __init__(self, name, schedule, task): 22 | super().__init__() 23 | self.name = name 24 | self.schedule = schedule 25 | self.task = task 26 | 27 | def store(self, ram): 28 | existing = self.retrieve(ram['schedules'], self.unique) 29 | if not existing: 30 | ram['schedules'].append(self) 31 | return self.retrieve(ram['schedules'], self.unique) 32 | return existing 33 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_rule.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | from app.utility.rule_set import RuleAction 5 | 6 | 7 | class RuleActionField(ma.fields.Field): 8 | """ 9 | Custom field to handle the RuleAction Enum. 10 | """ 11 | 12 | def _serialize(self, value, attr, obj, **kwargs): 13 | if value is None: 14 | return None 15 | return value.value 16 | 17 | def _deserialize(self, value, attr, data, **kwargs): 18 | return RuleAction[value] 19 | 20 | 21 | class RuleSchema(ma.Schema): 22 | 23 | trait = ma.fields.String() 24 | match = ma.fields.String() 25 | action = RuleActionField() 26 | 27 | @ma.post_load 28 | def build_rule(self, data, **_): 29 | return Rule(**data) 30 | 31 | 32 | class Rule(BaseObject): 33 | 34 | schema = RuleSchema() 35 | 36 | def __init__(self, action, trait, match='.*'): 37 | super().__init__() 38 | self.action = action 39 | self.trait = trait 40 | self.match = match 41 | -------------------------------------------------------------------------------- /app/service/interfaces/i_auth_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class AuthServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def apply(self, app, users): 8 | """ 9 | Set up security on server boot 10 | :param app: 11 | :param users: 12 | :return: None 13 | """ 14 | pass 15 | 16 | @staticmethod 17 | @abc.abstractmethod 18 | def logout_user(request): 19 | """ 20 | Log the user out 21 | :param request: 22 | :return: None 23 | """ 24 | pass 25 | 26 | @abc.abstractmethod 27 | def login_user(self, request): 28 | """ 29 | Kick off all scheduled jobs, as their schedule determines 30 | :return: 31 | """ 32 | pass 33 | 34 | @abc.abstractmethod 35 | def check_permissions(self, group, request): 36 | """ 37 | Check if a request is allowed based on the user permissions 38 | :param group: 39 | :param request: 40 | :return: None 41 | """ 42 | pass 43 | 44 | @abc.abstractmethod 45 | def get_permissions(self, request): 46 | pass 47 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_visibility.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class VisibilitySchema(ma.Schema): 7 | 8 | score = ma.fields.Integer() 9 | adjustments = ma.fields.List(ma.fields.Integer()) 10 | 11 | @ma.post_load 12 | def build_visibility(self, data, **_): 13 | return Visibility(**data) 14 | 15 | 16 | class Visibility(BaseObject): 17 | 18 | MIN_SCORE = 1 19 | MAX_SCORE = 100 20 | 21 | schema = VisibilitySchema() 22 | 23 | @property 24 | def display(self): 25 | return self.clean(dict(score=self.score)) 26 | 27 | @property 28 | def score(self): 29 | total_score = self._score + sum([a.offset for a in self.adjustments]) 30 | if total_score > self.MAX_SCORE: 31 | return self.MAX_SCORE 32 | elif total_score < self.MIN_SCORE: 33 | return self.MIN_SCORE 34 | return total_score 35 | 36 | def __init__(self): 37 | super().__init__() 38 | self._score = 50 39 | self.adjustments = [] 40 | 41 | def apply(self, adjustment): 42 | self.adjustments.append(adjustment) 43 | -------------------------------------------------------------------------------- /templates/obfuscators.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Obfuscators are designed for evasion

6 |

7 | When running an operation, you can select an obfuscator. By default, plain-text is 8 | selected. During the operation, before the agent collects an instruction, the server will wrap it 9 | with the obfuscation technique chosen - including instructions for how to unpack it 10 | before execution. 11 |

12 |

13 | 14 | {% for o in obfuscators %} 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |

{{ o.name }}

{{ o.description }}
22 |
23 |
24 |
-------------------------------------------------------------------------------- /app/contacts/contact_websocket.py: -------------------------------------------------------------------------------- 1 | import websockets 2 | 3 | from app.utility.base_world import BaseWorld 4 | 5 | 6 | class Contact(BaseWorld): 7 | 8 | def __init__(self, services): 9 | self.name = 'websocket' 10 | self.description = 'Accept data through web sockets' 11 | self.log = self.create_logger('contact_websocket') 12 | self.handler = Handler(services) 13 | 14 | async def start(self): 15 | web_socket = self.get_config('app.contact.websocket') 16 | try: 17 | await websockets.serve(self.handler.handle, *web_socket.split(':')) 18 | except OSError as e: 19 | self.log.error("WebSocket error: {}".format(e)) 20 | 21 | 22 | class Handler: 23 | 24 | def __init__(self, services): 25 | self.services = services 26 | self.handles = [] 27 | self.log = BaseWorld.create_logger('websocket_handler') 28 | 29 | async def handle(self, socket, path): 30 | try: 31 | for handle in [h for h in self.handles if path.split('/', 1)[1].startswith(h.tag)]: 32 | await handle.run(socket, path, self.services) 33 | except Exception as e: 34 | self.log.debug(e) 35 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_instruction.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class InstructionSchema(ma.Schema): 7 | id = ma.fields.String() 8 | sleep = ma.fields.Int() 9 | command = ma.fields.String() 10 | executor = ma.fields.String() 11 | timeout = ma.fields.Int() 12 | payloads = ma.fields.List(ma.fields.String()) 13 | deadman = ma.fields.Boolean() 14 | 15 | @ma.post_load 16 | def build_instruction(self, data, **_): 17 | return Instruction(**data) 18 | 19 | 20 | class Instruction(BaseObject): 21 | 22 | schema = InstructionSchema() 23 | 24 | @property 25 | def display(self): 26 | return self.clean(dict(id=self.id, sleep=self.sleep, command=self.command, executor=self.executor, 27 | timeout=self.timeout, payloads=self.payloads, deadman=self.deadman)) 28 | 29 | def __init__(self, id, command, executor, payloads=None, sleep=0, timeout=60, deadman=False): 30 | super().__init__() 31 | self.id = id 32 | self.sleep = sleep 33 | self.command = command 34 | self.executor = executor 35 | self.timeout = timeout 36 | self.payloads = payloads if payloads else [] 37 | self.deadman = deadman 38 | -------------------------------------------------------------------------------- /app/service/interfaces/i_planning_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class PlanningServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def get_links(self, operation, buckets, agent, trim): 8 | """ 9 | For an operation and agent combination, create links (that can be executed). 10 | When no agent is supplied, links for all agents are returned 11 | :param operation: 12 | :param buckets: 13 | :param agent: 14 | :param trim: call trim_links() on list of links before returning 15 | :return: a list of links 16 | """ 17 | pass 18 | 19 | @abc.abstractmethod 20 | def get_cleanup_links(self, operation, agent): 21 | """ 22 | For a given operation, create all cleanup links. 23 | If agent is supplied, only return cleanup links for that agent. 24 | :param operation: 25 | :param agent: 26 | :return: None 27 | """ 28 | pass 29 | 30 | @abc.abstractmethod 31 | def generate_and_trim_links(self, agent, operation, abilities, trim): 32 | pass 33 | 34 | @staticmethod 35 | @abc.abstractmethod 36 | def sort_links(self, links): 37 | """ 38 | Sort links by their score then by the order they are defined in an adversary profile 39 | """ 40 | pass 41 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_parser.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.objects.secondclass.c_parserconfig import ParserConfig, ParserConfigSchema 4 | from app.utility.base_object import BaseObject 5 | 6 | 7 | class ParserSchema(ma.Schema): 8 | 9 | module = ma.fields.String() 10 | parserconfigs = ma.fields.List(ma.fields.Nested(ParserConfigSchema())) 11 | 12 | @ma.pre_load 13 | def fix_relationships(self, parser, **_): 14 | if 'relationships' in parser: 15 | parser['parserconfigs'] = parser.pop('relationships') 16 | return parser 17 | 18 | @ma.post_load() 19 | def build_parser(self, data, **_): 20 | return Parser(**data) 21 | 22 | @ma.post_dump() 23 | def prepare_parser(self, data, **_): 24 | data['relationships'] = data.pop('parserconfigs') 25 | for pc, index in enumerate(data['relationships']): 26 | if isinstance(pc, ParserConfig): 27 | data['relationships'][index] = pc.display 28 | return data 29 | 30 | 31 | class Parser(BaseObject): 32 | 33 | schema = ParserSchema() 34 | 35 | @property 36 | def unique(self): 37 | return self.module 38 | 39 | def __init__(self, module, parserconfigs): 40 | super().__init__() 41 | self.module = module 42 | self.parserconfigs = parserconfigs 43 | -------------------------------------------------------------------------------- /app/contacts/contact_http.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from aiohttp import web 5 | 6 | from app.utility.base_world import BaseWorld 7 | 8 | 9 | class Contact(BaseWorld): 10 | 11 | def __init__(self, services): 12 | self.name = 'http' 13 | self.description = 'Accept beacons through a REST API endpoint' 14 | self.app_svc = services.get('app_svc') 15 | self.contact_svc = services.get('contact_svc') 16 | 17 | async def start(self): 18 | self.app_svc.application.router.add_route('POST', '/beacon', self._beacon) 19 | 20 | """ PRIVATE """ 21 | 22 | async def _beacon(self, request): 23 | try: 24 | profile = json.loads(self.contact_svc.decode_bytes(await request.read())) 25 | profile['paw'] = profile.get('paw') 26 | profile['contact'] = 'http' 27 | agent, instructions = await self.contact_svc.handle_heartbeat(**profile) 28 | response = dict(paw=agent.paw, 29 | sleep=await agent.calculate_sleep(), 30 | watchdog=agent.watchdog, 31 | instructions=json.dumps([json.dumps(i.display) for i in instructions])) 32 | return web.Response(text=self.contact_svc.encode_string(json.dumps(response))) 33 | except Exception as e: 34 | logging.error('Malformed beacon: %s' % e) 35 | -------------------------------------------------------------------------------- /app/contacts/contact_html.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from aiohttp_jinja2 import template 4 | 5 | from app.utility.base_world import BaseWorld 6 | 7 | 8 | class Contact(BaseWorld): 9 | 10 | def __init__(self, services): 11 | self.name = 'html' 12 | self.description = 'Accept beacons through an HTML page' 13 | self.app_svc = services.get('app_svc') 14 | self.contact_svc = services.get('contact_svc') 15 | 16 | async def start(self): 17 | self.app_svc.application.router.add_route('*', self.get_config('app.contact.html'), self._accept_beacon) 18 | 19 | """ PRIVATE """ 20 | 21 | @template('weather.html') 22 | async def _accept_beacon(self, request): 23 | try: 24 | profile = json.loads(self.decode_bytes(await request.text())) 25 | profile['paw'] = profile.get('paw') 26 | profile['contact'] = 'html' 27 | agent, instructions = await self.contact_svc.handle_heartbeat(**profile) 28 | response = dict(paw=agent.paw, 29 | sleep=await agent.calculate_sleep(), 30 | watchdog=agent.watchdog, 31 | instructions=json.dumps([json.dumps(i.display) for i in instructions])) 32 | return dict(instructions=self.encode_string(json.dumps(response))) 33 | except Exception: 34 | return dict(instructions=[]) 35 | -------------------------------------------------------------------------------- /app/objects/c_obfuscator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from importlib import import_module 3 | 4 | import marshmallow as ma 5 | 6 | from app.objects.interfaces.i_object import FirstClassObjectInterface 7 | from app.utility.base_object import BaseObject 8 | 9 | 10 | class ObfuscatorSchema(ma.Schema): 11 | 12 | name = ma.fields.String() 13 | description = ma.fields.String() 14 | module = ma.fields.String() 15 | 16 | 17 | class Obfuscator(FirstClassObjectInterface, BaseObject): 18 | schema = ObfuscatorSchema() 19 | display_schema = ObfuscatorSchema(exclude=['module']) 20 | 21 | @property 22 | def unique(self): 23 | return self.hash('%s' % self.name) 24 | 25 | def __init__(self, name, description, module): 26 | super().__init__() 27 | self.name = name 28 | self.description = description 29 | self.module = module 30 | 31 | def store(self, ram): 32 | existing = self.retrieve(ram['obfuscators'], self.unique) 33 | if not existing: 34 | ram['obfuscators'].append(self) 35 | return self.retrieve(ram['obfuscators'], self.unique) 36 | return existing 37 | 38 | def load(self, agent): 39 | try: 40 | mod = import_module(self.module) 41 | return mod.Obfuscation(agent) 42 | except Exception as e: 43 | logging.error('Error importing obfuscator=%s, %s' % (self.name, e)) 44 | -------------------------------------------------------------------------------- /static/css/navigate.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | padding: 25px; 3 | } 4 | .sidenav { 5 | height: 100%; 6 | width: 0; 7 | position: fixed; 8 | z-index: 110; 9 | top: 0; 10 | left: 0; 11 | background-color: #111; 12 | overflow-x: hidden; 13 | white-space: nowrap; 14 | transition: 0.4s; 15 | padding-top: 10px; 16 | } 17 | .sidenav a { 18 | padding: 8px 8px 8px 32px; 19 | text-decoration: none; 20 | font-size: 16px; 21 | color: #818181; 22 | display: block; 23 | transition: 0.3s; 24 | cursor: pointer; 25 | } 26 | .sidenav a:hover { 27 | color: #f1f1f1; 28 | } 29 | .sidenav .closebtn { 30 | position: absolute; 31 | top: 5px; 32 | right: 5px; 33 | font-size: 36px; 34 | margin-left: 50px; 35 | } 36 | .sidenav img { 37 | height: 80px; 38 | border-radius: 45%; 39 | } 40 | @media screen and (max-height: 450px) { 41 | .sidenav {padding-top: 5px;} 42 | .sidenav a {font-size: 14px;} 43 | } 44 | .nav-header { 45 | width: 250px; 46 | background-color: var(--navbar-color); 47 | } 48 | .nav-header p { 49 | font-size: 16px; 50 | padding: 2px; 51 | text-align: center; 52 | color: var(--font-color); 53 | } 54 | .footer { 55 | position: fixed; 56 | left: 0; 57 | bottom: 0; 58 | width: 100%; 59 | z-index: 105; 60 | background-color: var(--navbar-color); 61 | color: white; 62 | text-align: center; 63 | } 64 | .footer p { 65 | font-size:14px; 66 | margin:5px; 67 | } -------------------------------------------------------------------------------- /app/objects/secondclass/c_relationship.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | from app.objects.secondclass.c_fact import FactSchema 5 | 6 | 7 | class RelationshipSchema(ma.Schema): 8 | 9 | unique = ma.fields.String() 10 | source = ma.fields.Nested(FactSchema()) 11 | edge = ma.fields.String() 12 | target = ma.fields.Nested(FactSchema()) 13 | score = ma.fields.Integer() 14 | 15 | @ma.post_load 16 | def build_relationship(self, data, **_): 17 | return Relationship(**data) 18 | 19 | 20 | class Relationship(BaseObject): 21 | 22 | schema = RelationshipSchema() 23 | load_schema = RelationshipSchema(exclude=['unique']) 24 | 25 | @property 26 | def unique(self): 27 | return '%s%s%s' % (self.source, self.edge, self.target) 28 | 29 | @classmethod 30 | def from_json(cls, json): 31 | return cls(source=json['source'], edge=json.get('edge'), target=json.get('target'), score=json.get('score')) 32 | 33 | @property 34 | def display(self): 35 | return self.clean(dict(source=self.source, edge=self.edge, 36 | target=[self.target if self.target else 'Not Used'][0], score=self.score)) 37 | 38 | def __init__(self, source, edge=None, target=None, score=1): 39 | super().__init__() 40 | self.source = source 41 | self.edge = edge 42 | self.target = target 43 | self.score = score 44 | -------------------------------------------------------------------------------- /templates/contacts.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Contacts are touch-points for agents

6 |

7 | A contact is a connection point on the server for agents to communicate through. Agents can be 8 | custom written against one (or multiple) contacts. Each contact logs all agent connections - and 9 | all commands it hands out. Download a report for any contact below. 10 |

11 |

12 | 13 | {% for c in contacts %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |

{{ c.name }}

{{ c.description }}
22 |
23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | Login | Access 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 27 |
28 |
29 | 30 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /app/contacts/contact_udp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | from app.contacts.handles.h_beacon import Handle 5 | from app.utility.base_world import BaseWorld 6 | 7 | 8 | class Contact(BaseWorld): 9 | 10 | def __init__(self, services): 11 | self.name = 'udp' 12 | self.description = 'Accept streaming messages via UDP' 13 | self.log = self.create_logger('contact_udp') 14 | self.contact_svc = services.get('contact_svc') 15 | self.handler = Handler(services) 16 | 17 | async def start(self): 18 | loop = asyncio.get_event_loop() 19 | udp = self.get_config('app.contact.udp') 20 | addr, port = udp.split(':') 21 | loop.create_task(loop.create_datagram_endpoint(lambda: self.handler, local_addr=(addr, port))) 22 | 23 | 24 | class Handler(asyncio.DatagramProtocol): 25 | 26 | def __init__(self, services): 27 | super().__init__() 28 | self.services = services 29 | self.handles = [ 30 | Handle(tag='beacon') 31 | ] 32 | self.log = BaseWorld.create_logger('udp_handler') 33 | 34 | def datagram_received(self, data, addr): 35 | async def handle_msg(): 36 | try: 37 | message = json.loads(data.decode()) 38 | for handle in [h for h in self.handles if h.tag == message.pop('tag')]: 39 | await handle.run(message, self.services, addr[0]) 40 | except Exception as e: 41 | self.log.debug(e) 42 | asyncio.get_event_loop().create_task(handle_msg()) 43 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_parserconfig.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class ParserConfigSchema(ma.Schema): 7 | 8 | class Meta: 9 | unknown = ma.INCLUDE 10 | 11 | source = ma.fields.String() 12 | edge = ma.fields.String(missing=None) 13 | target = ma.fields.String(missing=None) 14 | custom_parser_vals = ma.fields.Mapping(keys=ma.fields.String(), values=ma.fields.String()) 15 | 16 | @ma.pre_load 17 | def check_edge_target(self, in_data, **_): 18 | if all(k in in_data.keys() for k in ['edge', 'target']) \ 19 | and (in_data['edge'] is None) and (in_data['target'] is not None): 20 | raise ma.ValidationError('Target provided without an edge.') 21 | return in_data 22 | 23 | @ma.post_load() 24 | def build_parserconfig(self, data, **_): 25 | return ParserConfig(**data) 26 | 27 | @ma.pre_dump() 28 | def remove_nones(self, data, **_): 29 | data.source = data.source or '' 30 | data.edge = data.edge or '' 31 | data.target = data.target or '' 32 | data.custom_parser_vals = data.custom_parser_vals or {} 33 | return data 34 | 35 | 36 | class ParserConfig(BaseObject): 37 | 38 | schema = ParserConfigSchema() 39 | 40 | def __init__(self, source, edge=None, target=None, custom_parser_vals=None): 41 | super().__init__() 42 | self.source = source 43 | self.edge = edge 44 | self.target = target 45 | self.custom_parser_vals = custom_parser_vals 46 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "plugins/sandcat"] 2 | path = plugins/sandcat 3 | url = https://github.com/mitre/sandcat.git 4 | [submodule "plugins/stockpile"] 5 | path = plugins/stockpile 6 | url = https://github.com/mitre/stockpile.git 7 | [submodule "plugins/ssl"] 8 | path = plugins/ssl 9 | url = https://github.com/mitre/ssl.git 10 | [submodule "plugins/caltack"] 11 | path = plugins/caltack 12 | url = https://github.com/mitre/caltack.git 13 | [submodule "plugins/mock"] 14 | path = plugins/mock 15 | url = https://github.com/mitre/mock.git 16 | [submodule "plugins/compass"] 17 | path = plugins/compass 18 | url = https://github.com/mitre/compass.git 19 | [submodule "plugins/access"] 20 | path = plugins/access 21 | url = https://github.com/mitre/access.git 22 | [submodule "plugins/atomic"] 23 | path = plugins/atomic 24 | url = https://github.com/mitre/atomic.git 25 | [submodule "plugins/response"] 26 | path = plugins/response 27 | url = https://github.com/mitre/response.git 28 | [submodule "plugins/gameboard"] 29 | path = plugins/gameboard 30 | url = https://github.com/mitre/gameboard.git 31 | [submodule "plugins/manx"] 32 | path = plugins/manx 33 | url = https://github.com/mitre/manx.git 34 | [submodule "plugins/training"] 35 | path = plugins/training 36 | url = https://github.com/mitre/training.git 37 | [submodule "plugins/builder"] 38 | path = plugins/builder 39 | url = https://github.com/mitre/builder.git 40 | [submodule "plugins/human"] 41 | path = plugins/human 42 | url = https://github.com/mitre/human.git 43 | [submodule "plugins/fieldmanual"] 44 | path = plugins/fieldmanual 45 | url = https://github.com/mitre/fieldmanual.git 46 | [submodule "plugins/debrief"] 47 | path = plugins/debrief 48 | url = https://github.com/mitre/debrief.git 49 | -------------------------------------------------------------------------------- /app/service/interfaces/i_app_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class AppServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def start_sniffer_untrusted_agents(self): 8 | """ 9 | Cyclic function that repeatedly checks if there are agents to be marked as untrusted 10 | :return: None 11 | """ 12 | pass 13 | 14 | @abc.abstractmethod 15 | def find_link(self, unique): 16 | """ 17 | Locate a given link by its unique property 18 | :param unique: 19 | :return: 20 | """ 21 | pass 22 | 23 | @abc.abstractmethod 24 | def find_op_with_link(self, link_id): 25 | """ 26 | Locate an operation with the given link ID 27 | :param link_id: 28 | :return: Operation or None 29 | """ 30 | 31 | @abc.abstractmethod 32 | def run_scheduler(self): 33 | """ 34 | Kick off all scheduled jobs, as their schedule determines 35 | :return: 36 | """ 37 | pass 38 | 39 | @abc.abstractmethod 40 | def resume_operations(self): 41 | """ 42 | Resume all unfinished operations 43 | :return: None 44 | """ 45 | pass 46 | 47 | @abc.abstractmethod 48 | def load_plugins(self, plugins): 49 | """ 50 | Store all plugins in the data store 51 | :return: 52 | """ 53 | pass 54 | 55 | @abc.abstractmethod 56 | def retrieve_compiled_file(self, name, platform): 57 | pass 58 | 59 | @abc.abstractmethod 60 | def teardown(self): 61 | pass 62 | 63 | @abc.abstractmethod 64 | def register_contacts(self): 65 | pass 66 | 67 | @abc.abstractmethod 68 | def load_plugin_expansions(self, plugins): 69 | pass 70 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_goal.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | 6 | class GoalSchema(ma.Schema): 7 | 8 | target = ma.fields.String() 9 | value = ma.fields.String() 10 | count = ma.fields.Integer() 11 | achieved = ma.fields.Boolean() 12 | operator = ma.fields.String() 13 | 14 | @ma.post_load 15 | def build_goal(self, data, **_): 16 | return Goal(**data) 17 | 18 | 19 | class Goal(BaseObject): 20 | 21 | schema = GoalSchema() 22 | MAX_GOAL_COUNT = 2**20 23 | 24 | @staticmethod 25 | def parse_operator(operator): 26 | if operator == '<': 27 | return lambda x, y: x < y 28 | if operator == '>': 29 | return lambda x, y: x > y 30 | if operator == '<=': 31 | return lambda x, y: x <= y 32 | if operator == '>=': 33 | return lambda x, y: x >= y 34 | if operator == 'in': 35 | return lambda x, y: x in y 36 | if operator == '*': 37 | return lambda x, y: True 38 | return lambda x, y: x == y 39 | 40 | def satisfied(self, all_facts=None): 41 | temp_count = 0 42 | for fact in (all_facts or []): 43 | if self.target == fact.trait and self.parse_operator(self.operator)(self.value, fact.value): 44 | temp_count += 1 45 | if temp_count >= self.count: 46 | self.achieved = True 47 | return self.achieved 48 | 49 | def __init__(self, target='exhaustion', value='complete', count=None, operator='=='): 50 | super().__init__() 51 | self.target = target 52 | self.value = value 53 | self.count = count if count is not None else self.MAX_GOAL_COUNT 54 | self.achieved = False 55 | self.operator = operator 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | skipsdist = True 8 | envlist = 9 | py{35,36,37,38,3} 10 | style 11 | coverage 12 | safety 13 | bandit 14 | skip_missing_interpreters = true 15 | 16 | [testenv] 17 | description = run tests 18 | passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* 19 | deps = 20 | -rrequirements.txt 21 | virtualenv!=20.0.22 22 | pre-commit 23 | pytest 24 | pytest-aiohttp 25 | coverage 26 | codecov 27 | commands = 28 | coverage run -p -m pytest --tb=short -Werror tests 29 | 30 | [testenv:py38] 31 | description = run tests 32 | passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_* 33 | deps = 34 | -rrequirements.txt 35 | virtualenv!=20.0.22 36 | pre-commit 37 | pytest 38 | pytest-aiohttp 39 | coverage 40 | codecov 41 | commands = 42 | coverage run -p -m pytest --tb=short tests 43 | 44 | [testenv:style] 45 | deps = pre-commit 46 | skip_install = true 47 | commands = 48 | pre-commit run --all-files --show-diff-on-failure 49 | 50 | [testenv:coverage] 51 | deps = 52 | coverage 53 | skip_install = true 54 | commands = 55 | coverage combine 56 | coverage html 57 | coverage report 58 | 59 | [testenv:coverage-ci] 60 | deps = 61 | coveralls 62 | coverage 63 | codecov 64 | skip_install = true 65 | commands = 66 | coverage combine 67 | coverage report 68 | codecov 69 | 70 | [testenv:safety] 71 | deps = 72 | safety 73 | skip_install = true 74 | whitelist_externals=find 75 | commands = 76 | safety check -r requirements.txt 77 | safety check -r requirements-dev.txt 78 | -------------------------------------------------------------------------------- /app/objects/c_objective.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.objects.interfaces.i_object import FirstClassObjectInterface 4 | from app.objects.secondclass.c_goal import GoalSchema 5 | from app.utility.base_object import BaseObject 6 | 7 | 8 | class ObjectiveSchema(ma.Schema): 9 | 10 | id = ma.fields.String() 11 | name = ma.fields.String() 12 | description = ma.fields.String() 13 | goals = ma.fields.List(ma.fields.Nested(GoalSchema())) 14 | percentage = ma.fields.Float() 15 | 16 | @ma.post_load 17 | def build_objective(self, data, **_): 18 | return Objective(**data) 19 | 20 | 21 | class Objective(FirstClassObjectInterface, BaseObject): 22 | 23 | schema = ObjectiveSchema() 24 | 25 | @property 26 | def unique(self): 27 | return self.hash('%s' % self.id) 28 | 29 | @property 30 | def percentage(self): 31 | if len(self.goals) > 0: 32 | return 100 * (len([g for g in self.goals if g.satisfied() is True])/len(self.goals)) 33 | return 0 34 | 35 | def completed(self, facts=None): 36 | return not any(x.satisfied(facts) is False for x in self.goals) 37 | 38 | def __init__(self, id='', name='', description='', goals=None): 39 | super().__init__() 40 | self.id = id 41 | self.name = name 42 | self.description = description 43 | self.goals = goals if goals else [] 44 | 45 | def store(self, ram): 46 | existing = self.retrieve(ram['objectives'], self.unique) 47 | if not existing: 48 | ram['objectives'].append(self) 49 | return self.retrieve(ram['objectives'], self.unique) 50 | existing.update('name', self.name) 51 | existing.update('description', self.description) 52 | existing.update('goals', self.goals) 53 | return existing 54 | -------------------------------------------------------------------------------- /tests/objects/test_agent.py: -------------------------------------------------------------------------------- 1 | from app.objects.c_ability import Ability 2 | from app.objects.c_agent import Agent 3 | from app.objects.secondclass.c_fact import Fact 4 | from app.utility.base_world import BaseWorld 5 | 6 | 7 | class TestAgent: 8 | 9 | def test_task_no_facts(self, loop, data_svc, obfuscator, init_base_world): 10 | ability = Ability(ability_id='123', test=BaseWorld.encode_string('whoami'), variations=[], 11 | executor='psh', platform='windows') 12 | agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['pwsh', 'psh'], platform='windows') 13 | loop.run_until_complete(agent.task([ability], obfuscator='plain-text')) 14 | assert 1 == len(agent.links) 15 | 16 | def test_task_missing_fact(self, loop, obfuscator, init_base_world): 17 | ability = Ability(ability_id='123', test=BaseWorld.encode_string('net user #{domain.user.name} /domain'), 18 | variations=[], executor='psh', platform='windows') 19 | agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['pwsh', 'psh'], platform='windows') 20 | loop.run_until_complete(agent.task([ability], obfuscator='plain-text')) 21 | assert 0 == len(agent.links) 22 | 23 | def test_task_with_facts(self, loop, obfuscator, init_base_world): 24 | ability = Ability(ability_id='123', test=BaseWorld.encode_string('net user #{domain.user.name} /domain'), 25 | variations=[], executor='psh', platform='windows') 26 | agent = Agent(paw='123', sleep_min=2, sleep_max=8, watchdog=0, executors=['pwsh', 'psh'], platform='windows') 27 | fact = Fact(trait='domain.user.name', value='bob') 28 | 29 | loop.run_until_complete(agent.task([ability], 'plain-text', [fact])) 30 | assert 1 == len(agent.links) 31 | -------------------------------------------------------------------------------- /app/utility/payload_encoder.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains helper functions for encoding and decoding payload files. 3 | 4 | If AV is running on the server host, then it may sometimes flag, quarantine, or delete 5 | CALDERA payloads. To help prevent this, encoded payloads can be used to prevent AV 6 | from breaking the server. The convention expected by the server is that 7 | encoded payloads will be XOR'ed with the DEFAULT_KEY contained in the payload_encoder.py 8 | module. 9 | 10 | Additionally, payload_encoder.py can be used from the command-line to add a new encoded payload. 11 | 12 | ``` 13 | python /path/to/payload_encoder.py input_file output_file 14 | ``` 15 | 16 | NOTE: In order for the server to detect the availability of an encoded payload, the payload file's 17 | name must end in the `.xored` extension. 18 | """ 19 | import array 20 | import argparse 21 | 22 | DEFAULT_KEY = [0x32, 0x45, 0x32, 0xca] 23 | 24 | 25 | def xor_bytes(in_bytes, key=None): 26 | if not key: 27 | key = DEFAULT_KEY 28 | arr = array.array('B', in_bytes) 29 | for i, val in enumerate(arr): 30 | cur_key = key[i % len(key)] 31 | arr[i] = val ^ cur_key 32 | return bytes(arr) 33 | 34 | 35 | def xor_file(input_file, output_file=None, key=None): 36 | with open(input_file, 'rb') as encoded_stream: 37 | buf = encoded_stream.read() 38 | buf = xor_bytes(buf, key=key) 39 | if output_file: 40 | with open(output_file, 'wb') as decoded_stream: 41 | decoded_stream.write(bytes(buf)) 42 | return buf 43 | 44 | 45 | if __name__ == '__main__': 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument('-key', default=DEFAULT_KEY) 48 | parser.add_argument('input') 49 | parser.add_argument('output') 50 | 51 | args = parser.parse_args() 52 | xor_file(args.input, output_file=args.output, key=args.key) 53 | -------------------------------------------------------------------------------- /app/utility/config_generator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | import secrets 4 | 5 | import jinja2 6 | import yaml 7 | 8 | 9 | CONFIG_MSG_TEMPLATE = jinja2.Template(""" 10 | Log into Caldera with the following admin credentials: 11 | Red: 12 | {%- if users.red.red %} 13 | USERNAME: red 14 | PASSWORD: {{ users.red.red }} 15 | {%- endif %} 16 | API_TOKEN: {{ api_key_red }} 17 | Blue: 18 | {%- if users.blue.blue %} 19 | USERNAME: blue 20 | PASSWORD: {{ users.blue.blue }} 21 | {%- endif %} 22 | API_TOKEN: {{ api_key_blue }} 23 | To modify these values, edit the {{ config_path }} file. 24 | """) 25 | 26 | 27 | def log_config_message(config_path): 28 | with pathlib.Path(config_path).open('r') as fle: 29 | config = yaml.safe_load(fle) 30 | logging.info(CONFIG_MSG_TEMPLATE.render(config_path=str(config_path), **config)) 31 | 32 | 33 | def make_secure_config(): 34 | with open('conf/default.yml', 'r') as fle: 35 | config = yaml.safe_load(fle) 36 | 37 | secret_options = ('api_key_blue', 'api_key_red', 'crypt_salt', 'encryption_key') 38 | for option in secret_options: 39 | config[option] = secrets.token_urlsafe() 40 | 41 | config['users'] = dict(red=dict(red=secrets.token_urlsafe()), 42 | blue=dict(blue=secrets.token_urlsafe())) 43 | 44 | return config 45 | 46 | 47 | def ensure_local_config(): 48 | """ 49 | Checks if a local.yml config file exists. If not, generates a new local.yml file using secure random values. 50 | """ 51 | local_conf_path = pathlib.Path('conf/local.yml') 52 | if local_conf_path.exists(): 53 | return 54 | 55 | logging.info('Creating new secure config in %s' % local_conf_path) 56 | with local_conf_path.open('w') as fle: 57 | yaml.safe_dump(make_secure_config(), fle, default_flow_style=False) 58 | 59 | log_config_message(local_conf_path) 60 | -------------------------------------------------------------------------------- /app/objects/secondclass/c_fact.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | 3 | from app.utility.base_object import BaseObject 4 | 5 | escape_ref = { 6 | 'sh': { 7 | 'special': ['\\', ' ', '$', '#', '^', '&', '*', '|', '`', '>', 8 | '<', '"', '\'', '[', ']', '{', '}', '?', '~', '%'], 9 | 'escape_prefix': '\\' 10 | }, 11 | 'psh': { 12 | 'special': ['`', '^', '(', ')', '[', ']', '|', '+', '%', 13 | '?', '$', '#', '&', '@', '>', '<', '\'', '"', ' '], 14 | 'escape_prefix': '`' 15 | }, 16 | 'cmd': { 17 | 'special': ['^', '&', '<', '>', '|', ' ', '?', '\'', '"'], 18 | 'escape_prefix': '^' 19 | } 20 | } 21 | 22 | 23 | class FactSchema(ma.Schema): 24 | 25 | unique = ma.fields.String() 26 | trait = ma.fields.String() 27 | value = ma.fields.Function(lambda x: x.value, deserialize=lambda x: str(x), allow_none=True) 28 | score = ma.fields.Integer() 29 | collected_by = ma.fields.String() 30 | technique_id = ma.fields.String() 31 | 32 | @ma.post_load() 33 | def build_fact(self, data, **_): 34 | return Fact(**data) 35 | 36 | 37 | class Fact(BaseObject): 38 | 39 | schema = FactSchema() 40 | load_schema = FactSchema(exclude=['unique']) 41 | 42 | @property 43 | def unique(self): 44 | return self.hash('%s%s' % (self.trait, self.value)) 45 | 46 | def escaped(self, executor): 47 | if executor not in escape_ref: 48 | return self.value 49 | escaped_value = str(self.value) 50 | for char in escape_ref[executor]['special']: 51 | escaped_value = escaped_value.replace(char, (escape_ref[executor]['escape_prefix'] + char)) 52 | return escaped_value 53 | 54 | def __init__(self, trait, value=None, score=1, collected_by=None, technique_id=None): 55 | super().__init__() 56 | self.trait = trait 57 | self.value = value 58 | self.score = score 59 | self.collected_by = collected_by 60 | self.technique_id = technique_id 61 | -------------------------------------------------------------------------------- /app/utility/base_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | 5 | class BaseParser: 6 | 7 | def __init__(self, parser_info): 8 | self.mappers = parser_info['mappers'] 9 | self.used_facts = parser_info['used_facts'] 10 | self.source_facts = parser_info['source_facts'] 11 | 12 | @staticmethod 13 | def set_value(search, match, used_facts): 14 | """ 15 | Determine the value of a source/target for a Relationship 16 | :param search: a fact property to look for; either a source or target fact 17 | :param match: a parsing match 18 | :param used_facts: a list of facts that were used in a command 19 | :return: either None, the value of a matched used_fact, or the parsing match 20 | """ 21 | if not search: 22 | return None 23 | for uf in used_facts: 24 | if search == uf.trait: 25 | return uf.value 26 | return match 27 | 28 | @staticmethod 29 | def email(blob): 30 | """ 31 | Parse out email addresses 32 | :param blob: 33 | :return: 34 | """ 35 | return re.findall(r'[\w\.-]+@[\w\.-]+', blob) 36 | 37 | @staticmethod 38 | def filename(blob): 39 | """ 40 | Parse out filenames 41 | :param blob: 42 | :return: 43 | """ 44 | return re.findall(r'\b\w+\.\w+', blob) 45 | 46 | @staticmethod 47 | def line(blob): 48 | """ 49 | Split a blob by line 50 | :param blob: 51 | :return: 52 | """ 53 | return [x.rstrip('\r') for x in blob.split('\n') if x] 54 | 55 | @staticmethod 56 | def ip(blob): 57 | return re.findall(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', blob) 58 | 59 | @staticmethod 60 | def broadcastip(blob): 61 | return re.findall(r'(?<=broadcast ).*', blob) 62 | 63 | @staticmethod 64 | def load_json(blob): 65 | try: 66 | return json.loads(blob) 67 | except Exception: 68 | return None 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | # Reporting issues 4 | * Describe (in detail) what should have happened. Include any supporting information (stack traces, errors, etc) 5 | * Include any steps to replicate the issue 6 | * Indicate OS and Python versions 7 | 8 | # Development environment setup 9 | 1. Clone repository: 10 | ``` 11 | git clone https://github.com/mitre/caldera.git --recursive 12 | ``` 13 | 14 | 2. Create a virtualenv: 15 | ``` 16 | python3 -m venv venv 17 | . venv/bin/activate 18 | ``` 19 | 20 | 3. Install dependencies: 21 | ``` 22 | pip install -r requirements.txt -r requirements-dev.txt 23 | ``` 24 | 25 | 4. Install the pre-commit hooks: 26 | ``` 27 | pre-commit install --install-hooks 28 | ``` 29 | 30 | # Developing 31 | We use the basic feature branch GIT flow. Fork this repository and create a feature branch off of master and when ready, submit a merge request. Make branch names and commits descriptive. A merge request should solve one problem, not many. 32 | 33 | * Tests should cover any code changes that you have made. The test that you write should fail without your patch. 34 | * [Run tests](#run-the-tests) 35 | 36 | # Run the tests 37 | Tests can be run by executing: 38 | ``` 39 | python -m pytest 40 | ``` 41 | This will run all unit tests in your current development environment. Depending on the level of the change, you might need to run the test suite on various versions of Python. The unit testing pipeline will run the entire suite across multiple Python versions that we support when you submit your PR. 42 | 43 | We utilize `tox` to test CALDERA in multiple versions of Python. This will only run the interpreter is present on your system. We currently test 3.6+. To run tox, execute: 44 | ``` 45 | tox 46 | ``` 47 | 48 | # Code Coverage 49 | You can generate code coverage reports manually or by running `tox`. To run them manually, you must have `coverage` installed and run the following commands: 50 | ``` 51 | coverage run -m pytest 52 | coverage report 53 | coverage html 54 | ``` 55 | You can find the code coverage report in `htmlcov/index.html` -------------------------------------------------------------------------------- /tests/services/test_file_svc.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from app.utility.payload_encoder import xor_file 5 | from tests import AsyncMock 6 | from asyncio import Future 7 | 8 | 9 | @pytest.mark.usefixtures( 10 | 'init_base_world' 11 | ) 12 | class TestFileService: 13 | 14 | def test_create_exfil_sub_directory(self, loop, file_svc): 15 | exfil_dir_name = 'unit-testing-Rocks' 16 | new_dir = loop.run_until_complete(file_svc.create_exfil_sub_directory(exfil_dir_name)) 17 | assert os.path.isdir(new_dir) 18 | os.rmdir(new_dir) 19 | 20 | def test_read_write_result_file(self, tmpdir, file_svc): 21 | link_id = '12345' 22 | output = 'output testing unit' 23 | # write output data 24 | file_svc.write_result_file(link_id=link_id, output=output, location=tmpdir) 25 | 26 | # read output data 27 | output_data = file_svc.read_result_file(link_id=link_id, location=tmpdir) 28 | assert output_data == output 29 | 30 | def test_pack_file(self, loop, mocker, tmpdir, file_svc, data_svc): 31 | payload = 'unittestpayload' 32 | payload_content = b'content' 33 | new_payload_content = b'new_content' 34 | packer_name = 'test' 35 | 36 | # create temp files 37 | file = tmpdir.join(payload) 38 | file.write(payload_content) 39 | 40 | # start mocking up methods 41 | packer = mocker.Mock(return_value=Future()) 42 | packer.return_value = packer 43 | packer.pack = AsyncMock(return_value=(payload, new_payload_content)) 44 | data_svc.locate = AsyncMock(return_value=[]) 45 | module = mocker.Mock() 46 | module.Packer = packer 47 | file_svc.packers[packer_name] = module 48 | file_svc.data_svc = data_svc 49 | file_svc.read_file = AsyncMock(return_value=(payload, payload_content)) 50 | 51 | file_path, content, display_name = loop.run_until_complete(file_svc.get_file(headers=dict(file='%s:%s' % (packer_name, payload)))) 52 | 53 | packer.pack.assert_called_once() 54 | assert payload == file_path 55 | assert content == new_payload_content 56 | -------------------------------------------------------------------------------- /app/service/interfaces/i_data_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class DataServiceInterface(abc.ABC): 5 | 6 | @staticmethod 7 | @abc.abstractmethod 8 | def destroy(): 9 | """ 10 | Clear out all data 11 | :return: 12 | """ 13 | pass 14 | 15 | @abc.abstractmethod 16 | def save_state(self): 17 | """ 18 | Accept all components of an agent profile and save a new agent or register an updated heartbeat. 19 | :return: the agent object, instructions to execute 20 | """ 21 | pass 22 | 23 | @abc.abstractmethod 24 | def restore_state(self): 25 | pass 26 | 27 | @abc.abstractmethod 28 | def apply(self, collection): 29 | """ 30 | Add a new collection to RAM 31 | :param collection: 32 | :return: 33 | """ 34 | pass 35 | 36 | @abc.abstractmethod 37 | def load_data(self, plugins): 38 | """ 39 | Non-blocking read all the data sources to populate the object store 40 | :return: None 41 | """ 42 | pass 43 | 44 | @abc.abstractmethod 45 | def reload_data(self, plugins): 46 | """ 47 | Blocking read all the data sources to populate the object store 48 | :return: None 49 | """ 50 | pass 51 | 52 | @abc.abstractmethod 53 | def store(self, c_object): 54 | """ 55 | Accept any c_object type and store it (create/update) in RAM 56 | :param c_object: 57 | :return: a single c_object 58 | """ 59 | pass 60 | 61 | @abc.abstractmethod 62 | def locate(self, object_name, match): 63 | """ 64 | Find all c_objects which match a search. Return all c_objects if no match. 65 | :param object_name: 66 | :param match: dict() 67 | :return: a list of c_object types 68 | """ 69 | pass 70 | 71 | @abc.abstractmethod 72 | def remove(self, object_name, match): 73 | """ 74 | Remove any c_objects which match a search 75 | :param object_name: 76 | :param match: dict() 77 | :return: 78 | """ 79 | pass 80 | -------------------------------------------------------------------------------- /templates/planners.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |

Planners

8 |

9 | A planner is a module which contains logic for how a running operation should make decisions about 10 | which abilities to use and in what order. Specifically, a planner's logic contains the decision making to 11 | execute a single phase of an operation. 12 |

13 |
14 |
15 | 21 |
22 |
23 | 24 |
25 |

26 |

27 |
28 |
29 |
30 | 31 | -------------------------------------------------------------------------------- /app/utility/rule_set.py: -------------------------------------------------------------------------------- 1 | import re 2 | from enum import Enum 3 | import ipaddress 4 | 5 | 6 | class RuleAction(Enum): 7 | ALLOW = 1 8 | DENY = 0 9 | 10 | 11 | class RuleSet: 12 | def __init__(self, rules): 13 | self.rules = rules 14 | 15 | async def is_fact_allowed(self, fact): 16 | allowed = True 17 | for rule in await self._applicable_rules(fact): 18 | if await self._is_ip_rule_match(rule, fact): 19 | allowed = await self._rule_judgement(rule.action) 20 | continue 21 | if await self._is_regex_rule_match(rule, fact): 22 | allowed = await self._rule_judgement(rule.action) 23 | return allowed 24 | 25 | async def _applicable_rules(self, fact): 26 | applicable_rules = [] 27 | for rule in self.rules: 28 | if rule.trait == fact.trait: 29 | applicable_rules.append(rule) 30 | return applicable_rules 31 | 32 | async def apply_rules(self, facts): 33 | if await self._has_rules(): 34 | valid_facts = [] 35 | for fact in facts: 36 | if await self.is_fact_allowed(fact): 37 | valid_facts.append(fact) 38 | return [valid_facts] 39 | else: 40 | return [facts] 41 | 42 | async def _has_rules(self): 43 | return len(self.rules) 44 | 45 | @staticmethod 46 | async def _rule_judgement(action): 47 | if action.value == RuleAction.DENY.value: 48 | return False 49 | return True 50 | 51 | @staticmethod 52 | async def _is_ip_network(value): 53 | try: 54 | ipaddress.IPv4Network(value) 55 | return True 56 | except (ValueError, ipaddress.AddressValueError): 57 | pass 58 | return False 59 | 60 | @staticmethod 61 | async def _is_regex_rule_match(rule, fact): 62 | return re.match(rule.match, fact.value) 63 | 64 | async def _is_ip_rule_match(self, rule, fact): 65 | if rule.match != '.*' and await self._is_ip_network(rule.match) and \ 66 | await self._is_ip_network(fact.value): 67 | if ipaddress.IPv4Network(fact.value).subnet_of(ipaddress.IPv4Network(rule.match)): 68 | return True 69 | return False 70 | -------------------------------------------------------------------------------- /tests/services/test_learning_svc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from app.objects.c_adversary import Adversary 3 | from app.objects.secondclass.c_fact import Fact 4 | from app.utility.base_world import BaseWorld 5 | 6 | 7 | @pytest.fixture 8 | def setup_learning_service(loop, data_svc, ability, operation, link): 9 | tability = ability(tactic='discovery', technique_id='T1033', technique='Find', name='test', 10 | test='d2hvYW1pCg==', description='find active user', cleanup='', executor='sh', 11 | platform='darwin', payloads=['wifi.sh'], parsers=[], requirements=[], privilege=None, 12 | variations=[]) 13 | loop.run_until_complete(data_svc.store(tability)) 14 | toperation = operation(name='sample', agents=None, adversary=Adversary(name='sample', adversary_id='XYZ', 15 | atomic_ordering=[], description='test')) 16 | loop.run_until_complete(data_svc.store(toperation)) 17 | tlink = link(ability=tability, command='', paw='') 18 | yield toperation, tlink 19 | 20 | 21 | class TestLearningSvc: 22 | 23 | def test_learn(self, loop, setup_learning_service, learning_svc): 24 | operation, link = setup_learning_service 25 | operation.add_link(link) 26 | loop.run_until_complete(learning_svc.learn( 27 | facts=operation.all_facts(), 28 | link=link, 29 | blob=BaseWorld.encode_string('i contain 1 ip address 192.168.0.1 and one file /etc/host.txt. that is all.')) 30 | ) 31 | assert len(link.facts) == 2 32 | 33 | def test_build_relationships(self, loop, setup_learning_service, learning_svc): 34 | _, link = setup_learning_service 35 | learning_svc.model.add(frozenset({'host.user.name', 'target.org.name'})) 36 | learning_svc.model.add(frozenset({'host.file.extension', 'host.user.name', 'domain.user.name'})) 37 | facts = [ 38 | Fact(trait='target.org.name', value='something'), 39 | Fact(trait='host.user.name', value='admin'), 40 | Fact(trait='host.user.name', value='root'), 41 | Fact(trait='domain.user.name', value='user'), 42 | Fact(trait='not.really.here', value='should never be found') 43 | ] 44 | loop.run_until_complete(learning_svc._build_relationships(link, facts)) 45 | assert len(link.relationships) == 4 46 | -------------------------------------------------------------------------------- /tests/services/test_contact_svc.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import pytest 4 | 5 | from app.service.rest_svc import RestService 6 | from app.objects.secondclass.c_result import Result 7 | 8 | 9 | class TestProcessor: 10 | 11 | async def preprocess(self, blob): 12 | return base64.b64encode(blob) 13 | 14 | async def postprocess(self, blob): 15 | return str(base64.b64decode(blob), 'utf-8') 16 | 17 | 18 | @pytest.fixture 19 | def setup_contact_service(loop, data_svc, agent, ability, operation, link, adversary): 20 | tability = ability(tactic='discovery', technique_id='T1033', technique='Find', name='test', 21 | test='d2hvYW1pCg==', description='find active user', cleanup='', executor='special_executor', 22 | platform='darwin', payloads=['wifi.sh'], parsers=[], requirements=[], privilege=None, 23 | variations=[]) 24 | tability.HOOKS['special_executor'] = TestProcessor() 25 | loop.run_until_complete(data_svc.store(tability)) 26 | tagent = agent(sleep_min=10, sleep_max=60, watchdog=0, executors=['special_executor']) 27 | loop.run_until_complete(data_svc.store(tagent)) 28 | toperation = operation(name='sample', agents=[tagent], adversary=adversary()) 29 | tlink = link(command='', paw=tagent.paw, ability=tability, id=12345) 30 | loop.run_until_complete(toperation.apply(tlink)) 31 | loop.run_until_complete(data_svc.store(toperation)) 32 | yield tlink 33 | 34 | 35 | @pytest.mark.usefixtures( 36 | 'app_svc', 37 | 'data_svc', 38 | 'file_svc', 39 | 'learning_svc', 40 | 'obfuscator' 41 | ) 42 | class TestContactSvc: 43 | async def test_save_ability_hooks(self, setup_contact_service, contact_svc): 44 | test_string = b'test_string' 45 | link = setup_contact_service 46 | rest_svc = RestService() 47 | result = dict( 48 | id=str(link.id), 49 | output=str(base64.b64encode(base64.b64encode(test_string)), 'utf-8'), 50 | pid=0, 51 | status=0 52 | ) 53 | await contact_svc._save(Result(**result)) 54 | result = await rest_svc.display_result(dict(link_id=str(link.id))) 55 | assert base64.b64decode(result['output']) == test_string 56 | 57 | # cleanup test 58 | try: 59 | os.remove(os.path.join('data', 'results', str(link.id))) 60 | except FileNotFoundError: 61 | print('Unable to cleanup test_save_ability_hooks result file') 62 | -------------------------------------------------------------------------------- /app/objects/c_planner.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import marshmallow as ma 4 | 5 | from app.objects.interfaces.i_object import FirstClassObjectInterface 6 | from app.utility.base_object import BaseObject 7 | from app.objects.secondclass.c_fact import Fact, FactSchema 8 | 9 | 10 | class PlannerSchema(ma.Schema): 11 | planner_id = ma.fields.String(data_key='id') 12 | name = ma.fields.String() 13 | module = ma.fields.String() 14 | params = ma.fields.Dict() 15 | description = ma.fields.String() 16 | stopping_conditions = ma.fields.List(ma.fields.Nested(FactSchema())) 17 | ignore_enforcement_modules = ma.fields.List(ma.fields.String()) 18 | 19 | @ma.post_load() 20 | def build_planner(self, data, **_): 21 | return Planner(**data) 22 | 23 | 24 | class Planner(FirstClassObjectInterface, BaseObject): 25 | 26 | schema = PlannerSchema() 27 | display_schema = PlannerSchema(exclude=['planner_id', 'ignore_enforcement_modules']) 28 | 29 | @property 30 | def unique(self): 31 | return self.hash(self.name) 32 | 33 | def __init__(self, planner_id, name, module, params, stopping_conditions=None, description=None, 34 | ignore_enforcement_modules=()): 35 | super().__init__() 36 | self.planner_id = planner_id 37 | self.name = name 38 | self.module = module 39 | self.params = params 40 | self.description = description 41 | self.stopping_conditions = self._set_stopping_conditions(stopping_conditions) 42 | self.ignore_enforcement_modules = ignore_enforcement_modules 43 | 44 | def store(self, ram): 45 | existing = self.retrieve(ram['planners'], self.unique) 46 | if not existing: 47 | ram['planners'].append(self) 48 | return self.retrieve(ram['planners'], self.unique) 49 | else: 50 | existing.update('stopping_conditions', self.stopping_conditions) 51 | existing.update('params', self.params) 52 | return existing 53 | 54 | async def which_plugin(self): 55 | for plugin in os.listdir('plugins'): 56 | if await self.walk_file_path(os.path.join('plugins', plugin, 'data', ''), '%s.yml' % self.planner_id): 57 | return plugin 58 | return None 59 | 60 | """ PRIVATE """ 61 | 62 | @staticmethod 63 | def _set_stopping_conditions(conditions): 64 | if conditions: 65 | return [Fact.load(dict(trait=trait, value=value)) for sc in conditions for trait, value in sc.items()] 66 | return [] 67 | -------------------------------------------------------------------------------- /tests/objects/test_objective.py: -------------------------------------------------------------------------------- 1 | from app.objects.secondclass.c_goal import Goal 2 | from app.objects.c_objective import Objective 3 | from app.objects.secondclass.c_fact import Fact 4 | 5 | 6 | class TestGoal: 7 | 8 | def test_satisfied(self): 9 | test_goal = Goal(target='target', value='value', count=1) 10 | assert test_goal.satisfied() is False 11 | assert test_goal.satisfied(all_facts=[Fact(trait='target', value='value')]) is True 12 | 13 | def test_multi_satisfied(self): 14 | test_goal = Goal(target='target', value='value', count=3) 15 | test_fact = Fact(trait='target', value='value') 16 | assert test_goal.satisfied(all_facts=[test_fact]) is False 17 | assert test_goal.satisfied(all_facts=[test_fact, test_fact, test_fact]) is True 18 | 19 | def test_operators(self): 20 | test_goal1 = Goal(target='target', value=2, count=1, operator='>') 21 | test_goal2 = Goal(target='target', value=2, count=1, operator='<=') 22 | test_goal3 = Goal(target='target', value='tes', count=1, operator='in') 23 | test_goal4 = Goal(target='target', value='', count=3, operator='*') 24 | test_facta = Fact(trait='target', value=1) 25 | test_factb = Fact(trait='target', value=2) 26 | test_factc = Fact(trait='target', value='test') 27 | assert test_goal1.satisfied(all_facts=[test_facta]) is True 28 | assert test_goal2.satisfied(all_facts=[test_facta]) is False 29 | assert test_goal2.satisfied(all_facts=[test_facta, test_factb]) is True 30 | assert test_goal3.satisfied(all_facts=[test_factc]) is True 31 | assert test_goal4.satisfied(all_facts=[test_facta, test_factb, test_factc]) is True 32 | 33 | def test_goals_satisfied(self): 34 | test_goal1 = Goal(target='target', value='value', count=1) 35 | test_goal2 = Goal(target='target2', value='value2', count=1) 36 | test_facta = Fact(trait='target', value='value') 37 | test_factb = Fact(trait='target2', value='value2') 38 | multi = Objective(id='123', name='test', goals=[test_goal1, test_goal2]) 39 | assert multi.completed([test_facta]) is False 40 | assert multi.completed([test_facta, test_factb]) is True 41 | 42 | def test_goals_percent(self): 43 | test_goal1 = Goal(target='target', value='value', count=1) 44 | test_goal2 = Goal(target='target2', value='value2', count=1) 45 | test_fact = Fact(trait='target', value='value') 46 | multi = Objective(id='123', name='test', goals=[test_goal1, test_goal2]) 47 | assert multi.completed([test_fact]) is False 48 | assert multi.percentage == 50 49 | -------------------------------------------------------------------------------- /static/css/modal.css: -------------------------------------------------------------------------------- 1 | input[type=text], input[type=password], input[type=number] { 2 | width: 100%; 3 | height: 30px; 4 | padding: 12px 20px; 5 | box-sizing: border-box; 6 | margin-top: 15px; 7 | border-radius: 10px; 8 | } 9 | select { 10 | overflow: hidden; 11 | background: white; 12 | font-size: 14px; 13 | height: 30px; 14 | width: 100%; 15 | padding: 5px 15px; 16 | margin-top: 15px; 17 | border-radius: 10px; 18 | } 19 | .modal button { 20 | color: white; 21 | padding: 14px 20px; 22 | margin: 8px 0; 23 | border: none; 24 | cursor: pointer; 25 | width: 90%; 26 | } 27 | .modal button:hover { 28 | opacity: 0.6; 29 | } 30 | 31 | .cancelbtn { 32 | width: auto; 33 | padding: 10px 18px; 34 | background-color: #7a1f1f; 35 | } 36 | .imgcontainer { 37 | text-align: center; 38 | position: relative; 39 | margin-bottom: 25px; 40 | } 41 | img.avatar { 42 | width: 40%; 43 | border-radius: 50%; 44 | } 45 | .container { 46 | padding: 16px; 47 | } 48 | span.psw { 49 | float: right; 50 | padding-top: 16px; 51 | } 52 | .modal { 53 | display: none; /* Hidden by default */ 54 | position: fixed; /* Stay in place */ 55 | z-index: 100; /* Sit on top */ 56 | left: 0; 57 | top: 0; 58 | width: 100%; /* Full width */ 59 | padding-top: 40px; 60 | overflow-y: scroll; 61 | height: 100%; 62 | -webkit-backdrop-filter: blur(15px); 63 | backdrop-filter: blur(15px); 64 | } 65 | .modal-content { 66 | margin: 5% auto 5% auto; 67 | width: 85%; 68 | } 69 | .modal p { 70 | color: #333; 71 | } 72 | .modal-box { 73 | max-height: 80%; 74 | overflow: auto; 75 | overflow-y: auto; 76 | } 77 | .close { 78 | position: absolute; 79 | right: 25px; 80 | top: 0; 81 | color: red; 82 | font-size: 35px; 83 | font-weight: bold; 84 | } 85 | .modal .approve{ 86 | background-color: green; 87 | } 88 | .modal .discard{ 89 | background-color: darkorange; 90 | } 91 | .modal .approveall{ 92 | background-color: darkgreen; 93 | } 94 | 95 | .close:hover, 96 | .close:focus { 97 | color: #7a1f1f; 98 | cursor: pointer; 99 | } 100 | #alert-modal { 101 | z-index: 120; 102 | } 103 | .config-arg input { 104 | padding: 5px; 105 | margin: 5px; 106 | text-align: left; 107 | font-size: 13px 108 | } 109 | .config-arg label { 110 | color: white; 111 | font-size: 14px 112 | } 113 | .config-arg { 114 | vertical-align: initial; 115 | } 116 | p.command { 117 | padding: 10px; 118 | width: 85%; 119 | color: white; 120 | font-size: 13px; 121 | font-style:italic; 122 | background-color: rgba(170, 170, 170, 0.15); 123 | } 124 | -------------------------------------------------------------------------------- /tests/objects/test_ability.py: -------------------------------------------------------------------------------- 1 | from app.objects.c_ability import Ability 2 | from app.objects.c_agent import Agent 3 | 4 | 5 | class TestAbility: 6 | 7 | def test_privileged_to_run__1(self, loop, data_svc): 8 | """ Test ability.privilege == None """ 9 | agent = loop.run_until_complete(data_svc.store(Agent(sleep_min=1, sleep_max=2, watchdog=0))) 10 | ability = loop.run_until_complete(data_svc.store( 11 | Ability(ability_id='123', privilege=None, variations=[]) 12 | )) 13 | assert agent.privileged_to_run(ability) 14 | 15 | def test_privileged_to_run__2(self, loop, data_svc): 16 | """ Test ability.privilege == 'User' """ 17 | agent = loop.run_until_complete(data_svc.store(Agent(sleep_min=1, sleep_max=2, watchdog=0))) 18 | ability = loop.run_until_complete(data_svc.store( 19 | Ability(ability_id='123', privilege='User', variations=[]) 20 | )) 21 | assert agent.privileged_to_run(ability) 22 | 23 | def test_privileged_to_run__3(self, loop, data_svc): 24 | """ Test ability.privilege == 'Elevated' """ 25 | agent = loop.run_until_complete(data_svc.store(Agent(sleep_min=1, sleep_max=2, watchdog=0))) 26 | ability = loop.run_until_complete(data_svc.store( 27 | Ability(ability_id='123', privilege='Elevated', variations=[]) 28 | )) 29 | assert not agent.privileged_to_run(ability) 30 | 31 | def test_privileged_to_run__4(self, loop, data_svc): 32 | """ Test ability.privilege == 'User' and agent.privilege == 'Elevated' """ 33 | agent = loop.run_until_complete(data_svc.store(Agent(sleep_min=1, sleep_max=2, watchdog=0, privilege='Elevated'))) 34 | ability = loop.run_until_complete(data_svc.store( 35 | Ability(ability_id='123', privilege='User', variations=[]) 36 | )) 37 | assert agent.privileged_to_run(ability) 38 | 39 | def test_privileged_to_run__5(self, loop, data_svc): 40 | """ Test ability.privilege == 'Elevated' and agent.privilege == 'Elevated' """ 41 | agent = loop.run_until_complete(data_svc.store(Agent(sleep_min=1, sleep_max=2, watchdog=0, privilege='Elevated'))) 42 | ability = loop.run_until_complete(data_svc.store( 43 | Ability(ability_id='123', privilege='Elevated', variations=[]) 44 | )) 45 | assert agent.privileged_to_run(ability) 46 | 47 | def test_privileged_to_run__6(self, loop, data_svc): 48 | """ Test ability.privilege == 'None' and agent.privilege == 'Elevated' """ 49 | agent = loop.run_until_complete(data_svc.store(Agent(sleep_min=1, sleep_max=2, watchdog=0, privilege='Elevated'))) 50 | ability = loop.run_until_complete(data_svc.store( 51 | Ability(ability_id='123', variations=[]) 52 | )) 53 | assert agent.privileged_to_run(ability) 54 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 22 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python', 'javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /app/api/packs/advanced.py: -------------------------------------------------------------------------------- 1 | from aiohttp_jinja2 import template 2 | 3 | from app.service.auth_svc import check_authorization 4 | from app.utility.base_world import BaseWorld 5 | 6 | 7 | class AdvancedPack(BaseWorld): 8 | 9 | def __init__(self, services): 10 | self.app_svc = services.get('app_svc') 11 | self.auth_svc = services.get('auth_svc') 12 | self.contact_svc = services.get('contact_svc') 13 | self.data_svc = services.get('data_svc') 14 | self.rest_svc = services.get('rest_svc') 15 | 16 | async def enable(self): 17 | self.app_svc.application.router.add_route('GET', '/advanced/sources', self._section_sources) 18 | self.app_svc.application.router.add_route('GET', '/advanced/objectives', self._section_objectives) 19 | self.app_svc.application.router.add_route('GET', '/advanced/planners', self._section_planners) 20 | self.app_svc.application.router.add_route('GET', '/advanced/contacts', self._section_contacts) 21 | self.app_svc.application.router.add_route('GET', '/advanced/obfuscators', self._section_obfuscators) 22 | self.app_svc.application.router.add_route('GET', '/advanced/configurations', self._section_configurations) 23 | 24 | """ PRIVATE """ 25 | 26 | @check_authorization 27 | @template('planners.html') 28 | async def _section_planners(self, request): 29 | planners = [p.display for p in await self.data_svc.locate('planners')] 30 | return dict(planners=planners) 31 | 32 | @check_authorization 33 | @template('contacts.html') 34 | async def _section_contacts(self, request): 35 | contacts = [dict(name=c.name, description=c.description) for c in self.contact_svc.contacts] 36 | return dict(contacts=contacts) 37 | 38 | @check_authorization 39 | @template('obfuscators.html') 40 | async def _section_obfuscators(self, request): 41 | obfuscators = [o.display for o in await self.data_svc.locate('obfuscators')] 42 | return dict(obfuscators=obfuscators) 43 | 44 | @check_authorization 45 | @template('configurations.html') 46 | async def _section_configurations(self, request): 47 | return dict(config=self.get_config(), plugins=[p for p in await self.data_svc.locate('plugins')]) 48 | 49 | @check_authorization 50 | @template('sources.html') 51 | async def _section_sources(self, request): 52 | access = await self.auth_svc.get_permissions(request) 53 | return dict(sources=[s.display for s in await self.data_svc.locate('sources', match=dict(access=tuple(access)))]) 54 | 55 | @check_authorization 56 | @template('objectives.html') 57 | async def _section_objectives(self, request): 58 | access = await self.auth_svc.get_permissions(request) 59 | return dict(objectives=[o.display for o in await self.data_svc.locate('objectives', match=dict(access=tuple(access)))]) 60 | -------------------------------------------------------------------------------- /app/objects/c_source.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import marshmallow as ma 4 | 5 | from app.objects.interfaces.i_object import FirstClassObjectInterface 6 | from app.objects.secondclass.c_fact import FactSchema 7 | from app.objects.secondclass.c_rule import RuleSchema 8 | from app.objects.secondclass.c_relationship import RelationshipSchema 9 | from app.utility.base_object import BaseObject 10 | 11 | 12 | class AdjustmentSchema(ma.Schema): 13 | 14 | ability_id = ma.fields.String() 15 | trait = ma.fields.String() 16 | value = ma.fields.String() 17 | offset = ma.fields.Integer() 18 | 19 | @ma.post_load() 20 | def build_adjustment(self, data, **_): 21 | return Adjustment(**data) 22 | 23 | 24 | Adjustment = namedtuple('Adjustment', 'ability_id trait value offset') 25 | 26 | 27 | class SourceSchema(ma.Schema): 28 | 29 | id = ma.fields.String() 30 | name = ma.fields.String() 31 | facts = ma.fields.List(ma.fields.Nested(FactSchema())) 32 | rules = ma.fields.List(ma.fields.Nested(RuleSchema())) 33 | adjustments = ma.fields.List(ma.fields.Nested(AdjustmentSchema(), required=False)) 34 | relationships = ma.fields.List(ma.fields.Nested(RelationshipSchema())) 35 | 36 | @ma.pre_load 37 | def fix_adjustments(self, in_data, **_): 38 | x = [] 39 | raw_adjustments = in_data.pop('adjustments', {}) 40 | if raw_adjustments: 41 | for ability_id, adjustments in raw_adjustments.items(): 42 | for trait, block in adjustments.items(): 43 | for change in block: 44 | x.append(dict(ability_id=ability_id, trait=trait, value=change.get('value'), 45 | offset=change.get('offset'))) 46 | in_data['adjustments'] = x 47 | return in_data 48 | 49 | @ma.post_load() 50 | def build_source(self, data, **_): 51 | data['id'] = data.pop('id') 52 | return Source(**data) 53 | 54 | 55 | class Source(FirstClassObjectInterface, BaseObject): 56 | 57 | schema = SourceSchema() 58 | display_schema = SourceSchema(exclude=('adjustments',)) 59 | 60 | @property 61 | def unique(self): 62 | return self.hash('%s' % self.id) 63 | 64 | def __init__(self, id, name, facts, relationships=(), rules=(), adjustments=()): 65 | super().__init__() 66 | self.id = id 67 | self.name = name 68 | self.facts = facts 69 | self.rules = rules 70 | self.adjustments = adjustments 71 | self.relationships = relationships 72 | 73 | def store(self, ram): 74 | existing = self.retrieve(ram['sources'], self.unique) 75 | if not existing: 76 | ram['sources'].append(self) 77 | return self.retrieve(ram['sources'], self.unique) 78 | existing.update('name', self.name) 79 | existing.update('facts', self.facts) 80 | existing.update('rules', self.rules) 81 | existing.update('relationships', self.relationships) 82 | return existing 83 | -------------------------------------------------------------------------------- /static/css/multi-select.css: -------------------------------------------------------------------------------- 1 | .ms-container{ 2 | background: transparent url('../img/switch.png') no-repeat 50% 50%; 3 | width: 70%; 4 | margin: 5px 0; 5 | } 6 | 7 | .ms-container:after{ 8 | content: "."; 9 | display: block; 10 | height: 0; 11 | line-height: 0; 12 | font-size: 0; 13 | clear: both; 14 | min-height: 0; 15 | visibility: hidden; 16 | } 17 | 18 | .ms-container .ms-selectable, .ms-container .ms-selection{ 19 | background: #000000; 20 | color: #555555; 21 | float: left; 22 | width: 45%; 23 | border: 1px solid #2a2a2a; 24 | } 25 | .ms-container .ms-selection{ 26 | float: right; 27 | } 28 | 29 | .ms-container .ms-list{ 30 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 31 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 32 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 33 | -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; 34 | -moz-transition: border linear 0.2s, box-shadow linear 0.2s; 35 | -ms-transition: border linear 0.2s, box-shadow linear 0.2s; 36 | -o-transition: border linear 0.2s, box-shadow linear 0.2s; 37 | transition: border linear 0.2s, box-shadow linear 0.2s; 38 | -webkit-border-radius: 3px; 39 | -moz-border-radius: 3px; 40 | border-radius: 3px; 41 | position: relative; 42 | height: 200px; 43 | padding: 0; 44 | overflow-y: auto; 45 | } 46 | 47 | .ms-container .ms-list.ms-focus{ 48 | border-color: rgba(82, 168, 236, 0.8); 49 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 50 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 51 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 52 | outline: 0; 53 | outline: thin dotted \9; 54 | } 55 | 56 | .ms-container ul{ 57 | margin: 0; 58 | list-style-type: none; 59 | padding: 0; 60 | } 61 | 62 | .ms-container .ms-optgroup-container{ 63 | width: 100%; 64 | } 65 | 66 | .ms-container .ms-optgroup-label{ 67 | margin: 0; 68 | padding: 5px 0px 0px 5px; 69 | cursor: pointer; 70 | color: #999; 71 | } 72 | 73 | .ms-container .ms-selectable li.ms-elem-selectable, 74 | .ms-container .ms-selection li.ms-elem-selection{ 75 | border-bottom: 1px #2a2a2a solid; 76 | padding: 2px 10px; 77 | color: #555; 78 | font-size: 14px; 79 | } 80 | 81 | .ms-container .ms-selectable li.ms-hover, 82 | .ms-container .ms-selection li.ms-hover{ 83 | cursor: pointer; 84 | color: #fff; 85 | text-decoration: none; 86 | background-color: var(--navbar-color); 87 | } 88 | 89 | .ms-container .ms-selectable li.disabled, 90 | .ms-container .ms-selection li.disabled{ 91 | background-color: #eee; 92 | color: #aaa; 93 | cursor: text; 94 | } 95 | 96 | .payload-select-header { 97 | color: white; 98 | padding: 3px; 99 | text-align: center; 100 | border-bottom: 1px solid white; 101 | } -------------------------------------------------------------------------------- /app/objects/c_adversary.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import marshmallow as ma 4 | 5 | from app.objects.interfaces.i_object import FirstClassObjectInterface 6 | from app.utility.base_object import BaseObject 7 | 8 | 9 | class AdversarySchema(ma.Schema): 10 | 11 | adversary_id = ma.fields.String() 12 | name = ma.fields.String() 13 | description = ma.fields.String() 14 | atomic_ordering = ma.fields.List(ma.fields.String()) 15 | objective = ma.fields.String() 16 | tags = ma.fields.List(ma.fields.String()) 17 | 18 | @ma.pre_load 19 | def fix_id(self, adversary, **_): 20 | if 'id' in adversary: 21 | adversary['adversary_id'] = adversary.pop('id') 22 | return adversary 23 | 24 | @ma.pre_load 25 | def phase_to_atomic_ordering(self, adversary, **_): 26 | """ 27 | Convert legacy adversary phases to atomic ordering 28 | """ 29 | if 'phases' in adversary and 'atomic_ordering' in adversary: 30 | raise ma.ValidationError('atomic_ordering and phases cannot be used at the same time', 'phases', adversary) 31 | elif 'phases' in adversary: 32 | adversary['atomic_ordering'] = [ab_id for phase in adversary.get('phases', {}).values() for ab_id in phase] 33 | del adversary['phases'] 34 | return adversary 35 | 36 | @ma.post_load 37 | def build_adversary(self, data, **_): 38 | return Adversary(**data) 39 | 40 | 41 | class Adversary(FirstClassObjectInterface, BaseObject): 42 | 43 | schema = AdversarySchema() 44 | 45 | @property 46 | def unique(self): 47 | return self.hash('%s' % self.adversary_id) 48 | 49 | def __init__(self, adversary_id, name, description, atomic_ordering, objective=None, tags=None): 50 | super().__init__() 51 | self.adversary_id = adversary_id 52 | self.name = name 53 | self.description = description 54 | self.atomic_ordering = atomic_ordering 55 | self.objective = objective 56 | self.tags = set(tags) if tags else set() 57 | 58 | def store(self, ram): 59 | existing = self.retrieve(ram['adversaries'], self.unique) 60 | if not existing: 61 | ram['adversaries'].append(self) 62 | return self.retrieve(ram['adversaries'], self.unique) 63 | existing.update('name', self.name) 64 | existing.update('description', self.description) 65 | existing.update('atomic_ordering', self.atomic_ordering) 66 | existing.update('objective', self.objective) 67 | existing.update('tags', self.tags) 68 | return existing 69 | 70 | def has_ability(self, ability): 71 | for a in self.atomic_ordering: 72 | if ability == a: 73 | return True 74 | return False 75 | 76 | async def which_plugin(self): 77 | for plugin in os.listdir('plugins'): 78 | if await self.walk_file_path(os.path.join('plugins', plugin, 'data', ''), '%s.yml' % self.adversary_id): 79 | return plugin 80 | return None 81 | -------------------------------------------------------------------------------- /app/utility/base_object.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from app.utility.base_world import BaseWorld 4 | 5 | 6 | class BaseObject(BaseWorld): 7 | 8 | schema = None 9 | display_schema = None 10 | load_schema = None 11 | 12 | def __init__(self): 13 | self._access = self.Access.APP 14 | self._created = self.get_current_timestamp() 15 | 16 | def match(self, criteria): 17 | if not criteria: 18 | return self 19 | criteria_matches = [] 20 | for k, v in criteria.items(): 21 | if type(v) is tuple: 22 | for val in v: 23 | if getattr(self, k) == val: 24 | criteria_matches.append(True) 25 | else: 26 | if getattr(self, k) == v: 27 | criteria_matches.append(True) 28 | if len(criteria_matches) >= len(criteria) and all(criteria_matches): 29 | return self 30 | 31 | def update(self, field, value): 32 | if (value or type(value) == list) and (value != self.__getattribute__(field)): 33 | self.__setattr__(field, value) 34 | 35 | def search_tags(self, value): 36 | tags = getattr(self, 'tags', None) 37 | if tags and value in tags: 38 | return True 39 | 40 | @staticmethod 41 | def retrieve(collection, unique): 42 | return next((i for i in collection if i.unique == unique), None) 43 | 44 | @staticmethod 45 | def hash(s): 46 | return s 47 | 48 | @staticmethod 49 | def clean(d): 50 | for k, v in d.items(): 51 | if v is None: 52 | d[k] = '' 53 | return d 54 | 55 | @property 56 | def access(self): 57 | return self._access 58 | 59 | @property 60 | def created(self): 61 | return self._created 62 | 63 | @property 64 | def display(self): 65 | if self.display_schema: 66 | dumped = self.display_schema.dump(self) 67 | elif self.schema: 68 | dumped = self.schema.dump(self) 69 | else: 70 | raise NotImplementedError 71 | return self.clean(dumped) 72 | 73 | @access.setter 74 | def access(self, value): 75 | self._access = value 76 | 77 | @created.setter 78 | def created(self, value): 79 | self._created = value 80 | 81 | def replace_app_props(self, encoded_string): 82 | if encoded_string: 83 | decoded_test = self.decode_bytes(encoded_string) 84 | for k, v in self.get_config().items(): 85 | if k.startswith('app.'): 86 | re_variable = re.compile(r'#{(%s.*?)}' % k, flags=re.DOTALL) 87 | decoded_test = re.sub(re_variable, str(v).strip(), decoded_test) 88 | return self.encode_string(decoded_test) 89 | 90 | @classmethod 91 | def load(cls, dict_obj): 92 | if cls.load_schema: 93 | return cls.load_schema.load(dict_obj) 94 | elif cls.schema: 95 | return cls.schema.load(dict_obj) 96 | else: 97 | raise NotImplementedError 98 | -------------------------------------------------------------------------------- /app/service/interfaces/i_rest_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class RestServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def persist_adversary(self, access, data): 8 | """ 9 | Save a new adversary from either the GUI or REST API. This writes a new YML file into the core data/ directory. 10 | :param access 11 | :param data: 12 | :return: the ID of the created adversary 13 | """ 14 | pass 15 | 16 | @abc.abstractmethod 17 | def update_planner(self, data): 18 | """ 19 | Update a new planner from either the GUI or REST API with new stopping conditions. 20 | This overwrites the existing YML file. 21 | :param data: 22 | :return: the ID of the created adversary 23 | """ 24 | pass 25 | 26 | @abc.abstractmethod 27 | def persist_ability(self, access, data): 28 | pass 29 | 30 | @abc.abstractmethod 31 | def persist_source(self, access, data): 32 | pass 33 | 34 | @abc.abstractmethod 35 | def delete_agent(self, data): 36 | pass 37 | 38 | @abc.abstractmethod 39 | def delete_ability(self, data): 40 | pass 41 | 42 | @abc.abstractmethod 43 | def delete_adversary(self, data): 44 | pass 45 | 46 | @abc.abstractmethod 47 | def delete_operation(self, data): 48 | pass 49 | 50 | @abc.abstractmethod 51 | def display_objects(self, object_name, data): 52 | pass 53 | 54 | @abc.abstractmethod 55 | def display_result(self, data): 56 | pass 57 | 58 | @abc.abstractmethod 59 | def display_operation_report(self, data): 60 | pass 61 | 62 | @abc.abstractmethod 63 | def download_contact_report(self, contact): 64 | pass 65 | 66 | @abc.abstractmethod 67 | def update_agent_data(self, data): 68 | pass 69 | 70 | @abc.abstractmethod 71 | def update_chain_data(self, data): 72 | pass 73 | 74 | @abc.abstractmethod 75 | def create_operation(self, access, data): 76 | pass 77 | 78 | @abc.abstractmethod 79 | def create_schedule(self, access, data): 80 | pass 81 | 82 | @abc.abstractmethod 83 | def list_payloads(self): 84 | pass 85 | 86 | @abc.abstractmethod 87 | def find_abilities(self, paw): 88 | pass 89 | 90 | @abc.abstractmethod 91 | def get_potential_links(self, op_id, paw): 92 | pass 93 | 94 | @abc.abstractmethod 95 | def apply_potential_link(self, link): 96 | pass 97 | 98 | @abc.abstractmethod 99 | def task_agent_with_ability(self, paw, ability_id, obfuscator, facts): 100 | pass 101 | 102 | @abc.abstractmethod 103 | def get_link_pin(self, json_data): 104 | pass 105 | 106 | @abc.abstractmethod 107 | def construct_agents_for_group(self, group): 108 | pass 109 | 110 | @abc.abstractmethod 111 | def update_config(self, data): 112 | pass 113 | 114 | @abc.abstractmethod 115 | def update_operation(self, op_id, state, autonomous): 116 | pass 117 | -------------------------------------------------------------------------------- /app/service/learning_svc.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import glob 3 | import re 4 | from base64 import b64decode 5 | from importlib import import_module 6 | 7 | from app.objects.secondclass.c_relationship import Relationship 8 | from app.service.interfaces.i_learning_svc import LearningServiceInterface 9 | from app.utility.base_service import BaseService 10 | 11 | 12 | class LearningService(LearningServiceInterface, BaseService): 13 | 14 | def __init__(self): 15 | self.log = self.add_service('learning_svc', self) 16 | self.model = set() 17 | self.parsers = self.add_parsers('app/learning') 18 | self.re_variable = re.compile(r'#{(.*?)}', flags=re.DOTALL) 19 | self.log.debug('Loaded %d parsers' % len(self.parsers)) 20 | 21 | @staticmethod 22 | def add_parsers(directory): 23 | parsers = [] 24 | for filepath in glob.iglob('%s/**.py' % directory): 25 | module = import_module(filepath.replace('/', '.').replace('\\', '.').replace('.py', '')) 26 | parsers.append(module.Parser()) 27 | return parsers 28 | 29 | async def build_model(self): 30 | for ability in await self.get_service('data_svc').locate('abilities'): 31 | if ability.test: 32 | variables = frozenset(re.findall(self.re_variable, self.decode_bytes(ability.test))) 33 | if len(variables) > 1: # relationships require at least 2 variables 34 | self.model.add(variables) 35 | self.model = set(self.model) 36 | 37 | async def learn(self, facts, link, blob): 38 | decoded_blob = b64decode(blob).decode('utf-8') 39 | 40 | found_facts = [] 41 | for parser in self.parsers: 42 | try: 43 | for fact in parser.parse(decoded_blob): 44 | await self._save_fact(link, facts, fact) 45 | found_facts.append(fact) 46 | except Exception as e: 47 | self.log.error(e) 48 | await self._update_scores(link, facts, increment=len(found_facts)) 49 | await self._build_relationships(link, found_facts) 50 | 51 | """ PRIVATE """ 52 | 53 | @staticmethod 54 | async def _update_scores(link, facts, increment): 55 | for uf in link.facts: 56 | for found_fact in facts: 57 | if found_fact.unique == uf.unique: 58 | found_fact.score += increment 59 | break 60 | 61 | @staticmethod 62 | async def _save_fact(link, facts, fact): 63 | if all(fact.trait) and not any(f.trait == fact.trait and f.value == fact.value for f in facts): 64 | fact.collected_by = link.paw 65 | fact.technique_id = link.ability.technique_id 66 | link.facts.append(fact) 67 | 68 | async def _build_relationships(self, link, facts): 69 | for relationship in self.model: 70 | matches = [] 71 | for fact in facts: 72 | if fact.trait in relationship: 73 | matches.append(fact) 74 | for pair in itertools.combinations(matches, r=2): 75 | if pair[0].trait != pair[1].trait: 76 | link.relationships.append(Relationship(source=pair[0], edge='has', target=pair[1])) 77 | -------------------------------------------------------------------------------- /app/utility/file_decryptor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | import base64 4 | import argparse 5 | 6 | from cryptography.fernet import Fernet 7 | from cryptography.hazmat.backends import default_backend 8 | from cryptography.hazmat.primitives import hashes 9 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 10 | 11 | description = """ 12 | This script is for the purpose of decrypting encrypted files that are exfilled by caldera 13 | default output files are created in the same dir as input file and postpended with '_decrypted' 14 | 15 | examples: 16 | python file_decryptor.py filename 17 | - uses only the defaults and will use the current caldera config if ran from the app/utility dir 18 | python file_decryptor.py -c default.yml filename 19 | - you can specify a specific config to pass in as well 20 | python file_decryptor.py -k ADMIN123 -s WORDSMOREWORDS filename 21 | - you can also forgo a config and directly pass in the key and salt values 22 | python filedescriptor.py -b64 ../../data/results/554667-212609 23 | - enables b64 decoding of the stored value as well (useful for results files) 24 | """ 25 | FILE_ENCRYPTION_FLAG = '%encrypted%' 26 | 27 | 28 | def get_encryptor(salt, key): 29 | generated_key = PBKDF2HMAC(algorithm=hashes.SHA256(), 30 | length=32, 31 | salt=bytes(salt, 'utf-8'), 32 | iterations=2 ** 20, 33 | backend=default_backend()) 34 | return Fernet(base64.urlsafe_b64encode(generated_key.derive(bytes(key, 'utf-8')))) 35 | 36 | 37 | def read(filename, encryptor): 38 | with open(filename, 'rb') as f: 39 | buf = f.read() 40 | if buf.startswith(bytes(FILE_ENCRYPTION_FLAG, encoding='utf-8')): 41 | buf = encryptor.decrypt(buf[len(FILE_ENCRYPTION_FLAG):]) 42 | return buf 43 | 44 | 45 | def decrypt(filename, configuration, output_file=None, b64decode=False): 46 | encryptor = get_encryptor(configuration['crypt_salt'], configuration['encryption_key']) 47 | if not output_file: 48 | output_file = filename + '_decrypted' 49 | with open(output_file, 'wb') as f: 50 | if b64decode: 51 | f.write(base64.b64decode(read(filename, encryptor))) 52 | else: 53 | f.write(read(filename, encryptor)) 54 | print(f'file decrypted and written to {output_file}') 55 | 56 | 57 | if __name__ == '__main__': 58 | parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter) 59 | parser.add_argument('-k', '--key') 60 | parser.add_argument('-s', '--salt') 61 | parser.add_argument('-c', '--config', default='../../conf/default.yml') 62 | parser.add_argument('-b64', action='store_true', help='b64 decode data after decryption') 63 | parser.add_argument('input') 64 | parser.add_argument('output', nargs='?') 65 | 66 | args = parser.parse_args() 67 | config = {} 68 | if args.key and args.salt: 69 | config = dict(crypt_salt=args.salt, encryption_key=args.key) 70 | elif args.config and os.path.exists(args.config): 71 | with open(args.config, encoding='utf-8') as conf: 72 | config = list(yaml.load_all(conf, Loader=yaml.FullLoader))[0] 73 | else: 74 | print('please pass in a path to the caldera config file or a crypt salt and api key for decryption') 75 | 76 | decrypt(args.input, config, output_file=args.output, b64decode=args.b64) 77 | -------------------------------------------------------------------------------- /app/objects/c_plugin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from importlib import import_module 4 | 5 | import marshmallow as ma 6 | 7 | from app.objects.interfaces.i_object import FirstClassObjectInterface 8 | from app.utility.base_object import BaseObject 9 | 10 | 11 | class PluginSchema(ma.Schema): 12 | name = ma.fields.String() 13 | enabled = ma.fields.Boolean() 14 | address = ma.fields.String() 15 | description = ma.fields.String() 16 | data_dir = ma.fields.String() 17 | access = ma.fields.Integer() 18 | 19 | @ma.post_load 20 | def build_plugin(self, data, **_): 21 | return Plugin(**data) 22 | 23 | 24 | class Plugin(FirstClassObjectInterface, BaseObject): 25 | 26 | schema = PluginSchema() 27 | display_schema = PluginSchema(only=['name', 'enabled', 'address']) 28 | 29 | @property 30 | def unique(self): 31 | return self.hash(self.name) 32 | 33 | def __init__(self, name='virtual', description=None, address=None, enabled=False, data_dir=None, access=None): 34 | super().__init__() 35 | self.name = name 36 | self.description = description 37 | self.address = address 38 | self.enabled = enabled 39 | self.data_dir = data_dir 40 | self.access = access if access else self.Access.APP 41 | self.version = self.get_version('plugins/%s' % self.name.lower()) 42 | 43 | def store(self, ram): 44 | existing = self.retrieve(ram['plugins'], self.unique) 45 | if not existing: 46 | ram['plugins'].append(self) 47 | return self.retrieve(ram['plugins'], self.unique) 48 | else: 49 | existing.update('enabled', self.enabled) 50 | return existing 51 | 52 | def load_plugin(self): 53 | try: 54 | plugin = self._load_module() 55 | self.description = plugin.description 56 | self.address = plugin.address 57 | self.access = getattr(self._load_module(), 'access', self.Access.APP) 58 | return True 59 | except Exception as e: 60 | logging.error('Error loading plugin=%s, %s' % (self.name, e)) 61 | return False 62 | 63 | async def enable(self, services): 64 | try: 65 | if os.path.exists('plugins/%s/data' % self.name.lower()): 66 | self.data_dir = 'plugins/%s/data' % self.name.lower() 67 | plugin = self._load_module().enable 68 | await plugin(services) 69 | self.enabled = True 70 | except Exception as e: 71 | logging.error('Error enabling plugin=%s, %s' % (self.name, e)) 72 | 73 | async def destroy(self, services): 74 | if self.enabled: 75 | destroyable = getattr(self._load_module(), 'destroy', None) 76 | if destroyable: 77 | await destroyable(services) 78 | 79 | async def expand(self, services): 80 | try: 81 | if self.enabled: 82 | expansion = getattr(self._load_module(), 'expansion', None) 83 | if expansion: 84 | await expansion(services) 85 | except Exception as e: 86 | logging.error('Error expanding plugin=%s, %s' % (self.name, e)) 87 | 88 | """ PRIVATE """ 89 | 90 | def _load_module(self): 91 | try: 92 | return import_module('plugins.%s.hook' % self.name) 93 | except Exception as e: 94 | logging.error('Error importing plugin=%s, %s' % (self.name, e)) 95 | -------------------------------------------------------------------------------- /templates/configurations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Configuration

6 |
7 |

Settings

8 |

9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for k, v in config.items() %} 26 | {% if k.startswith('app.') %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% endif %} 35 | {% endfor %} 36 |
reports_dirreports are saved here
exfil_direxfiled files are saved here
{{ k}}a fact to use in a command
37 |

38 |

Plugins

39 |

40 | 41 | {% for plugin in plugins %} 42 | 43 | 44 | 45 | 46 | 47 | {% if plugin.enabled %} 48 | 49 | {% else %} 50 | 51 | {% endif %} 52 | 53 | {% endfor %} 54 |
{{ plugin.name }}{{ plugin.version }}{{ plugin.description }}
55 |
56 |
57 |
58 |
59 | 60 | 74 | -------------------------------------------------------------------------------- /app/service/interfaces/i_file_svc.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class FileServiceInterface(abc.ABC): 5 | 6 | @abc.abstractmethod 7 | def get_file(self, headers): 8 | """ 9 | Retrieve file 10 | :param headers: headers dictionary. The `file` key is REQUIRED. 11 | :type headers: dict or dict-equivalent 12 | :return: File contents and optionally a display_name if the payload is a special payload 13 | :raises: KeyError if file key is not provided, FileNotFoundError if file cannot be found 14 | """ 15 | pass 16 | 17 | @abc.abstractmethod 18 | def save_file(self, filename, payload, target_dir): 19 | pass 20 | 21 | @abc.abstractmethod 22 | def create_exfil_sub_directory(self, dir_name): 23 | pass 24 | 25 | @abc.abstractmethod 26 | def save_multipart_file_upload(self, request, target_dir): 27 | """ 28 | Accept a multipart file via HTTP and save it to the server 29 | :param request: 30 | :param target_dir: The path of the directory to save the uploaded file to. 31 | """ 32 | pass 33 | 34 | @abc.abstractmethod 35 | def find_file_path(self, name, location): 36 | """ 37 | Find the location on disk of a file by name. 38 | :param name: 39 | :param location: 40 | :return: a tuple: the plugin the file is found in & the relative file path 41 | """ 42 | pass 43 | 44 | @abc.abstractmethod 45 | def read_file(self, name, location): 46 | """ 47 | Open a file and read the contents 48 | :param name: 49 | :param location: 50 | :return: a tuple (file_path, contents) 51 | """ 52 | pass 53 | 54 | @abc.abstractmethod 55 | def read_result_file(self, link_id, location): 56 | """ 57 | Read a result file. If file encryption is enabled, this method will return the plaintext 58 | content. 59 | :param link_id: The id of the link to return results from. 60 | :param location: The path to results directory. 61 | :return: 62 | """ 63 | pass 64 | 65 | @abc.abstractmethod 66 | def write_result_file(self, link_id, output, location): 67 | """ 68 | Writes the results of a link execution to disk. If file encryption is enabled, 69 | the results file will contain ciphertext. 70 | :param link_id: The link id of the result being written. 71 | :param output: The content of the link's output. 72 | :param location: The path to the results directory. 73 | :return: 74 | """ 75 | pass 76 | 77 | @abc.abstractmethod 78 | def add_special_payload(self, name, func): 79 | """ 80 | Call a special function when specific payloads are downloaded 81 | :param name: 82 | :param func: 83 | :return: 84 | """ 85 | pass 86 | 87 | @abc.abstractmethod 88 | def compile_go(self, platform, output, src_fle, arch, ldflags, cflags, buildmode, build_dir, loop): 89 | """ 90 | Dynamically compile a go file 91 | :param platform: 92 | :param output: 93 | :param src_fle: 94 | :param arch: Compile architecture selection (defaults to AMD64) 95 | :param ldflags: A string of ldflags to use when building the go executable 96 | :param cflags: A string of CFLAGS to pass to the go compiler 97 | :param buildmode: GO compiler buildmode flag 98 | :param build_dir: The path to build should take place in 99 | :return: 100 | """ 101 | pass 102 | 103 | @abc.abstractmethod 104 | def get_payload_name_from_uuid(self, payload): 105 | pass 106 | -------------------------------------------------------------------------------- /app/service/event_svc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | import json 4 | import websockets 5 | 6 | from app.service.interfaces.i_event_svc import EventServiceInterface 7 | from app.utility.base_service import BaseService 8 | 9 | 10 | class EventService(EventServiceInterface, BaseService): 11 | 12 | def __init__(self): 13 | self.log = self.add_service('event_svc', self) 14 | self.contact_svc = self.get_service('contact_svc') 15 | self.ws_uri = 'ws://{}'.format(self.get_config('app.contact.websocket')) 16 | self.global_listeners = [] 17 | self.default_exchange = 'caldera' 18 | self.default_queue = 'general' 19 | 20 | async def observe_event(self, callback, exchange=None, queue=None): 21 | """ 22 | Register a callback for a certain event. Callback is fired when 23 | an event of that type is observed. 24 | 25 | :param callback: Callback function 26 | :type callback: function 27 | :param exchange: event exchange 28 | :type exchange: str 29 | :param queue: event queue 30 | :type queue: str 31 | """ 32 | exchange = exchange or self.default_exchange 33 | queue = queue or self.default_queue 34 | path = '/'.join([exchange, queue]) 35 | handle = _Handle(path, callback) 36 | ws_contact = await self.contact_svc.get_contact('websocket') 37 | ws_contact.handler.handles.append(handle) 38 | 39 | async def register_global_event_listener(self, callback): 40 | """ 41 | Register a global event listener that is fired when any event 42 | is fired. 43 | 44 | :param callback: Callback function 45 | :type callback: function 46 | """ 47 | self.global_listeners.append(callback) 48 | 49 | async def notify_global_event_listeners(self, event, **callback_kwargs): 50 | """ 51 | Notify all registered global event listeners when an event is fired. 52 | 53 | :param event: Event string (i.e. '/') 54 | :type event: str 55 | """ 56 | for c in self.global_listeners: 57 | try: 58 | c(event, **callback_kwargs) 59 | except Exception as e: 60 | self.log.error("Global callback error: {}".format(e), exc_info=True) 61 | 62 | async def handle_exceptions(self, awaitable): 63 | try: 64 | return await awaitable 65 | except websockets.exceptions.ConnectionClosedOK: 66 | pass # No handler was registered for this event 67 | except Exception as e: 68 | self.log.error("WebSocket error: {}".format(e), exc_info=True) 69 | 70 | async def fire_event(self, exchange=None, queue=None, timestamp=True, **callback_kwargs): 71 | exchange = exchange or self.default_exchange 72 | queue = queue or self.default_queue 73 | metadata = {} 74 | if timestamp: 75 | metadata.update(dict(timestamp=datetime.datetime.now().timestamp())) 76 | callback_kwargs.update(dict(metadata=metadata)) 77 | uri = '/'.join([self.ws_uri, exchange, queue]) 78 | if self.global_listeners: 79 | asyncio.get_event_loop().create_task(self.notify_global_event_listeners('/'.join([exchange, queue]), 80 | **callback_kwargs)) 81 | d = json.dumps(callback_kwargs) 82 | async with websockets.connect(uri) as websocket: 83 | asyncio.get_event_loop().create_task(self.handle_exceptions(websocket.send(d))) 84 | 85 | 86 | class _Handle: 87 | 88 | def __init__(self, tag, callback): 89 | self.tag = tag 90 | self.callback = callback 91 | 92 | async def run(self, socket, path, services): 93 | return await self.callback(socket, path, services) 94 | -------------------------------------------------------------------------------- /tests/services/test_data_svc.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from app.objects.c_ability import Ability 4 | from app.objects.c_adversary import Adversary 5 | from app.objects.c_agent import Agent 6 | from app.objects.c_operation import Operation 7 | from app.objects.c_planner import Planner 8 | 9 | 10 | class TestDataService: 11 | 12 | def test_no_duplicate_adversary(self, loop, data_svc): 13 | loop.run_until_complete(data_svc.store( 14 | Adversary(adversary_id='123', name='test', description='test adversary', atomic_ordering=list()) 15 | )) 16 | loop.run_until_complete(data_svc.store( 17 | Adversary(adversary_id='123', name='test', description='test adversary', atomic_ordering=list()) 18 | )) 19 | adversaries = loop.run_until_complete(data_svc.locate('adversaries')) 20 | 21 | assert len(adversaries) == 1 22 | for x in adversaries: 23 | json.dumps(x.display) 24 | 25 | def test_no_duplicate_planner(self, loop, data_svc): 26 | loop.run_until_complete(data_svc.store(Planner(name='test', planner_id='some_id', module='some.path.here', params=None, description='description'))) 27 | loop.run_until_complete(data_svc.store(Planner(name='test', planner_id='some_id', module='some.path.here', params=None, description='description'))) 28 | planners = loop.run_until_complete(data_svc.locate('planners')) 29 | 30 | assert len(planners) == 1 31 | for x in planners: 32 | json.dumps(x.display) 33 | 34 | def test_multiple_agents(self, loop, data_svc): 35 | loop.run_until_complete(data_svc.store(Agent(sleep_min=2, sleep_max=8, watchdog=0))) 36 | loop.run_until_complete(data_svc.store(Agent(sleep_min=2, sleep_max=8, watchdog=0))) 37 | agents = loop.run_until_complete(data_svc.locate('agents')) 38 | 39 | assert len(agents) == 2 40 | for x in agents: 41 | json.dumps(x.display) 42 | 43 | def test_no_duplicate_ability(self, loop, data_svc): 44 | loop.run_until_complete(data_svc.store( 45 | Ability(ability_id='123', tactic='discovery', technique_id='1', technique='T1033', name='test', 46 | test='d2hvYW1pCg==', description='find active user', cleanup='', executor='sh', 47 | platform='darwin', payloads=['wifi.sh'], parsers=[], requirements=[], privilege=None, 48 | variations=[]) 49 | )) 50 | loop.run_until_complete(data_svc.store( 51 | Ability(ability_id='123', tactic='discovery', technique_id='1', technique='T1033', name='test', 52 | test='d2hvYW1pCg==', description='find active user', cleanup='', executor='sh', 53 | platform='darwin', payloads=['wifi.sh'], parsers=[], requirements=[], privilege=None, 54 | variations=[]) 55 | )) 56 | abilities = loop.run_until_complete(data_svc.locate('abilities')) 57 | 58 | assert len(abilities) == 1 59 | 60 | def test_operation(self, loop, data_svc): 61 | adversary = loop.run_until_complete(data_svc.store( 62 | Adversary(adversary_id='123', name='test', description='test adversary', atomic_ordering=list()) 63 | )) 64 | loop.run_until_complete(data_svc.store(Operation(name='my first op', agents=[], adversary=adversary))) 65 | 66 | operations = loop.run_until_complete(data_svc.locate('operations')) 67 | assert len(operations) == 1 68 | for x in operations: 69 | json.dumps(x.display) 70 | 71 | def test_remove(self, loop, data_svc): 72 | a1 = loop.run_until_complete(data_svc.store(Agent(sleep_min=2, sleep_max=8, watchdog=0))) 73 | agents = loop.run_until_complete(data_svc.locate('agents', match=dict(paw=a1.paw))) 74 | assert len(agents) == 1 75 | loop.run_until_complete(data_svc.remove('agents', match=dict(paw=a1.paw))) 76 | agents = loop.run_until_complete(data_svc.locate('agents', match=dict(paw=a1.paw))) 77 | assert len(agents) == 0 78 | -------------------------------------------------------------------------------- /static/js/ability.js: -------------------------------------------------------------------------------- 1 | 2 | function addPlatforms(abilities) { 3 | let ab = []; 4 | abilities.forEach(function(a) { 5 | let exists = false; 6 | for(let i in ab){ 7 | if(ab[i].ability_id === a.ability_id) { 8 | ab[i]['platform'].push(a.platform); 9 | ab[i]['executor'].push(a.executor); 10 | exists = true; 11 | break; 12 | } 13 | } 14 | if(!exists) { 15 | a_copy = Object.assign({}, a) 16 | a_copy['platform'] = [a.platform]; 17 | a_copy['executor'] = [a.executor]; 18 | ab.push(a_copy); 19 | } 20 | }); 21 | return ab; 22 | } 23 | 24 | function populateTechniques(parentId, exploits){ 25 | exploits = addPlatforms(exploits); 26 | let parent = $('#'+parentId); 27 | $(parent).find('#ability-technique-filter').empty().append(""); 28 | 29 | let tactic = $(parent).find('#ability-tactic-filter').find(":selected").data('tactic'); 30 | let found = []; 31 | exploits.forEach(function(ability) { 32 | if(ability.tactic.includes(tactic) && !found.includes(ability.technique_id)) { 33 | found.push(ability.technique_id); 34 | appendTechniqueToList(parentId, tactic, ability); 35 | } 36 | }); 37 | } 38 | 39 | /** 40 | * Populate the abilities dropdown based on selected technique 41 | * @param {string} parentId - Parent ID used to search for dropdowns 42 | * @param {object[]} abilities - Abilities object array 43 | */ 44 | function populateAbilities (parentId, abilities) { 45 | abilities = addPlatforms(abilities); 46 | let parent = $('#' + parentId); 47 | 48 | // Collect abilities matching technique 49 | let techniqueAbilities = []; 50 | let attack_id = $(parent).find('#ability-technique-filter').find(':selected').data('technique'); 51 | abilities.forEach(function (ability) { 52 | if (ability.technique_id === attack_id) { 53 | techniqueAbilities.push(ability); 54 | } 55 | }); 56 | 57 | // Clear, then populate the ability dropdown 58 | $(parent).find('#ability-ability-filter').empty().append(''); 59 | techniqueAbilities.forEach(function (ability) { 60 | appendAbilityToList(parentId, ability); 61 | }); 62 | } 63 | 64 | function appendTechniqueToList(parentId, tactic, value) { 65 | $('#'+parentId).find('#ability-technique-filter').append($("") 66 | .attr("value", value['technique_id']) 67 | .data("technique", value['technique_id']) 68 | .text(value['technique_id'] + ' | '+ value['technique_name'])); 69 | } 70 | 71 | function appendAbilityToList(parentId, value) { 72 | $('#'+parentId).find('#ability-ability-filter').append($("") 73 | .attr("value", value['name']) 74 | .data("ability", value) 75 | .text(value['name'])); 76 | } 77 | 78 | function searchAbilities(parent, abilities){ 79 | let pElem = $('#'+parent); 80 | pElem.find('#ability-technique-filter').empty().append("");; 81 | let abList = pElem.find('#ability-ability-filter'); 82 | abList.empty(); 83 | let val = pElem.find('#ability-search-filter').val().toLowerCase(); 84 | let added = []; 85 | if(val){ 86 | abilities.forEach(function(ab){ 87 | let cmd = atob(ab['test']); 88 | if ( 89 | ( 90 | ab['name'].toLowerCase().includes(val) || 91 | ab['description'].toLowerCase().includes(val) || 92 | cmd.toLowerCase().includes(val) 93 | ) && !added.includes(ab['ability_id']) 94 | ){ 95 | let composite = addPlatforms([ab]); 96 | added.push(ab['ability_id']); 97 | appendAbilityToList(parent, composite[0]); 98 | } 99 | }); 100 | } 101 | abList.prepend(""); 102 | } 103 | -------------------------------------------------------------------------------- /app/api/packs/campaign.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from collections import defaultdict 3 | 4 | from aiohttp_jinja2 import template 5 | 6 | from app.service.auth_svc import check_authorization 7 | from app.utility.base_world import BaseWorld 8 | 9 | 10 | class CampaignPack(BaseWorld): 11 | 12 | def __init__(self, services): 13 | self.auth_svc = services.get('auth_svc') 14 | self.app_svc = services.get('app_svc') 15 | self.data_svc = services.get('data_svc') 16 | self.rest_svc = services.get('rest_svc') 17 | 18 | async def enable(self): 19 | self.app_svc.application.router.add_route('GET', '/campaign/agents', self._section_agent) 20 | self.app_svc.application.router.add_route('GET', '/campaign/profiles', self._section_profiles) 21 | self.app_svc.application.router.add_route('GET', '/campaign/operations', self._section_operations) 22 | 23 | """ PRIVATE """ 24 | 25 | @check_authorization 26 | @template('agents.html') 27 | async def _section_agent(self, request): 28 | search = dict(access=tuple(await self.auth_svc.get_permissions(request))) 29 | agents = [h.display for h in await self.data_svc.locate('agents', match=search)] 30 | ability_ids = tuple(self.get_config(name='agents', prop='deployments')) 31 | abilities = await self.data_svc.locate('abilities', match=dict(ability_id=ability_ids)) 32 | agent_config = self.get_config(name='agents') 33 | return dict(agents=agents, abilities=self._rollup_abilities(abilities), agent_config=agent_config) 34 | 35 | @check_authorization 36 | @template('profiles.html') 37 | async def _section_profiles(self, request): 38 | access = dict(access=tuple(await self.auth_svc.get_permissions(request))) 39 | abilities = await self.data_svc.locate('abilities', match=access) 40 | objs = await self.data_svc.locate('objectives', match=access) 41 | platforms = dict() 42 | for a in abilities: 43 | if a.platform in platforms: 44 | platforms[a.platform].add(a.executor) 45 | else: 46 | platforms[a.platform] = set([a.executor]) 47 | for p in platforms: 48 | platforms[p] = list(platforms[p]) 49 | tactics = sorted(list(set(a.tactic.lower() for a in abilities))) 50 | payloads = await self.rest_svc.list_payloads() 51 | adversaries = sorted([a.display for a in await self.data_svc.locate('adversaries', match=access)], 52 | key=lambda a: a['name']) 53 | exploits = sorted([a.display for a in abilities], key=operator.itemgetter('technique_id', 'name')) 54 | objectives = sorted([a.display for a in objs], key=operator.itemgetter('id', 'name')) 55 | return dict(adversaries=adversaries, exploits=exploits, payloads=payloads, 56 | tactics=tactics, platforms=platforms, objectives=objectives) 57 | 58 | @check_authorization 59 | @template('operations.html') 60 | async def _section_operations(self, request): 61 | access = dict(access=tuple(await self.auth_svc.get_permissions(request))) 62 | hosts = [h.display for h in await self.data_svc.locate('agents', match=access)] 63 | groups = sorted(list(set(([h['group'] for h in hosts])))) 64 | adversaries = sorted([a.display for a in await self.data_svc.locate('adversaries', match=access)], 65 | key=lambda a: a['name']) 66 | sources = [s.display for s in await self.data_svc.locate('sources', match=access)] 67 | planners = sorted([p.display for p in await self.data_svc.locate('planners')], 68 | key=lambda p: p['name']) 69 | obfuscators = [o.display for o in await self.data_svc.locate('obfuscators')] 70 | operations = [o.display for o in await self.data_svc.locate('operations', match=access)] 71 | return dict(operations=operations, groups=groups, adversaries=adversaries, sources=sources, planners=planners, 72 | obfuscators=obfuscators) 73 | 74 | """ PRIVATE """ 75 | 76 | @staticmethod 77 | def _rollup_abilities(abilities): 78 | rolled = defaultdict(list) 79 | for a in abilities: 80 | rolled[a.ability_id].append(a.display) 81 | return dict(rolled) 82 | -------------------------------------------------------------------------------- /tests/utility/test_base_world.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import yaml 3 | 4 | from datetime import datetime 5 | from app.utility.base_world import BaseWorld 6 | 7 | 8 | class TestBaseWorld: 9 | 10 | default_config = dict(name='main', config={'app.contact.http': '0.0.0.0', 'plugins': ['sandcat', 'stockpile']}) 11 | 12 | default_yaml = dict(test_dir=1, implant_name='unittesting', test_int=1234) 13 | 14 | @pytest.fixture 15 | def reset_config(self): 16 | BaseWorld.apply_config(**self.default_config) 17 | yield 18 | BaseWorld._app_configuration = dict() 19 | 20 | @pytest.fixture 21 | def yaml_file(self, tmpdir): 22 | f = tmpdir.mkdir('yml').join('test.yml') 23 | yaml_str = yaml.dump(self.default_yaml) 24 | f.write(yaml_str) 25 | assert f.read() == yaml_str 26 | yield f 27 | 28 | @pytest.fixture 29 | def text_file(self, tmpdir): 30 | txt_str = 'Hello world!' 31 | f = tmpdir.mkdir('txt').join('test.txt') 32 | f.write(txt_str) 33 | assert f.read() == txt_str 34 | yield f 35 | 36 | @pytest.mark.usefixtures('reset_config') 37 | def test_apply_and_retrieve_config(self): 38 | new_config = dict(name='newconfig', config={'app.unit.test': 'abcd12345', 'plugins': ['stockpile']}) 39 | BaseWorld.apply_config(**new_config) 40 | 41 | assert BaseWorld.get_config(name='newconfig') == new_config['config'] 42 | 43 | @pytest.mark.usefixtures('reset_config') 44 | def test_get_prop_from_config(self): 45 | assert BaseWorld.get_config(name='main', prop='app.contact.http') == '0.0.0.0' 46 | 47 | @pytest.mark.usefixtures('reset_config') 48 | def test_set_prop_from_config(self): 49 | BaseWorld.set_config(name='main', prop='newprop', value='unittest') 50 | assert BaseWorld.get_config(name='main', prop='newprop') == 'unittest' 51 | 52 | def test_encode_and_decode_string(self): 53 | plaintext = 'unit testing string' 54 | encoded_text = 'dW5pdCB0ZXN0aW5nIHN0cmluZw==' 55 | encoded_str = BaseWorld.encode_string(plaintext) 56 | 57 | assert encoded_str == encoded_text 58 | 59 | decoded_str = BaseWorld.decode_bytes(encoded_text) 60 | assert decoded_str == plaintext 61 | 62 | def test_jitter(self): 63 | fraction = "1/5" 64 | frac_arr = fraction.split('/') 65 | jitter = BaseWorld.jitter(fraction) 66 | assert jitter >= int(frac_arr[0]) 67 | assert jitter <= int(frac_arr[1]) 68 | 69 | def test_strip_yml_no_path(self): 70 | yaml = BaseWorld.strip_yml(None) 71 | assert yaml == [] 72 | 73 | def test_strip_yml(self, yaml_file): 74 | yaml = BaseWorld.strip_yml(yaml_file) 75 | assert yaml == [self.default_yaml] 76 | 77 | def test_prepend_to_file(self, text_file): 78 | line = 'This is appended!' 79 | BaseWorld.prepend_to_file(text_file, line) 80 | assert 'This is appended!\nHello world!' == text_file.read() 81 | 82 | def test_get_current_timestamp(self): 83 | date_format = '%Y-%m-%d %H' 84 | output = BaseWorld.get_current_timestamp(date_format) 85 | cur_time = datetime.now().strftime(date_format) 86 | assert cur_time == output 87 | 88 | def test_is_not_base64(self): 89 | assert not BaseWorld.is_base64('not base64') 90 | 91 | def test_is_base64(self): 92 | b64str = 'aGVsbG8gd29ybGQgZnJvbSB1bml0IHRlc3QgbGFuZAo=' 93 | assert BaseWorld.is_base64(b64str) 94 | 95 | def test_walk_file_path_exists_nonxor(self, loop, text_file): 96 | ret = loop.run_until_complete(BaseWorld.walk_file_path(text_file.dirname, text_file.basename)) 97 | assert ret == text_file 98 | 99 | def test_walk_file_path_notexists(self, loop, text_file): 100 | ret = loop.run_until_complete(BaseWorld.walk_file_path(text_file.dirname, 'not-a-real.file')) 101 | assert ret is None 102 | 103 | def test_walk_file_path_xor_fn(self, loop, tmpdir): 104 | f = tmpdir.mkdir('txt').join('xorfile.txt.xored') 105 | f.write("test") 106 | ret = loop.run_until_complete(BaseWorld.walk_file_path(f.dirname, 'xorfile.txt')) 107 | assert ret == f 108 | -------------------------------------------------------------------------------- /static/css/timeline.css: -------------------------------------------------------------------------------- 1 | a, 2 | span, 3 | h1, 4 | h2, 5 | h3, 6 | span { 7 | text-decoration: none; 8 | } 9 | 10 | a:hover { 11 | color: #777; 12 | } 13 | .member-title { 14 | font-family: 'Raleway', sans-serif; 15 | letter-spacing: 1.5px; 16 | color: var(--font-color); 17 | font-weight: 100; 18 | font-size: 2.4em; 19 | margin: 0; 20 | border-bottom: 1px solid #777; 21 | padding-bottom: 0.2em; 22 | } 23 | #content { 24 | margin-top: 50px; 25 | text-align: center; 26 | } 27 | .timeline { 28 | border-left: 0.25em solid var(--theme-color); 29 | background: rgba(255, 255, 255, 0.1); 30 | margin: 2em auto; 31 | line-height: 1.4em; 32 | padding: 1em; 33 | padding-left: 3em; 34 | list-style: none; 35 | text-align: left; 36 | margin-left: 10em; 37 | margin-right: 3em; 38 | border-radius: 0.5em; 39 | min-width: 22em; 40 | } 41 | .event { 42 | min-width: 20em; 43 | width: 90%; 44 | vertical-align: middle; 45 | box-sizing: border-box; 46 | position: relative; 47 | } 48 | .timeline .event:before, 49 | .timeline .event:after { 50 | position: absolute; 51 | display: block; 52 | top: 1em; 53 | } 54 | .timeline .event:before { 55 | left: -15em; 56 | color: var(--font-color); 57 | content: attr(data-date); 58 | text-align: right; 59 | font-weight: 100; 60 | font-size: 0.9em; 61 | min-width: 9em; 62 | } 63 | .timeline .event:after { 64 | left: -3.5em; 65 | background: var(--primary-background); 66 | border-radius: 50%; 67 | height: 0.75em; 68 | width: 0.75em; 69 | content: ""; 70 | } 71 | .timeline .queued:after { 72 | box-shadow: 0 0 0 0.2em #555555; // darkgrey 73 | } 74 | .timeline .failure:after { 75 | box-shadow: 0 0 0 0.2em #CC3311; // red 76 | } 77 | .timeline .success:after { 78 | box-shadow: 0 0 0 0.2em #44AA99; 79 | } 80 | .timeline .timeout:after { 81 | box-shadow: 0 0 0 0.2em cornflowerblue; 82 | } 83 | .timeline .collected:after { 84 | box-shadow: 0 0 0 0.2em #FFB000; //orange 85 | } 86 | .timeline .untrusted:after { 87 | box-shadow: 0 0 0 0.2em white; 88 | } 89 | .timeline .visibility:after { 90 | box-shadow: 0 0 0 0.2em #F012BE; 91 | } 92 | .timeline .discarded:after {} 93 | 94 | .timeline .event .member-location, 95 | .timeline .event .member-parameters { 96 | display: none; 97 | } 98 | .timeline .event:last-of-type .member-location, 99 | .timeline .event:last-of-type .member-parameters { 100 | display: block; 101 | } 102 | .tactic-find-result { 103 | font-size: 14px; 104 | float: right; 105 | display: none; 106 | cursor: pointer; 107 | } 108 | .tactic-find-result.tactic-no-facts { 109 | display: block; 110 | font-size: 17px; 111 | margin-right: .14em; 112 | } 113 | .tactic-no-facts:before { 114 | content: "\2605"; 115 | } 116 | .tactic-find-result.tactic-has-facts { 117 | display: block; 118 | } 119 | .tactic-has-facts:before { 120 | content: "\2b50"; 121 | } 122 | .member-infos { 123 | padding: 10px; 124 | text-align: left; 125 | position: relative; 126 | } 127 | .member-infos > h1 { 128 | font-weight: bold; 129 | font-size: 1.4em; 130 | } 131 | .member-location a:before { 132 | margin-right: 5px; 133 | } 134 | .member-location { 135 | text-indent: 2px; 136 | } 137 | .member-result { 138 | color: var(--font-color); 139 | font-size: 13px; 140 | word-break: break-word; 141 | } 142 | .tooltip { 143 | position: relative; 144 | display: inline-block; 145 | font-size: 13px; 146 | } 147 | .tooltiptext { 148 | width: 120px; 149 | background-color: black; 150 | color: #fff; 151 | text-align: center; 152 | border-radius: 6px; 153 | padding: 5px 0; 154 | 155 | /* Position the tooltip */ 156 | position: absolute; 157 | font-size: 13px; 158 | } 159 | 160 | .cleanup ul{ 161 | padding-left: 0; 162 | } 163 | .cleanup .event:after { 164 | display: none; 165 | } 166 | 167 | #cleanup-title p { 168 | text-transform: uppercase; 169 | font-size: 15px; 170 | font-weight: 300; 171 | } 172 | 173 | .cleanup .event:before { 174 | content: attr(data-host-name); 175 | } 176 | 177 | .loop-section { 178 | border-bottom:3px solid #777; 179 | margin:0 9; 180 | margin-bottom:5px; 181 | } 182 | 183 | .hil-command { 184 | width: 97%; 185 | background-color: black; 186 | border-radius: 10px; 187 | padding: 5px; 188 | padding-left: 10px; 189 | margin: 5px; 190 | margin-bottom: 15px; 191 | } -------------------------------------------------------------------------------- /app/contacts/contact_tcp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import socket 4 | import time 5 | 6 | from app.utility.base_world import BaseWorld 7 | from plugins.manx.app.c_session import Session 8 | 9 | 10 | class Contact(BaseWorld): 11 | 12 | def __init__(self, services): 13 | self.name = 'tcp' 14 | self.description = 'Accept beacons through a raw TCP socket' 15 | self.log = self.create_logger('contact_tcp') 16 | self.contact_svc = services.get('contact_svc') 17 | self.tcp_handler = TcpSessionHandler(services, self.log) 18 | 19 | async def start(self): 20 | loop = asyncio.get_event_loop() 21 | tcp = self.get_config('app.contact.tcp') 22 | loop.create_task(asyncio.start_server(self.tcp_handler.accept, *tcp.split(':'), loop=loop)) 23 | loop.create_task(self.operation_loop()) 24 | 25 | async def operation_loop(self): 26 | while True: 27 | await self.tcp_handler.refresh() 28 | for session in self.tcp_handler.sessions: 29 | _, instructions = await self.contact_svc.handle_heartbeat(paw=session.paw) 30 | for instruction in instructions: 31 | try: 32 | self.log.debug('TCP instruction: %s' % instruction.id) 33 | status, _, response = await self.tcp_handler.send(session.id, self.decode_bytes(instruction.command)) 34 | beacon = dict(paw=session.paw, results=[dict(id=instruction.id, output=self.encode_string(response), status=status)]) 35 | await self.contact_svc.handle_heartbeat(**beacon) 36 | await asyncio.sleep(instruction.sleep) 37 | except Exception as e: 38 | self.log.debug('[-] operation exception: %s' % e) 39 | await asyncio.sleep(20) 40 | 41 | 42 | class TcpSessionHandler(BaseWorld): 43 | 44 | def __init__(self, services, log): 45 | self.services = services 46 | self.log = log 47 | self.sessions = [] 48 | 49 | async def refresh(self): 50 | for index, session in enumerate(self.sessions): 51 | try: 52 | session.connection.send(str.encode(' ')) 53 | except socket.error: 54 | del self.sessions[index] 55 | 56 | async def accept(self, reader, writer): 57 | try: 58 | profile = await self._handshake(reader) 59 | except Exception as e: 60 | self.log.debug('Handshake failed: %s' % e) 61 | return 62 | connection = writer.get_extra_info('socket') 63 | profile['executors'] = [e for e in profile['executors'].split(',') if e] 64 | profile['contact'] = 'tcp' 65 | agent, _ = await self.services.get('contact_svc').handle_heartbeat(**profile) 66 | new_session = Session(id=self.generate_number(size=6), paw=agent.paw, connection=connection) 67 | self.sessions.append(new_session) 68 | await self.send(new_session.id, agent.paw) 69 | 70 | async def send(self, session_id, cmd): 71 | try: 72 | conn = next(i.connection for i in self.sessions if i.id == int(session_id)) 73 | conn.send(str.encode(' ')) 74 | conn.send(str.encode('%s\n' % cmd)) 75 | response = await self._attempt_connection(conn, 3) 76 | response = json.loads(response) 77 | return response['status'], response["pwd"], response['response'] 78 | except Exception as e: 79 | return 1, '~$ ', e 80 | 81 | """ PRIVATE """ 82 | 83 | @staticmethod 84 | async def _handshake(reader): 85 | profile_bites = (await reader.readline()).strip() 86 | return json.loads(profile_bites) 87 | 88 | @staticmethod 89 | async def _attempt_connection(connection, max_tries): 90 | attempts = 0 91 | buffer = 4096 92 | data = b'' 93 | while True: 94 | try: 95 | part = connection.recv(buffer) 96 | data += part 97 | if len(part) < buffer: 98 | break 99 | except BlockingIOError as err: 100 | if attempts > max_tries: 101 | return json.dumps(dict(status=1, pwd='~$ ', response=str(err))) 102 | attempts += 1 103 | time.sleep(.1 * attempts) 104 | return str(data, 'utf-8') 105 | -------------------------------------------------------------------------------- /templates/BLUE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Blue | Dashboard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 |
20 | × 21 |
22 |
23 | 24 |
25 |
26 | 29 | agents 30 | defenders 31 | operations 32 | 35 | {% for plugin in plugins | sort(attribute='name') %} 36 | {% if plugin.address %} 37 | {{ plugin.name }} 38 | {% else %} 39 | {{ plugin.name }} 40 | {% endif %} 41 | {% endfor %} 42 | 45 | sources 46 | 49 | Startup errors 50 | Log out 51 |
52 |
53 | 54 | 55 |
56 | 57 | 58 | 59 | 71 | 72 | 82 | 83 | 93 | 94 |
95 |
96 |
97 | 98 |
99 | 100 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /templates/RED.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Red | Dashboard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 |
20 | × 21 |
22 |
23 | 24 |
25 |
26 | 29 | agents 30 | adversaries 31 | operations 32 | 35 | {% for plugin in plugins | sort(attribute='name') %} 36 | {% if plugin.address %} 37 | {{ plugin.name }} 38 | {% else %} 39 | {{ plugin.name }} 40 | {% endif %} 41 | {% endfor %} 42 | 45 | sources 46 | objectives 47 | planners 48 | contacts 49 | obfuscators 50 | configuration 51 | 54 | Startup errors 55 | Log out 56 |
57 |
58 | 59 |
60 | 61 | 62 | 63 | 75 | 76 | 86 | 87 | 97 | 98 |
99 |
100 |
101 | 102 |
103 | 104 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import logging 4 | import os 5 | import sys 6 | 7 | from aiohttp import web 8 | 9 | from app.api.rest_api import RestApi 10 | from app.service.app_svc import AppService 11 | from app.service.auth_svc import AuthService 12 | from app.service.contact_svc import ContactService 13 | from app.service.data_svc import DataService 14 | from app.service.event_svc import EventService 15 | from app.service.file_svc import FileSvc 16 | from app.service.learning_svc import LearningService 17 | from app.service.planning_svc import PlanningService 18 | from app.service.rest_svc import RestService 19 | from app.utility.base_world import BaseWorld 20 | from app.utility.config_generator import ensure_local_config 21 | 22 | 23 | def setup_logger(level=logging.DEBUG): 24 | logging.basicConfig(level=level, 25 | format='%(asctime)s - %(levelname)-5s (%(filename)s:%(lineno)s %(funcName)s) %(message)s', 26 | datefmt='%Y-%m-%d %H:%M:%S') 27 | for logger_name in logging.root.manager.loggerDict.keys(): 28 | if logger_name in ('aiohttp.server', 'asyncio'): 29 | continue 30 | else: 31 | logging.getLogger(logger_name).setLevel(100) 32 | logging.captureWarnings(True) 33 | 34 | 35 | async def start_server(): 36 | await auth_svc.apply(app_svc.application, BaseWorld.get_config('users')) 37 | runner = web.AppRunner(app_svc.application) 38 | await runner.setup() 39 | await web.TCPSite(runner, BaseWorld.get_config('host'), BaseWorld.get_config('port')).start() 40 | 41 | 42 | def run_tasks(services): 43 | loop = asyncio.get_event_loop() 44 | loop.create_task(app_svc.validate_requirements()) 45 | loop.run_until_complete(data_svc.restore_state()) 46 | loop.run_until_complete(RestApi(services).enable()) 47 | loop.run_until_complete(app_svc.register_contacts()) 48 | loop.run_until_complete(app_svc.load_plugins(args.plugins)) 49 | loop.run_until_complete(data_svc.load_data(loop.run_until_complete(data_svc.locate('plugins', dict(enabled=True))))) 50 | loop.run_until_complete(app_svc.load_plugin_expansions(loop.run_until_complete(data_svc.locate('plugins', dict(enabled=True))))) 51 | loop.create_task(app_svc.start_sniffer_untrusted_agents()) 52 | loop.create_task(app_svc.resume_operations()) 53 | loop.create_task(app_svc.run_scheduler()) 54 | loop.create_task(learning_svc.build_model()) 55 | loop.create_task(app_svc.watch_ability_files()) 56 | loop.run_until_complete(start_server()) 57 | try: 58 | logging.info('All systems ready.') 59 | loop.run_forever() 60 | except KeyboardInterrupt: 61 | loop.run_until_complete(services.get('app_svc').teardown(main_config_file=args.environment)) 62 | 63 | 64 | if __name__ == '__main__': 65 | def list_str(values): 66 | return values.split(',') 67 | sys.path.append('') 68 | parser = argparse.ArgumentParser('Welcome to the system') 69 | parser.add_argument('-E', '--environment', required=False, default='local', help='Select an env. file to use') 70 | parser.add_argument("-l", "--log", dest="logLevel", choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], 71 | help="Set the logging level", default='INFO') 72 | parser.add_argument('--fresh', action='store_true', required=False, default=False, 73 | help='remove object_store on start') 74 | parser.add_argument('-P', '--plugins', required=False, default=os.listdir('plugins'), 75 | help='Start up with a single plugin', type=list_str) 76 | parser.add_argument('--insecure', action='store_true', required=False, default=False, 77 | help='Start caldera with insecure default config values. Equivalent to "-E default".') 78 | 79 | args = parser.parse_args() 80 | setup_logger(getattr(logging, args.logLevel)) 81 | 82 | if args.insecure: 83 | logging.warning('--insecure flag set. Caldera will use the default.yml config file.') 84 | args.environment = 'default' 85 | elif args.environment == 'local': 86 | ensure_local_config() 87 | 88 | main_config_path = 'conf/%s.yml' % args.environment 89 | BaseWorld.apply_config('main', BaseWorld.strip_yml(main_config_path)[0]) 90 | logging.info('Using main config from %s' % main_config_path) 91 | BaseWorld.apply_config('agents', BaseWorld.strip_yml('conf/agents.yml')[0]) 92 | BaseWorld.apply_config('payloads', BaseWorld.strip_yml('conf/payloads.yml')[0]) 93 | 94 | data_svc = DataService() 95 | contact_svc = ContactService() 96 | planning_svc = PlanningService() 97 | rest_svc = RestService() 98 | auth_svc = AuthService() 99 | file_svc = FileSvc() 100 | learning_svc = LearningService() 101 | event_svc = EventService() 102 | app_svc = AppService(application=web.Application(client_max_size=5120**2)) 103 | 104 | if args.fresh: 105 | asyncio.get_event_loop().run_until_complete(data_svc.destroy()) 106 | run_tasks(services=app_svc.get_services()) 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://img.shields.io/badge/dynamic/json?color=blue&label=Release&query=tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fmitre%2Fcaldera%2Freleases%2Flatest)](https://github.com/mitre/caldera/releases/latest) 2 | [![Build Status](https://travis-ci.com/mitre/caldera.svg?branch=master)](https://travis-ci.com/mitre/caldera) 3 | [![codecov](https://codecov.io/gh/mitre/caldera/branch/master/graph/badge.svg)](https://codecov.io/gh/mitre/caldera) 4 | [![Documentation Status](https://readthedocs.org/projects/caldera/badge/?version=stable)](http://caldera.readthedocs.io/?badge=stable) 5 | 6 | # CALDERA™ 7 | 8 | *Full documentation, training and use-cases can be found [here](https://caldera.readthedocs.io/en/latest/).* 9 | 10 | CALDERA™ is a cyber security framework designed to easily run autonomous breach-and-simulation exercises. It can also be used to run manual red-team engagements or automated incident response. 11 | 12 | It is built on the [MITRE ATT&CK™ framework](https://attack.mitre.org/) and is an active research project at MITRE. 13 | 14 | The framework consists of two components: 15 | 16 | 1) **The core system**. This is the framework code, consisting of what is available in this repository. Included is 17 | an asynchronous command-and-control (C2) server with a REST API and a web interface. 18 | 2) **Plugins**. These are separate repositories that hang off of the core framework, providing additional functionality. 19 | Examples include agents, GUI interfaces, collections of TTPs and more. 20 | 21 | ## Plugins 22 | 23 | :star: Create your own plugin! Plugin generator: **[Skeleton](https://github.com/mitre/skeleton)** :star: 24 | 25 | ### Default 26 | - **[Access](https://github.com/mitre/access)** (red team initial access tools and techniques) 27 | - **[Atomic](https://github.com/mitre/atomic)** (Atomic Red Team project TTPs) 28 | - **[Builder](https://github.com/mitre/builder)** (dynamically compile payloads) 29 | - **[CalTack](https://github.com/mitre/caltack.git)** (embedded ATT&CK website) 30 | - **[Compass](https://github.com/mitre/compass)** (ATT&CK visualizations) 31 | - **[Debrief](https://github.com/mitre/debrief)** (operations insights) 32 | - **[Fieldmanual](https://github.com/mitre/fieldmanual)** (documentation) 33 | - **[GameBoard](https://github.com/mitre/gameboard)** (visualize joint red and blue operations) 34 | - **[Human](https://github.com/mitre/human)** (create simulated noise on an endpoint) 35 | - **[Manx](https://github.com/mitre/manx)** (shell functionality and reverse shell payloads) 36 | - **[Mock](https://github.com/mitre/mock)** (simulate agents in operations) 37 | - **[Response](https://github.com/mitre/response)** (incident response) 38 | - **[Sandcat](https://github.com/mitre/sandcat)** (default agent) 39 | - **[SSL](https://github.com/mitre/SSL)** (enable https for caldera) 40 | - **[Stockpile](https://github.com/mitre/stockpile)** (technique and profile storehouse) 41 | - **[Training](https://github.com/mitre/training)** (certification and training course) 42 | 43 | ### More 44 | These plugins are ready to use but are not included by default: 45 | - **[Pathfinder](https://github.com/center-for-threat-informed-defense/caldera_pathfinder)** (vulnerability scanning) 46 | 47 | ## Requirements 48 | 49 | These requirements are for the computer running the core framework: 50 | 51 | * Any Linux or MacOS 52 | * Python 3.6.1+ (with Pip3) 53 | * Google Chrome is our only supported browser 54 | * Recommended hardware to run on is 8GB+ RAM and 2+ CPUs 55 | 56 | ## Installation 57 | 58 | Start by cloning this repository recursively, passing the desired version/release in x.x.x format. This will pull in all available plugins. If you clone master - or any non-release branch - you may experience bugs. 59 | ```Bash 60 | git clone https://github.com/mitre/caldera.git --recursive --branch v2.9.0 61 | ``` 62 | 63 | Next, install the PIP requirements: 64 | ```Bash 65 | pip3 install -r requirements.txt 66 | ``` 67 | **Super-power your CALDERA server installation! [Install GoLang (1.13+)](https://golang.org/doc/install)** 68 | 69 | Finally, start the server. 70 | ```Bash 71 | python3 server.py --insecure 72 | ``` 73 | 74 | Collectively this would be: 75 | ```Bash 76 | git clone https://github.com/mitre/caldera.git --recursive --branch v2.9.0 77 | cd caldera 78 | pip3 install -r requirements.txt 79 | python3 server.py --insecure 80 | ``` 81 | 82 | Once started, you should log into http://localhost:8888 using the credentials red/admin. Then go into Plugins -> Training and complete the capture-the-flag style training course to learn how to use the framework. 83 | 84 | ## Video tutorial 85 | 86 | Watch the [following video](https://www.youtube.com/watch?v=_mVGjqu03fg) for a brief run through of how to run your first operation. 87 | 88 | ## Contributing 89 | 90 | Refer to our [contributor documentation](CONTRIBUTING.md). 91 | 92 | ## Licensing 93 | 94 | In addition to CALDERA™'s open source capabilities, MITRE maintains several in-house CALDERA™ plugins that offer 95 | more advanced functionality. For more information, or to discuss licensing opportunities, please reach out to 96 | caldera@mitre.org or directly to [MITRE's Technology Transfer Office](https://www.mitre.org/about/corporate-overview/contact-us#technologycontact). 97 | -------------------------------------------------------------------------------- /tests/web_server/test_core_endpoints.py: -------------------------------------------------------------------------------- 1 | import os 2 | from http import HTTPStatus 3 | from pathlib import Path 4 | 5 | import pytest 6 | import yaml 7 | from aiohttp import web 8 | 9 | from app.api.rest_api import RestApi 10 | from app.objects.c_agent import Agent 11 | from app.service.app_svc import AppService 12 | from app.service.auth_svc import AuthService 13 | from app.service.contact_svc import ContactService 14 | from app.service.data_svc import DataService 15 | from app.service.file_svc import FileSvc 16 | from app.service.learning_svc import LearningService 17 | from app.service.planning_svc import PlanningService 18 | from app.service.rest_svc import RestService 19 | from app.utility.base_service import BaseService 20 | from app.utility.base_world import BaseWorld 21 | 22 | 23 | @pytest.fixture 24 | def aiohttp_client(loop, aiohttp_client): 25 | 26 | async def initialize(): 27 | with open(Path(__file__).parents[2] / 'conf' / 'default.yml', 'r') as fle: 28 | BaseWorld.apply_config('main', yaml.safe_load(fle)) 29 | with open(Path(__file__).parents[2] / 'conf' / 'payloads.yml', 'r') as fle: 30 | BaseWorld.apply_config('payloads', yaml.safe_load(fle)) 31 | 32 | app_svc = AppService(web.Application()) 33 | _ = DataService() 34 | _ = RestService() 35 | _ = PlanningService() 36 | _ = LearningService() 37 | auth_svc = AuthService() 38 | _ = ContactService() 39 | _ = FileSvc() 40 | services = app_svc.get_services() 41 | os.chdir(str(Path(__file__).parents[2])) 42 | 43 | await app_svc.register_contacts() 44 | await app_svc.load_plugins(['sandcat', 'ssl']) 45 | _ = await RestApi(services).enable() 46 | await auth_svc.apply(app_svc.application, auth_svc.get_config('users')) 47 | return app_svc.application 48 | 49 | app = loop.run_until_complete(initialize()) 50 | return loop.run_until_complete(aiohttp_client(app)) 51 | 52 | 53 | @pytest.fixture 54 | def authorized_cookies(loop, aiohttp_client): 55 | async def get_cookie(): 56 | r = await aiohttp_client.post('/enter', allow_redirects=False, data=dict(username='admin', password='admin')) 57 | return r.cookies 58 | return loop.run_until_complete(get_cookie()) 59 | 60 | 61 | @pytest.fixture 62 | def sample_agent(loop, aiohttp_client): 63 | kwargs = dict(architecture='amd64', exe_name='sandcat.go', executors=['shellcode_amd64', 'sh'], 64 | group='red', host='testsystem.localdomain', location='./sandcat.go', pid=125266, 65 | platform='linux', ppid=124042, privilege='User', server='http://127.0.0.1:8888', 66 | username='testuser', paw=None, contact='http') 67 | 68 | agent = loop.run_until_complete( 69 | BaseService.get_service('data_svc').store(Agent(sleep_min=0, sleep_max=60, watchdog=0, **kwargs)) 70 | ) 71 | yield agent 72 | 73 | loop.run_until_complete( 74 | BaseService.get_service('data_svc').remove('agent', dict(paw=agent.paw)) 75 | ) 76 | 77 | 78 | async def test_home(aiohttp_client): 79 | resp = await aiohttp_client.get('/') 80 | assert resp.status == HTTPStatus.OK 81 | assert resp.content_type == 'text/html' 82 | 83 | 84 | async def test_access_denied(aiohttp_client): 85 | resp = await aiohttp_client.get('/enter') 86 | assert resp.status == HTTPStatus.UNAUTHORIZED 87 | 88 | 89 | async def test_login(aiohttp_client): 90 | resp = await aiohttp_client.post('/enter', allow_redirects=False, data=dict(username='admin', password='admin')) 91 | assert resp.status == HTTPStatus.FOUND 92 | assert resp.headers.get('Location') == '/' 93 | assert 'API_SESSION' in resp.cookies 94 | 95 | 96 | async def test_core(aiohttp_client, authorized_cookies): 97 | resp = await aiohttp_client.post('/api/rest', json=dict(index='agents'), cookies=authorized_cookies) 98 | assert resp.status == HTTPStatus.OK 99 | 100 | 101 | async def test_read_agent(aiohttp_client, authorized_cookies, sample_agent): 102 | resp = await aiohttp_client.post('/api/rest', json=dict(index='agents'), cookies=authorized_cookies) 103 | assert resp.status == HTTPStatus.OK 104 | agent_list = await resp.json() 105 | assert len(list(filter(lambda x: x['paw'] == sample_agent.paw, agent_list))) 106 | 107 | 108 | async def test_modify_agent(aiohttp_client, authorized_cookies, sample_agent): 109 | resp = await aiohttp_client.put('/api/rest', json=dict(index='agents', paw=sample_agent.paw, 110 | sleep_min=1, sleep_max=5), cookies=authorized_cookies) 111 | assert resp.status == HTTPStatus.OK 112 | agent_dict = await resp.json() 113 | assert agent_dict['sleep_max'] == 5 114 | assert sample_agent.sleep_min == 1 115 | assert sample_agent.sleep_max == 5 116 | 117 | 118 | async def test_invalid_request(aiohttp_client, authorized_cookies, sample_agent): 119 | resp = await aiohttp_client.put('/api/rest', json=dict(index='agents', paw=sample_agent.paw, 120 | sleep_min='notaninteger', sleep_max=5), 121 | cookies=authorized_cookies) 122 | assert resp.status == HTTPStatus.BAD_REQUEST 123 | messages = await resp.json() 124 | assert messages == dict(sleep_min=['Not a valid integer.']) 125 | -------------------------------------------------------------------------------- /app/utility/base_world.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import string 3 | import os 4 | import re 5 | import yaml 6 | import logging 7 | import subprocess 8 | import distutils.version 9 | from base64 import b64encode, b64decode 10 | from datetime import datetime 11 | from importlib import import_module 12 | from random import randint, choice 13 | from enum import Enum 14 | 15 | import dirhash 16 | import marshmallow as ma 17 | import marshmallow_enum as ma_enum 18 | 19 | 20 | class BaseWorld: 21 | """ 22 | A collection of base static functions for service & object module usage 23 | """ 24 | 25 | _app_configuration = dict() 26 | 27 | re_base64 = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', flags=re.DOTALL) 28 | 29 | @staticmethod 30 | def apply_config(name, config): 31 | BaseWorld._app_configuration[name] = config 32 | 33 | @staticmethod 34 | def get_config(prop=None, name=None): 35 | name = name if name else 'main' 36 | if prop: 37 | return BaseWorld._app_configuration[name].get(prop) 38 | return BaseWorld._app_configuration[name] 39 | 40 | @staticmethod 41 | def set_config(name, prop, value): 42 | if value is not None: 43 | logging.debug('Configuration (%s) update, setting %s=%s' % (name, prop, value)) 44 | BaseWorld._app_configuration[name][prop] = value 45 | 46 | @staticmethod 47 | def decode_bytes(s, strip_newlines=True): 48 | decoded = b64decode(s).decode('utf-8', errors='ignore') 49 | return decoded.replace('\r\n', '').replace('\n', '') if strip_newlines else decoded 50 | 51 | @staticmethod 52 | def encode_string(s): 53 | return str(b64encode(s.encode()), 'utf-8') 54 | 55 | @staticmethod 56 | def jitter(fraction): 57 | i = fraction.split('/') 58 | return randint(int(i[0]), int(i[1])) 59 | 60 | @staticmethod 61 | def create_logger(name): 62 | return logging.getLogger(name) 63 | 64 | @staticmethod 65 | def strip_yml(path): 66 | if path: 67 | with open(path, encoding='utf-8') as seed: 68 | return list(yaml.load_all(seed, Loader=yaml.FullLoader)) 69 | return [] 70 | 71 | @staticmethod 72 | def prepend_to_file(filename, line): 73 | with open(filename, 'r+') as f: 74 | content = f.read() 75 | f.seek(0, 0) 76 | f.write(line.rstrip('\r\n') + '\n' + content) 77 | 78 | @staticmethod 79 | def get_current_timestamp(date_format='%Y-%m-%d %H:%M:%S'): 80 | return datetime.now().strftime(date_format) 81 | 82 | @staticmethod 83 | async def load_module(module_type, module_info): 84 | module = import_module(module_info['module']) 85 | return getattr(module, module_type)(module_info) 86 | 87 | @staticmethod 88 | def generate_name(size=16): 89 | return ''.join(choice(string.ascii_lowercase) for _ in range(size)) 90 | 91 | @staticmethod 92 | def generate_number(size=6): 93 | return randint((10 ** (size - 1)), ((10 ** size) - 1)) 94 | 95 | @staticmethod 96 | def is_base64(s): 97 | try: 98 | b64decode(s, validate=True) 99 | return True 100 | except binascii.Error: 101 | return False 102 | 103 | @staticmethod 104 | def is_uuid4(s): 105 | if BaseWorld.re_base64.match(s): 106 | return True 107 | return False 108 | 109 | @staticmethod 110 | async def walk_file_path(path, target): 111 | for root, _, files in os.walk(path): 112 | if target in files: 113 | return os.path.join(root, target) 114 | if '%s.xored' % target in files: 115 | return os.path.join(root, '%s.xored' % target) 116 | return None 117 | 118 | @staticmethod 119 | def check_requirement(params): 120 | def check_module_version(module, version, attr=None, **kwargs): 121 | attr = attr if attr else '__version__' 122 | mod_version = getattr(import_module(module), attr, '') 123 | return compare_versions(mod_version, version) 124 | 125 | def check_program_version(command, version, **kwargs): 126 | output = subprocess.check_output(command.split(' '), stderr=subprocess.STDOUT, shell=False, timeout=10) 127 | return compare_versions(output.decode('utf-8'), version) 128 | 129 | def compare_versions(version_string, minimum_version): 130 | version = parse_version(version_string) 131 | return distutils.version.StrictVersion(version) >= distutils.version.StrictVersion(str(minimum_version)) 132 | 133 | def parse_version(version_string, pattern=r'([0-9]+(?:\.[0-9]+)+)'): 134 | groups = re.search(pattern, version_string) 135 | if groups: 136 | return groups[1] 137 | return '0.0.0' 138 | 139 | checkers = dict( 140 | python_module=check_module_version, 141 | installed_program=check_program_version 142 | ) 143 | 144 | try: 145 | requirement_type = params.get('type') 146 | return checkers[requirement_type](**params) 147 | except FileNotFoundError: 148 | return False 149 | except Exception as e: 150 | logging.getLogger('check_requirement').error(repr(e)) 151 | return False 152 | 153 | @staticmethod 154 | def get_version(path='.'): 155 | ignore = ['/plugins/', '/data', '.*/', '_*/'] 156 | included_extensions = ['*.py', '*.html', '*.js', '*.go'] 157 | version_file = os.path.join(path, 'VERSION.txt') 158 | if os.path.exists(version_file): 159 | with open(version_file, 'r') as f: 160 | version, md5 = f.read().strip().split('-') 161 | calculated_md5 = dirhash.dirhash(path, 'md5', ignore=ignore, match=included_extensions) 162 | if md5 == calculated_md5: 163 | return version 164 | return None 165 | 166 | class Access(Enum): 167 | APP = 0 168 | RED = 1 169 | BLUE = 2 170 | HIDDEN = 3 171 | 172 | class Privileges(Enum): 173 | User = 0 174 | Elevated = 1 175 | 176 | 177 | class AccessSchema(ma.Schema): 178 | access = ma_enum.EnumField(BaseWorld.Access) 179 | 180 | 181 | class PrivilegesSchema(ma.Schema): 182 | privilege = ma_enum.EnumField(BaseWorld.Privileges) 183 | -------------------------------------------------------------------------------- /static/js/shared.js: -------------------------------------------------------------------------------- 1 | /* HELPFUL functions to call */ 2 | 3 | function restRequest(type, data, callback, endpoint='/api/rest') { 4 | $.ajax({ 5 | url: endpoint, 6 | type: type, 7 | contentType: 'application/json', 8 | data: JSON.stringify(data), 9 | success: function(data, status, options) { 10 | callback(data); 11 | }, 12 | error: function (xhr, ajaxOptions, thrownError) { 13 | stream(thrownError); 14 | } 15 | }); 16 | } 17 | 18 | function validateFormState(conditions, selector){ 19 | (conditions) ? 20 | updateButtonState(selector, 'valid') : 21 | updateButtonState(selector, 'invalid'); 22 | } 23 | 24 | function downloadReport(endpoint, filename, data={}) { 25 | function downloadObjectAsJson(data){ 26 | stream('Downloading report: '+filename); 27 | let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2)); 28 | let downloadAnchorNode = document.createElement('a'); 29 | downloadAnchorNode.setAttribute("href", dataStr); 30 | downloadAnchorNode.setAttribute("download", filename + ".json"); 31 | document.body.appendChild(downloadAnchorNode); 32 | downloadAnchorNode.click(); 33 | downloadAnchorNode.remove(); 34 | } 35 | restRequest('POST', data, downloadObjectAsJson, endpoint); 36 | } 37 | 38 | function updateButtonState(selector, state) { 39 | (state === 'valid') ? 40 | $(selector).attr('class','button-success atomic-button') : 41 | $(selector).attr('class','button-notready atomic-button'); 42 | } 43 | 44 | function showHide(show, hide) { 45 | $(show).each(function(){$(this).prop('disabled', false).css('opacity', 1.0)}); 46 | $(hide).each(function(){$(this).prop('disabled', true).css('opacity', 0.5)}); 47 | } 48 | 49 | function uuidv4() { 50 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 51 | let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 52 | return v.toString(16); 53 | }); 54 | } 55 | 56 | function stream(msg, speak=false){ 57 | let streamer = $('#streamer'); 58 | if(streamer.text() != msg){ 59 | streamer.fadeOut(function() { 60 | if(speak) { window.speechSynthesis.speak(new SpeechSynthesisUtterance(msg)); } 61 | $(this).text(msg).fadeIn(1000); 62 | }); 63 | } 64 | } 65 | 66 | function doNothing() {} 67 | 68 | /* SECTIONS */ 69 | 70 | function viewSection(name, address){ 71 | function display(data) { 72 | let plugin = $($.parseHTML(data, keepScripts=true)); 73 | $('#section-container').append('
'); 74 | let newSection = $('#section-'+name); 75 | newSection.html(plugin); 76 | $('html, body').animate({scrollTop: newSection.offset().top}, 1000); 77 | } 78 | closeNav(); 79 | restRequest('GET', null, display, address); 80 | } 81 | 82 | function removeSection(identifier){ 83 | $('#'+identifier).remove(); 84 | } 85 | 86 | function toggleSidebar(identifier) { 87 | let sidebar = $('#'+identifier); 88 | if (sidebar.is(":visible")) { 89 | sidebar.hide(); 90 | } else { 91 | sidebar.show(); 92 | } 93 | } 94 | /* AUTOMATIC functions for all pages */ 95 | 96 | $(document).ready(function () { 97 | $(document).find("select").each(function () { 98 | if(!$(this).hasClass('avoid-alphabetizing')) { 99 | alphabetize_dropdown($(this)); 100 | let observer = new MutationObserver(function (mutations, obs) { 101 | obs.disconnect(); 102 | alphabetize_dropdown($(mutations[0].target)); 103 | obs.observe(mutations[0].target, {childList: true}); 104 | }); 105 | observer.observe(this, {childList: true}); 106 | } 107 | }); 108 | $(document).keyup(function(e){ 109 | if(e.key == "Escape"){ 110 | $('.modal').hide(); 111 | $('#mySidenav').width('0'); 112 | } 113 | }); 114 | $('body').click(function(event) { 115 | if(!$(event.target).closest('.modal-content').length && $(event.target).is('.modal')) { 116 | $('.modal').hide(); 117 | } 118 | if(!$(event.target).closest('#mySidenav').length && !$(event.target).is('.navbar span')) { 119 | $('#mySidenav').width('0'); 120 | } 121 | }); 122 | }); 123 | 124 | function alphabetize_dropdown(obj) { 125 | let selected_val = $(obj).children("option:selected").val(); 126 | let disabled = $(obj).find('option:disabled'); 127 | let opts_list = $(obj).find('option:enabled').clone(true); 128 | opts_list.sort(function (a, b) { 129 | return a.text.toLowerCase() == b.text.toLowerCase() ? 0 : a.text.toLowerCase() < b.text.toLowerCase() ? -1 : 1; 130 | }); 131 | $(obj).empty().append(opts_list).prepend(disabled); 132 | obj.val(selected_val); 133 | } 134 | 135 | (function($){ 136 | $.event.special.destroyed = { 137 | remove: function(o) { 138 | if (o.handler) { 139 | o.handler() 140 | } 141 | } 142 | } 143 | })(jQuery); 144 | 145 | $(document).ready(function () { 146 | stream('Welcome home. Go into the Agents tab to review your deployed agents.'); 147 | }); 148 | 149 | window.onerror = function(error, url, line) { 150 | let msg = 'Check your JavaScript console. '+error; 151 | if(msg.includes('TypeError')) { 152 | stream('Refresh your GUI'); 153 | } else { 154 | stream(msg); 155 | } 156 | }; 157 | 158 | function warn(msg){ 159 | document.getElementById("alert-modal").style.display="block"; 160 | $("#alert-text").html(msg); 161 | } 162 | 163 | function display_errors(errors){ 164 | function add_element(txt, level){ 165 | let newitem = $("#infolist-template").clone(); 166 | newitem.show(); 167 | newitem.find(".infolist-contents p").html(txt) 168 | if(!level){ 169 | newitem.find(".infolist-icon img").attr('src', '/gui/img/success.png') 170 | } 171 | $("#info-list").append(newitem); 172 | } 173 | document.getElementById("list-modal").style.display="block"; 174 | $("#info-list").empty(); 175 | if(errors.length === 0) { 176 | add_element("no errors to view", 0); 177 | } 178 | for(let id in errors){ 179 | add_element(errors[id].name + ": " + errors[id].msg, 1); 180 | } 181 | } 182 | 183 | function openNav() { 184 | $('#mySidenav').width('250px'); 185 | } 186 | function closeNav() { 187 | $('#mySidenav').width('0'); 188 | } 189 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import random 3 | import string 4 | import uuid 5 | import yaml 6 | from unittest import mock 7 | 8 | from app.objects.c_obfuscator import Obfuscator 9 | from app.utility.base_world import BaseWorld 10 | from app.service.app_svc import AppService 11 | from app.service.data_svc import DataService 12 | from app.service.contact_svc import ContactService 13 | from app.service.file_svc import FileSvc 14 | from app.service.learning_svc import LearningService 15 | from app.service.planning_svc import PlanningService 16 | from app.service.rest_svc import RestService 17 | from app.objects.c_adversary import Adversary 18 | from app.objects.c_ability import Ability 19 | from app.objects.c_operation import Operation 20 | from app.objects.c_plugin import Plugin 21 | from app.objects.c_agent import Agent 22 | from app.objects.secondclass.c_link import Link 23 | from app.objects.secondclass.c_fact import Fact 24 | 25 | 26 | @pytest.fixture(scope='session') 27 | def init_base_world(): 28 | with open('conf/default.yml') as c: 29 | BaseWorld.apply_config('main', yaml.load(c, Loader=yaml.FullLoader)) 30 | BaseWorld.apply_config('agents', BaseWorld.strip_yml('conf/agents.yml')[0]) 31 | BaseWorld.apply_config('payloads', BaseWorld.strip_yml('conf/payloads.yml')[0]) 32 | 33 | 34 | @pytest.fixture(scope='class') 35 | def app_svc(): 36 | async def _init_app_svc(): 37 | return AppService(None) 38 | 39 | def _app_svc(loop): 40 | return loop.run_until_complete(_init_app_svc()) 41 | return _app_svc 42 | 43 | 44 | @pytest.fixture(scope='class') 45 | def data_svc(): 46 | return DataService() 47 | 48 | 49 | @pytest.fixture(scope='class') 50 | def file_svc(): 51 | return FileSvc() 52 | 53 | 54 | @pytest.fixture(scope='class') 55 | def contact_svc(): 56 | return ContactService() 57 | 58 | 59 | @pytest.fixture(scope='class') 60 | def rest_svc(): 61 | """ 62 | The REST service requires the test's loop in order to be initialized in the same Thread 63 | as the test. This mitigates the issue where the service's calls to `asyncio.get_event_loop` 64 | would result in a RuntimeError indicating that there is no currentevent loop in the main 65 | thread. 66 | """ 67 | async def _init_rest_svc(): 68 | return RestService() 69 | 70 | def _rest_svc(loop): 71 | return loop.run_until_complete(_init_rest_svc()) 72 | return _rest_svc 73 | 74 | 75 | @pytest.fixture(scope='class') 76 | def planning_svc(): 77 | return PlanningService() 78 | 79 | 80 | @pytest.fixture(scope='class') 81 | def learning_svc(): 82 | return LearningService() 83 | 84 | 85 | @pytest.fixture(scope='class') 86 | def services(app_svc): 87 | return app_svc.get_services() 88 | 89 | 90 | @pytest.fixture(scope='class') 91 | def mocker(): 92 | return mock 93 | 94 | 95 | @pytest.fixture 96 | def adversary(): 97 | def _generate_adversary(adversary_id=None, name=None, description=None, phases=None): 98 | if not adversary_id: 99 | adversary_id = uuid.uuid4() 100 | if not name: 101 | name = ''.join(random.choice(string.ascii_uppercase) for _ in range(10)) 102 | if not description: 103 | description = "description" 104 | if not phases: 105 | phases = dict() 106 | return Adversary(adversary_id=adversary_id, name=name, description=description, atomic_ordering=phases) 107 | 108 | return _generate_adversary 109 | 110 | 111 | @pytest.fixture 112 | def ability(): 113 | def _generate_ability(ability_id=None, variations=None, *args, **kwargs): 114 | if not ability_id: 115 | ability_id = random.randint(0, 999999) 116 | if not variations: 117 | variations = [] 118 | return Ability(ability_id=ability_id, variations=variations, *args, **kwargs) 119 | 120 | return _generate_ability 121 | 122 | 123 | @pytest.fixture 124 | def operation(): 125 | def _generate_operation(name, agents, adversary, *args, **kwargs): 126 | return Operation(name=name, agents=agent, adversary=adversary, *args, **kwargs) 127 | 128 | return _generate_operation 129 | 130 | 131 | @pytest.fixture 132 | def demo_operation(loop, data_svc, operation, adversary): 133 | tadversary = loop.run_until_complete(data_svc.store(adversary())) 134 | return operation(name='my first op', agents=[], adversary=tadversary) 135 | 136 | 137 | @pytest.fixture 138 | def obfuscator(loop, data_svc): 139 | loop.run_until_complete(data_svc.store( 140 | Obfuscator(name='plain-text', 141 | description='Does no obfuscation to any command, instead running it in plain text', 142 | module='plugins.stockpile.app.obfuscators.plain_text') 143 | ) 144 | ) 145 | 146 | 147 | @pytest.fixture 148 | def agent(): 149 | def _generate_agent(sleep_min, sleep_max, watchdog, *args, **kwargs): 150 | return Agent(sleep_min=sleep_min, sleep_max=sleep_max, watchdog=watchdog, *args, **kwargs) 151 | 152 | return _generate_agent 153 | 154 | 155 | @pytest.fixture 156 | def link(): 157 | def _generate_link(command, paw, ability, *args, **kwargs): 158 | return Link.load(dict(ability=ability, command=command, paw=paw, *args, **kwargs)) 159 | 160 | return _generate_link 161 | 162 | 163 | @pytest.fixture 164 | def fact(): 165 | def _generate_fact(trait, *args, **kwargs): 166 | return Fact(trait=trait, *args, **kwargs) 167 | 168 | return _generate_fact 169 | 170 | 171 | @pytest.fixture 172 | def demo_plugin(): 173 | def _generate_plugin(enabled=False, gui=False, data_dir=None, access=None): 174 | name = ''.join(random.choice(string.ascii_lowercase) for _ in range(10)) 175 | desc = 'this is a good description' 176 | address = '/plugin/%s/gui' % name if gui else None 177 | return Plugin(name=name, description=desc, address=address, enabled=enabled, data_dir=data_dir, access=access) 178 | 179 | return _generate_plugin 180 | 181 | 182 | @pytest.fixture 183 | def agent_profile(): 184 | def _agent_profile(paw=None, group='red', platform='linux', executors=None, privilege='Elevated'): 185 | if not executors: 186 | executors = ['sh'] 187 | return dict( 188 | server='http://127.0.0.1:8888', 189 | username='username', 190 | group=group, 191 | host='hostname', 192 | platform=platform, 193 | architecture='x86_64', 194 | location='/path/to/agent', 195 | pid=random.randint(2, 32768), 196 | ppid=random.randint(2, 32768), 197 | executors=executors, 198 | privilege=privilege, 199 | exe_name='agent-exe-name', 200 | paw=paw 201 | ) 202 | 203 | return _agent_profile 204 | --------------------------------------------------------------------------------