fingerprint-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/LICENSE +663 -0
  2. package/README.ar.md +767 -0
  3. package/README.bn.md +767 -0
  4. package/README.bs.md +767 -0
  5. package/README.da.md +767 -0
  6. package/README.de.md +770 -0
  7. package/README.el.md +767 -0
  8. package/README.es.md +769 -0
  9. package/README.fr.md +769 -0
  10. package/README.hi.md +767 -0
  11. package/README.it.md +767 -0
  12. package/README.ja.md +767 -0
  13. package/README.ko.md +772 -0
  14. package/README.md +767 -0
  15. package/README.no.md +767 -0
  16. package/README.pl.md +767 -0
  17. package/README.pt-BR.md +767 -0
  18. package/README.ru.md +767 -0
  19. package/README.th.md +767 -0
  20. package/README.tr.md +767 -0
  21. package/README.uk.md +767 -0
  22. package/README.vi.md +767 -0
  23. package/README.zh-TW.md +768 -0
  24. package/README.zh.md +768 -0
  25. package/dist/app/index.d.ts +3 -0
  26. package/dist/app/index.d.ts.map +1 -0
  27. package/dist/app/index.js +614 -0
  28. package/dist/app/index.js.map +1 -0
  29. package/dist/composite/analyze.d.ts +3 -0
  30. package/dist/composite/analyze.d.ts.map +1 -0
  31. package/dist/composite/analyze.js +59 -0
  32. package/dist/composite/analyze.js.map +1 -0
  33. package/dist/composite/correlate.d.ts +3 -0
  34. package/dist/composite/correlate.d.ts.map +1 -0
  35. package/dist/composite/correlate.js +184 -0
  36. package/dist/composite/correlate.js.map +1 -0
  37. package/dist/composite/enumerate.d.ts +3 -0
  38. package/dist/composite/enumerate.d.ts.map +1 -0
  39. package/dist/composite/enumerate.js +94 -0
  40. package/dist/composite/enumerate.js.map +1 -0
  41. package/dist/composite/helpers.d.ts +22 -0
  42. package/dist/composite/helpers.d.ts.map +1 -0
  43. package/dist/composite/helpers.js +111 -0
  44. package/dist/composite/helpers.js.map +1 -0
  45. package/dist/composite/index.d.ts +3 -0
  46. package/dist/composite/index.d.ts.map +1 -0
  47. package/dist/composite/index.js +29 -0
  48. package/dist/composite/index.js.map +1 -0
  49. package/dist/composite/meta.d.ts +3 -0
  50. package/dist/composite/meta.d.ts.map +1 -0
  51. package/dist/composite/meta.js +23 -0
  52. package/dist/composite/meta.js.map +1 -0
  53. package/dist/composite/osint.d.ts +3 -0
  54. package/dist/composite/osint.d.ts.map +1 -0
  55. package/dist/composite/osint.js +40 -0
  56. package/dist/composite/osint.js.map +1 -0
  57. package/dist/composite/recon.d.ts +3 -0
  58. package/dist/composite/recon.d.ts.map +1 -0
  59. package/dist/composite/recon.js +131 -0
  60. package/dist/composite/recon.js.map +1 -0
  61. package/dist/composite/scan-dns.d.ts +3 -0
  62. package/dist/composite/scan-dns.d.ts.map +1 -0
  63. package/dist/composite/scan-dns.js +44 -0
  64. package/dist/composite/scan-dns.js.map +1 -0
  65. package/dist/composite/scan-http.d.ts +3 -0
  66. package/dist/composite/scan-http.d.ts.map +1 -0
  67. package/dist/composite/scan-http.js +68 -0
  68. package/dist/composite/scan-http.js.map +1 -0
  69. package/dist/composite/scan-paths.d.ts +3 -0
  70. package/dist/composite/scan-paths.d.ts.map +1 -0
  71. package/dist/composite/scan-paths.js +32 -0
  72. package/dist/composite/scan-paths.js.map +1 -0
  73. package/dist/composite/scan-ports.d.ts +3 -0
  74. package/dist/composite/scan-ports.d.ts.map +1 -0
  75. package/dist/composite/scan-ports.js +79 -0
  76. package/dist/composite/scan-ports.js.map +1 -0
  77. package/dist/composite/scan-services.d.ts +3 -0
  78. package/dist/composite/scan-services.d.ts.map +1 -0
  79. package/dist/composite/scan-services.js +111 -0
  80. package/dist/composite/scan-services.js.map +1 -0
  81. package/dist/composite/scan-tls.d.ts +3 -0
  82. package/dist/composite/scan-tls.d.ts.map +1 -0
  83. package/dist/composite/scan-tls.js +32 -0
  84. package/dist/composite/scan-tls.js.map +1 -0
  85. package/dist/composite/scan-waf.d.ts +3 -0
  86. package/dist/composite/scan-waf.d.ts.map +1 -0
  87. package/dist/composite/scan-waf.js +35 -0
  88. package/dist/composite/scan-waf.js.map +1 -0
  89. package/dist/correlation/index.d.ts +3 -0
  90. package/dist/correlation/index.d.ts.map +1 -0
  91. package/dist/correlation/index.js +1044 -0
  92. package/dist/correlation/index.js.map +1 -0
  93. package/dist/data/analytics-patterns.d.ts +21 -0
  94. package/dist/data/analytics-patterns.d.ts.map +1 -0
  95. package/dist/data/analytics-patterns.js +449 -0
  96. package/dist/data/analytics-patterns.js.map +1 -0
  97. package/dist/data/app-signatures.d.ts +52 -0
  98. package/dist/data/app-signatures.d.ts.map +1 -0
  99. package/dist/data/app-signatures.js +1143 -0
  100. package/dist/data/app-signatures.js.map +1 -0
  101. package/dist/data/c2-signatures.d.ts +54 -0
  102. package/dist/data/c2-signatures.d.ts.map +1 -0
  103. package/dist/data/c2-signatures.js +256 -0
  104. package/dist/data/c2-signatures.js.map +1 -0
  105. package/dist/data/cloud-ranges.d.ts +34 -0
  106. package/dist/data/cloud-ranges.d.ts.map +1 -0
  107. package/dist/data/cloud-ranges.js +628 -0
  108. package/dist/data/cloud-ranges.js.map +1 -0
  109. package/dist/data/cookie-patterns.d.ts +28 -0
  110. package/dist/data/cookie-patterns.d.ts.map +1 -0
  111. package/dist/data/cookie-patterns.js +225 -0
  112. package/dist/data/cookie-patterns.js.map +1 -0
  113. package/dist/data/error-signatures.d.ts +19 -0
  114. package/dist/data/error-signatures.d.ts.map +1 -0
  115. package/dist/data/error-signatures.js +321 -0
  116. package/dist/data/error-signatures.js.map +1 -0
  117. package/dist/data/favicon-hashes.d.ts +25 -0
  118. package/dist/data/favicon-hashes.d.ts.map +1 -0
  119. package/dist/data/favicon-hashes.js +249 -0
  120. package/dist/data/favicon-hashes.js.map +1 -0
  121. package/dist/data/h2-signatures.d.ts +50 -0
  122. package/dist/data/h2-signatures.d.ts.map +1 -0
  123. package/dist/data/h2-signatures.js +211 -0
  124. package/dist/data/h2-signatures.js.map +1 -0
  125. package/dist/data/header-order.d.ts +38 -0
  126. package/dist/data/header-order.d.ts.map +1 -0
  127. package/dist/data/header-order.js +185 -0
  128. package/dist/data/header-order.js.map +1 -0
  129. package/dist/data/jarm-signatures.d.ts +25 -0
  130. package/dist/data/jarm-signatures.d.ts.map +1 -0
  131. package/dist/data/jarm-signatures.js +213 -0
  132. package/dist/data/jarm-signatures.js.map +1 -0
  133. package/dist/data/saas-patterns.d.ts +23 -0
  134. package/dist/data/saas-patterns.d.ts.map +1 -0
  135. package/dist/data/saas-patterns.js +139 -0
  136. package/dist/data/saas-patterns.js.map +1 -0
  137. package/dist/data/sensitive-paths.d.ts +12 -0
  138. package/dist/data/sensitive-paths.d.ts.map +1 -0
  139. package/dist/data/sensitive-paths.js +1539 -0
  140. package/dist/data/sensitive-paths.js.map +1 -0
  141. package/dist/data/service-banners.d.ts +59 -0
  142. package/dist/data/service-banners.d.ts.map +1 -0
  143. package/dist/data/service-banners.js +323 -0
  144. package/dist/data/service-banners.js.map +1 -0
  145. package/dist/data/smtp-signatures.d.ts +12 -0
  146. package/dist/data/smtp-signatures.d.ts.map +1 -0
  147. package/dist/data/smtp-signatures.js +350 -0
  148. package/dist/data/smtp-signatures.js.map +1 -0
  149. package/dist/data/takeover-patterns.d.ts +37 -0
  150. package/dist/data/takeover-patterns.d.ts.map +1 -0
  151. package/dist/data/takeover-patterns.js +249 -0
  152. package/dist/data/takeover-patterns.js.map +1 -0
  153. package/dist/data/tech-patterns.d.ts +44 -0
  154. package/dist/data/tech-patterns.d.ts.map +1 -0
  155. package/dist/data/tech-patterns.js +690 -0
  156. package/dist/data/tech-patterns.js.map +1 -0
  157. package/dist/data/waf-signatures.d.ts +21 -0
  158. package/dist/data/waf-signatures.d.ts.map +1 -0
  159. package/dist/data/waf-signatures.js +318 -0
  160. package/dist/data/waf-signatures.js.map +1 -0
  161. package/dist/dns/index.d.ts +3 -0
  162. package/dist/dns/index.d.ts.map +1 -0
  163. package/dist/dns/index.js +1067 -0
  164. package/dist/dns/index.js.map +1 -0
  165. package/dist/enum/index.d.ts +3 -0
  166. package/dist/enum/index.d.ts.map +1 -0
  167. package/dist/enum/index.js +818 -0
  168. package/dist/enum/index.js.map +1 -0
  169. package/dist/h2/index.d.ts +3 -0
  170. package/dist/h2/index.d.ts.map +1 -0
  171. package/dist/h2/index.js +414 -0
  172. package/dist/h2/index.js.map +1 -0
  173. package/dist/http/index.d.ts +3 -0
  174. package/dist/http/index.d.ts.map +1 -0
  175. package/dist/http/index.js +2444 -0
  176. package/dist/http/index.js.map +1 -0
  177. package/dist/identify/index.d.ts +3 -0
  178. package/dist/identify/index.d.ts.map +1 -0
  179. package/dist/identify/index.js +447 -0
  180. package/dist/identify/index.js.map +1 -0
  181. package/dist/index.d.ts +3 -0
  182. package/dist/index.d.ts.map +1 -0
  183. package/dist/index.js +165 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/infra/index.d.ts +3 -0
  186. package/dist/infra/index.d.ts.map +1 -0
  187. package/dist/infra/index.js +989 -0
  188. package/dist/infra/index.js.map +1 -0
  189. package/dist/iot/index.d.ts +3 -0
  190. package/dist/iot/index.d.ts.map +1 -0
  191. package/dist/iot/index.js +610 -0
  192. package/dist/iot/index.js.map +1 -0
  193. package/dist/meta/index.d.ts +3 -0
  194. package/dist/meta/index.d.ts.map +1 -0
  195. package/dist/meta/index.js +442 -0
  196. package/dist/meta/index.js.map +1 -0
  197. package/dist/osint/index.d.ts +3 -0
  198. package/dist/osint/index.d.ts.map +1 -0
  199. package/dist/osint/index.js +687 -0
  200. package/dist/osint/index.js.map +1 -0
  201. package/dist/passive/index.d.ts +3 -0
  202. package/dist/passive/index.d.ts.map +1 -0
  203. package/dist/passive/index.js +944 -0
  204. package/dist/passive/index.js.map +1 -0
  205. package/dist/path/index.d.ts +3 -0
  206. package/dist/path/index.d.ts.map +1 -0
  207. package/dist/path/index.js +878 -0
  208. package/dist/path/index.js.map +1 -0
  209. package/dist/protocol/mcp-server.d.ts +4 -0
  210. package/dist/protocol/mcp-server.d.ts.map +1 -0
  211. package/dist/protocol/mcp-server.js +32 -0
  212. package/dist/protocol/mcp-server.js.map +1 -0
  213. package/dist/protocol/tools.d.ts +3 -0
  214. package/dist/protocol/tools.d.ts.map +1 -0
  215. package/dist/protocol/tools.js +5 -0
  216. package/dist/protocol/tools.js.map +1 -0
  217. package/dist/service/index.d.ts +3 -0
  218. package/dist/service/index.d.ts.map +1 -0
  219. package/dist/service/index.js +1053 -0
  220. package/dist/service/index.js.map +1 -0
  221. package/dist/smtp/index.d.ts +3 -0
  222. package/dist/smtp/index.d.ts.map +1 -0
  223. package/dist/smtp/index.js +437 -0
  224. package/dist/smtp/index.js.map +1 -0
  225. package/dist/ssh/index.d.ts +3 -0
  226. package/dist/ssh/index.d.ts.map +1 -0
  227. package/dist/ssh/index.js +676 -0
  228. package/dist/ssh/index.js.map +1 -0
  229. package/dist/tcp/index.d.ts +3 -0
  230. package/dist/tcp/index.d.ts.map +1 -0
  231. package/dist/tcp/index.js +369 -0
  232. package/dist/tcp/index.js.map +1 -0
  233. package/dist/timing/index.d.ts +3 -0
  234. package/dist/timing/index.d.ts.map +1 -0
  235. package/dist/timing/index.js +425 -0
  236. package/dist/timing/index.js.map +1 -0
  237. package/dist/tls/index.d.ts +3 -0
  238. package/dist/tls/index.d.ts.map +1 -0
  239. package/dist/tls/index.js +1332 -0
  240. package/dist/tls/index.js.map +1 -0
  241. package/dist/types/index.d.ts +26 -0
  242. package/dist/types/index.d.ts.map +1 -0
  243. package/dist/types/index.js +8 -0
  244. package/dist/types/index.js.map +1 -0
  245. package/dist/utils/cache.d.ts +11 -0
  246. package/dist/utils/cache.d.ts.map +1 -0
  247. package/dist/utils/cache.js +35 -0
  248. package/dist/utils/cache.js.map +1 -0
  249. package/dist/utils/murmurhash3.d.ts +3 -0
  250. package/dist/utils/murmurhash3.d.ts.map +1 -0
  251. package/dist/utils/murmurhash3.js +57 -0
  252. package/dist/utils/murmurhash3.js.map +1 -0
  253. package/dist/utils/rate-limiter.d.ts +10 -0
  254. package/dist/utils/rate-limiter.d.ts.map +1 -0
  255. package/dist/utils/rate-limiter.js +35 -0
  256. package/dist/utils/rate-limiter.js.map +1 -0
  257. package/dist/utils/require-key.d.ts +2 -0
  258. package/dist/utils/require-key.d.ts.map +1 -0
  259. package/dist/utils/require-key.js +8 -0
  260. package/dist/utils/require-key.js.map +1 -0
  261. package/dist/waf/index.d.ts +3 -0
  262. package/dist/waf/index.d.ts.map +1 -0
  263. package/dist/waf/index.js +767 -0
  264. package/dist/waf/index.js.map +1 -0
  265. package/dist/web/index.d.ts +3 -0
  266. package/dist/web/index.d.ts.map +1 -0
  267. package/dist/web/index.js +1366 -0
  268. package/dist/web/index.js.map +1 -0
  269. package/package.json +66 -0
@@ -0,0 +1,1053 @@
1
+ import { z } from "zod";
2
+ import { RateLimiter } from "../utils/rate-limiter.js";
3
+ import { TTLCache } from "../utils/cache.js";
4
+ import { json } from "../types/index.js";
5
+ import * as net from "node:net";
6
+ import { SERVICE_BANNERS, MYSQL_AUTH_PLUGIN_MAP } from "../data/service-banners.js";
7
+ const limiter = new RateLimiter(300);
8
+ const cache = new TTLCache(600_000);
9
+ // ─── TCP Helpers ───
10
+ function tcpConnect(host, port, timeoutMs = 5000) {
11
+ return new Promise((resolve, reject) => {
12
+ const socket = new net.Socket();
13
+ socket.setTimeout(timeoutMs);
14
+ socket.on("timeout", () => { socket.destroy(); reject(new Error("Connection timeout")); });
15
+ socket.on("error", reject);
16
+ socket.connect(port, host, () => resolve(socket));
17
+ });
18
+ }
19
+ function readBytes(socket, timeoutMs = 3000) {
20
+ return new Promise((resolve, reject) => {
21
+ const chunks = [];
22
+ const timer = setTimeout(() => { socket.destroy(); resolve(Buffer.concat(chunks)); }, timeoutMs);
23
+ socket.on("data", (chunk) => { chunks.push(chunk); });
24
+ socket.on("end", () => { clearTimeout(timer); resolve(Buffer.concat(chunks)); });
25
+ socket.on("error", (err) => { clearTimeout(timer); reject(err); });
26
+ });
27
+ }
28
+ function sendAndRead(socket, data, timeoutMs = 3000) {
29
+ return new Promise((resolve, reject) => {
30
+ const chunks = [];
31
+ const timer = setTimeout(() => { resolve(Buffer.concat(chunks)); }, timeoutMs);
32
+ socket.on("data", (chunk) => { chunks.push(chunk); });
33
+ socket.on("error", (err) => { clearTimeout(timer); reject(err); });
34
+ socket.write(data, () => { });
35
+ socket.on("end", () => { clearTimeout(timer); resolve(Buffer.concat(chunks)); });
36
+ });
37
+ }
38
+ // ─── Tool 1: svc_mysql_probe ───
39
+ async function mysqlProbe(args, _ctx) {
40
+ const host = args.host;
41
+ const port = args.port ?? 3306;
42
+ const cacheKey = `mysql:${host}:${port}`;
43
+ const cached = cache.get(cacheKey);
44
+ if (cached)
45
+ return json(cached);
46
+ await limiter.acquire();
47
+ let socket = null;
48
+ try {
49
+ socket = await tcpConnect(host, port);
50
+ const data = await readBytes(socket, 5000);
51
+ if (data.length < 5) {
52
+ const result = { host, port, error: "No greeting packet received or packet too short" };
53
+ cache.set(cacheKey, result);
54
+ return json(result);
55
+ }
56
+ // Parse MySQL greeting packet
57
+ // Bytes 0-2: packet length (3 bytes LE)
58
+ const packetLength = data.readUIntLE(0, 3);
59
+ // Byte 3: sequence number
60
+ const sequenceNumber = data.readUInt8(3);
61
+ // Byte 4: protocol version (usually 10)
62
+ const protocolVersion = data.readUInt8(4);
63
+ // Bytes 5+: server version (null-terminated string)
64
+ let offset = 5;
65
+ const versionEnd = data.indexOf(0x00, offset);
66
+ if (versionEnd === -1) {
67
+ const result = { host, port, error: "Malformed greeting: no null-terminated version string" };
68
+ cache.set(cacheKey, result);
69
+ return json(result);
70
+ }
71
+ const serverVersion = data.subarray(offset, versionEnd).toString("utf-8");
72
+ offset = versionEnd + 1;
73
+ // Connection ID (4 bytes LE)
74
+ let connectionId = 0;
75
+ if (offset + 4 <= data.length) {
76
+ connectionId = data.readUInt32LE(offset);
77
+ offset += 4;
78
+ }
79
+ // auth_plugin_data_part_1 (8 bytes) — skip over
80
+ if (offset + 8 <= data.length) {
81
+ offset += 8;
82
+ }
83
+ // filler (1 byte 0x00)
84
+ if (offset < data.length) {
85
+ offset += 1;
86
+ }
87
+ // capability_flags lower 2 bytes
88
+ let capabilityFlags = 0;
89
+ if (offset + 2 <= data.length) {
90
+ capabilityFlags = data.readUInt16LE(offset);
91
+ offset += 2;
92
+ }
93
+ // character_set (1 byte)
94
+ let characterSet = 0;
95
+ if (offset < data.length) {
96
+ characterSet = data.readUInt8(offset);
97
+ offset += 1;
98
+ }
99
+ // status_flags (2 bytes LE)
100
+ let statusFlags = 0;
101
+ if (offset + 2 <= data.length) {
102
+ statusFlags = data.readUInt16LE(offset);
103
+ offset += 2;
104
+ }
105
+ // capability_flags upper 2 bytes
106
+ if (offset + 2 <= data.length) {
107
+ capabilityFlags |= data.readUInt16LE(offset) << 16;
108
+ offset += 2;
109
+ }
110
+ // auth_data_len (1 byte)
111
+ let authDataLen = 0;
112
+ if (offset < data.length) {
113
+ authDataLen = data.readUInt8(offset);
114
+ offset += 1;
115
+ }
116
+ // reserved (10 bytes 0x00)
117
+ if (offset + 10 <= data.length) {
118
+ offset += 10;
119
+ }
120
+ // auth_plugin_data_part_2 (max(13, auth_data_len - 8) bytes)
121
+ const part2Len = Math.max(13, authDataLen - 8);
122
+ if (offset + part2Len <= data.length) {
123
+ offset += part2Len;
124
+ }
125
+ // auth_plugin_name (null-terminated)
126
+ let authPlugin = "";
127
+ if (offset < data.length) {
128
+ const pluginEnd = data.indexOf(0x00, offset);
129
+ if (pluginEnd !== -1) {
130
+ authPlugin = data.subarray(offset, pluginEnd).toString("utf-8");
131
+ }
132
+ else {
133
+ authPlugin = data.subarray(offset).toString("utf-8").replace(/\0/g, "");
134
+ }
135
+ }
136
+ // Determine variant
137
+ let variant = "MySQL";
138
+ if (/mariadb/i.test(serverVersion)) {
139
+ variant = "MariaDB";
140
+ }
141
+ else if (/percona/i.test(serverVersion)) {
142
+ variant = "Percona";
143
+ }
144
+ // Auth plugin info
145
+ const authPluginInfo = MYSQL_AUTH_PLUGIN_MAP[authPlugin] ?? "Unknown auth plugin";
146
+ // Character set name mapping (common ones)
147
+ const charsetMap = {
148
+ 8: "latin1",
149
+ 33: "utf8mb3",
150
+ 45: "utf8mb4",
151
+ 63: "binary",
152
+ 224: "utf8mb4_unicode_ci",
153
+ 255: "utf8mb4_0900_ai_ci",
154
+ };
155
+ const characterSetName = charsetMap[characterSet] ?? `charset_id_${characterSet}`;
156
+ // Intelligence notes
157
+ const intelligence = [];
158
+ if (variant === "MariaDB") {
159
+ intelligence.push("MariaDB fork detected — community-driven MySQL alternative.");
160
+ }
161
+ else if (variant === "Percona") {
162
+ intelligence.push("Percona Server detected — performance-optimized MySQL fork.");
163
+ }
164
+ if (authPlugin === "mysql_native_password") {
165
+ intelligence.push("Using mysql_native_password — default for MySQL 5.x. Consider upgrading to caching_sha2_password (MySQL 8.0+).");
166
+ }
167
+ else if (authPlugin === "caching_sha2_password") {
168
+ intelligence.push("Using caching_sha2_password — default for MySQL 8.0+, provides stronger authentication.");
169
+ }
170
+ else if (authPlugin === "mysql_old_password") {
171
+ intelligence.push("CRITICAL: Using deprecated mysql_old_password — extremely weak, vulnerable to replay attacks.");
172
+ }
173
+ if (protocolVersion !== 10) {
174
+ intelligence.push(`Non-standard protocol version ${protocolVersion} detected (expected 10).`);
175
+ }
176
+ // Try to send quit command gracefully
177
+ try {
178
+ const quitPacket = Buffer.from([0x01, 0x00, 0x00, 0x00, 0x01]); // COM_QUIT
179
+ socket.write(quitPacket);
180
+ }
181
+ catch {
182
+ // Ignore write errors on cleanup
183
+ }
184
+ const result = {
185
+ host,
186
+ port,
187
+ protocol_version: protocolVersion,
188
+ server_version: serverVersion,
189
+ variant,
190
+ connection_id: connectionId,
191
+ auth_plugin: authPlugin,
192
+ auth_plugin_info: authPluginInfo,
193
+ character_set: characterSetName,
194
+ capabilities: `0x${capabilityFlags.toString(16).padStart(8, "0")}`,
195
+ status_flags: `0x${statusFlags.toString(16).padStart(4, "0")}`,
196
+ intelligence,
197
+ };
198
+ cache.set(cacheKey, result);
199
+ return json(result);
200
+ }
201
+ catch (err) {
202
+ const error = err instanceof Error ? err.message : String(err);
203
+ const result = { host, port, error: `MySQL probe failed: ${error}` };
204
+ cache.set(cacheKey, result);
205
+ return json(result);
206
+ }
207
+ finally {
208
+ if (socket) {
209
+ try {
210
+ socket.destroy();
211
+ }
212
+ catch { /* ignore */ }
213
+ }
214
+ }
215
+ }
216
+ // ─── Tool 2: svc_postgres_probe ───
217
+ async function postgresProbe(args, _ctx) {
218
+ const host = args.host;
219
+ const port = args.port ?? 5432;
220
+ const cacheKey = `postgres:${host}:${port}`;
221
+ const cached = cache.get(cacheKey);
222
+ if (cached)
223
+ return json(cached);
224
+ await limiter.acquire();
225
+ let socket = null;
226
+ try {
227
+ socket = await tcpConnect(host, port);
228
+ // Build StartupMessage: length(4) + version(4) + "user\0postgres\0\0"
229
+ const userParam = Buffer.from("user\0postgres\0\0", "utf-8"); // user\0postgres\0 + trailing \0
230
+ const startupLen = 4 + 4 + userParam.length; // 4 (length) + 4 (version) + params
231
+ const startup = Buffer.alloc(startupLen);
232
+ startup.writeInt32BE(startupLen, 0); // length includes self
233
+ startup.writeInt32BE(196608, 4); // version 3.0 = 0x00030000
234
+ userParam.copy(startup, 8);
235
+ const response = await sendAndRead(socket, startup, 5000);
236
+ if (response.length === 0) {
237
+ const result = { host, port, error: "No response from PostgreSQL server" };
238
+ cache.set(cacheKey, result);
239
+ return json(result);
240
+ }
241
+ // Parse PostgreSQL protocol messages
242
+ let offset = 0;
243
+ let authMethod = "unknown";
244
+ const parameters = {};
245
+ let backendPid = 0;
246
+ let backendSecret = 0;
247
+ let serverVersion = "unknown";
248
+ let errorMessage = null;
249
+ while (offset < response.length) {
250
+ if (offset + 5 > response.length)
251
+ break;
252
+ const msgType = String.fromCharCode(response.readUInt8(offset));
253
+ const msgLen = response.readInt32BE(offset + 1); // includes self (4 bytes) but not the type byte
254
+ const msgEnd = offset + 1 + msgLen;
255
+ if (msgEnd > response.length)
256
+ break;
257
+ const payload = response.subarray(offset + 5, msgEnd);
258
+ switch (msgType) {
259
+ case "R": {
260
+ // Authentication
261
+ if (payload.length >= 4) {
262
+ const authType = payload.readInt32BE(0);
263
+ const authMap = {
264
+ 0: "AuthenticationOk",
265
+ 2: "KerberosV5",
266
+ 3: "CleartextPassword",
267
+ 5: "MD5Password",
268
+ 6: "SCMCredential",
269
+ 7: "GSS",
270
+ 8: "GSSContinue",
271
+ 9: "SSPI",
272
+ 10: "SASL",
273
+ 11: "SASLContinue",
274
+ 12: "SASLFinal",
275
+ };
276
+ authMethod = authMap[authType] ?? `Unknown(${authType})`;
277
+ }
278
+ break;
279
+ }
280
+ case "S": {
281
+ // ParameterStatus: key\0value\0
282
+ const paramStr = payload.toString("utf-8");
283
+ const nullIdx = paramStr.indexOf("\0");
284
+ if (nullIdx !== -1) {
285
+ const key = paramStr.substring(0, nullIdx);
286
+ const value = paramStr.substring(nullIdx + 1).replace(/\0$/, "");
287
+ parameters[key] = value;
288
+ if (key === "server_version") {
289
+ serverVersion = value;
290
+ }
291
+ }
292
+ break;
293
+ }
294
+ case "K": {
295
+ // BackendKeyData: pid(4) + secret(4)
296
+ if (payload.length >= 8) {
297
+ backendPid = payload.readInt32BE(0);
298
+ backendSecret = payload.readInt32BE(4);
299
+ }
300
+ break;
301
+ }
302
+ case "E": {
303
+ // ErrorResponse: severity\0message\0...
304
+ // Format: sequence of (byte field-type + string\0), terminated by \0
305
+ const errStr = payload.toString("utf-8");
306
+ const fields = {};
307
+ let eOffset = 0;
308
+ while (eOffset < errStr.length) {
309
+ const fieldType = errStr[eOffset];
310
+ if (fieldType === "\0")
311
+ break;
312
+ eOffset++;
313
+ const fieldEnd = errStr.indexOf("\0", eOffset);
314
+ if (fieldEnd === -1)
315
+ break;
316
+ fields[fieldType] = errStr.substring(eOffset, fieldEnd);
317
+ eOffset = fieldEnd + 1;
318
+ }
319
+ errorMessage = fields["M"] ?? errStr.slice(0, 200);
320
+ break;
321
+ }
322
+ case "Z": {
323
+ // ReadyForQuery — we're done
324
+ break;
325
+ }
326
+ default:
327
+ break;
328
+ }
329
+ offset = msgEnd;
330
+ }
331
+ // Send Terminate message
332
+ try {
333
+ const terminate = Buffer.alloc(5);
334
+ terminate.writeUInt8(0x58, 0); // 'X'
335
+ terminate.writeInt32BE(4, 1);
336
+ socket.write(terminate);
337
+ }
338
+ catch {
339
+ // Ignore write errors on cleanup
340
+ }
341
+ // Intelligence
342
+ const intelligence = [];
343
+ if (authMethod === "AuthenticationOk") {
344
+ intelligence.push("CRITICAL: Server accepted connection without authentication — trust-based auth or no password set.");
345
+ }
346
+ else if (authMethod === "MD5Password") {
347
+ intelligence.push("Using MD5 password authentication — consider upgrading to SCRAM-SHA-256 (scram-sha-256) for stronger security.");
348
+ }
349
+ else if (authMethod === "CleartextPassword") {
350
+ intelligence.push("CRITICAL: Cleartext password authentication — passwords sent in plain text over the wire.");
351
+ }
352
+ else if (authMethod === "SASL") {
353
+ intelligence.push("Using SASL authentication (likely SCRAM-SHA-256) — modern and secure.");
354
+ }
355
+ if (parameters["ssl_library"]) {
356
+ intelligence.push(`SSL library: ${parameters["ssl_library"]}`);
357
+ }
358
+ if (errorMessage) {
359
+ intelligence.push(`Server returned error: ${errorMessage}`);
360
+ }
361
+ if (serverVersion !== "unknown") {
362
+ const majorVersion = parseInt(serverVersion.split(".")[0], 10);
363
+ if (majorVersion < 14) {
364
+ intelligence.push(`PostgreSQL ${serverVersion} may be near or past end-of-life — check official support policy.`);
365
+ }
366
+ }
367
+ const result = {
368
+ host,
369
+ port,
370
+ server_version: serverVersion,
371
+ auth_method: authMethod,
372
+ parameters,
373
+ backend_pid: backendPid || undefined,
374
+ error: errorMessage ?? undefined,
375
+ intelligence,
376
+ };
377
+ cache.set(cacheKey, result);
378
+ return json(result);
379
+ }
380
+ catch (err) {
381
+ const error = err instanceof Error ? err.message : String(err);
382
+ const result = { host, port, error: `PostgreSQL probe failed: ${error}` };
383
+ cache.set(cacheKey, result);
384
+ return json(result);
385
+ }
386
+ finally {
387
+ if (socket) {
388
+ try {
389
+ socket.destroy();
390
+ }
391
+ catch { /* ignore */ }
392
+ }
393
+ }
394
+ }
395
+ // ─── Tool 3: svc_redis_probe ───
396
+ async function redisProbe(args, _ctx) {
397
+ const host = args.host;
398
+ const port = args.port ?? 6379;
399
+ const cacheKey = `redis:${host}:${port}`;
400
+ const cached = cache.get(cacheKey);
401
+ if (cached)
402
+ return json(cached);
403
+ await limiter.acquire();
404
+ let socket = null;
405
+ try {
406
+ socket = await tcpConnect(host, port);
407
+ // Send PING
408
+ const pingResponse = await sendAndRead(socket, "PING\r\n", 3000);
409
+ const pingStr = pingResponse.toString("utf-8").trim();
410
+ let authRequired = false;
411
+ let protectedMode = false;
412
+ if (pingStr.startsWith("-NOAUTH")) {
413
+ authRequired = true;
414
+ const intelligence = [
415
+ "Redis requires authentication — AUTH command needed before any other commands.",
416
+ "Ensure a strong password is configured (requirepass directive).",
417
+ ];
418
+ const result = {
419
+ host,
420
+ port,
421
+ version: null,
422
+ mode: null,
423
+ os: null,
424
+ arch: null,
425
+ uptime_seconds: null,
426
+ connected_clients: null,
427
+ memory: null,
428
+ role: null,
429
+ keyspace: null,
430
+ auth_required: true,
431
+ protected_mode: false,
432
+ intelligence,
433
+ };
434
+ cache.set(cacheKey, result);
435
+ return json(result);
436
+ }
437
+ if (pingStr.startsWith("-DENIED")) {
438
+ protectedMode = true;
439
+ const intelligence = [
440
+ "Redis is running in protected mode — connections from non-loopback interfaces are denied.",
441
+ "This is a security feature; disable only if proper authentication is configured.",
442
+ ];
443
+ const result = {
444
+ host,
445
+ port,
446
+ version: null,
447
+ mode: null,
448
+ os: null,
449
+ arch: null,
450
+ uptime_seconds: null,
451
+ connected_clients: null,
452
+ memory: null,
453
+ role: null,
454
+ keyspace: null,
455
+ auth_required: false,
456
+ protected_mode: true,
457
+ intelligence,
458
+ };
459
+ cache.set(cacheKey, result);
460
+ return json(result);
461
+ }
462
+ if (!pingStr.startsWith("+PONG")) {
463
+ const result = {
464
+ host,
465
+ port,
466
+ error: `Unexpected PING response: ${pingStr.slice(0, 200)}`,
467
+ auth_required: false,
468
+ protected_mode: false,
469
+ };
470
+ cache.set(cacheKey, result);
471
+ return json(result);
472
+ }
473
+ // PONG received — send INFO command
474
+ // Need a new socket since the previous sendAndRead may have consumed events
475
+ socket.destroy();
476
+ socket = await tcpConnect(host, port);
477
+ const infoResponse = await sendAndRead(socket, "INFO\r\n", 5000);
478
+ const infoStr = infoResponse.toString("utf-8");
479
+ // Parse INFO response — format: $length\r\n<bulk data>\r\n
480
+ // The bulk data contains key:value lines separated by \r\n, with section headers starting with #
481
+ const infoLines = infoStr.split("\r\n");
482
+ const infoMap = {};
483
+ for (const line of infoLines) {
484
+ if (line.startsWith("#") || line.startsWith("$") || line.length === 0)
485
+ continue;
486
+ const colonIdx = line.indexOf(":");
487
+ if (colonIdx !== -1) {
488
+ const key = line.substring(0, colonIdx).trim();
489
+ const value = line.substring(colonIdx + 1).trim();
490
+ infoMap[key] = value;
491
+ }
492
+ }
493
+ const version = infoMap["redis_version"] ?? null;
494
+ const mode = infoMap["redis_mode"] ?? null;
495
+ const os = infoMap["os"] ?? null;
496
+ const arch = infoMap["arch_bits"] ? `${infoMap["arch_bits"]}-bit` : null;
497
+ const uptimeSeconds = infoMap["uptime_in_seconds"] ? parseInt(infoMap["uptime_in_seconds"], 10) : null;
498
+ const connectedClients = infoMap["connected_clients"] ? parseInt(infoMap["connected_clients"], 10) : null;
499
+ const memory = {};
500
+ if (infoMap["used_memory_human"])
501
+ memory["used"] = infoMap["used_memory_human"];
502
+ if (infoMap["used_memory_peak_human"])
503
+ memory["peak"] = infoMap["used_memory_peak_human"];
504
+ const role = infoMap["role"] ?? null;
505
+ const connectedSlaves = infoMap["connected_slaves"] ? parseInt(infoMap["connected_slaves"], 10) : null;
506
+ // Parse keyspace
507
+ const keyspace = {};
508
+ for (const [key, value] of Object.entries(infoMap)) {
509
+ if (key.startsWith("db")) {
510
+ keyspace[key] = value;
511
+ }
512
+ }
513
+ // Send QUIT
514
+ try {
515
+ socket.write("QUIT\r\n");
516
+ }
517
+ catch {
518
+ // Ignore write errors on cleanup
519
+ }
520
+ // Intelligence
521
+ const intelligence = [];
522
+ if (!authRequired && !protectedMode) {
523
+ intelligence.push("WARNING: Redis accepted commands without authentication — no password is set (requirepass is empty).");
524
+ }
525
+ if (version) {
526
+ const majorMinor = version.split(".").slice(0, 2).join(".");
527
+ const major = parseInt(version.split(".")[0], 10);
528
+ if (major < 6) {
529
+ intelligence.push(`Redis ${version} is outdated — consider upgrading to 7.x for security patches and ACL support.`);
530
+ }
531
+ else if (major < 7) {
532
+ intelligence.push(`Redis ${version} — ACL support available (since 6.0). Ensure ACL rules are configured.`);
533
+ }
534
+ }
535
+ if (role === "master" && connectedSlaves !== null && connectedSlaves > 0) {
536
+ intelligence.push(`Master node with ${connectedSlaves} connected slave(s) — replication is active.`);
537
+ }
538
+ else if (role === "slave") {
539
+ intelligence.push("This instance is a replica (slave) — data is read from master.");
540
+ }
541
+ if (Object.keys(keyspace).length === 0) {
542
+ intelligence.push("No keyspace data found — database may be empty or INFO keyspace is restricted.");
543
+ }
544
+ const result = {
545
+ host,
546
+ port,
547
+ version,
548
+ mode,
549
+ os,
550
+ arch,
551
+ uptime_seconds: uptimeSeconds,
552
+ connected_clients: connectedClients,
553
+ memory: Object.keys(memory).length > 0 ? memory : null,
554
+ role,
555
+ connected_slaves: connectedSlaves,
556
+ keyspace: Object.keys(keyspace).length > 0 ? keyspace : null,
557
+ auth_required: authRequired,
558
+ protected_mode: protectedMode,
559
+ intelligence,
560
+ };
561
+ cache.set(cacheKey, result);
562
+ return json(result);
563
+ }
564
+ catch (err) {
565
+ const error = err instanceof Error ? err.message : String(err);
566
+ const result = { host, port, error: `Redis probe failed: ${error}` };
567
+ cache.set(cacheKey, result);
568
+ return json(result);
569
+ }
570
+ finally {
571
+ if (socket) {
572
+ try {
573
+ socket.destroy();
574
+ }
575
+ catch { /* ignore */ }
576
+ }
577
+ }
578
+ }
579
+ // ─── Tool 4: svc_ftp_probe ───
580
+ async function ftpProbe(args, _ctx) {
581
+ const host = args.host;
582
+ const port = args.port ?? 21;
583
+ const cacheKey = `ftp:${host}:${port}`;
584
+ const cached = cache.get(cacheKey);
585
+ if (cached)
586
+ return json(cached);
587
+ await limiter.acquire();
588
+ let socket = null;
589
+ try {
590
+ socket = await tcpConnect(host, port);
591
+ // Read 220 banner (FTP servers send greeting immediately)
592
+ const bannerData = await readBytes(socket, 5000);
593
+ const bannerStr = bannerData.toString("utf-8").trim();
594
+ if (!bannerStr.startsWith("220")) {
595
+ const result = {
596
+ host,
597
+ port,
598
+ error: `Unexpected FTP banner: ${bannerStr.slice(0, 200)}`,
599
+ banner: bannerStr.slice(0, 500),
600
+ };
601
+ cache.set(cacheKey, result);
602
+ return json(result);
603
+ }
604
+ // Match banner against SERVICE_BANNERS for FTP
605
+ let software = null;
606
+ let version = null;
607
+ const ftpBanners = SERVICE_BANNERS.filter((b) => b.service === "ftp");
608
+ for (const entry of ftpBanners) {
609
+ const regex = new RegExp(entry.pattern);
610
+ const match = bannerStr.match(regex);
611
+ if (match) {
612
+ software = entry.name;
613
+ if (match[1]) {
614
+ version = match[1];
615
+ }
616
+ break;
617
+ }
618
+ }
619
+ // If no SERVICE_BANNERS match, try additional known patterns
620
+ if (!software) {
621
+ if (/vsftpd/i.test(bannerStr)) {
622
+ software = "vsftpd";
623
+ const m = bannerStr.match(/vsftpd\s+([\d.]+)/i);
624
+ if (m)
625
+ version = m[1];
626
+ }
627
+ else if (/proftpd/i.test(bannerStr)) {
628
+ software = "ProFTPD";
629
+ const m = bannerStr.match(/ProFTPD\s+([\d.]+\w*)/i);
630
+ if (m)
631
+ version = m[1];
632
+ }
633
+ else if (/pure-ftpd/i.test(bannerStr)) {
634
+ software = "Pure-FTPd";
635
+ }
636
+ else if (/filezilla/i.test(bannerStr)) {
637
+ software = "FileZilla Server";
638
+ const m = bannerStr.match(/FileZilla Server\s+([\d.]+)/i);
639
+ if (m)
640
+ version = m[1];
641
+ }
642
+ else if (/microsoft ftp/i.test(bannerStr)) {
643
+ software = "Microsoft IIS FTP";
644
+ }
645
+ }
646
+ // Need new socket for SYST command (previous readBytes consumed events)
647
+ socket.destroy();
648
+ socket = await tcpConnect(host, port);
649
+ // Read and discard the banner again
650
+ await readBytes(socket, 3000);
651
+ // Send SYST command
652
+ let systemType = null;
653
+ try {
654
+ socket.destroy();
655
+ socket = await tcpConnect(host, port);
656
+ await readBytes(socket, 3000); // consume banner
657
+ const systResponse = await sendAndRead(socket, "SYST\r\n", 3000);
658
+ const systStr = systResponse.toString("utf-8").trim();
659
+ if (systStr.startsWith("215")) {
660
+ systemType = systStr.replace(/^215\s+/, "").trim();
661
+ }
662
+ }
663
+ catch {
664
+ // SYST may not be supported
665
+ }
666
+ // Send FEAT command
667
+ let features = [];
668
+ let tlsSupport = false;
669
+ try {
670
+ socket.destroy();
671
+ socket = await tcpConnect(host, port);
672
+ await readBytes(socket, 3000); // consume banner
673
+ const featResponse = await sendAndRead(socket, "FEAT\r\n", 3000);
674
+ const featStr = featResponse.toString("utf-8").trim();
675
+ if (featStr.includes("211")) {
676
+ const featLines = featStr.split("\r\n");
677
+ for (const line of featLines) {
678
+ const trimmed = line.trim();
679
+ // Feature lines are indented after "211-" header and before "211 End"
680
+ if (trimmed.startsWith("211-") || trimmed.startsWith("211 "))
681
+ continue;
682
+ if (trimmed.length > 0) {
683
+ features.push(trimmed);
684
+ }
685
+ }
686
+ // Check for TLS support
687
+ tlsSupport = features.some((f) => /AUTH\s+TLS/i.test(f) || /AUTH\s+SSL/i.test(f));
688
+ }
689
+ }
690
+ catch {
691
+ // FEAT may not be supported
692
+ }
693
+ // Send QUIT
694
+ try {
695
+ socket.write("QUIT\r\n");
696
+ }
697
+ catch {
698
+ // Ignore write errors on cleanup
699
+ }
700
+ // Intelligence
701
+ const intelligence = [];
702
+ if (software) {
703
+ intelligence.push(`Identified FTP server software: ${software}${version ? ` v${version}` : ""}.`);
704
+ }
705
+ if (!tlsSupport) {
706
+ intelligence.push("WARNING: No TLS support detected (AUTH TLS/SSL not in FEAT) — credentials transmitted in cleartext.");
707
+ }
708
+ else {
709
+ intelligence.push("TLS support available via AUTH TLS/SSL — encrypted connections possible.");
710
+ }
711
+ if (software === "vsftpd" && version) {
712
+ const major = parseInt(version.split(".")[0], 10);
713
+ const minor = parseInt(version.split(".")[1], 10);
714
+ if (major < 3 || (major === 3 && minor === 0 && parseInt(version.split(".")[2], 10) < 3)) {
715
+ intelligence.push("vsftpd version may be outdated — check for known CVEs.");
716
+ }
717
+ }
718
+ if (features.length === 0) {
719
+ intelligence.push("FEAT command returned no features or is not supported — limited protocol extension support.");
720
+ }
721
+ if (systemType) {
722
+ intelligence.push(`System type: ${systemType}`);
723
+ }
724
+ const result = {
725
+ host,
726
+ port,
727
+ banner: bannerStr.slice(0, 500),
728
+ software,
729
+ version,
730
+ system_type: systemType,
731
+ features,
732
+ tls_support: tlsSupport,
733
+ intelligence,
734
+ };
735
+ cache.set(cacheKey, result);
736
+ return json(result);
737
+ }
738
+ catch (err) {
739
+ const error = err instanceof Error ? err.message : String(err);
740
+ const result = { host, port, error: `FTP probe failed: ${error}` };
741
+ cache.set(cacheKey, result);
742
+ return json(result);
743
+ }
744
+ finally {
745
+ if (socket) {
746
+ try {
747
+ socket.destroy();
748
+ }
749
+ catch { /* ignore */ }
750
+ }
751
+ }
752
+ }
753
+ // VNC security type names
754
+ const VNC_SECURITY_TYPES = {
755
+ 0: "Invalid",
756
+ 1: "None",
757
+ 2: "VNC Authentication",
758
+ 5: "RA2",
759
+ 6: "RA2ne",
760
+ 16: "Tight",
761
+ 18: "TLS",
762
+ 19: "VeNCrypt",
763
+ 30: "Apple Remote Desktop",
764
+ };
765
+ async function vncRdpDetect(args, _ctx) {
766
+ const host = args.host;
767
+ const ports = args.ports ?? [3389, 5900, 5901];
768
+ const cacheKey = `vnc_rdp:${host}:${ports.join(",")}`;
769
+ const cached = cache.get(cacheKey);
770
+ if (cached)
771
+ return json(cached);
772
+ await limiter.acquire();
773
+ const results = [];
774
+ for (const port of ports) {
775
+ let socket = null;
776
+ try {
777
+ socket = await tcpConnect(host, port, 5000);
778
+ // Determine if this is a VNC or RDP port
779
+ if (port >= 5900 && port <= 5999) {
780
+ // VNC detection
781
+ const data = await readBytes(socket, 3000);
782
+ const vncBanner = data.toString("utf-8").trim();
783
+ if (vncBanner.startsWith("RFB")) {
784
+ // Parse "RFB xxx.yyy\n"
785
+ const versionMatch = vncBanner.match(/^RFB\s+(\d{3}\.\d{3})/);
786
+ const vncVersion = versionMatch ? versionMatch[1] : null;
787
+ // Read security types
788
+ const securityTypes = [];
789
+ // After version exchange, server sends number of security types + types
790
+ // We need to send our version back first
791
+ try {
792
+ socket.destroy();
793
+ socket = await tcpConnect(host, port, 5000);
794
+ const versionData = await readBytes(socket, 2000);
795
+ const versionStr = versionData.toString("utf-8").trim();
796
+ if (versionStr.startsWith("RFB")) {
797
+ // Send back the same version string
798
+ const sendVersion = versionStr.endsWith("\n") ? versionStr : versionStr + "\n";
799
+ const secData = await sendAndRead(socket, sendVersion + "\n", 2000);
800
+ if (secData.length > 0) {
801
+ const numTypes = secData.readUInt8(0);
802
+ for (let i = 1; i <= numTypes && i < secData.length; i++) {
803
+ const typeId = secData.readUInt8(i);
804
+ const typeName = VNC_SECURITY_TYPES[typeId] ?? `Unknown(${typeId})`;
805
+ securityTypes.push(typeName);
806
+ }
807
+ }
808
+ }
809
+ }
810
+ catch {
811
+ // Security type reading failed — we still have the version
812
+ }
813
+ results.push({
814
+ port,
815
+ protocol: "VNC",
816
+ version: vncVersion,
817
+ details: {
818
+ rfb_version: vncVersion,
819
+ security_types: securityTypes.length > 0 ? securityTypes : undefined,
820
+ },
821
+ open: true,
822
+ });
823
+ }
824
+ else {
825
+ // Port open but not VNC
826
+ results.push({
827
+ port,
828
+ protocol: "unknown",
829
+ version: null,
830
+ details: { banner: vncBanner.slice(0, 200) || null },
831
+ open: true,
832
+ });
833
+ }
834
+ }
835
+ else if (port === 3389) {
836
+ // RDP detection — send X.224 Connection Request TPDU
837
+ // TPKT header: 03 00 00 13 (version 3, reserved 0, length 19)
838
+ // X.224 CR: 0e e0 00 00 00 00 00 01 00 08 00 03 00 00 00
839
+ const x224Request = Buffer.from([
840
+ 0x03, 0x00, 0x00, 0x13, // TPKT header
841
+ 0x0e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, // X.224 CR header
842
+ 0x01, 0x00, 0x08, 0x00, // cookie/routing
843
+ 0x03, 0x00, 0x00, 0x00, // RDP Negotiation Request (protocol TLS | CredSSP)
844
+ ]);
845
+ const rdpResponse = await sendAndRead(socket, x224Request, 3000);
846
+ if (rdpResponse.length > 0) {
847
+ const isTPKT = rdpResponse.readUInt8(0) === 0x03;
848
+ // Check for X.224 Connection Confirm (0xd0)
849
+ let isConnectionConfirm = false;
850
+ let rdpProtocol = null;
851
+ if (isTPKT && rdpResponse.length >= 7) {
852
+ const x224Type = rdpResponse.readUInt8(5) & 0xf0;
853
+ isConnectionConfirm = x224Type === 0xd0;
854
+ // Parse RDP Negotiation Response if present
855
+ if (isConnectionConfirm && rdpResponse.length >= 19) {
856
+ // Look for RDP Negotiation Response at offset 11
857
+ try {
858
+ const negType = rdpResponse.readUInt8(11);
859
+ if (negType === 0x02) {
860
+ // TYPE_RDP_NEG_RSP
861
+ const selectedProto = rdpResponse.readUInt32LE(15);
862
+ const protoMap = {
863
+ 0: "Standard RDP Security",
864
+ 1: "TLS 1.0+",
865
+ 2: "CredSSP (NLA)",
866
+ 3: "TLS + CredSSP",
867
+ 8: "RDSTLS",
868
+ };
869
+ rdpProtocol = protoMap[selectedProto] ?? `Protocol(${selectedProto})`;
870
+ }
871
+ else if (negType === 0x03) {
872
+ // TYPE_RDP_NEG_FAILURE
873
+ rdpProtocol = "Negotiation failed";
874
+ }
875
+ }
876
+ catch {
877
+ // Failed to parse negotiation response
878
+ }
879
+ }
880
+ }
881
+ if (isConnectionConfirm) {
882
+ results.push({
883
+ port,
884
+ protocol: "RDP",
885
+ version: null,
886
+ details: {
887
+ tpkt_response: true,
888
+ connection_confirm: true,
889
+ negotiated_protocol: rdpProtocol,
890
+ response_length: rdpResponse.length,
891
+ },
892
+ open: true,
893
+ });
894
+ }
895
+ else {
896
+ // Port open, got a response, but not RDP Connection Confirm
897
+ results.push({
898
+ port,
899
+ protocol: "unknown",
900
+ version: null,
901
+ details: {
902
+ tpkt_response: isTPKT,
903
+ response_hex: rdpResponse.toString("hex").slice(0, 64),
904
+ response_length: rdpResponse.length,
905
+ },
906
+ open: true,
907
+ });
908
+ }
909
+ }
910
+ else {
911
+ // Port open but no response to X.224
912
+ results.push({
913
+ port,
914
+ protocol: "unknown",
915
+ version: null,
916
+ details: null,
917
+ open: true,
918
+ });
919
+ }
920
+ }
921
+ else {
922
+ // Non-standard port — try VNC first, then RDP
923
+ const data = await readBytes(socket, 2000);
924
+ const banner = data.toString("utf-8").trim();
925
+ if (banner.startsWith("RFB")) {
926
+ const versionMatch = banner.match(/^RFB\s+(\d{3}\.\d{3})/);
927
+ results.push({
928
+ port,
929
+ protocol: "VNC",
930
+ version: versionMatch ? versionMatch[1] : null,
931
+ details: { rfb_version: versionMatch ? versionMatch[1] : null },
932
+ open: true,
933
+ });
934
+ }
935
+ else {
936
+ results.push({
937
+ port,
938
+ protocol: "unknown",
939
+ version: null,
940
+ details: { banner: banner.slice(0, 200) || null },
941
+ open: true,
942
+ });
943
+ }
944
+ }
945
+ }
946
+ catch (err) {
947
+ const error = err instanceof Error ? err.message : String(err);
948
+ results.push({
949
+ port,
950
+ protocol: "unknown",
951
+ version: null,
952
+ details: { error },
953
+ open: false,
954
+ });
955
+ }
956
+ finally {
957
+ if (socket) {
958
+ try {
959
+ socket.destroy();
960
+ }
961
+ catch { /* ignore */ }
962
+ }
963
+ }
964
+ }
965
+ // Intelligence
966
+ const intelligence = [];
967
+ const vncResults = results.filter((r) => r.protocol === "VNC");
968
+ const rdpResults = results.filter((r) => r.protocol === "RDP");
969
+ const openResults = results.filter((r) => r.open);
970
+ if (vncResults.length > 0) {
971
+ intelligence.push(`VNC detected on port(s): ${vncResults.map((r) => r.port).join(", ")}.`);
972
+ for (const r of vncResults) {
973
+ const secTypes = r.details?.security_types;
974
+ if (secTypes && secTypes.includes("None")) {
975
+ intelligence.push(`CRITICAL: VNC on port ${r.port} allows unauthenticated access (security type "None").`);
976
+ }
977
+ if (secTypes && secTypes.includes("VNC Authentication")) {
978
+ intelligence.push(`VNC on port ${r.port} uses VNC Authentication — password-only, no username. Susceptible to brute force.`);
979
+ }
980
+ }
981
+ }
982
+ if (rdpResults.length > 0) {
983
+ intelligence.push(`RDP detected on port(s): ${rdpResults.map((r) => r.port).join(", ")}.`);
984
+ for (const r of rdpResults) {
985
+ const negotiated = r.details?.negotiated_protocol;
986
+ if (negotiated === "Standard RDP Security") {
987
+ intelligence.push(`WARNING: RDP on port ${r.port} using Standard RDP Security — no NLA, vulnerable to MITM and brute force.`);
988
+ }
989
+ else if (negotiated && negotiated.includes("CredSSP")) {
990
+ intelligence.push(`RDP on port ${r.port} supports Network Level Authentication (NLA/CredSSP) — good security posture.`);
991
+ }
992
+ }
993
+ }
994
+ if (openResults.length === 0) {
995
+ intelligence.push("No remote desktop services detected on the specified ports.");
996
+ }
997
+ const result = {
998
+ host,
999
+ results,
1000
+ intelligence,
1001
+ };
1002
+ cache.set(cacheKey, result);
1003
+ return json(result);
1004
+ }
1005
+ // ─── Tool Definitions ───
1006
+ export const serviceTools = [
1007
+ {
1008
+ name: "svc_mysql_probe",
1009
+ description: "MySQL/MariaDB fingerprinting via greeting packet analysis. Connects to the MySQL port, reads the initial handshake packet, and extracts server version, variant (MySQL/MariaDB/Percona), authentication plugin, character set, capability flags, and connection metadata.",
1010
+ schema: {
1011
+ host: z.string().describe("Target host"),
1012
+ port: z.number().optional().default(3306).describe("MySQL port (default: 3306)"),
1013
+ },
1014
+ execute: mysqlProbe,
1015
+ },
1016
+ {
1017
+ name: "svc_postgres_probe",
1018
+ description: "PostgreSQL fingerprinting via StartupMessage. Sends a protocol 3.0 startup message, parses Authentication, ParameterStatus, BackendKeyData, and Error responses to extract server version, auth method, encoding, timezone, and other server parameters.",
1019
+ schema: {
1020
+ host: z.string().describe("Target host"),
1021
+ port: z.number().optional().default(5432).describe("PostgreSQL port (default: 5432)"),
1022
+ },
1023
+ execute: postgresProbe,
1024
+ },
1025
+ {
1026
+ name: "svc_redis_probe",
1027
+ description: "Redis fingerprinting via PING and INFO commands. Detects authentication requirements, protected mode, and extracts version, mode, OS, architecture, uptime, connected clients, memory usage, replication role, and keyspace statistics.",
1028
+ schema: {
1029
+ host: z.string().describe("Target host"),
1030
+ port: z.number().optional().default(6379).describe("Redis port (default: 6379)"),
1031
+ },
1032
+ execute: redisProbe,
1033
+ },
1034
+ {
1035
+ name: "svc_ftp_probe",
1036
+ description: "FTP server fingerprinting via banner, SYST, and FEAT commands. Identifies server software (vsftpd, ProFTPD, Pure-FTPd, FileZilla, IIS FTP), extracts version, system type, supported features, and TLS availability.",
1037
+ schema: {
1038
+ host: z.string().describe("Target host"),
1039
+ port: z.number().optional().default(21).describe("FTP port (default: 21)"),
1040
+ },
1041
+ execute: ftpProbe,
1042
+ },
1043
+ {
1044
+ name: "svc_vnc_rdp_detect",
1045
+ description: "VNC and RDP remote desktop detection. Probes specified ports for VNC (RFB protocol handshake, security types) and RDP (X.224 Connection Request/Confirm, NLA negotiation). Reports protocol version, authentication methods, and security posture.",
1046
+ schema: {
1047
+ host: z.string().describe("Target host"),
1048
+ ports: z.array(z.number()).optional().default([3389, 5900, 5901]).describe("Ports to check (default: [3389, 5900, 5901])"),
1049
+ },
1050
+ execute: vncRdpDetect,
1051
+ },
1052
+ ];
1053
+ //# sourceMappingURL=index.js.map