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,40 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export default {
5
+ id: 'whatweb',
6
+ name: 'WhatWeb',
7
+ category: 'recon',
8
+ description: 'Empreinte technologique (CMS, frameworks)',
9
+ needs: 'url',
10
+ needsWorkdir: true,
11
+ bins: ['whatweb'],
12
+ winPaths: [],
13
+ install: 'gem install whatweb (ou https://github.com/urbanadventurer/WhatWeb)',
14
+
15
+ command(target, opts = {}) {
16
+ const out = join(opts.workdir, 'whatweb.json');
17
+ return { args: [`--log-json=${out}`, '--no-errors', target] };
18
+ },
19
+
20
+ parse({ workdir, target }) {
21
+ const out = join(workdir || '', 'whatweb.json');
22
+ if (!workdir || !existsSync(out)) return [];
23
+ let arr;
24
+ try { arr = JSON.parse(readFileSync(out, 'utf8')); } catch { return []; }
25
+ const findings = [];
26
+ for (const entry of [].concat(arr)) {
27
+ const plugins = entry.plugins || {};
28
+ const techs = Object.keys(plugins);
29
+ if (!techs.length) continue;
30
+ findings.push({
31
+ type: 'fingerprint',
32
+ severity: 'info',
33
+ title: `Technos: ${techs.slice(0, 12).join(', ')}`,
34
+ detail: entry.target || target,
35
+ ref: 'whatweb',
36
+ });
37
+ }
38
+ return findings;
39
+ },
40
+ };
@@ -0,0 +1,58 @@
1
+ import { normalize } from '../core/severity.js';
2
+
3
+ export default {
4
+ id: 'wpscan',
5
+ name: 'WPScan',
6
+ category: 'web',
7
+ description: 'Scanner de sécurité WordPress',
8
+ needs: 'url',
9
+ bins: ['wpscan'],
10
+ winPaths: [],
11
+ install: 'gem install wpscan (https://wpscan.com)',
12
+
13
+ options: [
14
+ { key: 'enumerate', label: 'Énumérer', type: 'select', default: '', choices: [
15
+ { value: '', label: 'Défaut' }, { value: 'vp', label: 'Plugins vulnérables' },
16
+ { value: 'ap', label: 'Tous les plugins' }, { value: 'u', label: 'Utilisateurs' },
17
+ ] },
18
+ { key: 'apiToken', label: 'API token (WPScan)', type: 'text', placeholder: 'optionnel — enrichit les vulns' },
19
+ ],
20
+
21
+ command(target, opts = {}) {
22
+ const args = ['--url', target, '--format', 'json', '--no-banner'];
23
+ if (opts.apiToken) args.push('--api-token', opts.apiToken);
24
+ if (opts.enumerate) args.push('--enumerate', opts.enumerate);
25
+ return { args };
26
+ },
27
+
28
+ parse({ stdout, target }) {
29
+ const start = stdout.indexOf('{');
30
+ if (start < 0) return [];
31
+ let data;
32
+ try { data = JSON.parse(stdout.slice(start)); } catch { return []; }
33
+
34
+ const findings = [];
35
+ const pushVulns = (vulns, label) => {
36
+ for (const v of vulns || []) {
37
+ findings.push({
38
+ type: 'cms',
39
+ severity: normalize(v.severity) === 'unknown' ? 'high' : normalize(v.severity),
40
+ title: `${label}: ${v.title}`,
41
+ detail: (v.references?.cve || []).map((c) => `CVE-${c}`).join(', ') || target,
42
+ ref: 'wpscan',
43
+ });
44
+ }
45
+ };
46
+
47
+ if (data.version) {
48
+ pushVulns(data.version.vulnerabilities, `WordPress ${data.version.number || ''}`.trim());
49
+ }
50
+ for (const [name, p] of Object.entries(data.plugins || {})) pushVulns(p.vulnerabilities, `plugin ${name}`);
51
+ for (const [name, t] of Object.entries(data.themes || {})) pushVulns(t.vulnerabilities, `thème ${name}`);
52
+
53
+ if (data.version?.number && !findings.length) {
54
+ findings.push({ type: 'cms', severity: 'info', title: `WordPress ${data.version.number} détecté`, detail: target, ref: 'wpscan' });
55
+ }
56
+ return findings;
57
+ },
58
+ };
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { normalize } from '../core/severity.js';
4
+
5
+ const RISK = { 3: 'high', 2: 'medium', 1: 'low', 0: 'info' };
6
+
7
+ // java.exe en chemin absolu : spawn(shell:false) avec un cwd custom ne résout
8
+ // pas un nom court ("java") -> ENOENT. On le localise via JAVA_HOME puis PATH.
9
+ function findJava() {
10
+ const exe = process.platform === 'win32' ? 'java.exe' : 'java';
11
+ const cands = [];
12
+ if (process.env.JAVA_HOME) cands.push(join(process.env.JAVA_HOME, 'bin', exe));
13
+ cands.push('C:/Program Files/Common Files/Oracle/Java/javapath/' + exe);
14
+ for (const p of (process.env.PATH || '').split(process.platform === 'win32' ? ';' : ':')) {
15
+ if (p) cands.push(join(p, exe));
16
+ }
17
+ for (const c of cands) { if (existsSync(c)) return c; }
18
+ return 'java';
19
+ }
20
+
21
+ export default {
22
+ id: 'zap',
23
+ name: 'OWASP ZAP',
24
+ category: 'web',
25
+ description: "Scanner d'applications web (OWASP)",
26
+ needs: 'url',
27
+ needsWorkdir: true,
28
+ bins: ['zap.bat', 'zap.sh', 'zap'],
29
+ winPaths: [
30
+ 'C:/Program Files/ZAP/Zed Attack Proxy/zap.bat',
31
+ 'C:/Program Files (x86)/ZAP/Zed Attack Proxy/zap.bat',
32
+ ],
33
+ install: 'https://www.zaproxy.org/download/ (ou: docker run zaproxy/zap-stable)',
34
+
35
+ command(target, opts = {}) {
36
+ const report = join(opts.workdir, 'zap.json');
37
+ // zap.bat lance `java -jar zap-x.y.z.jar` en chemin RELATIF : lancé depuis
38
+ // un dossier temp, il ne trouve pas son .jar (échec instantané). On appelle
39
+ // donc java directement avec le .jar en chemin absolu, en exécutant depuis
40
+ // le dossier d'installation de ZAP (pour ses ressources).
41
+ const dir = opts.bin ? dirname(opts.bin) : '';
42
+ let jar = '';
43
+ try { jar = readdirSync(dir).find((f) => /^zap.*\.jar$/i.test(f)) || ''; } catch { /* ignore */ }
44
+ const jarPath = jar ? join(dir, jar) : join(dir, 'zap.jar');
45
+ return {
46
+ cmd: findJava(),
47
+ cwd: dir || undefined,
48
+ args: ['-Xmx512m', '-jar', jarPath, '-cmd', '-quickurl', target, '-quickout', report, '-quickprogress'],
49
+ };
50
+ },
51
+
52
+ parse({ workdir, target }) {
53
+ const report = join(workdir || '', 'zap.json');
54
+ if (!workdir || !existsSync(report)) return [];
55
+ let data;
56
+ try { data = JSON.parse(readFileSync(report, 'utf8')); } catch { return []; }
57
+
58
+ const findings = [];
59
+ for (const site of data.site || []) {
60
+ for (const alert of site.alerts || []) {
61
+ const uri = alert.instances?.[0]?.uri || site['@name'] || target;
62
+ findings.push({
63
+ type: 'web',
64
+ severity: normalize(RISK[Number(alert.riskcode)] ?? alert.riskdesc),
65
+ title: alert.name || alert.alert || 'alerte ZAP',
66
+ detail: uri,
67
+ ref: alert.cweid ? `CWE-${alert.cweid}` : 'zap',
68
+ });
69
+ }
70
+ }
71
+ return findings;
72
+ },
73
+ };
package/src/ui/anim.js ADDED
@@ -0,0 +1,59 @@
1
+ // Petites animations terminal : spinner live + helpers. Tout se désactive
2
+ // proprement hors TTY (sortie pipée / CI) pour ne pas polluer les logs.
3
+ import chalk from 'chalk';
4
+
5
+ const ESC = '\x1b';
6
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
+
8
+ // Jeu d'icônes — style "épuré" : chevron doré en préfixe de menu, points
9
+ // colorés pour les sévérités. Cohérent, rend partout (pas d'emoji).
10
+ export const GOLD = '#d4a843';
11
+ export const chevron = chalk.hex(GOLD)('❯');
12
+ export const ICON = {
13
+ chevron: '❯', dot: '●',
14
+ ok: '✓', miss: '✗', warn: '!', star: '✦', report: '❯',
15
+ // toutes les sévérités = même point, la couleur porte le sens
16
+ critical: '●', high: '●', medium: '●', low: '●', info: '●',
17
+ };
18
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
19
+ export const isTTY = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && !process.env.CI;
20
+
21
+ const HIDE = `${ESC}[?25l`;
22
+ const SHOW = `${ESC}[?25h`;
23
+ const CLEARLINE = `\r${ESC}[2K`;
24
+
25
+ // Spinner avec lignes de résultat qui défilent au-dessus (spin.log).
26
+ export function startSpinner(text) {
27
+ if (!isTTY()) {
28
+ return { update() {}, log: (l) => console.log(l), stop: (l) => l && console.log(l) };
29
+ }
30
+ let i = 0;
31
+ let cur = text;
32
+ process.stdout.write(HIDE);
33
+ const render = () => {
34
+ i = (i + 1) % FRAMES.length;
35
+ process.stdout.write(CLEARLINE + chalk.hex('#f59e0b')(FRAMES[i]) + ' ' + cur);
36
+ };
37
+ const timer = setInterval(render, 80);
38
+ render();
39
+ return {
40
+ update(t) { cur = t; },
41
+ log(line) { process.stdout.write(CLEARLINE + line + '\n'); render(); },
42
+ stop(line) {
43
+ clearInterval(timer);
44
+ process.stdout.write(CLEARLINE + (line ? line + '\n' : '') + SHOW);
45
+ },
46
+ };
47
+ }
48
+
49
+ // Affiche des lignes une par une avec un léger délai (effet "déploiement").
50
+ export async function revealLines(lines, { delay = 26, indent = '' } = {}) {
51
+ if (!isTTY()) { console.log(lines.map((l) => indent + l).join('\n')); return; }
52
+ for (const l of lines) {
53
+ process.stdout.write(indent + l + '\n');
54
+ await sleep(delay);
55
+ }
56
+ }
57
+
58
+ // Efface la ligne courante (utilisé pour nettoyer après un spinner inline).
59
+ export const clearLine = () => { if (isTTY()) process.stdout.write(CLEARLINE); };
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { LOGO } from './logo.js';
3
+ import { isTTY, sleep, revealLines } from './anim.js';
4
+
5
+ const GOLD = '#d4a843';
6
+ const GOLD_HI = '#fff3cf';
7
+ const INDENT = ' ';
8
+ const CLR = '\r\x1b[2K'; // retour début de ligne + efface la ligne
9
+ // Aligne le texte sous le centre du logo.
10
+ const PAD = ' '.repeat(Math.max(0, Math.round((LOGO.length ? 44 : 20) / 2) - 6));
11
+
12
+ function wordmark(highlight = -1) {
13
+ const letters = 'ICARUS'.split('');
14
+ return letters
15
+ .map((c, i) => (i === highlight ? chalk.hex(GOLD_HI).bold(c) : chalk.hex(GOLD).bold(c)))
16
+ .join(' ');
17
+ }
18
+
19
+ // Bannière statique (utilisée partout, y compris sortie pipée).
20
+ export function banner() {
21
+ return (
22
+ '\n' +
23
+ LOGO.map((l) => INDENT + l).join('\n') +
24
+ '\n\n' +
25
+ PAD + wordmark() +
26
+ '\n'
27
+ );
28
+ }
29
+
30
+ // Bannière animée : déploiement du logo + balayage lumineux sur ICARUS.
31
+ export async function intro() {
32
+ if (!isTTY()) { console.log(banner()); return; }
33
+ process.stdout.write('\n');
34
+ await revealLines(LOGO, { delay: 22, indent: INDENT });
35
+ process.stdout.write('\n');
36
+
37
+ // balayage lumineux lettre par lettre
38
+ for (let h = 0; h <= 6; h++) {
39
+ process.stdout.write(CLR + PAD + wordmark(h));
40
+ await sleep(55);
41
+ }
42
+ process.stdout.write(CLR + PAD + wordmark() + '\n');
43
+ console.log('');
44
+ }
45
+
46
+ export function rule(label = '') {
47
+ const line = '─'.repeat(Math.max(0, 60 - label.length));
48
+ return chalk.gray(`── ${chalk.white.bold(label)} ${line}`);
49
+ }
package/src/ui/logo.js ADDED
@@ -0,0 +1,20 @@
1
+ // Généré par scripts/gen-logo.mjs depuis logo.png — ne pas éditer à la main.
2
+ export const LOGO = [
3
+ " \u001b[38;2;229;206;164m\u001b[48;2;238;209;162m▀\u001b[0m\u001b[38;2;222;196;153m▄\u001b[0m \u001b[38;2;210;181;134m▄\u001b[0m\u001b[38;2;204;176;130m\u001b[48;2;222;187;138m▀\u001b[0m ",
4
+ " \u001b[38;2;220;189;140m\u001b[48;2;213;181;135m▀\u001b[0m\u001b[38;2;235;202;153m\u001b[48;2;229;195;144m▀\u001b[0m\u001b[38;2;222;194;150m\u001b[48;2;231;197;147m▀\u001b[0m\u001b[38;2;222;191;145m▄\u001b[0m \u001b[38;2;208;177;130m▄\u001b[0m\u001b[38;2;210;179;132m\u001b[48;2;216;179;128m▀\u001b[0m\u001b[38;2;218;182;131m\u001b[48;2;213;177;126m▀\u001b[0m\u001b[38;2;205;171;124m\u001b[48;2;196;163;116m▀\u001b[0m ",
5
+ " \u001b[38;2;223;188;137m\u001b[48;2;204;171;123m▀\u001b[0m\u001b[38;2;225;190;140m\u001b[48;2;218;182;131m▀\u001b[0m\u001b[38;2;227;192;141m\u001b[48;2;221;186;134m▀\u001b[0m\u001b[38;2;221;190;142m\u001b[48;2;223;186;135m▀\u001b[0m\u001b[38;2;222;190;143m▄\u001b[0m \u001b[38;2;208;176;126m▄\u001b[0m\u001b[38;2;202;171;124m\u001b[48;2;209;173;121m▀\u001b[0m\u001b[38;2;213;177;125m\u001b[48;2;208;171;120m▀\u001b[0m\u001b[38;2;211;174;123m\u001b[48;2;205;169;118m▀\u001b[0m\u001b[38;2;208;171;121m\u001b[48;2;191;156;110m▀\u001b[0m ",
6
+ " \u001b[38;2;209;174;124m▀\u001b[0m\u001b[38;2;214;177;126m\u001b[48;2;208;171;121m▀\u001b[0m\u001b[38;2;216;180;129m\u001b[48;2;209;172;121m▀\u001b[0m\u001b[38;2;216;180;128m\u001b[48;2;211;174;123m▀\u001b[0m\u001b[38;2;219;185;135m\u001b[48;2;211;174;122m▀\u001b[0m\u001b[38;2;212;177;126m▄\u001b[0m \u001b[38;2;202;167;117m▄\u001b[0m\u001b[38;2;206;172;122m\u001b[48;2;200;162;109m▀\u001b[0m\u001b[38;2;204;167;115m\u001b[48;2;199;162;111m▀\u001b[0m\u001b[38;2;204;167;116m\u001b[48;2;197;160;109m▀\u001b[0m\u001b[38;2;202;165;114m\u001b[48;2;194;157;107m▀\u001b[0m\u001b[38;2;196;160;110m\u001b[48;2;172;139;98m▀\u001b[0m ",
7
+ " \u001b[38;2;183;149;104m▀\u001b[0m\u001b[38;2;203;167;116m▀\u001b[0m\u001b[38;2;205;167;116m\u001b[48;2;194;157;106m▀\u001b[0m\u001b[38;2;206;168;117m\u001b[48;2;201;163;111m▀\u001b[0m\u001b[38;2;207;170;118m\u001b[48;2;201;162;110m▀\u001b[0m\u001b[38;2;206;169;118m\u001b[48;2;202;164;112m▀\u001b[0m\u001b[38;2;202;166;115m▄\u001b[0m \u001b[38;2;191;156;106m▄\u001b[0m\u001b[38;2;197;161;111m\u001b[48;2;193;154;103m▀\u001b[0m\u001b[38;2;197;158;106m\u001b[48;2;191;153;102m▀\u001b[0m\u001b[38;2;196;159;107m\u001b[48;2;191;153;102m▀\u001b[0m\u001b[38;2;194;156;105m\u001b[48;2;185;148;100m▀\u001b[0m\u001b[38;2;190;153;104m▀\u001b[0m\u001b[38;2;170;137;94m▀\u001b[0m ",
8
+ " \u001b[38;2;187;150;102m▀\u001b[0m\u001b[38;2;196;157;105m\u001b[48;2;180;144;97m▀\u001b[0m\u001b[38;2;197;158;105m\u001b[48;2;192;153;100m▀\u001b[0m\u001b[38;2;198;158;106m\u001b[48;2;192;152;99m▀\u001b[0m\u001b[38;2;197;160;108m\u001b[48;2;192;153;100m▀\u001b[0m\u001b[38;2;193;154;102m▄\u001b[0m \u001b[38;2;186;148;99m▄\u001b[0m\u001b[38;2;187;150;101m\u001b[48;2;187;147;96m▀\u001b[0m\u001b[38;2;188;150;99m\u001b[48;2;186;147;96m▀\u001b[0m\u001b[38;2;188;149;99m\u001b[48;2;184;145;96m▀\u001b[0m\u001b[38;2;187;149;99m\u001b[48;2;170;135;91m▀\u001b[0m\u001b[38;2;177;141;93m▀\u001b[0m ",
9
+ " \u001b[38;2;179;144;97m▀\u001b[0m\u001b[38;2;185;147;96m▄\u001b[0m\u001b[38;2;179;145;98m▄\u001b[0m \u001b[38;2;184;145;93m▀\u001b[0m\u001b[38;2;187;147;96m\u001b[48;2;178;139;88m▀\u001b[0m\u001b[38;2;186;147;95m\u001b[48;2;183;143;92m▀\u001b[0m\u001b[38;2;188;148;97m\u001b[48;2;182;142;90m▀\u001b[0m\u001b[38;2;182;149;104m\u001b[48;2;185;146;95m▀\u001b[0m\u001b[38;2;182;147;101m▄\u001b[0m \u001b[38;2;183;144;93m▄\u001b[0m\u001b[38;2;184;146;95m\u001b[48;2;180;140;89m▀\u001b[0m\u001b[38;2;184;143;92m\u001b[48;2;181;141;90m▀\u001b[0m\u001b[38;2;184;144;93m\u001b[48;2;176;138;89m▀\u001b[0m\u001b[38;2;181;142;93m▀\u001b[0m\u001b[38;2;162;127;83m▀\u001b[0m\u001b[38;2;176;144;98m▄\u001b[0m\u001b[38;2;178;140;91m▄\u001b[0m\u001b[38;2;170;137;91m▀\u001b[0m ",
10
+ " \u001b[38;2;167;130;81m▀\u001b[0m\u001b[38;2;179;139;87m\u001b[48;2;176;137;86m▀\u001b[0m\u001b[38;2;182;146;96m\u001b[48;2;175;136;85m▀\u001b[0m\u001b[38;2;175;137;87m▄\u001b[0m \u001b[38;2;171;133;84m▀\u001b[0m\u001b[38;2;180;141;89m\u001b[48;2;168;131;83m▀\u001b[0m\u001b[38;2;178;138;87m\u001b[48;2;177;137;85m▀\u001b[0m\u001b[38;2;181;142;90m\u001b[48;2;177;138;87m▀\u001b[0m\u001b[38;2;183;147;101m\u001b[48;2;174;136;87m▀\u001b[0m \u001b[38;2;224;193;145m▄\u001b[0m\u001b[38;2;228;196;147m▄\u001b[0m \u001b[38;2;176;140;93m\u001b[48;2;176;137;87m▀\u001b[0m\u001b[38;2;179;140;89m\u001b[48;2;176;136;85m▀\u001b[0m\u001b[38;2;177;137;86m\u001b[48;2;176;136;85m▀\u001b[0m\u001b[38;2;178;139;88m\u001b[48;2;167;130;83m▀\u001b[0m\u001b[38;2;170;133;84m▀\u001b[0m \u001b[38;2;175;138;89m▄\u001b[0m\u001b[38;2;177;142;94m\u001b[48;2;172;133;83m▀\u001b[0m\u001b[38;2;174;135;85m\u001b[48;2;169;131;83m▀\u001b[0m\u001b[38;2;167;129;82m▀\u001b[0m ",
11
+ " \u001b[38;2;172;133;83m▀\u001b[0m\u001b[38;2;173;133;83m\u001b[48;2;169;130;81m▀\u001b[0m\u001b[38;2;173;134;83m\u001b[48;2;170;131;82m▀\u001b[0m\u001b[38;2;177;142;97m\u001b[48;2;170;132;82m▀\u001b[0m\u001b[38;2;174;135;86m▄\u001b[0m\u001b[38;2;176;136;84m\u001b[48;2;172;132;81m▀\u001b[0m\u001b[38;2;174;134;83m\u001b[48;2;171;131;81m▀\u001b[0m\u001b[38;2;175;136;85m\u001b[48;2;173;134;83m▀\u001b[0m \u001b[38;2;205;175;129m\u001b[48;2;208;174;126m▀\u001b[0m\u001b[38;2;228;193;142m\u001b[48;2;222;186;135m▀\u001b[0m\u001b[38;2;228;194;143m\u001b[48;2;222;187;136m▀\u001b[0m\u001b[38;2;209;180;137m\u001b[48;2;205;172;125m▀\u001b[0m \u001b[38;2;175;135;85m\u001b[48;2;171;133;83m▀\u001b[0m\u001b[38;2;174;135;84m\u001b[48;2;170;132;82m▀\u001b[0m\u001b[38;2;173;134;84m\u001b[48;2;169;130;81m▀\u001b[0m\u001b[38;2;156;122;79m\u001b[48;2;171;133;84m▀\u001b[0m\u001b[38;2;174;140;94m\u001b[48;2;168;129;80m▀\u001b[0m\u001b[38;2;170;132;83m\u001b[48;2;167;129;80m▀\u001b[0m\u001b[38;2;170;132;82m\u001b[48;2;165;128;81m▀\u001b[0m\u001b[38;2;167;130;82m\u001b[48;2;151;117;76m▀\u001b[0m\u001b[38;2;149;116;74m▀\u001b[0m ",
12
+ " \u001b[38;2;165;127;79m▀\u001b[0m\u001b[38;2;167;129;80m\u001b[48;2;161;125;77m▀\u001b[0m\u001b[38;2;169;130;80m\u001b[48;2;165;127;78m▀\u001b[0m\u001b[38;2;169;129;80m\u001b[48;2;165;127;77m▀\u001b[0m\u001b[38;2;169;129;79m\u001b[48;2;165;126;77m▀\u001b[0m\u001b[38;2;168;129;80m\u001b[48;2;164;126;78m▀\u001b[0m\u001b[38;2;155;121;76m▄\u001b[0m \u001b[38;2;218;183;131m\u001b[48;2;212;175;123m▀\u001b[0m\u001b[38;2;216;181;129m\u001b[48;2;211;176;124m▀\u001b[0m \u001b[38;2;166;128;79m\u001b[48;2;160;122;74m▀\u001b[0m\u001b[38;2;166;127;79m\u001b[48;2;161;123;75m▀\u001b[0m\u001b[38;2;165;127;79m\u001b[48;2;161;123;76m▀\u001b[0m\u001b[38;2;164;126;78m\u001b[48;2;160;123;76m▀\u001b[0m\u001b[38;2;163;126;79m\u001b[48;2;153;118;75m▀\u001b[0m\u001b[38;2;159;123;77m▀\u001b[0m ",
13
+ " \u001b[38;2;153;118;73m▀\u001b[0m\u001b[38;2;163;125;77m\u001b[48;2;147;112;70m▀\u001b[0m\u001b[38;2;162;123;75m\u001b[48;2;157;120;73m▀\u001b[0m\u001b[38;2;163;125;77m\u001b[48;2;160;123;75m▀\u001b[0m\u001b[38;2;161;125;78m\u001b[48;2;150;115;72m▀\u001b[0m \u001b[38;2;206;169;118m\u001b[48;2;197;160;110m▀\u001b[0m\u001b[38;2;203;167;117m\u001b[48;2;195;159;110m▀\u001b[0m \u001b[38;2;150;115;71m\u001b[48;2;146;112;69m▀\u001b[0m\u001b[38;2;158;120;73m\u001b[48;2;152;116;70m▀\u001b[0m\u001b[38;2;155;118;71m\u001b[48;2;149;114;69m▀\u001b[0m\u001b[38;2;155;119;73m\u001b[48;2;137;105;65m▀\u001b[0m\u001b[38;2;148;113;71m▀\u001b[0m ",
14
+ " \u001b[38;2;145;112;69m▀\u001b[0m\u001b[38;2;154;119;73m\u001b[48;2;143;109;67m▀\u001b[0m\u001b[38;2;148;113;70m\u001b[48;2;149;114;68m▀\u001b[0m \u001b[38;2;189;152;103m\u001b[48;2;174;139;92m▀\u001b[0m\u001b[38;2;185;148;99m\u001b[48;2;173;137;90m▀\u001b[0m \u001b[38;2;144;110;67m\u001b[48;2;140;107;64m▀\u001b[0m\u001b[38;2;146;110;67m\u001b[48;2;138;105;64m▀\u001b[0m\u001b[38;2;133;101;63m▀\u001b[0m ",
15
+ " \u001b[38;2;136;103;63m\u001b[48;2;130;99;61m▀\u001b[0m \u001b[38;2;167;131;84m\u001b[48;2;161;125;79m▀\u001b[0m\u001b[38;2;163;129;84m\u001b[48;2;158;123;78m▀\u001b[0m \u001b[38;2;136;103;62m\u001b[48;2;127;96;59m▀\u001b[0m ",
16
+ " \u001b[38;2;152;117;72m\u001b[48;2;145;110;67m▀\u001b[0m\u001b[38;2;152;117;73m\u001b[48;2;146;111;68m▀\u001b[0m ",
17
+ " \u001b[38;2;141;107;65m\u001b[48;2;135;102;60m▀\u001b[0m\u001b[38;2;142;107;64m\u001b[48;2;136;102;61m▀\u001b[0m ",
18
+ " \u001b[38;2;130;99;59m▀\u001b[0m "
19
+ ];
20
+ export const LOGO_WIDTH = 44;
@@ -0,0 +1,45 @@
1
+ // Affichage des résultats en table dans le terminal.
2
+
3
+ import chalk from 'chalk';
4
+ import Table from 'cli-table3';
5
+ import { rank, paint, SEVERITIES } from '../core/severity.js';
6
+
7
+ export function renderFindings(findings, { title = 'Résultats' } = {}) {
8
+ if (!findings.length) return chalk.gray('Aucun résultat enregistré.');
9
+
10
+ const sorted = [...findings].sort((a, b) => rank(a.severity) - rank(b.severity));
11
+ const table = new Table({
12
+ head: ['Sév.', 'Outil', 'Cible', 'Finding', 'Détail'].map((h) => chalk.bold(h)),
13
+ colWidths: [12, 10, 22, 38, 34],
14
+ wordWrap: true,
15
+ style: { head: [], border: ['grey'] },
16
+ });
17
+
18
+ for (const f of sorted) {
19
+ table.push([
20
+ paint(chalk, f.severity, f.severity),
21
+ f.tool,
22
+ truncate(f.target, 20),
23
+ truncate(f.title, 36),
24
+ truncate(f.detail || f.ref || '', 32),
25
+ ]);
26
+ }
27
+
28
+ return `\n${chalk.bold(title)} ${chalk.gray(`(${findings.length})`)}\n${table.toString()}\n${summary(findings)}`;
29
+ }
30
+
31
+ function summary(findings) {
32
+ const counts = {};
33
+ for (const f of findings) {
34
+ const s = (f.severity || 'unknown').toLowerCase();
35
+ counts[s] = (counts[s] || 0) + 1;
36
+ }
37
+ return SEVERITIES.filter((s) => counts[s])
38
+ .map((s) => paint(chalk, s, `${s}: ${counts[s]}`))
39
+ .join(' ');
40
+ }
41
+
42
+ function truncate(str, n) {
43
+ const s = String(str ?? '');
44
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
45
+ }
@@ -0,0 +1,253 @@
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Icarus — pentest</title>
7
+ <style>
8
+ :root{
9
+ --bg:#07070a; --panel:#0e0e12; --panel2:#121218; --border:#1e1e26;
10
+ --gold:#d4a843; --gold-hi:#f0c66b; --txt:#e9e7e1; --dim:#8b8b95;
11
+ --crit:#dc2626; --high:#f97316; --med:#eab308; --low:#3b82f6; --info:#7a7a86;
12
+ --ok:#3fb950; --bad:#f85149;
13
+ }
14
+ *{box-sizing:border-box}
15
+ html,body{margin:0;height:100%}
16
+ body{
17
+ background:radial-gradient(1200px 600px at 50% -10%, #14110a 0%, var(--bg) 55%);
18
+ color:var(--txt); font:14px/1.5 ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
19
+ -webkit-font-smoothing:antialiased;
20
+ }
21
+ a{color:var(--gold)}
22
+ .wrap{max-width:1180px;margin:0 auto;padding:26px 20px 60px}
23
+
24
+ header{text-align:center;padding:14px 0 22px}
25
+ header img{width:108px;height:108px;object-fit:contain;filter:drop-shadow(0 0 26px rgba(212,168,67,.35));animation:float 5s ease-in-out infinite}
26
+ @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
27
+ .word{font-size:30px;font-weight:800;letter-spacing:.5em;margin:10px 0 4px;
28
+ background:linear-gradient(180deg,var(--gold-hi),var(--gold));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
29
+ .sub{color:var(--dim);font-size:12px}
30
+ .warn{margin-top:10px;color:var(--gold);font-size:12px;opacity:.85}
31
+
32
+ .stats{display:flex;gap:10px;justify-content:center;margin:18px 0 6px;flex-wrap:wrap}
33
+ .stat{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:8px 16px;min-width:120px;text-align:center}
34
+ .stat b{display:block;font-size:20px;color:var(--gold-hi)}
35
+ .stat span{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em}
36
+
37
+ .grid{display:grid;grid-template-columns:380px 1fr;gap:18px;margin-top:18px}
38
+ @media(max-width:880px){.grid{grid-template-columns:1fr}}
39
+ .card{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px}
40
+ .card h2{margin:0 0 12px;font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);font-weight:700}
41
+
42
+ label.f{display:block;font-size:11px;color:var(--dim);margin:0 0 5px;text-transform:uppercase;letter-spacing:.08em}
43
+ input[type=text],input[type=number]{width:100%;background:var(--panel2);border:1px solid var(--border);border-radius:9px;
44
+ color:var(--txt);padding:11px 12px;font:inherit;outline:none}
45
+ input:focus{border-color:var(--gold)}
46
+ .row{display:flex;gap:10px}.row>div{flex:1}
47
+
48
+ .chips{display:flex;flex-wrap:wrap;gap:7px;margin:4px 0 2px}
49
+ .chip{background:var(--panel2);border:1px solid var(--border);color:var(--txt);border-radius:999px;
50
+ padding:6px 13px;font-size:12px;cursor:pointer;transition:.15s}
51
+ .chip:hover{border-color:var(--gold)}
52
+ .chip.on{background:linear-gradient(180deg,var(--gold-hi),var(--gold));color:#1a1407;border-color:var(--gold);font-weight:700}
53
+
54
+ .cat{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.12em;margin:14px 0 6px}
55
+ .tools{display:flex;flex-direction:column;gap:3px}
56
+ .tool{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;cursor:pointer;transition:.12s}
57
+ .tool:hover{background:var(--panel2)}
58
+ .tool.off{opacity:.4;cursor:not-allowed}
59
+ .tool .dot{width:8px;height:8px;border-radius:50%;flex:none}
60
+ .tool input{accent-color:var(--gold);width:15px;height:15px}
61
+ .tool .nm{font-weight:600}
62
+ .tool .ds{color:var(--dim);font-size:11px;margin-left:auto;text-align:right;max-width:140px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
63
+
64
+ .btns{display:flex;gap:10px;margin-top:16px}
65
+ button{font:inherit;border-radius:10px;padding:12px 16px;cursor:pointer;border:1px solid var(--border);
66
+ background:var(--panel2);color:var(--txt);transition:.15s;font-weight:600}
67
+ button:hover{border-color:var(--gold)}
68
+ button.primary{flex:1;background:linear-gradient(180deg,var(--gold-hi),var(--gold));color:#1a1407;border-color:var(--gold)}
69
+ button.primary:hover{filter:brightness(1.08)}
70
+ button:disabled{opacity:.5;cursor:not-allowed}
71
+
72
+ .term{background:#050507;border:1px solid var(--border);border-radius:12px;padding:14px;height:300px;overflow:auto;
73
+ font-size:12.5px;white-space:pre-wrap;word-break:break-word;color:#cfcfd6}
74
+ .term::-webkit-scrollbar{width:9px}.term::-webkit-scrollbar-thumb{background:#2a2a33;border-radius:9px}
75
+ .term .t-start{color:var(--gold-hi);font-weight:700;margin-top:8px}
76
+ .term .t-ok{color:var(--ok)}.term .t-err{color:var(--bad)}.term .t-info{color:var(--dim)}
77
+ .term .out{color:#7d7d88}
78
+
79
+ .findings{display:flex;flex-direction:column;gap:6px;margin-top:14px;max-height:420px;overflow:auto}
80
+ .find{display:grid;grid-template-columns:auto auto 1fr;gap:10px;align-items:start;background:var(--panel2);
81
+ border:1px solid var(--border);border-left-width:3px;border-radius:9px;padding:9px 12px}
82
+ .find .sev{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;display:flex;align-items:center;gap:6px;white-space:nowrap}
83
+ .find .sev .d{width:9px;height:9px;border-radius:50%}
84
+ .find .tl{font-weight:600}
85
+ .find .dt{color:var(--dim);font-size:11.5px;margin-top:2px;word-break:break-all}
86
+ .find .tool{justify-self:end;color:var(--gold);font-size:10px;opacity:.8}
87
+ .empty{color:var(--dim);text-align:center;padding:30px;font-size:13px}
88
+ .tabbar{display:flex;gap:6px;margin-bottom:8px}
89
+ .tab{font-size:12px;color:var(--dim);padding:6px 12px;border-radius:8px;cursor:pointer}
90
+ .tab.on{color:var(--gold);background:var(--panel2)}
91
+ footer{text-align:center;color:var(--dim);font-size:11px;margin-top:30px}
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div class="wrap">
96
+ <header>
97
+ <img src="/logo.png" alt="Icarus">
98
+ <div class="word">ICARUS</div>
99
+ <div class="sub">plateforme de pentest tout-en-un</div>
100
+ <div class="warn">⚠ Usage strictement autorisé — tes systèmes ou mandat écrit signé.</div>
101
+ </header>
102
+
103
+ <div class="stats">
104
+ <div class="stat"><b id="s-find">0</b><span>findings</span></div>
105
+ <div class="stat"><b id="s-scan">0</b><span>scans</span></div>
106
+ <div class="stat"><b id="s-tools">0</b><span>outils prêts</span></div>
107
+ </div>
108
+
109
+ <div class="grid">
110
+ <div class="card">
111
+ <h2>Configuration</h2>
112
+ <label class="f">Cible</label>
113
+ <input id="target" type="text" placeholder="example.com · http://site/p?id=1" autocomplete="off">
114
+
115
+ <div style="margin-top:14px">
116
+ <label class="f">Profil</label>
117
+ <div class="chips" id="profiles"></div>
118
+ </div>
119
+
120
+ <div class="row" style="margin-top:14px">
121
+ <div><label class="f">Timeout / outil (s)</label><input id="timeout" type="number" value="120" min="0"></div>
122
+ </div>
123
+
124
+ <label class="f" style="margin-top:16px">Outils</label>
125
+ <div id="toolwrap"></div>
126
+
127
+ <div class="btns">
128
+ <button class="primary" id="run">▸ Lancer le scan</button>
129
+ <button id="report" title="Rapport HTML">▤</button>
130
+ <button id="clear" title="Effacer les résultats">✕</button>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="card">
135
+ <h2>Exécution &amp; résultats</h2>
136
+ <div class="tabbar">
137
+ <div class="tab on" data-tab="out">Sortie live</div>
138
+ <div class="tab" data-tab="find">Findings (<span id="fcount">0</span>)</div>
139
+ </div>
140
+ <div id="pane-out" class="term">En attente — configure une cible et lance un scan.</div>
141
+ <div id="pane-find" class="findings" style="display:none"></div>
142
+ </div>
143
+ </div>
144
+
145
+ <footer>Icarus · interface web locale · <span id="host"></span></footer>
146
+ </div>
147
+
148
+ <script>
149
+ const SEV={critical:'var(--crit)',high:'var(--high)',medium:'var(--med)',low:'var(--low)',info:'var(--info)',unknown:'var(--info)'};
150
+ const ORDER=['critical','high','medium','low','info','unknown'];
151
+ const $=(s)=>document.querySelector(s);
152
+ const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.textContent=h;return e;};
153
+ let STATE={tools:[],profiles:{}}, selected=new Set();
154
+ $('#host').textContent=location.host;
155
+
156
+ async function loadState(){
157
+ const s=await (await fetch('/api/state')).json();
158
+ STATE=s;
159
+ $('#s-find').textContent=s.findingsCount;
160
+ $('#s-scan').textContent=s.scansCount;
161
+ $('#s-tools').textContent=s.tools.filter(t=>t.available).length;
162
+ renderProfiles(); renderTools(); loadFindings();
163
+ }
164
+
165
+ function renderProfiles(){
166
+ const box=$('#profiles'); box.innerHTML='';
167
+ for(const [id,p] of Object.entries(STATE.profiles)){
168
+ const c=el('div','chip',p.label); c.dataset.p=id;
169
+ c.onclick=()=>{ selected=new Set(p.tools.filter(x=>STATE.tools.find(t=>t.id===x&&t.available))); renderTools(); mark(id); };
170
+ box.appendChild(c);
171
+ }
172
+ }
173
+ function mark(id){ document.querySelectorAll('.chip').forEach(c=>c.classList.toggle('on',c.dataset.p===id)); }
174
+
175
+ function renderTools(){
176
+ const w=$('#toolwrap'); w.innerHTML='';
177
+ for(const cat of [...new Set(STATE.tools.map(t=>t.category))]){
178
+ w.appendChild(el('div','cat',cat));
179
+ const list=el('div','tools');
180
+ for(const t of STATE.tools.filter(t=>t.category===cat)){
181
+ const row=el('div','tool'+(t.available?'':' off'));
182
+ const dot=el('span','dot'); dot.style.background=t.available?'var(--ok)':'var(--bad)';
183
+ const cb=el('input'); cb.type='checkbox'; cb.disabled=!t.available; cb.checked=selected.has(t.id);
184
+ cb.onchange=()=>{ cb.checked?selected.add(t.id):selected.delete(t.id); mark(''); };
185
+ row.append(cb,dot,el('span','nm',t.name),el('span','ds',t.available?(t.description||''):'non installé'));
186
+ row.onclick=(e)=>{ if(e.target!==cb&&t.available){cb.checked=!cb.checked;cb.onchange();} };
187
+ list.appendChild(row);
188
+ }
189
+ w.appendChild(list);
190
+ }
191
+ }
192
+
193
+ const term=$('#pane-out');
194
+ function log(cls,txt){ const d=el('div',cls,txt); term.appendChild(d); term.scrollTop=term.scrollHeight; }
195
+
196
+ $('#run').onclick=async()=>{
197
+ const target=$('#target').value.trim();
198
+ if(!target){ $('#target').focus(); return; }
199
+ if(!selected.size){ alert('Choisis au moins un outil ou un profil.'); return; }
200
+ switchTab('out'); term.innerHTML='';
201
+ $('#run').disabled=true; $('#run').textContent='◌ scan…';
202
+ log('t-info',`▸ cible : ${target} · ${selected.size} outil(s)`);
203
+ try{
204
+ const res=await fetch('/api/scan',{method:'POST',headers:{'Content-Type':'application/json'},
205
+ body:JSON.stringify({target,tools:[...selected],timeout:Number($('#timeout').value)||0})});
206
+ const rd=res.body.getReader(), dec=new TextDecoder(); let buf='';
207
+ for(;;){ const {value,done}=await rd.read(); if(done)break; buf+=dec.decode(value,{stream:true});
208
+ let nl; while((nl=buf.indexOf('\n'))>=0){ const ln=buf.slice(0,nl); buf=buf.slice(nl+1); if(ln.trim())handleEv(JSON.parse(ln)); } }
209
+ }catch(e){ log('t-err','Erreur : '+e.message); }
210
+ $('#run').disabled=false; $('#run').textContent='▸ Lancer le scan'; loadState();
211
+ };
212
+
213
+ function handleEv(ev){
214
+ if(ev.type==='info') log('t-info',' '+ev.message);
215
+ else if(ev.type==='tool-start') log('t-start',`\n❯ ${ev.name} → ${ev.target}`);
216
+ else if(ev.type==='out') log('out', ev.chunk.replace(/\x1b\[[0-9;]*m/g,''));
217
+ else if(ev.type==='log') log('out',' '+ev.line);
218
+ else if(ev.type==='tool-done'){ ev.error?log('t-err',` ✗ ${ev.error}`):log('t-ok',` ✓ terminé — ${ev.findings.length} finding(s)`); }
219
+ else if(ev.type==='done') log('t-start',`\n■ Scan terminé — ${ev.totalFindings} finding(s) sur ${ev.tools} outil(s).`);
220
+ }
221
+
222
+ async function loadFindings(){
223
+ const f=await (await fetch('/api/findings')).json();
224
+ $('#fcount').textContent=f.length;
225
+ const box=$('#pane-find'); box.innerHTML='';
226
+ if(!f.length){ box.appendChild(el('div','empty','Aucun finding pour l’instant.')); return; }
227
+ f.sort((a,b)=>ORDER.indexOf(a.severity)-ORDER.indexOf(b.severity));
228
+ for(const x of f){
229
+ const col=SEV[x.severity]||SEV.info;
230
+ const r=el('div','find'); r.style.borderLeftColor=col;
231
+ const sev=el('div','sev'); sev.style.color=col;
232
+ const d=el('span','d'); d.style.background=col; sev.append(d,document.createTextNode(x.severity||'?'));
233
+ const mid=el('div'); mid.append(el('div','tl',x.title||''),el('div','dt',x.detail||''));
234
+ r.append(sev,el('div','tool',x.tool||''),mid);
235
+ box.appendChild(r);
236
+ }
237
+ }
238
+
239
+ function switchTab(t){
240
+ document.querySelectorAll('.tab').forEach(x=>x.classList.toggle('on',x.dataset.tab===t));
241
+ $('#pane-out').style.display=t==='out'?'block':'none';
242
+ $('#pane-find').style.display=t==='find'?'flex':'none';
243
+ if(t==='find') loadFindings();
244
+ }
245
+ document.querySelectorAll('.tab').forEach(x=>x.onclick=()=>switchTab(x.dataset.tab));
246
+
247
+ $('#report').onclick=async()=>{ const r=await (await fetch('/api/report',{method:'POST'})).json(); if(r.htmlUrl) window.open(r.htmlUrl,'_blank'); };
248
+ $('#clear').onclick=async()=>{ if(!confirm('Effacer tous les résultats ?'))return; await fetch('/api/clear',{method:'POST'}); term.innerHTML='Résultats effacés.'; loadState(); };
249
+
250
+ loadState();
251
+ </script>
252
+ </body>
253
+ </html>
Binary file