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.
- package/LICENSE +20 -20
- package/README.md +15 -1
- package/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -3
- package/src/canary-tokens.js +184 -184
- package/src/ioc/bootstrap.js +181 -181
- package/src/ioc/yaml-loader.js +223 -223
- package/src/maintainer-change.js +224 -224
- package/src/output-formatter.js +192 -192
- package/src/publish-anomaly.js +206 -206
- package/src/report.js +230 -230
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +40 -17
- package/src/scanner/ast.js +1 -0
- package/src/scanner/dataflow.js +14 -2
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/entropy.js +7 -0
- package/src/scanner/hash.js +118 -118
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +3 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
|
@@ -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
|
};
|
package/src/scanner/entropy.js
CHANGED
|
@@ -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;
|