muaddib-scanner 1.0.21 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +339 -325
- package/README.md +363 -350
- package/bin/muaddib.js +169 -33
- package/data/iocs.json +1 -1
- package/package.json +8 -2
- package/src/safe-install.js +163 -0
package/bin/muaddib.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
3
2
|
const { run } = require('../src/index.js');
|
|
4
3
|
const { updateIOCs } = require('../src/ioc/updater.js');
|
|
5
4
|
const { watch } = require('../src/watch.js');
|
|
6
5
|
const { startDaemon } = require('../src/daemon.js');
|
|
7
6
|
const { runScraper } = require('../src/ioc/scraper.js');
|
|
7
|
+
const { safeInstall } = require('../src/safe-install.js');
|
|
8
8
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
const command = args[0];
|
|
11
11
|
const options = args.slice(1);
|
|
12
12
|
|
|
13
|
+
// Parse options
|
|
13
14
|
let target = '.';
|
|
14
15
|
let jsonOutput = false;
|
|
15
16
|
let htmlOutput = null;
|
|
@@ -43,35 +44,133 @@ for (let i = 0; i < options.length; i++) {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
// Menu interactif si pas de commande
|
|
48
|
+
async function interactiveMenu() {
|
|
49
|
+
const { select, input, confirm } = await import('@inquirer/prompts');
|
|
50
|
+
|
|
47
51
|
console.log(`
|
|
52
|
+
╔══════════════════════════════════════════╗
|
|
53
|
+
║ MUAD'DIB - npm Supply Chain Hunter ║
|
|
54
|
+
║ "The worms must die." ║
|
|
55
|
+
╚══════════════════════════════════════════╝
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
const action = await select({
|
|
59
|
+
message: 'Que veux-tu faire ?',
|
|
60
|
+
choices: [
|
|
61
|
+
{ name: 'Scanner un projet', value: 'scan' },
|
|
62
|
+
{ name: 'Scanner avec mode paranoid', value: 'scan-paranoid' },
|
|
63
|
+
{ name: 'Surveiller un projet (watch)', value: 'watch' },
|
|
64
|
+
{ name: 'Lancer le daemon', value: 'daemon' },
|
|
65
|
+
{ name: 'Mettre a jour les IOCs', value: 'update' },
|
|
66
|
+
{ name: 'Scraper nouveaux IOCs', value: 'scrape' },
|
|
67
|
+
{ name: 'Quitter', value: 'quit' }
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (action === 'quit') {
|
|
72
|
+
console.log('Bye!');
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (action === 'scan' || action === 'scan-paranoid') {
|
|
77
|
+
const path = await input({
|
|
78
|
+
message: 'Chemin du projet :',
|
|
79
|
+
default: '.'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const outputFormat = await select({
|
|
83
|
+
message: 'Format de sortie :',
|
|
84
|
+
choices: [
|
|
85
|
+
{ name: 'Console (defaut)', value: 'console' },
|
|
86
|
+
{ name: 'JSON', value: 'json' },
|
|
87
|
+
{ name: 'HTML', value: 'html' },
|
|
88
|
+
{ name: 'SARIF (GitHub Security)', value: 'sarif' }
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const opts = {
|
|
93
|
+
json: outputFormat === 'json',
|
|
94
|
+
html: outputFormat === 'html' ? 'muaddib-report.html' : null,
|
|
95
|
+
sarif: outputFormat === 'sarif' ? 'muaddib-results.sarif' : null,
|
|
96
|
+
explain: true,
|
|
97
|
+
failLevel: 'high',
|
|
98
|
+
paranoid: action === 'scan-paranoid'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const exitCode = await run(path, opts);
|
|
102
|
+
process.exit(exitCode);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (action === 'watch') {
|
|
106
|
+
const path = await input({
|
|
107
|
+
message: 'Chemin du projet :',
|
|
108
|
+
default: '.'
|
|
109
|
+
});
|
|
110
|
+
watch(path);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (action === 'daemon') {
|
|
114
|
+
const useWebhook = await confirm({
|
|
115
|
+
message: 'Configurer un webhook Discord/Slack ?',
|
|
116
|
+
default: false
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let webhook = null;
|
|
120
|
+
if (useWebhook) {
|
|
121
|
+
webhook = await input({
|
|
122
|
+
message: 'URL du webhook :'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
startDaemon({ webhook });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (action === 'update') {
|
|
129
|
+
await updateIOCs();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (action === 'scrape') {
|
|
134
|
+
const result = await runScraper();
|
|
135
|
+
console.log(`[OK] ${result.added} nouveaux IOCs (total: ${result.total})`);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Main
|
|
141
|
+
if (!command || command === '--help' || command === '-h') {
|
|
142
|
+
if (command === '--help' || command === '-h') {
|
|
143
|
+
console.log(`
|
|
48
144
|
MUAD'DIB - npm Supply Chain Threat Hunter
|
|
49
145
|
|
|
50
146
|
Usage:
|
|
51
|
-
muaddib
|
|
52
|
-
muaddib
|
|
53
|
-
muaddib
|
|
54
|
-
muaddib
|
|
55
|
-
muaddib
|
|
56
|
-
muaddib
|
|
57
|
-
|
|
147
|
+
muaddib Mode interactif
|
|
148
|
+
muaddib scan [path] [options] Scanner un projet
|
|
149
|
+
muaddib watch [path] Surveiller en temps reel
|
|
150
|
+
muaddib daemon [options] Lancer le daemon
|
|
151
|
+
muaddib update Mettre a jour les IOCs
|
|
152
|
+
muaddib scrape Scraper nouveaux IOCs
|
|
153
|
+
muaddib install <pkg> Installer apres scan (safe)
|
|
154
|
+
|
|
58
155
|
Options:
|
|
59
|
-
--json
|
|
60
|
-
--html [file]
|
|
61
|
-
--sarif [file]
|
|
62
|
-
--explain
|
|
63
|
-
--fail-on [level]
|
|
64
|
-
|
|
65
|
-
--
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
156
|
+
--json Sortie JSON
|
|
157
|
+
--html [file] Rapport HTML
|
|
158
|
+
--sarif [file] Rapport SARIF (GitHub Security)
|
|
159
|
+
--explain Explications detaillees
|
|
160
|
+
--fail-on [level] Niveau d'echec (critical|high|medium|low)
|
|
161
|
+
--webhook [url] Webhook Discord/Slack
|
|
162
|
+
--paranoid Mode ultra-strict
|
|
163
|
+
`);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
interactiveMenu().catch(err => {
|
|
167
|
+
console.error('[ERROR]', err.message);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
170
|
+
} else if (command === 'scan') {
|
|
171
|
+
run(target, {
|
|
172
|
+
json: jsonOutput,
|
|
173
|
+
html: htmlOutput,
|
|
75
174
|
sarif: sarifOutput,
|
|
76
175
|
explain: explainMode,
|
|
77
176
|
failLevel: failLevel,
|
|
@@ -91,7 +190,7 @@ if (command === 'scan') {
|
|
|
91
190
|
});
|
|
92
191
|
} else if (command === 'scrape') {
|
|
93
192
|
runScraper().then(result => {
|
|
94
|
-
console.log(`[OK] ${result.added}
|
|
193
|
+
console.log(`[OK] ${result.added} nouveaux IOCs (total: ${result.total})`);
|
|
95
194
|
process.exit(0);
|
|
96
195
|
}).catch(err => {
|
|
97
196
|
console.error('[ERROR]', err.message);
|
|
@@ -99,13 +198,50 @@ if (command === 'scan') {
|
|
|
99
198
|
});
|
|
100
199
|
} else if (command === 'daemon') {
|
|
101
200
|
startDaemon({ webhook: webhookUrl });
|
|
201
|
+
} else if (command === 'install' || command === 'i') {
|
|
202
|
+
const packages = options.filter(o => !o.startsWith('-'));
|
|
203
|
+
const isDev = options.includes('--save-dev') || options.includes('-D');
|
|
204
|
+
const isGlobal = options.includes('-g') || options.includes('--global');
|
|
205
|
+
const force = options.includes('--force');
|
|
206
|
+
|
|
207
|
+
if (packages.length === 0) {
|
|
208
|
+
console.log('Usage: muaddib install <package> [<package>...] [--save-dev] [-g] [--force]');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
safeInstall(packages, { isDev, isGlobal, force }).then(result => {
|
|
213
|
+
if (result.blocked && !force) {
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}).catch(err => {
|
|
218
|
+
console.error('[ERROR]', err.message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
});
|
|
102
221
|
} else if (command === 'help') {
|
|
103
|
-
console.log(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
222
|
+
console.log(`
|
|
223
|
+
MUAD'DIB - npm Supply Chain Threat Hunter
|
|
224
|
+
|
|
225
|
+
Usage:
|
|
226
|
+
muaddib Mode interactif
|
|
227
|
+
muaddib scan [path] [options] Scanner un projet
|
|
228
|
+
muaddib watch [path] Surveiller en temps reel
|
|
229
|
+
muaddib daemon [options] Lancer le daemon
|
|
230
|
+
muaddib update Mettre a jour les IOCs
|
|
231
|
+
muaddib scrape Scraper nouveaux IOCs
|
|
232
|
+
|
|
233
|
+
Options:
|
|
234
|
+
--json Sortie JSON
|
|
235
|
+
--html [file] Rapport HTML
|
|
236
|
+
--sarif [file] Rapport SARIF (GitHub Security)
|
|
237
|
+
--explain Explications detaillees
|
|
238
|
+
--fail-on [level] Niveau d'echec (critical|high|medium|low)
|
|
239
|
+
--webhook [url] Webhook Discord/Slack
|
|
240
|
+
--paranoid Mode ultra-strict
|
|
241
|
+
`);
|
|
242
|
+
process.exit(0);
|
|
108
243
|
} else {
|
|
109
|
-
console.log(`
|
|
244
|
+
console.log(`Commande inconnue: ${command}`);
|
|
245
|
+
console.log('Tape "muaddib help" pour voir les commandes.');
|
|
110
246
|
process.exit(1);
|
|
111
247
|
}
|
package/data/iocs.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,9 +36,15 @@
|
|
|
36
36
|
"node": ">=18.0.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@inquirer/prompts": "^8.1.0",
|
|
40
|
+
"@vietmoney/react-big-calendar": "^0.0.1-security",
|
|
39
41
|
"acorn": "^8.14.0",
|
|
40
42
|
"acorn-walk": "^8.3.4",
|
|
43
|
+
"chalk": "^5.6.2",
|
|
44
|
+
"express": "^5.2.1",
|
|
45
|
+
"is-odd": "^3.0.1",
|
|
41
46
|
"js-yaml": "^4.1.0",
|
|
42
|
-
"lodash": "^4.17.21"
|
|
47
|
+
"lodash": "^4.17.21",
|
|
48
|
+
"yargs": "^18.0.0"
|
|
43
49
|
}
|
|
44
50
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { loadCachedIOCs } = require('./ioc/updater.js');
|
|
5
|
+
|
|
6
|
+
// Packages connus sûrs qui utilisent des patterns "suspects" légitimement
|
|
7
|
+
const TRUSTED_PACKAGES = [
|
|
8
|
+
'lodash', 'underscore', 'express', 'react', 'vue', 'angular',
|
|
9
|
+
'webpack', 'babel', 'typescript', 'esbuild', 'vite', 'rollup',
|
|
10
|
+
'jest', 'mocha', 'chai', 'sharp', 'bcrypt', 'argon2'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// Cache pour eviter de scanner deux fois le meme package
|
|
14
|
+
const scannedPackages = new Set();
|
|
15
|
+
|
|
16
|
+
// Verifier si un package est dans les IOCs
|
|
17
|
+
function checkIOCs(pkg, pkgName) {
|
|
18
|
+
try {
|
|
19
|
+
const iocs = loadCachedIOCs();
|
|
20
|
+
return iocs.packages?.find(p => p.name === pkg || p.name === pkgName);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Scanner un package et ses dependances recursivement
|
|
27
|
+
async function scanPackageRecursive(pkg, depth = 0, maxDepth = 3) {
|
|
28
|
+
const indent = ' '.repeat(depth);
|
|
29
|
+
const pkgName = pkg.replace(/^@[^/]+\//, '').split('@')[0];
|
|
30
|
+
|
|
31
|
+
// Eviter les boucles infinies
|
|
32
|
+
if (scannedPackages.has(pkg) || scannedPackages.has(pkgName)) {
|
|
33
|
+
return { safe: true };
|
|
34
|
+
}
|
|
35
|
+
scannedPackages.add(pkg);
|
|
36
|
+
scannedPackages.add(pkgName);
|
|
37
|
+
|
|
38
|
+
// Skip trusted packages
|
|
39
|
+
if (TRUSTED_PACKAGES.includes(pkgName)) {
|
|
40
|
+
if (depth === 0) console.log(`[OK] ${pkg} - Package de confiance`);
|
|
41
|
+
return { safe: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Limiter la profondeur
|
|
45
|
+
if (depth > maxDepth) {
|
|
46
|
+
return { safe: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (depth === 0) {
|
|
50
|
+
console.log(`[*] Analyse de ${pkg}...`);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`${indent}[*] Dependance: ${pkg}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Verifier IOCs
|
|
56
|
+
const malicious = checkIOCs(pkg, pkgName);
|
|
57
|
+
if (malicious) {
|
|
58
|
+
return {
|
|
59
|
+
safe: false,
|
|
60
|
+
package: pkg,
|
|
61
|
+
reason: 'known_malicious',
|
|
62
|
+
source: malicious.source || 'IOC Database',
|
|
63
|
+
description: malicious.description || 'Package malveillant connu',
|
|
64
|
+
depth
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Recuperer les infos du package
|
|
69
|
+
let pkgInfo;
|
|
70
|
+
try {
|
|
71
|
+
const infoRaw = execSync(`npm view ${pkg} --json 2>nul`, { encoding: 'utf8' });
|
|
72
|
+
pkgInfo = JSON.parse(infoRaw);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (depth === 0) console.log(`[!] Package ${pkg} introuvable sur npm`);
|
|
75
|
+
return { safe: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Scanner les dependances
|
|
79
|
+
const dependencies = pkgInfo.dependencies || {};
|
|
80
|
+
const depNames = Object.keys(dependencies);
|
|
81
|
+
|
|
82
|
+
if (depNames.length > 0 && depth < maxDepth) {
|
|
83
|
+
for (const depName of depNames) {
|
|
84
|
+
const depVersion = dependencies[depName];
|
|
85
|
+
const depPkg = depName; // On check juste le nom, pas la version specifique
|
|
86
|
+
|
|
87
|
+
const result = await scanPackageRecursive(depPkg, depth + 1, maxDepth);
|
|
88
|
+
if (!result.safe) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (depth === 0) {
|
|
95
|
+
console.log(`[OK] ${pkg} - Aucune menace (${depNames.length} dependances scannees)`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { safe: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function safeInstall(packages, options = {}) {
|
|
102
|
+
const { isDev, isGlobal, force } = options;
|
|
103
|
+
|
|
104
|
+
console.log(`
|
|
105
|
+
╔══════════════════════════════════════════╗
|
|
106
|
+
║ MUAD'DIB Safe Install ║
|
|
107
|
+
║ Scanning packages + dependencies... ║
|
|
108
|
+
╚══════════════════════════════════════════╝
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
// Reset le cache pour chaque install
|
|
112
|
+
scannedPackages.clear();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
for (const pkg of packages) {
|
|
116
|
+
const result = await scanPackageRecursive(pkg);
|
|
117
|
+
|
|
118
|
+
if (!result.safe) {
|
|
119
|
+
console.log(`
|
|
120
|
+
╔══════════════════════════════════════════╗
|
|
121
|
+
║ [!] PACKAGE MALVEILLANT DETECTE ║
|
|
122
|
+
╚══════════════════════════════════════════╝
|
|
123
|
+
`);
|
|
124
|
+
if (result.depth > 0) {
|
|
125
|
+
console.log(`Package demande: ${pkg}`);
|
|
126
|
+
console.log(`Dependance malveillante: ${result.package} (profondeur: ${result.depth})`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`Package: ${result.package}`);
|
|
129
|
+
}
|
|
130
|
+
console.log(`Source: ${result.source}`);
|
|
131
|
+
console.log(`Raison: ${result.description}`);
|
|
132
|
+
console.log('');
|
|
133
|
+
|
|
134
|
+
if (!force) {
|
|
135
|
+
console.log('[!] Installation BLOQUEE.');
|
|
136
|
+
return { blocked: true, package: result.package, threats: [{ type: 'known_malicious', severity: 'CRITICAL', message: result.description }] };
|
|
137
|
+
} else {
|
|
138
|
+
console.log('[!] --force active, installation malgre les menaces...');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Tout est clean, installer pour de vrai
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log('[*] Installation en cours...');
|
|
146
|
+
|
|
147
|
+
let cmd = `npm install ${packages.join(' ')}`;
|
|
148
|
+
if (isDev) cmd += ' --save-dev';
|
|
149
|
+
if (isGlobal) cmd += ' -g';
|
|
150
|
+
|
|
151
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
152
|
+
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log('[OK] Installation terminee.');
|
|
155
|
+
|
|
156
|
+
return { blocked: false };
|
|
157
|
+
|
|
158
|
+
} catch (e) {
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { safeInstall };
|