From 300f07dc0830b3a0c281194e88d5aafbcd20966b Mon Sep 17 00:00:00 2001 From: Nicolas FAUGEROUX Date: Fri, 14 Jun 2024 07:26:30 +0200 Subject: [PATCH] WIP: feat: add admin panel for pre-provisionning fix: remove source name impr: add progressbar fix: display name even if challenge is hidden chore: rename function in api.py to explicit path fix(ci): handle progressbar in challenge creation --- .github/workflows/ci.yml | 13 ++- __init__.py | 31 +++++--- api.py | 41 ++++++++++ assets/create.js | 18 +++++ assets/instances.js | 106 ++++++++++++++++++++++++- cypress.config.js | 1 + cypress/support/commands.js | 7 +- templates/chall_manager_config.html | 3 + templates/chall_manager_instances.html | 7 +- templates/chall_manager_mana.html | 18 +---- templates/chall_manager_panel.html | 79 ++++++++++++++++++ 11 files changed, 290 insertions(+), 34 deletions(-) create mode 100644 templates/chall_manager_panel.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c86e4b9..35f4118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,4 +107,15 @@ jobs: CYPRESS_CTFD_URL: "http://localhost:8000" CYPRESS_SCENARIO_PATH: "${{ github.workspace }}/demo-deploy.zip" CYPRESS_PLUGIN_SETTINGS_CM_API_URL: "http://chall-manager:9090/api/v1" - CYPRESS_PLUGIN_SETTINGS_CM_MANA_TOTAL: "50" \ No newline at end of file + CYPRESS_PLUGIN_SETTINGS_CM_MANA_TOTAL: "50" + + - uses: actions/upload-artifact@v1 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + - uses: actions/upload-artifact@v1 + if: always() + with: + name: cypress-videos + path: cypress/videos \ No newline at end of file diff --git a/__init__.py b/__init__.py index b158b38..2846bde 100644 --- a/__init__.py +++ b/__init__.py @@ -60,14 +60,14 @@ def load(app): # Route to configure Chall-manager plugins @page_blueprint.route('/admin/settings') @admins_only - def admin_list_configs(): + def admin_settings(): return render_template("chall_manager_config.html") # Route to monitor & manage running instances @page_blueprint.route('/admin/instances') @admins_only - def admin_list_challenges(): + def admin_instances(): cm_api_url = get_config("chall-manager:chall-manager_api_url") url = f"{cm_api_url}/challenge" @@ -92,12 +92,7 @@ def admin_list_challenges(): user_mode = get_config("user_mode") for i in instances: - if user_mode == "users": - i["sourceName"] = get_user_attrs(i["sourceId"]).name - if user_mode == "teams": - i["sourceName"] = get_team_attrs(i["sourceId"]).name - - i["challengeName"] = get_all_challenges(id=i["challengeId"])[0].name + i["challengeName"] = get_all_challenges(admin=True, id=i["challengeId"])[0].name return render_template("chall_manager_instances.html", instances=instances, @@ -106,15 +101,15 @@ def admin_list_challenges(): # Route to monitor & manage running instances @page_blueprint.route('/admin/mana') @admins_only - def admin_list_mana(): + def admin_mana(): cm_mana_total = get_config("chall-manager:chall-manager_mana_total") user_mode = get_config("user_mode") if user_mode == "users": - query_sql = """select id,name,mana from users;""" + query_sql = """select id,mana from users;""" elif user_mode == "teams": - query_sql = """select id,name,mana from teams;""" + query_sql = """select id,mana from teams;""" data = db.session.execute(text(query_sql)).fetchall() @@ -123,8 +118,7 @@ def admin_list_mana(): "data": [ { "id": item[0], - "name": item[1], - "mana": str(item[2]) # Convert the mana value to string + "mana": str(item[1]) # Convert the mana value to string } for item in data ] @@ -134,5 +128,16 @@ def admin_list_mana(): user_mode=user_mode, sources=sources["data"]) + # Route to monitor & manage running instances + @page_blueprint.route('/admin/panel') + @admins_only + def admin_panel(): + # retrieve custom challenges + challenges = db.session.execute(text("""select id, name from challenges c where c.type="dynamic_iac";""")) + + print(f" challengee = {challenges}") + + return render_template("chall_manager_panel.html", + challenges=challenges) app.register_blueprint(page_blueprint) diff --git a/api.py b/api.py index f835257..2104ea6 100644 --- a/api.py +++ b/api.py @@ -63,6 +63,47 @@ def get(): 'message': json.loads(r.text), }} + + @staticmethod + @admins_only + def post(): + # retrieve all instance deployed by chall-manager + cm_api_url = get_config("chall-manager:chall-manager_api_url") + + ## mandatory + challengeId = request.args.get("challengeId") + sourceId = request.args.get("sourceId") + + payload = {} + + if not challengeId or not sourceId: + return {'success': False, 'data':{ + 'message': "Missing argument : challengeId or sourceId", + }} + + # TODO check user inputs + + url = f"{cm_api_url}/instance" + + payload['sourceId'] = sourceId + payload['challengeId'] = challengeId + + headers = { + "Content-type": "application/json" + } + + try: + r = requests.post(url, data = json.dumps(payload), headers=headers) + except requests.exceptions.RequestException as e : + return {'success': False, 'data':{ + 'message': f"An error occured while Plugins communication with Challmanager API : {e}", + }} + + + return {'success': True, 'data': { + 'message': json.loads(r.text), + }} + @staticmethod @admins_only def patch(): diff --git a/assets/create.js b/assets/create.js index d97e277..a4e3398 100644 --- a/assets/create.js +++ b/assets/create.js @@ -66,10 +66,21 @@ $('#challenge-create-options #challenge_id').on('DOMSubtreeModified', function() const input = document.getElementById('scenario'); const file = input.files[0]; // Get the first file selected + // define progress bar + var pg = CTFd.ui.ezq.ezProgressBar({ + width: 0, + title: "Sending scenario to chall-manager", + }); + if (file) { sendFile(file).then(function(response) { console.log(response) params['scenarioId'] = response.data[0].id + + pg = CTFd.ui.ezq.ezProgressBar({ + target: pg, + width: 30, + }); // Step 2: Send the scenario file location to plugin that will create it on Chall-manager API console.log(params) @@ -85,10 +96,17 @@ $('#challenge-create-options #challenge_id').on('DOMSubtreeModified', function() }).then(function (a) { return a.json(); }).then(function (json) { + pg = CTFd.ui.ezq.ezProgressBar({ + target: pg, + width: 100, + }); console.log(json) if (json.success){ console.log(json.success) console.log(json.data.message.toString()) + setTimeout(function () { + pg.modal("hide"); + }, 500); CTFd.ui.ezq.ezToast({ title: "Success", body: "Scenario is upload on Chall-manager, hash : " + json.data.message.hash diff --git a/assets/instances.js b/assets/instances.js index 3edaad2..7f7ef20 100644 --- a/assets/instances.js +++ b/assets/instances.js @@ -26,6 +26,38 @@ async function renew_instance(challengeId, sourceId) { return response; } +async function create_instance(challengeId, sourceId) { + let response = await CTFd.fetch("/api/v1/plugins/ctfd-chall-manager/admin/instance?challengeId=" + challengeId + "&sourceId=" + sourceId, { + method: "POST", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json" + } + }); + console.log(response) + response = await response.json(); + return response; +} + +function parseRange(input) { + var sourceIds = [] + const pattern = /\d+-\d+/ + elem = input.split(',') + + for (let i=0; i a - b); +} $(".delete-instance").click(function (e) { e.preventDefault(); @@ -81,11 +113,25 @@ $('#instances-delete-button').click(function (e) { title: "Delete Containers", body: `Are you sure you want to delete the selected ${sourceId.length} instance(s)?`, success: async function () { + var pg = CTFd.ui.ezq.ezProgressBar({ + width: 0, + title: "Deleting progress", + }); + for (let i=0; i< sourceId.length; i++){ console.log(challengeIds[i], sourceIds[i]) await delete_instance(challengeIds[i], sourceIds[i]) + + var width = ( (i+1) / sourceIds.length) * 100; + console.log(width) + pg = CTFd.ui.ezq.ezProgressBar({ + target: pg, + width: width, + }); } - //await Promise.all(users.toArray().map((user) => delete_container(user))); + setTimeout(function () { + pg.modal("hide"); + }, 500); location.reload(); } }); @@ -107,13 +153,69 @@ $('#instances-renew-button').click(function (e) { title: "Renew Containers", body: `Are you sure you want to renew the selected ${sourceId.length} instance(s)?`, success: async function () { + var pg = CTFd.ui.ezq.ezProgressBar({ + width: 0, + title: "Renewall progress", + }); for (let i=0; i< sourceId.length; i++){ console.log(challengeIds[i], sourceIds[i]) await renew_instance(challengeIds[i], sourceIds[i]) + + var width = ( (i+1) / sourceIds.length) * 100; + console.log(width) + pg = CTFd.ui.ezq.ezProgressBar({ + target: pg, + width: width, + }); } - //await Promise.all(users.toArray().map((user) => delete_container(user))); + setTimeout(function () { + pg.modal("hide"); + }, 500); location.reload(); } }); }); +$('#instances-create-button').click(function (e) { + // let sourceId = $("input[data-source-id]:checked").map(function () { + // return $(this).data("source-id"); + // }); + + let sourceIds = parseRange(document.getElementById("sourceIds-expression-input").value) + + let challengeId = $("input[data-challenge-id]:checked").map(function () { + return $(this).data("challenge-id"); + }); + + let challengeIds = challengeId.toArray() + //let sourceIds = sourceId.toArray() + + CTFd.ui.ezq.ezQuery({ + title: "Create instances", + body: `Are you sure you want to create the selected ${sourceIds.length * challengeIds.length} instance(s)?`, + success: async function () { + var pg = CTFd.ui.ezq.ezProgressBar({ + width: 0, + title: "Creation progress", + }); + for (let i=0; i< sourceIds.length; i++){ + for (let j=0; j< challengeId.length; j++){ + console.log(challengeIds[j], sourceIds[i]) + await create_instance(challengeIds[j], sourceIds[i]) + + var width = ((j+1) * (i+1) / (sourceIds.length * challengeIds.length)) * 100; + console.log(width) + pg = CTFd.ui.ezq.ezProgressBar({ + target: pg, + width: width, + }); + } + } + setTimeout(function () { + pg.modal("hide"); + }, 500); + // location.reload(); + } + }); +}); + diff --git a/cypress.config.js b/cypress.config.js index 97f47c4..25b28e0 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -6,4 +6,5 @@ module.exports = defineConfig({ // implement node event listeners here }, }, + defaultCommandTimeout: 30000 }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index d3d0682..8c34d4c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -56,7 +56,12 @@ Cypress.Commands.add('create_challenge', (label, mana, updateStrategy, mode, mod // Create cy.get(".create-challenge-submit").contains("Create").click() - cy.wait(7500) + + // Wait that pop-up of Uploading disapears + cy.wait(500) // wait the pop-up is created + cy.get('[class="modal-title"]', { timeout: 15000 }) + .contains('Sending scenario to chall-manager') + .should("not.be.visible"); // Final options cy.get("[name=\"flag\"]").type(label) diff --git a/templates/chall_manager_config.html b/templates/chall_manager_config.html index 5f3f722..d0835a6 100644 --- a/templates/chall_manager_config.html +++ b/templates/chall_manager_config.html @@ -10,6 +10,9 @@ + {% endblock %} {% block panel %} diff --git a/templates/chall_manager_instances.html b/templates/chall_manager_instances.html index 61bf709..423b5c2 100644 --- a/templates/chall_manager_instances.html +++ b/templates/chall_manager_instances.html @@ -10,6 +10,9 @@ + {% endblock %} {% block panel %} @@ -66,11 +69,11 @@ {% if user_mode == "teams" %} - {{ instance.sourceName }} + {{ instance.sourceId }} {% else %} - {{ instance.sourceName }} + {{ instance.sourceId }} {% endif %} diff --git a/templates/chall_manager_mana.html b/templates/chall_manager_mana.html index eeb3faf..8376af7 100644 --- a/templates/chall_manager_mana.html +++ b/templates/chall_manager_mana.html @@ -10,6 +10,9 @@ + {% endblock %} {% block panel %} @@ -25,7 +28,6 @@ ID - Name Mana Used @@ -54,20 +56,6 @@ - -
- {% if user_mode == "teams" %} - - {{ source.name }} - - {% else %} - - {{ source.name }} - - {% endif %} -
- - {{ source.mana }} diff --git a/templates/chall_manager_panel.html b/templates/chall_manager_panel.html new file mode 100644 index 0000000..8b6eab9 --- /dev/null +++ b/templates/chall_manager_panel.html @@ -0,0 +1,79 @@ +{% extends "chall_manager_base.html" %} + +{% block menu %} + + + + +{% endblock %} + +{% block panel %} + + + + +
+ Source Id pattern : + + + Available IaC Challenges +
+ + + + + + + + + {% for challenge in challenges %} + + + + + {% endfor %} + + +
+
  + +
+
ID +
+
  + +
+
+ +
+
+
+ + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file