icoa-cli 2.19.42 → 2.19.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/exam.js +125 -25
- package/dist/commands/lang.js +34 -0
- package/package.json +1 -1
package/dist/commands/exam.js
CHANGED
|
@@ -226,12 +226,34 @@ function getHelpState(state) {
|
|
|
226
226
|
eliminated: state._eliminated || {},
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
|
+
// Track whether the practical section intro has been shown this session
|
|
230
|
+
let _practicalIntroShown = false;
|
|
229
231
|
function printQuestion(q, answer) {
|
|
230
232
|
const state = getExamState();
|
|
231
233
|
const total = Number(state?.session.questionCount || 30);
|
|
232
234
|
const answered = Object.keys(state?.answers || {}).length;
|
|
233
235
|
const help = getHelpState(state);
|
|
234
236
|
const eliminated = help.eliminated[q.number] || [];
|
|
237
|
+
const isPractical = q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
238
|
+
// Show practical section intro once when first entering Q31+
|
|
239
|
+
if (isPractical && !_practicalIntroShown && state?.session.examId !== 'demo-free') {
|
|
240
|
+
_practicalIntroShown = true;
|
|
241
|
+
console.log();
|
|
242
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
243
|
+
console.log(chalk.bold.white(' Practical Section — Python Required'));
|
|
244
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(chalk.white(' Three ways to run Python:'));
|
|
247
|
+
console.log();
|
|
248
|
+
console.log(chalk.yellow(' 1.') + chalk.white(' One-liner: ') + chalk.green('!python3 -c "print(1+1)"'));
|
|
249
|
+
console.log(chalk.yellow(' 2.') + chalk.white(' Interactive: ') + chalk.green('!python3') + chalk.gray(' → >>> import struct ...'));
|
|
250
|
+
console.log(chalk.yellow(' 3.') + chalk.white(' Script: ') + chalk.green("!cat << 'EOF' > s.py") + chalk.gray(' → ') + chalk.green('!python3 s.py'));
|
|
251
|
+
console.log();
|
|
252
|
+
console.log(chalk.white(' Need help? Type ') + chalk.bold.cyan('hint') + chalk.white(' to ask AI (25K tokens per section)'));
|
|
253
|
+
console.log(chalk.white(' Submit flag: ') + chalk.green('exam answer <n> ICOA{your_flag}'));
|
|
254
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
235
257
|
// Progress bar
|
|
236
258
|
printQuestionProgress(q.number, total, answered);
|
|
237
259
|
// Easter egg (position calculated by percentage of total)
|
|
@@ -242,22 +264,43 @@ function printQuestion(q, answer) {
|
|
|
242
264
|
console.log();
|
|
243
265
|
if (q.category)
|
|
244
266
|
console.log(chalk.cyan(` [${q.category}]`));
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
267
|
+
if (q.points && q.points > 2) {
|
|
268
|
+
console.log(chalk.cyan(` [${q.points} points]`));
|
|
269
|
+
}
|
|
270
|
+
if (isPractical) {
|
|
271
|
+
// Practical question display
|
|
272
|
+
const displayText = q.description || q.text;
|
|
273
|
+
console.log(chalk.bold.white(` Q${q.number}. `) + chalk.white(displayText.split('\n')[0]));
|
|
274
|
+
// Show remaining lines with proper indentation
|
|
275
|
+
const lines = displayText.split('\n').slice(1);
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
console.log(chalk.white(` ${line}`));
|
|
252
278
|
}
|
|
253
|
-
|
|
254
|
-
|
|
279
|
+
console.log();
|
|
280
|
+
if (answer) {
|
|
281
|
+
console.log(chalk.green(` Your answer: ${answer}`));
|
|
282
|
+
console.log();
|
|
255
283
|
}
|
|
256
|
-
|
|
257
|
-
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// MCQ display
|
|
287
|
+
console.log(chalk.bold.white(` Q${q.number}. `) + chalk.white(q.text));
|
|
288
|
+
console.log();
|
|
289
|
+
for (const key of ['A', 'B', 'C', 'D']) {
|
|
290
|
+
const isEliminated = eliminated.includes(key);
|
|
291
|
+
const selected = answer === key;
|
|
292
|
+
if (isEliminated) {
|
|
293
|
+
console.log(chalk.gray.strikethrough(` ${key}. ${q.options[key]}`) + chalk.red(` (${t('wrong')})`));
|
|
294
|
+
}
|
|
295
|
+
else if (selected) {
|
|
296
|
+
console.log(chalk.green.bold(` ▸ ${key}. ${q.options[key]}`));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
console.log(chalk.gray(` ${key}.`) + ' ' + chalk.white(q.options[key]));
|
|
300
|
+
}
|
|
258
301
|
}
|
|
302
|
+
console.log();
|
|
259
303
|
}
|
|
260
|
-
console.log();
|
|
261
304
|
// Q2 tutorial hint
|
|
262
305
|
const isDemo = state?.session.examId === 'demo-free';
|
|
263
306
|
const q2Helps = help.perQ[2] || 0;
|
|
@@ -265,14 +308,21 @@ function printQuestion(q, answer) {
|
|
|
265
308
|
console.log(chalk.yellow.bold(` ${t('qTutorial')}`));
|
|
266
309
|
console.log();
|
|
267
310
|
}
|
|
268
|
-
// Full menu
|
|
311
|
+
// Full menu
|
|
269
312
|
const remaining = help.max - help.used;
|
|
270
|
-
const helpLabel = remaining > 0
|
|
271
|
-
? chalk.gray(`${t('helpRemove')} `) + chalk.yellow(`(${remaining}/${help.max})`)
|
|
272
|
-
: (help.max < 8 ? chalk.gray(`${t('helpUsedUp')}`) : chalk.gray(`${t('helpAllUsed')} (${help.used}/${help.max})`));
|
|
273
313
|
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
274
|
-
|
|
275
|
-
|
|
314
|
+
if (isPractical) {
|
|
315
|
+
console.log(chalk.yellow(' exam answer <n> ICOA{...}') + chalk.gray(' submit flag'));
|
|
316
|
+
console.log(chalk.yellow(' hint') + chalk.gray(' ask AI for help'));
|
|
317
|
+
console.log(chalk.yellow(' !python3') + chalk.gray(' start Python'));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const helpLabel = remaining > 0
|
|
321
|
+
? chalk.gray(`${t('helpRemove')} `) + chalk.yellow(`(${remaining}/${help.max})`)
|
|
322
|
+
: (help.max < 8 ? chalk.gray(`${t('helpUsedUp')}`) : chalk.gray(`${t('helpAllUsed')} (${help.used}/${help.max})`));
|
|
323
|
+
console.log(chalk.yellow(' A/B/C/D') + chalk.gray(` ${t('answerThis')}`));
|
|
324
|
+
console.log(chalk.yellow(' help') + ' ' + helpLabel);
|
|
325
|
+
}
|
|
276
326
|
console.log(chalk.yellow(' next') + chalk.gray(' / ') + chalk.yellow('prev') + chalk.gray(` ${t('htpNav')}`));
|
|
277
327
|
console.log(chalk.yellow(` exam q 1..${total}`) + chalk.gray(` ${t('htpJump')}`));
|
|
278
328
|
console.log(chalk.yellow(' exam review') + chalk.gray(` ${t('htpReview')}`));
|
|
@@ -648,9 +698,10 @@ export function registerExamCommand(program) {
|
|
|
648
698
|
});
|
|
649
699
|
// ─── exam answer <n> <choice> ───
|
|
650
700
|
exam
|
|
651
|
-
.command('answer <n> <choice
|
|
652
|
-
.description('Answer question N
|
|
653
|
-
.action(async (n,
|
|
701
|
+
.command('answer <n> <choice...>')
|
|
702
|
+
.description('Answer question N (A/B/C/D for MCQ, ICOA{flag} for practical)')
|
|
703
|
+
.action(async (n, choiceParts) => {
|
|
704
|
+
const choice = choiceParts.join(' ');
|
|
654
705
|
logCommand(`exam answer ${n} ${choice}`);
|
|
655
706
|
const state = getExamState();
|
|
656
707
|
if (!state) {
|
|
@@ -665,10 +716,23 @@ export function registerExamCommand(program) {
|
|
|
665
716
|
printError(`Question ${n} not found (1-${state.questions.length}).`);
|
|
666
717
|
return;
|
|
667
718
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
719
|
+
// Detect question type: practical (ai4ctf/ctf4ai) or MCQ
|
|
720
|
+
const isPractical = q.type === 'ai4ctf' || q.type === 'ctf4ai' || (q.options && !q.options.A && !q.options.B);
|
|
721
|
+
let c;
|
|
722
|
+
if (isPractical) {
|
|
723
|
+
// Accept flag format: ICOA{...} or any string
|
|
724
|
+
c = choice.trim();
|
|
725
|
+
if (!c) {
|
|
726
|
+
printError('Please provide your flag: exam answer <n> ICOA{your_flag}');
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
c = choice.toUpperCase();
|
|
732
|
+
if (!['A', 'B', 'C', 'D'].includes(c)) {
|
|
733
|
+
printError('Choice must be A, B, C, or D.');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
672
736
|
}
|
|
673
737
|
// Q2 tutorial: must use help first before answering
|
|
674
738
|
if (state.session.examId === 'demo-free' && num === 2) {
|
|
@@ -1437,6 +1501,42 @@ export function registerExamCommand(program) {
|
|
|
1437
1501
|
else {
|
|
1438
1502
|
printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
|
|
1439
1503
|
}
|
|
1504
|
+
// Python usage tutorial for beginners
|
|
1505
|
+
console.log();
|
|
1506
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1507
|
+
console.log(chalk.bold.white(' How to use Python in ICOA CLI'));
|
|
1508
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1509
|
+
console.log();
|
|
1510
|
+
console.log(chalk.bold.yellow(' Method 1: One-liner') + chalk.gray(' (quick tasks)'));
|
|
1511
|
+
console.log(chalk.white(' Just add ! before the command:'));
|
|
1512
|
+
console.log(chalk.green(' !python3 -c "import base64; print(base64.b64decode(\'SGVsbG8=\'))"'));
|
|
1513
|
+
console.log(chalk.gray(' → b\'Hello\''));
|
|
1514
|
+
console.log();
|
|
1515
|
+
console.log(chalk.bold.yellow(' Method 2: Interactive Python') + chalk.gray(' (explore & experiment)'));
|
|
1516
|
+
console.log(chalk.white(' Start a Python session, type commands one by one:'));
|
|
1517
|
+
console.log(chalk.green(' !python3'));
|
|
1518
|
+
console.log(chalk.gray(' >>> from Crypto.Cipher import AES'));
|
|
1519
|
+
console.log(chalk.gray(' >>> key = bytes.fromhex("49434f41...")'));
|
|
1520
|
+
console.log(chalk.gray(' >>> cipher = AES.new(key, AES.MODE_CBC, iv)'));
|
|
1521
|
+
console.log(chalk.gray(' >>> print(cipher.decrypt(data))'));
|
|
1522
|
+
console.log(chalk.gray(' >>> exit()'));
|
|
1523
|
+
console.log();
|
|
1524
|
+
console.log(chalk.bold.yellow(' Method 3: Script file') + chalk.gray(' (complex solutions)'));
|
|
1525
|
+
console.log(chalk.white(' Write a .py file, then run it:'));
|
|
1526
|
+
console.log(chalk.green(" !cat << 'EOF' > solve.py"));
|
|
1527
|
+
console.log(chalk.gray(' from pwn import xor'));
|
|
1528
|
+
console.log(chalk.gray(' ct = bytes.fromhex("0a2b0e1c...")'));
|
|
1529
|
+
console.log(chalk.gray(' key = xor(ct[:5], b"ICOA{")'));
|
|
1530
|
+
console.log(chalk.gray(' print(xor(ct, key).decode())'));
|
|
1531
|
+
console.log(chalk.green(' EOF'));
|
|
1532
|
+
console.log(chalk.green(' !python3 solve.py'));
|
|
1533
|
+
console.log();
|
|
1534
|
+
console.log(chalk.bold.yellow(' AI Assistant'));
|
|
1535
|
+
console.log(chalk.white(' During the exam, type ') + chalk.bold.cyan('hint') + chalk.white(' to ask AI for help:'));
|
|
1536
|
+
console.log(chalk.gray(' "How do I decrypt AES-CBC in Python?"'));
|
|
1537
|
+
console.log(chalk.gray(' "Show me how to use struct.unpack"'));
|
|
1538
|
+
console.log(chalk.gray(' AI budget: 25K tokens for AI4CTF + 25K for CTF4AI'));
|
|
1539
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1440
1540
|
console.log();
|
|
1441
1541
|
console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
|
|
1442
1542
|
console.log();
|
package/dist/commands/lang.js
CHANGED
|
@@ -106,6 +106,40 @@ export function registerLangCommand(program) {
|
|
|
106
106
|
console.log(chalk.gray(' Language changed. Type: demo'));
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
else if (state && state.session.token) {
|
|
110
|
+
// Real exam with token: re-fetch questions in new language from server
|
|
111
|
+
try {
|
|
112
|
+
const { getConfig } = await import('../lib/config.js');
|
|
113
|
+
const { saveExamState } = await import('../lib/exam-state.js');
|
|
114
|
+
const { getDeviceFingerprint } = await import('../lib/access.js');
|
|
115
|
+
const config = getConfig();
|
|
116
|
+
const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
|
|
117
|
+
const token = state.session.token;
|
|
118
|
+
const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({ token, deviceHash: getDeviceFingerprint(), lang: code }),
|
|
122
|
+
signal: AbortSignal.timeout(10000),
|
|
123
|
+
});
|
|
124
|
+
if (res.ok) {
|
|
125
|
+
const json = await res.json();
|
|
126
|
+
const newQuestions = json.data.questions;
|
|
127
|
+
// Keep answers, interactions, aiUsage — only update question text
|
|
128
|
+
state.questions = newQuestions;
|
|
129
|
+
saveExamState(state);
|
|
130
|
+
const currentQ = state._lastQ || 1;
|
|
131
|
+
console.log();
|
|
132
|
+
printSuccess(`Exam questions updated to ${LANG_NAMES[code] || code}. Your answers are kept.`);
|
|
133
|
+
console.log(chalk.white(` Resume: exam q ${currentQ}`));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(chalk.yellow(' Could not reload questions in new language. Try again later.'));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
console.log(chalk.yellow(' Could not reach server. Language changed for UI only.'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
109
143
|
else if (state) {
|
|
110
144
|
const currentQ = state._lastQ || 1;
|
|
111
145
|
console.log();
|