From 5c025dbf1e6a47b3d34c2f72b13f961bc4c2a779 Mon Sep 17 00:00:00 2001 From: Spencer Pogorzelski <34356756+Scoder12@users.noreply.github.com> Date: Mon, 15 Jan 2024 21:17:11 -0800 Subject: [PATCH 001/123] git clone --depth 1 (#285) --- challenge/Dockerfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/challenge/Dockerfile b/challenge/Dockerfile index 8e0369a8f..02ce6585c 100644 --- a/challenge/Dockerfile +++ b/challenge/Dockerfile @@ -162,13 +162,13 @@ RUN < Date: Fri, 12 Jan 2024 17:46:24 -0700 Subject: [PATCH 002/123] rewrite get_user_belts to expose the belt requirements to importation --- dojo_plugin/utils/dojo/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/dojo_plugin/utils/dojo/__init__.py b/dojo_plugin/utils/dojo/__init__.py index a1c417a52..e6ab3fa46 100644 --- a/dojo_plugin/utils/dojo/__init__.py +++ b/dojo_plugin/utils/dojo/__init__.py @@ -398,17 +398,18 @@ def get_current_dojo_challenge(user=None): .first() ) +BELT_REQUIREMENTS = { + "orange": ["intro-to-cybersecurity"], + "yellow": ["program-security"], + "green": ["system-security"], + "blue": ["software-exploitation"], +} def get_user_belts(user): - belts = { - "Orange Belt": ["intro-to-cybersecurity"], - "Yellow Belt": ["intro-to-cybersecurity", "program-security"], - "Green Belt": ["intro-to-cybersecurity", "program-security", "system-security"], - "Blue Belt": ["intro-to-cybersecurity", "program-security", "system-security", "software-exploitation"], - } - result = [] - for belt, dojo_ids in belts.items(): - dojos = Dojos.query.filter(Dojos.official, Dojos.id.in_(dojo_ids)) - if all(dojo.completed(user) for dojo in dojos): - result.append(belt) + result = [ ] + for belt, dojo_id in BELT_REQUIREMENTS.items(): + dojo = Dojos.query.filter(Dojos.official, Dojos.id == dojo_id).one() + if not dojo.completed(user): + break + result.append(belt.title() + " Belt") return result From 776b4ebee0bd13fb3019779304efa22c042b9386 Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 12 Jan 2024 17:46:41 -0700 Subject: [PATCH 003/123] add logic for getting a list of dojo completers --- dojo_plugin/models/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dojo_plugin/models/__init__.py b/dojo_plugin/models/__init__.py index f0e8a7864..be1d79396 100644 --- a/dojo_plugin/models/__init__.py +++ b/dojo_plugin/models/__init__.py @@ -194,6 +194,12 @@ def viewable(cls, id=None, user=None): def solves(self, **kwargs): return DojoChallenges.solves(dojo=self, **kwargs) + def completers(self): + sq = Solves.query.join(DojoChallenges, Solves.challenge_id == DojoChallenges.challenge_id).add_columns( + Solves.user_id.label("solve_user_id"), db.func.count().label("solve_count"), db.func.max(Solves.date).label("last_solve") + ).filter(DojoChallenges.dojo == self).group_by(Solves.user_id).subquery() + return Users.query.join(sq).filter_by(solve_count=len(self.challenges)).order_by(sq.columns.last_solve).all() + def completed(self, user): return self.solves(user=user, ignore_visibility=True, ignore_admins=False).count() == len(self.challenges) From e14e13ec96ea249f425d309af3ae8750d46b9609 Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 12 Jan 2024 18:44:12 -0700 Subject: [PATCH 004/123] fix get_belts() and remove obsolete code --- dojo_plugin/api/v1/belts.py | 36 ++++++---------- dojo_plugin/api/v1/scoreboard.py | 2 +- dojo_plugin/models/__init__.py | 9 +++- dojo_plugin/utils/__init__.py | 69 ------------------------------ dojo_plugin/utils/dojo/__init__.py | 8 ++-- 5 files changed, 24 insertions(+), 100 deletions(-) diff --git a/dojo_plugin/api/v1/belts.py b/dojo_plugin/api/v1/belts.py index 75acc7a20..44f6f10e8 100644 --- a/dojo_plugin/api/v1/belts.py +++ b/dojo_plugin/api/v1/belts.py @@ -4,7 +4,8 @@ from CTFd.cache import cache from CTFd.models import db, Users, Solves -from ...utils import belt_challenges +from ...models import Dojos +from ...utils.dojo import BELT_REQUIREMENTS belts_namespace = Namespace("belts", description="Endpoint to manage belts") @@ -17,33 +18,20 @@ def get_belts(): "users": {}, } - for color, challenges in belt_challenges().items(): + for n,(color,dojo_id) in enumerate(BELT_REQUIREMENTS.items()): result["dates"][color] = {} - - belted_users = ( - db.session.query(Users.id, Users.name, db.func.max(Solves.date)) - .join(Solves, Users.id == Solves.user_id) - .filter(Solves.challenge_id.in_(challenges.subquery())) - .group_by(Users.id) - .having(db.func.count() == challenges.count()) - .order_by(db.func.max(Solves.date)) - ) - - for user_id, handle, date in belted_users: - result["dates"][color][user_id] = str(date) - result["users"][user_id] = { - "handle": handle, + dojo = Dojos.query.filter_by(id=dojo_id).first() + + for user,date in dojo.completions(): + if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: + continue + result["dates"][color][user.id] = str(date) + result["users"][user.id] = { + "handle": user.name, "color": color, + "rank_id": n, } - # TODO: support belt deadlines - for user_id, belt in list(result["users"].items()): - if belt["color"] == "yellow": - received = datetime.datetime.fromisoformat(result["dates"]["yellow"][user_id]) - if received > datetime.datetime.fromisoformat("2022-10-01"): - del result["users"][user_id] - del result["dates"]["yellow"][user_id] - return result diff --git a/dojo_plugin/api/v1/scoreboard.py b/dojo_plugin/api/v1/scoreboard.py index d86188209..af3b9236c 100644 --- a/dojo_plugin/api/v1/scoreboard.py +++ b/dojo_plugin/api/v1/scoreboard.py @@ -18,7 +18,7 @@ from sqlalchemy.orm.session import Session from ...models import Dojos, DojoChallenges, DojoUsers, DojoMembers, DojoAdmins, DojoStudents, DojoModules, DojoChallengeVisibilities -from ...utils import dojo_standings, dojo_completions, user_dojos, first_bloods, daily_solve_counts +from ...utils import dojo_standings, user_dojos, first_bloods, daily_solve_counts from ...utils.dojo import dojo_route, dojo_accessible from .belts import get_belts diff --git a/dojo_plugin/models/__init__.py b/dojo_plugin/models/__init__.py index be1d79396..03d216ad4 100644 --- a/dojo_plugin/models/__init__.py +++ b/dojo_plugin/models/__init__.py @@ -194,11 +194,16 @@ def viewable(cls, id=None, user=None): def solves(self, **kwargs): return DojoChallenges.solves(dojo=self, **kwargs) - def completers(self): + def completions(self): + """ + Returns a list of (User, completion_timestamp) tuples for users, sorted by time in ascending order. + """ sq = Solves.query.join(DojoChallenges, Solves.challenge_id == DojoChallenges.challenge_id).add_columns( Solves.user_id.label("solve_user_id"), db.func.count().label("solve_count"), db.func.max(Solves.date).label("last_solve") ).filter(DojoChallenges.dojo == self).group_by(Solves.user_id).subquery() - return Users.query.join(sq).filter_by(solve_count=len(self.challenges)).order_by(sq.columns.last_solve).all() + return Users.query.join(sq).filter_by( + solve_count=len(self.challenges) + ).add_column(sq.columns.last_solve).order_by(sq.columns.last_solve).all() def completed(self, user): return self.solves(user=user, ignore_visibility=True, ignore_admins=False).count() == len(self.challenges) diff --git a/dojo_plugin/utils/__init__.py b/dojo_plugin/utils/__init__.py index ad1fe575c..f3fbb33a9 100644 --- a/dojo_plugin/utils/__init__.py +++ b/dojo_plugin/utils/__init__.py @@ -238,35 +238,6 @@ def load_dojo(dojo_id, dojo_spec, user=None, dojo_dir=None, commit=True, log=log db.session.rollback() -def dojo_completions(): - all_solves = ( - db.session.query(DojoChallenges.dojo_id.label("dojo_id")) - .join(Solves, DojoChallenges.challenge_id == Solves.challenge_id) - .add_columns( - db.func.count(Solves.id).label("solves"), - Solves.user_id, - db.func.max(Solves.date).label("last_solve"), - db.func.min(Solves.date).label("first_solve"), - ) - .group_by(Solves.user_id, DojoChallenges.dojo_id) - .order_by("last_solve") - ).all() - all_challenges = ( - db.session.query(Dojos.id.label("dojo_id")) - .join(DojoChallenges, DojoChallenges.dojo_id == Dojos.id) - .add_columns(db.func.count(DojoChallenges.challenge_id).label("challenges")) - .group_by(Dojos.id) - ).all() - - chal_counts = { d.dojo_id: d.challenges for d in all_challenges } - completions = { } - for s in all_solves: - if s.solves == chal_counts[s.dojo_id]: - completions.setdefault(s.user_id, []).append({ - "dojo": s.dojo_id, "last_solve": s.last_solve, "first_solve": s.first_solve - }) - return completions - def first_bloods(): first_blood_string = db.func.min(Solves.date.cast(String)+"|"+Solves.user_id.cast(String)) first_blood_query = ( @@ -295,46 +266,6 @@ def daily_solve_counts(): ).all() return counts - -def belt_challenges(): - # TODO: move this concept into dojo yml - - yellow_categories = [ - "embryoio", - "babysuid", - "embryoasm", - "babyshell", - "babyjail", - "embryogdb", - "babyrev", - "babymem", - "toddlerone", - ] - - blue_categories = [ - *yellow_categories, - "babyrop", - "babyheap", - "babyrace", - "babykernel", - "toddlertwo", - ] - - color_categories = { - "yellow": yellow_categories, - "blue": blue_categories, - } - - return { - color: db.session.query(Challenges.id).filter( - Challenges.state == "visible", - Challenges.value > 0, - Challenges.id < 1000, - Challenges.category.in_(categories), - ) - for color, categories in color_categories.items() - } - # based on https://stackoverflow.com/questions/36408496/python-logging-handler-to-append-to-list class ListHandler(logging.Handler): # Inherit from logging.Handler def __init__(self, log_list): diff --git a/dojo_plugin/utils/dojo/__init__.py b/dojo_plugin/utils/dojo/__init__.py index e6ab3fa46..602bb0dc1 100644 --- a/dojo_plugin/utils/dojo/__init__.py +++ b/dojo_plugin/utils/dojo/__init__.py @@ -399,10 +399,10 @@ def get_current_dojo_challenge(user=None): ) BELT_REQUIREMENTS = { - "orange": ["intro-to-cybersecurity"], - "yellow": ["program-security"], - "green": ["system-security"], - "blue": ["software-exploitation"], + "orange": "intro-to-cybersecurity", + "yellow": "program-security", + "green": "system-security", + "blue": "software-exploitation", } def get_user_belts(user): From 15257c6cdd3b56a2170051e0047210eb2a60bc9b Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 12 Jan 2024 18:47:24 -0700 Subject: [PATCH 005/123] i'm tired of missing the motherfucking flags on the motherfucking scoreboard --- dojo_plugin/api/v1/scoreboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dojo_plugin/api/v1/scoreboard.py b/dojo_plugin/api/v1/scoreboard.py index af3b9236c..59eaa72b0 100644 --- a/dojo_plugin/api/v1/scoreboard.py +++ b/dojo_plugin/api/v1/scoreboard.py @@ -95,6 +95,7 @@ def hook_object_update(mapper, connection, target): invalidate_scoreboard_cache() def get_scoreboard_page(model, duration=None, page=1, per_page=20): + belt_data = get_belts() results = get_scoreboard_for(model, duration) start_idx = (page - 1) * per_page @@ -107,7 +108,7 @@ def standing(item): result = {key: item[key] for key in item.keys()} result["url"] = url_for("pwncollege_users.view_other", user_id=result["user_id"]) result["symbol"] = email_symbol_asset(result.pop("email")) - result["belt"] = belt_asset(None) # TODO + result["belt"] = belt_asset(belt_data["users"].get(result["user_id"], {"color":None})["color"]) result["badges"] = [] # TODO return result From 5f5488d0f5d9e8f0c6bb0950e51d42dc714c5afe Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 12 Jan 2024 18:59:09 -0700 Subject: [PATCH 006/123] well i'll say, oh deary me --- dojo_plugin/api/v1/belts.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dojo_plugin/api/v1/belts.py b/dojo_plugin/api/v1/belts.py index 44f6f10e8..06177792d 100644 --- a/dojo_plugin/api/v1/belts.py +++ b/dojo_plugin/api/v1/belts.py @@ -1,3 +1,4 @@ +import sqlalchemy import datetime from flask_restx import Namespace, Resource @@ -20,7 +21,11 @@ def get_belts(): for n,(color,dojo_id) in enumerate(BELT_REQUIREMENTS.items()): result["dates"][color] = {} - dojo = Dojos.query.filter_by(id=dojo_id).first() + try: + dojo = Dojos.query.filter_by(id=dojo_id).first() + except sqlalchemy.exc.NoResultsFound: + # We are likely missing the correct dojos in the DB (e.g., custom deployment) + break for user,date in dojo.completions(): if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: From 024ea6a70e5ec7f403d5121ff4ca8af79ef42e69 Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 12 Jan 2024 19:09:58 -0700 Subject: [PATCH 007/123] AWAY, EVIL EXCEPTION --- dojo_plugin/api/v1/belts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dojo_plugin/api/v1/belts.py b/dojo_plugin/api/v1/belts.py index 06177792d..a714ee8be 100644 --- a/dojo_plugin/api/v1/belts.py +++ b/dojo_plugin/api/v1/belts.py @@ -20,13 +20,13 @@ def get_belts(): } for n,(color,dojo_id) in enumerate(BELT_REQUIREMENTS.items()): - result["dates"][color] = {} - try: - dojo = Dojos.query.filter_by(id=dojo_id).first() - except sqlalchemy.exc.NoResultsFound: + dojo = Dojos.query.filter_by(id=dojo_id).first() + if not dojo: # We are likely missing the correct dojos in the DB (e.g., custom deployment) break + result["dates"][color] = {} + for user,date in dojo.completions(): if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: continue From d2eaf654571e22ea03b285ff81e74d8b0d62539f Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 12 Jan 2024 19:24:42 -0700 Subject: [PATCH 008/123] move util belt logic into a utils.belts module --- dojo_plugin/__init__.py | 2 +- dojo_plugin/api/v1/belts.py | 37 +---------------------- dojo_plugin/api/v1/scoreboard.py | 2 +- dojo_plugin/utils/belts.py | 48 ++++++++++++++++++++++++++++++ dojo_plugin/utils/dojo/__init__.py | 16 ---------- 5 files changed, 51 insertions(+), 54 deletions(-) create mode 100644 dojo_plugin/utils/belts.py diff --git a/dojo_plugin/__init__.py b/dojo_plugin/__init__.py index 71683473e..9f0d3fe63 100644 --- a/dojo_plugin/__init__.py +++ b/dojo_plugin/__init__.py @@ -19,7 +19,7 @@ from .config import DOJO_HOST, bootstrap from .utils import unserialize_user_flag, render_markdown from .utils.discord import get_discord_user, get_discord_roles, add_role, send_message -from .utils.dojo import get_user_belts +from .utils.belts import get_user_belts from .pages.dojos import dojos, dojos_override from .pages.dojo import dojo from .pages.workspace import workspace diff --git a/dojo_plugin/api/v1/belts.py b/dojo_plugin/api/v1/belts.py index a714ee8be..7f6e5dcf1 100644 --- a/dojo_plugin/api/v1/belts.py +++ b/dojo_plugin/api/v1/belts.py @@ -1,45 +1,10 @@ -import sqlalchemy -import datetime - from flask_restx import Namespace, Resource -from CTFd.cache import cache -from CTFd.models import db, Users, Solves - -from ...models import Dojos -from ...utils.dojo import BELT_REQUIREMENTS +from ...utils.belts import get_belts belts_namespace = Namespace("belts", description="Endpoint to manage belts") -@cache.memoize(timeout=60) -def get_belts(): - result = { - "dates": {}, - "users": {}, - } - - for n,(color,dojo_id) in enumerate(BELT_REQUIREMENTS.items()): - dojo = Dojos.query.filter_by(id=dojo_id).first() - if not dojo: - # We are likely missing the correct dojos in the DB (e.g., custom deployment) - break - - result["dates"][color] = {} - - for user,date in dojo.completions(): - if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: - continue - result["dates"][color][user.id] = str(date) - result["users"][user.id] = { - "handle": user.name, - "color": color, - "rank_id": n, - } - - return result - - @belts_namespace.route("") class Belts(Resource): def get(self): diff --git a/dojo_plugin/api/v1/scoreboard.py b/dojo_plugin/api/v1/scoreboard.py index 59eaa72b0..466e6ed0d 100644 --- a/dojo_plugin/api/v1/scoreboard.py +++ b/dojo_plugin/api/v1/scoreboard.py @@ -20,7 +20,7 @@ from ...models import Dojos, DojoChallenges, DojoUsers, DojoMembers, DojoAdmins, DojoStudents, DojoModules, DojoChallengeVisibilities from ...utils import dojo_standings, user_dojos, first_bloods, daily_solve_counts from ...utils.dojo import dojo_route, dojo_accessible -from .belts import get_belts +from ...utils.belts import get_belts SCOREBOARD_CACHE_TIMEOUT_SECONDS = 60 * 60 * 2 # two hours make to cache all scoreboards scoreboard_namespace = Namespace("scoreboard") diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py new file mode 100644 index 000000000..c75576e2b --- /dev/null +++ b/dojo_plugin/utils/belts.py @@ -0,0 +1,48 @@ +from CTFd.cache import cache +from ..models import Dojos + + +BELT_REQUIREMENTS = { + "orange": "intro-to-cybersecurity", + "yellow": "program-security", + "green": "system-security", + "blue": "software-exploitation", +} + +def get_user_belts(user): + result = [ ] + for belt, dojo_id in BELT_REQUIREMENTS.items(): + dojo = Dojos.query.filter(Dojos.official, Dojos.id == dojo_id).one() + if not dojo.completed(user): + break + result.append(belt.title() + " Belt") + return result + +@cache.memoize(timeout=60) +def get_belts(): + result = { + "dates": {}, + "users": {}, + } + + for n,(color,dojo_id) in enumerate(BELT_REQUIREMENTS.items()): + dojo = Dojos.query.filter_by(id=dojo_id).first() + if not dojo: + # We are likely missing the correct dojos in the DB (e.g., custom deployment) + break + + result["dates"][color] = {} + + for user,date in dojo.completions(): + if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: + continue + result["dates"][color][user.id] = str(date) + result["users"][user.id] = { + "handle": user.name, + "color": color, + "rank_id": n, + } + + return result + + diff --git a/dojo_plugin/utils/dojo/__init__.py b/dojo_plugin/utils/dojo/__init__.py index 602bb0dc1..3ce792a8a 100644 --- a/dojo_plugin/utils/dojo/__init__.py +++ b/dojo_plugin/utils/dojo/__init__.py @@ -397,19 +397,3 @@ def get_current_dojo_challenge(user=None): DojoChallenges.dojo == Dojos.from_id(container.labels.get("dojo.dojo_id")).first()) .first() ) - -BELT_REQUIREMENTS = { - "orange": "intro-to-cybersecurity", - "yellow": "program-security", - "green": "system-security", - "blue": "software-exploitation", -} - -def get_user_belts(user): - result = [ ] - for belt, dojo_id in BELT_REQUIREMENTS.items(): - dojo = Dojos.query.filter(Dojos.official, Dojos.id == dojo_id).one() - if not dojo.completed(user): - break - result.append(belt.title() + " Belt") - return result From 1de112e9d3b903765f3658c44fdeab88fdcb506f Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 14:36:15 -0700 Subject: [PATCH 009/123] add belts page --- dojo_plugin/__init__.py | 2 + dojo_plugin/pages/belts.py | 8 ++++ dojo_plugin/utils/belts.py | 9 +++- dojo_theme/templates/belts.html | 79 +++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 dojo_plugin/pages/belts.py create mode 100644 dojo_theme/templates/belts.html diff --git a/dojo_plugin/__init__.py b/dojo_plugin/__init__.py index 9f0d3fe63..802d09cfd 100644 --- a/dojo_plugin/__init__.py +++ b/dojo_plugin/__init__.py @@ -30,6 +30,7 @@ from .pages.discord import discord from .pages.course import course from .pages.writeups import writeups +from .pages.belts import belts from .api import api @@ -146,6 +147,7 @@ def load(app): app.register_blueprint(users) app.register_blueprint(course) app.register_blueprint(writeups) + app.register_blueprint(belts) app.register_blueprint(api, url_prefix="/pwncollege_api/v1") app.jinja_env.filters["markdown"] = render_markdown diff --git a/dojo_plugin/pages/belts.py b/dojo_plugin/pages/belts.py new file mode 100644 index 000000000..f40a18de5 --- /dev/null +++ b/dojo_plugin/pages/belts.py @@ -0,0 +1,8 @@ +from flask import Blueprint, render_template +from ..utils.belts import get_belts + +belts = Blueprint("pwncollege_belts", __name__) + +@belts.route("/belts") +def view_belts(): + return render_template("belts.html", belt_data=get_belts()) diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index c75576e2b..004a662c1 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -23,6 +23,7 @@ def get_belts(): result = { "dates": {}, "users": {}, + "ranks": {}, } for n,(color,dojo_id) in enumerate(BELT_REQUIREMENTS.items()): @@ -32,6 +33,7 @@ def get_belts(): break result["dates"][color] = {} + result["ranks"][color] = [] for user,date in dojo.completions(): if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: @@ -39,10 +41,13 @@ def get_belts(): result["dates"][color][user.id] = str(date) result["users"][user.id] = { "handle": user.name, + "site": user.website, "color": color, + "date": str(date), "rank_id": n, } - return result - + for user_id in result["users"]: + result["ranks"][result["users"][user_id]["color"]].append(user_id) + return result diff --git a/dojo_theme/templates/belts.html b/dojo_theme/templates/belts.html new file mode 100644 index 000000000..1cb7e639e --- /dev/null +++ b/dojo_theme/templates/belts.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

The Belted

+
+
+
+

+ The hacker ethos goes beyond the acquisition of a satisfactory grade in a college course. + A true hacker is never satisfied with the state of their knowledge. + They strive, or are irresistably driven towards, the achievement of absolute mastery of technical topics. +

+ +

+ Below is a list of true hackers: those who stared at the yellow box, and rather than flinching, dove in. +

+ +

+ NOTE: this list is a work in progress, and is missing some hallowed names from ancient times. + But don't worry! These masters' accomplishments can be viewed in the old belts page +

+ + {% for belt_color in belt_data.ranks|reverse %} +

{{belt_color.title()}} Belts

+

+ {% if belt_color == "blue" %} + It is the rare individual who perseveres against the hardest of challenges, pushing on when the night is at its darkest. + The hackers below are truly the most stout of spirit, fighting through countless seemingly-impossible challenges and proving themselves worthy to wear their blue belt among the clouds. + {% elif belt_color == "green" %} + A sapling grows toward the sun: a hacker emerges into the light. + These individuals have shown a unique ability to see through the haze of uncertainty to the clarity beyond. + They are ready to journey into the sky. + {% elif belt_color == "yellow" %} + The sky brightens as the sun rises. + The minds of these students are opening, admitting phenomenal cybersecurity concepts that few even suspect to exist. + With sunlight comes the possibility of growth and the promise of learning to come. + {% elif belt_color == "orange" %} + Dawn is on the horizon. + No longer do the names written below belong to those unknowing cybersecurity: instead, their owners now realize the vast tracts of concepts still unexplored, waiting ready to expand their minds. + {% endif %} +

+ +
    + {% for user_id in belt_data.ranks[belt_color] %} + {% set user = belt_data.users[user_id] %} +
  • + {{ user.handle }} + {% if user.name %}({{ user.name }}){% endif %} + {% if user.emoji %}{{ user.emoji }}{% endif %} + {% if user.site %} + + + + + {% endif %} + {% if user.mail %} + + + + + {% endif %} + (ascended {{ user.date }}) +
  • + {% endfor %} +
+
+ {% endfor %} + +

How to get on the above lists

+ You, too, can be listed among the legends above. + You earn your belts by fully completing the appropriate pwn.college dojos. + This takes skill, cunning, and perseverance, but you can do it. + Good luck. +
+{% endblock %} From c9eb5c44b45e9c3234006b73d310cac289f0011b Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 15:14:33 -0700 Subject: [PATCH 010/123] move belt assets helper into utils --- dojo_plugin/api/v1/scoreboard.py | 14 +------------- dojo_plugin/utils/belts.py | 5 +++++ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/dojo_plugin/api/v1/scoreboard.py b/dojo_plugin/api/v1/scoreboard.py index 466e6ed0d..06af198e6 100644 --- a/dojo_plugin/api/v1/scoreboard.py +++ b/dojo_plugin/api/v1/scoreboard.py @@ -20,7 +20,7 @@ from ...models import Dojos, DojoChallenges, DojoUsers, DojoMembers, DojoAdmins, DojoStudents, DojoModules, DojoChallengeVisibilities from ...utils import dojo_standings, user_dojos, first_bloods, daily_solve_counts from ...utils.dojo import dojo_route, dojo_accessible -from ...utils.belts import get_belts +from ...utils.belts import get_belts, belt_asset SCOREBOARD_CACHE_TIMEOUT_SECONDS = 60 * 60 * 2 # two hours make to cache all scoreboards scoreboard_namespace = Namespace("scoreboard") @@ -34,18 +34,6 @@ def email_symbol_asset(email): group = "hacker.png" return url_for("views.themes", path=f"img/dojo/{group}") - -def belt_asset(color): - if color == "black": - belt = "black.svg" - elif color == "blue": - belt = "blue.svg" - elif color == "yellow": - belt = "yellow.svg" - else: - belt = "white.svg" - return url_for("views.themes", path=f"img/dojo/{belt}") - @cache.memoize(timeout=SCOREBOARD_CACHE_TIMEOUT_SECONDS) def get_scoreboard_for(model, duration): duration_filter = ( diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index 004a662c1..62fbdadc8 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -1,4 +1,5 @@ from CTFd.cache import cache +from flask import url_for from ..models import Dojos @@ -9,6 +10,10 @@ "blue": "software-exploitation", } +def belt_asset(color): + belt = color + ".svg" if color in BELT_REQUIREMENTS else "white.svg" + return url_for("views.themes", path=f"img/dojo/{belt}") + def get_user_belts(user): result = [ ] for belt, dojo_id in BELT_REQUIREMENTS.items(): From 73d5081867fa3655a95bc1ed6d29bbf4c423af30 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 15:15:00 -0700 Subject: [PATCH 011/123] set a cutoff date after which belts become cumulative (currently 2023.10.1) --- dojo_plugin/utils/belts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index 62fbdadc8..dd9b46499 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -1,8 +1,11 @@ +import datetime + from CTFd.cache import cache from flask import url_for from ..models import Dojos +CUMULATIVE_CUTOFF = datetime.datetime(2023, 10, 1) BELT_REQUIREMENTS = { "orange": "intro-to-cybersecurity", "yellow": "program-security", @@ -41,7 +44,7 @@ def get_belts(): result["ranks"][color] = [] for user,date in dojo.completions(): - if result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: + if date > CUMULATIVE_CUTOFF and result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: continue result["dates"][color][user.id] = str(date) result["users"][user.id] = { From 6df5d406348125330df610fd34c0dfd6b9602a14 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 16:08:27 -0700 Subject: [PATCH 012/123] fetch belts from the awards table, named BELT_BLUE, BELT_GREEN, etc --- dojo_plugin/utils/belts.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index dd9b46499..5da33b910 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -1,6 +1,7 @@ import datetime from CTFd.cache import cache +from CTFd.models import Awards from flask import url_for from ..models import Dojos @@ -43,9 +44,18 @@ def get_belts(): result["dates"][color] = {} result["ranks"][color] = [] - for user,date in dojo.completions(): - if date > CUMULATIVE_CUTOFF and result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1: + belt_awards = [ (award.user, award.date) for award in Awards.query.filter_by(name="BELT_"+color.upper()) ] + awarded_ids = set(u.id for u,_ in belt_awards) + sorted_belts = sorted(belt_awards + dojo.completions(), key=lambda d: d[1]) + + for user,date in sorted_belts: + if ( + date > CUMULATIVE_CUTOFF and + user.id not in awarded_ids and + result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1 + ): continue + result["dates"][color][user.id] = str(date) result["users"][user.id] = { "handle": user.name, From d93c8b3c8eee304878712c5416242d9f5cdac105 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 18:54:45 -0700 Subject: [PATCH 013/123] some crazy polymorphic shenanigans --- dojo_plugin/models/__init__.py | 5 ++++- dojo_plugin/utils/belts.py | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dojo_plugin/models/__init__.py b/dojo_plugin/models/__init__.py index 03d216ad4..dea746680 100644 --- a/dojo_plugin/models/__init__.py +++ b/dojo_plugin/models/__init__.py @@ -20,7 +20,7 @@ from sqlalchemy.sql import or_, and_ from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method from sqlalchemy.ext.associationproxy import association_proxy -from CTFd.models import db, get_class_by_tablename, Challenges, Solves, Flags, Users, Admins +from CTFd.models import db, get_class_by_tablename, Challenges, Solves, Flags, Users, Admins, Awards from CTFd.utils.user import get_current_user, is_admin from ..config import DOJOS_DIR @@ -652,3 +652,6 @@ class DiscordUsers(db.Model): user = db.relationship("Users") __repr__ = columns_repr(["user", "discord_id"]) + +class Belts(Awards): + __mapper_args__ = {"polymorphic_identity": "belt"} diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index 5da33b910..e7a2bfb9d 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -1,9 +1,8 @@ import datetime from CTFd.cache import cache -from CTFd.models import Awards from flask import url_for -from ..models import Dojos +from ..models import Dojos, Belts CUMULATIVE_CUTOFF = datetime.datetime(2023, 10, 1) @@ -44,7 +43,7 @@ def get_belts(): result["dates"][color] = {} result["ranks"][color] = [] - belt_awards = [ (award.user, award.date) for award in Awards.query.filter_by(name="BELT_"+color.upper()) ] + belt_awards = [ (award.user, award.date) for award in Belts.query.filter_by(name=color) ] awarded_ids = set(u.id for u,_ in belt_awards) sorted_belts = sorted(belt_awards + dojo.completions(), key=lambda d: d[1]) From 85d70902d3b7eceb725c5d87e077d15ac59a8f89 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 19:02:03 -0700 Subject: [PATCH 014/123] automatically grant belt awards going forward --- dojo_plugin/__init__.py | 19 ++++++++++++++----- dojo_plugin/utils/belts.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/dojo_plugin/__init__.py b/dojo_plugin/__init__.py index 802d09cfd..fbb0fb561 100644 --- a/dojo_plugin/__init__.py +++ b/dojo_plugin/__init__.py @@ -15,7 +15,7 @@ from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge from CTFd.plugins.flags import FLAG_CLASSES, BaseFlag, FlagException -from .models import Dojos, DojoChallenges +from .models import Dojos, DojoChallenges, Belts from .config import DOJO_HOST, bootstrap from .utils import unserialize_user_flag, render_markdown from .utils.discord import get_discord_user, get_discord_roles, add_role, send_message @@ -43,18 +43,27 @@ class DojoChallenge(BaseChallenge): def solve(cls, user, team, challenge, request): super().solve(user, team, challenge, request) + current_belts = get_user_belts(user) + for belt in current_belts: + belt_award = Belts.query.filter_by(user=user, name=belt).one() + if belt_award: + continue + db.session.add(Belts(user=user, name=belt)) + db.session.commit() + discord_user = get_discord_user(user.id) if not discord_user: return discord_roles = get_discord_roles() - for belt in get_user_belts(user): - if discord_roles.get(belt) in discord_user["roles"]: + for belt in current_belts: + belt_role = belt.title() + " Belt" + if discord_roles.get(belt_role) in discord_user["roles"]: continue user_mention = f"<@{discord_user['user']['id']}>" - message = f"{user_mention} earned their {belt}! :tada:" + message = f"{user_mention} earned their {belt_role}! :tada:" print(message, flush=True) - add_role(discord_user["user"]["id"], belt) + add_role(discord_user["user"]["id"], belt_role) send_message(message, "belting-ceremony") diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index e7a2bfb9d..abf5c28ba 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -23,7 +23,7 @@ def get_user_belts(user): dojo = Dojos.query.filter(Dojos.official, Dojos.id == dojo_id).one() if not dojo.completed(user): break - result.append(belt.title() + " Belt") + result.append(belt) return result @cache.memoize(timeout=60) From c2e4fbcd1ea59a00659a1e871a8a6819b9ccb962 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 19:41:37 -0700 Subject: [PATCH 015/123] gracefully handle missing dojos --- dojo_plugin/utils/belts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index abf5c28ba..98f8922c0 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -20,7 +20,10 @@ def belt_asset(color): def get_user_belts(user): result = [ ] for belt, dojo_id in BELT_REQUIREMENTS.items(): - dojo = Dojos.query.filter(Dojos.official, Dojos.id == dojo_id).one() + dojo = Dojos.query.filter(Dojos.official, Dojos.id == dojo_id).first() + if not dojo: + # We are likely missing the correct dojos in the DB (e.g., custom deployment) + break if not dojo.completed(user): break result.append(belt) From 3ded88691fa60bf7eed80f670d4263627ba53403 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 13 Jan 2024 20:58:41 -0700 Subject: [PATCH 016/123] fix belt granting logic --- dojo_plugin/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo_plugin/__init__.py b/dojo_plugin/__init__.py index fbb0fb561..8eeada442 100644 --- a/dojo_plugin/__init__.py +++ b/dojo_plugin/__init__.py @@ -45,7 +45,7 @@ def solve(cls, user, team, challenge, request): current_belts = get_user_belts(user) for belt in current_belts: - belt_award = Belts.query.filter_by(user=user, name=belt).one() + belt_award = Belts.query.filter_by(user=user, name=belt).first() if belt_award: continue db.session.add(Belts(user=user, name=belt)) From 790140f90adbcd35d0a005e885805f1b486151b5 Mon Sep 17 00:00:00 2001 From: Robert Wasinger Date: Tue, 16 Jan 2024 00:51:28 -0700 Subject: [PATCH 017/123] update homepage to link to dynamic belts list --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index d797af2c3..a4e516bda 100644 --- a/index.html +++ b/index.html @@ -84,7 +84,7 @@

Learn to hack!

### How to get the actual belt? To get your belt, [send us an email](malto:pwn@pwn.college) from the email address associated with your pwn.college account once you’ve completed the necessary challenges. We’ll then get your belt over to you (eventually)! -**Who has earned belts?** We maintain a [list of hackers with pwn.college belts](https://static.pwn.college/belts). Once you achieve your belt, if you provide your name, email address, and emoji, we will add you to the [list](https://static.pwn.college/belts)! +**Who has earned belts?** We maintain a [list of hackers with pwn.college belts](https://pwn.college/belts). Once you achieve your belt, we will add you to the [list](https://pwn.college/belts)!

From dd283de35669a61d195b6f8a0d68ba856fcc5b71 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 16 Jan 2024 01:14:33 -0700 Subject: [PATCH 018/123] fetch belts just from the belt table, now that we have it all set up --- dojo_plugin/utils/belts.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/dojo_plugin/utils/belts.py b/dojo_plugin/utils/belts.py index 98f8922c0..63e44903c 100644 --- a/dojo_plugin/utils/belts.py +++ b/dojo_plugin/utils/belts.py @@ -5,7 +5,6 @@ from ..models import Dojos, Belts -CUMULATIVE_CUTOFF = datetime.datetime(2023, 10, 1) BELT_REQUIREMENTS = { "orange": "intro-to-cybersecurity", "yellow": "program-security", @@ -46,24 +45,13 @@ def get_belts(): result["dates"][color] = {} result["ranks"][color] = [] - belt_awards = [ (award.user, award.date) for award in Belts.query.filter_by(name=color) ] - awarded_ids = set(u.id for u,_ in belt_awards) - sorted_belts = sorted(belt_awards + dojo.completions(), key=lambda d: d[1]) - - for user,date in sorted_belts: - if ( - date > CUMULATIVE_CUTOFF and - user.id not in awarded_ids and - result["users"].get(user.id, {"rank_id":-1})["rank_id"] != n-1 - ): - continue - - result["dates"][color][user.id] = str(date) - result["users"][user.id] = { - "handle": user.name, - "site": user.website, + for belt in Belts.query.filter_by(name=color).order_by(Belts.date): + result["dates"][color][belt.user.id] = str(belt.date) + result["users"][belt.user.id] = { + "handle": belt.user.name, + "site": belt.user.website, "color": color, - "date": str(date), + "date": str(belt.date), "rank_id": n, } From 8c7943caa8da03fd476e2b0e58322235d664c469 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 16 Jan 2024 01:37:13 -0700 Subject: [PATCH 019/123] sort the belts list --- dojo_theme/templates/belts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo_theme/templates/belts.html b/dojo_theme/templates/belts.html index 1cb7e639e..2b72e2b41 100644 --- a/dojo_theme/templates/belts.html +++ b/dojo_theme/templates/belts.html @@ -43,7 +43,7 @@

{{belt_color.title()}} Belts