muaddib-scanner 2.10.85 → 2.10.87
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/package.json
CHANGED
|
@@ -175,7 +175,9 @@ async function processDeferredItem(stats) {
|
|
|
175
175
|
let sandboxResult;
|
|
176
176
|
try {
|
|
177
177
|
const canary = isCanaryEnabled();
|
|
178
|
-
|
|
178
|
+
// maxRuns=1: deferred items are T1b/T2, time bomb detection (3 runs) is a luxury.
|
|
179
|
+
// 90s instead of 270s per item → 3× faster deferred queue drain.
|
|
180
|
+
sandboxResult = await runSandbox(item.name, { canary, skipSemaphore: true, maxRuns: 1 });
|
|
179
181
|
console.log(`[DEFERRED] SANDBOX COMPLETE: ${key} -> score=${sandboxResult.score}, severity=${sandboxResult.severity}`);
|
|
180
182
|
} catch (err) {
|
|
181
183
|
console.error(`[DEFERRED] SANDBOX ERROR: ${key} — ${err.message}`);
|
package/src/monitor/queue.js
CHANGED
|
@@ -11,7 +11,7 @@ const path = require('path');
|
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const { Worker } = require('worker_threads');
|
|
13
13
|
const { run } = require('../index.js');
|
|
14
|
-
const { runSandbox, isDockerAvailable } = require('../sandbox/index.js');
|
|
14
|
+
const { runSandbox, isDockerAvailable, tryAcquireSandboxSlot, SANDBOX_CONCURRENCY_MAX } = require('../sandbox/index.js');
|
|
15
15
|
const { sendWebhook } = require('../webhook.js');
|
|
16
16
|
const { downloadToFile, extractTarGz, sanitizePackageName } = require('../shared/download.js');
|
|
17
17
|
const { MAX_TARBALL_SIZE } = require('../shared/constants.js');
|
|
@@ -285,6 +285,16 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
|
|
|
285
285
|
const cacheTrigger = meta._cacheTrigger || null;
|
|
286
286
|
|
|
287
287
|
try {
|
|
288
|
+
// Pre-download size check: reject packages known to exceed MAX_TARBALL_SIZE
|
|
289
|
+
// from registry metadata, without wasting a download + 300s timeout.
|
|
290
|
+
// unpackedSize is available from getNpmLatestTarball() after lazy resolution.
|
|
291
|
+
const metaSize = meta.unpackedSize || 0;
|
|
292
|
+
if (metaSize > MAX_TARBALL_SIZE) {
|
|
293
|
+
console.log(`[MONITOR] SIZE_REJECT: ${name}@${version} — metadata size ${(metaSize / 1024 / 1024).toFixed(1)}MB exceeds ${(MAX_TARBALL_SIZE / 1024 / 1024).toFixed(0)}MB limit (skipped without download)`);
|
|
294
|
+
stats.scanned++;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
288
298
|
const tgzPath = path.join(tmpDir, 'package.tar.gz');
|
|
289
299
|
|
|
290
300
|
// Layer 3: Check tarball cache before downloading
|
|
@@ -727,40 +737,61 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
|
|
|
727
737
|
if (shouldSandbox) {
|
|
728
738
|
try {
|
|
729
739
|
const canary = isCanaryEnabled();
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
740
|
+
const maxRuns = tier === '1a' ? undefined : 1;
|
|
741
|
+
|
|
742
|
+
if (tier === '1a') {
|
|
743
|
+
// T1a: mandatory sandbox — block-wait (high-confidence threats MUST get sandbox)
|
|
744
|
+
console.log(`[MONITOR] SANDBOX: launching for ${name}@${version}${canary ? ' (canary: on)' : ''}...`);
|
|
745
|
+
sandboxResult = await runSandbox(name, { canary, maxRuns });
|
|
746
|
+
} else if (tryAcquireSandboxSlot()) {
|
|
747
|
+
// T1b/T2: non-blocking — slot acquired atomically, run with skipSemaphore
|
|
748
|
+
const reason = tier === 2 ? ' (T2, queue low)' : ' (T1b, conditional)';
|
|
749
|
+
console.log(`[MONITOR] SANDBOX${reason}: launching for ${name}@${version}${canary ? ' (canary: on)' : ''}...`);
|
|
750
|
+
sandboxResult = await runSandbox(name, { canary, maxRuns, skipSemaphore: true });
|
|
751
|
+
} else {
|
|
752
|
+
// T1b/T2: all sandbox slots busy — defer instead of blocking worker
|
|
753
|
+
console.log(`[MONITOR] SANDBOX DEFER (slots full): ${name}@${version} (tier=${tier}, score=${riskScore})`);
|
|
754
|
+
enqueueDeferred({
|
|
755
|
+
name, version, ecosystem, tier, riskScore, tarballUrl,
|
|
756
|
+
enqueuedAt: Date.now(),
|
|
757
|
+
staticResult: result,
|
|
758
|
+
npmRegistryMeta,
|
|
759
|
+
retries: 0
|
|
760
|
+
});
|
|
761
|
+
stats.sandboxDeferred = (stats.sandboxDeferred || 0) + 1;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (sandboxResult) {
|
|
765
|
+
console.log(`[MONITOR] SANDBOX: ${name}@${version} → score: ${sandboxResult.score}, severity: ${sandboxResult.severity}`);
|
|
766
|
+
|
|
767
|
+
// Check for canary exfiltration findings and send dedicated alert
|
|
768
|
+
const canaryFindings = (sandboxResult.findings || []).filter(f => f.type === 'canary_exfiltration');
|
|
769
|
+
if (canaryFindings.length > 0) {
|
|
770
|
+
console.log(`[MONITOR] CANARY EXFILTRATION: ${name}@${version} — ${canaryFindings.length} token(s) stolen!`);
|
|
771
|
+
const canaryRuleId = 'canary_exfiltration';
|
|
772
|
+
const previousRules = alertedPackageRules.get(name);
|
|
773
|
+
const alreadyAlerted = previousRules && previousRules.has(canaryRuleId);
|
|
774
|
+
if (alreadyAlerted) {
|
|
775
|
+
console.log(`[MONITOR] DEDUP: ${name} canary exfiltration (already alerted today)`);
|
|
776
|
+
} else {
|
|
777
|
+
const url = getWebhookUrl();
|
|
778
|
+
if (url) {
|
|
779
|
+
const exfiltrations = canaryFindings.map(f => ({
|
|
780
|
+
token: f.detail.match(/exfiltrate (\S+)/)?.[1] || 'UNKNOWN',
|
|
781
|
+
foundIn: f.detail
|
|
782
|
+
}));
|
|
783
|
+
const payload = buildCanaryExfiltrationWebhookEmbed(name, version, exfiltrations);
|
|
784
|
+
try {
|
|
785
|
+
await sendWebhook(url, payload, { rawPayload: true });
|
|
786
|
+
console.log(`[MONITOR] Canary exfiltration webhook sent for ${name}@${version}`);
|
|
787
|
+
if (previousRules) {
|
|
788
|
+
previousRules.add(canaryRuleId);
|
|
789
|
+
} else {
|
|
790
|
+
alertedPackageRules.set(name, new Set([canaryRuleId]));
|
|
791
|
+
}
|
|
792
|
+
} catch (webhookErr) {
|
|
793
|
+
console.error(`[MONITOR] Canary webhook failed for ${name}@${version}: ${webhookErr.message}`);
|
|
761
794
|
}
|
|
762
|
-
} catch (webhookErr) {
|
|
763
|
-
console.error(`[MONITOR] Canary webhook failed for ${name}@${version}: ${webhookErr.message}`);
|
|
764
795
|
}
|
|
765
796
|
}
|
|
766
797
|
}
|
|
@@ -1153,11 +1184,13 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
|
|
|
1153
1184
|
let publishResult = null;
|
|
1154
1185
|
let maintainerResult = null;
|
|
1155
1186
|
|
|
1156
|
-
|
|
1187
|
+
const TEMPORAL_LOAD_SHED_THRESHOLD = 2000;
|
|
1188
|
+
const skipTemporal = item.fastTrack || scanQueue.length > TEMPORAL_LOAD_SHED_THRESHOLD;
|
|
1189
|
+
if (item.ecosystem === 'npm' && !skipTemporal) {
|
|
1157
1190
|
// Run all 4 temporal checks in parallel — each is independent.
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1191
|
+
// AST diff alone consumes 5 HTTP semaphore slots per package (2 tarball downloads + 3 metadata).
|
|
1192
|
+
// With 16 workers that's 80 slot requests for 10 slots → workers blocked 80% of the time.
|
|
1193
|
+
// Load-shed when queue > 2000: temporal analysis is a luxury during catch-up.
|
|
1161
1194
|
const [tempRes, astRes, pubRes, maintRes] = await Promise.allSettled([
|
|
1162
1195
|
runTemporalCheck(item.name, dailyAlerts),
|
|
1163
1196
|
runTemporalAstCheck(item.name, dailyAlerts),
|
|
@@ -1168,6 +1201,8 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
|
|
|
1168
1201
|
astResult = astRes.status === 'fulfilled' ? astRes.value : null;
|
|
1169
1202
|
publishResult = pubRes.status === 'fulfilled' ? pubRes.value : null;
|
|
1170
1203
|
maintainerResult = maintRes.status === 'fulfilled' ? maintRes.value : null;
|
|
1204
|
+
} else if (skipTemporal && item.ecosystem === 'npm' && !item.fastTrack) {
|
|
1205
|
+
console.log(`[MONITOR] TEMPORAL LOAD-SHED: ${item.name}@${item.version} (queue=${scanQueue.length} > ${TEMPORAL_LOAD_SHED_THRESHOLD})`);
|
|
1171
1206
|
}
|
|
1172
1207
|
|
|
1173
1208
|
// Abort check: if timeout fired during temporal checks, skip the expensive scan
|
package/src/sandbox/index.js
CHANGED
|
@@ -40,6 +40,19 @@ function acquireSandboxSlot() {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Non-blocking slot acquisition. Returns true if slot acquired, false if all busy.
|
|
45
|
+
* Atomic: check + acquire in a single synchronous call — no race condition.
|
|
46
|
+
* Used by T1b/T2 to defer instead of blocking when slots are full.
|
|
47
|
+
*/
|
|
48
|
+
function tryAcquireSandboxSlot() {
|
|
49
|
+
if (_sandboxSemaphore.active < SANDBOX_CONCURRENCY_MAX) {
|
|
50
|
+
_sandboxSemaphore.active++;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
function releaseSandboxSlot() {
|
|
44
57
|
if (_sandboxSemaphore.queue.length > 0) {
|
|
45
58
|
const next = _sandboxSemaphore.queue.shift();
|
|
@@ -641,12 +654,15 @@ async function runSandbox(packageName, options = {}) {
|
|
|
641
654
|
try {
|
|
642
655
|
const runtimeLabel = useGvisor ? 'gvisor' : 'docker';
|
|
643
656
|
const slotInfo = skipSem ? 'deferred-slot' : `${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX}`;
|
|
644
|
-
|
|
657
|
+
// maxRuns: cap number of time-offset runs. Default: all TIME_OFFSETS (3 runs).
|
|
658
|
+
// T1b/T2 use maxRuns=1 to reduce 270s→90s — time bomb detection is a luxury under load.
|
|
659
|
+
const effectiveRuns = Math.min(options.maxRuns || TIME_OFFSETS.length, TIME_OFFSETS.length);
|
|
660
|
+
console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}, runtime: ${runtimeLabel}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${effectiveRuns}, slots: ${slotInfo})...`);
|
|
645
661
|
|
|
646
662
|
const allRuns = [];
|
|
647
663
|
let bestResult = cleanResult;
|
|
648
664
|
|
|
649
|
-
for (let i = 0; i <
|
|
665
|
+
for (let i = 0; i < effectiveRuns; i++) {
|
|
650
666
|
const { offset, label } = TIME_OFFSETS[i];
|
|
651
667
|
console.log(`[SANDBOX] Run ${i + 1}/${TIME_OFFSETS.length} (${label})...`);
|
|
652
668
|
|
|
@@ -1044,4 +1060,4 @@ function displayResults(result) {
|
|
|
1044
1060
|
}
|
|
1045
1061
|
}
|
|
1046
1062
|
|
|
1047
|
-
module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, isGvisorAvailable, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore };
|
|
1063
|
+
module.exports = { buildSandboxImage, runSandbox, runSingleSandbox, scoreFindings, generateNetworkReport, EXFIL_PATTERNS, SAFE_DOMAINS, getSeverity, displayResults, isDockerAvailable, imageExists, isGvisorAvailable, STATIC_CANARY_TOKENS, detectStaticCanaryExfiltration, analyzePreloadLog, TIME_OFFSETS, SAFE_SANDBOX_CMDS, SANDBOX_CONCURRENCY_MAX, acquireSandboxSlot, tryAcquireSandboxSlot, releaseSandboxSlot, resetSandboxLimiter, getSandboxSemaphore };
|