debuganything 0.0.0-stage → 1.0.0
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/.claude-plugin/plugin.json +15 -0
- package/DISCLAIMER.md +101 -0
- package/GEMINI.md +97 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/assets/README.md +9 -0
- package/assets/debuganything-logo.png +0 -0
- package/assets/debuganything-mark-512.png +0 -0
- package/dist/chunk-32IDKAZK.js +9 -0
- package/dist/chunk-32IDKAZK.js.map +1 -0
- package/dist/chunk-5BXTDASF.js +135 -0
- package/dist/chunk-5BXTDASF.js.map +1 -0
- package/dist/chunk-5RAKTYW5.js +292 -0
- package/dist/chunk-5RAKTYW5.js.map +1 -0
- package/dist/chunk-7MMXFUUN.js +79 -0
- package/dist/chunk-7MMXFUUN.js.map +1 -0
- package/dist/chunk-D742WJPT.js +436 -0
- package/dist/chunk-D742WJPT.js.map +1 -0
- package/dist/chunk-EMR42ZJQ.js +102 -0
- package/dist/chunk-EMR42ZJQ.js.map +1 -0
- package/dist/chunk-MLDOO673.js +1416 -0
- package/dist/chunk-MLDOO673.js.map +1 -0
- package/dist/chunk-NFL22T3H.js +309 -0
- package/dist/chunk-NFL22T3H.js.map +1 -0
- package/dist/chunk-STFC7C5C.js +245 -0
- package/dist/chunk-STFC7C5C.js.map +1 -0
- package/dist/chunk-VGYXHXTY.js +1015 -0
- package/dist/chunk-VGYXHXTY.js.map +1 -0
- package/dist/chunk-YY6M5RIJ.js +215 -0
- package/dist/chunk-YY6M5RIJ.js.map +1 -0
- package/dist/cli.js +254 -0
- package/dist/cli.js.map +1 -0
- package/dist/docker-ORXYMES4.js +23 -0
- package/dist/docker-ORXYMES4.js.map +1 -0
- package/dist/hook-QYLLBUNK.js +14 -0
- package/dist/hook-QYLLBUNK.js.map +1 -0
- package/dist/import-CT55AEEE.js +268 -0
- package/dist/import-CT55AEEE.js.map +1 -0
- package/dist/investigate-2GFD6EWB.js +102 -0
- package/dist/investigate-2GFD6EWB.js.map +1 -0
- package/dist/lsof-GBLZJBJR.js +22 -0
- package/dist/lsof-GBLZJBJR.js.map +1 -0
- package/dist/psmem-UB3VCMG3.js +20 -0
- package/dist/psmem-UB3VCMG3.js.map +1 -0
- package/dist/report_html-2ENM7HLC.js +99 -0
- package/dist/report_html-2ENM7HLC.js.map +1 -0
- package/dist/server.js +190 -0
- package/dist/server.js.map +1 -0
- package/dist/setup-NTPRUTMT.js +265 -0
- package/dist/setup-NTPRUTMT.js.map +1 -0
- package/dist/target-WJE4WFYA.js +36 -0
- package/dist/target-WJE4WFYA.js.map +1 -0
- package/gemini-extension.json +9 -0
- package/hooks/debuganything-hook.mjs +36 -0
- package/hooks/hooks.json +10 -0
- package/package.json +85 -3
- package/server.json +30 -0
- package/skills/debug-anything/SKILL.md +102 -0
- package/skills/inspect-product/SKILL.md +138 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ResultBuilder,
|
|
4
|
+
humanBytes
|
|
5
|
+
} from "./chunk-5BXTDASF.js";
|
|
6
|
+
import {
|
|
7
|
+
targetPids
|
|
8
|
+
} from "./chunk-5RAKTYW5.js";
|
|
9
|
+
import {
|
|
10
|
+
platform
|
|
11
|
+
} from "./chunk-D742WJPT.js";
|
|
12
|
+
import {
|
|
13
|
+
isRoot,
|
|
14
|
+
looksLikePermissionError,
|
|
15
|
+
rootRequiredLimitation,
|
|
16
|
+
runCapped
|
|
17
|
+
} from "./chunk-STFC7C5C.js";
|
|
18
|
+
|
|
19
|
+
// src/adapters/lsof.ts
|
|
20
|
+
import { readFile } from "fs/promises";
|
|
21
|
+
function parseLsofFields(raw) {
|
|
22
|
+
const out = [];
|
|
23
|
+
let pid = 0;
|
|
24
|
+
let command = "";
|
|
25
|
+
let user;
|
|
26
|
+
let cur = null;
|
|
27
|
+
const flush = () => {
|
|
28
|
+
if (cur && cur.fd !== void 0) {
|
|
29
|
+
const name = cur.name ?? "";
|
|
30
|
+
out.push({
|
|
31
|
+
pid,
|
|
32
|
+
command,
|
|
33
|
+
user,
|
|
34
|
+
fd: cur.fd,
|
|
35
|
+
type: cur.type ?? "",
|
|
36
|
+
size: cur.size,
|
|
37
|
+
inode: cur.inode,
|
|
38
|
+
name,
|
|
39
|
+
tcpState: cur.tcpState,
|
|
40
|
+
protocol: cur.protocol,
|
|
41
|
+
deleted: /\(deleted\)/.test(name) || void 0
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
cur = null;
|
|
45
|
+
};
|
|
46
|
+
for (const line of raw.split("\n")) {
|
|
47
|
+
if (!line) continue;
|
|
48
|
+
const tag = line[0];
|
|
49
|
+
const val = line.slice(1);
|
|
50
|
+
switch (tag) {
|
|
51
|
+
case "p":
|
|
52
|
+
flush();
|
|
53
|
+
pid = Number(val);
|
|
54
|
+
break;
|
|
55
|
+
case "c":
|
|
56
|
+
command = val;
|
|
57
|
+
break;
|
|
58
|
+
case "u":
|
|
59
|
+
user = val;
|
|
60
|
+
break;
|
|
61
|
+
case "L":
|
|
62
|
+
user = val;
|
|
63
|
+
break;
|
|
64
|
+
case "f":
|
|
65
|
+
flush();
|
|
66
|
+
cur = { fd: val };
|
|
67
|
+
break;
|
|
68
|
+
case "t":
|
|
69
|
+
if (cur) cur.type = val;
|
|
70
|
+
break;
|
|
71
|
+
case "s":
|
|
72
|
+
if (cur) cur.size = Number(val);
|
|
73
|
+
break;
|
|
74
|
+
case "i":
|
|
75
|
+
if (cur) cur.inode = val;
|
|
76
|
+
break;
|
|
77
|
+
case "n":
|
|
78
|
+
if (cur) cur.name = val;
|
|
79
|
+
break;
|
|
80
|
+
case "P":
|
|
81
|
+
if (cur && (val === "TCP" || val === "UDP")) cur.protocol = val;
|
|
82
|
+
break;
|
|
83
|
+
case "T":
|
|
84
|
+
if (cur && val.startsWith("ST=")) cur.tcpState = val.slice(3);
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
flush();
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function splitAddr(s) {
|
|
94
|
+
const i = s.lastIndexOf(":");
|
|
95
|
+
if (i < 0) return { host: s, port: 0 };
|
|
96
|
+
return { host: s.slice(0, i), port: Number(s.slice(i + 1)) || 0 };
|
|
97
|
+
}
|
|
98
|
+
function summarizeSockets(entries) {
|
|
99
|
+
const listening = [];
|
|
100
|
+
const byState = {};
|
|
101
|
+
const remotes = /* @__PURE__ */ new Map();
|
|
102
|
+
let total = 0;
|
|
103
|
+
let unix = 0;
|
|
104
|
+
let udp = 0;
|
|
105
|
+
for (const e of entries) {
|
|
106
|
+
if (e.type === "unix") {
|
|
107
|
+
unix++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (e.type !== "IPv4" && e.type !== "IPv6") continue;
|
|
111
|
+
total++;
|
|
112
|
+
const proto = e.protocol ?? (e.tcpState ? "TCP" : "UDP");
|
|
113
|
+
if (proto === "UDP") udp++;
|
|
114
|
+
if (e.tcpState === "LISTEN") {
|
|
115
|
+
const { host, port } = splitAddr(e.name);
|
|
116
|
+
listening.push({ pid: e.pid, proto, addr: host, port });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (e.tcpState) byState[e.tcpState] = (byState[e.tcpState] ?? 0) + 1;
|
|
120
|
+
const arrow = e.name.indexOf("->");
|
|
121
|
+
if (arrow >= 0) {
|
|
122
|
+
const remote = e.name.slice(arrow + 2);
|
|
123
|
+
const { host } = splitAddr(remote);
|
|
124
|
+
const r = remotes.get(host) ?? { count: 0, states: {} };
|
|
125
|
+
r.count++;
|
|
126
|
+
if (e.tcpState) r.states[e.tcpState] = (r.states[e.tcpState] ?? 0) + 1;
|
|
127
|
+
remotes.set(host, r);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const byRemote = [...remotes].map(([remote, v]) => ({ remote, ...v })).sort((a, b) => b.count - a.count);
|
|
131
|
+
return { listening, byState, byRemote, total_sockets: total, unix_sockets: unix, udp };
|
|
132
|
+
}
|
|
133
|
+
function summarizeFiles(entries) {
|
|
134
|
+
const byType = {};
|
|
135
|
+
const numeric = entries.filter((e) => /^\d+/.test(e.fd));
|
|
136
|
+
const regular = numeric.filter((e) => e.type === "REG");
|
|
137
|
+
for (const e of numeric) byType[e.type || "?"] = (byType[e.type || "?"] ?? 0) + 1;
|
|
138
|
+
const largest = [...regular].filter((e) => typeof e.size === "number").sort((a, b) => (b.size ?? 0) - (a.size ?? 0)).slice(0, 10).map((e) => ({ fd: e.fd, size: e.size ?? 0, name: e.name }));
|
|
139
|
+
const deleted = numeric.filter((e) => e.deleted).map((e) => ({ fd: e.fd, size: e.size ?? 0, name: e.name }));
|
|
140
|
+
return { byType, total_fds: numeric.length, largest, deleted, regular_files: regular.length };
|
|
141
|
+
}
|
|
142
|
+
function parseProcLimits(text) {
|
|
143
|
+
const m = /^Max open files\s+(\S+)\s+(\S+)/m.exec(text);
|
|
144
|
+
if (!m) return { soft: null, hard: null };
|
|
145
|
+
const n = (s) => s === "unlimited" ? Infinity : Number(s);
|
|
146
|
+
return { soft: n(m[1]), hard: n(m[2]) };
|
|
147
|
+
}
|
|
148
|
+
async function runLsof(pids, extra = []) {
|
|
149
|
+
const args = ["-nP", "-w", "-a", "-F", "pcufatsinTP", ...extra];
|
|
150
|
+
if (pids.length) args.push("-p", pids.join(","));
|
|
151
|
+
const r = await runCapped("lsof", args, { timeoutMs: 2e4 });
|
|
152
|
+
return { entries: parseLsofFields(r.stdout), stderr: r.stderr, exitCode: r.exitCode, raw: r.stdout };
|
|
153
|
+
}
|
|
154
|
+
async function openFileLimit(pid) {
|
|
155
|
+
if (platform() === "linux") {
|
|
156
|
+
try {
|
|
157
|
+
return { ...parseProcLimits(await readFile(`/proc/${pid}/limits`, "utf8")), source: `/proc/${pid}/limits` };
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (platform() === "darwin") {
|
|
162
|
+
const r = await runCapped("launchctl", ["limit", "maxfiles"], { timeoutMs: 5e3 });
|
|
163
|
+
const m = /maxfiles\s+(\S+)\s+(\S+)/.exec(r.stdout);
|
|
164
|
+
if (m) {
|
|
165
|
+
const n = (s) => s === "unlimited" ? Infinity : Number(s);
|
|
166
|
+
return { soft: n(m[1]), hard: n(m[2]), source: "launchctl limit maxfiles (system default, not the target's own ulimit)" };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { soft: null, hard: null, source: "unknown" };
|
|
170
|
+
}
|
|
171
|
+
async function inspectSockets(ctx) {
|
|
172
|
+
const { session, target } = ctx;
|
|
173
|
+
const b = new ResultBuilder("inspect_sockets", session, target);
|
|
174
|
+
const pids = targetPids(target);
|
|
175
|
+
if (!pids.length) return b.fail("target has no host pid; cannot inspect sockets").build();
|
|
176
|
+
const { entries, stderr, raw } = await runLsof(pids, ["-i"]);
|
|
177
|
+
const art = await b.writeArtifact("lsof-sockets.txt", raw, "raw_text", `lsof -nP -i -p ${pids.join(",")}`);
|
|
178
|
+
if (entries.length === 0 && looksLikePermissionError(stderr)) {
|
|
179
|
+
b.limitation(rootRequiredLimitation("inspect_sockets", `lsof -nP -i -p ${pids.join(",")}`));
|
|
180
|
+
return b.fail("lsof could not read the target's fds (permission denied)").build();
|
|
181
|
+
}
|
|
182
|
+
const s = summarizeSockets(entries);
|
|
183
|
+
const listenEv = b.evidenceOf(
|
|
184
|
+
"socket_summary",
|
|
185
|
+
s.listening.length ? `listening: ${s.listening.map((l) => `${l.proto} ${l.addr}:${l.port} (pid ${l.pid})`).join(", ")}` : "no listening sockets",
|
|
186
|
+
s.listening,
|
|
187
|
+
{ artifact: art }
|
|
188
|
+
);
|
|
189
|
+
const stateEv = b.evidenceOf("socket_summary", `${s.total_sockets} inet socket(s): ${Object.entries(s.byState).map(([k, v]) => `${k}=${v}`).join(" ") || "none established"}; udp=${s.udp}; unix=${s.unix_sockets}`, { by_state: s.byState, total: s.total_sockets, udp: s.udp, unix: s.unix_sockets }, { artifact: art });
|
|
190
|
+
const remoteEv = b.evidenceOf("socket", s.byRemote.length ? `top remotes: ${s.byRemote.slice(0, 5).map((r) => `${r.remote} \xD7${r.count}`).join(", ")}` : "no remote connections", s.byRemote.slice(0, 30).map((r) => ({ remote: r.remote, count: r.count, states: r.states })), { artifact: art });
|
|
191
|
+
const all = await runLsof(pids);
|
|
192
|
+
const files = summarizeFiles(all.entries);
|
|
193
|
+
const lim = await openFileLimit(pids[0]);
|
|
194
|
+
const fdEv = b.evidenceOf("fd_summary", `${files.total_fds} open fd(s); open-file limit soft=${lim.soft ?? "?"} hard=${lim.hard ?? "?"} (${lim.source})`, { fds: files.total_fds, soft: lim.soft === Infinity ? "unlimited" : lim.soft, hard: lim.hard === Infinity ? "unlimited" : lim.hard, source: lim.source });
|
|
195
|
+
if (s.listening.length) b.finding("info", `listening on ${s.listening.map((l) => `${l.proto}/${l.port}`).join(", ")}`, [listenEv]);
|
|
196
|
+
const cw = s.byState.CLOSE_WAIT ?? 0;
|
|
197
|
+
if (cw >= 20) b.finding("high", `${cw} sockets in CLOSE_WAIT \u2014 the peer closed and the target never called close(); fd leak in the connection-handling code`, [stateEv], "inferred");
|
|
198
|
+
else if (cw >= 5) b.finding("warn", `${cw} sockets in CLOSE_WAIT \u2014 the target is slow to close connections the peer already closed`, [stateEv], "inferred");
|
|
199
|
+
const tw = s.byState.TIME_WAIT ?? 0;
|
|
200
|
+
if (tw >= 100) b.finding("warn", `${tw} sockets in TIME_WAIT \u2014 high connection churn (no keep-alive / connection pooling?)`, [stateEv], "inferred");
|
|
201
|
+
const syn = s.byState.SYN_SENT ?? 0;
|
|
202
|
+
if (syn >= 3) b.finding("warn", `${syn} sockets stuck in SYN_SENT \u2014 a remote is not answering connection attempts`, [stateEv, remoteEv], "observed");
|
|
203
|
+
const top = s.byRemote[0];
|
|
204
|
+
if (top && top.count >= 50) b.finding("warn", `${top.count} connections to ${top.remote} \u2014 connection pool exhaustion or missing reuse`, [remoteEv], "inferred");
|
|
205
|
+
if (lim.soft && lim.soft !== Infinity && files.total_fds / lim.soft > 0.8) b.finding("high", `${files.total_fds} fds open vs soft limit ${lim.soft} (${Math.round(100 * files.total_fds / lim.soft)}%) \u2014 EMFILE imminent`, [fdEv]);
|
|
206
|
+
if (!s.listening.length && !s.byRemote.length) b.finding("info", "target has no inet sockets open", [stateEv]);
|
|
207
|
+
if (platform() === "darwin" && !isRoot()) b.limitation("macOS lsof without root only sees processes owned by the current user");
|
|
208
|
+
if (pids.length > 1) b.limitation(`included ${pids.length} pids (target + descendants)`);
|
|
209
|
+
b.next("inspect_files", "trace_syscalls", s.byRemote.length ? "capture_traffic (needs consent)" : "profile_cpu");
|
|
210
|
+
return b.build();
|
|
211
|
+
}
|
|
212
|
+
async function inspectFiles(ctx, args) {
|
|
213
|
+
const { session, target } = ctx;
|
|
214
|
+
const b = new ResultBuilder("inspect_files", session, target);
|
|
215
|
+
const pids = targetPids(target);
|
|
216
|
+
if (!pids.length) return b.fail("target has no host pid; cannot inspect files").build();
|
|
217
|
+
const first = await runLsof(pids);
|
|
218
|
+
if (first.entries.length === 0 && looksLikePermissionError(first.stderr)) {
|
|
219
|
+
b.limitation(rootRequiredLimitation("inspect_files", `lsof -nP -p ${pids.join(",")}`));
|
|
220
|
+
return b.fail("lsof could not read the target's fds (permission denied)").build();
|
|
221
|
+
}
|
|
222
|
+
const art = await b.writeArtifact("lsof-files.txt", first.raw, "raw_text", `lsof -nP -p ${pids.join(",")}`);
|
|
223
|
+
const f1 = summarizeFiles(first.entries);
|
|
224
|
+
const window = Math.min(args.window_s ?? 3, 30);
|
|
225
|
+
let f2;
|
|
226
|
+
if (window > 0) {
|
|
227
|
+
await new Promise((r) => setTimeout(r, window * 1e3));
|
|
228
|
+
const second = await runLsof(pids);
|
|
229
|
+
f2 = summarizeFiles(second.entries);
|
|
230
|
+
}
|
|
231
|
+
const typeEv = b.evidenceOf("fd_summary", `${f1.total_fds} fd(s): ${Object.entries(f1.byType).sort((a, b2) => b2[1] - a[1]).map(([k, v]) => `${k}=${v}`).join(" ")}`, Object.entries(f1.byType).map(([type, count]) => ({ type, count })), { artifact: art });
|
|
232
|
+
const lim = await openFileLimit(pids[0]);
|
|
233
|
+
const limEv = b.evidenceOf("metric", `open-file limit soft=${lim.soft === Infinity ? "unlimited" : lim.soft ?? "?"} hard=${lim.hard === Infinity ? "unlimited" : lim.hard ?? "?"} (${lim.source})`, { name: "fd_limit_soft", value: lim.soft === Infinity ? -1 : lim.soft, hard: lim.hard === Infinity ? -1 : lim.hard, fds: f1.total_fds });
|
|
234
|
+
if (f1.largest.length) {
|
|
235
|
+
b.evidenceOf("fd", `largest open files: ${f1.largest.slice(0, 3).map((l) => `${l.name.split("/").pop()} ${humanBytes(l.size)}`).join(", ")}`, f1.largest, { artifact: art });
|
|
236
|
+
}
|
|
237
|
+
let growthEv;
|
|
238
|
+
if (f2) {
|
|
239
|
+
growthEv = b.evidenceOf("metric_series", `fds ${f1.total_fds} \u2192 ${f2.total_fds} over ${window}s (${f2.total_fds - f1.total_fds >= 0 ? "+" : ""}${f2.total_fds - f1.total_fds})`, { name: "open_fds", value: f2.total_fds, start: f1.total_fds, end: f2.total_fds, window_s: window });
|
|
240
|
+
}
|
|
241
|
+
if (f1.deleted.length) {
|
|
242
|
+
const delEv = b.evidenceOf("fd", `${f1.deleted.length} deleted-but-open file(s), ${humanBytes(f1.deleted.reduce((s, d) => s + d.size, 0))} held`, f1.deleted.slice(0, 20), { artifact: art });
|
|
243
|
+
b.finding("high", `${f1.deleted.length} file(s) deleted while still open hold ${humanBytes(f1.deleted.reduce((s, d) => s + d.size, 0))} of disk \u2014 "disk full but nothing there"`, [delEv]);
|
|
244
|
+
}
|
|
245
|
+
if (f2 && f2.total_fds - f1.total_fds >= Math.max(5, f1.total_fds * 0.1)) {
|
|
246
|
+
b.finding("high", `fd count grew ${f1.total_fds} \u2192 ${f2.total_fds} in ${window}s \u2014 fd leak`, [growthEv], "inferred");
|
|
247
|
+
}
|
|
248
|
+
if (lim.soft && lim.soft !== Infinity && f1.total_fds / lim.soft > 0.8) b.finding("high", `${f1.total_fds} fds vs soft limit ${lim.soft}`, [typeEv, limEv]);
|
|
249
|
+
if (f1.largest[0] && f1.largest[0].size > 1024 ** 3) b.finding("info", `largest open file is ${humanBytes(f1.largest[0].size)}: ${f1.largest[0].name}`, [b.evidenceOf("fd", "largest file", f1.largest[0], { artifact: art })]);
|
|
250
|
+
if (b.findingCount === 0) b.finding("info", `${f1.total_fds} fds open, dominated by ${Object.entries(f1.byType).sort((a, b2) => b2[1] - a[1])[0]?.[0] ?? "?"}`, [typeEv]);
|
|
251
|
+
if (platform() === "darwin") b.limitation("macOS lsof does not mark deleted-but-open files; that check only works on Linux");
|
|
252
|
+
b.next("inspect_sockets", "trace_syscalls");
|
|
253
|
+
return b.build();
|
|
254
|
+
}
|
|
255
|
+
async function whoHoldsPort(port) {
|
|
256
|
+
const target = { kind: "pid", id: `port:${port}`, host: "local" };
|
|
257
|
+
const b = new ResultBuilder("inspect_sockets", null, target);
|
|
258
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return b.fail(`invalid port ${port}`).build();
|
|
259
|
+
const [tcp, udp] = await Promise.all([
|
|
260
|
+
runCapped("lsof", ["-nP", "-w", "-F", "pcufatsinTP", `-iTCP:${port}`], { timeoutMs: 15e3 }),
|
|
261
|
+
runCapped("lsof", ["-nP", "-w", "-F", "pcufatsinTP", `-iUDP:${port}`], { timeoutMs: 15e3 })
|
|
262
|
+
]);
|
|
263
|
+
const entries = [...parseLsofFields(tcp.stdout), ...parseLsofFields(udp.stdout)];
|
|
264
|
+
const holders = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const e of entries) {
|
|
266
|
+
const h = holders.get(e.pid) ?? { pid: e.pid, command: e.command, user: e.user, states: [], names: [] };
|
|
267
|
+
if (e.tcpState) h.states.push(e.tcpState);
|
|
268
|
+
h.names.push(e.name);
|
|
269
|
+
holders.set(e.pid, h);
|
|
270
|
+
}
|
|
271
|
+
const list = [...holders.values()];
|
|
272
|
+
const ev = b.evidenceOf("socket", list.length ? `port ${port}: ${list.map((h) => `pid ${h.pid} ${h.command} [${[...new Set(h.states)].join(",") || "UDP"}]`).join("; ")}` : `nothing holds port ${port} (visible to this user)`, list);
|
|
273
|
+
const listener = list.find((h) => h.states.includes("LISTEN"));
|
|
274
|
+
if (listener) {
|
|
275
|
+
const { processInfo } = await import("./target-WJE4WFYA.js");
|
|
276
|
+
const info = await processInfo(listener.pid);
|
|
277
|
+
const pev = b.evidenceOf("process", `pid ${listener.pid}: ${info?.args ?? listener.command} (user ${info?.user ?? listener.user ?? "?"})`, info ?? listener);
|
|
278
|
+
b.finding("high", `port ${port} is held by pid ${listener.pid} (${listener.command}${info?.user ? `, user ${info.user}` : ""}) in LISTEN`, [ev, pev], "observed", `command: ${info?.args ?? "?"}. Debug Anything will not kill it; decide yourself.`);
|
|
279
|
+
} else if (list.length) {
|
|
280
|
+
b.finding("warn", `port ${port} has ${list.length} process(es) with non-listening sockets (${[...new Set(list.flatMap((h) => h.states))].join(",") || "UDP"}) \u2014 a TIME_WAIT/CLOSE_WAIT leftover can also block bind without SO_REUSEADDR`, [ev], "inferred");
|
|
281
|
+
} else {
|
|
282
|
+
b.finding("info", `no process visible to this user holds port ${port}`, [ev]);
|
|
283
|
+
if (!isRoot()) b.limitation("other users' processes are invisible without root; try: sudo lsof -nP -iTCP:" + port);
|
|
284
|
+
}
|
|
285
|
+
return b.build();
|
|
286
|
+
}
|
|
287
|
+
var lsofAdapter = {
|
|
288
|
+
name: "lsof",
|
|
289
|
+
tools: ["inspect_sockets", "inspect_files"],
|
|
290
|
+
platforms: ["linux", "darwin"],
|
|
291
|
+
requires: ["lsof"],
|
|
292
|
+
install: { linux: "sudo apt install lsof | sudo dnf install lsof", darwin: "preinstalled (/usr/sbin/lsof)" },
|
|
293
|
+
priority: 10,
|
|
294
|
+
async run(tool, args, ctx) {
|
|
295
|
+
if (tool === "inspect_sockets") return inspectSockets(ctx);
|
|
296
|
+
if (tool === "inspect_files") return inspectFiles(ctx, args);
|
|
297
|
+
throw new Error(`lsof adapter cannot run ${tool}`);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
parseLsofFields,
|
|
303
|
+
summarizeSockets,
|
|
304
|
+
summarizeFiles,
|
|
305
|
+
parseProcLimits,
|
|
306
|
+
whoHoldsPort,
|
|
307
|
+
lsofAdapter
|
|
308
|
+
};
|
|
309
|
+
//# sourceMappingURL=chunk-NFL22T3H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/lsof.ts"],"sourcesContent":["/**\n * lsof adapter: inspect_sockets, inspect_files, and the `debuganything port` lookup.\n * Works on Linux and macOS without root for your own processes.\n *\n * We use lsof's machine-readable field output (-F) so parsing does not depend\n * on column alignment.\n */\nimport { readFile } from \"node:fs/promises\";\nimport type { Adapter, AdapterContext } from \"./types.js\";\nimport type { Target, ToolResult } from \"../contracts.js\";\nimport { ResultBuilder, humanBytes } from \"../evidence.js\";\nimport { runCapped, looksLikePermissionError, isRoot, rootRequiredLimitation } from \"../safety.js\";\nimport { platform } from \"../session.js\";\nimport { targetPids } from \"../target.js\";\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\nexport interface LsofEntry {\n pid: number;\n command: string;\n user?: string;\n fd: string; // \"cwd\", \"txt\", \"20u\", ...\n type: string; // REG, DIR, CHR, FIFO, unix, IPv4, IPv6, KQUEUE, PIPE, ...\n size?: number;\n inode?: string;\n name: string;\n tcpState?: string; // LISTEN, ESTABLISHED, CLOSE_WAIT, ...\n protocol?: \"TCP\" | \"UDP\";\n deleted?: boolean;\n}\n\n/** Parse `lsof -nP -F pcufatsinTP` output. Pure. */\nexport function parseLsofFields(raw: string): LsofEntry[] {\n const out: LsofEntry[] = [];\n let pid = 0;\n let command = \"\";\n let user: string | undefined;\n let cur: Partial<LsofEntry> | null = null;\n const flush = () => {\n if (cur && cur.fd !== undefined) {\n const name = cur.name ?? \"\";\n out.push({\n pid,\n command,\n user,\n fd: cur.fd,\n type: cur.type ?? \"\",\n size: cur.size,\n inode: cur.inode,\n name,\n tcpState: cur.tcpState,\n protocol: cur.protocol,\n deleted: /\\(deleted\\)/.test(name) || undefined,\n });\n }\n cur = null;\n };\n for (const line of raw.split(\"\\n\")) {\n if (!line) continue;\n const tag = line[0]!;\n const val = line.slice(1);\n switch (tag) {\n case \"p\":\n flush();\n pid = Number(val);\n break;\n case \"c\":\n command = val;\n break;\n case \"u\":\n user = val;\n break;\n case \"L\":\n user = val;\n break;\n case \"f\":\n flush();\n cur = { fd: val };\n break;\n case \"t\":\n if (cur) cur.type = val;\n break;\n case \"s\":\n if (cur) cur.size = Number(val);\n break;\n case \"i\":\n if (cur) cur.inode = val;\n break;\n case \"n\":\n if (cur) cur.name = val;\n break;\n case \"P\":\n if (cur && (val === \"TCP\" || val === \"UDP\")) cur.protocol = val;\n break;\n case \"T\":\n if (cur && val.startsWith(\"ST=\")) cur.tcpState = val.slice(3);\n break;\n default:\n break;\n }\n }\n flush();\n return out;\n}\n\nexport interface SocketSummary {\n listening: Array<{ pid: number; proto: string; addr: string; port: number }>;\n byState: Record<string, number>;\n byRemote: Array<{ remote: string; count: number; states: Record<string, number> }>;\n total_sockets: number;\n unix_sockets: number;\n udp: number;\n}\n\nfunction splitAddr(s: string): { host: string; port: number } {\n const i = s.lastIndexOf(\":\");\n if (i < 0) return { host: s, port: 0 };\n return { host: s.slice(0, i), port: Number(s.slice(i + 1)) || 0 };\n}\n\n/** Summarize network entries. Pure. */\nexport function summarizeSockets(entries: LsofEntry[]): SocketSummary {\n const listening: SocketSummary[\"listening\"] = [];\n const byState: Record<string, number> = {};\n const remotes = new Map<string, { count: number; states: Record<string, number> }>();\n let total = 0;\n let unix = 0;\n let udp = 0;\n for (const e of entries) {\n if (e.type === \"unix\") {\n unix++;\n continue;\n }\n if (e.type !== \"IPv4\" && e.type !== \"IPv6\") continue;\n total++;\n const proto = e.protocol ?? (e.tcpState ? \"TCP\" : \"UDP\");\n if (proto === \"UDP\") udp++;\n if (e.tcpState === \"LISTEN\") {\n const { host, port } = splitAddr(e.name);\n listening.push({ pid: e.pid, proto, addr: host, port });\n continue;\n }\n if (e.tcpState) byState[e.tcpState] = (byState[e.tcpState] ?? 0) + 1;\n const arrow = e.name.indexOf(\"->\");\n if (arrow >= 0) {\n const remote = e.name.slice(arrow + 2);\n const { host } = splitAddr(remote);\n const r = remotes.get(host) ?? { count: 0, states: {} };\n r.count++;\n if (e.tcpState) r.states[e.tcpState] = (r.states[e.tcpState] ?? 0) + 1;\n remotes.set(host, r);\n }\n }\n const byRemote = [...remotes].map(([remote, v]) => ({ remote, ...v })).sort((a, b) => b.count - a.count);\n return { listening, byState, byRemote, total_sockets: total, unix_sockets: unix, udp };\n}\n\nexport interface FileSummary {\n byType: Record<string, number>;\n total_fds: number;\n largest: Array<{ fd: string; size: number; name: string }>;\n deleted: Array<{ fd: string; size: number; name: string }>;\n regular_files: number;\n}\n\n/** Summarize fd entries (numeric fds only). Pure. */\nexport function summarizeFiles(entries: LsofEntry[]): FileSummary {\n const byType: Record<string, number> = {};\n const numeric = entries.filter((e) => /^\\d+/.test(e.fd));\n const regular = numeric.filter((e) => e.type === \"REG\");\n for (const e of numeric) byType[e.type || \"?\"] = (byType[e.type || \"?\"] ?? 0) + 1;\n const largest = [...regular]\n .filter((e) => typeof e.size === \"number\")\n .sort((a, b) => (b.size ?? 0) - (a.size ?? 0))\n .slice(0, 10)\n .map((e) => ({ fd: e.fd, size: e.size ?? 0, name: e.name }));\n const deleted = numeric.filter((e) => e.deleted).map((e) => ({ fd: e.fd, size: e.size ?? 0, name: e.name }));\n return { byType, total_fds: numeric.length, largest, deleted, regular_files: regular.length };\n}\n\n/** Parse Linux /proc/<pid>/limits for Max open files. Pure. */\nexport function parseProcLimits(text: string): { soft: number | null; hard: number | null } {\n const m = /^Max open files\\s+(\\S+)\\s+(\\S+)/m.exec(text);\n if (!m) return { soft: null, hard: null };\n const n = (s: string) => (s === \"unlimited\" ? Infinity : Number(s));\n return { soft: n(m[1]!), hard: n(m[2]!) };\n}\n\n// ---------------------------------------------------------------------------\n// Running\n// ---------------------------------------------------------------------------\n\nasync function runLsof(pids: number[], extra: string[] = []): Promise<{ entries: LsofEntry[]; stderr: string; exitCode: number | null; raw: string }> {\n // -n no DNS, -P no port names, -F fields, -w suppress warnings. lsof exits 1 when some pids have nothing; fine.\n const args = [\"-nP\", \"-w\", \"-a\", \"-F\", \"pcufatsinTP\", ...extra];\n if (pids.length) args.push(\"-p\", pids.join(\",\"));\n const r = await runCapped(\"lsof\", args, { timeoutMs: 20_000 });\n return { entries: parseLsofFields(r.stdout), stderr: r.stderr, exitCode: r.exitCode, raw: r.stdout };\n}\n\nasync function openFileLimit(pid: number): Promise<{ soft: number | null; hard: number | null; source: string }> {\n if (platform() === \"linux\") {\n try {\n return { ...parseProcLimits(await readFile(`/proc/${pid}/limits`, \"utf8\")), source: `/proc/${pid}/limits` };\n } catch {\n /* fallthrough */\n }\n }\n // macOS: no per-process view without root; report launchd's maxfiles as an approximation.\n if (platform() === \"darwin\") {\n const r = await runCapped(\"launchctl\", [\"limit\", \"maxfiles\"], { timeoutMs: 5000 });\n const m = /maxfiles\\s+(\\S+)\\s+(\\S+)/.exec(r.stdout);\n if (m) {\n const n = (s: string) => (s === \"unlimited\" ? Infinity : Number(s));\n return { soft: n(m[1]!), hard: n(m[2]!), source: \"launchctl limit maxfiles (system default, not the target's own ulimit)\" };\n }\n }\n return { soft: null, hard: null, source: \"unknown\" };\n}\n\nasync function inspectSockets(ctx: AdapterContext): Promise<ToolResult> {\n const { session, target } = ctx;\n const b = new ResultBuilder(\"inspect_sockets\", session, target);\n const pids = targetPids(target);\n if (!pids.length) return b.fail(\"target has no host pid; cannot inspect sockets\").build();\n const { entries, stderr, raw } = await runLsof(pids, [\"-i\"]);\n const art = await b.writeArtifact(\"lsof-sockets.txt\", raw, \"raw_text\", `lsof -nP -i -p ${pids.join(\",\")}`);\n if (entries.length === 0 && looksLikePermissionError(stderr)) {\n b.limitation(rootRequiredLimitation(\"inspect_sockets\", `lsof -nP -i -p ${pids.join(\",\")}`));\n return b.fail(\"lsof could not read the target's fds (permission denied)\").build();\n }\n const s = summarizeSockets(entries);\n const listenEv = b.evidenceOf(\n \"socket_summary\",\n s.listening.length ? `listening: ${s.listening.map((l) => `${l.proto} ${l.addr}:${l.port} (pid ${l.pid})`).join(\", \")}` : \"no listening sockets\",\n s.listening,\n { artifact: art },\n );\n const stateEv = b.evidenceOf(\"socket_summary\", `${s.total_sockets} inet socket(s): ${Object.entries(s.byState).map(([k, v]) => `${k}=${v}`).join(\" \") || \"none established\"}; udp=${s.udp}; unix=${s.unix_sockets}`, { by_state: s.byState, total: s.total_sockets, udp: s.udp, unix: s.unix_sockets }, { artifact: art });\n const remoteEv = b.evidenceOf(\"socket\", s.byRemote.length ? `top remotes: ${s.byRemote.slice(0, 5).map((r) => `${r.remote} ×${r.count}`).join(\", \")}` : \"no remote connections\", s.byRemote.slice(0, 30).map((r) => ({ remote: r.remote, count: r.count, states: r.states })), { artifact: art });\n\n // fd count vs limit\n const all = await runLsof(pids);\n const files = summarizeFiles(all.entries);\n const lim = await openFileLimit(pids[0]!);\n const fdEv = b.evidenceOf(\"fd_summary\", `${files.total_fds} open fd(s); open-file limit soft=${lim.soft ?? \"?\"} hard=${lim.hard ?? \"?\"} (${lim.source})`, { fds: files.total_fds, soft: lim.soft === Infinity ? \"unlimited\" : lim.soft, hard: lim.hard === Infinity ? \"unlimited\" : lim.hard, source: lim.source });\n\n // Findings\n if (s.listening.length) b.finding(\"info\", `listening on ${s.listening.map((l) => `${l.proto}/${l.port}`).join(\", \")}`, [listenEv]);\n const cw = s.byState.CLOSE_WAIT ?? 0;\n if (cw >= 20) b.finding(\"high\", `${cw} sockets in CLOSE_WAIT — the peer closed and the target never called close(); fd leak in the connection-handling code`, [stateEv], \"inferred\");\n else if (cw >= 5) b.finding(\"warn\", `${cw} sockets in CLOSE_WAIT — the target is slow to close connections the peer already closed`, [stateEv], \"inferred\");\n const tw = s.byState.TIME_WAIT ?? 0;\n if (tw >= 100) b.finding(\"warn\", `${tw} sockets in TIME_WAIT — high connection churn (no keep-alive / connection pooling?)`, [stateEv], \"inferred\");\n const syn = s.byState.SYN_SENT ?? 0;\n if (syn >= 3) b.finding(\"warn\", `${syn} sockets stuck in SYN_SENT — a remote is not answering connection attempts`, [stateEv, remoteEv], \"observed\");\n const top = s.byRemote[0];\n if (top && top.count >= 50) b.finding(\"warn\", `${top.count} connections to ${top.remote} — connection pool exhaustion or missing reuse`, [remoteEv], \"inferred\");\n if (lim.soft && lim.soft !== Infinity && files.total_fds / lim.soft > 0.8) b.finding(\"high\", `${files.total_fds} fds open vs soft limit ${lim.soft} (${Math.round((100 * files.total_fds) / lim.soft)}%) — EMFILE imminent`, [fdEv]);\n if (!s.listening.length && !s.byRemote.length) b.finding(\"info\", \"target has no inet sockets open\", [stateEv]);\n\n if (platform() === \"darwin\" && !isRoot()) b.limitation(\"macOS lsof without root only sees processes owned by the current user\");\n if (pids.length > 1) b.limitation(`included ${pids.length} pids (target + descendants)`);\n b.next(\"inspect_files\", \"trace_syscalls\", s.byRemote.length ? \"capture_traffic (needs consent)\" : \"profile_cpu\");\n return b.build();\n}\n\nasync function inspectFiles(ctx: AdapterContext, args: { window_s?: number }): Promise<ToolResult> {\n const { session, target } = ctx;\n const b = new ResultBuilder(\"inspect_files\", session, target);\n const pids = targetPids(target);\n if (!pids.length) return b.fail(\"target has no host pid; cannot inspect files\").build();\n const first = await runLsof(pids);\n if (first.entries.length === 0 && looksLikePermissionError(first.stderr)) {\n b.limitation(rootRequiredLimitation(\"inspect_files\", `lsof -nP -p ${pids.join(\",\")}`));\n return b.fail(\"lsof could not read the target's fds (permission denied)\").build();\n }\n const art = await b.writeArtifact(\"lsof-files.txt\", first.raw, \"raw_text\", `lsof -nP -p ${pids.join(\",\")}`);\n const f1 = summarizeFiles(first.entries);\n const window = Math.min(args.window_s ?? 3, 30);\n let f2: FileSummary | undefined;\n if (window > 0) {\n await new Promise((r) => setTimeout(r, window * 1000));\n const second = await runLsof(pids);\n f2 = summarizeFiles(second.entries);\n }\n const typeEv = b.evidenceOf(\"fd_summary\", `${f1.total_fds} fd(s): ${Object.entries(f1.byType).sort((a, b2) => b2[1] - a[1]).map(([k, v]) => `${k}=${v}`).join(\" \")}`, Object.entries(f1.byType).map(([type, count]) => ({ type, count })), { artifact: art });\n const lim = await openFileLimit(pids[0]!);\n const limEv = b.evidenceOf(\"metric\", `open-file limit soft=${lim.soft === Infinity ? \"unlimited\" : lim.soft ?? \"?\"} hard=${lim.hard === Infinity ? \"unlimited\" : lim.hard ?? \"?\"} (${lim.source})`, { name: \"fd_limit_soft\", value: lim.soft === Infinity ? -1 : lim.soft, hard: lim.hard === Infinity ? -1 : lim.hard, fds: f1.total_fds });\n if (f1.largest.length) {\n b.evidenceOf(\"fd\", `largest open files: ${f1.largest.slice(0, 3).map((l) => `${l.name.split(\"/\").pop()} ${humanBytes(l.size)}`).join(\", \")}`, f1.largest, { artifact: art });\n }\n let growthEv: string | undefined;\n if (f2) {\n growthEv = b.evidenceOf(\"metric_series\", `fds ${f1.total_fds} → ${f2.total_fds} over ${window}s (${f2.total_fds - f1.total_fds >= 0 ? \"+\" : \"\"}${f2.total_fds - f1.total_fds})`, { name: \"open_fds\", value: f2.total_fds, start: f1.total_fds, end: f2.total_fds, window_s: window });\n }\n if (f1.deleted.length) {\n const delEv = b.evidenceOf(\"fd\", `${f1.deleted.length} deleted-but-open file(s), ${humanBytes(f1.deleted.reduce((s, d) => s + d.size, 0))} held`, f1.deleted.slice(0, 20), { artifact: art });\n b.finding(\"high\", `${f1.deleted.length} file(s) deleted while still open hold ${humanBytes(f1.deleted.reduce((s, d) => s + d.size, 0))} of disk — \"disk full but nothing there\"`, [delEv]);\n }\n if (f2 && f2.total_fds - f1.total_fds >= Math.max(5, f1.total_fds * 0.1)) {\n b.finding(\"high\", `fd count grew ${f1.total_fds} → ${f2.total_fds} in ${window}s — fd leak`, [growthEv!], \"inferred\");\n }\n if (lim.soft && lim.soft !== Infinity && f1.total_fds / lim.soft > 0.8) b.finding(\"high\", `${f1.total_fds} fds vs soft limit ${lim.soft}`, [typeEv, limEv]);\n if (f1.largest[0] && f1.largest[0].size > 1024 ** 3) b.finding(\"info\", `largest open file is ${humanBytes(f1.largest[0].size)}: ${f1.largest[0].name}`, [b.evidenceOf(\"fd\", \"largest file\", f1.largest[0], { artifact: art })]);\n if (b.findingCount === 0) b.finding(\"info\", `${f1.total_fds} fds open, dominated by ${Object.entries(f1.byType).sort((a, b2) => b2[1] - a[1])[0]?.[0] ?? \"?\"}`, [typeEv]);\n if (platform() === \"darwin\") b.limitation(\"macOS lsof does not mark deleted-but-open files; that check only works on Linux\");\n b.next(\"inspect_sockets\", \"trace_syscalls\");\n return b.build();\n}\n\n/** `debuganything port <n>`: who listens on a port. No session. */\nexport async function whoHoldsPort(port: number): Promise<ToolResult> {\n const target: Target = { kind: \"pid\", id: `port:${port}`, host: \"local\" };\n const b = new ResultBuilder(\"inspect_sockets\", null, target);\n if (!Number.isInteger(port) || port < 1 || port > 65535) return b.fail(`invalid port ${port}`).build();\n const [tcp, udp] = await Promise.all([\n runCapped(\"lsof\", [\"-nP\", \"-w\", \"-F\", \"pcufatsinTP\", `-iTCP:${port}`], { timeoutMs: 15_000 }),\n runCapped(\"lsof\", [\"-nP\", \"-w\", \"-F\", \"pcufatsinTP\", `-iUDP:${port}`], { timeoutMs: 15_000 }),\n ]);\n const entries = [...parseLsofFields(tcp.stdout), ...parseLsofFields(udp.stdout)];\n const holders = new Map<number, { pid: number; command: string; user?: string; states: string[]; names: string[] }>();\n for (const e of entries) {\n const h = holders.get(e.pid) ?? { pid: e.pid, command: e.command, user: e.user, states: [], names: [] };\n if (e.tcpState) h.states.push(e.tcpState);\n h.names.push(e.name);\n holders.set(e.pid, h);\n }\n const list = [...holders.values()];\n const ev = b.evidenceOf(\"socket\", list.length ? `port ${port}: ${list.map((h) => `pid ${h.pid} ${h.command} [${[...new Set(h.states)].join(\",\") || \"UDP\"}]`).join(\"; \")}` : `nothing holds port ${port} (visible to this user)`, list);\n const listener = list.find((h) => h.states.includes(\"LISTEN\"));\n if (listener) {\n const { processInfo } = await import(\"../target.js\");\n const info = await processInfo(listener.pid);\n const pev = b.evidenceOf(\"process\", `pid ${listener.pid}: ${info?.args ?? listener.command} (user ${info?.user ?? listener.user ?? \"?\"})`, info ?? listener);\n b.finding(\"high\", `port ${port} is held by pid ${listener.pid} (${listener.command}${info?.user ? `, user ${info.user}` : \"\"}) in LISTEN`, [ev, pev], \"observed\", `command: ${info?.args ?? \"?\"}. Debug Anything will not kill it; decide yourself.`);\n } else if (list.length) {\n b.finding(\"warn\", `port ${port} has ${list.length} process(es) with non-listening sockets (${[...new Set(list.flatMap((h) => h.states))].join(\",\") || \"UDP\"}) — a TIME_WAIT/CLOSE_WAIT leftover can also block bind without SO_REUSEADDR`, [ev], \"inferred\");\n } else {\n b.finding(\"info\", `no process visible to this user holds port ${port}`, [ev]);\n if (!isRoot()) b.limitation(\"other users' processes are invisible without root; try: sudo lsof -nP -iTCP:\" + port);\n }\n return b.build();\n}\n\nexport const lsofAdapter: Adapter = {\n name: \"lsof\",\n tools: [\"inspect_sockets\", \"inspect_files\"],\n platforms: [\"linux\", \"darwin\"],\n requires: [\"lsof\"],\n install: { linux: \"sudo apt install lsof | sudo dnf install lsof\", darwin: \"preinstalled (/usr/sbin/lsof)\" },\n priority: 10,\n async run(tool, args, ctx) {\n if (tool === \"inspect_sockets\") return inspectSockets(ctx);\n if (tool === \"inspect_files\") return inspectFiles(ctx, args as { window_s?: number });\n throw new Error(`lsof adapter cannot run ${tool}`);\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAOA,SAAS,gBAAgB;AA2BlB,SAAS,gBAAgB,KAA0B;AACxD,QAAM,MAAmB,CAAC;AAC1B,MAAI,MAAM;AACV,MAAI,UAAU;AACd,MAAI;AACJ,MAAI,MAAiC;AACrC,QAAM,QAAQ,MAAM;AAClB,QAAI,OAAO,IAAI,OAAO,QAAW;AAC/B,YAAM,OAAO,IAAI,QAAQ;AACzB,UAAI,KAAK;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI,IAAI;AAAA,QACR,MAAM,IAAI,QAAQ;AAAA,QAClB,MAAM,IAAI;AAAA,QACV,OAAO,IAAI;AAAA,QACX;AAAA,QACA,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd,SAAS,cAAc,KAAK,IAAI,KAAK;AAAA,MACvC,CAAC;AAAA,IACH;AACA,UAAM;AAAA,EACR;AACA,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAClC,QAAI,CAAC,KAAM;AACX,UAAM,MAAM,KAAK,CAAC;AAClB,UAAM,MAAM,KAAK,MAAM,CAAC;AACxB,YAAQ,KAAK;AAAA,MACX,KAAK;AACH,cAAM;AACN,cAAM,OAAO,GAAG;AAChB;AAAA,MACF,KAAK;AACH,kBAAU;AACV;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK;AACH,cAAM;AACN,cAAM,EAAE,IAAI,IAAI;AAChB;AAAA,MACF,KAAK;AACH,YAAI,IAAK,KAAI,OAAO;AACpB;AAAA,MACF,KAAK;AACH,YAAI,IAAK,KAAI,OAAO,OAAO,GAAG;AAC9B;AAAA,MACF,KAAK;AACH,YAAI,IAAK,KAAI,QAAQ;AACrB;AAAA,MACF,KAAK;AACH,YAAI,IAAK,KAAI,OAAO;AACpB;AAAA,MACF,KAAK;AACH,YAAI,QAAQ,QAAQ,SAAS,QAAQ,OAAQ,KAAI,WAAW;AAC5D;AAAA,MACF,KAAK;AACH,YAAI,OAAO,IAAI,WAAW,KAAK,EAAG,KAAI,WAAW,IAAI,MAAM,CAAC;AAC5D;AAAA,MACF;AACE;AAAA,IACJ;AAAA,EACF;AACA,QAAM;AACN,SAAO;AACT;AAWA,SAAS,UAAU,GAA2C;AAC5D,QAAM,IAAI,EAAE,YAAY,GAAG;AAC3B,MAAI,IAAI,EAAG,QAAO,EAAE,MAAM,GAAG,MAAM,EAAE;AACrC,SAAO,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,MAAM,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC,KAAK,EAAE;AAClE;AAGO,SAAS,iBAAiB,SAAqC;AACpE,QAAM,YAAwC,CAAC;AAC/C,QAAM,UAAkC,CAAC;AACzC,QAAM,UAAU,oBAAI,IAA+D;AACnF,MAAI,QAAQ;AACZ,MAAI,OAAO;AACX,MAAI,MAAM;AACV,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,SAAS,QAAQ;AACrB;AACA;AAAA,IACF;AACA,QAAI,EAAE,SAAS,UAAU,EAAE,SAAS,OAAQ;AAC5C;AACA,UAAM,QAAQ,EAAE,aAAa,EAAE,WAAW,QAAQ;AAClD,QAAI,UAAU,MAAO;AACrB,QAAI,EAAE,aAAa,UAAU;AAC3B,YAAM,EAAE,MAAM,KAAK,IAAI,UAAU,EAAE,IAAI;AACvC,gBAAU,KAAK,EAAE,KAAK,EAAE,KAAK,OAAO,MAAM,MAAM,KAAK,CAAC;AACtD;AAAA,IACF;AACA,QAAI,EAAE,SAAU,SAAQ,EAAE,QAAQ,KAAK,QAAQ,EAAE,QAAQ,KAAK,KAAK;AACnE,UAAM,QAAQ,EAAE,KAAK,QAAQ,IAAI;AACjC,QAAI,SAAS,GAAG;AACd,YAAM,SAAS,EAAE,KAAK,MAAM,QAAQ,CAAC;AACrC,YAAM,EAAE,KAAK,IAAI,UAAU,MAAM;AACjC,YAAM,IAAI,QAAQ,IAAI,IAAI,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAC,EAAE;AACtD,QAAE;AACF,UAAI,EAAE,SAAU,GAAE,OAAO,EAAE,QAAQ,KAAK,EAAE,OAAO,EAAE,QAAQ,KAAK,KAAK;AACrE,cAAQ,IAAI,MAAM,CAAC;AAAA,IACrB;AAAA,EACF;AACA,QAAM,WAAW,CAAC,GAAG,OAAO,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,EAAE,EAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACvG,SAAO,EAAE,WAAW,SAAS,UAAU,eAAe,OAAO,cAAc,MAAM,IAAI;AACvF;AAWO,SAAS,eAAe,SAAmC;AAChE,QAAM,SAAiC,CAAC;AACxC,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,OAAO,KAAK,EAAE,EAAE,CAAC;AACvD,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK;AACtD,aAAW,KAAK,QAAS,QAAO,EAAE,QAAQ,GAAG,KAAK,OAAO,EAAE,QAAQ,GAAG,KAAK,KAAK;AAChF,QAAM,UAAU,CAAC,GAAG,OAAO,EACxB,OAAO,CAAC,MAAM,OAAO,EAAE,SAAS,QAAQ,EACxC,KAAK,CAAC,GAAG,OAAO,EAAE,QAAQ,MAAM,EAAE,QAAQ,EAAE,EAC5C,MAAM,GAAG,EAAE,EACX,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,KAAK,EAAE;AAC7D,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,KAAK,EAAE;AAC3G,SAAO,EAAE,QAAQ,WAAW,QAAQ,QAAQ,SAAS,SAAS,eAAe,QAAQ,OAAO;AAC9F;AAGO,SAAS,gBAAgB,MAA4D;AAC1F,QAAM,IAAI,mCAAmC,KAAK,IAAI;AACtD,MAAI,CAAC,EAAG,QAAO,EAAE,MAAM,MAAM,MAAM,KAAK;AACxC,QAAM,IAAI,CAAC,MAAe,MAAM,cAAc,WAAW,OAAO,CAAC;AACjE,SAAO,EAAE,MAAM,EAAE,EAAE,CAAC,CAAE,GAAG,MAAM,EAAE,EAAE,CAAC,CAAE,EAAE;AAC1C;AAMA,eAAe,QAAQ,MAAgB,QAAkB,CAAC,GAA4F;AAEpJ,QAAM,OAAO,CAAC,OAAO,MAAM,MAAM,MAAM,eAAe,GAAG,KAAK;AAC9D,MAAI,KAAK,OAAQ,MAAK,KAAK,MAAM,KAAK,KAAK,GAAG,CAAC;AAC/C,QAAM,IAAI,MAAM,UAAU,QAAQ,MAAM,EAAE,WAAW,IAAO,CAAC;AAC7D,SAAO,EAAE,SAAS,gBAAgB,EAAE,MAAM,GAAG,QAAQ,EAAE,QAAQ,UAAU,EAAE,UAAU,KAAK,EAAE,OAAO;AACrG;AAEA,eAAe,cAAc,KAAoF;AAC/G,MAAI,SAAS,MAAM,SAAS;AAC1B,QAAI;AACF,aAAO,EAAE,GAAG,gBAAgB,MAAM,SAAS,SAAS,GAAG,WAAW,MAAM,CAAC,GAAG,QAAQ,SAAS,GAAG,UAAU;AAAA,IAC5G,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,SAAS,MAAM,UAAU;AAC3B,UAAM,IAAI,MAAM,UAAU,aAAa,CAAC,SAAS,UAAU,GAAG,EAAE,WAAW,IAAK,CAAC;AACjF,UAAM,IAAI,2BAA2B,KAAK,EAAE,MAAM;AAClD,QAAI,GAAG;AACL,YAAM,IAAI,CAAC,MAAe,MAAM,cAAc,WAAW,OAAO,CAAC;AACjE,aAAO,EAAE,MAAM,EAAE,EAAE,CAAC,CAAE,GAAG,MAAM,EAAE,EAAE,CAAC,CAAE,GAAG,QAAQ,yEAAyE;AAAA,IAC5H;AAAA,EACF;AACA,SAAO,EAAE,MAAM,MAAM,MAAM,MAAM,QAAQ,UAAU;AACrD;AAEA,eAAe,eAAe,KAA0C;AACtE,QAAM,EAAE,SAAS,OAAO,IAAI;AAC5B,QAAM,IAAI,IAAI,cAAc,mBAAmB,SAAS,MAAM;AAC9D,QAAM,OAAO,WAAW,MAAM;AAC9B,MAAI,CAAC,KAAK,OAAQ,QAAO,EAAE,KAAK,gDAAgD,EAAE,MAAM;AACxF,QAAM,EAAE,SAAS,QAAQ,IAAI,IAAI,MAAM,QAAQ,MAAM,CAAC,IAAI,CAAC;AAC3D,QAAM,MAAM,MAAM,EAAE,cAAc,oBAAoB,KAAK,YAAY,kBAAkB,KAAK,KAAK,GAAG,CAAC,EAAE;AACzG,MAAI,QAAQ,WAAW,KAAK,yBAAyB,MAAM,GAAG;AAC5D,MAAE,WAAW,uBAAuB,mBAAmB,kBAAkB,KAAK,KAAK,GAAG,CAAC,EAAE,CAAC;AAC1F,WAAO,EAAE,KAAK,0DAA0D,EAAE,MAAM;AAAA,EAClF;AACA,QAAM,IAAI,iBAAiB,OAAO;AAClC,QAAM,WAAW,EAAE;AAAA,IACjB;AAAA,IACA,EAAE,UAAU,SAAS,cAAc,EAAE,UAAU,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,SAAS,EAAE,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,KAAK;AAAA,IAC1H,EAAE;AAAA,IACF,EAAE,UAAU,IAAI;AAAA,EAClB;AACA,QAAM,UAAU,EAAE,WAAW,kBAAkB,GAAG,EAAE,aAAa,oBAAoB,OAAO,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,GAAG,KAAK,kBAAkB,SAAS,EAAE,GAAG,UAAU,EAAE,YAAY,IAAI,EAAE,UAAU,EAAE,SAAS,OAAO,EAAE,eAAe,KAAK,EAAE,KAAK,MAAM,EAAE,aAAa,GAAG,EAAE,UAAU,IAAI,CAAC;AACzT,QAAM,WAAW,EAAE,WAAW,UAAU,EAAE,SAAS,SAAS,gBAAgB,EAAE,SAAS,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,QAAK,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,CAAC,KAAK,yBAAyB,EAAE,SAAS,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,IAAI,CAAC;AAGhS,QAAM,MAAM,MAAM,QAAQ,IAAI;AAC9B,QAAM,QAAQ,eAAe,IAAI,OAAO;AACxC,QAAM,MAAM,MAAM,cAAc,KAAK,CAAC,CAAE;AACxC,QAAM,OAAO,EAAE,WAAW,cAAc,GAAG,MAAM,SAAS,qCAAqC,IAAI,QAAQ,GAAG,SAAS,IAAI,QAAQ,GAAG,KAAK,IAAI,MAAM,KAAK,EAAE,KAAK,MAAM,WAAW,MAAM,IAAI,SAAS,WAAW,cAAc,IAAI,MAAM,MAAM,IAAI,SAAS,WAAW,cAAc,IAAI,MAAM,QAAQ,IAAI,OAAO,CAAC;AAGlT,MAAI,EAAE,UAAU,OAAQ,GAAE,QAAQ,QAAQ,gBAAgB,EAAE,UAAU,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;AACjI,QAAM,KAAK,EAAE,QAAQ,cAAc;AACnC,MAAI,MAAM,GAAI,GAAE,QAAQ,QAAQ,GAAG,EAAE,8HAAyH,CAAC,OAAO,GAAG,UAAU;AAAA,WAC1K,MAAM,EAAG,GAAE,QAAQ,QAAQ,GAAG,EAAE,iGAA4F,CAAC,OAAO,GAAG,UAAU;AAC1J,QAAM,KAAK,EAAE,QAAQ,aAAa;AAClC,MAAI,MAAM,IAAK,GAAE,QAAQ,QAAQ,GAAG,EAAE,4FAAuF,CAAC,OAAO,GAAG,UAAU;AAClJ,QAAM,MAAM,EAAE,QAAQ,YAAY;AAClC,MAAI,OAAO,EAAG,GAAE,QAAQ,QAAQ,GAAG,GAAG,mFAA8E,CAAC,SAAS,QAAQ,GAAG,UAAU;AACnJ,QAAM,MAAM,EAAE,SAAS,CAAC;AACxB,MAAI,OAAO,IAAI,SAAS,GAAI,GAAE,QAAQ,QAAQ,GAAG,IAAI,KAAK,mBAAmB,IAAI,MAAM,uDAAkD,CAAC,QAAQ,GAAG,UAAU;AAC/J,MAAI,IAAI,QAAQ,IAAI,SAAS,YAAY,MAAM,YAAY,IAAI,OAAO,IAAK,GAAE,QAAQ,QAAQ,GAAG,MAAM,SAAS,2BAA2B,IAAI,IAAI,KAAK,KAAK,MAAO,MAAM,MAAM,YAAa,IAAI,IAAI,CAAC,6BAAwB,CAAC,IAAI,CAAC;AACnO,MAAI,CAAC,EAAE,UAAU,UAAU,CAAC,EAAE,SAAS,OAAQ,GAAE,QAAQ,QAAQ,mCAAmC,CAAC,OAAO,CAAC;AAE7G,MAAI,SAAS,MAAM,YAAY,CAAC,OAAO,EAAG,GAAE,WAAW,uEAAuE;AAC9H,MAAI,KAAK,SAAS,EAAG,GAAE,WAAW,YAAY,KAAK,MAAM,8BAA8B;AACvF,IAAE,KAAK,iBAAiB,kBAAkB,EAAE,SAAS,SAAS,oCAAoC,aAAa;AAC/G,SAAO,EAAE,MAAM;AACjB;AAEA,eAAe,aAAa,KAAqB,MAAkD;AACjG,QAAM,EAAE,SAAS,OAAO,IAAI;AAC5B,QAAM,IAAI,IAAI,cAAc,iBAAiB,SAAS,MAAM;AAC5D,QAAM,OAAO,WAAW,MAAM;AAC9B,MAAI,CAAC,KAAK,OAAQ,QAAO,EAAE,KAAK,8CAA8C,EAAE,MAAM;AACtF,QAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,MAAI,MAAM,QAAQ,WAAW,KAAK,yBAAyB,MAAM,MAAM,GAAG;AACxE,MAAE,WAAW,uBAAuB,iBAAiB,eAAe,KAAK,KAAK,GAAG,CAAC,EAAE,CAAC;AACrF,WAAO,EAAE,KAAK,0DAA0D,EAAE,MAAM;AAAA,EAClF;AACA,QAAM,MAAM,MAAM,EAAE,cAAc,kBAAkB,MAAM,KAAK,YAAY,eAAe,KAAK,KAAK,GAAG,CAAC,EAAE;AAC1G,QAAM,KAAK,eAAe,MAAM,OAAO;AACvC,QAAM,SAAS,KAAK,IAAI,KAAK,YAAY,GAAG,EAAE;AAC9C,MAAI;AACJ,MAAI,SAAS,GAAG;AACd,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,SAAS,GAAI,CAAC;AACrD,UAAM,SAAS,MAAM,QAAQ,IAAI;AACjC,SAAK,eAAe,OAAO,OAAO;AAAA,EACpC;AACA,QAAM,SAAS,EAAE,WAAW,cAAc,GAAG,GAAG,SAAS,WAAW,OAAO,QAAQ,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,GAAG,CAAC,IAAI,OAAO,QAAQ,GAAG,MAAM,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,UAAU,IAAI,CAAC;AAC5P,QAAM,MAAM,MAAM,cAAc,KAAK,CAAC,CAAE;AACxC,QAAM,QAAQ,EAAE,WAAW,UAAU,wBAAwB,IAAI,SAAS,WAAW,cAAc,IAAI,QAAQ,GAAG,SAAS,IAAI,SAAS,WAAW,cAAc,IAAI,QAAQ,GAAG,KAAK,IAAI,MAAM,KAAK,EAAE,MAAM,iBAAiB,OAAO,IAAI,SAAS,WAAW,KAAK,IAAI,MAAM,MAAM,IAAI,SAAS,WAAW,KAAK,IAAI,MAAM,KAAK,GAAG,UAAU,CAAC;AAC3U,MAAI,GAAG,QAAQ,QAAQ;AACrB,MAAE,WAAW,MAAM,uBAAuB,GAAG,QAAQ,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,IAAI,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC,IAAI,GAAG,SAAS,EAAE,UAAU,IAAI,CAAC;AAAA,EAC7K;AACA,MAAI;AACJ,MAAI,IAAI;AACN,eAAW,EAAE,WAAW,iBAAiB,OAAO,GAAG,SAAS,WAAM,GAAG,SAAS,SAAS,MAAM,MAAM,GAAG,YAAY,GAAG,aAAa,IAAI,MAAM,EAAE,GAAG,GAAG,YAAY,GAAG,SAAS,KAAK,EAAE,MAAM,YAAY,OAAO,GAAG,WAAW,OAAO,GAAG,WAAW,KAAK,GAAG,WAAW,UAAU,OAAO,CAAC;AAAA,EACtR;AACA,MAAI,GAAG,QAAQ,QAAQ;AACrB,UAAM,QAAQ,EAAE,WAAW,MAAM,GAAG,GAAG,QAAQ,MAAM,8BAA8B,WAAW,GAAG,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,GAAG,QAAQ,MAAM,GAAG,EAAE,GAAG,EAAE,UAAU,IAAI,CAAC;AAC5L,MAAE,QAAQ,QAAQ,GAAG,GAAG,QAAQ,MAAM,0CAA0C,WAAW,GAAG,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,iDAA4C,CAAC,KAAK,CAAC;AAAA,EAC3L;AACA,MAAI,MAAM,GAAG,YAAY,GAAG,aAAa,KAAK,IAAI,GAAG,GAAG,YAAY,GAAG,GAAG;AACxE,MAAE,QAAQ,QAAQ,iBAAiB,GAAG,SAAS,WAAM,GAAG,SAAS,OAAO,MAAM,oBAAe,CAAC,QAAS,GAAG,UAAU;AAAA,EACtH;AACA,MAAI,IAAI,QAAQ,IAAI,SAAS,YAAY,GAAG,YAAY,IAAI,OAAO,IAAK,GAAE,QAAQ,QAAQ,GAAG,GAAG,SAAS,sBAAsB,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC;AAC1J,MAAI,GAAG,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,OAAO,QAAQ,EAAG,GAAE,QAAQ,QAAQ,wBAAwB,WAAW,GAAG,QAAQ,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,WAAW,MAAM,gBAAgB,GAAG,QAAQ,CAAC,GAAG,EAAE,UAAU,IAAI,CAAC,CAAC,CAAC;AAC9N,MAAI,EAAE,iBAAiB,EAAG,GAAE,QAAQ,QAAQ,GAAG,GAAG,SAAS,2BAA2B,OAAO,QAAQ,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;AACxK,MAAI,SAAS,MAAM,SAAU,GAAE,WAAW,iFAAiF;AAC3H,IAAE,KAAK,mBAAmB,gBAAgB;AAC1C,SAAO,EAAE,MAAM;AACjB;AAGA,eAAsB,aAAa,MAAmC;AACpE,QAAM,SAAiB,EAAE,MAAM,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,QAAQ;AACxE,QAAM,IAAI,IAAI,cAAc,mBAAmB,MAAM,MAAM;AAC3D,MAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,MAAO,QAAO,EAAE,KAAK,gBAAgB,IAAI,EAAE,EAAE,MAAM;AACrG,QAAM,CAAC,KAAK,GAAG,IAAI,MAAM,QAAQ,IAAI;AAAA,IACnC,UAAU,QAAQ,CAAC,OAAO,MAAM,MAAM,eAAe,SAAS,IAAI,EAAE,GAAG,EAAE,WAAW,KAAO,CAAC;AAAA,IAC5F,UAAU,QAAQ,CAAC,OAAO,MAAM,MAAM,eAAe,SAAS,IAAI,EAAE,GAAG,EAAE,WAAW,KAAO,CAAC;AAAA,EAC9F,CAAC;AACD,QAAM,UAAU,CAAC,GAAG,gBAAgB,IAAI,MAAM,GAAG,GAAG,gBAAgB,IAAI,MAAM,CAAC;AAC/E,QAAM,UAAU,oBAAI,IAAgG;AACpH,aAAW,KAAK,SAAS;AACvB,UAAM,IAAI,QAAQ,IAAI,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,SAAS,MAAM,EAAE,MAAM,QAAQ,CAAC,GAAG,OAAO,CAAC,EAAE;AACtG,QAAI,EAAE,SAAU,GAAE,OAAO,KAAK,EAAE,QAAQ;AACxC,MAAE,MAAM,KAAK,EAAE,IAAI;AACnB,YAAQ,IAAI,EAAE,KAAK,CAAC;AAAA,EACtB;AACA,QAAM,OAAO,CAAC,GAAG,QAAQ,OAAO,CAAC;AACjC,QAAM,KAAK,EAAE,WAAW,UAAU,KAAK,SAAS,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,MAAM,OAAO,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK,CAAC,GAAG,IAAI,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,KAAK,GAAG,EAAE,KAAK,IAAI,CAAC,KAAK,sBAAsB,IAAI,2BAA2B,IAAI;AACrO,QAAM,WAAW,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,SAAS,QAAQ,CAAC;AAC7D,MAAI,UAAU;AACZ,UAAM,EAAE,YAAY,IAAI,MAAM,OAAO,sBAAc;AACnD,UAAM,OAAO,MAAM,YAAY,SAAS,GAAG;AAC3C,UAAM,MAAM,EAAE,WAAW,WAAW,OAAO,SAAS,GAAG,KAAK,MAAM,QAAQ,SAAS,OAAO,UAAU,MAAM,QAAQ,SAAS,QAAQ,GAAG,KAAK,QAAQ,QAAQ;AAC3J,MAAE,QAAQ,QAAQ,QAAQ,IAAI,mBAAmB,SAAS,GAAG,KAAK,SAAS,OAAO,GAAG,MAAM,OAAO,UAAU,KAAK,IAAI,KAAK,EAAE,eAAe,CAAC,IAAI,GAAG,GAAG,YAAY,YAAY,MAAM,QAAQ,GAAG,qDAAqD;AAAA,EACtP,WAAW,KAAK,QAAQ;AACtB,MAAE,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,KAAK,MAAM,4CAA4C,CAAC,GAAG,IAAI,IAAI,KAAK,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,KAAK,qFAAgF,CAAC,EAAE,GAAG,UAAU;AAAA,EAC7P,OAAO;AACL,MAAE,QAAQ,QAAQ,8CAA8C,IAAI,IAAI,CAAC,EAAE,CAAC;AAC5E,QAAI,CAAC,OAAO,EAAG,GAAE,WAAW,iFAAiF,IAAI;AAAA,EACnH;AACA,SAAO,EAAE,MAAM;AACjB;AAEO,IAAM,cAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO,CAAC,mBAAmB,eAAe;AAAA,EAC1C,WAAW,CAAC,SAAS,QAAQ;AAAA,EAC7B,UAAU,CAAC,MAAM;AAAA,EACjB,SAAS,EAAE,OAAO,mDAAmD,QAAQ,gCAAgC;AAAA,EAC7G,UAAU;AAAA,EACV,MAAM,IAAI,MAAM,MAAM,KAAK;AACzB,QAAI,SAAS,kBAAmB,QAAO,eAAe,GAAG;AACzD,QAAI,SAAS,gBAAiB,QAAO,aAAa,KAAK,IAA6B;AACpF,UAAM,IAAI,MAAM,2BAA2B,IAAI,EAAE;AAAA,EACnD;AACF;","names":[]}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/safety.ts
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { createWriteStream } from "fs";
|
|
6
|
+
import { stat } from "fs/promises";
|
|
7
|
+
var LIMITS = {
|
|
8
|
+
MAX_TOOL_WALL_MS: 6e4,
|
|
9
|
+
MAX_CAPTURE_WALL_MS: 3e4,
|
|
10
|
+
MAX_RAW_BYTES: 50 * 1024 * 1024,
|
|
11
|
+
MAX_PAYLOAD_BYTES: 16 * 1024,
|
|
12
|
+
MAX_EXCERPT_LINES: 40,
|
|
13
|
+
/** Grace between SIGTERM and SIGKILL. */
|
|
14
|
+
KILL_GRACE_MS: 2e3,
|
|
15
|
+
/** Extra slack on top of the requested duration before we SIGTERM the child. */
|
|
16
|
+
DURATION_SLACK_MS: 5e3
|
|
17
|
+
};
|
|
18
|
+
var CONSENT_REQUIRED = /* @__PURE__ */ new Set(["capture_traffic"]);
|
|
19
|
+
var ConsentRequiredError = class extends Error {
|
|
20
|
+
constructor(tool) {
|
|
21
|
+
super(`${tool} requires consent: true. Ask the user before passing it \u2014 the output may contain secrets or affect the target.`);
|
|
22
|
+
this.name = "ConsentRequiredError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function requireConsent(tool, args) {
|
|
26
|
+
if (CONSENT_REQUIRED.has(tool) && args.consent !== true) throw new ConsentRequiredError(tool);
|
|
27
|
+
}
|
|
28
|
+
function clampDuration(seconds, fallback, maxMs = LIMITS.MAX_TOOL_WALL_MS) {
|
|
29
|
+
const max = Math.floor(maxMs / 1e3);
|
|
30
|
+
const v = seconds ?? fallback;
|
|
31
|
+
if (!Number.isFinite(v) || v < 1) return 1;
|
|
32
|
+
return Math.min(Math.floor(v), max);
|
|
33
|
+
}
|
|
34
|
+
function assertPid(pid) {
|
|
35
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > 4194304) {
|
|
36
|
+
throw new Error(`invalid pid: ${String(pid)}`);
|
|
37
|
+
}
|
|
38
|
+
return pid;
|
|
39
|
+
}
|
|
40
|
+
function assertSafeIdentifier(s, what = "identifier") {
|
|
41
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,253}$/.test(s)) throw new Error(`unsafe ${what}: ${JSON.stringify(s)}`);
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
function assertSafeBpfFragment(s) {
|
|
45
|
+
if (s.length > 200) throw new Error("filter too long");
|
|
46
|
+
if (!/^[A-Za-z0-9 .:_\-()&|!=<>[\]]+$/.test(s)) throw new Error(`unsafe filter: ${JSON.stringify(s)}`);
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
var children = /* @__PURE__ */ new Set();
|
|
50
|
+
var exitHooked = false;
|
|
51
|
+
function hookExit() {
|
|
52
|
+
if (exitHooked) return;
|
|
53
|
+
exitHooked = true;
|
|
54
|
+
const killAll = () => {
|
|
55
|
+
for (const c of children) {
|
|
56
|
+
try {
|
|
57
|
+
c.kill("SIGTERM");
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
process.once("exit", killAll);
|
|
63
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
64
|
+
process.once(sig, () => {
|
|
65
|
+
killAll();
|
|
66
|
+
process.exit(130);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function killTrackedChildren() {
|
|
71
|
+
let n = 0;
|
|
72
|
+
for (const c of children) {
|
|
73
|
+
try {
|
|
74
|
+
c.kill("SIGTERM");
|
|
75
|
+
n++;
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
children.clear();
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
async function runCapped(file, args, opts) {
|
|
83
|
+
hookExit();
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
const timeoutMs = Math.min(opts.timeoutMs, LIMITS.MAX_TOOL_WALL_MS + LIMITS.DURATION_SLACK_MS);
|
|
86
|
+
const useFiles = Boolean(opts.stdoutFile || opts.stderrFile);
|
|
87
|
+
const captureInMemory = opts.captureInMemory ?? !useFiles;
|
|
88
|
+
const execaOpts = {
|
|
89
|
+
reject: false,
|
|
90
|
+
timeout: timeoutMs,
|
|
91
|
+
killSignal: "SIGTERM",
|
|
92
|
+
forceKillAfterDelay: LIMITS.KILL_GRACE_MS,
|
|
93
|
+
cwd: opts.cwd,
|
|
94
|
+
env: opts.env,
|
|
95
|
+
input: opts.input,
|
|
96
|
+
stdin: opts.input === void 0 ? "ignore" : "pipe",
|
|
97
|
+
buffer: false,
|
|
98
|
+
stdout: "pipe",
|
|
99
|
+
stderr: "pipe",
|
|
100
|
+
all: false,
|
|
101
|
+
stripFinalNewline: false
|
|
102
|
+
};
|
|
103
|
+
const child = execa(file, args, execaOpts);
|
|
104
|
+
children.add(child);
|
|
105
|
+
opts.track?.(child);
|
|
106
|
+
let bytes = 0;
|
|
107
|
+
let outputCapped = false;
|
|
108
|
+
const outChunks = [];
|
|
109
|
+
const errChunks = [];
|
|
110
|
+
const streams = [];
|
|
111
|
+
const outFile = opts.stdoutFile ? createWriteStream(opts.stdoutFile) : null;
|
|
112
|
+
const errFile = opts.stderrFile ? createWriteStream(opts.stderrFile) : null;
|
|
113
|
+
if (outFile) streams.push(outFile);
|
|
114
|
+
if (errFile) streams.push(errFile);
|
|
115
|
+
const onChunk = (chunk, memory, sink) => {
|
|
116
|
+
bytes += chunk.length;
|
|
117
|
+
if (bytes > LIMITS.MAX_RAW_BYTES) {
|
|
118
|
+
if (!outputCapped) {
|
|
119
|
+
outputCapped = true;
|
|
120
|
+
try {
|
|
121
|
+
child.kill("SIGTERM");
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (sink) sink.write(chunk);
|
|
128
|
+
if (captureInMemory) memory.push(chunk);
|
|
129
|
+
};
|
|
130
|
+
child.stdout?.on("data", (c) => onChunk(c, outChunks, outFile));
|
|
131
|
+
child.stderr?.on("data", (c) => onChunk(c, errChunks, errFile));
|
|
132
|
+
const result = await child;
|
|
133
|
+
children.delete(child);
|
|
134
|
+
await Promise.all(
|
|
135
|
+
streams.map(
|
|
136
|
+
(s) => new Promise((res) => {
|
|
137
|
+
s.end(() => res());
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
|
|
143
|
+
signal: result.signal ?? null,
|
|
144
|
+
stdout: Buffer.concat(outChunks).toString("utf8"),
|
|
145
|
+
stderr: Buffer.concat(errChunks).toString("utf8"),
|
|
146
|
+
timedOut: Boolean(result.timedOut),
|
|
147
|
+
outputCapped,
|
|
148
|
+
bytesWritten: bytes,
|
|
149
|
+
durationMs: Date.now() - started,
|
|
150
|
+
command: [file, ...args].join(" ")
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function fileSize(path) {
|
|
154
|
+
try {
|
|
155
|
+
return (await stat(path)).size;
|
|
156
|
+
} catch {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function isRoot() {
|
|
161
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
162
|
+
}
|
|
163
|
+
function rootRequiredLimitation(tool, command) {
|
|
164
|
+
return `${tool} needs elevated privileges and Debug Anything never escalates on its own. Run it yourself (sudo ${command}), save the output to a file, then: debuganything import <tool> <file> --session <id>`;
|
|
165
|
+
}
|
|
166
|
+
function looksLikePermissionError(stderr) {
|
|
167
|
+
return /operation not permitted|permission denied|ptrace.*(denied|not permitted)|EPERM|must be (run as )?root|you don't have permission|not permitted to attach|lacks permission|requires? (super ?user|root)/i.test(
|
|
168
|
+
stderr
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
var REDACTIONS = [
|
|
172
|
+
[/(authorization\s*[:=]\s*)?bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, "Bearer [REDACTED]"],
|
|
173
|
+
[/AKIA[0-9A-Z]{16}/g, "AKIA[REDACTED]"],
|
|
174
|
+
[/(aws_secret_access_key|aws_session_token)\s*[=:]\s*\S+/gi, "$1=[REDACTED]"],
|
|
175
|
+
[/(password|passwd|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key)(\s*[=:]\s*)(["']?)[^\s"'&;,]{4,}\3/gi, "$1$2$3[REDACTED]$3"],
|
|
176
|
+
[/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, "[REDACTED_JWT]"],
|
|
177
|
+
[/(sk|pk|rk)_(live|test)_[A-Za-z0-9]{10,}/g, "$1_$2_[REDACTED]"],
|
|
178
|
+
[/gh[pousr]_[A-Za-z0-9]{20,}/g, "gh*_[REDACTED]"],
|
|
179
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/g, "xox*-[REDACTED]"],
|
|
180
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]"],
|
|
181
|
+
[/(https?:\/\/)([^\s:@/]+):([^\s@/]+)@/gi, "$1$2:[REDACTED]@"]
|
|
182
|
+
];
|
|
183
|
+
function redact(text) {
|
|
184
|
+
let out = text;
|
|
185
|
+
for (const [re, rep] of REDACTIONS) out = out.replace(re, rep);
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function redactDeep(value) {
|
|
189
|
+
if (typeof value === "string") return redact(value);
|
|
190
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v));
|
|
191
|
+
if (value && typeof value === "object") {
|
|
192
|
+
const out = {};
|
|
193
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
function capPayload(result, max = LIMITS.MAX_PAYLOAD_BYTES) {
|
|
199
|
+
const size = (o) => Buffer.byteLength(JSON.stringify(o), "utf8");
|
|
200
|
+
if (size(result) <= max) return result;
|
|
201
|
+
const r = structuredClone(result);
|
|
202
|
+
let trimmed = false;
|
|
203
|
+
const cited = new Set((r.findings ?? []).flatMap((f) => f.evidence_ids));
|
|
204
|
+
for (let pass = 0; pass < 40 && size(r) > max; pass++) {
|
|
205
|
+
const byLen = r.evidence.map((e2, i) => ({ i, n: size(e2.data) })).sort((a, b) => b.n - a.n);
|
|
206
|
+
const top = byLen[0];
|
|
207
|
+
if (!top || top.n < 64) break;
|
|
208
|
+
const e = r.evidence[top.i];
|
|
209
|
+
if (Array.isArray(e.data) && e.data.length > 5) e.data = e.data.slice(0, Math.max(5, Math.floor(e.data.length / 2)));
|
|
210
|
+
else if (Array.isArray(e.data)) e.data = { _trimmed: true, kept: e.data.length, note: "see artifact" };
|
|
211
|
+
else if (typeof e.data === "string") e.data = e.data.slice(0, Math.max(64, Math.floor(e.data.length / 2))) + "\u2026";
|
|
212
|
+
else if (e.data && typeof e.data === "object") e.data = { _trimmed: true, keys: Object.keys(e.data).slice(0, 20) };
|
|
213
|
+
else break;
|
|
214
|
+
trimmed = true;
|
|
215
|
+
}
|
|
216
|
+
if (size(r) > max) {
|
|
217
|
+
const keep = r.evidence.filter((e, i) => i < 10 || e.id && cited.has(e.id));
|
|
218
|
+
if (keep.length < r.evidence.length) {
|
|
219
|
+
r.evidence = keep;
|
|
220
|
+
trimmed = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (trimmed) r.limitations.push(`payload trimmed to fit the ${Math.floor(max / 1024)} KB cap; full data is in the artifacts`);
|
|
224
|
+
return r;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export {
|
|
228
|
+
LIMITS,
|
|
229
|
+
ConsentRequiredError,
|
|
230
|
+
requireConsent,
|
|
231
|
+
clampDuration,
|
|
232
|
+
assertPid,
|
|
233
|
+
assertSafeIdentifier,
|
|
234
|
+
assertSafeBpfFragment,
|
|
235
|
+
killTrackedChildren,
|
|
236
|
+
runCapped,
|
|
237
|
+
fileSize,
|
|
238
|
+
isRoot,
|
|
239
|
+
rootRequiredLimitation,
|
|
240
|
+
looksLikePermissionError,
|
|
241
|
+
redact,
|
|
242
|
+
redactDeep,
|
|
243
|
+
capPayload
|
|
244
|
+
};
|
|
245
|
+
//# sourceMappingURL=chunk-STFC7C5C.js.map
|