muaddib-scanner 2.11.64 → 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
package/src/ioc/updater.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
208
|
-
|
|
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
|
|
214
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
595
|
+
cachedIOCsSig = null;
|
|
596
|
+
lastSourceCheck = 0;
|
|
564
597
|
}
|
|
565
598
|
|
|
566
599
|
/**
|
|
@@ -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:
|
|
109
|
-
resolve(
|
|
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,
|