muaddib-scanner 2.11.64 → 2.11.65

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.64",
3
+ "version": "2.11.65",
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-06T11:45:08.682Z",
3
+ "timestamp": "2026-06-06T16:38:57.648Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -13,6 +13,54 @@ const _inflightRequests = new Map(); // packageName → Promise
13
13
  const METADATA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
14
14
  const NEGATIVE_CACHE_TTL = 60 * 1000; // 60 seconds for failed fetches
15
15
  const METADATA_CACHE_MAX = 200;
16
+ // Heap-leak fix: how many of the newest versions keep their FULL body in the cached
17
+ // packument. Consumers (lifecycle/ast diff, maintainer-change) only diff the latest 2.
18
+ const META_KEEP_VERSIONS = Math.max(2, parseInt(process.env.MUADDIB_META_KEEP_VERSIONS, 10) || 3);
19
+
20
+ /**
21
+ * Shrink a full npm packument to what _metadataCache consumers actually need, so the
22
+ * cache never retains tens-of-MB packuments (packages with thousands of versions) —
23
+ * the root cause of the monitor's old_space leak → OOM restarts.
24
+ *
25
+ * Kept: every root field (small), the FULL `time` map (publish timeline — required by
26
+ * getLatestVersions + publish-anomaly), root `dist-tags`/`maintainers`, and the FULL
27
+ * bodies of the newest META_KEEP_VERSIONS versions (+ dist-tags.latest). Older versions
28
+ * are replaced by a truthy placeholder (1) so existence checks
29
+ * (`if (!versions[v]) continue`) and totalVersions counts stay correct without the bulk.
30
+ * The big optional blobs (`readme`, `_attachments`) are dropped.
31
+ * @param {object} parsed - full registry packument
32
+ * @returns {object} slimmed packument (safe drop-in for all current consumers)
33
+ */
34
+ function projectPackument(parsed) {
35
+ if (!parsed || typeof parsed !== 'object' || !parsed.versions || typeof parsed.versions !== 'object') {
36
+ return parsed;
37
+ }
38
+ const versions = parsed.versions;
39
+ const time = (parsed.time && typeof parsed.time === 'object') ? parsed.time : {};
40
+
41
+ // Newest META_KEEP_VERSIONS versions by publish date (same ordering as getLatestVersions).
42
+ const dated = [];
43
+ for (const [v, t] of Object.entries(time)) {
44
+ if (v === 'created' || v === 'modified') continue;
45
+ if (!versions[v]) continue;
46
+ dated.push([v, t]);
47
+ }
48
+ dated.sort((a, b) => new Date(b[1]) - new Date(a[1]));
49
+ const keep = new Set(dated.slice(0, META_KEEP_VERSIONS).map(e => e[0]));
50
+ const distTags = parsed['dist-tags'];
51
+ if (distTags && distTags.latest && versions[distTags.latest]) keep.add(distTags.latest);
52
+
53
+ const slimVersions = {};
54
+ for (const v of Object.keys(versions)) {
55
+ slimVersions[v] = keep.has(v) ? versions[v] : 1; // truthy placeholder for old versions
56
+ }
57
+
58
+ const slim = { ...parsed, versions: slimVersions };
59
+ delete slim.readme;
60
+ delete slim.readmeFilename;
61
+ delete slim._attachments;
62
+ return slim;
63
+ }
16
64
 
17
65
  const LIFECYCLE_SCRIPTS = [
18
66
  'preinstall',
@@ -99,14 +147,19 @@ function _fetchPackageMetadataHttp(packageName) {
99
147
  if (destroyed) return;
100
148
  try {
101
149
  const parsed = JSON.parse(data);
150
+ // Heap-leak fix: project to essentials BEFORE caching. A full packument can be
151
+ // tens of MB (packages with thousands of versions); retaining it whole bloated
152
+ // old_space → OOM restarts. Resolve the slim copy too so the full `parsed` is
153
+ // freed immediately (consumers only need time + the latest few version bodies).
154
+ const slim = projectPackument(parsed);
102
155
  // Store in cache on successful fetch
103
156
  if (_metadataCache.size >= METADATA_CACHE_MAX) {
104
157
  // Evict oldest entry
105
158
  const oldestKey = _metadataCache.keys().next().value;
106
159
  _metadataCache.delete(oldestKey);
107
160
  }
108
- _metadataCache.set(packageName, { data: parsed, fetchedAt: Date.now() });
109
- resolve(parsed);
161
+ _metadataCache.set(packageName, { data: slim, fetchedAt: Date.now() });
162
+ resolve(slim);
110
163
  } catch (e) {
111
164
  reject(new Error(`Invalid JSON from registry for ${packageName}: ${e.message}`));
112
165
  }
@@ -333,6 +386,7 @@ async function detectSuddenLifecycleChange(packageName) {
333
386
  module.exports = {
334
387
  fetchPackageMetadata,
335
388
  clearMetadataCache,
389
+ projectPackument,
336
390
  getLifecycleScripts,
337
391
  compareLifecycleScripts,
338
392
  getLatestVersions,