muaddib-scanner 1.0.1 → 1.0.3

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/bin/muaddib.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { run } = require('../src/index.js');
4
4
  const { updateIOCs } = require('../src/ioc/updater.js');
5
5
  const { watch } = require('../src/watch.js');
6
+ const { startDaemon } = require('../src/daemon.js');
6
7
 
7
8
  const args = process.argv.slice(2);
8
9
  const command = args[0];
@@ -13,7 +14,8 @@ let jsonOutput = false;
13
14
  let htmlOutput = null;
14
15
  let sarifOutput = null;
15
16
  let explainMode = false;
16
- let failLevel = 'high'; // Par defaut, fail sur HIGH et CRITICAL
17
+ let failLevel = 'high';
18
+ let webhookUrl = null;
17
19
 
18
20
  for (let i = 0; i < options.length; i++) {
19
21
  if (options[i] === '--json') {
@@ -29,6 +31,9 @@ for (let i = 0; i < options.length; i++) {
29
31
  } else if (options[i] === '--fail-on') {
30
32
  failLevel = options[i + 1] || 'high';
31
33
  i++;
34
+ } else if (options[i] === '--webhook') {
35
+ webhookUrl = options[i + 1];
36
+ i++;
32
37
  } else if (!options[i].startsWith('-')) {
33
38
  target = options[i];
34
39
  }
@@ -51,6 +56,8 @@ if (!command) {
51
56
  --explain Affiche les details de chaque detection
52
57
  --fail-on [level] Niveau de severite pour exit code (critical|high|medium|low)
53
58
  Defaut: high (fail sur HIGH et CRITICAL)
59
+ --webhook [url] Envoie une alerte Discord/Slack
60
+ muaddib daemon [options] Lance le daemon de surveillance
54
61
  `);
55
62
  process.exit(0);
56
63
  }
@@ -61,7 +68,8 @@ if (command === 'scan') {
61
68
  html: htmlOutput,
62
69
  sarif: sarifOutput,
63
70
  explain: explainMode,
64
- failLevel: failLevel
71
+ failLevel: failLevel,
72
+ webhook: webhookUrl
65
73
  }).then(exitCode => {
66
74
  process.exit(exitCode);
67
75
  });
@@ -75,10 +83,12 @@ if (command === 'scan') {
75
83
  process.exit(1);
76
84
  });
77
85
  } else if (command === 'help') {
78
- console.log('muaddib scan [path] [--json] [--html file] [--sarif file] [--explain] [--fail-on level]');
86
+ console.log('muaddib scan [path] [--json] [--html file] [--sarif file] [--explain] [--fail-on level] [--webhook url]');
79
87
  console.log('muaddib watch [path] - Surveille un projet en temps reel');
80
88
  console.log('muaddib update - Met a jour les IOCs');
89
+ } else if (command === 'daemon') {
90
+ startDaemon({ webhook: webhookUrl });
81
91
  } else {
82
92
  console.log(`Commande inconnue: ${command}`);
83
93
  process.exit(1);
84
- }
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "acorn": "^8.14.0",
40
40
  "acorn-walk": "^8.3.4",
41
- "js-yaml": "^4.1.0"
41
+ "js-yaml": "^4.1.0",
42
+ "lodash": "^4.17.21"
42
43
  }
43
44
  }
package/src/daemon.js ADDED
@@ -0,0 +1,142 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+ const { run } = require('./index.js');
5
+
6
+ let webhookUrl = null;
7
+ let watchedDirs = [];
8
+ let isRunning = false;
9
+
10
+ async function startDaemon(options = {}) {
11
+ webhookUrl = options.webhook || null;
12
+ isRunning = true;
13
+
14
+ console.log(`
15
+ ╔════════════════════════════════════════════╗
16
+ ║ MUAD'DIB Security Daemon ║
17
+ ║ Surveillance npm install active ║
18
+ ╚════════════════════════════════════════════╝
19
+ `);
20
+
21
+ console.log('[DAEMON] Demarrage...');
22
+ console.log(`[DAEMON] Webhook: ${webhookUrl ? 'Configure' : 'Non configure'}`);
23
+ console.log('[DAEMON] Ctrl+C pour arreter\n');
24
+
25
+ // Surveille le dossier courant
26
+ const cwd = process.cwd();
27
+ watchDirectory(cwd);
28
+
29
+ // Garde le processus actif
30
+ process.on('SIGINT', () => {
31
+ console.log('\n[DAEMON] Arret...');
32
+ isRunning = false;
33
+ process.exit(0);
34
+ });
35
+
36
+ // Boucle infinie
37
+ while (isRunning) {
38
+ await sleep(1000);
39
+ }
40
+ }
41
+
42
+ function watchDirectory(dir) {
43
+ const nodeModulesPath = path.join(dir, 'node_modules');
44
+ const packageLockPath = path.join(dir, 'package-lock.json');
45
+ const yarnLockPath = path.join(dir, 'yarn.lock');
46
+
47
+ console.log(`[DAEMON] Surveillance de ${dir}`);
48
+
49
+ // Surveille package-lock.json
50
+ if (fs.existsSync(packageLockPath)) {
51
+ watchFile(packageLockPath, dir);
52
+ }
53
+
54
+ // Surveille yarn.lock
55
+ if (fs.existsSync(yarnLockPath)) {
56
+ watchFile(yarnLockPath, dir);
57
+ }
58
+
59
+ // Surveille node_modules
60
+ if (fs.existsSync(nodeModulesPath)) {
61
+ watchNodeModules(nodeModulesPath, dir);
62
+ }
63
+
64
+ // Surveille la creation de node_modules
65
+ fs.watch(dir, (eventType, filename) => {
66
+ if (filename === 'node_modules' && eventType === 'rename') {
67
+ const nmPath = path.join(dir, 'node_modules');
68
+ if (fs.existsSync(nmPath)) {
69
+ console.log('[DAEMON] node_modules detecte, scan en cours...');
70
+ triggerScan(dir);
71
+ }
72
+ }
73
+ if (filename === 'package-lock.json' || filename === 'yarn.lock') {
74
+ console.log(`[DAEMON] ${filename} modifie, scan en cours...`);
75
+ triggerScan(dir);
76
+ }
77
+ });
78
+ }
79
+
80
+ function watchFile(filePath, projectDir) {
81
+ let lastMtime = fs.statSync(filePath).mtime.getTime();
82
+
83
+ fs.watch(filePath, (eventType) => {
84
+ if (eventType === 'change') {
85
+ const currentMtime = fs.statSync(filePath).mtime.getTime();
86
+ if (currentMtime !== lastMtime) {
87
+ lastMtime = currentMtime;
88
+ console.log(`[DAEMON] ${path.basename(filePath)} modifie`);
89
+ triggerScan(projectDir);
90
+ }
91
+ }
92
+ });
93
+ }
94
+
95
+ function watchNodeModules(nodeModulesPath, projectDir) {
96
+ fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
97
+ if (filename && filename.includes('package.json')) {
98
+ console.log(`[DAEMON] Nouveau package detecte: ${filename}`);
99
+ triggerScan(projectDir);
100
+ }
101
+ });
102
+ }
103
+
104
+ let scanTimeout = null;
105
+ let lastScanTime = 0;
106
+
107
+ function triggerScan(dir) {
108
+ const now = Date.now();
109
+
110
+ // Debounce: attend 3 secondes avant de scanner
111
+ if (scanTimeout) {
112
+ clearTimeout(scanTimeout);
113
+ }
114
+
115
+ // Evite les scans trop frequents (minimum 10 secondes entre chaque)
116
+ if (now - lastScanTime < 10000) {
117
+ scanTimeout = setTimeout(() => triggerScan(dir), 10000 - (now - lastScanTime));
118
+ return;
119
+ }
120
+
121
+ scanTimeout = setTimeout(async () => {
122
+ lastScanTime = Date.now();
123
+ console.log(`\n[DAEMON] ========== SCAN AUTOMATIQUE ==========`);
124
+ console.log(`[DAEMON] Cible: ${dir}`);
125
+ console.log(`[DAEMON] Heure: ${new Date().toLocaleTimeString()}\n`);
126
+
127
+ try {
128
+ await run(dir, { webhook: webhookUrl });
129
+ } catch (err) {
130
+ console.log(`[DAEMON] Erreur scan: ${err.message}`);
131
+ }
132
+
133
+ console.log(`\n[DAEMON] ======================================\n`);
134
+ console.log('[DAEMON] En attente de modifications...');
135
+ }, 3000);
136
+ }
137
+
138
+ function sleep(ms) {
139
+ return new Promise(resolve => setTimeout(resolve, ms));
140
+ }
141
+
142
+ module.exports = { startDaemon };
package/src/index.js CHANGED
@@ -9,6 +9,8 @@ const { getPlaybook } = require('./response/playbooks.js');
9
9
  const { getRule } = require('./rules/index.js');
10
10
  const { saveReport } = require('./report.js');
11
11
  const { saveSARIF } = require('./sarif.js');
12
+ const { scanTyposquatting } = require('./scanner/typosquat.js');
13
+ const { sendWebhook } = require('./webhook.js');
12
14
 
13
15
  async function run(targetPath, options = {}) {
14
16
  const threats = [];
@@ -34,6 +36,10 @@ async function run(targetPath, options = {}) {
34
36
  const dataflowThreats = await analyzeDataFlow(targetPath);
35
37
  threats.push(...dataflowThreats);
36
38
 
39
+ // Scan typosquatting
40
+ const typosquatThreats = await scanTyposquatting(targetPath);
41
+ threats.push(...typosquatThreats);
42
+
37
43
  // Enrichir chaque menace avec les regles
38
44
  const enrichedThreats = threats.map(t => {
39
45
  const rule = getRule(t.type);
@@ -48,15 +54,34 @@ async function run(targetPath, options = {}) {
48
54
  };
49
55
  });
50
56
 
57
+ // Calculer le score de risque (0-100)
58
+ const criticalCount = threats.filter(t => t.severity === 'CRITICAL').length;
59
+ const highCount = threats.filter(t => t.severity === 'HIGH').length;
60
+ const mediumCount = threats.filter(t => t.severity === 'MEDIUM').length;
61
+
62
+ let riskScore = 0;
63
+ riskScore += criticalCount * 25; // CRITICAL = 25 points
64
+ riskScore += highCount * 10; // HIGH = 10 points
65
+ riskScore += mediumCount * 3; // MEDIUM = 3 points
66
+ riskScore = Math.min(100, riskScore); // Cap a 100
67
+
68
+ const riskLevel = riskScore >= 75 ? 'CRITICAL'
69
+ : riskScore >= 50 ? 'HIGH'
70
+ : riskScore >= 25 ? 'MEDIUM'
71
+ : riskScore > 0 ? 'LOW'
72
+ : 'SAFE';
73
+
51
74
  const result = {
52
75
  target: targetPath,
53
76
  timestamp: new Date().toISOString(),
54
77
  threats: enrichedThreats,
55
78
  summary: {
56
79
  total: threats.length,
57
- critical: threats.filter(t => t.severity === 'CRITICAL').length,
58
- high: threats.filter(t => t.severity === 'HIGH').length,
59
- medium: threats.filter(t => t.severity === 'MEDIUM').length
80
+ critical: criticalCount,
81
+ high: highCount,
82
+ medium: mediumCount,
83
+ riskScore: riskScore,
84
+ riskLevel: riskLevel
60
85
  }
61
86
  };
62
87
 
@@ -100,10 +125,14 @@ async function run(targetPath, options = {}) {
100
125
  });
101
126
  }
102
127
  }
103
- // Sortie normale
128
+ // Sortie normale
104
129
  else {
105
130
  console.log(`\n[MUADDIB] Scan de ${targetPath}\n`);
106
131
 
132
+ // Afficher le score de risque
133
+ const scoreBar = '█'.repeat(Math.floor(result.summary.riskScore / 5)) + '░'.repeat(20 - Math.floor(result.summary.riskScore / 5));
134
+ console.log(`[SCORE] ${result.summary.riskScore}/100 [${scoreBar}] ${result.summary.riskLevel}\n`);
135
+
107
136
  if (threats.length === 0) {
108
137
  console.log('[OK] Aucune menace detectee.\n');
109
138
  } else {
@@ -124,7 +153,17 @@ async function run(targetPath, options = {}) {
124
153
  }
125
154
  }
126
155
 
127
- // Calculer exit code selon le niveau de fail
156
+ // Envoyer webhook si configure
157
+ if (options.webhook) {
158
+ try {
159
+ await sendWebhook(options.webhook, result);
160
+ console.log(`[OK] Alerte envoyee au webhook`);
161
+ } catch (err) {
162
+ console.log(`[WARN] Echec envoi webhook: ${err.message}`);
163
+ }
164
+ }
165
+
166
+ // Calculer exit code selon le niveau de fail
128
167
  const failLevel = options.failLevel || 'high';
129
168
  const severityLevels = {
130
169
  critical: ['CRITICAL'],
@@ -106,6 +106,9 @@ const PLAYBOOKS = {
106
106
 
107
107
  suspicious_dataflow:
108
108
  'CRITIQUE: Code lit des credentials et les envoie sur le reseau. Exfiltration probable. Isoler la machine, regenerer tous les secrets.',
109
+
110
+ typosquat_detected:
111
+ 'ATTENTION: Ce package a un nom tres similaire a un package populaire. Verifier que c\'est bien le bon package. Si erreur de frappe, corriger immediatement.',
109
112
  };
110
113
 
111
114
  function getPlaybook(threatType) {
@@ -179,7 +179,20 @@ const RULES = {
179
179
  'https://blog.phylum.io/shai-hulud-npm-worm'
180
180
  ],
181
181
  mitre: 'T1041'
182
- }
182
+ },
183
+
184
+ typosquat_detected: {
185
+ id: 'MUADDIB-TYPO-001',
186
+ name: 'Typosquatting Detected',
187
+ severity: 'HIGH',
188
+ confidence: 'high',
189
+ description: 'Package avec un nom tres similaire a un package populaire. Possible typosquatting.',
190
+ references: [
191
+ 'https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry',
192
+ 'https://snyk.io/blog/typosquatting-attacks/'
193
+ ],
194
+ mitre: 'T1195.002'
195
+ },
183
196
  };
184
197
 
185
198
  function getRule(type) {
@@ -0,0 +1,266 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // Top 100 packages npm les plus populaires (cibles de typosquatting)
5
+ const POPULAR_PACKAGES = [
6
+ 'lodash', 'express', 'react', 'axios', 'chalk', 'commander', 'moment',
7
+ 'request', 'async', 'bluebird', 'underscore', 'uuid', 'debug', 'mkdirp',
8
+ 'glob', 'minimist', 'webpack', 'babel-core', 'typescript', 'eslint',
9
+ 'prettier', 'jest', 'mocha', 'chai', 'sinon', 'mongoose', 'sequelize',
10
+ 'mysql', 'pg', 'redis', 'mongodb', 'socket.io', 'express-session',
11
+ 'body-parser', 'cookie-parser', 'cors', 'helmet', 'morgan', 'dotenv',
12
+ 'jsonwebtoken', 'bcrypt', 'passport', 'nodemailer', 'aws-sdk', 'stripe',
13
+ 'twilio', 'firebase', 'graphql', 'apollo-server', 'next', 'nuxt',
14
+ 'gatsby', 'vue', 'angular', 'svelte', 'electron', 'puppeteer', 'cheerio',
15
+ 'sharp', 'jimp', 'canvas', 'pdf-lib', 'exceljs', 'csv-parser', 'xml2js',
16
+ 'yaml', 'ini', 'config', 'yargs', 'inquirer', 'ora', 'chalk', 'colors',
17
+ 'winston', 'bunyan', 'pino', 'log4js', 'ramda', 'rxjs', 'immutable',
18
+ 'mobx', 'redux', 'zustand', 'formik', 'yup', 'joi', 'ajv', 'validator',
19
+ 'date-fns', 'dayjs', 'luxon', 'numeral', 'accounting', 'currency.js',
20
+ 'lodash-es', 'core-js', 'regenerator-runtime', 'tslib', 'classnames',
21
+ 'prop-types', 'cross-env', 'npm', 'yarn', 'pnpm', 'node-fetch', 'got'
22
+ ];
23
+
24
+ // Packages legitimes qui ressemblent a des populaires mais sont OK
25
+ const WHITELIST = [
26
+ 'acorn',
27
+ 'acorn-walk',
28
+ 'js-yaml',
29
+ 'cross-env',
30
+ 'node-fetch',
31
+ 'node-gyp',
32
+ 'core-js',
33
+ 'lodash-es',
34
+ 'date-fns',
35
+ 'ts-node',
36
+ 'ts-jest',
37
+ 'css-loader',
38
+ 'style-loader',
39
+ 'file-loader',
40
+ 'url-loader',
41
+ 'babel-loader',
42
+ 'vue-loader',
43
+ 'react-dom',
44
+ 'react-router',
45
+ 'react-redux',
46
+ 'vue-router',
47
+ 'express-session',
48
+ 'body-parser',
49
+ 'cookie-parser'
50
+ ];
51
+
52
+ // Techniques de typosquatting connues
53
+ const TYPOSQUAT_PATTERNS = [
54
+ { type: 'missing_char', fn: (name) => generateMissingChar(name) },
55
+ { type: 'extra_char', fn: (name) => generateExtraChar(name) },
56
+ { type: 'swapped_chars', fn: (name) => generateSwappedChars(name) },
57
+ { type: 'wrong_char', fn: (name) => generateWrongChar(name) },
58
+ { type: 'hyphen_tricks', fn: (name) => generateHyphenTricks(name) },
59
+ { type: 'suffix_tricks', fn: (name) => generateSuffixTricks(name) }
60
+ ];
61
+
62
+ async function scanTyposquatting(targetPath) {
63
+ const threats = [];
64
+ const packageJsonPath = path.join(targetPath, 'package.json');
65
+
66
+ if (!fs.existsSync(packageJsonPath)) {
67
+ return threats;
68
+ }
69
+
70
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
71
+ const dependencies = {
72
+ ...packageJson.dependencies,
73
+ ...packageJson.devDependencies,
74
+ ...packageJson.peerDependencies,
75
+ ...packageJson.optionalDependencies
76
+ };
77
+
78
+ for (const depName of Object.keys(dependencies)) {
79
+ const match = findTyposquatMatch(depName);
80
+ if (match) {
81
+ threats.push({
82
+ type: 'typosquat_detected',
83
+ severity: 'HIGH',
84
+ message: `Package "${depName}" ressemble a "${match.original}" (${match.type}). Possible typosquatting.`,
85
+ file: 'package.json',
86
+ details: {
87
+ suspicious: depName,
88
+ legitimate: match.original,
89
+ technique: match.type,
90
+ distance: match.distance
91
+ }
92
+ });
93
+ }
94
+ }
95
+
96
+ return threats;
97
+ }
98
+
99
+ function findTyposquatMatch(name) {
100
+ // Ignore les packages whitelistes
101
+ if (WHITELIST.includes(name)) return null;
102
+
103
+ // Ignore les packages scoped (@org/package)
104
+ if (name.startsWith('@')) return null;
105
+
106
+ for (const popular of POPULAR_PACKAGES) {
107
+ // Ignore si c'est exactement le meme
108
+ if (name === popular) continue;
109
+
110
+ const distance = levenshteinDistance(name, popular);
111
+
112
+ // Distance de 1 ou 2 = tres suspect
113
+ if (distance === 1) {
114
+ return {
115
+ original: popular,
116
+ type: detectTyposquatType(name, popular),
117
+ distance: distance
118
+ };
119
+ }
120
+
121
+ // Distance de 2 avec nom court = suspect
122
+ if (distance === 2 && popular.length <= 6) {
123
+ return {
124
+ original: popular,
125
+ type: detectTyposquatType(name, popular),
126
+ distance: distance
127
+ };
128
+ }
129
+
130
+ // Verifie les tricks de suffixe
131
+ if (isSuffixTrick(name, popular)) {
132
+ return {
133
+ original: popular,
134
+ type: 'suffix_trick',
135
+ distance: distance
136
+ };
137
+ }
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ function detectTyposquatType(typo, original) {
144
+ if (typo.length === original.length - 1) return 'missing_char';
145
+ if (typo.length === original.length + 1) return 'extra_char';
146
+ if (typo.length === original.length) {
147
+ // Verifie si swap
148
+ let diffs = 0;
149
+ for (let i = 0; i < typo.length; i++) {
150
+ if (typo[i] !== original[i]) diffs++;
151
+ }
152
+ if (diffs === 2) return 'swapped_chars';
153
+ return 'wrong_char';
154
+ }
155
+ return 'unknown';
156
+ }
157
+
158
+ function isSuffixTrick(name, popular) {
159
+ const suffixes = ['-js', '.js', '-node', '-npm', '-cli', '-api', '-lib', '-pkg', '-dev', '-pro'];
160
+ for (const suffix of suffixes) {
161
+ if (name === popular + suffix) return true;
162
+ if (name === popular.replace('-', '') + suffix) return true;
163
+ }
164
+ // Verifie aussi les prefixes
165
+ const prefixes = ['node-', 'npm-', 'js-', 'get-', 'the-'];
166
+ for (const prefix of prefixes) {
167
+ if (name === prefix + popular) return true;
168
+ }
169
+ return false;
170
+ }
171
+
172
+ function levenshteinDistance(a, b) {
173
+ const matrix = [];
174
+
175
+ for (let i = 0; i <= b.length; i++) {
176
+ matrix[i] = [i];
177
+ }
178
+
179
+ for (let j = 0; j <= a.length; j++) {
180
+ matrix[0][j] = j;
181
+ }
182
+
183
+ for (let i = 1; i <= b.length; i++) {
184
+ for (let j = 1; j <= a.length; j++) {
185
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
186
+ matrix[i][j] = matrix[i - 1][j - 1];
187
+ } else {
188
+ matrix[i][j] = Math.min(
189
+ matrix[i - 1][j - 1] + 1, // substitution
190
+ matrix[i][j - 1] + 1, // insertion
191
+ matrix[i - 1][j] + 1 // deletion
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ return matrix[b.length][a.length];
198
+ }
199
+
200
+ // Generateurs pour tests (pas utilises dans le scan, mais utiles pour enrichir les IOCs)
201
+ function generateMissingChar(name) {
202
+ const results = [];
203
+ for (let i = 0; i < name.length; i++) {
204
+ results.push(name.slice(0, i) + name.slice(i + 1));
205
+ }
206
+ return results;
207
+ }
208
+
209
+ function generateExtraChar(name) {
210
+ const results = [];
211
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789-';
212
+ for (let i = 0; i <= name.length; i++) {
213
+ for (const char of chars) {
214
+ results.push(name.slice(0, i) + char + name.slice(i));
215
+ }
216
+ }
217
+ return results;
218
+ }
219
+
220
+ function generateSwappedChars(name) {
221
+ const results = [];
222
+ for (let i = 0; i < name.length - 1; i++) {
223
+ const arr = name.split('');
224
+ [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
225
+ results.push(arr.join(''));
226
+ }
227
+ return results;
228
+ }
229
+
230
+ function generateWrongChar(name) {
231
+ const results = [];
232
+ const keyboard = {
233
+ 'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'serfcx', 'e': 'wsdfr',
234
+ 'f': 'drtgvc', 'g': 'ftyhbv', 'h': 'gyujnb', 'i': 'ujklo', 'j': 'huikmn',
235
+ 'k': 'jiolm', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'iklp',
236
+ 'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
237
+ 'u': 'yhji', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu', 'z': 'asx'
238
+ };
239
+ for (let i = 0; i < name.length; i++) {
240
+ const char = name[i].toLowerCase();
241
+ if (keyboard[char]) {
242
+ for (const replacement of keyboard[char]) {
243
+ results.push(name.slice(0, i) + replacement + name.slice(i + 1));
244
+ }
245
+ }
246
+ }
247
+ return results;
248
+ }
249
+
250
+ function generateHyphenTricks(name) {
251
+ const results = [];
252
+ // Ajouter des hyphens
253
+ for (let i = 1; i < name.length; i++) {
254
+ results.push(name.slice(0, i) + '-' + name.slice(i));
255
+ }
256
+ // Retirer des hyphens
257
+ results.push(name.replace(/-/g, ''));
258
+ return results;
259
+ }
260
+
261
+ function generateSuffixTricks(name) {
262
+ const suffixes = ['-js', '.js', '-node', '-npm', '-cli'];
263
+ return suffixes.map(s => name + s);
264
+ }
265
+
266
+ module.exports = { scanTyposquatting, levenshteinDistance };
package/src/webhook.js ADDED
@@ -0,0 +1,176 @@
1
+ const https = require('https');
2
+ const http = require('http');
3
+
4
+ async function sendWebhook(url, results) {
5
+ const isDiscord = url.includes('discord.com');
6
+ const isSlack = url.includes('hooks.slack.com');
7
+
8
+ let payload;
9
+
10
+ if (isDiscord) {
11
+ payload = formatDiscord(results);
12
+ } else if (isSlack) {
13
+ payload = formatSlack(results);
14
+ } else {
15
+ payload = formatGeneric(results);
16
+ }
17
+
18
+ return send(url, payload);
19
+ }
20
+
21
+ function formatDiscord(results) {
22
+ const { summary, threats, target } = results;
23
+
24
+ const color = summary.riskLevel === 'CRITICAL' ? 0xe74c3c
25
+ : summary.riskLevel === 'HIGH' ? 0xe67e22
26
+ : summary.riskLevel === 'MEDIUM' ? 0xf1c40f
27
+ : summary.riskLevel === 'LOW' ? 0x3498db
28
+ : 0x2ecc71;
29
+
30
+ const criticalThreats = threats
31
+ .filter(t => t.severity === 'CRITICAL')
32
+ .slice(0, 5)
33
+ .map(t => `- ${t.message}`)
34
+ .join('\n');
35
+
36
+ return {
37
+ embeds: [{
38
+ title: 'MUAD\'DIB Security Scan',
39
+ description: `Scan de **${target}**`,
40
+ color: color,
41
+ fields: [
42
+ {
43
+ name: 'Score de risque',
44
+ value: `**${summary.riskScore}/100** (${summary.riskLevel})`,
45
+ inline: true
46
+ },
47
+ {
48
+ name: 'Menaces',
49
+ value: `${summary.critical} CRITICAL\n${summary.high} HIGH\n${summary.medium} MEDIUM`,
50
+ inline: true
51
+ },
52
+ {
53
+ name: 'Total',
54
+ value: `**${summary.total}** menace(s)`,
55
+ inline: true
56
+ }
57
+ ],
58
+ footer: {
59
+ text: 'MUAD\'DIB - Supply-chain threat detection'
60
+ },
61
+ timestamp: results.timestamp
62
+ }]
63
+ };
64
+ }
65
+
66
+ function formatSlack(results) {
67
+ const { summary, threats, target } = results;
68
+
69
+ const emoji = summary.riskLevel === 'CRITICAL' ? ':rotating_light:'
70
+ : summary.riskLevel === 'HIGH' ? ':warning:'
71
+ : summary.riskLevel === 'MEDIUM' ? ':large_yellow_circle:'
72
+ : summary.riskLevel === 'LOW' ? ':information_source:'
73
+ : ':white_check_mark:';
74
+
75
+ const criticalList = threats
76
+ .filter(t => t.severity === 'CRITICAL')
77
+ .slice(0, 5)
78
+ .map(t => `• ${t.message}`)
79
+ .join('\n');
80
+
81
+ return {
82
+ blocks: [
83
+ {
84
+ type: 'header',
85
+ text: {
86
+ type: 'plain_text',
87
+ text: `${emoji} MUAD'DIB Security Scan`
88
+ }
89
+ },
90
+ {
91
+ type: 'section',
92
+ fields: [
93
+ {
94
+ type: 'mrkdwn',
95
+ text: `*Cible:*\n${target}`
96
+ },
97
+ {
98
+ type: 'mrkdwn',
99
+ text: `*Score:*\n${summary.riskScore}/100 (${summary.riskLevel})`
100
+ }
101
+ ]
102
+ },
103
+ {
104
+ type: 'section',
105
+ fields: [
106
+ {
107
+ type: 'mrkdwn',
108
+ text: `*CRITICAL:* ${summary.critical}`
109
+ },
110
+ {
111
+ type: 'mrkdwn',
112
+ text: `*HIGH:* ${summary.high}`
113
+ },
114
+ {
115
+ type: 'mrkdwn',
116
+ text: `*MEDIUM:* ${summary.medium}`
117
+ },
118
+ {
119
+ type: 'mrkdwn',
120
+ text: `*Total:* ${summary.total}`
121
+ }
122
+ ]
123
+ }
124
+ ]
125
+ };
126
+ }
127
+
128
+ function formatGeneric(results) {
129
+ return {
130
+ tool: 'MUADDIB',
131
+ target: results.target,
132
+ timestamp: results.timestamp,
133
+ summary: results.summary,
134
+ threats: results.threats.map(t => ({
135
+ type: t.type,
136
+ severity: t.severity,
137
+ message: t.message,
138
+ file: t.file
139
+ }))
140
+ };
141
+ }
142
+
143
+ function send(url, payload) {
144
+ return new Promise((resolve, reject) => {
145
+ const urlObj = new URL(url);
146
+ const protocol = urlObj.protocol === 'https:' ? https : http;
147
+
148
+ const options = {
149
+ hostname: urlObj.hostname,
150
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
151
+ path: urlObj.pathname + urlObj.search,
152
+ method: 'POST',
153
+ headers: {
154
+ 'Content-Type': 'application/json'
155
+ }
156
+ };
157
+
158
+ const req = protocol.request(options, (res) => {
159
+ let data = '';
160
+ res.on('data', chunk => data += chunk);
161
+ res.on('end', () => {
162
+ if (res.statusCode >= 200 && res.statusCode < 300) {
163
+ resolve({ success: true, status: res.statusCode });
164
+ } else {
165
+ reject(new Error(`Webhook failed: HTTP ${res.statusCode}`));
166
+ }
167
+ });
168
+ });
169
+
170
+ req.on('error', reject);
171
+ req.write(JSON.stringify(payload));
172
+ req.end();
173
+ });
174
+ }
175
+
176
+ module.exports = { sendWebhook };
@@ -198,6 +198,32 @@ test('MARQUEURS: Detecte The Second Coming', () => {
198
198
  assertIncludes(output, 'Second Coming', 'Devrait detecter marqueur The Second Coming');
199
199
  });
200
200
 
201
+ // ============================================
202
+ // TESTS UNITAIRES - DETECTION TYPOSQUATTING
203
+ // ============================================
204
+
205
+ console.log('\n=== TESTS TYPOSQUATTING ===\n');
206
+
207
+ test('TYPOSQUAT: Detecte lodahs (lodash)', () => {
208
+ const output = runScan(path.join(TESTS_DIR, 'typosquat'));
209
+ assertIncludes(output, 'lodahs', 'Devrait detecter lodahs');
210
+ });
211
+
212
+ test('TYPOSQUAT: Detecte axois (axios)', () => {
213
+ const output = runScan(path.join(TESTS_DIR, 'typosquat'));
214
+ assertIncludes(output, 'axois', 'Devrait detecter axois');
215
+ });
216
+
217
+ test('TYPOSQUAT: Detecte expres (express)', () => {
218
+ const output = runScan(path.join(TESTS_DIR, 'typosquat'));
219
+ assertIncludes(output, 'expres', 'Devrait detecter expres');
220
+ });
221
+
222
+ test('TYPOSQUAT: Severity HIGH', () => {
223
+ const output = runScan(path.join(TESTS_DIR, 'typosquat'));
224
+ assertIncludes(output, 'HIGH', 'Devrait etre HIGH');
225
+ });
226
+
201
227
  // ============================================
202
228
  // TESTS INTEGRATION - CLI
203
229
  // ============================================
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "test-typosquat",
3
+ "version": "1.0.0",
4
+ "dependencies": {
5
+ "lodahs": "^1.0.0",
6
+ "axois": "^1.0.0",
7
+ "expres": "^1.0.0",
8
+ "recat": "^1.0.0",
9
+ "momnet": "^1.0.0"
10
+ }
11
+ }