muaddib-scanner 2.11.65 → 2.11.66

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.65",
3
+ "version": "2.11.66",
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-06T16:38:57.648Z",
3
+ "timestamp": "2026-06-06T20:23:04.305Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -123,6 +123,11 @@ async function updateIOCs() {
123
123
  console.log('[4/4] Saved to cache: ' + CACHE_IOC_FILE);
124
124
  console.log('\n[OK] IOCs updated: ' + totalNpm + ' npm + ' + totalPyPI + ' PyPI packages');
125
125
 
126
+ // Fresh IOC files written — drop the in-process singleton so the next
127
+ // loadCachedIOCs() rebuilds from them (cross-process monitors pick the change
128
+ // up via the mtime/size source signature within SOURCE_CHECK_INTERVAL).
129
+ invalidateCache();
130
+
126
131
  return { total: totalNpm, totalPyPI: totalPyPI };
127
132
  }
128
133
 
@@ -202,16 +207,41 @@ function mergeIOCs(target, source) {
202
207
  return added;
203
208
  }
204
209
 
205
- // Cache to avoid reloading IOCs on each call
210
+ // IOC store cache. The optimized store is large (~240K entries → hundreds of MB),
211
+ // so it MUST be a stable singleton: rebuilding it duplicates that memory, and any
212
+ // in-flight async scan (sandbox/deferred/network) that captured a prior copy pins
213
+ // it — a periodic rebuild therefore accumulates copies. This was the monitor's
214
+ // old_space → OOM leak: a heap snapshot showed 7+ live copies of the 421K-entry
215
+ // Map retained via loadCachedIOCs closures + suspended Generators/Promises.
216
+ // Fix: rebuild ONLY when a source file actually changes (mtime/size signature) or
217
+ // on invalidateCache(); otherwise return the same object. The signature is
218
+ // re-checked at most every SOURCE_CHECK_INTERVAL so the hot path (called per
219
+ // scan/poll) does zero disk I/O.
220
+ const IOCS_DIR = path.join(__dirname, '..', '..', 'iocs');
221
+ const IOC_SOURCE_FILES = [
222
+ CACHE_IOC_FILE, LOCAL_IOC_FILE, LOCAL_COMPACT_FILE,
223
+ path.join(IOCS_DIR, 'packages.yaml'), path.join(IOCS_DIR, 'builtin.yaml'),
224
+ path.join(IOCS_DIR, 'hashes.yaml'), path.join(IOCS_DIR, 'string-iocs.yaml')
225
+ ];
226
+ function iocSourcesSignature() {
227
+ let sig = '';
228
+ for (const f of IOC_SOURCE_FILES) { try { const s = fs.statSync(f); sig += s.mtimeMs + ':' + s.size + ';'; } catch { sig += '0;'; } }
229
+ return sig;
230
+ }
231
+
206
232
  let cachedIOCsResult = null;
207
- let cachedIOCsTime = 0;
208
- const CACHE_TTL = 10000; // 10 seconds
233
+ let cachedIOCsSig = null;
234
+ let lastSourceCheck = 0;
235
+ const SOURCE_CHECK_INTERVAL = 10000; // re-stat source files at most every 10s
209
236
 
210
237
  function loadCachedIOCs() {
211
- // Return cache if still valid
212
238
  const now = Date.now();
213
- if (cachedIOCsResult && (now - cachedIOCsTime) < CACHE_TTL) {
214
- return cachedIOCsResult;
239
+ if (cachedIOCsResult) {
240
+ // Hot path: within the check window, return the singleton with no disk I/O.
241
+ if (now - lastSourceCheck < SOURCE_CHECK_INTERVAL) return cachedIOCsResult;
242
+ lastSourceCheck = now;
243
+ // Throttled freshness check: keep the singleton unless a source file changed.
244
+ if (iocSourcesSignature() === cachedIOCsSig) return cachedIOCsResult;
215
245
  }
216
246
 
217
247
  // Priority 1: YAML IOCs
@@ -279,9 +309,11 @@ function loadCachedIOCs() {
279
309
  // Create optimized structures for O(1) lookup
280
310
  const optimized = createOptimizedIOCs(merged);
281
311
 
282
- // Store in cache
312
+ // Store as the shared singleton; record the source signature so we only rebuild
313
+ // when the IOC files actually change (see loadCachedIOCs header).
283
314
  cachedIOCsResult = optimized;
284
- cachedIOCsTime = now;
315
+ cachedIOCsSig = iocSourcesSignature();
316
+ lastSourceCheck = now;
285
317
 
286
318
  return optimized;
287
319
  }
@@ -560,7 +592,8 @@ function expandCompactIOCs(compact) {
560
592
 
561
593
  function invalidateCache() {
562
594
  cachedIOCsResult = null;
563
- cachedIOCsTime = 0;
595
+ cachedIOCsSig = null;
596
+ lastSourceCheck = 0;
564
597
  }
565
598
 
566
599
  /**