cipher-security 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/bin/cipher.js +566 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +238 -0
  45. package/lib/brand.js +105 -0
  46. package/lib/commands.js +100 -0
  47. package/lib/complexity.js +377 -0
  48. package/lib/config.js +213 -0
  49. package/lib/gateway/client.js +309 -0
  50. package/lib/gateway/commands.js +991 -0
  51. package/lib/gateway/config-validate.js +109 -0
  52. package/lib/gateway/gateway.js +367 -0
  53. package/lib/gateway/index.js +62 -0
  54. package/lib/gateway/mode.js +309 -0
  55. package/lib/gateway/plugins.js +222 -0
  56. package/lib/gateway/prompt.js +214 -0
  57. package/lib/mcp/server.js +262 -0
  58. package/lib/memory/compressor.js +425 -0
  59. package/lib/memory/engine.js +763 -0
  60. package/lib/memory/evolution.js +668 -0
  61. package/lib/memory/index.js +58 -0
  62. package/lib/memory/orchestrator.js +506 -0
  63. package/lib/memory/retriever.js +515 -0
  64. package/lib/memory/synthesizer.js +333 -0
  65. package/lib/pipeline/async-scanner.js +510 -0
  66. package/lib/pipeline/binary-analysis.js +1043 -0
  67. package/lib/pipeline/dom-xss-scanner.js +435 -0
  68. package/lib/pipeline/github-actions.js +792 -0
  69. package/lib/pipeline/index.js +124 -0
  70. package/lib/pipeline/osint.js +498 -0
  71. package/lib/pipeline/sarif.js +373 -0
  72. package/lib/pipeline/scanner.js +880 -0
  73. package/lib/pipeline/template-manager.js +525 -0
  74. package/lib/pipeline/xss-scanner.js +353 -0
  75. package/lib/setup-wizard.js +288 -0
  76. package/package.json +31 -0
@@ -0,0 +1,684 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * INCIDENT mode agent — Forensic Triage.
7
+ *
8
+ * Analyzes PCAP data and produces structured forensic reports.
9
+ * Ported from autonomous/modes/incident.py.
10
+ *
11
+ * PCAP parsing uses Node.js Buffer API (readUInt32LE/BE, readUInt16BE, readUInt8).
12
+ * Critical: PCAP header magic 0xa1b2c3d4 (little-endian) vs 0xd4c3b2a1
13
+ * (big-endian) determines byte order for all subsequent reads.
14
+ *
15
+ * @module autonomous/modes/incident
16
+ */
17
+
18
+ import { readFileSync } from 'node:fs';
19
+ import { ModeAgentConfig, ToolRegistry } from '../framework.js';
20
+ import { ForensicValidator } from '../validators/forensic.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // PCAP constants
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const PCAP_MAGIC_LE = 0xA1B2C3D4;
27
+ const PCAP_MAGIC_BE = 0xD4C3B2A1;
28
+ const PCAP_GLOBAL_HEADER_SIZE = 24;
29
+ const PCAP_PACKET_HEADER_SIZE = 16;
30
+ const ETHERNET_HEADER_SIZE = 14;
31
+ const IPV4_MIN_HEADER_SIZE = 20;
32
+ const UDP_HEADER_SIZE = 8;
33
+ const DNS_HEADER_SIZE = 12;
34
+
35
+ const ETHERTYPE_IPV4 = 0x0800;
36
+ const PROTO_ICMP = 1;
37
+ const PROTO_TCP = 6;
38
+ const PROTO_UDP = 17;
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // PCAP parsing helpers (Buffer API)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Format 6-byte MAC address as colon-separated hex string.
46
+ * @param {Buffer} buf
47
+ * @param {number} offset
48
+ * @returns {string}
49
+ */
50
+ export function _formatMac(buf, offset) {
51
+ const bytes = [];
52
+ for (let i = 0; i < 6; i++) {
53
+ bytes.push(buf.readUInt8(offset + i).toString(16).padStart(2, '0'));
54
+ }
55
+ return bytes.join(':');
56
+ }
57
+
58
+ /**
59
+ * Format a 32-bit integer as a dotted-quad IPv4 address.
60
+ * @param {number} ipInt - 32-bit integer in network (big-endian) byte order
61
+ * @returns {string}
62
+ */
63
+ export function _formatIpv4(ipInt) {
64
+ return [
65
+ (ipInt >>> 24) & 0xff,
66
+ (ipInt >>> 16) & 0xff,
67
+ (ipInt >>> 8) & 0xff,
68
+ ipInt & 0xff,
69
+ ].join('.');
70
+ }
71
+
72
+ /**
73
+ * Parse 24-byte PCAP global header.
74
+ *
75
+ * @param {Buffer} data
76
+ * @returns {Object} header with magic, endian ('<' or '>'), version info, snaplen, link_type
77
+ * @throws {Error} If magic number is invalid
78
+ */
79
+ export function _parsePcapGlobalHeader(data) {
80
+ if (data.length < PCAP_GLOBAL_HEADER_SIZE) {
81
+ throw new Error(
82
+ `PCAP data too short for global header: ${data.length} bytes (need ${PCAP_GLOBAL_HEADER_SIZE})`
83
+ );
84
+ }
85
+
86
+ // Read magic to determine endianness
87
+ const magic = data.readUInt32LE(0);
88
+ let endian;
89
+ if (magic === PCAP_MAGIC_LE) {
90
+ endian = '<'; // little-endian
91
+ } else if (magic === PCAP_MAGIC_BE) {
92
+ endian = '>'; // big-endian
93
+ } else {
94
+ throw new Error(
95
+ `Invalid PCAP magic number: 0x${magic.toString(16).padStart(8, '0')} ` +
96
+ `(expected 0x${PCAP_MAGIC_LE.toString(16)} or 0x${PCAP_MAGIC_BE.toString(16)})`
97
+ );
98
+ }
99
+
100
+ const readU32 = endian === '<' ? (b, o) => b.readUInt32LE(o) : (b, o) => b.readUInt32BE(o);
101
+ const readU16 = endian === '<' ? (b, o) => b.readUInt16LE(o) : (b, o) => b.readUInt16BE(o);
102
+ const readI32 = endian === '<' ? (b, o) => b.readInt32LE(o) : (b, o) => b.readInt32BE(o);
103
+
104
+ return {
105
+ magic: readU32(data, 0),
106
+ endian,
107
+ version_major: readU16(data, 4),
108
+ version_minor: readU16(data, 6),
109
+ thiszone: readI32(data, 8),
110
+ sigfigs: readU32(data, 12),
111
+ snaplen: readU32(data, 16),
112
+ link_type: readU32(data, 20),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Parse a DNS QNAME from data starting at offset.
118
+ * @param {Buffer} data
119
+ * @param {number} offset
120
+ * @returns {string|null}
121
+ */
122
+ export function _parseDnsQueryName(data, offset) {
123
+ const labels = [];
124
+ let pos = offset;
125
+ const maxPos = Math.min(offset + 255, data.length);
126
+
127
+ while (pos < maxPos) {
128
+ let labelLen;
129
+ try {
130
+ labelLen = data.readUInt8(pos);
131
+ } catch {
132
+ return null;
133
+ }
134
+
135
+ if (labelLen === 0) break;
136
+
137
+ // Pointer (compression) — not expected in queries but handle it
138
+ if ((labelLen & 0xC0) === 0xC0) return null;
139
+
140
+ pos += 1;
141
+ if (pos + labelLen > data.length) return null;
142
+
143
+ try {
144
+ labels.push(data.subarray(pos, pos + labelLen).toString('ascii'));
145
+ } catch {
146
+ return null;
147
+ }
148
+
149
+ pos += labelLen;
150
+ }
151
+
152
+ return labels.length > 0 ? labels.join('.') : null;
153
+ }
154
+
155
+ /**
156
+ * Parse packet records from PCAP data starting at offset.
157
+ *
158
+ * Parses Ethernet → IPv4 → TCP/UDP → DNS (if port 53).
159
+ * Truncated/malformed packets are skipped gracefully.
160
+ *
161
+ * @param {Buffer} data
162
+ * @param {number} offset
163
+ * @param {string} endian - '<' for little-endian, '>' for big-endian
164
+ * @returns {Array<Object>} list of packet dicts
165
+ */
166
+ export function _parsePackets(data, offset, endian) {
167
+ const packets = [];
168
+ let pos = offset;
169
+
170
+ const readU32 = endian === '<' ? (b, o) => b.readUInt32LE(o) : (b, o) => b.readUInt32BE(o);
171
+
172
+ while (pos + PCAP_PACKET_HEADER_SIZE <= data.length) {
173
+ let tsSec, tsUsec, inclLen;
174
+ try {
175
+ tsSec = readU32(data, pos);
176
+ tsUsec = readU32(data, pos + 4);
177
+ inclLen = readU32(data, pos + 8);
178
+ // origLen = readU32(data, pos + 12); // not used
179
+ } catch {
180
+ break;
181
+ }
182
+
183
+ const pktStart = pos + PCAP_PACKET_HEADER_SIZE;
184
+ const pktEnd = pktStart + inclLen;
185
+
186
+ if (pktEnd > data.length) break; // Truncated file
187
+
188
+ const pktData = data.subarray(pktStart, pktEnd);
189
+ pos = pktEnd;
190
+
191
+ const packet = {
192
+ timestamp: tsSec + tsUsec / 1_000_000,
193
+ protocols: [],
194
+ src_ip: null,
195
+ dst_ip: null,
196
+ src_port: null,
197
+ dst_port: null,
198
+ dns_query: null,
199
+ };
200
+
201
+ // --- Ethernet frame ---
202
+ if (pktData.length < ETHERNET_HEADER_SIZE) {
203
+ packets.push(packet);
204
+ continue;
205
+ }
206
+
207
+ let ethertype;
208
+ try {
209
+ ethertype = pktData.readUInt16BE(12);
210
+ } catch {
211
+ packets.push(packet);
212
+ continue;
213
+ }
214
+
215
+ packet.protocols.push('Ethernet');
216
+
217
+ if (ethertype !== ETHERTYPE_IPV4) {
218
+ packets.push(packet);
219
+ continue;
220
+ }
221
+
222
+ // --- IPv4 header ---
223
+ const ipStart = ETHERNET_HEADER_SIZE;
224
+ if (pktData.length < ipStart + IPV4_MIN_HEADER_SIZE) {
225
+ packets.push(packet);
226
+ continue;
227
+ }
228
+
229
+ let ihl, protocol, srcIpRaw, dstIpRaw;
230
+ try {
231
+ const versionIhl = pktData.readUInt8(ipStart);
232
+ ihl = (versionIhl & 0x0F) * 4;
233
+ if (ihl < IPV4_MIN_HEADER_SIZE) ihl = IPV4_MIN_HEADER_SIZE;
234
+
235
+ protocol = pktData.readUInt8(ipStart + 9);
236
+ srcIpRaw = pktData.readUInt32BE(ipStart + 12);
237
+ dstIpRaw = pktData.readUInt32BE(ipStart + 16);
238
+ } catch {
239
+ packets.push(packet);
240
+ continue;
241
+ }
242
+
243
+ packet.protocols.push('IPv4');
244
+ packet.src_ip = _formatIpv4(srcIpRaw);
245
+ packet.dst_ip = _formatIpv4(dstIpRaw);
246
+
247
+ const transportStart = ipStart + ihl;
248
+
249
+ // --- TCP ---
250
+ if (protocol === PROTO_TCP) {
251
+ if (pktData.length >= transportStart + 4) {
252
+ try {
253
+ packet.src_port = pktData.readUInt16BE(transportStart);
254
+ packet.dst_port = pktData.readUInt16BE(transportStart + 2);
255
+ packet.protocols.push('TCP');
256
+ } catch {
257
+ // skip
258
+ }
259
+ }
260
+ }
261
+
262
+ // --- UDP ---
263
+ else if (protocol === PROTO_UDP) {
264
+ if (pktData.length >= transportStart + UDP_HEADER_SIZE) {
265
+ try {
266
+ packet.src_port = pktData.readUInt16BE(transportStart);
267
+ packet.dst_port = pktData.readUInt16BE(transportStart + 2);
268
+ packet.protocols.push('UDP');
269
+
270
+ // DNS query parsing (port 53)
271
+ if (packet.dst_port === 53 || packet.src_port === 53) {
272
+ const dnsStart = transportStart + UDP_HEADER_SIZE;
273
+ if (pktData.length >= dnsStart + DNS_HEADER_SIZE) {
274
+ const queryName = _parseDnsQueryName(pktData, dnsStart + DNS_HEADER_SIZE);
275
+ if (queryName) {
276
+ packet.dns_query = queryName;
277
+ packet.protocols.push('DNS');
278
+ }
279
+ }
280
+ }
281
+ } catch {
282
+ // skip
283
+ }
284
+ }
285
+ }
286
+
287
+ // --- ICMP ---
288
+ else if (protocol === PROTO_ICMP) {
289
+ packet.protocols.push('ICMP');
290
+ }
291
+
292
+ packets.push(packet);
293
+ }
294
+
295
+ return packets;
296
+ }
297
+
298
+ /**
299
+ * Load PCAP data from file path or context.
300
+ * @param {*} context
301
+ * @param {Object} toolInput
302
+ * @returns {Buffer}
303
+ */
304
+ function _loadPcapData(context, toolInput) {
305
+ const pcapPath = toolInput.pcap_path || '';
306
+
307
+ if (pcapPath) {
308
+ try {
309
+ return readFileSync(pcapPath);
310
+ } catch {
311
+ // Fall through to context
312
+ }
313
+ }
314
+
315
+ if (typeof context === 'object' && context !== null && context.pcap_data) {
316
+ return Buffer.isBuffer(context.pcap_data) ? context.pcap_data : Buffer.from(context.pcap_data);
317
+ }
318
+
319
+ throw new Error(`PCAP file not found: '${pcapPath}' and no pcap_data in context`);
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Tool handlers
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /**
327
+ * Analyze PCAP file: packet count, protocol breakdown, IP addresses.
328
+ * @param {*} context
329
+ * @param {Object} toolInput
330
+ * @returns {string}
331
+ */
332
+ export function _incidentAnalyzePcap(context, toolInput) {
333
+ let data;
334
+ try {
335
+ data = _loadPcapData(context, toolInput);
336
+ } catch (e) {
337
+ return `ERROR: ${e.message}`;
338
+ }
339
+
340
+ let header;
341
+ try {
342
+ header = _parsePcapGlobalHeader(data);
343
+ } catch (e) {
344
+ return `ERROR: ${e.message}`;
345
+ }
346
+
347
+ const packets = _parsePackets(data, PCAP_GLOBAL_HEADER_SIZE, header.endian);
348
+
349
+ if (packets.length === 0) {
350
+ return (
351
+ `PCAP Analysis: ${toolInput.pcap_path || 'in-memory'}\n` +
352
+ `Packets: 0\n` +
353
+ `No packets found in capture.`
354
+ );
355
+ }
356
+
357
+ // Duration
358
+ const timestamps = packets.map(p => p.timestamp);
359
+ const duration = timestamps.length > 1
360
+ ? Math.max(...timestamps) - Math.min(...timestamps)
361
+ : 0.0;
362
+
363
+ // Protocol counts
364
+ const protoCounts = { TCP: 0, UDP: 0, ICMP: 0, Other: 0 };
365
+ for (const pkt of packets) {
366
+ const protos = pkt.protocols;
367
+ if (protos.includes('TCP')) protoCounts.TCP++;
368
+ else if (protos.includes('UDP')) protoCounts.UDP++;
369
+ else if (protos.includes('ICMP')) protoCounts.ICMP++;
370
+ else protoCounts.Other++;
371
+ }
372
+
373
+ // Unique IPs
374
+ const ips = new Set();
375
+ for (const pkt of packets) {
376
+ if (pkt.src_ip) ips.add(pkt.src_ip);
377
+ if (pkt.dst_ip) ips.add(pkt.dst_ip);
378
+ }
379
+
380
+ const protoStr = Object.entries(protoCounts)
381
+ .filter(([, v]) => v > 0)
382
+ .map(([k, v]) => `${k}: ${v}`)
383
+ .join(', ');
384
+
385
+ return (
386
+ `PCAP Analysis: ${toolInput.pcap_path || 'in-memory'}\n` +
387
+ `Packets: ${packets.length}\n` +
388
+ `Duration: ${duration.toFixed(3)}s\n` +
389
+ `Protocols: ${protoStr}\n` +
390
+ `Unique IPs: ${[...ips].sort().join(', ')}`
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Extract unique connection tuples from PCAP.
396
+ * @param {*} context
397
+ * @param {Object} toolInput
398
+ * @returns {string}
399
+ */
400
+ export function _incidentExtractConnections(context, toolInput) {
401
+ let data;
402
+ try {
403
+ data = _loadPcapData(context, toolInput);
404
+ } catch (e) {
405
+ return `ERROR: ${e.message}`;
406
+ }
407
+
408
+ let header;
409
+ try {
410
+ header = _parsePcapGlobalHeader(data);
411
+ } catch (e) {
412
+ return `ERROR: ${e.message}`;
413
+ }
414
+
415
+ const packets = _parsePackets(data, PCAP_GLOBAL_HEADER_SIZE, header.endian);
416
+
417
+ const connections = new Set();
418
+ for (const pkt of packets) {
419
+ if (pkt.src_ip && pkt.src_port !== null) {
420
+ const proto = pkt.protocols.includes('TCP') ? 'TCP' : 'UDP';
421
+ connections.add(
422
+ `${pkt.src_ip}:${pkt.src_port} → ${pkt.dst_ip}:${pkt.dst_port} (${proto})`
423
+ );
424
+ }
425
+ }
426
+
427
+ if (connections.size === 0) {
428
+ return 'No connections found in capture.';
429
+ }
430
+
431
+ const lines = [...connections].sort();
432
+ return `Connections (${lines.length}):\n` + lines.map(c => ` ${c}`).join('\n');
433
+ }
434
+
435
+ /**
436
+ * Extract DNS query names from UDP port 53 packets.
437
+ * @param {*} context
438
+ * @param {Object} toolInput
439
+ * @returns {string}
440
+ */
441
+ export function _incidentExtractDnsQueries(context, toolInput) {
442
+ let data;
443
+ try {
444
+ data = _loadPcapData(context, toolInput);
445
+ } catch (e) {
446
+ return `ERROR: ${e.message}`;
447
+ }
448
+
449
+ let header;
450
+ try {
451
+ header = _parsePcapGlobalHeader(data);
452
+ } catch (e) {
453
+ return `ERROR: ${e.message}`;
454
+ }
455
+
456
+ const packets = _parsePackets(data, PCAP_GLOBAL_HEADER_SIZE, header.endian);
457
+
458
+ const domains = new Set();
459
+ for (const pkt of packets) {
460
+ if (pkt.dns_query) domains.add(pkt.dns_query);
461
+ }
462
+
463
+ if (domains.size === 0) {
464
+ return 'No DNS queries found in capture.';
465
+ }
466
+
467
+ const sorted = [...domains].sort();
468
+ return `DNS Queries (${sorted.length}):\n` + sorted.map(d => ` ${d}`).join('\n');
469
+ }
470
+
471
+ /**
472
+ * Store forensic report JSON in context.
473
+ * @param {*} context
474
+ * @param {Object} toolInput
475
+ * @returns {string}
476
+ */
477
+ export function _incidentWriteForensicReport(context, toolInput) {
478
+ const report = toolInput.report || '';
479
+
480
+ if (typeof context !== 'object' || context === null) {
481
+ return 'ERROR: Context must be a dict.';
482
+ }
483
+
484
+ let reportData;
485
+ if (typeof report === 'string') {
486
+ try {
487
+ reportData = JSON.parse(report);
488
+ } catch {
489
+ reportData = report;
490
+ }
491
+ } else {
492
+ reportData = report;
493
+ }
494
+
495
+ context.report = reportData;
496
+ const filename = toolInput.filename || 'forensic_report.json';
497
+
498
+ return (
499
+ `Forensic report stored as ${filename}. ` +
500
+ `Report is available in context['report'].`
501
+ );
502
+ }
503
+
504
+ // ---------------------------------------------------------------------------
505
+ // Tool schemas (Anthropic format)
506
+ // ---------------------------------------------------------------------------
507
+
508
+ const _ANALYZE_PCAP_SCHEMA = {
509
+ name: 'analyze_pcap',
510
+ description:
511
+ 'Analyze a PCAP capture file. Parses packet headers, extracts ' +
512
+ 'protocol statistics (TCP/UDP/ICMP), packet counts, capture ' +
513
+ 'duration, and unique IP addresses. Returns a formatted overview.',
514
+ input_schema: {
515
+ type: 'object',
516
+ properties: {
517
+ pcap_path: {
518
+ type: 'string',
519
+ description: 'Path to the PCAP file to analyze',
520
+ },
521
+ },
522
+ required: ['pcap_path'],
523
+ },
524
+ };
525
+
526
+ const _EXTRACT_CONNECTIONS_SCHEMA = {
527
+ name: 'extract_connections',
528
+ description:
529
+ 'Extract unique network connections from a PCAP file. Returns ' +
530
+ 'connection tuples: source IP:port → destination IP:port with protocol.',
531
+ input_schema: {
532
+ type: 'object',
533
+ properties: {
534
+ pcap_path: {
535
+ type: 'string',
536
+ description: 'Path to the PCAP file to analyze',
537
+ },
538
+ },
539
+ required: ['pcap_path'],
540
+ },
541
+ };
542
+
543
+ const _EXTRACT_DNS_QUERIES_SCHEMA = {
544
+ name: 'extract_dns_queries',
545
+ description:
546
+ 'Extract DNS query names from a PCAP file. Parses UDP port 53 ' +
547
+ 'packets and returns queried domain names.',
548
+ input_schema: {
549
+ type: 'object',
550
+ properties: {
551
+ pcap_path: {
552
+ type: 'string',
553
+ description: 'Path to the PCAP file to analyze',
554
+ },
555
+ },
556
+ required: ['pcap_path'],
557
+ },
558
+ };
559
+
560
+ const _WRITE_FORENSIC_REPORT_SCHEMA = {
561
+ name: 'write_forensic_report',
562
+ description:
563
+ 'Write the completed forensic report as JSON. Stored in context ' +
564
+ 'and validated for structural correctness.',
565
+ input_schema: {
566
+ type: 'object',
567
+ properties: {
568
+ report: {
569
+ type: 'string',
570
+ description:
571
+ 'Full JSON forensic report with required sections: summary, ' +
572
+ 'services, connections, findings, recommendations',
573
+ },
574
+ filename: {
575
+ type: 'string',
576
+ description: 'Filename for the report (e.g. forensic_report.json)',
577
+ },
578
+ },
579
+ required: ['report'],
580
+ },
581
+ };
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // System prompt template
585
+ // ---------------------------------------------------------------------------
586
+
587
+ const _INCIDENT_SYSTEM_PROMPT = `\
588
+ You are an expert digital forensics analyst performing incident triage on a \
589
+ network packet capture. Your task is to analyze the PCAP file, identify \
590
+ services, protocols, and anomalies, and produce a structured forensic report.
591
+
592
+ ## Target
593
+ PCAP File: {pcap_path}
594
+ Description: {pcap_description}
595
+
596
+ ## Workflow
597
+ 1. Use \`analyze_pcap\` to get an overview of the capture
598
+ 2. Use \`extract_connections\` to map the network topology
599
+ 3. Use \`extract_dns_queries\` to identify domain name activity
600
+ 4. Synthesize findings: identify services, flag suspicious activity, reference CVEs
601
+ 5. Use \`write_forensic_report\` to submit the complete JSON forensic report
602
+
603
+ ## Output Format
604
+ Your forensic report MUST be valid JSON with these required sections:
605
+ - **summary**: Brief text overview of the capture and key findings
606
+ - **services**: List of identified services (port, protocol, description)
607
+ - **connections**: List of notable connections (src, dst, port, notes)
608
+ - **findings**: List of findings (severity, description, evidence, CVE references)
609
+ - **recommendations**: List of recommended actions based on findings
610
+
611
+ ## CVE Format
612
+ Use standard format: CVE-YYYY-NNNNN (e.g., CVE-2024-12345).
613
+
614
+ ## Important Notes
615
+ - Focus on actionable findings — not every packet needs to be reported
616
+ - Prioritize findings by severity (critical, high, medium, low)
617
+ - Include evidence for each finding
618
+ - Flag any indicators of compromise (IOCs) prominently
619
+ `;
620
+
621
+ // ---------------------------------------------------------------------------
622
+ // Output parser (fallback for text-based output)
623
+ // ---------------------------------------------------------------------------
624
+
625
+ /**
626
+ * Extract JSON forensic report from LLM text output.
627
+ * @param {string} text
628
+ * @returns {Object}
629
+ */
630
+ export function _incidentOutputParser(text) {
631
+ if (!text || !text.trim()) {
632
+ return { report: {}, raw_text: text };
633
+ }
634
+
635
+ const matches = [...text.matchAll(/```(?:json)?\s*\n(.*?)```/gs)].map(m => m[1]);
636
+ const jsonText = matches.length > 0 ? matches.join('\n') : text;
637
+
638
+ try {
639
+ const parsed = JSON.parse(jsonText);
640
+ return typeof parsed === 'object' && parsed !== null ? parsed : { report: parsed };
641
+ } catch (e) {
642
+ return { raw_text: text, parse_error: e.message };
643
+ }
644
+ }
645
+
646
+ // ---------------------------------------------------------------------------
647
+ // Factory function
648
+ // ---------------------------------------------------------------------------
649
+
650
+ /**
651
+ * Build an INCIDENT-mode ModeAgentConfig for forensic triage.
652
+ * @returns {ModeAgentConfig}
653
+ */
654
+ function _makeIncidentConfig() {
655
+ const reg = new ToolRegistry();
656
+ reg.register('analyze_pcap', _ANALYZE_PCAP_SCHEMA, _incidentAnalyzePcap);
657
+ reg.register('extract_connections', _EXTRACT_CONNECTIONS_SCHEMA, _incidentExtractConnections);
658
+ reg.register('extract_dns_queries', _EXTRACT_DNS_QUERIES_SCHEMA, _incidentExtractDnsQueries);
659
+ reg.register('write_forensic_report', _WRITE_FORENSIC_REPORT_SCHEMA, _incidentWriteForensicReport);
660
+
661
+ return new ModeAgentConfig({
662
+ mode: 'INCIDENT',
663
+ toolRegistry: reg,
664
+ systemPromptTemplate: _INCIDENT_SYSTEM_PROMPT,
665
+ validator: new ForensicValidator(),
666
+ maxTurns: 15,
667
+ requiresSandbox: false,
668
+ completionCheck: null,
669
+ outputParser: _incidentOutputParser,
670
+ outputFormat: 'json',
671
+ });
672
+ }
673
+
674
+ // ---------------------------------------------------------------------------
675
+ // Registration function — called by runner.initModes()
676
+ // ---------------------------------------------------------------------------
677
+
678
+ /**
679
+ * Register INCIDENT mode with the given registerMode function.
680
+ * @param {Function} registerMode
681
+ */
682
+ export function register(registerMode) {
683
+ registerMode('INCIDENT', _makeIncidentConfig);
684
+ }