muaddib-scanner 1.0.10 → 1.0.11

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.10",
3
+ "version": "1.0.11",
4
4
  "description": "Supply-chain threat detection & response for npm",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,29 +11,32 @@ const EXCLUDED_FILES = [
11
11
  'src/response/playbooks.js'
12
12
  ];
13
13
 
14
- const EXCLUDED_DIRS = ['test', 'tests', 'node_modules', '.git', 'src', 'vscode-extension'];
14
+ const EXCLUDED_DIRS = [
15
+ 'test', 'tests', 'node_modules', '.git', 'src', 'vscode-extension',
16
+ 'scripts', 'bin', 'tools', 'build', 'dist', 'fixtures', 'examples',
17
+ '__tests__', '__mocks__', 'benchmark', 'benchmarks', 'docs', 'doc'
18
+ ];
15
19
 
16
20
  const DANGEROUS_CALLS = [
17
21
  'eval',
18
- 'Function',
19
- 'exec',
20
- 'execSync',
21
- 'spawn',
22
- 'spawnSync'
22
+ 'Function'
23
23
  ];
24
24
 
25
25
  const SENSITIVE_STRINGS = [
26
26
  '.npmrc',
27
27
  '.ssh',
28
- 'GITHUB_TOKEN',
29
- 'NPM_TOKEN',
30
- 'AWS_SECRET',
31
- 'api.github.com',
32
28
  'Shai-Hulud',
33
29
  'The Second Coming',
34
30
  'Goldox-T3chs'
35
31
  ];
36
32
 
33
+ // Strings qui ne sont PAS suspects
34
+ const SAFE_STRINGS = [
35
+ 'api.github.com',
36
+ 'registry.npmjs.org',
37
+ 'npmjs.com'
38
+ ];
39
+
37
40
  async function analyzeAST(targetPath) {
38
41
  const threats = [];
39
42
  const files = findJsFiles(targetPath);
@@ -45,6 +48,11 @@ async function analyzeAST(targetPath) {
45
48
  continue;
46
49
  }
47
50
 
51
+ // Ignorer les fichiers dans les dossiers de dev
52
+ if (isDevFile(relativePath)) {
53
+ continue;
54
+ }
55
+
48
56
  const content = fs.readFileSync(file, 'utf8');
49
57
  const fileThreats = analyzeFile(content, file, targetPath);
50
58
  threats.push(...fileThreats);
@@ -53,6 +61,27 @@ async function analyzeAST(targetPath) {
53
61
  return threats;
54
62
  }
55
63
 
64
+ function isDevFile(relativePath) {
65
+ const devPatterns = [
66
+ /^scripts\//,
67
+ /^bin\//,
68
+ /^tools\//,
69
+ /^build\//,
70
+ /^fixtures\//,
71
+ /^examples\//,
72
+ /^__tests__\//,
73
+ /^__mocks__\//,
74
+ /^benchmark/,
75
+ /^docs?\//,
76
+ /\.test\.js$/,
77
+ /\.spec\.js$/,
78
+ /test\.js$/,
79
+ /spec\.js$/
80
+ ];
81
+
82
+ return devPatterns.some(pattern => pattern.test(relativePath));
83
+ }
84
+
56
85
  function analyzeFile(content, filePath, basePath) {
57
86
  const threats = [];
58
87
  let ast;
@@ -64,7 +93,6 @@ function analyzeFile(content, filePath, basePath) {
64
93
  allowHashBang: true
65
94
  });
66
95
  } catch (e) {
67
- // Fichier non parseable, peut etre obfusque
68
96
  if (content.length > 1000 && content.split('\n').length < 10) {
69
97
  threats.push({
70
98
  type: 'possible_obfuscation',
@@ -76,7 +104,6 @@ function analyzeFile(content, filePath, basePath) {
76
104
  return threats;
77
105
  }
78
106
 
79
- // Analyse des appels de fonction
80
107
  walk.simple(ast, {
81
108
  CallExpression(node) {
82
109
  const callName = getCallName(node);
@@ -84,7 +111,7 @@ function analyzeFile(content, filePath, basePath) {
84
111
  if (DANGEROUS_CALLS.includes(callName)) {
85
112
  threats.push({
86
113
  type: 'dangerous_call_' + callName.toLowerCase(),
87
- severity: callName === 'eval' ? 'HIGH' : 'MEDIUM',
114
+ severity: 'HIGH',
88
115
  message: `Appel dangereux "${callName}" detecte.`,
89
116
  file: path.relative(basePath, filePath)
90
117
  });
@@ -104,6 +131,11 @@ function analyzeFile(content, filePath, basePath) {
104
131
 
105
132
  Literal(node) {
106
133
  if (typeof node.value === 'string') {
134
+ // Ignorer les strings safe
135
+ if (SAFE_STRINGS.some(s => node.value.includes(s))) {
136
+ return;
137
+ }
138
+
107
139
  for (const sensitive of SENSITIVE_STRINGS) {
108
140
  if (node.value.includes(sensitive)) {
109
141
  threats.push({
@@ -118,7 +150,6 @@ function analyzeFile(content, filePath, basePath) {
118
150
  },
119
151
 
120
152
  MemberExpression(node) {
121
- // Detecte process.env.XXX
122
153
  if (
123
154
  node.object?.object?.name === 'process' &&
124
155
  node.object?.property?.name === 'env'
@@ -3,13 +3,24 @@ const path = require('path');
3
3
  const acorn = require('acorn');
4
4
  const walk = require('acorn-walk');
5
5
 
6
- const EXCLUDED_DIRS = ['test', 'tests', 'node_modules', '.git', 'src', 'vscode-extension'];
6
+ const EXCLUDED_DIRS = [
7
+ 'test', 'tests', 'node_modules', '.git', 'src', 'vscode-extension',
8
+ 'scripts', 'bin', 'tools', 'build', 'dist', 'fixtures', 'examples',
9
+ '__tests__', '__mocks__', 'benchmark', 'benchmarks', 'docs', 'doc'
10
+ ];
7
11
 
8
12
  async function analyzeDataFlow(targetPath) {
9
13
  const threats = [];
10
14
  const files = findJsFiles(targetPath);
11
15
 
12
16
  for (const file of files) {
17
+ const relativePath = path.relative(targetPath, file).replace(/\\/g, '/');
18
+
19
+ // Ignorer les fichiers de dev/build/scripts
20
+ if (isDevFile(relativePath)) {
21
+ continue;
22
+ }
23
+
13
24
  const content = fs.readFileSync(file, 'utf8');
14
25
  const fileThreats = analyzeFile(content, file, targetPath);
15
26
  threats.push(...fileThreats);
@@ -18,6 +29,29 @@ async function analyzeDataFlow(targetPath) {
18
29
  return threats;
19
30
  }
20
31
 
32
+ function isDevFile(relativePath) {
33
+ const devPatterns = [
34
+ /^scripts\//,
35
+ /^bin\//,
36
+ /^tools\//,
37
+ /^build\//,
38
+ /^fixtures\//,
39
+ /^examples\//,
40
+ /^__tests__\//,
41
+ /^__mocks__\//,
42
+ /^benchmark/,
43
+ /^docs?\//,
44
+ /^compiler\//,
45
+ /^packages\/.*\/scripts\//,
46
+ /\.test\.js$/,
47
+ /\.spec\.js$/,
48
+ /test\.js$/,
49
+ /spec\.js$/
50
+ ];
51
+
52
+ return devPatterns.some(pattern => pattern.test(relativePath));
53
+ }
54
+
21
55
  function analyzeFile(content, filePath, basePath) {
22
56
  const threats = [];
23
57
  let ast;
@@ -32,15 +66,13 @@ function analyzeFile(content, filePath, basePath) {
32
66
  return threats;
33
67
  }
34
68
 
35
- const sources = []; // Ou les donnees sensibles sont lues
36
- const sinks = []; // Ou les donnees sont envoyees
69
+ const sources = [];
70
+ const sinks = [];
37
71
 
38
72
  walk.simple(ast, {
39
- // Detecte les lectures de fichiers sensibles
40
73
  CallExpression(node) {
41
74
  const callName = getCallName(node);
42
75
 
43
- // fs.readFileSync, fs.readFile
44
76
  if (callName === 'readFileSync' || callName === 'readFile') {
45
77
  const arg = node.arguments[0];
46
78
  if (arg && isCredentialPath(arg, content)) {
@@ -52,7 +84,6 @@ function analyzeFile(content, filePath, basePath) {
52
84
  }
53
85
  }
54
86
 
55
- // Detecte les envois reseau
56
87
  if (callName === 'request' || callName === 'fetch' || callName === 'post' || callName === 'get') {
57
88
  sinks.push({
58
89
  type: 'network_send',
@@ -61,7 +92,6 @@ function analyzeFile(content, filePath, basePath) {
61
92
  });
62
93
  }
63
94
 
64
- // exec avec curl/wget
65
95
  if (callName === 'exec' || callName === 'execSync') {
66
96
  const arg = node.arguments[0];
67
97
  if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
@@ -76,7 +106,6 @@ function analyzeFile(content, filePath, basePath) {
76
106
  }
77
107
  },
78
108
 
79
- // Detecte les acces process.env sensibles
80
109
  MemberExpression(node) {
81
110
  if (
82
111
  node.object?.object?.name === 'process' &&
@@ -94,7 +123,6 @@ function analyzeFile(content, filePath, basePath) {
94
123
  }
95
124
  });
96
125
 
97
- // Si on a des sources ET des sinks = flux suspect
98
126
  if (sources.length > 0 && sinks.length > 0) {
99
127
  threats.push({
100
128
  type: 'suspicious_dataflow',
@@ -126,7 +154,6 @@ function isCredentialPath(arg, content) {
126
154
  val.includes('.gitconfig') ||
127
155
  val.includes('.env');
128
156
  }
129
- // Verifie aussi les templates strings et concatenations
130
157
  if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
131
158
  return content.includes('.npmrc') ||
132
159
  content.includes('.ssh') ||
@@ -136,7 +163,8 @@ function isCredentialPath(arg, content) {
136
163
  }
137
164
 
138
165
  function isSensitiveEnv(name) {
139
- const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'GITHUB', 'AWS', 'AZURE', 'GCP'];
166
+ const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
167
+ // Ignore GITHUB — trop de faux positifs dans les scripts de release
140
168
  return sensitive.some(s => name.toUpperCase().includes(s));
141
169
  }
142
170
 
@@ -7,13 +7,13 @@ const POPULAR_PACKAGES = [
7
7
  'request', 'async', 'bluebird', 'underscore', 'uuid', 'debug', 'mkdirp',
8
8
  'glob', 'minimist', 'webpack', 'babel-core', 'typescript', 'eslint',
9
9
  'prettier', 'jest', 'mocha', 'chai', 'sinon', 'mongoose', 'sequelize',
10
- 'mysql', 'redis', 'mongodb', 'socket.io', 'express-session',
10
+ 'redis', 'mongodb', 'socket.io', 'express-session',
11
11
  'body-parser', 'cookie-parser', 'cors', 'helmet', 'morgan', 'dotenv',
12
12
  'jsonwebtoken', 'bcrypt', 'passport', 'nodemailer', 'aws-sdk', 'stripe',
13
- 'twilio', 'firebase', 'graphql', 'apollo-server', 'next', 'nuxt',
13
+ 'twilio', 'firebase', 'graphql', 'apollo-server', 'nuxt',
14
14
  'gatsby', 'angular', 'svelte', 'electron', 'puppeteer', 'cheerio',
15
15
  'sharp', 'jimp', 'canvas', 'pdf-lib', 'exceljs', 'csv-parser', 'xml2js',
16
- 'yaml', 'config', 'yargs', 'inquirer', 'ora', 'colors',
16
+ 'yaml', 'config', 'yargs', 'colors',
17
17
  'winston', 'bunyan', 'pino', 'log4js', 'ramda', 'immutable',
18
18
  'mobx', 'redux', 'zustand', 'formik', 'yup', 'ajv', 'validator',
19
19
  'date-fns', 'dayjs', 'luxon', 'numeral', 'accounting', 'currency.js',
@@ -28,7 +28,7 @@ const WHITELIST = [
28
28
  'co', 'q', 'n', 'i', 'a', 'v', 'x', 'y', 'z',
29
29
  'ejs', 'nyc', 'ini', 'joi', 'vue', 'npm', 'got', 'ora',
30
30
  'vary', 'mime', 'send', 'etag', 'raw', 'tar', 'uid', 'cjs',
31
- 'rxjs', 'yarn', 'pnpm',
31
+ 'rxjs', 'yarn', 'pnpm', 'next',
32
32
 
33
33
  // Packages legitimes avec noms similaires
34
34
  'acorn', 'acorn-walk', 'js-yaml', 'cross-env', 'node-fetch', 'node-gyp',
@@ -42,7 +42,24 @@ const WHITELIST = [
42
42
  'depd', 'destroy', 'encodeurl', 'escape-html', 'fresh', 'merge-descriptors',
43
43
  'methods', 'on-finished', 'parseurl', 'path-to-regexp', 'proxy-addr',
44
44
  'range-parser', 'safe-buffer', 'safer-buffer', 'setprototypeof',
45
- 'statuses', 'type-is', 'unpipe', 'utils-merge'
45
+ 'statuses', 'type-is', 'unpipe', 'utils-merge',
46
+
47
+ // Packages CLI et outils legitimes
48
+ 'jest-cli', 'prettier-2', 'prettier-1', 'eslint-cli',
49
+ 'inquirer', 'enquirer', 'prompts',
50
+ 'mysql2', 'pg-native', 'sqlite3', 'better-sqlite3',
51
+ 'node-sass', 'sass', 'less',
52
+ 'esbuild', 'rollup', 'parcel', 'vite',
53
+ 'husky', 'lint-staged', 'commitlint',
54
+ 'nodemon', 'pm2', 'forever', 'concurrently',
55
+ 'lerna', 'turbo', 'nx',
56
+ 'chalk', 'colors', 'picocolors', 'colorette',
57
+ 'commander', 'yargs', 'meow', 'cac',
58
+ 'execa', 'shelljs', 'cross-spawn',
59
+ 'rimraf', 'del', 'trash-cli',
60
+ 'globby', 'fast-glob', 'tiny-glob',
61
+ 'chokidar', 'watchpack', 'nsfw',
62
+ 'dotenv', 'dotenv-expand', 'env-cmd'
46
63
  ];
47
64
 
48
65
  // Seuil minimum de longueur pour eviter faux positifs
@@ -86,8 +103,10 @@ async function scanTyposquatting(targetPath) {
86
103
  }
87
104
 
88
105
  function findTyposquatMatch(name) {
106
+ const nameLower = name.toLowerCase();
107
+
89
108
  // Ignore les packages whitelistes
90
- if (WHITELIST.includes(name.toLowerCase())) return null;
109
+ if (WHITELIST.includes(nameLower)) return null;
91
110
 
92
111
  // Ignore les packages scoped (@org/package)
93
112
  if (name.startsWith('@')) return null;
@@ -95,14 +114,17 @@ function findTyposquatMatch(name) {
95
114
  // Ignore les packages tres courts (trop de faux positifs)
96
115
  if (name.length < MIN_PACKAGE_LENGTH) return null;
97
116
 
117
+ // Ignore les packages avec suffixes legitimes courants
118
+ if (isLegitimateVariant(nameLower)) return null;
119
+
98
120
  for (const popular of POPULAR_PACKAGES) {
99
121
  // Ignore si c'est exactement le meme
100
- if (name.toLowerCase() === popular.toLowerCase()) continue;
122
+ if (nameLower === popular.toLowerCase()) continue;
101
123
 
102
124
  // Ignore si le package populaire est trop court
103
125
  if (popular.length < MIN_PACKAGE_LENGTH) continue;
104
126
 
105
- const distance = levenshteinDistance(name.toLowerCase(), popular.toLowerCase());
127
+ const distance = levenshteinDistance(nameLower, popular.toLowerCase());
106
128
 
107
129
  // Distance de 1 = tres suspect (une seule lettre de difference)
108
130
  if (distance === 1) {
@@ -121,25 +143,45 @@ function findTyposquatMatch(name) {
121
143
  distance: distance
122
144
  };
123
145
  }
124
-
125
- // Verifie les tricks de suffixe
126
- if (isSuffixTrick(name, popular)) {
127
- return {
128
- original: popular,
129
- type: 'suffix_trick',
130
- distance: distance
131
- };
132
- }
133
146
  }
134
147
 
135
148
  return null;
136
149
  }
137
150
 
151
+ function isLegitimateVariant(name) {
152
+ // Suffixes legitimes qui ne sont PAS du typosquatting
153
+ const legitimateSuffixes = [
154
+ '-cli', '-core', '-utils', '-plugin', '-loader', '-webpack',
155
+ '-react', '-vue', '-angular', '-node', '-browser',
156
+ '-esm', '-cjs', '-umd',
157
+ '-types', '-typings',
158
+ '2', '3', '4', '5', // versions majeures (mysql2, etc)
159
+ '-v2', '-v3', '-next', '-latest', '-stable', '-lts'
160
+ ];
161
+
162
+ for (const suffix of legitimateSuffixes) {
163
+ if (name.endsWith(suffix)) return true;
164
+ }
165
+
166
+ // Prefixes legitimes
167
+ const legitimatePrefixes = [
168
+ '@types/', '@babel/', '@jest/', '@testing-library/',
169
+ 'eslint-plugin-', 'eslint-config-',
170
+ 'babel-plugin-', 'babel-preset-',
171
+ 'webpack-plugin-', 'rollup-plugin-', 'vite-plugin-'
172
+ ];
173
+
174
+ for (const prefix of legitimatePrefixes) {
175
+ if (name.startsWith(prefix)) return true;
176
+ }
177
+
178
+ return false;
179
+ }
180
+
138
181
  function detectTyposquatType(typo, original) {
139
182
  if (typo.length === original.length - 1) return 'missing_char';
140
183
  if (typo.length === original.length + 1) return 'extra_char';
141
184
  if (typo.length === original.length) {
142
- // Verifie si swap
143
185
  let diffs = 0;
144
186
  for (let i = 0; i < typo.length; i++) {
145
187
  if (typo[i] !== original[i]) diffs++;
@@ -150,25 +192,6 @@ function detectTyposquatType(typo, original) {
150
192
  return 'unknown';
151
193
  }
152
194
 
153
- function isSuffixTrick(name, popular) {
154
- const nameLower = name.toLowerCase();
155
- const popularLower = popular.toLowerCase();
156
-
157
- const suffixes = ['-js', '.js', '-node', '-npm', '-cli', '-api', '-lib', '-pkg', '-dev', '-pro'];
158
- for (const suffix of suffixes) {
159
- if (nameLower === popularLower + suffix) return true;
160
- if (nameLower === popularLower.replace('-', '') + suffix) return true;
161
- }
162
-
163
- // Verifie aussi les prefixes
164
- const prefixes = ['node-', 'npm-', 'js-', 'get-', 'the-'];
165
- for (const prefix of prefixes) {
166
- if (nameLower === prefix + popularLower) return true;
167
- }
168
-
169
- return false;
170
- }
171
-
172
195
  function levenshteinDistance(a, b) {
173
196
  const matrix = [];
174
197