muaddib-scanner 2.4.3 → 2.4.5

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.
@@ -1,224 +1,224 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { loadCachedIOCs } = require('../ioc/updater.js');
4
- const { REHABILITATED_PACKAGES } = require('../shared/constants.js');
5
-
6
- // Packages legitimes avec lifecycle scripts (ne pas alerter)
7
- const TRUSTED_PACKAGES = [
8
- 'esbuild', 'sharp', 'bcrypt', 'node-sass', 'puppeteer',
9
- 'playwright', 'sqlite3', 'better-sqlite3', 'canvas',
10
- 'grpc', 'fsevents', 'msgpackr-extract', 'lmdb', 'parcel',
11
- 'electron', 'node-gyp', 'prebuild-install', 'nan'
12
- ];
13
-
14
- // Fichiers legitimes qui ressemblent a des fichiers suspects
15
- const SAFE_FILES = {
16
- 'inject.js': ['async', 'awilix', 'inversify', 'bottlejs'],
17
- 'install.js': ['esbuild', 'sharp', 'bcrypt', 'node-sass', 'puppeteer', 'playwright', 'electron']
18
- };
19
-
20
- /**
21
- * Verifie si un package est dans la whitelist des packages rehabilites
22
- * @returns {boolean|null} true = safe, false = compromis, null = pas dans whitelist
23
- */
24
- function checkRehabilitatedPackage(pkgName, pkgVersion) {
25
- const rehab = REHABILITATED_PACKAGES[pkgName];
26
- if (!rehab) return null; // Pas dans la whitelist
27
-
28
- // Si marque comme safe = toutes versions sont OK
29
- if (rehab.safe === true) return true;
30
-
31
- // Sinon, verifier si la version est dans la liste des compromises
32
- if (rehab.compromised && rehab.compromised.includes(pkgVersion)) {
33
- return false; // Version specifiquement compromise
34
- }
35
-
36
- return true; // Version pas dans la liste des compromises = safe
37
- }
38
-
39
- async function scanDependencies(targetPath) {
40
- const threats = [];
41
- const nodeModulesPath = path.join(targetPath, 'node_modules');
42
- const iocs = loadCachedIOCs();
43
-
44
- if (!fs.existsSync(nodeModulesPath)) {
45
- return threats;
46
- }
47
-
48
- const packages = listPackages(nodeModulesPath);
49
-
50
- // Pre-compute files and markers lists once (outside the loop)
51
- const suspiciousFilesRaw = iocs.filesSet || iocs.files || [];
52
- const filesToCheck = suspiciousFilesRaw instanceof Set
53
- ? Array.from(suspiciousFilesRaw)
54
- : suspiciousFilesRaw;
55
-
56
- const markersRaw = iocs.markersSet || iocs.markers || [];
57
- const markersToCheck = markersRaw instanceof Set
58
- ? Array.from(markersRaw)
59
- : markersRaw;
60
-
61
- for (const pkg of packages) {
62
- // D'abord verifier la whitelist des packages rehabilites
63
- const rehabStatus = checkRehabilitatedPackage(pkg.name, pkg.version);
64
-
65
- if (rehabStatus === true) {
66
- // Package rehabilite et version safe, skip
67
- continue;
68
- }
69
-
70
- if (rehabStatus === false) {
71
- // Package rehabilite mais version specifiquement compromise
72
- const rehab = REHABILITATED_PACKAGES[pkg.name];
73
- threats.push({
74
- type: 'known_malicious_package',
75
- severity: 'CRITICAL',
76
- message: `Version compromise: ${pkg.name}@${pkg.version} (${rehab.note})`,
77
- file: `node_modules/${pkg.name}`,
78
- source: 'rehabilitated'
79
- });
80
- continue;
81
- }
82
-
83
- // rehabStatus === null : pas dans whitelist, continuer verification normale
84
-
85
- // Verifie si package connu malveillant (IOCs caches) AVEC VERSION
86
- // Utilise Map/Set pour lookup O(1) au lieu de O(n)
87
- let maliciousPkg = null;
88
-
89
- // Check 1: Package avec wildcard (toutes versions malveillantes)
90
- if (iocs.wildcardPackages && iocs.wildcardPackages.has(pkg.name) && iocs.packagesMap) {
91
- const pkgList = iocs.packagesMap.get(pkg.name);
92
- maliciousPkg = pkgList ? pkgList.find(p => p.version === '*') : null;
93
- }
94
- // Check 2: Version specifique via Map
95
- else if (iocs.packagesMap && iocs.packagesMap.has(pkg.name)) {
96
- const pkgList = iocs.packagesMap.get(pkg.name);
97
- maliciousPkg = pkgList ? pkgList.find(p => p.version === pkg.version) : null;
98
- }
99
- // Fallback: recherche lineaire (compatibilite ancienne API)
100
- else if (!iocs.packagesMap) {
101
- maliciousPkg = iocs.packages.find(p => {
102
- if (p.name !== pkg.name) return false;
103
- if (p.version === '*') return true;
104
- return p.version === pkg.version;
105
- });
106
- }
107
-
108
- if (maliciousPkg) {
109
- threats.push({
110
- type: 'known_malicious_package',
111
- severity: 'CRITICAL',
112
- message: `Package malveillant connu: ${pkg.name}@${maliciousPkg.version} (source: ${maliciousPkg.source})`,
113
- file: `node_modules/${pkg.name}`
114
- });
115
- continue;
116
- }
117
-
118
- // Skip trusted packages pour les checks suivants
119
- if (TRUSTED_PACKAGES.includes(pkg.name)) continue;
120
-
121
- // Verifie les fichiers suspects (IOCs caches) avec whitelist
122
- for (const suspFile of filesToCheck) {
123
- // Skip si fichier legitime pour ce package
124
- if (SAFE_FILES[suspFile] && SAFE_FILES[suspFile].includes(pkg.name)) {
125
- continue;
126
- }
127
-
128
- const filePath = path.join(pkg.path, suspFile);
129
- if (fs.existsSync(filePath)) {
130
- threats.push({
131
- type: 'suspicious_file',
132
- severity: 'HIGH',
133
- message: `Fichier suspect "${suspFile}" dans ${pkg.name}`,
134
- file: `node_modules/${pkg.name}/${suspFile}`
135
- });
136
- }
137
- }
138
-
139
- // Verifie les lifecycle scripts
140
- const pkgJsonPath = path.join(pkg.path, 'package.json');
141
- if (fs.existsSync(pkgJsonPath)) {
142
- try {
143
- const pkgContent = fs.readFileSync(pkgJsonPath, 'utf8');
144
-
145
- // Verifie les marqueurs Shai-Hulud
146
- for (const marker of markersToCheck) {
147
- if (pkgContent.includes(marker)) {
148
- threats.push({
149
- type: 'shai_hulud_marker',
150
- severity: 'CRITICAL',
151
- message: `Marqueur "${marker}" detecte dans ${pkg.name}`,
152
- file: `node_modules/${pkg.name}/package.json`
153
- });
154
- }
155
- }
156
- } catch {
157
- // JSON parse error, skip
158
- }
159
- }
160
- }
161
-
162
- return threats;
163
- }
164
-
165
- function listPackages(nodeModulesPath) {
166
- const packages = [];
167
- const items = fs.readdirSync(nodeModulesPath);
168
-
169
- for (const item of items) {
170
- if (item.startsWith('.')) continue;
171
-
172
- const itemPath = path.join(nodeModulesPath, item);
173
-
174
- try {
175
- const stat = fs.lstatSync(itemPath);
176
- if (stat.isSymbolicLink()) continue;
177
- if (!stat.isDirectory()) continue;
178
-
179
- if (item.startsWith('@')) {
180
- const scopedItems = fs.readdirSync(itemPath);
181
- for (const scopedItem of scopedItems) {
182
- const scopedPath = path.join(itemPath, scopedItem);
183
- const scopedStat = fs.lstatSync(scopedPath);
184
- if (scopedStat.isSymbolicLink()) continue;
185
- if (scopedStat.isDirectory()) {
186
- const version = getPackageVersion(scopedPath);
187
- packages.push({
188
- name: `${item}/${scopedItem}`,
189
- path: scopedPath,
190
- version: version
191
- });
192
- }
193
- }
194
- } else {
195
- const version = getPackageVersion(itemPath);
196
- packages.push({
197
- name: item,
198
- path: itemPath,
199
- version: version
200
- });
201
- }
202
- } catch {
203
- // Skip inaccessible
204
- }
205
- }
206
-
207
- return packages;
208
- }
209
-
210
- function getPackageVersion(pkgPath) {
211
- try {
212
- const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8'));
213
- return pkgJson.version || '*';
214
- } catch {
215
- return '*';
216
- }
217
- }
218
-
219
- module.exports = {
220
- scanDependencies,
221
- checkRehabilitatedPackage,
222
- TRUSTED_PACKAGES,
223
- SAFE_FILES
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadCachedIOCs } = require('../ioc/updater.js');
4
+ const { REHABILITATED_PACKAGES } = require('../shared/constants.js');
5
+
6
+ // Packages legitimes avec lifecycle scripts (ne pas alerter)
7
+ const TRUSTED_PACKAGES = [
8
+ 'esbuild', 'sharp', 'bcrypt', 'node-sass', 'puppeteer',
9
+ 'playwright', 'sqlite3', 'better-sqlite3', 'canvas',
10
+ 'grpc', 'fsevents', 'msgpackr-extract', 'lmdb', 'parcel',
11
+ 'electron', 'node-gyp', 'prebuild-install', 'nan'
12
+ ];
13
+
14
+ // Fichiers legitimes qui ressemblent a des fichiers suspects
15
+ const SAFE_FILES = {
16
+ 'inject.js': ['async', 'awilix', 'inversify', 'bottlejs'],
17
+ 'install.js': ['esbuild', 'sharp', 'bcrypt', 'node-sass', 'puppeteer', 'playwright', 'electron']
18
+ };
19
+
20
+ /**
21
+ * Verifie si un package est dans la whitelist des packages rehabilites
22
+ * @returns {boolean|null} true = safe, false = compromis, null = pas dans whitelist
23
+ */
24
+ function checkRehabilitatedPackage(pkgName, pkgVersion) {
25
+ const rehab = REHABILITATED_PACKAGES[pkgName];
26
+ if (!rehab) return null; // Pas dans la whitelist
27
+
28
+ // Si marque comme safe = toutes versions sont OK
29
+ if (rehab.safe === true) return true;
30
+
31
+ // Sinon, verifier si la version est dans la liste des compromises
32
+ if (rehab.compromised && rehab.compromised.includes(pkgVersion)) {
33
+ return false; // Version specifiquement compromise
34
+ }
35
+
36
+ return true; // Version pas dans la liste des compromises = safe
37
+ }
38
+
39
+ async function scanDependencies(targetPath) {
40
+ const threats = [];
41
+ const nodeModulesPath = path.join(targetPath, 'node_modules');
42
+ const iocs = loadCachedIOCs();
43
+
44
+ if (!fs.existsSync(nodeModulesPath)) {
45
+ return threats;
46
+ }
47
+
48
+ const packages = listPackages(nodeModulesPath);
49
+
50
+ // Pre-compute files and markers lists once (outside the loop)
51
+ const suspiciousFilesRaw = iocs.filesSet || iocs.files || [];
52
+ const filesToCheck = suspiciousFilesRaw instanceof Set
53
+ ? Array.from(suspiciousFilesRaw)
54
+ : suspiciousFilesRaw;
55
+
56
+ const markersRaw = iocs.markersSet || iocs.markers || [];
57
+ const markersToCheck = markersRaw instanceof Set
58
+ ? Array.from(markersRaw)
59
+ : markersRaw;
60
+
61
+ for (const pkg of packages) {
62
+ // D'abord verifier la whitelist des packages rehabilites
63
+ const rehabStatus = checkRehabilitatedPackage(pkg.name, pkg.version);
64
+
65
+ if (rehabStatus === true) {
66
+ // Package rehabilite et version safe, skip
67
+ continue;
68
+ }
69
+
70
+ if (rehabStatus === false) {
71
+ // Package rehabilite mais version specifiquement compromise
72
+ const rehab = REHABILITATED_PACKAGES[pkg.name];
73
+ threats.push({
74
+ type: 'known_malicious_package',
75
+ severity: 'CRITICAL',
76
+ message: `Version compromise: ${pkg.name}@${pkg.version} (${rehab.note})`,
77
+ file: `node_modules/${pkg.name}`,
78
+ source: 'rehabilitated'
79
+ });
80
+ continue;
81
+ }
82
+
83
+ // rehabStatus === null : pas dans whitelist, continuer verification normale
84
+
85
+ // Verifie si package connu malveillant (IOCs caches) AVEC VERSION
86
+ // Utilise Map/Set pour lookup O(1) au lieu de O(n)
87
+ let maliciousPkg = null;
88
+
89
+ // Check 1: Package avec wildcard (toutes versions malveillantes)
90
+ if (iocs.wildcardPackages && iocs.wildcardPackages.has(pkg.name) && iocs.packagesMap) {
91
+ const pkgList = iocs.packagesMap.get(pkg.name);
92
+ maliciousPkg = pkgList ? pkgList.find(p => p.version === '*') : null;
93
+ }
94
+ // Check 2: Version specifique via Map
95
+ else if (iocs.packagesMap && iocs.packagesMap.has(pkg.name)) {
96
+ const pkgList = iocs.packagesMap.get(pkg.name);
97
+ maliciousPkg = pkgList ? pkgList.find(p => p.version === pkg.version) : null;
98
+ }
99
+ // Fallback: recherche lineaire (compatibilite ancienne API)
100
+ else if (!iocs.packagesMap) {
101
+ maliciousPkg = iocs.packages.find(p => {
102
+ if (p.name !== pkg.name) return false;
103
+ if (p.version === '*') return true;
104
+ return p.version === pkg.version;
105
+ });
106
+ }
107
+
108
+ if (maliciousPkg) {
109
+ threats.push({
110
+ type: 'known_malicious_package',
111
+ severity: 'CRITICAL',
112
+ message: `Package malveillant connu: ${pkg.name}@${maliciousPkg.version} (source: ${maliciousPkg.source})`,
113
+ file: `node_modules/${pkg.name}`
114
+ });
115
+ continue;
116
+ }
117
+
118
+ // Skip trusted packages pour les checks suivants
119
+ if (TRUSTED_PACKAGES.includes(pkg.name)) continue;
120
+
121
+ // Verifie les fichiers suspects (IOCs caches) avec whitelist
122
+ for (const suspFile of filesToCheck) {
123
+ // Skip si fichier legitime pour ce package
124
+ if (SAFE_FILES[suspFile] && SAFE_FILES[suspFile].includes(pkg.name)) {
125
+ continue;
126
+ }
127
+
128
+ const filePath = path.join(pkg.path, suspFile);
129
+ if (fs.existsSync(filePath)) {
130
+ threats.push({
131
+ type: 'suspicious_file',
132
+ severity: 'HIGH',
133
+ message: `Fichier suspect "${suspFile}" dans ${pkg.name}`,
134
+ file: `node_modules/${pkg.name}/${suspFile}`
135
+ });
136
+ }
137
+ }
138
+
139
+ // Verifie les lifecycle scripts
140
+ const pkgJsonPath = path.join(pkg.path, 'package.json');
141
+ if (fs.existsSync(pkgJsonPath)) {
142
+ try {
143
+ const pkgContent = fs.readFileSync(pkgJsonPath, 'utf8');
144
+
145
+ // Verifie les marqueurs Shai-Hulud
146
+ for (const marker of markersToCheck) {
147
+ if (pkgContent.includes(marker)) {
148
+ threats.push({
149
+ type: 'shai_hulud_marker',
150
+ severity: 'CRITICAL',
151
+ message: `Marqueur "${marker}" detecte dans ${pkg.name}`,
152
+ file: `node_modules/${pkg.name}/package.json`
153
+ });
154
+ }
155
+ }
156
+ } catch {
157
+ // JSON parse error, skip
158
+ }
159
+ }
160
+ }
161
+
162
+ return threats;
163
+ }
164
+
165
+ function listPackages(nodeModulesPath) {
166
+ const packages = [];
167
+ const items = fs.readdirSync(nodeModulesPath);
168
+
169
+ for (const item of items) {
170
+ if (item.startsWith('.')) continue;
171
+
172
+ const itemPath = path.join(nodeModulesPath, item);
173
+
174
+ try {
175
+ const stat = fs.lstatSync(itemPath);
176
+ if (stat.isSymbolicLink()) continue;
177
+ if (!stat.isDirectory()) continue;
178
+
179
+ if (item.startsWith('@')) {
180
+ const scopedItems = fs.readdirSync(itemPath);
181
+ for (const scopedItem of scopedItems) {
182
+ const scopedPath = path.join(itemPath, scopedItem);
183
+ const scopedStat = fs.lstatSync(scopedPath);
184
+ if (scopedStat.isSymbolicLink()) continue;
185
+ if (scopedStat.isDirectory()) {
186
+ const version = getPackageVersion(scopedPath);
187
+ packages.push({
188
+ name: `${item}/${scopedItem}`,
189
+ path: scopedPath,
190
+ version: version
191
+ });
192
+ }
193
+ }
194
+ } else {
195
+ const version = getPackageVersion(itemPath);
196
+ packages.push({
197
+ name: item,
198
+ path: itemPath,
199
+ version: version
200
+ });
201
+ }
202
+ } catch {
203
+ // Skip inaccessible
204
+ }
205
+ }
206
+
207
+ return packages;
208
+ }
209
+
210
+ function getPackageVersion(pkgPath) {
211
+ try {
212
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8'));
213
+ return pkgJson.version || '*';
214
+ } catch {
215
+ return '*';
216
+ }
217
+ }
218
+
219
+ module.exports = {
220
+ scanDependencies,
221
+ checkRehabilitatedPackage,
222
+ TRUSTED_PACKAGES,
223
+ SAFE_FILES
224
224
  };
@@ -13,6 +13,12 @@ const ENCODING_TABLE_RE = /(?:encoding|tables|unicode|charmap|codepage)/i;
13
13
  // Minimum string length to analyze (short strings naturally have low entropy)
14
14
  const MIN_STRING_LENGTH = 50;
15
15
 
16
+ // Maximum string length to analyze — strings >1000 chars are data blobs
17
+ // (certificates, unicode tables, embedded binary), not malware payloads.
18
+ // Real malware uses 50-500 char encoded payloads; making payloads longer
19
+ // defeats the purpose of obfuscation.
20
+ const MAX_STRING_LENGTH = 1000;
21
+
16
22
  // Thresholds (string-level only — file-level entropy removed, see design notes)
17
23
  const STRING_ENTROPY_MEDIUM = 5.5;
18
24
  const STRING_ENTROPY_HIGH = 6.5;
@@ -222,6 +228,7 @@ function scanEntropy(targetPath, options = {}) {
222
228
  const strings = extractStringLiterals(content);
223
229
  for (const str of strings) {
224
230
  if (str.length < MIN_STRING_LENGTH) continue;
231
+ if (str.length > MAX_STRING_LENGTH) continue;
225
232
 
226
233
  // Skip whitelisted patterns
227
234
  if (isWhitelistedString(str, relativePath)) continue;