sonobat 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2546 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import Database from "better-sqlite3";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/db/schema.ts
8
+ var SCHEMA_SQL = `
9
+ PRAGMA foreign_keys = ON;
10
+
11
+ -- ============================================================
12
+ -- \u5B9F\u884C\u5358\u4F4D\uFF08\u4EFB\u610F\uFF09
13
+ -- ============================================================
14
+ CREATE TABLE IF NOT EXISTS scans (
15
+ id TEXT PRIMARY KEY,
16
+ started_at TEXT NOT NULL,
17
+ finished_at TEXT,
18
+ notes TEXT
19
+ );
20
+
21
+ -- ============================================================
22
+ -- \u751F\u51FA\u529B\uFF08\u30D5\u30A1\u30A4\u30EB\uFF09\u53C2\u7167
23
+ -- ============================================================
24
+ CREATE TABLE IF NOT EXISTS artifacts (
25
+ id TEXT PRIMARY KEY,
26
+ scan_id TEXT,
27
+ tool TEXT NOT NULL, -- "nmap" | "ffuf" | "nuclei"
28
+ kind TEXT NOT NULL, -- "tool_output" | "http_request" | "http_response"
29
+ path TEXT NOT NULL, -- \u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u30D1\u30B9 or URI
30
+ sha256 TEXT,
31
+ captured_at TEXT NOT NULL,
32
+ attrs_json TEXT, -- \u4EFB\u610F\u30E1\u30BF\uFF08\u30B3\u30DE\u30F3\u30C9\u3001\u5F15\u6570\u7B49\uFF09
33
+ FOREIGN KEY (scan_id) REFERENCES scans(id) ON DELETE SET NULL
34
+ );
35
+
36
+ CREATE INDEX IF NOT EXISTS idx_artifacts_tool ON artifacts(tool);
37
+
38
+ -- ============================================================
39
+ -- \u30DB\u30B9\u30C8
40
+ -- ============================================================
41
+ CREATE TABLE IF NOT EXISTS hosts (
42
+ id TEXT PRIMARY KEY,
43
+ authority_kind TEXT NOT NULL, -- "IP" | "DOMAIN"
44
+ authority TEXT NOT NULL UNIQUE, -- IP \u30A2\u30C9\u30EC\u30B9\u307E\u305F\u306F\u30C9\u30E1\u30A4\u30F3\u540D
45
+ resolved_ips_json TEXT NOT NULL DEFAULT '[]',
46
+ created_at TEXT NOT NULL,
47
+ updated_at TEXT NOT NULL
48
+ );
49
+
50
+ -- ============================================================
51
+ -- \u30D0\u30FC\u30C1\u30E3\u30EB\u30DB\u30B9\u30C8
52
+ -- ============================================================
53
+ CREATE TABLE IF NOT EXISTS vhosts (
54
+ id TEXT PRIMARY KEY,
55
+ host_id TEXT NOT NULL,
56
+ hostname TEXT NOT NULL,
57
+ source TEXT, -- "nmap" | "cert" | "header" | "manual"
58
+ evidence_artifact_id TEXT NOT NULL,
59
+ created_at TEXT NOT NULL,
60
+ UNIQUE (host_id, hostname),
61
+ FOREIGN KEY (host_id) REFERENCES hosts(id) ON DELETE CASCADE,
62
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
63
+ );
64
+
65
+ CREATE INDEX IF NOT EXISTS idx_vhosts_host ON vhosts(host_id);
66
+
67
+ -- ============================================================
68
+ -- \u30B5\u30FC\u30D3\u30B9
69
+ -- ============================================================
70
+ CREATE TABLE IF NOT EXISTS services (
71
+ id TEXT PRIMARY KEY,
72
+ host_id TEXT NOT NULL,
73
+ transport TEXT NOT NULL, -- "tcp" | "udp"
74
+ port INTEGER NOT NULL,
75
+ app_proto TEXT NOT NULL, -- "http" | "ssh" | "ftp" \u7B49
76
+ proto_confidence TEXT NOT NULL, -- "high" | "medium" | "low"
77
+ banner TEXT,
78
+ product TEXT,
79
+ version TEXT,
80
+ state TEXT NOT NULL, -- "open" | "closed" | "filtered"
81
+ evidence_artifact_id TEXT NOT NULL,
82
+ created_at TEXT NOT NULL,
83
+ updated_at TEXT NOT NULL,
84
+ UNIQUE (host_id, transport, port),
85
+ FOREIGN KEY (host_id) REFERENCES hosts(id) ON DELETE CASCADE,
86
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
87
+ );
88
+
89
+ CREATE INDEX IF NOT EXISTS idx_services_host ON services(host_id);
90
+
91
+ -- ============================================================
92
+ -- \u30B5\u30FC\u30D3\u30B9\u89B3\u6E2C\uFF08key-value\uFF09
93
+ -- ============================================================
94
+ CREATE TABLE IF NOT EXISTS service_observations (
95
+ id TEXT PRIMARY KEY,
96
+ service_id TEXT NOT NULL,
97
+ key TEXT NOT NULL,
98
+ value TEXT NOT NULL,
99
+ confidence TEXT NOT NULL, -- "high" | "medium" | "low"
100
+ evidence_artifact_id TEXT NOT NULL,
101
+ created_at TEXT NOT NULL,
102
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE,
103
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
104
+ );
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_svc_obs_service ON service_observations(service_id);
107
+
108
+ -- ============================================================
109
+ -- HTTP \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8
110
+ -- ============================================================
111
+ CREATE TABLE IF NOT EXISTS http_endpoints (
112
+ id TEXT PRIMARY KEY,
113
+ service_id TEXT NOT NULL,
114
+ vhost_id TEXT,
115
+ base_uri TEXT NOT NULL, -- "http://example.com:80"
116
+ method TEXT NOT NULL, -- "GET" | "POST" | ...
117
+ path TEXT NOT NULL, -- "/admin"\uFF08\u30AF\u30A8\u30EA\u306F\u542B\u3081\u306A\u3044\uFF09
118
+ status_code INTEGER,
119
+ content_length INTEGER,
120
+ words INTEGER,
121
+ lines INTEGER,
122
+ evidence_artifact_id TEXT NOT NULL,
123
+ created_at TEXT NOT NULL,
124
+ UNIQUE (service_id, method, path),
125
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE,
126
+ FOREIGN KEY (vhost_id) REFERENCES vhosts(id) ON DELETE SET NULL,
127
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
128
+ );
129
+
130
+ CREATE INDEX IF NOT EXISTS idx_endpoints_service ON http_endpoints(service_id);
131
+
132
+ -- ============================================================
133
+ -- \u5165\u529B\u30D1\u30E9\u30E1\u30FC\u30BF
134
+ -- ============================================================
135
+ CREATE TABLE IF NOT EXISTS inputs (
136
+ id TEXT PRIMARY KEY,
137
+ service_id TEXT NOT NULL,
138
+ location TEXT NOT NULL, -- "query" | "path" | "body" | "header" | "cookie"
139
+ name TEXT NOT NULL,
140
+ type_hint TEXT, -- "string" | "int" | "json" \u7B49\uFF08\u4EFB\u610F\uFF09
141
+ created_at TEXT NOT NULL,
142
+ updated_at TEXT NOT NULL,
143
+ UNIQUE (service_id, location, name),
144
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
145
+ );
146
+
147
+ CREATE INDEX IF NOT EXISTS idx_inputs_service ON inputs(service_id);
148
+
149
+ -- ============================================================
150
+ -- \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8 \u2194 \u5165\u529B\uFF08\u591A\u5BFE\u591A\uFF09
151
+ -- ============================================================
152
+ CREATE TABLE IF NOT EXISTS endpoint_inputs (
153
+ id TEXT PRIMARY KEY,
154
+ endpoint_id TEXT NOT NULL,
155
+ input_id TEXT NOT NULL,
156
+ evidence_artifact_id TEXT NOT NULL,
157
+ created_at TEXT NOT NULL,
158
+ UNIQUE (endpoint_id, input_id),
159
+ FOREIGN KEY (endpoint_id) REFERENCES http_endpoints(id) ON DELETE CASCADE,
160
+ FOREIGN KEY (input_id) REFERENCES inputs(id) ON DELETE CASCADE,
161
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
162
+ );
163
+
164
+ CREATE INDEX IF NOT EXISTS idx_ep_inputs_endpoint ON endpoint_inputs(endpoint_id);
165
+ CREATE INDEX IF NOT EXISTS idx_ep_inputs_input ON endpoint_inputs(input_id);
166
+
167
+ -- ============================================================
168
+ -- \u89B3\u6E2C\u5024
169
+ -- ============================================================
170
+ CREATE TABLE IF NOT EXISTS observations (
171
+ id TEXT PRIMARY KEY,
172
+ input_id TEXT NOT NULL,
173
+ raw_value TEXT NOT NULL,
174
+ norm_value TEXT NOT NULL,
175
+ body_path TEXT, -- JSON Pointer \u7B49\uFF08\u4F8B: "/user/name"\uFF09
176
+ source TEXT NOT NULL, -- "ffuf_url" | "req_query" | "req_body" | "manual"
177
+ confidence TEXT NOT NULL, -- "high" | "medium" | "low"
178
+ evidence_artifact_id TEXT NOT NULL,
179
+ observed_at TEXT NOT NULL,
180
+ FOREIGN KEY (input_id) REFERENCES inputs(id) ON DELETE CASCADE,
181
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
182
+ );
183
+
184
+ CREATE INDEX IF NOT EXISTS idx_obs_input ON observations(input_id);
185
+
186
+ -- ============================================================
187
+ -- \u8A8D\u8A3C\u60C5\u5831
188
+ -- ============================================================
189
+ CREATE TABLE IF NOT EXISTS credentials (
190
+ id TEXT PRIMARY KEY,
191
+ service_id TEXT NOT NULL,
192
+ endpoint_id TEXT, -- HTTP \u306E\u5834\u5408\u306E\u307F\uFF08\u4EFB\u610F\uFF09
193
+ username TEXT NOT NULL,
194
+ secret TEXT NOT NULL,
195
+ secret_type TEXT NOT NULL, -- "password" | "token" | "api_key" | "ssh_key"
196
+ source TEXT NOT NULL, -- "brute_force" | "default" | "leaked" | "manual"
197
+ confidence TEXT NOT NULL, -- "high" | "medium" | "low"
198
+ evidence_artifact_id TEXT NOT NULL,
199
+ created_at TEXT NOT NULL,
200
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE,
201
+ FOREIGN KEY (endpoint_id) REFERENCES http_endpoints(id) ON DELETE SET NULL,
202
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
203
+ );
204
+
205
+ CREATE INDEX IF NOT EXISTS idx_creds_service ON credentials(service_id);
206
+ CREATE INDEX IF NOT EXISTS idx_creds_endpoint ON credentials(endpoint_id);
207
+
208
+ -- ============================================================
209
+ -- \u8106\u5F31\u6027
210
+ -- ============================================================
211
+ CREATE TABLE IF NOT EXISTS vulnerabilities (
212
+ id TEXT PRIMARY KEY,
213
+ service_id TEXT NOT NULL,
214
+ endpoint_id TEXT, -- HTTP \u306E\u5834\u5408\u306E\u307F\uFF08\u4EFB\u610F\uFF09
215
+ vuln_type TEXT NOT NULL, -- "sqli" | "xss" | "rce" | "lfi" | "ssrf" | ...
216
+ title TEXT NOT NULL,
217
+ description TEXT,
218
+ severity TEXT NOT NULL, -- "critical" | "high" | "medium" | "low" | "info"
219
+ confidence TEXT NOT NULL, -- "high" | "medium" | "low"
220
+ evidence_artifact_id TEXT NOT NULL,
221
+ created_at TEXT NOT NULL,
222
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE,
223
+ FOREIGN KEY (endpoint_id) REFERENCES http_endpoints(id) ON DELETE SET NULL,
224
+ FOREIGN KEY (evidence_artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
225
+ );
226
+
227
+ CREATE INDEX IF NOT EXISTS idx_vulns_service ON vulnerabilities(service_id);
228
+ CREATE INDEX IF NOT EXISTS idx_vulns_endpoint ON vulnerabilities(endpoint_id);
229
+ CREATE INDEX IF NOT EXISTS idx_vulns_severity ON vulnerabilities(severity);
230
+
231
+ -- ============================================================
232
+ -- CVE \u60C5\u5831
233
+ -- ============================================================
234
+ CREATE TABLE IF NOT EXISTS cves (
235
+ id TEXT PRIMARY KEY,
236
+ vulnerability_id TEXT NOT NULL,
237
+ cve_id TEXT NOT NULL, -- "CVE-YYYY-NNNNN"
238
+ description TEXT,
239
+ cvss_score REAL,
240
+ cvss_vector TEXT,
241
+ reference_url TEXT,
242
+ created_at TEXT NOT NULL,
243
+ FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE
244
+ );
245
+
246
+ CREATE INDEX IF NOT EXISTS idx_cves_vuln ON cves(vulnerability_id);
247
+ CREATE INDEX IF NOT EXISTS idx_cves_cveid ON cves(cve_id);
248
+ `;
249
+
250
+ // src/db/migrate.ts
251
+ function migrateDatabase(db2) {
252
+ db2.pragma("foreign_keys = ON");
253
+ db2.exec(SCHEMA_SQL);
254
+ }
255
+
256
+ // src/mcp/server.ts
257
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
258
+
259
+ // src/mcp/tools/query.ts
260
+ import { z } from "zod";
261
+
262
+ // src/db/repository/host-repository.ts
263
+ import crypto from "crypto";
264
+ function rowToHost(row) {
265
+ return {
266
+ id: row.id,
267
+ authorityKind: row.authority_kind,
268
+ authority: row.authority,
269
+ resolvedIpsJson: row.resolved_ips_json,
270
+ createdAt: row.created_at,
271
+ updatedAt: row.updated_at
272
+ };
273
+ }
274
+ var HostRepository = class {
275
+ db;
276
+ constructor(db2) {
277
+ this.db = db2;
278
+ }
279
+ /** Insert a new Host and return the full entity. */
280
+ create(input) {
281
+ const id = crypto.randomUUID();
282
+ const now = (/* @__PURE__ */ new Date()).toISOString();
283
+ const stmt = this.db.prepare(
284
+ `INSERT INTO hosts (id, authority_kind, authority, resolved_ips_json, created_at, updated_at)
285
+ VALUES (?, ?, ?, ?, ?, ?)`
286
+ );
287
+ stmt.run(
288
+ id,
289
+ input.authorityKind,
290
+ input.authority,
291
+ input.resolvedIpsJson,
292
+ now,
293
+ now
294
+ );
295
+ return {
296
+ id,
297
+ authorityKind: input.authorityKind,
298
+ authority: input.authority,
299
+ resolvedIpsJson: input.resolvedIpsJson,
300
+ createdAt: now,
301
+ updatedAt: now
302
+ };
303
+ }
304
+ /** Find a Host by its primary key. Returns undefined if not found. */
305
+ findById(id) {
306
+ const stmt = this.db.prepare(
307
+ `SELECT id, authority_kind, authority, resolved_ips_json, created_at, updated_at
308
+ FROM hosts
309
+ WHERE id = ?`
310
+ );
311
+ const row = stmt.get(id);
312
+ return row ? rowToHost(row) : void 0;
313
+ }
314
+ /** Return all Hosts. */
315
+ findAll() {
316
+ const stmt = this.db.prepare(
317
+ `SELECT id, authority_kind, authority, resolved_ips_json, created_at, updated_at
318
+ FROM hosts`
319
+ );
320
+ const rows = stmt.all();
321
+ return rows.map(rowToHost);
322
+ }
323
+ /** Find a Host by its unique authority value. Returns undefined if not found. */
324
+ findByAuthority(authority) {
325
+ const stmt = this.db.prepare(
326
+ `SELECT id, authority_kind, authority, resolved_ips_json, created_at, updated_at
327
+ FROM hosts
328
+ WHERE authority = ?`
329
+ );
330
+ const row = stmt.get(authority);
331
+ return row ? rowToHost(row) : void 0;
332
+ }
333
+ /**
334
+ * Update an existing Host with the provided fields.
335
+ * Always bumps `updated_at`. Returns the updated entity, or undefined if
336
+ * the Host was not found.
337
+ */
338
+ update(id, input) {
339
+ const setClauses = [];
340
+ const params = [];
341
+ if (input.resolvedIpsJson !== void 0) {
342
+ setClauses.push("resolved_ips_json = ?");
343
+ params.push(input.resolvedIpsJson);
344
+ }
345
+ const now = (/* @__PURE__ */ new Date()).toISOString();
346
+ setClauses.push("updated_at = ?");
347
+ params.push(now);
348
+ params.push(id);
349
+ const sql = `UPDATE hosts SET ${setClauses.join(", ")} WHERE id = ?`;
350
+ const stmt = this.db.prepare(sql);
351
+ const result = stmt.run(...params);
352
+ if (result.changes === 0) {
353
+ return void 0;
354
+ }
355
+ return this.findById(id);
356
+ }
357
+ /** Delete a Host by id. Returns true if a row was deleted. */
358
+ delete(id) {
359
+ const stmt = this.db.prepare("DELETE FROM hosts WHERE id = ?");
360
+ const result = stmt.run(id);
361
+ return result.changes > 0;
362
+ }
363
+ };
364
+
365
+ // src/db/repository/service-repository.ts
366
+ import crypto2 from "crypto";
367
+ function rowToService(row) {
368
+ return {
369
+ id: row.id,
370
+ hostId: row.host_id,
371
+ transport: row.transport,
372
+ port: row.port,
373
+ appProto: row.app_proto,
374
+ protoConfidence: row.proto_confidence,
375
+ banner: row.banner ?? void 0,
376
+ product: row.product ?? void 0,
377
+ version: row.version ?? void 0,
378
+ state: row.state,
379
+ evidenceArtifactId: row.evidence_artifact_id,
380
+ createdAt: row.created_at,
381
+ updatedAt: row.updated_at
382
+ };
383
+ }
384
+ var ServiceRepository = class {
385
+ db;
386
+ constructor(db2) {
387
+ this.db = db2;
388
+ }
389
+ /** Insert a new Service and return the full entity. */
390
+ create(input) {
391
+ const id = crypto2.randomUUID();
392
+ const now = (/* @__PURE__ */ new Date()).toISOString();
393
+ const stmt = this.db.prepare(
394
+ `INSERT INTO services (id, host_id, transport, port, app_proto, proto_confidence, banner, product, version, state, evidence_artifact_id, created_at, updated_at)
395
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
396
+ );
397
+ stmt.run(
398
+ id,
399
+ input.hostId,
400
+ input.transport,
401
+ input.port,
402
+ input.appProto,
403
+ input.protoConfidence,
404
+ input.banner ?? null,
405
+ input.product ?? null,
406
+ input.version ?? null,
407
+ input.state,
408
+ input.evidenceArtifactId,
409
+ now,
410
+ now
411
+ );
412
+ return {
413
+ id,
414
+ hostId: input.hostId,
415
+ transport: input.transport,
416
+ port: input.port,
417
+ appProto: input.appProto,
418
+ protoConfidence: input.protoConfidence,
419
+ banner: input.banner,
420
+ product: input.product,
421
+ version: input.version,
422
+ state: input.state,
423
+ evidenceArtifactId: input.evidenceArtifactId,
424
+ createdAt: now,
425
+ updatedAt: now
426
+ };
427
+ }
428
+ /** Find a Service by its primary key. Returns undefined if not found. */
429
+ findById(id) {
430
+ const stmt = this.db.prepare(
431
+ `SELECT id, host_id, transport, port, app_proto, proto_confidence, banner, product, version, state, evidence_artifact_id, created_at, updated_at
432
+ FROM services
433
+ WHERE id = ?`
434
+ );
435
+ const row = stmt.get(id);
436
+ return row ? rowToService(row) : void 0;
437
+ }
438
+ /** Find all Services belonging to a given host. */
439
+ findByHostId(hostId) {
440
+ const stmt = this.db.prepare(
441
+ `SELECT id, host_id, transport, port, app_proto, proto_confidence, banner, product, version, state, evidence_artifact_id, created_at, updated_at
442
+ FROM services
443
+ WHERE host_id = ?`
444
+ );
445
+ const rows = stmt.all(hostId);
446
+ return rows.map(rowToService);
447
+ }
448
+ /**
449
+ * Update an existing Service with the provided fields.
450
+ * Always bumps `updated_at`. Returns the updated entity, or undefined if
451
+ * the Service was not found.
452
+ */
453
+ update(id, input) {
454
+ const setClauses = [];
455
+ const params = [];
456
+ if (input.appProto !== void 0) {
457
+ setClauses.push("app_proto = ?");
458
+ params.push(input.appProto);
459
+ }
460
+ if (input.protoConfidence !== void 0) {
461
+ setClauses.push("proto_confidence = ?");
462
+ params.push(input.protoConfidence);
463
+ }
464
+ if (input.banner !== void 0) {
465
+ setClauses.push("banner = ?");
466
+ params.push(input.banner);
467
+ }
468
+ if (input.product !== void 0) {
469
+ setClauses.push("product = ?");
470
+ params.push(input.product);
471
+ }
472
+ if (input.version !== void 0) {
473
+ setClauses.push("version = ?");
474
+ params.push(input.version);
475
+ }
476
+ if (input.state !== void 0) {
477
+ setClauses.push("state = ?");
478
+ params.push(input.state);
479
+ }
480
+ const now = (/* @__PURE__ */ new Date()).toISOString();
481
+ setClauses.push("updated_at = ?");
482
+ params.push(now);
483
+ params.push(id);
484
+ const sql = `UPDATE services SET ${setClauses.join(", ")} WHERE id = ?`;
485
+ const stmt = this.db.prepare(sql);
486
+ const result = stmt.run(...params);
487
+ if (result.changes === 0) {
488
+ return void 0;
489
+ }
490
+ return this.findById(id);
491
+ }
492
+ /** Delete a Service by id. Returns true if a row was deleted. */
493
+ delete(id) {
494
+ const stmt = this.db.prepare("DELETE FROM services WHERE id = ?");
495
+ const result = stmt.run(id);
496
+ return result.changes > 0;
497
+ }
498
+ };
499
+
500
+ // src/db/repository/vhost-repository.ts
501
+ import crypto3 from "crypto";
502
+ function rowToVhost(row) {
503
+ return {
504
+ id: row.id,
505
+ hostId: row.host_id,
506
+ hostname: row.hostname,
507
+ source: row.source ?? void 0,
508
+ evidenceArtifactId: row.evidence_artifact_id,
509
+ createdAt: row.created_at
510
+ };
511
+ }
512
+ var VhostRepository = class {
513
+ db;
514
+ constructor(db2) {
515
+ this.db = db2;
516
+ }
517
+ /** Insert a new Vhost and return the full entity. */
518
+ create(input) {
519
+ const id = crypto3.randomUUID();
520
+ const now = (/* @__PURE__ */ new Date()).toISOString();
521
+ const stmt = this.db.prepare(
522
+ `INSERT INTO vhosts (id, host_id, hostname, source, evidence_artifact_id, created_at)
523
+ VALUES (?, ?, ?, ?, ?, ?)`
524
+ );
525
+ stmt.run(
526
+ id,
527
+ input.hostId,
528
+ input.hostname,
529
+ input.source ?? null,
530
+ input.evidenceArtifactId,
531
+ now
532
+ );
533
+ return {
534
+ id,
535
+ hostId: input.hostId,
536
+ hostname: input.hostname,
537
+ source: input.source,
538
+ evidenceArtifactId: input.evidenceArtifactId,
539
+ createdAt: now
540
+ };
541
+ }
542
+ /** Find a Vhost by its primary key. Returns undefined if not found. */
543
+ findById(id) {
544
+ const stmt = this.db.prepare(
545
+ `SELECT id, host_id, hostname, source, evidence_artifact_id, created_at
546
+ FROM vhosts
547
+ WHERE id = ?`
548
+ );
549
+ const row = stmt.get(id);
550
+ return row ? rowToVhost(row) : void 0;
551
+ }
552
+ /** Return all Vhosts belonging to a given host. */
553
+ findByHostId(hostId) {
554
+ const stmt = this.db.prepare(
555
+ `SELECT id, host_id, hostname, source, evidence_artifact_id, created_at
556
+ FROM vhosts
557
+ WHERE host_id = ?`
558
+ );
559
+ const rows = stmt.all(hostId);
560
+ return rows.map(rowToVhost);
561
+ }
562
+ /** Delete a Vhost by id. Returns true if a row was deleted. */
563
+ delete(id) {
564
+ const stmt = this.db.prepare("DELETE FROM vhosts WHERE id = ?");
565
+ const result = stmt.run(id);
566
+ return result.changes > 0;
567
+ }
568
+ };
569
+
570
+ // src/db/repository/http-endpoint-repository.ts
571
+ import crypto4 from "crypto";
572
+ function rowToHttpEndpoint(row) {
573
+ return {
574
+ id: row.id,
575
+ serviceId: row.service_id,
576
+ vhostId: row.vhost_id ?? void 0,
577
+ baseUri: row.base_uri,
578
+ method: row.method,
579
+ path: row.path,
580
+ statusCode: row.status_code ?? void 0,
581
+ contentLength: row.content_length ?? void 0,
582
+ words: row.words ?? void 0,
583
+ lines: row.lines ?? void 0,
584
+ evidenceArtifactId: row.evidence_artifact_id,
585
+ createdAt: row.created_at
586
+ };
587
+ }
588
+ var HttpEndpointRepository = class {
589
+ db;
590
+ constructor(db2) {
591
+ this.db = db2;
592
+ }
593
+ /** Insert a new HttpEndpoint and return the full entity. */
594
+ create(input) {
595
+ const id = crypto4.randomUUID();
596
+ const now = (/* @__PURE__ */ new Date()).toISOString();
597
+ const stmt = this.db.prepare(
598
+ `INSERT INTO http_endpoints (id, service_id, vhost_id, base_uri, method, path, status_code, content_length, words, lines, evidence_artifact_id, created_at)
599
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
600
+ );
601
+ stmt.run(
602
+ id,
603
+ input.serviceId,
604
+ input.vhostId ?? null,
605
+ input.baseUri,
606
+ input.method,
607
+ input.path,
608
+ input.statusCode ?? null,
609
+ input.contentLength ?? null,
610
+ input.words ?? null,
611
+ input.lines ?? null,
612
+ input.evidenceArtifactId,
613
+ now
614
+ );
615
+ return {
616
+ id,
617
+ serviceId: input.serviceId,
618
+ vhostId: input.vhostId,
619
+ baseUri: input.baseUri,
620
+ method: input.method,
621
+ path: input.path,
622
+ statusCode: input.statusCode,
623
+ contentLength: input.contentLength,
624
+ words: input.words,
625
+ lines: input.lines,
626
+ evidenceArtifactId: input.evidenceArtifactId,
627
+ createdAt: now
628
+ };
629
+ }
630
+ /** Find an HttpEndpoint by its primary key. Returns undefined if not found. */
631
+ findById(id) {
632
+ const stmt = this.db.prepare(
633
+ `SELECT id, service_id, vhost_id, base_uri, method, path, status_code, content_length, words, lines, evidence_artifact_id, created_at
634
+ FROM http_endpoints
635
+ WHERE id = ?`
636
+ );
637
+ const row = stmt.get(id);
638
+ return row ? rowToHttpEndpoint(row) : void 0;
639
+ }
640
+ /** Return all HttpEndpoints for a given service. */
641
+ findByServiceId(serviceId) {
642
+ const stmt = this.db.prepare(
643
+ `SELECT id, service_id, vhost_id, base_uri, method, path, status_code, content_length, words, lines, evidence_artifact_id, created_at
644
+ FROM http_endpoints
645
+ WHERE service_id = ?`
646
+ );
647
+ const rows = stmt.all(serviceId);
648
+ return rows.map(rowToHttpEndpoint);
649
+ }
650
+ /** Delete an HttpEndpoint by id. Returns true if a row was deleted. */
651
+ delete(id) {
652
+ const stmt = this.db.prepare(
653
+ "DELETE FROM http_endpoints WHERE id = ?"
654
+ );
655
+ const result = stmt.run(id);
656
+ return result.changes > 0;
657
+ }
658
+ };
659
+
660
+ // src/db/repository/input-repository.ts
661
+ import crypto5 from "crypto";
662
+ function rowToInput(row) {
663
+ return {
664
+ id: row.id,
665
+ serviceId: row.service_id,
666
+ location: row.location,
667
+ name: row.name,
668
+ typeHint: row.type_hint ?? void 0,
669
+ createdAt: row.created_at,
670
+ updatedAt: row.updated_at
671
+ };
672
+ }
673
+ var InputRepository = class {
674
+ db;
675
+ constructor(db2) {
676
+ this.db = db2;
677
+ }
678
+ /** Insert a new Input and return the full entity. */
679
+ create(input) {
680
+ const id = crypto5.randomUUID();
681
+ const now = (/* @__PURE__ */ new Date()).toISOString();
682
+ const stmt = this.db.prepare(
683
+ `INSERT INTO inputs (id, service_id, location, name, type_hint, created_at, updated_at)
684
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
685
+ );
686
+ stmt.run(
687
+ id,
688
+ input.serviceId,
689
+ input.location,
690
+ input.name,
691
+ input.typeHint ?? null,
692
+ now,
693
+ now
694
+ );
695
+ return {
696
+ id,
697
+ serviceId: input.serviceId,
698
+ location: input.location,
699
+ name: input.name,
700
+ typeHint: input.typeHint,
701
+ createdAt: now,
702
+ updatedAt: now
703
+ };
704
+ }
705
+ /** Find an Input by its primary key. Returns undefined if not found. */
706
+ findById(id) {
707
+ const stmt = this.db.prepare(
708
+ `SELECT id, service_id, location, name, type_hint, created_at, updated_at
709
+ FROM inputs
710
+ WHERE id = ?`
711
+ );
712
+ const row = stmt.get(id);
713
+ return row ? rowToInput(row) : void 0;
714
+ }
715
+ /**
716
+ * Find all Inputs for a given service.
717
+ * If location is provided, further filter by location.
718
+ */
719
+ findByServiceId(serviceId, location) {
720
+ if (location !== void 0) {
721
+ const stmt2 = this.db.prepare(
722
+ `SELECT id, service_id, location, name, type_hint, created_at, updated_at
723
+ FROM inputs
724
+ WHERE service_id = ? AND location = ?`
725
+ );
726
+ const rows2 = stmt2.all(serviceId, location);
727
+ return rows2.map(rowToInput);
728
+ }
729
+ const stmt = this.db.prepare(
730
+ `SELECT id, service_id, location, name, type_hint, created_at, updated_at
731
+ FROM inputs
732
+ WHERE service_id = ?`
733
+ );
734
+ const rows = stmt.all(serviceId);
735
+ return rows.map(rowToInput);
736
+ }
737
+ /**
738
+ * Update an existing Input with the provided fields.
739
+ * Always bumps `updated_at`. Returns the updated entity, or undefined if
740
+ * the Input was not found.
741
+ */
742
+ update(id, input) {
743
+ const setClauses = [];
744
+ const params = [];
745
+ if (input.typeHint !== void 0) {
746
+ setClauses.push("type_hint = ?");
747
+ params.push(input.typeHint);
748
+ }
749
+ const now = (/* @__PURE__ */ new Date()).toISOString();
750
+ setClauses.push("updated_at = ?");
751
+ params.push(now);
752
+ params.push(id);
753
+ const sql = `UPDATE inputs SET ${setClauses.join(", ")} WHERE id = ?`;
754
+ const stmt = this.db.prepare(sql);
755
+ const result = stmt.run(...params);
756
+ if (result.changes === 0) {
757
+ return void 0;
758
+ }
759
+ return this.findById(id);
760
+ }
761
+ /** Delete an Input by id. Returns true if a row was deleted. */
762
+ delete(id) {
763
+ const stmt = this.db.prepare("DELETE FROM inputs WHERE id = ?");
764
+ const result = stmt.run(id);
765
+ return result.changes > 0;
766
+ }
767
+ };
768
+
769
+ // src/db/repository/observation-repository.ts
770
+ import crypto6 from "crypto";
771
+ function rowToObservation(row) {
772
+ return {
773
+ id: row.id,
774
+ inputId: row.input_id,
775
+ rawValue: row.raw_value,
776
+ normValue: row.norm_value,
777
+ bodyPath: row.body_path ?? void 0,
778
+ source: row.source,
779
+ confidence: row.confidence,
780
+ evidenceArtifactId: row.evidence_artifact_id,
781
+ observedAt: row.observed_at
782
+ };
783
+ }
784
+ var ObservationRepository = class {
785
+ db;
786
+ constructor(db2) {
787
+ this.db = db2;
788
+ }
789
+ /** Insert a new Observation and return the full entity. */
790
+ create(input) {
791
+ const id = crypto6.randomUUID();
792
+ const stmt = this.db.prepare(
793
+ `INSERT INTO observations (id, input_id, raw_value, norm_value, body_path, source, confidence, evidence_artifact_id, observed_at)
794
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
795
+ );
796
+ stmt.run(
797
+ id,
798
+ input.inputId,
799
+ input.rawValue,
800
+ input.normValue,
801
+ input.bodyPath ?? null,
802
+ input.source,
803
+ input.confidence,
804
+ input.evidenceArtifactId,
805
+ input.observedAt
806
+ );
807
+ return {
808
+ id,
809
+ inputId: input.inputId,
810
+ rawValue: input.rawValue,
811
+ normValue: input.normValue,
812
+ bodyPath: input.bodyPath,
813
+ source: input.source,
814
+ confidence: input.confidence,
815
+ evidenceArtifactId: input.evidenceArtifactId,
816
+ observedAt: input.observedAt
817
+ };
818
+ }
819
+ /** Find an Observation by its primary key. Returns undefined if not found. */
820
+ findById(id) {
821
+ const stmt = this.db.prepare(
822
+ `SELECT id, input_id, raw_value, norm_value, body_path, source, confidence, evidence_artifact_id, observed_at
823
+ FROM observations
824
+ WHERE id = ?`
825
+ );
826
+ const row = stmt.get(id);
827
+ return row ? rowToObservation(row) : void 0;
828
+ }
829
+ /** Return all Observations for a given input ID. */
830
+ findByInputId(inputId) {
831
+ const stmt = this.db.prepare(
832
+ `SELECT id, input_id, raw_value, norm_value, body_path, source, confidence, evidence_artifact_id, observed_at
833
+ FROM observations
834
+ WHERE input_id = ?`
835
+ );
836
+ const rows = stmt.all(inputId);
837
+ return rows.map(rowToObservation);
838
+ }
839
+ /** Delete an Observation by id. Returns true if a row was deleted. */
840
+ delete(id) {
841
+ const stmt = this.db.prepare("DELETE FROM observations WHERE id = ?");
842
+ const result = stmt.run(id);
843
+ return result.changes > 0;
844
+ }
845
+ };
846
+
847
+ // src/db/repository/credential-repository.ts
848
+ import crypto7 from "crypto";
849
+ function rowToCredential(row) {
850
+ return {
851
+ id: row.id,
852
+ serviceId: row.service_id,
853
+ endpointId: row.endpoint_id ?? void 0,
854
+ username: row.username,
855
+ secret: row.secret,
856
+ secretType: row.secret_type,
857
+ source: row.source,
858
+ confidence: row.confidence,
859
+ evidenceArtifactId: row.evidence_artifact_id,
860
+ createdAt: row.created_at
861
+ };
862
+ }
863
+ var CredentialRepository = class {
864
+ db;
865
+ constructor(db2) {
866
+ this.db = db2;
867
+ }
868
+ /** Insert a new Credential and return the full entity. */
869
+ create(input) {
870
+ const id = crypto7.randomUUID();
871
+ const now = (/* @__PURE__ */ new Date()).toISOString();
872
+ const stmt = this.db.prepare(
873
+ `INSERT INTO credentials (id, service_id, endpoint_id, username, secret, secret_type, source, confidence, evidence_artifact_id, created_at)
874
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
875
+ );
876
+ stmt.run(
877
+ id,
878
+ input.serviceId,
879
+ input.endpointId ?? null,
880
+ input.username,
881
+ input.secret,
882
+ input.secretType,
883
+ input.source,
884
+ input.confidence,
885
+ input.evidenceArtifactId,
886
+ now
887
+ );
888
+ return {
889
+ id,
890
+ serviceId: input.serviceId,
891
+ endpointId: input.endpointId ?? void 0,
892
+ username: input.username,
893
+ secret: input.secret,
894
+ secretType: input.secretType,
895
+ source: input.source,
896
+ confidence: input.confidence,
897
+ evidenceArtifactId: input.evidenceArtifactId,
898
+ createdAt: now
899
+ };
900
+ }
901
+ /** Find a Credential by its primary key. Returns undefined if not found. */
902
+ findById(id) {
903
+ const stmt = this.db.prepare(
904
+ `SELECT id, service_id, endpoint_id, username, secret, secret_type, source, confidence, evidence_artifact_id, created_at
905
+ FROM credentials
906
+ WHERE id = ?`
907
+ );
908
+ const row = stmt.get(id);
909
+ return row ? rowToCredential(row) : void 0;
910
+ }
911
+ /** Return all Credentials for a given service. */
912
+ findByServiceId(serviceId) {
913
+ const stmt = this.db.prepare(
914
+ `SELECT id, service_id, endpoint_id, username, secret, secret_type, source, confidence, evidence_artifact_id, created_at
915
+ FROM credentials
916
+ WHERE service_id = ?`
917
+ );
918
+ const rows = stmt.all(serviceId);
919
+ return rows.map(rowToCredential);
920
+ }
921
+ /** Return all Credentials across all services. */
922
+ findAll() {
923
+ const stmt = this.db.prepare(
924
+ `SELECT id, service_id, endpoint_id, username, secret, secret_type, source, confidence, evidence_artifact_id, created_at
925
+ FROM credentials`
926
+ );
927
+ const rows = stmt.all();
928
+ return rows.map(rowToCredential);
929
+ }
930
+ /** Delete a Credential by id. Returns true if a row was deleted. */
931
+ delete(id) {
932
+ const stmt = this.db.prepare("DELETE FROM credentials WHERE id = ?");
933
+ const result = stmt.run(id);
934
+ return result.changes > 0;
935
+ }
936
+ };
937
+
938
+ // src/db/repository/vulnerability-repository.ts
939
+ import crypto8 from "crypto";
940
+ function rowToVulnerability(row) {
941
+ return {
942
+ id: row.id,
943
+ serviceId: row.service_id,
944
+ endpointId: row.endpoint_id ?? void 0,
945
+ vulnType: row.vuln_type,
946
+ title: row.title,
947
+ description: row.description ?? void 0,
948
+ severity: row.severity,
949
+ confidence: row.confidence,
950
+ evidenceArtifactId: row.evidence_artifact_id,
951
+ createdAt: row.created_at
952
+ };
953
+ }
954
+ var VulnerabilityRepository = class {
955
+ db;
956
+ constructor(db2) {
957
+ this.db = db2;
958
+ }
959
+ /** Insert a new Vulnerability and return the full entity. */
960
+ create(input) {
961
+ const id = crypto8.randomUUID();
962
+ const now = (/* @__PURE__ */ new Date()).toISOString();
963
+ const stmt = this.db.prepare(
964
+ `INSERT INTO vulnerabilities (id, service_id, endpoint_id, vuln_type, title, description, severity, confidence, evidence_artifact_id, created_at)
965
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
966
+ );
967
+ stmt.run(
968
+ id,
969
+ input.serviceId,
970
+ input.endpointId ?? null,
971
+ input.vulnType,
972
+ input.title,
973
+ input.description ?? null,
974
+ input.severity,
975
+ input.confidence,
976
+ input.evidenceArtifactId,
977
+ now
978
+ );
979
+ return {
980
+ id,
981
+ serviceId: input.serviceId,
982
+ endpointId: input.endpointId,
983
+ vulnType: input.vulnType,
984
+ title: input.title,
985
+ description: input.description,
986
+ severity: input.severity,
987
+ confidence: input.confidence,
988
+ evidenceArtifactId: input.evidenceArtifactId,
989
+ createdAt: now
990
+ };
991
+ }
992
+ /** Find a Vulnerability by its primary key. Returns undefined if not found. */
993
+ findById(id) {
994
+ const stmt = this.db.prepare(
995
+ `SELECT id, service_id, endpoint_id, vuln_type, title, description, severity, confidence, evidence_artifact_id, created_at
996
+ FROM vulnerabilities
997
+ WHERE id = ?`
998
+ );
999
+ const row = stmt.get(id);
1000
+ return row ? rowToVulnerability(row) : void 0;
1001
+ }
1002
+ /**
1003
+ * Return all Vulnerabilities for a given service.
1004
+ * Optionally filter by severity when the parameter is provided.
1005
+ */
1006
+ findByServiceId(serviceId, severity) {
1007
+ if (severity !== void 0) {
1008
+ const stmt2 = this.db.prepare(
1009
+ `SELECT id, service_id, endpoint_id, vuln_type, title, description, severity, confidence, evidence_artifact_id, created_at
1010
+ FROM vulnerabilities
1011
+ WHERE service_id = ? AND severity = ?`
1012
+ );
1013
+ const rows2 = stmt2.all(serviceId, severity);
1014
+ return rows2.map(rowToVulnerability);
1015
+ }
1016
+ const stmt = this.db.prepare(
1017
+ `SELECT id, service_id, endpoint_id, vuln_type, title, description, severity, confidence, evidence_artifact_id, created_at
1018
+ FROM vulnerabilities
1019
+ WHERE service_id = ?`
1020
+ );
1021
+ const rows = stmt.all(serviceId);
1022
+ return rows.map(rowToVulnerability);
1023
+ }
1024
+ /**
1025
+ * Return all Vulnerabilities across all services.
1026
+ * Optionally filter by severity when the parameter is provided.
1027
+ */
1028
+ findAll(severity) {
1029
+ if (severity !== void 0) {
1030
+ const stmt2 = this.db.prepare(
1031
+ `SELECT id, service_id, endpoint_id, vuln_type, title, description, severity, confidence, evidence_artifact_id, created_at
1032
+ FROM vulnerabilities
1033
+ WHERE severity = ?`
1034
+ );
1035
+ const rows2 = stmt2.all(severity);
1036
+ return rows2.map(rowToVulnerability);
1037
+ }
1038
+ const stmt = this.db.prepare(
1039
+ `SELECT id, service_id, endpoint_id, vuln_type, title, description, severity, confidence, evidence_artifact_id, created_at
1040
+ FROM vulnerabilities`
1041
+ );
1042
+ const rows = stmt.all();
1043
+ return rows.map(rowToVulnerability);
1044
+ }
1045
+ /** Delete a Vulnerability by id. Returns true if a row was deleted. */
1046
+ delete(id) {
1047
+ const stmt = this.db.prepare("DELETE FROM vulnerabilities WHERE id = ?");
1048
+ const result = stmt.run(id);
1049
+ return result.changes > 0;
1050
+ }
1051
+ };
1052
+
1053
+ // src/mcp/tools/query.ts
1054
+ function registerQueryTools(server2, db2) {
1055
+ const hostRepo = new HostRepository(db2);
1056
+ const serviceRepo = new ServiceRepository(db2);
1057
+ const vhostRepo = new VhostRepository(db2);
1058
+ const httpEndpointRepo = new HttpEndpointRepository(db2);
1059
+ const inputRepo = new InputRepository(db2);
1060
+ const observationRepo = new ObservationRepository(db2);
1061
+ const credentialRepo = new CredentialRepository(db2);
1062
+ const vulnRepo = new VulnerabilityRepository(db2);
1063
+ server2.tool("list_hosts", "List all discovered hosts", {}, async () => {
1064
+ const hosts = hostRepo.findAll();
1065
+ return { content: [{ type: "text", text: JSON.stringify(hosts, null, 2) }] };
1066
+ });
1067
+ server2.tool(
1068
+ "get_host",
1069
+ "Get detailed information about a host including services and vhosts",
1070
+ { hostId: z.string().describe("Host UUID") },
1071
+ async ({ hostId }) => {
1072
+ const host = hostRepo.findById(hostId);
1073
+ if (!host) {
1074
+ return { content: [{ type: "text", text: `Host not found: ${hostId}` }], isError: true };
1075
+ }
1076
+ const services = serviceRepo.findByHostId(hostId);
1077
+ const vhosts = vhostRepo.findByHostId(hostId);
1078
+ const result = { ...host, services, vhosts };
1079
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1080
+ }
1081
+ );
1082
+ server2.tool(
1083
+ "list_services",
1084
+ "List all services for a host",
1085
+ { hostId: z.string().describe("Host UUID") },
1086
+ async ({ hostId }) => {
1087
+ const services = serviceRepo.findByHostId(hostId);
1088
+ return { content: [{ type: "text", text: JSON.stringify(services, null, 2) }] };
1089
+ }
1090
+ );
1091
+ server2.tool(
1092
+ "list_endpoints",
1093
+ "List all HTTP endpoints for a service",
1094
+ { serviceId: z.string().describe("Service UUID") },
1095
+ async ({ serviceId }) => {
1096
+ const endpoints = httpEndpointRepo.findByServiceId(serviceId);
1097
+ return { content: [{ type: "text", text: JSON.stringify(endpoints, null, 2) }] };
1098
+ }
1099
+ );
1100
+ server2.tool(
1101
+ "list_inputs",
1102
+ "List all input parameters for a service, optionally filtered by location",
1103
+ {
1104
+ serviceId: z.string().describe("Service UUID"),
1105
+ location: z.string().optional().describe("Filter by location (query, path, body, header, cookie)")
1106
+ },
1107
+ async ({ serviceId, location }) => {
1108
+ const inputs = inputRepo.findByServiceId(serviceId, location);
1109
+ return { content: [{ type: "text", text: JSON.stringify(inputs, null, 2) }] };
1110
+ }
1111
+ );
1112
+ server2.tool(
1113
+ "list_observations",
1114
+ "List all observations for an input parameter",
1115
+ { inputId: z.string().describe("Input UUID") },
1116
+ async ({ inputId }) => {
1117
+ const observations = observationRepo.findByInputId(inputId);
1118
+ return { content: [{ type: "text", text: JSON.stringify(observations, null, 2) }] };
1119
+ }
1120
+ );
1121
+ server2.tool(
1122
+ "list_credentials",
1123
+ "List credentials, optionally filtered by service",
1124
+ {
1125
+ serviceId: z.string().optional().describe("Service UUID (optional, omit to list all)")
1126
+ },
1127
+ async ({ serviceId }) => {
1128
+ const credentials = serviceId ? credentialRepo.findByServiceId(serviceId) : credentialRepo.findAll();
1129
+ return { content: [{ type: "text", text: JSON.stringify(credentials, null, 2) }] };
1130
+ }
1131
+ );
1132
+ server2.tool(
1133
+ "list_vulnerabilities",
1134
+ "List vulnerabilities, optionally filtered by service and/or severity",
1135
+ {
1136
+ serviceId: z.string().optional().describe("Service UUID (optional, omit to list all)"),
1137
+ severity: z.string().optional().describe("Filter by severity (critical, high, medium, low, info)")
1138
+ },
1139
+ async ({ serviceId, severity }) => {
1140
+ const vulns = serviceId ? vulnRepo.findByServiceId(serviceId, severity) : vulnRepo.findAll(severity);
1141
+ return { content: [{ type: "text", text: JSON.stringify(vulns, null, 2) }] };
1142
+ }
1143
+ );
1144
+ }
1145
+
1146
+ // src/mcp/tools/ingest.ts
1147
+ import { z as z2 } from "zod";
1148
+
1149
+ // src/engine/ingest.ts
1150
+ import fs from "fs";
1151
+ import crypto13 from "crypto";
1152
+ import path from "path";
1153
+
1154
+ // src/db/repository/artifact-repository.ts
1155
+ import crypto9 from "crypto";
1156
+ function rowToArtifact(row) {
1157
+ return {
1158
+ id: row.id,
1159
+ ...row.scan_id !== null ? { scanId: row.scan_id } : {},
1160
+ tool: row.tool,
1161
+ kind: row.kind,
1162
+ path: row.path,
1163
+ ...row.sha256 !== null ? { sha256: row.sha256 } : {},
1164
+ capturedAt: row.captured_at,
1165
+ ...row.attrs_json !== null ? { attrsJson: row.attrs_json } : {}
1166
+ };
1167
+ }
1168
+ var ArtifactRepository = class {
1169
+ db;
1170
+ insertStmt;
1171
+ selectByIdStmt;
1172
+ selectAllStmt;
1173
+ selectByToolStmt;
1174
+ constructor(db2) {
1175
+ this.db = db2;
1176
+ this.insertStmt = this.db.prepare(
1177
+ "INSERT INTO artifacts (id, scan_id, tool, kind, path, sha256, captured_at, attrs_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
1178
+ );
1179
+ this.selectByIdStmt = this.db.prepare(
1180
+ "SELECT id, scan_id, tool, kind, path, sha256, captured_at, attrs_json FROM artifacts WHERE id = ?"
1181
+ );
1182
+ this.selectAllStmt = this.db.prepare(
1183
+ "SELECT id, scan_id, tool, kind, path, sha256, captured_at, attrs_json FROM artifacts"
1184
+ );
1185
+ this.selectByToolStmt = this.db.prepare(
1186
+ "SELECT id, scan_id, tool, kind, path, sha256, captured_at, attrs_json FROM artifacts WHERE tool = ?"
1187
+ );
1188
+ }
1189
+ /** Create a new Artifact record and return the full entity. */
1190
+ create(input) {
1191
+ const id = crypto9.randomUUID();
1192
+ this.insertStmt.run(
1193
+ id,
1194
+ input.scanId ?? null,
1195
+ input.tool,
1196
+ input.kind,
1197
+ input.path,
1198
+ input.sha256 ?? null,
1199
+ input.capturedAt,
1200
+ input.attrsJson ?? null
1201
+ );
1202
+ return {
1203
+ id,
1204
+ ...input.scanId !== void 0 ? { scanId: input.scanId } : {},
1205
+ tool: input.tool,
1206
+ kind: input.kind,
1207
+ path: input.path,
1208
+ ...input.sha256 !== void 0 ? { sha256: input.sha256 } : {},
1209
+ capturedAt: input.capturedAt,
1210
+ ...input.attrsJson !== void 0 ? { attrsJson: input.attrsJson } : {}
1211
+ };
1212
+ }
1213
+ /** Find an Artifact by its UUID. Returns undefined if not found. */
1214
+ findById(id) {
1215
+ const row = this.selectByIdStmt.get(id);
1216
+ if (row === void 0) {
1217
+ return void 0;
1218
+ }
1219
+ return rowToArtifact(row);
1220
+ }
1221
+ /** Return all Artifact records. */
1222
+ findAll() {
1223
+ const rows = this.selectAllStmt.all();
1224
+ return rows.map(rowToArtifact);
1225
+ }
1226
+ /** Return all Artifact records for the given tool name. */
1227
+ findByTool(tool) {
1228
+ const rows = this.selectByToolStmt.all(tool);
1229
+ return rows.map(rowToArtifact);
1230
+ }
1231
+ };
1232
+
1233
+ // src/parser/nmap-parser.ts
1234
+ import { XMLParser } from "fast-xml-parser";
1235
+
1236
+ // src/types/parser.ts
1237
+ function emptyParseResult() {
1238
+ return {
1239
+ hosts: [],
1240
+ services: [],
1241
+ serviceObservations: [],
1242
+ httpEndpoints: [],
1243
+ inputs: [],
1244
+ endpointInputs: [],
1245
+ observations: [],
1246
+ vulnerabilities: [],
1247
+ cves: []
1248
+ };
1249
+ }
1250
+
1251
+ // src/parser/nmap-parser.ts
1252
+ function ensureArray(value) {
1253
+ if (value === void 0 || value === null) {
1254
+ return [];
1255
+ }
1256
+ return Array.isArray(value) ? value : [value];
1257
+ }
1258
+ function toProtoConfidence(conf) {
1259
+ const n = conf !== void 0 ? Number(conf) : 0;
1260
+ if (n === 10) return "high";
1261
+ if (n >= 7) return "medium";
1262
+ return "low";
1263
+ }
1264
+ function toOsConfidence(accuracy) {
1265
+ const n = Number(accuracy);
1266
+ if (n >= 90) return "high";
1267
+ if (n >= 50) return "medium";
1268
+ return "low";
1269
+ }
1270
+ function isHttps(service) {
1271
+ return service["@_name"] === "https" || service["@_tunnel"] === "ssl";
1272
+ }
1273
+ function buildBanner(service) {
1274
+ const parts = [];
1275
+ if (service["@_product"]) parts.push(service["@_product"]);
1276
+ if (service["@_version"]) parts.push(service["@_version"]);
1277
+ if (service["@_extrainfo"]) parts.push(service["@_extrainfo"]);
1278
+ return parts.length > 0 ? parts.join(" ") : void 0;
1279
+ }
1280
+ function getIpv4Address(addresses) {
1281
+ const ipv4 = addresses.find((a) => a["@_addrtype"] === "ipv4");
1282
+ return ipv4?.["@_addr"];
1283
+ }
1284
+ function parseNmapXml(xml) {
1285
+ const parser = new XMLParser({
1286
+ ignoreAttributes: false,
1287
+ attributeNamePrefix: "@_",
1288
+ allowBooleanAttributes: true
1289
+ });
1290
+ const parsed = parser.parse(xml);
1291
+ const nmapRun = parsed;
1292
+ const result = emptyParseResult();
1293
+ const hosts = ensureArray(nmapRun.nmaprun?.host);
1294
+ for (const host of hosts) {
1295
+ processHost(host, result);
1296
+ }
1297
+ return result;
1298
+ }
1299
+ function processHost(host, result) {
1300
+ const addresses = ensureArray(host.address);
1301
+ const authority = getIpv4Address(addresses);
1302
+ if (authority === void 0) {
1303
+ return;
1304
+ }
1305
+ const parsedHost = {
1306
+ authority,
1307
+ authorityKind: "IP"
1308
+ };
1309
+ result.hosts.push(parsedHost);
1310
+ const ports = ensureArray(host.ports?.port);
1311
+ const services = [];
1312
+ for (const port of ports) {
1313
+ const service = processPort(port, authority);
1314
+ services.push(service);
1315
+ result.services.push(service);
1316
+ }
1317
+ const osMatches = ensureArray(host.os?.osmatch);
1318
+ if (osMatches.length > 0) {
1319
+ processOsMatches(osMatches, authority, services, result);
1320
+ }
1321
+ }
1322
+ function processPort(port, hostAuthority) {
1323
+ const service = port.service;
1324
+ const serviceName = service?.["@_name"] ?? "";
1325
+ const appProto = service !== void 0 && isHttps(service) ? "https" : serviceName;
1326
+ const banner = service !== void 0 ? buildBanner(service) : void 0;
1327
+ const protoConfidence = toProtoConfidence(service?.["@_conf"]);
1328
+ return {
1329
+ hostAuthority,
1330
+ transport: port["@_protocol"],
1331
+ port: Number(port["@_portid"]),
1332
+ appProto,
1333
+ protoConfidence,
1334
+ banner,
1335
+ product: service?.["@_product"],
1336
+ version: service?.["@_version"],
1337
+ state: port.state["@_state"]
1338
+ };
1339
+ }
1340
+ function processOsMatches(osMatches, hostAuthority, services, result) {
1341
+ const firstService = services.length > 0 ? services[0] : void 0;
1342
+ const transport2 = firstService?.transport ?? "tcp";
1343
+ const port = firstService?.port ?? 0;
1344
+ for (const osMatch of osMatches) {
1345
+ const observation = {
1346
+ hostAuthority,
1347
+ transport: transport2,
1348
+ port,
1349
+ key: "os",
1350
+ value: osMatch["@_name"],
1351
+ confidence: toOsConfidence(osMatch["@_accuracy"])
1352
+ };
1353
+ result.serviceObservations.push(observation);
1354
+ }
1355
+ }
1356
+
1357
+ // src/parser/ffuf-parser.ts
1358
+ var IP_REGEX = /^\d{1,3}(\.\d{1,3}){3}$/;
1359
+ function isRecord(value) {
1360
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1361
+ }
1362
+ function validateFfufJson(raw) {
1363
+ if (!isRecord(raw)) {
1364
+ throw new Error("ffuf JSON: root must be an object");
1365
+ }
1366
+ if (typeof raw["commandline"] !== "string") {
1367
+ throw new Error("ffuf JSON: commandline must be a string");
1368
+ }
1369
+ const config = raw["config"];
1370
+ if (!isRecord(config)) {
1371
+ throw new Error("ffuf JSON: config must be an object");
1372
+ }
1373
+ if (typeof config["url"] !== "string" || typeof config["method"] !== "string") {
1374
+ throw new Error("ffuf JSON: config.url and config.method must be strings");
1375
+ }
1376
+ if (!Array.isArray(raw["results"])) {
1377
+ throw new Error("ffuf JSON: results must be an array");
1378
+ }
1379
+ const results = [];
1380
+ for (const item of raw["results"]) {
1381
+ if (!isRecord(item)) {
1382
+ throw new Error("ffuf JSON: each result must be an object");
1383
+ }
1384
+ results.push({
1385
+ input: isRecord(item["input"]) ? Object.fromEntries(
1386
+ Object.entries(item["input"]).map(([k, v]) => [
1387
+ k,
1388
+ String(v)
1389
+ ])
1390
+ ) : {},
1391
+ status: typeof item["status"] === "number" ? item["status"] : 0,
1392
+ length: typeof item["length"] === "number" ? item["length"] : 0,
1393
+ words: typeof item["words"] === "number" ? item["words"] : 0,
1394
+ lines: typeof item["lines"] === "number" ? item["lines"] : 0,
1395
+ url: typeof item["url"] === "string" ? item["url"] : "",
1396
+ host: typeof item["host"] === "string" ? item["host"] : ""
1397
+ });
1398
+ }
1399
+ return {
1400
+ commandline: raw["commandline"],
1401
+ config: {
1402
+ url: config["url"],
1403
+ method: config["method"]
1404
+ },
1405
+ results
1406
+ };
1407
+ }
1408
+ function parseUrl(urlStr) {
1409
+ const parsed = new URL(urlStr);
1410
+ const scheme = parsed.protocol.replace(":", "");
1411
+ let port;
1412
+ if (parsed.port !== "") {
1413
+ port = Number(parsed.port);
1414
+ } else {
1415
+ port = scheme === "https" ? 443 : 80;
1416
+ }
1417
+ return {
1418
+ scheme,
1419
+ hostname: parsed.hostname,
1420
+ port,
1421
+ pathname: parsed.pathname,
1422
+ searchParams: parsed.searchParams
1423
+ };
1424
+ }
1425
+ function determineAuthorityKind(hostname) {
1426
+ return IP_REGEX.test(hostname) ? "IP" : "DOMAIN";
1427
+ }
1428
+ function parseFfufJson(jsonContent) {
1429
+ const raw = JSON.parse(jsonContent);
1430
+ const ffuf = validateFfufJson(raw);
1431
+ const method = ffuf.config.method;
1432
+ if (ffuf.results.length === 0) {
1433
+ return emptyParseResult();
1434
+ }
1435
+ const hostsMap = /* @__PURE__ */ new Map();
1436
+ const servicesMap = /* @__PURE__ */ new Map();
1437
+ const endpointsMap = /* @__PURE__ */ new Map();
1438
+ const inputsMap = /* @__PURE__ */ new Map();
1439
+ const endpointInputsMap = /* @__PURE__ */ new Map();
1440
+ const observationsMap = /* @__PURE__ */ new Map();
1441
+ for (const result of ffuf.results) {
1442
+ if (result.url === "") {
1443
+ continue;
1444
+ }
1445
+ const parsed = parseUrl(result.url);
1446
+ const { scheme, hostname, port, pathname, searchParams } = parsed;
1447
+ const authorityKind = determineAuthorityKind(hostname);
1448
+ const baseUri = `${scheme}://${hostname}:${port}`;
1449
+ if (!hostsMap.has(hostname)) {
1450
+ hostsMap.set(hostname, {
1451
+ authority: hostname,
1452
+ authorityKind
1453
+ });
1454
+ }
1455
+ const serviceKey = `${hostname}:${port}`;
1456
+ if (!servicesMap.has(serviceKey)) {
1457
+ servicesMap.set(serviceKey, {
1458
+ hostAuthority: hostname,
1459
+ transport: "tcp",
1460
+ port,
1461
+ appProto: scheme,
1462
+ protoConfidence: "high",
1463
+ state: "open"
1464
+ });
1465
+ }
1466
+ const endpointKey = `${method}:${pathname}`;
1467
+ if (!endpointsMap.has(endpointKey)) {
1468
+ endpointsMap.set(endpointKey, {
1469
+ hostAuthority: hostname,
1470
+ port,
1471
+ baseUri,
1472
+ method,
1473
+ path: pathname,
1474
+ statusCode: result.status,
1475
+ contentLength: result.length,
1476
+ words: result.words,
1477
+ lines: result.lines
1478
+ });
1479
+ }
1480
+ for (const [paramName, paramValue] of searchParams.entries()) {
1481
+ if (!inputsMap.has(paramName)) {
1482
+ inputsMap.set(paramName, {
1483
+ hostAuthority: hostname,
1484
+ port,
1485
+ location: "query",
1486
+ name: paramName
1487
+ });
1488
+ }
1489
+ const eiKey = `${method}:${pathname}:query:${paramName}`;
1490
+ if (!endpointInputsMap.has(eiKey)) {
1491
+ endpointInputsMap.set(eiKey, {
1492
+ hostAuthority: hostname,
1493
+ port,
1494
+ method,
1495
+ path: pathname,
1496
+ inputLocation: "query",
1497
+ inputName: paramName
1498
+ });
1499
+ }
1500
+ const obsKey = `query:${paramName}:${paramValue}`;
1501
+ if (!observationsMap.has(obsKey)) {
1502
+ observationsMap.set(obsKey, {
1503
+ hostAuthority: hostname,
1504
+ port,
1505
+ inputLocation: "query",
1506
+ inputName: paramName,
1507
+ rawValue: paramValue,
1508
+ normValue: paramValue,
1509
+ source: "ffuf_url",
1510
+ confidence: "high"
1511
+ });
1512
+ }
1513
+ }
1514
+ }
1515
+ return {
1516
+ hosts: [...hostsMap.values()],
1517
+ services: [...servicesMap.values()],
1518
+ serviceObservations: [],
1519
+ httpEndpoints: [...endpointsMap.values()],
1520
+ inputs: [...inputsMap.values()],
1521
+ endpointInputs: [...endpointInputsMap.values()],
1522
+ observations: [...observationsMap.values()],
1523
+ vulnerabilities: [],
1524
+ cves: []
1525
+ };
1526
+ }
1527
+
1528
+ // src/parser/nuclei-parser.ts
1529
+ function isRecord2(value) {
1530
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1531
+ }
1532
+ function isStringArray(value) {
1533
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
1534
+ }
1535
+ function isNucleiClassification(value) {
1536
+ if (!isRecord2(value)) return false;
1537
+ if ("cve-id" in value && !isStringArray(value["cve-id"])) return false;
1538
+ if ("cvss-metrics" in value && typeof value["cvss-metrics"] !== "string" && value["cvss-metrics"] !== void 0)
1539
+ return false;
1540
+ if ("cvss-score" in value && typeof value["cvss-score"] !== "number" && value["cvss-score"] !== void 0)
1541
+ return false;
1542
+ return true;
1543
+ }
1544
+ function isNucleiInfo(value) {
1545
+ if (!isRecord2(value)) return false;
1546
+ if (typeof value.name !== "string") return false;
1547
+ if (typeof value.severity !== "string") return false;
1548
+ if (!isStringArray(value.tags)) return false;
1549
+ if ("classification" in value && value.classification !== void 0 && !isNucleiClassification(value.classification))
1550
+ return false;
1551
+ return true;
1552
+ }
1553
+ function isNucleiFinding(value) {
1554
+ if (!isRecord2(value)) return false;
1555
+ if (typeof value["template-id"] !== "string") return false;
1556
+ if (!isNucleiInfo(value.info)) return false;
1557
+ if (typeof value.type !== "string") return false;
1558
+ if (typeof value.host !== "string") return false;
1559
+ if (typeof value["matched-at"] !== "string") return false;
1560
+ if (typeof value.ip !== "string") return false;
1561
+ if (typeof value.port !== "string") return false;
1562
+ if (typeof value.scheme !== "string") return false;
1563
+ if (typeof value.url !== "string") return false;
1564
+ return true;
1565
+ }
1566
+ function extractRawPathname(urlStr) {
1567
+ const schemeEnd = urlStr.indexOf("://");
1568
+ if (schemeEnd === -1) {
1569
+ return "/";
1570
+ }
1571
+ const afterAuthority = urlStr.indexOf("/", schemeEnd + 3);
1572
+ if (afterAuthority === -1) {
1573
+ return "/";
1574
+ }
1575
+ const queryStart = urlStr.indexOf("?", afterAuthority);
1576
+ const fragmentStart = urlStr.indexOf("#", afterAuthority);
1577
+ let pathEnd = urlStr.length;
1578
+ if (queryStart !== -1 && queryStart < pathEnd) {
1579
+ pathEnd = queryStart;
1580
+ }
1581
+ if (fragmentStart !== -1 && fragmentStart < pathEnd) {
1582
+ pathEnd = fragmentStart;
1583
+ }
1584
+ return urlStr.substring(afterAuthority, pathEnd);
1585
+ }
1586
+ function inferVulnType(tags) {
1587
+ const priorityTags = ["sqli", "xss", "rce", "lfi", "ssrf"];
1588
+ for (const tag of priorityTags) {
1589
+ if (tags.includes(tag)) {
1590
+ return tag;
1591
+ }
1592
+ }
1593
+ return "other";
1594
+ }
1595
+ function parseNucleiJsonl(jsonl) {
1596
+ const result = emptyParseResult();
1597
+ if (jsonl.trim() === "") {
1598
+ return result;
1599
+ }
1600
+ const lines = jsonl.split("\n");
1601
+ const seenHosts = /* @__PURE__ */ new Set();
1602
+ const seenServices = /* @__PURE__ */ new Set();
1603
+ for (const line of lines) {
1604
+ const trimmed = line.trim();
1605
+ if (trimmed === "") {
1606
+ continue;
1607
+ }
1608
+ const parsed = JSON.parse(trimmed);
1609
+ if (!isNucleiFinding(parsed)) {
1610
+ continue;
1611
+ }
1612
+ processFinding(parsed, result, seenHosts, seenServices);
1613
+ }
1614
+ return result;
1615
+ }
1616
+ function processFinding(finding, result, seenHosts, seenServices) {
1617
+ const ip = finding.ip;
1618
+ const port = Number(finding.port);
1619
+ const scheme = finding.scheme;
1620
+ const matchedAt = finding["matched-at"];
1621
+ if (!seenHosts.has(ip)) {
1622
+ seenHosts.add(ip);
1623
+ const host = {
1624
+ authority: ip,
1625
+ authorityKind: "IP"
1626
+ };
1627
+ result.hosts.push(host);
1628
+ }
1629
+ const serviceKey = `${ip}:${port}`;
1630
+ if (!seenServices.has(serviceKey)) {
1631
+ seenServices.add(serviceKey);
1632
+ const service = {
1633
+ hostAuthority: ip,
1634
+ transport: "tcp",
1635
+ port,
1636
+ appProto: scheme,
1637
+ protoConfidence: "high",
1638
+ state: "open"
1639
+ };
1640
+ result.services.push(service);
1641
+ }
1642
+ const rawPath = extractRawPathname(matchedAt);
1643
+ const baseUri = `${scheme}://${ip}:${port}`;
1644
+ const endpoint = {
1645
+ hostAuthority: ip,
1646
+ port,
1647
+ baseUri,
1648
+ method: "GET",
1649
+ path: rawPath
1650
+ };
1651
+ result.httpEndpoints.push(endpoint);
1652
+ const info = finding.info;
1653
+ const vulnerability = {
1654
+ hostAuthority: ip,
1655
+ port,
1656
+ method: "GET",
1657
+ path: rawPath,
1658
+ vulnType: inferVulnType(info.tags),
1659
+ title: info.name,
1660
+ severity: info.severity,
1661
+ confidence: "high"
1662
+ };
1663
+ result.vulnerabilities.push(vulnerability);
1664
+ const classification = info.classification;
1665
+ if (classification !== void 0 && isRecord2(classification) && "cve-id" in classification && isStringArray(classification["cve-id"]) && classification["cve-id"].length > 0) {
1666
+ for (const cveId of classification["cve-id"]) {
1667
+ const cve = {
1668
+ vulnerabilityTitle: info.name,
1669
+ cveId,
1670
+ cvssScore: classification["cvss-score"],
1671
+ cvssVector: classification["cvss-metrics"]
1672
+ };
1673
+ result.cves.push(cve);
1674
+ }
1675
+ }
1676
+ }
1677
+
1678
+ // src/db/repository/service-observation-repository.ts
1679
+ import crypto10 from "crypto";
1680
+ function rowToServiceObservation(row) {
1681
+ return {
1682
+ id: row.id,
1683
+ serviceId: row.service_id,
1684
+ key: row.key,
1685
+ value: row.value,
1686
+ confidence: row.confidence,
1687
+ evidenceArtifactId: row.evidence_artifact_id,
1688
+ createdAt: row.created_at
1689
+ };
1690
+ }
1691
+ var ServiceObservationRepository = class {
1692
+ db;
1693
+ constructor(db2) {
1694
+ this.db = db2;
1695
+ }
1696
+ /** Insert a new ServiceObservation and return the full entity. */
1697
+ create(input) {
1698
+ const id = crypto10.randomUUID();
1699
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1700
+ const stmt = this.db.prepare(
1701
+ `INSERT INTO service_observations (id, service_id, key, value, confidence, evidence_artifact_id, created_at)
1702
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
1703
+ );
1704
+ stmt.run(
1705
+ id,
1706
+ input.serviceId,
1707
+ input.key,
1708
+ input.value,
1709
+ input.confidence,
1710
+ input.evidenceArtifactId,
1711
+ now
1712
+ );
1713
+ return {
1714
+ id,
1715
+ serviceId: input.serviceId,
1716
+ key: input.key,
1717
+ value: input.value,
1718
+ confidence: input.confidence,
1719
+ evidenceArtifactId: input.evidenceArtifactId,
1720
+ createdAt: now
1721
+ };
1722
+ }
1723
+ /** Find a ServiceObservation by its primary key. Returns undefined if not found. */
1724
+ findById(id) {
1725
+ const stmt = this.db.prepare(
1726
+ `SELECT id, service_id, key, value, confidence, evidence_artifact_id, created_at
1727
+ FROM service_observations
1728
+ WHERE id = ?`
1729
+ );
1730
+ const row = stmt.get(id);
1731
+ return row ? rowToServiceObservation(row) : void 0;
1732
+ }
1733
+ /** Return all ServiceObservations for a given service. */
1734
+ findByServiceId(serviceId) {
1735
+ const stmt = this.db.prepare(
1736
+ `SELECT id, service_id, key, value, confidence, evidence_artifact_id, created_at
1737
+ FROM service_observations
1738
+ WHERE service_id = ?`
1739
+ );
1740
+ const rows = stmt.all(serviceId);
1741
+ return rows.map(rowToServiceObservation);
1742
+ }
1743
+ /** Delete a ServiceObservation by id. Returns true if a row was deleted. */
1744
+ delete(id) {
1745
+ const stmt = this.db.prepare("DELETE FROM service_observations WHERE id = ?");
1746
+ const result = stmt.run(id);
1747
+ return result.changes > 0;
1748
+ }
1749
+ };
1750
+
1751
+ // src/db/repository/endpoint-input-repository.ts
1752
+ import crypto11 from "crypto";
1753
+ function rowToEndpointInput(row) {
1754
+ return {
1755
+ id: row.id,
1756
+ endpointId: row.endpoint_id,
1757
+ inputId: row.input_id,
1758
+ evidenceArtifactId: row.evidence_artifact_id,
1759
+ createdAt: row.created_at
1760
+ };
1761
+ }
1762
+ var EndpointInputRepository = class {
1763
+ db;
1764
+ constructor(db2) {
1765
+ this.db = db2;
1766
+ }
1767
+ /** Insert a new EndpointInput and return the full entity. */
1768
+ create(input) {
1769
+ const id = crypto11.randomUUID();
1770
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1771
+ const stmt = this.db.prepare(
1772
+ `INSERT INTO endpoint_inputs (id, endpoint_id, input_id, evidence_artifact_id, created_at)
1773
+ VALUES (?, ?, ?, ?, ?)`
1774
+ );
1775
+ stmt.run(
1776
+ id,
1777
+ input.endpointId,
1778
+ input.inputId,
1779
+ input.evidenceArtifactId,
1780
+ now
1781
+ );
1782
+ return {
1783
+ id,
1784
+ endpointId: input.endpointId,
1785
+ inputId: input.inputId,
1786
+ evidenceArtifactId: input.evidenceArtifactId,
1787
+ createdAt: now
1788
+ };
1789
+ }
1790
+ /** Find an EndpointInput by its primary key. Returns undefined if not found. */
1791
+ findById(id) {
1792
+ const stmt = this.db.prepare(
1793
+ `SELECT id, endpoint_id, input_id, evidence_artifact_id, created_at
1794
+ FROM endpoint_inputs
1795
+ WHERE id = ?`
1796
+ );
1797
+ const row = stmt.get(id);
1798
+ return row ? rowToEndpointInput(row) : void 0;
1799
+ }
1800
+ /** Find all EndpointInputs for a given endpoint. */
1801
+ findByEndpointId(endpointId) {
1802
+ const stmt = this.db.prepare(
1803
+ `SELECT id, endpoint_id, input_id, evidence_artifact_id, created_at
1804
+ FROM endpoint_inputs
1805
+ WHERE endpoint_id = ?`
1806
+ );
1807
+ const rows = stmt.all(endpointId);
1808
+ return rows.map(rowToEndpointInput);
1809
+ }
1810
+ /** Find all EndpointInputs for a given input. */
1811
+ findByInputId(inputId) {
1812
+ const stmt = this.db.prepare(
1813
+ `SELECT id, endpoint_id, input_id, evidence_artifact_id, created_at
1814
+ FROM endpoint_inputs
1815
+ WHERE input_id = ?`
1816
+ );
1817
+ const rows = stmt.all(inputId);
1818
+ return rows.map(rowToEndpointInput);
1819
+ }
1820
+ /** Delete an EndpointInput by id. Returns true if a row was deleted. */
1821
+ delete(id) {
1822
+ const stmt = this.db.prepare("DELETE FROM endpoint_inputs WHERE id = ?");
1823
+ const result = stmt.run(id);
1824
+ return result.changes > 0;
1825
+ }
1826
+ };
1827
+
1828
+ // src/db/repository/cve-repository.ts
1829
+ import crypto12 from "crypto";
1830
+ function rowToCve(row) {
1831
+ return {
1832
+ id: row.id,
1833
+ vulnerabilityId: row.vulnerability_id,
1834
+ cveId: row.cve_id,
1835
+ description: row.description ?? void 0,
1836
+ cvssScore: row.cvss_score ?? void 0,
1837
+ cvssVector: row.cvss_vector ?? void 0,
1838
+ referenceUrl: row.reference_url ?? void 0,
1839
+ createdAt: row.created_at
1840
+ };
1841
+ }
1842
+ var CveRepository = class {
1843
+ db;
1844
+ constructor(db2) {
1845
+ this.db = db2;
1846
+ }
1847
+ /** Insert a new Cve and return the full entity. */
1848
+ create(input) {
1849
+ const id = crypto12.randomUUID();
1850
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1851
+ const stmt = this.db.prepare(
1852
+ `INSERT INTO cves (id, vulnerability_id, cve_id, description, cvss_score, cvss_vector, reference_url, created_at)
1853
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1854
+ );
1855
+ stmt.run(
1856
+ id,
1857
+ input.vulnerabilityId,
1858
+ input.cveId,
1859
+ input.description ?? null,
1860
+ input.cvssScore ?? null,
1861
+ input.cvssVector ?? null,
1862
+ input.referenceUrl ?? null,
1863
+ now
1864
+ );
1865
+ return {
1866
+ id,
1867
+ vulnerabilityId: input.vulnerabilityId,
1868
+ cveId: input.cveId,
1869
+ description: input.description,
1870
+ cvssScore: input.cvssScore,
1871
+ cvssVector: input.cvssVector,
1872
+ referenceUrl: input.referenceUrl,
1873
+ createdAt: now
1874
+ };
1875
+ }
1876
+ /** Find a Cve by its primary key. Returns undefined if not found. */
1877
+ findById(id) {
1878
+ const stmt = this.db.prepare(
1879
+ `SELECT id, vulnerability_id, cve_id, description, cvss_score, cvss_vector, reference_url, created_at
1880
+ FROM cves
1881
+ WHERE id = ?`
1882
+ );
1883
+ const row = stmt.get(id);
1884
+ return row ? rowToCve(row) : void 0;
1885
+ }
1886
+ /** Return all Cves associated with a given vulnerability. */
1887
+ findByVulnerabilityId(vulnerabilityId) {
1888
+ const stmt = this.db.prepare(
1889
+ `SELECT id, vulnerability_id, cve_id, description, cvss_score, cvss_vector, reference_url, created_at
1890
+ FROM cves
1891
+ WHERE vulnerability_id = ?`
1892
+ );
1893
+ const rows = stmt.all(vulnerabilityId);
1894
+ return rows.map(rowToCve);
1895
+ }
1896
+ /** Delete a Cve by id. Returns true if a row was deleted. */
1897
+ delete(id) {
1898
+ const stmt = this.db.prepare("DELETE FROM cves WHERE id = ?");
1899
+ const result = stmt.run(id);
1900
+ return result.changes > 0;
1901
+ }
1902
+ };
1903
+
1904
+ // src/engine/normalizer.ts
1905
+ function normalize(db2, artifactId, parseResult) {
1906
+ const hostRepo = new HostRepository(db2);
1907
+ const serviceRepo = new ServiceRepository(db2);
1908
+ const serviceObsRepo = new ServiceObservationRepository(db2);
1909
+ const httpEndpointRepo = new HttpEndpointRepository(db2);
1910
+ const inputRepo = new InputRepository(db2);
1911
+ const endpointInputRepo = new EndpointInputRepository(db2);
1912
+ const observationRepo = new ObservationRepository(db2);
1913
+ const vulnRepo = new VulnerabilityRepository(db2);
1914
+ const cveRepo = new CveRepository(db2);
1915
+ const run = db2.transaction(() => {
1916
+ const result = {
1917
+ hostsCreated: 0,
1918
+ servicesCreated: 0,
1919
+ serviceObservationsCreated: 0,
1920
+ httpEndpointsCreated: 0,
1921
+ inputsCreated: 0,
1922
+ endpointInputsCreated: 0,
1923
+ observationsCreated: 0,
1924
+ vulnerabilitiesCreated: 0,
1925
+ cvesCreated: 0
1926
+ };
1927
+ const hostIdByAuthority = /* @__PURE__ */ new Map();
1928
+ const serviceIdByKey = /* @__PURE__ */ new Map();
1929
+ const endpointIdByKey = /* @__PURE__ */ new Map();
1930
+ const inputIdByKey = /* @__PURE__ */ new Map();
1931
+ const vulnIdByTitle = /* @__PURE__ */ new Map();
1932
+ for (const parsed of parseResult.hosts) {
1933
+ const existing = hostRepo.findByAuthority(parsed.authority);
1934
+ if (existing) {
1935
+ hostIdByAuthority.set(parsed.authority, existing.id);
1936
+ } else {
1937
+ const host = hostRepo.create({
1938
+ authorityKind: parsed.authorityKind,
1939
+ authority: parsed.authority,
1940
+ resolvedIpsJson: JSON.stringify(parsed.resolvedIps ?? [])
1941
+ });
1942
+ hostIdByAuthority.set(parsed.authority, host.id);
1943
+ result.hostsCreated++;
1944
+ }
1945
+ }
1946
+ for (const parsed of parseResult.services) {
1947
+ const hostId = hostIdByAuthority.get(parsed.hostAuthority);
1948
+ if (!hostId) continue;
1949
+ const svcKey = `${hostId}:${parsed.transport}:${parsed.port}`;
1950
+ if (serviceIdByKey.has(svcKey)) continue;
1951
+ const existingServices = serviceRepo.findByHostId(hostId);
1952
+ const existing = existingServices.find(
1953
+ (s) => s.transport === parsed.transport && s.port === parsed.port
1954
+ );
1955
+ if (existing) {
1956
+ serviceIdByKey.set(svcKey, existing.id);
1957
+ } else {
1958
+ const service = serviceRepo.create({
1959
+ hostId,
1960
+ transport: parsed.transport,
1961
+ port: parsed.port,
1962
+ appProto: parsed.appProto,
1963
+ protoConfidence: parsed.protoConfidence,
1964
+ banner: parsed.banner,
1965
+ product: parsed.product,
1966
+ version: parsed.version,
1967
+ state: parsed.state,
1968
+ evidenceArtifactId: artifactId
1969
+ });
1970
+ serviceIdByKey.set(svcKey, service.id);
1971
+ result.servicesCreated++;
1972
+ }
1973
+ }
1974
+ function resolveServiceId(hostAuthority, port) {
1975
+ const hostId = hostIdByAuthority.get(hostAuthority);
1976
+ if (!hostId) return void 0;
1977
+ return serviceIdByKey.get(`${hostId}:tcp:${port}`);
1978
+ }
1979
+ for (const parsed of parseResult.serviceObservations) {
1980
+ const hostId = hostIdByAuthority.get(parsed.hostAuthority);
1981
+ if (!hostId) continue;
1982
+ const svcKey = `${hostId}:${parsed.transport}:${parsed.port}`;
1983
+ const serviceId = serviceIdByKey.get(svcKey);
1984
+ if (!serviceId) continue;
1985
+ serviceObsRepo.create({
1986
+ serviceId,
1987
+ key: parsed.key,
1988
+ value: parsed.value,
1989
+ confidence: parsed.confidence,
1990
+ evidenceArtifactId: artifactId
1991
+ });
1992
+ result.serviceObservationsCreated++;
1993
+ }
1994
+ for (const parsed of parseResult.httpEndpoints) {
1995
+ const serviceId = resolveServiceId(parsed.hostAuthority, parsed.port);
1996
+ if (!serviceId) continue;
1997
+ const epKey = `${serviceId}:${parsed.method}:${parsed.path}`;
1998
+ if (endpointIdByKey.has(epKey)) continue;
1999
+ const existingEndpoints = httpEndpointRepo.findByServiceId(serviceId);
2000
+ const existing = existingEndpoints.find(
2001
+ (e) => e.method === parsed.method && e.path === parsed.path
2002
+ );
2003
+ if (existing) {
2004
+ endpointIdByKey.set(epKey, existing.id);
2005
+ } else {
2006
+ const endpoint = httpEndpointRepo.create({
2007
+ serviceId,
2008
+ baseUri: parsed.baseUri,
2009
+ method: parsed.method,
2010
+ path: parsed.path,
2011
+ statusCode: parsed.statusCode,
2012
+ contentLength: parsed.contentLength,
2013
+ words: parsed.words,
2014
+ lines: parsed.lines,
2015
+ evidenceArtifactId: artifactId
2016
+ });
2017
+ endpointIdByKey.set(epKey, endpoint.id);
2018
+ result.httpEndpointsCreated++;
2019
+ }
2020
+ }
2021
+ for (const parsed of parseResult.inputs) {
2022
+ const serviceId = resolveServiceId(parsed.hostAuthority, parsed.port);
2023
+ if (!serviceId) continue;
2024
+ const inKey = `${serviceId}:${parsed.location}:${parsed.name}`;
2025
+ if (inputIdByKey.has(inKey)) continue;
2026
+ const existingInputs = inputRepo.findByServiceId(serviceId);
2027
+ const existing = existingInputs.find(
2028
+ (i) => i.location === parsed.location && i.name === parsed.name
2029
+ );
2030
+ if (existing) {
2031
+ inputIdByKey.set(inKey, existing.id);
2032
+ } else {
2033
+ const input = inputRepo.create({
2034
+ serviceId,
2035
+ location: parsed.location,
2036
+ name: parsed.name,
2037
+ typeHint: parsed.typeHint
2038
+ });
2039
+ inputIdByKey.set(inKey, input.id);
2040
+ result.inputsCreated++;
2041
+ }
2042
+ }
2043
+ for (const parsed of parseResult.endpointInputs) {
2044
+ const serviceId = resolveServiceId(parsed.hostAuthority, parsed.port);
2045
+ if (!serviceId) continue;
2046
+ const epKey = `${serviceId}:${parsed.method}:${parsed.path}`;
2047
+ const endpointId = endpointIdByKey.get(epKey);
2048
+ if (!endpointId) continue;
2049
+ const inKey = `${serviceId}:${parsed.inputLocation}:${parsed.inputName}`;
2050
+ const inputId = inputIdByKey.get(inKey);
2051
+ if (!inputId) continue;
2052
+ const existingLinks = endpointInputRepo.findByEndpointId(endpointId);
2053
+ const alreadyLinked = existingLinks.some((l) => l.inputId === inputId);
2054
+ if (alreadyLinked) continue;
2055
+ endpointInputRepo.create({
2056
+ endpointId,
2057
+ inputId,
2058
+ evidenceArtifactId: artifactId
2059
+ });
2060
+ result.endpointInputsCreated++;
2061
+ }
2062
+ for (const parsed of parseResult.observations) {
2063
+ const serviceId = resolveServiceId(parsed.hostAuthority, parsed.port);
2064
+ if (!serviceId) continue;
2065
+ const inKey = `${serviceId}:${parsed.inputLocation}:${parsed.inputName}`;
2066
+ const inputId = inputIdByKey.get(inKey);
2067
+ if (!inputId) continue;
2068
+ observationRepo.create({
2069
+ inputId,
2070
+ rawValue: parsed.rawValue,
2071
+ normValue: parsed.normValue,
2072
+ source: parsed.source,
2073
+ confidence: parsed.confidence,
2074
+ evidenceArtifactId: artifactId,
2075
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
2076
+ });
2077
+ result.observationsCreated++;
2078
+ }
2079
+ for (const parsed of parseResult.vulnerabilities) {
2080
+ const serviceId = resolveServiceId(parsed.hostAuthority, parsed.port);
2081
+ if (!serviceId) continue;
2082
+ let endpointId;
2083
+ if (parsed.method && parsed.path) {
2084
+ const epKey = `${serviceId}:${parsed.method}:${parsed.path}`;
2085
+ endpointId = endpointIdByKey.get(epKey);
2086
+ }
2087
+ const vuln = vulnRepo.create({
2088
+ serviceId,
2089
+ endpointId,
2090
+ vulnType: parsed.vulnType,
2091
+ title: parsed.title,
2092
+ description: parsed.description,
2093
+ severity: parsed.severity,
2094
+ confidence: parsed.confidence,
2095
+ evidenceArtifactId: artifactId
2096
+ });
2097
+ vulnIdByTitle.set(parsed.title, vuln.id);
2098
+ result.vulnerabilitiesCreated++;
2099
+ }
2100
+ for (const parsed of parseResult.cves) {
2101
+ const vulnId = vulnIdByTitle.get(parsed.vulnerabilityTitle);
2102
+ if (!vulnId) continue;
2103
+ cveRepo.create({
2104
+ vulnerabilityId: vulnId,
2105
+ cveId: parsed.cveId,
2106
+ description: parsed.description,
2107
+ cvssScore: parsed.cvssScore,
2108
+ cvssVector: parsed.cvssVector,
2109
+ referenceUrl: parsed.referenceUrl
2110
+ });
2111
+ result.cvesCreated++;
2112
+ }
2113
+ return result;
2114
+ });
2115
+ return run();
2116
+ }
2117
+
2118
+ // src/engine/ingest.ts
2119
+ function ingestContent(db2, tool, content, filePath) {
2120
+ const sha256 = crypto13.createHash("sha256").update(content).digest("hex");
2121
+ const artifactRepo = new ArtifactRepository(db2);
2122
+ const artifact = artifactRepo.create({
2123
+ tool,
2124
+ kind: "tool_output",
2125
+ path: filePath,
2126
+ sha256,
2127
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2128
+ });
2129
+ let parseResult;
2130
+ switch (tool) {
2131
+ case "nmap":
2132
+ parseResult = parseNmapXml(content);
2133
+ break;
2134
+ case "ffuf":
2135
+ parseResult = parseFfufJson(content);
2136
+ break;
2137
+ case "nuclei":
2138
+ parseResult = parseNucleiJsonl(content);
2139
+ break;
2140
+ default: {
2141
+ const _exhaustive = tool;
2142
+ throw new Error(`Unknown tool: ${String(_exhaustive)}`);
2143
+ }
2144
+ }
2145
+ const normalizeResult = normalize(db2, artifact.id, parseResult);
2146
+ return {
2147
+ artifactId: artifact.id,
2148
+ normalizeResult
2149
+ };
2150
+ }
2151
+ function ingest(db2, input) {
2152
+ const resolved = path.resolve(input.path);
2153
+ const content = fs.readFileSync(resolved, "utf-8");
2154
+ return ingestContent(db2, input.tool, content, resolved);
2155
+ }
2156
+
2157
+ // src/mcp/tools/ingest.ts
2158
+ function registerIngestTool(server2, db2) {
2159
+ server2.tool(
2160
+ "ingest_file",
2161
+ "Ingest a tool output file (nmap XML, ffuf JSON, nuclei JSONL) into the AttackDataGraph",
2162
+ {
2163
+ path: z2.string().describe("Absolute path to the tool output file"),
2164
+ tool: z2.enum(["nmap", "ffuf", "nuclei"]).describe("Tool that produced the output")
2165
+ },
2166
+ async ({ path: path2, tool }) => {
2167
+ try {
2168
+ const result = ingest(db2, { path: path2, tool });
2169
+ const nr = result.normalizeResult;
2170
+ const summary = [
2171
+ `Ingested ${tool} output from ${path2}`,
2172
+ `Artifact ID: ${result.artifactId}`,
2173
+ `Created: ${nr.hostsCreated} hosts, ${nr.servicesCreated} services, ${nr.httpEndpointsCreated} endpoints, ${nr.inputsCreated} inputs, ${nr.observationsCreated} observations, ${nr.vulnerabilitiesCreated} vulnerabilities, ${nr.cvesCreated} CVEs`
2174
+ ].join("\n");
2175
+ return { content: [{ type: "text", text: summary }] };
2176
+ } catch (err) {
2177
+ const message = err instanceof Error ? err.message : String(err);
2178
+ return { content: [{ type: "text", text: `Ingest failed: ${message}` }], isError: true };
2179
+ }
2180
+ }
2181
+ );
2182
+ }
2183
+
2184
+ // src/mcp/tools/propose.ts
2185
+ import { z as z3 } from "zod";
2186
+
2187
+ // src/engine/proposer.ts
2188
+ function propose(db2, hostId) {
2189
+ const hostRepo = new HostRepository(db2);
2190
+ const serviceRepo = new ServiceRepository(db2);
2191
+ const httpEndpointRepo = new HttpEndpointRepository(db2);
2192
+ const inputRepo = new InputRepository(db2);
2193
+ const observationRepo = new ObservationRepository(db2);
2194
+ const vhostRepo = new VhostRepository(db2);
2195
+ const vulnRepo = new VulnerabilityRepository(db2);
2196
+ const actions = [];
2197
+ let hosts;
2198
+ if (hostId !== void 0) {
2199
+ const host = hostRepo.findById(hostId);
2200
+ if (host === void 0) {
2201
+ return [];
2202
+ }
2203
+ hosts = [host];
2204
+ } else {
2205
+ hosts = hostRepo.findAll();
2206
+ }
2207
+ for (const host of hosts) {
2208
+ const services = serviceRepo.findByHostId(host.id);
2209
+ if (services.length === 0) {
2210
+ actions.push({
2211
+ kind: "nmap_scan",
2212
+ description: `Port scan ${host.authority} to discover services`,
2213
+ command: `nmap -p- -sV ${host.authority}`,
2214
+ params: { hostId: host.id }
2215
+ });
2216
+ continue;
2217
+ }
2218
+ for (const service of services) {
2219
+ if (service.appProto !== "http" && service.appProto !== "https") {
2220
+ continue;
2221
+ }
2222
+ const baseUri = `${service.appProto}://${host.authority}:${service.port}`;
2223
+ proposeForHttpService(
2224
+ actions,
2225
+ host,
2226
+ service,
2227
+ baseUri,
2228
+ httpEndpointRepo,
2229
+ inputRepo,
2230
+ observationRepo,
2231
+ vhostRepo,
2232
+ vulnRepo
2233
+ );
2234
+ }
2235
+ }
2236
+ return actions;
2237
+ }
2238
+ function proposeForHttpService(actions, host, service, baseUri, httpEndpointRepo, inputRepo, observationRepo, vhostRepo, vulnRepo) {
2239
+ const endpoints = httpEndpointRepo.findByServiceId(service.id);
2240
+ if (endpoints.length === 0) {
2241
+ actions.push({
2242
+ kind: "ffuf_discovery",
2243
+ description: `Discover endpoints on ${baseUri}`,
2244
+ command: `ffuf -u ${baseUri}/FUZZ -w /usr/share/wordlists/dirb/common.txt`,
2245
+ params: { hostId: host.id, serviceId: service.id }
2246
+ });
2247
+ }
2248
+ for (const endpoint of endpoints) {
2249
+ const inputs = inputRepo.findByServiceId(service.id);
2250
+ if (inputs.length === 0) {
2251
+ actions.push({
2252
+ kind: "parameter_discovery",
2253
+ description: `Discover input parameters for ${baseUri}${endpoint.path}`,
2254
+ params: { hostId: host.id, serviceId: service.id, endpointId: endpoint.id }
2255
+ });
2256
+ }
2257
+ for (const input of inputs) {
2258
+ const observations = observationRepo.findByInputId(input.id);
2259
+ if (observations.length === 0) {
2260
+ actions.push({
2261
+ kind: "value_collection",
2262
+ description: `Collect observed values for input "${input.name}" (${input.location})`,
2263
+ params: {
2264
+ hostId: host.id,
2265
+ serviceId: service.id,
2266
+ endpointId: endpoint.id,
2267
+ inputId: input.id
2268
+ }
2269
+ });
2270
+ }
2271
+ }
2272
+ }
2273
+ const vhosts = vhostRepo.findByHostId(host.id);
2274
+ if (vhosts.length === 0) {
2275
+ actions.push({
2276
+ kind: "vhost_discovery",
2277
+ description: `Discover virtual hosts for ${host.authority}`,
2278
+ params: { hostId: host.id, serviceId: service.id }
2279
+ });
2280
+ }
2281
+ const vulns = vulnRepo.findByServiceId(service.id);
2282
+ if (vulns.length === 0) {
2283
+ actions.push({
2284
+ kind: "nuclei_scan",
2285
+ description: `Scan ${baseUri} for known vulnerabilities`,
2286
+ command: `nuclei -u ${baseUri} -jsonl`,
2287
+ params: { hostId: host.id, serviceId: service.id }
2288
+ });
2289
+ }
2290
+ }
2291
+
2292
+ // src/mcp/tools/propose.ts
2293
+ function registerProposeTool(server2, db2) {
2294
+ server2.tool(
2295
+ "propose",
2296
+ "Analyze the AttackDataGraph for missing data and propose next-step actions",
2297
+ {
2298
+ hostId: z3.string().optional().describe("Limit proposals to a specific host (optional)")
2299
+ },
2300
+ async ({ hostId }) => {
2301
+ const actions = propose(db2, hostId);
2302
+ if (actions.length === 0) {
2303
+ return {
2304
+ content: [{ type: "text", text: "No actions proposed. All discovered data appears complete." }]
2305
+ };
2306
+ }
2307
+ return { content: [{ type: "text", text: JSON.stringify(actions, null, 2) }] };
2308
+ }
2309
+ );
2310
+ }
2311
+
2312
+ // src/mcp/tools/mutation.ts
2313
+ import { z as z4 } from "zod";
2314
+ function getOrCreateManualArtifact(db2) {
2315
+ const artifactRepo = new ArtifactRepository(db2);
2316
+ const existing = artifactRepo.findByTool("manual");
2317
+ if (existing.length > 0) {
2318
+ return existing[0].id;
2319
+ }
2320
+ const artifact = artifactRepo.create({
2321
+ tool: "manual",
2322
+ kind: "manual_entry",
2323
+ path: "manual",
2324
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2325
+ });
2326
+ return artifact.id;
2327
+ }
2328
+ function registerMutationTools(server2, db2) {
2329
+ server2.tool(
2330
+ "add_host",
2331
+ "Manually add a host to the AttackDataGraph",
2332
+ {
2333
+ authority: z4.string().describe("IP address or domain name"),
2334
+ authorityKind: z4.enum(["IP", "DOMAIN"]).describe("Type of authority")
2335
+ },
2336
+ async ({ authority, authorityKind }) => {
2337
+ const hostRepo = new HostRepository(db2);
2338
+ const existing = hostRepo.findByAuthority(authority);
2339
+ if (existing) {
2340
+ return {
2341
+ content: [{ type: "text", text: `Host already exists: ${JSON.stringify(existing, null, 2)}` }]
2342
+ };
2343
+ }
2344
+ const host = hostRepo.create({
2345
+ authorityKind,
2346
+ authority,
2347
+ resolvedIpsJson: "[]"
2348
+ });
2349
+ return { content: [{ type: "text", text: JSON.stringify(host, null, 2) }] };
2350
+ }
2351
+ );
2352
+ server2.tool(
2353
+ "add_credential",
2354
+ "Manually add a credential for a service",
2355
+ {
2356
+ serviceId: z4.string().describe("Service UUID"),
2357
+ username: z4.string().describe("Username"),
2358
+ secret: z4.string().describe("Secret value (password, token, etc.)"),
2359
+ secretType: z4.enum(["password", "token", "api_key", "ssh_key"]).describe("Type of secret"),
2360
+ source: z4.enum(["brute_force", "default", "leaked", "manual"]).describe("How the credential was obtained"),
2361
+ confidence: z4.enum(["high", "medium", "low"]).describe("Confidence level").default("medium")
2362
+ },
2363
+ async ({ serviceId, username, secret, secretType, source, confidence }) => {
2364
+ const artifactId = getOrCreateManualArtifact(db2);
2365
+ const credentialRepo = new CredentialRepository(db2);
2366
+ const credential = credentialRepo.create({
2367
+ serviceId,
2368
+ username,
2369
+ secret,
2370
+ secretType,
2371
+ source,
2372
+ confidence,
2373
+ evidenceArtifactId: artifactId
2374
+ });
2375
+ return { content: [{ type: "text", text: JSON.stringify(credential, null, 2) }] };
2376
+ }
2377
+ );
2378
+ server2.tool(
2379
+ "add_vulnerability",
2380
+ "Manually add a vulnerability for a service",
2381
+ {
2382
+ serviceId: z4.string().describe("Service UUID"),
2383
+ vulnType: z4.string().describe("Vulnerability type (sqli, xss, rce, lfi, ssrf, etc.)"),
2384
+ title: z4.string().describe("Vulnerability title"),
2385
+ severity: z4.enum(["critical", "high", "medium", "low", "info"]).describe("Severity level"),
2386
+ confidence: z4.enum(["high", "medium", "low"]).describe("Confidence level").default("medium"),
2387
+ endpointId: z4.string().optional().describe("HTTP endpoint UUID (optional)"),
2388
+ description: z4.string().optional().describe("Detailed description (optional)")
2389
+ },
2390
+ async ({ serviceId, vulnType, title, severity, confidence, endpointId, description }) => {
2391
+ const artifactId = getOrCreateManualArtifact(db2);
2392
+ const vulnRepo = new VulnerabilityRepository(db2);
2393
+ const vuln = vulnRepo.create({
2394
+ serviceId,
2395
+ endpointId,
2396
+ vulnType,
2397
+ title,
2398
+ description,
2399
+ severity,
2400
+ confidence,
2401
+ evidenceArtifactId: artifactId
2402
+ });
2403
+ return { content: [{ type: "text", text: JSON.stringify(vuln, null, 2) }] };
2404
+ }
2405
+ );
2406
+ server2.tool(
2407
+ "link_cve",
2408
+ "Link a CVE record to an existing vulnerability",
2409
+ {
2410
+ vulnerabilityId: z4.string().describe("Vulnerability UUID"),
2411
+ cveId: z4.string().describe("CVE identifier (e.g. CVE-2021-44228)"),
2412
+ description: z4.string().optional().describe("CVE description"),
2413
+ cvssScore: z4.number().optional().describe("CVSS score (0.0 - 10.0)"),
2414
+ cvssVector: z4.string().optional().describe("CVSS vector string"),
2415
+ referenceUrl: z4.string().optional().describe("Reference URL")
2416
+ },
2417
+ async ({ vulnerabilityId, cveId, description, cvssScore, cvssVector, referenceUrl }) => {
2418
+ const cveRepo = new CveRepository(db2);
2419
+ const cve = cveRepo.create({
2420
+ vulnerabilityId,
2421
+ cveId,
2422
+ description,
2423
+ cvssScore,
2424
+ cvssVector,
2425
+ referenceUrl
2426
+ });
2427
+ return { content: [{ type: "text", text: JSON.stringify(cve, null, 2) }] };
2428
+ }
2429
+ );
2430
+ }
2431
+
2432
+ // src/mcp/resources.ts
2433
+ function registerResources(server2, db2) {
2434
+ const hostRepo = new HostRepository(db2);
2435
+ const serviceRepo = new ServiceRepository(db2);
2436
+ const vhostRepo = new VhostRepository(db2);
2437
+ const httpEndpointRepo = new HttpEndpointRepository(db2);
2438
+ const inputRepo = new InputRepository(db2);
2439
+ const vulnRepo = new VulnerabilityRepository(db2);
2440
+ server2.resource("hosts", "sonobat://hosts", { description: "List of all discovered hosts" }, async () => {
2441
+ const hosts = hostRepo.findAll();
2442
+ return {
2443
+ contents: [
2444
+ {
2445
+ uri: "sonobat://hosts",
2446
+ mimeType: "application/json",
2447
+ text: JSON.stringify(hosts, null, 2)
2448
+ }
2449
+ ]
2450
+ };
2451
+ });
2452
+ server2.resource(
2453
+ "host-detail",
2454
+ "sonobat://hosts/{id}",
2455
+ { description: "Detailed host tree with services, endpoints, inputs, and vulnerabilities" },
2456
+ async (uri) => {
2457
+ const hostId = uri.pathname.split("/").pop() ?? "";
2458
+ const host = hostRepo.findById(hostId);
2459
+ if (!host) {
2460
+ return {
2461
+ contents: [
2462
+ {
2463
+ uri: uri.href,
2464
+ mimeType: "application/json",
2465
+ text: JSON.stringify({ error: `Host not found: ${hostId}` })
2466
+ }
2467
+ ]
2468
+ };
2469
+ }
2470
+ const services = serviceRepo.findByHostId(hostId);
2471
+ const vhosts = vhostRepo.findByHostId(hostId);
2472
+ const serviceTree = services.map((service) => {
2473
+ const endpoints = httpEndpointRepo.findByServiceId(service.id);
2474
+ const inputs = inputRepo.findByServiceId(service.id);
2475
+ const vulnerabilities = vulnRepo.findByServiceId(service.id);
2476
+ return { ...service, endpoints, inputs, vulnerabilities };
2477
+ });
2478
+ const result = { ...host, services: serviceTree, vhosts };
2479
+ return {
2480
+ contents: [
2481
+ {
2482
+ uri: uri.href,
2483
+ mimeType: "application/json",
2484
+ text: JSON.stringify(result, null, 2)
2485
+ }
2486
+ ]
2487
+ };
2488
+ }
2489
+ );
2490
+ server2.resource(
2491
+ "summary",
2492
+ "sonobat://summary",
2493
+ { description: "Summary statistics of the AttackDataGraph" },
2494
+ async () => {
2495
+ const counts = {};
2496
+ const tables = [
2497
+ "hosts",
2498
+ "services",
2499
+ "http_endpoints",
2500
+ "inputs",
2501
+ "observations",
2502
+ "credentials",
2503
+ "vulnerabilities",
2504
+ "cves",
2505
+ "vhosts",
2506
+ "artifacts"
2507
+ ];
2508
+ for (const table of tables) {
2509
+ const row = db2.prepare(`SELECT COUNT(*) as count FROM ${table}`).get();
2510
+ counts[table] = row.count;
2511
+ }
2512
+ return {
2513
+ contents: [
2514
+ {
2515
+ uri: "sonobat://summary",
2516
+ mimeType: "application/json",
2517
+ text: JSON.stringify(counts, null, 2)
2518
+ }
2519
+ ]
2520
+ };
2521
+ }
2522
+ );
2523
+ }
2524
+
2525
+ // src/mcp/server.ts
2526
+ function createMcpServer(db2) {
2527
+ const server2 = new McpServer({
2528
+ name: "sonobat",
2529
+ version: "0.1.0"
2530
+ });
2531
+ registerQueryTools(server2, db2);
2532
+ registerIngestTool(server2, db2);
2533
+ registerProposeTool(server2, db2);
2534
+ registerMutationTools(server2, db2);
2535
+ registerResources(server2, db2);
2536
+ return server2;
2537
+ }
2538
+
2539
+ // src/index.ts
2540
+ var DB_PATH = process.env["SONOBAT_DB_PATH"] ?? "sonobat.db";
2541
+ var db = new Database(DB_PATH);
2542
+ migrateDatabase(db);
2543
+ var server = createMcpServer(db);
2544
+ var transport = new StdioServerTransport();
2545
+ await server.connect(transport);
2546
+ //# sourceMappingURL=index.js.map