muaddib-scanner 2.3.3 → 2.4.0
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/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -1
- package/src/canary-tokens.js +184 -184
- package/src/index.js +10 -1
- 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/response/playbooks.js +3 -0
- package/src/rules/index.js +11 -0
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +2 -2
- package/src/scanner/ast.js +3 -4
- package/src/scanner/dataflow.js +12 -7
- package/src/scanner/deobfuscate.js +579 -591
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/hash.js +118 -118
- package/src/scanner/module-graph.js +2 -6
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/package.js +36 -0
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +33 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/shared/constants.js +113 -93
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-ast-diff.js +317 -319
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
package/src/temporal-ast-diff.js
CHANGED
|
@@ -1,319 +1,317 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const acorn = require('acorn');
|
|
6
|
-
const walk = require('acorn-walk');
|
|
7
|
-
const { findJsFiles, forEachSafeFile, debugLog } = require('./utils.js');
|
|
8
|
-
const { fetchPackageMetadata, getLatestVersions } = require('./temporal-analysis.js');
|
|
9
|
-
const { downloadToFile, extractTarGz, sanitizePackageName } = require('./shared/download.js');
|
|
10
|
-
|
|
11
|
-
const { MAX_FILE_SIZE, ACORN_OPTIONS } = require('./shared/constants.js');
|
|
12
|
-
|
|
13
|
-
const REGISTRY_URL = 'https://registry.npmjs.org';
|
|
14
|
-
const METADATA_TIMEOUT = 10_000;
|
|
15
|
-
|
|
16
|
-
const SENSITIVE_PATHS = [
|
|
17
|
-
'/etc/passwd', '/etc/shadow', '.env', '.npmrc', '.ssh',
|
|
18
|
-
'.aws/credentials', '.bash_history', '.gitconfig'
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
// Severity mapping for each pattern
|
|
22
|
-
const PATTERN_SEVERITY = {
|
|
23
|
-
child_process: 'CRITICAL',
|
|
24
|
-
eval: 'CRITICAL',
|
|
25
|
-
Function: 'CRITICAL',
|
|
26
|
-
'net.connect': 'CRITICAL',
|
|
27
|
-
'process.env': 'HIGH',
|
|
28
|
-
fetch: 'HIGH',
|
|
29
|
-
http_request: 'HIGH',
|
|
30
|
-
https_request: 'HIGH',
|
|
31
|
-
'dns.lookup': 'MEDIUM',
|
|
32
|
-
'fs.readFile_sensitive': 'MEDIUM'
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// --- HTTP helpers ---
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Fetch version-specific metadata from npm registry.
|
|
39
|
-
* @param {string} packageName
|
|
40
|
-
* @param {string} version
|
|
41
|
-
* @returns {Promise<object>}
|
|
42
|
-
*/
|
|
43
|
-
function fetchVersionMetadata(packageName, version) {
|
|
44
|
-
const encodedName = encodeURIComponent(packageName).replace('%40', '@');
|
|
45
|
-
const url = `${REGISTRY_URL}/${encodedName}/${encodeURIComponent(version)}`;
|
|
46
|
-
const urlObj = new URL(url);
|
|
47
|
-
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
const req = https.request({
|
|
50
|
-
hostname: urlObj.hostname,
|
|
51
|
-
path: urlObj.pathname,
|
|
52
|
-
method: 'GET',
|
|
53
|
-
headers: { 'User-Agent': 'MUADDIB-Scanner/3.0', 'Accept': 'application/json' }
|
|
54
|
-
}, (res) => {
|
|
55
|
-
if (res.statusCode === 404) {
|
|
56
|
-
res.resume();
|
|
57
|
-
return reject(new Error(`Version ${version} not found for package ${packageName}`));
|
|
58
|
-
}
|
|
59
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
60
|
-
res.resume();
|
|
61
|
-
return reject(new Error(`Registry returned HTTP ${res.statusCode} for ${packageName}@${version}`));
|
|
62
|
-
}
|
|
63
|
-
let data = '';
|
|
64
|
-
res.on('data', chunk => { data += chunk; });
|
|
65
|
-
res.on('end', () => {
|
|
66
|
-
try { resolve(JSON.parse(data)); }
|
|
67
|
-
catch (e) { reject(new Error(`Invalid JSON for ${packageName}@${version}: ${e.message}`)); }
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
req.on('error', err => reject(new Error(`Network error fetching ${packageName}@${version}: ${err.message}`)));
|
|
71
|
-
req.setTimeout(METADATA_TIMEOUT, () => {
|
|
72
|
-
req.destroy();
|
|
73
|
-
reject(new Error(`Timeout fetching metadata for ${packageName}@${version}`));
|
|
74
|
-
});
|
|
75
|
-
req.end();
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// --- Core functions ---
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Fetch and extract a specific version of an npm package.
|
|
83
|
-
* @param {string} packageName - npm package name (scoped or unscoped)
|
|
84
|
-
* @param {string} version - Exact version string (e.g. "4.17.21")
|
|
85
|
-
* @returns {Promise<{dir: string, cleanup: Function}>}
|
|
86
|
-
*/
|
|
87
|
-
async function fetchPackageTarball(packageName, version) {
|
|
88
|
-
const meta = await fetchVersionMetadata(packageName, version);
|
|
89
|
-
const tarballUrl = meta.dist && meta.dist.tarball;
|
|
90
|
-
if (!tarballUrl) {
|
|
91
|
-
throw new Error(`No tarball URL found for ${packageName}@${version}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const safeName = sanitizePackageName(packageName);
|
|
95
|
-
const tmpBase = path.join(os.tmpdir(), 'muaddib-ast-diff');
|
|
96
|
-
if (!fs.existsSync(tmpBase)) fs.mkdirSync(tmpBase, { recursive: true });
|
|
97
|
-
const tmpDir = fs.mkdtempSync(path.join(tmpBase, `${safeName}-${version}-`));
|
|
98
|
-
|
|
99
|
-
let extractedDir;
|
|
100
|
-
try {
|
|
101
|
-
const tgzPath = path.join(tmpDir, 'package.tar.gz');
|
|
102
|
-
await downloadToFile(tarballUrl, tgzPath);
|
|
103
|
-
extractedDir = extractTarGz(tgzPath, tmpDir);
|
|
104
|
-
} catch (err) {
|
|
105
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { debugLog('tmpDir cleanup failed:', e.message); }
|
|
106
|
-
throw err;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const cleanup = () => {
|
|
110
|
-
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { debugLog('tmpDir cleanup failed:', e.message); }
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
return { dir: extractedDir, cleanup };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Extract dangerous AST patterns from all .js files in a directory.
|
|
118
|
-
* @param {string} directory - Path to the extracted package
|
|
119
|
-
* @returns {Set<string>} Set of pattern names found
|
|
120
|
-
*/
|
|
121
|
-
function extractDangerousPatterns(directory) {
|
|
122
|
-
const patterns = new Set();
|
|
123
|
-
const files = findJsFiles(directory);
|
|
124
|
-
forEachSafeFile(files, (file, content) => {
|
|
125
|
-
extractPatternsFromSource(content, patterns);
|
|
126
|
-
});
|
|
127
|
-
return patterns;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Parse a single JS source string and add detected pattern names to the set.
|
|
132
|
-
* @param {string} source - JS source code
|
|
133
|
-
* @param {Set<string>} patterns - Accumulator set
|
|
134
|
-
*/
|
|
135
|
-
function extractPatternsFromSource(source, patterns) {
|
|
136
|
-
let ast;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (mod === '
|
|
156
|
-
if (mod === '
|
|
157
|
-
if (mod === '
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
node.callee.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (mod === '
|
|
216
|
-
if (mod === '
|
|
217
|
-
if (mod === '
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
*
|
|
227
|
-
* @param {string}
|
|
228
|
-
* @
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
*
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
pattern
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
PATTERN_SEVERITY
|
|
319
|
-
};
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const acorn = require('acorn');
|
|
6
|
+
const walk = require('acorn-walk');
|
|
7
|
+
const { findJsFiles, forEachSafeFile, debugLog } = require('./utils.js');
|
|
8
|
+
const { fetchPackageMetadata, getLatestVersions } = require('./temporal-analysis.js');
|
|
9
|
+
const { downloadToFile, extractTarGz, sanitizePackageName } = require('./shared/download.js');
|
|
10
|
+
|
|
11
|
+
const { MAX_FILE_SIZE, ACORN_OPTIONS, safeParse } = require('./shared/constants.js');
|
|
12
|
+
|
|
13
|
+
const REGISTRY_URL = 'https://registry.npmjs.org';
|
|
14
|
+
const METADATA_TIMEOUT = 10_000;
|
|
15
|
+
|
|
16
|
+
const SENSITIVE_PATHS = [
|
|
17
|
+
'/etc/passwd', '/etc/shadow', '.env', '.npmrc', '.ssh',
|
|
18
|
+
'.aws/credentials', '.bash_history', '.gitconfig'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Severity mapping for each pattern
|
|
22
|
+
const PATTERN_SEVERITY = {
|
|
23
|
+
child_process: 'CRITICAL',
|
|
24
|
+
eval: 'CRITICAL',
|
|
25
|
+
Function: 'CRITICAL',
|
|
26
|
+
'net.connect': 'CRITICAL',
|
|
27
|
+
'process.env': 'HIGH',
|
|
28
|
+
fetch: 'HIGH',
|
|
29
|
+
http_request: 'HIGH',
|
|
30
|
+
https_request: 'HIGH',
|
|
31
|
+
'dns.lookup': 'MEDIUM',
|
|
32
|
+
'fs.readFile_sensitive': 'MEDIUM'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// --- HTTP helpers ---
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fetch version-specific metadata from npm registry.
|
|
39
|
+
* @param {string} packageName
|
|
40
|
+
* @param {string} version
|
|
41
|
+
* @returns {Promise<object>}
|
|
42
|
+
*/
|
|
43
|
+
function fetchVersionMetadata(packageName, version) {
|
|
44
|
+
const encodedName = encodeURIComponent(packageName).replace('%40', '@');
|
|
45
|
+
const url = `${REGISTRY_URL}/${encodedName}/${encodeURIComponent(version)}`;
|
|
46
|
+
const urlObj = new URL(url);
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const req = https.request({
|
|
50
|
+
hostname: urlObj.hostname,
|
|
51
|
+
path: urlObj.pathname,
|
|
52
|
+
method: 'GET',
|
|
53
|
+
headers: { 'User-Agent': 'MUADDIB-Scanner/3.0', 'Accept': 'application/json' }
|
|
54
|
+
}, (res) => {
|
|
55
|
+
if (res.statusCode === 404) {
|
|
56
|
+
res.resume();
|
|
57
|
+
return reject(new Error(`Version ${version} not found for package ${packageName}`));
|
|
58
|
+
}
|
|
59
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
60
|
+
res.resume();
|
|
61
|
+
return reject(new Error(`Registry returned HTTP ${res.statusCode} for ${packageName}@${version}`));
|
|
62
|
+
}
|
|
63
|
+
let data = '';
|
|
64
|
+
res.on('data', chunk => { data += chunk; });
|
|
65
|
+
res.on('end', () => {
|
|
66
|
+
try { resolve(JSON.parse(data)); }
|
|
67
|
+
catch (e) { reject(new Error(`Invalid JSON for ${packageName}@${version}: ${e.message}`)); }
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
req.on('error', err => reject(new Error(`Network error fetching ${packageName}@${version}: ${err.message}`)));
|
|
71
|
+
req.setTimeout(METADATA_TIMEOUT, () => {
|
|
72
|
+
req.destroy();
|
|
73
|
+
reject(new Error(`Timeout fetching metadata for ${packageName}@${version}`));
|
|
74
|
+
});
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Core functions ---
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch and extract a specific version of an npm package.
|
|
83
|
+
* @param {string} packageName - npm package name (scoped or unscoped)
|
|
84
|
+
* @param {string} version - Exact version string (e.g. "4.17.21")
|
|
85
|
+
* @returns {Promise<{dir: string, cleanup: Function}>}
|
|
86
|
+
*/
|
|
87
|
+
async function fetchPackageTarball(packageName, version) {
|
|
88
|
+
const meta = await fetchVersionMetadata(packageName, version);
|
|
89
|
+
const tarballUrl = meta.dist && meta.dist.tarball;
|
|
90
|
+
if (!tarballUrl) {
|
|
91
|
+
throw new Error(`No tarball URL found for ${packageName}@${version}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const safeName = sanitizePackageName(packageName);
|
|
95
|
+
const tmpBase = path.join(os.tmpdir(), 'muaddib-ast-diff');
|
|
96
|
+
if (!fs.existsSync(tmpBase)) fs.mkdirSync(tmpBase, { recursive: true });
|
|
97
|
+
const tmpDir = fs.mkdtempSync(path.join(tmpBase, `${safeName}-${version}-`));
|
|
98
|
+
|
|
99
|
+
let extractedDir;
|
|
100
|
+
try {
|
|
101
|
+
const tgzPath = path.join(tmpDir, 'package.tar.gz');
|
|
102
|
+
await downloadToFile(tarballUrl, tgzPath);
|
|
103
|
+
extractedDir = extractTarGz(tgzPath, tmpDir);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { debugLog('tmpDir cleanup failed:', e.message); }
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { debugLog('tmpDir cleanup failed:', e.message); }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return { dir: extractedDir, cleanup };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract dangerous AST patterns from all .js files in a directory.
|
|
118
|
+
* @param {string} directory - Path to the extracted package
|
|
119
|
+
* @returns {Set<string>} Set of pattern names found
|
|
120
|
+
*/
|
|
121
|
+
function extractDangerousPatterns(directory) {
|
|
122
|
+
const patterns = new Set();
|
|
123
|
+
const files = findJsFiles(directory);
|
|
124
|
+
forEachSafeFile(files, (file, content) => {
|
|
125
|
+
extractPatternsFromSource(content, patterns);
|
|
126
|
+
});
|
|
127
|
+
return patterns;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse a single JS source string and add detected pattern names to the set.
|
|
132
|
+
* @param {string} source - JS source code
|
|
133
|
+
* @param {Set<string>} patterns - Accumulator set
|
|
134
|
+
*/
|
|
135
|
+
function extractPatternsFromSource(source, patterns) {
|
|
136
|
+
let ast = safeParse(source);
|
|
137
|
+
if (!ast) return;
|
|
138
|
+
|
|
139
|
+
walk.simple(ast, {
|
|
140
|
+
CallExpression(node) {
|
|
141
|
+
// eval()
|
|
142
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'eval') {
|
|
143
|
+
patterns.add('eval');
|
|
144
|
+
}
|
|
145
|
+
// Function() as a call
|
|
146
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
147
|
+
patterns.add('Function');
|
|
148
|
+
}
|
|
149
|
+
// require('module')
|
|
150
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'require' &&
|
|
151
|
+
node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
|
|
152
|
+
const mod = node.arguments[0].value;
|
|
153
|
+
if (mod === 'child_process') patterns.add('child_process');
|
|
154
|
+
if (mod === 'http') patterns.add('http_request');
|
|
155
|
+
if (mod === 'https') patterns.add('https_request');
|
|
156
|
+
if (mod === 'dns') patterns.add('dns.lookup');
|
|
157
|
+
if (mod === 'net') patterns.add('net.connect');
|
|
158
|
+
}
|
|
159
|
+
// fetch()
|
|
160
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'fetch') {
|
|
161
|
+
patterns.add('fetch');
|
|
162
|
+
}
|
|
163
|
+
// dns.lookup(), dns.resolve(), etc.
|
|
164
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
165
|
+
node.callee.object.type === 'Identifier' && node.callee.object.name === 'dns') {
|
|
166
|
+
patterns.add('dns.lookup');
|
|
167
|
+
}
|
|
168
|
+
// net.connect(), net.createConnection()
|
|
169
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
170
|
+
node.callee.object.type === 'Identifier' && node.callee.object.name === 'net') {
|
|
171
|
+
patterns.add('net.connect');
|
|
172
|
+
}
|
|
173
|
+
// http.request(), http.get(), https.request(), https.get()
|
|
174
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
175
|
+
node.callee.object.type === 'Identifier') {
|
|
176
|
+
if (node.callee.object.name === 'http') patterns.add('http_request');
|
|
177
|
+
if (node.callee.object.name === 'https') patterns.add('https_request');
|
|
178
|
+
}
|
|
179
|
+
// fs.readFile/readFileSync on sensitive paths
|
|
180
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
181
|
+
node.callee.object.type === 'Identifier' && node.callee.object.name === 'fs' &&
|
|
182
|
+
node.callee.property &&
|
|
183
|
+
(node.callee.property.name === 'readFile' || node.callee.property.name === 'readFileSync')) {
|
|
184
|
+
if (node.arguments.length > 0 && node.arguments[0].type === 'Literal' &&
|
|
185
|
+
typeof node.arguments[0].value === 'string') {
|
|
186
|
+
const filePath = node.arguments[0].value;
|
|
187
|
+
if (SENSITIVE_PATHS.some(s => filePath.includes(s))) {
|
|
188
|
+
patterns.add('fs.readFile_sensitive');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
NewExpression(node) {
|
|
195
|
+
// new Function()
|
|
196
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
197
|
+
patterns.add('Function');
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
MemberExpression(node) {
|
|
202
|
+
// process.env
|
|
203
|
+
if (node.object.type === 'Identifier' && node.object.name === 'process' &&
|
|
204
|
+
node.property && node.property.name === 'env') {
|
|
205
|
+
patterns.add('process.env');
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
ImportDeclaration(node) {
|
|
210
|
+
// import ... from 'child_process'
|
|
211
|
+
if (node.source && node.source.type === 'Literal') {
|
|
212
|
+
const mod = node.source.value;
|
|
213
|
+
if (mod === 'child_process') patterns.add('child_process');
|
|
214
|
+
if (mod === 'http') patterns.add('http_request');
|
|
215
|
+
if (mod === 'https') patterns.add('https_request');
|
|
216
|
+
if (mod === 'dns') patterns.add('dns.lookup');
|
|
217
|
+
if (mod === 'net') patterns.add('net.connect');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Compare AST patterns between two versions of a package.
|
|
225
|
+
* @param {string} packageName - npm package name
|
|
226
|
+
* @param {string} versionA - Older version
|
|
227
|
+
* @param {string} versionB - Newer version
|
|
228
|
+
* @returns {Promise<{added: string[], removed: string[]}>}
|
|
229
|
+
* added = patterns in versionB but NOT in versionA (new dangerous capabilities)
|
|
230
|
+
* removed = patterns in versionA but NOT in versionB
|
|
231
|
+
*/
|
|
232
|
+
async function compareAstPatterns(packageName, versionA, versionB) {
|
|
233
|
+
let cleanupA = null;
|
|
234
|
+
let cleanupB = null;
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const [resultA, resultB] = await Promise.all([
|
|
238
|
+
fetchPackageTarball(packageName, versionA),
|
|
239
|
+
fetchPackageTarball(packageName, versionB)
|
|
240
|
+
]);
|
|
241
|
+
cleanupA = resultA.cleanup;
|
|
242
|
+
cleanupB = resultB.cleanup;
|
|
243
|
+
|
|
244
|
+
const patternsA = extractDangerousPatterns(resultA.dir);
|
|
245
|
+
const patternsB = extractDangerousPatterns(resultB.dir);
|
|
246
|
+
|
|
247
|
+
const added = [...patternsB].filter(p => !patternsA.has(p));
|
|
248
|
+
const removed = [...patternsA].filter(p => !patternsB.has(p));
|
|
249
|
+
|
|
250
|
+
return { added, removed };
|
|
251
|
+
} finally {
|
|
252
|
+
if (cleanupA) cleanupA();
|
|
253
|
+
if (cleanupB) cleanupB();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Detect sudden dangerous API additions between the two most recent versions.
|
|
259
|
+
* @param {string} packageName - npm package name
|
|
260
|
+
* @returns {Promise<object>} Detection result
|
|
261
|
+
*/
|
|
262
|
+
async function detectSuddenAstChanges(packageName) {
|
|
263
|
+
const metadata = await fetchPackageMetadata(packageName);
|
|
264
|
+
const latest = getLatestVersions(metadata, 2);
|
|
265
|
+
|
|
266
|
+
if (latest.length < 2) {
|
|
267
|
+
return {
|
|
268
|
+
packageName,
|
|
269
|
+
latestVersion: latest.length > 0 ? latest[0].version : null,
|
|
270
|
+
previousVersion: null,
|
|
271
|
+
suspicious: false,
|
|
272
|
+
findings: [],
|
|
273
|
+
metadata: {
|
|
274
|
+
latestPublishedAt: latest.length > 0 ? latest[0].publishedAt : null,
|
|
275
|
+
previousPublishedAt: null
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const [newestEntry, previousEntry] = latest;
|
|
281
|
+
const diff = await compareAstPatterns(packageName, previousEntry.version, newestEntry.version);
|
|
282
|
+
|
|
283
|
+
const findings = [];
|
|
284
|
+
for (const pattern of diff.added) {
|
|
285
|
+
const severity = PATTERN_SEVERITY[pattern] || 'MEDIUM';
|
|
286
|
+
findings.push({
|
|
287
|
+
type: 'dangerous_api_added',
|
|
288
|
+
pattern,
|
|
289
|
+
severity,
|
|
290
|
+
description: `Package now uses ${pattern} (not present in previous version)`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
packageName,
|
|
296
|
+
latestVersion: newestEntry.version,
|
|
297
|
+
previousVersion: previousEntry.version,
|
|
298
|
+
suspicious: findings.length > 0,
|
|
299
|
+
findings,
|
|
300
|
+
metadata: {
|
|
301
|
+
latestPublishedAt: newestEntry.publishedAt,
|
|
302
|
+
previousPublishedAt: previousEntry.publishedAt
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
fetchPackageTarball,
|
|
309
|
+
extractDangerousPatterns,
|
|
310
|
+
compareAstPatterns,
|
|
311
|
+
detectSuddenAstChanges,
|
|
312
|
+
// Exported for testing
|
|
313
|
+
extractPatternsFromSource,
|
|
314
|
+
fetchVersionMetadata,
|
|
315
|
+
SENSITIVE_PATHS,
|
|
316
|
+
PATTERN_SEVERITY
|
|
317
|
+
};
|