muaddib-scanner 2.11.88 → 2.11.90
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/queue.js
CHANGED
|
@@ -150,10 +150,14 @@ const BURST_PREALERT_MIN_VERSIONS = (() => {
|
|
|
150
150
|
const n = parseInt(process.env.MUADDIB_BURST_MIN_VERSIONS, 10);
|
|
151
151
|
return Number.isFinite(n) && n >= 2 ? n : 10;
|
|
152
152
|
})();
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
153
|
+
// Burst ping throttle (FPR/notif audit 2026-06): name -> last burst-alert timestamp.
|
|
154
|
+
// Was a lifetime Set (dedup once per process), which both (a) silenced a genuine
|
|
155
|
+
// re-burst days later and (b) on a process that runs for weeks accumulated spam from
|
|
156
|
+
// every monorepo/CI nightly that re-bursts. Now a 24h-cooldown Map: one alert per
|
|
157
|
+
// package per day max. Bounded — cleared at the cap so it can never grow without limit.
|
|
158
|
+
const _burstAlerted = new Map();
|
|
156
159
|
const BURST_ALERTED_MAX = 20_000;
|
|
160
|
+
const BURST_ALERT_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24h
|
|
157
161
|
|
|
158
162
|
// Stage 3 — sandbox gate. Static-score threshold below which T1b/T2 packages
|
|
159
163
|
// are NOT sandboxed (static result alone is authoritative). Tightens the prior
|
|
@@ -1530,9 +1534,22 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
|
|
|
1530
1534
|
const isBurst = burstCount >= BURST_PREALERT_MIN_VERSIONS;
|
|
1531
1535
|
if (isBurst) {
|
|
1532
1536
|
item.isBurst = true;
|
|
1533
|
-
|
|
1537
|
+
// Anti-flood (notification only — the burst versions are STILL queued and scanned
|
|
1538
|
+
// below regardless; muting the heads-up never weakens detection):
|
|
1539
|
+
// 1) Established packages (mature + many versions) bursting are monorepo / CI
|
|
1540
|
+
// nightly churn, not the Shai-Hulud account-takeover signal (that is a NEW /
|
|
1541
|
+
// low-reputation package suddenly bursting). A real takeover of an established
|
|
1542
|
+
// package is still caught by the per-version scan + the atoSignal above.
|
|
1543
|
+
// 2) 24h cooldown per package so a package re-bursting all day pings at most once.
|
|
1544
|
+
const _np = item._npmInfo || npmInfo || {};
|
|
1545
|
+
const _established = Number.isFinite(_np.age_days) && _np.age_days > 730 &&
|
|
1546
|
+
Number.isFinite(_np.version_count) && _np.version_count > 100;
|
|
1547
|
+
const _now = Date.now();
|
|
1548
|
+
const _last = _burstAlerted.get(item.name);
|
|
1549
|
+
const _onCooldown = _last && (_now - _last) < BURST_ALERT_COOLDOWN_MS;
|
|
1550
|
+
if (!_established && !_onCooldown) {
|
|
1534
1551
|
if (_burstAlerted.size >= BURST_ALERTED_MAX) _burstAlerted.clear();
|
|
1535
|
-
_burstAlerted.
|
|
1552
|
+
_burstAlerted.set(item.name, _now);
|
|
1536
1553
|
stats.burstPreAlerts = (stats.burstPreAlerts || 0) + 1;
|
|
1537
1554
|
console.log(`[MONITOR] BURST PRE-ALERT: ${item.name} — ${burstCount} versions in the recent window`);
|
|
1538
1555
|
sendBurstPreAlert(item.name, burstCount, item.ecosystem).catch(err => {
|
|
@@ -217,6 +217,10 @@ async function getPackageMetadata(packageName) {
|
|
|
217
217
|
// / pinned-old / vendored versions bypass the cap so we don't mask attacks
|
|
218
218
|
// captured in static fixtures (e.g. eslint-scope 3.7.2, chalk 5.6.1).
|
|
219
219
|
latest_version: latestVersion || null,
|
|
220
|
+
// P4 : full dist-tags map ({ latest, next, canary, ... }) so scoring can tell a
|
|
221
|
+
// maintainer-controlled pre-release channel version (inherits partial reputation)
|
|
222
|
+
// from a historical / pinned-old version (no reputation).
|
|
223
|
+
dist_tags: (meta['dist-tags'] && typeof meta['dist-tags'] === 'object') ? meta['dist-tags'] : null,
|
|
220
224
|
// F3 : list of maintainer email addresses (lowercased, unique) for DNS
|
|
221
225
|
// MX / RDAP downstream checks. Empty array if no emails published.
|
|
222
226
|
maintainer_emails: maintainerEmails,
|
package/src/scoring.js
CHANGED
|
@@ -1874,6 +1874,23 @@ function _hasMaliceSignal(threats) {
|
|
|
1874
1874
|
return threats.some(t => t.severity === 'HIGH' || t.severity === 'CRITICAL');
|
|
1875
1875
|
}
|
|
1876
1876
|
|
|
1877
|
+
// P4 (pre-release reputation): known maintainer-controlled pre-release dist-tag names.
|
|
1878
|
+
const _PRERELEASE_TAG_RE = /^(next|canary|nightly|rc|beta|alpha|dev|experimental|preview|snapshot|edge|insider|insiders)$/i;
|
|
1879
|
+
|
|
1880
|
+
// True when the scanned version is published on a NON-latest pre-release dist-tag
|
|
1881
|
+
// (e.g. dist-tags = { latest: "3.19.1", next: "3.19.0-nightly-..." } and we scan the
|
|
1882
|
+
// nightly). Maintainer-controlled, so an attacker cannot put a payload on `next`
|
|
1883
|
+
// without the account — and Track R still floors confirmed malice regardless.
|
|
1884
|
+
function _isPrereleaseChannelVersion(metadata) {
|
|
1885
|
+
const dt = metadata && metadata.dist_tags;
|
|
1886
|
+
const sv = metadata && metadata.scan_version;
|
|
1887
|
+
if (!dt || typeof dt !== 'object' || typeof sv !== 'string') return false;
|
|
1888
|
+
for (const [tag, ver] of Object.entries(dt)) {
|
|
1889
|
+
if (ver === sv && tag !== 'latest' && _PRERELEASE_TAG_RE.test(tag)) return true;
|
|
1890
|
+
}
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1877
1894
|
function applyReputationFactor(result, metadata) {
|
|
1878
1895
|
if (!result || !result.summary || !metadata) return null;
|
|
1879
1896
|
// FPR plan : the reputation factor describes "how trustworthy this package
|
|
@@ -1884,12 +1901,21 @@ function applyReputationFactor(result, metadata) {
|
|
|
1884
1901
|
// scan version is unknown (CLI scanning a directory without version field),
|
|
1885
1902
|
// we fail open : skip the factor entirely rather than apply a multiplier
|
|
1886
1903
|
// we cannot situate in time.
|
|
1904
|
+
// P4 (pre-release reputation): a canary/nightly/rc of an established package is a NEW
|
|
1905
|
+
// (not historical) version on a maintainer-controlled pre-release dist-tag, so it should
|
|
1906
|
+
// inherit the package's reputation — but only PARTIALLY (it is not the verified latest).
|
|
1907
|
+
// Any OTHER non-latest version (historical / pinned-old / vendored) keeps the full skip.
|
|
1908
|
+
let _prereleaseAttenuation = 1.0;
|
|
1887
1909
|
if (
|
|
1888
1910
|
typeof metadata.latest_version === 'string' &&
|
|
1889
1911
|
typeof metadata.scan_version === 'string' &&
|
|
1890
1912
|
metadata.latest_version !== metadata.scan_version
|
|
1891
1913
|
) {
|
|
1892
|
-
|
|
1914
|
+
if (_isPrereleaseChannelVersion(metadata)) {
|
|
1915
|
+
_prereleaseAttenuation = 0.85;
|
|
1916
|
+
} else {
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1893
1919
|
}
|
|
1894
1920
|
if (
|
|
1895
1921
|
typeof metadata.latest_version === 'string' &&
|
|
@@ -1900,9 +1926,14 @@ function applyReputationFactor(result, metadata) {
|
|
|
1900
1926
|
// P3 hardening: a valid attestation must NOT earn a trust bonus on a package that
|
|
1901
1927
|
// also shows malice (TeamPCP attested-malware scenario). Withhold it here, where
|
|
1902
1928
|
// the threat list is available.
|
|
1903
|
-
|
|
1929
|
+
let factor = _factorFromMetadata(metadata, {
|
|
1904
1930
|
allowProvenanceBonus: !_hasMaliceSignal(result.threats)
|
|
1905
1931
|
});
|
|
1932
|
+
// P4: blend the factor toward neutral (1.0) for pre-release channel versions — they
|
|
1933
|
+
// get most of the reputation suppression but not 100% (they are not the verified latest).
|
|
1934
|
+
if (_prereleaseAttenuation !== 1.0) {
|
|
1935
|
+
factor = 1.0 + (factor - 1.0) * _prereleaseAttenuation;
|
|
1936
|
+
}
|
|
1906
1937
|
if (factor === 1.0) {
|
|
1907
1938
|
result.summary.reputationFactor = 1.0;
|
|
1908
1939
|
return null;
|