icoa-cli 2.19.82 → 2.19.84

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.
@@ -44,7 +44,7 @@ const PYTHON_LIBS = [
44
44
  { name: 'pyserial', check: 'python3 -c "import serial"', install: 'pyserial==3.5', category: 'Security Tools' },
45
45
  ];
46
46
  // ══════════════════════════════════════════════════════════
47
- // 82 System Tools — brew (Mac) / apt (Ubuntu) / choco (Win)
47
+ // 83 System Tools — brew (Mac) / apt (Ubuntu) / choco (Win)
48
48
  // ══════════════════════════════════════════════════════════
49
49
  const W = process.platform === 'win32';
50
50
  const CMD = (unix, win) => W ? (win || `where ${unix}`) : `which ${unix}`;
@@ -109,6 +109,8 @@ const SYSTEM_TOOLS = [
109
109
  { name: 'xxd', check: CMD('xxd'), brew: 'vim', apt: 'xxd', category: 'Forensics' },
110
110
  { name: 'pdftotext', check: CMD('pdftotext'), brew: 'poppler', apt: 'poppler-utils', category: 'Forensics' },
111
111
  { name: 'pngcheck', check: CMD('pngcheck'), brew: 'pngcheck', apt: 'pngcheck', category: 'Forensics' },
112
+ // sleuthkit — disk-image forensics (mmls/fls/icat/blkcat + 20 more)
113
+ { name: 'sleuthkit', check: CMD('mmls'), brew: 'sleuthkit', apt: 'sleuthkit', choco: 'sleuthkit', category: 'Forensics' },
112
114
  // Crypto & Password (4)
113
115
  { name: 'john', check: CMD('john'), brew: 'john', apt: 'john', choco: 'john', category: 'Crypto & Password' },
114
116
  { name: 'hashcat', check: CMD('hashcat'), brew: 'hashcat', apt: 'hashcat', choco: 'hashcat', category: 'Crypto & Password' },
@@ -270,12 +272,12 @@ function getPythonFullVersion() {
270
272
  }
271
273
  export function registerEnvCommand(program) {
272
274
  const envCmd = program.command('env').description('Manage competition environment');
273
- envCmd.command('status').alias('check').description('Check all 109 tools').action(() => showStatus());
275
+ envCmd.command('status').alias('check').description('Check all 110 tools').action(() => showStatus());
274
276
  envCmd.command('setup').description('Install all Python libraries + system tools').action(async () => { await installAll(); });
275
277
  envCmd.command('python').description('Show Python 3.12 install guide for your platform').action(() => showPythonInstallGuide());
276
278
  envCmd.action(() => showStatus());
277
279
  }
278
- // Lightweight Python 3.12 install guide for Selection mode (no 109-tool check).
280
+ // Lightweight Python 3.12 install guide for Selection mode (no 110-tool check).
279
281
  // Detects distro on Linux so contestants get the right command the first time.
280
282
  function showPythonInstallGuide() {
281
283
  const os = platform();
@@ -405,7 +407,7 @@ function showStatus() {
405
407
  console.log(chalk.gray(' You need these for most challenges'));
406
408
  console.log(chalk.yellow(' Recommended') + chalk.gray(' pycryptodome, beautifulsoup4, scapy, sympy'));
407
409
  console.log(chalk.gray(' Covers Web, Crypto, and Forensics'));
408
- console.log(chalk.gray(' Full (109) All tools for every category'));
410
+ console.log(chalk.gray(' Full (110) All tools for every category'));
409
411
  console.log();
410
412
  console.log(chalk.gray(' Missing tools? Run ') + chalk.bold.cyan('env setup') + chalk.gray(' to install everything.'));
411
413
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
@@ -502,7 +504,7 @@ function showStatus() {
502
504
  const total = installed + missing;
503
505
  console.log();
504
506
  console.log(chalk.gray(' ─────────────────────────────────────────────'));
505
- console.log(` ${chalk.green(`✓ ${installed}/${total}`)} ${missing > 0 ? chalk.red(`✗ ${missing} missing`) : chalk.green('All 109 ready!')}`);
507
+ console.log(` ${chalk.green(`✓ ${installed}/${total}`)} ${missing > 0 ? chalk.red(`✗ ${missing} missing`) : chalk.green('All 110 ready!')}`);
506
508
  if (missing > 0) {
507
509
  console.log(chalk.gray(' Install everything: ') + chalk.white('env setup'));
508
510
  }
@@ -101,7 +101,7 @@ async function playDemoIntro() {
101
101
  await waitOrSkip(1500);
102
102
  if (skipped)
103
103
  return;
104
- console.log(chalk.gray(' · 109 CTF tools pre-configured'));
104
+ console.log(chalk.gray(' · 110 CTF tools pre-configured'));
105
105
  await waitOrSkip(400);
106
106
  if (skipped)
107
107
  return;
@@ -883,15 +883,9 @@ export function registerExamCommand(program) {
883
883
  }
884
884
  return;
885
885
  }
886
- // Eliminate one wrong option (async for demo answers)
887
- const doEliminate = (correct) => {
888
- const wrongRemaining = remaining.filter((k) => k !== correct && !eliminated.includes(k));
889
- if (wrongRemaining.length === 0)
890
- return;
891
- // Pick random wrong option to eliminate
892
- const toRemove = wrongRemaining[Math.floor(Math.random() * wrongRemaining.length)];
886
+ // Apply elimination of a specific option
887
+ const applyEliminate = (toRemove) => {
893
888
  eliminated.push(toRemove);
894
- // Update state
895
889
  if (!help.eliminated[currentQ])
896
890
  help.eliminated[currentQ] = [];
897
891
  help.eliminated[currentQ] = eliminated;
@@ -909,31 +903,42 @@ export function registerExamCommand(program) {
909
903
  console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
910
904
  printQuestion(q, state.answers[q.number]);
911
905
  };
906
+ // Pick a random wrong option to eliminate (needs correct answer)
907
+ const doEliminate = (correct) => {
908
+ const wrongRemaining = remaining.filter((k) => k !== correct && !eliminated.includes(k));
909
+ if (wrongRemaining.length === 0)
910
+ return;
911
+ applyEliminate(wrongRemaining[Math.floor(Math.random() * wrongRemaining.length)]);
912
+ };
912
913
  if (state.session.examId === 'demo-free' && q.answer) {
913
914
  doEliminate(q.answer);
914
915
  }
915
916
  else {
916
- // For server exams, we don't know the answer — eliminate random non-selected option
917
- const userAnswer = state.answers[currentQ];
918
- const candidates = remaining.filter((k) => k !== userAnswer);
919
- const toRemove = candidates[Math.floor(Math.random() * candidates.length)];
920
- eliminated.push(toRemove);
921
- if (!help.eliminated[currentQ])
922
- help.eliminated[currentQ] = [];
923
- help.eliminated[currentQ] = eliminated;
924
- help.perQ[currentQ] = qHelps + 1;
925
- help.used++;
926
- state._helpUsed = help.used;
927
- state._helpMax = help.max;
928
- state._helpPerQ = help.perQ;
929
- state._eliminated = help.eliminated;
930
- if (!state.interactions)
931
- state.interactions = [];
932
- state.interactions.push({ ts: new Date().toISOString(), q: currentQ, type: 'help_used', result: `eliminated ${toRemove}` });
933
- saveExamState(state);
934
- console.log();
935
- console.log(chalk.yellow(` 💡 Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
936
- printQuestion(q, state.answers[q.number]);
917
+ // Server-authoritative: ask server which wrong option to eliminate
918
+ const config = getConfig();
919
+ const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
920
+ const token = state.session?.token || '';
921
+ try {
922
+ const res = await fetch(`${serverUrl}/api/icoa/exams/${state.session.examId}/help`, {
923
+ method: 'POST',
924
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'icoa-cli' },
925
+ body: JSON.stringify({ token, question: currentQ, eliminated }),
926
+ signal: AbortSignal.timeout(5000),
927
+ });
928
+ const json = await res.json();
929
+ const toRemove = json?.data?.eliminate;
930
+ if (toRemove && ['A', 'B', 'C', 'D'].includes(toRemove)) {
931
+ applyEliminate(toRemove);
932
+ }
933
+ else {
934
+ console.log();
935
+ console.log(chalk.gray(' Server did not return a valid option. Try again.'));
936
+ }
937
+ }
938
+ catch {
939
+ console.log();
940
+ console.log(chalk.gray(' Could not reach server for help. Check your connection.'));
941
+ }
937
942
  }
938
943
  });
939
944
  // ─── exam more-help ───
package/dist/repl.js CHANGED
@@ -245,7 +245,7 @@ export async function startRepl(program, resumeMode) {
245
245
  console.log(chalk.bold.white(' What Makes ICOA Different'));
246
246
  console.log(chalk.gray(' · AI-native AI teammate, AI adversary, AI translation'));
247
247
  console.log(chalk.gray(' · CLI OS Complete competition environment in terminal'));
248
- console.log(chalk.gray(' · 109 tools pwntools, z3, gdb, nmap... pre-configured'));
248
+ console.log(chalk.gray(' · 110 tools pwntools, z3, gdb, nmap, sleuthkit... pre-configured'));
249
249
  console.log(chalk.gray(' · Global scale 15,000+ concurrent exams · 15 languages'));
250
250
  console.log();
251
251
  console.log(chalk.bold.white(' Competition Format'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.82",
3
+ "version": "2.19.84",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {