muaddib-scanner 1.0.1 → 1.0.2

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": "muaddib-scanner",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ 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');
12
13
 
13
14
  async function run(targetPath, options = {}) {
14
15
  const threats = [];
@@ -34,6 +35,10 @@ async function run(targetPath, options = {}) {
34
35
  const dataflowThreats = await analyzeDataFlow(targetPath);
35
36
  threats.push(...dataflowThreats);
36
37
 
38
+ // Scan typosquatting
39
+ const typosquatThreats = await scanTyposquatting(targetPath);
40
+ threats.push(...typosquatThreats);
41
+
37
42
  // Enrichir chaque menace avec les regles
38
43
  const enrichedThreats = threats.map(t => {
39
44
  const rule = getRule(t.type);
@@ -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,235 @@
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
+ // Techniques de typosquatting connues
25
+ const TYPOSQUAT_PATTERNS = [
26
+ { type: 'missing_char', fn: (name) => generateMissingChar(name) },
27
+ { type: 'extra_char', fn: (name) => generateExtraChar(name) },
28
+ { type: 'swapped_chars', fn: (name) => generateSwappedChars(name) },
29
+ { type: 'wrong_char', fn: (name) => generateWrongChar(name) },
30
+ { type: 'hyphen_tricks', fn: (name) => generateHyphenTricks(name) },
31
+ { type: 'suffix_tricks', fn: (name) => generateSuffixTricks(name) }
32
+ ];
33
+
34
+ async function scanTyposquatting(targetPath) {
35
+ const threats = [];
36
+ const packageJsonPath = path.join(targetPath, 'package.json');
37
+
38
+ if (!fs.existsSync(packageJsonPath)) {
39
+ return threats;
40
+ }
41
+
42
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
43
+ const dependencies = {
44
+ ...packageJson.dependencies,
45
+ ...packageJson.devDependencies,
46
+ ...packageJson.peerDependencies,
47
+ ...packageJson.optionalDependencies
48
+ };
49
+
50
+ for (const depName of Object.keys(dependencies)) {
51
+ const match = findTyposquatMatch(depName);
52
+ if (match) {
53
+ threats.push({
54
+ type: 'typosquat_detected',
55
+ severity: 'HIGH',
56
+ message: `Package "${depName}" ressemble a "${match.original}" (${match.type}). Possible typosquatting.`,
57
+ file: 'package.json',
58
+ details: {
59
+ suspicious: depName,
60
+ legitimate: match.original,
61
+ technique: match.type,
62
+ distance: match.distance
63
+ }
64
+ });
65
+ }
66
+ }
67
+
68
+ return threats;
69
+ }
70
+
71
+ function findTyposquatMatch(name) {
72
+ // Ignore les packages scoped (@org/package)
73
+ if (name.startsWith('@')) return null;
74
+
75
+ for (const popular of POPULAR_PACKAGES) {
76
+ // Ignore si c'est exactement le meme
77
+ if (name === popular) continue;
78
+
79
+ const distance = levenshteinDistance(name, popular);
80
+
81
+ // Distance de 1 ou 2 = tres suspect
82
+ if (distance === 1) {
83
+ return {
84
+ original: popular,
85
+ type: detectTyposquatType(name, popular),
86
+ distance: distance
87
+ };
88
+ }
89
+
90
+ // Distance de 2 avec nom court = suspect
91
+ if (distance === 2 && popular.length <= 6) {
92
+ return {
93
+ original: popular,
94
+ type: detectTyposquatType(name, popular),
95
+ distance: distance
96
+ };
97
+ }
98
+
99
+ // Verifie les tricks de suffixe
100
+ if (isSuffixTrick(name, popular)) {
101
+ return {
102
+ original: popular,
103
+ type: 'suffix_trick',
104
+ distance: distance
105
+ };
106
+ }
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ function detectTyposquatType(typo, original) {
113
+ if (typo.length === original.length - 1) return 'missing_char';
114
+ if (typo.length === original.length + 1) return 'extra_char';
115
+ if (typo.length === original.length) {
116
+ // Verifie si swap
117
+ let diffs = 0;
118
+ for (let i = 0; i < typo.length; i++) {
119
+ if (typo[i] !== original[i]) diffs++;
120
+ }
121
+ if (diffs === 2) return 'swapped_chars';
122
+ return 'wrong_char';
123
+ }
124
+ return 'unknown';
125
+ }
126
+
127
+ function isSuffixTrick(name, popular) {
128
+ const suffixes = ['-js', '.js', '-node', '-npm', '-cli', '-api', '-lib', '-pkg', '-dev', '-pro'];
129
+ for (const suffix of suffixes) {
130
+ if (name === popular + suffix) return true;
131
+ if (name === popular.replace('-', '') + suffix) return true;
132
+ }
133
+ // Verifie aussi les prefixes
134
+ const prefixes = ['node-', 'npm-', 'js-', 'get-', 'the-'];
135
+ for (const prefix of prefixes) {
136
+ if (name === prefix + popular) return true;
137
+ }
138
+ return false;
139
+ }
140
+
141
+ function levenshteinDistance(a, b) {
142
+ const matrix = [];
143
+
144
+ for (let i = 0; i <= b.length; i++) {
145
+ matrix[i] = [i];
146
+ }
147
+
148
+ for (let j = 0; j <= a.length; j++) {
149
+ matrix[0][j] = j;
150
+ }
151
+
152
+ for (let i = 1; i <= b.length; i++) {
153
+ for (let j = 1; j <= a.length; j++) {
154
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
155
+ matrix[i][j] = matrix[i - 1][j - 1];
156
+ } else {
157
+ matrix[i][j] = Math.min(
158
+ matrix[i - 1][j - 1] + 1, // substitution
159
+ matrix[i][j - 1] + 1, // insertion
160
+ matrix[i - 1][j] + 1 // deletion
161
+ );
162
+ }
163
+ }
164
+ }
165
+
166
+ return matrix[b.length][a.length];
167
+ }
168
+
169
+ // Generateurs pour tests (pas utilises dans le scan, mais utiles pour enrichir les IOCs)
170
+ function generateMissingChar(name) {
171
+ const results = [];
172
+ for (let i = 0; i < name.length; i++) {
173
+ results.push(name.slice(0, i) + name.slice(i + 1));
174
+ }
175
+ return results;
176
+ }
177
+
178
+ function generateExtraChar(name) {
179
+ const results = [];
180
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789-';
181
+ for (let i = 0; i <= name.length; i++) {
182
+ for (const char of chars) {
183
+ results.push(name.slice(0, i) + char + name.slice(i));
184
+ }
185
+ }
186
+ return results;
187
+ }
188
+
189
+ function generateSwappedChars(name) {
190
+ const results = [];
191
+ for (let i = 0; i < name.length - 1; i++) {
192
+ const arr = name.split('');
193
+ [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
194
+ results.push(arr.join(''));
195
+ }
196
+ return results;
197
+ }
198
+
199
+ function generateWrongChar(name) {
200
+ const results = [];
201
+ const keyboard = {
202
+ 'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'serfcx', 'e': 'wsdfr',
203
+ 'f': 'drtgvc', 'g': 'ftyhbv', 'h': 'gyujnb', 'i': 'ujklo', 'j': 'huikmn',
204
+ 'k': 'jiolm', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'iklp',
205
+ 'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
206
+ 'u': 'yhji', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu', 'z': 'asx'
207
+ };
208
+ for (let i = 0; i < name.length; i++) {
209
+ const char = name[i].toLowerCase();
210
+ if (keyboard[char]) {
211
+ for (const replacement of keyboard[char]) {
212
+ results.push(name.slice(0, i) + replacement + name.slice(i + 1));
213
+ }
214
+ }
215
+ }
216
+ return results;
217
+ }
218
+
219
+ function generateHyphenTricks(name) {
220
+ const results = [];
221
+ // Ajouter des hyphens
222
+ for (let i = 1; i < name.length; i++) {
223
+ results.push(name.slice(0, i) + '-' + name.slice(i));
224
+ }
225
+ // Retirer des hyphens
226
+ results.push(name.replace(/-/g, ''));
227
+ return results;
228
+ }
229
+
230
+ function generateSuffixTricks(name) {
231
+ const suffixes = ['-js', '.js', '-node', '-npm', '-cli'];
232
+ return suffixes.map(s => name + s);
233
+ }
234
+
235
+ module.exports = { scanTyposquatting, levenshteinDistance };
@@ -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
+ }