muaddib-scanner 2.11.151 → 2.11.153

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.11.151",
3
+ "version": "2.11.153",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -44,7 +44,7 @@
44
44
  "url": "https://github.com/DNSZLSK/muad-dib/issues"
45
45
  },
46
46
  "engines": {
47
- "node": ">=18.0.0"
47
+ "node": ">=20.0.0"
48
48
  },
49
49
  "dependencies": {
50
50
  "@inquirer/prompts": "8.5.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-07-02T13:05:31.321Z",
3
+ "timestamp": "2026-07-03T09:17:36.812Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -18,6 +18,28 @@ const { analyzePreloadLog } = require('./analyzer.js');
18
18
  const { classifyDomain } = require('./network-allowlist.js');
19
19
  const { parseGvisorLogs, cleanupGvisorLogs } = require('./gvisor-parser.js');
20
20
 
21
+ // ── Event-loop stall attribution for the sandbox's SYNCHRONOUS docker CLI calls ──
22
+ // (2026-07-03) These execFileSync/execSync 'docker' calls run on the MAIN thread.
23
+ // When dockerd/runsc is slow or a gVisor container is wedged, they can block the
24
+ // daemon's memory-breaker loop (daemon.js) — the suspected source of the un-attributed
25
+ // (`op:null`) multi-minute loop-stalls that precede the cgroup OOM kills. Breadcrumb
26
+ // each one with the SAME beginOp/endOp mechanism as the extraction sites (queue.js) so
27
+ // the next stall NAMES the docker op instead of guessing. Pure observability: no change
28
+ // to args, timeouts, return values, or error paths. The require is GUARDED so the shared
29
+ // sandbox module stays usable from the CLI one-shot (and any context without the monitor);
30
+ // beginOp/endOp are cheap module-var writes that never throw and no-op when the lag
31
+ // sampler isn't running.
32
+ let _beginOp = () => null;
33
+ let _endOp = () => {};
34
+ try {
35
+ ({ beginOp: _beginOp, endOp: _endOp } = require('../monitor/event-loop-monitor.js'));
36
+ } catch { /* monitor module unavailable (e.g. CLI) — breadcrumbs stay no-op */ }
37
+
38
+ function dockerSync(label, meta, fn) {
39
+ const _crumb = _beginOp(`docker:${label}`, meta || null);
40
+ try { return fn(); } finally { _endOp(_crumb); }
41
+ }
42
+
21
43
  const DOCKER_IMAGE = 'muaddib-sandbox';
22
44
  const CONTAINER_TIMEOUT = 120000; // 120 seconds
23
45
  const SINGLE_RUN_TIMEOUT = 90000; // 90 seconds per run in multi-run mode (gVisor ~30% I/O overhead)
@@ -88,7 +110,7 @@ const _liveContainers = new Set();
88
110
  function killAllSandboxContainers() {
89
111
  const names = Array.from(_liveContainers);
90
112
  for (const name of names) {
91
- try { execFileSync('docker', ['rm', '-f', name], { stdio: 'pipe', timeout: 5000 }); } catch { /* already gone */ }
113
+ try { dockerSync('rm', { container: name }, () => execFileSync('docker', ['rm', '-f', name], { stdio: 'pipe', timeout: 5000 })); } catch { /* already gone */ }
92
114
  _liveContainers.delete(name);
93
115
  }
94
116
  if (names.length > 0) {
@@ -177,7 +199,7 @@ const EXFIL_PATTERNS = [
177
199
 
178
200
  function isDockerAvailable() {
179
201
  try {
180
- execSync('docker info', { stdio: 'pipe', timeout: 10000 });
202
+ dockerSync('info', null, () => execSync('docker info', { stdio: 'pipe', timeout: 10000 }));
181
203
  return true;
182
204
  } catch {
183
205
  return false;
@@ -186,7 +208,7 @@ function isDockerAvailable() {
186
208
 
187
209
  function imageExists() {
188
210
  try {
189
- execFileSync('docker', ['image', 'inspect', DOCKER_IMAGE], { stdio: 'pipe', timeout: 10000 });
211
+ dockerSync('image-inspect', null, () => execFileSync('docker', ['image', 'inspect', DOCKER_IMAGE], { stdio: 'pipe', timeout: 10000 }));
190
212
  return true;
191
213
  } catch {
192
214
  return false;
@@ -201,7 +223,7 @@ let _sandboxRuntimeWarned = false; // throttle the runtime warning/refusal log
201
223
  function isGvisorAvailable() {
202
224
  if (_gvisorAvailableCache !== undefined) return _gvisorAvailableCache;
203
225
  try {
204
- const info = execSync('docker info', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 });
226
+ const info = dockerSync('info', null, () => execSync('docker info', { encoding: 'utf8', stdio: 'pipe', timeout: 10000 }));
205
227
  _gvisorAvailableCache = /\brunsc\b/.test(info);
206
228
  } catch {
207
229
  _gvisorAvailableCache = false;
@@ -430,8 +452,8 @@ async function runSingleSandbox(packageName, options = {}) {
430
452
  // `docker kill` alone leaves the container record, and under gVisor the runsc
431
453
  // sandbox can survive it — rm -f reaps both. --rm makes this a no-op on clean exit.
432
454
  const killContainer = () => {
433
- try { execFileSync('docker', ['kill', containerName], { stdio: 'pipe', timeout: 5000 }); } catch { /* may be gone */ }
434
- try { execFileSync('docker', ['rm', '-f', containerName], { stdio: 'pipe', timeout: 5000 }); } catch { /* already removed */ }
455
+ try { dockerSync('kill', { container: containerName }, () => execFileSync('docker', ['kill', containerName], { stdio: 'pipe', timeout: 5000 })); } catch { /* may be gone */ }
456
+ try { dockerSync('rm', { container: containerName }, () => execFileSync('docker', ['rm', '-f', containerName], { stdio: 'pipe', timeout: 5000 })); } catch { /* already removed */ }
435
457
  };
436
458
 
437
459
  // Kill + arm a watchdog that force-resolves INCONCLUSIVE if the docker client
@@ -485,9 +507,9 @@ async function runSingleSandbox(packageName, options = {}) {
485
507
  // Capture container ID for gVisor log retrieval (once, while container is running)
486
508
  if (gvisorMode && !gvisorContainerId) {
487
509
  try {
488
- gvisorContainerId = execFileSync('docker', ['inspect', '--format={{.Id}}', containerName], {
510
+ gvisorContainerId = dockerSync('inspect', { container: containerName }, () => execFileSync('docker', ['inspect', '--format={{.Id}}', containerName], {
489
511
  encoding: 'utf8', stdio: 'pipe', timeout: 5000
490
- }).trim();
512
+ })).trim();
491
513
  } catch { /* container not yet ready, will retry on next data event */ }
492
514
  }
493
515