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.
- package/LICENSE +663 -0
- package/README.ar.md +767 -0
- package/README.bn.md +767 -0
- package/README.bs.md +767 -0
- package/README.da.md +767 -0
- package/README.de.md +770 -0
- package/README.el.md +767 -0
- package/README.es.md +769 -0
- package/README.fr.md +769 -0
- package/README.hi.md +767 -0
- package/README.it.md +767 -0
- package/README.ja.md +767 -0
- package/README.ko.md +772 -0
- package/README.md +767 -0
- package/README.no.md +767 -0
- package/README.pl.md +767 -0
- package/README.pt-BR.md +767 -0
- package/README.ru.md +767 -0
- package/README.th.md +767 -0
- package/README.tr.md +767 -0
- package/README.uk.md +767 -0
- package/README.vi.md +767 -0
- package/README.zh-TW.md +768 -0
- package/README.zh.md +768 -0
- package/dist/app/index.d.ts +3 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +614 -0
- package/dist/app/index.js.map +1 -0
- package/dist/composite/analyze.d.ts +3 -0
- package/dist/composite/analyze.d.ts.map +1 -0
- package/dist/composite/analyze.js +59 -0
- package/dist/composite/analyze.js.map +1 -0
- package/dist/composite/correlate.d.ts +3 -0
- package/dist/composite/correlate.d.ts.map +1 -0
- package/dist/composite/correlate.js +184 -0
- package/dist/composite/correlate.js.map +1 -0
- package/dist/composite/enumerate.d.ts +3 -0
- package/dist/composite/enumerate.d.ts.map +1 -0
- package/dist/composite/enumerate.js +94 -0
- package/dist/composite/enumerate.js.map +1 -0
- package/dist/composite/helpers.d.ts +22 -0
- package/dist/composite/helpers.d.ts.map +1 -0
- package/dist/composite/helpers.js +111 -0
- package/dist/composite/helpers.js.map +1 -0
- package/dist/composite/index.d.ts +3 -0
- package/dist/composite/index.d.ts.map +1 -0
- package/dist/composite/index.js +29 -0
- package/dist/composite/index.js.map +1 -0
- package/dist/composite/meta.d.ts +3 -0
- package/dist/composite/meta.d.ts.map +1 -0
- package/dist/composite/meta.js +23 -0
- package/dist/composite/meta.js.map +1 -0
- package/dist/composite/osint.d.ts +3 -0
- package/dist/composite/osint.d.ts.map +1 -0
- package/dist/composite/osint.js +40 -0
- package/dist/composite/osint.js.map +1 -0
- package/dist/composite/recon.d.ts +3 -0
- package/dist/composite/recon.d.ts.map +1 -0
- package/dist/composite/recon.js +131 -0
- package/dist/composite/recon.js.map +1 -0
- package/dist/composite/scan-dns.d.ts +3 -0
- package/dist/composite/scan-dns.d.ts.map +1 -0
- package/dist/composite/scan-dns.js +44 -0
- package/dist/composite/scan-dns.js.map +1 -0
- package/dist/composite/scan-http.d.ts +3 -0
- package/dist/composite/scan-http.d.ts.map +1 -0
- package/dist/composite/scan-http.js +68 -0
- package/dist/composite/scan-http.js.map +1 -0
- package/dist/composite/scan-paths.d.ts +3 -0
- package/dist/composite/scan-paths.d.ts.map +1 -0
- package/dist/composite/scan-paths.js +32 -0
- package/dist/composite/scan-paths.js.map +1 -0
- package/dist/composite/scan-ports.d.ts +3 -0
- package/dist/composite/scan-ports.d.ts.map +1 -0
- package/dist/composite/scan-ports.js +79 -0
- package/dist/composite/scan-ports.js.map +1 -0
- package/dist/composite/scan-services.d.ts +3 -0
- package/dist/composite/scan-services.d.ts.map +1 -0
- package/dist/composite/scan-services.js +111 -0
- package/dist/composite/scan-services.js.map +1 -0
- package/dist/composite/scan-tls.d.ts +3 -0
- package/dist/composite/scan-tls.d.ts.map +1 -0
- package/dist/composite/scan-tls.js +32 -0
- package/dist/composite/scan-tls.js.map +1 -0
- package/dist/composite/scan-waf.d.ts +3 -0
- package/dist/composite/scan-waf.d.ts.map +1 -0
- package/dist/composite/scan-waf.js +35 -0
- package/dist/composite/scan-waf.js.map +1 -0
- package/dist/correlation/index.d.ts +3 -0
- package/dist/correlation/index.d.ts.map +1 -0
- package/dist/correlation/index.js +1044 -0
- package/dist/correlation/index.js.map +1 -0
- package/dist/data/analytics-patterns.d.ts +21 -0
- package/dist/data/analytics-patterns.d.ts.map +1 -0
- package/dist/data/analytics-patterns.js +449 -0
- package/dist/data/analytics-patterns.js.map +1 -0
- package/dist/data/app-signatures.d.ts +52 -0
- package/dist/data/app-signatures.d.ts.map +1 -0
- package/dist/data/app-signatures.js +1143 -0
- package/dist/data/app-signatures.js.map +1 -0
- package/dist/data/c2-signatures.d.ts +54 -0
- package/dist/data/c2-signatures.d.ts.map +1 -0
- package/dist/data/c2-signatures.js +256 -0
- package/dist/data/c2-signatures.js.map +1 -0
- package/dist/data/cloud-ranges.d.ts +34 -0
- package/dist/data/cloud-ranges.d.ts.map +1 -0
- package/dist/data/cloud-ranges.js +628 -0
- package/dist/data/cloud-ranges.js.map +1 -0
- package/dist/data/cookie-patterns.d.ts +28 -0
- package/dist/data/cookie-patterns.d.ts.map +1 -0
- package/dist/data/cookie-patterns.js +225 -0
- package/dist/data/cookie-patterns.js.map +1 -0
- package/dist/data/error-signatures.d.ts +19 -0
- package/dist/data/error-signatures.d.ts.map +1 -0
- package/dist/data/error-signatures.js +321 -0
- package/dist/data/error-signatures.js.map +1 -0
- package/dist/data/favicon-hashes.d.ts +25 -0
- package/dist/data/favicon-hashes.d.ts.map +1 -0
- package/dist/data/favicon-hashes.js +249 -0
- package/dist/data/favicon-hashes.js.map +1 -0
- package/dist/data/h2-signatures.d.ts +50 -0
- package/dist/data/h2-signatures.d.ts.map +1 -0
- package/dist/data/h2-signatures.js +211 -0
- package/dist/data/h2-signatures.js.map +1 -0
- package/dist/data/header-order.d.ts +38 -0
- package/dist/data/header-order.d.ts.map +1 -0
- package/dist/data/header-order.js +185 -0
- package/dist/data/header-order.js.map +1 -0
- package/dist/data/jarm-signatures.d.ts +25 -0
- package/dist/data/jarm-signatures.d.ts.map +1 -0
- package/dist/data/jarm-signatures.js +213 -0
- package/dist/data/jarm-signatures.js.map +1 -0
- package/dist/data/saas-patterns.d.ts +23 -0
- package/dist/data/saas-patterns.d.ts.map +1 -0
- package/dist/data/saas-patterns.js +139 -0
- package/dist/data/saas-patterns.js.map +1 -0
- package/dist/data/sensitive-paths.d.ts +12 -0
- package/dist/data/sensitive-paths.d.ts.map +1 -0
- package/dist/data/sensitive-paths.js +1539 -0
- package/dist/data/sensitive-paths.js.map +1 -0
- package/dist/data/service-banners.d.ts +59 -0
- package/dist/data/service-banners.d.ts.map +1 -0
- package/dist/data/service-banners.js +323 -0
- package/dist/data/service-banners.js.map +1 -0
- package/dist/data/smtp-signatures.d.ts +12 -0
- package/dist/data/smtp-signatures.d.ts.map +1 -0
- package/dist/data/smtp-signatures.js +350 -0
- package/dist/data/smtp-signatures.js.map +1 -0
- package/dist/data/takeover-patterns.d.ts +37 -0
- package/dist/data/takeover-patterns.d.ts.map +1 -0
- package/dist/data/takeover-patterns.js +249 -0
- package/dist/data/takeover-patterns.js.map +1 -0
- package/dist/data/tech-patterns.d.ts +44 -0
- package/dist/data/tech-patterns.d.ts.map +1 -0
- package/dist/data/tech-patterns.js +690 -0
- package/dist/data/tech-patterns.js.map +1 -0
- package/dist/data/waf-signatures.d.ts +21 -0
- package/dist/data/waf-signatures.d.ts.map +1 -0
- package/dist/data/waf-signatures.js +318 -0
- package/dist/data/waf-signatures.js.map +1 -0
- package/dist/dns/index.d.ts +3 -0
- package/dist/dns/index.d.ts.map +1 -0
- package/dist/dns/index.js +1067 -0
- package/dist/dns/index.js.map +1 -0
- package/dist/enum/index.d.ts +3 -0
- package/dist/enum/index.d.ts.map +1 -0
- package/dist/enum/index.js +818 -0
- package/dist/enum/index.js.map +1 -0
- package/dist/h2/index.d.ts +3 -0
- package/dist/h2/index.d.ts.map +1 -0
- package/dist/h2/index.js +414 -0
- package/dist/h2/index.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2444 -0
- package/dist/http/index.js.map +1 -0
- package/dist/identify/index.d.ts +3 -0
- package/dist/identify/index.d.ts.map +1 -0
- package/dist/identify/index.js +447 -0
- package/dist/identify/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/index.d.ts +3 -0
- package/dist/infra/index.d.ts.map +1 -0
- package/dist/infra/index.js +989 -0
- package/dist/infra/index.js.map +1 -0
- package/dist/iot/index.d.ts +3 -0
- package/dist/iot/index.d.ts.map +1 -0
- package/dist/iot/index.js +610 -0
- package/dist/iot/index.js.map +1 -0
- package/dist/meta/index.d.ts +3 -0
- package/dist/meta/index.d.ts.map +1 -0
- package/dist/meta/index.js +442 -0
- package/dist/meta/index.js.map +1 -0
- package/dist/osint/index.d.ts +3 -0
- package/dist/osint/index.d.ts.map +1 -0
- package/dist/osint/index.js +687 -0
- package/dist/osint/index.js.map +1 -0
- package/dist/passive/index.d.ts +3 -0
- package/dist/passive/index.d.ts.map +1 -0
- package/dist/passive/index.js +944 -0
- package/dist/passive/index.js.map +1 -0
- package/dist/path/index.d.ts +3 -0
- package/dist/path/index.d.ts.map +1 -0
- package/dist/path/index.js +878 -0
- package/dist/path/index.js.map +1 -0
- package/dist/protocol/mcp-server.d.ts +4 -0
- package/dist/protocol/mcp-server.d.ts.map +1 -0
- package/dist/protocol/mcp-server.js +32 -0
- package/dist/protocol/mcp-server.js.map +1 -0
- package/dist/protocol/tools.d.ts +3 -0
- package/dist/protocol/tools.d.ts.map +1 -0
- package/dist/protocol/tools.js +5 -0
- package/dist/protocol/tools.js.map +1 -0
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +1053 -0
- package/dist/service/index.js.map +1 -0
- package/dist/smtp/index.d.ts +3 -0
- package/dist/smtp/index.d.ts.map +1 -0
- package/dist/smtp/index.js +437 -0
- package/dist/smtp/index.js.map +1 -0
- package/dist/ssh/index.d.ts +3 -0
- package/dist/ssh/index.d.ts.map +1 -0
- package/dist/ssh/index.js +676 -0
- package/dist/ssh/index.js.map +1 -0
- package/dist/tcp/index.d.ts +3 -0
- package/dist/tcp/index.d.ts.map +1 -0
- package/dist/tcp/index.js +369 -0
- package/dist/tcp/index.js.map +1 -0
- package/dist/timing/index.d.ts +3 -0
- package/dist/timing/index.d.ts.map +1 -0
- package/dist/timing/index.js +425 -0
- package/dist/timing/index.js.map +1 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/tls/index.d.ts.map +1 -0
- package/dist/tls/index.js +1332 -0
- package/dist/tls/index.js.map +1 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +35 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/murmurhash3.d.ts +3 -0
- package/dist/utils/murmurhash3.d.ts.map +1 -0
- package/dist/utils/murmurhash3.js +57 -0
- package/dist/utils/murmurhash3.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +10 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +35 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/require-key.d.ts +2 -0
- package/dist/utils/require-key.d.ts.map +1 -0
- package/dist/utils/require-key.js +8 -0
- package/dist/utils/require-key.js.map +1 -0
- package/dist/waf/index.d.ts +3 -0
- package/dist/waf/index.d.ts.map +1 -0
- package/dist/waf/index.js +767 -0
- package/dist/waf/index.js.map +1 -0
- package/dist/web/index.d.ts +3 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +1366 -0
- package/dist/web/index.js.map +1 -0
- 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
|