├── 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 | {{ o.name }}
18 | {{ o.description }}
19 |
20 | {% endfor %}
21 |
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 |
25 |
26 |
--------------------------------------------------------------------------------
/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 | Login | Access
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
23 |
24 |
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 |
37 |
38 |
Plugins
39 |
40 |
41 | {% for plugin in plugins %}
42 |
43 |
44 | {{ plugin.name }}
45 | {{ plugin.version }}
46 | {{ plugin.description }}
47 | {% if plugin.enabled %}
48 |
49 | {% else %}
50 | enable
51 | {% endif %}
52 |
53 | {% endfor %}
54 |
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("Choose a technique ");
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('' + techniqueAbilities.length + ' abilities ');
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("All Techniques ");;
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(""+added.length+" abilities ");
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 |
15 |
16 | ☰ navigate
17 |
18 |
19 |
53 |
54 |
55 |
56 |
57 |
Chrome is the only supported browser. Please change to that or some website components may not work.
58 |
59 |
71 |
72 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
91 |
92 |
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 |
15 |
16 | ☰ navigate
17 |
18 |
19 |
58 |
59 |
60 |
61 |
Chrome is the only supported browser. Please change to that or some website components may not work.
62 |
63 |
75 |
76 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
95 |
96 |
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 | [](https://github.com/mitre/caldera/releases/latest)
2 | [](https://travis-ci.com/mitre/caldera)
3 | [](https://codecov.io/gh/mitre/caldera)
4 | [](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 |
--------------------------------------------------------------------------------