Skip to content

Commit 9ceb72d

Browse files
authored
Merge pull request #2874 from mitre/magma
Magma
2 parents 84e2a91 + f7c1a3c commit 9ceb72d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+470
-122
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@
4646
[submodule "plugins/emu"]
4747
path = plugins/emu
4848
url = https://github.com/mitre/emu.git
49+
[submodule "plugins/magma"]
50+
path = plugins/magma
51+
url = https://github.com/mitre/magma.git

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ python -m pytest
4040
```
4141
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.
4242

43-
We utilize `tox` to test CALDERA in multiple versions of Python. This will only run if the interpreter is present on your system. To run tox, execute:
43+
We utilize `tox` to test Caldera in multiple versions of Python. This will only run if the interpreter is present on your system. To run tox, execute:
4444
```
4545
tox
4646
```

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The framework consists of two components:
1616
an asynchronous command-and-control (C2) server with a REST API and a web interface.
1717
2) **Plugins**. These repositories expand the core framework capabilities and providing additional functionality. Examples include agents, reporting, collections of TTPs and more.
1818

19-
## Resources and Socials
19+
## Resources & Socials
2020
* 📜 [Documentation, training, and use-cases](https://caldera.readthedocs.io/en/latest/)
2121
* ✍️ [Caldera's blog](https://medium.com/@mitrecaldera/welcome-to-the-official-mitre-caldera-blog-page-f34c2cdfef09)
2222
* 🌐 [Homepage](https://caldera.mitre.org)
@@ -37,6 +37,7 @@ These plugins are supported and maintained by the Caldera team.
3737
- **[Fieldmanual](https://github.com/mitre/fieldmanual)** (documentation)
3838
- **[GameBoard](https://github.com/mitre/gameboard)** (visualize joint red and blue operations)
3939
- **[Human](https://github.com/mitre/human)** (create simulated noise on an endpoint)
40+
- **[Magma](https://github.com/mitre/magma)** (VueJS UI for Caldera v5)
4041
- **[Manx](https://github.com/mitre/manx)** (shell functionality and reverse shell payloads)
4142
- **[Response](https://github.com/mitre/response)** (incident response)
4243
- **[Sandcat](https://github.com/mitre/sandcat)** (default agent)
@@ -59,6 +60,7 @@ These requirements are for the computer running the core framework:
5960
* Python 3.8+ (with Pip3)
6061
* Recommended hardware to run on is 8GB+ RAM and 2+ CPUs
6162
* Recommended: GoLang 1.17+ to dynamically compile GoLang-based agents.
63+
* NodeJS (v16+ recommended for v5 VueJS UI)
6264

6365
## Installation
6466

@@ -67,7 +69,7 @@ Concise installation steps:
6769
git clone https://github.com/mitre/caldera.git --recursive
6870
cd caldera
6971
pip3 install -r requirements.txt
70-
python3 server.py --insecure
72+
python3 server.py --insecure --build
7173
```
7274

7375
Full steps:
@@ -84,11 +86,28 @@ pip3 install -r requirements.txt
8486

8587
Finally, start the server.
8688
```Bash
87-
python3 server.py --insecure
89+
python3 server.py --insecure --build
8890
```
89-
91+
The --build flag automatically installs any VueJS UI dependencies, bundles the UI into a dist directory, and is served by the Caldera server. You will only have to use the --build flag again if you add any plugins or make any changes to the UI.
9092
Once started, log into http://localhost:8888 using the default credentials red/admin. Then go into Plugins -> Training and complete the capture-the-flag style training course to learn how to use Caldera.
9193

94+
If you prefer to not use the new VueJS UI, revert to Caldera v4.2.0. Correspondingly, do not use the `--build` flag for earlier versions as not required.
95+
96+
### User Interface Development
97+
98+
If you'll be developing the UI, there are a few more additional installation steps.
99+
100+
**Requirements**
101+
* NodeJS (v16+ recommended)
102+
103+
**Setup**
104+
105+
1. Add the Magma submodule if you haven't already: `git submodule add https://gitlab.mitre.org/caldera/other/magma`
106+
1. Install NodeJS dependencies: `cd plugins/magma && npm install && cd ..`
107+
1. Start the Caldera server with an additional flag: `python3 server.py --uidev localhost`
108+
109+
Your Caldera server is available at http://localhost:8888 as usual, but there will now be a hot-reloading development server for the VueJS front-end available at http://localhost:3000. Both logs from the server and the front-end will display in the terminal you launched the server from.
110+
92111
## Docker Deployment
93112
To build a Caldera docker image, ensure you have docker installed and perform the following actions:
94113
```Bash

SECURITY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ Under this policy, "research" means activities in which you:
2828

2929
## Reporting a vulnerability
3030

31-
Information submitted under this policy will be used for defensive purposes only, i.e. to mitigate or remediate vulnerabilities. Since CALDERA is run by a not-for-profit and is open source by nature, by
31+
Information submitted under this policy will be used for defensive purposes only, i.e. to mitigate or remediate vulnerabilities. Since Caldera is run by a not-for-profit and is open source by nature, by
3232
submitting a vulnerability, you acknowledge that you have no expectation of payment. However, we will ensure that credit is given to the bug finder.
3333

3434
## What we would like to see from you
3535

3636
To help us triage and prioritize submissions, please include the following in your report:
3737

38-
- Affected version of CALDERA (committed hash or version number), operating system used, and python version.
38+
- Affected version of Caldera (committed hash or version number), operating system used, and python version.
3939

4040
- Describe the location the vulnerability was discovered and the potential impact of exploitation.
4141

@@ -49,7 +49,7 @@ When you choose to share your contact information with us, we commit to coordina
4949

5050
- Within ***10 business days***, we will acknowledge that your report has been received.
5151

52-
- After notifying the CALDERA team, we will open reported issues to the public within ***90 days***, or after a fix is released (whichever comes first).
52+
- After notifying the Caldera team, we will open reported issues to the public within ***90 days***, or after a fix is released (whichever comes first).
5353

5454
- To the best of our ability, we will confirm the existence of the vulnerability to you and be as transparent as possible about what steps we are taking during the remediation process, including on issues or challenges that may delay resolution.
5555

app/api/rest_api.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import marshmallow as ma
99
from aiohttp import web
10-
from aiohttp_jinja2 import template, render_template
10+
from aiohttp_jinja2 import render_template
1111

1212
from app.api.packs.advanced import AdvancedPack
1313
from app.api.packs.campaign import CampaignPack
@@ -29,44 +29,33 @@ def __init__(self, services):
2929
asyncio.get_event_loop().create_task(AdvancedPack(services).enable())
3030

3131
async def enable(self):
32+
self.app_svc.application.router.add_static('/assets', 'plugins/magma/dist/assets/', append_version=True)
33+
# TODO: only serve static files in legacy plugin mode
3234
self.app_svc.application.router.add_static('/gui', 'static/', append_version=True)
3335
# unauthorized GUI endpoints
34-
self.app_svc.application.router.add_route('*', '/', self.landing)
35-
self.app_svc.application.router.add_route('*', '/enter', self.validate_login)
36-
self.app_svc.application.router.add_route('*', '/logout', self.logout)
37-
self.app_svc.application.router.add_route('GET', '/login', self.login)
36+
self.app_svc.application.router.add_route('GET', '/', self.landing)
37+
self.app_svc.application.router.add_route('POST', '/enter', self.validate_login)
38+
self.app_svc.application.router.add_route('POST', '/logout', self.logout)
3839
# unauthorized API endpoints
3940
self.app_svc.application.router.add_route('*', '/file/download', self.download_file)
4041
self.app_svc.application.router.add_route('POST', '/file/upload', self.upload_file)
4142
# authorized API endpoints
4243
self.app_svc.application.router.add_route('*', '/api/rest', self.rest_core)
4344
self.app_svc.application.router.add_route('GET', '/api/{index}', self.rest_core_info)
4445
self.app_svc.application.router.add_route('GET', '/file/download_exfil', self.download_exfil_file)
45-
46-
@template('login.html', status=401)
47-
async def login(self, request):
48-
return dict()
46+
self.app_svc.application.router.add_route('GET', '/{tail:(?!plugin/).*}', self.handle_catch)
4947

5048
async def validate_login(self, request):
5149
return await self.auth_svc.login_user(request)
5250

53-
@template('login.html')
5451
async def logout(self, request):
5552
await self.auth_svc.logout_user(request)
5653

5754
async def landing(self, request):
58-
access = await self.auth_svc.get_permissions(request)
59-
if not access:
60-
# If user doesn't have access, server will attempt to redirect to login.
61-
return await self.auth_svc.login_redirect(request)
62-
plugins = await self.data_svc.locate('plugins', {'access': tuple(access), **dict(enabled=True)})
63-
data = dict(plugins=[p.display for p in plugins], errors=self.app_svc.errors + self._request_errors(request))
64-
template_name = access[0].name
65-
if template_name == "RED":
66-
template_name = "core_red"
67-
elif template_name == "BLUE":
68-
template_name = "core_blue"
69-
return render_template(f"{template_name}.html", request, data)
55+
return render_template("index.html", request, {})
56+
57+
async def handle_catch(self, request):
58+
return render_template("index.html", request, {})
7059

7160
@check_authorization
7261
async def rest_core(self, request):

app/api/v2/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
def make_app(services):
55
from .responses import json_request_validation_middleware
6-
from .security import authentication_required_middleware_factory
6+
from .security import authentication_required_middleware_factory, pass_option_middleware
77

88
app = web.Application(
99
middlewares=[
10+
pass_option_middleware,
1011
authentication_required_middleware_factory(services['auth_svc']),
1112
json_request_validation_middleware
1213
]
@@ -54,4 +55,7 @@ def make_app(services):
5455
from .handlers.contact_api import ContactApi
5556
ContactApi(services).add_routes(app)
5657

58+
from .handlers.payload_api import PayloadApi
59+
PayloadApi(services).add_routes(app)
60+
5761
return app

app/api/v2/handlers/contact_api.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def add_routes(self, app: web.Application):
1515
router = app.router
1616
router.add_get('/contacts/{name}', self.get_contact_report)
1717
router.add_get('/contacts', self.get_available_contact_reports)
18+
router.add_get('/contactlist', self.get_contact_list)
1819

1920
@aiohttp_apispec.docs(tags=['contacts'],
2021
summary='Retrieve a List of Beacons made by Agents to the specified Contact',
@@ -43,3 +44,7 @@ async def get_contact_report(self, request: web.Request):
4344
async def get_available_contact_reports(self, request: web.Request):
4445
contacts = self._api_manager.get_available_contact_reports()
4546
return web.json_response(contacts)
47+
48+
async def get_contact_list(self, request: web.Request):
49+
contacts = [dict(name=c.name, description=c.description) for c in self._api_manager.contact_svc.contacts]
50+
return web.json_response(contacts)

app/api/v2/handlers/health_api.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ def add_routes(self, app: web.Application):
1919
router.add_get('/health', security.authentication_exempt(self.get_health_info))
2020

2121
@aiohttp_apispec.docs(tags=['health'],
22-
summary='Health endpoints returns the status of CALDERA',
23-
description='Returns the status of CALDERA and additional details including versions of system components')
22+
summary='Health endpoints returns the status of Caldera',
23+
description='Returns the status of Caldera and additional details including versions of system components')
2424
@aiohttp_apispec.response_schema(CalderaInfoSchema, 200, description='Includes all loaded plugins and system components.')
2525
async def get_health_info(self, request):
2626
loaded_plugins_sorted = sorted(self._app_svc.get_loaded_plugins(), key=operator.attrgetter('name'))
27+
access = await self._auth_svc.get_permissions(request)
2728

2829
mapping = {
29-
'application': 'CALDERA',
30+
'application': 'Caldera',
3031
'version': app.get_version(),
32+
'access': access[0].name,
3133
'plugins': loaded_plugins_sorted
3234
}
3335

app/api/v2/handlers/operation_api.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from app.api.v2.responses import JsonHttpNotFound
99
from app.api.v2.schemas.base_schemas import BaseGetAllQuerySchema, BaseGetOneQuerySchema
1010
from app.api.v2.schemas.link_result_schema import LinkResultSchema
11-
from app.objects.c_operation import Operation, OperationSchema, OperationOutputRequestSchema
11+
from app.objects.c_operation import Operation, OperationSchema, OperationSchemaAlt, OperationOutputRequestSchema
1212
from app.objects.secondclass.c_link import LinkSchema
1313

1414

@@ -21,6 +21,7 @@ def __init__(self, services):
2121
def add_routes(self, app: web.Application):
2222
router = app.router
2323
router.add_get('/operations', self.get_operations)
24+
router.add_get('/operations/summary', self.get_operations_summary)
2425
router.add_get('/operations/{id}', self.get_operation_by_id)
2526
router.add_post('/operations', self.create_operation)
2627
router.add_patch('/operations/{id}', self.update_operation)
@@ -37,7 +38,7 @@ def add_routes(self, app: web.Application):
3738

3839
@aiohttp_apispec.docs(tags=['operations'],
3940
summary='Retrieve operations',
40-
description='Retrieve all CALDERA operations from memory. Use fields from the '
41+
description='Retrieve all Caldera operations from memory. Use fields from the '
4142
'`BaseGetAllQuerySchema` in the request body to filter.')
4243
@aiohttp_apispec.querystring_schema(BaseGetAllQuerySchema)
4344
@aiohttp_apispec.response_schema(OperationSchema(many=True, partial=True),
@@ -48,7 +49,7 @@ async def get_operations(self, request: web.Request):
4849

4950
@aiohttp_apispec.docs(tags=['operations'],
5051
summary='Retrieve an operation by operation id',
51-
description='Retrieve one CALDERA operation from memory based on the operation id (String '
52+
description='Retrieve one Caldera operation from memory based on the operation id (String '
5253
'UUID). Use fields from the `BaseGetOneQuerySchema` in the request body to add '
5354
'`include` and `exclude` filters.',
5455
parameters=[{
@@ -66,8 +67,28 @@ async def get_operation_by_id(self, request: web.Request):
6667
return web.json_response(operation)
6768

6869
@aiohttp_apispec.docs(tags=['operations'],
69-
summary='Create a new CALDERA operation record',
70-
description='Create a new CALDERA operation using the format provided in the '
70+
summary='Retrieve operations (alternate)',
71+
description='Retrieve all Caldera operations from memory, with an alternate selection'
72+
' of properties. Use fields from the `BaseGetAllQuerySchema` in the request'
73+
' body to filter.')
74+
@aiohttp_apispec.querystring_schema(BaseGetAllQuerySchema)
75+
@aiohttp_apispec.response_schema(OperationSchemaAlt(many=True, partial=True),
76+
description='The response is a list of all operations.')
77+
async def get_operations_summary(self, request: web.Request):
78+
remove_props = ['chain', 'host_group', 'source', 'visibility']
79+
operations = await self.get_all_objects(request)
80+
operations_mod = []
81+
for op in operations:
82+
op['agents'] = self._api_manager.get_agents(op)
83+
op['hosts'] = await self._api_manager.get_hosts(op)
84+
for prop in remove_props:
85+
op.pop(prop, None)
86+
operations_mod.append(op)
87+
return web.json_response(operations_mod)
88+
89+
@aiohttp_apispec.docs(tags=['operations'],
90+
summary='Create a new Caldera operation record',
91+
description='Create a new Caldera operation using the format provided in the '
7192
'`OperationSchema`. Required schema fields are as follows: "name", '
7293
'"adversary.adversary_id", "planner.id", and "source.id"')
7394
@aiohttp_apispec.request_schema(OperationSchema)
@@ -79,7 +100,7 @@ async def create_operation(self, request: web.Request):
79100

80101
@aiohttp_apispec.docs(tags=['operations'],
81102
summary='Update fields within an operation',
82-
description='Update one CALDERA operation in memory based on the operation id (String '
103+
description='Update one Caldera operation in memory based on the operation id (String '
83104
'UUID). The `state`, `autonomous` and `obfuscator` fields in the operation '
84105
'object may be edited in the request body using the `OperationSchema`.',
85106
parameters=[{
@@ -98,7 +119,7 @@ async def update_operation(self, request: web.Request):
98119

99120
@aiohttp_apispec.docs(tags=['operations'],
100121
summary='Delete an operation by operation id',
101-
description='Delete one CALDERA operation from memory based on the operation id (String '
122+
description='Delete one Caldera operation from memory based on the operation id (String '
102123
'UUID).',
103124
parameters=[{
104125
'in': 'path',

app/api/v2/handlers/payload_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import itertools
2+
import pathlib
3+
4+
import aiohttp_apispec
5+
from aiohttp import web
6+
import marshmallow as ma
7+
8+
from app.api.v2.handlers.base_api import BaseApi
9+
from app.api.v2.schemas.base_schemas import BaseGetAllQuerySchema
10+
11+
12+
class PayloadSchema(ma.Schema):
13+
payloads = ma.fields.List(ma.fields.String())
14+
15+
16+
class PayloadApi(BaseApi):
17+
def __init__(self, services):
18+
super().__init__(auth_svc=services['auth_svc'])
19+
self.data_svc = services['data_svc']
20+
self.file_svc = services['file_svc']
21+
22+
def add_routes(self, app: web.Application):
23+
router = app.router
24+
router.add_get('/payloads', self.get_payloads)
25+
26+
@aiohttp_apispec.docs(tags=['payloads'],
27+
summary='Retrieve payloads',
28+
description='Retrieves all stored payloads.')
29+
@aiohttp_apispec.querystring_schema(BaseGetAllQuerySchema)
30+
@aiohttp_apispec.response_schema(PayloadSchema(),
31+
description='Returns a list of all payloads in PayloadSchema format.')
32+
async def get_payloads(self, request: web.Request):
33+
cwd = pathlib.Path.cwd()
34+
payload_dirs = [cwd / 'data' / 'payloads']
35+
payload_dirs.extend(cwd / 'plugins' / plugin.name / 'payloads'
36+
for plugin in await self.data_svc.locate('plugins') if plugin.enabled)
37+
payloads = {
38+
self.file_svc.remove_xored_extension(p.name)
39+
for p in itertools.chain.from_iterable(p_dir.glob('[!.]*') for p_dir in payload_dirs)
40+
if p.is_file()
41+
}
42+
43+
return web.json_response(list(payloads))

0 commit comments

Comments
 (0)