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.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/package.json +40 -0
- package/scripts/postinstall.mjs +9 -0
- package/src/core/codescan.js +121 -0
- package/src/core/detect.js +64 -0
- package/src/core/registry.js +52 -0
- package/src/core/resolvers.js +53 -0
- package/src/core/runner.js +94 -0
- package/src/core/setup.js +102 -0
- package/src/core/severity.js +35 -0
- package/src/core/store.js +82 -0
- package/src/core/target.js +5 -0
- package/src/index.js +425 -0
- package/src/report/html.js +166 -0
- package/src/tools/dalfox.js +42 -0
- package/src/tools/ffuf.js +45 -0
- package/src/tools/gobuster.js +44 -0
- package/src/tools/httpx.js +48 -0
- package/src/tools/katana.js +45 -0
- package/src/tools/metasploit.js +59 -0
- package/src/tools/naabu.js +59 -0
- package/src/tools/nikto.js +34 -0
- package/src/tools/nmap.js +96 -0
- package/src/tools/nuclei.js +51 -0
- package/src/tools/sqlmap.js +64 -0
- package/src/tools/subfinder.js +27 -0
- package/src/tools/wafw00f.js +33 -0
- package/src/tools/whatweb.js +40 -0
- package/src/tools/wpscan.js +58 -0
- package/src/tools/zap.js +73 -0
- package/src/ui/anim.js +59 -0
- package/src/ui/banner.js +49 -0
- package/src/ui/logo.js +20 -0
- package/src/ui/results.js +45 -0
- package/src/web/public/index.html +253 -0
- package/src/web/public/logo.png +0 -0
- package/src/web/server.js +141 -0
- package/tools/resolvers.txt +7 -0
- package/wordlists/common.txt +140 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Icarus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Icarus
|
|
2
|
+
|
|
3
|
+
Plateforme de pentest tout-en-un. Regroupe les meilleurs outils (nmap, nuclei,
|
|
4
|
+
sqlmap, ZAP, ffuf, katana, nikto, dalfox, httpx…) derrière une seule CLI + TUI,
|
|
5
|
+
avec une base de résultats commune, des rapports HTML/JSON, une interface web
|
|
6
|
+
locale et un analyseur de code statique (SAST).
|
|
7
|
+
|
|
8
|
+
> ⚠️ **Usage strictement autorisé** : uniquement sur tes propres systèmes ou
|
|
9
|
+
> avec un mandat écrit signé. Scanner un tiers sans autorisation est illégal.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Pré-requis : **Node.js ≥ 20**.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g icarus-cmd
|
|
17
|
+
icarus setup # télécharge les binaires Go (nuclei, httpx, ffuf, katana…)
|
|
18
|
+
icarus doctor # affiche ce qu'il reste à installer (nmap, ZAP, sqlmap…)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`icarus setup` récupère les binaires précompilés depuis les releases GitHub
|
|
22
|
+
(Windows / Linux / macOS, amd64 & arm64). Les outils à runtime (nmap, OWASP ZAP,
|
|
23
|
+
nikto, sqlmap, wafw00f, wpscan, Metasploit) s'installent à part — `icarus doctor`
|
|
24
|
+
donne la commande pour chacun.
|
|
25
|
+
|
|
26
|
+
## Utilisation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
icarus # TUI interactive
|
|
30
|
+
icarus scan example.com --profile full --timeout 300
|
|
31
|
+
icarus scan http://site/p?id=1 --tools sqlmap,nuclei
|
|
32
|
+
icarus code ./mon-projet # analyse statique du code (SAST)
|
|
33
|
+
icarus web # interface web locale (http://localhost:7373)
|
|
34
|
+
icarus report # rapport HTML + JSON
|
|
35
|
+
icarus doctor # état des outils
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Options de scan
|
|
39
|
+
|
|
40
|
+
| Option | Effet |
|
|
41
|
+
|--------|-------|
|
|
42
|
+
| `--profile recon\|web\|vuln\|full` | enchaîne un profil d'outils |
|
|
43
|
+
| `--tools nmap,nuclei,sqlmap` | choisit des outils précis |
|
|
44
|
+
| `--parallel` | lance tous les outils en même temps |
|
|
45
|
+
| `--fast` | mode rapide (sévérités hautes, options légères) |
|
|
46
|
+
| `--timeout 300` | délai max par outil (s) — mets large pour nuclei |
|
|
47
|
+
|
|
48
|
+
### Analyse de code (SAST)
|
|
49
|
+
|
|
50
|
+
`icarus code <fichier|dossier>` cherche des motifs de vulnérabilités dans le
|
|
51
|
+
source : secrets en dur, injections (SQL/commande/code), XSS, désérialisation
|
|
52
|
+
non sûre, TLS désactivé, crypto faible… Multi-langage (JS/TS, Python, PHP, Java,
|
|
53
|
+
Go, Ruby…).
|
|
54
|
+
|
|
55
|
+
## Licence
|
|
56
|
+
|
|
57
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "icarus-cmd",
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"icarus": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"wordlists",
|
|
12
|
+
"tools/resolvers.txt",
|
|
13
|
+
"scripts/postinstall.mjs",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/index.js",
|
|
18
|
+
"setup": "node src/index.js setup",
|
|
19
|
+
"web": "node src/index.js web",
|
|
20
|
+
"doctor": "node src/index.js doctor",
|
|
21
|
+
"postinstall": "node scripts/postinstall.mjs || true"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pentest", "security", "sast", "nuclei", "nmap", "sqlmap",
|
|
28
|
+
"ffuf", "recon", "vulnerability-scanner", "cli", "infosec"
|
|
29
|
+
],
|
|
30
|
+
"author": "Icarus",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@inquirer/prompts": "^7.2.1",
|
|
34
|
+
"chalk": "^5.4.1",
|
|
35
|
+
"cli-table3": "^0.6.5"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"pngjs": "^7.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
const c = (s, n) => `\x1b[${n}m${s}\x1b[0m`;
|
|
4
|
+
console.log('');
|
|
5
|
+
console.log(' ' + c('Icarus installé.', '1') + ' Étape suivante :');
|
|
6
|
+
console.log(' ' + c('icarus setup', '33') + ' télécharge nuclei, httpx, ffuf, katana… (binaires)');
|
|
7
|
+
console.log(' ' + c('icarus doctor', '33') + ' voir ce qui reste (nmap, ZAP, sqlmap…)');
|
|
8
|
+
console.log(' ' + c('icarus', '33') + ' lance l\'interface');
|
|
9
|
+
console.log('');
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
6
|
+
import { join, extname, basename, relative } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const SKIP_DIRS = new Set([
|
|
9
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'vendor', '.next', 'coverage',
|
|
10
|
+
'__pycache__', '.venv', 'venv', 'target', 'bin', 'obj', '.idea', '.vscode',
|
|
11
|
+
]);
|
|
12
|
+
const TEXT_EXTS = new Set([
|
|
13
|
+
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.php', '.rb', '.java',
|
|
14
|
+
'.go', '.cs', '.c', '.cpp', '.h', '.sh', '.bash', '.ps1', '.sql', '.html',
|
|
15
|
+
'.env', '.yml', '.yaml', '.json', '.xml', '.ini', '.conf', '.cfg', '.txt',
|
|
16
|
+
]);
|
|
17
|
+
const MAX_FILE = 1.5 * 1024 * 1024; // 1.5 Mo
|
|
18
|
+
|
|
19
|
+
const ANY = null; // règle valable pour toutes extensions
|
|
20
|
+
|
|
21
|
+
// exts = liste d'extensions ('.js') ou ANY ; re testée ligne par ligne.
|
|
22
|
+
const RULES = [
|
|
23
|
+
// ---- Secrets / credentials (tous fichiers) ----
|
|
24
|
+
{ id: 'secret-aws-key', severity: 'critical', exts: ANY, title: 'Cle AWS Access Key en dur', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
25
|
+
{ id: 'secret-private-key', severity: 'critical', exts: ANY, title: 'Cle privee en dur', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
26
|
+
{ 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
|
+
{ id: 'secret-bearer', severity: 'high', exts: ANY, title: 'Token Bearer en dur', re: /bearer\s+[a-z0-9._\-]{20,}/i },
|
|
28
|
+
{ id: 'secret-slack', severity: 'high', exts: ANY, title: 'Token Slack en dur', re: /xox[baprs]-[0-9A-Za-z-]{10,}/ },
|
|
29
|
+
|
|
30
|
+
// ---- Injection de commande / code ----
|
|
31
|
+
{ id: 'js-eval', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'], title: 'eval() — execution de code', re: /\beval\s*\(/ },
|
|
32
|
+
{ id: 'js-new-function', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'], title: 'new Function() — execution dynamique', re: /\bnew\s+Function\s*\(/ },
|
|
33
|
+
{ 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*\([^)]*\+/ },
|
|
34
|
+
{ id: 'py-eval-exec', severity: 'high', exts: ['.py'], title: 'eval()/exec() — execution de code', re: /\b(?:eval|exec)\s*\(/ },
|
|
35
|
+
{ id: 'py-os-system', severity: 'high', exts: ['.py'], title: 'os.system() — injection commande', re: /\bos\.system\s*\(/ },
|
|
36
|
+
{ id: 'py-subprocess-shell', severity: 'high', exts: ['.py'], title: 'subprocess shell=True — injection commande', re: /subprocess\.[a-z]+\([^)]*shell\s*=\s*True/ },
|
|
37
|
+
{ 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
|
+
|
|
39
|
+
// ---- Injection SQL ----
|
|
40
|
+
{ 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
|
+
{ 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
|
+
|
|
43
|
+
// ---- XSS ----
|
|
44
|
+
{ id: 'js-innerhtml', severity: 'medium', exts: ['.js', '.ts', '.tsx', '.jsx', '.html'], title: 'innerHTML/outerHTML — risque XSS', re: /\.(?:inner|outer)HTML\s*=/ },
|
|
45
|
+
{ id: 'react-dangerously', severity: 'medium', exts: ['.jsx', '.tsx', '.js', '.ts'], title: 'dangerouslySetInnerHTML — risque XSS', re: /dangerouslySetInnerHTML/ },
|
|
46
|
+
{ id: 'js-doc-write', severity: 'medium', exts: ['.js', '.ts', '.html'], title: 'document.write — risque XSS', re: /document\.write\s*\(/ },
|
|
47
|
+
{ id: 'php-echo-super', severity: 'medium', exts: ['.php'], title: 'echo direct de $_GET/$_POST — XSS', re: /echo\s+[^;]*\$_(?:GET|POST|REQUEST)/i },
|
|
48
|
+
|
|
49
|
+
// ---- Deserialisation / parsing dangereux ----
|
|
50
|
+
{ id: 'py-pickle', severity: 'high', exts: ['.py'], title: 'pickle.loads — deserialisation non sure', re: /pickle\.loads?\s*\(/ },
|
|
51
|
+
{ id: 'py-yaml-load', severity: 'high', exts: ['.py'], title: 'yaml.load sans SafeLoader', re: /yaml\.load\s*\((?![^)]*Safe)/ },
|
|
52
|
+
{ id: 'php-unserialize', severity: 'high', exts: ['.php'], title: 'unserialize() — deserialisation non sure', re: /\bunserialize\s*\(/ },
|
|
53
|
+
|
|
54
|
+
// ---- TLS / crypto ----
|
|
55
|
+
{ id: 'tls-node-reject', severity: 'high', exts: ['.js', '.mjs', '.cjs', '.ts'], title: 'rejectUnauthorized:false — TLS non verifie', re: /rejectUnauthorized\s*:\s*false/ },
|
|
56
|
+
{ id: 'tls-py-verify', severity: 'high', exts: ['.py'], title: 'verify=False — TLS non verifie', re: /verify\s*=\s*False/ },
|
|
57
|
+
{ id: 'tls-curl-insecure', severity: 'medium', exts: ANY, title: 'curl -k / --insecure — TLS non verifie', re: /curl\s+[^\n]*(?:\s-k\b|--insecure)/ },
|
|
58
|
+
{ id: 'crypto-weak', severity: 'medium', exts: ANY, title: 'Hash faible (MD5/SHA1)', re: /\b(?:md5|sha1)\s*\(/i },
|
|
59
|
+
{ id: 'js-math-random', severity: 'low', exts: ['.js', '.ts'], title: 'Math.random() — non cryptographique', re: /Math\.random\s*\(\)/ },
|
|
60
|
+
|
|
61
|
+
// ---- Divers ----
|
|
62
|
+
{ id: 'flask-debug', severity: 'medium', exts: ['.py'], title: 'Flask debug=True — execution de code', re: /\.run\s*\([^)]*debug\s*=\s*True/ },
|
|
63
|
+
{ id: 'cors-wildcard', severity: 'low', exts: ANY, title: 'CORS Access-Control-Allow-Origin: *', re: /Access-Control-Allow-Origin['"\s:]+\*/ },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function listFiles(root) {
|
|
67
|
+
const out = [];
|
|
68
|
+
const walk = (p) => {
|
|
69
|
+
let st;
|
|
70
|
+
try { st = statSync(p); } catch { return; }
|
|
71
|
+
if (st.isDirectory()) {
|
|
72
|
+
if (SKIP_DIRS.has(basename(p))) return;
|
|
73
|
+
let entries = [];
|
|
74
|
+
try { entries = readdirSync(p); } catch { return; }
|
|
75
|
+
for (const e of entries) walk(join(p, e));
|
|
76
|
+
} else if (st.isFile() && st.size <= MAX_FILE) {
|
|
77
|
+
const ext = extname(p).toLowerCase();
|
|
78
|
+
if (TEXT_EXTS.has(ext) || basename(p).startsWith('.env')) out.push(p);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(root);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scanFile(file, root) {
|
|
86
|
+
const findings = [];
|
|
87
|
+
let text;
|
|
88
|
+
try { text = readFileSync(file, 'utf8'); } catch { return findings; }
|
|
89
|
+
const ext = extname(file).toLowerCase();
|
|
90
|
+
const rel = relative(root, file) || basename(file);
|
|
91
|
+
const lines = text.split(/\r?\n/);
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
if (line.length > 1000) continue;
|
|
96
|
+
for (const rule of RULES) {
|
|
97
|
+
if (rule.exts !== ANY && !rule.exts.includes(ext)) continue;
|
|
98
|
+
if (rule.re.test(line)) {
|
|
99
|
+
findings.push({
|
|
100
|
+
type: 'code',
|
|
101
|
+
severity: rule.severity,
|
|
102
|
+
title: rule.title,
|
|
103
|
+
detail: `${rel}:${i + 1} ${line.trim().slice(0, 120)}`,
|
|
104
|
+
ref: rule.id,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Analyse un fichier ou un dossier. Renvoie { findings, files, root }.
|
|
113
|
+
export function analyzeCode(target) {
|
|
114
|
+
if (!existsSync(target)) return { error: `Chemin introuvable : ${target}`, findings: [], files: 0 };
|
|
115
|
+
const st = statSync(target);
|
|
116
|
+
const files = st.isDirectory() ? listFiles(target) : [target];
|
|
117
|
+
const root = st.isDirectory() ? target : join(target, '..');
|
|
118
|
+
const findings = [];
|
|
119
|
+
for (const f of files) findings.push(...scanFile(f, root));
|
|
120
|
+
return { findings, files: files.length, root: target };
|
|
121
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
|
|
12
|
+
// Dossier où Icarus installe les binaires précompilés (install-tools.ps1).
|
|
13
|
+
const BUNDLED_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'tools', 'bin');
|
|
14
|
+
|
|
15
|
+
function whichBin(name) {
|
|
16
|
+
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
17
|
+
try {
|
|
18
|
+
const out = execFileSync(finder, [name], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
19
|
+
.toString()
|
|
20
|
+
.trim()
|
|
21
|
+
.split(/\r?\n/)[0];
|
|
22
|
+
return out && existsSync(out) ? out : (out || null);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Renvoie le chemin résolu du binaire d'un outil, ou null si introuvable.
|
|
29
|
+
export function resolveBin(tool, { fresh = false } = {}) {
|
|
30
|
+
if (!fresh && cache.has(tool.id)) return cache.get(tool.id);
|
|
31
|
+
|
|
32
|
+
const candidates = [];
|
|
33
|
+
// 1) binaires installés localement par Icarus (tools/bin)
|
|
34
|
+
for (const b of tool.bins || []) {
|
|
35
|
+
const local = join(BUNDLED_DIR, b.endsWith('.exe') ? b : `${b}.exe`);
|
|
36
|
+
if (existsSync(local)) candidates.push(local);
|
|
37
|
+
}
|
|
38
|
+
// 2) chemins d'installation Windows connus
|
|
39
|
+
for (const p of tool.winPaths || []) {
|
|
40
|
+
if (existsSync(p)) candidates.push(p);
|
|
41
|
+
}
|
|
42
|
+
// 3) PATH
|
|
43
|
+
for (const b of tool.bins || []) {
|
|
44
|
+
const r = whichBin(b);
|
|
45
|
+
if (r) candidates.push(r);
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
let found = null;
|
|
52
|
+
for (const c of candidates) {
|
|
53
|
+
if (typeof tool.verify === 'function' && !tool.verify(c)) continue;
|
|
54
|
+
found = c;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
cache.set(tool.id, found);
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearCache() {
|
|
63
|
+
cache.clear();
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
import { resolveBin } from './detect.js';
|
|
6
|
+
|
|
7
|
+
import nmap from '../tools/nmap.js';
|
|
8
|
+
import naabu from '../tools/naabu.js';
|
|
9
|
+
import subfinder from '../tools/subfinder.js';
|
|
10
|
+
import httpx from '../tools/httpx.js';
|
|
11
|
+
import katana from '../tools/katana.js';
|
|
12
|
+
import whatweb from '../tools/whatweb.js';
|
|
13
|
+
import wafw00f from '../tools/wafw00f.js';
|
|
14
|
+
import nuclei from '../tools/nuclei.js';
|
|
15
|
+
import ffuf from '../tools/ffuf.js';
|
|
16
|
+
import gobuster from '../tools/gobuster.js';
|
|
17
|
+
import nikto from '../tools/nikto.js';
|
|
18
|
+
import sqlmap from '../tools/sqlmap.js';
|
|
19
|
+
import dalfox from '../tools/dalfox.js';
|
|
20
|
+
import wpscan from '../tools/wpscan.js';
|
|
21
|
+
import zap from '../tools/zap.js';
|
|
22
|
+
import metasploit from '../tools/metasploit.js';
|
|
23
|
+
|
|
24
|
+
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
|
|
28
|
+
];
|
|
29
|
+
|
|
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
|
+
export const PROFILES = {
|
|
33
|
+
recon: { label: 'Reconnaissance', tools: ['nmap', 'naabu', 'subfinder', 'httpx', 'katana', 'whatweb', 'wafw00f'] },
|
|
34
|
+
web: { label: 'Application web', tools: ['httpx', 'whatweb', 'katana', 'ffuf', 'gobuster', 'nikto', 'sqlmap', 'dalfox', 'wpscan', 'zap'] },
|
|
35
|
+
vuln: { label: 'Vulnérabilités', tools: ['nuclei', 'nikto', 'sqlmap', 'dalfox', 'metasploit'] },
|
|
36
|
+
full: { label: 'Complet (tout)', tools: TOOLS.map((t) => t.id) },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function getTool(id) {
|
|
40
|
+
return TOOLS.find((t) => t.id === id) || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function toolStatus({ fresh = false } = {}) {
|
|
44
|
+
return TOOLS.map((t) => {
|
|
45
|
+
const bin = resolveBin(t, { fresh });
|
|
46
|
+
return { tool: t, bin, available: Boolean(bin) };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function availableIds() {
|
|
51
|
+
return toolStatus().filter((s) => s.available).map((s) => s.tool.id);
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { tmpdir, homedir } from 'node:os';
|
|
16
|
+
import { getServers } from 'node:dns';
|
|
17
|
+
|
|
18
|
+
const BUNDLED = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'tools', 'resolvers.txt');
|
|
19
|
+
|
|
20
|
+
function pickPath() {
|
|
21
|
+
for (const base of [tmpdir(), homedir()]) {
|
|
22
|
+
if (base && !/\s/.test(base)) return join(base, '.icarus-resolvers.txt');
|
|
23
|
+
}
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildList() {
|
|
28
|
+
const out = [];
|
|
29
|
+
try {
|
|
30
|
+
for (const s of getServers()) {
|
|
31
|
+
const ip = String(s).replace(/%.*$/, '').replace(/^\[|\]$/g, '');
|
|
32
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) out.push(ip); // IPv4 seulement
|
|
33
|
+
}
|
|
34
|
+
} catch { /* ignore */ }
|
|
35
|
+
// 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.
|
|
37
|
+
if (out.length) return out.slice(0, 1);
|
|
38
|
+
// Secours : liste statique fournie avec le projet.
|
|
39
|
+
try { return readFileSync(BUNDLED, 'utf8').split(/\r?\n/).map((l) => l.trim()).filter(Boolean); }
|
|
40
|
+
catch { return []; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let resolved = '';
|
|
44
|
+
try {
|
|
45
|
+
const dst = pickPath();
|
|
46
|
+
const list = buildList();
|
|
47
|
+
if (dst && list.length) {
|
|
48
|
+
writeFileSync(dst, list.join('\r\n') + '\r\n'); // CRLF obligatoire pour nuclei
|
|
49
|
+
resolved = dst;
|
|
50
|
+
}
|
|
51
|
+
} catch { resolved = ''; }
|
|
52
|
+
|
|
53
|
+
export const RESOLVERS_FILE = resolved;
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { mkdtempSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { resolveBin } from './detect.js';
|
|
9
|
+
import { recordScan, addFindings } from './store.js';
|
|
10
|
+
|
|
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
|
+
export function runProcess(cmd, args, { onData, cwd, env, timeoutMs = 0, shell = false } = {}) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let stdout = '';
|
|
16
|
+
let stderr = '';
|
|
17
|
+
let timedOut = false;
|
|
18
|
+
let timer;
|
|
19
|
+
let child;
|
|
20
|
+
try {
|
|
21
|
+
child = spawn(cmd, args, { cwd, env: { ...process.env, ...env }, shell });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
resolve({ code: -1, stdout, stderr: err.message, error: err });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (timeoutMs > 0) {
|
|
27
|
+
timer = setTimeout(() => {
|
|
28
|
+
timedOut = true;
|
|
29
|
+
onData?.(`\n[icarus] ⏱ timeout ${Math.round(timeoutMs / 1000)}s — process arrêté\n`, 'err');
|
|
30
|
+
child.kill('SIGKILL');
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
}
|
|
33
|
+
child.stdout.on('data', (d) => { const s = d.toString(); stdout += s; onData?.(s, 'out'); });
|
|
34
|
+
child.stderr.on('data', (d) => { const s = d.toString(); stderr += s; onData?.(s, 'err'); });
|
|
35
|
+
child.on('error', (err) => { clearTimeout(timer); resolve({ code: -1, stdout, stderr: `${stderr}\n${err.message}`, error: err, timedOut }); });
|
|
36
|
+
child.on('close', (code) => { clearTimeout(timer); resolve({ code, stdout, stderr, timedOut }); });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Orchestration d'un outil de bout en bout : détection -> commande -> run ->
|
|
41
|
+
// parse -> stockage. Renvoie { ok, findings, code, error }.
|
|
42
|
+
export async function runTool(tool, target, { opts = {}, onData, onLog } = {}) {
|
|
43
|
+
const log = onLog || (() => {});
|
|
44
|
+
const bin = resolveBin(tool);
|
|
45
|
+
if (!bin) {
|
|
46
|
+
return { ok: false, findings: [], code: -1, error: `${tool.name} introuvable. Installe-le : ${tool.install}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let workdir;
|
|
50
|
+
if (tool.needsWorkdir) workdir = mkdtempSync(join(tmpdir(), `pentesthub-${tool.id}-`));
|
|
51
|
+
|
|
52
|
+
const built = tool.command(target, { ...opts, bin, workdir });
|
|
53
|
+
let cmd = built.cmd || bin;
|
|
54
|
+
let args = built.args || [];
|
|
55
|
+
let shell = false;
|
|
56
|
+
|
|
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
|
+
if (process.platform === 'win32' && !built.cmd && /\.(bat|cmd)$/i.test(cmd)) {
|
|
63
|
+
const q = (s) => (/[\s"&()[\]{}^=;!'+,`~%<>|]/.test(String(s))
|
|
64
|
+
? `"${String(s).replace(/"/g, '""')}"`
|
|
65
|
+
: String(s));
|
|
66
|
+
cmd = [cmd, ...args].map(q).join(' ');
|
|
67
|
+
args = [];
|
|
68
|
+
shell = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Certains lanceurs (ZAP zap.bat) chargent leurs fichiers via des chemins
|
|
72
|
+
// relatifs et doivent donc s'exécuter depuis leur dossier d'installation.
|
|
73
|
+
// Un outil peut imposer son répertoire de travail via built.cwd.
|
|
74
|
+
const runCwd = built.cwd || workdir;
|
|
75
|
+
|
|
76
|
+
const timeoutMs = Number(opts.timeout) > 0 ? Number(opts.timeout) * 1000 : 0;
|
|
77
|
+
log(`$ ${cmd} ${args.join(' ')}${timeoutMs ? ` (timeout ${opts.timeout}s)` : ''}`);
|
|
78
|
+
const result = await runProcess(cmd, args, { onData, cwd: runCwd, timeoutMs, shell });
|
|
79
|
+
|
|
80
|
+
const scanId = recordScan({ tool: tool.id, target, code: result.code, command: `${cmd} ${args.join(' ')}` });
|
|
81
|
+
|
|
82
|
+
let parsed = [];
|
|
83
|
+
try {
|
|
84
|
+
parsed = (await tool.parse({ ...result, target, workdir, opts })) || [];
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log(`(parser ${tool.id} a échoué: ${err.message})`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const findings = addFindings(
|
|
90
|
+
parsed.map((f) => ({ scanId, tool: tool.id, target, ...f }))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return { ok: result.code === 0 || findings.length > 0, findings, code: result.code, error: result.error };
|
|
94
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
import { mkdtempSync, mkdirSync, existsSync, copyFileSync, readdirSync, createWriteStream, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { Readable } from 'node:stream';
|
|
12
|
+
import { pipeline } from 'node:stream/promises';
|
|
13
|
+
|
|
14
|
+
const BIN_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'tools', 'bin');
|
|
15
|
+
|
|
16
|
+
const TOOLS = [
|
|
17
|
+
{ id: 'nuclei', repo: 'projectdiscovery/nuclei' },
|
|
18
|
+
{ id: 'httpx', repo: 'projectdiscovery/httpx' },
|
|
19
|
+
{ id: 'naabu', repo: 'projectdiscovery/naabu' },
|
|
20
|
+
{ id: 'katana', repo: 'projectdiscovery/katana' },
|
|
21
|
+
{ id: 'subfinder', repo: 'projectdiscovery/subfinder' },
|
|
22
|
+
{ id: 'ffuf', repo: 'ffuf/ffuf' },
|
|
23
|
+
{ id: 'gobuster', repo: 'OJ/gobuster' },
|
|
24
|
+
{ id: 'dalfox', repo: 'hahwul/dalfox' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function osKeys() {
|
|
28
|
+
const p = process.platform;
|
|
29
|
+
const a = process.arch;
|
|
30
|
+
const os = p === 'win32' ? /(?:windows|win)/i : p === 'darwin' ? /(?:darwin|macos|mac|apple)/i : /linux/i;
|
|
31
|
+
const arch = a === 'arm64' ? /(?:arm64|aarch64)/i : /(?:amd64|x86_64|x64)/i;
|
|
32
|
+
const ext = p === 'win32' ? '.exe' : '';
|
|
33
|
+
return { os, arch, ext, notArm: a !== 'arm64' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function ghJson(url) {
|
|
37
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'IcarusSetup', Accept: 'application/vnd.github+json' } });
|
|
38
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function download(url, dest) {
|
|
43
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'IcarusSetup' } });
|
|
44
|
+
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
45
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Installe un outil. Renvoie true/false. log(msg) pour le suivi.
|
|
49
|
+
async function installTool(t, keys, tmp, log) {
|
|
50
|
+
const rel = await ghJson(`https://api.github.com/repos/${t.repo}/releases/latest`);
|
|
51
|
+
const asset = (rel.assets || []).find((x) =>
|
|
52
|
+
keys.os.test(x.name) && keys.arch.test(x.name) &&
|
|
53
|
+
!(keys.notArm && /arm/i.test(x.name) && !/x86_64|amd64|x64/i.test(x.name)) &&
|
|
54
|
+
/\.(zip|tar\.gz|tgz)$/i.test(x.name));
|
|
55
|
+
if (!asset) throw new Error('aucun binaire pour cet OS/arch');
|
|
56
|
+
|
|
57
|
+
const archive = join(tmp, asset.name);
|
|
58
|
+
await download(asset.browser_download_url, archive);
|
|
59
|
+
const ex = join(tmp, t.id);
|
|
60
|
+
mkdirSync(ex, { recursive: true });
|
|
61
|
+
execFileSync('tar', ['-xf', archive, '-C', ex], { stdio: 'ignore' });
|
|
62
|
+
|
|
63
|
+
const exeName = t.id + keys.ext;
|
|
64
|
+
const found = (function find(dir) {
|
|
65
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
66
|
+
const p = join(dir, e.name);
|
|
67
|
+
if (e.isDirectory()) { const r = find(p); if (r) return r; }
|
|
68
|
+
else if (e.name.toLowerCase() === exeName.toLowerCase()) return p;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
})(ex);
|
|
72
|
+
if (!found) throw new Error(`${exeName} introuvable dans l'archive`);
|
|
73
|
+
copyFileSync(found, join(BIN_DIR, exeName));
|
|
74
|
+
return asset.name;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function setupTools({ log = console.log } = {}) {
|
|
78
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
79
|
+
const keys = osKeys();
|
|
80
|
+
const tmp = mkdtempSync(join(tmpdir(), 'icarus-setup-'));
|
|
81
|
+
const ok = [];
|
|
82
|
+
const fail = [];
|
|
83
|
+
|
|
84
|
+
log(`\n Icarus setup — ${process.platform}/${process.arch} → ${BIN_DIR}\n`);
|
|
85
|
+
for (const t of TOOLS) {
|
|
86
|
+
try {
|
|
87
|
+
const name = await installTool(t, keys, tmp, log);
|
|
88
|
+
log(` ✓ ${t.id.padEnd(10)} ${name}`);
|
|
89
|
+
ok.push(t.id);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
log(` ✗ ${t.id.padEnd(10)} ${e.message}`);
|
|
92
|
+
fail.push(t.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
96
|
+
|
|
97
|
+
log(`\n Installés (${ok.length}) : ${ok.join(', ') || '-'}`);
|
|
98
|
+
if (fail.length) log(` Échecs (${fail.length}) : ${fail.join(', ')}`);
|
|
99
|
+
log(`\n Reste à installer manuellement (voir « icarus doctor ») :`);
|
|
100
|
+
log(` nmap, OWASP ZAP, nikto, sqlmap, wafw00f, wpscan, Metasploit.\n`);
|
|
101
|
+
return { ok, fail };
|
|
102
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
export const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info', 'unknown'];
|
|
5
|
+
|
|
6
|
+
// Plus le rang est petit, plus c'est grave (utile pour trier).
|
|
7
|
+
export function rank(sev) {
|
|
8
|
+
const i = SEVERITIES.indexOf(String(sev || 'unknown').toLowerCase());
|
|
9
|
+
return i < 0 ? SEVERITIES.length : i;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Normalise n'importe quelle étiquette de sévérité vers notre échelle.
|
|
13
|
+
export function normalize(sev) {
|
|
14
|
+
const s = String(sev || '').toLowerCase().trim();
|
|
15
|
+
if (['critical', 'crit', '4'].includes(s)) return 'critical';
|
|
16
|
+
if (['high', 'haute', '3'].includes(s)) return 'high';
|
|
17
|
+
if (['medium', 'moyenne', 'moderate', '2'].includes(s)) return 'medium';
|
|
18
|
+
if (['low', 'basse', 'minor', '1'].includes(s)) return 'low';
|
|
19
|
+
if (['info', 'informational', 'information', '0'].includes(s)) return 'info';
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const COLORS = {
|
|
24
|
+
critical: (c, t) => c.bgRed.white.bold(` ${t} `),
|
|
25
|
+
high: (c, t) => c.red.bold(t),
|
|
26
|
+
medium: (c, t) => c.yellow(t),
|
|
27
|
+
low: (c, t) => c.cyan(t),
|
|
28
|
+
info: (c, t) => c.gray(t),
|
|
29
|
+
unknown: (c, t) => c.gray(t),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function paint(chalk, sev, text = sev) {
|
|
33
|
+
const fn = COLORS[normalize(sev)] || COLORS.unknown;
|
|
34
|
+
return fn(chalk, text);
|
|
35
|
+
}
|