muaddib-scanner 2.11.161 → 2.11.163

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.161",
3
+ "version": "2.11.163",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "node_modules",
3
- "timestamp": "2026-07-09T09:23:51.701Z",
3
+ "timestamp": "2026-07-10T08:26:38.815Z",
4
4
  "threats": [
5
5
  {
6
6
  "type": "string_mutation_obfuscation",
@@ -27,6 +27,18 @@ const path = require('path');
27
27
  const { isMainThread, threadId } = require('worker_threads');
28
28
  const { sanitizePackageName } = require('../shared/download.js');
29
29
 
30
+ // Event-loop stall attribution (event-loop-monitor.js): the periodic JSONL
31
+ // compactions below are O(file-size) SYNCHRONOUS rewrites that run on the monitor
32
+ // main thread. At the 500K-entry scan-ledger steady state this is the leading
33
+ // suspect for the multi-minute loop stalls that blind the RSS breaker — yet it was
34
+ // uninstrumented, so a stall in it mis-attributed to a stale prior breadcrumb
35
+ // (docker:rm with durationMs << lagMs). Wrapping each compaction NAMES it: a stall
36
+ // now logs op.label='compact:*' with durationMs ~ lagMs. Guarded require so state.js
37
+ // stays usable from the CLI / scan-worker where the sampler isn't running
38
+ // (runInstrumented is then a transparent passthrough). Never throws.
39
+ let _runInstrumented = (label, meta, fn) => fn();
40
+ try { ({ runInstrumented: _runInstrumented } = require('./event-loop-monitor.js')); } catch { /* sampler unavailable — passthrough */ }
41
+
30
42
  // --- File path constants ---
31
43
 
32
44
  const STATE_FILE = path.join(__dirname, '..', '..', 'data', 'monitor-state.json');
@@ -761,23 +773,26 @@ function loadTemporalDetections() {
761
773
  * runStateMigrations to enforce caps after migration.
762
774
  */
763
775
  function _compactTemporalDetectionsJsonl() {
764
- try {
765
- const total = _countJsonlLines(TEMPORAL_DETECTIONS_FILE);
766
- if (total <= MAX_TEMPORAL_DETECTIONS) return;
767
- const toDrop = total - MAX_TEMPORAL_DETECTIONS;
768
- let skipped = 0;
769
- const kept = [];
770
- _iterateJsonlSync(TEMPORAL_DETECTIONS_FILE, (entry) => {
771
- if (skipped < toDrop) { skipped++; return; }
772
- kept.push(JSON.stringify(entry));
773
- });
774
- const tmpFile = TEMPORAL_DETECTIONS_FILE + '.tmp';
775
- fs.writeFileSync(tmpFile, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
776
- fs.renameSync(tmpFile, TEMPORAL_DETECTIONS_FILE);
777
- console.log(`[MONITOR] COMPACT temporal-detections: ${total} -> ${kept.length} entries`);
778
- } catch (err) {
779
- console.error(`[MONITOR] Temporal detections compaction failed: ${err.message}`);
780
- }
776
+ // Same O(file) synchronous rewrite shape as the scan-ledger/detections compactions.
777
+ _runInstrumented('compact:temporal-detections', { cap: MAX_TEMPORAL_DETECTIONS }, () => {
778
+ try {
779
+ const total = _countJsonlLines(TEMPORAL_DETECTIONS_FILE);
780
+ if (total <= MAX_TEMPORAL_DETECTIONS) return;
781
+ const toDrop = total - MAX_TEMPORAL_DETECTIONS;
782
+ let skipped = 0;
783
+ const kept = [];
784
+ _iterateJsonlSync(TEMPORAL_DETECTIONS_FILE, (entry) => {
785
+ if (skipped < toDrop) { skipped++; return; }
786
+ kept.push(JSON.stringify(entry));
787
+ });
788
+ const tmpFile = TEMPORAL_DETECTIONS_FILE + '.tmp';
789
+ fs.writeFileSync(tmpFile, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
790
+ fs.renameSync(tmpFile, TEMPORAL_DETECTIONS_FILE);
791
+ console.log(`[MONITOR] COMPACT temporal-detections: ${total} -> ${kept.length} entries`);
792
+ } catch (err) {
793
+ console.error(`[MONITOR] Temporal detections compaction failed: ${err.message}`);
794
+ }
795
+ });
781
796
  }
782
797
 
783
798
  // --- State persistence ---
@@ -996,28 +1011,32 @@ function getDetectionStats() {
996
1011
  * stays consistent. No-op when the file is already under cap.
997
1012
  */
998
1013
  function _compactDetectionsJsonl() {
999
- try {
1000
- const total = _countJsonlLines(DETECTIONS_FILE);
1001
- if (total <= MAX_DETECTIONS) return;
1002
- const toDrop = total - MAX_DETECTIONS;
1003
- let skipped = 0;
1004
- const kept = [];
1005
- const newDedup = new Set();
1006
- _iterateJsonlSync(DETECTIONS_FILE, (entry) => {
1007
- if (skipped < toDrop) { skipped++; return; }
1008
- kept.push(JSON.stringify(entry));
1009
- if (entry && entry.package && entry.version) {
1010
- newDedup.add(`${entry.package}@${entry.version}`);
1011
- }
1012
- });
1013
- const tmpFile = DETECTIONS_FILE + '.tmp';
1014
- fs.writeFileSync(tmpFile, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
1015
- fs.renameSync(tmpFile, DETECTIONS_FILE);
1016
- _detectionDedupSet = newDedup;
1017
- console.log(`[MONITOR] COMPACT detections: ${total} -> ${kept.length} entries`);
1018
- } catch (err) {
1019
- console.error(`[MONITOR] Detections compaction failed: ${err.message}`);
1020
- }
1014
+ // Same O(file) synchronous rewrite as the scan-ledger (smaller cap, but same
1015
+ // main-thread blocking shape). Named so a stall here can't hide behind a stale crumb.
1016
+ _runInstrumented('compact:detections', { cap: MAX_DETECTIONS }, () => {
1017
+ try {
1018
+ const total = _countJsonlLines(DETECTIONS_FILE);
1019
+ if (total <= MAX_DETECTIONS) return;
1020
+ const toDrop = total - MAX_DETECTIONS;
1021
+ let skipped = 0;
1022
+ const kept = [];
1023
+ const newDedup = new Set();
1024
+ _iterateJsonlSync(DETECTIONS_FILE, (entry) => {
1025
+ if (skipped < toDrop) { skipped++; return; }
1026
+ kept.push(JSON.stringify(entry));
1027
+ if (entry && entry.package && entry.version) {
1028
+ newDedup.add(`${entry.package}@${entry.version}`);
1029
+ }
1030
+ });
1031
+ const tmpFile = DETECTIONS_FILE + '.tmp';
1032
+ fs.writeFileSync(tmpFile, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
1033
+ fs.renameSync(tmpFile, DETECTIONS_FILE);
1034
+ _detectionDedupSet = newDedup;
1035
+ console.log(`[MONITOR] COMPACT detections: ${total} -> ${kept.length} entries`);
1036
+ } catch (err) {
1037
+ console.error(`[MONITOR] Detections compaction failed: ${err.message}`);
1038
+ }
1039
+ });
1021
1040
  }
1022
1041
 
1023
1042
  // --- Per-scan ledger (Phase 0a: operational coverage observability) ---
@@ -1122,23 +1141,28 @@ function appendScanLedger(e) {
1122
1141
  * No-op when already under cap. Streams (never loads the whole file at once).
1123
1142
  */
1124
1143
  function _compactScanLedgerJsonl() {
1125
- try {
1126
- const total = _countJsonlLines(SCAN_LEDGER_FILE);
1127
- if (total <= MAX_SCAN_LEDGER) return;
1128
- const toDrop = total - MAX_SCAN_LEDGER;
1129
- let skipped = 0;
1130
- const kept = [];
1131
- _iterateJsonlSync(SCAN_LEDGER_FILE, (entry) => {
1132
- if (skipped < toDrop) { skipped++; return; }
1133
- kept.push(JSON.stringify(entry));
1134
- });
1135
- const tmpFile = SCAN_LEDGER_FILE + '.tmp';
1136
- fs.writeFileSync(tmpFile, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
1137
- fs.renameSync(tmpFile, SCAN_LEDGER_FILE);
1138
- console.log(`[MONITOR] COMPACT scan-ledger: ${total} -> ${kept.length} entries`);
1139
- } catch (err) {
1140
- console.error(`[MONITOR] Scan-ledger compaction failed: ${err.message}`);
1141
- }
1144
+ // Leading loop-stall suspect (see require note above): count + parse + re-stringify
1145
+ // + join + 127MB write + rename, all synchronous over ~500K entries. Instrument the
1146
+ // whole body (the count pass can block too) so a stall NAMES 'compact:scan-ledger'.
1147
+ _runInstrumented('compact:scan-ledger', { cap: MAX_SCAN_LEDGER }, () => {
1148
+ try {
1149
+ const total = _countJsonlLines(SCAN_LEDGER_FILE);
1150
+ if (total <= MAX_SCAN_LEDGER) return;
1151
+ const toDrop = total - MAX_SCAN_LEDGER;
1152
+ let skipped = 0;
1153
+ const kept = [];
1154
+ _iterateJsonlSync(SCAN_LEDGER_FILE, (entry) => {
1155
+ if (skipped < toDrop) { skipped++; return; }
1156
+ kept.push(JSON.stringify(entry));
1157
+ });
1158
+ const tmpFile = SCAN_LEDGER_FILE + '.tmp';
1159
+ fs.writeFileSync(tmpFile, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
1160
+ fs.renameSync(tmpFile, SCAN_LEDGER_FILE);
1161
+ console.log(`[MONITOR] COMPACT scan-ledger: ${total} -> ${kept.length} entries`);
1162
+ } catch (err) {
1163
+ console.error(`[MONITOR] Scan-ledger compaction failed: ${err.message}`);
1164
+ }
1165
+ });
1142
1166
  }
1143
1167
 
1144
1168
  /** Stream the scan-ledger into an array (tests + Phase 0b rollup). */
@@ -59,6 +59,18 @@ function dockerSync(label, meta, fn) {
59
59
  try { return fn(); } finally { _endOp(_crumb); }
60
60
  }
61
61
 
62
+ // Same breadcrumb mechanism as dockerSync, for the SYNCHRONOUS post-container
63
+ // result-handling that also runs on the main thread (the deferred sandbox worker is a
64
+ // setInterval on the main loop — deferred-sandbox.js): JSON.parse of container stdout,
65
+ // gVisor kernel-log parse/cleanup, preload-log analysis. Uninstrumented today, so a
66
+ // multi-minute stall in any of them mis-attributes to the last docker:* crumb (the
67
+ // docker:rm-leurre seen in data/loop-stalls.jsonl with durationMs << lagMs). Leaf-only:
68
+ // the wrapped fn must NOT nest another beginOp/dockerSync (single-slot model).
69
+ function syncStep(label, meta, fn) {
70
+ const _crumb = _beginOp(label, meta || null);
71
+ try { return fn(); } finally { _endOp(_crumb); }
72
+ }
73
+
62
74
  const DOCKER_IMAGE = 'muaddib-sandbox';
63
75
  const CONTAINER_TIMEOUT = 120000; // 120 seconds
64
76
  const SINGLE_RUN_TIMEOUT = 90000; // 90 seconds per run in multi-run mode (gVisor ~30% I/O overhead)
@@ -595,7 +607,7 @@ async function runSingleSandbox(packageName, options = {}) {
595
607
  }
596
608
  jsonStr = stdout.substring(jsonStart, jsonEnd + 1);
597
609
  }
598
- report = JSON.parse(jsonStr);
610
+ report = syncStep('sandbox:parse-report', { bytes: jsonStr.length }, () => JSON.parse(jsonStr));
599
611
  if (local && report) {
600
612
  report.package = displayName;
601
613
  }
@@ -636,7 +648,7 @@ async function runSingleSandbox(packageName, options = {}) {
636
648
  // connections, and process data come from gVisor's kernel-level tracing.
637
649
  if (gvisorMode && gvisorContainerId) {
638
650
  const gvisorLogDir = process.env.MUADDIB_GVISOR_LOG_DIR || '/tmp/runsc';
639
- const gvisorData = parseGvisorLogs(gvisorContainerId, gvisorLogDir);
651
+ const gvisorData = syncStep('sandbox:gvisor-parse', { container: gvisorContainerId }, () => parseGvisorLogs(gvisorContainerId, gvisorLogDir));
640
652
 
641
653
  // Merge gVisor findings into report without duplicating
642
654
  if (!report.sensitive_files) report.sensitive_files = { read: [], written: [] };
@@ -665,14 +677,14 @@ async function runSingleSandbox(packageName, options = {}) {
665
677
  }
666
678
 
667
679
  // Cleanup gVisor logs to prevent disk fill
668
- cleanupGvisorLogs(gvisorContainerId, gvisorLogDir);
680
+ syncStep('sandbox:gvisor-cleanup', { container: gvisorContainerId }, () => cleanupGvisorLogs(gvisorContainerId, gvisorLogDir));
669
681
  }
670
682
 
671
683
  const { score, findings } = scoreFindings(report);
672
684
 
673
685
  // Analyze preload log for behavioral findings
674
686
  if (report.preload_log) {
675
- const preloadResult = analyzePreloadLog(report.preload_log);
687
+ const preloadResult = syncStep('sandbox:preload-analyze', null, () => analyzePreloadLog(report.preload_log));
676
688
  for (const finding of preloadResult.findings) {
677
689
  findings.push(finding);
678
690
  }
package/stats.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "GENERATED by scripts/collect-stats.js — do not edit by hand. Run `npm run docs:stats`.",
3
- "version": "2.11.161",
3
+ "version": "2.11.163",
4
4
  "scanners": 21,
5
5
  "rulesTotal": 275,
6
6
  "rulesCore": 270,
package/zizmor.yml ADDED
@@ -0,0 +1,18 @@
1
+ # zizmor configuration — https://docs.zizmor.sh/configuration/
2
+ #
3
+ # Suppress a finding ONLY when it is a confirmed false positive, and keep the
4
+ # rationale on the same line. Schema:
5
+ #
6
+ # rules:
7
+ # <audit-id>: # e.g. template-injection, artipacked, cache-poisoning
8
+ # ignore:
9
+ # - <workflow>.yml # ignore the whole file
10
+ # - <workflow>.yml:LINE # ignore one line
11
+ # - <workflow>.yml:LINE:COL# ignore one exact finding
12
+ #
13
+ # Policy for this repo: prefer FIXING findings over ignoring them. As of the first
14
+ # run every finding is real (template-injection in publish.yml, artipacked on the
15
+ # checkout steps, cache-poisoning, adhoc-packages), so nothing is suppressed here.
16
+ # Add an entry below only after confirming a genuine false positive.
17
+
18
+ rules: {}