icarus-cmd 0.3.1 → 1.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icarus-cmd",
3
- "version": "0.3.1",
3
+ "version": "1.0.1",
4
4
  "description": "Icarus — plateforme de pentest tout-en-un : nmap, nuclei, sqlmap, ZAP, ffuf, katana & co regroupés. CLI + TUI + interface web locale, analyse de code (SAST), base de résultats commune et rapports.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,8 +24,17 @@
24
24
  "node": ">=20"
25
25
  },
26
26
  "keywords": [
27
- "pentest", "security", "sast", "nuclei", "nmap", "sqlmap",
28
- "ffuf", "recon", "vulnerability-scanner", "cli", "infosec"
27
+ "pentest",
28
+ "security",
29
+ "sast",
30
+ "nuclei",
31
+ "nmap",
32
+ "sqlmap",
33
+ "ffuf",
34
+ "recon",
35
+ "vulnerability-scanner",
36
+ "cli",
37
+ "infosec"
29
38
  ],
30
39
  "author": "Icarus",
31
40
  "license": "MIT",
@@ -1,5 +1,3 @@
1
- // Message d'après-installation. AUCUN téléchargement ici (mauvaise pratique +
2
- // certains outils demandent l'admin) : on guide juste l'utilisateur.
3
1
  const c = (s, n) => `\x1b[${n}m${s}\x1b[0m`;
4
2
  console.log('');
5
3
  console.log(' ' + c('Icarus installé.', '1') + ' Étape suivante :');
@@ -1,7 +1,3 @@
1
- // Analyse statique (SAST) légère : parcourt un fichier ou un dossier et applique
2
- // des règles regex par langage pour repérer des motifs de vulnérabilités
3
- // classiques (injection, secrets en dur, crypto faible, désérialisation…).
4
- // Zéro dépendance : chaque règle = { id, severity, exts, re, title }.
5
1
  import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
6
2
  import { join, extname, basename, relative } from 'node:path';
7
3
 
@@ -14,20 +10,18 @@ const TEXT_EXTS = new Set([
14
10
  '.go', '.cs', '.c', '.cpp', '.h', '.sh', '.bash', '.ps1', '.sql', '.html',
15
11
  '.env', '.yml', '.yaml', '.json', '.xml', '.ini', '.conf', '.cfg', '.txt',
16
12
  ]);
17
- const MAX_FILE = 1.5 * 1024 * 1024; // 1.5 Mo
13
+ const MAX_FILE = 1.5 * 1024 * 1024;
18
14
 
19
- const ANY = null; // règle valable pour toutes extensions
15
+ const ANY = null;
20
16
 
21
- // exts = liste d'extensions ('.js') ou ANY ; re testée ligne par ligne.
22
17
  const RULES = [
23
- // ---- Secrets / credentials (tous fichiers) ----
18
+
24
19
  { id: 'secret-aws-key', severity: 'critical', exts: ANY, title: 'Cle AWS Access Key en dur', re: /\bAKIA[0-9A-Z]{16}\b/ },
25
20
  { id: 'secret-private-key', severity: 'critical', exts: ANY, title: 'Cle privee en dur', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
26
21
  { id: 'secret-generic', severity: 'high', exts: ANY, title: 'Secret/mot de passe en dur', re: /\b(?:api[_-]?key|secret|passwd|password|token|access[_-]?key|client[_-]?secret)\b\s*[:=]\s*['"][^'"\s]{8,}['"]/i },
27
22
  { id: 'secret-bearer', severity: 'high', exts: ANY, title: 'Token Bearer en dur', re: /bearer\s+[a-z0-9._\-]{20,}/i },
28
23
  { id: 'secret-slack', severity: 'high', exts: ANY, title: 'Token Slack en dur', re: /xox[baprs]-[0-9A-Za-z-]{10,}/ },
29
24
 
30
- // ---- Injection de commande / code ----
31
25
  { id: 'js-eval', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'], title: 'eval() — execution de code', re: /\beval\s*\(/ },
32
26
  { id: 'js-new-function', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'], title: 'new Function() — execution dynamique', re: /\bnew\s+Function\s*\(/ },
33
27
  { id: 'js-child-exec', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts'], title: 'child_process.exec avec variable — injection cmd', re: /\bexec(?:Sync)?\s*\(\s*[`'"][^`'"]*\$\{|\bexec(?:Sync)?\s*\([^)]*\+/ },
@@ -36,29 +30,24 @@ const RULES = [
36
30
  { id: 'py-subprocess-shell', severity: 'high', exts: ['.py'], title: 'subprocess shell=True — injection commande', re: /subprocess\.[a-z]+\([^)]*shell\s*=\s*True/ },
37
31
  { id: 'php-cmd', severity: 'high', exts: ['.php'], title: 'system/exec/shell_exec/passthru — injection commande', re: /\b(?:system|exec|shell_exec|passthru|popen|proc_open)\s*\(/ },
38
32
 
39
- // ---- Injection SQL ----
40
33
  { id: 'sql-concat', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts', '.py', '.php', '.java', '.rb', '.cs'], title: 'Requete SQL concatenee — risque SQLi', re: /(?:select|insert|update|delete)\b[^;'"]*['"`]\s*(?:\+|\.|%|\$\{|f")/i },
41
34
  { id: 'php-sql-super', severity: 'high', exts: ['.php'], title: 'Variable $_GET/$_POST dans requete SQL', re: /(?:query|mysqli?_query|->query)\s*\([^)]*\$_(?:GET|POST|REQUEST|COOKIE)/i },
42
35
 
43
- // ---- XSS ----
44
36
  { id: 'js-innerhtml', severity: 'medium', exts: ['.js', '.ts', '.tsx', '.jsx', '.html'], title: 'innerHTML/outerHTML — risque XSS', re: /\.(?:inner|outer)HTML\s*=/ },
45
37
  { id: 'react-dangerously', severity: 'medium', exts: ['.jsx', '.tsx', '.js', '.ts'], title: 'dangerouslySetInnerHTML — risque XSS', re: /dangerouslySetInnerHTML/ },
46
38
  { id: 'js-doc-write', severity: 'medium', exts: ['.js', '.ts', '.html'], title: 'document.write — risque XSS', re: /document\.write\s*\(/ },
47
39
  { id: 'php-echo-super', severity: 'medium', exts: ['.php'], title: 'echo direct de $_GET/$_POST — XSS', re: /echo\s+[^;]*\$_(?:GET|POST|REQUEST)/i },
48
40
 
49
- // ---- Deserialisation / parsing dangereux ----
50
41
  { id: 'py-pickle', severity: 'high', exts: ['.py'], title: 'pickle.loads — deserialisation non sure', re: /pickle\.loads?\s*\(/ },
51
42
  { id: 'py-yaml-load', severity: 'high', exts: ['.py'], title: 'yaml.load sans SafeLoader', re: /yaml\.load\s*\((?![^)]*Safe)/ },
52
43
  { id: 'php-unserialize', severity: 'high', exts: ['.php'], title: 'unserialize() — deserialisation non sure', re: /\bunserialize\s*\(/ },
53
44
 
54
- // ---- TLS / crypto ----
55
45
  { id: 'tls-node-reject', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts'], title: 'rejectUnauthorized:false — TLS non verifie', re: /rejectUnauthorized\s*:\s*false/ },
56
46
  { id: 'tls-py-verify', severity: 'high', exts: ['.py'], title: 'verify=False — TLS non verifie', re: /verify\s*=\s*False/ },
57
47
  { id: 'tls-curl-insecure', severity: 'medium', exts: ANY, title: 'curl -k / --insecure — TLS non verifie', re: /curl\s+[^\n]*(?:\s-k\b|--insecure)/ },
58
48
  { id: 'crypto-weak', severity: 'medium', exts: ANY, title: 'Hash faible (MD5/SHA1)', re: /\b(?:md5|sha1)\s*\(/i },
59
49
  { id: 'js-math-random', severity: 'low', exts: ['.js', '.ts'], title: 'Math.random() — non cryptographique', re: /Math\.random\s*\(\)/ },
60
50
 
61
- // ---- Divers ----
62
51
  { id: 'flask-debug', severity: 'medium', exts: ['.py'], title: 'Flask debug=True — execution de code', re: /\.run\s*\([^)]*debug\s*=\s*True/ },
63
52
  { id: 'cors-wildcard', severity: 'low', exts: ANY, title: 'CORS Access-Control-Allow-Origin: *', re: /Access-Control-Allow-Origin['"\s:]+\*/ },
64
53
  ];
@@ -109,7 +98,6 @@ function scanFile(file, root) {
109
98
  return findings;
110
99
  }
111
100
 
112
- // Analyse un fichier ou un dossier. Renvoie { findings, files, root }.
113
101
  export function analyzeCode(target) {
114
102
  if (!existsSync(target)) return { error: `Chemin introuvable : ${target}`, findings: [], files: 0 };
115
103
  const st = statSync(target);
@@ -1,7 +1,3 @@
1
- // Détection d'un outil : on le cherche dans le PATH (via `where`/`which`),
2
- // puis dans une liste de chemins d'installation connus (utile sous Windows où
3
- // nmap/ZAP n'ajoutent pas toujours leur dossier au PATH).
4
-
5
1
  import { existsSync } from 'node:fs';
6
2
  import { execFileSync } from 'node:child_process';
7
3
  import { join, dirname } from 'node:path';
@@ -9,7 +5,6 @@ import { fileURLToPath } from 'node:url';
9
5
 
10
6
  const cache = new Map();
11
7
 
12
- // Dossier où Icarus installe les binaires précompilés (install-tools.ps1).
13
8
  const BUNDLED_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'tools', 'bin');
14
9
 
15
10
  function whichBin(name) {
@@ -25,29 +20,25 @@ function whichBin(name) {
25
20
  }
26
21
  }
27
22
 
28
- // Renvoie le chemin résolu du binaire d'un outil, ou null si introuvable.
29
23
  export function resolveBin(tool, { fresh = false } = {}) {
30
24
  if (!fresh && cache.has(tool.id)) return cache.get(tool.id);
31
25
 
32
26
  const candidates = [];
33
- // 1) binaires installés localement par Icarus (tools/bin)
27
+
34
28
  for (const b of tool.bins || []) {
35
29
  const local = join(BUNDLED_DIR, b.endsWith('.exe') ? b : `${b}.exe`);
36
30
  if (existsSync(local)) candidates.push(local);
37
31
  }
38
- // 2) chemins d'installation Windows connus
32
+
39
33
  for (const p of tool.winPaths || []) {
40
34
  if (existsSync(p)) candidates.push(p);
41
35
  }
42
- // 3) PATH
36
+
43
37
  for (const b of tool.bins || []) {
44
38
  const r = whichBin(b);
45
39
  if (r) candidates.push(r);
46
40
  }
47
41
 
48
- // Certains outils partagent un nom de binaire avec un autre logiciel
49
- // (ex: le scanner httpx de ProjectDiscovery vs la lib Python httpx).
50
- // tool.verify(bin) permet d'écarter le mauvais.
51
42
  let found = null;
52
43
  for (const c of candidates) {
53
44
  if (typeof tool.verify === 'function' && !tool.verify(c)) continue;
@@ -1,7 +1,3 @@
1
- // Registre central des outils. Pour ajouter un outil : créer src/tools/xxx.js,
2
- // l'importer ici et l'ajouter à TOOLS. Tout le reste (TUI, scans, rapports,
3
- // profils) le prend en compte automatiquement.
4
-
5
1
  import { resolveBin } from './detect.js';
6
2
 
7
3
  import nmap from '../tools/nmap.js';
@@ -22,13 +18,11 @@ import zap from '../tools/zap.js';
22
18
  import metasploit from '../tools/metasploit.js';
23
19
 
24
20
  export const TOOLS = [
25
- nmap, naabu, subfinder, httpx, katana, whatweb, wafw00f, // recon
26
- nuclei, metasploit, // vuln
27
- ffuf, gobuster, nikto, sqlmap, dalfox, wpscan, zap, // web
21
+ nmap, naabu, subfinder, httpx, katana, whatweb, wafw00f,
22
+ nuclei, metasploit,
23
+ ffuf, gobuster, nikto, sqlmap, dalfox, wpscan, zap,
28
24
  ];
29
25
 
30
- // Profils = enchaînements d'outils par objectif. Le profil ne lance que les
31
- // outils réellement installés (les autres sont ignorés silencieusement).
32
26
  export const PROFILES = {
33
27
  recon: { label: 'Reconnaissance', tools: ['nmap', 'naabu', 'subfinder', 'httpx', 'katana', 'whatweb', 'wafw00f'] },
34
28
  web: { label: 'Application web', tools: ['httpx', 'whatweb', 'katana', 'ffuf', 'gobuster', 'nikto', 'sqlmap', 'dalfox', 'wpscan', 'zap'] },
@@ -1,14 +1,3 @@
1
- // Résolveurs DNS imposés aux outils ProjectDiscovery (nuclei, httpx, katana,
2
- // subfinder). Leur résolveur Go interne échoue souvent ("no address found for
3
- // host"), surtout sous VPN (Mullvad force son propre DNS et bloque les autres) :
4
- // du coup l'appli trouve 0 vuln alors que tout marche par ailleurs.
5
- //
6
- // On génère donc un fichier -resolvers à partir du DNS SYSTÈME ACTIF
7
- // (dns.getServers() — le primaire en tête, c'est celui que l'OS utilise et qui
8
- // marche). Deux pièges Windows gérés ici :
9
- // 1) nuclie ne lit le fichier qu'en fins de ligne CRLF ;
10
- // 2) son chemin ne doit pas contenir d'espaces (le projet vit dans
11
- // "…/pentest tool/…") -> on écrit dans tmp/home.
12
1
  import { dirname, join } from 'node:path';
13
2
  import { fileURLToPath } from 'node:url';
14
3
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
@@ -33,9 +22,9 @@ function buildList() {
33
22
  }
34
23
  } catch { /* ignore */ }
35
24
  // Le primaire SEUL : c'est le DNS que l'OS utilise (donc joignable même sous
36
- // VPN), et ça évite que nuclei pioche un DNS secondaire bloqué par le VPN.
25
+
37
26
  if (out.length) return out.slice(0, 1);
38
- // Secours : liste statique fournie avec le projet.
27
+
39
28
  try { return readFileSync(BUNDLED, 'utf8').split(/\r?\n/).map((l) => l.trim()).filter(Boolean); }
40
29
  catch { return []; }
41
30
  }
@@ -45,7 +34,7 @@ try {
45
34
  const dst = pickPath();
46
35
  const list = buildList();
47
36
  if (dst && list.length) {
48
- writeFileSync(dst, list.join('\r\n') + '\r\n'); // CRLF obligatoire pour nuclei
37
+ writeFileSync(dst, list.join('\r\n') + '\r\n');
49
38
  resolved = dst;
50
39
  }
51
40
  } catch { resolved = ''; }
@@ -1,6 +1,3 @@
1
- // Lance un outil, streame sa sortie en direct dans le terminal, capture tout,
2
- // puis délègue au parser de l'outil pour produire des findings normalisés.
3
-
4
1
  import { spawn } from 'node:child_process';
5
2
  import { mkdtempSync } from 'node:fs';
6
3
  import { tmpdir } from 'node:os';
@@ -8,8 +5,6 @@ import { join } from 'node:path';
8
5
  import { resolveBin } from './detect.js';
9
6
  import { recordScan, addFindings } from './store.js';
10
7
 
11
- // Exécute un binaire et renvoie { code, stdout, stderr, timedOut }.
12
- // timeoutMs > 0 : tue le process s'il dépasse le délai.
13
8
  export function runProcess(cmd, args, { onData, cwd, env, timeoutMs = 0, shell = false } = {}) {
14
9
  return new Promise((resolve) => {
15
10
  let stdout = '';
@@ -37,8 +32,6 @@ export function runProcess(cmd, args, { onData, cwd, env, timeoutMs = 0, shell =
37
32
  });
38
33
  }
39
34
 
40
- // Orchestration d'un outil de bout en bout : détection -> commande -> run ->
41
- // parse -> stockage. Renvoie { ok, findings, code, error }.
42
35
  export async function runTool(tool, target, { opts = {}, onData, onLog } = {}) {
43
36
  const log = onLog || (() => {});
44
37
  const bin = resolveBin(tool);
@@ -54,11 +47,6 @@ export async function runTool(tool, target, { opts = {}, onData, onLog } = {}) {
54
47
  let args = built.args || [];
55
48
  let shell = false;
56
49
 
57
- // Sous Windows, Node ≥20 refuse de spawn directement un .bat/.cmd
58
- // (EINVAL, correctif CVE-2024-27980). On reconstruit une ligne de commande
59
- // correctement quotée et on laisse cmd.exe l'exécuter via shell:true.
60
- // Les outils qui définissent déjà built.cmd (ex. metasploit via `cmd /c`)
61
- // gèrent leur propre invocation et ne sont pas touchés.
62
50
  if (process.platform === 'win32' && !built.cmd && /\.(bat|cmd)$/i.test(cmd)) {
63
51
  const q = (s) => (/[\s"&()[\]{}^=;!'+,`~%<>|]/.test(String(s))
64
52
  ? `"${String(s).replace(/"/g, '""')}"`
package/src/core/setup.js CHANGED
@@ -1,8 +1,3 @@
1
- // `icarus setup` — télécharge les binaires précompilés (releases GitHub) des
2
- // outils Go et les place dans tools/bin. Cross-platform (Windows/Linux/macOS) :
3
- // détecte OS+arch, choisit le bon asset, extrait via `tar` (présent par défaut
4
- // sur Win10+, Linux, macOS). Les outils à runtime (nmap, ZAP, Metasploit,
5
- // nikto, wpscan…) ne sont pas gérés ici : `icarus doctor` indique comment.
6
1
  import { mkdtempSync, mkdirSync, existsSync, copyFileSync, readdirSync, createWriteStream, rmSync } from 'node:fs';
7
2
  import { tmpdir } from 'node:os';
8
3
  import { join, dirname } from 'node:path';
@@ -45,7 +40,6 @@ async function download(url, dest) {
45
40
  await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
46
41
  }
47
42
 
48
- // Installe un outil. Renvoie true/false. log(msg) pour le suivi.
49
43
  async function installTool(t, keys, tmp, log) {
50
44
  const rel = await ghJson(`https://api.github.com/repos/${t.repo}/releases/latest`);
51
45
  const asset = (rel.assets || []).find((x) =>
@@ -92,7 +86,7 @@ export async function setupTools({ log = console.log } = {}) {
92
86
  fail.push(t.id);
93
87
  }
94
88
  }
95
- try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
89
+ try { rmSync(tmp, { recursive: true, force: true }); } catch { }
96
90
 
97
91
  log(`\n Installés (${ok.length}) : ${ok.join(', ') || '-'}`);
98
92
  if (fail.length) log(` Échecs (${fail.length}) : ${fail.join(', ')}`);
@@ -1,15 +1,10 @@
1
- // Modèle de sévérité partagé par tous les outils, pour normaliser des sorties
2
- // très hétérogènes (nuclei dit "high", ZAP dit "3", etc.) en une seule échelle.
3
-
4
1
  export const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info', 'unknown'];
5
2
 
6
- // Plus le rang est petit, plus c'est grave (utile pour trier).
7
3
  export function rank(sev) {
8
4
  const i = SEVERITIES.indexOf(String(sev || 'unknown').toLowerCase());
9
5
  return i < 0 ? SEVERITIES.length : i;
10
6
  }
11
7
 
12
- // Normalise n'importe quelle étiquette de sévérité vers notre échelle.
13
8
  export function normalize(sev) {
14
9
  const s = String(sev || '').toLowerCase().trim();
15
10
  if (['critical', 'crit', '4'].includes(s)) return 'critical';
package/src/core/store.js CHANGED
@@ -1,7 +1,3 @@
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
1
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
6
2
  import { dirname, join } from 'node:path';
7
3
  import { fileURLToPath } from 'node:url';
@@ -29,7 +25,6 @@ function save(db) {
29
25
  writeFileSync(DB_FILE, JSON.stringify(db, null, 2), 'utf8');
30
26
  }
31
27
 
32
- // Enregistre une exécution d'outil et renvoie son id.
33
28
  export function recordScan({ tool, target, code, command }) {
34
29
  const db = load();
35
30
  const scan = {
@@ -45,7 +40,6 @@ export function recordScan({ tool, target, code, command }) {
45
40
  return scan.id;
46
41
  }
47
42
 
48
- // Ajoute des findings normalisés (le runner les enrichit avec scanId/tool/target).
49
43
  export function addFindings(findings) {
50
44
  if (!findings?.length) return [];
51
45
  const db = load();
@@ -1,5 +1,3 @@
1
- // Helpers de cible partagés (TUI + Web).
2
-
3
1
  export const toHost = (t) => String(t).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
4
2
  export const toUrl = (t) => (/^https?:\/\//.test(t) ? t : `http://${t}`);
5
3
  export const effectiveTarget = (tool, target) => (tool.needs === 'url' ? toUrl(target) : toHost(target));
package/src/index.js CHANGED
@@ -1,11 +1,4 @@
1
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
2
  import chalk from 'chalk';
10
3
  import Table from 'cli-table3';
11
4
  import { select, input, checkbox, confirm } from '@inquirer/prompts';
@@ -31,11 +24,9 @@ const effectiveTarget = (tool, target) => (tool.needs === 'url' ? toUrl(target)
31
24
  function openBrowser(url) {
32
25
  const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
33
26
  const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
34
- try { spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref(); } catch { /* ignore */ }
27
+ try { spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref(); } catch { }
35
28
  }
36
29
 
37
- // ---------- Affichages partagés ----------
38
-
39
30
  function statusTable({ fresh = false } = {}) {
40
31
  const table = new Table({
41
32
  head: ['Outil', 'Catégorie', 'État', 'Chemin / installation'].map((h) => chalk.bold(h)),
@@ -64,7 +55,6 @@ function summarize(findings) {
64
55
  .join(' ');
65
56
  }
66
57
 
67
- // Résout une liste d'ids d'outils en gardant ceux installés, et signale les autres.
68
58
  function resolveTools(ids) {
69
59
  const avail = new Set(availableIds());
70
60
  const run = ids.filter((id) => getTool(id) && avail.has(id));
@@ -72,8 +62,6 @@ function resolveTools(ids) {
72
62
  return { run, skipped };
73
63
  }
74
64
 
75
- // ---------- Exécution d'un scan ----------
76
-
77
65
  const FAST_OPTS = { fast: true, topPorts: '100', depth: 1, severity: 'critical,high', level: 1, risk: 1 };
78
66
 
79
67
  async function runScan(target, toolIds, { opts = {}, quiet = false, parallel = false } = {}) {
@@ -116,8 +104,6 @@ async function runScan(target, toolIds, { opts = {}, quiet = false, parallel = f
116
104
  return all;
117
105
  }
118
106
 
119
- // ---------- TUI interactive ----------
120
-
121
107
  async function chooseTools(target) {
122
108
  const mode = await select({
123
109
  message: 'Comment choisir les outils ?',
@@ -242,8 +228,6 @@ async function tui() {
242
228
  }
243
229
  }
244
230
 
245
- // ---------- CLI non interactive ----------
246
-
247
231
  function parseFlags(args) {
248
232
  const flags = {};
249
233
  const positional = [];
@@ -307,11 +291,17 @@ async function main() {
307
291
  const { flags } = parseFlags(rest);
308
292
  const port = Number(flags.port) || 7373;
309
293
  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
294
+ try {
295
+ const { url } = await startWeb({ port });
296
+ console.log(chalk.green(' Interface web Icarus lancée :') + ' ' + chalk.hex('#fb923c').bold(url));
297
+ console.log(chalk.gray(' (Ctrl+C pour arrêter)'));
298
+ if (!flags['no-open']) openBrowser(url);
299
+ } catch (err) {
300
+ console.log(chalk.red(`✗ Serveur non démarré : ${err.message}`));
301
+ console.log(chalk.gray(' Essaie un autre port : icarus web --port 8080'));
302
+ process.exit(1);
303
+ }
304
+ return;
315
305
  }
316
306
 
317
307
  if (cmd === 'report') {
@@ -338,9 +328,8 @@ async function main() {
338
328
  return doScanCli(positional[0], flags);
339
329
  }
340
330
 
341
- // Cible "nue" : `icarus example.com`, ou `icarus ./fichier.js` -> analyse code
342
331
  if (!cmd.startsWith('-')) {
343
- if (existsSync(cmd)) return doCodeScan(cmd); // chemin local => SAST
332
+ if (existsSync(cmd)) return doCodeScan(cmd);
344
333
  const { flags } = parseFlags(rest);
345
334
  return doScanCli(cmd, flags);
346
335
  }
@@ -354,12 +343,11 @@ const SEV_COLOR = { critical: '#dc2626', high: '#f97316', medium: '#eab308', low
354
343
  const SEV_ICON = { critical: ICON.critical, high: ICON.high, medium: ICON.medium, low: ICON.low, info: ICON.info, unknown: ICON.info };
355
344
  const sevTag = (s) => chalk.hex(SEV_COLOR[s] || '#9ca3af').bold(`${SEV_ICON[s] || '•'} ${String(s || '?').toUpperCase()}`);
356
345
 
357
- // Analyse statique du code (SAST) d'un fichier ou dossier local.
358
346
  async function doCodeScan(target) {
359
347
  console.log('\n' + rule(`ANALYSE CODE — ${target}`));
360
348
  const spin = startSpinner(chalk.gray('analyse du code…'));
361
349
  const res = analyzeCode(target);
362
- await sleep(180); // laisse voir le spinner même sur petit scan
350
+ await sleep(180);
363
351
  spin.stop();
364
352
  if (res.error) { console.log(chalk.red(` ${ICON.warn} ` + res.error)); process.exit(1); }
365
353
 
@@ -375,7 +363,7 @@ async function doCodeScan(target) {
375
363
  for (const f of stored) {
376
364
  console.log(' ' + sevTag(f.severity) + ' ' + chalk.bold(f.title));
377
365
  console.log(' ' + chalk.gray(f.detail));
378
- if (stagger) await sleep(35); // révélation animée
366
+ if (stagger) await sleep(35);
379
367
  }
380
368
  const counts = {};
381
369
  for (const f of stored) counts[f.severity] = (counts[f.severity] || 0) + 1;
@@ -1,6 +1,3 @@
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
1
  import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
5
2
  import { join } from 'node:path';
6
3
  import { DATA_DIR } from '../core/store.js';
@@ -26,9 +26,9 @@ export default {
26
26
 
27
27
  let items = [];
28
28
  try {
29
- items = JSON.parse(raw); // tableau de PoCs
29
+ items = JSON.parse(raw);
30
30
  } catch {
31
- // certains builds écrivent du JSONL
31
+
32
32
  items = raw.split(/\r?\n/).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
33
33
  }
34
34
  return [].concat(items).map((p) => ({
@@ -1,5 +1,3 @@
1
- // gobuster : brute-force de dossiers/fichiers web. Parsing de la sortie texte.
2
-
3
1
  import { existsSync } from 'node:fs';
4
2
  import { join, dirname } from 'node:path';
5
3
  import { fileURLToPath } from 'node:url';
@@ -27,7 +25,7 @@ export default {
27
25
 
28
26
  parse({ stdout }) {
29
27
  const findings = [];
30
- // Lignes type: /admin (Status: 301) [Size: 0]
28
+
31
29
  const re = /^(\/\S*)\s+\(Status:\s*(\d+)\)(?:\s*\[Size:\s*(\d+)\])?/gim;
32
30
  let m;
33
31
  while ((m = re.exec(stdout)) !== null) {
@@ -1,4 +1,3 @@
1
- // Collision de nom avec la lib Python `httpx` : verify() valide le bon binaire.
2
1
  import { execFileSync } from 'node:child_process';
3
2
  import { RESOLVERS_FILE } from '../core/resolvers.js';
4
3
 
@@ -1,4 +1,3 @@
1
- // Sur Windows msfconsole est un .bat -> lancement via `cmd /c`.
2
1
  export default {
3
2
  id: 'metasploit',
4
3
  name: 'Metasploit',
@@ -10,9 +10,6 @@ export default {
10
10
  winPaths: [],
11
11
  install: 'go install github.com/projectdiscovery/naabu/v2/cmd/naabu@latest',
12
12
 
13
- // Sous Windows, naabu dépend de Npcap (wpcap.dll). Sans elle, le binaire
14
- // plante au lancement (spawn UNKNOWN). On le considère alors indisponible
15
- // pour qu'Icarus l'ignore proprement (nmap couvre déjà le scan de ports).
16
13
  verify() {
17
14
  if (process.platform !== 'win32') return true;
18
15
  return [
@@ -23,7 +23,7 @@ export default {
23
23
 
24
24
  command(target, opts = {}) {
25
25
  const args = ['-u', target, '-jsonl', '-silent', '-no-color'];
26
- // Sous VPN, le résolveur interne de nuclei échoue -> on force des DNS valides.
26
+
27
27
  if (RESOLVERS_FILE) args.push('-resolvers', RESOLVERS_FILE);
28
28
  if (opts.severity) args.push('-severity', opts.severity);
29
29
  if (opts.tags) args.push('-tags', opts.tags);
@@ -3,7 +3,7 @@ export default {
3
3
  name: 'sqlmap',
4
4
  category: 'web',
5
5
  description: "Détection d'injections SQL",
6
- needs: 'url', // idéalement une URL avec paramètre, ex: http://site/p?id=1
6
+ needs: 'url',
7
7
  bins: ['sqlmap', 'sqlmap.exe'],
8
8
  winPaths: [],
9
9
  install: 'pip install sqlmap (ou https://sqlmap.org)',
@@ -53,7 +53,6 @@ export default {
53
53
  findings.push({ type: 'sqli', severity: 'info', title: 'Aucune injection SQL trouvée', detail: target, ref: 'sqlmap' });
54
54
  }
55
55
 
56
- // Bases de données dumpées avec --dbs
57
56
  const dbBlock = stdout.match(/available databases \[\d+\]:([\s\S]*?)(?:\n\n|\n\[)/i);
58
57
  if (dbBlock) {
59
58
  const dbs = dbBlock[1].split(/\r?\n/).map((l) => l.replace(/^\s*\[\*\]\s*/, '').trim()).filter(Boolean);
package/src/tools/zap.js CHANGED
@@ -4,8 +4,6 @@ import { normalize } from '../core/severity.js';
4
4
 
5
5
  const RISK = { 3: 'high', 2: 'medium', 1: 'low', 0: 'info' };
6
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
7
  function findJava() {
10
8
  const exe = process.platform === 'win32' ? 'java.exe' : 'java';
11
9
  const cands = [];
@@ -34,10 +32,7 @@ export default {
34
32
 
35
33
  command(target, opts = {}) {
36
34
  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).
35
+
41
36
  const dir = opts.bin ? dirname(opts.bin) : '';
42
37
  let jar = '';
43
38
  try { jar = readdirSync(dir).find((f) => /^zap.*\.jar$/i.test(f)) || ''; } catch { /* ignore */ }
package/src/ui/anim.js CHANGED
@@ -1,18 +1,14 @@
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
1
  import chalk from 'chalk';
4
2
 
5
3
  const ESC = '\x1b';
6
4
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
5
 
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
6
  export const GOLD = '#d4a843';
11
7
  export const chevron = chalk.hex(GOLD)('❯');
12
8
  export const ICON = {
13
9
  chevron: '❯', dot: '●',
14
10
  ok: '✓', miss: '✗', warn: '!', star: '✦', report: '❯',
15
- // toutes les sévérités = même point, la couleur porte le sens
11
+
16
12
  critical: '●', high: '●', medium: '●', low: '●', info: '●',
17
13
  };
18
14
  export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -22,7 +18,6 @@ const HIDE = `${ESC}[?25l`;
22
18
  const SHOW = `${ESC}[?25h`;
23
19
  const CLEARLINE = `\r${ESC}[2K`;
24
20
 
25
- // Spinner avec lignes de résultat qui défilent au-dessus (spin.log).
26
21
  export function startSpinner(text) {
27
22
  if (!isTTY()) {
28
23
  return { update() {}, log: (l) => console.log(l), stop: (l) => l && console.log(l) };
@@ -55,5 +50,4 @@ export async function revealLines(lines, { delay = 26, indent = '' } = {}) {
55
50
  }
56
51
  }
57
52
 
58
- // Efface la ligne courante (utilisé pour nettoyer après un spinner inline).
59
53
  export const clearLine = () => { if (isTTY()) process.stdout.write(CLEARLINE); };
package/src/ui/banner.js CHANGED
@@ -5,8 +5,8 @@ import { isTTY, sleep, revealLines } from './anim.js';
5
5
  const GOLD = '#d4a843';
6
6
  const GOLD_HI = '#fff3cf';
7
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.
8
+ const CLR = '\r\x1b[2K';
9
+
10
10
  const PAD = ' '.repeat(Math.max(0, Math.round((LOGO.length ? 44 : 20) / 2) - 6));
11
11
 
12
12
  function wordmark(highlight = -1) {
@@ -27,14 +27,12 @@ export function banner() {
27
27
  );
28
28
  }
29
29
 
30
- // Bannière animée : déploiement du logo + balayage lumineux sur ICARUS.
31
30
  export async function intro() {
32
31
  if (!isTTY()) { console.log(banner()); return; }
33
32
  process.stdout.write('\n');
34
33
  await revealLines(LOGO, { delay: 22, indent: INDENT });
35
34
  process.stdout.write('\n');
36
35
 
37
- // balayage lumineux lettre par lettre
38
36
  for (let h = 0; h <= 6; h++) {
39
37
  process.stdout.write(CLR + PAD + wordmark(h));
40
38
  await sleep(55);
package/src/ui/logo.js CHANGED
@@ -1,4 +1,3 @@
1
- // Généré par scripts/gen-logo.mjs depuis logo.png — ne pas éditer à la main.
2
1
  export const LOGO = [
3
2
  " \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
3
  " \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 ",
package/src/ui/results.js CHANGED
@@ -1,5 +1,3 @@
1
- // Affichage des résultats en table dans le terminal.
2
-
3
1
  import chalk from 'chalk';
4
2
  import Table from 'cli-table3';
5
3
  import { rank, paint, SEVERITIES } from '../core/severity.js';
package/src/web/server.js CHANGED
@@ -1,7 +1,3 @@
1
- // Serveur web local d'Icarus : sert l'UI, expose une API REST et streame la
2
- // sortie des scans en direct (NDJSON sur la réponse POST). Aucune dépendance
3
- // externe : http natif uniquement.
4
-
5
1
  import { createServer } from 'node:http';
6
2
  import { readFileSync, existsSync, createReadStream } from 'node:fs';
7
3
  import { join, dirname, extname, basename } from 'node:path';
@@ -30,7 +26,6 @@ function readBody(req) {
30
26
  });
31
27
  }
32
28
 
33
- // Schéma "public" des outils (avec options) pour que l'UI génère ses contrôles.
34
29
  function toolsState() {
35
30
  return toolStatus({ fresh: true }).map(({ tool, bin, available }) => ({
36
31
  id: tool.id,
@@ -127,7 +122,6 @@ export function startWeb({ port = 7373 } = {}) {
127
122
  return serveStatic(res, f);
128
123
  }
129
124
 
130
- // Fichiers statiques de l'UI.
131
125
  const rel = url.pathname === '/' ? 'index.html' : url.pathname.replace(/^\//, '');
132
126
  return serveStatic(res, join(PUBLIC, basename(rel)));
133
127
  } catch (err) {
@@ -135,7 +129,17 @@ export function startWeb({ port = 7373 } = {}) {
135
129
  }
136
130
  });
137
131
 
138
- return new Promise((resolve) => {
139
- server.listen(port, () => resolve({ server, url: `http://localhost:${port}` }));
132
+ return new Promise((resolve, reject) => {
133
+ let p = port;
134
+ let tries = 0;
135
+ server.on('error', (err) => {
136
+ if (err.code === 'EADDRINUSE' && tries++ < 15) {
137
+ server.listen(++p);
138
+ } else {
139
+ reject(err);
140
+ }
141
+ });
142
+ server.once('listening', () => resolve({ server, url: `http://localhost:${p}`, port: p }));
143
+ server.listen(p);
140
144
  });
141
145
  }