@jshookmcp/jshook 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-CqGDXmfc.mjs} +160 -54
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-BdjRfNx7.mjs} +5 -5
- package/dist/ConsoleMonitor-DykL3IAw.mjs +2269 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-ETyy0xyo.mjs} +1 -1
- package/dist/DetailedDataManager-HT49OrvF.mjs +217 -0
- package/dist/EventBus-DFKvADm3.mjs +141 -0
- package/dist/EvidenceGraphBridge-318Oi0Lf.mjs +153 -0
- package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-BDMsY2Dz.mjs} +27 -13
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-BN4UQWnX.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-Cc2AFq1Y.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-DruMgsgj.mjs} +21 -21
- package/dist/HookGeneratorBuilders.core.generators.storage-CTbB4Lcx.mjs +566 -0
- package/dist/InstrumentationSession-DLH0vd-z.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CMtviNW_.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-ITgb_NMi.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-CiL7Z3ey.mjs} +50 -21
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-D9Lkovvn.mjs} +20 -56
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-BBlAixF5.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-DMQ44gen.mjs} +16 -16
- package/dist/PageController-BPJNqqBN.mjs +431 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-K7wN8Z-w.mjs} +10 -7
- package/dist/PrerequisiteError-TuyZIs6n.mjs +20 -0
- package/dist/ProcessRegistry-zGg12QbE.mjs +74 -0
- package/dist/ResponseBuilder-CJXWmWNw.mjs +143 -0
- package/dist/ReverseEvidenceGraph-C02-gXOh.mjs +269 -0
- package/dist/ScriptManager-ZuWD-0Jg.mjs +3003 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-D-z0umeT.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-Cav5AVSL.mjs} +9 -6
- package/dist/ToolCatalog-5OJdMiF0.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-DbCFGyrg.mjs +45 -0
- package/dist/ToolRegistry-B9krbTtI.mjs +180 -0
- package/dist/ToolRouter.policy-BGDAGyeH.mjs +344 -0
- package/dist/TraceRecorder-B41Z5XBj.mjs +1286 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-C2kjj0ze.mjs} +19 -13
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-CKrGOTpo.mjs} +3 -3
- package/dist/WorkflowEngine-DJ6M4opp.mjs +569 -0
- package/dist/analysis-BHeJW2Nb.mjs +1234 -0
- package/dist/antidebug-BRKeyt27.mjs +1081 -0
- package/dist/artifactRetention-CPXkUJXp.mjs +598 -0
- package/dist/artifacts-DkfosXH3.mjs +59 -0
- package/dist/authorization-schema-DRqyJMSk.mjs +31 -0
- package/dist/betterSqlite3-DLSBZodi.mjs +74 -0
- package/dist/binary-instrument--V3MAhJ4.mjs +971 -0
- package/dist/bind-helpers-ClV34xdn.mjs +42 -0
- package/dist/boringssl-inspector-Bo_LOLaS.mjs +180 -0
- package/dist/browser-Dx3_S2cG.mjs +4369 -0
- package/dist/capabilities-CcHlvWgK.mjs +33 -0
- package/dist/concurrency-Drev_Vz9.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-CDZLOoVv.mjs} +105 -48
- package/dist/coordination-DgItD9DL.mjs +259 -0
- package/dist/debugger-RS3RSAqs.mjs +1288 -0
- package/dist/definitions-BEoYofW5.mjs +47 -0
- package/dist/definitions-BRaefg3u.mjs +365 -0
- package/dist/definitions-BbkvZkiv.mjs +96 -0
- package/dist/definitions-BtWSHJ3o.mjs +17 -0
- package/dist/definitions-C1gCHO0i.mjs +43 -0
- package/dist/definitions-CDOg_b-l.mjs +138 -0
- package/dist/definitions-CVPD9hzZ.mjs +54 -0
- package/dist/definitions-Cea8Lgl7.mjs +94 -0
- package/dist/definitions-DAgIyjxM.mjs +10 -0
- package/dist/definitions-DJA27nsL.mjs +66 -0
- package/dist/definitions-DKPFU3LW.mjs +25 -0
- package/dist/definitions-DPRpZQ96.mjs +47 -0
- package/dist/definitions-DUE5gmdn.mjs +18 -0
- package/dist/definitions-DYVjOtxa.mjs +26 -0
- package/dist/definitions-DcYLVLCo.mjs +37 -0
- package/dist/definitions-Pp5LI2H4.mjs +27 -0
- package/dist/definitions-j9KdHVNR.mjs +14 -0
- package/dist/definitions-uzkjBwa7.mjs +258 -0
- package/dist/definitions-va-AnLuQ.mjs +28 -0
- package/dist/encoding-DJeqHmpd.mjs +1079 -0
- package/dist/evidence-graph-bridge-DcYizFk2.mjs +136 -0
- package/dist/{factory-CibqTNC8.mjs → factory-C90tBff6.mjs} +41 -56
- package/dist/flat-target-session-Dgax2Cy3.mjs +29 -0
- package/dist/graphql-CoHrhweh.mjs +1197 -0
- package/dist/handlers-4jmR0nMs.mjs +898 -0
- package/dist/handlers-BAHPxcch.mjs +789 -0
- package/dist/handlers-BOs9b907.mjs +2600 -0
- package/dist/handlers-BWXEy6ef.mjs +917 -0
- package/dist/handlers-Bndn6QvE.mjs +111 -0
- package/dist/handlers-BqC4bD4s.mjs +681 -0
- package/dist/handlers-BtYq60bM2.mjs +276 -0
- package/dist/handlers-BzgcB4iv.mjs +799 -0
- package/dist/handlers-CRyRWj2b.mjs +859 -0
- package/dist/handlers-CVv2H1uq.mjs +592 -0
- package/dist/handlers-Dl5a7JS4.mjs +572 -0
- package/dist/handlers-Dx2d7jt7.mjs +2537 -0
- package/dist/handlers-Dz9PYsCa.mjs +2805 -0
- package/dist/handlers-HujRKC3b.mjs +661 -0
- package/dist/handlers.impl-XWXkQfyi.mjs +807 -0
- package/dist/hooks-B1B8NRHL.mjs +898 -0
- package/dist/index.mjs +491 -259
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-PRMkLVRW.mjs +835 -0
- package/dist/manifest-67Bok-Si.mjs +58 -0
- package/dist/manifest-6lNTMZAB2.mjs +87 -0
- package/dist/manifest-B2duEHiH.mjs +90 -0
- package/dist/manifest-B6EY9Vm8.mjs +57 -0
- package/dist/manifest-B6nKSbyY.mjs +95 -0
- package/dist/manifest-BL8AQNPF.mjs +106 -0
- package/dist/manifest-BSZvJJmV.mjs +47 -0
- package/dist/manifest-BU7qzUyX.mjs +418 -0
- package/dist/manifest-Bl62e8WK.mjs +49 -0
- package/dist/manifest-Bo5cXjdt.mjs +82 -0
- package/dist/manifest-BpS4gtUK.mjs +1347 -0
- package/dist/manifest-Bv65_e2W.mjs +101 -0
- package/dist/manifest-BytNIF4Z.mjs +117 -0
- package/dist/manifest-C-xtsjS3.mjs +81 -0
- package/dist/manifest-CDYl7OhA.mjs +66 -0
- package/dist/manifest-CRZ3xmkD.mjs +61 -0
- package/dist/manifest-CoW6u4Tp.mjs +132 -0
- package/dist/manifest-Cq5zN_8A.mjs +50 -0
- package/dist/manifest-D7YZM_2e.mjs +194 -0
- package/dist/manifest-DE_VrAeQ.mjs +314 -0
- package/dist/manifest-DGsXSCpT.mjs +39 -0
- package/dist/manifest-DJ2vfEuW.mjs +156 -0
- package/dist/manifest-DPXDYhEu.mjs +80 -0
- package/dist/manifest-Dd4fQb0a.mjs +322 -0
- package/dist/manifest-Deq6opGg.mjs +223 -0
- package/dist/manifest-DfJTafJK.mjs +37 -0
- package/dist/manifest-DgOdgN_j.mjs +50 -0
- package/dist/manifest-DlbMW4v4.mjs +47 -0
- package/dist/manifest-DmVfbH0w.mjs +374 -0
- package/dist/manifest-Dog6Ddjr.mjs +109 -0
- package/dist/manifest-DvgU5FWb.mjs +58 -0
- package/dist/manifest-HsfDBs7j.mjs +50 -0
- package/dist/manifest-I8oQHvCG.mjs +186 -0
- package/dist/manifest-NvH_a-av.mjs +786 -0
- package/dist/manifest-cEJU1v0Z.mjs +129 -0
- package/dist/manifest-wOl5XLB12.mjs +112 -0
- package/dist/modules-tZozf0LQ.mjs +10635 -0
- package/dist/mojo-ipc-DXNEXEqb.mjs +640 -0
- package/dist/network-CPVvwvFg.mjs +3852 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-um7lCRY3.mjs} +219 -216
- package/dist/parse-args-B4cY5Vx5.mjs +39 -0
- package/dist/platform-CYeFoTWp.mjs +2161 -0
- package/dist/process-BTbgcVc6.mjs +1306 -0
- package/dist/proxy-r8YN6nP1.mjs +192 -0
- package/dist/registry-Bl8ZQW61.mjs +34 -0
- package/dist/response-CWhh2aLo.mjs +34 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-BoZnSoj-.mjs +586 -0
- package/dist/sourcemap-BIDHUVXy.mjs +934 -0
- package/dist/ssrf-policy-Dsqd-DTX.mjs +166 -0
- package/dist/streaming-Dal6utPp.mjs +725 -0
- package/dist/tool-builder-BHJp32mV.mjs +186 -0
- package/dist/transform-DRVgGG90.mjs +1011 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-BYx5UOeG.mjs +1044 -0
- package/dist/webcrack-Be0_FccV.mjs +747 -0
- package/dist/workflow-BpuKEtvn.mjs +725 -0
- package/package.json +82 -49
- package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
- package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-jJFy20G-.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-BWmPgQsv.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-K3V4vGsw.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-nnMvEohD.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-DDBWs9UP.mjs} +1 -1
|
@@ -0,0 +1,2537 @@
|
|
|
1
|
+
import { an as PROTO_HTTP_CONFIDENCE, cn as PROTO_TLS_MIN_RECORD_LEN, ln as PROTO_WS_CONFIDENCE, on as PROTO_SSH_CONFIDENCE, sn as PROTO_TLS_CONFIDENCE } from "./constants-CDZLOoVv.mjs";
|
|
2
|
+
import { n as asJsonResponse } from "./response-CWhh2aLo.mjs";
|
|
3
|
+
import { i as argObject, o as argStringArray, s as argStringRequired } from "./parse-args-B4cY5Vx5.mjs";
|
|
4
|
+
import { readFile as readFile$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
5
|
+
import { isIP } from "node:net";
|
|
6
|
+
//#region src/server/domains/protocol-analysis/handlers/fingerprint-utils.ts
|
|
7
|
+
const TLS_RECORD_TYPES = {
|
|
8
|
+
20: "ChangeCipherSpec",
|
|
9
|
+
21: "Alert",
|
|
10
|
+
22: "Handshake",
|
|
11
|
+
23: "ApplicationData"
|
|
12
|
+
};
|
|
13
|
+
const TLS_VERSIONS = {
|
|
14
|
+
"0300": "SSL 3.0",
|
|
15
|
+
"0301": "TLS 1.0",
|
|
16
|
+
"0302": "TLS 1.1",
|
|
17
|
+
"0303": "TLS 1.2",
|
|
18
|
+
"0304": "TLS 1.3"
|
|
19
|
+
};
|
|
20
|
+
const TLS_CIPHER_NAMES = {
|
|
21
|
+
"1301": "TLS_AES_128_GCM_SHA256",
|
|
22
|
+
"1302": "TLS_AES_256_GCM_SHA384",
|
|
23
|
+
"1303": "TLS_CHACHA20_POLY1305_SHA256",
|
|
24
|
+
c02b: "TLS_ECDHE_ECDSA_AES_128_GCM_SHA256",
|
|
25
|
+
c02f: "TLS_ECDHE_RSA_AES_128_GCM_SHA256",
|
|
26
|
+
c02c: "TLS_ECDHE_ECDSA_AES_256_GCM_SHA384",
|
|
27
|
+
c030: "TLS_ECDHE_RSA_AES_256_GCM_SHA384",
|
|
28
|
+
cca9: "TLS_ECDHE_ECDSA_CHACHA20_POLY1305",
|
|
29
|
+
cca8: "TLS_ECDHE_RSA_CHACHA20_POLY1305",
|
|
30
|
+
"009c": "TLS_RSA_AES_128_GCM_SHA256",
|
|
31
|
+
"009d": "TLS_RSA_AES_256_GCM_SHA384",
|
|
32
|
+
"002f": "TLS_RSA_AES_128_CBC_SHA",
|
|
33
|
+
"0035": "TLS_RSA_AES_256_CBC_SHA",
|
|
34
|
+
c013: "TLS_ECDHE_RSA_AES_128_CBC_SHA",
|
|
35
|
+
c014: "TLS_ECDHE_RSA_AES_256_CBC_SHA",
|
|
36
|
+
"00ff": "TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
|
|
37
|
+
"5600": "TLS_FALLBACK_SCSV"
|
|
38
|
+
};
|
|
39
|
+
const DNS_RCODES = {
|
|
40
|
+
0: "NOERROR",
|
|
41
|
+
1: "FORMERR",
|
|
42
|
+
2: "SERVFAIL",
|
|
43
|
+
3: "NXDOMAIN",
|
|
44
|
+
4: "NOTIMP",
|
|
45
|
+
5: "REFUSED"
|
|
46
|
+
};
|
|
47
|
+
const DNS_OPTYPES = {
|
|
48
|
+
0: "QUERY",
|
|
49
|
+
1: "IQUERY",
|
|
50
|
+
2: "STATUS",
|
|
51
|
+
3: "UNASSIGNED",
|
|
52
|
+
4: "NOTIFY",
|
|
53
|
+
5: "UPDATE"
|
|
54
|
+
};
|
|
55
|
+
const HTTP_METHODS = {
|
|
56
|
+
"474554": "GET",
|
|
57
|
+
"504f5354": "POST",
|
|
58
|
+
"505554": "PUT",
|
|
59
|
+
"44454c45": "DELETE",
|
|
60
|
+
"48454144": "HEAD",
|
|
61
|
+
"50415443": "PATCH",
|
|
62
|
+
"4f505449": "OPTIONS",
|
|
63
|
+
"434f4e4e": "CONNECT"
|
|
64
|
+
};
|
|
65
|
+
const TLS_EXTENSION_NAMES = {
|
|
66
|
+
"0000": "server_name",
|
|
67
|
+
"000a": "supported_groups",
|
|
68
|
+
"000b": "ec_point_formats",
|
|
69
|
+
"000d": "signature_algorithms",
|
|
70
|
+
"0010": "application_layer_protocol_negotiation",
|
|
71
|
+
"0015": "padding",
|
|
72
|
+
"0017": "extended_master_secret",
|
|
73
|
+
"001b": "compress_certificate",
|
|
74
|
+
"0023": "session_ticket",
|
|
75
|
+
"0029": "pre_shared_key",
|
|
76
|
+
"002b": "supported_versions",
|
|
77
|
+
"002d": "psk_key_exchange_modes",
|
|
78
|
+
"0033": "key_share",
|
|
79
|
+
"0039": "quic_transport_parameters",
|
|
80
|
+
"4469": "next_protocol_negotiation",
|
|
81
|
+
fe0d: "encrypted_client_hello",
|
|
82
|
+
ff01: "renegotiation_info"
|
|
83
|
+
};
|
|
84
|
+
const WS_OPCODES = {
|
|
85
|
+
0: "continuation",
|
|
86
|
+
1: "text",
|
|
87
|
+
2: "binary",
|
|
88
|
+
8: "close",
|
|
89
|
+
9: "ping",
|
|
90
|
+
10: "pong"
|
|
91
|
+
};
|
|
92
|
+
function readU8(hex, offset) {
|
|
93
|
+
return Number.parseInt(hex.substring(offset * 2, offset * 2 + 2), 16);
|
|
94
|
+
}
|
|
95
|
+
function readU16(hex, offset) {
|
|
96
|
+
return Number.parseInt(hex.substring(offset * 2, offset * 2 + 4), 16);
|
|
97
|
+
}
|
|
98
|
+
function hexSlice(hex, offset, len) {
|
|
99
|
+
return hex.substring(offset * 2, (offset + len) * 2);
|
|
100
|
+
}
|
|
101
|
+
function isZeroedDnsHeader(hex) {
|
|
102
|
+
return hex.length >= 24 && /^0{24}$/i.test(hex.slice(0, 24));
|
|
103
|
+
}
|
|
104
|
+
function parseTlsClientHello(hex) {
|
|
105
|
+
if (hex.length < 44) return null;
|
|
106
|
+
const recordType = readU8(hex, 0);
|
|
107
|
+
if (recordType !== 22) return null;
|
|
108
|
+
const recordVersion = hexSlice(hex, 1, 2);
|
|
109
|
+
const recordLen = readU16(hex, 3);
|
|
110
|
+
if (hex.length / 2 < 5 + recordLen) return null;
|
|
111
|
+
if (readU8(hex, 5) !== 1) return null;
|
|
112
|
+
const result = {
|
|
113
|
+
recordType: TLS_RECORD_TYPES[recordType] ?? `0x${recordType.toString(16)}`,
|
|
114
|
+
recordVersion: TLS_VERSIONS[recordVersion] ?? recordVersion,
|
|
115
|
+
recordLength: recordLen,
|
|
116
|
+
handshakeType: "ClientHello"
|
|
117
|
+
};
|
|
118
|
+
let pos = 9;
|
|
119
|
+
if (pos + 2 > hex.length / 2) return result;
|
|
120
|
+
const clientVersion = hexSlice(hex, pos, 2);
|
|
121
|
+
result.clientVersion = TLS_VERSIONS[clientVersion] ?? clientVersion;
|
|
122
|
+
pos += 34;
|
|
123
|
+
if (pos >= hex.length / 2) return result;
|
|
124
|
+
const sessionIdLen = readU8(hex, pos);
|
|
125
|
+
pos += 1 + sessionIdLen;
|
|
126
|
+
if (pos + 2 > hex.length / 2) return result;
|
|
127
|
+
const cipherLen = readU16(hex, pos);
|
|
128
|
+
pos += 2;
|
|
129
|
+
const ciphers = [];
|
|
130
|
+
for (let i = 0; i < cipherLen / 2 && pos + 2 <= hex.length / 2; i++) {
|
|
131
|
+
const cipherHex = hexSlice(hex, pos, 2).toLowerCase();
|
|
132
|
+
ciphers.push({
|
|
133
|
+
hex: cipherHex,
|
|
134
|
+
name: TLS_CIPHER_NAMES[cipherHex] ?? `Unknown(0x${cipherHex})`
|
|
135
|
+
});
|
|
136
|
+
pos += 2;
|
|
137
|
+
}
|
|
138
|
+
result.cipherSuites = ciphers;
|
|
139
|
+
result.cipherSuiteCount = ciphers.length;
|
|
140
|
+
if (pos >= hex.length / 2) return result;
|
|
141
|
+
const compLen = readU8(hex, pos);
|
|
142
|
+
pos += 1 + compLen;
|
|
143
|
+
if (pos + 2 > hex.length / 2) return result;
|
|
144
|
+
const extTotalLen = readU16(hex, pos);
|
|
145
|
+
pos += 2;
|
|
146
|
+
const extEnd = pos + extTotalLen;
|
|
147
|
+
const extensions = [];
|
|
148
|
+
while (pos + 4 <= extEnd && pos + 4 <= hex.length / 2) {
|
|
149
|
+
const extType = hexSlice(hex, pos, 2).toLowerCase();
|
|
150
|
+
const extLen = readU16(hex, pos + 2);
|
|
151
|
+
extensions.push({
|
|
152
|
+
type: extType,
|
|
153
|
+
length: extLen,
|
|
154
|
+
name: TLS_EXTENSION_NAMES[extType]
|
|
155
|
+
});
|
|
156
|
+
pos += 4 + extLen;
|
|
157
|
+
}
|
|
158
|
+
result.extensions = extensions;
|
|
159
|
+
result.extensionCount = extensions.length;
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function parseDnsHeader(hex) {
|
|
163
|
+
if (hex.length < 24) return null;
|
|
164
|
+
const txId = readU16(hex, 0);
|
|
165
|
+
const flags1 = readU8(hex, 2);
|
|
166
|
+
const flags2 = readU8(hex, 3);
|
|
167
|
+
const qr = flags1 >> 7 & 1;
|
|
168
|
+
const opcode = flags1 >> 3 & 15;
|
|
169
|
+
const aa = flags1 >> 2 & 1;
|
|
170
|
+
const tc = flags1 >> 1 & 1;
|
|
171
|
+
const rd = flags1 & 1;
|
|
172
|
+
const ra = flags2 >> 7 & 1;
|
|
173
|
+
const z = flags2 >> 4 & 7;
|
|
174
|
+
const rcode = flags2 & 15;
|
|
175
|
+
return {
|
|
176
|
+
transactionId: `0x${txId.toString(16).padStart(4, "0")}`,
|
|
177
|
+
flags: {
|
|
178
|
+
qr: qr === 1 ? "Response" : "Query",
|
|
179
|
+
opcode: DNS_OPTYPES[opcode] ?? opcode,
|
|
180
|
+
authoritativeAnswer: !!aa,
|
|
181
|
+
truncation: !!tc,
|
|
182
|
+
recursionDesired: !!rd,
|
|
183
|
+
recursionAvailable: !!ra,
|
|
184
|
+
reserved: z,
|
|
185
|
+
responseCode: DNS_RCODES[rcode] ?? rcode
|
|
186
|
+
},
|
|
187
|
+
questionCount: readU16(hex, 4),
|
|
188
|
+
answerCount: readU16(hex, 6),
|
|
189
|
+
authorityCount: readU16(hex, 8),
|
|
190
|
+
additionalCount: readU16(hex, 10)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function isLikelyDnsHeader(hex) {
|
|
194
|
+
if (hex.length < 24 || isZeroedDnsHeader(hex)) return false;
|
|
195
|
+
const flags1 = readU8(hex, 2);
|
|
196
|
+
const flags2 = readU8(hex, 3);
|
|
197
|
+
const qr = flags1 >> 7 & 1;
|
|
198
|
+
const opcode = flags1 >> 3 & 15;
|
|
199
|
+
const rcode = flags2 & 15;
|
|
200
|
+
const qdcount = readU16(hex, 4);
|
|
201
|
+
const ancount = readU16(hex, 6);
|
|
202
|
+
if (opcode > 2) return false;
|
|
203
|
+
if (qdcount + ancount === 0) return false;
|
|
204
|
+
if (qr === 0 && rcode !== 0) return false;
|
|
205
|
+
if (qr === 1 && rcode > 5) return false;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/protocol-schema.ts
|
|
210
|
+
function isRecord$1(value) {
|
|
211
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
212
|
+
}
|
|
213
|
+
function parseFieldSpec(value, index) {
|
|
214
|
+
if (!isRecord$1(value)) throw new Error(`fields[${index}] must be an object`);
|
|
215
|
+
const name = value.name;
|
|
216
|
+
const offset = value.offset;
|
|
217
|
+
const length = value.length;
|
|
218
|
+
const type = value.type;
|
|
219
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
220
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) throw new Error(`fields[${index}].offset must be a non-negative integer`);
|
|
221
|
+
if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) throw new Error(`fields[${index}].length must be a positive integer`);
|
|
222
|
+
if (type !== "int" && type !== "string" && type !== "bytes" && type !== "bool" && type !== "float") throw new Error(`fields[${index}].type is invalid`);
|
|
223
|
+
return {
|
|
224
|
+
name,
|
|
225
|
+
offset,
|
|
226
|
+
length,
|
|
227
|
+
type
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function parseLegacyField(value, index) {
|
|
231
|
+
if (!isRecord$1(value)) throw new Error(`fields[${index}] must be an object`);
|
|
232
|
+
const name = value.name;
|
|
233
|
+
const offset = value.offset;
|
|
234
|
+
const length = value.length;
|
|
235
|
+
const type = value.type;
|
|
236
|
+
const description = value.description;
|
|
237
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
238
|
+
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) throw new Error(`fields[${index}].offset must be a non-negative integer`);
|
|
239
|
+
if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) throw new Error(`fields[${index}].length must be a positive integer`);
|
|
240
|
+
if (type !== "uint8" && type !== "uint16" && type !== "uint32" && type !== "int64" && type !== "float" && type !== "string" && type !== "bytes") throw new Error(`fields[${index}].type is invalid`);
|
|
241
|
+
return {
|
|
242
|
+
name,
|
|
243
|
+
offset,
|
|
244
|
+
length,
|
|
245
|
+
type,
|
|
246
|
+
...typeof description === "string" ? { description } : {}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function parsePatternSpec(name, value) {
|
|
250
|
+
const rawFields = value.fields;
|
|
251
|
+
if (!Array.isArray(rawFields)) throw new Error("spec.fields must be an array");
|
|
252
|
+
const fieldDelimiter = typeof value.fieldDelimiter === "string" && value.fieldDelimiter.length > 0 ? value.fieldDelimiter : void 0;
|
|
253
|
+
const byteOrderValue = value.byteOrder;
|
|
254
|
+
const byteOrder = byteOrderValue === "le" || byteOrderValue === "be" ? byteOrderValue : void 0;
|
|
255
|
+
return {
|
|
256
|
+
name,
|
|
257
|
+
...fieldDelimiter ? { fieldDelimiter } : {},
|
|
258
|
+
...byteOrder ? { byteOrder } : {},
|
|
259
|
+
fields: rawFields.map((field, index) => parseFieldSpec(field, index))
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function parseEncryptionInfo(value) {
|
|
263
|
+
if (!isRecord$1(value)) return;
|
|
264
|
+
const type = value.type;
|
|
265
|
+
if (type !== "aes" && type !== "xor" && type !== "rc4" && type !== "custom") return;
|
|
266
|
+
const key = typeof value.key === "string" ? value.key : void 0;
|
|
267
|
+
const iv = typeof value.iv === "string" ? value.iv : void 0;
|
|
268
|
+
const notes = typeof value.notes === "string" ? value.notes : void 0;
|
|
269
|
+
return {
|
|
270
|
+
type,
|
|
271
|
+
...key ? { key } : {},
|
|
272
|
+
...iv ? { iv } : {},
|
|
273
|
+
...notes ? { notes } : {}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function parseProtocolMessage(value, index) {
|
|
277
|
+
if (!isRecord$1(value)) throw new Error(`messages[${index}] must be an object`);
|
|
278
|
+
const direction = value.direction;
|
|
279
|
+
const timestamp = value.timestamp;
|
|
280
|
+
const fields = value.fields;
|
|
281
|
+
const raw = value.raw;
|
|
282
|
+
if (direction !== "req" && direction !== "res") throw new Error(`messages[${index}].direction must be "req" or "res"`);
|
|
283
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) throw new Error(`messages[${index}].timestamp must be a number`);
|
|
284
|
+
if (!isRecord$1(fields)) throw new Error(`messages[${index}].fields must be an object`);
|
|
285
|
+
if (typeof raw !== "string") throw new Error(`messages[${index}].raw must be a string`);
|
|
286
|
+
return {
|
|
287
|
+
direction,
|
|
288
|
+
timestamp,
|
|
289
|
+
fields,
|
|
290
|
+
raw
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/payload/core.ts
|
|
295
|
+
const TEXT_ENCODINGS = ["utf8", "ascii"];
|
|
296
|
+
const BINARY_ENCODINGS = [
|
|
297
|
+
"utf8",
|
|
298
|
+
"ascii",
|
|
299
|
+
"hex",
|
|
300
|
+
"base64"
|
|
301
|
+
];
|
|
302
|
+
const PAYLOAD_FIELD_TYPES = [
|
|
303
|
+
"u8",
|
|
304
|
+
"u16",
|
|
305
|
+
"u32",
|
|
306
|
+
"i8",
|
|
307
|
+
"i16",
|
|
308
|
+
"i32",
|
|
309
|
+
"string",
|
|
310
|
+
"bytes"
|
|
311
|
+
];
|
|
312
|
+
const MUTATION_STRATEGIES = [
|
|
313
|
+
"set_byte",
|
|
314
|
+
"flip_bit",
|
|
315
|
+
"overwrite_bytes",
|
|
316
|
+
"append_bytes",
|
|
317
|
+
"truncate",
|
|
318
|
+
"increment_integer"
|
|
319
|
+
];
|
|
320
|
+
function parseEndian(value, fallback = "big") {
|
|
321
|
+
return value === "little" ? "little" : fallback;
|
|
322
|
+
}
|
|
323
|
+
function parseNonNegativeInteger(value, label) {
|
|
324
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`${label} must be a non-negative integer`);
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
function parsePositiveInteger(value, label) {
|
|
328
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`);
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function parseInteger(value, label) {
|
|
332
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer`);
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
function parseByte(value, label) {
|
|
336
|
+
const parsed = parseInteger(value, label);
|
|
337
|
+
if (parsed < 0 || parsed > 255) throw new Error(`${label} must be between 0 and 255`);
|
|
338
|
+
return parsed;
|
|
339
|
+
}
|
|
340
|
+
function parseOptionalLength(value, label) {
|
|
341
|
+
return value === void 0 ? void 0 : parsePositiveInteger(value, label);
|
|
342
|
+
}
|
|
343
|
+
function parseEncoding(value, allowed, fallback, label) {
|
|
344
|
+
if (value === void 0) return fallback;
|
|
345
|
+
if (typeof value !== "string" || !allowed.includes(value)) throw new Error(`${label} is invalid`);
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
function expectString(value, label) {
|
|
349
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
function normalizeHexString(value, label) {
|
|
353
|
+
const normalized = value.replace(/^0x/i, "").replace(/\s+/g, "");
|
|
354
|
+
if (normalized.length === 0) return normalized;
|
|
355
|
+
if (normalized.length % 2 !== 0 || /[^0-9a-f]/i.test(normalized)) throw new Error(`${label} must be a valid even-length hex string`);
|
|
356
|
+
return normalized.toLowerCase();
|
|
357
|
+
}
|
|
358
|
+
function decodeBinaryValue(value, encoding, label) {
|
|
359
|
+
switch (encoding) {
|
|
360
|
+
case "utf8":
|
|
361
|
+
case "ascii": return Buffer.from(value, encoding);
|
|
362
|
+
case "hex": return Buffer.from(normalizeHexString(value, label), "hex");
|
|
363
|
+
case "base64": return Buffer.from(value, "base64");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function getNumericRange(width, signed) {
|
|
367
|
+
const bits = width * 8;
|
|
368
|
+
if (signed) return {
|
|
369
|
+
min: -(2 ** (bits - 1)),
|
|
370
|
+
max: 2 ** (bits - 1) - 1
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
min: 0,
|
|
374
|
+
max: 2 ** bits - 1
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function getFieldNumericMetadata(type) {
|
|
378
|
+
switch (type) {
|
|
379
|
+
case "u8": return {
|
|
380
|
+
width: 1,
|
|
381
|
+
signed: false
|
|
382
|
+
};
|
|
383
|
+
case "u16": return {
|
|
384
|
+
width: 2,
|
|
385
|
+
signed: false
|
|
386
|
+
};
|
|
387
|
+
case "u32": return {
|
|
388
|
+
width: 4,
|
|
389
|
+
signed: false
|
|
390
|
+
};
|
|
391
|
+
case "i8": return {
|
|
392
|
+
width: 1,
|
|
393
|
+
signed: true
|
|
394
|
+
};
|
|
395
|
+
case "i16": return {
|
|
396
|
+
width: 2,
|
|
397
|
+
signed: true
|
|
398
|
+
};
|
|
399
|
+
case "i32": return {
|
|
400
|
+
width: 4,
|
|
401
|
+
signed: true
|
|
402
|
+
};
|
|
403
|
+
default: return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function writeIntegerToBuffer(buffer, value, width, signed, endian) {
|
|
407
|
+
if (signed) switch (width) {
|
|
408
|
+
case 1:
|
|
409
|
+
buffer.writeInt8(value, 0);
|
|
410
|
+
return;
|
|
411
|
+
case 2:
|
|
412
|
+
if (endian === "little") buffer.writeInt16LE(value, 0);
|
|
413
|
+
else buffer.writeInt16BE(value, 0);
|
|
414
|
+
return;
|
|
415
|
+
case 4:
|
|
416
|
+
if (endian === "little") buffer.writeInt32LE(value, 0);
|
|
417
|
+
else buffer.writeInt32BE(value, 0);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
switch (width) {
|
|
421
|
+
case 1:
|
|
422
|
+
buffer.writeUInt8(value, 0);
|
|
423
|
+
return;
|
|
424
|
+
case 2:
|
|
425
|
+
if (endian === "little") buffer.writeUInt16LE(value, 0);
|
|
426
|
+
else buffer.writeUInt16BE(value, 0);
|
|
427
|
+
return;
|
|
428
|
+
case 4:
|
|
429
|
+
if (endian === "little") buffer.writeUInt32LE(value, 0);
|
|
430
|
+
else buffer.writeUInt32BE(value, 0);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function readIntegerFromBuffer(buffer, offset, width, signed, endian) {
|
|
435
|
+
if (signed) switch (width) {
|
|
436
|
+
case 1: return buffer.readInt8(offset);
|
|
437
|
+
case 2: return endian === "little" ? buffer.readInt16LE(offset) : buffer.readInt16BE(offset);
|
|
438
|
+
case 4: return endian === "little" ? buffer.readInt32LE(offset) : buffer.readInt32BE(offset);
|
|
439
|
+
}
|
|
440
|
+
switch (width) {
|
|
441
|
+
case 1: return buffer.readUInt8(offset);
|
|
442
|
+
case 2: return endian === "little" ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
|
|
443
|
+
case 4: return endian === "little" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function applyFixedLength(encoded, length, padByte) {
|
|
447
|
+
if (length === void 0 || encoded.length === length) return encoded;
|
|
448
|
+
if (encoded.length > length) return encoded.subarray(0, length);
|
|
449
|
+
return Buffer.concat([encoded, Buffer.alloc(length - encoded.length, padByte)]);
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/payload/template.ts
|
|
453
|
+
function parsePayloadTemplateField(value, index) {
|
|
454
|
+
if (!isRecord$1(value)) throw new Error(`fields[${index}] must be an object`);
|
|
455
|
+
const name = value.name;
|
|
456
|
+
const type = value.type;
|
|
457
|
+
const rawValue = value.value;
|
|
458
|
+
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
459
|
+
if (typeof type !== "string" || !PAYLOAD_FIELD_TYPES.includes(type)) throw new Error(`fields[${index}].type is invalid`);
|
|
460
|
+
const fieldType = type;
|
|
461
|
+
const numericMetadata = getFieldNumericMetadata(fieldType);
|
|
462
|
+
if (numericMetadata) {
|
|
463
|
+
const numericValue = parseInteger(rawValue, `fields[${index}].value`);
|
|
464
|
+
const range = getNumericRange(numericMetadata.width, numericMetadata.signed);
|
|
465
|
+
if (numericValue < range.min || numericValue > range.max) throw new Error(`fields[${index}].value is out of range for ${type} (${range.min}..${range.max})`);
|
|
466
|
+
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`);
|
|
467
|
+
return {
|
|
468
|
+
name,
|
|
469
|
+
type: fieldType,
|
|
470
|
+
value: numericValue
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const stringValue = expectString(rawValue, `fields[${index}].value`);
|
|
474
|
+
const length = parseOptionalLength(value.length, `fields[${index}].length`);
|
|
475
|
+
const padByte = value.padByte === void 0 ? 0 : parseByte(value.padByte, `fields[${index}].padByte`);
|
|
476
|
+
if (type === "string") return {
|
|
477
|
+
name,
|
|
478
|
+
type: "string",
|
|
479
|
+
value: stringValue,
|
|
480
|
+
encoding: parseEncoding(value.encoding, TEXT_ENCODINGS, "utf8", `fields[${index}].encoding`),
|
|
481
|
+
...length !== void 0 ? { length } : {},
|
|
482
|
+
padByte
|
|
483
|
+
};
|
|
484
|
+
return {
|
|
485
|
+
name,
|
|
486
|
+
type: "bytes",
|
|
487
|
+
value: stringValue,
|
|
488
|
+
encoding: parseEncoding(value.encoding, BINARY_ENCODINGS, "hex", `fields[${index}].encoding`),
|
|
489
|
+
...length !== void 0 ? { length } : {},
|
|
490
|
+
padByte
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function encodePayloadTemplateField(field, endian) {
|
|
494
|
+
switch (field.type) {
|
|
495
|
+
case "u8":
|
|
496
|
+
case "u16":
|
|
497
|
+
case "u32":
|
|
498
|
+
case "i8":
|
|
499
|
+
case "i16":
|
|
500
|
+
case "i32": {
|
|
501
|
+
const numericMetadata = getFieldNumericMetadata(field.type);
|
|
502
|
+
if (!numericMetadata) throw new Error(`Unsupported numeric field type: ${field.type}`);
|
|
503
|
+
const buffer = Buffer.alloc(numericMetadata.width);
|
|
504
|
+
writeIntegerToBuffer(buffer, field.value, numericMetadata.width, numericMetadata.signed, endian);
|
|
505
|
+
return buffer;
|
|
506
|
+
}
|
|
507
|
+
case "string": return applyFixedLength(Buffer.from(field.value, field.encoding), field.length, field.padByte);
|
|
508
|
+
case "bytes": return applyFixedLength(decodeBinaryValue(field.value, field.encoding, `field ${field.name}`), field.length, field.padByte);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function buildPayloadFromTemplate(fields, endian) {
|
|
512
|
+
const buffers = [];
|
|
513
|
+
const segments = [];
|
|
514
|
+
let offset = 0;
|
|
515
|
+
for (const field of fields) {
|
|
516
|
+
const encoded = encodePayloadTemplateField(field, endian);
|
|
517
|
+
buffers.push(encoded);
|
|
518
|
+
segments.push({
|
|
519
|
+
name: field.name,
|
|
520
|
+
offset,
|
|
521
|
+
length: encoded.length,
|
|
522
|
+
hex: encoded.toString("hex")
|
|
523
|
+
});
|
|
524
|
+
offset += encoded.length;
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
payload: Buffer.concat(buffers),
|
|
528
|
+
segments
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/payload/mutation.ts
|
|
533
|
+
function parsePayloadMutation(value, index) {
|
|
534
|
+
if (!isRecord$1(value)) throw new Error(`mutations[${index}] must be an object`);
|
|
535
|
+
const strategy = value.strategy;
|
|
536
|
+
if (typeof strategy !== "string" || !MUTATION_STRATEGIES.includes(strategy)) throw new Error(`mutations[${index}].strategy is invalid`);
|
|
537
|
+
switch (strategy) {
|
|
538
|
+
case "set_byte": return {
|
|
539
|
+
strategy: "set_byte",
|
|
540
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
541
|
+
value: parseByte(value.value, `mutations[${index}].value`)
|
|
542
|
+
};
|
|
543
|
+
case "flip_bit": return {
|
|
544
|
+
strategy: "flip_bit",
|
|
545
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
546
|
+
bit: (() => {
|
|
547
|
+
const bit = parseInteger(value.bit, `mutations[${index}].bit`);
|
|
548
|
+
if (bit < 0 || bit > 7) throw new Error(`mutations[${index}].bit must be between 0 and 7`);
|
|
549
|
+
return bit;
|
|
550
|
+
})()
|
|
551
|
+
};
|
|
552
|
+
case "overwrite_bytes": return {
|
|
553
|
+
strategy: "overwrite_bytes",
|
|
554
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
555
|
+
data: decodeBinaryValue(expectString(value.data, `mutations[${index}].data`), parseEncoding(value.encoding, BINARY_ENCODINGS, "hex", `mutations[${index}].encoding`), `mutations[${index}].data`)
|
|
556
|
+
};
|
|
557
|
+
case "append_bytes": return {
|
|
558
|
+
strategy: "append_bytes",
|
|
559
|
+
data: decodeBinaryValue(expectString(value.data, `mutations[${index}].data`), parseEncoding(value.encoding, BINARY_ENCODINGS, "hex", `mutations[${index}].encoding`), `mutations[${index}].data`)
|
|
560
|
+
};
|
|
561
|
+
case "truncate": return {
|
|
562
|
+
strategy: "truncate",
|
|
563
|
+
length: parseNonNegativeInteger(value.length, `mutations[${index}].length`)
|
|
564
|
+
};
|
|
565
|
+
case "increment_integer": {
|
|
566
|
+
const width = value.width;
|
|
567
|
+
if (width !== 1 && width !== 2 && width !== 4) throw new Error(`mutations[${index}].width must be 1, 2, or 4`);
|
|
568
|
+
return {
|
|
569
|
+
strategy: "increment_integer",
|
|
570
|
+
offset: parseNonNegativeInteger(value.offset, `mutations[${index}].offset`),
|
|
571
|
+
width,
|
|
572
|
+
delta: parseInteger(value.delta, `mutations[${index}].delta`),
|
|
573
|
+
endian: parseEndian(value.endian),
|
|
574
|
+
signed: value.signed === true
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function applyPayloadMutation(payload, mutation, index) {
|
|
580
|
+
const working = Buffer.from(payload);
|
|
581
|
+
switch (mutation.strategy) {
|
|
582
|
+
case "set_byte":
|
|
583
|
+
if (mutation.offset >= working.length) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
584
|
+
working[mutation.offset] = mutation.value;
|
|
585
|
+
return {
|
|
586
|
+
payload: working,
|
|
587
|
+
summary: {
|
|
588
|
+
index,
|
|
589
|
+
strategy: mutation.strategy,
|
|
590
|
+
detail: `set payload[${mutation.offset}] to ${mutation.value}`
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
case "flip_bit":
|
|
594
|
+
if (mutation.offset >= working.length) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
595
|
+
{
|
|
596
|
+
const currentByte = working[mutation.offset];
|
|
597
|
+
working[mutation.offset] = currentByte ^ 1 << mutation.bit;
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
payload: working,
|
|
601
|
+
summary: {
|
|
602
|
+
index,
|
|
603
|
+
strategy: mutation.strategy,
|
|
604
|
+
detail: `flipped bit ${mutation.bit} at offset ${mutation.offset}`
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
case "overwrite_bytes":
|
|
608
|
+
if (mutation.offset + mutation.data.length > working.length) throw new Error(`mutations[${index}] overwrite exceeds payload length`);
|
|
609
|
+
mutation.data.copy(working, mutation.offset);
|
|
610
|
+
return {
|
|
611
|
+
payload: working,
|
|
612
|
+
summary: {
|
|
613
|
+
index,
|
|
614
|
+
strategy: mutation.strategy,
|
|
615
|
+
detail: `overwrote ${mutation.data.length} bytes at offset ${mutation.offset}`
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
case "append_bytes": return {
|
|
619
|
+
payload: Buffer.concat([working, mutation.data]),
|
|
620
|
+
summary: {
|
|
621
|
+
index,
|
|
622
|
+
strategy: mutation.strategy,
|
|
623
|
+
detail: `appended ${mutation.data.length} bytes`
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
case "truncate":
|
|
627
|
+
if (mutation.length > working.length) throw new Error(`mutations[${index}] length exceeds payload size`);
|
|
628
|
+
return {
|
|
629
|
+
payload: working.subarray(0, mutation.length),
|
|
630
|
+
summary: {
|
|
631
|
+
index,
|
|
632
|
+
strategy: mutation.strategy,
|
|
633
|
+
detail: `truncated payload to ${mutation.length} bytes`
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
case "increment_integer": {
|
|
637
|
+
if (mutation.offset + mutation.width > working.length) throw new Error(`mutations[${index}] integer range exceeds payload length`);
|
|
638
|
+
const next = readIntegerFromBuffer(working, mutation.offset, mutation.width, mutation.signed, mutation.endian) + mutation.delta;
|
|
639
|
+
const range = getNumericRange(mutation.width, mutation.signed);
|
|
640
|
+
if (next < range.min || next > range.max) throw new Error(`mutations[${index}] integer overflow (${range.min}..${range.max})`);
|
|
641
|
+
writeIntegerToBuffer(working.subarray(mutation.offset, mutation.offset + mutation.width), next, mutation.width, mutation.signed, mutation.endian);
|
|
642
|
+
return {
|
|
643
|
+
payload: working,
|
|
644
|
+
summary: {
|
|
645
|
+
index,
|
|
646
|
+
strategy: mutation.strategy,
|
|
647
|
+
detail: `incremented ${mutation.signed ? "signed" : "unsigned"} ${mutation.width}-byte integer at offset ${mutation.offset} by ${mutation.delta}`
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/network-packet/types.ts
|
|
655
|
+
const ETHER_TYPE_MAP = Object.freeze({
|
|
656
|
+
arp: 2054,
|
|
657
|
+
ipv4: 2048,
|
|
658
|
+
ipv6: 34525,
|
|
659
|
+
vlan: 33024
|
|
660
|
+
});
|
|
661
|
+
const IP_PROTOCOL_MAP = Object.freeze({
|
|
662
|
+
icmp: 1,
|
|
663
|
+
igmp: 2,
|
|
664
|
+
tcp: 6,
|
|
665
|
+
udp: 17,
|
|
666
|
+
gre: 47,
|
|
667
|
+
esp: 50,
|
|
668
|
+
ah: 51,
|
|
669
|
+
icmpv6: 58,
|
|
670
|
+
ospf: 89
|
|
671
|
+
});
|
|
672
|
+
const PCAP_LINK_TYPE_MAP = Object.freeze({
|
|
673
|
+
loopback: 0,
|
|
674
|
+
ethernet: 1,
|
|
675
|
+
raw: 101
|
|
676
|
+
});
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/network-packet/addressing.ts
|
|
679
|
+
function parseNamedOrNumericValue(value, label, map, max) {
|
|
680
|
+
if (typeof value === "number") {
|
|
681
|
+
if (!Number.isInteger(value) || value < 0 || value > max) throw new Error(`${label} must be an integer between 0 and ${max}`);
|
|
682
|
+
return value;
|
|
683
|
+
}
|
|
684
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label} must be a non-empty string or integer`);
|
|
685
|
+
const normalized = value.trim().toLowerCase();
|
|
686
|
+
const mapped = map[normalized];
|
|
687
|
+
if (mapped !== void 0) return mapped;
|
|
688
|
+
if (/^\d+$/.test(normalized)) {
|
|
689
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
690
|
+
if (parsed > max) throw new Error(`${label} must be less than or equal to ${max}`);
|
|
691
|
+
return parsed;
|
|
692
|
+
}
|
|
693
|
+
const hex = normalizeHexString(normalized, label);
|
|
694
|
+
const parsed = Number.parseInt(hex, 16);
|
|
695
|
+
if (parsed > max) throw new Error(`${label} must be less than or equal to ${max}`);
|
|
696
|
+
return parsed;
|
|
697
|
+
}
|
|
698
|
+
function parseMacAddress(value, label) {
|
|
699
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label} must be a non-empty MAC address string`);
|
|
700
|
+
const normalized = value.trim().toLowerCase().replace(/^0x/, "").replace(/[:\-.\s]/g, "");
|
|
701
|
+
if (!/^[0-9a-f]{12}$/i.test(normalized)) throw new Error(`${label} must be a valid 6-byte MAC address`);
|
|
702
|
+
const canonical = normalized.match(/.{2}/g)?.join(":");
|
|
703
|
+
if (!canonical) throw new Error(`${label} must be a valid 6-byte MAC address`);
|
|
704
|
+
return {
|
|
705
|
+
canonical,
|
|
706
|
+
bytes: Buffer.from(normalized, "hex")
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function parseIpv4Address(value, label) {
|
|
710
|
+
if (typeof value !== "string" || isIP(value.trim()) !== 4) throw new Error(`${label} must be a valid IPv4 address`);
|
|
711
|
+
const octets = value.trim().split(".").map((part) => Number.parseInt(part, 10));
|
|
712
|
+
return Buffer.from(octets);
|
|
713
|
+
}
|
|
714
|
+
function parseIpv6Groups(value, label) {
|
|
715
|
+
if (value.length === 0) return [];
|
|
716
|
+
return value.split(":").flatMap((part) => {
|
|
717
|
+
if (part.length === 0) return [];
|
|
718
|
+
if (part.includes(".")) {
|
|
719
|
+
const ipv4 = parseIpv4Address(part, label);
|
|
720
|
+
return [ipv4.readUInt16BE(0).toString(16), ipv4.readUInt16BE(2).toString(16)];
|
|
721
|
+
}
|
|
722
|
+
if (!/^[0-9a-f]{1,4}$/i.test(part)) throw new Error(`${label} contains an invalid IPv6 group`);
|
|
723
|
+
return [part];
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function parseIpv6Address(value, label) {
|
|
727
|
+
if (typeof value !== "string") throw new Error(`${label} must be a valid IPv6 address`);
|
|
728
|
+
const normalized = value.trim().toLowerCase().split("%")[0] ?? "";
|
|
729
|
+
if (isIP(normalized) !== 6) throw new Error(`${label} must be a valid IPv6 address`);
|
|
730
|
+
const segments = normalized.split("::");
|
|
731
|
+
if (segments.length > 2) throw new Error(`${label} must be a valid IPv6 address`);
|
|
732
|
+
const head = parseIpv6Groups(segments[0] ?? "", label);
|
|
733
|
+
const tail = parseIpv6Groups(segments[1] ?? "", label);
|
|
734
|
+
const groups = segments.length === 2 ? [
|
|
735
|
+
...head,
|
|
736
|
+
...Array.from({ length: 8 - head.length - tail.length }, () => "0"),
|
|
737
|
+
...tail
|
|
738
|
+
] : head;
|
|
739
|
+
if (groups.length !== 8) throw new Error(`${label} must expand to exactly 8 IPv6 groups`);
|
|
740
|
+
const output = Buffer.alloc(16);
|
|
741
|
+
for (const [index, group] of groups.entries()) output.writeUInt16BE(Number.parseInt(group, 16), index * 2);
|
|
742
|
+
return output;
|
|
743
|
+
}
|
|
744
|
+
function parseIpAddress(value, version, label) {
|
|
745
|
+
return version === "ipv4" ? parseIpv4Address(value, label) : parseIpv6Address(value, label);
|
|
746
|
+
}
|
|
747
|
+
function parseEtherType(value, label) {
|
|
748
|
+
return parseNamedOrNumericValue(value, label, ETHER_TYPE_MAP, 65535);
|
|
749
|
+
}
|
|
750
|
+
function parseIpProtocol(value, label) {
|
|
751
|
+
return parseNamedOrNumericValue(value, label, IP_PROTOCOL_MAP, 255);
|
|
752
|
+
}
|
|
753
|
+
function parsePcapLinkType(value, label) {
|
|
754
|
+
return parseNamedOrNumericValue(value, label, PCAP_LINK_TYPE_MAP, 4294967295);
|
|
755
|
+
}
|
|
756
|
+
function parseChecksumEndian(value) {
|
|
757
|
+
return value === "little" ? "little" : "big";
|
|
758
|
+
}
|
|
759
|
+
function parsePacketEndianness(value) {
|
|
760
|
+
return value === "big" ? "big" : "little";
|
|
761
|
+
}
|
|
762
|
+
function parseTimestampPrecision(value) {
|
|
763
|
+
return value === "nano" ? "nano" : "micro";
|
|
764
|
+
}
|
|
765
|
+
function parseHexPayload$1(value, label) {
|
|
766
|
+
if (typeof value !== "string") throw new Error(`${label} must be a hex string`);
|
|
767
|
+
return Buffer.from(normalizeHexString(value, label), "hex");
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/network-packet/packet-build.ts
|
|
771
|
+
function computeInternetChecksum(buffer) {
|
|
772
|
+
let sum = 0;
|
|
773
|
+
for (let offset = 0; offset < buffer.length; offset += 2) {
|
|
774
|
+
const high = buffer[offset] ?? 0;
|
|
775
|
+
const low = buffer[offset + 1] ?? 0;
|
|
776
|
+
sum += high << 8 | low;
|
|
777
|
+
while (sum > 65535) sum = (sum & 65535) + (sum >>> 16);
|
|
778
|
+
}
|
|
779
|
+
return ~sum & 65535;
|
|
780
|
+
}
|
|
781
|
+
function buildEthernetFrame(destinationMac, sourceMac, etherType, payload) {
|
|
782
|
+
const header = Buffer.alloc(14);
|
|
783
|
+
destinationMac.bytes.copy(header, 0);
|
|
784
|
+
sourceMac.bytes.copy(header, 6);
|
|
785
|
+
header.writeUInt16BE(etherType, 12);
|
|
786
|
+
return Buffer.concat([header, payload]);
|
|
787
|
+
}
|
|
788
|
+
function buildArpPayload(args) {
|
|
789
|
+
if (args.hardwareSize !== args.senderMac.bytes.length || args.hardwareSize !== args.targetMac.bytes.length) throw new Error("hardwareSize must match the provided MAC address lengths");
|
|
790
|
+
if (args.protocolSize !== args.senderIp.length || args.protocolSize !== args.targetIp.length) throw new Error("protocolSize must match the provided IP address lengths");
|
|
791
|
+
const buffer = Buffer.alloc(8 + args.hardwareSize * 2 + args.protocolSize * 2);
|
|
792
|
+
let offset = 0;
|
|
793
|
+
buffer.writeUInt16BE(args.hardwareType, offset);
|
|
794
|
+
offset += 2;
|
|
795
|
+
buffer.writeUInt16BE(args.protocolType, offset);
|
|
796
|
+
offset += 2;
|
|
797
|
+
buffer.writeUInt8(args.hardwareSize, offset++);
|
|
798
|
+
buffer.writeUInt8(args.protocolSize, offset++);
|
|
799
|
+
buffer.writeUInt16BE(args.operation === "reply" ? 2 : 1, offset);
|
|
800
|
+
offset += 2;
|
|
801
|
+
args.senderMac.bytes.copy(buffer, offset);
|
|
802
|
+
offset += args.hardwareSize;
|
|
803
|
+
args.senderIp.copy(buffer, offset);
|
|
804
|
+
offset += args.protocolSize;
|
|
805
|
+
args.targetMac.bytes.copy(buffer, offset);
|
|
806
|
+
offset += args.hardwareSize;
|
|
807
|
+
args.targetIp.copy(buffer, offset);
|
|
808
|
+
return buffer;
|
|
809
|
+
}
|
|
810
|
+
function buildIpv4Packet(args) {
|
|
811
|
+
const header = Buffer.alloc(20);
|
|
812
|
+
header[0] = 69;
|
|
813
|
+
header[1] = (args.dscp & 63) << 2 | args.ecn & 3;
|
|
814
|
+
header.writeUInt16BE(header.length + args.payload.length, 2);
|
|
815
|
+
header.writeUInt16BE(args.identification, 4);
|
|
816
|
+
const flags = (args.dontFragment ? 1 : 0) << 1 | (args.moreFragments ? 1 : 0);
|
|
817
|
+
header.writeUInt16BE((flags & 7) << 13 | args.fragmentOffset & 8191, 6);
|
|
818
|
+
header[8] = args.ttl;
|
|
819
|
+
header[9] = args.protocol;
|
|
820
|
+
header.writeUInt16BE(0, 10);
|
|
821
|
+
args.sourceIp.copy(header, 12);
|
|
822
|
+
args.destinationIp.copy(header, 16);
|
|
823
|
+
const checksum = computeInternetChecksum(header);
|
|
824
|
+
header.writeUInt16BE(checksum, 10);
|
|
825
|
+
return {
|
|
826
|
+
packet: Buffer.concat([header, args.payload]),
|
|
827
|
+
checksum
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function buildIpv6Packet(args) {
|
|
831
|
+
const header = Buffer.alloc(40);
|
|
832
|
+
const versionTrafficFlow = 6 << 28 | (((args.dscp & 63) << 2 | args.ecn & 3) & 255) << 20 | args.flowLabel & 1048575;
|
|
833
|
+
header.writeUInt32BE(versionTrafficFlow >>> 0, 0);
|
|
834
|
+
header.writeUInt16BE(args.payload.length, 4);
|
|
835
|
+
header.writeUInt8(args.protocol, 6);
|
|
836
|
+
header.writeUInt8(args.hopLimit, 7);
|
|
837
|
+
args.sourceIp.copy(header, 8);
|
|
838
|
+
args.destinationIp.copy(header, 24);
|
|
839
|
+
return Buffer.concat([header, args.payload]);
|
|
840
|
+
}
|
|
841
|
+
function buildIcmpEcho(args) {
|
|
842
|
+
const packet = Buffer.alloc(8 + args.payload.length);
|
|
843
|
+
packet[0] = args.operation === "reply" ? 0 : 8;
|
|
844
|
+
packet[1] = 0;
|
|
845
|
+
packet.writeUInt16BE(0, 2);
|
|
846
|
+
packet.writeUInt16BE(args.identifier, 4);
|
|
847
|
+
packet.writeUInt16BE(args.sequenceNumber, 6);
|
|
848
|
+
args.payload.copy(packet, 8);
|
|
849
|
+
const checksum = computeInternetChecksum(packet);
|
|
850
|
+
packet.writeUInt16BE(checksum, 2);
|
|
851
|
+
return {
|
|
852
|
+
packet,
|
|
853
|
+
checksum
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/server/domains/protocol-analysis/handlers/shared/network-packet/pcap.ts
|
|
858
|
+
function writeUint32(buffer, offset, value, endianness) {
|
|
859
|
+
if (endianness === "little") buffer.writeUInt32LE(value, offset);
|
|
860
|
+
else buffer.writeUInt32BE(value, offset);
|
|
861
|
+
}
|
|
862
|
+
function writeUint16(buffer, offset, value, endianness) {
|
|
863
|
+
if (endianness === "little") buffer.writeUInt16LE(value, offset);
|
|
864
|
+
else buffer.writeUInt16BE(value, offset);
|
|
865
|
+
}
|
|
866
|
+
function readUint32(buffer, offset, endianness) {
|
|
867
|
+
return endianness === "little" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
|
|
868
|
+
}
|
|
869
|
+
function readUint16(buffer, offset, endianness) {
|
|
870
|
+
return endianness === "little" ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
|
|
871
|
+
}
|
|
872
|
+
function getPcapMagic(endianness, precision) {
|
|
873
|
+
const hex = endianness === "little" ? precision === "nano" ? "4d3cb2a1" : "d4c3b2a1" : precision === "nano" ? "a1b23c4d" : "a1b2c3d4";
|
|
874
|
+
return Buffer.from(hex, "hex");
|
|
875
|
+
}
|
|
876
|
+
function parsePcapHeader(buffer) {
|
|
877
|
+
if (buffer.length < 24) throw new Error("PCAP file is too small to contain a global header");
|
|
878
|
+
const magic = buffer.subarray(0, 4).toString("hex");
|
|
879
|
+
let endianness;
|
|
880
|
+
let timestampPrecision;
|
|
881
|
+
switch (magic) {
|
|
882
|
+
case "d4c3b2a1":
|
|
883
|
+
endianness = "little";
|
|
884
|
+
timestampPrecision = "micro";
|
|
885
|
+
break;
|
|
886
|
+
case "4d3cb2a1":
|
|
887
|
+
endianness = "little";
|
|
888
|
+
timestampPrecision = "nano";
|
|
889
|
+
break;
|
|
890
|
+
case "a1b2c3d4":
|
|
891
|
+
endianness = "big";
|
|
892
|
+
timestampPrecision = "micro";
|
|
893
|
+
break;
|
|
894
|
+
case "a1b23c4d":
|
|
895
|
+
endianness = "big";
|
|
896
|
+
timestampPrecision = "nano";
|
|
897
|
+
break;
|
|
898
|
+
default: throw new Error("Unsupported capture format: only classic PCAP files are supported");
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
endianness,
|
|
902
|
+
timestampPrecision,
|
|
903
|
+
versionMajor: readUint16(buffer, 4, endianness),
|
|
904
|
+
versionMinor: readUint16(buffer, 6, endianness),
|
|
905
|
+
snapLength: readUint32(buffer, 16, endianness),
|
|
906
|
+
linkType: readUint32(buffer, 20, endianness)
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function parsePcapPacketInput(value, index) {
|
|
910
|
+
if (!isRecord$1(value)) throw new Error(`packets[${index}] must be an object`);
|
|
911
|
+
const data = parseHexPayload$1(value.dataHex, `packets[${index}].dataHex`);
|
|
912
|
+
const timestampSeconds = value.timestampSeconds === void 0 ? 0 : parseNonNegativeInteger(value.timestampSeconds, `packets[${index}].timestampSeconds`);
|
|
913
|
+
const timestampFraction = value.timestampFraction === void 0 ? 0 : parseNonNegativeInteger(value.timestampFraction, `packets[${index}].timestampFraction`);
|
|
914
|
+
const originalLength = value.originalLength === void 0 ? data.length : parsePositiveInteger(value.originalLength, `packets[${index}].originalLength`);
|
|
915
|
+
if (originalLength < data.length) throw new Error(`packets[${index}].originalLength must be >= included packet length`);
|
|
916
|
+
return {
|
|
917
|
+
data,
|
|
918
|
+
timestampSeconds,
|
|
919
|
+
timestampFraction,
|
|
920
|
+
originalLength
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function buildClassicPcap(args) {
|
|
924
|
+
const globalHeader = Buffer.alloc(24);
|
|
925
|
+
getPcapMagic(args.endianness, args.timestampPrecision).copy(globalHeader, 0);
|
|
926
|
+
writeUint16(globalHeader, 4, 2, args.endianness);
|
|
927
|
+
writeUint16(globalHeader, 6, 4, args.endianness);
|
|
928
|
+
writeUint32(globalHeader, 8, 0, args.endianness);
|
|
929
|
+
writeUint32(globalHeader, 12, 0, args.endianness);
|
|
930
|
+
writeUint32(globalHeader, 16, args.snapLength, args.endianness);
|
|
931
|
+
writeUint32(globalHeader, 20, args.linkType, args.endianness);
|
|
932
|
+
const records = args.packets.map((packet) => {
|
|
933
|
+
const header = Buffer.alloc(16);
|
|
934
|
+
writeUint32(header, 0, packet.timestampSeconds, args.endianness);
|
|
935
|
+
writeUint32(header, 4, packet.timestampFraction, args.endianness);
|
|
936
|
+
writeUint32(header, 8, packet.data.length, args.endianness);
|
|
937
|
+
writeUint32(header, 12, packet.originalLength, args.endianness);
|
|
938
|
+
return Buffer.concat([header, packet.data]);
|
|
939
|
+
});
|
|
940
|
+
return Buffer.concat([globalHeader, ...records]);
|
|
941
|
+
}
|
|
942
|
+
function readClassicPcap(buffer, maxPackets, maxBytesPerPacket) {
|
|
943
|
+
const header = parsePcapHeader(buffer);
|
|
944
|
+
const packets = [];
|
|
945
|
+
let offset = 24;
|
|
946
|
+
while (offset < buffer.length) {
|
|
947
|
+
if (maxPackets !== void 0 && packets.length >= maxPackets) break;
|
|
948
|
+
if (offset + 16 > buffer.length) throw new Error("PCAP file ends with an incomplete packet header");
|
|
949
|
+
const timestampSeconds = readUint32(buffer, offset, header.endianness);
|
|
950
|
+
const timestampFraction = readUint32(buffer, offset + 4, header.endianness);
|
|
951
|
+
const includedLength = readUint32(buffer, offset + 8, header.endianness);
|
|
952
|
+
const originalLength = readUint32(buffer, offset + 12, header.endianness);
|
|
953
|
+
offset += 16;
|
|
954
|
+
if (offset + includedLength > buffer.length) throw new Error("PCAP file ends with an incomplete packet payload");
|
|
955
|
+
const packetBytes = buffer.subarray(offset, offset + includedLength);
|
|
956
|
+
offset += includedLength;
|
|
957
|
+
const limit = maxBytesPerPacket === void 0 ? packetBytes.length : maxBytesPerPacket;
|
|
958
|
+
const visibleLength = Math.min(limit, packetBytes.length);
|
|
959
|
+
packets.push({
|
|
960
|
+
index: packets.length,
|
|
961
|
+
timestampSeconds,
|
|
962
|
+
timestampFraction,
|
|
963
|
+
includedLength,
|
|
964
|
+
originalLength,
|
|
965
|
+
dataHex: packetBytes.subarray(0, visibleLength).toString("hex"),
|
|
966
|
+
truncated: visibleLength < packetBytes.length
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
header,
|
|
971
|
+
packets
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/modules/protocol-analysis/ProtocolPatternUtils.ts
|
|
976
|
+
const PRINTABLE_MIN = 32;
|
|
977
|
+
const PRINTABLE_MAX = 126;
|
|
978
|
+
const DELIMITER_CANDIDATES = [
|
|
979
|
+
Buffer.from([44]),
|
|
980
|
+
Buffer.from([124]),
|
|
981
|
+
Buffer.from([58]),
|
|
982
|
+
Buffer.from([59]),
|
|
983
|
+
Buffer.from([9]),
|
|
984
|
+
Buffer.from([0]),
|
|
985
|
+
Buffer.from([13, 10])
|
|
986
|
+
];
|
|
987
|
+
function normalizeHexPayload(value) {
|
|
988
|
+
return value.replace(/^0x/i, "").replace(/\s+/g, "").toLowerCase();
|
|
989
|
+
}
|
|
990
|
+
function isHexPayload(value) {
|
|
991
|
+
const normalized = normalizeHexPayload(value);
|
|
992
|
+
if (normalized.length === 0 || normalized.length % 2 !== 0) return false;
|
|
993
|
+
return /^[0-9a-f]+$/i.test(normalized);
|
|
994
|
+
}
|
|
995
|
+
function parseHexPayload(value) {
|
|
996
|
+
if (!isHexPayload(value)) return null;
|
|
997
|
+
return Buffer.from(normalizeHexPayload(value), "hex");
|
|
998
|
+
}
|
|
999
|
+
function isPrintableByte(value) {
|
|
1000
|
+
return value >= PRINTABLE_MIN && value <= PRINTABLE_MAX;
|
|
1001
|
+
}
|
|
1002
|
+
function printableRatio(buffer) {
|
|
1003
|
+
if (buffer.length === 0) return 0;
|
|
1004
|
+
let printableCount = 0;
|
|
1005
|
+
for (const value of buffer.values()) if (isPrintableByte(value)) printableCount += 1;
|
|
1006
|
+
return printableCount / buffer.length;
|
|
1007
|
+
}
|
|
1008
|
+
function averagePrintableRatio(buffers) {
|
|
1009
|
+
if (buffers.length === 0) return 0;
|
|
1010
|
+
return buffers.reduce((accumulator, buffer) => accumulator + printableRatio(buffer), 0) / buffers.length;
|
|
1011
|
+
}
|
|
1012
|
+
function splitBuffer(buffer, delimiter) {
|
|
1013
|
+
if (delimiter.length === 0) return [buffer];
|
|
1014
|
+
const parts = [];
|
|
1015
|
+
let start = 0;
|
|
1016
|
+
let index = buffer.indexOf(delimiter, start);
|
|
1017
|
+
while (index >= 0) {
|
|
1018
|
+
parts.push(buffer.subarray(start, index));
|
|
1019
|
+
start = index + delimiter.length;
|
|
1020
|
+
index = buffer.indexOf(delimiter, start);
|
|
1021
|
+
}
|
|
1022
|
+
parts.push(buffer.subarray(start));
|
|
1023
|
+
return parts;
|
|
1024
|
+
}
|
|
1025
|
+
function bufferToDelimiterString(buffer) {
|
|
1026
|
+
return printableRatio(buffer) === 1 ? buffer.toString("utf8") : buffer.toString("hex");
|
|
1027
|
+
}
|
|
1028
|
+
function parsePayloads(hexPayloads) {
|
|
1029
|
+
const buffers = [];
|
|
1030
|
+
for (const hexPayload of hexPayloads) {
|
|
1031
|
+
const payload = parseHexPayload(hexPayload);
|
|
1032
|
+
if (payload) buffers.push(payload);
|
|
1033
|
+
}
|
|
1034
|
+
return buffers;
|
|
1035
|
+
}
|
|
1036
|
+
function decodeInteger(buffer, byteOrder) {
|
|
1037
|
+
if (buffer.length === 0) return null;
|
|
1038
|
+
if (buffer.length === 1) return buffer.readUInt8(0);
|
|
1039
|
+
if (buffer.length === 2) return byteOrder === "le" ? buffer.readUInt16LE(0) : buffer.readUInt16BE(0);
|
|
1040
|
+
if (buffer.length === 4) return byteOrder === "le" ? buffer.readUInt32LE(0) : buffer.readUInt32BE(0);
|
|
1041
|
+
if (buffer.length === 8) {
|
|
1042
|
+
const value = byteOrder === "le" ? Number(buffer.readBigUInt64LE(0)) : Number(buffer.readBigUInt64BE(0));
|
|
1043
|
+
return Number.isFinite(value) ? value : null;
|
|
1044
|
+
}
|
|
1045
|
+
let value = 0;
|
|
1046
|
+
const bytes = byteOrder === "le" ? [...buffer.values()].toReversed() : [...buffer.values()];
|
|
1047
|
+
for (const byte of bytes) value = value * 256 + byte;
|
|
1048
|
+
return Number.isFinite(value) ? value : null;
|
|
1049
|
+
}
|
|
1050
|
+
function decodeFloat(buffer, byteOrder) {
|
|
1051
|
+
if (buffer.length === 4) {
|
|
1052
|
+
const value = byteOrder === "le" ? buffer.readFloatLE(0) : buffer.readFloatBE(0);
|
|
1053
|
+
return Number.isFinite(value) ? value : null;
|
|
1054
|
+
}
|
|
1055
|
+
if (buffer.length === 8) {
|
|
1056
|
+
const value = byteOrder === "le" ? buffer.readDoubleLE(0) : buffer.readDoubleBE(0);
|
|
1057
|
+
return Number.isFinite(value) ? value : null;
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
function countOccurrences(buffer, delimiter) {
|
|
1062
|
+
if (delimiter.length === 0) return 0;
|
|
1063
|
+
let count = 0;
|
|
1064
|
+
let start = 0;
|
|
1065
|
+
let index = buffer.indexOf(delimiter, start);
|
|
1066
|
+
while (index >= 0) {
|
|
1067
|
+
count += 1;
|
|
1068
|
+
start = index + delimiter.length;
|
|
1069
|
+
index = buffer.indexOf(delimiter, start);
|
|
1070
|
+
}
|
|
1071
|
+
return count;
|
|
1072
|
+
}
|
|
1073
|
+
function inferFieldType(samples) {
|
|
1074
|
+
if (samples.length === 0) return "bytes";
|
|
1075
|
+
if (samples.every((sample) => sample.length === 1) && samples.every((sample) => sample[0] === 0 || sample[0] === 1)) return "bool";
|
|
1076
|
+
if (averagePrintableRatio(samples) >= .7) return "string";
|
|
1077
|
+
if (samples.every((sample) => sample.length === 4) && looksLikeFloatSamples(samples)) return "float";
|
|
1078
|
+
if (samples.every((sample) => sample.length <= 4)) return "int";
|
|
1079
|
+
return "bytes";
|
|
1080
|
+
}
|
|
1081
|
+
function looksLikeFloatSamples(samples) {
|
|
1082
|
+
const decoded = [];
|
|
1083
|
+
for (const sample of samples) {
|
|
1084
|
+
const value = decodeFloat(sample, "be");
|
|
1085
|
+
if (value === null) return false;
|
|
1086
|
+
decoded.push(value);
|
|
1087
|
+
}
|
|
1088
|
+
return decoded.some((value) => Math.abs(value) > .001 && Math.abs(value) < 1e6);
|
|
1089
|
+
}
|
|
1090
|
+
function isPrintableColumn(buffers, offset) {
|
|
1091
|
+
let valueCount = 0;
|
|
1092
|
+
let printableCount = 0;
|
|
1093
|
+
for (const buffer of buffers) {
|
|
1094
|
+
const value = buffer[offset];
|
|
1095
|
+
if (value === void 0) continue;
|
|
1096
|
+
valueCount += 1;
|
|
1097
|
+
if (isPrintableByte(value)) printableCount += 1;
|
|
1098
|
+
}
|
|
1099
|
+
return valueCount > 0 && printableCount / valueCount >= .8;
|
|
1100
|
+
}
|
|
1101
|
+
function isBooleanColumn(buffers, offset) {
|
|
1102
|
+
let valueCount = 0;
|
|
1103
|
+
for (const buffer of buffers) {
|
|
1104
|
+
const value = buffer[offset];
|
|
1105
|
+
if (value === void 0) continue;
|
|
1106
|
+
valueCount += 1;
|
|
1107
|
+
if (value !== 0 && value !== 1) return false;
|
|
1108
|
+
}
|
|
1109
|
+
return valueCount > 0;
|
|
1110
|
+
}
|
|
1111
|
+
function buildDelimitedFields(buffers, delimiter) {
|
|
1112
|
+
if (delimiter.length === 0) return [];
|
|
1113
|
+
const tokenized = buffers.map((buffer) => splitBuffer(buffer, delimiter));
|
|
1114
|
+
const firstRow = tokenized[0];
|
|
1115
|
+
if (!firstRow || firstRow.length < 2) return [];
|
|
1116
|
+
const tokenCount = firstRow.length;
|
|
1117
|
+
if (!tokenized.every((parts) => parts.length === tokenCount)) return [];
|
|
1118
|
+
const fields = [];
|
|
1119
|
+
let currentOffset = 0;
|
|
1120
|
+
for (let index = 0; index < tokenCount; index += 1) {
|
|
1121
|
+
const template = firstRow[index];
|
|
1122
|
+
if (!template) continue;
|
|
1123
|
+
const samples = tokenized.map((parts) => parts[index]).filter((part) => Buffer.isBuffer(part));
|
|
1124
|
+
fields.push({
|
|
1125
|
+
name: `field_${index + 1}`,
|
|
1126
|
+
offset: currentOffset,
|
|
1127
|
+
length: template.length,
|
|
1128
|
+
type: inferFieldType(samples)
|
|
1129
|
+
});
|
|
1130
|
+
currentOffset += template.length + delimiter.length;
|
|
1131
|
+
}
|
|
1132
|
+
return fields;
|
|
1133
|
+
}
|
|
1134
|
+
function buildFixedWidthFields(buffers) {
|
|
1135
|
+
const minLength = Math.min(...buffers.map((buffer) => buffer.length));
|
|
1136
|
+
const fields = [];
|
|
1137
|
+
let offset = 0;
|
|
1138
|
+
while (offset < minLength && fields.length < 24) {
|
|
1139
|
+
if (isPrintableColumn(buffers, offset)) {
|
|
1140
|
+
let end = offset + 1;
|
|
1141
|
+
while (end < minLength && isPrintableColumn(buffers, end)) end += 1;
|
|
1142
|
+
fields.push({
|
|
1143
|
+
name: `field_${fields.length + 1}`,
|
|
1144
|
+
offset,
|
|
1145
|
+
length: end - offset,
|
|
1146
|
+
type: "string"
|
|
1147
|
+
});
|
|
1148
|
+
offset = end;
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (isBooleanColumn(buffers, offset)) {
|
|
1152
|
+
fields.push({
|
|
1153
|
+
name: `field_${fields.length + 1}`,
|
|
1154
|
+
offset,
|
|
1155
|
+
length: 1,
|
|
1156
|
+
type: "bool"
|
|
1157
|
+
});
|
|
1158
|
+
offset += 1;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const remaining = minLength - offset;
|
|
1162
|
+
if (remaining >= 4) {
|
|
1163
|
+
if (looksLikeFloatSamples(buffers.map((buffer) => buffer.subarray(offset, offset + 4)))) {
|
|
1164
|
+
fields.push({
|
|
1165
|
+
name: `field_${fields.length + 1}`,
|
|
1166
|
+
offset,
|
|
1167
|
+
length: 4,
|
|
1168
|
+
type: "float"
|
|
1169
|
+
});
|
|
1170
|
+
offset += 4;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const segmentLength = remaining >= 4 ? 4 : Math.min(remaining, 2);
|
|
1175
|
+
const samples = buffers.map((buffer) => buffer.subarray(offset, offset + segmentLength));
|
|
1176
|
+
fields.push({
|
|
1177
|
+
name: `field_${fields.length + 1}`,
|
|
1178
|
+
offset,
|
|
1179
|
+
length: segmentLength,
|
|
1180
|
+
type: inferFieldType(samples)
|
|
1181
|
+
});
|
|
1182
|
+
offset += segmentLength;
|
|
1183
|
+
}
|
|
1184
|
+
return fields;
|
|
1185
|
+
}
|
|
1186
|
+
function inferDelimiter(buffers) {
|
|
1187
|
+
for (const candidate of DELIMITER_CANDIDATES) {
|
|
1188
|
+
const counts = buffers.map((buffer) => countOccurrences(buffer, candidate));
|
|
1189
|
+
const firstCount = counts[0];
|
|
1190
|
+
if (typeof firstCount === "number" && firstCount >= 2 && counts.every((count) => count === firstCount)) return bufferToDelimiterString(candidate);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function inferByteOrder(buffers) {
|
|
1194
|
+
const minLength = Math.min(...buffers.map((buffer) => buffer.length));
|
|
1195
|
+
if (minLength < 2) return "be";
|
|
1196
|
+
let leScore = 0;
|
|
1197
|
+
let beScore = 0;
|
|
1198
|
+
const limit = Math.min(minLength - 1, 8);
|
|
1199
|
+
for (let offset = 0; offset < limit; offset += 2) {
|
|
1200
|
+
let leSmallValues = 0;
|
|
1201
|
+
let beSmallValues = 0;
|
|
1202
|
+
for (const buffer of buffers) {
|
|
1203
|
+
const little = buffer.readUInt16LE(offset);
|
|
1204
|
+
const big = buffer.readUInt16BE(offset);
|
|
1205
|
+
if (little < 4096) leSmallValues += 1;
|
|
1206
|
+
if (big < 4096) beSmallValues += 1;
|
|
1207
|
+
}
|
|
1208
|
+
if (leSmallValues > beSmallValues) leScore += 1;
|
|
1209
|
+
else if (beSmallValues > leSmallValues) beScore += 1;
|
|
1210
|
+
}
|
|
1211
|
+
return leScore > beScore ? "le" : "be";
|
|
1212
|
+
}
|
|
1213
|
+
function labelMagicFields(fields, buffers) {
|
|
1214
|
+
if (fields.length === 0 || buffers.length < 2) return fields;
|
|
1215
|
+
const minLen = Math.min(...buffers.map((b) => b.length));
|
|
1216
|
+
let commonPrefixLen = 0;
|
|
1217
|
+
for (let offset = 0; offset < minLen; offset += 1) {
|
|
1218
|
+
const byte = buffers[0][offset];
|
|
1219
|
+
if (buffers.every((b) => b[offset] === byte)) commonPrefixLen = offset + 1;
|
|
1220
|
+
else break;
|
|
1221
|
+
}
|
|
1222
|
+
if (commonPrefixLen === 0) return fields;
|
|
1223
|
+
let magicLabelApplied = false;
|
|
1224
|
+
return fields.map((field) => {
|
|
1225
|
+
if (!magicLabelApplied && field.offset === 0 && commonPrefixLen >= 2) {
|
|
1226
|
+
magicLabelApplied = true;
|
|
1227
|
+
return {
|
|
1228
|
+
...field,
|
|
1229
|
+
name: "magic"
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
if (magicLabelApplied && field.type === "int" && field.length <= 2 && field.offset <= commonPrefixLen) return {
|
|
1233
|
+
...field,
|
|
1234
|
+
name: "version"
|
|
1235
|
+
};
|
|
1236
|
+
return field;
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
//#endregion
|
|
1240
|
+
//#region src/modules/protocol-analysis/ProtocolPatternEngine.ts
|
|
1241
|
+
var ProtocolPatternEngine = class {
|
|
1242
|
+
patterns = /* @__PURE__ */ new Map();
|
|
1243
|
+
legacyPatterns = /* @__PURE__ */ new Map();
|
|
1244
|
+
definePattern(name, specOrFields, options) {
|
|
1245
|
+
const legacyPattern = Array.isArray(specOrFields) ? this.createLegacyPattern(name, specOrFields, options) : this.createLegacyPatternFromSpec(name, specOrFields);
|
|
1246
|
+
const spec = this.createSpecFromLegacyPattern(legacyPattern);
|
|
1247
|
+
this.patterns.set(name, spec);
|
|
1248
|
+
this.legacyPatterns.set(name, legacyPattern);
|
|
1249
|
+
if (Array.isArray(specOrFields)) return legacyPattern;
|
|
1250
|
+
}
|
|
1251
|
+
detectPattern(hexPayload) {
|
|
1252
|
+
const payload = parseHexPayload(hexPayload);
|
|
1253
|
+
if (!payload) return null;
|
|
1254
|
+
let bestMatch = null;
|
|
1255
|
+
for (const pattern of this.patterns.values()) {
|
|
1256
|
+
const totalChecks = pattern.fields.length + (pattern.fieldDelimiter ? 1 : 0);
|
|
1257
|
+
if (totalChecks === 0) continue;
|
|
1258
|
+
let matches = 0;
|
|
1259
|
+
if (pattern.fieldDelimiter && this.payloadContainsDelimiter(payload, pattern.fieldDelimiter)) matches += 1;
|
|
1260
|
+
for (const field of pattern.fields) if (this.matchesField(payload, field, pattern.byteOrder ?? "be")) matches += 1;
|
|
1261
|
+
const confidence = Number((matches / totalChecks).toFixed(2));
|
|
1262
|
+
if (confidence <= 0) continue;
|
|
1263
|
+
const candidate = {
|
|
1264
|
+
pattern,
|
|
1265
|
+
confidence,
|
|
1266
|
+
matches,
|
|
1267
|
+
total: totalChecks
|
|
1268
|
+
};
|
|
1269
|
+
if (!bestMatch || candidate.confidence > bestMatch.confidence || candidate.confidence === bestMatch.confidence && candidate.matches > bestMatch.matches) bestMatch = candidate;
|
|
1270
|
+
}
|
|
1271
|
+
return bestMatch;
|
|
1272
|
+
}
|
|
1273
|
+
autoDetect(hexPayloads) {
|
|
1274
|
+
const buffers = parsePayloads(hexPayloads);
|
|
1275
|
+
if (buffers.length === 0) return null;
|
|
1276
|
+
const delimiter = inferDelimiter(buffers);
|
|
1277
|
+
const fields = this.inferFields(hexPayloads);
|
|
1278
|
+
return {
|
|
1279
|
+
name: "auto-detected-pattern",
|
|
1280
|
+
fieldDelimiter: delimiter,
|
|
1281
|
+
byteOrder: inferByteOrder(buffers),
|
|
1282
|
+
fields
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
inferFields(hexPayloads) {
|
|
1286
|
+
const buffers = parsePayloads(hexPayloads);
|
|
1287
|
+
if (buffers.length === 0) return [];
|
|
1288
|
+
const delimiter = inferDelimiter(buffers);
|
|
1289
|
+
if (delimiter) {
|
|
1290
|
+
const fields = buildDelimitedFields(buffers, this.parseDelimiter(delimiter));
|
|
1291
|
+
if (fields.length > 0) return labelMagicFields(fields, buffers);
|
|
1292
|
+
}
|
|
1293
|
+
return labelMagicFields(buildFixedWidthFields(buffers), buffers);
|
|
1294
|
+
}
|
|
1295
|
+
autoDetectPattern(payloads, options) {
|
|
1296
|
+
const hexPayloads = payloads.map((payload) => payload.toString("hex"));
|
|
1297
|
+
const detected = this.autoDetect(hexPayloads);
|
|
1298
|
+
const name = options?.name ?? detected?.name ?? "auto_detected";
|
|
1299
|
+
if (!detected) {
|
|
1300
|
+
const emptyPattern = this.createLegacyPattern(name, []);
|
|
1301
|
+
this.patterns.set(name, this.createSpecFromLegacyPattern(emptyPattern));
|
|
1302
|
+
this.legacyPatterns.set(name, emptyPattern);
|
|
1303
|
+
return emptyPattern;
|
|
1304
|
+
}
|
|
1305
|
+
const namedPattern = {
|
|
1306
|
+
...detected,
|
|
1307
|
+
name
|
|
1308
|
+
};
|
|
1309
|
+
this.definePattern(name, namedPattern);
|
|
1310
|
+
return this.getPattern(name) ?? this.createLegacyPatternFromSpec(name, namedPattern);
|
|
1311
|
+
}
|
|
1312
|
+
getPattern(name) {
|
|
1313
|
+
return this.legacyPatterns.get(name);
|
|
1314
|
+
}
|
|
1315
|
+
listPatterns() {
|
|
1316
|
+
return [...this.patterns.keys()];
|
|
1317
|
+
}
|
|
1318
|
+
exportProto(pattern) {
|
|
1319
|
+
const legacyPattern = this.isLegacyPattern(pattern) ? pattern : this.createLegacyPatternFromSpec(pattern.name, pattern);
|
|
1320
|
+
const lines = [
|
|
1321
|
+
`// Protocol: ${legacyPattern.name}`,
|
|
1322
|
+
`// Byte order: ${legacyPattern.byteOrder}`,
|
|
1323
|
+
""
|
|
1324
|
+
];
|
|
1325
|
+
if (legacyPattern.encryption) {
|
|
1326
|
+
lines.push(`// Encryption: ${legacyPattern.encryption.type}`);
|
|
1327
|
+
if (legacyPattern.encryption.notes) lines.push(`// Notes: ${legacyPattern.encryption.notes}`);
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
}
|
|
1330
|
+
lines.push(`message ${this.toPascalCase(legacyPattern.name)} {`);
|
|
1331
|
+
for (let index = 0; index < legacyPattern.fields.length; index += 1) {
|
|
1332
|
+
const field = legacyPattern.fields[index];
|
|
1333
|
+
if (!field) continue;
|
|
1334
|
+
const comment = field.description ? ` // ${field.description}` : "";
|
|
1335
|
+
lines.push(` ${this.toProtoType(field.type)} ${field.name} = ${index + 1};${comment}`);
|
|
1336
|
+
}
|
|
1337
|
+
lines.push("}");
|
|
1338
|
+
lines.push("");
|
|
1339
|
+
return lines.join("\n");
|
|
1340
|
+
}
|
|
1341
|
+
payloadContainsDelimiter(payload, delimiter) {
|
|
1342
|
+
const delimiterBuffer = this.parseDelimiter(delimiter);
|
|
1343
|
+
if (delimiterBuffer.length === 0) return false;
|
|
1344
|
+
return payload.includes(delimiterBuffer);
|
|
1345
|
+
}
|
|
1346
|
+
matchesField(payload, field, byteOrder) {
|
|
1347
|
+
if (field.offset < 0 || field.length <= 0 || payload.length < field.offset + field.length) return false;
|
|
1348
|
+
const slice = payload.subarray(field.offset, field.offset + field.length);
|
|
1349
|
+
switch (field.type) {
|
|
1350
|
+
case "bytes": return slice.length === field.length;
|
|
1351
|
+
case "bool": return slice.length === 1 && (slice[0] === 0 || slice[0] === 1);
|
|
1352
|
+
case "string": return printableRatio(slice) >= .6;
|
|
1353
|
+
case "int": return decodeInteger(slice, byteOrder) !== null;
|
|
1354
|
+
case "float": return decodeFloat(slice, byteOrder) !== null;
|
|
1355
|
+
default: return false;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
createLegacyPattern(name, fields, options) {
|
|
1359
|
+
return {
|
|
1360
|
+
name,
|
|
1361
|
+
fields: fields.map((field) => ({
|
|
1362
|
+
name: field.name,
|
|
1363
|
+
offset: field.offset,
|
|
1364
|
+
length: field.length,
|
|
1365
|
+
type: field.type,
|
|
1366
|
+
...field.description ? { description: field.description } : {}
|
|
1367
|
+
})).toSorted((left, right) => left.offset - right.offset),
|
|
1368
|
+
byteOrder: options?.byteOrder ?? "big",
|
|
1369
|
+
...options?.encryption ? { encryption: options.encryption } : {}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
createLegacyPatternFromSpec(name, spec) {
|
|
1373
|
+
return {
|
|
1374
|
+
name,
|
|
1375
|
+
fieldDelimiter: spec.fieldDelimiter,
|
|
1376
|
+
byteOrder: spec.byteOrder === "le" ? "little" : "big",
|
|
1377
|
+
fields: spec.fields.map((field) => ({
|
|
1378
|
+
name: field.name,
|
|
1379
|
+
offset: field.offset,
|
|
1380
|
+
length: field.length,
|
|
1381
|
+
type: this.toLegacyFieldType(field),
|
|
1382
|
+
...field.description ? { description: field.description } : {}
|
|
1383
|
+
}))
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
createSpecFromLegacyPattern(pattern) {
|
|
1387
|
+
return {
|
|
1388
|
+
name: pattern.name,
|
|
1389
|
+
fieldDelimiter: pattern.fieldDelimiter,
|
|
1390
|
+
byteOrder: pattern.byteOrder === "little" ? "le" : "be",
|
|
1391
|
+
fields: pattern.fields.map((field) => ({
|
|
1392
|
+
name: field.name,
|
|
1393
|
+
offset: field.offset,
|
|
1394
|
+
length: field.length,
|
|
1395
|
+
type: this.toSpecFieldType(field.type),
|
|
1396
|
+
...field.description ? { description: field.description } : {}
|
|
1397
|
+
}))
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
isLegacyPattern(pattern) {
|
|
1401
|
+
return pattern.byteOrder === "big" || pattern.byteOrder === "little";
|
|
1402
|
+
}
|
|
1403
|
+
toLegacyFieldType(field) {
|
|
1404
|
+
if (field.type === "float") return "float";
|
|
1405
|
+
if (field.type === "string") return "string";
|
|
1406
|
+
if (field.type === "bytes") return "bytes";
|
|
1407
|
+
if (field.length === 1) return "uint8";
|
|
1408
|
+
if (field.length === 2) return "uint16";
|
|
1409
|
+
if (field.length === 4) return "uint32";
|
|
1410
|
+
return "int64";
|
|
1411
|
+
}
|
|
1412
|
+
toSpecFieldType(fieldType) {
|
|
1413
|
+
if (fieldType === "float") return "float";
|
|
1414
|
+
if (fieldType === "string") return "string";
|
|
1415
|
+
if (fieldType === "bytes") return "bytes";
|
|
1416
|
+
return "int";
|
|
1417
|
+
}
|
|
1418
|
+
parseDelimiter(delimiter) {
|
|
1419
|
+
if (isHexPayload(delimiter)) {
|
|
1420
|
+
const parsed = parseHexPayload(delimiter);
|
|
1421
|
+
if (parsed) return parsed;
|
|
1422
|
+
}
|
|
1423
|
+
return Buffer.from(delimiter, "utf8");
|
|
1424
|
+
}
|
|
1425
|
+
toProtoType(type) {
|
|
1426
|
+
return {
|
|
1427
|
+
uint8: "uint32",
|
|
1428
|
+
uint16: "uint32",
|
|
1429
|
+
uint32: "uint32",
|
|
1430
|
+
int64: "int64",
|
|
1431
|
+
float: "float",
|
|
1432
|
+
string: "string",
|
|
1433
|
+
bytes: "bytes"
|
|
1434
|
+
}[type];
|
|
1435
|
+
}
|
|
1436
|
+
toPascalCase(name) {
|
|
1437
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_").split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("") || "Message";
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
//#endregion
|
|
1441
|
+
//#region src/modules/protocol-analysis/StateMachineInferrer.ts
|
|
1442
|
+
function isRecord(value) {
|
|
1443
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1444
|
+
}
|
|
1445
|
+
function normalizeText(value) {
|
|
1446
|
+
return value.trim().toLowerCase();
|
|
1447
|
+
}
|
|
1448
|
+
function calculateEntropy(buffer) {
|
|
1449
|
+
if (buffer.length === 0) return 0;
|
|
1450
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
1451
|
+
for (const byte of buffer) frequency.set(byte, (frequency.get(byte) ?? 0) + 1);
|
|
1452
|
+
let entropy = 0;
|
|
1453
|
+
for (const count of frequency.values()) {
|
|
1454
|
+
const p = count / buffer.length;
|
|
1455
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
1456
|
+
}
|
|
1457
|
+
return entropy;
|
|
1458
|
+
}
|
|
1459
|
+
function printableRatioOf(value) {
|
|
1460
|
+
if (value.length === 0) return 0;
|
|
1461
|
+
let count = 0;
|
|
1462
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1463
|
+
const code = value.charCodeAt(i);
|
|
1464
|
+
if (code >= 32 && code <= 126) count += 1;
|
|
1465
|
+
}
|
|
1466
|
+
return count / value.length;
|
|
1467
|
+
}
|
|
1468
|
+
var StateMachineInferrer = class {
|
|
1469
|
+
infer(messages) {
|
|
1470
|
+
if (messages.length === 0) return {
|
|
1471
|
+
states: [],
|
|
1472
|
+
transitions: [],
|
|
1473
|
+
initial: "",
|
|
1474
|
+
initialState: "",
|
|
1475
|
+
finalStates: []
|
|
1476
|
+
};
|
|
1477
|
+
const statesBySignature = /* @__PURE__ */ new Map();
|
|
1478
|
+
const transitionsByKey = /* @__PURE__ */ new Map();
|
|
1479
|
+
let previousStateId = "";
|
|
1480
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
1481
|
+
const message = messages[index];
|
|
1482
|
+
if (!message) continue;
|
|
1483
|
+
const signature = this.buildSignature(message);
|
|
1484
|
+
let state = statesBySignature.get(signature);
|
|
1485
|
+
if (!state) {
|
|
1486
|
+
state = {
|
|
1487
|
+
id: `state_${statesBySignature.size + 1}`,
|
|
1488
|
+
name: this.buildStateName(message, statesBySignature.size + 1),
|
|
1489
|
+
type: this.inferStateType(message, index === messages.length - 1)
|
|
1490
|
+
};
|
|
1491
|
+
statesBySignature.set(signature, state);
|
|
1492
|
+
} else state.type = this.mergeStateTypes(state.type, this.inferStateType(message, index === messages.length - 1));
|
|
1493
|
+
if (previousStateId && message.timestamp !== void 0) {
|
|
1494
|
+
const firstMessage = messages[0];
|
|
1495
|
+
if (firstMessage && firstMessage.timestamp !== void 0) state.timeout = message.timestamp - firstMessage.timestamp;
|
|
1496
|
+
}
|
|
1497
|
+
if (previousStateId) {
|
|
1498
|
+
const event = this.buildEventName(message);
|
|
1499
|
+
const condition = this.buildCondition(message.fields);
|
|
1500
|
+
const action = this.buildAction(message);
|
|
1501
|
+
const transitionKey = `${previousStateId}:${state.id}:${event}`;
|
|
1502
|
+
if (!transitionsByKey.has(transitionKey)) transitionsByKey.set(transitionKey, {
|
|
1503
|
+
from: previousStateId,
|
|
1504
|
+
to: state.id,
|
|
1505
|
+
event,
|
|
1506
|
+
confidence: this.computeTransitionConfidence(message),
|
|
1507
|
+
...condition ? { condition } : {},
|
|
1508
|
+
...action ? { action } : {}
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
previousStateId = state.id;
|
|
1512
|
+
}
|
|
1513
|
+
const firstMessage = messages[0];
|
|
1514
|
+
const initial = firstMessage ? statesBySignature.get(this.buildSignature(firstMessage))?.id ?? "" : "";
|
|
1515
|
+
return {
|
|
1516
|
+
states: [...statesBySignature.values()],
|
|
1517
|
+
transitions: [...transitionsByKey.values()],
|
|
1518
|
+
initial,
|
|
1519
|
+
initialState: initial,
|
|
1520
|
+
finalStates: this.collectTerminalStates([...statesBySignature.values()])
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
visualize(machine) {
|
|
1524
|
+
if (machine.states.length === 0) return [
|
|
1525
|
+
"```mermaid",
|
|
1526
|
+
"stateDiagram-v2",
|
|
1527
|
+
" [*] --> empty",
|
|
1528
|
+
"```"
|
|
1529
|
+
].join("\n");
|
|
1530
|
+
const lines = ["```mermaid", "stateDiagram-v2"];
|
|
1531
|
+
const initial = machine.initialState ?? machine.initial;
|
|
1532
|
+
if (initial) lines.push(` [*] --> ${initial}`);
|
|
1533
|
+
for (const state of machine.states) {
|
|
1534
|
+
const stateType = state.type ?? "normal";
|
|
1535
|
+
const label = stateType === "normal" ? state.name : `${state.name} (${stateType})`;
|
|
1536
|
+
lines.push(` state "${label}" as ${state.id}`);
|
|
1537
|
+
}
|
|
1538
|
+
for (const transition of machine.transitions) {
|
|
1539
|
+
const parts = [transition.event ?? transition.trigger ?? "transition"];
|
|
1540
|
+
if (typeof transition.confidence === "number") parts.push(`(${transition.confidence.toFixed(2)})`);
|
|
1541
|
+
if (transition.condition) parts.push(`[${transition.condition}]`);
|
|
1542
|
+
if (transition.action) parts.push(`/ ${transition.action}`);
|
|
1543
|
+
lines.push(` ${transition.from} --> ${transition.to} : ${parts.join(" ")}`);
|
|
1544
|
+
}
|
|
1545
|
+
const finalStateSet = new Set(machine.finalStates ?? []);
|
|
1546
|
+
for (const state of machine.states) {
|
|
1547
|
+
const stateType = state.type ?? "normal";
|
|
1548
|
+
if (stateType === "accept" || stateType === "reject" || finalStateSet.has(state.id)) lines.push(` ${state.id} --> [*]`);
|
|
1549
|
+
}
|
|
1550
|
+
lines.push("```");
|
|
1551
|
+
return lines.join("\n");
|
|
1552
|
+
}
|
|
1553
|
+
inferStateMachine(messages) {
|
|
1554
|
+
const normalizedMessages = messages.map((message) => ({
|
|
1555
|
+
direction: message.direction === "out" ? "req" : "res",
|
|
1556
|
+
timestamp: message.timestamp ?? 0,
|
|
1557
|
+
fields: {},
|
|
1558
|
+
raw: message.payload.length > 0 ? message.payload.toString("utf8") : message.payload.toString("hex"),
|
|
1559
|
+
rawBuffer: message.payload
|
|
1560
|
+
}));
|
|
1561
|
+
return this.infer(normalizedMessages);
|
|
1562
|
+
}
|
|
1563
|
+
generateMermaid(machine) {
|
|
1564
|
+
return this.visualize(machine);
|
|
1565
|
+
}
|
|
1566
|
+
simplify(machine) {
|
|
1567
|
+
if (machine.states.length < 2) return {
|
|
1568
|
+
...machine,
|
|
1569
|
+
initialState: machine.initialState ?? machine.initial,
|
|
1570
|
+
finalStates: machine.finalStates ?? this.collectTerminalStates(machine.states)
|
|
1571
|
+
};
|
|
1572
|
+
const stateToGroup = /* @__PURE__ */ new Map();
|
|
1573
|
+
const groupRepresentative = /* @__PURE__ */ new Map();
|
|
1574
|
+
for (const state of machine.states) {
|
|
1575
|
+
const prefix = this.getPayloadPrefix(state);
|
|
1576
|
+
if (!prefix) continue;
|
|
1577
|
+
const existingGroup = [...groupRepresentative.entries()].find(([key]) => key === prefix);
|
|
1578
|
+
if (existingGroup) stateToGroup.set(state.id, existingGroup[0]);
|
|
1579
|
+
else {
|
|
1580
|
+
groupRepresentative.set(prefix, state.id);
|
|
1581
|
+
stateToGroup.set(state.id, prefix);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
1585
|
+
const groupIdToPrimary = /* @__PURE__ */ new Map();
|
|
1586
|
+
for (const [prefix, primaryId] of groupRepresentative) groupIdToPrimary.set(prefix, primaryId);
|
|
1587
|
+
for (const state of machine.states) {
|
|
1588
|
+
const prefix = this.getPayloadPrefix(state);
|
|
1589
|
+
if (!prefix) continue;
|
|
1590
|
+
const primary = groupIdToPrimary.get(prefix);
|
|
1591
|
+
if (primary && primary !== state.id) mergeMap.set(state.id, primary);
|
|
1592
|
+
}
|
|
1593
|
+
if (mergeMap.size === 0) return {
|
|
1594
|
+
...machine,
|
|
1595
|
+
initialState: machine.initialState ?? machine.initial,
|
|
1596
|
+
finalStates: machine.finalStates ?? this.collectTerminalStates(machine.states)
|
|
1597
|
+
};
|
|
1598
|
+
const newStates = machine.states.filter((state) => !mergeMap.has(state.id));
|
|
1599
|
+
const newTransitions = machine.transitions.map((t) => ({
|
|
1600
|
+
...t,
|
|
1601
|
+
from: mergeMap.get(t.from) ?? t.from,
|
|
1602
|
+
to: mergeMap.get(t.to) ?? t.to
|
|
1603
|
+
})).filter((t) => t.from !== t.to);
|
|
1604
|
+
const rawInitialState = machine.initialState ?? machine.initial ?? "";
|
|
1605
|
+
const initialState = mergeMap.get(rawInitialState) ?? rawInitialState;
|
|
1606
|
+
return {
|
|
1607
|
+
states: newStates,
|
|
1608
|
+
transitions: newTransitions,
|
|
1609
|
+
initial: initialState,
|
|
1610
|
+
initialState,
|
|
1611
|
+
finalStates: machine.finalStates.map((fs) => mergeMap.get(fs) ?? fs).filter((fs, index, arr) => arr.indexOf(fs) === index)
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
getPayloadPrefix(state) {
|
|
1615
|
+
const payload = state.expectedPayload;
|
|
1616
|
+
if (!payload || payload.length < 8) return null;
|
|
1617
|
+
return payload.slice(0, 8).toLowerCase();
|
|
1618
|
+
}
|
|
1619
|
+
buildSignature(message) {
|
|
1620
|
+
const fieldKeys = Object.keys(message.fields).toSorted().join(",");
|
|
1621
|
+
const rawPrefix = normalizeText(message.rawBuffer ? message.rawBuffer.toString("hex") : message.raw).slice(0, 24);
|
|
1622
|
+
return `${message.direction}|${fieldKeys}|${rawPrefix}`;
|
|
1623
|
+
}
|
|
1624
|
+
buildStateName(message, position) {
|
|
1625
|
+
const directionName = message.direction === "req" ? "send" : "recv";
|
|
1626
|
+
const primaryField = this.findPrimaryFieldName(message.fields);
|
|
1627
|
+
const raw = message.raw;
|
|
1628
|
+
if (raw.length === 0) return `${directionName}_empty`;
|
|
1629
|
+
const buf = message.rawBuffer;
|
|
1630
|
+
const hexContent = Buffer.isBuffer(buf) ? buf.toString("hex") : raw;
|
|
1631
|
+
if (hexContent.startsWith("16") || hexContent.startsWith("15") || hexContent.startsWith("17")) return `${directionName}_tls_handshake`;
|
|
1632
|
+
const trimmed = raw.trimStart();
|
|
1633
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return `${directionName}_json_${primaryField || `step_${position}`}`;
|
|
1634
|
+
if (printableRatioOf(raw) >= .7) {
|
|
1635
|
+
const lower = normalizeText(raw);
|
|
1636
|
+
if (lower.includes("close") || lower.includes("fin") || lower.includes("bye")) return `${directionName}_close`;
|
|
1637
|
+
if (lower.startsWith("get ") || lower.startsWith("post ") || lower.startsWith("http")) return `${directionName}_text_http`;
|
|
1638
|
+
return `${directionName}_text_${primaryField || `step_${position}`}`;
|
|
1639
|
+
}
|
|
1640
|
+
if (Buffer.isBuffer(buf) && buf.length >= 32) {
|
|
1641
|
+
if (calculateEntropy(buf) > 6) return `${directionName}_encrypted`;
|
|
1642
|
+
}
|
|
1643
|
+
if (Buffer.isBuffer(buf) && buf.length <= 4) return `${directionName}_control`;
|
|
1644
|
+
return `${directionName}_${primaryField || `step_${position}`}`;
|
|
1645
|
+
}
|
|
1646
|
+
findPrimaryFieldName(fields) {
|
|
1647
|
+
const firstKey = Object.keys(fields).toSorted()[0];
|
|
1648
|
+
return firstKey ? firstKey : "";
|
|
1649
|
+
}
|
|
1650
|
+
inferStateType(message, isLastMessage) {
|
|
1651
|
+
const text = normalizeText(message.raw);
|
|
1652
|
+
const statusValue = this.findStatusValue(message.fields);
|
|
1653
|
+
if (this.containsRejectSignal(text) || this.containsRejectSignal(statusValue)) return "reject";
|
|
1654
|
+
if (this.containsAcceptSignal(text) || this.containsAcceptSignal(statusValue) || isLastMessage && message.direction === "res") return "accept";
|
|
1655
|
+
return "normal";
|
|
1656
|
+
}
|
|
1657
|
+
mergeStateTypes(current, next) {
|
|
1658
|
+
if (current === "reject" || next === "reject") return "reject";
|
|
1659
|
+
if (current === "accept" || next === "accept") return "accept";
|
|
1660
|
+
return "normal";
|
|
1661
|
+
}
|
|
1662
|
+
findStatusValue(fields) {
|
|
1663
|
+
for (const key of [
|
|
1664
|
+
"status",
|
|
1665
|
+
"result",
|
|
1666
|
+
"code",
|
|
1667
|
+
"reason",
|
|
1668
|
+
"message"
|
|
1669
|
+
]) {
|
|
1670
|
+
const value = fields[key];
|
|
1671
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1672
|
+
}
|
|
1673
|
+
return "";
|
|
1674
|
+
}
|
|
1675
|
+
containsRejectSignal(value) {
|
|
1676
|
+
return [
|
|
1677
|
+
"error",
|
|
1678
|
+
"fail",
|
|
1679
|
+
"denied",
|
|
1680
|
+
"reject",
|
|
1681
|
+
"timeout",
|
|
1682
|
+
"invalid"
|
|
1683
|
+
].some((token) => normalizeText(value).includes(token));
|
|
1684
|
+
}
|
|
1685
|
+
containsAcceptSignal(value) {
|
|
1686
|
+
return [
|
|
1687
|
+
"ok",
|
|
1688
|
+
"success",
|
|
1689
|
+
"accept",
|
|
1690
|
+
"ready",
|
|
1691
|
+
"done",
|
|
1692
|
+
"complete"
|
|
1693
|
+
].some((token) => normalizeText(value).includes(token));
|
|
1694
|
+
}
|
|
1695
|
+
buildEventName(message) {
|
|
1696
|
+
const statusValue = this.findStatusValue(message.fields);
|
|
1697
|
+
if (statusValue) return `${message.direction}_${normalizeText(statusValue).replace(/[^a-z0-9]+/g, "_")}`;
|
|
1698
|
+
const primaryField = this.findPrimaryFieldName(message.fields);
|
|
1699
|
+
if (primaryField) return `${message.direction}_${primaryField}`;
|
|
1700
|
+
return `${message.direction}_message`;
|
|
1701
|
+
}
|
|
1702
|
+
buildCondition(fields) {
|
|
1703
|
+
const statusValue = this.findStatusValue(fields);
|
|
1704
|
+
if (statusValue) return `status=${statusValue}`;
|
|
1705
|
+
const keys = Object.keys(fields).toSorted().slice(0, 2);
|
|
1706
|
+
if (keys.length === 0) return;
|
|
1707
|
+
const fragments = [];
|
|
1708
|
+
for (const key of keys) {
|
|
1709
|
+
const value = fields[key];
|
|
1710
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") fragments.push(`${key}=${value}`);
|
|
1711
|
+
}
|
|
1712
|
+
return fragments.length > 0 ? fragments.join(", ") : void 0;
|
|
1713
|
+
}
|
|
1714
|
+
buildAction(message) {
|
|
1715
|
+
const statusValue = this.findStatusValue(message.fields);
|
|
1716
|
+
if (this.containsRejectSignal(statusValue) || this.containsRejectSignal(message.raw)) return "reject";
|
|
1717
|
+
if (this.containsAcceptSignal(statusValue) || this.containsAcceptSignal(message.raw)) return "complete";
|
|
1718
|
+
const rawText = normalizeText(message.raw);
|
|
1719
|
+
if (rawText.includes("ack")) return "acknowledge";
|
|
1720
|
+
if (rawText.includes("retry")) return "retry";
|
|
1721
|
+
if (Object.keys(message.fields).length > 0 && isRecord(message.fields)) return message.direction === "req" ? "send" : "receive";
|
|
1722
|
+
}
|
|
1723
|
+
collectTerminalStates(states) {
|
|
1724
|
+
const terminalIds = states.filter((state) => {
|
|
1725
|
+
const stateType = state.type ?? "normal";
|
|
1726
|
+
return stateType === "accept" || stateType === "reject";
|
|
1727
|
+
}).map((state) => state.id);
|
|
1728
|
+
for (const state of states) {
|
|
1729
|
+
const lower = normalizeText(state.name);
|
|
1730
|
+
if (lower.includes("close") || lower.includes("fin") || lower.includes("bye")) {
|
|
1731
|
+
if (!terminalIds.includes(state.id)) terminalIds.push(state.id);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return terminalIds;
|
|
1735
|
+
}
|
|
1736
|
+
computeTransitionConfidence(message) {
|
|
1737
|
+
let confidence = .3;
|
|
1738
|
+
if (Object.keys(message.fields).length > 0) confidence += .3;
|
|
1739
|
+
if (this.findStatusValue(message.fields)) confidence += .2;
|
|
1740
|
+
if (message.raw.length > 0) confidence += .2;
|
|
1741
|
+
return Math.min(confidence, 1);
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
//#endregion
|
|
1745
|
+
//#region src/server/domains/protocol-analysis/handlers/base.ts
|
|
1746
|
+
const EMPTY_STATE_MACHINE = {
|
|
1747
|
+
states: [],
|
|
1748
|
+
transitions: [],
|
|
1749
|
+
initial: "",
|
|
1750
|
+
initialState: "",
|
|
1751
|
+
finalStates: []
|
|
1752
|
+
};
|
|
1753
|
+
var ProtocolAnalysisBaseHandlers = class {
|
|
1754
|
+
engine;
|
|
1755
|
+
inferrer;
|
|
1756
|
+
eventBus;
|
|
1757
|
+
constructor(engine, inferrer, eventBus) {
|
|
1758
|
+
this.engine = engine;
|
|
1759
|
+
this.inferrer = inferrer;
|
|
1760
|
+
this.eventBus = eventBus;
|
|
1761
|
+
}
|
|
1762
|
+
emitEvent(event, payload) {
|
|
1763
|
+
this.eventBus?.emit(event, {
|
|
1764
|
+
...payload,
|
|
1765
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
getEngine() {
|
|
1769
|
+
if (!this.engine) this.engine = new ProtocolPatternEngine();
|
|
1770
|
+
return this.engine;
|
|
1771
|
+
}
|
|
1772
|
+
getInferrer() {
|
|
1773
|
+
if (!this.inferrer) this.inferrer = new StateMachineInferrer();
|
|
1774
|
+
return this.inferrer;
|
|
1775
|
+
}
|
|
1776
|
+
errorMessage(error) {
|
|
1777
|
+
return error instanceof Error ? error.message : String(error);
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region src/server/domains/protocol-analysis/handlers/pattern-handlers.ts
|
|
1782
|
+
var ProtocolAnalysisPatternHandlers = class extends ProtocolAnalysisBaseHandlers {
|
|
1783
|
+
async handleDefinePattern(args) {
|
|
1784
|
+
try {
|
|
1785
|
+
const name = typeof args.name === "string" && args.name.trim().length > 0 ? args.name : "unnamed_pattern";
|
|
1786
|
+
const specObject = argObject(args, "spec");
|
|
1787
|
+
if (specObject) {
|
|
1788
|
+
const spec = parsePatternSpec(name, specObject);
|
|
1789
|
+
this.getEngine().definePattern(name, spec);
|
|
1790
|
+
return {
|
|
1791
|
+
patternId: name,
|
|
1792
|
+
pattern: this.getEngine().getPattern(name) ?? {
|
|
1793
|
+
name,
|
|
1794
|
+
fields: [],
|
|
1795
|
+
byteOrder: "big"
|
|
1796
|
+
},
|
|
1797
|
+
success: true
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const fields = (Array.isArray(args.fields) ? args.fields : []).map((field, index) => parseLegacyField(field, index));
|
|
1801
|
+
const byteOrder = args.byteOrder === "little" || args.byteOrder === "big" ? args.byteOrder : void 0;
|
|
1802
|
+
const encryption = parseEncryptionInfo(args.encryption);
|
|
1803
|
+
return {
|
|
1804
|
+
patternId: name,
|
|
1805
|
+
pattern: this.getEngine().definePattern(name, fields, {
|
|
1806
|
+
...byteOrder ? { byteOrder } : {},
|
|
1807
|
+
...encryption ? { encryption } : {}
|
|
1808
|
+
}),
|
|
1809
|
+
success: true
|
|
1810
|
+
};
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
return {
|
|
1813
|
+
patternId: "error",
|
|
1814
|
+
pattern: {
|
|
1815
|
+
name: "error",
|
|
1816
|
+
fields: [],
|
|
1817
|
+
byteOrder: "big"
|
|
1818
|
+
},
|
|
1819
|
+
success: false,
|
|
1820
|
+
error: this.errorMessage(error)
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
async handleAutoDetect(args) {
|
|
1825
|
+
try {
|
|
1826
|
+
const hexPayloads = (() => {
|
|
1827
|
+
const newPayloads = argStringArray(args, "hexPayloads");
|
|
1828
|
+
if (newPayloads.length > 0) return newPayloads;
|
|
1829
|
+
return argStringArray(args, "payloads");
|
|
1830
|
+
})();
|
|
1831
|
+
const detected = this.getEngine().autoDetect(hexPayloads);
|
|
1832
|
+
const patternName = typeof args.name === "string" && args.name.trim().length > 0 ? args.name : void 0;
|
|
1833
|
+
if (!detected) return {
|
|
1834
|
+
patterns: [this.getEngine().autoDetectPattern([], patternName ? { name: patternName } : {})],
|
|
1835
|
+
success: true
|
|
1836
|
+
};
|
|
1837
|
+
const namedPattern = {
|
|
1838
|
+
...detected,
|
|
1839
|
+
name: patternName ?? detected.name
|
|
1840
|
+
};
|
|
1841
|
+
this.getEngine().definePattern(namedPattern.name, namedPattern);
|
|
1842
|
+
const result = this.getEngine().getPattern(namedPattern.name) ?? {
|
|
1843
|
+
name: namedPattern.name,
|
|
1844
|
+
fields: [],
|
|
1845
|
+
byteOrder: "big"
|
|
1846
|
+
};
|
|
1847
|
+
this.emitEvent("protocol:pattern_detected", {
|
|
1848
|
+
patternName: namedPattern.name,
|
|
1849
|
+
confidence: 0
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
patterns: [result],
|
|
1853
|
+
success: true
|
|
1854
|
+
};
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
return {
|
|
1857
|
+
patterns: [],
|
|
1858
|
+
success: false,
|
|
1859
|
+
error: this.errorMessage(error)
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async handleInferFields(args) {
|
|
1864
|
+
try {
|
|
1865
|
+
const hexPayloads = argStringArray(args, "hexPayloads");
|
|
1866
|
+
return {
|
|
1867
|
+
success: true,
|
|
1868
|
+
fields: this.getEngine().inferFields(hexPayloads)
|
|
1869
|
+
};
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
return {
|
|
1872
|
+
fields: [],
|
|
1873
|
+
success: false,
|
|
1874
|
+
error: this.errorMessage(error)
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async handleExportSchema(args) {
|
|
1879
|
+
try {
|
|
1880
|
+
const patternId = argStringRequired(args, "patternId");
|
|
1881
|
+
const pattern = this.getEngine().getPattern(patternId);
|
|
1882
|
+
if (!pattern) return { schema: `// Error: pattern '${patternId}' not found` };
|
|
1883
|
+
return { schema: this.getEngine().exportProto(pattern) };
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
return { schema: `// Error: ${this.errorMessage(error)}` };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
async handleInferStateMachine(args) {
|
|
1889
|
+
try {
|
|
1890
|
+
const rawMessages = args.messages;
|
|
1891
|
+
if (!Array.isArray(rawMessages)) throw new Error("messages must be an array");
|
|
1892
|
+
const hasLegacyShape = rawMessages.some((message) => isRecord$1(message) && (message.direction === "in" || message.direction === "out"));
|
|
1893
|
+
let stateMachine;
|
|
1894
|
+
if (hasLegacyShape) {
|
|
1895
|
+
const legacyMessages = rawMessages.map((message, index) => {
|
|
1896
|
+
if (!isRecord$1(message)) throw new Error(`messages[${index}] must be an object`);
|
|
1897
|
+
const direction = message.direction;
|
|
1898
|
+
const payloadHex = typeof message.payloadHex === "string" ? message.payloadHex : "";
|
|
1899
|
+
const timestamp = typeof message.timestamp === "number" ? message.timestamp : void 0;
|
|
1900
|
+
const payload = Buffer.from(payloadHex.replace(/\s+/g, ""), "hex");
|
|
1901
|
+
if (direction !== "in" && direction !== "out") throw new Error(`messages[${index}].direction must be "in" or "out"`);
|
|
1902
|
+
return {
|
|
1903
|
+
direction,
|
|
1904
|
+
payload,
|
|
1905
|
+
...timestamp !== void 0 ? { timestamp } : {}
|
|
1906
|
+
};
|
|
1907
|
+
});
|
|
1908
|
+
stateMachine = this.getInferrer().inferStateMachine(legacyMessages);
|
|
1909
|
+
} else {
|
|
1910
|
+
const messages = rawMessages.map((message, index) => parseProtocolMessage(message, index));
|
|
1911
|
+
stateMachine = this.getInferrer().infer(messages);
|
|
1912
|
+
}
|
|
1913
|
+
if (args.simplify === true) stateMachine = this.getInferrer().simplify(stateMachine);
|
|
1914
|
+
return {
|
|
1915
|
+
stateMachine,
|
|
1916
|
+
mermaid: this.getInferrer().generateMermaid(stateMachine),
|
|
1917
|
+
success: true
|
|
1918
|
+
};
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
return {
|
|
1921
|
+
stateMachine: { ...EMPTY_STATE_MACHINE },
|
|
1922
|
+
success: false,
|
|
1923
|
+
error: this.errorMessage(error)
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
async handleVisualizeState(args) {
|
|
1928
|
+
try {
|
|
1929
|
+
const stateMachineValue = args.stateMachine;
|
|
1930
|
+
if (!isRecord$1(stateMachineValue)) return { mermaidDiagram: this.getInferrer().generateMermaid(EMPTY_STATE_MACHINE) };
|
|
1931
|
+
const states = Array.isArray(stateMachineValue.states) ? stateMachineValue.states : [];
|
|
1932
|
+
const transitions = Array.isArray(stateMachineValue.transitions) ? stateMachineValue.transitions : [];
|
|
1933
|
+
const initialState = typeof stateMachineValue.initialState === "string" ? stateMachineValue.initialState : "";
|
|
1934
|
+
const finalStates = Array.isArray(stateMachineValue.finalStates) ? stateMachineValue.finalStates.filter((state) => typeof state === "string") : [];
|
|
1935
|
+
return { mermaidDiagram: this.getInferrer().generateMermaid({
|
|
1936
|
+
states: states.filter((state) => isRecord$1(state)),
|
|
1937
|
+
transitions: transitions.filter((transition) => isRecord$1(transition)),
|
|
1938
|
+
initial: initialState,
|
|
1939
|
+
initialState,
|
|
1940
|
+
finalStates
|
|
1941
|
+
}) };
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
return { mermaidDiagram: `stateDiagram-v2\n note right of empty: ${this.errorMessage(error)}` };
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
//#endregion
|
|
1948
|
+
//#region src/server/domains/protocol-analysis/handlers/payload-handlers.ts
|
|
1949
|
+
var ProtocolAnalysisPayloadHandlers = class extends ProtocolAnalysisPatternHandlers {
|
|
1950
|
+
async handlePayloadTemplateBuild(args) {
|
|
1951
|
+
try {
|
|
1952
|
+
const rawFields = args.fields;
|
|
1953
|
+
if (!Array.isArray(rawFields)) throw new Error("fields must be an array");
|
|
1954
|
+
const { payload, segments } = buildPayloadFromTemplate(rawFields.map((field, index) => parsePayloadTemplateField(field, index)), parseEndian(args.endian));
|
|
1955
|
+
this.emitEvent("protocol:payload_built", {
|
|
1956
|
+
byteLength: payload.length,
|
|
1957
|
+
fieldCount: segments.length
|
|
1958
|
+
});
|
|
1959
|
+
return {
|
|
1960
|
+
hexPayload: payload.toString("hex"),
|
|
1961
|
+
byteLength: payload.length,
|
|
1962
|
+
fields: segments,
|
|
1963
|
+
success: true
|
|
1964
|
+
};
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
return {
|
|
1967
|
+
hexPayload: "",
|
|
1968
|
+
byteLength: 0,
|
|
1969
|
+
fields: [],
|
|
1970
|
+
success: false,
|
|
1971
|
+
error: this.errorMessage(error)
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
async handlePayloadMutate(args) {
|
|
1976
|
+
let originalHex = "";
|
|
1977
|
+
try {
|
|
1978
|
+
if (typeof args.hexPayload !== "string") throw new Error("hexPayload must be a string");
|
|
1979
|
+
originalHex = normalizeHexString(args.hexPayload, "hexPayload");
|
|
1980
|
+
const rawMutations = args.mutations;
|
|
1981
|
+
if (!Array.isArray(rawMutations)) throw new Error("mutations must be an array");
|
|
1982
|
+
let payload = Buffer.from(originalHex, "hex");
|
|
1983
|
+
const appliedMutations = [];
|
|
1984
|
+
for (const [index, rawMutation] of rawMutations.entries()) {
|
|
1985
|
+
const mutation = parsePayloadMutation(rawMutation, index);
|
|
1986
|
+
const result = applyPayloadMutation(payload, mutation, index);
|
|
1987
|
+
payload = result.payload;
|
|
1988
|
+
appliedMutations.push(result.summary);
|
|
1989
|
+
}
|
|
1990
|
+
this.emitEvent("protocol:payload_mutated", {
|
|
1991
|
+
byteLength: payload.length,
|
|
1992
|
+
mutationCount: appliedMutations.length
|
|
1993
|
+
});
|
|
1994
|
+
return {
|
|
1995
|
+
originalHex,
|
|
1996
|
+
mutatedHex: payload.toString("hex"),
|
|
1997
|
+
byteLength: payload.length,
|
|
1998
|
+
appliedMutations,
|
|
1999
|
+
success: true
|
|
2000
|
+
};
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
return {
|
|
2003
|
+
originalHex,
|
|
2004
|
+
mutatedHex: "",
|
|
2005
|
+
byteLength: 0,
|
|
2006
|
+
appliedMutations: [],
|
|
2007
|
+
success: false,
|
|
2008
|
+
error: this.errorMessage(error)
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
//#endregion
|
|
2014
|
+
//#region src/server/domains/protocol-analysis/handlers/link-layer-handlers.ts
|
|
2015
|
+
var ProtocolAnalysisLinkLayerHandlers = class extends ProtocolAnalysisPayloadHandlers {
|
|
2016
|
+
async handleEthernetFrameBuild(args) {
|
|
2017
|
+
try {
|
|
2018
|
+
const destinationMac = parseMacAddress(args.destinationMac, "destinationMac");
|
|
2019
|
+
const sourceMac = parseMacAddress(args.sourceMac, "sourceMac");
|
|
2020
|
+
const etherType = parseEtherType(args.etherType, "etherType");
|
|
2021
|
+
const frame = buildEthernetFrame(destinationMac, sourceMac, etherType, parseHexPayload$1(args.payloadHex, "payloadHex"));
|
|
2022
|
+
this.emitEvent("protocol:ethernet_frame_built", {
|
|
2023
|
+
byteLength: frame.length,
|
|
2024
|
+
etherType: `0x${etherType.toString(16).padStart(4, "0")}`
|
|
2025
|
+
});
|
|
2026
|
+
return {
|
|
2027
|
+
destinationMac: destinationMac.canonical,
|
|
2028
|
+
sourceMac: sourceMac.canonical,
|
|
2029
|
+
etherType,
|
|
2030
|
+
etherTypeHex: `0x${etherType.toString(16).padStart(4, "0")}`,
|
|
2031
|
+
byteLength: frame.length,
|
|
2032
|
+
headerHex: frame.subarray(0, 14).toString("hex"),
|
|
2033
|
+
frameHex: frame.toString("hex"),
|
|
2034
|
+
success: true
|
|
2035
|
+
};
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
return {
|
|
2038
|
+
destinationMac: "",
|
|
2039
|
+
sourceMac: "",
|
|
2040
|
+
etherType: 0,
|
|
2041
|
+
etherTypeHex: "0x0000",
|
|
2042
|
+
byteLength: 0,
|
|
2043
|
+
headerHex: "",
|
|
2044
|
+
frameHex: "",
|
|
2045
|
+
success: false,
|
|
2046
|
+
error: this.errorMessage(error)
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async handleArpBuild(args) {
|
|
2051
|
+
try {
|
|
2052
|
+
const operation = args.operation === "reply" ? "reply" : "request";
|
|
2053
|
+
const senderMac = parseMacAddress(args.senderMac, "senderMac");
|
|
2054
|
+
const targetMac = parseMacAddress(args.targetMac ?? "00:00:00:00:00:00", "targetMac");
|
|
2055
|
+
const senderIp = parseIpv4Address(args.senderIp, "senderIp");
|
|
2056
|
+
const targetIp = parseIpv4Address(args.targetIp, "targetIp");
|
|
2057
|
+
const payload = buildArpPayload({
|
|
2058
|
+
operation,
|
|
2059
|
+
hardwareType: args.hardwareType === void 0 ? 1 : parseNonNegativeInteger(args.hardwareType, "hardwareType"),
|
|
2060
|
+
protocolType: parseEtherType(args.protocolType ?? "ipv4", "protocolType"),
|
|
2061
|
+
hardwareSize: args.hardwareSize === void 0 ? 6 : parsePositiveInteger(args.hardwareSize, "hardwareSize"),
|
|
2062
|
+
protocolSize: args.protocolSize === void 0 ? 4 : parsePositiveInteger(args.protocolSize, "protocolSize"),
|
|
2063
|
+
senderMac,
|
|
2064
|
+
senderIp,
|
|
2065
|
+
targetMac,
|
|
2066
|
+
targetIp
|
|
2067
|
+
});
|
|
2068
|
+
this.emitEvent("protocol:arp_built", {
|
|
2069
|
+
operation,
|
|
2070
|
+
byteLength: payload.length
|
|
2071
|
+
});
|
|
2072
|
+
return {
|
|
2073
|
+
operation,
|
|
2074
|
+
byteLength: payload.length,
|
|
2075
|
+
payloadHex: payload.toString("hex"),
|
|
2076
|
+
senderMac: senderMac.canonical,
|
|
2077
|
+
senderIp: args.senderIp,
|
|
2078
|
+
targetMac: targetMac.canonical,
|
|
2079
|
+
targetIp: args.targetIp,
|
|
2080
|
+
success: true
|
|
2081
|
+
};
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
return {
|
|
2084
|
+
operation: null,
|
|
2085
|
+
byteLength: 0,
|
|
2086
|
+
payloadHex: "",
|
|
2087
|
+
senderMac: "",
|
|
2088
|
+
senderIp: "",
|
|
2089
|
+
targetMac: "",
|
|
2090
|
+
targetIp: "",
|
|
2091
|
+
success: false,
|
|
2092
|
+
error: this.errorMessage(error)
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/server/domains/protocol-analysis/handlers/ip-packet-handlers.ts
|
|
2099
|
+
var ProtocolAnalysisIpPacketHandlers = class extends ProtocolAnalysisLinkLayerHandlers {
|
|
2100
|
+
async handleRawIpPacketBuild(args) {
|
|
2101
|
+
try {
|
|
2102
|
+
const version = args.version === "ipv6" ? "ipv6" : "ipv4";
|
|
2103
|
+
const payload = parseHexPayload$1(args.payloadHex ?? "", "payloadHex");
|
|
2104
|
+
const protocol = parseIpProtocol(args.protocol, "protocol");
|
|
2105
|
+
const dscp = args.dscp === void 0 ? 0 : parseNonNegativeInteger(args.dscp, "dscp");
|
|
2106
|
+
const ecn = args.ecn === void 0 ? 0 : parseNonNegativeInteger(args.ecn, "ecn");
|
|
2107
|
+
if (dscp > 63) throw new Error("dscp must be between 0 and 63");
|
|
2108
|
+
if (ecn > 3) throw new Error("ecn must be between 0 and 3");
|
|
2109
|
+
if (version === "ipv4") {
|
|
2110
|
+
const ttl = args.ttl === void 0 ? 64 : parseByte(args.ttl, "ttl");
|
|
2111
|
+
const identification = args.identification === void 0 ? 0 : parseNonNegativeInteger(args.identification, "identification");
|
|
2112
|
+
const fragmentOffset = args.fragmentOffset === void 0 ? 0 : parseNonNegativeInteger(args.fragmentOffset, "fragmentOffset");
|
|
2113
|
+
if (identification > 65535) throw new Error("identification must be between 0 and 65535");
|
|
2114
|
+
if (fragmentOffset > 8191) throw new Error("fragmentOffset must be between 0 and 8191");
|
|
2115
|
+
const { packet, checksum } = buildIpv4Packet({
|
|
2116
|
+
sourceIp: parseIpAddress(args.sourceIp, "ipv4", "sourceIp"),
|
|
2117
|
+
destinationIp: parseIpAddress(args.destinationIp, "ipv4", "destinationIp"),
|
|
2118
|
+
protocol,
|
|
2119
|
+
payload,
|
|
2120
|
+
ttl,
|
|
2121
|
+
identification,
|
|
2122
|
+
dontFragment: args.dontFragment === true,
|
|
2123
|
+
moreFragments: args.moreFragments === true,
|
|
2124
|
+
fragmentOffset,
|
|
2125
|
+
dscp,
|
|
2126
|
+
ecn
|
|
2127
|
+
});
|
|
2128
|
+
this.emitEvent("protocol:ip_packet_built", {
|
|
2129
|
+
version,
|
|
2130
|
+
protocol,
|
|
2131
|
+
byteLength: packet.length
|
|
2132
|
+
});
|
|
2133
|
+
return {
|
|
2134
|
+
version,
|
|
2135
|
+
protocol,
|
|
2136
|
+
byteLength: packet.length,
|
|
2137
|
+
headerLength: 20,
|
|
2138
|
+
packetHex: packet.toString("hex"),
|
|
2139
|
+
headerHex: packet.subarray(0, 20).toString("hex"),
|
|
2140
|
+
payloadHex: payload.toString("hex"),
|
|
2141
|
+
checksumHex: checksum.toString(16).padStart(4, "0"),
|
|
2142
|
+
success: true
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
const hopLimit = args.hopLimit === void 0 ? args.ttl === void 0 ? 64 : parseByte(args.ttl, "ttl") : parseByte(args.hopLimit, "hopLimit");
|
|
2146
|
+
const flowLabel = args.flowLabel === void 0 ? 0 : parseNonNegativeInteger(args.flowLabel, "flowLabel");
|
|
2147
|
+
if (flowLabel > 1048575) throw new Error("flowLabel must be between 0 and 1048575");
|
|
2148
|
+
const packet = buildIpv6Packet({
|
|
2149
|
+
sourceIp: parseIpAddress(args.sourceIp, "ipv6", "sourceIp"),
|
|
2150
|
+
destinationIp: parseIpAddress(args.destinationIp, "ipv6", "destinationIp"),
|
|
2151
|
+
protocol,
|
|
2152
|
+
payload,
|
|
2153
|
+
hopLimit,
|
|
2154
|
+
dscp,
|
|
2155
|
+
ecn,
|
|
2156
|
+
flowLabel
|
|
2157
|
+
});
|
|
2158
|
+
this.emitEvent("protocol:ip_packet_built", {
|
|
2159
|
+
version,
|
|
2160
|
+
protocol,
|
|
2161
|
+
byteLength: packet.length
|
|
2162
|
+
});
|
|
2163
|
+
return {
|
|
2164
|
+
version,
|
|
2165
|
+
protocol,
|
|
2166
|
+
byteLength: packet.length,
|
|
2167
|
+
headerLength: 40,
|
|
2168
|
+
packetHex: packet.toString("hex"),
|
|
2169
|
+
headerHex: packet.subarray(0, 40).toString("hex"),
|
|
2170
|
+
payloadHex: payload.toString("hex"),
|
|
2171
|
+
checksumHex: null,
|
|
2172
|
+
success: true
|
|
2173
|
+
};
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
return {
|
|
2176
|
+
version: null,
|
|
2177
|
+
protocol: null,
|
|
2178
|
+
byteLength: 0,
|
|
2179
|
+
headerLength: 0,
|
|
2180
|
+
packetHex: "",
|
|
2181
|
+
headerHex: "",
|
|
2182
|
+
payloadHex: "",
|
|
2183
|
+
checksumHex: null,
|
|
2184
|
+
success: false,
|
|
2185
|
+
error: this.errorMessage(error)
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
async handleIcmpEchoBuild(args) {
|
|
2190
|
+
try {
|
|
2191
|
+
const operation = args.operation === "reply" ? "reply" : "request";
|
|
2192
|
+
const identifier = args.identifier === void 0 ? 0 : parseNonNegativeInteger(args.identifier, "identifier");
|
|
2193
|
+
const sequenceNumber = args.sequenceNumber === void 0 ? 0 : parseNonNegativeInteger(args.sequenceNumber, "sequenceNumber");
|
|
2194
|
+
if (identifier > 65535) throw new Error("identifier must be between 0 and 65535");
|
|
2195
|
+
if (sequenceNumber > 65535) throw new Error("sequenceNumber must be between 0 and 65535");
|
|
2196
|
+
const payload = parseHexPayload$1(args.payloadHex ?? "", "payloadHex");
|
|
2197
|
+
const { packet, checksum } = buildIcmpEcho({
|
|
2198
|
+
operation,
|
|
2199
|
+
identifier,
|
|
2200
|
+
sequenceNumber,
|
|
2201
|
+
payload
|
|
2202
|
+
});
|
|
2203
|
+
const checksumHex = checksum.toString(16).padStart(4, "0");
|
|
2204
|
+
this.emitEvent("protocol:icmp_echo_built", {
|
|
2205
|
+
operation,
|
|
2206
|
+
byteLength: packet.length,
|
|
2207
|
+
checksumHex
|
|
2208
|
+
});
|
|
2209
|
+
return {
|
|
2210
|
+
operation,
|
|
2211
|
+
identifier,
|
|
2212
|
+
sequenceNumber,
|
|
2213
|
+
checksum,
|
|
2214
|
+
checksumHex,
|
|
2215
|
+
byteLength: packet.length,
|
|
2216
|
+
packetHex: packet.toString("hex"),
|
|
2217
|
+
payloadHex: payload.toString("hex"),
|
|
2218
|
+
success: true
|
|
2219
|
+
};
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
return {
|
|
2222
|
+
operation: null,
|
|
2223
|
+
identifier: null,
|
|
2224
|
+
sequenceNumber: null,
|
|
2225
|
+
checksum: null,
|
|
2226
|
+
checksumHex: "",
|
|
2227
|
+
byteLength: 0,
|
|
2228
|
+
packetHex: "",
|
|
2229
|
+
payloadHex: "",
|
|
2230
|
+
success: false,
|
|
2231
|
+
error: this.errorMessage(error)
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
//#endregion
|
|
2237
|
+
//#region src/server/domains/protocol-analysis/handlers/checksum-handlers.ts
|
|
2238
|
+
var ProtocolAnalysisChecksumHandlers = class extends ProtocolAnalysisIpPacketHandlers {
|
|
2239
|
+
async handleChecksumApply(args) {
|
|
2240
|
+
try {
|
|
2241
|
+
const payload = parseHexPayload$1(args.hexPayload, "hexPayload");
|
|
2242
|
+
const rangeStart = args.startOffset === void 0 ? 0 : parseNonNegativeInteger(args.startOffset, "startOffset");
|
|
2243
|
+
const rangeEnd = args.endOffset === void 0 ? payload.length : parseNonNegativeInteger(args.endOffset, "endOffset");
|
|
2244
|
+
if (rangeStart > rangeEnd || rangeEnd > payload.length) throw new Error("checksum range must stay within the payload");
|
|
2245
|
+
const zeroOffset = args.zeroOffset === void 0 ? void 0 : parseNonNegativeInteger(args.zeroOffset, "zeroOffset");
|
|
2246
|
+
const zeroLength = args.zeroLength === void 0 ? 2 : parsePositiveInteger(args.zeroLength, "zeroLength");
|
|
2247
|
+
const writeOffset = args.writeOffset === void 0 ? zeroOffset : parseNonNegativeInteger(args.writeOffset, "writeOffset");
|
|
2248
|
+
const endian = parseChecksumEndian(args.endian);
|
|
2249
|
+
const working = Buffer.from(payload);
|
|
2250
|
+
if (zeroOffset !== void 0) {
|
|
2251
|
+
if (zeroOffset + zeroLength > working.length) throw new Error("zeroOffset and zeroLength must stay within the payload");
|
|
2252
|
+
working.fill(0, zeroOffset, zeroOffset + zeroLength);
|
|
2253
|
+
}
|
|
2254
|
+
const checksum = computeInternetChecksum(working.subarray(rangeStart, rangeEnd));
|
|
2255
|
+
if (writeOffset !== void 0) {
|
|
2256
|
+
if (writeOffset + 2 > working.length) throw new Error("writeOffset must leave room for a 16-bit checksum field");
|
|
2257
|
+
if (endian === "little") working.writeUInt16LE(checksum, writeOffset);
|
|
2258
|
+
else working.writeUInt16BE(checksum, writeOffset);
|
|
2259
|
+
}
|
|
2260
|
+
const checksumHex = checksum.toString(16).padStart(4, "0");
|
|
2261
|
+
this.emitEvent("protocol:checksum_applied", {
|
|
2262
|
+
checksumHex,
|
|
2263
|
+
byteLength: working.length
|
|
2264
|
+
});
|
|
2265
|
+
return {
|
|
2266
|
+
checksumHex,
|
|
2267
|
+
checksum,
|
|
2268
|
+
mutatedHex: working.toString("hex"),
|
|
2269
|
+
byteLength: working.length,
|
|
2270
|
+
rangeStart,
|
|
2271
|
+
rangeEnd,
|
|
2272
|
+
success: true
|
|
2273
|
+
};
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
return {
|
|
2276
|
+
checksumHex: "",
|
|
2277
|
+
checksum: 0,
|
|
2278
|
+
mutatedHex: "",
|
|
2279
|
+
byteLength: 0,
|
|
2280
|
+
rangeStart: 0,
|
|
2281
|
+
rangeEnd: 0,
|
|
2282
|
+
success: false,
|
|
2283
|
+
error: this.errorMessage(error)
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
//#endregion
|
|
2289
|
+
//#region src/server/domains/protocol-analysis/handlers/packet-build-handlers.ts
|
|
2290
|
+
/**
|
|
2291
|
+
* ProtocolAnalysisPacketBuildHandlers — thin facade over packet build handlers.
|
|
2292
|
+
*/
|
|
2293
|
+
var ProtocolAnalysisPacketBuildHandlers = class extends ProtocolAnalysisChecksumHandlers {};
|
|
2294
|
+
//#endregion
|
|
2295
|
+
//#region src/server/domains/protocol-analysis/handlers/pcap-handlers.ts
|
|
2296
|
+
var ProtocolAnalysisPcapHandlers = class extends ProtocolAnalysisPacketBuildHandlers {
|
|
2297
|
+
async handlePcapWrite(args) {
|
|
2298
|
+
try {
|
|
2299
|
+
const path = this.parseRequiredPath(args);
|
|
2300
|
+
if (!Array.isArray(args.packets)) throw new Error("packets must be an array");
|
|
2301
|
+
const packets = args.packets.map((entry, index) => parsePcapPacketInput(entry, index));
|
|
2302
|
+
const endianness = parsePacketEndianness(args.endianness);
|
|
2303
|
+
const timestampPrecision = parseTimestampPrecision(args.timestampPrecision);
|
|
2304
|
+
const snapLength = args.snapLength === void 0 ? 65535 : parsePositiveInteger(args.snapLength, "snapLength");
|
|
2305
|
+
const linkType = parsePcapLinkType(args.linkType ?? "ethernet", "linkType");
|
|
2306
|
+
const buffer = buildClassicPcap({
|
|
2307
|
+
packets,
|
|
2308
|
+
endianness,
|
|
2309
|
+
timestampPrecision,
|
|
2310
|
+
snapLength,
|
|
2311
|
+
linkType
|
|
2312
|
+
});
|
|
2313
|
+
await writeFile$1(path, buffer);
|
|
2314
|
+
this.emitEvent("protocol:pcap_written", {
|
|
2315
|
+
path,
|
|
2316
|
+
packetCount: packets.length,
|
|
2317
|
+
byteLength: buffer.length
|
|
2318
|
+
});
|
|
2319
|
+
return {
|
|
2320
|
+
path,
|
|
2321
|
+
packetCount: packets.length,
|
|
2322
|
+
byteLength: buffer.length,
|
|
2323
|
+
endianness,
|
|
2324
|
+
timestampPrecision,
|
|
2325
|
+
linkType,
|
|
2326
|
+
success: true
|
|
2327
|
+
};
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
return {
|
|
2330
|
+
path: typeof args.path === "string" ? args.path : "",
|
|
2331
|
+
packetCount: 0,
|
|
2332
|
+
byteLength: 0,
|
|
2333
|
+
endianness: null,
|
|
2334
|
+
timestampPrecision: null,
|
|
2335
|
+
linkType: null,
|
|
2336
|
+
success: false,
|
|
2337
|
+
error: this.errorMessage(error)
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
async handlePcapRead(args) {
|
|
2342
|
+
try {
|
|
2343
|
+
const path = this.parseRequiredPath(args);
|
|
2344
|
+
const maxPackets = args.maxPackets === void 0 ? void 0 : parsePositiveInteger(args.maxPackets, "maxPackets");
|
|
2345
|
+
const maxBytesPerPacket = args.maxBytesPerPacket === void 0 ? void 0 : parsePositiveInteger(args.maxBytesPerPacket, "maxBytesPerPacket");
|
|
2346
|
+
const { header, packets } = readClassicPcap(await readFile$1(path), maxPackets, maxBytesPerPacket);
|
|
2347
|
+
this.emitEvent("protocol:pcap_read", {
|
|
2348
|
+
path,
|
|
2349
|
+
packetCount: packets.length
|
|
2350
|
+
});
|
|
2351
|
+
return {
|
|
2352
|
+
path,
|
|
2353
|
+
header,
|
|
2354
|
+
packets,
|
|
2355
|
+
success: true
|
|
2356
|
+
};
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
return {
|
|
2359
|
+
path: typeof args.path === "string" ? args.path : "",
|
|
2360
|
+
header: null,
|
|
2361
|
+
packets: [],
|
|
2362
|
+
success: false,
|
|
2363
|
+
error: this.errorMessage(error)
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
parseRequiredPath(args) {
|
|
2368
|
+
if (typeof args.path !== "string" || args.path.trim().length === 0) throw new Error("path must be a non-empty string");
|
|
2369
|
+
return args.path;
|
|
2370
|
+
}
|
|
2371
|
+
};
|
|
2372
|
+
//#endregion
|
|
2373
|
+
//#region src/server/domains/protocol-analysis/handlers/packet-handlers.ts
|
|
2374
|
+
/**
|
|
2375
|
+
* ProtocolAnalysisPacketHandlers — thin facade over packet build + PCAP handlers.
|
|
2376
|
+
*/
|
|
2377
|
+
var ProtocolAnalysisPacketHandlers = class extends ProtocolAnalysisPcapHandlers {};
|
|
2378
|
+
//#endregion
|
|
2379
|
+
//#region src/server/domains/protocol-analysis/handlers/fingerprint-handlers.ts
|
|
2380
|
+
/**
|
|
2381
|
+
* ProtocolAnalysisFingerprintHandlers — protocol fingerprint heuristics.
|
|
2382
|
+
*/
|
|
2383
|
+
var ProtocolAnalysisFingerprintHandlers = class extends ProtocolAnalysisPacketHandlers {
|
|
2384
|
+
async handleProtoFingerprint(args) {
|
|
2385
|
+
const hexPayloads = argStringArray(args, "hexPayloads");
|
|
2386
|
+
const includeKnown = args.includeKnownProtocols !== false;
|
|
2387
|
+
const includeHints = args.includeFieldHints !== false;
|
|
2388
|
+
if (hexPayloads.length === 0) return asJsonResponse({
|
|
2389
|
+
success: false,
|
|
2390
|
+
error: "hexPayloads is required"
|
|
2391
|
+
});
|
|
2392
|
+
return asJsonResponse({
|
|
2393
|
+
success: true,
|
|
2394
|
+
fingerprints: hexPayloads.map((hex, index) => {
|
|
2395
|
+
const clean = hex.replace(/\s/g, "");
|
|
2396
|
+
const matches = [];
|
|
2397
|
+
const actualBytes = clean.length / 2;
|
|
2398
|
+
const tlsRecordLen = actualBytes >= 5 ? readU16(clean, 3) : -1;
|
|
2399
|
+
const hasCompleteTlsRecord = Number.isFinite(tlsRecordLen) && tlsRecordLen >= 0 && actualBytes >= 5 + tlsRecordLen;
|
|
2400
|
+
const isTlsClientHello = hasCompleteTlsRecord && tlsRecordLen >= PROTO_TLS_MIN_RECORD_LEN && readU8(clean, 0) === 22 && readU8(clean, 5) === 1;
|
|
2401
|
+
const isDns = isLikelyDnsHeader(clean);
|
|
2402
|
+
const isHttp = Object.keys(HTTP_METHODS).some((method) => clean.toUpperCase().startsWith(method));
|
|
2403
|
+
const isSsh = clean.toUpperCase().startsWith("5353482D");
|
|
2404
|
+
const isWs = clean.length >= 4 && (() => {
|
|
2405
|
+
const b0 = readU8(clean, 0);
|
|
2406
|
+
const b1 = readU8(clean, 1);
|
|
2407
|
+
const opcode = b0 & 15;
|
|
2408
|
+
if (opcode === 0) return false;
|
|
2409
|
+
const validOpcode = opcode <= 10 && !(opcode >= 3 && opcode <= 7);
|
|
2410
|
+
const masked = (b1 >> 7 & 1) === 1;
|
|
2411
|
+
const wsByteCount = clean.length / 2;
|
|
2412
|
+
let payloadLen = b1 & 127;
|
|
2413
|
+
let headerBytes = 2;
|
|
2414
|
+
if (payloadLen === 126) {
|
|
2415
|
+
if (wsByteCount < 4) return false;
|
|
2416
|
+
payloadLen = readU16(clean, 2);
|
|
2417
|
+
headerBytes = 4;
|
|
2418
|
+
} else if (payloadLen === 127) {
|
|
2419
|
+
if (wsByteCount < 10) return false;
|
|
2420
|
+
const hi32 = readU16(clean, 2) << 16 | readU16(clean, 4);
|
|
2421
|
+
const lo32 = readU16(clean, 6) << 16 | readU16(clean, 8);
|
|
2422
|
+
payloadLen = hi32 > 0 ? 4294967295 : lo32;
|
|
2423
|
+
headerBytes = 10;
|
|
2424
|
+
}
|
|
2425
|
+
return validOpcode && wsByteCount >= headerBytes + (masked ? 4 : 0) + payloadLen;
|
|
2426
|
+
})();
|
|
2427
|
+
let deepParse = null;
|
|
2428
|
+
if (isTlsClientHello) {
|
|
2429
|
+
matches.push({
|
|
2430
|
+
protocol: "TLS ClientHello",
|
|
2431
|
+
layer: "L6-TLS",
|
|
2432
|
+
confidence: PROTO_TLS_CONFIDENCE
|
|
2433
|
+
});
|
|
2434
|
+
if (includeHints) deepParse = parseTlsClientHello(clean);
|
|
2435
|
+
} else if (isHttp) {
|
|
2436
|
+
matches.push({
|
|
2437
|
+
protocol: "HTTP/1.x",
|
|
2438
|
+
layer: "L7-HTTP",
|
|
2439
|
+
confidence: PROTO_HTTP_CONFIDENCE
|
|
2440
|
+
});
|
|
2441
|
+
if (includeHints) deepParse = {
|
|
2442
|
+
method: Object.entries(HTTP_METHODS).find(([prefix]) => clean.toUpperCase().startsWith(prefix))?.[1] ?? "UNKNOWN",
|
|
2443
|
+
httpVersion: clean.indexOf("2048545450") > 0 ? "1.x" : "unknown"
|
|
2444
|
+
};
|
|
2445
|
+
} else if (isSsh) {
|
|
2446
|
+
matches.push({
|
|
2447
|
+
protocol: "SSH",
|
|
2448
|
+
layer: "L7-SSH",
|
|
2449
|
+
confidence: PROTO_SSH_CONFIDENCE
|
|
2450
|
+
});
|
|
2451
|
+
if (includeHints && clean.length >= 20) deepParse = { banner: Buffer.from(clean.substring(0, Math.min(clean.length, 80)), "hex").toString("ascii") };
|
|
2452
|
+
} else if (isWs) {
|
|
2453
|
+
matches.push({
|
|
2454
|
+
protocol: "WebSocket",
|
|
2455
|
+
layer: "L7-WS",
|
|
2456
|
+
confidence: PROTO_WS_CONFIDENCE
|
|
2457
|
+
});
|
|
2458
|
+
if (includeHints && clean.length >= 4) {
|
|
2459
|
+
const b0 = readU8(clean, 0);
|
|
2460
|
+
const b1 = readU8(clean, 1);
|
|
2461
|
+
const opcode = b0 & 15;
|
|
2462
|
+
const masked = b1 >> 7 & 1;
|
|
2463
|
+
let payloadLen = b1 & 127;
|
|
2464
|
+
let headerSize = 2;
|
|
2465
|
+
if (payloadLen === 126) {
|
|
2466
|
+
payloadLen = clean.length >= 4 ? readU16(clean, 2) : 0;
|
|
2467
|
+
headerSize = 4;
|
|
2468
|
+
} else if (payloadLen === 127) {
|
|
2469
|
+
if (clean.length >= 20) {
|
|
2470
|
+
const hi32 = readU16(clean, 2) << 16 | readU16(clean, 4);
|
|
2471
|
+
const lo32 = readU16(clean, 6) << 16 | readU16(clean, 8);
|
|
2472
|
+
payloadLen = hi32 > 0 ? 4294967295 : lo32;
|
|
2473
|
+
} else payloadLen = 0;
|
|
2474
|
+
headerSize = 10;
|
|
2475
|
+
}
|
|
2476
|
+
if (masked) headerSize += 4;
|
|
2477
|
+
deepParse = {
|
|
2478
|
+
fin: b0 >> 7 & 1,
|
|
2479
|
+
rsv1: b0 >> 6 & 1,
|
|
2480
|
+
opcode,
|
|
2481
|
+
opcodeName: WS_OPCODES[opcode] ?? `reserved(${opcode})`,
|
|
2482
|
+
masked: !!masked,
|
|
2483
|
+
payloadLength: payloadLen,
|
|
2484
|
+
headerSize
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
} else if (isDns) {
|
|
2488
|
+
matches.push({
|
|
2489
|
+
protocol: "DNS",
|
|
2490
|
+
layer: "L7-DNS",
|
|
2491
|
+
confidence: .85
|
|
2492
|
+
});
|
|
2493
|
+
if (includeHints) deepParse = parseDnsHeader(clean);
|
|
2494
|
+
}
|
|
2495
|
+
if (includeKnown && matches.length === 0) {
|
|
2496
|
+
if (hasCompleteTlsRecord && /^160301|^160302|^160303/i.test(clean.substring(0, 8))) matches.push({
|
|
2497
|
+
protocol: "TLS Record",
|
|
2498
|
+
layer: "L6-TLS",
|
|
2499
|
+
confidence: .9
|
|
2500
|
+
});
|
|
2501
|
+
if (clean.substring(0, 8).startsWith("50524920")) matches.push({
|
|
2502
|
+
protocol: "HTTP/2 PRI",
|
|
2503
|
+
layer: "L7-HTTP2",
|
|
2504
|
+
confidence: .9
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
const fieldHints = [];
|
|
2508
|
+
if (includeHints && !deepParse && clean.length >= 8) {
|
|
2509
|
+
const first2 = readU16(clean, 0);
|
|
2510
|
+
if (first2 > 0 && first2 < clean.length / 2) fieldHints.push({
|
|
2511
|
+
offset: 0,
|
|
2512
|
+
hint: `possible length field (${first2} bytes)`
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
return {
|
|
2516
|
+
index,
|
|
2517
|
+
size: actualBytes,
|
|
2518
|
+
protocolMatches: matches.length > 0 ? matches : [{
|
|
2519
|
+
protocol: "unknown",
|
|
2520
|
+
layer: "unknown",
|
|
2521
|
+
confidence: 0
|
|
2522
|
+
}],
|
|
2523
|
+
...deepParse ? { parsedFields: deepParse } : {},
|
|
2524
|
+
...fieldHints.length > 0 ? { fieldHints } : {}
|
|
2525
|
+
};
|
|
2526
|
+
})
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
//#endregion
|
|
2531
|
+
//#region src/server/domains/protocol-analysis/handlers/handler-class.ts
|
|
2532
|
+
/**
|
|
2533
|
+
* ProtocolAnalysisHandlers — thin facade over the split handler chain.
|
|
2534
|
+
*/
|
|
2535
|
+
var ProtocolAnalysisHandlers = class extends ProtocolAnalysisFingerprintHandlers {};
|
|
2536
|
+
//#endregion
|
|
2537
|
+
export { ProtocolAnalysisHandlers };
|