def test_can_delete_dynamic_challenge(): app = create_ctfd(enable_plugins=True) with app.app_context(): register_user(app) client = login_as_user(app, name="admin", password="******") challenge_data = { "name": "name", "category": "category", "description": "description", "value": 100, "decay": 20, "minimum": 1, "state": "hidden", "type": "dynamic", } r = client.post("/api/v1/challenges", json=challenge_data) assert r.get_json().get("data")["id"] == 1 challenges = DynamicChallenge.query.all() assert len(challenges) == 1 challenge = challenges[0] DynamicValueChallenge.delete(challenge) challenges = DynamicChallenge.query.all() assert len(challenges) == 0 destroy_ctfd(app)
def test_dynamic_challenge_doesnt_lose_value_on_update(): """Dynamic challenge updates without changing any values or solves shouldn't change the current value. See #1043""" app = create_ctfd(enable_plugins=True) with app.app_context(): challenge_data = { "name": "name", "category": "category", "description": "description", "value": 10000, "decay": 4, "minimum": 10, "state": "visible", "type": "dynamic", } req = FakeRequest(form=challenge_data) challenge = DynamicValueChallenge.create(req) challenge_id = challenge.id gen_flag(app.db, challenge_id=challenge.id, content="flag") register_user(app) with login_as_user(app) as client: data = {"submission": "flag", "challenge_id": challenge_id} r = client.post("/api/v1/challenges/attempt", json=data) assert r.status_code == 200 assert r.get_json()["data"]["status"] == "correct" chal = Challenges.query.filter_by(id=challenge_id).first() prev_chal_value = chal.value chal = DynamicValueChallenge.update(chal, req) assert prev_chal_value == chal.value destroy_ctfd(app)
def solve(cls, user, team, challenge, request): super().solve(user, team, challenge, request) if challenge.dynamic_score == 1: DynamicValueChallenge.calculate_value(challenge) db.session.commit()
def test_can_add_requirement_dynamic_challenge(): """Test that requirements can be added to dynamic challenges""" app = create_ctfd(enable_plugins=True) with app.app_context(): challenge_data = { "name": "name", "category": "category", "description": "description", "value": 100, "decay": 20, "minimum": 1, "state": "hidden", "type": "dynamic" } req = FakeRequest(form=challenge_data) challenge = DynamicValueChallenge.create(req) assert challenge.value == 100 assert challenge.initial == 100 assert challenge.decay == 20 assert challenge.minimum == 1 challenge_data = { "name": "second_name", "category": "category", "description": "new_description", "value": "200", "initial": "200", "decay": "40", "minimum": "5", "max_attempts": "0", "state": "visible" } req = FakeRequest(form=challenge_data) challenge = DynamicValueChallenge.create(req) assert challenge.name == 'second_name' assert challenge.description == "new_description" assert challenge.value == 200 assert challenge.initial == 200 assert challenge.decay == 40 assert challenge.minimum == 5 assert challenge.state == "visible" challenge_data = { "requirements": [1] } req = FakeRequest(form=challenge_data) challenge = DynamicValueChallenge.update(challenge, req) assert challenge.requirements == [1] destroy_ctfd(app)
def solve(cls, user, team, challenge, request): """ This method is used to insert Solves into the database in order to mark a challenge as solved. :param team: The Team object from the database :param chal: The Challenge object from the database :param request: The request the user submitted :return: """ super().solve(user, team, challenge, request) if challenge.dynamic_score == 1: DynamicValueChallenge.calculate_value(challenge) db.session.commit() db.session.close()
def test_can_update_dynamic_challenge(): app = create_ctfd(enable_plugins=True) with app.app_context(): challenge_data = { "name": "name", "category": "category", "description": "description", "value": 100, "decay": 20, "minimum": 1, "state": "hidden", "type": "dynamic", } req = FakeRequest(form=challenge_data) challenge = DynamicValueChallenge.create(req) assert challenge.value == 100 assert challenge.initial == 100 assert challenge.decay == 20 assert challenge.minimum == 1 challenge_data = { "name": "new_name", "category": "category", "description": "new_description", "value": "200", "initial": "200", "decay": "40", "minimum": "5", "max_attempts": "0", "state": "visible", } req = FakeRequest(form=challenge_data) challenge = DynamicValueChallenge.update(challenge, req) assert challenge.name == "new_name" assert challenge.description == "new_description" assert challenge.value == 200 assert challenge.initial == 200 assert challenge.decay == 40 assert challenge.minimum == 5 assert challenge.state == "visible" destroy_ctfd(app)
def update(cls, challenge, request): data = request.form or request.get_json() for attr, value in data.items(): # We need to set these to floats so that the next operations don't operate on strings if attr in ("initial", "minimum", "decay"): value = float(value) setattr(challenge, attr, value) if challenge.dynamic_score == 1: return DynamicValueChallenge.calculate_value(challenge) db.session.commit() return challenge
def update(cls, challenge, request): """ This method is used to update the information associated with a challenge. This should be kept strictly to the Challenges table and any child tables. :param challenge: :param request: :return: """ data = request.form or request.get_json() for attr, value in data.items(): # We need to set these to floats so that the next operations don't operate on strings if attr in ("initial", "minimum", "decay"): value = float(value) setattr(challenge, attr, value) if challenge.dynamic_score == 1: return DynamicValueChallenge.calculate_value(challenge) db.session.commit() return challenge
def import_challenges(in_file, dst_attachments, move=False): from CTFd.models import db, Challenges, Flags, Tags, ChallengeFiles, Hints from CTFd.utils import uploads from CTFd.plugins.dynamic_challenges import DynamicChallenge, DynamicValueChallenge with open(in_file, "r") as in_stream: data = list(yaml.safe_load_all(in_stream)) if len(data) == 0 or data[0] is None or "challs" not in data[ 0] or data[0]["challs"] is None: raise ValueError("Invalid YAML format. Missing field 'challs'.") for chal in data[0]["challs"]: for req_field in REQ_FIELDS: if req_field not in chal: raise ValueError( "Invalid YAML format. Missing field '{0}'.".format( req_field)) if chal.get("type", "standard") == "dynamic": for req_field in ["minimum", "decay"]: if req_field not in chal: raise ValueError( "Invalid YAML format. Missing field '{0}'.".format( req_field)) if chal["flags"] is None: raise ValueError("Invalid YAML format. Missing field 'flag'.") for flag in chal["flags"]: if "flag" not in flag: raise ValueError( "Invalid YAML format. Missing field 'flag'.") flag["flag"] = flag["flag"].strip() if "type" not in flag: flag["type"] = "static" matching_chal = Challenges.query.filter_by( name=chal["name"].strip()).first() if matching_chal: print(("Updating {}: Duplicate challenge " "found in DB (id: {})").format( chal["name"].encode("utf8"), matching_chal.id)) Tags.query.filter_by(challenge_id=matching_chal.id).delete() ChallengeFiles.query.filter_by( challenge_id=matching_chal.id).delete() Flags.query.filter_by(challenge_id=matching_chal.id).delete() Hints.query.filter_by(challenge_id=matching_chal.id).delete() matching_chal.name = chal["name"].strip() matching_chal.description = chal["description"].strip() matching_chal.category = chal["category"].strip() if chal.get("type", "standard") == "standard": matching_chal.value = chal["value"] if chal.get("type", "standard") == "dynamic": matching_chal.minimum = chal["minimum"] matching_chal.decay = chal["decay"] matching_chal.initial = chal["value"] DynamicValueChallenge.calculate_value(matching_chal) db.session.commit() chal_dbobj = matching_chal else: print("Adding {}".format(chal["name"].encode("utf8"))) chal_type = chal.get("type", "standard") if chal_type == "standard": # We ignore traling and leading whitespace when # importing challenges chal_dbobj = Challenges( name=chal["name"].strip(), description=chal["description"].strip(), value=chal["value"], category=chal["category"].strip(), ) elif chal_type == "dynamic": # We ignore traling and leading whitespace when # importing challenges chal_dbobj = DynamicChallenge( name=chal["name"].strip(), description=chal["description"].strip(), category=chal["category"].strip(), value=int(chal["value"]), minimum=int(chal["minimum"]), decay=int(chal["decay"]), ) else: raise ValueError("Unknown type of challenge") db.session.add(chal_dbobj) db.session.commit() for tag in chal.get("tags", []): tag_dbobj = Tags(challenge_id=chal_dbobj.id, value=tag) db.session.add(tag_dbobj) for flag in chal["flags"]: flag_db = Flags(challenge_id=chal_dbobj.id, content=flag["flag"], type=flag["type"]) db.session.add(flag_db) for hint in chal.get("hints", []): hint_dbobj = Hints(challenge_id=chal_dbobj.id, content=hint["content"], cost=hint["cost"]) db.session.add(hint_dbobj) chal_dbobj.state = "hidden" if ( "hidden" in chal and chal["hidden"] == True) else "visible" chal_dbobj.max_attempts = chal[ "max_attempts"] if "max_attempts" in chal else 0 if "files" in chal: from io import FileIO for filename in chal["files"]: try: # upload_file takes a werkzeug.FileStorage object, but we # can get close enough with a file object with a # filename property added filepath = os.path.join(os.path.dirname(in_file), filename) with FileIO(filepath, mode="rb") as f: f.filename = os.path.basename(f.name) uploads.upload_file(file=f, challenge=chal_dbobj.id, type="challenge") except FileNotFoundError: raise ValueError( "Unable to import challenges. Missing file: " + filename) db.session.commit() # update challenge prerequisites after all the challenges were created with open(in_file, "r") as in_stream: data = list(yaml.safe_load_all(in_stream)) for chal in data[0]["challs"]: chal_dbobj = Challenges.query.filter_by( name=chal["name"].strip()).first() prerequisites = set() for prerequisite in chal.get("prerequisites", []): prerequisites.update([ c.id for c in Challenges.query.filter_by( name=prerequisite).all() ]) chal_dbobj.requirements = {"prerequisites": list(prerequisites)} db.session.commit() db.session.close()