muaddib-scanner 2.11.131 → 2.11.132

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.131",
3
+ "version": "2.11.132",
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-27T16:14:41.754Z",
3
+ "timestamp": "2026-06-28T11:47:00.100Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -405,6 +405,17 @@ function evaluateCacheTrigger(name, docMeta, doc, opts = {}) {
405
405
  return { shouldCache: true, reason: 'first_publish', retentionDays: TARBALL_CACHE_DEFAULT_RETENTION_DAYS };
406
406
  }
407
407
 
408
+ // Trigger 4: account-takeover / burst -- 30-day retention (high-risk fast-takedown class).
409
+ // These are batch/registry-derived (published version vs dist-tags.latest; per-name recent
410
+ // version count), NOT knowable from name+docMeta at ingestion, so the caller
411
+ // (preResolveNpmBatch) computes them from _npmInfo and passes opts.{atoSignal,isBurst}.
412
+ // Closes the Leo Platform gap (June 2026): 20 existing packages re-published at non-latest
413
+ // versions matched neither typosquat nor first_publish -> never prefetched -> the malicious
414
+ // tarballs were 404'd before scan. ATO catches that class; burst catches Miasma-style floods.
415
+ if (opts.atoSignal || opts.isBurst) {
416
+ return { shouldCache: true, reason: opts.atoSignal ? 'ato' : 'burst', retentionDays: TARBALL_CACHE_HIGH_RISK_RETENTION_DAYS };
417
+ }
418
+
408
419
  return { shouldCache: false, reason: '', retentionDays: 0 };
409
420
  }
410
421
 
@@ -598,6 +598,14 @@ function preResolveShouldShed(scanQueue) {
598
598
  // in-flight work, not all the chunks that already completed. When scanQueue
599
599
  // is omitted (unit tests, lib usage), items are only mutated in place and the
600
600
  // caller decides when to push.
601
+ // Burst threshold for the capture-at-publish prefetch trigger. Mirrors
602
+ // BURST_PREALERT_MIN_VERSIONS (queue.js:212) and reads the SAME env var so ops tunes
603
+ // one knob. Per-name version count in the recent-publish window.
604
+ const BURST_MIN_VERSIONS_PREFETCH = (() => {
605
+ const n = parseInt(process.env.MUADDIB_BURST_MIN_VERSIONS, 10);
606
+ return Number.isFinite(n) && n >= 2 ? n : 10;
607
+ })();
608
+
601
609
  async function preResolveNpmBatch(items, stats, scanQueue) {
602
610
  if (!items || items.length === 0) return;
603
611
  const start = Date.now();
@@ -644,6 +652,25 @@ async function preResolveNpmBatch(items, stats, scanQueue) {
644
652
  // weekly_downloads (from getWeeklyDownloads) so the triage block in
645
653
  // queue.js can read meta directly without re-fetching.
646
654
  item._npmInfo = npmInfo;
655
+ // Capture-at-publish trigger for the fast-takedown class (Leo Platform / Miasma,
656
+ // June 2026). ATO (published version != dist-tags.latest) and per-name burst are
657
+ // only knowable from the registry data fetched here, so the name-only
658
+ // evaluateCacheTrigger at ingestion (change.doc is null post-2025) cannot set them.
659
+ // Feed them in now so the burst/ATO subset is prefetched too — only when no
660
+ // higher-priority (ioc/typosquat) trigger already fired. queue.js still computes
661
+ // the same signals later for scan-queue protection + extras enqueue (idempotent).
662
+ if (!item._cacheTrigger) {
663
+ const atoSignal = !!(npmInfo.latestTagVersion && item.version &&
664
+ item.version !== npmInfo.latestTagVersion);
665
+ const burstCount = Number.isFinite(npmInfo.recentWindowCount)
666
+ ? npmInfo.recentWindowCount
667
+ : ((Array.isArray(npmInfo.recentVersions) ? npmInfo.recentVersions.length : 0) + 1);
668
+ const isBurst = burstCount >= BURST_MIN_VERSIONS_PREFETCH;
669
+ if (atoSignal || isBurst) {
670
+ const trig = evaluateCacheTrigger(item.name, null, null, { atoSignal, isBurst });
671
+ if (trig.shouldCache) item._cacheTrigger = trig;
672
+ }
673
+ }
647
674
  resolved++;
648
675
  } else {
649
676
  failed++;