icoa-cli 2.19.74 → 2.19.76
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/repl.js +81 -6
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -17,6 +17,55 @@ import { startLogSync, stopLogSync } from './lib/log-sync.js';
|
|
|
17
17
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
|
+
// Pre-import commonly-used CTF modules on interactive Python startup.
|
|
21
|
+
// Failures are silent — selectors import only what's available so a partial
|
|
22
|
+
// exam setup doesn't crash the shell. Creates ~/.icoa/python-startup.py once.
|
|
23
|
+
const PYTHON_STARTUP_CONTENT = `# ICOA exam interactive startup — auto-loaded by PYTHONSTARTUP
|
|
24
|
+
import base64, struct, hashlib, re, json, os, sys, binascii
|
|
25
|
+
try: import requests
|
|
26
|
+
except ImportError: pass
|
|
27
|
+
try: from Crypto.Cipher import AES
|
|
28
|
+
except ImportError: pass
|
|
29
|
+
try: from Crypto.Util.Padding import pad, unpad
|
|
30
|
+
except ImportError: pass
|
|
31
|
+
try: from pwn import xor, p32, u32, p64, u64
|
|
32
|
+
except ImportError: pass
|
|
33
|
+
try: import bs4
|
|
34
|
+
except ImportError: pass
|
|
35
|
+
try: import numpy as np
|
|
36
|
+
except ImportError: pass
|
|
37
|
+
`;
|
|
38
|
+
function ensurePythonStartup() {
|
|
39
|
+
const icoaDir = join(homedir(), '.icoa');
|
|
40
|
+
if (!existsSync(icoaDir))
|
|
41
|
+
mkdirSync(icoaDir, { recursive: true });
|
|
42
|
+
const path = join(icoaDir, 'python-startup.py');
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
const { writeFileSync } = require('node:fs');
|
|
45
|
+
writeFileSync(path, PYTHON_STARTUP_CONTENT);
|
|
46
|
+
}
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
function printPythonBanner() {
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
52
|
+
console.log(chalk.bold.white(' Python ready — ICOA exam toolkit pre-loaded'));
|
|
53
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(chalk.white(' Already imported: ') + chalk.gray('base64, struct, hashlib, re, json, binascii'));
|
|
56
|
+
console.log(chalk.white(' Also available: ') + chalk.gray('requests, bs4, numpy, AES, pad/unpad, xor, p32/u32/p64/u64'));
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(chalk.yellow(' Quick examples:'));
|
|
59
|
+
console.log(chalk.gray(' base64.b64decode("aGVsbG8=") ') + chalk.gray('# decode base64'));
|
|
60
|
+
console.log(chalk.gray(' bytes.fromhex("48656c6c6f") ') + chalk.gray('# hex → bytes'));
|
|
61
|
+
console.log(chalk.gray(' "ICOA{x}".encode() ') + chalk.gray('# str → bytes'));
|
|
62
|
+
console.log(chalk.gray(' [chr(c) for c in [73,67,79,65]] ') + chalk.gray('# ASCII codes'));
|
|
63
|
+
console.log(chalk.gray(' xor(bytes.fromhex("0a2b"), b"IC") ') + chalk.gray('# pwntools XOR'));
|
|
64
|
+
console.log();
|
|
65
|
+
console.log(chalk.gray(' Exit: ') + chalk.white('exit()') + chalk.gray(' or Ctrl-D'));
|
|
66
|
+
console.log(chalk.cyan(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
67
|
+
console.log();
|
|
68
|
+
}
|
|
20
69
|
// Compute the REPL prompt based on current state.
|
|
21
70
|
// Real exam wins over demo (matches getExamState() priority). Chat modes have
|
|
22
71
|
// their own prompts and are handled separately.
|
|
@@ -792,6 +841,16 @@ export async function startRepl(program, resumeMode) {
|
|
|
792
841
|
}
|
|
793
842
|
// Ensure workspace directory
|
|
794
843
|
const cwd = ensureWorkspace();
|
|
844
|
+
// Interactive Python (no script args, no -c): print welcome banner and
|
|
845
|
+
// pre-import common CTF modules via a startup file. Makes entry painless
|
|
846
|
+
// for contestants — they land in a shell with base64/struct/hashlib/pwn
|
|
847
|
+
// already imported and see a 4-line cheat sheet of common one-liners.
|
|
848
|
+
const isInteractivePython = /^(\S*python3?(\.\d+)?)\s*$/.test(resolvedInput);
|
|
849
|
+
if (isInteractivePython) {
|
|
850
|
+
const startupPath = ensurePythonStartup();
|
|
851
|
+
resolvedInput = `PYTHONSTARTUP="${startupPath}" ${resolvedInput}`;
|
|
852
|
+
printPythonBanner();
|
|
853
|
+
}
|
|
795
854
|
// Route to Docker sandbox if available, otherwise system shell (in workspace)
|
|
796
855
|
processing = true;
|
|
797
856
|
try {
|
|
@@ -873,20 +932,36 @@ export async function startRepl(program, resumeMode) {
|
|
|
873
932
|
}
|
|
874
933
|
function runSystemCommand(input, rl, cwd) {
|
|
875
934
|
return new Promise((resolve) => {
|
|
935
|
+
// Fully release the TTY so interactive children (python3, bash, etc.)
|
|
936
|
+
// get a clean terminal with proper echo. `rl.pause()` alone doesn't
|
|
937
|
+
// reset raw mode or release stdin — readline keeps it in line-editing
|
|
938
|
+
// mode where typed characters don't display until our REPL reads a line.
|
|
939
|
+
const stdin = process.stdin;
|
|
940
|
+
const wasRaw = stdin.isTTY ? !!stdin.isRaw : false;
|
|
876
941
|
rl.pause();
|
|
942
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function') {
|
|
943
|
+
try {
|
|
944
|
+
stdin.setRawMode(false);
|
|
945
|
+
}
|
|
946
|
+
catch { }
|
|
947
|
+
}
|
|
877
948
|
const child = spawn(input, {
|
|
878
949
|
shell: true,
|
|
879
950
|
stdio: 'inherit',
|
|
880
951
|
cwd: cwd || process.cwd(),
|
|
881
952
|
});
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
953
|
+
const restore = () => {
|
|
954
|
+
if (stdin.isTTY && typeof stdin.setRawMode === 'function' && wasRaw) {
|
|
955
|
+
try {
|
|
956
|
+
stdin.setRawMode(true);
|
|
957
|
+
}
|
|
958
|
+
catch { }
|
|
959
|
+
}
|
|
887
960
|
rl.resume();
|
|
888
961
|
resolve();
|
|
889
|
-
}
|
|
962
|
+
};
|
|
963
|
+
child.on('close', restore);
|
|
964
|
+
child.on('error', restore);
|
|
890
965
|
});
|
|
891
966
|
}
|
|
892
967
|
function mapCommand(input) {
|