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.
- package/bin/cipher.js +566 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +991 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +288 -0
- 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
|
+
}
|