muaddib-scanner 2.11.167 → 2.11.169
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/README.md
CHANGED
|
@@ -345,7 +345,7 @@ npm test
|
|
|
345
345
|
|
|
346
346
|
### Testing
|
|
347
347
|
|
|
348
|
-
- **<!--stat:tests-->
|
|
348
|
+
- **<!--stat:tests-->4561<!--/stat:tests--> tests** across <!--stat:testFiles-->152<!--/stat:testFiles--> modular test files
|
|
349
349
|
- **56 fuzz tests** - Malformed inputs, ReDoS, unicode, binary
|
|
350
350
|
- **Datadog 17K benchmark** - 14,587 confirmed malware samples (in-scope)
|
|
351
351
|
- **Ground truth validation** - 96 real-world attacks (95.74% TPR@3, 88.30% TPR@20 — v2.11.48 full measure on 94 in-scope)
|
package/package.json
CHANGED
package/src/monitor/ingestion.js
CHANGED
|
@@ -383,6 +383,31 @@ function parsePyPIRss(xml) {
|
|
|
383
383
|
const RECENT_PUBLISH_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
384
384
|
const RECENT_PUBLISH_MAX = 5;
|
|
385
385
|
|
|
386
|
+
// Burst versions beyond RECENT_PUBLISH_MAX are dropped by recency. But a buried burst
|
|
387
|
+
// version that ADDS an install hook the latest lacks is exactly the "bury the payload in a
|
|
388
|
+
// publish burst" evasion (jscrambler@8.14.0 added a preinstall absent from 8.13.0). We keep
|
|
389
|
+
// such signal-bearing versions for scanning instead of dropping them — bounded by
|
|
390
|
+
// BURST_SIGNAL_KEEP_MAX so an adversarial 1000-version burst can't blow the enqueue list.
|
|
391
|
+
const INSTALL_HOOK_KEYS = ['preinstall', 'install', 'postinstall'];
|
|
392
|
+
const BURST_SIGNAL_KEEP_MAX = 10;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* True when `scripts` carries an install hook (preinstall/install/postinstall) that the
|
|
396
|
+
* baseline (latest) manifest lacks or defines differently — a cheap manifest-level delta
|
|
397
|
+
* that flags a burst version worth scanning even past the recency cap.
|
|
398
|
+
* @param {Object} scripts - candidate version's package.json scripts
|
|
399
|
+
* @param {Object} baseScripts - the latest (baseline) version's scripts
|
|
400
|
+
* @returns {boolean}
|
|
401
|
+
*/
|
|
402
|
+
function _hasInstallHookDelta(scripts, baseScripts) {
|
|
403
|
+
const s = scripts || {};
|
|
404
|
+
const b = baseScripts || {};
|
|
405
|
+
for (const h of INSTALL_HOOK_KEYS) {
|
|
406
|
+
if (s[h] && s[h] !== b[h]) return true;
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
386
411
|
/**
|
|
387
412
|
* Pure function: pick the most-recently-published version from a packument and
|
|
388
413
|
* return its metadata, plus context useful for ATO detection.
|
|
@@ -460,20 +485,35 @@ function selectMostRecentVersion(packument, options = {}) {
|
|
|
460
485
|
// so a 96-version Miasma burst is distinguishable from a legit 3-5 patch-release day (the
|
|
461
486
|
// capped list alone tops out at maxRecent+1 and can't tell them apart).
|
|
462
487
|
result.recentWindowCount = 1; // includes the most-recent version itself
|
|
488
|
+
let signalKeeps = 0; // burst versions kept past the recency cap for an install-hook delta
|
|
463
489
|
if (versionTimes.length > 1) {
|
|
464
490
|
const cutoff = versionTimes[0][1] - recentWindowMs;
|
|
465
491
|
for (let i = 1; i < versionTimes.length; i++) {
|
|
466
492
|
const [v, ts] = versionTimes[i];
|
|
467
493
|
if (ts < cutoff) break; // sorted desc, so once we cross the cutoff we're done
|
|
468
494
|
result.recentWindowCount++;
|
|
495
|
+
const vData = versions[v];
|
|
469
496
|
if (result.recentVersions.length >= maxRecent) {
|
|
497
|
+
// Past the recency cap → normally a coverage loss. But KEEP a version that adds an
|
|
498
|
+
// install hook the latest lacks (the "bury the payload in a publish burst" evasion,
|
|
499
|
+
// jscrambler shape), bounded by BURST_SIGNAL_KEEP_MAX so a burst can't blow the list.
|
|
500
|
+
if (vData && signalKeeps < BURST_SIGNAL_KEEP_MAX && _hasInstallHookDelta(vData.scripts, result.scripts)) {
|
|
501
|
+
signalKeeps++;
|
|
502
|
+
result.recentVersions.push({
|
|
503
|
+
version: vData.version || v,
|
|
504
|
+
tarball: (vData.dist && vData.dist.tarball) || null,
|
|
505
|
+
unpackedSize: (vData.dist && vData.dist.unpackedSize) || 0,
|
|
506
|
+
scripts: vData.scripts || {},
|
|
507
|
+
keptForSignal: true,
|
|
508
|
+
});
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
470
511
|
// Burst beyond the enqueue cap: collect the version so the caller ledgers it as a
|
|
471
512
|
// coverage loss (it is never enqueued/scanned). Keeps a Miasma-style burst that
|
|
472
513
|
// outruns maxRecent visible instead of vanishing silently (CLAUDE.md "no silent caps").
|
|
473
514
|
result.droppedBurstVersions.push(v);
|
|
474
515
|
continue; // enqueue list capped; count continues
|
|
475
516
|
}
|
|
476
|
-
const vData = versions[v];
|
|
477
517
|
if (!vData) continue;
|
|
478
518
|
result.recentVersions.push({
|
|
479
519
|
version: vData.version || v,
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
* into those dirs and reads only an 8 KB prefix per file, so it catches a multi-MB carrier
|
|
30
30
|
* regardless of the 10 MB parse cap.
|
|
31
31
|
*
|
|
32
|
-
* FP≈0: minified/bundled `dist/*.js` is still printable text (
|
|
33
|
-
* byte) — only
|
|
32
|
+
* FP≈0: minified/bundled `dist/*.js` is still printable text (low control-byte ratio, no leading
|
|
33
|
+
* control byte); a lone NUL delimiter is tolerated — only dense binary content trips it. Severity
|
|
34
|
+
* HIGH (not single-fire): on its
|
|
34
35
|
* own it does not cross the alert threshold; it stacks with the install-hook signal, which
|
|
35
36
|
* the COMPOUND-020 `install_native_drop_exec` correlator escalates to CRITICAL.
|
|
36
37
|
*/
|
|
Binary file
|
package/stats.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "GENERATED by scripts/collect-stats.js — do not edit by hand. Run `npm run docs:stats`.",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.169",
|
|
4
4
|
"scanners": 22,
|
|
5
5
|
"rulesTotal": 277,
|
|
6
6
|
"rulesCore": 272,
|
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
"moduleGraph": 9,
|
|
13
13
|
"pythonAstDetectors": 6,
|
|
14
14
|
"testFiles": 152,
|
|
15
|
-
"tests":
|
|
15
|
+
"tests": 4561
|
|
16
16
|
}
|