muaddib-scanner 2.11.94 → 2.11.96
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/monitor/spill.js
CHANGED
|
@@ -83,9 +83,24 @@ function _writeEntries(file, entries) {
|
|
|
83
83
|
fs.renameSync(tmp, file);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Conservative upper bound on a spilled line's byte size (the SPILL_FIELDS
|
|
87
|
+
// record + ts + newline run ~150-250 bytes). Used for the O(1) stat-gated
|
|
88
|
+
// compaction trigger below — overestimating only makes compaction fire a bit
|
|
89
|
+
// late, never early, so the file ceiling stays ~MUADDIB_SPILL_MAX.
|
|
90
|
+
const SPILL_LINE_BYTES_EST = 256;
|
|
91
|
+
|
|
86
92
|
/**
|
|
87
93
|
* Append evicted queue items to the backlog. Never throws; on write failure the
|
|
88
94
|
* caller's fallback is the pre-spill behavior (drop, ledgered).
|
|
95
|
+
*
|
|
96
|
+
* HOT PATH — runs INSIDE the EMERGENCY memory handler (evictFromScanQueueBulk),
|
|
97
|
+
* so it MUST be append-only and allocation-free beyond the write buffer. The
|
|
98
|
+
* 2026-06-11 freeze was caused by calling _compactBacklog (which reads + parses
|
|
99
|
+
* the WHOLE backlog) on every spill: a large allocation during a reclaim stall
|
|
100
|
+
* that wedged the handler before it could free RSS. Compaction now fires ONLY
|
|
101
|
+
* when a cheap statSync shows the file is genuinely near the cap (normally
|
|
102
|
+
* never during an EMERGENCY — the backlog is far below the byte budget there),
|
|
103
|
+
* and the calm-time drain also keeps it bounded.
|
|
89
104
|
* @param {Array<object>} items evicted scan-queue items
|
|
90
105
|
* @returns {number} how many items were actually persisted
|
|
91
106
|
*/
|
|
@@ -107,7 +122,11 @@ function spillItems(items) {
|
|
|
107
122
|
written++;
|
|
108
123
|
}
|
|
109
124
|
if (buf) fs.appendFileSync(file, buf, 'utf8');
|
|
110
|
-
|
|
125
|
+
// O(1) stat-gated compaction: only read+rewrite the file when it is actually
|
|
126
|
+
// near the cap. NO whole-file read on the normal EMERGENCY spill path.
|
|
127
|
+
let size = 0;
|
|
128
|
+
try { size = fs.statSync(file).size; } catch { /* fresh file */ }
|
|
129
|
+
if (size > _maxEntries() * SPILL_LINE_BYTES_EST) _compactBacklog(file);
|
|
111
130
|
} catch {
|
|
112
131
|
return 0; // degrade to drop-with-ledger at the call site
|
|
113
132
|
}
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
const dns = require('dns');
|
|
19
19
|
const { debugLog } = require('../utils.js');
|
|
20
|
-
const { isShadowEnabled, recordShadowDivergence } = require('../shared/shadow.js');
|
|
21
20
|
|
|
22
21
|
const MX_TIMEOUT_MS = 3000;
|
|
23
22
|
const MX_CACHE_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
@@ -226,8 +225,11 @@ async function fetchRdap(domain, options = {}) {
|
|
|
226
225
|
}
|
|
227
226
|
|
|
228
227
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
228
|
+
* @deprecated Retired from the production emission path by the 2026-06-11 V2
|
|
229
|
+
* flip — `checkCompromisedDomain` now emits via `isCompromisedDomainV2`. Kept
|
|
230
|
+
* (exported + unit-tested) as the documented former semantics: creation AFTER
|
|
231
|
+
* first publish with a 30-day pre-publish margin. The margin was the source of
|
|
232
|
+
* ~23% of historical FPs (a dev buying a domain weeks before shipping v1).
|
|
231
233
|
*/
|
|
232
234
|
function isCompromisedDomain(creationDateISO, packageCreatedAtISO) {
|
|
233
235
|
if (!creationDateISO || !packageCreatedAtISO) return false;
|
|
@@ -292,12 +294,16 @@ function isCompromisedDomainV2(creationDateISO, firstPublishISO, domain) {
|
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
/**
|
|
295
|
-
* F1 entry point
|
|
297
|
+
* F1 entry point — emits `compromised_email_domain` (HIGH×high = 10, composite-only:
|
|
298
|
+
* sub-T1, and the tier-1b corroboration gate requires ≥2 distinct signals). LIVE
|
|
299
|
+
* semantics since 2026-06-11 = `isCompromisedDomainV2` (strict creation > first
|
|
300
|
+
* publish, public email providers excluded). The former V1 30-day-margin
|
|
301
|
+
* semantics is retired (see isCompromisedDomain @deprecated).
|
|
296
302
|
* @param {object|null} meta - Digested metadata. Reads maintainer_emails + created_at
|
|
297
303
|
* (= the package's FIRST publish date, both npm and PyPI sides).
|
|
298
304
|
* @param {object} options - { fetchRdap } for tests to inject a mock.
|
|
299
|
-
* { shadowCtx
|
|
300
|
-
*
|
|
305
|
+
* { shadowCtx } is accepted but ignored since the flip (kept so existing callers
|
|
306
|
+
* in processor.js / pypi-maintainer.js need no change).
|
|
301
307
|
* @returns {Promise<Array>} threats array
|
|
302
308
|
*/
|
|
303
309
|
async function checkCompromisedDomain(meta, options = {}) {
|
|
@@ -321,25 +327,12 @@ async function checkCompromisedDomain(meta, options = {}) {
|
|
|
321
327
|
continue;
|
|
322
328
|
}
|
|
323
329
|
if (!rdap || !rdap.creationDate) continue;
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const v2 = isCompromisedDomainV2(rdap.creationDate, meta.created_at, domain);
|
|
331
|
-
if (v1 !== v2) {
|
|
332
|
-
const ctx = options.shadowCtx || {};
|
|
333
|
-
recordShadowDivergence({
|
|
334
|
-
detector: 'compromised_email_domain',
|
|
335
|
-
package: ctx.name, version: ctx.version, ecosystem: ctx.ecosystem,
|
|
336
|
-
oldVerdict: v1, newVerdict: v2,
|
|
337
|
-
evidence: { domain, creationDate: rdap.creationDate, firstPublish: meta.created_at, oldMarginDays: 30 }
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
} catch { /* shadow must never affect the scan */ }
|
|
342
|
-
if (isCompromisedDomain(rdap.creationDate, meta.created_at)) {
|
|
330
|
+
// V2 is the LIVE semantics since the 2026-06-11 flip (backtest #545 era:
|
|
331
|
+
// V2 strict-creation + public-provider exclusion killed 307/1346 historical
|
|
332
|
+
// FP alerts — the @tronweb3/* wallet-adapter monorepo flood — and added 0
|
|
333
|
+
// new flags / 0 FN; V2 ⊂ V1, so no shadow net is needed). The old V1 margin
|
|
334
|
+
// shadow hook is retired with the flip.
|
|
335
|
+
if (isCompromisedDomainV2(rdap.creationDate, meta.created_at, domain)) {
|
|
343
336
|
const cd = rdap.creationDate.slice(0, 10);
|
|
344
337
|
const pd = meta.created_at.slice(0, 10);
|
|
345
338
|
threats.push({
|