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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.85",
3
+ "version": "2.10.87",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -175,7 +175,9 @@ async function processDeferredItem(stats) {
175
175
  let sandboxResult;
176
176
  try {
177
177
  const canary = isCanaryEnabled();
178
- sandboxResult = await runSandbox(item.name, { canary, skipSemaphore: true });
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}`);
@@ -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 reason = tier === 2 ? ' (T2, queue low)' : tier === '1b' ? ' (T1b, conditional)' : '';
731
- console.log(`[MONITOR] SANDBOX${reason}: launching for ${name}@${version}${canary ? ' (canary: on)' : ''}...`);
732
- sandboxResult = await runSandbox(name, { canary });
733
- console.log(`[MONITOR] SANDBOX: ${name}@${version} score: ${sandboxResult.score}, severity: ${sandboxResult.severity}`);
734
-
735
- // Check for canary exfiltration findings and send dedicated alert
736
- const canaryFindings = (sandboxResult.findings || []).filter(f => f.type === 'canary_exfiltration');
737
- if (canaryFindings.length > 0) {
738
- console.log(`[MONITOR] CANARY EXFILTRATION: ${name}@${version} ${canaryFindings.length} token(s) stolen!`);
739
- // Dedup: skip if this package was already alerted with canary_exfiltration
740
- const canaryRuleId = 'canary_exfiltration';
741
- const previousRules = alertedPackageRules.get(name);
742
- const alreadyAlerted = previousRules && previousRules.has(canaryRuleId);
743
- if (alreadyAlerted) {
744
- console.log(`[MONITOR] DEDUP: ${name} canary exfiltration (already alerted today)`);
745
- } else {
746
- const url = getWebhookUrl();
747
- if (url) {
748
- const exfiltrations = canaryFindings.map(f => ({
749
- token: f.detail.match(/exfiltrate (\S+)/)?.[1] || 'UNKNOWN',
750
- foundIn: f.detail
751
- }));
752
- const payload = buildCanaryExfiltrationWebhookEmbed(name, version, exfiltrations);
753
- try {
754
- await sendWebhook(url, payload, { rawPayload: true });
755
- console.log(`[MONITOR] Canary exfiltration webhook sent for ${name}@${version}`);
756
- // Track in dedup map
757
- if (previousRules) {
758
- previousRules.add(canaryRuleId);
759
- } else {
760
- alertedPackageRules.set(name, new Set([canaryRuleId]));
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
- if (item.ecosystem === 'npm' && !item.fastTrack) {
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
- // With metadata cache (temporal-analysis.js), the 4 modules share 1 HTTP request.
1159
- // Skipped for fast-track packages (large boring packages temporal checks make
1160
- // 4 HTTP requests to npm registry per package, pointless for 50MB enterprise packages).
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
@@ -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
- console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}, runtime: ${runtimeLabel}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${TIME_OFFSETS.length}, slots: ${slotInfo})...`);
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 < TIME_OFFSETS.length; 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 };
@@ -14,7 +14,7 @@
14
14
  * NOT covered: api.npmjs.org (different server), replicate.npmjs.com (CouchDB changes stream).
15
15
  */
16
16
 
17
- const REGISTRY_SEMAPHORE_MAX = 10;
17
+ const REGISTRY_SEMAPHORE_MAX = 20;
18
18
  const RATE_LIMIT_PER_SEC = 30;
19
19
 
20
20
  // --- Concurrency semaphore ---