icoa-cli 2.19.43 → 2.19.45
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 +132 -37
- package/dist/lib/exam-state.d.ts +3 -1
- package/dist/lib/exam-state.js +56 -5
- 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) {
|
|
@@ -1066,7 +1130,8 @@ export function registerExamCommand(program) {
|
|
|
1066
1130
|
.description('Enter exam with access token (no login needed)')
|
|
1067
1131
|
.action(async (code) => {
|
|
1068
1132
|
logCommand(`exam token ${code}`);
|
|
1069
|
-
const
|
|
1133
|
+
const { getRealExamState } = await import('../lib/exam-state.js');
|
|
1134
|
+
const existing = getRealExamState();
|
|
1070
1135
|
if (existing) {
|
|
1071
1136
|
printWarning(`Exam "${existing.session.examName}" is already in progress.`);
|
|
1072
1137
|
printInfo('Use "exam review" or "exam submit" first.');
|
|
@@ -1191,17 +1256,11 @@ export function registerExamCommand(program) {
|
|
|
1191
1256
|
}
|
|
1192
1257
|
const DEMO_QUESTIONS = pickDemoQuestions(DEMO_PICK_SIZE);
|
|
1193
1258
|
const DEMO_SESSION = getLocalizedDemoSession();
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
else {
|
|
1201
|
-
printWarning(`Exam "${existing.session.examName}" is in progress.`);
|
|
1202
|
-
printInfo('Submit it first: exam submit');
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1259
|
+
// Demo uses separate state file — doesn't conflict with real exam
|
|
1260
|
+
const { getDemoState, clearExamState: clearState } = await import('../lib/exam-state.js');
|
|
1261
|
+
const existingDemo = getDemoState();
|
|
1262
|
+
if (existingDemo) {
|
|
1263
|
+
clearState('demo-free');
|
|
1205
1264
|
}
|
|
1206
1265
|
console.log();
|
|
1207
1266
|
printHeader('ICOA Demo Exam — Free Practice');
|
|
@@ -1437,6 +1496,42 @@ export function registerExamCommand(program) {
|
|
|
1437
1496
|
else {
|
|
1438
1497
|
printWarning(`${installed.length}/${PACKAGES.length} packages installed. ${failed.length} failed: ${failed.join(', ')}`);
|
|
1439
1498
|
}
|
|
1499
|
+
// Python usage tutorial for beginners
|
|
1500
|
+
console.log();
|
|
1501
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1502
|
+
console.log(chalk.bold.white(' How to use Python in ICOA CLI'));
|
|
1503
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1504
|
+
console.log();
|
|
1505
|
+
console.log(chalk.bold.yellow(' Method 1: One-liner') + chalk.gray(' (quick tasks)'));
|
|
1506
|
+
console.log(chalk.white(' Just add ! before the command:'));
|
|
1507
|
+
console.log(chalk.green(' !python3 -c "import base64; print(base64.b64decode(\'SGVsbG8=\'))"'));
|
|
1508
|
+
console.log(chalk.gray(' → b\'Hello\''));
|
|
1509
|
+
console.log();
|
|
1510
|
+
console.log(chalk.bold.yellow(' Method 2: Interactive Python') + chalk.gray(' (explore & experiment)'));
|
|
1511
|
+
console.log(chalk.white(' Start a Python session, type commands one by one:'));
|
|
1512
|
+
console.log(chalk.green(' !python3'));
|
|
1513
|
+
console.log(chalk.gray(' >>> from Crypto.Cipher import AES'));
|
|
1514
|
+
console.log(chalk.gray(' >>> key = bytes.fromhex("49434f41...")'));
|
|
1515
|
+
console.log(chalk.gray(' >>> cipher = AES.new(key, AES.MODE_CBC, iv)'));
|
|
1516
|
+
console.log(chalk.gray(' >>> print(cipher.decrypt(data))'));
|
|
1517
|
+
console.log(chalk.gray(' >>> exit()'));
|
|
1518
|
+
console.log();
|
|
1519
|
+
console.log(chalk.bold.yellow(' Method 3: Script file') + chalk.gray(' (complex solutions)'));
|
|
1520
|
+
console.log(chalk.white(' Write a .py file, then run it:'));
|
|
1521
|
+
console.log(chalk.green(" !cat << 'EOF' > solve.py"));
|
|
1522
|
+
console.log(chalk.gray(' from pwn import xor'));
|
|
1523
|
+
console.log(chalk.gray(' ct = bytes.fromhex("0a2b0e1c...")'));
|
|
1524
|
+
console.log(chalk.gray(' key = xor(ct[:5], b"ICOA{")'));
|
|
1525
|
+
console.log(chalk.gray(' print(xor(ct, key).decode())'));
|
|
1526
|
+
console.log(chalk.green(' EOF'));
|
|
1527
|
+
console.log(chalk.green(' !python3 solve.py'));
|
|
1528
|
+
console.log();
|
|
1529
|
+
console.log(chalk.bold.yellow(' AI Assistant'));
|
|
1530
|
+
console.log(chalk.white(' During the exam, type ') + chalk.bold.cyan('hint') + chalk.white(' to ask AI for help:'));
|
|
1531
|
+
console.log(chalk.gray(' "How do I decrypt AES-CBC in Python?"'));
|
|
1532
|
+
console.log(chalk.gray(' "Show me how to use struct.unpack"'));
|
|
1533
|
+
console.log(chalk.gray(' AI budget: 25K tokens for AI4CTF + 25K for CTF4AI'));
|
|
1534
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
1440
1535
|
console.log();
|
|
1441
1536
|
console.log(chalk.white(' Next step: ') + chalk.bold.cyan('exam <token>'));
|
|
1442
1537
|
console.log();
|
package/dist/lib/exam-state.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExamState } from '../types/index.js';
|
|
2
2
|
export declare function getExamState(): ExamState | null;
|
|
3
|
+
export declare function getDemoState(): ExamState | null;
|
|
4
|
+
export declare function getRealExamState(): ExamState | null;
|
|
3
5
|
export declare function saveExamState(state: ExamState): void;
|
|
4
|
-
export declare function clearExamState(): void;
|
|
6
|
+
export declare function clearExamState(examId?: string): void;
|
|
5
7
|
export declare function isExamActive(): boolean;
|
|
6
8
|
export declare function getExamDeadline(): Date | null;
|
|
7
9
|
export declare function addInteraction(interaction: import('../types/index.js').ExamInteraction): void;
|
package/dist/lib/exam-state.js
CHANGED
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getIcoaDir } from './config.js';
|
|
4
|
+
// Demo and real exam use separate state files so they don't block each other
|
|
4
5
|
function stateFile() {
|
|
5
6
|
return join(getIcoaDir(), 'exam-state.json');
|
|
6
7
|
}
|
|
8
|
+
function demoStateFile() {
|
|
9
|
+
return join(getIcoaDir(), 'demo-state.json');
|
|
10
|
+
}
|
|
11
|
+
// Internal: pick the right file based on examId
|
|
12
|
+
function resolveStateFile(examId) {
|
|
13
|
+
return examId === 'demo-free' ? demoStateFile() : stateFile();
|
|
14
|
+
}
|
|
7
15
|
export function getExamState() {
|
|
16
|
+
// Check both files, return the most recently active one
|
|
17
|
+
const files = [stateFile(), demoStateFile()];
|
|
18
|
+
let latest = null;
|
|
19
|
+
let latestTime = 0;
|
|
20
|
+
for (const f of files) {
|
|
21
|
+
if (!existsSync(f))
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
const state = JSON.parse(readFileSync(f, 'utf-8'));
|
|
25
|
+
const t = new Date(state.session.confirmedAt || state.session.startedAt).getTime();
|
|
26
|
+
if (t > latestTime) {
|
|
27
|
+
latest = state;
|
|
28
|
+
latestTime = t;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
return latest;
|
|
34
|
+
}
|
|
35
|
+
export function getDemoState() {
|
|
36
|
+
const f = demoStateFile();
|
|
37
|
+
if (!existsSync(f))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(f, 'utf-8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function getRealExamState() {
|
|
8
47
|
const f = stateFile();
|
|
9
48
|
if (!existsSync(f))
|
|
10
49
|
return null;
|
|
@@ -16,12 +55,24 @@ export function getExamState() {
|
|
|
16
55
|
}
|
|
17
56
|
}
|
|
18
57
|
export function saveExamState(state) {
|
|
19
|
-
|
|
58
|
+
const f = resolveStateFile(state.session.examId);
|
|
59
|
+
writeFileSync(f, JSON.stringify(state, null, 2));
|
|
20
60
|
}
|
|
21
|
-
export function clearExamState() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
export function clearExamState(examId) {
|
|
62
|
+
if (examId) {
|
|
63
|
+
const f = resolveStateFile(examId);
|
|
64
|
+
if (existsSync(f))
|
|
65
|
+
unlinkSync(f);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Clear the currently active exam
|
|
69
|
+
const state = getExamState();
|
|
70
|
+
if (state) {
|
|
71
|
+
const f = resolveStateFile(state.session.examId);
|
|
72
|
+
if (existsSync(f))
|
|
73
|
+
unlinkSync(f);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
25
76
|
}
|
|
26
77
|
export function isExamActive() {
|
|
27
78
|
return getExamState() !== null;
|