Skip to content

Commit

Permalink
WIP: feat: add admin panel for pre-provisionning
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NicoFgrx committed Jun 16, 2024
1 parent 5637eb1 commit 300f07d
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 34 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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
31 changes: 18 additions & 13 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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,
Expand All @@ -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()

Expand All @@ -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
]
Expand All @@ -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)
41 changes: 41 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
18 changes: 18 additions & 0 deletions assets/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
106 changes: 104 additions & 2 deletions assets/instances.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<elem.length;i++){
if (pattern.test(elem[i])){
let low = Number(elem[i].split('-')[0])
let high = Number(elem[i].split('-')[1])
for (let j=low; j<=high;j++){
sourceIds.push(Number(j))
}
} else {
sourceIds.push(Number(elem[i]))
}
}
return sourceIds.sort((a, b) => a - b);
}

$(".delete-instance").click(function (e) {
e.preventDefault();
Expand Down Expand Up @@ -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();
}
});
Expand All @@ -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();
}
});
});

1 change: 1 addition & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ module.exports = defineConfig({
// implement node event listeners here
},
},
defaultCommandTimeout: 30000
});
7 changes: 6 additions & 1 deletion cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions templates/chall_manager_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<li class="nav-item">
<a class="nav-link" href="/plugins/ctfd-chall-manager/admin/mana">🔗 Mana</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/plugins/ctfd-chall-manager/admin/panel">🔗 Panel</a>
</li>
{% endblock %}

{% block panel %}
Expand Down
7 changes: 5 additions & 2 deletions templates/chall_manager_instances.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<li class="nav-item">
<a class="nav-link" href="/plugins/ctfd-chall-manager/admin/mana">🔗 Mana</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/plugins/ctfd-chall-manager/admin/panel">🔗 Panel</a>
</li>
{% endblock %}

{% block panel %}
Expand Down Expand Up @@ -66,11 +69,11 @@
<td class="text-center">
{% if user_mode == "teams" %}
<a href="{{ url_for('admin.teams_detail', team_id=instance.sourceId) }}">
{{ instance.sourceName }}
{{ instance.sourceId }}
</a>
{% else %}
<a href="{{ url_for('admin.users_detail', user_id=instance.sourceId) }}">
{{ instance.sourceName }}
{{ instance.sourceId }}
</a>
{% endif %}
</td>
Expand Down
Loading

0 comments on commit 300f07d

Please sign in to comment.