├── .gitignore
├── LICENSE
├── README.md
├── __init__.py
├── admin_routes.py
├── assets
├── create.html
├── create.js
├── update.html
├── update.js
├── view.html
└── view.js
├── config.json
├── container_manager.py
├── helpers.py
├── image-readme
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── demo.gif
├── http.png
├── main.png
├── manage.png
└── tcp.png
├── models.py
├── requirements.txt
├── settings.json
├── templates
├── config
│ └── container_status.html
├── container_base.html
├── container_cheat.html
├── container_dashboard.html
└── container_settings.html
└── user_routes.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Phan Nhat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CTFd Docker Containers Plugin
2 |
3 |
4 |
CTFd Docker Containers Plugin
5 |
6 | A plugin to create containerized challenges for your CTF contest.
7 |
8 |
9 |
10 | ## Table of Contents
11 | 1. [Getting Started](#getting-started)
12 | - [Prerequisites](#prerequisites)
13 | - [Installation](#installation)
14 | 2. [Usage](#usage)
15 | - [Using Local Docker Daemon](#using-local-docker-daemon)
16 | - [Using Remote Docker via SSH](#using-remote-docker-via-ssh)
17 | 3. [Demo](#demo)
18 | 4. [Roadmap](#roadmap)
19 | 5. [License](#license)
20 | 6. [Contact](#contact)
21 |
22 | ---
23 |
24 | ## Getting Started
25 |
26 | This section provides instructions for setting up the project locally.
27 |
28 | ### Prerequisites
29 |
30 | To use this plugin, you should have:
31 | - Experience hosting CTFd with Docker
32 | - Basic knowledge of Docker
33 | - SSH access to remote servers (if using remote Docker)
34 |
35 | ### Installation
36 |
37 | 1. **Clone this repository:**
38 | ```bash
39 | git clone https://github.com/phannhat17/CTFd-Docker-Plugin.git
40 | ```
41 | 2. **Rename the folder:**
42 | ```bash
43 | mv CTFd-Docker-Plugin containers
44 | ```
45 | 3. **Move the folder to the CTFd plugins directory:**
46 | ```bash
47 | mv containers /path/to/CTFd/plugins/
48 | ```
49 |
50 | [Back to top](#ctfd-docker-containers-plugin)
51 |
52 | ---
53 |
54 | ## Usage
55 |
56 | ### Using Local Docker Daemon
57 |
58 | #### Case A: **CTFd Running Directly on Host:**
59 | - Go to the plugin settings page: `/containers/settings`
60 | - Fill in all fields except the `Base URL`.
61 |
62 | 
63 |
64 | #### Case B: **CTFd Running via Docker:**
65 | - Map the Docker socket into the CTFd container by modify the `docker-compose.yml` file:
66 | ```bash
67 | services:
68 | ctfd:
69 | ...
70 | volumes:
71 | - /var/run/docker.sock:/var/run/docker.sock
72 | ...
73 | ```
74 | - Restart CTFd
75 | - Go to the plugin settings page: `/containers/settings`
76 | - Fill in all fields except the `Base URL`.
77 |
78 | ### Using Remote Docker via SSH
79 |
80 | For remote Docker, the CTFd host must have SSH access to the remote server.
81 |
82 | #### Prerequisites:
83 | - **SSH access** from the CTFd host to the Docker server
84 | - The remote server's fingerprint should be in the `known_hosts` file
85 | - SSH key files (`id_rsa`) and an SSH config file should be available
86 |
87 | #### Case A: **CTFd Running via Docker**
88 |
89 | 1. **Prepare SSH Config:**
90 | ```bash
91 | mkdir ssh_config
92 | cp ~/.ssh/id_rsa ~/.ssh/known_hosts ~/.ssh/config ssh_config/
93 | ```
94 |
95 | 2. **Mount SSH Config into the CTFd container:**
96 | ```yaml
97 | services:
98 | ctfd:
99 | ...
100 | volumes:
101 | - ./ssh_config:/root/.ssh:ro
102 | ...
103 | ```
104 |
105 | 3. **Restart CTFd:**
106 | ```bash
107 | docker-compose down
108 | docker-compose up -d
109 | ```
110 |
111 | #### Case B: **CTFd Running Directly on Host**
112 |
113 | 1. **Ensure SSH Access:**
114 | - Test the connection:
115 | ```bash
116 | ssh user@remote-server
117 | ```
118 |
119 | 2. **Configure Docker Base URL:**
120 | - In the CTFd plugin settings page (`/containers/settings`), set:
121 | ```
122 | Base URL: ssh://user@remote-server
123 | ```
124 |
125 | 3. **Restart CTFd:**
126 | ```bash
127 | sudo systemctl restart ctfd
128 | ```
129 |
130 | [Back to top](#ctfd-docker-containers-plugin)
131 |
132 | ---
133 |
134 | ## Demo
135 |
136 | ### Admin Dashboard
137 | - Manage running containers
138 | - Filter by challenge or player
139 |
140 | 
141 |
142 | ### Challenge View
143 |
144 | **Web Access** | **TCP Access**
145 | :-------------:|:-------------:
146 |  | 
147 |
148 | ### Live Demo
149 |
150 | 
151 |
152 | [Back to top](#ctfd-docker-containers-plugin)
153 |
154 | ---
155 |
156 | ## Roadmap
157 |
158 | - [x] Support for user mode
159 | - [x] Admin dashboard with team/user filtering
160 | - [x] Compatibility with the core-beta theme
161 | - [x] Monitor share flag
162 | - [ ] Monitor detail on share flag
163 | - [ ] Prevent container creation on solved challenge
164 |
165 | For more features and known issues, check the [open issues](https://github.com/phannhat17/CTFd-Docker-Plugin/issues).
166 |
167 | [Back to top](#ctfd-docker-containers-plugin)
168 |
169 | ---
170 |
171 | ## License
172 |
173 | Distributed under the MIT License. See `LICENSE.txt` for details.
174 |
175 | > This plugin is an upgrade of [andyjsmith's plugin](https://github.com/andyjsmith/CTFd-Docker-Plugin) with additional features.
176 |
177 | If there are licensing concerns, please reach out via email (contact below).
178 |
179 | [Back to top](#ctfd-docker-containers-plugin)
180 |
181 | ---
182 |
183 | ## Contact
184 |
185 | **Phan Nhat**
186 | - **Discord:** ftpotato
187 | - **Email:** contact@phannhat.id.vn
188 | - **Project Link:** [CTFd Docker Plugin](https://github.com/phannhat17/CTFd-Docker-Plugin)
189 |
190 | [Back to top](#ctfd-docker-containers-plugin)
191 |
192 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 |
3 | import time
4 | import json
5 | import datetime
6 | import math
7 |
8 | from flask import Blueprint, request, Flask, render_template, url_for, redirect, flash
9 |
10 | from CTFd.models import db, Solves, Teams, Users
11 | from CTFd.plugins import register_plugin_assets_directory
12 | from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge
13 | from CTFd.utils.modes import get_model
14 | from .models import ContainerChallengeModel, ContainerInfoModel, ContainerSettingsModel, ContainerFlagModel, ContainerCheatLog
15 | from .container_manager import ContainerManager, ContainerException
16 | from .admin_routes import admin_bp, set_container_manager as set_admin_manager
17 | from .user_routes import containers_bp, set_container_manager as set_user_manager
18 | from .helpers import *
19 | from CTFd.utils.user import get_current_user
20 |
21 | settings = json.load(open(get_settings_path()))
22 |
23 | class ContainerChallenge(BaseChallenge):
24 | id = settings["plugin-info"]["id"]
25 | name = settings["plugin-info"]["name"]
26 | templates = settings["plugin-info"]["templates"]
27 | scripts = settings["plugin-info"]["scripts"]
28 | route = settings["plugin-info"]["base_path"]
29 |
30 | challenge_model = ContainerChallengeModel
31 |
32 | @classmethod
33 | def read(cls, challenge):
34 | """
35 | This method is in used to access the data of a challenge in a format processable by the front end.
36 |
37 | :param challenge:
38 | :return: Challenge object, data dictionary to be returned to the user
39 | """
40 | data = {
41 | "id": challenge.id,
42 | "name": challenge.name,
43 | "value": challenge.value,
44 | "image": challenge.image,
45 | "port": challenge.port,
46 | "command": challenge.command,
47 | "connection_type": challenge.connection_type,
48 | "initial": challenge.initial,
49 | "decay": challenge.decay,
50 | "minimum": challenge.minimum,
51 | "description": challenge.description,
52 | "connection_info": challenge.connection_info,
53 | "category": challenge.category,
54 | "state": challenge.state,
55 | "max_attempts": challenge.max_attempts,
56 | "type": challenge.type,
57 | "type_data": {
58 | "id": cls.id,
59 | "name": cls.name,
60 | "templates": cls.templates,
61 | "scripts": cls.scripts,
62 | },
63 | }
64 | return data
65 |
66 | @classmethod
67 | def calculate_value(cls, challenge):
68 | Model = get_model()
69 |
70 | solve_count = (
71 | Solves.query.join(Model, Solves.account_id == Model.id)
72 | .filter(
73 | Solves.challenge_id == challenge.id,
74 | Model.hidden == False,
75 | Model.banned == False,
76 | )
77 | .count()
78 | )
79 |
80 | # If the solve count is 0 we shouldn't manipulate the solve count to
81 | # let the math update back to normal
82 | if solve_count != 0:
83 | # We subtract -1 to allow the first solver to get max point value
84 | solve_count -= 1
85 |
86 | # It is important that this calculation takes into account floats.
87 | # Hence this file uses from __future__ import division
88 | value = (
89 | ((challenge.minimum - challenge.initial) / (challenge.decay**2))
90 | * (solve_count**2)
91 | ) + challenge.initial
92 |
93 | value = math.ceil(value)
94 |
95 | if value < challenge.minimum:
96 | value = challenge.minimum
97 |
98 | challenge.value = value
99 | db.session.commit()
100 | return challenge
101 |
102 | @classmethod
103 | def update(cls, challenge, request):
104 | """
105 | This method is used to update the information associated with a challenge. This should be kept strictly to the
106 | Challenges table and any child tables.
107 | :param challenge:
108 | :param request:
109 | :return:
110 | """
111 | data = request.form or request.get_json()
112 |
113 | for attr, value in data.items():
114 | # We need to set these to floats so that the next operations don't operate on strings
115 | if attr in ("initial", "minimum", "decay"):
116 | value = float(value)
117 | setattr(challenge, attr, value)
118 |
119 | return ContainerChallenge.calculate_value(challenge)
120 |
121 | @classmethod
122 | def solve(cls, user, team, challenge, request):
123 | super().solve(user, team, challenge, request)
124 |
125 | cls.calculate_value(challenge)
126 |
127 | @classmethod
128 | def attempt(cls, challenge, request):
129 | # 1) Gather user/team & submitted_flag
130 | try:
131 | user, x_id, submitted_flag = get_xid_and_flag()
132 | except ValueError as e:
133 | return False, str(e)
134 |
135 | # 2) Get running container
136 | container_info = None
137 | try:
138 | container_info = get_active_container(challenge.id, x_id)
139 | except ValueError as e:
140 | return False, str(e)
141 |
142 | # 3) Check if container is actually running
143 | from . import container_manager
144 | if not container_manager or not container_manager.is_container_running(container_info.container_id):
145 | return False, "Your container is not running; you cannot submit yet."
146 |
147 | # Validate the flag belongs to the user/team
148 | try:
149 | container_flag = get_container_flag(submitted_flag, user, container_manager, container_info, challenge)
150 | except ValueError as e:
151 | return False, str(e) # Return incorrect flag message if not cheating
152 |
153 | # 6) Mark used & kill container => success
154 | container_flag.used = True
155 | db.session.commit()
156 |
157 | # **If the challenge is static, delete both flag and container records**
158 | if challenge.flag_mode == "static":
159 | db.session.delete(container_flag)
160 | db.session.commit()
161 |
162 | # **If the challenge is random, keep the flag but delete only the container info**
163 | if challenge.flag_mode == "random":
164 | db.session.query(ContainerFlagModel).filter_by(container_id=container_info.container_id).update({"container_id": None})
165 | db.session.commit()
166 |
167 | # Remove container info record
168 | container = ContainerInfoModel.query.filter_by(container_id=container_info.container_id).first()
169 | if container:
170 | db.session.delete(container)
171 | db.session.commit()
172 |
173 | # Kill the container
174 | container_manager.kill_container(container_info.container_id)
175 |
176 | return True, "Correct"
177 |
178 | container_manager = None # Global
179 |
180 | def load(app: Flask):
181 | # Ensure database is initialized
182 | app.db.create_all()
183 |
184 | # Register the challenge type
185 | CHALLENGE_CLASSES["container"] = ContainerChallenge
186 |
187 | register_plugin_assets_directory(
188 | app, base_path=settings["plugin-info"]["base_path"]
189 | )
190 |
191 | global container_manager
192 | container_settings = settings_to_dict(ContainerSettingsModel.query.all())
193 | container_manager = ContainerManager(container_settings, app)
194 |
195 | base_bp = Blueprint(
196 | "containers",
197 | __name__,
198 | template_folder=settings["blueprint"]["template_folder"],
199 | static_folder=settings["blueprint"]["static_folder"]
200 | )
201 |
202 | set_admin_manager(container_manager)
203 | set_user_manager(container_manager)
204 |
205 | # Register the blueprints
206 | app.register_blueprint(admin_bp) # Admin APIs
207 | app.register_blueprint(containers_bp) # User APIs
208 |
209 |
210 | app.register_blueprint(base_bp)
211 |
--------------------------------------------------------------------------------
/admin_routes.py:
--------------------------------------------------------------------------------
1 | import json
2 | from flask import Blueprint, request, jsonify, render_template, url_for, redirect, Flask, flash
3 | from CTFd.models import db
4 | from .models import ContainerChallengeModel, ContainerInfoModel, ContainerSettingsModel, ContainerCheatLog
5 | from .container_manager import ContainerManager, ContainerException
6 | from CTFd.utils.decorators import admins_only
7 | from .helpers import *
8 |
9 | admin_bp = Blueprint("container_admin", __name__, url_prefix="/containers/admin")
10 |
11 | container_manager = None
12 |
13 | def set_container_manager(manager):
14 | global container_manager
15 | container_manager = manager
16 |
17 | # Admin dashboard
18 | @admin_bp.route("/dashboard", methods=["GET"])
19 | @admins_only
20 | def route_containers_dashboard():
21 | connected = False
22 | try:
23 | connected = container_manager.is_connected()
24 | except ContainerException:
25 | pass
26 |
27 | running_containers = ContainerInfoModel.query.order_by(
28 | ContainerInfoModel.timestamp.desc()
29 | ).all()
30 |
31 | for i, container in enumerate(running_containers):
32 | try:
33 | running_containers[i].is_running = container_manager.is_container_running(
34 | container.container_id
35 | )
36 | except ContainerException:
37 | running_containers[i].is_running = False
38 |
39 | return render_template(
40 | "container_dashboard.html",
41 | containers=running_containers,
42 | connected=connected,
43 | )
44 |
45 | @admin_bp.route("/settings", methods=["GET"])
46 | @admins_only
47 | def route_containers_settings():
48 | connected = False
49 | try:
50 | connected = container_manager.is_connected()
51 | except ContainerException:
52 | pass
53 |
54 | return render_template(
55 | "container_settings.html",
56 | settings=container_manager.settings,
57 | connected=connected,
58 | )
59 |
60 | @admin_bp.route("/cheat", methods=["GET"])
61 | @admins_only
62 | def route_containers_cheat():
63 | connected = False
64 | try:
65 | connected = container_manager.is_connected()
66 | except ContainerException:
67 | pass
68 |
69 | cheat_logs = ContainerCheatLog.query.order_by(ContainerCheatLog.timestamp.desc()).all()
70 |
71 | return render_template(
72 | "container_cheat.html",
73 | connected=connected,
74 | cheat_logs=cheat_logs
75 | )
76 |
77 | # Admin API
78 | @admin_bp.route("/api/settings", methods=["POST"])
79 | @admins_only
80 | def route_update_settings():
81 |
82 | required_fields = [
83 | "docker_base_url",
84 | "docker_hostname",
85 | "container_expiration",
86 | "container_maxmemory",
87 | "container_maxcpu",
88 | "max_containers",
89 | ]
90 |
91 | # Validate required fields
92 | for field in required_fields:
93 | if request.form.get(field) is None:
94 | return {"error": f"{field} is required."}, 400
95 |
96 | # Update settings dynamically
97 | for key in required_fields:
98 | value = request.form.get(key)
99 | setting = ContainerSettingsModel.query.filter_by(key=key).first()
100 |
101 | if not setting:
102 | setting = ContainerSettingsModel(key=key, value=value)
103 | db.session.add(setting)
104 | else:
105 | setting.value = value
106 |
107 | db.session.commit()
108 |
109 | # Refresh container manager settings
110 | container_manager.settings = settings_to_dict(
111 | ContainerSettingsModel.query.all()
112 | )
113 |
114 | if container_manager.settings.get("docker_base_url") is not None:
115 | try:
116 | container_manager.initialize_connection(container_manager.settings, Flask)
117 | except ContainerException as err:
118 | flash(str(err), "error")
119 | return redirect(url_for(".route_containers_settings"))
120 |
121 | return redirect(url_for(".route_containers_dashboard"))
122 |
123 | @admin_bp.route("/api/kill", methods=["POST"])
124 | @admins_only
125 | def route_admin_kill_container():
126 | try:
127 | validate_request(request.json, ["container_id"])
128 | return kill_container(container_manager, request.json.get("container_id"))
129 | except ValueError as err:
130 | return {"error": str(err)}, 400
131 |
132 | @admin_bp.route("/api/purge", methods=["POST"])
133 | @admins_only
134 | def route_purge_containers():
135 | """Bulk delete multiple containers"""
136 | try:
137 | validate_request(request.json, ["container_ids"])
138 | container_ids = request.json.get("container_ids", [])
139 | if not container_ids:
140 | return {"error": "No containers selected"}, 400
141 |
142 | deleted_count = 0
143 | for container_id in container_ids:
144 | container = ContainerInfoModel.query.filter_by(container_id=container_id).first()
145 | if container:
146 | try:
147 | container_manager.kill_container(container_id)
148 | db.session.delete(container)
149 | deleted_count += 1
150 | except ContainerException:
151 | continue
152 |
153 | db.session.commit()
154 | return {"success": f"Deleted {deleted_count} container(s)"}
155 | except ValueError as err:
156 | return {"error": str(err)}, 400
157 |
158 | @admin_bp.route("/api/images", methods=["GET"])
159 | @admins_only
160 | def route_get_images():
161 | try:
162 | images = container_manager.get_images()
163 | except ContainerException as err:
164 | return {"error": str(err)}
165 |
166 | return {"images": images}
167 |
168 | @admin_bp.route("/api/running_containers", methods=["GET"])
169 | @admins_only
170 | def route_get_running_containers():
171 | running_containers = ContainerInfoModel.query.order_by(
172 | ContainerInfoModel.timestamp.desc()
173 | ).all()
174 |
175 | connected = False
176 | try:
177 | connected = container_manager.is_connected()
178 | except ContainerException:
179 | pass
180 |
181 | # Create lists to store unique teams and challenges
182 | unique_teams = set()
183 | unique_challenges = set()
184 |
185 | for i, container in enumerate(running_containers):
186 | try:
187 | running_containers[i].is_running = (
188 | container_manager.is_container_running(container.container_id)
189 | )
190 | except ContainerException:
191 | running_containers[i].is_running = False
192 |
193 | # Add team and challenge to the unique sets
194 | if is_team_mode() is True:
195 | unique_teams.add(f"{container.team.name} [{container.team_id}]")
196 | else:
197 | unique_teams.add(f"{container.user.name} [{container.user_id}]")
198 | unique_challenges.add(
199 | f"{container.challenge.name} [{container.challenge_id}]"
200 | )
201 |
202 | # Convert unique sets to lists
203 | unique_teams_list = list(unique_teams)
204 | unique_challenges_list = list(unique_challenges)
205 |
206 | # Create a list of dictionaries containing running_containers data
207 | running_containers_data = []
208 | for container in running_containers:
209 | if is_team_mode() is True:
210 | container_data = {
211 | "container_id": container.container_id,
212 | "image": container.challenge.image,
213 | "challenge": f"{container.challenge.name} [{container.challenge_id}]",
214 | "team": f"{container.team.name} [{container.team_id}]",
215 | "port": container.port,
216 | "created": container.timestamp,
217 | "expires": container.expires,
218 | "is_running": container.is_running,
219 | }
220 | else:
221 | container_data = {
222 | "container_id": container.container_id,
223 | "image": container.challenge.image,
224 | "challenge": f"{container.challenge.name} [{container.challenge_id}]",
225 | "user": f"{container.user.name} [{container.user_id}]",
226 | "port": container.port,
227 | "created": container.timestamp,
228 | "expires": container.expires,
229 | "is_running": container.is_running,
230 | }
231 | running_containers_data.append(container_data)
232 |
233 | # Create a JSON response containing running_containers_data, unique teams, and unique challenges
234 | response_data = {
235 | "containers": running_containers_data,
236 | "connected": connected,
237 | "teams": unique_teams_list,
238 | "challenges": unique_challenges_list,
239 | }
240 |
241 | # Return the JSON response
242 | return jsonify(response_data)
243 |
--------------------------------------------------------------------------------
/assets/create.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/challenges/create.html" %}
2 |
3 | {% block header %}
4 |
5 | Container challenges dynamically spin up a Docker container for each team to run the challenge on.
6 |