muaddib-scanner 2.11.162 → 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
package/src/monitor/state.js
CHANGED
|
@@ -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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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). */
|
package/src/sandbox/index.js
CHANGED
|
@@ -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
|
}
|