muaddib-scanner 2.10.82 → 2.10.85
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
|
@@ -37,11 +37,14 @@ const TIMEOUT_RATE_MIN_SAMPLES = 20;
|
|
|
37
37
|
let _prevScanned = 0;
|
|
38
38
|
let _prevTimeouts = 0;
|
|
39
39
|
|
|
40
|
-
// Throughput plateau detection: if we scaled up but throughput didn't increase
|
|
41
|
-
// we've hit I/O saturation
|
|
42
|
-
//
|
|
40
|
+
// Throughput plateau detection: if we scaled up but throughput didn't increase
|
|
41
|
+
// over MULTIPLE consecutive windows, we've hit I/O saturation.
|
|
42
|
+
// Requires 2 consecutive flat windows to trigger — a single 30s window has too
|
|
43
|
+
// much variance from sandbox timeouts (90-270s) to be reliable.
|
|
43
44
|
let _prevThroughput = 0;
|
|
44
45
|
let _lastScaleDirection = 0; // +1 = scaled up, -1 = scaled down, 0 = stable
|
|
46
|
+
let _plateauStreak = 0; // consecutive windows where throughput didn't improve after scale-up
|
|
47
|
+
const PLATEAU_STREAK_REQUIRED = 2; // must see flat throughput N times before triggering
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* Compute new target concurrency from system signals.
|
|
@@ -85,16 +88,24 @@ function computeTarget(current, queueDepth, stats) {
|
|
|
85
88
|
return { target, reason: `high_timeout_rate (${(timeoutRate * 100).toFixed(0)}%, ${timeoutDelta}/${scannedDelta})` };
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
// Priority 3: Throughput plateau — scaled up
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// Scale back instead of continuing to add workers.
|
|
91
|
+
// Priority 3: Throughput plateau — scaled up recently but throughput flat/down.
|
|
92
|
+
// Requires PLATEAU_STREAK_REQUIRED consecutive flat windows to trigger.
|
|
93
|
+
// A single bad window (sandbox timeout finishing in wrong 30s slot) is noise, not saturation.
|
|
92
94
|
if (_lastScaleDirection > 0 && _prevThroughput > 0 && scannedDelta > 0 && scannedDelta <= _prevThroughput) {
|
|
93
|
-
|
|
95
|
+
_plateauStreak++;
|
|
96
|
+
if (_plateauStreak >= PLATEAU_STREAK_REQUIRED) {
|
|
97
|
+
const prevTp = _prevThroughput;
|
|
98
|
+
_prevThroughput = scannedDelta;
|
|
99
|
+
_lastScaleDirection = -1;
|
|
100
|
+
_plateauStreak = 0;
|
|
101
|
+
return { target: clamp(current - 2), reason: `throughput_plateau (${prevTp}→${scannedDelta} scans/30s × ${PLATEAU_STREAK_REQUIRED} windows)` };
|
|
102
|
+
}
|
|
103
|
+
// Not enough consecutive flat windows yet — keep current level, don't scale up further
|
|
94
104
|
_prevThroughput = scannedDelta;
|
|
95
|
-
|
|
96
|
-
return { target: clamp(current - 2), reason: `throughput_plateau (${prevTp}→${scannedDelta} scans/30s, more workers didn't help)` };
|
|
105
|
+
return { target: current, reason: `plateau_warning (${_plateauStreak}/${PLATEAU_STREAK_REQUIRED}, ${scannedDelta} scans/30s)` };
|
|
97
106
|
}
|
|
107
|
+
// Throughput improved or no scale-up context — reset streak
|
|
108
|
+
_plateauStreak = 0;
|
|
98
109
|
|
|
99
110
|
// Priority 4: Queue depth — scale up for backlog, down toward base when idle
|
|
100
111
|
if (queueDepth > QUEUE_BACKLOG_THRESHOLD) {
|
|
@@ -128,6 +139,7 @@ function resetDeltas() {
|
|
|
128
139
|
_prevTimeouts = 0;
|
|
129
140
|
_prevThroughput = 0;
|
|
130
141
|
_lastScaleDirection = 0;
|
|
142
|
+
_plateauStreak = 0;
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
module.exports = {
|
package/src/monitor/ingestion.js
CHANGED
|
@@ -442,6 +442,10 @@ async function pollNpmChanges(state, scanQueue, stats) {
|
|
|
442
442
|
// Layer 3: Evaluate if this package should be cached
|
|
443
443
|
const cacheTrigger = evaluateCacheTrigger(name, docMeta, change.doc || null);
|
|
444
444
|
|
|
445
|
+
// Layer 2: Extract tarball URL from CouchDB doc (eliminates lazy resolution 404 race)
|
|
446
|
+
// NOTE: fastTrack flag is computed in resolveTarballAndScan() AFTER metadata
|
|
447
|
+
// resolution via getNpmLatestTarball(). It cannot be computed here because
|
|
448
|
+
// post-May 2025, include_docs is deprecated and change.doc is always null.
|
|
445
449
|
scanQueue.push({
|
|
446
450
|
name,
|
|
447
451
|
version: docMeta ? docMeta.version : '',
|
|
@@ -643,7 +647,7 @@ async function pollPyPI(state, scanQueue) {
|
|
|
643
647
|
* @param {Array} scanQueue - Mutable scan queue array
|
|
644
648
|
* @param {Object} stats - Mutable stats object
|
|
645
649
|
*/
|
|
646
|
-
const SOFT_BACKPRESSURE_THRESHOLD =
|
|
650
|
+
const SOFT_BACKPRESSURE_THRESHOLD = 30_000;
|
|
647
651
|
|
|
648
652
|
async function poll(state, scanQueue, stats) {
|
|
649
653
|
// Soft backpressure: skip poll when queue is very deep.
|
package/src/monitor/queue.js
CHANGED
|
@@ -336,7 +336,7 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
|
|
|
336
336
|
let alreadyExtracted = false;
|
|
337
337
|
let extractedDir = null;
|
|
338
338
|
|
|
339
|
-
if (unpackedSize > LARGE_PACKAGE_SIZE) {
|
|
339
|
+
if (unpackedSize > LARGE_PACKAGE_SIZE || meta.fastTrack) {
|
|
340
340
|
// Exception 1: IOC match — always full scan
|
|
341
341
|
let isKnownIOC = false;
|
|
342
342
|
try {
|
|
@@ -678,7 +678,10 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
|
|
|
678
678
|
stats.suspect++;
|
|
679
679
|
|
|
680
680
|
// Fire-and-forget tarball archiving — never blocks the pipeline
|
|
681
|
-
|
|
681
|
+
// Skip for fast-track packages (large boring enterprise packages — not worth archiving)
|
|
682
|
+
if (meta.fastTrack) {
|
|
683
|
+
console.log(`[MONITOR] FAST-TRACK SKIP: ${name}@${version} — skipping archive + LLM (static-only)`);
|
|
684
|
+
} else archiveSuspectTarball(name, version, tarballUrl, {
|
|
682
685
|
score: riskScore,
|
|
683
686
|
priority: tierLabel,
|
|
684
687
|
rulesTriggered: (result.threats || []).map(t => t.ruleId || t.type).filter(Boolean),
|
|
@@ -687,13 +690,35 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
|
|
|
687
690
|
console.warn(`[Archive] Failed for ${name}@${version}: ${err.message}`);
|
|
688
691
|
});
|
|
689
692
|
|
|
690
|
-
// Sandbox decision based on tier
|
|
693
|
+
// Sandbox decision based on tier + smart skip for large low-signal packages.
|
|
694
|
+
// Large packages (>15MB or >80 deps) with only MEDIUM/LOW findings timeout
|
|
695
|
+
// systematically (90s × 3 = INCONCLUSIVE = 0 detection). Skipping frees slots
|
|
696
|
+
// for real suspects. Guard-fous: any HIGH/CRITICAL, temporal anomaly, maintainer
|
|
697
|
+
// change, or dormant spike → sandbox runs regardless of size.
|
|
698
|
+
const SANDBOX_SIZE_SKIP_BYTES = 15 * 1024 * 1024; // 15MB
|
|
699
|
+
const SANDBOX_DEPS_SKIP = 80;
|
|
700
|
+
const isLargePackage = (meta.unpackedSize || 0) > SANDBOX_SIZE_SKIP_BYTES ||
|
|
701
|
+
(meta.dependencyCount || 0) > SANDBOX_DEPS_SKIP;
|
|
702
|
+
const hasHighOrCriticalFinding = (result.summary.critical || 0) > 0 || (result.summary.high || 0) > 0;
|
|
703
|
+
const hasTemporalSignal = (result.threats || []).some(t =>
|
|
704
|
+
t.type === 'postinstall_added' || t.type === 'preinstall_added' ||
|
|
705
|
+
t.type === 'install_added' || t.type === 'maintainer_change' ||
|
|
706
|
+
t.type === 'dormant_spike' || t.type === 'publish_anomaly'
|
|
707
|
+
);
|
|
708
|
+
const skipSandboxLargePackage = (isLargePackage || meta.fastTrack) && !hasHighOrCriticalFinding && !hasTemporalSignal;
|
|
709
|
+
|
|
710
|
+
if (skipSandboxLargePackage && meta.fastTrack) {
|
|
711
|
+
console.log(`[MONITOR] FAST-TRACK: ${name}@${version} — large package static-only (${((meta.unpackedSize || 0) / 1024 / 1024).toFixed(1)}MB, no lifecycle scripts)`);
|
|
712
|
+
} else if (skipSandboxLargePackage) {
|
|
713
|
+
console.log(`[MONITOR] SANDBOX SKIP (large low-signal): ${name}@${version} (${((meta.unpackedSize || 0) / 1024 / 1024).toFixed(1)}MB, deps=${meta.dependencyCount || '?'}, no HIGH/CRIT, no temporal)`);
|
|
714
|
+
}
|
|
715
|
+
|
|
691
716
|
// T1a: mandatory sandbox (HC malice types, TIER1_TYPES non-LOW, lifecycle + intent compound)
|
|
692
717
|
// T1b: conditional sandbox (HIGH/CRITICAL without HC type — bundler FP zone)
|
|
693
718
|
// → sandbox only if score >= 25 (significant risk) or queue pressure is low
|
|
694
719
|
// T2: sandbox if queue < 50 (as before)
|
|
695
720
|
let sandboxResult = null;
|
|
696
|
-
const shouldSandbox = isSandboxEnabled() && sandboxAvailable && (
|
|
721
|
+
const shouldSandbox = !skipSandboxLargePackage && isSandboxEnabled() && sandboxAvailable && (
|
|
697
722
|
tier === '1a' ||
|
|
698
723
|
(tier === '1b' && (riskScore >= 25 || scanQueue.length < 20)) ||
|
|
699
724
|
(tier === 2 && scanQueue.length < 50)
|
|
@@ -845,8 +870,9 @@ async function scanPackage(name, version, ecosystem, tarballUrl, registryMeta, s
|
|
|
845
870
|
// Record daily alert with post-reputation score for top suspects ranking
|
|
846
871
|
dailyAlerts.push({ name, version, ecosystem, findingsCount: result.summary.total, score: adjustedResult.summary.riskScore || 0, tier });
|
|
847
872
|
// LLM Detective: AI-powered analysis for T1a/T1b suspects
|
|
873
|
+
// Skip for fast-track (large boring packages — LLM analysis adds 10-30s for no value)
|
|
848
874
|
let llmResult = null;
|
|
849
|
-
if ((tier === '1a' || tier === '1b') && (adjustedResult.summary.riskScore || 0) >= 25) {
|
|
875
|
+
if (!meta.fastTrack && (tier === '1a' || tier === '1b') && (adjustedResult.summary.riskScore || 0) >= 25) {
|
|
850
876
|
try {
|
|
851
877
|
const { investigatePackage, isLlmEnabled, getLlmMode } = require('../ml/llm-detective.js');
|
|
852
878
|
if (isLlmEnabled()) {
|
|
@@ -1057,6 +1083,19 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
|
|
|
1057
1083
|
if (npmInfo.version) item.version = npmInfo.version;
|
|
1058
1084
|
if (npmInfo.unpackedSize) item.unpackedSize = npmInfo.unpackedSize;
|
|
1059
1085
|
if (npmInfo.scripts) item.registryScripts = npmInfo.scripts;
|
|
1086
|
+
|
|
1087
|
+
// Fast-track decision: large packages (>15MB) with no lifecycle scripts and no IOC match.
|
|
1088
|
+
// Computed HERE (after metadata resolution), not at ingestion time — post-May 2025
|
|
1089
|
+
// CouchDB changes feed has no docs, so metadata is only available after lazy fetch.
|
|
1090
|
+
// Fast-track packages get: quick static scan (package.json + shell only), no AST,
|
|
1091
|
+
// no sandbox, no LLM, no archiving. Exits in ~2-3s instead of 30-300s.
|
|
1092
|
+
const FAST_TRACK_SIZE_BYTES = 15 * 1024 * 1024;
|
|
1093
|
+
if (!item.isIOCMatch && (item.unpackedSize || 0) > FAST_TRACK_SIZE_BYTES) {
|
|
1094
|
+
const scripts = item.registryScripts || {};
|
|
1095
|
+
if (!scripts.preinstall && !scripts.postinstall && !scripts.install) {
|
|
1096
|
+
item.fastTrack = true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1060
1099
|
} catch (err) {
|
|
1061
1100
|
console.error(`[MONITOR] ERROR resolving npm tarball for ${item.name}: ${err.message}`);
|
|
1062
1101
|
recordError(err, stats);
|
|
@@ -1114,9 +1153,11 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
|
|
|
1114
1153
|
let publishResult = null;
|
|
1115
1154
|
let maintainerResult = null;
|
|
1116
1155
|
|
|
1117
|
-
if (item.ecosystem === 'npm') {
|
|
1156
|
+
if (item.ecosystem === 'npm' && !item.fastTrack) {
|
|
1118
1157
|
// Run all 4 temporal checks in parallel — each is independent.
|
|
1119
1158
|
// With metadata cache (temporal-analysis.js), the 4 modules share 1 HTTP request.
|
|
1159
|
+
// Skipped for fast-track packages (large boring packages — temporal checks make
|
|
1160
|
+
// 4 HTTP requests to npm registry per package, pointless for 50MB enterprise packages).
|
|
1120
1161
|
const [tempRes, astRes, pubRes, maintRes] = await Promise.allSettled([
|
|
1121
1162
|
runTemporalCheck(item.name, dailyAlerts),
|
|
1122
1163
|
runTemporalAstCheck(item.name, dailyAlerts),
|
|
@@ -1135,7 +1176,8 @@ async function resolveTarballAndScan(item, stats, dailyAlerts, recentlyScanned,
|
|
|
1135
1176
|
const scanResult = await scanPackage(item.name, item.version, item.ecosystem, item.tarballUrl, {
|
|
1136
1177
|
unpackedSize: item.unpackedSize || 0,
|
|
1137
1178
|
registryScripts: item.registryScripts || null,
|
|
1138
|
-
_cacheTrigger: item._cacheTrigger || null
|
|
1179
|
+
_cacheTrigger: item._cacheTrigger || null,
|
|
1180
|
+
fastTrack: item.fastTrack || false
|
|
1139
1181
|
}, stats, dailyAlerts, recentlyScanned, downloadsCache, scanQueue, sandboxAvailable);
|
|
1140
1182
|
const sandboxResult = scanResult && scanResult.sandboxResult;
|
|
1141
1183
|
const staticClean = scanResult && scanResult.staticClean;
|
package/src/sandbox/index.js
CHANGED
|
@@ -224,7 +224,12 @@ async function runSingleSandbox(packageName, options = {}) {
|
|
|
224
224
|
'--cap-drop=ALL'
|
|
225
225
|
];
|
|
226
226
|
|
|
227
|
-
// gVisor runtime: use runsc instead of default runc
|
|
227
|
+
// gVisor runtime: use runsc instead of default runc.
|
|
228
|
+
// Performance: configure --directfs and --overlay2=all:memory in daemon.json:
|
|
229
|
+
// "runsc": { "path": "/usr/bin/runsc", "runtimeArgs": ["--directfs", "--overlay2=all:memory"] }
|
|
230
|
+
// --directfs: bypass gofer process for direct filesystem access (fewer RPCs, faster I/O)
|
|
231
|
+
// --overlay2=all:memory: sandbox writes go to tmpfs instead of host (faster, isolated)
|
|
232
|
+
// These flags require gVisor >= 2023-06-01.
|
|
228
233
|
if (gvisorMode) {
|
|
229
234
|
dockerArgs.push('--runtime=runsc');
|
|
230
235
|
dockerArgs.push('-e', 'MUADDIB_GVISOR=1');
|