@blacksandscyber/mcp-server-bursar 0.5.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.
Files changed (68) hide show
  1. package/README.md +230 -0
  2. package/build/config.d.ts +45 -0
  3. package/build/config.js +177 -0
  4. package/build/http-transport.d.ts +16 -0
  5. package/build/http-transport.js +191 -0
  6. package/build/index.d.ts +16 -0
  7. package/build/index.js +31 -0
  8. package/build/server.d.ts +41 -0
  9. package/build/server.js +902 -0
  10. package/build/shared/errors.d.ts +50 -0
  11. package/build/shared/errors.js +69 -0
  12. package/build/shared/linkBuilder.d.ts +93 -0
  13. package/build/shared/linkBuilder.js +148 -0
  14. package/build/shared/logger.d.ts +10 -0
  15. package/build/shared/logger.js +28 -0
  16. package/build/shield/bootRole.d.ts +60 -0
  17. package/build/shield/bootRole.js +145 -0
  18. package/build/shield/client.d.ts +265 -0
  19. package/build/shield/client.js +656 -0
  20. package/build/shield/deploy/index.d.ts +69 -0
  21. package/build/shield/deploy/index.js +569 -0
  22. package/build/shield/discovery/dataStoreDetector.d.ts +3 -0
  23. package/build/shield/discovery/dataStoreDetector.js +125 -0
  24. package/build/shield/discovery/dockerScanner.d.ts +34 -0
  25. package/build/shield/discovery/dockerScanner.js +543 -0
  26. package/build/shield/discovery/endpointScanner.d.ts +3 -0
  27. package/build/shield/discovery/endpointScanner.js +306 -0
  28. package/build/shield/discovery/environmentScanner.d.ts +86 -0
  29. package/build/shield/discovery/environmentScanner.js +545 -0
  30. package/build/shield/discovery/externalServiceDetector.d.ts +3 -0
  31. package/build/shield/discovery/externalServiceDetector.js +98 -0
  32. package/build/shield/discovery/frameworkDetector.d.ts +3 -0
  33. package/build/shield/discovery/frameworkDetector.js +114 -0
  34. package/build/shield/discovery/manifestGenerator.d.ts +12 -0
  35. package/build/shield/discovery/manifestGenerator.js +124 -0
  36. package/build/shield/discovery/piiDetector.d.ts +5 -0
  37. package/build/shield/discovery/piiDetector.js +203 -0
  38. package/build/shield/discovery/severity.d.ts +47 -0
  39. package/build/shield/discovery/severity.js +138 -0
  40. package/build/shield/discovery/topologyNormalizer.d.ts +109 -0
  41. package/build/shield/discovery/topologyNormalizer.js +416 -0
  42. package/build/shield/identity.d.ts +53 -0
  43. package/build/shield/identity.js +70 -0
  44. package/build/shield/install/configMerge.d.ts +91 -0
  45. package/build/shield/install/configMerge.js +324 -0
  46. package/build/shield/install/keystore.d.ts +25 -0
  47. package/build/shield/install/keystore.js +156 -0
  48. package/build/shield/install/orchestrator.d.ts +33 -0
  49. package/build/shield/install/orchestrator.js +404 -0
  50. package/build/shield/install/transports/awsSsm.d.ts +43 -0
  51. package/build/shield/install/transports/awsSsm.js +378 -0
  52. package/build/shield/install/transports/bootstrapToken.d.ts +39 -0
  53. package/build/shield/install/transports/bootstrapToken.js +117 -0
  54. package/build/shield/install/transports/ssh.d.ts +50 -0
  55. package/build/shield/install/transports/ssh.js +569 -0
  56. package/build/shield/install/types.d.ts +139 -0
  57. package/build/shield/install/types.js +10 -0
  58. package/build/shield/protocol-walkthrough.d.ts +65 -0
  59. package/build/shield/protocol-walkthrough.js +392 -0
  60. package/build/shield/provision/appProvisioner.d.ts +15 -0
  61. package/build/shield/provision/appProvisioner.js +25 -0
  62. package/build/shield/types.d.ts +261 -0
  63. package/build/shield/types.js +4 -0
  64. package/build/shield/verify/postureReporter.d.ts +4 -0
  65. package/build/shield/verify/postureReporter.js +31 -0
  66. package/dxt/blacksands-ca.crt +67 -0
  67. package/dxt/scripts/setup.js +520 -0
  68. package/package.json +76 -0
@@ -0,0 +1,545 @@
1
+ "use strict";
2
+ /**
3
+ * macOS environment scanner — Phase 1, READ-ONLY host inspection.
4
+ *
5
+ * Produces the raw infra-plane material (host + networks + containers + volumes
6
+ * + candidate service nodes) that {@link topologyNormalizer} folds into the
7
+ * frozen topology envelope. The vocabulary here is deliberately neutral; the
8
+ * normalizer is responsible for conforming to the viz contract.
9
+ *
10
+ * SECURITY MODEL (hard requirements):
11
+ * - Only the three allowlisted binaries below are ever executed.
12
+ * - Every spawn uses execFile with an ARGUMENT ARRAY — never a shell string,
13
+ * never string-interpolation of any value. There is nothing untrusted to
14
+ * interpolate (all args are literals), but we keep the discipline anyway.
15
+ * - Every spawn has a timeout. A missing binary, a non-zero exit, malformed
16
+ * output, or a timeout DEGRADES GRACEFULLY: we return whatever we have plus
17
+ * a human-readable note. The tool must never crash.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.scanMacEnvironment = scanMacEnvironment;
54
+ const node_child_process_1 = require("node:child_process");
55
+ const os = __importStar(require("node:os"));
56
+ const logger_1 = require("../../shared/logger");
57
+ // ── Binary allowlist ────────────────────────────────────────────────────────
58
+ // Apple Containerization CLI, plus two read-only port inspectors.
59
+ const ALLOWED_BINARIES = ["container", "lsof", "netstat"];
60
+ const EXEC_TIMEOUT_MS = 8_000;
61
+ const MAX_BUFFER = 8 * 1024 * 1024; // 8 MB — container inspect on a busy host
62
+ function runAllowed(binary, args) {
63
+ // Defensive: the type system already constrains `binary`, but assert at
64
+ // runtime so a future refactor can't smuggle in an arbitrary command.
65
+ if (!ALLOWED_BINARIES.includes(binary)) {
66
+ return Promise.resolve({ ok: false, stdout: "", stderr: "", error: `binary not allowlisted: ${binary}` });
67
+ }
68
+ return new Promise((resolve) => {
69
+ (0, node_child_process_1.execFile)(binary, args, { timeout: EXEC_TIMEOUT_MS, maxBuffer: MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
70
+ if (err) {
71
+ // ENOENT → binary absent. Other errors → ran but failed/timed out.
72
+ const isMissing = err.code === "ENOENT";
73
+ resolve({
74
+ ok: false,
75
+ stdout: stdout ?? "",
76
+ stderr: stderr ?? "",
77
+ error: isMissing ? "missing" : err.message,
78
+ });
79
+ return;
80
+ }
81
+ resolve({ ok: true, stdout: stdout ?? "", stderr: stderr ?? "", error: null });
82
+ });
83
+ });
84
+ }
85
+ /** Best-effort JSON parse; returns null instead of throwing. */
86
+ function tryParseJson(text) {
87
+ const trimmed = text.trim();
88
+ if (!trimmed)
89
+ return null;
90
+ try {
91
+ return JSON.parse(trimmed);
92
+ }
93
+ catch {
94
+ // Some CLIs emit newline-delimited JSON objects (one per line). Try that.
95
+ const lines = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
96
+ const out = [];
97
+ for (const line of lines) {
98
+ try {
99
+ out.push(JSON.parse(line));
100
+ }
101
+ catch {
102
+ return null; // not NDJSON either — caller falls back to text parsing
103
+ }
104
+ }
105
+ return out.length ? out : null;
106
+ }
107
+ }
108
+ // Pull a string field by trying several candidate key names (CLIs differ).
109
+ function pick(obj, keys) {
110
+ for (const k of keys) {
111
+ const v = obj[k];
112
+ if (typeof v === "string" && v.length)
113
+ return v;
114
+ if (typeof v === "number")
115
+ return String(v);
116
+ }
117
+ return null;
118
+ }
119
+ function asRecord(v) {
120
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
121
+ }
122
+ // ── Host facts (pure Node, no subprocess) ────────────────────────────────────
123
+ function collectHostFacts(containerCliPresent) {
124
+ const cpus = os.cpus() ?? [];
125
+ const totalMemBytes = os.totalmem();
126
+ const ifaces = os.networkInterfaces();
127
+ const interfaces = [];
128
+ for (const [name, addrs] of Object.entries(ifaces)) {
129
+ for (const a of addrs ?? []) {
130
+ if (a.internal)
131
+ continue; // skip loopback
132
+ const fam = a.family;
133
+ interfaces.push({
134
+ name,
135
+ address: a.address,
136
+ family: typeof fam === "number" ? `IPv${fam}` : fam,
137
+ cidr: a.cidr ?? null,
138
+ });
139
+ }
140
+ }
141
+ return {
142
+ hostname: os.hostname(),
143
+ arch: os.arch(),
144
+ platform: os.platform(),
145
+ release: os.release(),
146
+ vcpu: cpus.length,
147
+ cpuModel: cpus[0]?.model?.trim() ?? "unknown",
148
+ totalMemBytes,
149
+ totalMemGB: `${Math.round(totalMemBytes / 1024 ** 3)} GB`,
150
+ interfaces,
151
+ containerCliPresent,
152
+ };
153
+ }
154
+ // ── Apple `container` CLI parsing ────────────────────────────────────────────
155
+ //
156
+ // The Apple Containerization `container` CLI is young and its `--format json`
157
+ // support varies by version, so each parser tries JSON first and falls back to
158
+ // whitespace-delimited text. We never assume a fixed column layout beyond the
159
+ // first token being an id/name.
160
+ async function listContainers(notes) {
161
+ // `--all` so stopped containers appear (the viz shows stopped nodes).
162
+ const json = await runAllowed("container", ["ls", "--all", "--format", "json"]);
163
+ if (json.ok) {
164
+ const parsed = tryParseJson(json.stdout);
165
+ const arr = Array.isArray(parsed) ? parsed : parsed != null ? [parsed] : [];
166
+ if (arr.length) {
167
+ const out = [];
168
+ for (const raw of arr) {
169
+ const rec = asRecord(raw);
170
+ if (!rec)
171
+ continue;
172
+ const id = pick(rec, ["id", "ID", "Id"]) ?? pick(rec, ["name", "Name"]) ?? "";
173
+ if (!id)
174
+ continue;
175
+ const name = pick(rec, ["name", "Name"]) ?? id;
176
+ const statusText = (pick(rec, ["status", "Status", "state", "State"]) ?? "unknown").toLowerCase();
177
+ out.push({
178
+ id,
179
+ name,
180
+ image: pick(rec, ["image", "Image"]),
181
+ status: statusText,
182
+ running: statusText.includes("run") || statusText.includes("up"),
183
+ ip: pick(rec, ["ip", "IP", "address", "Address"]),
184
+ networks: extractNetworkNames(rec),
185
+ volumes: [],
186
+ ports: extractPorts(rec),
187
+ command: pick(rec, ["command", "Command", "cmd"]),
188
+ });
189
+ }
190
+ if (out.length) {
191
+ await enrichContainers(out, notes);
192
+ return out;
193
+ }
194
+ }
195
+ // JSON requested but unparseable — fall through to text mode.
196
+ notes.push("container ls --format json returned unparseable output; used text fallback");
197
+ }
198
+ else if (json.error === "missing") {
199
+ notes.push("`container` CLI not found — container/network/volume discovery skipped");
200
+ return [];
201
+ }
202
+ else {
203
+ notes.push(`container ls failed (${json.error}); attempting text fallback`);
204
+ }
205
+ // Text fallback: `container ls --all` columnar output. First column = id/name.
206
+ const text = await runAllowed("container", ["ls", "--all"]);
207
+ if (!text.ok) {
208
+ if (text.error === "missing") {
209
+ notes.push("`container` CLI not found — container discovery skipped");
210
+ }
211
+ else {
212
+ notes.push(`container ls (text) failed (${text.error}); no containers discovered`);
213
+ }
214
+ return [];
215
+ }
216
+ const containers = parseContainerTable(text.stdout);
217
+ await enrichContainers(containers, notes);
218
+ return containers;
219
+ }
220
+ function extractNetworkNames(rec) {
221
+ const v = rec["networks"] ?? rec["Networks"] ?? rec["network"] ?? rec["Network"];
222
+ if (Array.isArray(v)) {
223
+ return v.map((n) => (typeof n === "string" ? n : asRecord(n) ? pick(asRecord(n), ["name", "Name"]) : null))
224
+ .filter((x) => !!x);
225
+ }
226
+ if (typeof v === "string" && v)
227
+ return [v];
228
+ return [];
229
+ }
230
+ function extractPorts(rec) {
231
+ const v = rec["ports"] ?? rec["Ports"] ?? rec["portMappings"] ?? rec["PortMappings"];
232
+ if (Array.isArray(v)) {
233
+ return v.map((p) => (typeof p === "string" ? p : asRecord(p) ? JSON.stringify(p) : null))
234
+ .filter((x) => !!x);
235
+ }
236
+ if (typeof v === "string" && v)
237
+ return [v];
238
+ return [];
239
+ }
240
+ function parseContainerTable(stdout) {
241
+ const lines = stdout.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim().length);
242
+ if (lines.length === 0)
243
+ return [];
244
+ // Drop a header row if the first token looks like a column label.
245
+ const startIdx = /^(id|name|container)\b/i.test(lines[0]) ? 1 : 0;
246
+ const out = [];
247
+ for (let i = startIdx; i < lines.length; i++) {
248
+ const cols = lines[i].split(/\s{2,}|\t/).map((c) => c.trim()).filter(Boolean);
249
+ if (!cols.length)
250
+ continue;
251
+ const id = cols[0];
252
+ // Heuristic: find an image-looking token (contains ':' or '/') and a
253
+ // status-looking token (running/stopped/exited/created).
254
+ const image = cols.find((c) => /[:/]/.test(c) && c !== id) ?? null;
255
+ const statusTok = cols.find((c) => /^(running|stopped|exited|created|paused|up)/i.test(c)) ?? "unknown";
256
+ const status = statusTok.toLowerCase();
257
+ out.push({
258
+ id,
259
+ name: cols[cols.length - 1] && cols[cols.length - 1] !== id ? cols[cols.length - 1] : id,
260
+ image,
261
+ status,
262
+ running: status.includes("run") || status.includes("up"),
263
+ ip: cols.find((c) => /^\d{1,3}(\.\d{1,3}){3}$/.test(c)) ?? null,
264
+ networks: [],
265
+ volumes: [],
266
+ ports: [],
267
+ command: null,
268
+ });
269
+ }
270
+ return out;
271
+ }
272
+ /**
273
+ * Best-effort per-container enrichment via `container inspect <id>`. Fills in
274
+ * networks/volumes/ip/command/ports when the list view didn't. Failures are
275
+ * silent per-container (we already have a node from the list).
276
+ */
277
+ async function enrichContainers(containers, notes) {
278
+ let inspectFailures = 0;
279
+ for (const c of containers) {
280
+ // c.id is derived from CLI output. With execFile there is no shell, so this
281
+ // is not command injection — but an id beginning with "-" would be parsed as
282
+ // a flag by `container`. Reject such ids and terminate option parsing with --.
283
+ if (!c.id || /^-/.test(c.id)) {
284
+ inspectFailures++;
285
+ continue;
286
+ }
287
+ const res = await runAllowed("container", ["inspect", "--", c.id]);
288
+ if (!res.ok) {
289
+ inspectFailures++;
290
+ continue;
291
+ }
292
+ const parsed = tryParseJson(res.stdout);
293
+ const rec = Array.isArray(parsed) ? asRecord(parsed[0]) : asRecord(parsed);
294
+ if (!rec)
295
+ continue;
296
+ const config = asRecord(rec["config"] ?? rec["Config"]) ?? rec;
297
+ const networkSettings = asRecord(rec["networks"] ?? rec["Networks"] ?? rec["NetworkSettings"]);
298
+ if (!c.image)
299
+ c.image = pick(config, ["image", "Image"]);
300
+ if (!c.command)
301
+ c.command = pick(config, ["command", "Command", "cmd", "Cmd"]) ?? c.command;
302
+ if (!c.ip)
303
+ c.ip = pick(rec, ["ip", "IP", "address", "Address"]);
304
+ const nets = extractNetworkNames(rec);
305
+ if (nets.length)
306
+ c.networks = nets;
307
+ else if (networkSettings) {
308
+ const keys = Object.keys(networkSettings);
309
+ if (keys.length)
310
+ c.networks = keys;
311
+ }
312
+ const vols = rec["volumes"] ?? rec["Volumes"] ?? rec["Mounts"] ?? rec["mounts"];
313
+ if (Array.isArray(vols)) {
314
+ c.volumes = vols
315
+ .map((v) => {
316
+ if (typeof v === "string")
317
+ return v;
318
+ const vr = asRecord(v);
319
+ return vr ? pick(vr, ["name", "Name", "source", "Source", "destination", "Destination"]) : null;
320
+ })
321
+ .filter((x) => !!x);
322
+ }
323
+ const ports = extractPorts(rec);
324
+ if (ports.length)
325
+ c.ports = ports;
326
+ }
327
+ if (inspectFailures && inspectFailures === containers.length) {
328
+ notes.push("container inspect unavailable for all containers; node detail is partial");
329
+ }
330
+ }
331
+ async function listNetworks(notes, containerCliPresent) {
332
+ if (!containerCliPresent)
333
+ return [];
334
+ const json = await runAllowed("container", ["network", "ls", "--format", "json"]);
335
+ let arr = [];
336
+ if (json.ok) {
337
+ const parsed = tryParseJson(json.stdout);
338
+ arr = Array.isArray(parsed) ? parsed : parsed != null ? [parsed] : [];
339
+ }
340
+ if (arr.length) {
341
+ const out = [];
342
+ for (const raw of arr) {
343
+ const rec = asRecord(raw);
344
+ if (!rec)
345
+ continue;
346
+ const name = pick(rec, ["name", "Name"]) ?? pick(rec, ["id", "ID"]);
347
+ if (!name)
348
+ continue;
349
+ out.push({
350
+ id: pick(rec, ["id", "ID"]) ?? name,
351
+ name,
352
+ subnet: pick(rec, ["subnet", "Subnet", "cidr", "CIDR"]),
353
+ gateway: pick(rec, ["gateway", "Gateway"]),
354
+ driver: pick(rec, ["driver", "Driver", "mode", "Mode"]),
355
+ });
356
+ }
357
+ if (out.length)
358
+ return out;
359
+ }
360
+ // Text fallback.
361
+ const text = await runAllowed("container", ["network", "ls"]);
362
+ if (!text.ok) {
363
+ if (text.error !== "missing")
364
+ notes.push(`container network ls failed (${text.error})`);
365
+ return [];
366
+ }
367
+ const lines = text.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
368
+ const start = /^(name|id|network)\b/i.test(lines[0] ?? "") ? 1 : 0;
369
+ const out = [];
370
+ for (let i = start; i < lines.length; i++) {
371
+ const cols = lines[i].split(/\s{2,}|\t/).map((c) => c.trim()).filter(Boolean);
372
+ if (!cols.length)
373
+ continue;
374
+ out.push({
375
+ id: cols[0],
376
+ name: cols[0],
377
+ subnet: cols.find((c) => /\d+\.\d+\.\d+\.\d+\/\d+/.test(c)) ?? null,
378
+ gateway: null,
379
+ driver: null,
380
+ });
381
+ }
382
+ return out;
383
+ }
384
+ async function listVolumes(notes, containerCliPresent) {
385
+ if (!containerCliPresent)
386
+ return [];
387
+ const json = await runAllowed("container", ["volume", "ls", "--format", "json"]);
388
+ let arr = [];
389
+ if (json.ok) {
390
+ const parsed = tryParseJson(json.stdout);
391
+ arr = Array.isArray(parsed) ? parsed : parsed != null ? [parsed] : [];
392
+ }
393
+ if (arr.length) {
394
+ const out = [];
395
+ for (const raw of arr) {
396
+ const rec = asRecord(raw);
397
+ if (!rec)
398
+ continue;
399
+ const name = pick(rec, ["name", "Name"]) ?? pick(rec, ["id", "ID"]);
400
+ if (!name)
401
+ continue;
402
+ out.push({
403
+ id: pick(rec, ["id", "ID"]) ?? name,
404
+ name,
405
+ driver: pick(rec, ["driver", "Driver"]),
406
+ source: pick(rec, ["source", "Source", "mountpoint", "Mountpoint"]),
407
+ });
408
+ }
409
+ if (out.length)
410
+ return out;
411
+ }
412
+ const text = await runAllowed("container", ["volume", "ls"]);
413
+ if (!text.ok) {
414
+ if (text.error !== "missing")
415
+ notes.push(`container volume ls failed (${text.error})`);
416
+ return [];
417
+ }
418
+ const lines = text.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
419
+ const start = /^(name|id|volume|driver)\b/i.test(lines[0] ?? "") ? 1 : 0;
420
+ const out = [];
421
+ for (let i = start; i < lines.length; i++) {
422
+ const cols = lines[i].split(/\s{2,}|\t/).map((c) => c.trim()).filter(Boolean);
423
+ if (!cols.length)
424
+ continue;
425
+ out.push({ id: cols[0], name: cols[cols.length - 1] || cols[0], driver: null, source: null });
426
+ }
427
+ return out;
428
+ }
429
+ // ── Listening ports (lsof primary, netstat fallback) ─────────────────────────
430
+ async function listListeningPorts(notes) {
431
+ // -nP: numeric host+port (no DNS / service-name resolution, faster + safer).
432
+ const lsofRes = await runAllowed("lsof", ["-iTCP", "-sTCP:LISTEN", "-nP"]);
433
+ if (lsofRes.ok) {
434
+ return parseLsof(lsofRes.stdout);
435
+ }
436
+ if (lsofRes.error !== "missing") {
437
+ notes.push(`lsof failed (${lsofRes.error}); trying netstat`);
438
+ }
439
+ const netRes = await runAllowed("netstat", ["-anv", "-p", "tcp"]);
440
+ if (!netRes.ok) {
441
+ notes.push(netRes.error === "missing"
442
+ ? "neither lsof nor netstat available — listening ports not discovered"
443
+ : `netstat failed (${netRes.error}) — listening ports not discovered`);
444
+ return [];
445
+ }
446
+ return parseNetstat(netRes.stdout);
447
+ }
448
+ function parseLsof(stdout) {
449
+ // Columns: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
450
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
451
+ const seen = new Set();
452
+ const out = [];
453
+ for (const line of lines) {
454
+ if (/^COMMAND\b/i.test(line))
455
+ continue;
456
+ const cols = line.split(/\s+/);
457
+ if (cols.length < 9)
458
+ continue;
459
+ const name = cols[cols.length - 1]; // e.g. "127.0.0.1:5432" or "*:8080"
460
+ const m = name.match(/^(.*):(\d+)$/);
461
+ if (!m)
462
+ continue;
463
+ const port = Number(m[2]);
464
+ if (!Number.isInteger(port))
465
+ continue;
466
+ const address = m[1] || "*";
467
+ const key = `${address}:${port}`;
468
+ if (seen.has(key))
469
+ continue;
470
+ seen.add(key);
471
+ const pid = Number(cols[1]);
472
+ out.push({
473
+ port,
474
+ proto: "tcp",
475
+ address,
476
+ process: cols[0] || null,
477
+ pid: Number.isInteger(pid) ? pid : null,
478
+ });
479
+ }
480
+ return out;
481
+ }
482
+ function parseNetstat(stdout) {
483
+ // macOS netstat -anv -p tcp: ... Local Address ... state. LISTEN rows only.
484
+ const lines = stdout.split("\n");
485
+ const seen = new Set();
486
+ const out = [];
487
+ for (const line of lines) {
488
+ if (!/\bLISTEN\b/.test(line))
489
+ continue;
490
+ const cols = line.trim().split(/\s+/);
491
+ // Local address is typically column index 3 on macOS (tcp4/tcp6 layouts).
492
+ const localCol = cols.find((c) => /\.\d+$/.test(c)) ?? cols[3];
493
+ if (!localCol)
494
+ continue;
495
+ const m = localCol.match(/^(.*)\.(\d+)$/); // macOS uses host.port notation
496
+ if (!m)
497
+ continue;
498
+ const port = Number(m[2]);
499
+ if (!Number.isInteger(port))
500
+ continue;
501
+ const address = m[1] || "*";
502
+ const key = `${address}:${port}`;
503
+ if (seen.has(key))
504
+ continue;
505
+ seen.add(key);
506
+ out.push({ port, proto: "tcp", address, process: null, pid: null });
507
+ }
508
+ return out;
509
+ }
510
+ // ── Public entry point ───────────────────────────────────────────────────────
511
+ /**
512
+ * Inspect the local macOS host READ-ONLY and return the raw environment. Never
513
+ * throws: any failure is captured in `notes` and the corresponding section is
514
+ * returned empty/partial.
515
+ */
516
+ async function scanMacEnvironment() {
517
+ const notes = [];
518
+ // Probe `container` once so host facts and the list calls agree on presence.
519
+ const probe = await runAllowed("container", ["--version"]);
520
+ const containerCliPresent = probe.ok;
521
+ if (!containerCliPresent && probe.error === "missing") {
522
+ notes.push("Apple `container` CLI not installed — infra plane limited to host + listening services");
523
+ }
524
+ const host = collectHostFacts(containerCliPresent);
525
+ // Run the independent inspections; each is internally graceful.
526
+ let containers = [];
527
+ let networks = [];
528
+ let volumes = [];
529
+ let listeningPorts = [];
530
+ try {
531
+ [containers, networks, volumes, listeningPorts] = await Promise.all([
532
+ listContainers(notes),
533
+ listNetworks(notes, containerCliPresent),
534
+ listVolumes(notes, containerCliPresent),
535
+ listListeningPorts(notes),
536
+ ]);
537
+ }
538
+ catch (err) {
539
+ // Belt-and-suspenders: the helpers already swallow their own errors.
540
+ logger_1.logger.warn("scanMacEnvironment: unexpected error during inspection", { err: String(err) });
541
+ notes.push(`environment scan partially failed: ${String(err)}`);
542
+ }
543
+ return { host, networks, containers, volumes, listeningPorts, notes };
544
+ }
545
+ //# sourceMappingURL=environmentScanner.js.map
@@ -0,0 +1,3 @@
1
+ import type { ExternalService } from "../types";
2
+ export declare function detectExternalServices(projectPath: string): Promise<ExternalService[]>;
3
+ //# sourceMappingURL=externalServiceDetector.d.ts.map
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.detectExternalServices = detectExternalServices;
37
+ /** Detect external third-party service integrations. */
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const glob_1 = require("glob");
41
+ const SERVICE_SIGNATURES = {
42
+ stripe: { name: "Stripe", domain: "api.stripe.com", port: 443, protocol: "https", purpose: "payments" },
43
+ "@stripe/stripe-js": { name: "Stripe", domain: "api.stripe.com", port: 443, protocol: "https", purpose: "payments" },
44
+ twilio: { name: "Twilio", domain: "api.twilio.com", port: 443, protocol: "https", purpose: "communications" },
45
+ sendgrid: { name: "SendGrid", domain: "api.sendgrid.com", port: 443, protocol: "https", purpose: "email" },
46
+ "@sendgrid/mail": { name: "SendGrid", domain: "api.sendgrid.com", port: 443, protocol: "https", purpose: "email" },
47
+ "@aws-sdk": { name: "AWS", domain: "amazonaws.com", port: 443, protocol: "https", purpose: "cloud_services" },
48
+ boto3: { name: "AWS", domain: "amazonaws.com", port: 443, protocol: "https", purpose: "cloud_services" },
49
+ firebase: { name: "Firebase", domain: "firebaseio.com", port: 443, protocol: "https", purpose: "backend_services" },
50
+ openai: { name: "OpenAI", domain: "api.openai.com", port: 443, protocol: "https", purpose: "ai_services" },
51
+ "@anthropic-ai/sdk": { name: "Anthropic", domain: "api.anthropic.com", port: 443, protocol: "https", purpose: "ai_services" },
52
+ resend: { name: "Resend", domain: "api.resend.com", port: 443, protocol: "https", purpose: "email" },
53
+ plaid: { name: "Plaid", domain: "production.plaid.com", port: 443, protocol: "https", purpose: "fintech" },
54
+ };
55
+ async function detectExternalServices(projectPath) {
56
+ const services = new Map();
57
+ // package.json
58
+ const pkgPath = path.join(projectPath, "package.json");
59
+ if (fs.existsSync(pkgPath)) {
60
+ try {
61
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
62
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
63
+ for (const [dep, svc] of Object.entries(SERVICE_SIGNATURES)) {
64
+ if (allDeps[dep])
65
+ services.set(svc.name, { ...svc, detectedFrom: "package.json" });
66
+ }
67
+ }
68
+ catch { /* ignore */ }
69
+ }
70
+ // requirements.txt
71
+ const reqPath = path.join(projectPath, "requirements.txt");
72
+ if (fs.existsSync(reqPath)) {
73
+ const content = fs.readFileSync(reqPath, "utf8").toLowerCase();
74
+ for (const [dep, svc] of Object.entries(SERVICE_SIGNATURES)) {
75
+ if (content.includes(dep.toLowerCase()))
76
+ services.set(svc.name, { ...svc, detectedFrom: "requirements.txt" });
77
+ }
78
+ }
79
+ // Source code URL patterns
80
+ const files = await (0, glob_1.glob)("**/*.{js,ts,py,jsx,tsx}", {
81
+ cwd: projectPath,
82
+ ignore: ["node_modules/**", "venv/**", ".venv/**", "dist/**", "build/**"],
83
+ absolute: true,
84
+ });
85
+ const urlPattern = /https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
86
+ for (const file of files.slice(0, 100)) {
87
+ const content = fs.readFileSync(file, "utf8");
88
+ let match;
89
+ while ((match = urlPattern.exec(content)) !== null) {
90
+ const domain = match[1];
91
+ if (!domain.includes("localhost") && !domain.includes("127.0.0.1") && !domain.includes("example.com") && !services.has(domain)) {
92
+ services.set(domain, { name: domain, domain, port: 443, protocol: "https", purpose: "external_api", detectedFrom: path.relative(projectPath, file) });
93
+ }
94
+ }
95
+ }
96
+ return Array.from(services.values());
97
+ }
98
+ //# sourceMappingURL=externalServiceDetector.js.map
@@ -0,0 +1,3 @@
1
+ import type { FrameworkInfo } from "../types";
2
+ export declare function detectFramework(projectPath: string): Promise<FrameworkInfo>;
3
+ //# sourceMappingURL=frameworkDetector.d.ts.map