icarus-cmd 0.3.1

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.
@@ -0,0 +1,82 @@
1
+ // Stockage des résultats : un simple fichier JSON (pas de dépendance native à
2
+ // compiler sous Windows). Tous les outils écrivent leurs "findings" normalisés
3
+ // au même endroit -> c'est ça, "tout regrouper".
4
+
5
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
10
+ export const DATA_DIR = join(ROOT, 'data');
11
+ const DB_FILE = join(DATA_DIR, 'db.json');
12
+
13
+ function ensureDir() {
14
+ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
15
+ }
16
+
17
+ function load() {
18
+ ensureDir();
19
+ if (!existsSync(DB_FILE)) return { scans: [], findings: [] };
20
+ try {
21
+ return JSON.parse(readFileSync(DB_FILE, 'utf8'));
22
+ } catch {
23
+ return { scans: [], findings: [] };
24
+ }
25
+ }
26
+
27
+ function save(db) {
28
+ ensureDir();
29
+ writeFileSync(DB_FILE, JSON.stringify(db, null, 2), 'utf8');
30
+ }
31
+
32
+ // Enregistre une exécution d'outil et renvoie son id.
33
+ export function recordScan({ tool, target, code, command }) {
34
+ const db = load();
35
+ const scan = {
36
+ id: crypto.randomUUID(),
37
+ tool,
38
+ target,
39
+ code,
40
+ command,
41
+ startedAt: new Date().toISOString(),
42
+ };
43
+ db.scans.push(scan);
44
+ save(db);
45
+ return scan.id;
46
+ }
47
+
48
+ // Ajoute des findings normalisés (le runner les enrichit avec scanId/tool/target).
49
+ export function addFindings(findings) {
50
+ if (!findings?.length) return [];
51
+ const db = load();
52
+ const enriched = findings.map((f) => ({
53
+ id: crypto.randomUUID(),
54
+ timestamp: new Date().toISOString(),
55
+ severity: 'unknown',
56
+ type: 'finding',
57
+ ...f,
58
+ }));
59
+ db.findings.push(...enriched);
60
+ save(db);
61
+ return enriched;
62
+ }
63
+
64
+ export function allFindings() {
65
+ return load().findings;
66
+ }
67
+
68
+ export function findingsForTarget(target) {
69
+ return load().findings.filter((f) => f.target === target);
70
+ }
71
+
72
+ export function allScans() {
73
+ return load().scans;
74
+ }
75
+
76
+ export function targets() {
77
+ return [...new Set(load().findings.map((f) => f.target))];
78
+ }
79
+
80
+ export function clearAll() {
81
+ save({ scans: [], findings: [] });
82
+ }
@@ -0,0 +1,5 @@
1
+ // Helpers de cible partagés (TUI + Web).
2
+
3
+ export const toHost = (t) => String(t).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
4
+ export const toUrl = (t) => (/^https?:\/\//.test(t) ? t : `http://${t}`);
5
+ export const effectiveTarget = (tool, target) => (tool.needs === 'url' ? toUrl(target) : toHost(target));
package/src/index.js ADDED
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env node
2
+ // Icarus — point d'entrée.
3
+ // (sans argument) -> TUI interactive
4
+ // doctor -> état des outils (installé / manquant)
5
+ // scan <cible> [options] -> scan non interactif (scriptable)
6
+ // report -> rapport HTML + JSON depuis les résultats
7
+ // help -> aide
8
+
9
+ import chalk from 'chalk';
10
+ import Table from 'cli-table3';
11
+ import { select, input, checkbox, confirm } from '@inquirer/prompts';
12
+
13
+ import { TOOLS, PROFILES, getTool, toolStatus, availableIds } from './core/registry.js';
14
+ import { runTool } from './core/runner.js';
15
+ import { allFindings, findingsForTarget, allScans, clearAll, targets, recordScan, addFindings } from './core/store.js';
16
+ import { analyzeCode } from './core/codescan.js';
17
+ import { setupTools } from './core/setup.js';
18
+ import { existsSync } from 'node:fs';
19
+ import { banner, intro, rule } from './ui/banner.js';
20
+ import { startSpinner, ICON, chevron, sleep, isTTY } from './ui/anim.js';
21
+ import { renderFindings } from './ui/results.js';
22
+ import { generateReport } from './report/html.js';
23
+ import { startWeb } from './web/server.js';
24
+ import { paint, rank } from './core/severity.js';
25
+ import { spawn } from 'node:child_process';
26
+
27
+ const toHost = (t) => String(t).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
28
+ const toUrl = (t) => (/^https?:\/\//.test(t) ? t : `http://${t}`);
29
+ const effectiveTarget = (tool, target) => (tool.needs === 'url' ? toUrl(target) : toHost(target));
30
+
31
+ function openBrowser(url) {
32
+ const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
33
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
34
+ try { spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref(); } catch { /* ignore */ }
35
+ }
36
+
37
+ // ---------- Affichages partagés ----------
38
+
39
+ function statusTable({ fresh = false } = {}) {
40
+ const table = new Table({
41
+ head: ['Outil', 'Catégorie', 'État', 'Chemin / installation'].map((h) => chalk.bold(h)),
42
+ style: { head: [], border: ['grey'] },
43
+ colWidths: [14, 12, 16, 58],
44
+ wordWrap: true,
45
+ });
46
+ for (const { tool, bin, available } of toolStatus({ fresh })) {
47
+ table.push([
48
+ tool.name,
49
+ tool.category,
50
+ available ? chalk.green('✓ installé') : chalk.red('✗ manquant'),
51
+ available ? chalk.gray(bin) : chalk.yellow(tool.install),
52
+ ]);
53
+ }
54
+ return table.toString();
55
+ }
56
+
57
+ function summarize(findings) {
58
+ if (!findings.length) return chalk.gray('aucun finding');
59
+ const counts = {};
60
+ for (const f of findings) counts[f.severity] = (counts[f.severity] || 0) + 1;
61
+ return Object.entries(counts)
62
+ .sort((a, b) => rank(a[0]) - rank(b[0]))
63
+ .map(([s, n]) => paint(chalk, s, `${s}:${n}`))
64
+ .join(' ');
65
+ }
66
+
67
+ // Résout une liste d'ids d'outils en gardant ceux installés, et signale les autres.
68
+ function resolveTools(ids) {
69
+ const avail = new Set(availableIds());
70
+ const run = ids.filter((id) => getTool(id) && avail.has(id));
71
+ const skipped = ids.filter((id) => getTool(id) && !avail.has(id));
72
+ return { run, skipped };
73
+ }
74
+
75
+ // ---------- Exécution d'un scan ----------
76
+
77
+ const FAST_OPTS = { fast: true, topPorts: '100', depth: 1, severity: 'critical,high', level: 1, risk: 1 };
78
+
79
+ async function runScan(target, toolIds, { opts = {}, quiet = false, parallel = false } = {}) {
80
+ const all = [];
81
+
82
+ if (parallel) {
83
+ console.log('');
84
+ let done = 0;
85
+ const total = toolIds.length;
86
+ const spin = startSpinner(chalk.gray(`0/${total} — ${toolIds.map((id) => getTool(id).name).join(', ')}`));
87
+ await Promise.all(toolIds.map((id) => {
88
+ const tool = getTool(id);
89
+ const et = effectiveTarget(tool, target);
90
+ return runTool(tool, et, { opts }).then((res) => {
91
+ done += 1;
92
+ if (!res.ok && res.error) spin.log(chalk.red(` ⚠ ${chalk.bold(tool.name)}: ${res.error}`));
93
+ else spin.log(` ${chalk.green('✓')} ${chalk.bold(tool.name)} ${summarize(res.findings)}`);
94
+ spin.update(chalk.gray(`${done}/${total} terminés…`));
95
+ all.push(...res.findings);
96
+ });
97
+ }));
98
+ spin.stop(chalk.gray(` ▸ ${total} outils terminés.`));
99
+ } else {
100
+ for (const id of toolIds) {
101
+ const tool = getTool(id);
102
+ const et = effectiveTarget(tool, target);
103
+ console.log('\n' + chalk.bgHex('#f59e0b').black.bold(` ${tool.name} `) + chalk.hex('#fb923c')(` → ${et}`));
104
+ const res = await runTool(tool, et, {
105
+ opts,
106
+ onLog: (l) => console.log(chalk.gray(l)),
107
+ onData: quiet ? undefined : (s) => process.stdout.write(chalk.dim(s)),
108
+ });
109
+ if (!res.ok && res.error) console.log(chalk.red(` ⚠ ${res.error}`));
110
+ console.log(chalk.bold(` ${tool.name}: `) + summarize(res.findings));
111
+ all.push(...res.findings);
112
+ }
113
+ }
114
+
115
+ console.log('\n' + chalk.bold('Bilan du scan : ') + summarize(all));
116
+ return all;
117
+ }
118
+
119
+ // ---------- TUI interactive ----------
120
+
121
+ async function chooseTools(target) {
122
+ const mode = await select({
123
+ message: 'Comment choisir les outils ?',
124
+ choices: [
125
+ ...Object.entries(PROFILES).map(([id, p]) => ({
126
+ name: `${chevron} Profil ${chalk.bold(p.label)} ${chalk.gray(`(${p.tools.length} outils)`)}`,
127
+ value: `profile:${id}`,
128
+ })),
129
+ { name: `${chevron} Sélection manuelle`, value: 'manual' },
130
+ ],
131
+ });
132
+
133
+ if (mode.startsWith('profile:')) {
134
+ const { run, skipped } = resolveTools(PROFILES[mode.split(':')[1]].tools);
135
+ if (skipped.length) console.log(chalk.gray(` (ignorés, non installés : ${skipped.join(', ')})`));
136
+ return run;
137
+ }
138
+
139
+ const statuses = toolStatus();
140
+ return checkbox({
141
+ message: 'Outils à lancer (espace = cocher) :',
142
+ choices: statuses.map(({ tool, available }) => ({
143
+ name: `${tool.name.padEnd(10)} ${chalk.gray(tool.description)}${available ? '' : chalk.red(' [manquant]')}`,
144
+ value: tool.id,
145
+ checked: available && tool.category === 'recon',
146
+ disabled: available ? false : `→ ${tool.install}`,
147
+ })),
148
+ });
149
+ }
150
+
151
+ async function gatherOpts(picks, target) {
152
+ const opts = {};
153
+ if (picks.includes('nmap')) {
154
+ opts.vuln = await confirm({ message: 'nmap : activer les scripts NSE de détection de vulns (--script vuln) ?', default: false });
155
+ }
156
+ if (picks.includes('sqlmap')) {
157
+ const url = toUrl(target);
158
+ if (!url.includes('?')) console.log(chalk.yellow(` Astuce sqlmap : une URL avec paramètre marche mieux (ex: ${url}?id=1)`));
159
+ opts.level = Number(await input({ message: 'sqlmap --level (1-5) :', default: '1' })) || 1;
160
+ }
161
+ return opts;
162
+ }
163
+
164
+ async function tui() {
165
+ await intro();
166
+ let target = targets()[0] || null;
167
+
168
+ for (;;) {
169
+ const nAvail = availableIds().length;
170
+ console.log(
171
+ '\n' + chalk.gray('Cible : ') +
172
+ (target ? chalk.hex('#fb923c')(target) : chalk.red('(aucune)')) +
173
+ chalk.gray(` · outils dispo : ${nAvail}/${TOOLS.length}`)
174
+ );
175
+
176
+ const action = await select({
177
+ message: 'Menu Icarus',
178
+ choices: [
179
+ { name: `${chevron} Définir / changer la cible`, value: 'target' },
180
+ { name: `${chevron} Lancer un scan`, value: 'scan', disabled: !target && '(définis une cible)' },
181
+ { name: `${chevron} Voir les résultats`, value: 'results' },
182
+ { name: `${chevron} Générer un rapport (HTML + JSON)`, value: 'report' },
183
+ { name: `${chevron} État des outils`, value: 'status' },
184
+ { name: `${chevron} Effacer les résultats`, value: 'clear' },
185
+ { name: `${chevron} Quitter`, value: 'quit' },
186
+ ],
187
+ });
188
+
189
+ if (action === 'quit') { console.log(chalk.gray(`${ICON.star} Bon vol.`)); return; }
190
+
191
+ if (action === 'target') {
192
+ const v = (await input({ message: 'Cible (IP, hostname ou URL) :', default: target || '' })).trim();
193
+ if (v) target = v;
194
+ }
195
+
196
+ if (action === 'status') console.log('\n' + statusTable({ fresh: true }));
197
+
198
+ if (action === 'clear') {
199
+ if (await confirm({ message: 'Effacer TOUS les résultats stockés ?', default: false })) {
200
+ clearAll();
201
+ console.log(chalk.yellow('Résultats effacés.'));
202
+ }
203
+ }
204
+
205
+ if (action === 'results') {
206
+ const ts = targets();
207
+ const scope = ts.length > 1
208
+ ? await select({ message: 'Quels résultats ?', choices: [{ name: 'Tous', value: '__all__' }, ...ts.map((t) => ({ name: t, value: t }))] })
209
+ : '__all__';
210
+ const data = scope === '__all__' ? allFindings() : findingsForTarget(scope);
211
+ console.log(renderFindings(data, { title: `Résultats — ${scope === '__all__' ? 'tout' : scope}` }));
212
+ }
213
+
214
+ if (action === 'report') {
215
+ const { htmlPath, jsonPath } = generateReport(allFindings(), { scans: allScans() });
216
+ console.log(chalk.green('\n✓ Rapport généré :'));
217
+ console.log(' ' + chalk.hex('#fb923c')(htmlPath));
218
+ console.log(' ' + chalk.gray(jsonPath));
219
+ }
220
+
221
+ if (action === 'scan') {
222
+ const picks = await chooseTools(target);
223
+ if (!picks.length) { console.log(chalk.yellow('Aucun outil disponible/sélectionné.')); continue; }
224
+
225
+ const mode = await select({
226
+ message: 'Mode',
227
+ choices: [
228
+ { name: `${chevron} Rapide (parallèle, options légères)`, value: 'fast' },
229
+ { name: `${chevron} Parallèle (tout en même temps)`, value: 'parallel' },
230
+ { name: `${chevron} Séquentiel (sortie live détaillée)`, value: 'seq' },
231
+ ],
232
+ });
233
+
234
+ const opts = mode === 'fast' ? { ...FAST_OPTS, timeout: 60 } : await gatherOpts(picks, target);
235
+
236
+ if (await confirm({ message: `Lancer ${picks.length} outil(s) sur ${chalk.hex('#fb923c')(target)} ?`, default: true })) {
237
+ console.log('\n' + rule('SCAN'));
238
+ const found = await runScan(target, picks, { opts, parallel: mode !== 'seq' });
239
+ console.log(renderFindings(found, { title: `Résultats — ${target}` }));
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ // ---------- CLI non interactive ----------
246
+
247
+ function parseFlags(args) {
248
+ const flags = {};
249
+ const positional = [];
250
+ for (let i = 0; i < args.length; i++) {
251
+ if (args[i].startsWith('--')) {
252
+ const key = args[i].slice(2);
253
+ const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
254
+ flags[key] = val;
255
+ } else positional.push(args[i]);
256
+ }
257
+ return { flags, positional };
258
+ }
259
+
260
+ function help() {
261
+ console.log(banner());
262
+ console.log(`${chalk.bold('Usage')}
263
+ ${chalk.hex('#fb923c')('icarus')} TUI interactive (terminal)
264
+ ${chalk.hex('#fb923c')('icarus web')} Lance l'interface web locale (http://localhost:7373)
265
+ ${chalk.hex('#fb923c')('icarus setup')} Télécharge les outils (nuclei, httpx, ffuf…)
266
+ ${chalk.hex('#fb923c')('icarus doctor')} État des outils (installé/manquant)
267
+ ${chalk.hex('#fb923c')('icarus scan <cible>')} Scan non interactif
268
+ ${chalk.hex('#fb923c')('icarus code <fichier|dossier>')} Analyse statique du code (SAST) — failles dans le code
269
+ ${chalk.hex('#fb923c')('icarus report')} Rapport HTML + JSON
270
+ ${chalk.hex('#fb923c')('icarus help')} Cette aide
271
+
272
+ ${chalk.gray('icarus web --port 8080 change le port')}
273
+ ${chalk.gray('icarus web --no-open ne pas ouvrir le navigateur')}
274
+
275
+ ${chalk.bold('Options de scan')}
276
+ ${chalk.gray('--fast mode rapide (parallèle + options légères)')}
277
+ ${chalk.gray('--parallel lance tous les outils en même temps')}
278
+ ${chalk.gray('--profile recon|web|vuln|full enchaîne un profil d\'outils')}
279
+ ${chalk.gray('--tools nmap,nuclei,sqlmap choisit des outils précis')}
280
+ ${chalk.gray('--vuln nmap: scripts NSE de vulns')}
281
+ ${chalk.gray('--level 3 --risk 2 options sqlmap')}
282
+ ${chalk.gray('--timeout 90 délai max par outil (s)')}
283
+ ${chalk.gray('--quiet sans sortie live')}
284
+
285
+ ${chalk.bold('Profils')}
286
+ ${Object.entries(PROFILES).map(([id, p]) => ` ${chalk.hex('#fb923c')(id.padEnd(7))} ${p.label} ${chalk.gray(`(${p.tools.join(', ')})`)}`).join('\n')}
287
+
288
+ ${chalk.bold('Exemples')}
289
+ ${chalk.gray('icarus scan example.com --profile recon')}
290
+ ${chalk.gray('icarus scan http://site/p?id=1 --tools sqlmap --level 3')}
291
+ `);
292
+ }
293
+
294
+ async function main() {
295
+ const [cmd, ...rest] = process.argv.slice(2);
296
+
297
+ if (!cmd) return tui();
298
+ if (['help', '-h', '--help'].includes(cmd)) return help();
299
+
300
+ if (cmd === 'doctor') {
301
+ console.log(banner());
302
+ console.log(statusTable({ fresh: true }));
303
+ return;
304
+ }
305
+
306
+ if (cmd === 'web') {
307
+ const { flags } = parseFlags(rest);
308
+ const port = Number(flags.port) || 7373;
309
+ console.log(banner());
310
+ const { url } = await startWeb({ port });
311
+ console.log(chalk.green('✓ Interface web Icarus lancée :') + ' ' + chalk.hex('#fb923c').bold(url));
312
+ console.log(chalk.gray(' (Ctrl+C pour arrêter)'));
313
+ if (!flags['no-open']) openBrowser(url);
314
+ return; // le serveur garde le process en vie
315
+ }
316
+
317
+ if (cmd === 'report') {
318
+ const { htmlPath, jsonPath } = generateReport(allFindings(), { scans: allScans() });
319
+ console.log(chalk.green('✓ Rapport généré :'));
320
+ console.log(' ' + htmlPath);
321
+ console.log(' ' + jsonPath);
322
+ return;
323
+ }
324
+
325
+ if (cmd === 'setup' || cmd === 'install') {
326
+ console.log(banner());
327
+ await setupTools();
328
+ return;
329
+ }
330
+
331
+ if (cmd === 'code' || cmd === 'sast') {
332
+ const { positional } = parseFlags(rest);
333
+ return doCodeScan(positional[0] || '.');
334
+ }
335
+
336
+ if (cmd === 'scan') {
337
+ const { flags, positional } = parseFlags(rest);
338
+ return doScanCli(positional[0], flags);
339
+ }
340
+
341
+ // Cible "nue" : `icarus example.com`, ou `icarus ./fichier.js` -> analyse code
342
+ if (!cmd.startsWith('-')) {
343
+ if (existsSync(cmd)) return doCodeScan(cmd); // chemin local => SAST
344
+ const { flags } = parseFlags(rest);
345
+ return doScanCli(cmd, flags);
346
+ }
347
+
348
+ console.log(chalk.red(`Commande inconnue : ${cmd}`));
349
+ help();
350
+ process.exit(1);
351
+ }
352
+
353
+ const SEV_COLOR = { critical: '#dc2626', high: '#f97316', medium: '#eab308', low: '#3b82f6', info: '#6b7280', unknown: '#9ca3af' };
354
+ const SEV_ICON = { critical: ICON.critical, high: ICON.high, medium: ICON.medium, low: ICON.low, info: ICON.info, unknown: ICON.info };
355
+ const sevTag = (s) => chalk.hex(SEV_COLOR[s] || '#9ca3af').bold(`${SEV_ICON[s] || '•'} ${String(s || '?').toUpperCase()}`);
356
+
357
+ // Analyse statique du code (SAST) d'un fichier ou dossier local.
358
+ async function doCodeScan(target) {
359
+ console.log('\n' + rule(`ANALYSE CODE — ${target}`));
360
+ const spin = startSpinner(chalk.gray('analyse du code…'));
361
+ const res = analyzeCode(target);
362
+ await sleep(180); // laisse voir le spinner même sur petit scan
363
+ spin.stop();
364
+ if (res.error) { console.log(chalk.red(` ${ICON.warn} ` + res.error)); process.exit(1); }
365
+
366
+ const scanId = recordScan({ tool: 'codescan', target, code: 0, command: `codescan ${target}` });
367
+ const stored = addFindings(res.findings.map((f) => ({ scanId, tool: 'codescan', target, ...f })));
368
+
369
+ console.log(chalk.gray(` ${res.files} fichier(s) analysé(s)\n`));
370
+ if (!stored.length) { console.log(chalk.green(` ${ICON.ok} Aucun motif de vulnérabilité détecté.\n`)); return; }
371
+
372
+ const order = ['critical', 'high', 'medium', 'low', 'info', 'unknown'];
373
+ stored.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity));
374
+ const stagger = isTTY() && stored.length <= 40;
375
+ for (const f of stored) {
376
+ console.log(' ' + sevTag(f.severity) + ' ' + chalk.bold(f.title));
377
+ console.log(' ' + chalk.gray(f.detail));
378
+ if (stagger) await sleep(35); // révélation animée
379
+ }
380
+ const counts = {};
381
+ for (const f of stored) counts[f.severity] = (counts[f.severity] || 0) + 1;
382
+ console.log('\n ' + chalk.bold('Bilan ') + order.filter((s) => counts[s]).map((s) => sevTag(s) + ' ' + counts[s]).join(' '));
383
+ console.log(chalk.gray(`\n ${ICON.report} Rapport complet : icarus report\n`));
384
+ }
385
+
386
+ async function doScanCli(target, flags) {
387
+ if (!target) { console.log(chalk.red('Cible manquante. Ex : icarus example.com · icarus scan example.com --profile recon')); process.exit(1); }
388
+
389
+ let ids;
390
+ if (flags.profile) {
391
+ const prof = PROFILES[flags.profile];
392
+ if (!prof) { console.log(chalk.red(`Profil inconnu : ${flags.profile}. Dispo : ${Object.keys(PROFILES).join(', ')}`)); process.exit(1); }
393
+ ids = prof.tools;
394
+ } else if (flags.tools) {
395
+ ids = String(flags.tools).split(',').map((s) => s.trim());
396
+ } else {
397
+ ids = availableIds();
398
+ }
399
+
400
+ const { run, skipped } = resolveTools(ids);
401
+ if (skipped.length) console.log(chalk.gray(`Ignorés (non installés) : ${skipped.join(', ')}`));
402
+ if (!run.length) { console.log(chalk.red('Aucun outil disponible. Lance : icarus doctor')); process.exit(1); }
403
+
404
+ const opts = {};
405
+ if (flags.fast) Object.assign(opts, FAST_OPTS);
406
+ if (flags.vuln) opts.vuln = true;
407
+ if (flags.scripts) opts.scripts = true;
408
+ if (flags.level) opts.level = Number(flags.level);
409
+ if (flags.risk) opts.risk = Number(flags.risk);
410
+ if (flags.depth) opts.depth = Number(flags.depth);
411
+ if (flags.wordlist) opts.wordlist = String(flags.wordlist);
412
+ opts.timeout = Number(flags.timeout) || (flags.fast ? 60 : 0);
413
+
414
+ const parallel = Boolean(flags.parallel || flags.fast);
415
+
416
+ console.log(banner());
417
+ await runScan(target, run, { opts, quiet: Boolean(flags.quiet), parallel });
418
+ console.log(chalk.gray('\nGénère le rapport avec : ') + chalk.hex('#fb923c')('icarus report') + chalk.gray(' · ou ouvre le web : ') + chalk.hex('#fb923c')('icarus web'));
419
+ }
420
+
421
+ main().catch((err) => {
422
+ if (err?.name === 'ExitPromptError') { console.log(chalk.gray('\nInterrompu.')); process.exit(0); }
423
+ console.error(chalk.red('Erreur :'), err?.message || err);
424
+ process.exit(1);
425
+ });
@@ -0,0 +1,166 @@
1
+ // Génère un rapport HTML autonome (un seul fichier, interactif) + un export JSON,
2
+ // à partir des findings et scans stockés.
3
+
4
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { DATA_DIR } from '../core/store.js';
7
+ import { rank, normalize, SEVERITIES } from '../core/severity.js';
8
+
9
+ const COLORS = {
10
+ critical: '#b91c1c', high: '#ef4444', medium: '#f59e0b',
11
+ low: '#06b6d4', info: '#64748b', unknown: '#475569',
12
+ };
13
+
14
+ const esc = (s) =>
15
+ String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
16
+
17
+ export function generateReport(findings, { scans = [] } = {}) {
18
+ const reportsDir = join(DATA_DIR, 'reports');
19
+ if (!existsSync(reportsDir)) mkdirSync(reportsDir, { recursive: true });
20
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
21
+
22
+ const jsonPath = join(reportsDir, `icarus-${stamp}.json`);
23
+ writeFileSync(jsonPath, JSON.stringify({ tool: 'Icarus', generatedAt: new Date().toISOString(), scans, findings }, null, 2));
24
+
25
+ const sorted = [...findings].sort((a, b) => rank(a.severity) - rank(b.severity) || String(a.target).localeCompare(b.target));
26
+ const counts = {};
27
+ for (const f of findings) { const s = normalize(f.severity); counts[s] = (counts[s] || 0) + 1; }
28
+ const total = findings.length || 0;
29
+ const targetsList = [...new Set(findings.map((f) => f.target))];
30
+ const toolsUsed = [...new Set(findings.map((f) => f.tool))];
31
+
32
+ // Cartes de synthèse par sévérité.
33
+ const cards = SEVERITIES.filter((s) => counts[s]).map((sev) =>
34
+ `<div class="card" style="--c:${COLORS[sev]}">
35
+ <div class="num">${counts[sev]}</div><div class="lbl">${esc(sev)}</div>
36
+ </div>`).join('');
37
+
38
+ // Barre de répartition.
39
+ const bar = SEVERITIES.filter((s) => counts[s]).map((s) =>
40
+ `<span style="width:${((counts[s] / total) * 100).toFixed(1)}%;background:${COLORS[s]}" title="${esc(s)}: ${counts[s]}"></span>`).join('');
41
+
42
+ // Puces de filtre.
43
+ const chips = SEVERITIES.filter((s) => counts[s]).map((s) =>
44
+ `<button class="chip on" data-sev="${s}" style="--c:${COLORS[s]}">${esc(s)} <b>${counts[s]}</b></button>`).join('');
45
+
46
+ const rows = sorted.map((f) => {
47
+ const sev = normalize(f.severity);
48
+ const hay = `${f.tool} ${f.target} ${f.title} ${f.detail} ${f.ref}`.toLowerCase();
49
+ return `<tr data-sev="${sev}" data-text="${esc(hay)}">
50
+ <td><span class="sev" style="background:${COLORS[sev]}">${esc(sev)}</span></td>
51
+ <td class="mono">${esc(f.tool)}</td>
52
+ <td class="mono">${esc(f.target)}</td>
53
+ <td>${esc(f.title)}</td>
54
+ <td class="muted">${esc(f.detail || '')}</td>
55
+ <td class="mono muted">${esc(f.ref || '')}</td>
56
+ </tr>`;
57
+ }).join('');
58
+
59
+ const scanRows = scans.map((s) =>
60
+ `<tr><td class="mono">${esc(s.tool)}</td><td class="mono">${esc(s.target)}</td>
61
+ <td>${s.code === 0 ? '<span class="ok">0</span>' : `<span class="bad">${esc(s.code)}</span>`}</td>
62
+ <td class="mono muted">${esc(s.command)}</td>
63
+ <td class="muted">${esc(new Date(s.startedAt).toLocaleString('fr-FR'))}</td></tr>`).join('');
64
+
65
+ const html = `<!doctype html><html lang="fr"><head><meta charset="utf-8">
66
+ <meta name="viewport" content="width=device-width,initial-scale=1">
67
+ <title>Icarus — Rapport de pentest</title>
68
+ <style>
69
+ :root{--bg:#0b1120;--panel:#111c33;--panel2:#16213b;--line:#243149;--txt:#e2e8f0;--muted:#94a3b8;--accent:#f59e0b}
70
+ *{box-sizing:border-box}
71
+ body{font:14px/1.55 'Segoe UI',system-ui,sans-serif;margin:0;background:var(--bg);color:var(--txt)}
72
+ header{padding:28px 36px;background:linear-gradient(135deg,#1e293b,#0b1120);border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
73
+ .brand{font-size:26px;font-weight:800;letter-spacing:1px}
74
+ .brand span{color:var(--accent)}
75
+ .sub{color:var(--muted);font-size:13px;margin-top:4px}
76
+ .meta{text-align:right;color:var(--muted);font-size:13px}
77
+ section{padding:22px 36px}
78
+ h2{font-size:13px;text-transform:uppercase;letter-spacing:1.4px;color:var(--muted);margin:0 0 14px}
79
+ .cards{display:flex;gap:14px;flex-wrap:wrap}
80
+ .card{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--c);border-radius:12px;padding:14px 22px;min-width:104px}
81
+ .card .num{font-size:30px;font-weight:800;color:var(--c)}
82
+ .card .lbl{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted)}
83
+ .stat{display:flex;gap:26px;flex-wrap:wrap;margin-top:8px;color:var(--muted)}
84
+ .stat b{color:var(--txt);font-size:18px}
85
+ .bar{display:flex;height:14px;border-radius:8px;overflow:hidden;margin-top:16px;border:1px solid var(--line)}
86
+ .bar span{display:block}
87
+ .controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
88
+ .chip{background:transparent;border:1px solid var(--c);color:var(--c);border-radius:20px;padding:5px 13px;font-size:12px;cursor:pointer;text-transform:uppercase;letter-spacing:.5px;opacity:.45;transition:.15s}
89
+ .chip.on{background:var(--c);color:#0b1120;opacity:1;font-weight:700}
90
+ .chip b{font-weight:800}
91
+ input.search{flex:1;min-width:200px;background:var(--panel);border:1px solid var(--line);color:var(--txt);border-radius:8px;padding:8px 12px;font-size:13px}
92
+ table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
93
+ th,td{padding:9px 12px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
94
+ th{background:var(--panel2);font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);position:sticky;top:0}
95
+ tr:hover td{background:#1a2740}
96
+ .sev{color:#fff;padding:2px 9px;border-radius:6px;font-size:11px;text-transform:uppercase;font-weight:700}
97
+ .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
98
+ .muted{color:var(--muted)}
99
+ .ok{color:#22c55e;font-weight:700}.bad{color:#ef4444;font-weight:700}
100
+ details{margin-top:8px}summary{cursor:pointer;color:var(--muted)}
101
+ footer{padding:24px 36px;color:var(--muted);font-size:12px;border-top:1px solid var(--line)}
102
+ .empty{padding:30px;text-align:center;color:var(--muted)}
103
+ </style></head><body>
104
+ <header>
105
+ <div><div class="brand">ICARUS<span>.</span></div><div class="sub">Plateforme de pentest tout-en-un — rapport consolidé</div></div>
106
+ <div class="meta">Généré le ${esc(new Date().toLocaleString('fr-FR'))}<br>${esc(targetsList.join(', ') || '—')}</div>
107
+ </header>
108
+
109
+ <section>
110
+ <h2>Résumé exécutif</h2>
111
+ <div class="cards">${cards || '<div class="card" style="--c:#475569"><div class="num">0</div><div class="lbl">findings</div></div>'}</div>
112
+ <div class="stat">
113
+ <div><b>${total}</b> findings</div>
114
+ <div><b>${targetsList.length}</b> cible(s)</div>
115
+ <div><b>${toolsUsed.length}</b> outil(s)</div>
116
+ <div><b>${scans.length}</b> scan(s)</div>
117
+ </div>
118
+ <div class="bar">${bar}</div>
119
+ </section>
120
+
121
+ <section>
122
+ <h2>Findings</h2>
123
+ <div class="controls">
124
+ ${chips}
125
+ <input class="search" id="q" placeholder="🔎 filtrer (outil, cible, texte…)">
126
+ </div>
127
+ <table id="tbl"><thead><tr>
128
+ <th>Sévérité</th><th>Outil</th><th>Cible</th><th>Finding</th><th>Détail</th><th>Réf</th>
129
+ </tr></thead><tbody>${rows || '<tr><td colspan="6" class="empty">Aucun résultat enregistré.</td></tr>'}</tbody></table>
130
+ </section>
131
+
132
+ <section>
133
+ <details><summary>Annexe — journal des scans (${scans.length})</summary>
134
+ <table style="margin-top:12px"><thead><tr><th>Outil</th><th>Cible</th><th>Code</th><th>Commande</th><th>Date</th></tr></thead>
135
+ <tbody>${scanRows || '<tr><td colspan="5" class="empty">—</td></tr>'}</tbody></table>
136
+ </details>
137
+ </section>
138
+
139
+ <footer>Rapport généré par <b>Icarus</b> · usage strictement autorisé. Les findings doivent être validés manuellement (faux positifs possibles).</footer>
140
+
141
+ <script>
142
+ const active = new Set([...document.querySelectorAll('.chip')].map(c=>c.dataset.sev));
143
+ const rows = [...document.querySelectorAll('#tbl tbody tr')];
144
+ const q = document.getElementById('q');
145
+ function apply(){
146
+ const term = q.value.trim().toLowerCase();
147
+ for(const r of rows){
148
+ if(!r.dataset.sev) continue;
149
+ const okSev = active.has(r.dataset.sev);
150
+ const okTxt = !term || (r.dataset.text||'').includes(term);
151
+ r.style.display = okSev && okTxt ? '' : 'none';
152
+ }
153
+ }
154
+ document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
155
+ c.classList.toggle('on');
156
+ if(active.has(c.dataset.sev)) active.delete(c.dataset.sev); else active.add(c.dataset.sev);
157
+ apply();
158
+ }));
159
+ q.addEventListener('input', apply);
160
+ </script>
161
+ </body></html>`;
162
+
163
+ const htmlPath = join(reportsDir, `icarus-${stamp}.html`);
164
+ writeFileSync(htmlPath, html, 'utf8');
165
+ return { htmlPath, jsonPath };
166
+ }