muaddib-scanner 2.10.47 → 2.10.48
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 +1 -1
- package/src/monitor/deferred-sandbox.js +21 -26
- package/src/monitor/state.js +2 -0
- package/src/sandbox/index.js +14 -8
- package/ci-test.sarif +0 -3796
- package/muaddib-results.sarif +0 -20356
package/package.json
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
* Items are sorted by riskScore DESC (highest-risk first) to defend
|
|
7
7
|
* against queue-poisoning attacks.
|
|
8
8
|
*
|
|
9
|
-
* The worker
|
|
9
|
+
* The worker owns a dedicated sandbox slot (_deferredSlotBusy) that is
|
|
10
|
+
* completely independent from the shared semaphore used by T1a/T1b/T2.
|
|
11
|
+
* This guarantees the deferred worker can always process, regardless of
|
|
12
|
+
* how many main-path sandboxes are running. The VPS supports N+1
|
|
13
|
+
* concurrent gVisor containers (3 main + 1 deferred).
|
|
10
14
|
*/
|
|
11
15
|
const fs = require('fs');
|
|
12
16
|
const path = require('path');
|
|
13
|
-
const { runSandbox
|
|
17
|
+
const { runSandbox } = require('../sandbox/index.js');
|
|
14
18
|
const { isCanaryEnabled } = require('./classify.js');
|
|
15
19
|
const { getWebhookUrl, alertedPackageRules, persistAlert, buildAlertData } = require('./webhook.js');
|
|
16
20
|
const { sendWebhook } = require('../webhook.js');
|
|
@@ -22,15 +26,13 @@ const DEFERRED_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
|
22
26
|
const DEFERRED_MAX_RETRIES = 2;
|
|
23
27
|
const DEFERRED_WORKER_INTERVAL_MS = 30_000; // 30s
|
|
24
28
|
const DEFERRED_STATE_FILE = path.join(__dirname, '..', '..', 'data', 'deferred-queue.json');
|
|
25
|
-
const SKIP_LOG_INTERVAL = 10; // Log every N skipped ticks (throttle)
|
|
26
|
-
const ANTI_STARVATION_TICKS = 20; // Force processing after N consecutive skips (~10min)
|
|
27
29
|
|
|
28
30
|
// ── Mutable state ──
|
|
29
31
|
const _deferredQueue = [];
|
|
30
32
|
const _deferredSeen = new Set(); // name@version dedup
|
|
31
33
|
let _workerHandle = null;
|
|
32
34
|
let _stats = null; // reference to shared stats object
|
|
33
|
-
let
|
|
35
|
+
let _deferredSlotBusy = false; // Dedicated slot: true while deferred sandbox is running
|
|
34
36
|
|
|
35
37
|
// ── Queue management ──
|
|
36
38
|
|
|
@@ -129,25 +131,12 @@ async function processDeferredItem(stats) {
|
|
|
129
131
|
|
|
130
132
|
if (_deferredQueue.length === 0) return null;
|
|
131
133
|
|
|
132
|
-
// 2.
|
|
133
|
-
|
|
134
|
-
const slotsFullForDeferred = sem.active >= SANDBOX_CONCURRENCY_MAX - 1;
|
|
135
|
-
const forceAntiStarvation = _consecutiveSkips >= ANTI_STARVATION_TICKS && sem.active < SANDBOX_CONCURRENCY_MAX;
|
|
136
|
-
|
|
137
|
-
if (slotsFullForDeferred && !forceAntiStarvation) {
|
|
138
|
-
_consecutiveSkips++;
|
|
134
|
+
// 2. Dedicated slot check — completely independent from main semaphore
|
|
135
|
+
if (_deferredSlotBusy) {
|
|
139
136
|
if (stats) stats.deferredSkipped = (stats.deferredSkipped || 0) + 1;
|
|
140
|
-
if (_consecutiveSkips % SKIP_LOG_INTERVAL === 0) {
|
|
141
|
-
console.log(`[DEFERRED] YIELD: ${_consecutiveSkips} consecutive skips (slots=${sem.active}/${SANDBOX_CONCURRENCY_MAX}, pending=${_deferredQueue.length})`);
|
|
142
|
-
}
|
|
143
137
|
return null;
|
|
144
138
|
}
|
|
145
139
|
|
|
146
|
-
if (forceAntiStarvation) {
|
|
147
|
-
console.log(`[DEFERRED] ANTI-STARVATION: forcing processing after ${_consecutiveSkips} skips (slots=${sem.active}/${SANDBOX_CONCURRENCY_MAX}, pending=${_deferredQueue.length})`);
|
|
148
|
-
}
|
|
149
|
-
_consecutiveSkips = 0;
|
|
150
|
-
|
|
151
140
|
// 3. Pick highest-score item
|
|
152
141
|
const item = _deferredQueue.shift();
|
|
153
142
|
const key = `${item.name}@${item.version}`;
|
|
@@ -155,11 +144,12 @@ async function processDeferredItem(stats) {
|
|
|
155
144
|
|
|
156
145
|
console.log(`[DEFERRED] PROCESSING: ${key} (tier=${item.tier === 2 ? 'T2' : 'T1b'}, score=${item.riskScore}, retries=${item.retries})`);
|
|
157
146
|
|
|
158
|
-
// 4. Run sandbox
|
|
147
|
+
// 4. Run sandbox on dedicated slot (bypasses shared semaphore)
|
|
148
|
+
_deferredSlotBusy = true;
|
|
159
149
|
let sandboxResult;
|
|
160
150
|
try {
|
|
161
151
|
const canary = isCanaryEnabled();
|
|
162
|
-
sandboxResult = await runSandbox(item.name, { canary });
|
|
152
|
+
sandboxResult = await runSandbox(item.name, { canary, skipSemaphore: true });
|
|
163
153
|
console.log(`[DEFERRED] SANDBOX COMPLETE: ${key} -> score=${sandboxResult.score}, severity=${sandboxResult.severity}`);
|
|
164
154
|
} catch (err) {
|
|
165
155
|
console.error(`[DEFERRED] SANDBOX ERROR: ${key} — ${err.message}`);
|
|
@@ -174,6 +164,8 @@ async function processDeferredItem(stats) {
|
|
|
174
164
|
console.log(`[DEFERRED] RE-ENQUEUED: ${key} for retry (attempt ${item.retries + 1}/${DEFERRED_MAX_RETRIES})`);
|
|
175
165
|
}
|
|
176
166
|
return null;
|
|
167
|
+
} finally {
|
|
168
|
+
_deferredSlotBusy = false;
|
|
177
169
|
}
|
|
178
170
|
|
|
179
171
|
// 5. Follow-up webhook if sandbox found something
|
|
@@ -391,10 +383,14 @@ function _resetDeferredQueue() {
|
|
|
391
383
|
_deferredQueue.length = 0;
|
|
392
384
|
_deferredSeen.clear();
|
|
393
385
|
_stats = null;
|
|
394
|
-
|
|
386
|
+
_deferredSlotBusy = false;
|
|
395
387
|
stopDeferredWorker();
|
|
396
388
|
}
|
|
397
389
|
|
|
390
|
+
function isDeferredSlotBusy() {
|
|
391
|
+
return _deferredSlotBusy;
|
|
392
|
+
}
|
|
393
|
+
|
|
398
394
|
module.exports = {
|
|
399
395
|
enqueueDeferred,
|
|
400
396
|
getDeferredQueue,
|
|
@@ -406,12 +402,11 @@ module.exports = {
|
|
|
406
402
|
restoreDeferredQueue,
|
|
407
403
|
buildDeferredFollowUpEmbed,
|
|
408
404
|
pruneExpired,
|
|
405
|
+
isDeferredSlotBusy,
|
|
409
406
|
_resetDeferredQueue,
|
|
410
407
|
DEFERRED_QUEUE_MAX,
|
|
411
408
|
DEFERRED_TTL_MS,
|
|
412
409
|
DEFERRED_MAX_RETRIES,
|
|
413
410
|
DEFERRED_WORKER_INTERVAL_MS,
|
|
414
|
-
DEFERRED_STATE_FILE
|
|
415
|
-
SKIP_LOG_INTERVAL,
|
|
416
|
-
ANTI_STARVATION_TICKS
|
|
411
|
+
DEFERRED_STATE_FILE
|
|
417
412
|
};
|
package/src/monitor/state.js
CHANGED
|
@@ -690,6 +690,7 @@ function loadDailyStats(stats, dailyAlerts) {
|
|
|
690
690
|
stats.mlFiltered = data.mlFiltered || 0;
|
|
691
691
|
stats.llmAnalyzed = data.llmAnalyzed || 0;
|
|
692
692
|
stats.llmSuppressed = data.llmSuppressed || 0;
|
|
693
|
+
stats.changesStreamPackages = data.changesStreamPackages || 0;
|
|
693
694
|
if (Array.isArray(data.dailyAlerts)) {
|
|
694
695
|
dailyAlerts.length = 0;
|
|
695
696
|
dailyAlerts.push(...data.dailyAlerts);
|
|
@@ -716,6 +717,7 @@ function saveDailyStats(stats, dailyAlerts) {
|
|
|
716
717
|
mlFiltered: stats.mlFiltered,
|
|
717
718
|
llmAnalyzed: stats.llmAnalyzed || 0,
|
|
718
719
|
llmSuppressed: stats.llmSuppressed || 0,
|
|
720
|
+
changesStreamPackages: stats.changesStreamPackages || 0,
|
|
719
721
|
dailyAlerts: dailyAlerts.slice()
|
|
720
722
|
};
|
|
721
723
|
atomicWriteFileSync(DAILY_STATS_FILE, JSON.stringify(data, null, 2));
|
package/src/sandbox/index.js
CHANGED
|
@@ -23,7 +23,8 @@ const CONTAINER_TIMEOUT = 120000; // 120 seconds
|
|
|
23
23
|
const SINGLE_RUN_TIMEOUT = 90000; // 90 seconds per run in multi-run mode (gVisor ~30% I/O overhead)
|
|
24
24
|
|
|
25
25
|
// ── Sandbox concurrency limiter ──
|
|
26
|
-
// Prevents Docker container saturation under load (
|
|
26
|
+
// Prevents Docker container saturation under load (main-path T1a/T1b/T2).
|
|
27
|
+
// The deferred worker manages its own dedicated slot outside this semaphore.
|
|
27
28
|
// Pattern: same semaphore as src/shared/http-limiter.js.
|
|
28
29
|
const SANDBOX_CONCURRENCY_MAX = Math.max(1, parseInt(process.env.MUADDIB_SANDBOX_CONCURRENCY, 10) || 3);
|
|
29
30
|
|
|
@@ -565,16 +566,21 @@ async function runSandbox(packageName, options = {}) {
|
|
|
565
566
|
|
|
566
567
|
const mode = strict ? 'strict' : 'permissive';
|
|
567
568
|
|
|
568
|
-
// Acquire sandbox slot — blocks if SANDBOX_CONCURRENCY_MAX containers already running
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
569
|
+
// Acquire sandbox slot — blocks if SANDBOX_CONCURRENCY_MAX containers already running.
|
|
570
|
+
// skipSemaphore: deferred worker manages its own dedicated slot outside this semaphore.
|
|
571
|
+
const skipSem = options.skipSemaphore === true;
|
|
572
|
+
if (!skipSem) {
|
|
573
|
+
const queueLen = _sandboxSemaphore.queue.length;
|
|
574
|
+
if (queueLen > 0) {
|
|
575
|
+
console.log(`[SANDBOX] Waiting for sandbox slot (${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX} active, ${queueLen} queued)...`);
|
|
576
|
+
}
|
|
577
|
+
await acquireSandboxSlot();
|
|
572
578
|
}
|
|
573
|
-
await acquireSandboxSlot();
|
|
574
579
|
|
|
575
580
|
try {
|
|
576
581
|
const runtimeLabel = useGvisor ? 'gvisor' : 'docker';
|
|
577
|
-
|
|
582
|
+
const slotInfo = skipSem ? 'deferred-slot' : `${_sandboxSemaphore.active}/${SANDBOX_CONCURRENCY_MAX}`;
|
|
583
|
+
console.log(`[SANDBOX] Analyzing "${displayName}" in isolated container (mode: ${mode}, runtime: ${runtimeLabel}${canaryEnabled ? ', canary: on' : ''}${local ? ', local' : ''}, runs: ${TIME_OFFSETS.length}, slots: ${slotInfo})...`);
|
|
578
584
|
|
|
579
585
|
const allRuns = [];
|
|
580
586
|
let bestResult = cleanResult;
|
|
@@ -639,7 +645,7 @@ async function runSandbox(packageName, options = {}) {
|
|
|
639
645
|
displayResults(bestResult);
|
|
640
646
|
return bestResult;
|
|
641
647
|
} finally {
|
|
642
|
-
releaseSandboxSlot();
|
|
648
|
+
if (!skipSem) releaseSandboxSlot();
|
|
643
649
|
}
|
|
644
650
|
}
|
|
645
651
|
|