@jshookmcp/jshook 0.2.9 → 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 +2 -2
- package/README.zh.md +2 -2
- package/dist/{AntiCheatDetector-BNk-EoBt.mjs → AntiCheatDetector-CqGDXmfc.mjs} +159 -53
- package/dist/{CodeInjector-Cq8q01kp.mjs → CodeInjector-BdjRfNx7.mjs} +5 -5
- package/dist/{ConsoleMonitor-CPVQW1Y-.mjs → ConsoleMonitor-DykL3IAw.mjs} +85 -17
- package/dist/{DetailedDataManager-BQQcxh64.mjs → DetailedDataManager-HT49OrvF.mjs} +1 -1
- package/dist/{ExtensionManager-CWYgw0YW.mjs → ExtensionManager-BDMsY2Dz.mjs} +15 -8
- package/dist/{HardwareBreakpoint-B9gZCdFP.mjs → HardwareBreakpoint-Cc2AFq1Y.mjs} +3 -3
- package/dist/{HeapAnalyzer-BLDH0dCv.mjs → HeapAnalyzer-DruMgsgj.mjs} +20 -20
- package/dist/{HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs → HookGeneratorBuilders.core.generators.storage-CTbB4Lcx.mjs} +1 -74
- package/dist/{InstrumentationSession-CvPC7Jwy.mjs → InstrumentationSession-DLH0vd-z.mjs} +2 -2
- package/dist/{MemoryController-CbVdCIJF.mjs → MemoryController-CMtviNW_.mjs} +3 -3
- package/dist/{MemoryScanSession-BsDZbLYm.mjs → MemoryScanSession-ITgb_NMi.mjs} +2 -2
- package/dist/{MemoryScanner-Bcpml6II.mjs → MemoryScanner-CiL7Z3ey.mjs} +12 -9
- package/dist/{NativeMemoryManager.impl-dZtA1ZGn.mjs → NativeMemoryManager.impl-D9Lkovvn.mjs} +13 -10
- package/dist/{NativeMemoryManager.utils-B-FjA2mJ.mjs → NativeMemoryManager.utils-BBlAixF5.mjs} +1 -1
- package/dist/{PEAnalyzer-D1lzJ_VG.mjs → PEAnalyzer-DMQ44gen.mjs} +15 -15
- package/dist/{PageController-Bqm2kZ_X.mjs → PageController-BPJNqqBN.mjs} +18 -4
- package/dist/{PointerChainEngine-BOhyVsjx.mjs → PointerChainEngine-K7wN8Z-w.mjs} +10 -7
- package/dist/ProcessRegistry-zGg12QbE.mjs +74 -0
- package/dist/{ResponseBuilder-D3iFYx2N.mjs → ResponseBuilder-CJXWmWNw.mjs} +10 -10
- package/dist/{ScriptManager-aHHq0X7U.mjs → ScriptManager-ZuWD-0Jg.mjs} +195 -192
- package/dist/{Speedhack-CqdIFlQl.mjs → Speedhack-D-z0umeT.mjs} +2 -2
- package/dist/{StructureAnalyzer-DhFaPvRO.mjs → StructureAnalyzer-Cav5AVSL.mjs} +9 -6
- package/dist/{ToolCatalog-C0JGZoOm.mjs → ToolCatalog-5OJdMiF0.mjs} +81 -81
- package/dist/{ToolProbe-oC7aPrkv.mjs → ToolProbe-DbCFGyrg.mjs} +1 -1
- package/dist/{ToolRegistry-BjaF4oNz.mjs → ToolRegistry-B9krbTtI.mjs} +51 -2
- package/dist/{ToolRouter.policy-BWV67ZK-.mjs → ToolRouter.policy-BGDAGyeH.mjs} +60 -20
- package/dist/TraceRecorder-B41Z5XBj.mjs +1286 -0
- package/dist/{Win32API-CePkipZY.mjs → Win32API-C2kjj0ze.mjs} +18 -12
- package/dist/{Win32Debug-BvKs-gxc.mjs → Win32Debug-CKrGOTpo.mjs} +2 -2
- package/dist/{WorkflowEngine-CuvkZtWu.mjs → WorkflowEngine-DJ6M4opp.mjs} +226 -255
- package/dist/analysis-BHeJW2Nb.mjs +1234 -0
- package/dist/{antidebug-CqDTB_uk.mjs → antidebug-BRKeyt27.mjs} +3 -3
- package/dist/{artifactRetention-CFEprwPw.mjs → artifactRetention-CPXkUJXp.mjs} +13 -6
- package/dist/{artifacts-Bk2-_uPq.mjs → artifacts-DkfosXH3.mjs} +1 -1
- package/dist/authorization-schema-DRqyJMSk.mjs +31 -0
- package/dist/{binary-instrument-CXfpx6fT.mjs → binary-instrument--V3MAhJ4.mjs} +19 -27
- package/dist/bind-helpers-ClV34xdn.mjs +42 -0
- package/dist/{boringssl-inspector-BH2D3VKc.mjs → boringssl-inspector-Bo_LOLaS.mjs} +1 -1
- package/dist/{browser-BpOr5PEx.mjs → browser-Dx3_S2cG.mjs} +324 -37
- package/dist/capabilities-CcHlvWgK.mjs +33 -0
- package/dist/{constants-B0OANIBL.mjs → constants-CDZLOoVv.mjs} +18 -3
- package/dist/{coordination-qUbyF8KU.mjs → coordination-DgItD9DL.mjs} +2 -2
- package/dist/{debugger-gnKxRSN0.mjs → debugger-RS3RSAqs.mjs} +30 -13
- package/dist/definitions-BEoYofW5.mjs +47 -0
- package/dist/{definitions-bAhHQJq9.mjs → definitions-BRaefg3u.mjs} +11 -5
- package/dist/{definitions-DVGfrn7y.mjs → definitions-BbkvZkiv.mjs} +2 -2
- package/dist/definitions-BtWSHJ3o.mjs +17 -0
- package/dist/{definitions-BMfYXoNC.mjs → definitions-C1gCHO0i.mjs} +1 -1
- package/dist/{definitions-C1UvM5Iy.mjs → definitions-CDOg_b-l.mjs} +14 -2
- package/dist/definitions-CVPD9hzZ.mjs +54 -0
- package/dist/{definitions-Cke7zEb8.mjs → definitions-Cea8Lgl7.mjs} +1 -1
- package/dist/definitions-DAgIyjxM.mjs +10 -0
- package/dist/{definitions-B4rAvHNZ.mjs → definitions-DJA27nsL.mjs} +12 -9
- package/dist/{definitions-ClJLzsJQ.mjs → definitions-DKPFU3LW.mjs} +1 -1
- package/dist/{definitions-D3VsGcvz.mjs → definitions-DPRpZQ96.mjs} +7 -7
- package/dist/{definitions-B18eyf0B.mjs → definitions-DUE5gmdn.mjs} +1 -1
- package/dist/definitions-DYVjOtxa.mjs +26 -0
- package/dist/{definitions-BB_4jnmy.mjs → definitions-DcYLVLCo.mjs} +1 -1
- package/dist/{definitions-Beid2EB3.mjs → definitions-Pp5LI2H4.mjs} +1 -1
- package/dist/definitions-j9KdHVNR.mjs +14 -0
- package/dist/definitions-uzkjBwa7.mjs +258 -0
- package/dist/{definitions-Cq-zroAU.mjs → definitions-va-AnLuQ.mjs} +4 -4
- package/dist/{encoding-Bvz5jLRv.mjs → encoding-DJeqHmpd.mjs} +18 -4
- package/dist/{evidence-graph-bridge-C_fv9PuC.mjs → evidence-graph-bridge-DcYizFk2.mjs} +1 -0
- package/dist/{factory-DxlGh9Xf.mjs → factory-C90tBff6.mjs} +6 -6
- package/dist/flat-target-session-Dgax2Cy3.mjs +29 -0
- package/dist/{graphql-DYWzJ29s.mjs → graphql-CoHrhweh.mjs} +205 -34
- package/dist/{handlers-C67ktuRN.mjs → handlers-4jmR0nMs.mjs} +220 -32
- package/dist/{handlers-DlCJN4Td.mjs → handlers-BAHPxcch.mjs} +122 -90
- package/dist/{handlers-9sAbfIg-.mjs → handlers-BOs9b907.mjs} +849 -801
- package/dist/{handlers-DxGIq15_2.mjs → handlers-BWXEy6ef.mjs} +16 -16
- package/dist/{handlers-tB9Mp9ZK.mjs → handlers-Bndn6QvE.mjs} +31 -4
- package/dist/{handlers-CTsDAO6p.mjs → handlers-BqC4bD4s.mjs} +1 -1
- package/dist/{handlers-C87g8oCe.mjs → handlers-BtYq60bM2.mjs} +1 -1
- package/dist/{handlers-DeLOCd5m.mjs → handlers-BzgcB4iv.mjs} +17 -17
- package/dist/{handlers-Cgyg6c0U.mjs → handlers-CRyRWj2b.mjs} +237 -23
- package/dist/{handlers-U6L4xhuF.mjs → handlers-CVv2H1uq.mjs} +24 -17
- package/dist/{handlers-tiy7EIBp.mjs → handlers-Dl5a7JS4.mjs} +3 -3
- package/dist/{handlers-D6j6yka7.mjs → handlers-Dx2d7jt7.mjs} +1893 -1480
- package/dist/{handlers-Bl8zkwz1.mjs → handlers-Dz9PYsCa.mjs} +95 -6
- package/dist/handlers-HujRKC3b.mjs +661 -0
- package/dist/{handlers.impl-DS0d9fUw.mjs → handlers.impl-XWXkQfyi.mjs} +70 -24
- package/dist/{hooks-CzCWByww.mjs → hooks-B1B8NRHL.mjs} +3 -3
- package/dist/index.mjs +154 -144
- package/dist/{maintenance-P7ePRXQC.mjs → maintenance-PRMkLVRW.mjs} +35 -30
- package/dist/manifest-67Bok-Si.mjs +58 -0
- package/dist/{manifest-B3QVVeBS.mjs → manifest-6lNTMZAB2.mjs} +33 -28
- package/dist/manifest-B2duEHiH.mjs +90 -0
- package/dist/manifest-B6EY9Vm8.mjs +57 -0
- package/dist/{manifest-gZ4s_UtG.mjs → manifest-B6nKSbyY.mjs} +32 -33
- package/dist/{manifest-2ToTpjv8.mjs → manifest-BL8AQNPF.mjs} +31 -31
- package/dist/{manifest-DzwvxPJX.mjs → manifest-BSZvJJmV.mjs} +23 -14
- package/dist/{manifest-Sc_0JQ13.mjs → manifest-BU7qzUyX.mjs} +23 -23
- package/dist/{manifest-CT7zZBV1.mjs → manifest-Bl62e8WK.mjs} +24 -23
- 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-BqrQ4Tpj.mjs → manifest-C-xtsjS3.mjs} +23 -23
- package/dist/{manifest-NXctwWQq.mjs → manifest-CDYl7OhA.mjs} +36 -38
- 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-CAhOuvSl.mjs → manifest-D7YZM_2e.mjs} +75 -85
- package/dist/{manifest-DCyjf4n2.mjs → manifest-DE_VrAeQ.mjs} +27 -7
- package/dist/manifest-DGsXSCpT.mjs +39 -0
- package/dist/{manifest-BB2J8IMJ.mjs → manifest-DJ2vfEuW.mjs} +48 -41
- package/dist/{manifest-3g71z6Bg.mjs → manifest-DPXDYhEu.mjs} +26 -25
- package/dist/manifest-Dd4fQb0a.mjs +322 -0
- package/dist/{manifest-CXsRWjjI.mjs → manifest-Deq6opGg.mjs} +95 -96
- package/dist/{manifest-C9RT5nk32.mjs → manifest-DfJTafJK.mjs} +14 -11
- package/dist/manifest-DgOdgN_j.mjs +50 -0
- package/dist/{manifest-BmtZzQiQ2.mjs → manifest-DlbMW4v4.mjs} +17 -15
- package/dist/{manifest-DrbmZcFl2.mjs → manifest-DmVfbH0w.mjs} +212 -91
- 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-Dh8WBmEW.mjs → manifest-cEJU1v0Z.mjs} +24 -24
- package/dist/manifest-wOl5XLB12.mjs +112 -0
- package/dist/{modules-C184v-S9.mjs → modules-tZozf0LQ.mjs} +130 -860
- package/dist/{mojo-ipc-B_H61Afw.mjs → mojo-ipc-DXNEXEqb.mjs} +141 -26
- package/dist/{network-671Cw6hV.mjs → network-CPVvwvFg.mjs} +1329 -823
- package/dist/{outputPaths-B1uGmrWZ.mjs → outputPaths-um7lCRY3.mjs} +4 -8
- package/dist/{platform-WmNn8Sxb.mjs → platform-CYeFoTWp.mjs} +101 -10
- package/dist/{process-QcbIy5Zq.mjs → process-BTbgcVc6.mjs} +251 -346
- package/dist/{proxy-DqNs0bAd.mjs → proxy-r8YN6nP1.mjs} +30 -8
- package/dist/{registry-D-6e18lB.mjs → registry-Bl8ZQW61.mjs} +3 -3
- package/dist/{response-BQVP-xUn.mjs → response-CWhh2aLo.mjs} +7 -1
- package/dist/{shared-state-board-DV-dpHFJ.mjs → shared-state-board-BoZnSoj-.mjs} +2 -2
- package/dist/{sourcemap-Dq8ez8vS.mjs → sourcemap-BIDHUVXy.mjs} +350 -66
- package/dist/{streaming-BUQ0VJsg.mjs → streaming-Dal6utPp.mjs} +13 -13
- package/dist/{tool-builder-DCbIC5Eo.mjs → tool-builder-BHJp32mV.mjs} +1 -1
- package/dist/{transform-CiYJfNX0.mjs → transform-DRVgGG90.mjs} +18 -14
- package/dist/wasm-BYx5UOeG.mjs +1044 -0
- package/dist/webcrack-Be0_FccV.mjs +747 -0
- package/dist/{workflow-f3xJOcjx.mjs → workflow-BpuKEtvn.mjs} +8 -8
- package/package.json +76 -43
- package/dist/TraceRecorder-DgxyVbdQ.mjs +0 -519
- package/dist/analysis-CL9uACt9.mjs +0 -463
- package/dist/bind-helpers-xFfRF-qm.mjs +0 -22
- package/dist/definitions-6M-eejaT.mjs +0 -53
- package/dist/definitions-B3QdlrHv.mjs +0 -34
- package/dist/definitions-CXEI7QC72.mjs +0 -216
- package/dist/definitions-C_4r7Fo-2.mjs +0 -14
- package/dist/definitions-CkFDALoa.mjs +0 -26
- package/dist/definitions-Cy3Sl6gV.mjs +0 -34
- package/dist/definitions-LKpC3-nL.mjs +0 -9
- package/dist/handlers-DdFzXLvF.mjs +0 -446
- package/dist/manifest-82baTv4U.mjs +0 -45
- package/dist/manifest-BKbgbSiY.mjs +0 -60
- package/dist/manifest-Bcf-TJzH.mjs +0 -848
- package/dist/manifest-Bnd7kqEY.mjs +0 -55
- package/dist/manifest-BqQX6OQC2.mjs +0 -65
- package/dist/manifest-Br4RPFt5.mjs +0 -370
- package/dist/manifest-C5qDjysN.mjs +0 -107
- package/dist/manifest-CBYWCUBJ.mjs +0 -51
- package/dist/manifest-CFADCRa1.mjs +0 -37
- package/dist/manifest-CQVhavRF.mjs +0 -114
- package/dist/manifest-CV12bcrF.mjs +0 -121
- package/dist/manifest-CZLUCfG02.mjs +0 -95
- package/dist/manifest-D6phHKFd.mjs +0 -131
- package/dist/manifest-DHsnKgP6.mjs +0 -60
- package/dist/manifest-Df_dliIe.mjs +0 -55
- package/dist/manifest-DhKRAT8_.mjs +0 -92
- package/dist/manifest-DlpTj4ic2.mjs +0 -193
- package/dist/manifest-DuwHjUa5.mjs +0 -70
- package/dist/manifest-qSleDqdO.mjs +0 -1023
- package/dist/wasm-DQTnHDs4.mjs +0 -531
- /package/dist/{CacheAdapters-CDe5WPSV.mjs → CacheAdapters-jJFy20G-.mjs} +0 -0
- /package/dist/{DarwinAPI-BNPxu0RH.mjs → DarwinAPI-ETyy0xyo.mjs} +0 -0
- /package/dist/{EventBus-DgPmwpeu.mjs → EventBus-DFKvADm3.mjs} +0 -0
- /package/dist/{EvidenceGraphBridge-SFesNera.mjs → EvidenceGraphBridge-318Oi0Lf.mjs} +0 -0
- /package/dist/{FingerprintManager-gzWtkKuf.mjs → FingerprintManager-BN4UQWnX.mjs} +0 -0
- /package/dist/{PrerequisiteError-Dl33Svkz.mjs → PrerequisiteError-TuyZIs6n.mjs} +0 -0
- /package/dist/{ReverseEvidenceGraph-Dlsk94LC.mjs → ReverseEvidenceGraph-C02-gXOh.mjs} +0 -0
- /package/dist/{StealthVerifier-Bo4T3bz8.mjs → StealthVerifier-BWmPgQsv.mjs} +0 -0
- /package/dist/{VersionDetector-CwVLVdDM.mjs → VersionDetector-K3V4vGsw.mjs} +0 -0
- /package/dist/{betterSqlite3-0pqusHHH.mjs → betterSqlite3-DLSBZodi.mjs} +0 -0
- /package/dist/{concurrency-Bt0yv1kJ.mjs → concurrency-Drev_Vz9.mjs} +0 -0
- /package/dist/{formatAddress-DVkj9kpI.mjs → formatAddress-nnMvEohD.mjs} +0 -0
- /package/dist/{parse-args-BlRjqlkL.mjs → parse-args-B4cY5Vx5.mjs} +0 -0
- /package/dist/{ssrf-policy-ZaUfvhq7.mjs → ssrf-policy-Dsqd-DTX.mjs} +0 -0
- /package/dist/{types-CPhOReNX.mjs → types-DDBWs9UP.mjs} +0 -0
|
@@ -1,1531 +1,1756 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
4
|
import { readFile as readFile$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
3
5
|
import { isIP } from "node:net";
|
|
4
|
-
//#region src/
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
};
|
|
18
192
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
23
207
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
212
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
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
|
+
};
|
|
30
229
|
}
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|
|
36
248
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
};
|
|
40
261
|
}
|
|
41
|
-
function
|
|
42
|
-
if (
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
};
|
|
53
275
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
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
|
+
};
|
|
56
292
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
322
|
}
|
|
65
|
-
function
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
if (buffer.length === 2) return byteOrder === "le" ? buffer.readUInt16LE(0) : buffer.readUInt16BE(0);
|
|
69
|
-
if (buffer.length === 4) return byteOrder === "le" ? buffer.readUInt32LE(0) : buffer.readUInt32BE(0);
|
|
70
|
-
if (buffer.length === 8) {
|
|
71
|
-
const value = byteOrder === "le" ? Number(buffer.readBigUInt64LE(0)) : Number(buffer.readBigUInt64BE(0));
|
|
72
|
-
return Number.isFinite(value) ? value : null;
|
|
73
|
-
}
|
|
74
|
-
let value = 0;
|
|
75
|
-
const bytes = byteOrder === "le" ? [...buffer.values()].toReversed() : [...buffer.values()];
|
|
76
|
-
for (const byte of bytes) value = value * 256 + byte;
|
|
77
|
-
return Number.isFinite(value) ? value : null;
|
|
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;
|
|
78
326
|
}
|
|
79
|
-
function
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
return Number.isFinite(value) ? value : null;
|
|
83
|
-
}
|
|
84
|
-
if (buffer.length === 8) {
|
|
85
|
-
const value = byteOrder === "le" ? buffer.readDoubleLE(0) : buffer.readDoubleBE(0);
|
|
86
|
-
return Number.isFinite(value) ? value : null;
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
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;
|
|
89
330
|
}
|
|
90
|
-
function
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
let start = 0;
|
|
94
|
-
let index = buffer.indexOf(delimiter, start);
|
|
95
|
-
while (index >= 0) {
|
|
96
|
-
count += 1;
|
|
97
|
-
start = index + delimiter.length;
|
|
98
|
-
index = buffer.indexOf(delimiter, start);
|
|
99
|
-
}
|
|
100
|
-
return count;
|
|
331
|
+
function parseInteger(value, label) {
|
|
332
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label} must be an integer`);
|
|
333
|
+
return value;
|
|
101
334
|
}
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
if (samples.every((sample) => sample.length === 4) && looksLikeFloatSamples(samples)) return "float";
|
|
107
|
-
if (samples.every((sample) => sample.length <= 4)) return "int";
|
|
108
|
-
return "bytes";
|
|
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;
|
|
109
339
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
for (const sample of samples) {
|
|
113
|
-
const value = decodeFloat(sample, "be");
|
|
114
|
-
if (value === null) return false;
|
|
115
|
-
decoded.push(value);
|
|
116
|
-
}
|
|
117
|
-
return decoded.some((value) => Math.abs(value) > .001 && Math.abs(value) < 1e6);
|
|
340
|
+
function parseOptionalLength(value, label) {
|
|
341
|
+
return value === void 0 ? void 0 : parsePositiveInteger(value, label);
|
|
118
342
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const value = buffer[offset];
|
|
124
|
-
if (value === void 0) continue;
|
|
125
|
-
valueCount += 1;
|
|
126
|
-
if (isPrintableByte(value)) printableCount += 1;
|
|
127
|
-
}
|
|
128
|
-
return valueCount > 0 && printableCount / valueCount >= .8;
|
|
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;
|
|
129
347
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const value = buffer[offset];
|
|
134
|
-
if (value === void 0) continue;
|
|
135
|
-
valueCount += 1;
|
|
136
|
-
if (value !== 0 && value !== 1) return false;
|
|
137
|
-
}
|
|
138
|
-
return valueCount > 0;
|
|
348
|
+
function expectString(value, label) {
|
|
349
|
+
if (typeof value !== "string") throw new Error(`${label} must be a string`);
|
|
350
|
+
return value;
|
|
139
351
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const tokenCount = firstRow.length;
|
|
146
|
-
if (!tokenized.every((parts) => parts.length === tokenCount)) return [];
|
|
147
|
-
const fields = [];
|
|
148
|
-
let currentOffset = 0;
|
|
149
|
-
for (let index = 0; index < tokenCount; index += 1) {
|
|
150
|
-
const template = firstRow[index];
|
|
151
|
-
if (!template) continue;
|
|
152
|
-
const samples = tokenized.map((parts) => parts[index]).filter((part) => Buffer.isBuffer(part));
|
|
153
|
-
fields.push({
|
|
154
|
-
name: `field_${index + 1}`,
|
|
155
|
-
offset: currentOffset,
|
|
156
|
-
length: template.length,
|
|
157
|
-
type: inferFieldType(samples)
|
|
158
|
-
});
|
|
159
|
-
currentOffset += template.length + delimiter.length;
|
|
160
|
-
}
|
|
161
|
-
return fields;
|
|
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();
|
|
162
357
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let end = offset + 1;
|
|
170
|
-
while (end < minLength && isPrintableColumn(buffers, end)) end += 1;
|
|
171
|
-
fields.push({
|
|
172
|
-
name: `field_${fields.length + 1}`,
|
|
173
|
-
offset,
|
|
174
|
-
length: end - offset,
|
|
175
|
-
type: "string"
|
|
176
|
-
});
|
|
177
|
-
offset = end;
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (isBooleanColumn(buffers, offset)) {
|
|
181
|
-
fields.push({
|
|
182
|
-
name: `field_${fields.length + 1}`,
|
|
183
|
-
offset,
|
|
184
|
-
length: 1,
|
|
185
|
-
type: "bool"
|
|
186
|
-
});
|
|
187
|
-
offset += 1;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const remaining = minLength - offset;
|
|
191
|
-
if (remaining >= 4) {
|
|
192
|
-
if (looksLikeFloatSamples(buffers.map((buffer) => buffer.subarray(offset, offset + 4)))) {
|
|
193
|
-
fields.push({
|
|
194
|
-
name: `field_${fields.length + 1}`,
|
|
195
|
-
offset,
|
|
196
|
-
length: 4,
|
|
197
|
-
type: "float"
|
|
198
|
-
});
|
|
199
|
-
offset += 4;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const segmentLength = remaining >= 4 ? 4 : Math.min(remaining, 2);
|
|
204
|
-
const samples = buffers.map((buffer) => buffer.subarray(offset, offset + segmentLength));
|
|
205
|
-
fields.push({
|
|
206
|
-
name: `field_${fields.length + 1}`,
|
|
207
|
-
offset,
|
|
208
|
-
length: segmentLength,
|
|
209
|
-
type: inferFieldType(samples)
|
|
210
|
-
});
|
|
211
|
-
offset += segmentLength;
|
|
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");
|
|
212
364
|
}
|
|
213
|
-
return fields;
|
|
214
365
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
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
|
+
};
|
|
221
376
|
}
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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;
|
|
239
404
|
}
|
|
240
|
-
return leScore > beScore ? "le" : "be";
|
|
241
405
|
}
|
|
242
|
-
function
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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;
|
|
250
432
|
}
|
|
251
|
-
if (commonPrefixLen === 0) return fields;
|
|
252
|
-
let magicLabelApplied = false;
|
|
253
|
-
return fields.map((field) => {
|
|
254
|
-
if (!magicLabelApplied && field.offset === 0 && commonPrefixLen >= 2) {
|
|
255
|
-
magicLabelApplied = true;
|
|
256
|
-
return {
|
|
257
|
-
...field,
|
|
258
|
-
name: "magic"
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (magicLabelApplied && field.type === "int" && field.length <= 2 && field.offset <= commonPrefixLen) return {
|
|
262
|
-
...field,
|
|
263
|
-
name: "version"
|
|
264
|
-
};
|
|
265
|
-
return field;
|
|
266
|
-
});
|
|
267
433
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
definePattern(name, specOrFields, options) {
|
|
274
|
-
const legacyPattern = Array.isArray(specOrFields) ? this.createLegacyPattern(name, specOrFields, options) : this.createLegacyPatternFromSpec(name, specOrFields);
|
|
275
|
-
const spec = this.createSpecFromLegacyPattern(legacyPattern);
|
|
276
|
-
this.patterns.set(name, spec);
|
|
277
|
-
this.legacyPatterns.set(name, legacyPattern);
|
|
278
|
-
if (Array.isArray(specOrFields)) return legacyPattern;
|
|
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);
|
|
279
439
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
for (const pattern of this.patterns.values()) {
|
|
285
|
-
const totalChecks = pattern.fields.length + (pattern.fieldDelimiter ? 1 : 0);
|
|
286
|
-
if (totalChecks === 0) continue;
|
|
287
|
-
let matches = 0;
|
|
288
|
-
if (pattern.fieldDelimiter && this.payloadContainsDelimiter(payload, pattern.fieldDelimiter)) matches += 1;
|
|
289
|
-
for (const field of pattern.fields) if (this.matchesField(payload, field, pattern.byteOrder ?? "be")) matches += 1;
|
|
290
|
-
const confidence = Number((matches / totalChecks).toFixed(2));
|
|
291
|
-
if (confidence <= 0) continue;
|
|
292
|
-
const candidate = {
|
|
293
|
-
pattern,
|
|
294
|
-
confidence,
|
|
295
|
-
matches,
|
|
296
|
-
total: totalChecks
|
|
297
|
-
};
|
|
298
|
-
if (!bestMatch || candidate.confidence > bestMatch.confidence || candidate.confidence === bestMatch.confidence && candidate.matches > bestMatch.matches) bestMatch = candidate;
|
|
299
|
-
}
|
|
300
|
-
return bestMatch;
|
|
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);
|
|
301
444
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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`);
|
|
307
467
|
return {
|
|
308
|
-
name
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
fields
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
inferFields(hexPayloads) {
|
|
315
|
-
const buffers = parsePayloads(hexPayloads);
|
|
316
|
-
if (buffers.length === 0) return [];
|
|
317
|
-
const delimiter = inferDelimiter(buffers);
|
|
318
|
-
if (delimiter) {
|
|
319
|
-
const fields = buildDelimitedFields(buffers, this.parseDelimiter(delimiter));
|
|
320
|
-
if (fields.length > 0) return labelMagicFields(fields, buffers);
|
|
321
|
-
}
|
|
322
|
-
return labelMagicFields(buildFixedWidthFields(buffers), buffers);
|
|
323
|
-
}
|
|
324
|
-
autoDetectPattern(payloads, options) {
|
|
325
|
-
const hexPayloads = payloads.map((payload) => payload.toString("hex"));
|
|
326
|
-
const detected = this.autoDetect(hexPayloads);
|
|
327
|
-
const name = options?.name ?? detected?.name ?? "auto_detected";
|
|
328
|
-
if (!detected) {
|
|
329
|
-
const emptyPattern = this.createLegacyPattern(name, []);
|
|
330
|
-
this.patterns.set(name, this.createSpecFromLegacyPattern(emptyPattern));
|
|
331
|
-
this.legacyPatterns.set(name, emptyPattern);
|
|
332
|
-
return emptyPattern;
|
|
333
|
-
}
|
|
334
|
-
const namedPattern = {
|
|
335
|
-
...detected,
|
|
336
|
-
name
|
|
468
|
+
name,
|
|
469
|
+
type: fieldType,
|
|
470
|
+
value: numericValue
|
|
337
471
|
};
|
|
338
|
-
this.definePattern(name, namedPattern);
|
|
339
|
-
return this.getPattern(name) ?? this.createLegacyPatternFromSpec(name, namedPattern);
|
|
340
|
-
}
|
|
341
|
-
getPattern(name) {
|
|
342
|
-
return this.legacyPatterns.get(name);
|
|
343
472
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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;
|
|
365
506
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return lines.join("\n");
|
|
369
|
-
}
|
|
370
|
-
payloadContainsDelimiter(payload, delimiter) {
|
|
371
|
-
const delimiterBuffer = this.parseDelimiter(delimiter);
|
|
372
|
-
if (delimiterBuffer.length === 0) return false;
|
|
373
|
-
return payload.includes(delimiterBuffer);
|
|
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);
|
|
374
509
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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;
|
|
386
525
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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`)
|
|
399
542
|
};
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
offset: field.offset,
|
|
409
|
-
length: field.length,
|
|
410
|
-
type: this.toLegacyFieldType(field),
|
|
411
|
-
...field.description ? { description: field.description } : {}
|
|
412
|
-
}))
|
|
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
|
+
})()
|
|
413
551
|
};
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
fieldDelimiter: pattern.fieldDelimiter,
|
|
419
|
-
byteOrder: pattern.byteOrder === "little" ? "le" : "be",
|
|
420
|
-
fields: pattern.fields.map((field) => ({
|
|
421
|
-
name: field.name,
|
|
422
|
-
offset: field.offset,
|
|
423
|
-
length: field.length,
|
|
424
|
-
type: this.toSpecFieldType(field.type),
|
|
425
|
-
...field.description ? { description: field.description } : {}
|
|
426
|
-
}))
|
|
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`)
|
|
427
556
|
};
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
parseDelimiter(delimiter) {
|
|
448
|
-
if (isHexPayload(delimiter)) {
|
|
449
|
-
const parsed = parseHexPayload$1(delimiter);
|
|
450
|
-
if (parsed) return parsed;
|
|
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
|
+
};
|
|
451
576
|
}
|
|
452
|
-
return Buffer.from(delimiter, "utf8");
|
|
453
|
-
}
|
|
454
|
-
toProtoType(type) {
|
|
455
|
-
return {
|
|
456
|
-
uint8: "uint32",
|
|
457
|
-
uint16: "uint32",
|
|
458
|
-
uint32: "uint32",
|
|
459
|
-
int64: "int64",
|
|
460
|
-
float: "float",
|
|
461
|
-
string: "string",
|
|
462
|
-
bytes: "bytes"
|
|
463
|
-
}[type];
|
|
464
|
-
}
|
|
465
|
-
toPascalCase(name) {
|
|
466
|
-
return name.replace(/[^a-zA-Z0-9_]/g, "_").split("_").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("") || "Message";
|
|
467
577
|
}
|
|
468
|
-
};
|
|
469
|
-
//#endregion
|
|
470
|
-
//#region src/modules/protocol-analysis/StateMachineInferrer.ts
|
|
471
|
-
function isRecord$1(value) {
|
|
472
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
473
|
-
}
|
|
474
|
-
function normalizeText(value) {
|
|
475
|
-
return value.trim().toLowerCase();
|
|
476
578
|
}
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
var StateMachineInferrer = class {
|
|
498
|
-
infer(messages) {
|
|
499
|
-
if (messages.length === 0) return {
|
|
500
|
-
states: [],
|
|
501
|
-
transitions: [],
|
|
502
|
-
initial: "",
|
|
503
|
-
initialState: "",
|
|
504
|
-
finalStates: []
|
|
505
|
-
};
|
|
506
|
-
const statesBySignature = /* @__PURE__ */ new Map();
|
|
507
|
-
const transitionsByKey = /* @__PURE__ */ new Map();
|
|
508
|
-
let previousStateId = "";
|
|
509
|
-
for (let index = 0; index < messages.length; index += 1) {
|
|
510
|
-
const message = messages[index];
|
|
511
|
-
if (!message) continue;
|
|
512
|
-
const signature = this.buildSignature(message);
|
|
513
|
-
let state = statesBySignature.get(signature);
|
|
514
|
-
if (!state) {
|
|
515
|
-
state = {
|
|
516
|
-
id: `state_${statesBySignature.size + 1}`,
|
|
517
|
-
name: this.buildStateName(message, statesBySignature.size + 1),
|
|
518
|
-
type: this.inferStateType(message, index === messages.length - 1)
|
|
519
|
-
};
|
|
520
|
-
statesBySignature.set(signature, state);
|
|
521
|
-
} else state.type = this.mergeStateTypes(state.type, this.inferStateType(message, index === messages.length - 1));
|
|
522
|
-
if (previousStateId && message.timestamp !== void 0) {
|
|
523
|
-
const firstMessage = messages[0];
|
|
524
|
-
if (firstMessage && firstMessage.timestamp !== void 0) state.timeout = message.timestamp - firstMessage.timestamp;
|
|
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;
|
|
525
598
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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`
|
|
539
624
|
}
|
|
540
|
-
previousStateId = state.id;
|
|
541
|
-
}
|
|
542
|
-
const firstMessage = messages[0];
|
|
543
|
-
const initial = firstMessage ? statesBySignature.get(this.buildSignature(firstMessage))?.id ?? "" : "";
|
|
544
|
-
return {
|
|
545
|
-
states: [...statesBySignature.values()],
|
|
546
|
-
transitions: [...transitionsByKey.values()],
|
|
547
|
-
initial,
|
|
548
|
-
initialState: initial,
|
|
549
|
-
finalStates: this.collectTerminalStates([...statesBySignature.values()])
|
|
550
625
|
};
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
for (const state of machine.states) {
|
|
576
|
-
const stateType = state.type ?? "normal";
|
|
577
|
-
if (stateType === "accept" || stateType === "reject" || finalStateSet.has(state.id)) lines.push(` ${state.id} --> [*]`);
|
|
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
|
+
};
|
|
578
650
|
}
|
|
579
|
-
lines.push("```");
|
|
580
|
-
return lines.join("\n");
|
|
581
651
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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;
|
|
591
683
|
}
|
|
592
|
-
|
|
593
|
-
|
|
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;
|
|
594
692
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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)];
|
|
612
721
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
for (const state of machine.states) {
|
|
617
|
-
const prefix = this.getPayloadPrefix(state);
|
|
618
|
-
if (!prefix) continue;
|
|
619
|
-
const primary = groupIdToPrimary.get(prefix);
|
|
620
|
-
if (primary && primary !== state.id) mergeMap.set(state.id, primary);
|
|
621
|
-
}
|
|
622
|
-
if (mergeMap.size === 0) return {
|
|
623
|
-
...machine,
|
|
624
|
-
initialState: machine.initialState ?? machine.initial,
|
|
625
|
-
finalStates: machine.finalStates ?? this.collectTerminalStates(machine.states)
|
|
626
|
-
};
|
|
627
|
-
const newStates = machine.states.filter((state) => !mergeMap.has(state.id));
|
|
628
|
-
const newTransitions = machine.transitions.map((t) => ({
|
|
629
|
-
...t,
|
|
630
|
-
from: mergeMap.get(t.from) ?? t.from,
|
|
631
|
-
to: mergeMap.get(t.to) ?? t.to
|
|
632
|
-
})).filter((t) => t.from !== t.to);
|
|
633
|
-
const rawInitialState = machine.initialState ?? machine.initial ?? "";
|
|
634
|
-
const initialState = mergeMap.get(rawInitialState) ?? rawInitialState;
|
|
635
|
-
return {
|
|
636
|
-
states: newStates,
|
|
637
|
-
transitions: newTransitions,
|
|
638
|
-
initial: initialState,
|
|
639
|
-
initialState,
|
|
640
|
-
finalStates: machine.finalStates.map((fs) => mergeMap.get(fs) ?? fs).filter((fs, index, arr) => arr.indexOf(fs) === index)
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
getPayloadPrefix(state) {
|
|
644
|
-
const payload = state.expectedPayload;
|
|
645
|
-
if (!payload || payload.length < 8) return null;
|
|
646
|
-
return payload.slice(0, 8).toLowerCase();
|
|
647
|
-
}
|
|
648
|
-
buildSignature(message) {
|
|
649
|
-
const fieldKeys = Object.keys(message.fields).toSorted().join(",");
|
|
650
|
-
const rawPrefix = normalizeText(message._rawBuffer ? message._rawBuffer.toString("hex") : message.raw).slice(0, 24);
|
|
651
|
-
return `${message.direction}|${fieldKeys}|${rawPrefix}`;
|
|
652
|
-
}
|
|
653
|
-
buildStateName(message, position) {
|
|
654
|
-
const directionName = message.direction === "req" ? "send" : "recv";
|
|
655
|
-
const primaryField = this.findPrimaryFieldName(message.fields);
|
|
656
|
-
const raw = message.raw;
|
|
657
|
-
if (raw.length === 0) return `${directionName}_empty`;
|
|
658
|
-
const buf = message._rawBuffer;
|
|
659
|
-
const hexContent = Buffer.isBuffer(buf) ? buf.toString("hex") : raw;
|
|
660
|
-
if (hexContent.startsWith("16") || hexContent.startsWith("15") || hexContent.startsWith("17")) return `${directionName}_tls_handshake`;
|
|
661
|
-
const trimmed = raw.trimStart();
|
|
662
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return `${directionName}_json_${primaryField || `step_${position}`}`;
|
|
663
|
-
if (printableRatioOf(raw) >= .7) {
|
|
664
|
-
const lower = normalizeText(raw);
|
|
665
|
-
if (lower.includes("close") || lower.includes("fin") || lower.includes("bye")) return `${directionName}_close`;
|
|
666
|
-
if (lower.startsWith("get ") || lower.startsWith("post ") || lower.startsWith("http")) return `${directionName}_text_http`;
|
|
667
|
-
return `${directionName}_text_${primaryField || `step_${position}`}`;
|
|
668
|
-
}
|
|
669
|
-
if (Buffer.isBuffer(buf) && buf.length >= 32) {
|
|
670
|
-
if (calculateEntropy(buf) > 6) return `${directionName}_encrypted`;
|
|
671
|
-
}
|
|
672
|
-
if (Buffer.isBuffer(buf) && buf.length <= 4) return `${directionName}_control`;
|
|
673
|
-
return `${directionName}_${primaryField || `step_${position}`}`;
|
|
674
|
-
}
|
|
675
|
-
findPrimaryFieldName(fields) {
|
|
676
|
-
const firstKey = Object.keys(fields).toSorted()[0];
|
|
677
|
-
return firstKey ? firstKey : "";
|
|
678
|
-
}
|
|
679
|
-
inferStateType(message, isLastMessage) {
|
|
680
|
-
const text = normalizeText(message.raw);
|
|
681
|
-
const statusValue = this.findStatusValue(message.fields);
|
|
682
|
-
if (this.containsRejectSignal(text) || this.containsRejectSignal(statusValue)) return "reject";
|
|
683
|
-
if (this.containsAcceptSignal(text) || this.containsAcceptSignal(statusValue) || isLastMessage && message.direction === "res") return "accept";
|
|
684
|
-
return "normal";
|
|
685
|
-
}
|
|
686
|
-
mergeStateTypes(current, next) {
|
|
687
|
-
if (current === "reject" || next === "reject") return "reject";
|
|
688
|
-
if (current === "accept" || next === "accept") return "accept";
|
|
689
|
-
return "normal";
|
|
690
|
-
}
|
|
691
|
-
findStatusValue(fields) {
|
|
692
|
-
for (const key of [
|
|
693
|
-
"status",
|
|
694
|
-
"result",
|
|
695
|
-
"code",
|
|
696
|
-
"reason",
|
|
697
|
-
"message"
|
|
698
|
-
]) {
|
|
699
|
-
const value = fields[key];
|
|
700
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
701
|
-
}
|
|
702
|
-
return "";
|
|
703
|
-
}
|
|
704
|
-
containsRejectSignal(value) {
|
|
705
|
-
return [
|
|
706
|
-
"error",
|
|
707
|
-
"fail",
|
|
708
|
-
"denied",
|
|
709
|
-
"reject",
|
|
710
|
-
"timeout",
|
|
711
|
-
"invalid"
|
|
712
|
-
].some((token) => normalizeText(value).includes(token));
|
|
713
|
-
}
|
|
714
|
-
containsAcceptSignal(value) {
|
|
715
|
-
return [
|
|
716
|
-
"ok",
|
|
717
|
-
"success",
|
|
718
|
-
"accept",
|
|
719
|
-
"ready",
|
|
720
|
-
"done",
|
|
721
|
-
"complete"
|
|
722
|
-
].some((token) => normalizeText(value).includes(token));
|
|
723
|
-
}
|
|
724
|
-
buildEventName(message) {
|
|
725
|
-
const statusValue = this.findStatusValue(message.fields);
|
|
726
|
-
if (statusValue) return `${message.direction}_${normalizeText(statusValue).replace(/[^a-z0-9]+/g, "_")}`;
|
|
727
|
-
const primaryField = this.findPrimaryFieldName(message.fields);
|
|
728
|
-
if (primaryField) return `${message.direction}_${primaryField}`;
|
|
729
|
-
return `${message.direction}_message`;
|
|
730
|
-
}
|
|
731
|
-
buildCondition(fields) {
|
|
732
|
-
const statusValue = this.findStatusValue(fields);
|
|
733
|
-
if (statusValue) return `status=${statusValue}`;
|
|
734
|
-
const keys = Object.keys(fields).toSorted().slice(0, 2);
|
|
735
|
-
if (keys.length === 0) return;
|
|
736
|
-
const fragments = [];
|
|
737
|
-
for (const key of keys) {
|
|
738
|
-
const value = fields[key];
|
|
739
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") fragments.push(`${key}=${value}`);
|
|
740
|
-
}
|
|
741
|
-
return fragments.length > 0 ? fragments.join(", ") : void 0;
|
|
742
|
-
}
|
|
743
|
-
buildAction(message) {
|
|
744
|
-
const statusValue = this.findStatusValue(message.fields);
|
|
745
|
-
if (this.containsRejectSignal(statusValue) || this.containsRejectSignal(message.raw)) return "reject";
|
|
746
|
-
if (this.containsAcceptSignal(statusValue) || this.containsAcceptSignal(message.raw)) return "complete";
|
|
747
|
-
const rawText = normalizeText(message.raw);
|
|
748
|
-
if (rawText.includes("ack")) return "acknowledge";
|
|
749
|
-
if (rawText.includes("retry")) return "retry";
|
|
750
|
-
if (Object.keys(message.fields).length > 0 && isRecord$1(message.fields)) return message.direction === "req" ? "send" : "receive";
|
|
751
|
-
}
|
|
752
|
-
collectTerminalStates(states) {
|
|
753
|
-
const terminalIds = states.filter((state) => {
|
|
754
|
-
const stateType = state.type ?? "normal";
|
|
755
|
-
return stateType === "accept" || stateType === "reject";
|
|
756
|
-
}).map((state) => state.id);
|
|
757
|
-
for (const state of states) {
|
|
758
|
-
const lower = normalizeText(state.name);
|
|
759
|
-
if (lower.includes("close") || lower.includes("fin") || lower.includes("bye")) {
|
|
760
|
-
if (!terminalIds.includes(state.id)) terminalIds.push(state.id);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return terminalIds;
|
|
764
|
-
}
|
|
765
|
-
computeTransitionConfidence(message) {
|
|
766
|
-
let confidence = .3;
|
|
767
|
-
if (Object.keys(message.fields).length > 0) confidence += .3;
|
|
768
|
-
if (this.findStatusValue(message.fields)) confidence += .2;
|
|
769
|
-
if (message.raw.length > 0) confidence += .2;
|
|
770
|
-
return Math.min(confidence, 1);
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
//#endregion
|
|
774
|
-
//#region src/server/domains/protocol-analysis/handlers/shared.ts
|
|
775
|
-
function isRecord(value) {
|
|
776
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
722
|
+
if (!/^[0-9a-f]{1,4}$/i.test(part)) throw new Error(`${label} contains an invalid IPv6 group`);
|
|
723
|
+
return [part];
|
|
724
|
+
});
|
|
777
725
|
}
|
|
778
|
-
function
|
|
779
|
-
if (
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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;
|
|
794
743
|
}
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
const name = value.name;
|
|
798
|
-
const offset = value.offset;
|
|
799
|
-
const length = value.length;
|
|
800
|
-
const type = value.type;
|
|
801
|
-
const description = value.description;
|
|
802
|
-
if (typeof name !== "string" || name.trim().length === 0) throw new Error(`fields[${index}].name must be a non-empty string`);
|
|
803
|
-
if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) throw new Error(`fields[${index}].offset must be a non-negative integer`);
|
|
804
|
-
if (typeof length !== "number" || !Number.isInteger(length) || length <= 0) throw new Error(`fields[${index}].length must be a positive integer`);
|
|
805
|
-
if (type !== "uint8" && type !== "uint16" && type !== "uint32" && type !== "int64" && type !== "float" && type !== "string" && type !== "bytes") throw new Error(`fields[${index}].type is invalid`);
|
|
806
|
-
return {
|
|
807
|
-
name,
|
|
808
|
-
offset,
|
|
809
|
-
length,
|
|
810
|
-
type,
|
|
811
|
-
...typeof description === "string" ? { description } : {}
|
|
812
|
-
};
|
|
744
|
+
function parseIpAddress(value, version, label) {
|
|
745
|
+
return version === "ipv4" ? parseIpv4Address(value, label) : parseIpv6Address(value, label);
|
|
813
746
|
}
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
if (!Array.isArray(rawFields)) throw new Error("spec.fields must be an array");
|
|
817
|
-
const fieldDelimiter = typeof value.fieldDelimiter === "string" && value.fieldDelimiter.length > 0 ? value.fieldDelimiter : void 0;
|
|
818
|
-
const byteOrderValue = value.byteOrder;
|
|
819
|
-
const byteOrder = byteOrderValue === "le" || byteOrderValue === "be" ? byteOrderValue : void 0;
|
|
820
|
-
return {
|
|
821
|
-
name,
|
|
822
|
-
...fieldDelimiter ? { fieldDelimiter } : {},
|
|
823
|
-
...byteOrder ? { byteOrder } : {},
|
|
824
|
-
fields: rawFields.map((field, index) => parseFieldSpec(field, index))
|
|
825
|
-
};
|
|
747
|
+
function parseEtherType(value, label) {
|
|
748
|
+
return parseNamedOrNumericValue(value, label, ETHER_TYPE_MAP, 65535);
|
|
826
749
|
}
|
|
827
|
-
function
|
|
828
|
-
|
|
829
|
-
const type = value.type;
|
|
830
|
-
if (type !== "aes" && type !== "xor" && type !== "rc4" && type !== "custom") return;
|
|
831
|
-
const key = typeof value.key === "string" ? value.key : void 0;
|
|
832
|
-
const iv = typeof value.iv === "string" ? value.iv : void 0;
|
|
833
|
-
const notes = typeof value.notes === "string" ? value.notes : void 0;
|
|
834
|
-
return {
|
|
835
|
-
type,
|
|
836
|
-
...key ? { key } : {},
|
|
837
|
-
...iv ? { iv } : {},
|
|
838
|
-
...notes ? { notes } : {}
|
|
839
|
-
};
|
|
750
|
+
function parseIpProtocol(value, label) {
|
|
751
|
+
return parseNamedOrNumericValue(value, label, IP_PROTOCOL_MAP, 255);
|
|
840
752
|
}
|
|
841
|
-
function
|
|
842
|
-
|
|
843
|
-
const direction = value.direction;
|
|
844
|
-
const timestamp = value.timestamp;
|
|
845
|
-
const fields = value.fields;
|
|
846
|
-
const raw = value.raw;
|
|
847
|
-
if (direction !== "req" && direction !== "res") throw new Error(`messages[${index}].direction must be "req" or "res"`);
|
|
848
|
-
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) throw new Error(`messages[${index}].timestamp must be a number`);
|
|
849
|
-
if (!isRecord(fields)) throw new Error(`messages[${index}].fields must be an object`);
|
|
850
|
-
if (typeof raw !== "string") throw new Error(`messages[${index}].raw must be a string`);
|
|
851
|
-
return {
|
|
852
|
-
direction,
|
|
853
|
-
timestamp,
|
|
854
|
-
fields,
|
|
855
|
-
raw
|
|
856
|
-
};
|
|
753
|
+
function parsePcapLinkType(value, label) {
|
|
754
|
+
return parseNamedOrNumericValue(value, label, PCAP_LINK_TYPE_MAP, 4294967295);
|
|
857
755
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
"utf8",
|
|
861
|
-
"ascii",
|
|
862
|
-
"hex",
|
|
863
|
-
"base64"
|
|
864
|
-
];
|
|
865
|
-
const PAYLOAD_FIELD_TYPES = [
|
|
866
|
-
"u8",
|
|
867
|
-
"u16",
|
|
868
|
-
"u32",
|
|
869
|
-
"i8",
|
|
870
|
-
"i16",
|
|
871
|
-
"i32",
|
|
872
|
-
"string",
|
|
873
|
-
"bytes"
|
|
874
|
-
];
|
|
875
|
-
const MUTATION_STRATEGIES = [
|
|
876
|
-
"set_byte",
|
|
877
|
-
"flip_bit",
|
|
878
|
-
"overwrite_bytes",
|
|
879
|
-
"append_bytes",
|
|
880
|
-
"truncate",
|
|
881
|
-
"increment_integer"
|
|
882
|
-
];
|
|
883
|
-
const ETHER_TYPE_MAP = Object.freeze({
|
|
884
|
-
arp: 2054,
|
|
885
|
-
ipv4: 2048,
|
|
886
|
-
ipv6: 34525,
|
|
887
|
-
vlan: 33024
|
|
888
|
-
});
|
|
889
|
-
const IP_PROTOCOL_MAP = Object.freeze({
|
|
890
|
-
icmp: 1,
|
|
891
|
-
igmp: 2,
|
|
892
|
-
tcp: 6,
|
|
893
|
-
udp: 17,
|
|
894
|
-
gre: 47,
|
|
895
|
-
esp: 50,
|
|
896
|
-
ah: 51,
|
|
897
|
-
icmpv6: 58,
|
|
898
|
-
ospf: 89
|
|
899
|
-
});
|
|
900
|
-
const PCAP_LINK_TYPE_MAP = Object.freeze({
|
|
901
|
-
loopback: 0,
|
|
902
|
-
ethernet: 1,
|
|
903
|
-
raw: 101
|
|
904
|
-
});
|
|
905
|
-
function parseEndian(value, fallback = "big") {
|
|
906
|
-
return value === "little" ? "little" : fallback;
|
|
756
|
+
function parseChecksumEndian(value) {
|
|
757
|
+
return value === "little" ? "little" : "big";
|
|
907
758
|
}
|
|
908
|
-
function
|
|
909
|
-
|
|
910
|
-
return value;
|
|
759
|
+
function parsePacketEndianness(value) {
|
|
760
|
+
return value === "big" ? "big" : "little";
|
|
911
761
|
}
|
|
912
|
-
function
|
|
913
|
-
|
|
914
|
-
return value;
|
|
762
|
+
function parseTimestampPrecision(value) {
|
|
763
|
+
return value === "nano" ? "nano" : "micro";
|
|
915
764
|
}
|
|
916
|
-
function
|
|
917
|
-
if (typeof value !== "
|
|
918
|
-
return value;
|
|
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");
|
|
919
768
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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;
|
|
924
780
|
}
|
|
925
|
-
function
|
|
926
|
-
|
|
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]);
|
|
927
787
|
}
|
|
928
|
-
function
|
|
929
|
-
if (
|
|
930
|
-
if (
|
|
931
|
-
|
|
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;
|
|
932
809
|
}
|
|
933
|
-
function
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
+
};
|
|
938
829
|
}
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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]);
|
|
946
840
|
}
|
|
947
|
-
function
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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);
|
|
953
851
|
return {
|
|
954
|
-
|
|
955
|
-
|
|
852
|
+
packet,
|
|
853
|
+
checksum
|
|
956
854
|
};
|
|
957
855
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
};
|
|
964
|
-
case "u16": return {
|
|
965
|
-
width: 2,
|
|
966
|
-
signed: false
|
|
967
|
-
};
|
|
968
|
-
case "u32": return {
|
|
969
|
-
width: 4,
|
|
970
|
-
signed: false
|
|
971
|
-
};
|
|
972
|
-
case "i8": return {
|
|
973
|
-
width: 1,
|
|
974
|
-
signed: true
|
|
975
|
-
};
|
|
976
|
-
case "i16": return {
|
|
977
|
-
width: 2,
|
|
978
|
-
signed: true
|
|
979
|
-
};
|
|
980
|
-
case "i32": return {
|
|
981
|
-
width: 4,
|
|
982
|
-
signed: true
|
|
983
|
-
};
|
|
984
|
-
default: return null;
|
|
985
|
-
}
|
|
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);
|
|
986
861
|
}
|
|
987
|
-
function
|
|
988
|
-
if (
|
|
989
|
-
|
|
990
|
-
buffer.writeInt8(value, 0);
|
|
991
|
-
return;
|
|
992
|
-
case 2:
|
|
993
|
-
if (endian === "little") buffer.writeInt16LE(value, 0);
|
|
994
|
-
else buffer.writeInt16BE(value, 0);
|
|
995
|
-
return;
|
|
996
|
-
case 4:
|
|
997
|
-
if (endian === "little") buffer.writeInt32LE(value, 0);
|
|
998
|
-
else buffer.writeInt32BE(value, 0);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
switch (width) {
|
|
1002
|
-
case 1:
|
|
1003
|
-
buffer.writeUInt8(value, 0);
|
|
1004
|
-
return;
|
|
1005
|
-
case 2:
|
|
1006
|
-
if (endian === "little") buffer.writeUInt16LE(value, 0);
|
|
1007
|
-
else buffer.writeUInt16BE(value, 0);
|
|
1008
|
-
return;
|
|
1009
|
-
case 4:
|
|
1010
|
-
if (endian === "little") buffer.writeUInt32LE(value, 0);
|
|
1011
|
-
else buffer.writeUInt32BE(value, 0);
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
862
|
+
function writeUint16(buffer, offset, value, endianness) {
|
|
863
|
+
if (endianness === "little") buffer.writeUInt16LE(value, offset);
|
|
864
|
+
else buffer.writeUInt16BE(value, offset);
|
|
1014
865
|
}
|
|
1015
|
-
function
|
|
1016
|
-
|
|
1017
|
-
case 1: return buffer.readInt8(offset);
|
|
1018
|
-
case 2: return endian === "little" ? buffer.readInt16LE(offset) : buffer.readInt16BE(offset);
|
|
1019
|
-
case 4: return endian === "little" ? buffer.readInt32LE(offset) : buffer.readInt32BE(offset);
|
|
1020
|
-
}
|
|
1021
|
-
switch (width) {
|
|
1022
|
-
case 1: return buffer.readUInt8(offset);
|
|
1023
|
-
case 2: return endian === "little" ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
|
|
1024
|
-
case 4: return endian === "little" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
|
|
1025
|
-
}
|
|
866
|
+
function readUint32(buffer, offset, endianness) {
|
|
867
|
+
return endianness === "little" ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
|
|
1026
868
|
}
|
|
1027
|
-
function
|
|
1028
|
-
|
|
1029
|
-
if (encoded.length > length) return encoded.subarray(0, length);
|
|
1030
|
-
return Buffer.concat([encoded, Buffer.alloc(length - encoded.length, padByte)]);
|
|
869
|
+
function readUint16(buffer, offset, endianness) {
|
|
870
|
+
return endianness === "little" ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
|
|
1031
871
|
}
|
|
1032
|
-
function
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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");
|
|
1051
899
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
encoding: parseEncoding(value.encoding, TEXT_ENCODINGS, "utf8", `fields[${index}].encoding`),
|
|
1060
|
-
...length !== void 0 ? { length } : {},
|
|
1061
|
-
padByte
|
|
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)
|
|
1062
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`);
|
|
1063
916
|
return {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
...length !== void 0 ? { length } : {},
|
|
1069
|
-
padByte
|
|
917
|
+
data,
|
|
918
|
+
timestampSeconds,
|
|
919
|
+
timestampFraction,
|
|
920
|
+
originalLength
|
|
1070
921
|
};
|
|
1071
922
|
}
|
|
1072
|
-
function
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
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]);
|
|
1089
941
|
}
|
|
1090
|
-
function
|
|
1091
|
-
const
|
|
1092
|
-
const
|
|
1093
|
-
let offset =
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
|
1102
967
|
});
|
|
1103
|
-
offset += encoded.length;
|
|
1104
968
|
}
|
|
1105
969
|
return {
|
|
1106
|
-
|
|
1107
|
-
|
|
970
|
+
header,
|
|
971
|
+
packets
|
|
1108
972
|
};
|
|
1109
973
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
}
|
|
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);
|
|
1158
1021
|
}
|
|
1022
|
+
parts.push(buffer.subarray(start));
|
|
1023
|
+
return parts;
|
|
1159
1024
|
}
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
summary: {
|
|
1169
|
-
index,
|
|
1170
|
-
strategy: mutation.strategy,
|
|
1171
|
-
detail: `set payload[${mutation.offset}] to ${mutation.value}`
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
case "flip_bit":
|
|
1175
|
-
if (mutation.offset >= working.length) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
1176
|
-
{
|
|
1177
|
-
const currentByte = working.at(mutation.offset);
|
|
1178
|
-
if (currentByte === void 0) throw new Error(`mutations[${index}] offset is outside the payload`);
|
|
1179
|
-
working[mutation.offset] = currentByte ^ 1 << mutation.bit;
|
|
1180
|
-
}
|
|
1181
|
-
return {
|
|
1182
|
-
payload: working,
|
|
1183
|
-
summary: {
|
|
1184
|
-
index,
|
|
1185
|
-
strategy: mutation.strategy,
|
|
1186
|
-
detail: `flipped bit ${mutation.bit} at offset ${mutation.offset}`
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
case "overwrite_bytes":
|
|
1190
|
-
if (mutation.offset + mutation.data.length > working.length) throw new Error(`mutations[${index}] overwrite exceeds payload length`);
|
|
1191
|
-
mutation.data.copy(working, mutation.offset);
|
|
1192
|
-
return {
|
|
1193
|
-
payload: working,
|
|
1194
|
-
summary: {
|
|
1195
|
-
index,
|
|
1196
|
-
strategy: mutation.strategy,
|
|
1197
|
-
detail: `overwrote ${mutation.data.length} bytes at offset ${mutation.offset}`
|
|
1198
|
-
}
|
|
1199
|
-
};
|
|
1200
|
-
case "append_bytes": return {
|
|
1201
|
-
payload: Buffer.concat([working, mutation.data]),
|
|
1202
|
-
summary: {
|
|
1203
|
-
index,
|
|
1204
|
-
strategy: mutation.strategy,
|
|
1205
|
-
detail: `appended ${mutation.data.length} bytes`
|
|
1206
|
-
}
|
|
1207
|
-
};
|
|
1208
|
-
case "truncate":
|
|
1209
|
-
if (mutation.length > working.length) throw new Error(`mutations[${index}] length exceeds payload size`);
|
|
1210
|
-
return {
|
|
1211
|
-
payload: working.subarray(0, mutation.length),
|
|
1212
|
-
summary: {
|
|
1213
|
-
index,
|
|
1214
|
-
strategy: mutation.strategy,
|
|
1215
|
-
detail: `truncated payload to ${mutation.length} bytes`
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
case "increment_integer": {
|
|
1219
|
-
if (mutation.offset + mutation.width > working.length) throw new Error(`mutations[${index}] integer range exceeds payload length`);
|
|
1220
|
-
const next = readIntegerFromBuffer(working, mutation.offset, mutation.width, mutation.signed, mutation.endian) + mutation.delta;
|
|
1221
|
-
const range = getNumericRange(mutation.width, mutation.signed);
|
|
1222
|
-
if (next < range.min || next > range.max) throw new Error(`mutations[${index}] integer overflow (${range.min}..${range.max})`);
|
|
1223
|
-
writeIntegerToBuffer(working.subarray(mutation.offset, mutation.offset + mutation.width), next, mutation.width, mutation.signed, mutation.endian);
|
|
1224
|
-
return {
|
|
1225
|
-
payload: working,
|
|
1226
|
-
summary: {
|
|
1227
|
-
index,
|
|
1228
|
-
strategy: mutation.strategy,
|
|
1229
|
-
detail: `incremented ${mutation.signed ? "signed" : "unsigned"} ${mutation.width}-byte integer at offset ${mutation.offset} by ${mutation.delta}`
|
|
1230
|
-
}
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
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);
|
|
1233
1033
|
}
|
|
1034
|
+
return buffers;
|
|
1234
1035
|
}
|
|
1235
|
-
function
|
|
1236
|
-
if (
|
|
1237
|
-
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
if (
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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;
|
|
1245
1049
|
}
|
|
1246
|
-
function
|
|
1247
|
-
if (
|
|
1248
|
-
|
|
1249
|
-
return value;
|
|
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;
|
|
1250
1054
|
}
|
|
1251
|
-
if (
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if (mapped !== void 0) return mapped;
|
|
1255
|
-
if (/^\d+$/.test(normalized)) {
|
|
1256
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
1257
|
-
if (parsed > max) throw new Error(`${label} must be less than or equal to ${max}`);
|
|
1258
|
-
return parsed;
|
|
1055
|
+
if (buffer.length === 8) {
|
|
1056
|
+
const value = byteOrder === "le" ? buffer.readDoubleLE(0) : buffer.readDoubleBE(0);
|
|
1057
|
+
return Number.isFinite(value) ? value : null;
|
|
1259
1058
|
}
|
|
1260
|
-
|
|
1261
|
-
const parsed = Number.parseInt(hex, 16);
|
|
1262
|
-
if (parsed > max) throw new Error(`${label} must be less than or equal to ${max}`);
|
|
1263
|
-
return parsed;
|
|
1264
|
-
}
|
|
1265
|
-
function parseIpv4Address(value, label) {
|
|
1266
|
-
if (typeof value !== "string" || isIP(value.trim()) !== 4) throw new Error(`${label} must be a valid IPv4 address`);
|
|
1267
|
-
const octets = value.trim().split(".").map((part) => Number.parseInt(part, 10));
|
|
1268
|
-
return Buffer.from(octets);
|
|
1269
|
-
}
|
|
1270
|
-
function parseIpv6Groups(value, label) {
|
|
1271
|
-
if (value.length === 0) return [];
|
|
1272
|
-
return value.split(":").flatMap((part) => {
|
|
1273
|
-
if (part.length === 0) return [];
|
|
1274
|
-
if (part.includes(".")) {
|
|
1275
|
-
const ipv4 = parseIpv4Address(part, label);
|
|
1276
|
-
return [ipv4.readUInt16BE(0).toString(16), ipv4.readUInt16BE(2).toString(16)];
|
|
1277
|
-
}
|
|
1278
|
-
if (!/^[0-9a-f]{1,4}$/i.test(part)) throw new Error(`${label} contains an invalid IPv6 group`);
|
|
1279
|
-
return [part];
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
function parseIpv6Address(value, label) {
|
|
1283
|
-
if (typeof value !== "string") throw new Error(`${label} must be a valid IPv6 address`);
|
|
1284
|
-
const normalized = value.trim().toLowerCase().split("%")[0] ?? "";
|
|
1285
|
-
if (isIP(normalized) !== 6) throw new Error(`${label} must be a valid IPv6 address`);
|
|
1286
|
-
const segments = normalized.split("::");
|
|
1287
|
-
if (segments.length > 2) throw new Error(`${label} must be a valid IPv6 address`);
|
|
1288
|
-
const head = parseIpv6Groups(segments[0] ?? "", label);
|
|
1289
|
-
const tail = parseIpv6Groups(segments[1] ?? "", label);
|
|
1290
|
-
const groups = segments.length === 2 ? [
|
|
1291
|
-
...head,
|
|
1292
|
-
...Array.from({ length: 8 - head.length - tail.length }, () => "0"),
|
|
1293
|
-
...tail
|
|
1294
|
-
] : head;
|
|
1295
|
-
if (groups.length !== 8) throw new Error(`${label} must expand to exactly 8 IPv6 groups`);
|
|
1296
|
-
const output = Buffer.alloc(16);
|
|
1297
|
-
for (const [index, group] of groups.entries()) output.writeUInt16BE(Number.parseInt(group, 16), index * 2);
|
|
1298
|
-
return output;
|
|
1299
|
-
}
|
|
1300
|
-
function parseIpAddress(value, version, label) {
|
|
1301
|
-
return version === "ipv4" ? parseIpv4Address(value, label) : parseIpv6Address(value, label);
|
|
1302
|
-
}
|
|
1303
|
-
function parseEtherType(value, label) {
|
|
1304
|
-
return parseNamedOrNumericValue(value, label, ETHER_TYPE_MAP, 65535);
|
|
1305
|
-
}
|
|
1306
|
-
function parseIpProtocol(value, label) {
|
|
1307
|
-
return parseNamedOrNumericValue(value, label, IP_PROTOCOL_MAP, 255);
|
|
1308
|
-
}
|
|
1309
|
-
function parsePcapLinkType(value, label) {
|
|
1310
|
-
return parseNamedOrNumericValue(value, label, PCAP_LINK_TYPE_MAP, 4294967295);
|
|
1059
|
+
return null;
|
|
1311
1060
|
}
|
|
1312
|
-
function
|
|
1313
|
-
|
|
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;
|
|
1314
1072
|
}
|
|
1315
|
-
function
|
|
1316
|
-
|
|
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";
|
|
1317
1080
|
}
|
|
1318
|
-
function
|
|
1319
|
-
|
|
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);
|
|
1320
1089
|
}
|
|
1321
|
-
function
|
|
1322
|
-
|
|
1323
|
-
|
|
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;
|
|
1324
1100
|
}
|
|
1325
|
-
function
|
|
1326
|
-
let
|
|
1327
|
-
for (
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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;
|
|
1332
1108
|
}
|
|
1333
|
-
return
|
|
1109
|
+
return valueCount > 0;
|
|
1334
1110
|
}
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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;
|
|
1341
1133
|
}
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const buffer = Buffer.alloc(8 + args.hardwareSize * 2 + args.protocolSize * 2);
|
|
1134
|
+
function buildFixedWidthFields(buffers) {
|
|
1135
|
+
const minLength = Math.min(...buffers.map((buffer) => buffer.length));
|
|
1136
|
+
const fields = [];
|
|
1346
1137
|
let offset = 0;
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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;
|
|
1383
1185
|
}
|
|
1384
|
-
function
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
header.writeUInt8(args.hopLimit, 7);
|
|
1391
|
-
args.sourceIp.copy(header, 8);
|
|
1392
|
-
args.destinationIp.copy(header, 24);
|
|
1393
|
-
return Buffer.concat([header, args.payload]);
|
|
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
|
+
}
|
|
1394
1192
|
}
|
|
1395
|
-
function
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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";
|
|
1409
1212
|
}
|
|
1410
|
-
function
|
|
1411
|
-
if (
|
|
1412
|
-
|
|
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
|
+
});
|
|
1413
1238
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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);
|
|
1417
1444
|
}
|
|
1418
|
-
function
|
|
1419
|
-
return
|
|
1445
|
+
function normalizeText(value) {
|
|
1446
|
+
return value.trim().toLowerCase();
|
|
1420
1447
|
}
|
|
1421
|
-
function
|
|
1422
|
-
|
|
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;
|
|
1423
1458
|
}
|
|
1424
|
-
function
|
|
1425
|
-
|
|
1426
|
-
|
|
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;
|
|
1427
1467
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
+
};
|
|
1451
1613
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
versionMinor: readUint16(buffer, 6, endianness),
|
|
1457
|
-
snapLength: readUint32(buffer, 16, endianness),
|
|
1458
|
-
linkType: readUint32(buffer, 20, endianness)
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
function parsePcapPacketInput(value, index) {
|
|
1462
|
-
if (!isRecord(value)) throw new Error(`packets[${index}] must be an object`);
|
|
1463
|
-
const data = parseHexPayload(value.dataHex, `packets[${index}].dataHex`);
|
|
1464
|
-
const timestampSeconds = value.timestampSeconds === void 0 ? 0 : parseNonNegativeInteger(value.timestampSeconds, `packets[${index}].timestampSeconds`);
|
|
1465
|
-
const timestampFraction = value.timestampFraction === void 0 ? 0 : parseNonNegativeInteger(value.timestampFraction, `packets[${index}].timestampFraction`);
|
|
1466
|
-
const originalLength = value.originalLength === void 0 ? data.length : parsePositiveInteger(value.originalLength, `packets[${index}].originalLength`);
|
|
1467
|
-
if (originalLength < data.length) throw new Error(`packets[${index}].originalLength must be >= included packet length`);
|
|
1468
|
-
return {
|
|
1469
|
-
data,
|
|
1470
|
-
timestampSeconds,
|
|
1471
|
-
timestampFraction,
|
|
1472
|
-
originalLength
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
function buildClassicPcap(args) {
|
|
1476
|
-
const globalHeader = Buffer.alloc(24);
|
|
1477
|
-
getPcapMagic(args.endianness, args.timestampPrecision).copy(globalHeader, 0);
|
|
1478
|
-
writeUint16(globalHeader, 4, 2, args.endianness);
|
|
1479
|
-
writeUint16(globalHeader, 6, 4, args.endianness);
|
|
1480
|
-
writeUint32(globalHeader, 8, 0, args.endianness);
|
|
1481
|
-
writeUint32(globalHeader, 12, 0, args.endianness);
|
|
1482
|
-
writeUint32(globalHeader, 16, args.snapLength, args.endianness);
|
|
1483
|
-
writeUint32(globalHeader, 20, args.linkType, args.endianness);
|
|
1484
|
-
const records = args.packets.map((packet) => {
|
|
1485
|
-
const header = Buffer.alloc(16);
|
|
1486
|
-
writeUint32(header, 0, packet.timestampSeconds, args.endianness);
|
|
1487
|
-
writeUint32(header, 4, packet.timestampFraction, args.endianness);
|
|
1488
|
-
writeUint32(header, 8, packet.data.length, args.endianness);
|
|
1489
|
-
writeUint32(header, 12, packet.originalLength, args.endianness);
|
|
1490
|
-
return Buffer.concat([header, packet.data]);
|
|
1491
|
-
});
|
|
1492
|
-
return Buffer.concat([globalHeader, ...records]);
|
|
1493
|
-
}
|
|
1494
|
-
function readClassicPcap(buffer, maxPackets, maxBytesPerPacket) {
|
|
1495
|
-
const header = parsePcapHeader(buffer);
|
|
1496
|
-
const packets = [];
|
|
1497
|
-
let offset = 24;
|
|
1498
|
-
while (offset < buffer.length) {
|
|
1499
|
-
if (maxPackets !== void 0 && packets.length >= maxPackets) break;
|
|
1500
|
-
if (offset + 16 > buffer.length) throw new Error("PCAP file ends with an incomplete packet header");
|
|
1501
|
-
const timestampSeconds = readUint32(buffer, offset, header.endianness);
|
|
1502
|
-
const timestampFraction = readUint32(buffer, offset + 4, header.endianness);
|
|
1503
|
-
const includedLength = readUint32(buffer, offset + 8, header.endianness);
|
|
1504
|
-
const originalLength = readUint32(buffer, offset + 12, header.endianness);
|
|
1505
|
-
offset += 16;
|
|
1506
|
-
if (offset + includedLength > buffer.length) throw new Error("PCAP file ends with an incomplete packet payload");
|
|
1507
|
-
const packetBytes = buffer.subarray(offset, offset + includedLength);
|
|
1508
|
-
offset += includedLength;
|
|
1509
|
-
const limit = maxBytesPerPacket === void 0 ? packetBytes.length : maxBytesPerPacket;
|
|
1510
|
-
const visibleLength = Math.min(limit, packetBytes.length);
|
|
1511
|
-
packets.push({
|
|
1512
|
-
index: packets.length,
|
|
1513
|
-
timestampSeconds,
|
|
1514
|
-
timestampFraction,
|
|
1515
|
-
includedLength,
|
|
1516
|
-
originalLength,
|
|
1517
|
-
dataHex: packetBytes.subarray(0, visibleLength).toString("hex"),
|
|
1518
|
-
truncated: visibleLength < packetBytes.length
|
|
1519
|
-
});
|
|
1614
|
+
getPayloadPrefix(state) {
|
|
1615
|
+
const payload = state.expectedPayload;
|
|
1616
|
+
if (!payload || payload.length < 8) return null;
|
|
1617
|
+
return payload.slice(0, 8).toLowerCase();
|
|
1520
1618
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
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
|
+
};
|
|
1526
1744
|
//#endregion
|
|
1527
|
-
//#region src/server/domains/protocol-analysis/handlers/
|
|
1528
|
-
|
|
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 {
|
|
1529
1754
|
engine;
|
|
1530
1755
|
inferrer;
|
|
1531
1756
|
eventBus;
|
|
@@ -1534,6 +1759,27 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1534
1759
|
this.inferrer = inferrer;
|
|
1535
1760
|
this.eventBus = eventBus;
|
|
1536
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 {
|
|
1537
1783
|
async handleDefinePattern(args) {
|
|
1538
1784
|
try {
|
|
1539
1785
|
const name = typeof args.name === "string" && args.name.trim().length > 0 ? args.name : "unnamed_pattern";
|
|
@@ -1571,7 +1817,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1571
1817
|
byteOrder: "big"
|
|
1572
1818
|
},
|
|
1573
1819
|
success: false,
|
|
1574
|
-
error:
|
|
1820
|
+
error: this.errorMessage(error)
|
|
1575
1821
|
};
|
|
1576
1822
|
}
|
|
1577
1823
|
}
|
|
@@ -1598,10 +1844,9 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1598
1844
|
fields: [],
|
|
1599
1845
|
byteOrder: "big"
|
|
1600
1846
|
};
|
|
1601
|
-
this.
|
|
1847
|
+
this.emitEvent("protocol:pattern_detected", {
|
|
1602
1848
|
patternName: namedPattern.name,
|
|
1603
|
-
confidence: 0
|
|
1604
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1849
|
+
confidence: 0
|
|
1605
1850
|
});
|
|
1606
1851
|
return {
|
|
1607
1852
|
patterns: [result],
|
|
@@ -1611,7 +1856,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1611
1856
|
return {
|
|
1612
1857
|
patterns: [],
|
|
1613
1858
|
success: false,
|
|
1614
|
-
error:
|
|
1859
|
+
error: this.errorMessage(error)
|
|
1615
1860
|
};
|
|
1616
1861
|
}
|
|
1617
1862
|
}
|
|
@@ -1626,7 +1871,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1626
1871
|
return {
|
|
1627
1872
|
fields: [],
|
|
1628
1873
|
success: false,
|
|
1629
|
-
error:
|
|
1874
|
+
error: this.errorMessage(error)
|
|
1630
1875
|
};
|
|
1631
1876
|
}
|
|
1632
1877
|
}
|
|
@@ -1637,18 +1882,18 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1637
1882
|
if (!pattern) return { schema: `// Error: pattern '${patternId}' not found` };
|
|
1638
1883
|
return { schema: this.getEngine().exportProto(pattern) };
|
|
1639
1884
|
} catch (error) {
|
|
1640
|
-
return { schema: `// Error: ${
|
|
1885
|
+
return { schema: `// Error: ${this.errorMessage(error)}` };
|
|
1641
1886
|
}
|
|
1642
1887
|
}
|
|
1643
1888
|
async handleInferStateMachine(args) {
|
|
1644
1889
|
try {
|
|
1645
1890
|
const rawMessages = args.messages;
|
|
1646
1891
|
if (!Array.isArray(rawMessages)) throw new Error("messages must be an array");
|
|
1647
|
-
const hasLegacyShape = rawMessages.some((message) => isRecord(message) && (message.direction === "in" || message.direction === "out"));
|
|
1892
|
+
const hasLegacyShape = rawMessages.some((message) => isRecord$1(message) && (message.direction === "in" || message.direction === "out"));
|
|
1648
1893
|
let stateMachine;
|
|
1649
1894
|
if (hasLegacyShape) {
|
|
1650
1895
|
const legacyMessages = rawMessages.map((message, index) => {
|
|
1651
|
-
if (!isRecord(message)) throw new Error(`messages[${index}] must be an object`);
|
|
1896
|
+
if (!isRecord$1(message)) throw new Error(`messages[${index}] must be an object`);
|
|
1652
1897
|
const direction = message.direction;
|
|
1653
1898
|
const payloadHex = typeof message.payloadHex === "string" ? message.payloadHex : "";
|
|
1654
1899
|
const timestamp = typeof message.timestamp === "number" ? message.timestamp : void 0;
|
|
@@ -1673,43 +1918,35 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1673
1918
|
};
|
|
1674
1919
|
} catch (error) {
|
|
1675
1920
|
return {
|
|
1676
|
-
stateMachine: {
|
|
1677
|
-
states: [],
|
|
1678
|
-
transitions: [],
|
|
1679
|
-
initial: "",
|
|
1680
|
-
initialState: "",
|
|
1681
|
-
finalStates: []
|
|
1682
|
-
},
|
|
1921
|
+
stateMachine: { ...EMPTY_STATE_MACHINE },
|
|
1683
1922
|
success: false,
|
|
1684
|
-
error:
|
|
1923
|
+
error: this.errorMessage(error)
|
|
1685
1924
|
};
|
|
1686
1925
|
}
|
|
1687
1926
|
}
|
|
1688
1927
|
async handleVisualizeState(args) {
|
|
1689
1928
|
try {
|
|
1690
1929
|
const stateMachineValue = args.stateMachine;
|
|
1691
|
-
if (!isRecord(stateMachineValue)) return { mermaidDiagram: this.getInferrer().generateMermaid(
|
|
1692
|
-
states: [],
|
|
1693
|
-
transitions: [],
|
|
1694
|
-
initial: "",
|
|
1695
|
-
initialState: "",
|
|
1696
|
-
finalStates: []
|
|
1697
|
-
}) };
|
|
1930
|
+
if (!isRecord$1(stateMachineValue)) return { mermaidDiagram: this.getInferrer().generateMermaid(EMPTY_STATE_MACHINE) };
|
|
1698
1931
|
const states = Array.isArray(stateMachineValue.states) ? stateMachineValue.states : [];
|
|
1699
1932
|
const transitions = Array.isArray(stateMachineValue.transitions) ? stateMachineValue.transitions : [];
|
|
1700
1933
|
const initialState = typeof stateMachineValue.initialState === "string" ? stateMachineValue.initialState : "";
|
|
1701
1934
|
const finalStates = Array.isArray(stateMachineValue.finalStates) ? stateMachineValue.finalStates.filter((state) => typeof state === "string") : [];
|
|
1702
1935
|
return { mermaidDiagram: this.getInferrer().generateMermaid({
|
|
1703
|
-
states: states.filter((state) => isRecord(state)),
|
|
1704
|
-
transitions: transitions.filter((transition) => isRecord(transition)),
|
|
1936
|
+
states: states.filter((state) => isRecord$1(state)),
|
|
1937
|
+
transitions: transitions.filter((transition) => isRecord$1(transition)),
|
|
1705
1938
|
initial: initialState,
|
|
1706
1939
|
initialState,
|
|
1707
1940
|
finalStates
|
|
1708
1941
|
}) };
|
|
1709
1942
|
} catch (error) {
|
|
1710
|
-
return { mermaidDiagram: `stateDiagram-v2\n note right of empty: ${
|
|
1943
|
+
return { mermaidDiagram: `stateDiagram-v2\n note right of empty: ${this.errorMessage(error)}` };
|
|
1711
1944
|
}
|
|
1712
1945
|
}
|
|
1946
|
+
};
|
|
1947
|
+
//#endregion
|
|
1948
|
+
//#region src/server/domains/protocol-analysis/handlers/payload-handlers.ts
|
|
1949
|
+
var ProtocolAnalysisPayloadHandlers = class extends ProtocolAnalysisPatternHandlers {
|
|
1713
1950
|
async handlePayloadTemplateBuild(args) {
|
|
1714
1951
|
try {
|
|
1715
1952
|
const rawFields = args.fields;
|
|
@@ -1731,7 +1968,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1731
1968
|
byteLength: 0,
|
|
1732
1969
|
fields: [],
|
|
1733
1970
|
success: false,
|
|
1734
|
-
error:
|
|
1971
|
+
error: this.errorMessage(error)
|
|
1735
1972
|
};
|
|
1736
1973
|
}
|
|
1737
1974
|
}
|
|
@@ -1768,16 +2005,20 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1768
2005
|
byteLength: 0,
|
|
1769
2006
|
appliedMutations: [],
|
|
1770
2007
|
success: false,
|
|
1771
|
-
error:
|
|
2008
|
+
error: this.errorMessage(error)
|
|
1772
2009
|
};
|
|
1773
2010
|
}
|
|
1774
2011
|
}
|
|
2012
|
+
};
|
|
2013
|
+
//#endregion
|
|
2014
|
+
//#region src/server/domains/protocol-analysis/handlers/link-layer-handlers.ts
|
|
2015
|
+
var ProtocolAnalysisLinkLayerHandlers = class extends ProtocolAnalysisPayloadHandlers {
|
|
1775
2016
|
async handleEthernetFrameBuild(args) {
|
|
1776
2017
|
try {
|
|
1777
2018
|
const destinationMac = parseMacAddress(args.destinationMac, "destinationMac");
|
|
1778
2019
|
const sourceMac = parseMacAddress(args.sourceMac, "sourceMac");
|
|
1779
2020
|
const etherType = parseEtherType(args.etherType, "etherType");
|
|
1780
|
-
const frame = buildEthernetFrame(destinationMac, sourceMac, etherType, parseHexPayload(args.payloadHex, "payloadHex"));
|
|
2021
|
+
const frame = buildEthernetFrame(destinationMac, sourceMac, etherType, parseHexPayload$1(args.payloadHex, "payloadHex"));
|
|
1781
2022
|
this.emitEvent("protocol:ethernet_frame_built", {
|
|
1782
2023
|
byteLength: frame.length,
|
|
1783
2024
|
etherType: `0x${etherType.toString(16).padStart(4, "0")}`
|
|
@@ -1802,7 +2043,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1802
2043
|
headerHex: "",
|
|
1803
2044
|
frameHex: "",
|
|
1804
2045
|
success: false,
|
|
1805
|
-
error:
|
|
2046
|
+
error: this.errorMessage(error)
|
|
1806
2047
|
};
|
|
1807
2048
|
}
|
|
1808
2049
|
}
|
|
@@ -1848,14 +2089,18 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1848
2089
|
targetMac: "",
|
|
1849
2090
|
targetIp: "",
|
|
1850
2091
|
success: false,
|
|
1851
|
-
error:
|
|
2092
|
+
error: this.errorMessage(error)
|
|
1852
2093
|
};
|
|
1853
2094
|
}
|
|
1854
2095
|
}
|
|
2096
|
+
};
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/server/domains/protocol-analysis/handlers/ip-packet-handlers.ts
|
|
2099
|
+
var ProtocolAnalysisIpPacketHandlers = class extends ProtocolAnalysisLinkLayerHandlers {
|
|
1855
2100
|
async handleRawIpPacketBuild(args) {
|
|
1856
2101
|
try {
|
|
1857
2102
|
const version = args.version === "ipv6" ? "ipv6" : "ipv4";
|
|
1858
|
-
const payload = parseHexPayload(args.payloadHex ?? "", "payloadHex");
|
|
2103
|
+
const payload = parseHexPayload$1(args.payloadHex ?? "", "payloadHex");
|
|
1859
2104
|
const protocol = parseIpProtocol(args.protocol, "protocol");
|
|
1860
2105
|
const dscp = args.dscp === void 0 ? 0 : parseNonNegativeInteger(args.dscp, "dscp");
|
|
1861
2106
|
const ecn = args.ecn === void 0 ? 0 : parseNonNegativeInteger(args.ecn, "ecn");
|
|
@@ -1937,7 +2182,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1937
2182
|
payloadHex: "",
|
|
1938
2183
|
checksumHex: null,
|
|
1939
2184
|
success: false,
|
|
1940
|
-
error:
|
|
2185
|
+
error: this.errorMessage(error)
|
|
1941
2186
|
};
|
|
1942
2187
|
}
|
|
1943
2188
|
}
|
|
@@ -1948,7 +2193,7 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1948
2193
|
const sequenceNumber = args.sequenceNumber === void 0 ? 0 : parseNonNegativeInteger(args.sequenceNumber, "sequenceNumber");
|
|
1949
2194
|
if (identifier > 65535) throw new Error("identifier must be between 0 and 65535");
|
|
1950
2195
|
if (sequenceNumber > 65535) throw new Error("sequenceNumber must be between 0 and 65535");
|
|
1951
|
-
const payload = parseHexPayload(args.payloadHex ?? "", "payloadHex");
|
|
2196
|
+
const payload = parseHexPayload$1(args.payloadHex ?? "", "payloadHex");
|
|
1952
2197
|
const { packet, checksum } = buildIcmpEcho({
|
|
1953
2198
|
operation,
|
|
1954
2199
|
identifier,
|
|
@@ -1983,13 +2228,17 @@ var ProtocolAnalysisHandlers = class {
|
|
|
1983
2228
|
packetHex: "",
|
|
1984
2229
|
payloadHex: "",
|
|
1985
2230
|
success: false,
|
|
1986
|
-
error:
|
|
2231
|
+
error: this.errorMessage(error)
|
|
1987
2232
|
};
|
|
1988
2233
|
}
|
|
1989
2234
|
}
|
|
2235
|
+
};
|
|
2236
|
+
//#endregion
|
|
2237
|
+
//#region src/server/domains/protocol-analysis/handlers/checksum-handlers.ts
|
|
2238
|
+
var ProtocolAnalysisChecksumHandlers = class extends ProtocolAnalysisIpPacketHandlers {
|
|
1990
2239
|
async handleChecksumApply(args) {
|
|
1991
2240
|
try {
|
|
1992
|
-
const payload = parseHexPayload(args.hexPayload, "hexPayload");
|
|
2241
|
+
const payload = parseHexPayload$1(args.hexPayload, "hexPayload");
|
|
1993
2242
|
const rangeStart = args.startOffset === void 0 ? 0 : parseNonNegativeInteger(args.startOffset, "startOffset");
|
|
1994
2243
|
const rangeEnd = args.endOffset === void 0 ? payload.length : parseNonNegativeInteger(args.endOffset, "endOffset");
|
|
1995
2244
|
if (rangeStart > rangeEnd || rangeEnd > payload.length) throw new Error("checksum range must stay within the payload");
|
|
@@ -2031,13 +2280,23 @@ var ProtocolAnalysisHandlers = class {
|
|
|
2031
2280
|
rangeStart: 0,
|
|
2032
2281
|
rangeEnd: 0,
|
|
2033
2282
|
success: false,
|
|
2034
|
-
error:
|
|
2283
|
+
error: this.errorMessage(error)
|
|
2035
2284
|
};
|
|
2036
2285
|
}
|
|
2037
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 {
|
|
2038
2297
|
async handlePcapWrite(args) {
|
|
2039
2298
|
try {
|
|
2040
|
-
|
|
2299
|
+
const path = this.parseRequiredPath(args);
|
|
2041
2300
|
if (!Array.isArray(args.packets)) throw new Error("packets must be an array");
|
|
2042
2301
|
const packets = args.packets.map((entry, index) => parsePcapPacketInput(entry, index));
|
|
2043
2302
|
const endianness = parsePacketEndianness(args.endianness);
|
|
@@ -2051,14 +2310,14 @@ var ProtocolAnalysisHandlers = class {
|
|
|
2051
2310
|
snapLength,
|
|
2052
2311
|
linkType
|
|
2053
2312
|
});
|
|
2054
|
-
await writeFile$1(
|
|
2313
|
+
await writeFile$1(path, buffer);
|
|
2055
2314
|
this.emitEvent("protocol:pcap_written", {
|
|
2056
|
-
path
|
|
2315
|
+
path,
|
|
2057
2316
|
packetCount: packets.length,
|
|
2058
2317
|
byteLength: buffer.length
|
|
2059
2318
|
});
|
|
2060
2319
|
return {
|
|
2061
|
-
path
|
|
2320
|
+
path,
|
|
2062
2321
|
packetCount: packets.length,
|
|
2063
2322
|
byteLength: buffer.length,
|
|
2064
2323
|
endianness,
|
|
@@ -2075,22 +2334,22 @@ var ProtocolAnalysisHandlers = class {
|
|
|
2075
2334
|
timestampPrecision: null,
|
|
2076
2335
|
linkType: null,
|
|
2077
2336
|
success: false,
|
|
2078
|
-
error:
|
|
2337
|
+
error: this.errorMessage(error)
|
|
2079
2338
|
};
|
|
2080
2339
|
}
|
|
2081
2340
|
}
|
|
2082
2341
|
async handlePcapRead(args) {
|
|
2083
2342
|
try {
|
|
2084
|
-
|
|
2343
|
+
const path = this.parseRequiredPath(args);
|
|
2085
2344
|
const maxPackets = args.maxPackets === void 0 ? void 0 : parsePositiveInteger(args.maxPackets, "maxPackets");
|
|
2086
2345
|
const maxBytesPerPacket = args.maxBytesPerPacket === void 0 ? void 0 : parsePositiveInteger(args.maxBytesPerPacket, "maxBytesPerPacket");
|
|
2087
|
-
const { header, packets } = readClassicPcap(await readFile$1(
|
|
2346
|
+
const { header, packets } = readClassicPcap(await readFile$1(path), maxPackets, maxBytesPerPacket);
|
|
2088
2347
|
this.emitEvent("protocol:pcap_read", {
|
|
2089
|
-
path
|
|
2348
|
+
path,
|
|
2090
2349
|
packetCount: packets.length
|
|
2091
2350
|
});
|
|
2092
2351
|
return {
|
|
2093
|
-
path
|
|
2352
|
+
path,
|
|
2094
2353
|
header,
|
|
2095
2354
|
packets,
|
|
2096
2355
|
success: true
|
|
@@ -2101,24 +2360,178 @@ var ProtocolAnalysisHandlers = class {
|
|
|
2101
2360
|
header: null,
|
|
2102
2361
|
packets: [],
|
|
2103
2362
|
success: false,
|
|
2104
|
-
error:
|
|
2363
|
+
error: this.errorMessage(error)
|
|
2105
2364
|
};
|
|
2106
2365
|
}
|
|
2107
2366
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
getEngine() {
|
|
2115
|
-
if (!this.engine) this.engine = new ProtocolPatternEngine();
|
|
2116
|
-
return this.engine;
|
|
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;
|
|
2117
2370
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
+
});
|
|
2121
2528
|
}
|
|
2122
2529
|
};
|
|
2123
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
|
|
2124
2537
|
export { ProtocolAnalysisHandlers };
|