diff --git a/front/src/components/Dialogs/TaskImportDialog.vue b/front/src/components/Dialogs/TaskImportDialog.vue index 6242abfbd..3dcf21c72 100644 --- a/front/src/components/Dialogs/TaskImportDialog.vue +++ b/front/src/components/Dialogs/TaskImportDialog.vue @@ -49,6 +49,7 @@ color="positive" :disable="btnDisable" :label="btnLabel" + :loading="loading" @click="btnClick" /> @@ -60,15 +61,9 @@ import { useDialogPluginComponent } from 'quasar'; import ctfnote from 'src/ctfnote'; import { Ctf } from 'src/ctfnote/models'; -import parsers from 'src/ctfnote/parsers'; +import parsers, { ParsedTask } from 'src/ctfnote/parsers'; import { defineComponent, ref } from 'vue'; -type ParsedTask = { - title: string; - category: string; - keep: boolean; -}; - export default defineComponent({ props: { ctf: { type: Object as () => Ctf, required: true }, @@ -96,6 +91,7 @@ export default defineComponent({ onDialogHide, onDialogOK, onDialogCancel, + loading: ref(false), }; }, computed: { @@ -151,19 +147,19 @@ export default defineComponent({ return { ...task, keep: !taskSet.has(hash) }; }); }, - btnClick() { + async btnClick() { if (this.tab == 'parse') { this.parsedTasks = this.parseTasks(this.model); this.tab = 'confirm'; } else { - this.parsedTasks + this.loading = true; + const result = this.parsedTasks .filter((t) => t.keep) .map((task) => { - void this.createTask(this.ctf.id, { - title: task.title, - category: task.category, - }); + return this.createTask(this.ctf.id, task); }); + await Promise.all(result); + this.loading = false; this.onDialogOK(); } }, diff --git a/front/src/ctfnote/parsers/htb.ts b/front/src/ctfnote/parsers/htb.ts new file mode 100644 index 000000000..ca81b69a8 --- /dev/null +++ b/front/src/ctfnote/parsers/htb.ts @@ -0,0 +1,79 @@ +import { ParsedTask, Parser } from '.'; +import { parseJson, parseJsonStrict } from '../utils'; + +// output of https://ctf.hackthebox.com/api/public/challengeCategories +const challengeCategories: { [index: number]: string } = { + 1: 'Fullpwn', + 2: 'Web', + 3: 'Pwn', + 4: 'Crypto', + 5: 'Reversing', + 6: 'Stego', + 7: 'Forensics', + 8: 'Misc', + 9: 'Start', + 10: 'PCAP', + 11: 'Coding', + 12: 'Mobile', + 13: 'OSINT', + 14: 'Blockchain', + 15: 'Hardware', + 16: 'Warmup', + 17: 'Attack', + 18: 'Defence', + 20: 'Cloud', + 21: 'Scada', +}; + +const HTBParser: Parser = { + name: 'HTB parser', + hint: 'paste https://ctf.hackthebox.com/api/ctf/ from the network tab', + + parse(s: string): ParsedTask[] { + const tasks = []; + const data = parseJsonStrict<{ + challenges: Array<{ + id: number; + name: string; + description: string; + challenge_category_id: number; + }>; + }>(s); + if (data == null) { + return []; + } + + for (const challenge of data.challenges) { + if ( + !challenge.description || + !challenge.name || + !challenge.challenge_category_id + ) { + continue; + } + + let category = challengeCategories[challenge.challenge_category_id]; + if (category == null) category = 'Unknown'; + + tasks.push({ + title: challenge.name, + category: category, + description: challenge.description, + }); + } + return tasks; + }, + isValid(s) { + const data = parseJson<{ + challenges: Array<{ + id: number; + name: string; + description: string; + challenge_category_id: number; + }>; + }>(s); + return Array.isArray(data?.challenges); + }, +}; + +export default HTBParser; diff --git a/front/src/ctfnote/parsers/index.ts b/front/src/ctfnote/parsers/index.ts index cf9181058..35bd25a38 100644 --- a/front/src/ctfnote/parsers/index.ts +++ b/front/src/ctfnote/parsers/index.ts @@ -1,10 +1,13 @@ import CTFDParser from './ctfd'; import ECSCParser from './ecsc'; import RawParser from './raw'; +import HTBParser from './htb'; export type ParsedTask = { title: string; category: string; + description?: string; + keep?: boolean; }; export type Parser = { @@ -14,4 +17,4 @@ export type Parser = { parse(s: string): ParsedTask[]; }; -export default [RawParser, CTFDParser, ECSCParser]; +export default [RawParser, CTFDParser, ECSCParser, HTBParser];