muaddib-scanner 2.11.99 → 2.11.100

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.11.99",
3
+ "version": "2.11.100",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-06-11T16:29:51.081Z",
3
+ "timestamp": "2026-06-11T17:09:48.912Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -56,14 +56,18 @@ const _lane = { active: 0, queue: [] };
56
56
  /**
57
57
  * Pure classifier. `truncated` (the bounded measurement walk overflowed its
58
58
  * depth/file caps) classifies heavy by default — defensive: an unmeasurable
59
- * package is exactly the kind that blows a worker.
60
- * @param {{totalJsBytes: number, truncated: boolean}|null} weight
59
+ * package is exactly the kind that blows a worker. Compares weightedJsBytes
60
+ * (plain + ×12 minified — see measureJsWeight in queue.js: raw bytes alone
61
+ * missed the minified explosions, powerlines 517KB → 1151MB heap) and falls
62
+ * back to totalJsBytes for callers that don't weight.
63
+ * @param {{totalJsBytes: number, weightedJsBytes?: number, truncated: boolean}|null} weight
61
64
  * @param {number} [thresholdBytes]
62
65
  */
63
66
  function isHeavyScan(weight, thresholdBytes = heavyScanBytesThreshold()) {
64
67
  if (!weight) return false;
65
68
  if (weight.truncated) return true;
66
- return (weight.totalJsBytes || 0) >= thresholdBytes;
69
+ const effective = Number.isFinite(weight.weightedJsBytes) ? weight.weightedJsBytes : (weight.totalJsBytes || 0);
70
+ return effective >= thresholdBytes;
67
71
  }
68
72
 
69
73
  /**
@@ -311,6 +311,36 @@ function countPackageFiles(dir) {
311
311
  const JS_WEIGHT_MAX_DEPTH = 8;
312
312
  const JS_WEIGHT_MAX_FILES = 2000;
313
313
  const JS_WEIGHT_FILE_PATTERN = /\.(?:[cm]?js|[jt]sx?)$/i;
314
+ // Minified JS expands SUPER-linearly in the worker (live counter-examples
315
+ // from the 16:18 rollout, 2026-06-11: powerlines = 517KB JS of which 449KB
316
+ // minified → 1151MB heap, ~2300×; @lethevimlet/sshift = ~1.9MB minified →
317
+ // 1.38GB — both sailed under the raw-bytes threshold as 'light'). Plain
318
+ // source stays roughly linear (the 12MB-heap mode of the bimodal
319
+ // distribution). So minified bytes count ×12 toward the heavy threshold —
320
+ // ≥~256KB of minified JS crosses the 3MiB default. Detection: average line
321
+ // length over the first 4KB; plain code sits at 40-120 chars, minified
322
+ // bundles at 800+ (often a single line). 250 splits cleanly even when a
323
+ // license header pads the probe window.
324
+ const JS_MINIFIED_WEIGHT = 12;
325
+ const JS_MINIFIED_AVG_LINE = 250;
326
+ const JS_MINIFIED_PROBE_BYTES = 4096;
327
+
328
+ /** Probe the first 4KB of a file (never loads the rest) for minification. */
329
+ function probeIsMinified(filePath) {
330
+ let fd = null;
331
+ try {
332
+ fd = fs.openSync(filePath, 'r');
333
+ const buf = Buffer.alloc(JS_MINIFIED_PROBE_BYTES);
334
+ const n = fs.readSync(fd, buf, 0, JS_MINIFIED_PROBE_BYTES, 0);
335
+ if (n <= 0) return false;
336
+ const head = buf.toString('utf8', 0, n);
337
+ return (head.length / head.split('\n').length) > JS_MINIFIED_AVG_LINE;
338
+ } catch {
339
+ return false;
340
+ } finally {
341
+ if (fd !== null) { try { fs.closeSync(fd); } catch { /* best-effort */ } }
342
+ }
343
+ }
314
344
 
315
345
  /**
316
346
  * Measure how much parsable JS a package carries — the heavy-lane
@@ -325,11 +355,16 @@ const JS_WEIGHT_FILE_PATTERN = /\.(?:[cm]?js|[jt]sx?)$/i;
325
355
  * Bounded walk; an overflow (depth/file caps) returns truncated:true, which
326
356
  * isHeavyScan classifies heavy by default.
327
357
  *
358
+ * weightedJsBytes = plain bytes + JS_MINIFIED_WEIGHT × minified bytes — the
359
+ * value isHeavyScan compares against the threshold (raw bytes alone missed
360
+ * the minified explosions, see JS_MINIFIED_WEIGHT above).
361
+ *
328
362
  * @param {string} dir - extracted package directory
329
- * @returns {{ totalJsBytes: number, maxJsFileBytes: number, truncated: boolean }}
363
+ * @returns {{ totalJsBytes: number, minifiedJsBytes: number, weightedJsBytes: number, maxJsFileBytes: number, truncated: boolean }}
330
364
  */
331
365
  function measureJsWeight(dir) {
332
366
  let totalJsBytes = 0;
367
+ let minifiedJsBytes = 0;
333
368
  let maxJsFileBytes = 0;
334
369
  let seen = 0;
335
370
  let truncated = false;
@@ -347,17 +382,20 @@ function measureJsWeight(dir) {
347
382
  walk(path.join(current, entry.name), depth + 1);
348
383
  } else if (entry.isFile() && JS_WEIGHT_FILE_PATTERN.test(entry.name)) {
349
384
  if (++seen > JS_WEIGHT_MAX_FILES) { truncated = true; return; }
385
+ const filePath = path.join(current, entry.name);
350
386
  let size;
351
- try { size = fs.statSync(path.join(current, entry.name)).size; } catch { continue; }
387
+ try { size = fs.statSync(filePath).size; } catch { continue; }
352
388
  if (size > perFileCap) continue; // executor skips these — they never reach the AST
353
389
  totalJsBytes += size;
390
+ if (probeIsMinified(filePath)) minifiedJsBytes += size;
354
391
  if (size > maxJsFileBytes) maxJsFileBytes = size;
355
392
  }
356
393
  }
357
394
  }
358
395
 
359
396
  walk(dir, 0);
360
- return { totalJsBytes, maxJsFileBytes, truncated };
397
+ const weightedJsBytes = (totalJsBytes - minifiedJsBytes) + JS_MINIFIED_WEIGHT * minifiedJsBytes;
398
+ return { totalJsBytes, minifiedJsBytes, weightedJsBytes, maxJsFileBytes, truncated };
361
399
  }
362
400
 
363
401
  /**
@@ -490,7 +528,7 @@ function runScanInWorker(extractedDir, timeoutMs, scanContext = null, signal = n
490
528
  appendWorkerMem({
491
529
  ev: 'spawn', tid: _wmTid,
492
530
  name: _sc.name, version: _sc.version, ecosystem: _sc.ecosystem,
493
- lane: _sc._lane, jsBytes: _sc._jsBytes,
531
+ lane: _sc._lane, jsBytes: _sc._jsBytes, jsMin: _sc._jsMin,
494
532
  rss: process.memoryUsage().rss
495
533
  });
496
534
 
@@ -774,7 +812,8 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
774
812
  // event (runScanInWorker) so lane×heap-peak cross-checks are possible
775
813
  // post-rollout (hard criterion: zero 'light' scans peaking >512MB).
776
814
  _lane: lane,
777
- _jsBytes: jsWeight.totalJsBytes
815
+ _jsBytes: jsWeight.totalJsBytes,
816
+ _jsMin: jsWeight.minifiedJsBytes || 0
778
817
  };
779
818
  // Hand the main-thread-fetched metadata to the worker so its processor skips
780
819
  // the per-worker getPackageMetadata fetch (429-storm fix). npm only; the key