muaddib-scanner 2.10.41 → 2.10.42

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.41",
3
+ "version": "2.10.42",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -12,6 +12,8 @@ const { processQueue, SCAN_CONCURRENCY } = require('./queue.js');
12
12
  const { startHealthcheck } = require('./healthcheck.js');
13
13
 
14
14
  const POLL_INTERVAL = 60_000;
15
+ const PROCESS_LOOP_INTERVAL = 2_000; // Queue check interval when empty
16
+ const QUEUE_WARNING_THRESHOLD = 5_000; // Warn if queue depth exceeds this
15
17
 
16
18
  function sleep(ms) {
17
19
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -171,15 +173,21 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
171
173
  console.log('[MONITOR] npm changes stream enabled (replicate.npmjs.com) with RSS fallback');
172
174
  console.log(`[MONITOR] Scan concurrency: ${SCAN_CONCURRENCY} (MUADDIB_SCAN_CONCURRENCY to override)`);
173
175
  console.log(`[MONITOR] Sandbox concurrency: ${SANDBOX_CONCURRENCY_MAX} (MUADDIB_SANDBOX_CONCURRENCY to override)`);
174
- console.log(`[MONITOR] Polling every ${POLL_INTERVAL / 1000}s. Ctrl+C to stop.\n`);
176
+ console.log(`[MONITOR] Polling every ${POLL_INTERVAL / 1000}s (decoupled from processing). Ctrl+C to stop.\n`);
175
177
 
176
178
  let running = true;
179
+ let pollIntervalHandle = null; // Decoupled poll timer — set after initial poll
177
180
 
178
181
  // Graceful shutdown handler (shared by SIGINT and SIGTERM)
179
182
  // Daily report is NEVER sent on shutdown — it only fires at 08:00 Paris time.
180
183
  // Counters are persisted to disk so they survive the restart.
181
184
  async function gracefulShutdown(signal) {
182
185
  console.log(`\n[MONITOR] Received ${signal} — shutting down...`);
186
+ running = false;
187
+ if (pollIntervalHandle) {
188
+ clearInterval(pollIntervalHandle);
189
+ pollIntervalHandle = null;
190
+ }
183
191
  healthcheck.stop();
184
192
  // Flush all pending scope groups before exit
185
193
  for (const [scope, group] of pendingGrouped) {
@@ -191,25 +199,47 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
191
199
  saveState(state, stats);
192
200
  reportStats(stats);
193
201
  console.log('[MONITOR] State saved. Bye!');
194
- running = false;
195
202
  process.exit(0);
196
203
  }
197
204
 
198
205
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
199
206
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
200
207
 
201
- // Initial poll + scan
208
+ // Initial poll + scan (sequential for first run)
202
209
  await poll(state, scanQueue, stats);
203
210
  saveState(state, stats);
204
211
  await processQueue(scanQueue, stats, dailyAlerts, recentlyScanned, downloadsCache, sandboxAvailableRef.value);
205
212
 
206
- // Interval polling
213
+ // ─── Decoupled polling ───
214
+ // Poll runs on its own interval, independent of processing.
215
+ // This ensures new packages are ingested even while a large batch is being scanned.
216
+ // Without this, a 2h processing batch blocks all polling — packages published and
217
+ // removed during that window are never seen (e.g. axios/plain-crypto-js 2026-03-30).
218
+ let pollInProgress = false;
219
+ pollIntervalHandle = setInterval(async () => {
220
+ if (!running || pollInProgress) return;
221
+ pollInProgress = true;
222
+ try {
223
+ await poll(state, scanQueue, stats);
224
+ saveState(state, stats);
225
+ if (scanQueue.length > QUEUE_WARNING_THRESHOLD) {
226
+ console.log(`[MONITOR] WARNING: scan queue depth ${scanQueue.length} — processing may be lagging behind ingestion`);
227
+ }
228
+ } catch (err) {
229
+ console.error('[MONITOR] Poll error (interval):', err.message);
230
+ } finally {
231
+ pollInProgress = false;
232
+ }
233
+ }, POLL_INTERVAL);
234
+
235
+ // ─── Continuous processing loop ───
236
+ // Consumes scanQueue independently of polling. Workers inside processQueue
237
+ // check scanQueue.length > 0 after each item, so items added by a concurrent
238
+ // poll are picked up immediately by running workers.
207
239
  while (running) {
208
- await sleep(POLL_INTERVAL);
209
- if (!running) break;
210
- await poll(state, scanQueue, stats);
211
- saveState(state, stats);
212
- await processQueue(scanQueue, stats, dailyAlerts, recentlyScanned, downloadsCache, sandboxAvailableRef.value);
240
+ if (scanQueue.length > 0) {
241
+ await processQueue(scanQueue, stats, dailyAlerts, recentlyScanned, downloadsCache, sandboxAvailableRef.value);
242
+ }
213
243
 
214
244
  // Hourly stats report + cache purge
215
245
  if (Date.now() - stats.lastReportTime >= 3600_000) {
@@ -221,6 +251,9 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
221
251
  if (isDailyReportDue(stats)) {
222
252
  await sendDailyReport(stats, dailyAlerts, recentlyScanned, downloadsCache);
223
253
  }
254
+
255
+ // Short pause before re-checking queue — yields event loop for poll interval
256
+ await sleep(PROCESS_LOOP_INTERVAL);
224
257
  }
225
258
  }
226
259
 
@@ -231,5 +264,7 @@ module.exports = {
231
264
  reportStats,
232
265
  isDailyReportDue,
233
266
  sleep,
234
- POLL_INTERVAL
267
+ POLL_INTERVAL,
268
+ PROCESS_LOOP_INTERVAL,
269
+ QUEUE_WARNING_THRESHOLD
235
270
  };