muaddib-scanner 2.10.18 → 2.10.19

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.18",
3
+ "version": "2.10.19",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -79,9 +79,23 @@ async function getPackageMetadata(packageName) {
79
79
  // Validate package name before building URL
80
80
  if (!NPM_PACKAGE_REGEX.test(packageName)) return null;
81
81
 
82
- // 1. Registry metadata
83
- const registryUrl = REGISTRY_URL + '/' + encodeURIComponent(packageName);
84
- const meta = await fetchWithRetry(registryUrl);
82
+ // 1. Registry metadata — read from temporal-analysis cache if warm (monitor pipeline
83
+ // pre-fetches metadata for temporal checks). Only reads the Map, never fires HTTP.
84
+ // Falls back to own fetchWithRetry (with retries + 429 handling) on cache miss.
85
+ let meta = null;
86
+ try {
87
+ const { _metadataCache, METADATA_CACHE_TTL } = require('../temporal-analysis.js');
88
+ const cached = _metadataCache.get(packageName);
89
+ if (cached && (Date.now() - cached.fetchedAt) < METADATA_CACHE_TTL) {
90
+ meta = cached.data;
91
+ }
92
+ } catch {
93
+ // temporal-analysis not available — fall through to fetchWithRetry
94
+ }
95
+ if (!meta) {
96
+ const registryUrl = REGISTRY_URL + '/' + encodeURIComponent(packageName);
97
+ meta = await fetchWithRetry(registryUrl);
98
+ }
85
99
  if (!meta) return null;
86
100
 
87
101
  const createdAt = meta.time?.created || null;
@@ -4,6 +4,13 @@ const REGISTRY_URL = 'https://registry.npmjs.org';
4
4
  const TIMEOUT_MS = 10_000;
5
5
  const MAX_RESPONSE_SIZE = 50 * 1024 * 1024; // 50MB (some packages have lots of versions)
6
6
 
7
+ // Metadata cache: avoids duplicate HTTP requests when multiple temporal modules
8
+ // fetch the same package metadata within a short window (monitor pipeline).
9
+ const _metadataCache = new Map(); // packageName → { data, fetchedAt }
10
+ const _inflightRequests = new Map(); // packageName → Promise
11
+ const METADATA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
12
+ const METADATA_CACHE_MAX = 200;
13
+
7
14
  const LIFECYCLE_SCRIPTS = [
8
15
  'preinstall',
9
16
  'install',
@@ -16,11 +23,10 @@ const LIFECYCLE_SCRIPTS = [
16
23
  ];
17
24
 
18
25
  /**
19
- * Fetch full package metadata from the npm registry.
20
- * @param {string} packageName - npm package name (scoped or unscoped)
21
- * @returns {Promise<object>} Full registry metadata (versions, time, maintainers, etc.)
26
+ * Raw HTTP fetch always hits the npm registry. Use fetchPackageMetadata() instead,
27
+ * which adds caching and inflight dedup.
22
28
  */
23
- function fetchPackageMetadata(packageName) {
29
+ function _fetchPackageMetadataImpl(packageName) {
24
30
  const encodedName = encodeURIComponent(packageName).replace('%40', '@');
25
31
  const url = `${REGISTRY_URL}/${encodedName}`;
26
32
  const urlObj = new URL(url);
@@ -68,7 +74,15 @@ function fetchPackageMetadata(packageName) {
68
74
  res.on('end', () => {
69
75
  if (destroyed) return;
70
76
  try {
71
- resolve(JSON.parse(data));
77
+ const parsed = JSON.parse(data);
78
+ // Store in cache on successful fetch
79
+ if (_metadataCache.size >= METADATA_CACHE_MAX) {
80
+ // Evict oldest entry
81
+ const oldestKey = _metadataCache.keys().next().value;
82
+ _metadataCache.delete(oldestKey);
83
+ }
84
+ _metadataCache.set(packageName, { data: parsed, fetchedAt: Date.now() });
85
+ resolve(parsed);
72
86
  } catch (e) {
73
87
  reject(new Error(`Invalid JSON from registry for ${packageName}: ${e.message}`));
74
88
  }
@@ -88,6 +102,39 @@ function fetchPackageMetadata(packageName) {
88
102
  });
89
103
  }
90
104
 
105
+ /**
106
+ * Fetch full package metadata from the npm registry with caching and inflight dedup.
107
+ * Multiple callers requesting the same package within 5 minutes share one HTTP request.
108
+ * @param {string} packageName - npm package name (scoped or unscoped)
109
+ * @returns {Promise<object>} Full registry metadata (versions, time, maintainers, etc.)
110
+ */
111
+ function fetchPackageMetadata(packageName) {
112
+ // Check cache first (TTL-based)
113
+ const cached = _metadataCache.get(packageName);
114
+ if (cached && (Date.now() - cached.fetchedAt) < METADATA_CACHE_TTL) {
115
+ return Promise.resolve(cached.data);
116
+ }
117
+
118
+ // Dedup inflight requests — if the same package is already being fetched, reuse that Promise
119
+ if (_inflightRequests.has(packageName)) {
120
+ return _inflightRequests.get(packageName);
121
+ }
122
+
123
+ const promise = _fetchPackageMetadataImpl(packageName).finally(() => {
124
+ _inflightRequests.delete(packageName);
125
+ });
126
+ _inflightRequests.set(packageName, promise);
127
+ return promise;
128
+ }
129
+
130
+ /**
131
+ * Clear the metadata cache. Exported for tests and monitor reset.
132
+ */
133
+ function clearMetadataCache() {
134
+ _metadataCache.clear();
135
+ _inflightRequests.clear();
136
+ }
137
+
91
138
  /**
92
139
  * Extract lifecycle scripts from a package.json object.
93
140
  * @param {object} packageJson - A package.json object (or a version entry from registry metadata)
@@ -253,8 +300,14 @@ async function detectSuddenLifecycleChange(packageName) {
253
300
 
254
301
  module.exports = {
255
302
  fetchPackageMetadata,
303
+ clearMetadataCache,
256
304
  getLifecycleScripts,
257
305
  compareLifecycleScripts,
258
306
  getLatestVersions,
259
- detectSuddenLifecycleChange
307
+ detectSuddenLifecycleChange,
308
+ // Exposed for tests only
309
+ _metadataCache,
310
+ _inflightRequests,
311
+ METADATA_CACHE_TTL,
312
+ METADATA_CACHE_MAX
260
313
  };