icoa-cli 1.0.0
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/connect.d.ts +2 -0
- package/dist/commands/connect.js +66 -0
- package/dist/commands/ctf.d.ts +2 -0
- package/dist/commands/ctf.js +472 -0
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.js +52 -0
- package/dist/commands/hint.d.ts +2 -0
- package/dist/commands/hint.js +107 -0
- package/dist/commands/lang.d.ts +2 -0
- package/dist/commands/lang.js +42 -0
- package/dist/commands/log.d.ts +2 -0
- package/dist/commands/log.js +36 -0
- package/dist/commands/note.d.ts +2 -0
- package/dist/commands/note.js +32 -0
- package/dist/commands/ref.d.ts +2 -0
- package/dist/commands/ref.js +63 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +88 -0
- package/dist/commands/shell.d.ts +2 -0
- package/dist/commands/shell.js +55 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +78 -0
- package/dist/lib/budget.d.ts +8 -0
- package/dist/lib/budget.js +29 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +60 -0
- package/dist/lib/ctfd-client.d.ts +22 -0
- package/dist/lib/ctfd-client.js +161 -0
- package/dist/lib/gemini.d.ts +7 -0
- package/dist/lib/gemini.js +108 -0
- package/dist/lib/logger.d.ts +6 -0
- package/dist/lib/logger.js +59 -0
- package/dist/lib/translation.d.ts +1 -0
- package/dist/lib/translation.js +40 -0
- package/dist/lib/ui.d.ts +10 -0
- package/dist/lib/ui.js +59 -0
- package/dist/types/index.d.ts +125 -0
- package/dist/types/index.js +29 -0
- package/package.json +43 -0
- package/refs/ROPgadget.txt +67 -0
- package/refs/base64.txt +63 -0
- package/refs/bash.txt +79 -0
- package/refs/binwalk.txt +43 -0
- package/refs/bs4.txt +61 -0
- package/refs/checksec.txt +57 -0
- package/refs/curl.txt +73 -0
- package/refs/cyberchef.txt +78 -0
- package/refs/exiftool.txt +50 -0
- package/refs/ffuf.txt +73 -0
- package/refs/gcc.txt +66 -0
- package/refs/gdb.txt +83 -0
- package/refs/hashcat.txt +64 -0
- package/refs/hint.txt +42 -0
- package/refs/icoa.txt +36 -0
- package/refs/john.txt +74 -0
- package/refs/linux.txt +58 -0
- package/refs/nc.txt +64 -0
- package/refs/nmap.txt +57 -0
- package/refs/numpy.txt +59 -0
- package/refs/openssl.txt +75 -0
- package/refs/pillow.txt +67 -0
- package/refs/pwntools.txt +79 -0
- package/refs/pycrypto.txt +77 -0
- package/refs/python.txt +94 -0
- package/refs/r2.txt +85 -0
- package/refs/regex.txt +73 -0
- package/refs/requests.txt +83 -0
- package/refs/rules.txt +28 -0
- package/refs/scapy.txt +80 -0
- package/refs/sqlmap.txt +69 -0
- package/refs/steghide.txt +71 -0
- package/refs/struct.txt +61 -0
- package/refs/sympy.txt +77 -0
- package/refs/tshark.txt +65 -0
- package/refs/vim.txt +74 -0
- package/refs/volatility.txt +41 -0
- package/refs/z3.txt +78 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createWriteStream, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
export class CTFdClient {
|
|
6
|
+
baseUrl;
|
|
7
|
+
token;
|
|
8
|
+
constructor(baseUrl, token) {
|
|
9
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
10
|
+
this.token = token;
|
|
11
|
+
}
|
|
12
|
+
async request(method, path, body) {
|
|
13
|
+
const url = `${this.baseUrl}/api/v1${path}`;
|
|
14
|
+
const headers = {
|
|
15
|
+
Authorization: `Token ${this.token}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
};
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method,
|
|
20
|
+
headers,
|
|
21
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
const text = await res.text().catch(() => 'Unknown error');
|
|
25
|
+
throw new Error(`CTFd API error (${res.status}): ${text}`);
|
|
26
|
+
}
|
|
27
|
+
const json = (await res.json());
|
|
28
|
+
if (json.success === false) {
|
|
29
|
+
throw new Error(`CTFd error: ${json.errors?.join(', ') || 'Unknown error'}`);
|
|
30
|
+
}
|
|
31
|
+
return json.data;
|
|
32
|
+
}
|
|
33
|
+
async testConnection() {
|
|
34
|
+
return this.request('GET', '/users/me');
|
|
35
|
+
}
|
|
36
|
+
async getChallenges() {
|
|
37
|
+
return this.request('GET', '/challenges');
|
|
38
|
+
}
|
|
39
|
+
async getChallenge(id) {
|
|
40
|
+
return this.request('GET', `/challenges/${id}`);
|
|
41
|
+
}
|
|
42
|
+
async submitFlag(challengeId, submission) {
|
|
43
|
+
const res = await fetch(`${this.baseUrl}/api/v1/challenges/attempt`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Token ${this.token}`,
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({ challenge_id: challengeId, submission }),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const text = await res.text().catch(() => 'Unknown error');
|
|
53
|
+
throw new Error(`CTFd API error (${res.status}): ${text}`);
|
|
54
|
+
}
|
|
55
|
+
const json = (await res.json());
|
|
56
|
+
return json.data;
|
|
57
|
+
}
|
|
58
|
+
async getScoreboard() {
|
|
59
|
+
return this.request('GET', '/scoreboard');
|
|
60
|
+
}
|
|
61
|
+
async getTeam() {
|
|
62
|
+
return this.request('GET', '/teams/me');
|
|
63
|
+
}
|
|
64
|
+
async getCompetitionMeta() {
|
|
65
|
+
const res = await fetch(this.baseUrl);
|
|
66
|
+
const html = await res.text();
|
|
67
|
+
const startMatch = html.match(/'start'\s*:\s*(\d+)/);
|
|
68
|
+
const endMatch = html.match(/'end'\s*:\s*(\d+)/);
|
|
69
|
+
const modeMatch = html.match(/'userMode'\s*:\s*"([^"]+)"/);
|
|
70
|
+
const csrfMatch = html.match(/'csrfNonce'\s*:\s*"([^"]+)"/);
|
|
71
|
+
return {
|
|
72
|
+
start: startMatch ? parseInt(startMatch[1]) : null,
|
|
73
|
+
end: endMatch ? parseInt(endMatch[1]) : null,
|
|
74
|
+
userMode: modeMatch?.[1] || 'users',
|
|
75
|
+
csrfNonce: csrfMatch?.[1] || '',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async getChallengeFiles(id) {
|
|
79
|
+
const challenge = await this.getChallenge(id);
|
|
80
|
+
return challenge.files || [];
|
|
81
|
+
}
|
|
82
|
+
async downloadFile(filePath, destDir) {
|
|
83
|
+
mkdirSync(destDir, { recursive: true });
|
|
84
|
+
const url = filePath.startsWith('http')
|
|
85
|
+
? filePath
|
|
86
|
+
: `${this.baseUrl}/${filePath.replace(/^\//, '')}`;
|
|
87
|
+
const res = await fetch(url, {
|
|
88
|
+
headers: { Authorization: `Token ${this.token}` },
|
|
89
|
+
redirect: 'follow',
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok || !res.body) {
|
|
92
|
+
throw new Error(`Failed to download: ${url}`);
|
|
93
|
+
}
|
|
94
|
+
// Extract clean filename (strip query params like ?token=xxx)
|
|
95
|
+
const rawName = filePath.split('/').pop() || 'file';
|
|
96
|
+
const fileName = rawName.split('?')[0];
|
|
97
|
+
const destPath = join(destDir, fileName);
|
|
98
|
+
const fileStream = createWriteStream(destPath);
|
|
99
|
+
await pipeline(Readable.fromWeb(res.body), fileStream);
|
|
100
|
+
return destPath;
|
|
101
|
+
}
|
|
102
|
+
async loginWithCredentials(username, password) {
|
|
103
|
+
// Step 1: GET /login to get nonce
|
|
104
|
+
const loginPageRes = await fetch(`${this.baseUrl}/login`);
|
|
105
|
+
const loginHtml = await loginPageRes.text();
|
|
106
|
+
// Try multiple nonce patterns (CTFd versions differ)
|
|
107
|
+
const nonceMatch = loginHtml.match(/name="nonce"[^>]*value="([^"]+)"/) ||
|
|
108
|
+
loginHtml.match(/value="([^"]+)"[^>]*name="nonce"/) ||
|
|
109
|
+
loginHtml.match(/id="nonce"[^>]*value="([^"]+)"/) ||
|
|
110
|
+
loginHtml.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
111
|
+
const nonce = nonceMatch?.[1] || '';
|
|
112
|
+
if (!nonce) {
|
|
113
|
+
throw new Error('Could not extract CSRF nonce from login page.');
|
|
114
|
+
}
|
|
115
|
+
// Extract cookies
|
|
116
|
+
const cookies = loginPageRes.headers.getSetCookie?.() || [];
|
|
117
|
+
const cookieStr = cookies.map((c) => c.split(';')[0]).join('; ');
|
|
118
|
+
// Step 2: POST /login with credentials
|
|
119
|
+
const loginRes = await fetch(`${this.baseUrl}/login`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
123
|
+
Cookie: cookieStr,
|
|
124
|
+
},
|
|
125
|
+
body: new URLSearchParams({ name: username, password, nonce, _submit: 'Submit' }),
|
|
126
|
+
redirect: 'manual',
|
|
127
|
+
});
|
|
128
|
+
const loginCookies = loginRes.headers.getSetCookie?.() || [];
|
|
129
|
+
const allCookies = [...cookies, ...loginCookies].map((c) => c.split(';')[0]).join('; ');
|
|
130
|
+
// Check if login was successful (redirect to /challenges or /, not back to /login)
|
|
131
|
+
const location = loginRes.headers.get('location') || '';
|
|
132
|
+
if (location.includes('/login')) {
|
|
133
|
+
throw new Error('Invalid username or password.');
|
|
134
|
+
}
|
|
135
|
+
// Step 3: Generate API token
|
|
136
|
+
// First get CSRF nonce from settings page
|
|
137
|
+
const settingsRes = await fetch(`${this.baseUrl}/settings`, {
|
|
138
|
+
headers: { Cookie: allCookies },
|
|
139
|
+
});
|
|
140
|
+
const settingsHtml = await settingsRes.text();
|
|
141
|
+
const csrfMatch = settingsHtml.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
142
|
+
const csrfNonce = csrfMatch?.[1] || nonce;
|
|
143
|
+
const tokenRes = await fetch(`${this.baseUrl}/api/v1/tokens`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
Cookie: allCookies,
|
|
148
|
+
'CSRF-Token': csrfNonce,
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({ expiration: '2026-12-31T23:59:59+00:00' }),
|
|
151
|
+
});
|
|
152
|
+
if (!tokenRes.ok) {
|
|
153
|
+
throw new Error('Failed to generate API token. Please use a manual access token instead.');
|
|
154
|
+
}
|
|
155
|
+
const tokenJson = (await tokenRes.json());
|
|
156
|
+
if (tokenJson.success && tokenJson.data?.value) {
|
|
157
|
+
return tokenJson.data.value;
|
|
158
|
+
}
|
|
159
|
+
throw new Error('Failed to generate API token. Response: ' + JSON.stringify(tokenJson));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HintLevel, ChallengeContext } from '../types/index.js';
|
|
2
|
+
export declare function generateHint(level: HintLevel, question: string, context?: ChallengeContext): Promise<{
|
|
3
|
+
text: string;
|
|
4
|
+
tokensUsed: number;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function translateText(text: string, targetLang: string): Promise<string>;
|
|
7
|
+
export declare function setApiKey(key: string): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { getConfig, saveConfig } from './config.js';
|
|
3
|
+
const SYSTEM_PROMPTS = {
|
|
4
|
+
A: `You are an AI assistant in a cybersecurity CTF competition called ICOA.
|
|
5
|
+
You are providing Level A (General Guidance) to a competitor.
|
|
6
|
+
|
|
7
|
+
STRICT RULES:
|
|
8
|
+
- Only answer conceptual questions
|
|
9
|
+
- Do NOT mention specific vulnerability names
|
|
10
|
+
- Do NOT provide any code, commands, or tool usage
|
|
11
|
+
- Do NOT mention specific attack techniques
|
|
12
|
+
- Use questions to guide the competitor toward their own discovery
|
|
13
|
+
- If the competitor asks you to solve the challenge, refuse and redirect
|
|
14
|
+
- Never output anything matching flag format: icoa{...}`,
|
|
15
|
+
B: `You are an AI assistant in ICOA CTF, providing Level B (Deep Analysis).
|
|
16
|
+
|
|
17
|
+
RULES:
|
|
18
|
+
- You MAY identify specific vulnerability types (e.g., "buffer overflow")
|
|
19
|
+
- You MAY suggest which category of tool to use (e.g., "a debugger")
|
|
20
|
+
- Do NOT provide complete commands or working code
|
|
21
|
+
- Do NOT provide exploit code or payloads
|
|
22
|
+
- Do NOT provide flags or flag fragments
|
|
23
|
+
- Never output anything matching: icoa{...}`,
|
|
24
|
+
C: `You are an AI assistant in ICOA CTF, providing Level C (Critical Assist).
|
|
25
|
+
|
|
26
|
+
RULES:
|
|
27
|
+
- You MAY provide the key conceptual breakthrough
|
|
28
|
+
- You MAY name specific algorithms or approaches
|
|
29
|
+
- Do NOT provide complete exploit code
|
|
30
|
+
- Do NOT provide the flag
|
|
31
|
+
- Never output anything matching: icoa{...}`,
|
|
32
|
+
};
|
|
33
|
+
function buildSystemPrompt(level, context) {
|
|
34
|
+
let prompt = SYSTEM_PROMPTS[level];
|
|
35
|
+
if (context) {
|
|
36
|
+
prompt += `\n\nThe competitor is currently working on:\nChallenge: ${context.name}\nCategory: ${context.category}`;
|
|
37
|
+
}
|
|
38
|
+
return prompt;
|
|
39
|
+
}
|
|
40
|
+
function filterFlagPatterns(text) {
|
|
41
|
+
return text.replace(/icoa\{[^}]*\}/gi, '[FLAG REDACTED]');
|
|
42
|
+
}
|
|
43
|
+
function getApiKey() {
|
|
44
|
+
// Priority: env var > config
|
|
45
|
+
const envKey = process.env.GEMINI_API_KEY;
|
|
46
|
+
if (envKey)
|
|
47
|
+
return envKey;
|
|
48
|
+
const config = getConfig();
|
|
49
|
+
if (config.geminiApiKey)
|
|
50
|
+
return config.geminiApiKey;
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
export async function generateHint(level, question, context) {
|
|
54
|
+
let apiKey = getApiKey();
|
|
55
|
+
if (!apiKey) {
|
|
56
|
+
// Try to prompt for key interactively
|
|
57
|
+
try {
|
|
58
|
+
const { input } = await import('@inquirer/prompts');
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(' Gemini API key not configured.');
|
|
61
|
+
console.log(' Get one free at: https://aistudio.google.com/apikey');
|
|
62
|
+
console.log();
|
|
63
|
+
apiKey = await input({ message: 'Enter your Gemini API Key:' });
|
|
64
|
+
if (apiKey.trim()) {
|
|
65
|
+
apiKey = apiKey.trim();
|
|
66
|
+
saveConfig({ geminiApiKey: apiKey });
|
|
67
|
+
console.log(' Key saved for future use.');
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw new Error('No API key provided.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error('Gemini API key not configured.\n' +
|
|
76
|
+
'Set GEMINI_API_KEY environment variable, or run: icoa setup');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
80
|
+
const model = genAI.getGenerativeModel({
|
|
81
|
+
model: 'gemini-2.5-pro-preview-05-06',
|
|
82
|
+
systemInstruction: buildSystemPrompt(level, context),
|
|
83
|
+
});
|
|
84
|
+
const result = await model.generateContent(question);
|
|
85
|
+
const response = result.response;
|
|
86
|
+
const text = filterFlagPatterns(response.text());
|
|
87
|
+
const usage = response.usageMetadata;
|
|
88
|
+
const tokensUsed = (usage?.promptTokenCount || 0) + (usage?.candidatesTokenCount || 0);
|
|
89
|
+
return { text, tokensUsed };
|
|
90
|
+
}
|
|
91
|
+
export async function translateText(text, targetLang) {
|
|
92
|
+
const apiKey = getApiKey();
|
|
93
|
+
if (!apiKey) {
|
|
94
|
+
throw new Error('Gemini API key not configured for translation.');
|
|
95
|
+
}
|
|
96
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
97
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-pro-preview-05-06' });
|
|
98
|
+
const prompt = `Translate the following CTF challenge description to ${targetLang}.
|
|
99
|
+
Keep all technical terms, code, commands, URLs, and flag formats in English.
|
|
100
|
+
Only translate the narrative/descriptive text.
|
|
101
|
+
|
|
102
|
+
${text}`;
|
|
103
|
+
const result = await model.generateContent(prompt);
|
|
104
|
+
return result.response.text();
|
|
105
|
+
}
|
|
106
|
+
export function setApiKey(key) {
|
|
107
|
+
saveConfig({ geminiApiKey: key });
|
|
108
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LogEntry, HintLevel } from '../types/index.js';
|
|
2
|
+
export declare function logEntry(entry: LogEntry): void;
|
|
3
|
+
export declare function logCommand(command: string): void;
|
|
4
|
+
export declare function logHint(level: HintLevel, question: string, challengeId?: number): void;
|
|
5
|
+
export declare function logSubmission(challengeId: number, flag: string): void;
|
|
6
|
+
export declare function getSessionLog(): LogEntry[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getConfig, getIcoaDir } from './config.js';
|
|
4
|
+
function getLogPath() {
|
|
5
|
+
return join(getIcoaDir(), 'session.log');
|
|
6
|
+
}
|
|
7
|
+
export function logEntry(entry) {
|
|
8
|
+
try {
|
|
9
|
+
appendFileSync(getLogPath(), JSON.stringify(entry) + '\n');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Silently fail — logging should never break the CLI
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function logCommand(command) {
|
|
16
|
+
const config = getConfig();
|
|
17
|
+
logEntry({
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
level: 'command',
|
|
20
|
+
input: command,
|
|
21
|
+
sessionId: config.sessionId,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function logHint(level, question, challengeId) {
|
|
25
|
+
const config = getConfig();
|
|
26
|
+
logEntry({
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
level: level,
|
|
29
|
+
input: question,
|
|
30
|
+
challengeId,
|
|
31
|
+
sessionId: config.sessionId,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function logSubmission(challengeId, flag) {
|
|
35
|
+
const config = getConfig();
|
|
36
|
+
logEntry({
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
level: 'submit',
|
|
39
|
+
input: flag,
|
|
40
|
+
challengeId,
|
|
41
|
+
sessionId: config.sessionId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function getSessionLog() {
|
|
45
|
+
const logPath = getLogPath();
|
|
46
|
+
if (!existsSync(logPath))
|
|
47
|
+
return [];
|
|
48
|
+
try {
|
|
49
|
+
const raw = readFileSync(logPath, 'utf-8');
|
|
50
|
+
return raw
|
|
51
|
+
.trim()
|
|
52
|
+
.split('\n')
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map((line) => JSON.parse(line));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getTranslation(text: string, challengeId: number, targetLang: string): Promise<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getIcoaDir } from './config.js';
|
|
4
|
+
import { translateText } from './gemini.js';
|
|
5
|
+
function getCachePath(lang, challengeId) {
|
|
6
|
+
const dir = join(getIcoaDir(), 'translations', lang);
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
return join(dir, `${challengeId}.json`);
|
|
9
|
+
}
|
|
10
|
+
export async function getTranslation(text, challengeId, targetLang) {
|
|
11
|
+
if (targetLang === 'en')
|
|
12
|
+
return text;
|
|
13
|
+
const cachePath = getCachePath(targetLang, challengeId);
|
|
14
|
+
// Check cache
|
|
15
|
+
if (existsSync(cachePath)) {
|
|
16
|
+
try {
|
|
17
|
+
const cached = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
18
|
+
if (cached.translation)
|
|
19
|
+
return cached.translation;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Cache corrupt, regenerate
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Translate via Gemini
|
|
26
|
+
const translation = await translateText(text, getLangName(targetLang));
|
|
27
|
+
// Cache result
|
|
28
|
+
writeFileSync(cachePath, JSON.stringify({ original: text, translation, timestamp: new Date().toISOString() }));
|
|
29
|
+
return translation;
|
|
30
|
+
}
|
|
31
|
+
function getLangName(code) {
|
|
32
|
+
const names = {
|
|
33
|
+
en: 'English',
|
|
34
|
+
zh: 'Chinese (Simplified)',
|
|
35
|
+
ja: 'Japanese',
|
|
36
|
+
ko: 'Korean',
|
|
37
|
+
es: 'Spanish',
|
|
38
|
+
};
|
|
39
|
+
return names[code] || 'English';
|
|
40
|
+
}
|
package/dist/lib/ui.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function printSuccess(msg: string): void;
|
|
2
|
+
export declare function printError(msg: string): void;
|
|
3
|
+
export declare function printWarning(msg: string): void;
|
|
4
|
+
export declare function printInfo(msg: string): void;
|
|
5
|
+
export declare function printTable(headers: string[], rows: string[][]): void;
|
|
6
|
+
export declare function printMarkdown(text: string): void;
|
|
7
|
+
export declare function createSpinner(text: string): import("ora").Ora;
|
|
8
|
+
export declare function formatCountdown(targetDate: Date): string;
|
|
9
|
+
export declare function printHeader(title: string): void;
|
|
10
|
+
export declare function printKeyValue(key: string, value: string): void;
|
package/dist/lib/ui.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { Marked } from 'marked';
|
|
5
|
+
import { markedTerminal } from 'marked-terminal';
|
|
6
|
+
const marked = new Marked(markedTerminal());
|
|
7
|
+
export function printSuccess(msg) {
|
|
8
|
+
console.log(chalk.green('✓ ') + msg);
|
|
9
|
+
}
|
|
10
|
+
export function printError(msg) {
|
|
11
|
+
console.log(chalk.red('✗ ') + msg);
|
|
12
|
+
}
|
|
13
|
+
export function printWarning(msg) {
|
|
14
|
+
console.log(chalk.yellow('⚠ ') + msg);
|
|
15
|
+
}
|
|
16
|
+
export function printInfo(msg) {
|
|
17
|
+
console.log(chalk.blue('ℹ ') + msg);
|
|
18
|
+
}
|
|
19
|
+
export function printTable(headers, rows) {
|
|
20
|
+
const table = new Table({
|
|
21
|
+
head: headers.map((h) => chalk.cyan.bold(h)),
|
|
22
|
+
style: { head: [], border: [] },
|
|
23
|
+
});
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
table.push(row);
|
|
26
|
+
}
|
|
27
|
+
console.log(table.toString());
|
|
28
|
+
}
|
|
29
|
+
export function printMarkdown(text) {
|
|
30
|
+
const rendered = marked.parse(text);
|
|
31
|
+
if (typeof rendered === 'string') {
|
|
32
|
+
console.log(rendered);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function createSpinner(text) {
|
|
36
|
+
return ora({ text, color: 'cyan' });
|
|
37
|
+
}
|
|
38
|
+
export function formatCountdown(targetDate) {
|
|
39
|
+
const now = new Date();
|
|
40
|
+
const diff = targetDate.getTime() - now.getTime();
|
|
41
|
+
if (diff <= 0)
|
|
42
|
+
return '00:00:00';
|
|
43
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
44
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
45
|
+
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
46
|
+
return [
|
|
47
|
+
hours.toString().padStart(2, '0'),
|
|
48
|
+
minutes.toString().padStart(2, '0'),
|
|
49
|
+
seconds.toString().padStart(2, '0'),
|
|
50
|
+
].join(':');
|
|
51
|
+
}
|
|
52
|
+
export function printHeader(title) {
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(chalk.cyan.bold(` ${title}`));
|
|
55
|
+
console.log(chalk.cyan(' ' + '─'.repeat(title.length + 4)));
|
|
56
|
+
}
|
|
57
|
+
export function printKeyValue(key, value) {
|
|
58
|
+
console.log(` ${chalk.gray(key + ':')} ${value}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export interface CTFdChallenge {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
category: string;
|
|
6
|
+
value: number;
|
|
7
|
+
type: string;
|
|
8
|
+
state: string;
|
|
9
|
+
max_attempts: number;
|
|
10
|
+
tags: string[];
|
|
11
|
+
files: string[];
|
|
12
|
+
hints: {
|
|
13
|
+
id: number;
|
|
14
|
+
cost: number;
|
|
15
|
+
}[];
|
|
16
|
+
solves: number;
|
|
17
|
+
solved_by_me: boolean;
|
|
18
|
+
connection_info?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CTFdChallengeListItem {
|
|
21
|
+
id: number;
|
|
22
|
+
name: string;
|
|
23
|
+
category: string;
|
|
24
|
+
value: number;
|
|
25
|
+
type: string;
|
|
26
|
+
solves: number;
|
|
27
|
+
solved_by_me: boolean;
|
|
28
|
+
tags: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface CTFdUser {
|
|
31
|
+
id: number;
|
|
32
|
+
name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
team_id: number | null;
|
|
35
|
+
country: string | null;
|
|
36
|
+
score: number;
|
|
37
|
+
place: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface CTFdTeam {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
score: number;
|
|
43
|
+
place: string | null;
|
|
44
|
+
members: {
|
|
45
|
+
id: number;
|
|
46
|
+
name: string;
|
|
47
|
+
}[];
|
|
48
|
+
}
|
|
49
|
+
export interface CTFdScoreboardEntry {
|
|
50
|
+
pos: number;
|
|
51
|
+
account_id: number;
|
|
52
|
+
account_url: string;
|
|
53
|
+
account_type: string;
|
|
54
|
+
name: string;
|
|
55
|
+
score: number;
|
|
56
|
+
members?: {
|
|
57
|
+
id: number;
|
|
58
|
+
name: string;
|
|
59
|
+
}[];
|
|
60
|
+
}
|
|
61
|
+
export interface CTFdAttemptResponse {
|
|
62
|
+
success: boolean;
|
|
63
|
+
data: {
|
|
64
|
+
status: 'correct' | 'incorrect' | 'already_solved' | 'paused' | 'ratelimited';
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export interface CTFdApiResponse<T> {
|
|
69
|
+
success: boolean;
|
|
70
|
+
data: T;
|
|
71
|
+
errors?: string[];
|
|
72
|
+
}
|
|
73
|
+
export interface CTFdTokenResponse {
|
|
74
|
+
success: boolean;
|
|
75
|
+
data: {
|
|
76
|
+
id: number;
|
|
77
|
+
type: string;
|
|
78
|
+
value: string;
|
|
79
|
+
created: string;
|
|
80
|
+
expiration: string;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export interface IcoaConfig {
|
|
84
|
+
ctfdUrl: string;
|
|
85
|
+
token: string;
|
|
86
|
+
language: string;
|
|
87
|
+
sessionId: string;
|
|
88
|
+
competitionCode: string;
|
|
89
|
+
teamId: number | null;
|
|
90
|
+
userId: number | null;
|
|
91
|
+
userName: string;
|
|
92
|
+
teamName: string;
|
|
93
|
+
currentChallengeId: number | null;
|
|
94
|
+
currentChallengeName: string;
|
|
95
|
+
currentChallengeCategory: string;
|
|
96
|
+
competitionState: CompetitionState;
|
|
97
|
+
competitionStartsAt: string;
|
|
98
|
+
competitionEndsAt: string;
|
|
99
|
+
geminiApiKey: string;
|
|
100
|
+
}
|
|
101
|
+
export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
|
|
102
|
+
export type HintLevel = 'A' | 'B' | 'C';
|
|
103
|
+
export interface HintBudget {
|
|
104
|
+
a: number;
|
|
105
|
+
b: number;
|
|
106
|
+
c: number;
|
|
107
|
+
tokensUsed: number;
|
|
108
|
+
tokenCap: number;
|
|
109
|
+
}
|
|
110
|
+
export interface LogEntry {
|
|
111
|
+
timestamp: string;
|
|
112
|
+
level: 'A' | 'B' | 'C' | 'command' | 'submit';
|
|
113
|
+
input: string;
|
|
114
|
+
challengeId?: number;
|
|
115
|
+
sessionId: string;
|
|
116
|
+
}
|
|
117
|
+
export interface ChallengeContext {
|
|
118
|
+
name: string;
|
|
119
|
+
category: string;
|
|
120
|
+
description?: string;
|
|
121
|
+
}
|
|
122
|
+
export declare const DEFAULT_BUDGET: HintBudget;
|
|
123
|
+
export declare const DEFAULT_CONFIG: IcoaConfig;
|
|
124
|
+
export declare const SUPPORTED_LANGUAGES: readonly ["en", "zh", "ja", "ko", "es"];
|
|
125
|
+
export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ========================
|
|
2
|
+
// CTFd API Response Types
|
|
3
|
+
// ========================
|
|
4
|
+
export const DEFAULT_BUDGET = {
|
|
5
|
+
a: 50,
|
|
6
|
+
b: 10,
|
|
7
|
+
c: 2,
|
|
8
|
+
tokensUsed: 0,
|
|
9
|
+
tokenCap: 50000,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_CONFIG = {
|
|
12
|
+
ctfdUrl: '',
|
|
13
|
+
token: '',
|
|
14
|
+
language: 'en',
|
|
15
|
+
sessionId: '',
|
|
16
|
+
competitionCode: '',
|
|
17
|
+
teamId: null,
|
|
18
|
+
userId: null,
|
|
19
|
+
userName: '',
|
|
20
|
+
teamName: '',
|
|
21
|
+
currentChallengeId: null,
|
|
22
|
+
currentChallengeName: '',
|
|
23
|
+
currentChallengeCategory: '',
|
|
24
|
+
competitionState: 'unknown',
|
|
25
|
+
competitionStartsAt: '',
|
|
26
|
+
competitionEndsAt: '',
|
|
27
|
+
geminiApiKey: '',
|
|
28
|
+
};
|
|
29
|
+
export const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko', 'es'];
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "icoa-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"icoa": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"refs"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ctf",
|
|
20
|
+
"cli",
|
|
21
|
+
"cybersecurity",
|
|
22
|
+
"icoa",
|
|
23
|
+
"competition"
|
|
24
|
+
],
|
|
25
|
+
"license": "SEE LICENSE IN licenses/apache-2.0.txt",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@google/generative-ai": "^0.24.0",
|
|
28
|
+
"chalk": "^5.4.1",
|
|
29
|
+
"cli-table3": "^0.6.5",
|
|
30
|
+
"commander": "^13.1.0",
|
|
31
|
+
"@inquirer/prompts": "^7.5.0",
|
|
32
|
+
"marked": "^15.0.7",
|
|
33
|
+
"marked-terminal": "^7.3.0",
|
|
34
|
+
"ora": "^8.2.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.15.3",
|
|
38
|
+
"typescript": "^5.8.3"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|