icoa-cli 2.19.17 โ†’ 2.19.18

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.
@@ -405,13 +405,6 @@ export function registerExamCommand(program) {
405
405
  const q = state.questions.find((qq) => qq.number === currentQ);
406
406
  if (!q)
407
407
  return;
408
- // Find the correct answer for demo
409
- let correctAnswer = null;
410
- if (state.session.examId === 'demo-free') {
411
- import('../lib/demo-exam.js').then(({ DEMO_ANSWERS }) => {
412
- correctAnswer = DEMO_ANSWERS[currentQ];
413
- });
414
- }
415
408
  // Check: already used 2 helps on this question
416
409
  if (qHelps >= 2) {
417
410
  console.log();
@@ -468,9 +461,8 @@ export function registerExamCommand(program) {
468
461
  console.log(chalk.yellow(` ๐Ÿ’ก Option ${toRemove} eliminated!`) + chalk.gray(` (help ${help.used}/${help.max})`));
469
462
  printQuestion(q, state.answers[q.number]);
470
463
  };
471
- if (state.session.examId === 'demo-free') {
472
- const { DEMO_ANSWERS } = await import('../lib/demo-exam.js');
473
- doEliminate(DEMO_ANSWERS[currentQ]);
464
+ if (state.session.examId === 'demo-free' && q.answer) {
465
+ doEliminate(q.answer);
474
466
  }
475
467
  else {
476
468
  // For server exams, we don't know the answer โ€” eliminate random non-selected option
@@ -671,28 +663,34 @@ export function registerExamCommand(program) {
671
663
  // Demo exam: grade locally
672
664
  if (state.session.examId === 'demo-free') {
673
665
  try {
674
- const { DEMO_ANSWERS, getLocalizedExplanations } = await import('../lib/demo-exam.js');
675
666
  drawProgress(0, t('grading'));
676
667
  await sleep(300);
677
668
  let score = 0;
678
669
  const wrongQuestions = [];
679
- for (const [qn, ans] of Object.entries(state.answers)) {
680
- const num = Number(qn);
681
- if (DEMO_ANSWERS[num] === ans) {
670
+ const categoryStats = {};
671
+ for (const q of state.questions) {
672
+ const cat = q.category || 'Other';
673
+ if (!categoryStats[cat])
674
+ categoryStats[cat] = { correct: 0, total: 0 };
675
+ categoryStats[cat].total++;
676
+ const userAns = state.answers[q.number];
677
+ if (userAns && q.answer && userAns === q.answer) {
682
678
  score++;
679
+ categoryStats[cat].correct++;
683
680
  }
684
681
  else {
685
- wrongQuestions.push(num);
686
- }
687
- }
688
- // Also count unanswered as wrong
689
- for (const q of state.questions) {
690
- if (!state.answers[q.number])
691
682
  wrongQuestions.push(q.number);
683
+ }
692
684
  }
693
685
  wrongQuestions.sort((a, b) => a - b);
694
686
  drawProgress(100, t('complete'));
695
687
  console.log();
688
+ // Elapsed time
689
+ const startedMs = new Date(state.session.startedAt).getTime();
690
+ const elapsedSec = Math.max(0, Math.round((Date.now() - startedMs) / 1000));
691
+ const elapsedMin = Math.floor(elapsedSec / 60);
692
+ const elapsedS = elapsedSec % 60;
693
+ const elapsedLabel = `${elapsedMin}m ${elapsedS}s`;
696
694
  const questionsSnapshot = [...state.questions];
697
695
  const answersSnapshot = { ...state.answers };
698
696
  const helpState = getHelpState(state);
@@ -728,14 +726,27 @@ export function registerExamCommand(program) {
728
726
  console.log();
729
727
  console.log(chalk.bold(` ${t('score')}: ${score}/${total} (${percentage}%)`));
730
728
  console.log(chalk.bold(` ${percentage >= 60 ? chalk.green(t('passed')) : chalk.red(t('notPassed'))}`));
729
+ console.log(chalk.gray(` Elapsed: ${elapsedLabel}`));
731
730
  console.log();
732
731
  console.log(chalk.yellow(' International Cyber Olympiad in AI 2026'));
733
732
  console.log(chalk.gray(' Sydney, Australia ยท Jun 27 - Jul 2, 2026'));
734
733
  console.log();
735
734
  console.log(chalk.cyan(' โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'));
735
+ // Per-category breakdown
736
+ const catEntries = Object.entries(categoryStats);
737
+ if (catEntries.length > 0) {
738
+ console.log();
739
+ console.log(chalk.bold.white(' By category'));
740
+ for (const [cat, s] of catEntries) {
741
+ const pct = s.total > 0 ? Math.round(s.correct / s.total * 100) : 0;
742
+ const color = pct >= 80 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
743
+ console.log(' ' + color(`${cat.padEnd(20)} ${s.correct}/${s.total} (${pct}%)`));
744
+ }
745
+ console.log();
746
+ console.log(chalk.cyan(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
747
+ }
736
748
  // Show wrong answers with explanations
737
749
  if (wrongQuestions.length > 0) {
738
- const explanations = getLocalizedExplanations();
739
750
  console.log();
740
751
  console.log(chalk.yellow(` ${wrongQuestions.length} ${t('incorrectIntro')}`));
741
752
  console.log();
@@ -744,7 +755,7 @@ export function registerExamCommand(program) {
744
755
  if (!q)
745
756
  continue;
746
757
  const userAns = answersSnapshot[qn];
747
- const correctAns = DEMO_ANSWERS[qn];
758
+ const correctAns = q.answer;
748
759
  console.log(chalk.white(` Q${qn}. ${q.text}`));
749
760
  if (userAns) {
750
761
  console.log(chalk.red(` ${t('yourAnswer')}: ${userAns}. ${q.options[userAns]}`));
@@ -752,9 +763,11 @@ export function registerExamCommand(program) {
752
763
  else {
753
764
  console.log(chalk.yellow(` ${t('yourAnswer')}: โ€”`));
754
765
  }
755
- console.log(chalk.green(` ${t('correct')}: ${correctAns}. ${q.options[correctAns]}`));
756
- if (explanations[qn]) {
757
- console.log(chalk.gray(` โ†’ ${explanations[qn]}`));
766
+ if (correctAns) {
767
+ console.log(chalk.green(` ${t('correct')}: ${correctAns}. ${q.options[correctAns]}`));
768
+ }
769
+ if (q.explanation) {
770
+ console.log(chalk.gray(` โ†’ ${q.explanation}`));
758
771
  }
759
772
  console.log();
760
773
  }
@@ -952,8 +965,8 @@ export function registerExamCommand(program) {
952
965
  .description('Try a free practice exam (no account needed)')
953
966
  .action(async () => {
954
967
  logCommand('exam demo');
955
- const { getLocalizedDemoQuestions, getLocalizedDemoSession } = await import('../lib/demo-exam.js');
956
- const DEMO_QUESTIONS = getLocalizedDemoQuestions();
968
+ const { pickDemoQuestions, getLocalizedDemoSession, DEMO_PICK_SIZE, DEMO_POOL_SIZE } = await import('../lib/demo-exam.js');
969
+ const DEMO_QUESTIONS = pickDemoQuestions(DEMO_PICK_SIZE);
957
970
  const DEMO_SESSION = getLocalizedDemoSession();
958
971
  const existing = getExamState();
959
972
  if (existing) {
@@ -971,7 +984,7 @@ export function registerExamCommand(program) {
971
984
  printHeader('ICOA Demo Exam โ€” Free Practice');
972
985
  console.log();
973
986
  console.log(chalk.white(' Free practice ยท No account needed ยท No time limit'));
974
- console.log(chalk.white(' 15 questions ยท Pick one answer per question'));
987
+ console.log(chalk.white(` ${DEMO_PICK_SIZE} random questions from a pool of ${DEMO_POOL_SIZE} ยท Pick one answer per question`));
975
988
  console.log();
976
989
  printHowToPlay();
977
990
  console.log();
@@ -981,7 +994,7 @@ export function registerExamCommand(program) {
981
994
  if (currentLang === 'en') {
982
995
  console.log(chalk.gray(' Questions in English. To switch language first:'));
983
996
  console.log(chalk.gray(' lang es (Espaรฑol) ยท lang zh (ไธญๆ–‡) ยท lang ko (ํ•œ๊ตญ์–ด) ยท lang ja (ๆ—ฅๆœฌ่ชž)'));
984
- console.log(chalk.gray(' lang fr ยท lang ar ยท lang pt ยท lang to see all 15'));
997
+ console.log(chalk.gray(' lang fr ยท lang ar ยท lang pt ยท 15 languages supported'));
985
998
  }
986
999
  else {
987
1000
  console.log(chalk.green(` Language: ${currentLang}`));
@@ -992,7 +1005,7 @@ export function registerExamCommand(program) {
992
1005
  console.log();
993
1006
  drawProgress(0, 'Preparing questions...');
994
1007
  await sleep(200);
995
- drawProgress(40, 'Setting up timer...');
1008
+ drawProgress(40, 'Shuffling options...');
996
1009
  await sleep(200);
997
1010
  drawProgress(80, 'Almost ready...');
998
1011
  await sleep(150);
@@ -1000,7 +1013,7 @@ export function registerExamCommand(program) {
1000
1013
  console.log();
1001
1014
  console.log();
1002
1015
  saveExamState({ session, questions: DEMO_QUESTIONS, answers: {} });
1003
- printKeyValue('Questions', '15');
1016
+ printKeyValue('Questions', String(DEMO_PICK_SIZE));
1004
1017
  printKeyValue('Duration', 'No time limit');
1005
1018
  // Show first question
1006
1019
  printQuestion(DEMO_QUESTIONS[0]);
@@ -50,22 +50,28 @@ export function registerLangCommand(program) {
50
50
  }
51
51
  saveConfig({ language: code });
52
52
  printSuccess(`Language set to: ${LANG_NAMES[code] || code}`);
53
- // If demo exam in progress, reload questions in new language
53
+ // If demo exam in progress, restart with fresh random pick in new language.
54
+ // (Progress is lost because questions are randomly picked from a 30-question pool
55
+ // and each attempt shuffles option positions โ€” mid-exam translation would
56
+ // desync the answer key.)
54
57
  const state = getExamState();
55
58
  if (state && state.session.examId === 'demo-free') {
56
59
  try {
57
- const { getLocalizedDemoQuestions, getLocalizedDemoSession } = await import('../lib/demo-exam.js');
58
- state.questions = getLocalizedDemoQuestions();
60
+ const { pickDemoQuestions, getLocalizedDemoSession, DEMO_PICK_SIZE } = await import('../lib/demo-exam.js');
61
+ const freshQuestions = pickDemoQuestions(DEMO_PICK_SIZE);
62
+ state.questions = freshQuestions;
63
+ state.answers = {};
59
64
  state.session.examName = getLocalizedDemoSession().examName;
65
+ state.session.startedAt = new Date().toISOString();
66
+ state._lastQ = 1;
60
67
  const { saveExamState } = await import('../lib/exam-state.js');
61
68
  saveExamState(state);
62
- const currentQ = state._lastQ || 1;
63
69
  console.log();
64
- console.log(chalk.green(` Demo questions reloaded in ${LANG_NAMES[code] || code}.`));
65
- console.log(chalk.white(` Resuming: exam q ${currentQ}`));
70
+ console.log(chalk.green(` Demo restarted in ${LANG_NAMES[code] || code}.`));
71
+ console.log(chalk.white(' Type: exam q 1'));
66
72
  }
67
73
  catch {
68
- console.log(chalk.gray(' Language changed. Resume with: exam q 1'));
74
+ console.log(chalk.gray(' Language changed. Type: demo'));
69
75
  }
70
76
  }
71
77
  else if (state) {
@@ -1,18 +1,26 @@
1
1
  import type { ExamQuestion, ExamSession } from '../types/index.js';
2
+ export declare const DEMO_POOL_SIZE = 30;
3
+ export declare const DEMO_PICK_SIZE = 10;
2
4
  export declare const DEMO_SESSION: ExamSession;
3
- export declare const DEMO_ANSWERS: Record<number, string>;
5
+ export declare const DEMO_ANSWERS: Record<number, 'A' | 'B' | 'C' | 'D'>;
4
6
  export declare const DEMO_EXPLANATIONS: Record<number, string>;
5
7
  export declare const DEMO_QUESTIONS: ExamQuestion[];
8
+ /**
9
+ * Pick `n` random questions from the pool, shuffle each question's options,
10
+ * renumber 1..n. Returns fully self-contained questions with answer + explanation
11
+ * populated, so downstream code does not need to look up DEMO_ANSWERS by number.
12
+ */
13
+ export declare function pickDemoQuestions(n?: number): ExamQuestion[];
6
14
  /**
7
15
  * Get localized explanations for demo questions.
8
16
  * Reads from translations/<lang>/demo-explanations.json.
9
- * Falls back to English if not available.
17
+ * Falls back to English for any missing entries.
10
18
  */
11
19
  export declare function getLocalizedExplanations(): Record<number, string>;
12
20
  /**
13
21
  * Get demo questions translated to user's language.
14
22
  * Reads from translations/<lang>/demo.json (bundled static file).
15
- * Falls back to English if not available.
23
+ * Per-question fallback: missing entries use the English source.
16
24
  */
17
25
  export declare function getLocalizedDemoQuestions(): ExamQuestion[];
18
26
  /**
@@ -3,71 +3,166 @@ import { join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { getConfig } from './config.js';
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export const DEMO_POOL_SIZE = 30;
7
+ export const DEMO_PICK_SIZE = 10;
6
8
  export const DEMO_SESSION = {
7
9
  examId: 'demo-free',
8
10
  examName: 'ICOA Demo Exam โ€” Free Practice',
9
11
  startedAt: '',
10
- durationMinutes: 0, // 0 = no time limit for demo
11
- questionCount: 15,
12
+ durationMinutes: 0,
13
+ questionCount: DEMO_PICK_SIZE,
12
14
  country: 'ALL',
13
15
  };
16
+ // Answer key for the 30-question pool (balanced A=7 B=7 C=8 D=8)
17
+ // Q16-30 option order was rebalanced from translate-demo.js to distribute answers.
14
18
  export const DEMO_ANSWERS = {
15
- 1: 'C', 2: 'A', 3: 'D', 4: 'B', 5: 'A', 6: 'D', 7: 'C', 8: 'B',
16
- 9: 'A', 10: 'D', 11: 'C', 12: 'B', 13: 'A', 14: 'C', 15: 'D',
19
+ 1: 'C', 2: 'B', 3: 'C', 4: 'B', 5: 'C', 6: 'B', 7: 'B', 8: 'C',
20
+ 9: 'C', 10: 'B', 11: 'C', 12: 'B', 13: 'B', 14: 'B', 15: 'B',
21
+ 16: 'D', 17: 'D', 18: 'C', 19: 'C', 20: 'B', 21: 'A', 22: 'D', 23: 'C',
22
+ 24: 'B', 25: 'A', 26: 'B', 27: 'D', 28: 'C', 29: 'A', 30: 'B',
17
23
  };
18
24
  export const DEMO_EXPLANATIONS = {
19
25
  1: 'RSA is an asymmetric (public-key) cipher. AES, DES, and Blowfish are all symmetric ciphers that use the same key for encryption and decryption.',
20
26
  2: 'SQL injection occurs when user input is inserted directly into database queries without proper sanitization, allowing attackers to manipulate the query.',
21
27
  3: 'HTTP 403 means Forbidden โ€” the server understood the request but refuses to authorize it. 401 is Unauthorized, 404 is Not Found, 500 is Internal Server Error.',
22
- 4: 'Wireshark is the standard tool for capturing and analyzing network packets. Burp Suite is for web testing, John the Ripper for password cracking, Ghidra for reverse engineering.',
23
- 5: 'XSS stands for Cross-Site Scripting โ€” a vulnerability where attackers inject malicious scripts into web pages viewed by other users.',
24
- 6: 'A Trojan disguises itself as legitimate software to trick users into installing it. Unlike worms, Trojans do not self-replicate.',
25
- 7: 'SSH (Secure Shell) runs on port 22 by default. Port 21 is FTP, 80 is HTTP, 443 is HTTPS.',
26
- 8: 'A cryptographic hash is a one-way function that produces a fixed-size digest. It cannot be reversed, unlike encryption.',
27
- 9: 'Two-factor authentication (2FA) requires two distinct types of credentials โ€” something you know (password) and something you have (phone/token) or are (biometrics).',
28
- 10: 'The command "netstat -tulpn" shows all listening TCP/UDP ports with process info. "ls -la" lists files, "chmod" changes permissions, "cat /etc/passwd" shows user accounts.',
29
- 11: 'A Man-in-the-Middle attack intercepts and potentially modifies communications between two parties who believe they are communicating directly with each other.',
30
- 12: 'The principle of least privilege means granting users only the minimum permissions necessary to perform their tasks, reducing the attack surface.',
31
- 13: 'Phishing is a social engineering attack that tricks people into revealing sensitive information. Buffer overflow, SQL injection, and port scanning are technical attacks.',
32
- 14: 'A VPN (Virtual Private Network) creates an encrypted tunnel for internet traffic, protecting data from interception and masking the user\'s IP address.',
33
- 15: 'Passwords should be stored as salted hashes. Plain text and Base64 are insecure. AES encryption is reversible if the key is compromised, but hashing with salt is one-way.',
28
+ 4: 'A nonce (number used once) is a random value used in cryptographic protocols to prevent replay attacks โ€” ensuring each request or message is unique and cannot be reused by an attacker.',
29
+ 5: 'Wireshark is the standard tool for capturing and analyzing network packets. Burp Suite is for web testing, John the Ripper for password cracking, Ghidra for reverse engineering.',
30
+ 6: 'XSS stands for Cross-Site Scripting โ€” a vulnerability where attackers inject malicious scripts into web pages viewed by other users.',
31
+ 7: 'A firewall filters network traffic based on security rules, blocking unauthorized access while allowing legitimate communication. It does not encrypt, scan for viruses, or speed up connections.',
32
+ 8: 'A Trojan disguises itself as legitimate software to trick users into installing it. Unlike worms, Trojans do not self-replicate.',
33
+ 9: 'HTTPS (HTTP Secure) uses TLS/SSL to encrypt web traffic, protecting data from eavesdropping and tampering. HTTP, FTP, and SMTP are not secure by default.',
34
+ 10: 'A cryptographic hash is a one-way function that produces a fixed-size digest. It cannot be reversed, unlike encryption.',
35
+ 11: 'Ghidra is a reverse engineering and binary analysis tool developed by the NSA. Nmap scans ports, SQLMap tests SQL injection, Nikto scans web servers.',
36
+ 12: 'DNS Spoofing (cache poisoning) manipulates DNS responses to redirect victims to attacker-controlled servers. It is distinct from phishing, SQLi, and brute force.',
37
+ 13: 'SSH (Secure Shell) runs on port 22 by default. Port 21 is FTP, 80 is HTTP, 443 is HTTPS.',
38
+ 14: 'Two-factor authentication (2FA) requires two distinct types of credentials โ€” something you know (password) and something you have (phone/token) or are (biometrics).',
39
+ 15: 'The command "netstat -tulpn" shows all listening TCP/UDP ports with process info. "ls -la" lists files, "chmod" changes permissions, "cat /etc/passwd" shows user accounts.',
40
+ 16: 'A Man-in-the-Middle (MitM) attack intercepts and potentially modifies communications between two parties who believe they are communicating directly with each other.',
41
+ 17: 'SHA-256 is a cryptographic hash function. AES-256 is a symmetric cipher, RSA-2048 is an asymmetric cipher, and Diffie-Hellman is a key exchange protocol.',
42
+ 18: 'The principle of least privilege means granting users only the minimum permissions necessary to perform their tasks, reducing the attack surface.',
43
+ 19: 'Nmap is the standard port scanning tool. Wireshark captures packets, Metasploit is an exploitation framework, and Hashcat is a password cracker.',
44
+ 20: 'Ransomware encrypts the victim\'s files and demands payment (usually cryptocurrency) in exchange for the decryption key.',
45
+ 21: 'Symmetric encryption uses a single shared key for both encrypting and decrypting. Asymmetric encryption uses a key pair โ€” a public key to encrypt and a private key to decrypt.',
46
+ 22: 'Remote Code Execution (RCE) allows an attacker to run arbitrary code on a target server, often leading to full system compromise. CSRF, clickjacking, and open redirect have different impacts.',
47
+ 23: 'OWASP (Open Web Application Security Project) is a non-profit organization that publishes widely-used web security standards and guides, including the OWASP Top 10.',
48
+ 24: 'The chmod command changes file permissions in Linux. chown changes ownership, chgrp changes group, and passwd changes user passwords.',
49
+ 25: 'An SSL/TLS certificate is a digital document issued by a trusted Certificate Authority that verifies a website\'s identity and enables encrypted HTTPS connections.',
50
+ 26: 'Phishing is a social engineering attack that tricks people into revealing sensitive information. Buffer overflow, SQL injection, and port scanning are technical attacks.',
51
+ 27: 'The grep command searches for text patterns in files using regular expressions. It is one of the most commonly used Linux text processing tools.',
52
+ 28: 'A VPN (Virtual Private Network) creates an encrypted tunnel for internet traffic, protecting data from interception and masking the user\'s IP address.',
53
+ 29: 'CSRF (Cross-Site Request Forgery) tricks a logged-in user\'s browser into performing unwanted actions on a trusted site, such as changing account settings or transferring funds.',
54
+ 30: 'Passwords should be stored as salted cryptographic hashes. Plain text and Base64 are insecure, and AES encryption is reversible if the key is compromised. Salted hashing is one-way and resistant to rainbow tables.',
34
55
  };
56
+ // 30-question pool. Q16-30 options were rebalanced from the original translate-demo.js layout
57
+ // to achieve an even answer distribution across A/B/C/D.
58
+ // Translation files (translations/<lang>/demo.json) must match this option order.
35
59
  export const DEMO_QUESTIONS = [
36
60
  { number: 1, text: 'Which algorithm is NOT a symmetric cipher?', category: 'Cryptography',
37
- options: { A: 'AES', B: 'DES', C: 'RSA', D: 'Blowfish' } },
61
+ options: { A: 'AES', B: 'RSA', C: 'DES', D: 'Blowfish' } },
38
62
  { number: 2, text: 'What does SQL injection exploit?', category: 'Web Security',
39
- options: { A: 'Unsanitized user input in database queries', B: 'Buffer overflow in web server', C: 'Misconfigured firewall rules', D: 'Weak encryption algorithms' } },
63
+ options: { A: 'Buffer overflow in web server', B: 'Unsanitized user input in database queries', C: 'Weak encryption algorithms', D: 'Misconfigured firewall rules' } },
40
64
  { number: 3, text: 'Which HTTP status code indicates "Forbidden"?', category: 'Web Security',
41
- options: { A: '401', B: '404', C: '500', D: '403' } },
42
- { number: 4, text: 'Which tool is commonly used for network packet capture?', category: 'Network',
43
- options: { A: 'Burp Suite', B: 'Wireshark', C: 'John the Ripper', D: 'Ghidra' } },
44
- { number: 5, text: 'What does XSS stand for in cybersecurity?', category: 'Web Security',
45
- options: { A: 'Cross-Site Scripting', B: 'Extended Security System', C: 'XML Secure Socket', D: 'Cross-Server Sharing' } },
46
- { number: 6, text: 'Which type of malware disguises itself as legitimate software?', category: 'Malware',
47
- options: { A: 'Worm', B: 'Ransomware', C: 'Adware', D: 'Trojan' } },
48
- { number: 7, text: 'What is the standard port for SSH?', category: 'Network',
49
- options: { A: '21', B: '80', C: '22', D: '443' } },
50
- { number: 8, text: 'What is a cryptographic hash?', category: 'Cryptography',
65
+ options: { A: '401', B: '404', C: '403', D: '500' } },
66
+ { number: 4, text: 'What is the primary purpose of a nonce in cryptography?', category: 'Cryptography',
67
+ options: { A: 'Encrypt data at rest', B: 'Prevent replay attacks', C: 'Generate random passwords', D: 'Compress data before encryption' } },
68
+ { number: 5, text: 'Which tool is commonly used for network packet capture?', category: 'Network',
69
+ options: { A: 'Burp Suite', B: 'Ghidra', C: 'Wireshark', D: 'John the Ripper' } },
70
+ { number: 6, text: 'What does XSS stand for in cybersecurity?', category: 'Web Security',
71
+ options: { A: 'Extended Security System', B: 'Cross-Site Scripting', C: 'XML Secure Socket', D: 'Cross-Server Sharing' } },
72
+ { number: 7, text: 'What is the primary function of a firewall?', category: 'Network',
73
+ options: { A: 'Encrypt network data', B: 'Filter network traffic based on security rules', C: 'Detect viruses in files', D: 'Speed up internet connection' } },
74
+ { number: 8, text: 'Which type of malware disguises itself as legitimate software?', category: 'Malware',
75
+ options: { A: 'Worm', B: 'Ransomware', C: 'Trojan', D: 'Adware' } },
76
+ { number: 9, text: 'Which protocol provides secure communication on the web?', category: 'Network',
77
+ options: { A: 'HTTP', B: 'FTP', C: 'HTTPS', D: 'SMTP' } },
78
+ { number: 10, text: 'What is a cryptographic hash?', category: 'Cryptography',
51
79
  options: { A: 'A reversible encryption key', B: 'A one-way function producing a fixed-size digest', C: 'An authentication protocol', D: 'A type of digital signature' } },
52
- { number: 9, text: 'What is two-factor authentication (2FA)?', category: 'Authentication',
53
- options: { A: 'Verifying identity with two distinct types of credentials', B: 'Using two different passwords', C: 'Encrypting data twice', D: 'Connecting through two networks' } },
54
- { number: 10, text: 'Which Linux command shows open ports on a system?', category: 'Linux',
55
- options: { A: 'ls -la', B: 'chmod 777', C: 'cat /etc/passwd', D: 'netstat -tulpn' } },
56
- { number: 11, text: 'What is a Man-in-the-Middle (MitM) attack?', category: 'Network',
57
- options: { A: 'Accessing a server without authorization', B: 'Guessing passwords by brute force', C: 'Intercepting and modifying communications between two parties', D: 'Sending multiple requests to overload a server' } },
58
- { number: 12, text: 'What is the principle of least privilege?', category: 'Security',
59
- options: { A: 'Give root access to all users', B: 'Grant only the permissions necessary to perform a task', C: 'Use the shortest password possible', D: 'Disable all firewalls' } },
60
- { number: 13, text: 'Which of the following is a social engineering attack?', category: 'Security',
61
- options: { A: 'Phishing', B: 'Buffer overflow', C: 'SQL Injection', D: 'Port scanning' } },
62
- { number: 14, text: 'What is a VPN?', category: 'Network',
80
+ { number: 11, text: 'Which tool is used for binary analysis?', category: 'Reverse Engineering',
81
+ options: { A: 'Nmap', B: 'SQLMap', C: 'Ghidra', D: 'Nikto' } },
82
+ { number: 12, text: 'Which attack manipulates DNS requests to redirect traffic?', category: 'Network',
83
+ options: { A: 'Phishing', B: 'DNS Spoofing', C: 'SQL Injection', D: 'Brute Force' } },
84
+ { number: 13, text: 'What is the standard port for SSH?', category: 'Network',
85
+ options: { A: '21', B: '22', C: '80', D: '443' } },
86
+ { number: 14, text: 'What is two-factor authentication (2FA)?', category: 'Authentication',
87
+ options: { A: 'Using two different passwords', B: 'Verifying identity with two distinct types of credentials', C: 'Encrypting data twice', D: 'Connecting through two networks' } },
88
+ { number: 15, text: 'Which Linux command shows open ports on a system?', category: 'Linux',
89
+ options: { A: 'ls -la', B: 'netstat -tulpn', C: 'chmod 777', D: 'cat /etc/passwd' } },
90
+ { number: 16, text: 'What is a Man-in-the-Middle (MitM) attack?', category: 'Network',
91
+ options: { A: 'Accessing a server without authorization', B: 'Guessing passwords by brute force', C: 'Sending multiple requests to overload a server', D: 'Intercepting and modifying communications between two parties' } },
92
+ { number: 17, text: 'Which of these is a hash algorithm?', category: 'Cryptography',
93
+ options: { A: 'AES-256', B: 'Diffie-Hellman', C: 'RSA-2048', D: 'SHA-256' } },
94
+ { number: 18, text: 'What is the principle of least privilege?', category: 'Security',
95
+ options: { A: 'Give root access to all users', B: 'Use the shortest password possible', C: 'Grant only the permissions necessary to perform a task', D: 'Disable all firewalls' } },
96
+ { number: 19, text: 'Which tool is commonly used for port scanning?', category: 'Network',
97
+ options: { A: 'Wireshark', B: 'Metasploit', C: 'Nmap', D: 'Hashcat' } },
98
+ { number: 20, text: 'What is ransomware?', category: 'Malware',
99
+ options: { A: 'Software that shows unwanted ads', B: 'Software that encrypts files and demands payment to decrypt', C: 'Software that records keystrokes', D: 'Software that replicates across networks' } },
100
+ { number: 21, text: 'What is the difference between symmetric and asymmetric encryption?', category: 'Cryptography',
101
+ options: { A: 'Symmetric uses the same key to encrypt and decrypt; asymmetric uses two different keys', B: 'Symmetric is slower than asymmetric', C: 'Asymmetric only works with small files', D: 'There is no significant difference' } },
102
+ { number: 22, text: 'Which vulnerability allows arbitrary code execution on a web server?', category: 'Web Security',
103
+ options: { A: 'CSRF', B: 'Open Redirect', C: 'Clickjacking', D: 'Remote Code Execution (RCE)' } },
104
+ { number: 23, text: 'What is OWASP?', category: 'Security',
105
+ options: { A: 'A security operating system', B: 'A type of firewall', C: 'An organization that publishes web security standards and guides', D: 'A programming language for security' } },
106
+ { number: 24, text: 'Which Linux command changes file permissions?', category: 'Linux',
107
+ options: { A: 'chown', B: 'chmod', C: 'chgrp', D: 'passwd' } },
108
+ { number: 25, text: 'What is an SSL/TLS certificate?', category: 'Cryptography',
109
+ options: { A: 'A digital document that verifies a website identity', B: 'A file containing malware', C: 'A private key for SSH', D: 'A type of encrypted database' } },
110
+ { number: 26, text: 'Which of the following is a social engineering attack?', category: 'Security',
111
+ options: { A: 'Buffer overflow', B: 'Phishing', C: 'SQL Injection', D: 'Port scanning' } },
112
+ { number: 27, text: 'What does the Linux command "grep" do?', category: 'Linux',
113
+ options: { A: 'Compresses files', B: 'Configures the network', C: 'Shows active processes', D: 'Searches for text patterns in files' } },
114
+ { number: 28, text: 'What is a VPN?', category: 'Network',
63
115
  options: { A: 'A type of virus', B: 'A file transfer protocol', C: 'A virtual private network that encrypts internet traffic', D: 'A vulnerability scanner' } },
64
- { number: 15, text: 'What is the best practice for storing passwords in a database?', category: 'Security',
65
- options: { A: 'Plain text', B: 'Encrypted with AES', C: 'Encoded in Base64', D: 'Hashed with salt' } },
116
+ { number: 29, text: 'What is CSRF (Cross-Site Request Forgery)?', category: 'Web Security',
117
+ options: { A: 'An attack that forces a user browser to perform unauthorized actions', B: 'A data encryption method', C: 'A type of network scanner', D: 'A file compression technique' } },
118
+ { number: 30, text: 'What is the best practice for storing passwords in a database?', category: 'Security',
119
+ options: { A: 'Plain text', B: 'Hashed with salt', C: 'Encrypted with AES', D: 'Encoded in Base64' } },
66
120
  ];
121
+ /** Fisher-Yates shuffle, returns a new array. */
122
+ function shuffle(arr) {
123
+ const out = [...arr];
124
+ for (let i = out.length - 1; i > 0; i--) {
125
+ const j = Math.floor(Math.random() * (i + 1));
126
+ [out[i], out[j]] = [out[j], out[i]];
127
+ }
128
+ return out;
129
+ }
130
+ /** Shuffle the four options within a single question; recompute the answer letter. */
131
+ function shuffleQuestionOptions(q) {
132
+ if (!q.answer)
133
+ return q;
134
+ const correctText = q.options[q.answer];
135
+ const keys = ['A', 'B', 'C', 'D'];
136
+ const values = shuffle(keys.map((k) => q.options[k]));
137
+ const newOptions = { A: values[0], B: values[1], C: values[2], D: values[3] };
138
+ const newAnswer = keys.find((k) => newOptions[k] === correctText);
139
+ return { ...q, options: newOptions, answer: newAnswer };
140
+ }
141
+ /**
142
+ * Pick `n` random questions from the pool, shuffle each question's options,
143
+ * renumber 1..n. Returns fully self-contained questions with answer + explanation
144
+ * populated, so downstream code does not need to look up DEMO_ANSWERS by number.
145
+ */
146
+ export function pickDemoQuestions(n = DEMO_PICK_SIZE) {
147
+ const translatedPool = getLocalizedDemoQuestions();
148
+ const explanations = getLocalizedExplanations();
149
+ // Enrich pool with answer + explanation keyed by ORIGINAL number
150
+ const enriched = translatedPool.map((q) => ({
151
+ ...q,
152
+ answer: DEMO_ANSWERS[q.number],
153
+ explanation: explanations[q.number],
154
+ }));
155
+ // Shuffle pool, pick n, shuffle each question's options, renumber 1..n
156
+ const picked = shuffle(enriched).slice(0, n);
157
+ return picked.map((q, i) => ({
158
+ ...shuffleQuestionOptions(q),
159
+ number: i + 1,
160
+ }));
161
+ }
67
162
  /**
68
163
  * Get localized explanations for demo questions.
69
164
  * Reads from translations/<lang>/demo-explanations.json.
70
- * Falls back to English if not available.
165
+ * Falls back to English for any missing entries.
71
166
  */
72
167
  export function getLocalizedExplanations() {
73
168
  const lang = getConfig().language;
@@ -78,8 +173,10 @@ export function getLocalizedExplanations() {
78
173
  return DEMO_EXPLANATIONS;
79
174
  try {
80
175
  const data = JSON.parse(readFileSync(path, 'utf-8'));
81
- if (data && typeof data === 'object')
82
- return data;
176
+ if (data && typeof data === 'object') {
177
+ // Merge: translated entries override English, missing entries stay English
178
+ return { ...DEMO_EXPLANATIONS, ...data };
179
+ }
83
180
  }
84
181
  catch { }
85
182
  return DEMO_EXPLANATIONS;
@@ -87,7 +184,7 @@ export function getLocalizedExplanations() {
87
184
  /**
88
185
  * Get demo questions translated to user's language.
89
186
  * Reads from translations/<lang>/demo.json (bundled static file).
90
- * Falls back to English if not available.
187
+ * Per-question fallback: missing entries use the English source.
91
188
  */
92
189
  export function getLocalizedDemoQuestions() {
93
190
  const lang = getConfig().language;
@@ -98,8 +195,13 @@ export function getLocalizedDemoQuestions() {
98
195
  return DEMO_QUESTIONS;
99
196
  try {
100
197
  const data = JSON.parse(readFileSync(path, 'utf-8'));
101
- if (Array.isArray(data) && data.length >= DEMO_QUESTIONS.length)
102
- return data;
198
+ if (!Array.isArray(data))
199
+ return DEMO_QUESTIONS;
200
+ // Per-question merge: use translated where available, English fallback otherwise.
201
+ return DEMO_QUESTIONS.map((eng) => {
202
+ const tr = data.find((d) => d.number === eng.number);
203
+ return tr && tr.options ? tr : eng;
204
+ });
103
205
  }
104
206
  catch { }
105
207
  return DEMO_QUESTIONS;
package/dist/repl.js CHANGED
@@ -56,53 +56,47 @@ export async function startRepl(program, resumeMode) {
56
56
  default: savedMode || 'selection',
57
57
  });
58
58
  if (selected === 'about') {
59
+ console.clear();
59
60
  console.log();
60
61
  console.log(chalk.cyan(' โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'));
61
- console.log();
62
- console.log(chalk.bold.white(' โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—'));
63
- console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—'));
64
- console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘'));
65
- console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘'));
66
- console.log(chalk.bold.white(' โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘'));
67
- console.log(chalk.bold.white(' โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•'));
68
- console.log();
69
- console.log(chalk.bold.yellow(' The World\'s First'));
70
- console.log(chalk.bold.white(' AI-Native CLI Operating System for'));
71
- console.log(chalk.bold.white(' Cybersecurity & AI Security Competition'));
72
- console.log(chalk.bold.white(' and Olympiad for K-12'));
73
- console.log();
74
- console.log(chalk.white(' One terminal. 15 languages. 15,000 concurrent participants.'));
75
- console.log(chalk.white(' 109 pre-configured CTF tools. Zero browser required.'));
76
- console.log();
77
- console.log(chalk.cyan(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
62
+ console.log(chalk.bold.yellow(' ICOA') + chalk.white(' โ€” AI-Native CLI OS for Cyber & AI Security'));
63
+ console.log(chalk.gray(' Olympiad & Competition ยท K-12 to University'));
64
+ console.log(chalk.cyan(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
78
65
  console.log();
79
66
  console.log(chalk.bold.white(' What Makes ICOA Different'));
80
67
  console.log(chalk.gray(' ยท AI-native AI teammate, AI adversary, AI translation'));
81
68
  console.log(chalk.gray(' ยท CLI OS Complete competition environment in terminal'));
82
69
  console.log(chalk.gray(' ยท 109 tools pwntools, z3, gdb, nmap... pre-configured'));
83
- console.log(chalk.gray(' ยท Global scale 15,000+ concurrent exams, single server'));
84
- console.log(chalk.gray(' ยท 15 languages Real-time AI translation for all content'));
70
+ console.log(chalk.gray(' ยท Global scale 15,000+ concurrent exams ยท 15 languages'));
85
71
  console.log();
86
72
  console.log(chalk.bold.white(' Competition Format'));
87
- console.log(chalk.green.bold(' AI4CTF') + chalk.gray(' [Day 1] AI as your teammate โ€” 5hr jeopardy CTF'));
88
- console.log(chalk.red.bold(' CTF4AI') + chalk.gray(' [Day 2] Challenge & evaluate AI โ€” adversarial ML, red-teaming'));
89
- console.log();
90
- console.log(chalk.white(' Sydney, Australia') + chalk.gray(' ยท Jun 27 - Jul 2, 2026'));
91
- console.log(chalk.gray(' 40+ countries and regions represented'));
73
+ console.log(' ' + chalk.green.bold('AI4CTF') + chalk.gray(' [Day 1] AI as teammate โ€” 5hr jeopardy CTF'));
74
+ console.log(' ' + chalk.red.bold('CTF4AI') + chalk.gray(' [Day 2] Challenge AI โ€” adversarial ML, red-team'));
92
75
  console.log();
93
- console.log(chalk.cyan(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
94
- console.log();
95
- console.log(chalk.bold.white(' Organized by'));
96
- console.log(chalk.gray(' ASRA โ€” Australia STEM and Robotics Advancement Association Inc'));
97
- console.log(chalk.gray(' ICO Foundation Inc (Australia)'));
98
- console.log();
99
- console.log(chalk.bold.white(' Contact & Accreditation'));
100
- console.log(chalk.cyan(' australia@icoa2026.au'));
101
- console.log(chalk.cyan(' accreditation@icoa2026.au'));
102
- console.log(chalk.cyan.underline(' https://icoa2026.au'));
76
+ console.log(chalk.white(' Sydney, Australia') + chalk.gray(' ยท Jun 27 - Jul 2, 2026 ยท 40+ countries'));
103
77
  console.log();
78
+ console.log(chalk.bold.white(' Organized by') + chalk.gray(' ASRA (Australia) ยท ICO Foundation Inc'));
79
+ console.log(chalk.bold.white(' Contact ') + chalk.cyan(' australia@icoa2026.au ยท accreditation@icoa2026.au'));
80
+ console.log(chalk.bold.white(' Website ') + chalk.cyan.underline(' https://icoa2026.au'));
104
81
  console.log(chalk.cyan(' โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'));
105
82
  console.log();
83
+ console.log(chalk.gray(' Press ') + chalk.yellow('Enter') + chalk.gray(' to return...'));
84
+ await new Promise((resolve) => {
85
+ const onKey = (_chunk) => {
86
+ process.stdin.removeListener('data', onKey);
87
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
88
+ process.stdin.setRawMode(false);
89
+ }
90
+ process.stdin.pause();
91
+ resolve();
92
+ };
93
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
94
+ process.stdin.setRawMode(true);
95
+ }
96
+ process.stdin.resume();
97
+ process.stdin.once('data', onKey);
98
+ });
99
+ console.clear();
106
100
  // Loop back to mode selection
107
101
  continue;
108
102
  }
@@ -149,6 +149,8 @@ export interface ExamQuestion {
149
149
  C: string;
150
150
  D: string;
151
151
  };
152
+ answer?: 'A' | 'B' | 'C' | 'D';
153
+ explanation?: string;
152
154
  }
153
155
  export interface ExamSession {
154
156
  examId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.17",
3
+ "version": "2.19.18",
4
4
  "description": "ICOA CLI โ€” The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {