@jshookmcp/jshook 0.2.8 → 0.2.9
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/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-BNk-EoBt.mjs} +3 -3
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-Cq8q01kp.mjs} +5 -5
- package/dist/ConsoleMonitor-CPVQW1Y-.mjs +2201 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-BNPxu0RH.mjs} +1 -1
- package/dist/DetailedDataManager-BQQcxh64.mjs +217 -0
- package/dist/EventBus-DgPmwpeu.mjs +141 -0
- package/dist/EvidenceGraphBridge-SFesNera.mjs +153 -0
- package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-CWYgw0YW.mjs} +13 -6
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-gzWtkKuf.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-B9gZCdFP.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-BLDH0dCv.mjs} +4 -4
- package/dist/HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs +639 -0
- package/dist/InstrumentationSession-CvPC7Jwy.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CbVdCIJF.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-BsDZbLYm.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-Bcpml6II.mjs} +44 -18
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-dZtA1ZGn.mjs} +14 -53
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-B-FjA2mJ.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-D1lzJ_VG.mjs} +2 -2
- package/dist/PageController-Bqm2kZ_X.mjs +417 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-BOhyVsjx.mjs} +4 -4
- package/dist/PrerequisiteError-Dl33Svkz.mjs +20 -0
- package/dist/ResponseBuilder-D3iFYx2N.mjs +143 -0
- package/dist/ReverseEvidenceGraph-Dlsk94LC.mjs +269 -0
- package/dist/ScriptManager-aHHq0X7U.mjs +3000 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-CqdIFlQl.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-DhFaPvRO.mjs} +3 -3
- package/dist/ToolCatalog-C0JGZoOm.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-oC7aPrkv.mjs +45 -0
- package/dist/ToolRegistry-BjaF4oNz.mjs +131 -0
- package/dist/ToolRouter.policy-BWV67ZK-.mjs +304 -0
- package/dist/TraceRecorder-DgxyVbdQ.mjs +519 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-CePkipZY.mjs} +1 -1
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-BvKs-gxc.mjs} +2 -2
- package/dist/WorkflowEngine-CuvkZtWu.mjs +598 -0
- package/dist/analysis-CL9uACt9.mjs +463 -0
- package/dist/antidebug-CqDTB_uk.mjs +1081 -0
- package/dist/artifactRetention-CFEprwPw.mjs +591 -0
- package/dist/artifacts-Bk2-_uPq.mjs +59 -0
- package/dist/betterSqlite3-0pqusHHH.mjs +74 -0
- package/dist/binary-instrument-CXfpx6fT.mjs +979 -0
- package/dist/bind-helpers-xFfRF-qm.mjs +22 -0
- package/dist/boringssl-inspector-BH2D3VKc.mjs +180 -0
- package/dist/browser-BpOr5PEx.mjs +4082 -0
- package/dist/concurrency-Bt0yv1kJ.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-B0OANIBL.mjs} +88 -46
- package/dist/coordination-qUbyF8KU.mjs +259 -0
- package/dist/debugger-gnKxRSN0.mjs +1271 -0
- package/dist/definitions-6M-eejaT.mjs +53 -0
- package/dist/definitions-B18eyf0B.mjs +18 -0
- package/dist/definitions-B3QdlrHv.mjs +34 -0
- package/dist/definitions-B4rAvHNZ.mjs +63 -0
- package/dist/definitions-BB_4jnmy.mjs +37 -0
- package/dist/definitions-BMfYXoNC.mjs +43 -0
- package/dist/definitions-Beid2EB3.mjs +27 -0
- package/dist/definitions-C1UvM5Iy.mjs +126 -0
- package/dist/definitions-CXEI7QC72.mjs +216 -0
- package/dist/definitions-C_4r7Fo-2.mjs +14 -0
- package/dist/definitions-CkFDALoa.mjs +26 -0
- package/dist/definitions-Cke7zEb8.mjs +94 -0
- package/dist/definitions-ClJLzsJQ.mjs +25 -0
- package/dist/definitions-Cq-zroAU.mjs +28 -0
- package/dist/definitions-Cy3Sl6gV.mjs +34 -0
- package/dist/definitions-D3VsGcvz.mjs +47 -0
- package/dist/definitions-DVGfrn7y.mjs +96 -0
- package/dist/definitions-LKpC3-nL.mjs +9 -0
- package/dist/definitions-bAhHQJq9.mjs +359 -0
- package/dist/encoding-Bvz5jLRv.mjs +1065 -0
- package/dist/evidence-graph-bridge-C_fv9PuC.mjs +135 -0
- package/dist/{factory-CibqTNC8.mjs → factory-DxlGh9Xf.mjs} +37 -52
- package/dist/graphql-DYWzJ29s.mjs +1026 -0
- package/dist/handlers-9sAbfIg-.mjs +2552 -0
- package/dist/handlers-Bl8zkwz1.mjs +2716 -0
- package/dist/handlers-C67ktuRN.mjs +710 -0
- package/dist/handlers-C87g8oCe.mjs +276 -0
- package/dist/handlers-CTsDAO6p.mjs +681 -0
- package/dist/handlers-Cgyg6c0U.mjs +645 -0
- package/dist/handlers-D6j6yka7.mjs +2124 -0
- package/dist/handlers-DdFzXLvF.mjs +446 -0
- package/dist/handlers-DeLOCd5m.mjs +799 -0
- package/dist/handlers-DlCJN4Td.mjs +757 -0
- package/dist/handlers-DxGIq15_2.mjs +917 -0
- package/dist/handlers-U6L4xhuF.mjs +585 -0
- package/dist/handlers-tB9Mp9ZK.mjs +84 -0
- package/dist/handlers-tiy7EIBp.mjs +572 -0
- package/dist/handlers.impl-DS0d9fUw.mjs +761 -0
- package/dist/hooks-CzCWByww.mjs +898 -0
- package/dist/index.mjs +377 -155
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-P7ePRXQC.mjs +830 -0
- package/dist/manifest-2ToTpjv8.mjs +106 -0
- package/dist/manifest-3g71z6Bg.mjs +79 -0
- package/dist/manifest-82baTv4U.mjs +45 -0
- package/dist/manifest-B3QVVeBS.mjs +82 -0
- package/dist/manifest-BB2J8IMJ.mjs +149 -0
- package/dist/manifest-BKbgbSiY.mjs +60 -0
- package/dist/manifest-Bcf-TJzH.mjs +848 -0
- package/dist/manifest-BmtZzQiQ2.mjs +45 -0
- package/dist/manifest-Bnd7kqEY.mjs +55 -0
- package/dist/manifest-BqQX6OQC2.mjs +65 -0
- package/dist/manifest-BqrQ4Tpj.mjs +81 -0
- package/dist/manifest-Br4RPFt5.mjs +370 -0
- package/dist/manifest-C5qDjysN.mjs +107 -0
- package/dist/manifest-C9RT5nk32.mjs +34 -0
- package/dist/manifest-CAhOuvSl.mjs +204 -0
- package/dist/manifest-CBYWCUBJ.mjs +51 -0
- package/dist/manifest-CFADCRa1.mjs +37 -0
- package/dist/manifest-CQVhavRF.mjs +114 -0
- package/dist/manifest-CT7zZBV1.mjs +48 -0
- package/dist/manifest-CV12bcrF.mjs +121 -0
- package/dist/manifest-CXsRWjjI.mjs +224 -0
- package/dist/manifest-CZLUCfG02.mjs +95 -0
- package/dist/manifest-D6phHKFd.mjs +131 -0
- package/dist/manifest-DCyjf4n2.mjs +294 -0
- package/dist/manifest-DHsnKgP6.mjs +60 -0
- package/dist/manifest-Df_dliIe.mjs +55 -0
- package/dist/manifest-Dh8WBmEW.mjs +129 -0
- package/dist/manifest-DhKRAT8_.mjs +92 -0
- package/dist/manifest-DlpTj4ic2.mjs +193 -0
- package/dist/manifest-DrbmZcFl2.mjs +253 -0
- package/dist/manifest-DuwHjUa5.mjs +70 -0
- package/dist/manifest-DzwvxPJX.mjs +38 -0
- package/dist/manifest-NXctwWQq.mjs +68 -0
- package/dist/manifest-Sc_0JQ13.mjs +418 -0
- package/dist/manifest-gZ4s_UtG.mjs +96 -0
- package/dist/manifest-qSleDqdO.mjs +1023 -0
- package/dist/modules-C184v-S9.mjs +11365 -0
- package/dist/mojo-ipc-B_H61Afw.mjs +525 -0
- package/dist/network-671Cw6hV.mjs +3346 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-B1uGmrWZ.mjs} +219 -212
- package/dist/parse-args-BlRjqlkL.mjs +39 -0
- package/dist/platform-WmNn8Sxb.mjs +2070 -0
- package/dist/process-QcbIy5Zq.mjs +1401 -0
- package/dist/proxy-DqNs0bAd.mjs +170 -0
- package/dist/registry-D-6e18lB.mjs +34 -0
- package/dist/response-BQVP-xUn.mjs +28 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-DV-dpHFJ.mjs +586 -0
- package/dist/sourcemap-Dq8ez8vS.mjs +650 -0
- package/dist/ssrf-policy-ZaUfvhq7.mjs +166 -0
- package/dist/streaming-BUQ0VJsg.mjs +725 -0
- package/dist/tool-builder-DCbIC5Eo.mjs +186 -0
- package/dist/transform-CiYJfNX0.mjs +1007 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-DQTnHDs4.mjs +531 -0
- package/dist/workflow-f3xJOcjx.mjs +725 -0
- package/package.json +16 -16
- package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
- package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-CDe5WPSV.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-Bo4T3bz8.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-CwVLVdDM.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-DVkj9kpI.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-CPhOReNX.mjs} +1 -1
|
@@ -0,0 +1,2124 @@
|
|
|
1
|
+
import { i as argObject, o as argStringArray, s as argStringRequired } from "./parse-args-BlRjqlkL.mjs";
|
|
2
|
+
import { readFile as readFile$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
//#region src/modules/protocol-analysis/ProtocolPatternUtils.ts
|
|
5
|
+
const PRINTABLE_MIN = 32;
|
|
6
|
+
const PRINTABLE_MAX = 126;
|
|
7
|
+
const DELIMITER_CANDIDATES = [
|
|
8
|
+
Buffer.from([44]),
|
|
9
|
+
Buffer.from([124]),
|
|
10
|
+
Buffer.from([58]),
|
|
11
|
+
Buffer.from([59]),
|
|
12
|
+
Buffer.from([9]),
|
|
13
|
+
Buffer.from([0]),
|
|
14
|
+
Buffer.from([13, 10])
|
|
15
|
+
];
|
|
16
|
+
function normalizeHexPayload(value) {
|
|
17
|
+
return value.replace(/^0x/i, "").replace(/\s+/g, "").toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
function isHexPayload(value) {
|
|
20
|
+
const normalized = normalizeHexPayload(value);
|
|
21
|
+
if (normalized.length === 0 || normalized.length % 2 !== 0) return false;
|
|
22
|
+
return /^[0-9a-f]+$/i.test(normalized);
|
|
23
|
+
}
|
|
24
|
+
function parseHexPayload$1(value) {
|
|
25
|
+
if (!isHexPayload(value)) return null;
|
|
26
|
+
return Buffer.from(normalizeHexPayload(value), "hex");
|
|
27
|
+
}
|
|
28
|
+
function isPrintableByte(value) {
|
|
29
|
+
return value >= PRINTABLE_MIN && value <= PRINTABLE_MAX;
|
|
30
|
+
}
|
|
31
|
+
function printableRatio(buffer) {
|
|
32
|
+
if (buffer.length === 0) return 0;
|
|
33
|
+
let printableCount = 0;
|
|
34
|
+
for (const value of buffer.values()) if (isPrintableByte(value)) printableCount += 1;
|
|
35
|
+
return printableCount / buffer.length;
|
|
36
|
+
}
|
|
37
|
+
function averagePrintableRatio(buffers) {
|
|
38
|
+
if (buffers.length === 0) return 0;
|
|
39
|
+
return buffers.reduce((accumulator, buffer) => accumulator + printableRatio(buffer), 0) / buffers.length;
|
|
40
|
+
}
|
|
41
|
+
function splitBuffer(buffer, delimiter) {
|
|
42
|
+
if (delimiter.length === 0) return [buffer];
|
|
43
|
+
const parts = [];
|
|
44
|
+
let start = 0;
|
|
45
|
+
let index = buffer.indexOf(delimiter, start);
|
|
46
|
+
while (index >= 0) {
|
|
47
|
+
parts.push(buffer.subarray(start, index));
|
|
48
|
+
start = index + delimiter.length;
|
|
49
|
+
index = buffer.indexOf(delimiter, start);
|
|
50
|
+
}
|
|
51
|
+
parts.push(buffer.subarray(start));
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
function bufferToDelimiterString(buffer) {
|
|
55
|
+
return printableRatio(buffer) === 1 ? buffer.toString("utf8") : buffer.toString("hex");
|
|
56
|
+
}
|
|
57
|
+
function parsePayloads(hexPayloads) {
|
|
58
|
+
const buffers = [];
|
|
59
|
+
for (const hexPayload of hexPayloads) {
|
|
60
|
+
const payload = parseHexPayload$1(hexPayload);
|
|
61
|
+
if (payload) buffers.push(payload);
|
|
62
|
+
}
|
|
63
|
+
return buffers;
|
|
64
|
+
}
|
|
65
|
+
function decodeInteger(buffer, byteOrder) {
|
|
66
|
+
if (buffer.length === 0) return null;
|
|
67
|
+
if (buffer.length === 1) return buffer.readUInt8(0);
|
|
68
|
+
if (buffer.length === 2) return byteOrder === "le" ? buffer.readUInt16LE(0) : buffer.readUInt16BE(0);
|
|
69
|
+
if (buffer.length === 4) return byteOrder === "le" ? buffer.readUInt32LE(0) : buffer.readUInt32BE(0);
|
|
70
|
+
if (buffer.length === 8) {
|
|
71
|
+
const value = byteOrder === "le" ? Number(buffer.readBigUInt64LE(0)) : Number(buffer.readBigUInt64BE(0));
|
|
72
|
+
return Number.isFinite(value) ? value : null;
|
|
73
|
+
}
|
|
74
|
+
let value = 0;
|
|
75
|
+
const bytes = byteOrder === "le" ? [...buffer.values()].toReversed() : [...buffer.values()];
|
|
76
|
+
for (const byte of bytes) value = value * 256 + byte;
|
|
77
|
+
return Number.isFinite(value) ? value : null;
|
|
78
|
+
}
|
|
79
|
+
function decodeFloat(buffer, byteOrder) {
|
|
80
|
+
if (buffer.length === 4) {
|
|
81
|
+
const value = byteOrder === "le" ? buffer.readFloatLE(0) : buffer.readFloatBE(0);
|
|
82
|
+
return Number.isFinite(value) ? value : null;
|
|
83
|
+
}
|
|
84
|
+
if (buffer.length === 8) {
|
|
85
|
+
const value = byteOrder === "le" ? buffer.readDoubleLE(0) : buffer.readDoubleBE(0);
|
|
86
|
+
return Number.isFinite(value) ? value : null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function countOccurrences(buffer, delimiter) {
|
|
91
|
+
if (delimiter.length === 0) return 0;
|
|
92
|
+
let count = 0;
|
|
93
|
+
let start = 0;
|
|
94
|
+
let index = buffer.indexOf(delimiter, start);
|
|
95
|
+
while (index >= 0) {
|
|
96
|
+
count += 1;
|
|
97
|
+
start = index + delimiter.length;
|
|
98
|
+
index = buffer.indexOf(delimiter, start);
|
|
99
|
+
}
|
|
100
|
+
return count;
|
|
101
|
+
}
|
|
102
|
+
function inferFieldType(samples) {
|
|
103
|
+
if (samples.length === 0) return "bytes";
|
|
104
|
+
if (samples.every((sample) => sample.length === 1) && samples.every((sample) => sample[0] === 0 || sample[0] === 1)) return "bool";
|
|
105
|
+
if (averagePrintableRatio(samples) >= .7) return "string";
|
|
106
|
+
if (samples.every((sample) => sample.length === 4) && looksLikeFloatSamples(samples)) return "float";
|
|
107
|
+
if (samples.every((sample) => sample.length <= 4)) return "int";
|
|
108
|
+
return "bytes";
|
|
109
|
+
}
|
|
110
|
+
function looksLikeFloatSamples(samples) {
|
|
111
|
+
const decoded = [];
|
|
112
|
+
for (const sample of samples) {
|
|
113
|
+
const value = decodeFloat(sample, "be");
|
|
114
|
+
if (value === null) return false;
|
|
115
|
+
decoded.push(value);
|
|
116
|
+
}
|
|
117
|
+
return decoded.some((value) => Math.abs(value) > .001 && Math.abs(value) < 1e6);
|
|
118
|
+
}
|
|
119
|
+
function isPrintableColumn(buffers, offset) {
|
|
120
|
+
let valueCount = 0;
|
|
121
|
+
let printableCount = 0;
|
|
122
|
+
for (const buffer of buffers) {
|
|
123
|
+
const value = buffer[offset];
|
|
124
|
+
if (value === void 0) continue;
|
|
125
|
+
valueCount += 1;
|
|
126
|
+
if (isPrintableByte(value)) printableCount += 1;
|
|
127
|
+
}
|
|
128
|
+
return valueCount > 0 && printableCount / valueCount >= .8;
|
|
129
|
+
}
|
|
130
|
+
function isBooleanColumn(buffers, offset) {
|
|
131
|
+
let valueCount = 0;
|
|
132
|
+
for (const buffer of buffers) {
|
|
133
|
+
const value = buffer[offset];
|
|
134
|
+
if (value === void 0) continue;
|
|
135
|
+
valueCount += 1;
|
|
136
|
+
if (value !== 0 && value !== 1) return false;
|
|
137
|
+
}
|
|
138
|
+
return valueCount > 0;
|
|
139
|
+
}
|
|
140
|
+
function buildDelimitedFields(buffers, delimiter) {
|
|
141
|
+
if (delimiter.length === 0) return [];
|
|
142
|
+
const tokenized = buffers.map((buffer) => splitBuffer(buffer, delimiter));
|
|
143
|
+
const firstRow = tokenized[0];
|
|
144
|
+
if (!firstRow || firstRow.length < 2) return [];
|
|
145
|
+
const tokenCount = firstRow.length;
|
|
146
|
+
if (!tokenized.every((parts) => parts.length === tokenCount)) return [];
|
|
147
|
+
const fields = [];
|
|
148
|
+
let currentOffset = 0;
|
|
149
|
+
for (let index = 0; index < tokenCount; index += 1) {
|
|
150
|
+
const template = firstRow[index];
|
|
151
|
+
if (!template) continue;
|
|
152
|
+
const samples = tokenized.map((parts) => parts[index]).filter((part) => Buffer.isBuffer(part));
|
|
153
|
+
fields.push({
|
|
154
|
+
name: `field_${index + 1}`,
|
|
155
|
+
offset: currentOffset,
|
|
156
|
+
length: template.length,
|
|
157
|
+
type: inferFieldType(samples)
|
|
158
|
+
});
|
|
159
|
+
currentOffset += template.length + delimiter.length;
|
|
160
|
+
}
|
|
161
|
+
return fields;
|
|
162
|
+
}
|
|
163
|
+
function buildFixedWidthFields(buffers) {
|
|
164
|
+
const minLength = Math.min(...buffers.map((buffer) => buffer.length));
|
|
165
|
+
const fields = [];
|
|
166
|
+
let offset = 0;
|
|
167
|
+
while (offset < minLength && fields.length < 24) {
|
|
168
|
+
if (isPrintableColumn(buffers, offset)) {
|
|
169
|
+
let end = offset + 1;
|
|
170
|
+
while (end < minLength && isPrintableColumn(buffers, end)) end += 1;
|
|
171
|
+
fields.push({
|
|
172
|
+
name: `field_${fields.length + 1}`,
|
|
173
|
+
offset,
|
|
174
|
+
length: end - offset,
|
|
175
|
+
type: "string"
|
|
176
|
+
});
|
|
177
|
+
offset = end;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (isBooleanColumn(buffers, offset)) {
|
|
181
|
+
fields.push({
|
|
182
|
+
name: `field_${fields.length + 1}`,
|
|
183
|
+
offset,
|
|
184
|
+
length: 1,
|
|
185
|
+
type: "bool"
|
|
186
|
+
});
|
|
187
|
+
offset += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const remaining = minLength - offset;
|
|
191
|
+
if (remaining >= 4) {
|
|
192
|
+
if (looksLikeFloatSamples(buffers.map((buffer) => buffer.subarray(offset, offset + 4)))) {
|
|
193
|
+
fields.push({
|
|
194
|
+
name: `field_${fields.length + 1}`,
|
|
195
|
+
offset,
|
|
196
|
+
length: 4,
|
|
197
|
+
type: "float"
|
|
198
|
+
});
|
|
199
|
+
offset += 4;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const segmentLength = remaining >= 4 ? 4 : Math.min(remaining, 2);
|
|
204
|
+
const samples = buffers.map((buffer) => buffer.subarray(offset, offset + segmentLength));
|
|
205
|
+
fields.push({
|
|
206
|
+
name: `field_${fields.length + 1}`,
|
|
207
|
+
offset,
|
|
208
|
+
length: segmentLength,
|
|
209
|
+
type: inferFieldType(samples)
|
|
210
|
+
});
|
|
211
|
+
offset += segmentLength;
|
|
212
|
+
}
|
|
213
|
+
return fields;
|
|
214
|
+
}
|
|
215
|
+
function inferDelimiter(buffers) {
|
|
216
|
+
for (const candidate of DELIMITER_CANDIDATES) {
|
|
217
|
+
const counts = buffers.map((buffer) => countOccurrences(buffer, candidate));
|
|
218
|
+
const firstCount = counts[0];
|
|
219
|
+
if (typeof firstCount === "number" && firstCount >= 2 && counts.every((count) => count === firstCount)) return bufferToDelimiterString(candidate);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function inferByteOrder(buffers) {
|
|
223
|
+
const minLength = Math.min(...buffers.map((buffer) => buffer.length));
|
|
224
|
+
if (minLength < 2) return "be";
|
|
225
|
+
let leScore = 0;
|
|
226
|
+
let beScore = 0;
|
|
227
|
+
const limit = Math.min(minLength - 1, 8);
|
|
228
|
+
for (let offset = 0; offset < limit; offset += 2) {
|
|
229
|
+
let leSmallValues = 0;
|
|
230
|
+
let beSmallValues = 0;
|
|
231
|
+
for (const buffer of buffers) {
|
|
232
|
+
const little = buffer.readUInt16LE(offset);
|
|
233
|
+
const big = buffer.readUInt16BE(offset);
|
|
234
|
+
if (little < 4096) leSmallValues += 1;
|
|
235
|
+
if (big < 4096) beSmallValues += 1;
|
|
236
|
+
}
|
|
237
|
+
if (leSmallValues > beSmallValues) leScore += 1;
|
|
238
|
+
else if (beSmallValues > leSmallValues) beScore += 1;
|
|
239
|
+
}
|
|
240
|
+
return leScore > beScore ? "le" : "be";
|
|
241
|
+
}
|
|
242
|
+
function labelMagicFields(fields, buffers) {
|
|
243
|
+
if (fields.length === 0 || buffers.length < 2) return fields;
|
|
244
|
+
const minLen = Math.min(...buffers.map((b) => b.length));
|
|
245
|
+
let commonPrefixLen = 0;
|
|
246
|
+
for (let offset = 0; offset < minLen; offset += 1) {
|
|
247
|
+
const byte = buffers[0][offset];
|
|
248
|
+
if (buffers.every((b) => b[offset] === byte)) commonPrefixLen = offset + 1;
|
|
249
|
+
else break;
|
|
250
|
+
}
|
|
251
|
+
if (commonPrefixLen === 0) return fields;
|
|
252
|
+
let magicLabelApplied = false;
|
|
253
|
+
return fields.map((field) => {
|
|
254
|
+
if (!magicLabelApplied && field.offset === 0 && commonPrefixLen >= 2) {
|
|
255
|
+
magicLabelApplied = true;
|
|
256
|
+
return {
|
|
257
|
+
...field,
|
|
258
|
+
name: "magic"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (magicLabelApplied && field.type === "int" && field.length <= 2 && field.offset <= commonPrefixLen) return {
|
|
262
|
+
...field,
|
|
263
|
+
name: "version"
|
|
264
|
+
};
|
|
265
|
+
return field;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/modules/protocol-analysis/ProtocolPatternEngine.ts
|
|
270
|
+
var ProtocolPatternEngine = class {
|
|
271
|
+
patterns = /* @__PURE__ */ new Map();
|
|
272
|
+
legacyPatterns = /* @__PURE__ */ new Map();
|
|
273
|
+
definePattern(name, specOrFields, options) {
|
|
274
|
+
const legacyPattern = Array.isArray(specOrFields) ? this.createLegacyPattern(name, specOrFields, options) : this.createLegacyPatternFromSpec(name, specOrFields);
|
|
275
|
+
const spec = this.createSpecFromLegacyPattern(legacyPattern);
|
|
276
|
+
this.patterns.set(name, spec);
|
|
277
|
+
this.legacyPatterns.set(name, legacyPattern);
|
|
278
|
+
if (Array.isArray(specOrFields)) return legacyPattern;
|
|
279
|
+
}
|
|
280
|
+
detectPattern(hexPayload) {
|
|
281
|
+
const payload = parseHexPayload$1(hexPayload);
|
|
282
|
+
if (!payload) return null;
|
|
283
|
+
let bestMatch = null;
|
|
284
|
+
for (const pattern of this.patterns.values()) {
|
|
285
|
+
const totalChecks = pattern.fields.length + (pattern.fieldDelimiter ? 1 : 0);
|
|
286
|
+
if (totalChecks === 0) continue;
|
|
287
|
+
let matches = 0;
|
|
288
|
+
if (pattern.fieldDelimiter && this.payloadContainsDelimiter(payload, pattern.fieldDelimiter)) matches += 1;
|
|
289
|
+
for (const field of pattern.fields) if (this.matchesField(payload, field, pattern.byteOrder ?? "be")) matches += 1;
|
|
290
|
+
const confidence = Number((matches / totalChecks).toFixed(2));
|
|
291
|
+
if (confidence <= 0) continue;
|
|
292
|
+
const candidate = {
|
|
293
|
+
pattern,
|
|
294
|
+
confidence,
|
|
295
|
+
matches,
|
|
296
|
+
total: totalChecks
|
|
297
|
+
};
|
|
298
|
+
if (!bestMatch || candidate.confidence > bestMatch.confidence || candidate.confidence === bestMatch.confidence && candidate.matches > bestMatch.matches) bestMatch = candidate;
|
|
299
|
+
}
|
|
300
|
+
return bestMatch;
|
|
301
|
+
}
|
|
302
|
+
autoDetect(hexPayloads) {
|
|
303
|
+
const buffers = parsePayloads(hexPayloads);
|
|
304
|
+
if (buffers.length === 0) return null;
|
|
305
|
+
const delimiter = inferDelimiter(buffers);
|
|
306
|
+
const fields = this.inferFields(hexPayloads);
|
|
307
|
+
return {
|
|
308
|
+
name: "auto-detected-pattern",
|
|
309
|
+
fieldDelimiter: delimiter,
|
|
310
|
+
byteOrder: inferByteOrder(buffers),
|
|
311
|
+
fields
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
inferFields(hexPayloads) {
|
|
315
|
+
const buffers = parsePayloads(hexPayloads);
|
|
316
|
+
if (buffers.length === 0) return [];
|
|
317
|
+
const delimiter = inferDelimiter(buffers);
|
|
318
|
+
if (delimiter) {
|
|
319
|
+
const fields = buildDelimitedFields(buffers, this.parseDelimiter(delimiter));
|
|
320
|
+
if (fields.length > 0) return labelMagicFields(fields, buffers);
|
|
321
|
+
}
|
|
322
|
+
return labelMagicFields(buildFixedWidthFields(buffers), buffers);
|
|
323
|
+
}
|
|
324
|
+
autoDetectPattern(payloads, options) {
|
|
325
|
+
const hexPayloads = payloads.map((payload) => payload.toString("hex"));
|
|
326
|
+
const detected = this.autoDetect(hexPayloads);
|
|
327
|
+
const name = options?.name ?? detected?.name ?? "auto_detected";
|
|
328
|
+
if (!detected) {
|
|
329
|
+
const emptyPattern = this.createLegacyPattern(name, []);
|
|
330
|
+
this.patterns.set(name, this.createSpecFromLegacyPattern(emptyPattern));
|
|
331
|
+
this.legacyPatterns.set(name, emptyPattern);
|
|
332
|
+
return emptyPattern;
|
|
333
|
+
}
|
|
334
|
+
const namedPattern = {
|
|
335
|
+
...detected,
|
|
336
|
+
name
|
|
337
|
+
};
|
|
338
|
+
this.definePattern(name, namedPattern);
|
|
339
|
+
return this.getPattern(name) ?? this.createLegacyPatternFromSpec(name, namedPattern);
|
|
340
|
+
}
|
|
341
|
+
getPattern(name) {
|
|
342
|
+
return this.legacyPatterns.get(name);
|
|
343
|
+
}
|
|
344
|
+
listPatterns() {
|
|
345
|
+
return [...this.patterns.keys()];
|
|
346
|
+
}
|
|
347
|
+
exportProto(pattern) {
|
|
348
|
+
const legacyPattern = this.isLegacyPattern(pattern) ? pattern : this.createLegacyPatternFromSpec(pattern.name, pattern);
|
|
349
|
+
const lines = [
|
|
350
|
+
`// Protocol: ${legacyPattern.name}`,
|
|
351
|
+
`// Byte order: ${legacyPattern.byteOrder}`,
|
|
352
|
+
""
|
|
353
|
+
];
|
|
354
|
+
if (legacyPattern.encryption) {
|
|
355
|
+
lines.push(`// Encryption: ${legacyPattern.encryption.type}`);
|
|
356
|
+
if (legacyPattern.encryption.notes) lines.push(`// Notes: ${legacyPattern.encryption.notes}`);
|
|
357
|
+
lines.push("");
|
|
358
|
+
}
|
|
359
|
+
lines.push(`message ${this.toPascalCase(legacyPattern.name)} {`);
|
|
360
|
+
for (let index = 0; index < legacyPattern.fields.length; index += 1) {
|
|
361
|
+
const field = legacyPattern.fields[index];
|
|
362
|
+
if (!field) continue;
|
|
363
|
+
const comment = field.description ? ` // ${field.description}` : "";
|
|
364
|
+
lines.push(` ${this.toProtoType(field.type)} ${field.name} = ${index + 1};${comment}`);
|
|
365
|
+
}
|
|
366
|
+
lines.push("}");
|
|
367
|
+
lines.push("");
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
370
|
+
payloadContainsDelimiter(payload, delimiter) {
|
|
371
|
+
const delimiterBuffer = this.parseDelimiter(delimiter);
|
|
372
|
+
if (delimiterBuffer.length === 0) return false;
|
|
373
|
+
return payload.includes(delimiterBuffer);
|
|
374
|
+
}
|
|
375
|
+
matchesField(payload, field, byteOrder) {
|
|
376
|
+
if (field.offset < 0 || field.length <= 0 || payload.length < field.offset + field.length) return false;
|
|
377
|
+
const slice = payload.subarray(field.offset, field.offset + field.length);
|
|
378
|
+
switch (field.type) {
|
|
379
|
+
case "bytes": return slice.length === field.length;
|
|
380
|
+
case "bool": return slice.length === 1 && (slice[0] === 0 || slice[0] === 1);
|
|
381
|
+
case "string": return printableRatio(slice) >= .6;
|
|
382
|
+
case "int": return decodeInteger(slice, byteOrder) !== null;
|
|
383
|
+
case "float": return decodeFloat(slice, byteOrder) !== null;
|
|
384
|
+
default: return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
createLegacyPattern(name, fields, options) {
|
|
388
|
+
return {
|
|
389
|
+
name,
|
|
390
|
+
fields: fields.map((field) => ({
|
|
391
|
+
name: field.name,
|
|
392
|
+
offset: field.offset,
|
|
393
|
+
length: field.length,
|
|
394
|
+
type: field.type,
|
|
395
|
+
...field.description ? { description: field.description } : {}
|
|
396
|
+
})).toSorted((left, right) => left.offset - right.offset),
|
|
397
|
+
byteOrder: options?.byteOrder ?? "big",
|
|
398
|
+
...options?.encryption ? { encryption: options.encryption } : {}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
createLegacyPatternFromSpec(name, spec) {
|
|
402
|
+
return {
|
|
403
|
+
name,
|
|
404
|
+
fieldDelimiter: spec.fieldDelimiter,
|
|
405
|
+
byteOrder: spec.byteOrder === "le" ? "little" : "big",
|
|
406
|
+
fields: spec.fields.map((field) => ({
|
|
407
|
+
name: field.name,
|
|
408
|
+
offset: field.offset,
|
|
409
|
+
length: field.length,
|
|
410
|
+
type: this.toLegacyFieldType(field),
|
|
411
|
+
...field.description ? { description: field.description } : {}
|
|
412
|
+
}))
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
createSpecFromLegacyPattern(pattern) {
|
|
416
|
+
return {
|
|
417
|
+
name: pattern.name,
|
|
418
|
+
fieldDelimiter: pattern.fieldDelimiter,
|
|
419
|
+
byteOrder: pattern.byteOrder === "little" ? "le" : "be",
|
|
420
|
+
fields: pattern.fields.map((field) => ({
|
|
421
|
+
name: field.name,
|
|
422
|
+
offset: field.offset,
|
|
423
|
+
length: field.length,
|
|
424
|
+
type: this.toSpecFieldType(field.type),
|
|
425
|
+
...field.description ? { description: field.description } : {}
|
|
426
|
+
}))
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
isLegacyPattern(pattern) {
|
|
430
|
+
return pattern.byteOrder === "big" || pattern.byteOrder === "little";
|
|
431
|
+
}
|
|
432
|
+
toLegacyFieldType(field) {
|
|
433
|
+
if (field.type === "float") return "float";
|
|
434
|
+
if (field.type === "string") return "string";
|
|
435
|
+
if (field.type === "bytes") return "bytes";
|
|
436
|
+
if (field.length === 1) return "uint8";
|
|
437
|
+
if (field.length === 2) return "uint16";
|
|
438
|
+
if (field.length === 4) return "uint32";
|
|
439
|
+
return "int64";
|
|
440
|
+
}
|
|
441
|
+
toSpecFieldType(fieldType) {
|
|
442
|
+
if (fieldType === "float") return "float";
|
|
443
|
+
if (fieldType === "string") return "string";
|
|
444
|
+
if (fieldType === "bytes") return "bytes";
|
|
445
|
+
return "int";
|
|
446
|
+
}
|
|
447
|
+
parseDelimiter(delimiter) {
|
|
448
|
+
if (isHexPayload(delimiter)) {
|
|
449
|
+
const parsed = parseHexPayload$1(delimiter);
|
|
450
|
+
if (parsed) return parsed;
|
|
451
|
+
}
|
|
452
|
+
return Buffer.from(delimiter, "utf8");
|
|
453
|
+
}
|
|
454
|
+
toProtoType(type) {
|
|
455
|
+
return {
|
|
456
|
+
uint8: "uint32",
|
|
457
|
+
uint16: "uint32",
|
|
458
|
+
uint32: "uint32",
|
|
459
|
+
int64: "int64",
|
|
460
|
+
float: "float",
|
|
461
|
+
string: "string",
|
|
462
|
+
bytes: "bytes"
|
|
463
|
+
}[type];
|
|
464
|
+
}
|
|
465
|
+
toPascalCase(name) {
|
|
466
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_").split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("") || "Message";
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/modules/protocol-analysis/StateMachineInferrer.ts
|
|
471
|
+
function isRecord$1(value) {
|
|
472
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
473
|
+
}
|
|
474
|
+
function normalizeText(value) {
|
|
475
|
+
return value.trim().toLowerCase();
|
|
476
|
+
}
|
|
477
|
+
function calculateEntropy(buffer) {
|
|
478
|
+
if (buffer.length === 0) return 0;
|
|
479
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
480
|
+
for (const byte of buffer) frequency.set(byte, (frequency.get(byte) ?? 0) + 1);
|
|
481
|
+
let entropy = 0;
|
|
482
|
+
for (const count of frequency.values()) {
|
|
483
|
+
const p = count / buffer.length;
|
|
484
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
485
|
+
}
|
|
486
|
+
return entropy;
|
|
487
|
+
}
|
|
488
|
+
function printableRatioOf(value) {
|
|
489
|
+
if (value.length === 0) return 0;
|
|
490
|
+
let count = 0;
|
|
491
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
492
|
+
const code = value.charCodeAt(i);
|
|
493
|
+
if (code >= 32 && code <= 126) count += 1;
|
|
494
|
+
}
|
|
495
|
+
return count / value.length;
|
|
496
|
+
}
|
|
497
|
+
var StateMachineInferrer = class {
|
|
498
|
+
infer(messages) {
|
|
499
|
+
if (messages.length === 0) return {
|
|
500
|
+
states: [],
|
|
501
|
+
transitions: [],
|
|
502
|
+
initial: "",
|
|
503
|
+
initialState: "",
|
|
504
|
+
finalStates: []
|
|
505
|
+
};
|
|
506
|
+
const statesBySignature = /* @__PURE__ */ new Map();
|
|
507
|
+
const transitionsByKey = /* @__PURE__ */ new Map();
|
|
508
|
+
let previousStateId = "";
|
|
509
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
510
|
+
const message = messages[index];
|
|
511
|
+
if (!message) continue;
|
|
512
|
+
const signature = this.buildSignature(message);
|
|
513
|
+
let state = statesBySignature.get(signature);
|
|
514
|
+
if (!state) {
|
|
515
|
+
state = {
|
|
516
|
+
id: `state_${statesBySignature.size + 1}`,
|
|
517
|
+
name: this.buildStateName(message, statesBySignature.size + 1),
|
|
518
|
+
type: this.inferStateType(message, index === messages.length - 1)
|
|
519
|
+
};
|
|
520
|
+
statesBySignature.set(signature, state);
|
|
521
|
+
} else state.type = this.mergeStateTypes(state.type, this.inferStateType(message, index === messages.length - 1));
|
|
522
|
+
if (previousStateId && message.timestamp !== void 0) {
|
|
523
|
+
const firstMessage = messages[0];
|
|
524
|
+
if (firstMessage && firstMessage.timestamp !== void 0) state.timeout = message.timestamp - firstMessage.timestamp;
|
|
525
|
+
}
|
|
526
|
+
if (previousStateId) {
|
|
527
|
+
const event = this.buildEventName(message);
|
|
528
|
+
const condition = this.buildCondition(message.fields);
|
|
529
|
+
const action = this.buildAction(message);
|
|
530
|
+
const transitionKey = `${previousStateId}:${state.id}:${event}`;
|
|
531
|
+
if (!transitionsByKey.has(transitionKey)) transitionsByKey.set(transitionKey, {
|
|
532
|
+
from: previousStateId,
|
|
533
|
+
to: state.id,
|
|
534
|
+
event,
|
|
535
|
+
confidence: this.computeTransitionConfidence(message),
|
|
536
|
+
...condition ? { condition } : {},
|
|
537
|
+
...action ? { action } : {}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
previousStateId = state.id;
|
|
541
|
+
}
|
|
542
|
+
const firstMessage = messages[0];
|
|
543
|
+
const initial = firstMessage ? statesBySignature.get(this.buildSignature(firstMessage))?.id ?? "" : "";
|
|
544
|
+
return {
|
|
545
|
+
states: [...statesBySignature.values()],
|
|
546
|
+
transitions: [...transitionsByKey.values()],
|
|
547
|
+
initial,
|
|
548
|
+
initialState: initial,
|
|
549
|
+
finalStates: this.collectTerminalStates([...statesBySignature.values()])
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
visualize(machine) {
|
|
553
|
+
if (machine.states.length === 0) return [
|
|
554
|
+
"```mermaid",
|
|
555
|
+
"stateDiagram-v2",
|
|
556
|
+
" [*] --> empty",
|
|
557
|
+
"```"
|
|
558
|
+
].join("\n");
|
|
559
|
+
const lines = ["```mermaid", "stateDiagram-v2"];
|
|
560
|
+
const initial = machine.initialState ?? machine.initial;
|
|
561
|
+
if (initial) lines.push(` [*] --> ${initial}`);
|
|
562
|
+
for (const state of machine.states) {
|
|
563
|
+
const stateType = state.type ?? "normal";
|
|
564
|
+
const label = stateType === "normal" ? state.name : `${state.name} (${stateType})`;
|
|
565
|
+
lines.push(` state "${label}" as ${state.id}`);
|
|
566
|
+
}
|
|
567
|
+
for (const transition of machine.transitions) {
|
|
568
|
+
const parts = [transition.event ?? transition.trigger ?? "transition"];
|
|
569
|
+
if (typeof transition.confidence === "number") parts.push(`(${transition.confidence.toFixed(2)})`);
|
|
570
|
+
if (transition.condition) parts.push(`[${transition.condition}]`);
|
|
571
|
+
if (transition.action) parts.push(`/ ${transition.action}`);
|
|
572
|
+
lines.push(` ${transition.from} --> ${transition.to} : ${parts.join(" ")}`);
|
|
573
|
+
}
|
|
574
|
+
const finalStateSet = new Set(machine.finalStates ?? []);
|
|
575
|
+
for (const state of machine.states) {
|
|
576
|
+
const stateType = state.type ?? "normal";
|
|
577
|
+
if (stateType === "accept" || stateType === "reject" || finalStateSet.has(state.id)) lines.push(` ${state.id} --> [*]`);
|
|
578
|
+
}
|
|
579
|
+
lines.push("```");
|
|
580
|
+
return lines.join("\n");
|
|
581
|
+
}
|
|
582
|
+
inferStateMachine(messages) {
|
|
583
|
+
const normalizedMessages = messages.map((message) => ({
|
|
584
|
+
direction: message.direction === "out" ? "req" : "res",
|
|
585
|
+
timestamp: message.timestamp ?? 0,
|
|
586
|
+
fields: {},
|
|
587
|
+
raw: message.payload.length > 0 ? message.payload.toString("utf8") : message.payload.toString("hex"),
|
|
588
|
+
_rawBuffer: message.payload
|
|
589
|
+
}));
|
|
590
|
+
return this.infer(normalizedMessages);
|
|
591
|
+
}
|
|
592
|
+
generateMermaid(machine) {
|
|
593
|
+
return this.visualize(machine);
|
|
594
|
+
}
|
|
595
|
+
simplify(machine) {
|
|
596
|
+
if (machine.states.length < 2) return {
|
|
597
|
+
...machine,
|
|
598
|
+
initialState: machine.initialState ?? machine.initial,
|
|
599
|
+
finalStates: machine.finalStates ?? this.collectTerminalStates(machine.states)
|
|
600
|
+
};
|
|
601
|
+
const stateToGroup = /* @__PURE__ */ new Map();
|
|
602
|
+
const groupRepresentative = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const state of machine.states) {
|
|
604
|
+
const prefix = this.getPayloadPrefix(state);
|
|
605
|
+
if (!prefix) continue;
|
|
606
|
+
const existingGroup = [...groupRepresentative.entries()].find(([key]) => key === prefix);
|
|
607
|
+
if (existingGroup) stateToGroup.set(state.id, existingGroup[0]);
|
|
608
|
+
else {
|
|
609
|
+
groupRepresentative.set(prefix, state.id);
|
|
610
|
+
stateToGroup.set(state.id, prefix);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
614
|
+
const groupIdToPrimary = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const [prefix, primaryId] of groupRepresentative) groupIdToPrimary.set(prefix, primaryId);
|
|
616
|
+
for (const state of machine.states) {
|
|
617
|
+
const prefix = this.getPayloadPrefix(state);
|
|
618
|
+
if (!prefix) continue;
|
|
619
|
+
const primary = groupIdToPrimary.get(prefix);
|
|
620
|
+
if (primary && primary !== state.id) mergeMap.set(state.id, primary);
|
|
621
|
+
}
|
|
622
|
+
if (mergeMap.size === 0) return {
|
|
623
|
+
...machine,
|
|
624
|
+
initialState: machine.initialState ?? machine.initial,
|
|
625
|
+
finalStates: machine.finalStates ?? this.collectTerminalStates(machine.states)
|
|
626
|
+
};
|
|
627
|
+
const newStates = machine.states.filter((state) => !mergeMap.has(state.id));
|
|
628
|
+
const newTransitions = machine.transitions.map((t) => ({
|
|
629
|
+
...t,
|
|
630
|
+
from: mergeMap.get(t.from) ?? t.from,
|
|
631
|
+
to: mergeMap.get(t.to) ?? t.to
|
|
632
|
+
})).filter((t) => t.from !== t.to);
|
|
633
|
+
const rawInitialState = machine.initialState ?? machine.initial ?? "";
|
|
634
|
+
const initialState = mergeMap.get(rawInitialState) ?? rawInitialState;
|
|
635
|
+
return {
|
|
636
|
+
states: newStates,
|
|
637
|
+
transitions: newTransitions,
|
|
638
|
+
initial: initialState,
|
|
639
|
+
initialState,
|
|
640
|
+
finalStates: machine.finalStates.map((fs) => mergeMap.get(fs) ?? fs).filter((fs, index, arr) => arr.indexOf(fs) === index)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
getPayloadPrefix(state) {
|
|
644
|
+
const payload = state.expectedPayload;
|
|
645
|
+
if (!payload || payload.length < 8) return null;
|
|
646
|
+
return payload.slice(0, 8).toLowerCase();
|
|
647
|
+
}
|
|
648
|
+
buildSignature(message) {
|
|
649
|
+
const fieldKeys = Object.keys(message.fields).toSorted().join(",");
|
|
650
|
+
const rawPrefix = normalizeText(message._rawBuffer ? message._rawBuffer.toString("hex") : message.raw).slice(0, 24);
|
|
651
|
+
return `${message.direction}|${fieldKeys}|${rawPrefix}`;
|
|
652
|
+
}
|
|
653
|
+
buildStateName(message, position) {
|
|
654
|
+
const directionName = message.direction === "req" ? "send" : "recv";
|
|
655
|
+
const primaryField = this.findPrimaryFieldName(message.fields);
|
|
656
|
+
const raw = message.raw;
|
|
657
|
+
if (raw.length === 0) return `${directionName}_empty`;
|
|
658
|
+
const buf = message._rawBuffer;
|
|
659
|
+
const hexContent = Buffer.isBuffer(buf) ? buf.toString("hex") : raw;
|
|
660
|
+
if (hexContent.startsWith("16") || hexContent.startsWith("15") || hexContent.startsWith("17")) return `${directionName}_tls_handshake`;
|
|
661
|
+
const trimmed = raw.trimStart();
|
|
662
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return `${directionName}_json_${primaryField || `step_${position}`}`;
|
|
663
|
+
if (printableRatioOf(raw) >= .7) {
|
|
664
|
+
const lower = normalizeText(raw);
|
|
665
|
+
if (lower.includes("close") || lower.includes("fin") || lower.includes("bye")) return `${directionName}_close`;
|
|
666
|
+
if (lower.startsWith("get ") || lower.startsWith("post ") || lower.startsWith("http")) return `${directionName}_text_http`;
|
|
667
|
+
return `${directionName}_text_${primaryField || `step_${position}`}`;
|
|
668
|
+
}
|
|
669
|
+
if (Buffer.isBuffer(buf) && buf.length >= 32) {
|
|
670
|
+
if (calculateEntropy(buf) > 6) return `${directionName}_encrypted`;
|
|
671
|
+
}
|
|
672
|
+
if (Buffer.isBuffer(buf) && buf.length <= 4) return `${directionName}_control`;
|
|
673
|
+
return `${directionName}_${primaryField || `step_${position}`}`;
|
|
674
|
+
}
|
|
675
|
+
findPrimaryFieldName(fields) {
|
|
676
|
+
const firstKey = Object.keys(fields).toSorted()[0];
|
|
677
|
+
return firstKey ? firstKey : "";
|
|
678
|
+
}
|
|
679
|
+
inferStateType(message, isLastMessage) {
|
|
680
|
+
const text = normalizeText(message.raw);
|
|
681
|
+
const statusValue = this.findStatusValue(message.fields);
|
|
682
|
+
if (this.containsRejectSignal(text) || this.containsRejectSignal(statusValue)) return "reject";
|
|
683
|
+
if (this.containsAcceptSignal(text) || this.containsAcceptSignal(statusValue) || isLastMessage && message.direction === "res") return "accept";
|
|
684
|
+
return "normal";
|
|
685
|
+
}
|
|
686
|
+
mergeStateTypes(current, next) {
|
|
687
|
+
if (current === "reject" || next === "reject") return "reject";
|
|
688
|
+
if (current === "accept" || next === "accept") return "accept";
|
|
689
|
+
return "normal";
|
|
690
|
+
}
|
|
691
|
+
findStatusValue(fields) {
|
|
692
|
+
for (const key of [
|
|
693
|
+
"status",
|
|
694
|
+
"result",
|
|
695
|
+
"code",
|
|
696
|
+
"reason",
|
|
697
|
+
"message"
|
|
698
|
+
]) {
|
|
699
|
+
const value = fields[key];
|
|
700
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
701
|
+
}
|
|
702
|
+
return "";
|
|
703
|
+
}
|
|
704
|
+
containsRejectSignal(value) {
|
|
705
|
+
return [
|
|
706
|
+
"error",
|
|
707
|
+
"fail",
|
|
708
|
+
"denied",
|
|
709
|
+
"reject",
|
|
710
|
+
"timeout",
|
|
711
|
+
"invalid"
|
|
712
|
+
].some((token) => normalizeText(value).includes(token));
|
|
713
|
+
}
|
|
714
|
+
containsAcceptSignal(value) {
|
|
715
|
+
return [
|
|
716
|
+
"ok",
|
|
717
|
+
"success",
|
|
718
|
+
"accept",
|
|
719
|
+
"ready",
|
|
720
|
+
"done",
|
|
721
|
+
"complete"
|
|
722
|
+
].some((token) => normalizeText(value).includes(token));
|
|
723
|
+
}
|
|
724
|
+
buildEventName(message) {
|
|
725
|
+
const statusValue = this.findStatusValue(message.fields);
|
|
726
|
+
if (statusValue) return `${message.direction}_${normalizeText(statusValue).replace(/[^a-z0-9]+/g, "_")}`;
|
|
727
|
+
const primaryField = this.findPrimaryFieldName(message.fields);
|
|
728
|
+
if (primaryField) return `${message.direction}_${primaryField}`;
|
|
729
|
+
return `${message.direction}_message`;
|
|
730
|
+
}
|
|
731
|
+
buildCondition(fields) {
|
|
732
|
+
const statusValue = this.findStatusValue(fields);
|
|
733
|
+
if (statusValue) return `status=${statusValue}`;
|
|
734
|
+
const keys = Object.keys(fields).toSorted().slice(0, 2);
|
|
735
|
+
if (keys.length === 0) return;
|
|
736
|
+
const fragments = [];
|
|
737
|
+
for (const key of keys) {
|
|
738
|
+
const value = fields[key];
|
|
739
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") fragments.push(`${key}=${value}`);
|
|
740
|
+
}
|
|
741
|
+
return fragments.length > 0 ? fragments.join(", ") : void 0;
|
|
742
|
+
}
|
|
743
|
+
buildAction(message) {
|
|
744
|
+
const statusValue = this.findStatusValue(message.fields);
|
|
745
|
+
if (this.containsRejectSignal(statusValue) || this.containsRejectSignal(message.raw)) return "reject";
|
|
746
|
+
if (this.containsAcceptSignal(statusValue) || this.containsAcceptSignal(message.raw)) return "complete";
|
|
747
|
+
const rawText = normalizeText(message.raw);
|
|
748
|
+
if (rawText.includes("ack")) return "acknowledge";
|
|
749
|
+
if (rawText.includes("retry")) return "retry";
|
|
750
|
+
if (Object.keys(message.fields).length > 0 && isRecord$1(message.fields)) return message.direction === "req" ? "send" : "receive";
|
|
751
|
+
}
|
|
752
|
+
collectTerminalStates(states) {
|
|
753
|
+
const terminalIds = states.filter((state) => {
|
|
754
|
+
const stateType = state.type ?? "normal";
|
|
755
|
+
return stateType === "accept" || stateType === "reject";
|
|
756
|
+
}).map((state) => state.id);
|
|
757
|
+
for (const state of states) {
|
|
758
|
+
const lower = normalizeText(state.name);
|
|
759
|
+
if (lower.includes("close") || lower.includes("fin") || lower.includes("bye")) {
|
|
760
|
+
if (!terminalIds.includes(state.id)) terminalIds.push(state.id);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return terminalIds;
|
|
764
|
+
}
|
|
765
|
+
computeTransitionConfidence(message) {
|
|
766
|
+
let confidence = .3;
|
|
767
|
+
if (Object.keys(message.fields).length > 0) confidence += .3;
|
|
768
|
+
if (this.findStatusValue(message.fields)) confidence += .2;
|
|
769
|
+
if (message.raw.length > 0) confidence += .2;
|
|
770
|
+
return Math.min(confidence, 1);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/server/domains/protocol-analysis/handlers/shared.ts
|
|
775
|
+
function isRecord(value) {
|
|
776
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
777
|
+
}
|
|
778
|
+
function parseFieldSpec(value, index) {
|
|
779
|
+
if (!isRecord(value)) throw new Error(`fields[${index}] must be an object`);
|
|
780
|
+
const name = value.name;
|
|
781
|
+
const offset = value.offset;
|
|
782
|
+
const length = value.length;
|
|
783
|
+
const type = value.type;
|
|
784
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
785
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) throw new Error(`fields[${index}].offset must be a non-negative integer`);
|
|
786
|
+
if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) throw new Error(`fields[${index}].length must be a positive integer`);
|
|
787
|
+
if (type !== "int" && type !== "string" && type !== "bytes" && type !== "bool" && type !== "float") throw new Error(`fields[${index}].type is invalid`);
|
|
788
|
+
return {
|
|
789
|
+
name,
|
|
790
|
+
offset,
|
|
791
|
+
length,
|
|
792
|
+
type
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function parseLegacyField(value, index) {
|
|
796
|
+
if (!isRecord(value)) throw new Error(`fields[${index}] must be an object`);
|
|
797
|
+
const name = value.name;
|
|
798
|
+
const offset = value.offset;
|
|
799
|
+
const length = value.length;
|
|
800
|
+
const type = value.type;
|
|
801
|
+
const description = value.description;
|
|
802
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
803
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) throw new Error(`fields[${index}].offset must be a non-negative integer`);
|
|
804
|
+
if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) throw new Error(`fields[${index}].length must be a positive integer`);
|
|
805
|
+
if (type !== "uint8" && type !== "uint16" && type !== "uint32" && type !== "int64" && type !== "float" && type !== "string" && type !== "bytes") throw new Error(`fields[${index}].type is invalid`);
|
|
806
|
+
return {
|
|
807
|
+
name,
|
|
808
|
+
offset,
|
|
809
|
+
length,
|
|
810
|
+
type,
|
|
811
|
+
...typeof description === "string" ? { description } : {}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function parsePatternSpec(name, value) {
|
|
815
|
+
const rawFields = value.fields;
|
|
816
|
+
if (!Array.isArray(rawFields)) throw new Error("spec.fields must be an array");
|
|
817
|
+
const fieldDelimiter = typeof value.fieldDelimiter === "string" && value.fieldDelimiter.length > 0 ? value.fieldDelimiter : void 0;
|
|
818
|
+
const byteOrderValue = value.byteOrder;
|
|
819
|
+
const byteOrder = byteOrderValue === "le" || byteOrderValue === "be" ? byteOrderValue : void 0;
|
|
820
|
+
return {
|
|
821
|
+
name,
|
|
822
|
+
...fieldDelimiter ? { fieldDelimiter } : {},
|
|
823
|
+
...byteOrder ? { byteOrder } : {},
|
|
824
|
+
fields: rawFields.map((field, index) => parseFieldSpec(field, index))
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function parseEncryptionInfo(value) {
|
|
828
|
+
if (!isRecord(value)) return;
|
|
829
|
+
const type = value.type;
|
|
830
|
+
if (type !== "aes" && type !== "xor" && type !== "rc4" && type !== "custom") return;
|
|
831
|
+
const key = typeof value.key === "string" ? value.key : void 0;
|
|
832
|
+
const iv = typeof value.iv === "string" ? value.iv : void 0;
|
|
833
|
+
const notes = typeof value.notes === "string" ? value.notes : void 0;
|
|
834
|
+
return {
|
|
835
|
+
type,
|
|
836
|
+
...key ? { key } : {},
|
|
837
|
+
...iv ? { iv } : {},
|
|
838
|
+
...notes ? { notes } : {}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function parseProtocolMessage(value, index) {
|
|
842
|
+
if (!isRecord(value)) throw new Error(`messages[${index}] must be an object`);
|
|
843
|
+
const direction = value.direction;
|
|
844
|
+
const timestamp = value.timestamp;
|
|
845
|
+
const fields = value.fields;
|
|
846
|
+
const raw = value.raw;
|
|
847
|
+
if (direction !== "req" && direction !== "res") throw new Error(`messages[${index}].direction must be "req" or "res"`);
|
|
848
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) throw new Error(`messages[${index}].timestamp must be a number`);
|
|
849
|
+
if (!isRecord(fields)) throw new Error(`messages[${index}].fields must be an object`);
|
|
850
|
+
if (typeof raw !== "string") throw new Error(`messages[${index}].raw must be a string`);
|
|
851
|
+
return {
|
|
852
|
+
direction,
|
|
853
|
+
timestamp,
|
|
854
|
+
fields,
|
|
855
|
+
raw
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
const TEXT_ENCODINGS = ["utf8", "ascii"];
|
|
859
|
+
const BINARY_ENCODINGS = [
|
|
860
|
+
"utf8",
|
|
861
|
+
"ascii",
|
|
862
|
+
"hex",
|
|
863
|
+
"base64"
|
|
864
|
+
];
|
|
865
|
+
const PAYLOAD_FIELD_TYPES = [
|
|
866
|
+
"u8",
|
|
867
|
+
"u16",
|
|
868
|
+
"u32",
|
|
869
|
+
"i8",
|
|
870
|
+
"i16",
|
|
871
|
+
"i32",
|
|
872
|
+
"string",
|
|
873
|
+
"bytes"
|
|
874
|
+
];
|
|
875
|
+
const MUTATION_STRATEGIES = [
|
|
876
|
+
"set_byte",
|
|
877
|
+
"flip_bit",
|
|
878
|
+
"overwrite_bytes",
|
|
879
|
+
"append_bytes",
|
|
880
|
+
"truncate",
|
|
881
|
+
"increment_integer"
|
|
882
|
+
];
|
|
883
|
+
const ETHER_TYPE_MAP = Object.freeze({
|
|
884
|
+
arp: 2054,
|
|
885
|
+
ipv4: 2048,
|
|
886
|
+
ipv6: 34525,
|
|
887
|
+
vlan: 33024
|
|
888
|
+
});
|
|
889
|
+
const IP_PROTOCOL_MAP = Object.freeze({
|
|
890
|
+
icmp: 1,
|
|
891
|
+
igmp: 2,
|
|
892
|
+
tcp: 6,
|
|
893
|
+
udp: 17,
|
|
894
|
+
gre: 47,
|
|
895
|
+
esp: 50,
|
|
896
|
+
ah: 51,
|
|
897
|
+
icmpv6: 58,
|
|
898
|
+
ospf: 89
|
|
899
|
+
});
|
|
900
|
+
const PCAP_LINK_TYPE_MAP = Object.freeze({
|
|
901
|
+
loopback: 0,
|
|
902
|
+
ethernet: 1,
|
|
903
|
+
raw: 101
|
|
904
|
+
});
|
|
905
|
+
function parseEndian(value, fallback = "big") {
|
|
906
|
+
return value === "little" ? "little" : fallback;
|
|
907
|
+
}
|
|
908
|
+
function parseNonNegativeInteger(value, label) {
|
|
909
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`${label} must be a non-negative integer`);
|
|
910
|
+
return value;
|
|
911
|
+
}
|
|
912
|
+
function parsePositiveInteger(value, label) {
|
|
913
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
|
|
914
|
+
return value;
|
|
915
|
+
}
|
|
916
|
+
function parseInteger(value, label) {
|
|
917
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer`);
|
|
918
|
+
return value;
|
|
919
|
+
}
|
|
920
|
+
function parseByte(value, label) {
|
|
921
|
+
const parsed = parseInteger(value, label);
|
|
922
|
+
if (parsed < 0 || parsed > 255) throw new Error(`${label} must be between 0 and 255`);
|
|
923
|
+
return parsed;
|
|
924
|
+
}
|
|
925
|
+
function parseOptionalLength(value, label) {
|
|
926
|
+
return value === void 0 ? void 0 : parsePositiveInteger(value, label);
|
|
927
|
+
}
|
|
928
|
+
function parseEncoding(value, allowed, fallback, label) {
|
|
929
|
+
if (value === void 0) return fallback;
|
|
930
|
+
if (typeof value !== "string" || !allowed.includes(value)) throw new Error(`${label} is invalid`);
|
|
931
|
+
return value;
|
|
932
|
+
}
|
|
933
|
+
function normalizeHexString(value, label) {
|
|
934
|
+
const normalized = value.replace(/^0x/i, "").replace(/\s+/g, "");
|
|
935
|
+
if (normalized.length === 0) return normalized;
|
|
936
|
+
if (normalized.length % 2 !== 0 || /[^0-9a-f]/i.test(normalized)) throw new Error(`${label} must be a valid even-length hex string`);
|
|
937
|
+
return normalized.toLowerCase();
|
|
938
|
+
}
|
|
939
|
+
function decodeBinaryValue(value, encoding, label) {
|
|
940
|
+
switch (encoding) {
|
|
941
|
+
case "utf8":
|
|
942
|
+
case "ascii": return Buffer.from(value, encoding);
|
|
943
|
+
case "hex": return Buffer.from(normalizeHexString(value, label), "hex");
|
|
944
|
+
case "base64": return Buffer.from(value, "base64");
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function getNumericRange(width, signed) {
|
|
948
|
+
const bits = width * 8;
|
|
949
|
+
if (signed) return {
|
|
950
|
+
min: -(2 ** (bits - 1)),
|
|
951
|
+
max: 2 ** (bits - 1) - 1
|
|
952
|
+
};
|
|
953
|
+
return {
|
|
954
|
+
min: 0,
|
|
955
|
+
max: 2 ** bits - 1
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function getFieldNumericMetadata(type) {
|
|
959
|
+
switch (type) {
|
|
960
|
+
case "u8": return {
|
|
961
|
+
width: 1,
|
|
962
|
+
signed: false
|
|
963
|
+
};
|
|
964
|
+
case "u16": return {
|
|
965
|
+
width: 2,
|
|
966
|
+
signed: false
|
|
967
|
+
};
|
|
968
|
+
case "u32": return {
|
|
969
|
+
width: 4,
|
|
970
|
+
signed: false
|
|
971
|
+
};
|
|
972
|
+
case "i8": return {
|
|
973
|
+
width: 1,
|
|
974
|
+
signed: true
|
|
975
|
+
};
|
|
976
|
+
case "i16": return {
|
|
977
|
+
width: 2,
|
|
978
|
+
signed: true
|
|
979
|
+
};
|
|
980
|
+
case "i32": return {
|
|
981
|
+
width: 4,
|
|
982
|
+
signed: true
|
|
983
|
+
};
|
|
984
|
+
default: return null;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function writeIntegerToBuffer(buffer, value, width, signed, endian) {
|
|
988
|
+
if (signed) switch (width) {
|
|
989
|
+
case 1:
|
|
990
|
+
buffer.writeInt8(value, 0);
|
|
991
|
+
return;
|
|
992
|
+
case 2:
|
|
993
|
+
if (endian === "little") buffer.writeInt16LE(value, 0);
|
|
994
|
+
else buffer.writeInt16BE(value, 0);
|
|
995
|
+
return;
|
|
996
|
+
case 4:
|
|
997
|
+
if (endian === "little") buffer.writeInt32LE(value, 0);
|
|
998
|
+
else buffer.writeInt32BE(value, 0);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
switch (width) {
|
|
1002
|
+
case 1:
|
|
1003
|
+
buffer.writeUInt8(value, 0);
|
|
1004
|
+
return;
|
|
1005
|
+
case 2:
|
|
1006
|
+
if (endian === "little") buffer.writeUInt16LE(value, 0);
|
|
1007
|
+
else buffer.writeUInt16BE(value, 0);
|
|
1008
|
+
return;
|
|
1009
|
+
case 4:
|
|
1010
|
+
if (endian === "little") buffer.writeUInt32LE(value, 0);
|
|
1011
|
+
else buffer.writeUInt32BE(value, 0);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function readIntegerFromBuffer(buffer, offset, width, signed, endian) {
|
|
1016
|
+
if (signed) switch (width) {
|
|
1017
|
+
case 1: return buffer.readInt8(offset);
|
|
1018
|
+
case 2: return endian === "little" ? buffer.readInt16LE(offset) : buffer.readInt16BE(offset);
|
|
1019
|
+
case 4: return endian === "little" ? buffer.readInt32LE(offset) : buffer.readInt32BE(offset);
|
|
1020
|
+
}
|
|
1021
|
+
switch (width) {
|
|
1022
|
+
case 1: return buffer.readUInt8(offset);
|
|
1023
|
+
case 2: return endian === "little" ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
|
|
1024
|
+
case 4: return endian === "little" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function applyFixedLength(encoded, length, padByte) {
|
|
1028
|
+
if (length === void 0 || encoded.length === length) return encoded;
|
|
1029
|
+
if (encoded.length > length) return encoded.subarray(0, length);
|
|
1030
|
+
return Buffer.concat([encoded, Buffer.alloc(length - encoded.length, padByte)]);
|
|
1031
|
+
}
|
|
1032
|
+
function parsePayloadTemplateField(value, index) {
|
|
1033
|
+
if (!isRecord(value)) throw new Error(`fields[${index}] must be an object`);
|
|
1034
|
+
const name = value.name;
|
|
1035
|
+
const type = value.type;
|
|
1036
|
+
const rawValue = value.value;
|
|
1037
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
1038
|
+
if (typeof type !== "string" || !PAYLOAD_FIELD_TYPES.includes(type)) throw new Error(`fields[${index}].type is invalid`);
|
|
1039
|
+
const fieldType = type;
|
|
1040
|
+
const numericMetadata = getFieldNumericMetadata(fieldType);
|
|
1041
|
+
if (numericMetadata) {
|
|
1042
|
+
const numericValue = parseInteger(rawValue, `fields[${index}].value`);
|
|
1043
|
+
const range = getNumericRange(numericMetadata.width, numericMetadata.signed);
|
|
1044
|
+
if (numericValue < range.min || numericValue > range.max) throw new Error(`fields[${index}].value is out of range for ${type} (${range.min}..${range.max})`);
|
|
1045
|
+
if (value.length !== void 0 || value.padByte !== void 0 || value.encoding !== void 0) throw new Error(`fields[${index}] does not support length, padByte, or encoding`);
|
|
1046
|
+
return {
|
|
1047
|
+
name,
|
|
1048
|
+
type: fieldType,
|
|
1049
|
+
value: numericValue
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
if (typeof rawValue !== "string") throw new Error(`fields[${index}].value must be a string`);
|
|
1053
|
+
const length = parseOptionalLength(value.length, `fields[${index}].length`);
|
|
1054
|
+
const padByte = value.padByte === void 0 ? 0 : parseByte(value.padByte, `fields[${index}].padByte`);
|
|
1055
|
+
if (type === "string") return {
|
|
1056
|
+
name,
|
|
1057
|
+
type: "string",
|
|
1058
|
+
value: rawValue,
|
|
1059
|
+
encoding: parseEncoding(value.encoding, TEXT_ENCODINGS, "utf8", `fields[${index}].encoding`),
|
|
1060
|
+
...length !== void 0 ? { length } : {},
|
|
1061
|
+
padByte
|
|
1062
|
+
};
|
|
1063
|
+
return {
|
|
1064
|
+
name,
|
|
1065
|
+
type: "bytes",
|
|
1066
|
+
value: rawValue,
|
|
1067
|
+
encoding: parseEncoding(value.encoding, BINARY_ENCODINGS, "hex", `fields[${index}].encoding`),
|
|
1068
|
+
...length !== void 0 ? { length } : {},
|
|
1069
|
+
padByte
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function encodePayloadTemplateField(field, endian) {
|
|
1073
|
+
switch (field.type) {
|
|
1074
|
+
case "u8":
|
|
1075
|
+
case "u16":
|
|
1076
|
+
case "u32":
|
|
1077
|
+
case "i8":
|
|
1078
|
+
case "i16":
|
|
1079
|
+
case "i32": {
|
|
1080
|
+
const numericMetadata = getFieldNumericMetadata(field.type);
|
|
1081
|
+
if (!numericMetadata) throw new Error(`Unsupported numeric field type: ${field.type}`);
|
|
1082
|
+
const buffer = Buffer.alloc(numericMetadata.width);
|
|
1083
|
+
writeIntegerToBuffer(buffer, field.value, numericMetadata.width, numericMetadata.signed, endian);
|
|
1084
|
+
return buffer;
|
|
1085
|
+
}
|
|
1086
|
+
case "string": return applyFixedLength(Buffer.from(field.value, field.encoding), field.length, field.padByte);
|
|
1087
|
+
case "bytes": return applyFixedLength(decodeBinaryValue(field.value, field.encoding, `field ${field.name}`), field.length, field.padByte);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function buildPayloadFromTemplate(fields, endian) {
|
|
1091
|
+
const buffers = [];
|
|
1092
|
+
const segments = [];
|
|
1093
|
+
let offset = 0;
|
|
1094
|
+
for (const field of fields) {
|
|
1095
|
+
const encoded = encodePayloadTemplateField(field, endian);
|
|
1096
|
+
buffers.push(encoded);
|
|
1097
|
+
segments.push({
|
|
1098
|
+
name: field.name,
|
|
1099
|
+
offset,
|
|
1100
|
+
length: encoded.length,
|
|
1101
|
+
hex: encoded.toString("hex")
|
|
1102
|
+
});
|
|
1103
|
+
offset += encoded.length;
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
payload: Buffer.concat(buffers),
|
|
1107
|
+
segments
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function parsePayloadMutation(value, index) {
|
|
1111
|
+
if (!isRecord(value)) throw new Error(`mutations[${index}] must be an object`);
|
|
1112
|
+
const strategy = value.strategy;
|
|
1113
|
+
if (typeof strategy !== "string" || !MUTATION_STRATEGIES.includes(strategy)) throw new Error(`mutations[${index}].strategy is invalid`);
|
|
1114
|
+
switch (strategy) {
|
|
1115
|
+
case "set_byte": return {
|
|
1116
|
+
strategy: "set_byte",
|
|
1117
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
1118
|
+
value: parseByte(value.value, `mutations[${index}].value`)
|
|
1119
|
+
};
|
|
1120
|
+
case "flip_bit": return {
|
|
1121
|
+
strategy: "flip_bit",
|
|
1122
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
1123
|
+
bit: (() => {
|
|
1124
|
+
const bit = parseInteger(value.bit, `mutations[${index}].bit`);
|
|
1125
|
+
if (bit < 0 || bit > 7) throw new Error(`mutations[${index}].bit must be between 0 and 7`);
|
|
1126
|
+
return bit;
|
|
1127
|
+
})()
|
|
1128
|
+
};
|
|
1129
|
+
case "overwrite_bytes": return {
|
|
1130
|
+
strategy: "overwrite_bytes",
|
|
1131
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
1132
|
+
data: decodeBinaryValue(typeof value.data === "string" ? value.data : (() => {
|
|
1133
|
+
throw new Error(`mutations[${index}].data must be a string`);
|
|
1134
|
+
})(), parseEncoding(value.encoding, BINARY_ENCODINGS, "hex", `mutations[${index}].encoding`), `mutations[${index}].data`)
|
|
1135
|
+
};
|
|
1136
|
+
case "append_bytes": return {
|
|
1137
|
+
strategy: "append_bytes",
|
|
1138
|
+
data: decodeBinaryValue(typeof value.data === "string" ? value.data : (() => {
|
|
1139
|
+
throw new Error(`mutations[${index}].data must be a string`);
|
|
1140
|
+
})(), parseEncoding(value.encoding, BINARY_ENCODINGS, "hex", `mutations[${index}].encoding`), `mutations[${index}].data`)
|
|
1141
|
+
};
|
|
1142
|
+
case "truncate": return {
|
|
1143
|
+
strategy: "truncate",
|
|
1144
|
+
length: parseNonNegativeInteger(value.length, `mutations[${index}].length`)
|
|
1145
|
+
};
|
|
1146
|
+
case "increment_integer": {
|
|
1147
|
+
const width = value.width;
|
|
1148
|
+
if (width !== 1 && width !== 2 && width !== 4) throw new Error(`mutations[${index}].width must be 1, 2, or 4`);
|
|
1149
|
+
return {
|
|
1150
|
+
strategy: "increment_integer",
|
|
1151
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
1152
|
+
width,
|
|
1153
|
+
delta: parseInteger(value.delta, `mutations[${index}].delta`),
|
|
1154
|
+
endian: parseEndian(value.endian),
|
|
1155
|
+
signed: value.signed === true
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function applyPayloadMutation(payload, mutation, index) {
|
|
1161
|
+
const working = Buffer.from(payload);
|
|
1162
|
+
switch (mutation.strategy) {
|
|
1163
|
+
case "set_byte":
|
|
1164
|
+
if (mutation.offset >= working.length) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
1165
|
+
working[mutation.offset] = mutation.value;
|
|
1166
|
+
return {
|
|
1167
|
+
payload: working,
|
|
1168
|
+
summary: {
|
|
1169
|
+
index,
|
|
1170
|
+
strategy: mutation.strategy,
|
|
1171
|
+
detail: `set payload[${mutation.offset}] to ${mutation.value}`
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
case "flip_bit":
|
|
1175
|
+
if (mutation.offset >= working.length) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
1176
|
+
{
|
|
1177
|
+
const currentByte = working.at(mutation.offset);
|
|
1178
|
+
if (currentByte === void 0) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
1179
|
+
working[mutation.offset] = currentByte ^ 1 << mutation.bit;
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
payload: working,
|
|
1183
|
+
summary: {
|
|
1184
|
+
index,
|
|
1185
|
+
strategy: mutation.strategy,
|
|
1186
|
+
detail: `flipped bit ${mutation.bit} at offset ${mutation.offset}`
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
case "overwrite_bytes":
|
|
1190
|
+
if (mutation.offset + mutation.data.length > working.length) throw new Error(`mutations[${index}] overwrite exceeds payload length`);
|
|
1191
|
+
mutation.data.copy(working, mutation.offset);
|
|
1192
|
+
return {
|
|
1193
|
+
payload: working,
|
|
1194
|
+
summary: {
|
|
1195
|
+
index,
|
|
1196
|
+
strategy: mutation.strategy,
|
|
1197
|
+
detail: `overwrote ${mutation.data.length} bytes at offset ${mutation.offset}`
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
case "append_bytes": return {
|
|
1201
|
+
payload: Buffer.concat([working, mutation.data]),
|
|
1202
|
+
summary: {
|
|
1203
|
+
index,
|
|
1204
|
+
strategy: mutation.strategy,
|
|
1205
|
+
detail: `appended ${mutation.data.length} bytes`
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
case "truncate":
|
|
1209
|
+
if (mutation.length > working.length) throw new Error(`mutations[${index}] length exceeds payload size`);
|
|
1210
|
+
return {
|
|
1211
|
+
payload: working.subarray(0, mutation.length),
|
|
1212
|
+
summary: {
|
|
1213
|
+
index,
|
|
1214
|
+
strategy: mutation.strategy,
|
|
1215
|
+
detail: `truncated payload to ${mutation.length} bytes`
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
case "increment_integer": {
|
|
1219
|
+
if (mutation.offset + mutation.width > working.length) throw new Error(`mutations[${index}] integer range exceeds payload length`);
|
|
1220
|
+
const next = readIntegerFromBuffer(working, mutation.offset, mutation.width, mutation.signed, mutation.endian) + mutation.delta;
|
|
1221
|
+
const range = getNumericRange(mutation.width, mutation.signed);
|
|
1222
|
+
if (next < range.min || next > range.max) throw new Error(`mutations[${index}] integer overflow (${range.min}..${range.max})`);
|
|
1223
|
+
writeIntegerToBuffer(working.subarray(mutation.offset, mutation.offset + mutation.width), next, mutation.width, mutation.signed, mutation.endian);
|
|
1224
|
+
return {
|
|
1225
|
+
payload: working,
|
|
1226
|
+
summary: {
|
|
1227
|
+
index,
|
|
1228
|
+
strategy: mutation.strategy,
|
|
1229
|
+
detail: `incremented ${mutation.signed ? "signed" : "unsigned"} ${mutation.width}-byte integer at offset ${mutation.offset} by ${mutation.delta}`
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function parseMacAddress(value, label) {
|
|
1236
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label} must be a non-empty MAC address string`);
|
|
1237
|
+
const normalized = value.trim().toLowerCase().replace(/^0x/, "").replace(/[:\-.\s]/g, "");
|
|
1238
|
+
if (!/^[0-9a-f]{12}$/i.test(normalized)) throw new Error(`${label} must be a valid 6-byte MAC address`);
|
|
1239
|
+
const canonical = normalized.match(/.{2}/g)?.join(":");
|
|
1240
|
+
if (!canonical) throw new Error(`${label} must be a valid 6-byte MAC address`);
|
|
1241
|
+
return {
|
|
1242
|
+
canonical,
|
|
1243
|
+
bytes: Buffer.from(normalized, "hex")
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function parseNamedOrNumericValue(value, label, map, max) {
|
|
1247
|
+
if (typeof value === "number") {
|
|
1248
|
+
if (!Number.isInteger(value) || value < 0 || value > max) throw new Error(`${label} must be an integer between 0 and ${max}`);
|
|
1249
|
+
return value;
|
|
1250
|
+
}
|
|
1251
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string or integer`);
|
|
1252
|
+
const normalized = value.trim().toLowerCase();
|
|
1253
|
+
const mapped = map[normalized];
|
|
1254
|
+
if (mapped !== void 0) return mapped;
|
|
1255
|
+
if (/^\d+$/.test(normalized)) {
|
|
1256
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
1257
|
+
if (parsed > max) throw new Error(`${label} must be less than or equal to ${max}`);
|
|
1258
|
+
return parsed;
|
|
1259
|
+
}
|
|
1260
|
+
const hex = normalizeHexString(normalized, label);
|
|
1261
|
+
const parsed = Number.parseInt(hex, 16);
|
|
1262
|
+
if (parsed > max) throw new Error(`${label} must be less than or equal to ${max}`);
|
|
1263
|
+
return parsed;
|
|
1264
|
+
}
|
|
1265
|
+
function parseIpv4Address(value, label) {
|
|
1266
|
+
if (typeof value !== "string" || isIP(value.trim()) !== 4) throw new Error(`${label} must be a valid IPv4 address`);
|
|
1267
|
+
const octets = value.trim().split(".").map((part) => Number.parseInt(part, 10));
|
|
1268
|
+
return Buffer.from(octets);
|
|
1269
|
+
}
|
|
1270
|
+
function parseIpv6Groups(value, label) {
|
|
1271
|
+
if (value.length === 0) return [];
|
|
1272
|
+
return value.split(":").flatMap((part) => {
|
|
1273
|
+
if (part.length === 0) return [];
|
|
1274
|
+
if (part.includes(".")) {
|
|
1275
|
+
const ipv4 = parseIpv4Address(part, label);
|
|
1276
|
+
return [ipv4.readUInt16BE(0).toString(16), ipv4.readUInt16BE(2).toString(16)];
|
|
1277
|
+
}
|
|
1278
|
+
if (!/^[0-9a-f]{1,4}$/i.test(part)) throw new Error(`${label} contains an invalid IPv6 group`);
|
|
1279
|
+
return [part];
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
function parseIpv6Address(value, label) {
|
|
1283
|
+
if (typeof value !== "string") throw new Error(`${label} must be a valid IPv6 address`);
|
|
1284
|
+
const normalized = value.trim().toLowerCase().split("%")[0] ?? "";
|
|
1285
|
+
if (isIP(normalized) !== 6) throw new Error(`${label} must be a valid IPv6 address`);
|
|
1286
|
+
const segments = normalized.split("::");
|
|
1287
|
+
if (segments.length > 2) throw new Error(`${label} must be a valid IPv6 address`);
|
|
1288
|
+
const head = parseIpv6Groups(segments[0] ?? "", label);
|
|
1289
|
+
const tail = parseIpv6Groups(segments[1] ?? "", label);
|
|
1290
|
+
const groups = segments.length === 2 ? [
|
|
1291
|
+
...head,
|
|
1292
|
+
...Array.from({ length: 8 - head.length - tail.length }, () => "0"),
|
|
1293
|
+
...tail
|
|
1294
|
+
] : head;
|
|
1295
|
+
if (groups.length !== 8) throw new Error(`${label} must expand to exactly 8 IPv6 groups`);
|
|
1296
|
+
const output = Buffer.alloc(16);
|
|
1297
|
+
for (const [index, group] of groups.entries()) output.writeUInt16BE(Number.parseInt(group, 16), index * 2);
|
|
1298
|
+
return output;
|
|
1299
|
+
}
|
|
1300
|
+
function parseIpAddress(value, version, label) {
|
|
1301
|
+
return version === "ipv4" ? parseIpv4Address(value, label) : parseIpv6Address(value, label);
|
|
1302
|
+
}
|
|
1303
|
+
function parseEtherType(value, label) {
|
|
1304
|
+
return parseNamedOrNumericValue(value, label, ETHER_TYPE_MAP, 65535);
|
|
1305
|
+
}
|
|
1306
|
+
function parseIpProtocol(value, label) {
|
|
1307
|
+
return parseNamedOrNumericValue(value, label, IP_PROTOCOL_MAP, 255);
|
|
1308
|
+
}
|
|
1309
|
+
function parsePcapLinkType(value, label) {
|
|
1310
|
+
return parseNamedOrNumericValue(value, label, PCAP_LINK_TYPE_MAP, 4294967295);
|
|
1311
|
+
}
|
|
1312
|
+
function parseChecksumEndian(value) {
|
|
1313
|
+
return value === "little" ? "little" : "big";
|
|
1314
|
+
}
|
|
1315
|
+
function parsePacketEndianness(value) {
|
|
1316
|
+
return value === "big" ? "big" : "little";
|
|
1317
|
+
}
|
|
1318
|
+
function parseTimestampPrecision(value) {
|
|
1319
|
+
return value === "nano" ? "nano" : "micro";
|
|
1320
|
+
}
|
|
1321
|
+
function parseHexPayload(value, label) {
|
|
1322
|
+
if (typeof value !== "string") throw new Error(`${label} must be a hex string`);
|
|
1323
|
+
return Buffer.from(normalizeHexString(value, label), "hex");
|
|
1324
|
+
}
|
|
1325
|
+
function computeInternetChecksum(buffer) {
|
|
1326
|
+
let sum = 0;
|
|
1327
|
+
for (let offset = 0; offset < buffer.length; offset += 2) {
|
|
1328
|
+
const high = buffer[offset] ?? 0;
|
|
1329
|
+
const low = buffer[offset + 1] ?? 0;
|
|
1330
|
+
sum += high << 8 | low;
|
|
1331
|
+
while (sum > 65535) sum = (sum & 65535) + (sum >>> 16);
|
|
1332
|
+
}
|
|
1333
|
+
return ~sum & 65535;
|
|
1334
|
+
}
|
|
1335
|
+
function buildEthernetFrame(destinationMac, sourceMac, etherType, payload) {
|
|
1336
|
+
const header = Buffer.alloc(14);
|
|
1337
|
+
destinationMac.bytes.copy(header, 0);
|
|
1338
|
+
sourceMac.bytes.copy(header, 6);
|
|
1339
|
+
header.writeUInt16BE(etherType, 12);
|
|
1340
|
+
return Buffer.concat([header, payload]);
|
|
1341
|
+
}
|
|
1342
|
+
function buildArpPayload(args) {
|
|
1343
|
+
if (args.hardwareSize !== args.senderMac.bytes.length || args.hardwareSize !== args.targetMac.bytes.length) throw new Error("hardwareSize must match the provided MAC address lengths");
|
|
1344
|
+
if (args.protocolSize !== args.senderIp.length || args.protocolSize !== args.targetIp.length) throw new Error("protocolSize must match the provided IP address lengths");
|
|
1345
|
+
const buffer = Buffer.alloc(8 + args.hardwareSize * 2 + args.protocolSize * 2);
|
|
1346
|
+
let offset = 0;
|
|
1347
|
+
buffer.writeUInt16BE(args.hardwareType, offset);
|
|
1348
|
+
offset += 2;
|
|
1349
|
+
buffer.writeUInt16BE(args.protocolType, offset);
|
|
1350
|
+
offset += 2;
|
|
1351
|
+
buffer.writeUInt8(args.hardwareSize, offset++);
|
|
1352
|
+
buffer.writeUInt8(args.protocolSize, offset++);
|
|
1353
|
+
buffer.writeUInt16BE(args.operation === "reply" ? 2 : 1, offset);
|
|
1354
|
+
offset += 2;
|
|
1355
|
+
args.senderMac.bytes.copy(buffer, offset);
|
|
1356
|
+
offset += args.hardwareSize;
|
|
1357
|
+
args.senderIp.copy(buffer, offset);
|
|
1358
|
+
offset += args.protocolSize;
|
|
1359
|
+
args.targetMac.bytes.copy(buffer, offset);
|
|
1360
|
+
offset += args.hardwareSize;
|
|
1361
|
+
args.targetIp.copy(buffer, offset);
|
|
1362
|
+
return buffer;
|
|
1363
|
+
}
|
|
1364
|
+
function buildIpv4Packet(args) {
|
|
1365
|
+
const header = Buffer.alloc(20);
|
|
1366
|
+
header[0] = 69;
|
|
1367
|
+
header[1] = (args.dscp & 63) << 2 | args.ecn & 3;
|
|
1368
|
+
header.writeUInt16BE(header.length + args.payload.length, 2);
|
|
1369
|
+
header.writeUInt16BE(args.identification, 4);
|
|
1370
|
+
const flags = (args.dontFragment ? 1 : 0) << 1 | (args.moreFragments ? 1 : 0);
|
|
1371
|
+
header.writeUInt16BE((flags & 7) << 13 | args.fragmentOffset & 8191, 6);
|
|
1372
|
+
header[8] = args.ttl;
|
|
1373
|
+
header[9] = args.protocol;
|
|
1374
|
+
header.writeUInt16BE(0, 10);
|
|
1375
|
+
args.sourceIp.copy(header, 12);
|
|
1376
|
+
args.destinationIp.copy(header, 16);
|
|
1377
|
+
const checksum = computeInternetChecksum(header);
|
|
1378
|
+
header.writeUInt16BE(checksum, 10);
|
|
1379
|
+
return {
|
|
1380
|
+
packet: Buffer.concat([header, args.payload]),
|
|
1381
|
+
checksum
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function buildIpv6Packet(args) {
|
|
1385
|
+
const header = Buffer.alloc(40);
|
|
1386
|
+
const versionTrafficFlow = 6 << 28 | (((args.dscp & 63) << 2 | args.ecn & 3) & 255) << 20 | args.flowLabel & 1048575;
|
|
1387
|
+
header.writeUInt32BE(versionTrafficFlow >>> 0, 0);
|
|
1388
|
+
header.writeUInt16BE(args.payload.length, 4);
|
|
1389
|
+
header.writeUInt8(args.protocol, 6);
|
|
1390
|
+
header.writeUInt8(args.hopLimit, 7);
|
|
1391
|
+
args.sourceIp.copy(header, 8);
|
|
1392
|
+
args.destinationIp.copy(header, 24);
|
|
1393
|
+
return Buffer.concat([header, args.payload]);
|
|
1394
|
+
}
|
|
1395
|
+
function buildIcmpEcho(args) {
|
|
1396
|
+
const packet = Buffer.alloc(8 + args.payload.length);
|
|
1397
|
+
packet[0] = args.operation === "reply" ? 0 : 8;
|
|
1398
|
+
packet[1] = 0;
|
|
1399
|
+
packet.writeUInt16BE(0, 2);
|
|
1400
|
+
packet.writeUInt16BE(args.identifier, 4);
|
|
1401
|
+
packet.writeUInt16BE(args.sequenceNumber, 6);
|
|
1402
|
+
args.payload.copy(packet, 8);
|
|
1403
|
+
const checksum = computeInternetChecksum(packet);
|
|
1404
|
+
packet.writeUInt16BE(checksum, 2);
|
|
1405
|
+
return {
|
|
1406
|
+
packet,
|
|
1407
|
+
checksum
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function writeUint32(buffer, offset, value, endianness) {
|
|
1411
|
+
if (endianness === "little") buffer.writeUInt32LE(value, offset);
|
|
1412
|
+
else buffer.writeUInt32BE(value, offset);
|
|
1413
|
+
}
|
|
1414
|
+
function writeUint16(buffer, offset, value, endianness) {
|
|
1415
|
+
if (endianness === "little") buffer.writeUInt16LE(value, offset);
|
|
1416
|
+
else buffer.writeUInt16BE(value, offset);
|
|
1417
|
+
}
|
|
1418
|
+
function readUint32(buffer, offset, endianness) {
|
|
1419
|
+
return endianness === "little" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
|
|
1420
|
+
}
|
|
1421
|
+
function readUint16(buffer, offset, endianness) {
|
|
1422
|
+
return endianness === "little" ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
|
|
1423
|
+
}
|
|
1424
|
+
function getPcapMagic(endianness, precision) {
|
|
1425
|
+
const hex = endianness === "little" ? precision === "nano" ? "4d3cb2a1" : "d4c3b2a1" : precision === "nano" ? "a1b23c4d" : "a1b2c3d4";
|
|
1426
|
+
return Buffer.from(hex, "hex");
|
|
1427
|
+
}
|
|
1428
|
+
function parsePcapHeader(buffer) {
|
|
1429
|
+
if (buffer.length < 24) throw new Error("PCAP file is too small to contain a global header");
|
|
1430
|
+
const magic = buffer.subarray(0, 4).toString("hex");
|
|
1431
|
+
let endianness;
|
|
1432
|
+
let timestampPrecision;
|
|
1433
|
+
switch (magic) {
|
|
1434
|
+
case "d4c3b2a1":
|
|
1435
|
+
endianness = "little";
|
|
1436
|
+
timestampPrecision = "micro";
|
|
1437
|
+
break;
|
|
1438
|
+
case "4d3cb2a1":
|
|
1439
|
+
endianness = "little";
|
|
1440
|
+
timestampPrecision = "nano";
|
|
1441
|
+
break;
|
|
1442
|
+
case "a1b2c3d4":
|
|
1443
|
+
endianness = "big";
|
|
1444
|
+
timestampPrecision = "micro";
|
|
1445
|
+
break;
|
|
1446
|
+
case "a1b23c4d":
|
|
1447
|
+
endianness = "big";
|
|
1448
|
+
timestampPrecision = "nano";
|
|
1449
|
+
break;
|
|
1450
|
+
default: throw new Error("Unsupported capture format: only classic PCAP files are supported");
|
|
1451
|
+
}
|
|
1452
|
+
return {
|
|
1453
|
+
endianness,
|
|
1454
|
+
timestampPrecision,
|
|
1455
|
+
versionMajor: readUint16(buffer, 4, endianness),
|
|
1456
|
+
versionMinor: readUint16(buffer, 6, endianness),
|
|
1457
|
+
snapLength: readUint32(buffer, 16, endianness),
|
|
1458
|
+
linkType: readUint32(buffer, 20, endianness)
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
function parsePcapPacketInput(value, index) {
|
|
1462
|
+
if (!isRecord(value)) throw new Error(`packets[${index}] must be an object`);
|
|
1463
|
+
const data = parseHexPayload(value.dataHex, `packets[${index}].dataHex`);
|
|
1464
|
+
const timestampSeconds = value.timestampSeconds === void 0 ? 0 : parseNonNegativeInteger(value.timestampSeconds, `packets[${index}].timestampSeconds`);
|
|
1465
|
+
const timestampFraction = value.timestampFraction === void 0 ? 0 : parseNonNegativeInteger(value.timestampFraction, `packets[${index}].timestampFraction`);
|
|
1466
|
+
const originalLength = value.originalLength === void 0 ? data.length : parsePositiveInteger(value.originalLength, `packets[${index}].originalLength`);
|
|
1467
|
+
if (originalLength < data.length) throw new Error(`packets[${index}].originalLength must be >= included packet length`);
|
|
1468
|
+
return {
|
|
1469
|
+
data,
|
|
1470
|
+
timestampSeconds,
|
|
1471
|
+
timestampFraction,
|
|
1472
|
+
originalLength
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function buildClassicPcap(args) {
|
|
1476
|
+
const globalHeader = Buffer.alloc(24);
|
|
1477
|
+
getPcapMagic(args.endianness, args.timestampPrecision).copy(globalHeader, 0);
|
|
1478
|
+
writeUint16(globalHeader, 4, 2, args.endianness);
|
|
1479
|
+
writeUint16(globalHeader, 6, 4, args.endianness);
|
|
1480
|
+
writeUint32(globalHeader, 8, 0, args.endianness);
|
|
1481
|
+
writeUint32(globalHeader, 12, 0, args.endianness);
|
|
1482
|
+
writeUint32(globalHeader, 16, args.snapLength, args.endianness);
|
|
1483
|
+
writeUint32(globalHeader, 20, args.linkType, args.endianness);
|
|
1484
|
+
const records = args.packets.map((packet) => {
|
|
1485
|
+
const header = Buffer.alloc(16);
|
|
1486
|
+
writeUint32(header, 0, packet.timestampSeconds, args.endianness);
|
|
1487
|
+
writeUint32(header, 4, packet.timestampFraction, args.endianness);
|
|
1488
|
+
writeUint32(header, 8, packet.data.length, args.endianness);
|
|
1489
|
+
writeUint32(header, 12, packet.originalLength, args.endianness);
|
|
1490
|
+
return Buffer.concat([header, packet.data]);
|
|
1491
|
+
});
|
|
1492
|
+
return Buffer.concat([globalHeader, ...records]);
|
|
1493
|
+
}
|
|
1494
|
+
function readClassicPcap(buffer, maxPackets, maxBytesPerPacket) {
|
|
1495
|
+
const header = parsePcapHeader(buffer);
|
|
1496
|
+
const packets = [];
|
|
1497
|
+
let offset = 24;
|
|
1498
|
+
while (offset < buffer.length) {
|
|
1499
|
+
if (maxPackets !== void 0 && packets.length >= maxPackets) break;
|
|
1500
|
+
if (offset + 16 > buffer.length) throw new Error("PCAP file ends with an incomplete packet header");
|
|
1501
|
+
const timestampSeconds = readUint32(buffer, offset, header.endianness);
|
|
1502
|
+
const timestampFraction = readUint32(buffer, offset + 4, header.endianness);
|
|
1503
|
+
const includedLength = readUint32(buffer, offset + 8, header.endianness);
|
|
1504
|
+
const originalLength = readUint32(buffer, offset + 12, header.endianness);
|
|
1505
|
+
offset += 16;
|
|
1506
|
+
if (offset + includedLength > buffer.length) throw new Error("PCAP file ends with an incomplete packet payload");
|
|
1507
|
+
const packetBytes = buffer.subarray(offset, offset + includedLength);
|
|
1508
|
+
offset += includedLength;
|
|
1509
|
+
const limit = maxBytesPerPacket === void 0 ? packetBytes.length : maxBytesPerPacket;
|
|
1510
|
+
const visibleLength = Math.min(limit, packetBytes.length);
|
|
1511
|
+
packets.push({
|
|
1512
|
+
index: packets.length,
|
|
1513
|
+
timestampSeconds,
|
|
1514
|
+
timestampFraction,
|
|
1515
|
+
includedLength,
|
|
1516
|
+
originalLength,
|
|
1517
|
+
dataHex: packetBytes.subarray(0, visibleLength).toString("hex"),
|
|
1518
|
+
truncated: visibleLength < packetBytes.length
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
return {
|
|
1522
|
+
header,
|
|
1523
|
+
packets
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/server/domains/protocol-analysis/handlers/handler-class.ts
|
|
1528
|
+
var ProtocolAnalysisHandlers = class {
|
|
1529
|
+
engine;
|
|
1530
|
+
inferrer;
|
|
1531
|
+
eventBus;
|
|
1532
|
+
constructor(engine, inferrer, eventBus) {
|
|
1533
|
+
this.engine = engine;
|
|
1534
|
+
this.inferrer = inferrer;
|
|
1535
|
+
this.eventBus = eventBus;
|
|
1536
|
+
}
|
|
1537
|
+
async handleDefinePattern(args) {
|
|
1538
|
+
try {
|
|
1539
|
+
const name = typeof args.name === "string" && args.name.trim().length > 0 ? args.name : "unnamed_pattern";
|
|
1540
|
+
const specObject = argObject(args, "spec");
|
|
1541
|
+
if (specObject) {
|
|
1542
|
+
const spec = parsePatternSpec(name, specObject);
|
|
1543
|
+
this.getEngine().definePattern(name, spec);
|
|
1544
|
+
return {
|
|
1545
|
+
patternId: name,
|
|
1546
|
+
pattern: this.getEngine().getPattern(name) ?? {
|
|
1547
|
+
name,
|
|
1548
|
+
fields: [],
|
|
1549
|
+
byteOrder: "big"
|
|
1550
|
+
},
|
|
1551
|
+
success: true
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
const fields = (Array.isArray(args.fields) ? args.fields : []).map((field, index) => parseLegacyField(field, index));
|
|
1555
|
+
const byteOrder = args.byteOrder === "little" || args.byteOrder === "big" ? args.byteOrder : void 0;
|
|
1556
|
+
const encryption = parseEncryptionInfo(args.encryption);
|
|
1557
|
+
return {
|
|
1558
|
+
patternId: name,
|
|
1559
|
+
pattern: this.getEngine().definePattern(name, fields, {
|
|
1560
|
+
...byteOrder ? { byteOrder } : {},
|
|
1561
|
+
...encryption ? { encryption } : {}
|
|
1562
|
+
}),
|
|
1563
|
+
success: true
|
|
1564
|
+
};
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
return {
|
|
1567
|
+
patternId: "error",
|
|
1568
|
+
pattern: {
|
|
1569
|
+
name: "error",
|
|
1570
|
+
fields: [],
|
|
1571
|
+
byteOrder: "big"
|
|
1572
|
+
},
|
|
1573
|
+
success: false,
|
|
1574
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
async handleAutoDetect(args) {
|
|
1579
|
+
try {
|
|
1580
|
+
const hexPayloads = (() => {
|
|
1581
|
+
const newPayloads = argStringArray(args, "hexPayloads");
|
|
1582
|
+
if (newPayloads.length > 0) return newPayloads;
|
|
1583
|
+
return argStringArray(args, "payloads");
|
|
1584
|
+
})();
|
|
1585
|
+
const detected = this.getEngine().autoDetect(hexPayloads);
|
|
1586
|
+
const patternName = typeof args.name === "string" && args.name.trim().length > 0 ? args.name : void 0;
|
|
1587
|
+
if (!detected) return {
|
|
1588
|
+
patterns: [this.getEngine().autoDetectPattern([], patternName ? { name: patternName } : {})],
|
|
1589
|
+
success: true
|
|
1590
|
+
};
|
|
1591
|
+
const namedPattern = {
|
|
1592
|
+
...detected,
|
|
1593
|
+
name: patternName ?? detected.name
|
|
1594
|
+
};
|
|
1595
|
+
this.getEngine().definePattern(namedPattern.name, namedPattern);
|
|
1596
|
+
const result = this.getEngine().getPattern(namedPattern.name) ?? {
|
|
1597
|
+
name: namedPattern.name,
|
|
1598
|
+
fields: [],
|
|
1599
|
+
byteOrder: "big"
|
|
1600
|
+
};
|
|
1601
|
+
this.eventBus?.emit("protocol:pattern_detected", {
|
|
1602
|
+
patternName: namedPattern.name,
|
|
1603
|
+
confidence: 0,
|
|
1604
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1605
|
+
});
|
|
1606
|
+
return {
|
|
1607
|
+
patterns: [result],
|
|
1608
|
+
success: true
|
|
1609
|
+
};
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
return {
|
|
1612
|
+
patterns: [],
|
|
1613
|
+
success: false,
|
|
1614
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
async handleInferFields(args) {
|
|
1619
|
+
try {
|
|
1620
|
+
const hexPayloads = argStringArray(args, "hexPayloads");
|
|
1621
|
+
return {
|
|
1622
|
+
success: true,
|
|
1623
|
+
fields: this.getEngine().inferFields(hexPayloads)
|
|
1624
|
+
};
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
return {
|
|
1627
|
+
fields: [],
|
|
1628
|
+
success: false,
|
|
1629
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
async handleExportSchema(args) {
|
|
1634
|
+
try {
|
|
1635
|
+
const patternId = argStringRequired(args, "patternId");
|
|
1636
|
+
const pattern = this.getEngine().getPattern(patternId);
|
|
1637
|
+
if (!pattern) return { schema: `// Error: pattern '${patternId}' not found` };
|
|
1638
|
+
return { schema: this.getEngine().exportProto(pattern) };
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
return { schema: `// Error: ${error instanceof Error ? error.message : String(error)}` };
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
async handleInferStateMachine(args) {
|
|
1644
|
+
try {
|
|
1645
|
+
const rawMessages = args.messages;
|
|
1646
|
+
if (!Array.isArray(rawMessages)) throw new Error("messages must be an array");
|
|
1647
|
+
const hasLegacyShape = rawMessages.some((message) => isRecord(message) && (message.direction === "in" || message.direction === "out"));
|
|
1648
|
+
let stateMachine;
|
|
1649
|
+
if (hasLegacyShape) {
|
|
1650
|
+
const legacyMessages = rawMessages.map((message, index) => {
|
|
1651
|
+
if (!isRecord(message)) throw new Error(`messages[${index}] must be an object`);
|
|
1652
|
+
const direction = message.direction;
|
|
1653
|
+
const payloadHex = typeof message.payloadHex === "string" ? message.payloadHex : "";
|
|
1654
|
+
const timestamp = typeof message.timestamp === "number" ? message.timestamp : void 0;
|
|
1655
|
+
const payload = Buffer.from(payloadHex.replace(/\s+/g, ""), "hex");
|
|
1656
|
+
if (direction !== "in" && direction !== "out") throw new Error(`messages[${index}].direction must be "in" or "out"`);
|
|
1657
|
+
return {
|
|
1658
|
+
direction,
|
|
1659
|
+
payload,
|
|
1660
|
+
...timestamp !== void 0 ? { timestamp } : {}
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1663
|
+
stateMachine = this.getInferrer().inferStateMachine(legacyMessages);
|
|
1664
|
+
} else {
|
|
1665
|
+
const messages = rawMessages.map((message, index) => parseProtocolMessage(message, index));
|
|
1666
|
+
stateMachine = this.getInferrer().infer(messages);
|
|
1667
|
+
}
|
|
1668
|
+
if (args.simplify === true) stateMachine = this.getInferrer().simplify(stateMachine);
|
|
1669
|
+
return {
|
|
1670
|
+
stateMachine,
|
|
1671
|
+
mermaid: this.getInferrer().generateMermaid(stateMachine),
|
|
1672
|
+
success: true
|
|
1673
|
+
};
|
|
1674
|
+
} catch (error) {
|
|
1675
|
+
return {
|
|
1676
|
+
stateMachine: {
|
|
1677
|
+
states: [],
|
|
1678
|
+
transitions: [],
|
|
1679
|
+
initial: "",
|
|
1680
|
+
initialState: "",
|
|
1681
|
+
finalStates: []
|
|
1682
|
+
},
|
|
1683
|
+
success: false,
|
|
1684
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
async handleVisualizeState(args) {
|
|
1689
|
+
try {
|
|
1690
|
+
const stateMachineValue = args.stateMachine;
|
|
1691
|
+
if (!isRecord(stateMachineValue)) return { mermaidDiagram: this.getInferrer().generateMermaid({
|
|
1692
|
+
states: [],
|
|
1693
|
+
transitions: [],
|
|
1694
|
+
initial: "",
|
|
1695
|
+
initialState: "",
|
|
1696
|
+
finalStates: []
|
|
1697
|
+
}) };
|
|
1698
|
+
const states = Array.isArray(stateMachineValue.states) ? stateMachineValue.states : [];
|
|
1699
|
+
const transitions = Array.isArray(stateMachineValue.transitions) ? stateMachineValue.transitions : [];
|
|
1700
|
+
const initialState = typeof stateMachineValue.initialState === "string" ? stateMachineValue.initialState : "";
|
|
1701
|
+
const finalStates = Array.isArray(stateMachineValue.finalStates) ? stateMachineValue.finalStates.filter((state) => typeof state === "string") : [];
|
|
1702
|
+
return { mermaidDiagram: this.getInferrer().generateMermaid({
|
|
1703
|
+
states: states.filter((state) => isRecord(state)),
|
|
1704
|
+
transitions: transitions.filter((transition) => isRecord(transition)),
|
|
1705
|
+
initial: initialState,
|
|
1706
|
+
initialState,
|
|
1707
|
+
finalStates
|
|
1708
|
+
}) };
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
return { mermaidDiagram: `stateDiagram-v2\n note right of empty: ${error instanceof Error ? error.message : String(error)}` };
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
async handlePayloadTemplateBuild(args) {
|
|
1714
|
+
try {
|
|
1715
|
+
const rawFields = args.fields;
|
|
1716
|
+
if (!Array.isArray(rawFields)) throw new Error("fields must be an array");
|
|
1717
|
+
const { payload, segments } = buildPayloadFromTemplate(rawFields.map((field, index) => parsePayloadTemplateField(field, index)), parseEndian(args.endian));
|
|
1718
|
+
this.emitEvent("protocol:payload_built", {
|
|
1719
|
+
byteLength: payload.length,
|
|
1720
|
+
fieldCount: segments.length
|
|
1721
|
+
});
|
|
1722
|
+
return {
|
|
1723
|
+
hexPayload: payload.toString("hex"),
|
|
1724
|
+
byteLength: payload.length,
|
|
1725
|
+
fields: segments,
|
|
1726
|
+
success: true
|
|
1727
|
+
};
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
return {
|
|
1730
|
+
hexPayload: "",
|
|
1731
|
+
byteLength: 0,
|
|
1732
|
+
fields: [],
|
|
1733
|
+
success: false,
|
|
1734
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
async handlePayloadMutate(args) {
|
|
1739
|
+
let originalHex = "";
|
|
1740
|
+
try {
|
|
1741
|
+
if (typeof args.hexPayload !== "string") throw new Error("hexPayload must be a string");
|
|
1742
|
+
originalHex = normalizeHexString(args.hexPayload, "hexPayload");
|
|
1743
|
+
const rawMutations = args.mutations;
|
|
1744
|
+
if (!Array.isArray(rawMutations)) throw new Error("mutations must be an array");
|
|
1745
|
+
let payload = Buffer.from(originalHex, "hex");
|
|
1746
|
+
const appliedMutations = [];
|
|
1747
|
+
for (const [index, rawMutation] of rawMutations.entries()) {
|
|
1748
|
+
const mutation = parsePayloadMutation(rawMutation, index);
|
|
1749
|
+
const result = applyPayloadMutation(payload, mutation, index);
|
|
1750
|
+
payload = result.payload;
|
|
1751
|
+
appliedMutations.push(result.summary);
|
|
1752
|
+
}
|
|
1753
|
+
this.emitEvent("protocol:payload_mutated", {
|
|
1754
|
+
byteLength: payload.length,
|
|
1755
|
+
mutationCount: appliedMutations.length
|
|
1756
|
+
});
|
|
1757
|
+
return {
|
|
1758
|
+
originalHex,
|
|
1759
|
+
mutatedHex: payload.toString("hex"),
|
|
1760
|
+
byteLength: payload.length,
|
|
1761
|
+
appliedMutations,
|
|
1762
|
+
success: true
|
|
1763
|
+
};
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
return {
|
|
1766
|
+
originalHex,
|
|
1767
|
+
mutatedHex: "",
|
|
1768
|
+
byteLength: 0,
|
|
1769
|
+
appliedMutations: [],
|
|
1770
|
+
success: false,
|
|
1771
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async handleEthernetFrameBuild(args) {
|
|
1776
|
+
try {
|
|
1777
|
+
const destinationMac = parseMacAddress(args.destinationMac, "destinationMac");
|
|
1778
|
+
const sourceMac = parseMacAddress(args.sourceMac, "sourceMac");
|
|
1779
|
+
const etherType = parseEtherType(args.etherType, "etherType");
|
|
1780
|
+
const frame = buildEthernetFrame(destinationMac, sourceMac, etherType, parseHexPayload(args.payloadHex, "payloadHex"));
|
|
1781
|
+
this.emitEvent("protocol:ethernet_frame_built", {
|
|
1782
|
+
byteLength: frame.length,
|
|
1783
|
+
etherType: `0x${etherType.toString(16).padStart(4, "0")}`
|
|
1784
|
+
});
|
|
1785
|
+
return {
|
|
1786
|
+
destinationMac: destinationMac.canonical,
|
|
1787
|
+
sourceMac: sourceMac.canonical,
|
|
1788
|
+
etherType,
|
|
1789
|
+
etherTypeHex: `0x${etherType.toString(16).padStart(4, "0")}`,
|
|
1790
|
+
byteLength: frame.length,
|
|
1791
|
+
headerHex: frame.subarray(0, 14).toString("hex"),
|
|
1792
|
+
frameHex: frame.toString("hex"),
|
|
1793
|
+
success: true
|
|
1794
|
+
};
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
return {
|
|
1797
|
+
destinationMac: "",
|
|
1798
|
+
sourceMac: "",
|
|
1799
|
+
etherType: 0,
|
|
1800
|
+
etherTypeHex: "0x0000",
|
|
1801
|
+
byteLength: 0,
|
|
1802
|
+
headerHex: "",
|
|
1803
|
+
frameHex: "",
|
|
1804
|
+
success: false,
|
|
1805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
async handleArpBuild(args) {
|
|
1810
|
+
try {
|
|
1811
|
+
const operation = args.operation === "reply" ? "reply" : "request";
|
|
1812
|
+
const senderMac = parseMacAddress(args.senderMac, "senderMac");
|
|
1813
|
+
const targetMac = parseMacAddress(args.targetMac ?? "00:00:00:00:00:00", "targetMac");
|
|
1814
|
+
const senderIp = parseIpv4Address(args.senderIp, "senderIp");
|
|
1815
|
+
const targetIp = parseIpv4Address(args.targetIp, "targetIp");
|
|
1816
|
+
const payload = buildArpPayload({
|
|
1817
|
+
operation,
|
|
1818
|
+
hardwareType: args.hardwareType === void 0 ? 1 : parseNonNegativeInteger(args.hardwareType, "hardwareType"),
|
|
1819
|
+
protocolType: parseEtherType(args.protocolType ?? "ipv4", "protocolType"),
|
|
1820
|
+
hardwareSize: args.hardwareSize === void 0 ? 6 : parsePositiveInteger(args.hardwareSize, "hardwareSize"),
|
|
1821
|
+
protocolSize: args.protocolSize === void 0 ? 4 : parsePositiveInteger(args.protocolSize, "protocolSize"),
|
|
1822
|
+
senderMac,
|
|
1823
|
+
senderIp,
|
|
1824
|
+
targetMac,
|
|
1825
|
+
targetIp
|
|
1826
|
+
});
|
|
1827
|
+
this.emitEvent("protocol:arp_built", {
|
|
1828
|
+
operation,
|
|
1829
|
+
byteLength: payload.length
|
|
1830
|
+
});
|
|
1831
|
+
return {
|
|
1832
|
+
operation,
|
|
1833
|
+
byteLength: payload.length,
|
|
1834
|
+
payloadHex: payload.toString("hex"),
|
|
1835
|
+
senderMac: senderMac.canonical,
|
|
1836
|
+
senderIp: args.senderIp,
|
|
1837
|
+
targetMac: targetMac.canonical,
|
|
1838
|
+
targetIp: args.targetIp,
|
|
1839
|
+
success: true
|
|
1840
|
+
};
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
return {
|
|
1843
|
+
operation: null,
|
|
1844
|
+
byteLength: 0,
|
|
1845
|
+
payloadHex: "",
|
|
1846
|
+
senderMac: "",
|
|
1847
|
+
senderIp: "",
|
|
1848
|
+
targetMac: "",
|
|
1849
|
+
targetIp: "",
|
|
1850
|
+
success: false,
|
|
1851
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async handleRawIpPacketBuild(args) {
|
|
1856
|
+
try {
|
|
1857
|
+
const version = args.version === "ipv6" ? "ipv6" : "ipv4";
|
|
1858
|
+
const payload = parseHexPayload(args.payloadHex ?? "", "payloadHex");
|
|
1859
|
+
const protocol = parseIpProtocol(args.protocol, "protocol");
|
|
1860
|
+
const dscp = args.dscp === void 0 ? 0 : parseNonNegativeInteger(args.dscp, "dscp");
|
|
1861
|
+
const ecn = args.ecn === void 0 ? 0 : parseNonNegativeInteger(args.ecn, "ecn");
|
|
1862
|
+
if (dscp > 63) throw new Error("dscp must be between 0 and 63");
|
|
1863
|
+
if (ecn > 3) throw new Error("ecn must be between 0 and 3");
|
|
1864
|
+
if (version === "ipv4") {
|
|
1865
|
+
const ttl = args.ttl === void 0 ? 64 : parseByte(args.ttl, "ttl");
|
|
1866
|
+
const identification = args.identification === void 0 ? 0 : parseNonNegativeInteger(args.identification, "identification");
|
|
1867
|
+
const fragmentOffset = args.fragmentOffset === void 0 ? 0 : parseNonNegativeInteger(args.fragmentOffset, "fragmentOffset");
|
|
1868
|
+
if (identification > 65535) throw new Error("identification must be between 0 and 65535");
|
|
1869
|
+
if (fragmentOffset > 8191) throw new Error("fragmentOffset must be between 0 and 8191");
|
|
1870
|
+
const { packet, checksum } = buildIpv4Packet({
|
|
1871
|
+
sourceIp: parseIpAddress(args.sourceIp, "ipv4", "sourceIp"),
|
|
1872
|
+
destinationIp: parseIpAddress(args.destinationIp, "ipv4", "destinationIp"),
|
|
1873
|
+
protocol,
|
|
1874
|
+
payload,
|
|
1875
|
+
ttl,
|
|
1876
|
+
identification,
|
|
1877
|
+
dontFragment: args.dontFragment === true,
|
|
1878
|
+
moreFragments: args.moreFragments === true,
|
|
1879
|
+
fragmentOffset,
|
|
1880
|
+
dscp,
|
|
1881
|
+
ecn
|
|
1882
|
+
});
|
|
1883
|
+
this.emitEvent("protocol:ip_packet_built", {
|
|
1884
|
+
version,
|
|
1885
|
+
protocol,
|
|
1886
|
+
byteLength: packet.length
|
|
1887
|
+
});
|
|
1888
|
+
return {
|
|
1889
|
+
version,
|
|
1890
|
+
protocol,
|
|
1891
|
+
byteLength: packet.length,
|
|
1892
|
+
headerLength: 20,
|
|
1893
|
+
packetHex: packet.toString("hex"),
|
|
1894
|
+
headerHex: packet.subarray(0, 20).toString("hex"),
|
|
1895
|
+
payloadHex: payload.toString("hex"),
|
|
1896
|
+
checksumHex: checksum.toString(16).padStart(4, "0"),
|
|
1897
|
+
success: true
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
const hopLimit = args.hopLimit === void 0 ? args.ttl === void 0 ? 64 : parseByte(args.ttl, "ttl") : parseByte(args.hopLimit, "hopLimit");
|
|
1901
|
+
const flowLabel = args.flowLabel === void 0 ? 0 : parseNonNegativeInteger(args.flowLabel, "flowLabel");
|
|
1902
|
+
if (flowLabel > 1048575) throw new Error("flowLabel must be between 0 and 1048575");
|
|
1903
|
+
const packet = buildIpv6Packet({
|
|
1904
|
+
sourceIp: parseIpAddress(args.sourceIp, "ipv6", "sourceIp"),
|
|
1905
|
+
destinationIp: parseIpAddress(args.destinationIp, "ipv6", "destinationIp"),
|
|
1906
|
+
protocol,
|
|
1907
|
+
payload,
|
|
1908
|
+
hopLimit,
|
|
1909
|
+
dscp,
|
|
1910
|
+
ecn,
|
|
1911
|
+
flowLabel
|
|
1912
|
+
});
|
|
1913
|
+
this.emitEvent("protocol:ip_packet_built", {
|
|
1914
|
+
version,
|
|
1915
|
+
protocol,
|
|
1916
|
+
byteLength: packet.length
|
|
1917
|
+
});
|
|
1918
|
+
return {
|
|
1919
|
+
version,
|
|
1920
|
+
protocol,
|
|
1921
|
+
byteLength: packet.length,
|
|
1922
|
+
headerLength: 40,
|
|
1923
|
+
packetHex: packet.toString("hex"),
|
|
1924
|
+
headerHex: packet.subarray(0, 40).toString("hex"),
|
|
1925
|
+
payloadHex: payload.toString("hex"),
|
|
1926
|
+
checksumHex: null,
|
|
1927
|
+
success: true
|
|
1928
|
+
};
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
return {
|
|
1931
|
+
version: null,
|
|
1932
|
+
protocol: null,
|
|
1933
|
+
byteLength: 0,
|
|
1934
|
+
headerLength: 0,
|
|
1935
|
+
packetHex: "",
|
|
1936
|
+
headerHex: "",
|
|
1937
|
+
payloadHex: "",
|
|
1938
|
+
checksumHex: null,
|
|
1939
|
+
success: false,
|
|
1940
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async handleIcmpEchoBuild(args) {
|
|
1945
|
+
try {
|
|
1946
|
+
const operation = args.operation === "reply" ? "reply" : "request";
|
|
1947
|
+
const identifier = args.identifier === void 0 ? 0 : parseNonNegativeInteger(args.identifier, "identifier");
|
|
1948
|
+
const sequenceNumber = args.sequenceNumber === void 0 ? 0 : parseNonNegativeInteger(args.sequenceNumber, "sequenceNumber");
|
|
1949
|
+
if (identifier > 65535) throw new Error("identifier must be between 0 and 65535");
|
|
1950
|
+
if (sequenceNumber > 65535) throw new Error("sequenceNumber must be between 0 and 65535");
|
|
1951
|
+
const payload = parseHexPayload(args.payloadHex ?? "", "payloadHex");
|
|
1952
|
+
const { packet, checksum } = buildIcmpEcho({
|
|
1953
|
+
operation,
|
|
1954
|
+
identifier,
|
|
1955
|
+
sequenceNumber,
|
|
1956
|
+
payload
|
|
1957
|
+
});
|
|
1958
|
+
const checksumHex = checksum.toString(16).padStart(4, "0");
|
|
1959
|
+
this.emitEvent("protocol:icmp_echo_built", {
|
|
1960
|
+
operation,
|
|
1961
|
+
byteLength: packet.length,
|
|
1962
|
+
checksumHex
|
|
1963
|
+
});
|
|
1964
|
+
return {
|
|
1965
|
+
operation,
|
|
1966
|
+
identifier,
|
|
1967
|
+
sequenceNumber,
|
|
1968
|
+
checksum,
|
|
1969
|
+
checksumHex,
|
|
1970
|
+
byteLength: packet.length,
|
|
1971
|
+
packetHex: packet.toString("hex"),
|
|
1972
|
+
payloadHex: payload.toString("hex"),
|
|
1973
|
+
success: true
|
|
1974
|
+
};
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
return {
|
|
1977
|
+
operation: null,
|
|
1978
|
+
identifier: null,
|
|
1979
|
+
sequenceNumber: null,
|
|
1980
|
+
checksum: null,
|
|
1981
|
+
checksumHex: "",
|
|
1982
|
+
byteLength: 0,
|
|
1983
|
+
packetHex: "",
|
|
1984
|
+
payloadHex: "",
|
|
1985
|
+
success: false,
|
|
1986
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
async handleChecksumApply(args) {
|
|
1991
|
+
try {
|
|
1992
|
+
const payload = parseHexPayload(args.hexPayload, "hexPayload");
|
|
1993
|
+
const rangeStart = args.startOffset === void 0 ? 0 : parseNonNegativeInteger(args.startOffset, "startOffset");
|
|
1994
|
+
const rangeEnd = args.endOffset === void 0 ? payload.length : parseNonNegativeInteger(args.endOffset, "endOffset");
|
|
1995
|
+
if (rangeStart > rangeEnd || rangeEnd > payload.length) throw new Error("checksum range must stay within the payload");
|
|
1996
|
+
const zeroOffset = args.zeroOffset === void 0 ? void 0 : parseNonNegativeInteger(args.zeroOffset, "zeroOffset");
|
|
1997
|
+
const zeroLength = args.zeroLength === void 0 ? 2 : parsePositiveInteger(args.zeroLength, "zeroLength");
|
|
1998
|
+
const writeOffset = args.writeOffset === void 0 ? zeroOffset : parseNonNegativeInteger(args.writeOffset, "writeOffset");
|
|
1999
|
+
const endian = parseChecksumEndian(args.endian);
|
|
2000
|
+
const working = Buffer.from(payload);
|
|
2001
|
+
if (zeroOffset !== void 0) {
|
|
2002
|
+
if (zeroOffset + zeroLength > working.length) throw new Error("zeroOffset and zeroLength must stay within the payload");
|
|
2003
|
+
working.fill(0, zeroOffset, zeroOffset + zeroLength);
|
|
2004
|
+
}
|
|
2005
|
+
const checksum = computeInternetChecksum(working.subarray(rangeStart, rangeEnd));
|
|
2006
|
+
if (writeOffset !== void 0) {
|
|
2007
|
+
if (writeOffset + 2 > working.length) throw new Error("writeOffset must leave room for a 16-bit checksum field");
|
|
2008
|
+
if (endian === "little") working.writeUInt16LE(checksum, writeOffset);
|
|
2009
|
+
else working.writeUInt16BE(checksum, writeOffset);
|
|
2010
|
+
}
|
|
2011
|
+
const checksumHex = checksum.toString(16).padStart(4, "0");
|
|
2012
|
+
this.emitEvent("protocol:checksum_applied", {
|
|
2013
|
+
checksumHex,
|
|
2014
|
+
byteLength: working.length
|
|
2015
|
+
});
|
|
2016
|
+
return {
|
|
2017
|
+
checksumHex,
|
|
2018
|
+
checksum,
|
|
2019
|
+
mutatedHex: working.toString("hex"),
|
|
2020
|
+
byteLength: working.length,
|
|
2021
|
+
rangeStart,
|
|
2022
|
+
rangeEnd,
|
|
2023
|
+
success: true
|
|
2024
|
+
};
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
return {
|
|
2027
|
+
checksumHex: "",
|
|
2028
|
+
checksum: 0,
|
|
2029
|
+
mutatedHex: "",
|
|
2030
|
+
byteLength: 0,
|
|
2031
|
+
rangeStart: 0,
|
|
2032
|
+
rangeEnd: 0,
|
|
2033
|
+
success: false,
|
|
2034
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async handlePcapWrite(args) {
|
|
2039
|
+
try {
|
|
2040
|
+
if (typeof args.path !== "string" || args.path.trim().length === 0) throw new Error("path must be a non-empty string");
|
|
2041
|
+
if (!Array.isArray(args.packets)) throw new Error("packets must be an array");
|
|
2042
|
+
const packets = args.packets.map((entry, index) => parsePcapPacketInput(entry, index));
|
|
2043
|
+
const endianness = parsePacketEndianness(args.endianness);
|
|
2044
|
+
const timestampPrecision = parseTimestampPrecision(args.timestampPrecision);
|
|
2045
|
+
const snapLength = args.snapLength === void 0 ? 65535 : parsePositiveInteger(args.snapLength, "snapLength");
|
|
2046
|
+
const linkType = parsePcapLinkType(args.linkType ?? "ethernet", "linkType");
|
|
2047
|
+
const buffer = buildClassicPcap({
|
|
2048
|
+
packets,
|
|
2049
|
+
endianness,
|
|
2050
|
+
timestampPrecision,
|
|
2051
|
+
snapLength,
|
|
2052
|
+
linkType
|
|
2053
|
+
});
|
|
2054
|
+
await writeFile$1(args.path, buffer);
|
|
2055
|
+
this.emitEvent("protocol:pcap_written", {
|
|
2056
|
+
path: args.path,
|
|
2057
|
+
packetCount: packets.length,
|
|
2058
|
+
byteLength: buffer.length
|
|
2059
|
+
});
|
|
2060
|
+
return {
|
|
2061
|
+
path: args.path,
|
|
2062
|
+
packetCount: packets.length,
|
|
2063
|
+
byteLength: buffer.length,
|
|
2064
|
+
endianness,
|
|
2065
|
+
timestampPrecision,
|
|
2066
|
+
linkType,
|
|
2067
|
+
success: true
|
|
2068
|
+
};
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
return {
|
|
2071
|
+
path: typeof args.path === "string" ? args.path : "",
|
|
2072
|
+
packetCount: 0,
|
|
2073
|
+
byteLength: 0,
|
|
2074
|
+
endianness: null,
|
|
2075
|
+
timestampPrecision: null,
|
|
2076
|
+
linkType: null,
|
|
2077
|
+
success: false,
|
|
2078
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
async handlePcapRead(args) {
|
|
2083
|
+
try {
|
|
2084
|
+
if (typeof args.path !== "string" || args.path.trim().length === 0) throw new Error("path must be a non-empty string");
|
|
2085
|
+
const maxPackets = args.maxPackets === void 0 ? void 0 : parsePositiveInteger(args.maxPackets, "maxPackets");
|
|
2086
|
+
const maxBytesPerPacket = args.maxBytesPerPacket === void 0 ? void 0 : parsePositiveInteger(args.maxBytesPerPacket, "maxBytesPerPacket");
|
|
2087
|
+
const { header, packets } = readClassicPcap(await readFile$1(args.path), maxPackets, maxBytesPerPacket);
|
|
2088
|
+
this.emitEvent("protocol:pcap_read", {
|
|
2089
|
+
path: args.path,
|
|
2090
|
+
packetCount: packets.length
|
|
2091
|
+
});
|
|
2092
|
+
return {
|
|
2093
|
+
path: args.path,
|
|
2094
|
+
header,
|
|
2095
|
+
packets,
|
|
2096
|
+
success: true
|
|
2097
|
+
};
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
return {
|
|
2100
|
+
path: typeof args.path === "string" ? args.path : "",
|
|
2101
|
+
header: null,
|
|
2102
|
+
packets: [],
|
|
2103
|
+
success: false,
|
|
2104
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
emitEvent(event, payload) {
|
|
2109
|
+
this.eventBus?.emit(event, {
|
|
2110
|
+
...payload,
|
|
2111
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
getEngine() {
|
|
2115
|
+
if (!this.engine) this.engine = new ProtocolPatternEngine();
|
|
2116
|
+
return this.engine;
|
|
2117
|
+
}
|
|
2118
|
+
getInferrer() {
|
|
2119
|
+
if (!this.inferrer) this.inferrer = new StateMachineInferrer();
|
|
2120
|
+
return this.inferrer;
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
//#endregion
|
|
2124
|
+
export { ProtocolAnalysisHandlers };
|