muaddib-scanner 2.10.30 → 2.10.32
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/README.md +6 -6
- package/package.json +2 -2
- package/sbom.json +0 -0
- package/src/monitor/daemon.js +26 -1
- package/src/response/playbooks.js +7 -0
- package/src/rules/index.js +18 -0
- package/src/sandbox/index.js +113 -60
- package/src/scanner/ast-detectors/handle-call-expression.js +20 -0
- package/src/scanner/ast-detectors/handle-new-expression.js +12 -0
- package/src/scanner/ast-detectors/handle-variable-declarator.js +13 -0
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
npm and PyPI supply-chain attacks are exploding. Shai-Hulud compromised 25K+ repos in 2025. Existing tools detect threats but don't help you respond.
|
|
32
32
|
|
|
33
|
-
MUAD'DIB combines **14 parallel scanners** (
|
|
33
|
+
MUAD'DIB combines **14 parallel scanners** (195 detection rules), a **deobfuscation engine**, **inter-module dataflow analysis**, **compound scoring**, **ML classifiers** (XGBoost), and Docker sandbox to detect known threats and suspicious behavioral patterns in npm and PyPI packages.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -195,7 +195,7 @@ muaddib replay # Ground truth validation (46/49 TPR)
|
|
|
195
195
|
| GitHub Actions | Shai-Hulud backdoor detection |
|
|
196
196
|
| Hash Scanner | Known malicious file hashes |
|
|
197
197
|
|
|
198
|
-
###
|
|
198
|
+
### 195 detection rules
|
|
199
199
|
|
|
200
200
|
All rules are mapped to MITRE ATT&CK techniques. See [SECURITY.md](SECURITY.md#detection-rules-v21021) for the complete rules reference.
|
|
201
201
|
|
|
@@ -271,7 +271,7 @@ With pre-commit framework:
|
|
|
271
271
|
```yaml
|
|
272
272
|
repos:
|
|
273
273
|
- repo: https://github.com/DNSZLSK/muad-dib
|
|
274
|
-
rev: v2.10.
|
|
274
|
+
rev: v2.10.31
|
|
275
275
|
hooks:
|
|
276
276
|
- id: muaddib-scan
|
|
277
277
|
```
|
|
@@ -288,7 +288,7 @@ repos:
|
|
|
288
288
|
| **FPR** (Benign random) | **7.5%** (15/200) | 200 random npm packages, stratified sampling |
|
|
289
289
|
| **ADR** (Adversarial + Holdout) | **94.0%** (101/107) | 67 adversarial + 40 holdout (107 available on disk), global threshold=20 |
|
|
290
290
|
|
|
291
|
-
**
|
|
291
|
+
**2868 tests** across 62 files. **195 rules** (190 RULES + 5 PARANOID).
|
|
292
292
|
|
|
293
293
|
> **Methodology caveats:**
|
|
294
294
|
> - TPR measured on 49 Node.js attack samples (3 browser-only excluded from 51 total)
|
|
@@ -329,7 +329,7 @@ npm test
|
|
|
329
329
|
|
|
330
330
|
### Testing
|
|
331
331
|
|
|
332
|
-
- **
|
|
332
|
+
- **2868 tests** across 62 modular test files
|
|
333
333
|
- **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
|
|
334
334
|
- **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
|
|
335
335
|
- **Ground truth validation** - 51 real-world attacks (93.9% TPR)
|
|
@@ -351,7 +351,7 @@ npm test
|
|
|
351
351
|
- [Evaluation Methodology](docs/EVALUATION_METHODOLOGY.md) - Experimental protocol, holdout scores
|
|
352
352
|
- [Threat Model](docs/threat-model.md) - What MUAD'DIB detects and doesn't detect
|
|
353
353
|
- [Adversarial Evaluation](ADVERSARIAL.md) - Red team samples and ADR results
|
|
354
|
-
- [Security Policy](SECURITY.md) - Detection rules reference (
|
|
354
|
+
- [Security Policy](SECURITY.md) - Detection rules reference (195 rules)
|
|
355
355
|
- [Security Audit](docs/SECURITY_AUDIT.md) - Bypass validation report
|
|
356
356
|
- [FP Analysis](docs/EVALUATION.md) - Historical false positive analysis
|
|
357
357
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muaddib-scanner",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.32",
|
|
4
4
|
"description": "Supply-chain threat detection & response for npm & PyPI/Python",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@eslint/js": "10.0.1",
|
|
58
|
-
"eslint": "10.0
|
|
58
|
+
"eslint": "10.1.0",
|
|
59
59
|
"eslint-plugin-security": "^4.0.0",
|
|
60
60
|
"globals": "17.4.0"
|
|
61
61
|
}
|
package/sbom.json
ADDED
|
Binary file
|
package/src/monitor/daemon.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const os = require('os');
|
|
4
|
-
const { isDockerAvailable } = require('../sandbox/index.js');
|
|
5
|
+
const { isDockerAvailable, SANDBOX_CONCURRENCY_MAX } = require('../sandbox/index.js');
|
|
5
6
|
const { setVerboseMode, isSandboxEnabled, isCanaryEnabled } = require('./classify.js');
|
|
6
7
|
const { loadState, saveState, loadDailyStats, saveDailyStats, purgeTarballCache, getParisHour } = require('./state.js');
|
|
7
8
|
const { isTemporalEnabled, isTemporalAstEnabled, isTemporalPublishEnabled, isTemporalMaintainerEnabled } = require('./temporal.js');
|
|
@@ -33,6 +34,26 @@ function cleanupOrphanTmpDirs() {
|
|
|
33
34
|
} catch {}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
function cleanupOrphanContainers() {
|
|
38
|
+
try {
|
|
39
|
+
// List running containers with the sandbox name prefix (npm-audit-*)
|
|
40
|
+
const output = execFileSync('docker', ['ps', '-q', '--filter', 'name=npm-audit-'], {
|
|
41
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
timeout: 10000
|
|
43
|
+
}).toString().trim();
|
|
44
|
+
if (!output) return;
|
|
45
|
+
const ids = output.split(/\s+/).filter(Boolean);
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
try {
|
|
48
|
+
execFileSync('docker', ['rm', '-f', id], { stdio: 'pipe', timeout: 10000 });
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
console.log(`[MONITOR] Cleaned up ${ids.length} orphan sandbox container(s)`);
|
|
52
|
+
} catch {
|
|
53
|
+
// Docker not available or command failed — skip silently
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
function reportStats(stats) {
|
|
37
58
|
const avg = stats.scanned > 0 ? (stats.totalTimeMs / stats.scanned / 1000).toFixed(1) : '0.0';
|
|
38
59
|
const { t1, t1a, t1b, t2, t3 } = stats.suspectByTier;
|
|
@@ -67,6 +88,8 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
|
|
|
67
88
|
|
|
68
89
|
// Cleanup temp dirs from previous runs (SIGTERM/crash may leave orphans)
|
|
69
90
|
cleanupOrphanTmpDirs();
|
|
91
|
+
// Kill orphan sandbox containers from previous crash (npm-audit-* prefix)
|
|
92
|
+
cleanupOrphanContainers();
|
|
70
93
|
// Layer 3: Purge expired cached tarballs on startup
|
|
71
94
|
purgeTarballCache();
|
|
72
95
|
|
|
@@ -137,6 +160,7 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
|
|
|
137
160
|
console.log(`[MONITOR] State loaded — npm last: ${state.npmLastPackage || 'none'}, pypi last: ${state.pypiLastPackage || 'none'}, npm seq: ${state.npmLastSeq || 'none'}`);
|
|
138
161
|
console.log('[MONITOR] npm changes stream enabled (replicate.npmjs.com) with RSS fallback');
|
|
139
162
|
console.log(`[MONITOR] Scan concurrency: ${SCAN_CONCURRENCY} (MUADDIB_SCAN_CONCURRENCY to override)`);
|
|
163
|
+
console.log(`[MONITOR] Sandbox concurrency: ${SANDBOX_CONCURRENCY_MAX} (MUADDIB_SANDBOX_CONCURRENCY to override)`);
|
|
140
164
|
console.log(`[MONITOR] Polling every ${POLL_INTERVAL / 1000}s. Ctrl+C to stop.\n`);
|
|
141
165
|
|
|
142
166
|
let running = true;
|
|
@@ -193,6 +217,7 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
|
|
|
193
217
|
module.exports = {
|
|
194
218
|
startMonitor,
|
|
195
219
|
cleanupOrphanTmpDirs,
|
|
220
|
+
cleanupOrphanContainers,
|
|
196
221
|
reportStats,
|
|
197
222
|
isDailyReportDue,
|
|
198
223
|
sleep,
|
|
@@ -511,6 +511,13 @@ const PLAYBOOKS = {
|
|
|
511
511
|
'CRITIQUE: Un Proxy JavaScript avec trap set/get/apply est combine avec un appel reseau. ' +
|
|
512
512
|
'Technique d\'interception: le Proxy capture toutes les ecritures de proprietes (credentials, tokens, config) ' +
|
|
513
513
|
'et les exfiltre via HTTPS/fetch/dgram. Supprimer le package. Auditer tous les modules qui importent ce package.',
|
|
514
|
+
proxy_globalthis_intercept:
|
|
515
|
+
'CRITIQUE: new Proxy(globalThis/global) intercepte tous les acces au scope global. ' +
|
|
516
|
+
'L\'attaquant peut hooker eval, Function, require de maniere transparente via le handler Proxy. ' +
|
|
517
|
+
'Supprimer le package immediatement.',
|
|
518
|
+
reflect_bind_code_execution:
|
|
519
|
+
'CRITIQUE: Reflect.apply() avec methode prototype (bind/call/apply) et thisArg=Function/eval. ' +
|
|
520
|
+
'Evasion de 2nd niveau contournant la detection Reflect.apply(eval). Supprimer le package.',
|
|
514
521
|
detached_credential_exfil:
|
|
515
522
|
'CRITIQUE: Process detache avec acces aux credentials et exfiltration reseau. ' +
|
|
516
523
|
'Technique DPRK/Lazarus: le process fils survit au parent (detached:true, unref()) et exfiltre des secrets en arriere-plan. ' +
|
package/src/rules/index.js
CHANGED
|
@@ -2142,6 +2142,24 @@ const RULES = {
|
|
|
2142
2142
|
],
|
|
2143
2143
|
mitre: 'T1082'
|
|
2144
2144
|
},
|
|
2145
|
+
proxy_globalthis_intercept: {
|
|
2146
|
+
id: 'MUADDIB-AST-083',
|
|
2147
|
+
name: 'Proxy GlobalThis Interception',
|
|
2148
|
+
severity: 'CRITICAL',
|
|
2149
|
+
confidence: 'high',
|
|
2150
|
+
description: 'new Proxy(globalThis/global/window/self) — intercepts all global scope access, enabling transparent hooking of eval/Function/require.',
|
|
2151
|
+
references: ['https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy'],
|
|
2152
|
+
mitre: 'T1574'
|
|
2153
|
+
},
|
|
2154
|
+
reflect_bind_code_execution: {
|
|
2155
|
+
id: 'MUADDIB-AST-084',
|
|
2156
|
+
name: 'Reflect.apply Prototype Method Code Execution',
|
|
2157
|
+
severity: 'CRITICAL',
|
|
2158
|
+
confidence: 'high',
|
|
2159
|
+
description: 'Reflect.apply(Function.prototype.bind/call/apply, Function, [...]) — indirect code execution via Reflect with prototype method as target.',
|
|
2160
|
+
references: ['https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply'],
|
|
2161
|
+
mitre: 'T1059'
|
|
2162
|
+
},
|
|
2145
2163
|
lifecycle_missing_script: {
|
|
2146
2164
|
id: 'MUADDIB-PKG-017',
|
|
2147
2165
|
name: 'Phantom Lifecycle Script',
|
package/src/sandbox/index.js
CHANGED
|
@@ -20,6 +20,41 @@ const DOCKER_IMAGE = 'muaddib-sandbox';
|
|
|
20
20
|
const CONTAINER_TIMEOUT = 120000; // 120 seconds
|
|
21
21
|
const SINGLE_RUN_TIMEOUT = 60000; // 60 seconds per run in multi-run mode
|
|
22
22
|
|
|
23
|
+
// ── Sandbox concurrency limiter ──
|
|
24
|
+
// Prevents Docker container saturation under load (16 workers × 3 runs = 48 containers).
|
|
25
|
+
// Pattern: same semaphore as src/shared/http-limiter.js.
|
|
26
|
+
const SANDBOX_CONCURRENCY_MAX = Math.max(1, parseInt(process.env.MUADDIB_SANDBOX_CONCURRENCY, 10) || 3);
|
|
27
|
+
|
|
28
|
+
const _sandboxSemaphore = { active: 0, queue: [] };
|
|
29
|
+
|
|
30
|
+
function acquireSandboxSlot() {
|
|
31
|
+
if (_sandboxSemaphore.active < SANDBOX_CONCURRENCY_MAX) {
|
|
32
|
+
_sandboxSemaphore.active++;
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
_sandboxSemaphore.queue.push(resolve);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function releaseSandboxSlot() {
|
|
41
|
+
if (_sandboxSemaphore.queue.length > 0) {
|
|
42
|
+
const next = _sandboxSemaphore.queue.shift();
|
|
43
|
+
next(); // Transfers slot to next waiter (active count stays the same)
|
|
44
|
+
} else {
|
|
45
|
+
_sandboxSemaphore.active--;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resetSandboxLimiter() {
|
|
50
|
+
_sandboxSemaphore.active = 0;
|
|
51
|
+
_sandboxSemaphore.queue.length = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSandboxSemaphore() {
|
|
55
|
+
return _sandboxSemaphore;
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
// Time offsets for multi-run sandbox execution (ms)
|
|
24
59
|
const TIME_OFFSETS = [
|
|
25
60
|
{ offset: 0, label: 'immediate' },
|
|
@@ -238,11 +273,17 @@ async function runSingleSandbox(packageName, options = {}) {
|
|
|
238
273
|
// Timeout: kill container
|
|
239
274
|
const timer = setTimeout(() => {
|
|
240
275
|
timedOut = true;
|
|
241
|
-
console.log(`[SANDBOX] Timeout (${runTimeout / 1000}s). Killing container...`);
|
|
276
|
+
console.log(`[SANDBOX] Timeout (${runTimeout / 1000}s). Killing container ${containerName}...`);
|
|
242
277
|
try {
|
|
243
278
|
execFileSync('docker', ['kill', containerName], { stdio: 'pipe', timeout: 5000 });
|
|
244
279
|
} catch {
|
|
245
|
-
|
|
280
|
+
// docker kill failed (container in intermediate state) — force remove
|
|
281
|
+
try {
|
|
282
|
+
execFileSync('docker', ['rm', '-f', containerName], { stdio: 'pipe', timeout: 5000 });
|
|
283
|
+
} catch {
|
|
284
|
+
// Last resort: kill the docker client process (container may survive as orphan)
|
|
285
|
+
proc.kill('SIGKILL');
|
|
286
|
+
}
|
|
246
287
|
}
|
|
247
288
|
}, runTimeout);
|
|
248
289
|
|
|
@@ -441,69 +482,81 @@ async function runSandbox(packageName, options = {}) {
|
|
|
441
482
|
}
|
|
442
483
|
|
|
443
484
|
const mode = strict ? 'strict' : 'permissive';
|
|
444
|
-
console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${TIME_OFFSETS.length})...`);
|
|
445
|
-
|
|
446
|
-
const allRuns = [];
|
|
447
|
-
let bestResult = cleanResult;
|
|
448
|
-
|
|
449
|
-
for (let i = 0; i < TIME_OFFSETS.length; i++) {
|
|
450
|
-
const { offset, label } = TIME_OFFSETS[i];
|
|
451
|
-
console.log(`[SANDBOX] Run ${i + 1}/${TIME_OFFSETS.length} (${label})...`);
|
|
452
|
-
|
|
453
|
-
const runResult = await runSingleSandbox(packageName, {
|
|
454
|
-
strict,
|
|
455
|
-
canaryTokens,
|
|
456
|
-
local,
|
|
457
|
-
localAbsPath,
|
|
458
|
-
displayName,
|
|
459
|
-
timeOffset: offset,
|
|
460
|
-
runTimeout: SINGLE_RUN_TIMEOUT
|
|
461
|
-
});
|
|
462
485
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
findingCount: runResult.findings.length
|
|
470
|
-
});
|
|
486
|
+
// Acquire sandbox slot — blocks if SANDBOX_CONCURRENCY_MAX containers already running
|
|
487
|
+
const queueLen = _sandboxSemaphore.queue.length;
|
|
488
|
+
if (queueLen > 0) {
|
|
489
|
+
console.log(`[SANDBOX] Waiting for sandbox slot (${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX} active, ${queueLen} queued)...`);
|
|
490
|
+
}
|
|
491
|
+
await acquireSandboxSlot();
|
|
471
492
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
493
|
+
try {
|
|
494
|
+
console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${TIME_OFFSETS.length}, slots: ${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX})...`);
|
|
495
|
+
|
|
496
|
+
const allRuns = [];
|
|
497
|
+
let bestResult = cleanResult;
|
|
498
|
+
|
|
499
|
+
for (let i = 0; i < TIME_OFFSETS.length; i++) {
|
|
500
|
+
const { offset, label } = TIME_OFFSETS[i];
|
|
501
|
+
console.log(`[SANDBOX] Run ${i + 1}/${TIME_OFFSETS.length} (${label})...`);
|
|
502
|
+
|
|
503
|
+
const runResult = await runSingleSandbox(packageName, {
|
|
504
|
+
strict,
|
|
505
|
+
canaryTokens,
|
|
506
|
+
local,
|
|
507
|
+
localAbsPath,
|
|
508
|
+
displayName,
|
|
509
|
+
timeOffset: offset,
|
|
510
|
+
runTimeout: SINGLE_RUN_TIMEOUT
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
allRuns.push({
|
|
514
|
+
run: i + 1,
|
|
515
|
+
label,
|
|
516
|
+
timeOffset: offset,
|
|
517
|
+
score: runResult.score,
|
|
518
|
+
severity: runResult.severity,
|
|
519
|
+
findingCount: runResult.findings.length
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Keep the result with the highest score
|
|
523
|
+
if (runResult.score > bestResult.score) {
|
|
524
|
+
bestResult = runResult;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Early exit: CRITICAL found, skip remaining runs
|
|
528
|
+
if (runResult.score >= 80) {
|
|
529
|
+
console.log(`[SANDBOX] Critical score (${runResult.score}) detected in run ${i + 1}. Skipping remaining runs.`);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
475
532
|
}
|
|
476
533
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
534
|
+
// If all runs were inconclusive (timeout), propagate inconclusive status
|
|
535
|
+
// instead of returning CLEAN (which would cause false FP relabeling)
|
|
536
|
+
if (bestResult.score === 0 && allRuns.length > 0 && allRuns.every(r => r.score === -1)) {
|
|
537
|
+
bestResult = {
|
|
538
|
+
score: -1,
|
|
539
|
+
severity: 'INCONCLUSIVE',
|
|
540
|
+
findings: [{
|
|
541
|
+
type: 'timeout',
|
|
542
|
+
severity: 'MEDIUM',
|
|
543
|
+
detail: `All ${allRuns.length} runs exceeded timeout — package too large or slow install`,
|
|
544
|
+
evidence: `All ${allRuns.length} runs timed out`
|
|
545
|
+
}],
|
|
546
|
+
raw_report: null,
|
|
547
|
+
suspicious: false,
|
|
548
|
+
inconclusive: true
|
|
549
|
+
};
|
|
481
550
|
}
|
|
482
|
-
}
|
|
483
551
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
bestResult
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
severity: 'MEDIUM',
|
|
493
|
-
detail: `All ${allRuns.length} runs exceeded timeout — package too large or slow install`,
|
|
494
|
-
evidence: `All ${allRuns.length} runs timed out`
|
|
495
|
-
}],
|
|
496
|
-
raw_report: null,
|
|
497
|
-
suspicious: false,
|
|
498
|
-
inconclusive: true
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Attach multi-run metadata
|
|
503
|
-
bestResult.all_runs = allRuns;
|
|
504
|
-
|
|
505
|
-
displayResults(bestResult);
|
|
506
|
-
return bestResult;
|
|
552
|
+
// Attach multi-run metadata
|
|
553
|
+
bestResult.all_runs = allRuns;
|
|
554
|
+
|
|
555
|
+
displayResults(bestResult);
|
|
556
|
+
return bestResult;
|
|
557
|
+
} finally {
|
|
558
|
+
releaseSandboxSlot();
|
|
559
|
+
}
|
|
507
560
|
}
|
|
508
561
|
|
|
509
562
|
// ── Static canary detection ──
|
|
@@ -812,4 +865,4 @@ function displayResults(result) {
|
|
|
812
865
|
}
|
|
813
866
|
}
|
|
814
867
|
|
|
815
|
-
module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS };
|
|
868
|
+
module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore };
|
|
@@ -1496,6 +1496,26 @@ function handleCallExpression(node, ctx) {
|
|
|
1496
1496
|
file: ctx.relFile
|
|
1497
1497
|
});
|
|
1498
1498
|
}
|
|
1499
|
+
// Bypass fix: Reflect.apply(Function.prototype.bind/call/apply, Function, [...])
|
|
1500
|
+
if (target.type === 'MemberExpression') {
|
|
1501
|
+
const methodProp = target.property;
|
|
1502
|
+
const methodName = methodProp?.type === 'Identifier' ? methodProp.name :
|
|
1503
|
+
(methodProp?.type === 'Literal' ? String(methodProp.value) : null);
|
|
1504
|
+
if (methodName === 'bind' || methodName === 'call' || methodName === 'apply') {
|
|
1505
|
+
const thisArg = node.arguments[1];
|
|
1506
|
+
if (thisArg?.type === 'Identifier' &&
|
|
1507
|
+
(thisArg.name === 'Function' || thisArg.name === 'eval' ||
|
|
1508
|
+
ctx.evalAliases?.has(thisArg.name))) {
|
|
1509
|
+
ctx.hasDynamicExec = true;
|
|
1510
|
+
ctx.threats.push({
|
|
1511
|
+
type: 'reflect_bind_code_execution',
|
|
1512
|
+
severity: 'CRITICAL',
|
|
1513
|
+
message: `Reflect.apply(*.${methodName}, ${thisArg.name}, [...]) — indirect ${thisArg.name} invocation via prototype method, bypasses Reflect.apply(${thisArg.name}) detection.`,
|
|
1514
|
+
file: ctx.relFile
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1499
1519
|
// B1: Reflect.apply(require, null, ['child_process']) — bypasses require() call detection
|
|
1500
1520
|
if (target.type === 'Identifier' && target.name === 'require') {
|
|
1501
1521
|
const argsArray = node.arguments[2];
|
|
@@ -58,6 +58,18 @@ function handleNewExpression(node, ctx) {
|
|
|
58
58
|
file: ctx.relFile
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
// Detect new Proxy(globalThis/global/window/self, handler) — intercepts all global access
|
|
62
|
+
if (target.type === 'Identifier' &&
|
|
63
|
+
(target.name === 'globalThis' || target.name === 'global' ||
|
|
64
|
+
target.name === 'window' || target.name === 'self' ||
|
|
65
|
+
ctx.globalThisAliases.has(target.name))) {
|
|
66
|
+
ctx.threats.push({
|
|
67
|
+
type: 'proxy_globalthis_intercept',
|
|
68
|
+
severity: 'CRITICAL',
|
|
69
|
+
message: `new Proxy(${target.name}, handler) — intercepts all global object access. Attacker can hook eval/Function/require transparently.`,
|
|
70
|
+
file: ctx.relFile
|
|
71
|
+
});
|
|
72
|
+
}
|
|
61
73
|
// Detect new Proxy(obj, handler) where handler has set/get traps — data interception
|
|
62
74
|
// Real-world technique: export a Proxy that intercepts all property sets/gets to exfiltrate
|
|
63
75
|
// data flowing through the module. Combined with network (hasNetworkInFile) → credential theft.
|
|
@@ -58,6 +58,19 @@ function handleVariableDeclarator(node, ctx) {
|
|
|
58
58
|
ctx.globalThisAliases.add(node.id.name);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Track Proxy(globalThis) as globalThis alias for downstream detection
|
|
62
|
+
if (node.init?.type === 'NewExpression' &&
|
|
63
|
+
node.init.callee?.type === 'Identifier' && node.init.callee.name === 'Proxy' &&
|
|
64
|
+
node.init.arguments?.length >= 2) {
|
|
65
|
+
const proxyTarget = node.init.arguments[0];
|
|
66
|
+
if (proxyTarget?.type === 'Identifier' &&
|
|
67
|
+
(proxyTarget.name === 'globalThis' || proxyTarget.name === 'global' ||
|
|
68
|
+
proxyTarget.name === 'window' || proxyTarget.name === 'self' ||
|
|
69
|
+
ctx.globalThisAliases.has(proxyTarget.name))) {
|
|
70
|
+
ctx.globalThisAliases.add(node.id.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
// B1: const E = eval; const F = Function;
|
|
62
75
|
if (node.init?.type === 'Identifier' &&
|
|
63
76
|
(node.init.name === 'eval' || node.init.name === 'Function')) {
|