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,66 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { CTFdClient } from '../lib/ctfd-client.js';
|
|
4
|
+
import { getConfig, isConnected } from '../lib/config.js';
|
|
5
|
+
import { logCommand } from '../lib/logger.js';
|
|
6
|
+
import { printError, printInfo, printSuccess, createSpinner } from '../lib/ui.js';
|
|
7
|
+
export function registerConnectCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('connect <id>')
|
|
10
|
+
.description('Connect to challenge remote target')
|
|
11
|
+
.action(async (id) => {
|
|
12
|
+
logCommand(`connect ${id}`);
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
if (!isConnected()) {
|
|
15
|
+
printError('Not connected. Run: icoa ctf join <url>');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const client = new CTFdClient(config.ctfdUrl, config.token);
|
|
19
|
+
const spinner = createSpinner('Fetching connection info...');
|
|
20
|
+
spinner.start();
|
|
21
|
+
try {
|
|
22
|
+
const challenge = await client.getChallenge(parseInt(id));
|
|
23
|
+
spinner.stop();
|
|
24
|
+
if (!challenge.connection_info) {
|
|
25
|
+
printInfo('This challenge has no connection information.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const connInfo = challenge.connection_info.trim();
|
|
29
|
+
printInfo(`Connection: ${chalk.white(connInfo)}`);
|
|
30
|
+
console.log();
|
|
31
|
+
// Parse connection info and execute
|
|
32
|
+
if (connInfo.match(/^nc\s+/i) || connInfo.match(/^ncat\s+/i)) {
|
|
33
|
+
// netcat connection
|
|
34
|
+
printSuccess('Connecting...');
|
|
35
|
+
execSync(connInfo, { stdio: 'inherit' });
|
|
36
|
+
}
|
|
37
|
+
else if (connInfo.match(/^ssh\s+/i)) {
|
|
38
|
+
// SSH connection
|
|
39
|
+
printSuccess('Connecting via SSH...');
|
|
40
|
+
execSync(connInfo, { stdio: 'inherit' });
|
|
41
|
+
}
|
|
42
|
+
else if (connInfo.includes(':')) {
|
|
43
|
+
// host:port format
|
|
44
|
+
const [host, port] = connInfo.split(':');
|
|
45
|
+
printSuccess(`Connecting to ${host}:${port}...`);
|
|
46
|
+
execSync(`nc ${host} ${port}`, { stdio: 'inherit' });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
printInfo('Could not auto-detect connection type.');
|
|
50
|
+
printInfo(`Connection info: ${connInfo}`);
|
|
51
|
+
printInfo('Connect manually using the information above.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err.status !== undefined) {
|
|
56
|
+
// Process exited (normal for nc/ssh)
|
|
57
|
+
console.log();
|
|
58
|
+
printInfo('Connection closed.');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
spinner.fail('Failed to connect');
|
|
62
|
+
printError(err.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { input, select } from '@inquirer/prompts';
|
|
3
|
+
import { CTFdClient } from '../lib/ctfd-client.js';
|
|
4
|
+
import { getConfig, saveConfig, getBudget } from '../lib/config.js';
|
|
5
|
+
import { logCommand, logSubmission } from '../lib/logger.js';
|
|
6
|
+
import { getTranslation } from '../lib/translation.js';
|
|
7
|
+
import { printSuccess, printError, printWarning, printInfo, printTable, printMarkdown, printHeader, printKeyValue, createSpinner, formatCountdown, } from '../lib/ui.js';
|
|
8
|
+
function requireConnection() {
|
|
9
|
+
const config = getConfig();
|
|
10
|
+
if (!config.ctfdUrl || !config.token) {
|
|
11
|
+
printError('Not connected to CTFd. Run: icoa ctf join <url>');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return { config, client: new CTFdClient(config.ctfdUrl, config.token) };
|
|
15
|
+
}
|
|
16
|
+
export function registerCtfCommands(program) {
|
|
17
|
+
const ctf = program.command('ctf').description('Competition commands');
|
|
18
|
+
// ─── icoa ctf join <url> ───
|
|
19
|
+
ctf
|
|
20
|
+
.command('join <url>')
|
|
21
|
+
.description('Connect to a CTFd instance')
|
|
22
|
+
.action(async (url) => {
|
|
23
|
+
logCommand(`ctf join ${url}`);
|
|
24
|
+
console.log();
|
|
25
|
+
printInfo(`Connecting to ${chalk.bold(url)}`);
|
|
26
|
+
const authMethod = await select({
|
|
27
|
+
message: 'Authentication method:',
|
|
28
|
+
choices: [
|
|
29
|
+
{ name: 'Access Token (from CTFd Settings)', value: 'token' },
|
|
30
|
+
{ name: 'Username & Password', value: 'login' },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
let token;
|
|
34
|
+
if (authMethod === 'token') {
|
|
35
|
+
token = await input({ message: 'Enter your CTFd Access Token:' });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const username = await input({ message: 'Username:' });
|
|
39
|
+
const password = await input({ message: 'Password:' });
|
|
40
|
+
const spinner = createSpinner('Logging in...');
|
|
41
|
+
spinner.start();
|
|
42
|
+
try {
|
|
43
|
+
const client = new CTFdClient(url, '');
|
|
44
|
+
token = await client.loginWithCredentials(username, password);
|
|
45
|
+
spinner.succeed('Login successful');
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
spinner.fail('Login failed');
|
|
49
|
+
printError(err.message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Test connection
|
|
54
|
+
const spinner = createSpinner('Testing connection...');
|
|
55
|
+
spinner.start();
|
|
56
|
+
try {
|
|
57
|
+
const client = new CTFdClient(url, token);
|
|
58
|
+
const user = await client.testConnection();
|
|
59
|
+
spinner.succeed('Connected successfully');
|
|
60
|
+
console.log();
|
|
61
|
+
printKeyValue('User', user.name);
|
|
62
|
+
printKeyValue('Score', String(user.score || 0));
|
|
63
|
+
if (user.team_id) {
|
|
64
|
+
printKeyValue('Team ID', String(user.team_id));
|
|
65
|
+
}
|
|
66
|
+
// Fetch competition metadata (times, mode)
|
|
67
|
+
try {
|
|
68
|
+
const meta = await client.getCompetitionMeta();
|
|
69
|
+
const configUpdate = {
|
|
70
|
+
ctfdUrl: url,
|
|
71
|
+
token: token,
|
|
72
|
+
userId: user.id,
|
|
73
|
+
userName: user.name,
|
|
74
|
+
teamId: user.team_id,
|
|
75
|
+
};
|
|
76
|
+
if (meta.start) {
|
|
77
|
+
configUpdate.competitionStartsAt = new Date(meta.start * 1000).toISOString();
|
|
78
|
+
printKeyValue('Starts', new Date(meta.start * 1000).toLocaleString());
|
|
79
|
+
}
|
|
80
|
+
if (meta.end) {
|
|
81
|
+
configUpdate.competitionEndsAt = new Date(meta.end * 1000).toISOString();
|
|
82
|
+
printKeyValue('Ends', new Date(meta.end * 1000).toLocaleString());
|
|
83
|
+
}
|
|
84
|
+
// Determine competition state
|
|
85
|
+
const now = Date.now() / 1000;
|
|
86
|
+
if (meta.start && now < meta.start) {
|
|
87
|
+
configUpdate.competitionState = 'pre_competition';
|
|
88
|
+
}
|
|
89
|
+
else if (meta.end && now > meta.end) {
|
|
90
|
+
configUpdate.competitionState = 'finished';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
configUpdate.competitionState = 'live';
|
|
94
|
+
}
|
|
95
|
+
printKeyValue('Status', configUpdate.competitionState);
|
|
96
|
+
saveConfig(configUpdate);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Fallback: save without competition times
|
|
100
|
+
saveConfig({
|
|
101
|
+
ctfdUrl: url,
|
|
102
|
+
token: token,
|
|
103
|
+
userId: user.id,
|
|
104
|
+
userName: user.name,
|
|
105
|
+
teamId: user.team_id,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
console.log();
|
|
109
|
+
printSuccess('Connection saved. You are ready to compete!');
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
spinner.fail('Connection failed');
|
|
113
|
+
printError(err.message);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// ─── icoa ctf activate <code> ───
|
|
117
|
+
ctf
|
|
118
|
+
.command('activate <code>')
|
|
119
|
+
.description('Validate competition code')
|
|
120
|
+
.action(async (code) => {
|
|
121
|
+
logCommand(`ctf activate ${code}`);
|
|
122
|
+
const { client } = requireConnection();
|
|
123
|
+
const spinner = createSpinner('Validating competition code...');
|
|
124
|
+
spinner.start();
|
|
125
|
+
try {
|
|
126
|
+
// Try custom ICOA endpoint first
|
|
127
|
+
const res = await fetch(getConfig().ctfdUrl + '/api/v1/icoa/activate', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `Token ${getConfig().token}`,
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({ code }),
|
|
134
|
+
});
|
|
135
|
+
if (res.ok) {
|
|
136
|
+
const data = (await res.json());
|
|
137
|
+
spinner.succeed('Competition code validated');
|
|
138
|
+
saveConfig({
|
|
139
|
+
competitionCode: code,
|
|
140
|
+
teamId: data.data?.team_id || getConfig().teamId,
|
|
141
|
+
competitionState: 'live',
|
|
142
|
+
});
|
|
143
|
+
printSuccess(`Activated: ${code}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Fallback: just store the code locally
|
|
147
|
+
spinner.succeed('Competition code saved');
|
|
148
|
+
saveConfig({ competitionCode: code });
|
|
149
|
+
printInfo('Code stored locally. Server validation will be attempted during competition.');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Server doesn't have custom endpoint, store locally
|
|
154
|
+
spinner.succeed('Competition code saved');
|
|
155
|
+
saveConfig({ competitionCode: code });
|
|
156
|
+
printInfo('Code stored locally.');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// ─── icoa ctf challenges ───
|
|
160
|
+
ctf
|
|
161
|
+
.command('challenges')
|
|
162
|
+
.description('List all challenges')
|
|
163
|
+
.action(async () => {
|
|
164
|
+
logCommand('ctf challenges');
|
|
165
|
+
const { client } = requireConnection();
|
|
166
|
+
const spinner = createSpinner('Loading challenges...');
|
|
167
|
+
spinner.start();
|
|
168
|
+
try {
|
|
169
|
+
const challenges = await client.getChallenges();
|
|
170
|
+
spinner.stop();
|
|
171
|
+
if (!challenges || challenges.length === 0) {
|
|
172
|
+
printInfo('No challenges available yet.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Group by category
|
|
176
|
+
const byCategory = new Map();
|
|
177
|
+
for (const c of challenges) {
|
|
178
|
+
const cat = c.category || 'Uncategorized';
|
|
179
|
+
if (!byCategory.has(cat))
|
|
180
|
+
byCategory.set(cat, []);
|
|
181
|
+
byCategory.get(cat).push(c);
|
|
182
|
+
}
|
|
183
|
+
const solved = challenges.filter((c) => c.solved_by_me).length;
|
|
184
|
+
printHeader(`Challenges (${solved}/${challenges.length} solved)`);
|
|
185
|
+
// Sort categories and display
|
|
186
|
+
const rows = [...byCategory.entries()]
|
|
187
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
188
|
+
.flatMap(([category, challs]) => {
|
|
189
|
+
const catRow = [chalk.cyan.bold(`── ${category} ──`), '', '', '', ''];
|
|
190
|
+
const challRows = challs
|
|
191
|
+
.sort((a, b) => a.value - b.value)
|
|
192
|
+
.map((c) => [
|
|
193
|
+
String(c.id),
|
|
194
|
+
c.solved_by_me ? chalk.gray.strikethrough(c.name) : c.name,
|
|
195
|
+
chalk.gray(c.category),
|
|
196
|
+
c.solved_by_me ? chalk.gray(String(c.value)) : chalk.yellow(String(c.value)),
|
|
197
|
+
c.solved_by_me ? chalk.green('✓') : chalk.gray('○'),
|
|
198
|
+
]);
|
|
199
|
+
return [catRow, ...challRows];
|
|
200
|
+
});
|
|
201
|
+
printTable(['#', 'Name', 'Category', 'Points', 'Solved'], rows);
|
|
202
|
+
console.log(chalk.gray(` ${challenges.length} challenges, ${solved} solved`));
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
spinner.fail('Failed to load challenges');
|
|
206
|
+
printError(err.message);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// ─── icoa ctf open <id> ───
|
|
210
|
+
ctf
|
|
211
|
+
.command('open <id>')
|
|
212
|
+
.description('View challenge details')
|
|
213
|
+
.action(async (id) => {
|
|
214
|
+
logCommand(`ctf open ${id}`);
|
|
215
|
+
const { config, client } = requireConnection();
|
|
216
|
+
const spinner = createSpinner('Loading challenge...');
|
|
217
|
+
spinner.start();
|
|
218
|
+
try {
|
|
219
|
+
const challenge = await client.getChallenge(parseInt(id));
|
|
220
|
+
spinner.stop();
|
|
221
|
+
// Save current challenge context for hints
|
|
222
|
+
saveConfig({
|
|
223
|
+
currentChallengeId: challenge.id,
|
|
224
|
+
currentChallengeName: challenge.name,
|
|
225
|
+
currentChallengeCategory: challenge.category,
|
|
226
|
+
});
|
|
227
|
+
printHeader(`${challenge.name} [${challenge.category}]`);
|
|
228
|
+
printKeyValue('Points', String(challenge.value));
|
|
229
|
+
printKeyValue('Solves', String(challenge.solves));
|
|
230
|
+
if (challenge.connection_info) {
|
|
231
|
+
printKeyValue('Connection', challenge.connection_info);
|
|
232
|
+
}
|
|
233
|
+
console.log();
|
|
234
|
+
// Display description (with optional translation)
|
|
235
|
+
let description = challenge.description;
|
|
236
|
+
if (config.language !== 'en') {
|
|
237
|
+
try {
|
|
238
|
+
description = await getTranslation(description, challenge.id, config.language);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Translation failed, use English
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
printMarkdown(description);
|
|
245
|
+
// Show files
|
|
246
|
+
if (challenge.files && challenge.files.length > 0) {
|
|
247
|
+
console.log();
|
|
248
|
+
printInfo(`Files (${challenge.files.length}): use ${chalk.white(`icoa files ${id}`)} to download`);
|
|
249
|
+
}
|
|
250
|
+
// Show hints
|
|
251
|
+
if (challenge.hints && challenge.hints.length > 0) {
|
|
252
|
+
printInfo(`CTFd Hints available: ${challenge.hints.length}`);
|
|
253
|
+
}
|
|
254
|
+
// Show connection info
|
|
255
|
+
if (challenge.connection_info) {
|
|
256
|
+
console.log();
|
|
257
|
+
printInfo(`Quick connect: ${chalk.white(`icoa connect ${id}`)}`);
|
|
258
|
+
}
|
|
259
|
+
// Show helpful next actions
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(chalk.gray(` Next: icoa hint "how to approach this?" | icoa ctf submit ${id} "icoa{flag}"`));
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
spinner.fail('Failed to load challenge');
|
|
265
|
+
printError(err.message);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
// ─── icoa ctf submit <id> <flag> ───
|
|
269
|
+
ctf
|
|
270
|
+
.command('submit <id> <flag>')
|
|
271
|
+
.description('Submit a flag')
|
|
272
|
+
.action(async (id, flag) => {
|
|
273
|
+
logCommand(`ctf submit ${id}`);
|
|
274
|
+
logSubmission(parseInt(id), flag);
|
|
275
|
+
const { client } = requireConnection();
|
|
276
|
+
const spinner = createSpinner('Submitting flag...');
|
|
277
|
+
spinner.start();
|
|
278
|
+
try {
|
|
279
|
+
const result = await client.submitFlag(parseInt(id), flag);
|
|
280
|
+
spinner.stop();
|
|
281
|
+
switch (result.status) {
|
|
282
|
+
case 'correct':
|
|
283
|
+
console.log();
|
|
284
|
+
console.log(chalk.green.bold(' 🎉 Correct! ') + chalk.white(result.message));
|
|
285
|
+
console.log();
|
|
286
|
+
break;
|
|
287
|
+
case 'incorrect':
|
|
288
|
+
printError('Incorrect. ' + result.message);
|
|
289
|
+
break;
|
|
290
|
+
case 'already_solved':
|
|
291
|
+
printWarning('Already solved.');
|
|
292
|
+
break;
|
|
293
|
+
case 'paused':
|
|
294
|
+
printWarning('Competition is paused.');
|
|
295
|
+
break;
|
|
296
|
+
case 'ratelimited':
|
|
297
|
+
printWarning('Rate limited. Please wait before trying again.');
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
printInfo(result.message || 'Unknown response');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
spinner.fail('Submission failed');
|
|
305
|
+
printError(err.message);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// ─── icoa ctf scoreboard ───
|
|
309
|
+
ctf
|
|
310
|
+
.command('scoreboard [top]')
|
|
311
|
+
.description('View scoreboard (optional: top N)')
|
|
312
|
+
.action(async (top) => {
|
|
313
|
+
logCommand('ctf scoreboard');
|
|
314
|
+
const { config, client } = requireConnection();
|
|
315
|
+
const spinner = createSpinner('Loading scoreboard...');
|
|
316
|
+
spinner.start();
|
|
317
|
+
try {
|
|
318
|
+
const scoreboard = await client.getScoreboard();
|
|
319
|
+
spinner.stop();
|
|
320
|
+
if (!scoreboard || scoreboard.length === 0) {
|
|
321
|
+
printInfo('Scoreboard is empty.');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
printHeader('Scoreboard');
|
|
325
|
+
const rows = scoreboard.map((entry) => {
|
|
326
|
+
const isMyTeam = entry.account_id === config.teamId;
|
|
327
|
+
const row = [
|
|
328
|
+
String(entry.pos),
|
|
329
|
+
entry.name,
|
|
330
|
+
String(entry.score),
|
|
331
|
+
];
|
|
332
|
+
if (isMyTeam) {
|
|
333
|
+
return row.map((cell) => chalk.yellow.bold(cell));
|
|
334
|
+
}
|
|
335
|
+
return row;
|
|
336
|
+
});
|
|
337
|
+
printTable(['Rank', 'Team', 'Score'], rows);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
spinner.fail('Failed to load scoreboard');
|
|
341
|
+
printError(err.message);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
// ─── icoa ctf status ───
|
|
345
|
+
ctf
|
|
346
|
+
.command('status')
|
|
347
|
+
.description('Show competition status')
|
|
348
|
+
.action(async () => {
|
|
349
|
+
logCommand('ctf status');
|
|
350
|
+
const { config, client } = requireConnection();
|
|
351
|
+
printHeader('Competition Status');
|
|
352
|
+
// Competition state
|
|
353
|
+
const stateColors = {
|
|
354
|
+
pre_competition: chalk.yellow,
|
|
355
|
+
demo: chalk.blue,
|
|
356
|
+
live: chalk.green,
|
|
357
|
+
finished: chalk.red,
|
|
358
|
+
unknown: chalk.gray,
|
|
359
|
+
};
|
|
360
|
+
const stateLabels = {
|
|
361
|
+
pre_competition: 'PRE-COMPETITION',
|
|
362
|
+
demo: 'DEMO',
|
|
363
|
+
live: 'LIVE',
|
|
364
|
+
finished: 'FINISHED',
|
|
365
|
+
unknown: 'UNKNOWN',
|
|
366
|
+
};
|
|
367
|
+
const state = config.competitionState || 'unknown';
|
|
368
|
+
const colorFn = stateColors[state] || chalk.gray;
|
|
369
|
+
console.log(` ${chalk.gray('State:')} ${chalk.bold(colorFn(stateLabels[state] || state))}`);
|
|
370
|
+
// User info
|
|
371
|
+
try {
|
|
372
|
+
const user = await client.testConnection();
|
|
373
|
+
printKeyValue('User', chalk.white.bold(user.name));
|
|
374
|
+
printKeyValue('Score', chalk.yellow.bold(String(user.score || 0)));
|
|
375
|
+
printKeyValue('Rank', chalk.cyan(user.place || 'N/A'));
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
printKeyValue('User', config.userName || 'Unknown');
|
|
379
|
+
}
|
|
380
|
+
// Hint budget with visual bars
|
|
381
|
+
console.log();
|
|
382
|
+
console.log(` ${chalk.gray('Hint Budget:')}`);
|
|
383
|
+
const budget = getBudget();
|
|
384
|
+
const bar = (used, total, color) => {
|
|
385
|
+
const width = 20;
|
|
386
|
+
const filled = Math.round((used / total) * width);
|
|
387
|
+
const empty = width - filled;
|
|
388
|
+
return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty)) + ` ${used}/${total}`;
|
|
389
|
+
};
|
|
390
|
+
console.log(` A ${bar(budget.a, 50, chalk.green)}`);
|
|
391
|
+
console.log(` B ${bar(budget.b, 10, chalk.yellow)}`);
|
|
392
|
+
console.log(` C ${bar(budget.c, 2, chalk.red)}`);
|
|
393
|
+
console.log(` ${chalk.gray('Tokens')} ${bar(budget.tokenCap - budget.tokensUsed, budget.tokenCap, chalk.cyan)}`);
|
|
394
|
+
// Time
|
|
395
|
+
if (config.competitionEndsAt) {
|
|
396
|
+
console.log();
|
|
397
|
+
const endsAt = new Date(config.competitionEndsAt);
|
|
398
|
+
const now = new Date();
|
|
399
|
+
if (now < endsAt) {
|
|
400
|
+
printKeyValue('Time Remaining', chalk.yellow.bold(formatCountdown(endsAt)));
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
printKeyValue('Time', chalk.red('Competition ended'));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
console.log();
|
|
407
|
+
});
|
|
408
|
+
// ─── icoa ctf time ───
|
|
409
|
+
ctf
|
|
410
|
+
.command('time')
|
|
411
|
+
.description('Show competition countdown')
|
|
412
|
+
.action(() => {
|
|
413
|
+
logCommand('ctf time');
|
|
414
|
+
const config = getConfig();
|
|
415
|
+
if (!config.competitionStartsAt && !config.competitionEndsAt) {
|
|
416
|
+
printInfo('Competition times not configured.');
|
|
417
|
+
printInfo('They will be set when the competition server provides timing information.');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const now = new Date();
|
|
421
|
+
const startsAt = config.competitionStartsAt ? new Date(config.competitionStartsAt) : null;
|
|
422
|
+
const endsAt = config.competitionEndsAt ? new Date(config.competitionEndsAt) : null;
|
|
423
|
+
printHeader('Competition Timer');
|
|
424
|
+
if (startsAt && now < startsAt) {
|
|
425
|
+
console.log(chalk.yellow(' Competition has not started yet.'));
|
|
426
|
+
console.log();
|
|
427
|
+
const update = () => {
|
|
428
|
+
process.stdout.write(`\r ${chalk.bold('Starts in:')} ${formatCountdown(startsAt)} `);
|
|
429
|
+
};
|
|
430
|
+
update();
|
|
431
|
+
const interval = setInterval(() => {
|
|
432
|
+
if (new Date() >= startsAt) {
|
|
433
|
+
clearInterval(interval);
|
|
434
|
+
console.log();
|
|
435
|
+
printSuccess('Competition has started! Good luck!');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
update();
|
|
439
|
+
}, 1000);
|
|
440
|
+
process.on('SIGINT', () => {
|
|
441
|
+
clearInterval(interval);
|
|
442
|
+
console.log();
|
|
443
|
+
process.exit(0);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else if (endsAt && now < endsAt) {
|
|
447
|
+
console.log(chalk.green(' Competition is LIVE'));
|
|
448
|
+
console.log();
|
|
449
|
+
const update = () => {
|
|
450
|
+
process.stdout.write(`\r ${chalk.bold('Time remaining:')} ${formatCountdown(endsAt)} `);
|
|
451
|
+
};
|
|
452
|
+
update();
|
|
453
|
+
const interval = setInterval(() => {
|
|
454
|
+
if (new Date() >= endsAt) {
|
|
455
|
+
clearInterval(interval);
|
|
456
|
+
console.log();
|
|
457
|
+
printWarning('Competition has ended!');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
update();
|
|
461
|
+
}, 1000);
|
|
462
|
+
process.on('SIGINT', () => {
|
|
463
|
+
clearInterval(interval);
|
|
464
|
+
console.log();
|
|
465
|
+
process.exit(0);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
console.log(chalk.red(' Competition has ended.'));
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { CTFdClient } from '../lib/ctfd-client.js';
|
|
5
|
+
import { getConfig, isConnected } from '../lib/config.js';
|
|
6
|
+
import { logCommand } from '../lib/logger.js';
|
|
7
|
+
import { printError, createSpinner } from '../lib/ui.js';
|
|
8
|
+
export function registerFilesCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('files <id>')
|
|
11
|
+
.description('Download challenge files')
|
|
12
|
+
.action(async (id) => {
|
|
13
|
+
logCommand(`files ${id}`);
|
|
14
|
+
const config = getConfig();
|
|
15
|
+
if (!isConnected()) {
|
|
16
|
+
printError('Not connected. Run: icoa ctf join <url>');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const client = new CTFdClient(config.ctfdUrl, config.token);
|
|
20
|
+
const destDir = join(homedir(), 'icoa-challenges', id);
|
|
21
|
+
const spinner = createSpinner('Fetching challenge files...');
|
|
22
|
+
spinner.start();
|
|
23
|
+
try {
|
|
24
|
+
const files = await client.getChallengeFiles(parseInt(id));
|
|
25
|
+
if (!files || files.length === 0) {
|
|
26
|
+
spinner.info('No files attached to this challenge.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
spinner.text = `Downloading ${files.length} file(s)...`;
|
|
30
|
+
const downloaded = [];
|
|
31
|
+
for (const filePath of files) {
|
|
32
|
+
try {
|
|
33
|
+
const dest = await client.downloadFile(filePath, destDir);
|
|
34
|
+
downloaded.push(dest);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
spinner.warn(`Failed to download: ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
spinner.succeed(`Downloaded ${downloaded.length} file(s)`);
|
|
41
|
+
console.log(chalk.gray(` Location: ${destDir}`));
|
|
42
|
+
for (const f of downloaded) {
|
|
43
|
+
console.log(chalk.gray(` → ${f.split('/').pop()}`));
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
spinner.fail('Failed to download files');
|
|
49
|
+
printError(err.message);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|