muaddib-scanner 2.11.167 → 2.11.169

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 CHANGED
@@ -345,7 +345,7 @@ npm test
345
345
 
346
346
  ### Testing
347
347
 
348
- - **<!--stat:tests-->4559<!--/stat:tests--> tests** across <!--stat:testFiles-->152<!--/stat:testFiles--> modular test files
348
+ - **<!--stat:tests-->4561<!--/stat:tests--> tests** across <!--stat:testFiles-->152<!--/stat:testFiles--> modular test files
349
349
  - **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
350
350
  - **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
351
351
  - **Ground truth validation** - 96 real-world attacks (95.74% TPR@3, 88.30% TPR@20 — v2.11.48 full measure on 94 in-scope)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.11.167",
3
+ "version": "2.11.169",
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-07-11T20:50:59.135Z",
3
+ "timestamp": "2026-07-13T15:38:17.450Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -383,6 +383,31 @@ function parsePyPIRss(xml) {
383
383
  const RECENT_PUBLISH_WINDOW_MS = 24 * 60 * 60 * 1000;
384
384
  const RECENT_PUBLISH_MAX = 5;
385
385
 
386
+ // Burst versions beyond RECENT_PUBLISH_MAX are dropped by recency. But a buried burst
387
+ // version that ADDS an install hook the latest lacks is exactly the "bury the payload in a
388
+ // publish burst" evasion (jscrambler@8.14.0 added a preinstall absent from 8.13.0). We keep
389
+ // such signal-bearing versions for scanning instead of dropping them — bounded by
390
+ // BURST_SIGNAL_KEEP_MAX so an adversarial 1000-version burst can't blow the enqueue list.
391
+ const INSTALL_HOOK_KEYS = ['preinstall', 'install', 'postinstall'];
392
+ const BURST_SIGNAL_KEEP_MAX = 10;
393
+
394
+ /**
395
+ * True when `scripts` carries an install hook (preinstall/install/postinstall) that the
396
+ * baseline (latest) manifest lacks or defines differently — a cheap manifest-level delta
397
+ * that flags a burst version worth scanning even past the recency cap.
398
+ * @param {Object} scripts - candidate version's package.json scripts
399
+ * @param {Object} baseScripts - the latest (baseline) version's scripts
400
+ * @returns {boolean}
401
+ */
402
+ function _hasInstallHookDelta(scripts, baseScripts) {
403
+ const s = scripts || {};
404
+ const b = baseScripts || {};
405
+ for (const h of INSTALL_HOOK_KEYS) {
406
+ if (s[h] && s[h] !== b[h]) return true;
407
+ }
408
+ return false;
409
+ }
410
+
386
411
  /**
387
412
  * Pure function: pick the most-recently-published version from a packument and
388
413
  * return its metadata, plus context useful for ATO detection.
@@ -460,20 +485,35 @@ function selectMostRecentVersion(packument, options = {}) {
460
485
  // so a 96-version Miasma burst is distinguishable from a legit 3-5 patch-release day (the
461
486
  // capped list alone tops out at maxRecent+1 and can't tell them apart).
462
487
  result.recentWindowCount = 1; // includes the most-recent version itself
488
+ let signalKeeps = 0; // burst versions kept past the recency cap for an install-hook delta
463
489
  if (versionTimes.length > 1) {
464
490
  const cutoff = versionTimes[0][1] - recentWindowMs;
465
491
  for (let i = 1; i < versionTimes.length; i++) {
466
492
  const [v, ts] = versionTimes[i];
467
493
  if (ts < cutoff) break; // sorted desc, so once we cross the cutoff we're done
468
494
  result.recentWindowCount++;
495
+ const vData = versions[v];
469
496
  if (result.recentVersions.length >= maxRecent) {
497
+ // Past the recency cap → normally a coverage loss. But KEEP a version that adds an
498
+ // install hook the latest lacks (the "bury the payload in a publish burst" evasion,
499
+ // jscrambler shape), bounded by BURST_SIGNAL_KEEP_MAX so a burst can't blow the list.
500
+ if (vData && signalKeeps < BURST_SIGNAL_KEEP_MAX && _hasInstallHookDelta(vData.scripts, result.scripts)) {
501
+ signalKeeps++;
502
+ result.recentVersions.push({
503
+ version: vData.version || v,
504
+ tarball: (vData.dist && vData.dist.tarball) || null,
505
+ unpackedSize: (vData.dist && vData.dist.unpackedSize) || 0,
506
+ scripts: vData.scripts || {},
507
+ keptForSignal: true,
508
+ });
509
+ continue;
510
+ }
470
511
  // Burst beyond the enqueue cap: collect the version so the caller ledgers it as a
471
512
  // coverage loss (it is never enqueued/scanned). Keeps a Miasma-style burst that
472
513
  // outruns maxRecent visible instead of vanishing silently (CLAUDE.md "no silent caps").
473
514
  result.droppedBurstVersions.push(v);
474
515
  continue; // enqueue list capped; count continues
475
516
  }
476
- const vData = versions[v];
477
517
  if (!vData) continue;
478
518
  result.recentVersions.push({
479
519
  version: vData.version || v,
@@ -29,8 +29,9 @@
29
29
  * into those dirs and reads only an 8 KB prefix per file, so it catches a multi-MB carrier
30
30
  * regardless of the 10 MB parse cap.
31
31
  *
32
- * FP≈0: minified/bundled `dist/*.js` is still printable text (no NUL, no leading control
33
- * byte) — only genuinely binary content trips it. Severity HIGH (not single-fire): on its
32
+ * FP≈0: minified/bundled `dist/*.js` is still printable text (low control-byte ratio, no leading
33
+ * control byte); a lone NUL delimiter is tolerated — only dense binary content trips it. Severity
34
+ * HIGH (not single-fire): on its
34
35
  * own it does not cross the alert threshold; it stacks with the install-hook signal, which
35
36
  * the COMPOUND-020 `install_native_drop_exec` correlator escalates to CRITICAL.
36
37
  */
Binary file
package/stats.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "GENERATED by scripts/collect-stats.js — do not edit by hand. Run `npm run docs:stats`.",
3
- "version": "2.11.167",
3
+ "version": "2.11.169",
4
4
  "scanners": 22,
5
5
  "rulesTotal": 277,
6
6
  "rulesCore": 272,
@@ -12,5 +12,5 @@
12
12
  "moduleGraph": 9,
13
13
  "pythonAstDetectors": 6,
14
14
  "testFiles": 152,
15
- "tests": 4559
15
+ "tests": 4561
16
16
  }