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,1332 @@
1
+ import { z } from "zod";
2
+ import { RateLimiter } from "../utils/rate-limiter.js";
3
+ import { TTLCache } from "../utils/cache.js";
4
+ import * as tls from "node:tls";
5
+ import * as net from "node:net";
6
+ import { X509Certificate, createHash } from "node:crypto";
7
+ import { json } from "../types/index.js";
8
+ // ─── Module-Level Singletons ───
9
+ const limiter = new RateLimiter(200);
10
+ const cache = new TTLCache(600_000); // 10min cache
11
+ // ─── Helper: TLS Connection ───
12
+ async function tlsConnect(host, port, options) {
13
+ return new Promise((resolve, reject) => {
14
+ const opts = {
15
+ host,
16
+ port,
17
+ rejectUnauthorized: false,
18
+ servername: host,
19
+ timeout: 10000,
20
+ ...options,
21
+ };
22
+ const socket = tls.connect(opts, () => {
23
+ const cert = socket.getPeerCertificate(true);
24
+ resolve({ socket, cert });
25
+ });
26
+ socket.on("error", reject);
27
+ socket.on("timeout", () => {
28
+ socket.destroy();
29
+ reject(new Error("TLS connection timeout"));
30
+ });
31
+ });
32
+ }
33
+ // ─── Helper: Safely close a TLS socket ───
34
+ function safeClose(socket) {
35
+ try {
36
+ socket.end();
37
+ socket.destroy();
38
+ }
39
+ catch {
40
+ // ignore
41
+ }
42
+ }
43
+ // ─── Helper: Parse certificate subject/issuer fields ───
44
+ function parseDN(dn) {
45
+ if (!dn)
46
+ return {};
47
+ const result = {};
48
+ for (const [key, value] of Object.entries(dn)) {
49
+ result[key] = Array.isArray(value) ? value.join(", ") : String(value ?? "");
50
+ }
51
+ return result;
52
+ }
53
+ // ─── Helper: Get SHA-256 fingerprint from raw DER cert ───
54
+ function sha256Fingerprint(raw) {
55
+ if (!raw)
56
+ return "N/A";
57
+ return createHash("sha256").update(raw).digest("hex");
58
+ }
59
+ // ─── Helper: DNS resolution for cross-referencing ───
60
+ async function resolveHostname(hostname) {
61
+ const dns = await import("node:dns/promises");
62
+ try {
63
+ const result = await dns.resolve4(hostname);
64
+ return result;
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ // ─── Helper: Extract SAN list from certificate ───
71
+ function extractSANs(cert) {
72
+ const dns = [];
73
+ const ips = [];
74
+ const subjectaltname = cert.subjectaltname ?? "";
75
+ if (!subjectaltname)
76
+ return { dns, ips };
77
+ for (const entry of subjectaltname.split(",")) {
78
+ const trimmed = entry.trim();
79
+ if (trimmed.startsWith("DNS:")) {
80
+ dns.push(trimmed.slice(4));
81
+ }
82
+ else if (trimmed.startsWith("IP Address:")) {
83
+ ips.push(trimmed.slice(11));
84
+ }
85
+ }
86
+ return { dns, ips };
87
+ }
88
+ // ─── Helper: Detect malware certificate patterns ───
89
+ function detectMalwareCertPatterns(cert) {
90
+ const warnings = [];
91
+ const subject = parseDN(cert.subject);
92
+ const issuer = parseDN(cert.issuer);
93
+ // CN=localhost
94
+ if (subject.CN === "localhost") {
95
+ warnings.push("Subject CN is 'localhost' — common in malware/C2 certificates");
96
+ }
97
+ // Internet Widgits Pty Ltd (OpenSSL default)
98
+ if (subject.O === "Internet Widgits Pty Ltd") {
99
+ warnings.push("Subject O is 'Internet Widgits Pty Ltd' (OpenSSL default) — common in malware certs");
100
+ }
101
+ // Self-signed detection
102
+ const isSelfSigned = subject.CN === issuer.CN &&
103
+ subject.O === issuer.O &&
104
+ (subject.C ?? "") === (issuer.C ?? "");
105
+ if (isSelfSigned) {
106
+ warnings.push("Certificate is self-signed");
107
+ }
108
+ // Very long validity (>5 years)
109
+ if (cert.valid_from && cert.valid_to) {
110
+ const from = new Date(cert.valid_from).getTime();
111
+ const to = new Date(cert.valid_to).getTime();
112
+ const years = (to - from) / (1000 * 60 * 60 * 24 * 365.25);
113
+ if (years > 5) {
114
+ warnings.push(`Certificate validity period is ${years.toFixed(1)} years (>5 years) — suspicious`);
115
+ }
116
+ }
117
+ // Self-signed with no SAN
118
+ const sans = extractSANs(cert);
119
+ if (isSelfSigned && sans.dns.length === 0 && sans.ips.length === 0) {
120
+ warnings.push("Self-signed certificate with no SAN — strong malware indicator");
121
+ }
122
+ return warnings;
123
+ }
124
+ // ─── Helper: Walk certificate chain ───
125
+ function walkCertChain(cert) {
126
+ const chain = [];
127
+ const visited = new Set();
128
+ let current = cert;
129
+ while (current) {
130
+ const serial = current.serialNumber ?? "unknown";
131
+ if (visited.has(serial))
132
+ break;
133
+ visited.add(serial);
134
+ chain.push({
135
+ subject: parseDN(current.subject),
136
+ issuer: parseDN(current.issuer),
137
+ serialNumber: serial,
138
+ validFrom: current.valid_from ?? "N/A",
139
+ validTo: current.valid_to ?? "N/A",
140
+ fingerprint256: current.fingerprint256 ?? sha256Fingerprint(current.raw),
141
+ });
142
+ // Walk up the chain
143
+ const issuerCert = current.issuerCertificate;
144
+ if (!issuerCert || issuerCert === current)
145
+ break;
146
+ current = issuerCert;
147
+ }
148
+ return chain;
149
+ }
150
+ // ─── Helper: Compute RDN ordering string ───
151
+ function rdnOrder(dn) {
152
+ // Standard RDN field order as they appear in the certificate
153
+ const knownFields = ["CN", "O", "OU", "L", "ST", "C", "emailAddress", "serialNumber"];
154
+ const order = [];
155
+ for (const field of knownFields) {
156
+ if (dn[field] !== undefined && dn[field] !== "") {
157
+ order.push(field);
158
+ }
159
+ }
160
+ // Include any non-standard fields
161
+ for (const key of Object.keys(dn)) {
162
+ if (!knownFields.includes(key) && dn[key] !== undefined && dn[key] !== "") {
163
+ order.push(key);
164
+ }
165
+ }
166
+ return order.join(",");
167
+ }
168
+ // ─── Helper: Extract extension OIDs from X509Certificate ───
169
+ function extractExtensionOIDs(cert) {
170
+ try {
171
+ if (!cert.raw)
172
+ return [];
173
+ const x509 = new X509Certificate(cert.raw);
174
+ const infoAccess = x509.infoAccess;
175
+ const keyUsage = x509.keyUsage;
176
+ const oids = [];
177
+ // Standard extensions we can detect
178
+ if (x509.subjectAltName)
179
+ oids.push("2.5.29.17"); // SAN
180
+ if (keyUsage && keyUsage.length > 0)
181
+ oids.push("2.5.29.15"); // Key Usage
182
+ if (infoAccess) {
183
+ oids.push("1.3.6.1.5.5.7.1.1"); // Authority Info Access
184
+ if (infoAccess.includes("OCSP"))
185
+ oids.push("1.3.6.1.5.5.7.48.1"); // OCSP
186
+ if (infoAccess.includes("CA Issuers"))
187
+ oids.push("1.3.6.1.5.5.7.48.2"); // CA Issuers
188
+ }
189
+ // Basic Constraints — detect via ca property
190
+ if (x509.ca !== undefined)
191
+ oids.push("2.5.29.19");
192
+ // Subject Key Identifier
193
+ if (cert.serialNumber)
194
+ oids.push("2.5.29.14");
195
+ // Authority Key Identifier
196
+ if (cert.issuerCertificate)
197
+ oids.push("2.5.29.35");
198
+ // Extended Key Usage — check via ext_key_usage
199
+ if (cert.ext_key_usage)
200
+ oids.push("2.5.29.37");
201
+ // CRL Distribution Points
202
+ oids.push("2.5.29.31");
203
+ // SCT (Certificate Transparency)
204
+ // OID: 1.3.6.1.4.1.11129.2.4.2
205
+ // We check the raw cert for this OID as a rough heuristic
206
+ if (cert.raw) {
207
+ const rawHex = cert.raw.toString("hex");
208
+ // SCT extension OID in DER: 060a2b06010401d679020402
209
+ if (rawHex.includes("060a2b06010401d679020402")) {
210
+ oids.push("1.3.6.1.4.1.11129.2.4.2");
211
+ }
212
+ }
213
+ return oids;
214
+ }
215
+ catch {
216
+ return [];
217
+ }
218
+ }
219
+ // ─── Helper: Detect key type and size from X509Certificate ───
220
+ function detectKeyInfo(cert) {
221
+ try {
222
+ if (!cert.raw)
223
+ return { type: "unknown", size: "unknown" };
224
+ const x509 = new X509Certificate(cert.raw);
225
+ const pubkey = x509.publicKey;
226
+ const keyType = pubkey.asymmetricKeyType ?? "unknown";
227
+ const keySize = pubkey.asymmetricKeySize ?? "unknown";
228
+ return { type: keyType.toUpperCase(), size: keySize };
229
+ }
230
+ catch {
231
+ // Fallback: try to infer from bits field
232
+ return {
233
+ type: cert.asn1Curve ? "ECDSA" : "RSA",
234
+ size: cert.bits ?? "unknown",
235
+ };
236
+ }
237
+ }
238
+ // ─── Helper: Check for CT SCT extension ───
239
+ function hasSCTExtension(cert) {
240
+ if (!cert.raw)
241
+ return false;
242
+ const rawHex = cert.raw.toString("hex");
243
+ // SCT extension OID in DER encoding: 060a2b06010401d679020402
244
+ return rawHex.includes("060a2b06010401d679020402");
245
+ }
246
+ // ──────────────────────────────────────────────────────────────────────────────
247
+ // Tool 1: tls_probe — TLS Handshake Analysis
248
+ // ──────────────────────────────────────────────────────────────────────────────
249
+ const tlsProbe = {
250
+ name: "tls_probe",
251
+ description: "TLS handshake analysis. Connects via TLS and captures negotiated version, cipher suite, ALPN result, session ticket support, OCSP stapling, renegotiation info, and certificate validation status.",
252
+ schema: {
253
+ host: z.string().describe("Target hostname or IP"),
254
+ port: z.number().optional().default(443).describe("TLS port"),
255
+ },
256
+ async execute(args) {
257
+ const host = args.host;
258
+ const port = args.port ?? 443;
259
+ const cacheKey = `tls_probe:${host}:${port}`;
260
+ const cached = cache.get(cacheKey);
261
+ if (cached)
262
+ return json(cached);
263
+ await limiter.acquire();
264
+ const { socket, cert } = await tlsConnect(host, port);
265
+ try {
266
+ const cipher = socket.getCipher();
267
+ const protocol = socket.getProtocol();
268
+ const alpn = socket.alpnProtocol ?? null;
269
+ const authorized = socket.authorized;
270
+ const authError = socket.authorizationError;
271
+ const ephemeralKeyInfo = socket.getEphemeralKeyInfo?.() ?? null;
272
+ const peerFinished = socket.getPeerFinished();
273
+ const finished = socket.getFinished();
274
+ // Session info
275
+ const session = socket.getSession();
276
+ const hasSession = !!session;
277
+ // TLS version numerical check
278
+ const isTls13 = protocol === "TLSv1.3";
279
+ const result = {
280
+ host,
281
+ port,
282
+ tls: {
283
+ version: protocol,
284
+ cipher: {
285
+ name: cipher?.name ?? "unknown",
286
+ standardName: cipher?.standardName ?? cipher?.name ?? "unknown",
287
+ version: cipher?.version ?? "unknown",
288
+ bits: cipher?.bits ?? null,
289
+ },
290
+ alpn: alpn || null,
291
+ authorized,
292
+ authorizationError: authError || null,
293
+ ephemeralKey: ephemeralKeyInfo
294
+ ? {
295
+ type: ephemeralKeyInfo.type ?? "unknown",
296
+ size: ephemeralKeyInfo.size ?? null,
297
+ name: ephemeralKeyInfo.name ?? null,
298
+ }
299
+ : null,
300
+ sessionTicket: hasSession,
301
+ isTls13,
302
+ peerCertificate: {
303
+ subject: parseDN(cert.subject),
304
+ issuer: parseDN(cert.issuer),
305
+ validFrom: cert.valid_from ?? "N/A",
306
+ validTo: cert.valid_to ?? "N/A",
307
+ serialNumber: cert.serialNumber ?? "N/A",
308
+ },
309
+ renegotiation: {
310
+ peerFinished: !!peerFinished,
311
+ finished: !!finished,
312
+ },
313
+ },
314
+ };
315
+ cache.set(cacheKey, result);
316
+ return json(result);
317
+ }
318
+ finally {
319
+ safeClose(socket);
320
+ }
321
+ },
322
+ };
323
+ // ──────────────────────────────────────────────────────────────────────────────
324
+ // Tool 2: tls_jarm — JARM Fingerprint
325
+ // ──────────────────────────────────────────────────────────────────────────────
326
+ // JARM probe configurations: 10 different TLS connection parameters
327
+ const JARM_PROBES = [
328
+ // Probe 1: TLS 1.2 only, all ciphers
329
+ { label: "tls12_all", minVersion: "TLSv1.2", maxVersion: "TLSv1.2" },
330
+ // Probe 2: TLS 1.3 only
331
+ { label: "tls13_only", minVersion: "TLSv1.3", maxVersion: "TLSv1.3" },
332
+ // Probe 3: TLS 1.2, forward-secrecy only
333
+ { label: "tls12_fs", minVersion: "TLSv1.2", maxVersion: "TLSv1.2", ciphers: "ECDHE:DHE" },
334
+ // Probe 4: TLS 1.2, RSA key exchange
335
+ { label: "tls12_rsa", minVersion: "TLSv1.2", maxVersion: "TLSv1.2", ciphers: "RSA" },
336
+ // Probe 5: TLS 1.2, AES-GCM only
337
+ {
338
+ label: "tls12_aesgcm",
339
+ minVersion: "TLSv1.2",
340
+ maxVersion: "TLSv1.2",
341
+ ciphers: "AESGCM",
342
+ },
343
+ // Probe 6: TLS 1.2, ChaCha20 only
344
+ {
345
+ label: "tls12_chacha",
346
+ minVersion: "TLSv1.2",
347
+ maxVersion: "TLSv1.2",
348
+ ciphers: "CHACHA20",
349
+ },
350
+ // Probe 7: TLS 1.2 with ALPN h2
351
+ { label: "tls12_h2", minVersion: "TLSv1.2", maxVersion: "TLSv1.2", alpn: ["h2"] },
352
+ // Probe 8: TLS 1.2 with ALPN http/1.1
353
+ {
354
+ label: "tls12_h1",
355
+ minVersion: "TLSv1.2",
356
+ maxVersion: "TLSv1.2",
357
+ alpn: ["http/1.1"],
358
+ },
359
+ // Probe 9: Widest range (TLS 1.2 - 1.3)
360
+ { label: "tls_wide", minVersion: "TLSv1.2", maxVersion: "TLSv1.3" },
361
+ // Probe 10: TLS 1.3 with ALPN h2
362
+ { label: "tls13_h2", minVersion: "TLSv1.3", maxVersion: "TLSv1.3", alpn: ["h2"] },
363
+ ];
364
+ async function jarmProbe(host, port, probe) {
365
+ try {
366
+ const opts = {
367
+ minVersion: probe.minVersion,
368
+ maxVersion: probe.maxVersion,
369
+ };
370
+ if (probe.ciphers) {
371
+ opts.ciphers = probe.ciphers;
372
+ }
373
+ if (probe.alpn) {
374
+ opts.ALPNProtocols = probe.alpn;
375
+ }
376
+ const { socket } = await tlsConnect(host, port, opts);
377
+ try {
378
+ const cipher = socket.getCipher();
379
+ const protocol = socket.getProtocol();
380
+ if (!cipher || !protocol)
381
+ return "000";
382
+ // Extract first 3 hex chars from cipher name hash + TLS version indicator
383
+ const cipherHash = createHash("md5").update(cipher.name ?? "").digest("hex");
384
+ const versionByte = protocol === "TLSv1.3" ? "d" : protocol === "TLSv1.2" ? "c" : "0";
385
+ return cipherHash.slice(0, 3) + versionByte;
386
+ }
387
+ finally {
388
+ safeClose(socket);
389
+ }
390
+ }
391
+ catch {
392
+ return "0000";
393
+ }
394
+ }
395
+ const tlsJarm = {
396
+ name: "tls_jarm",
397
+ description: "JARM TLS fingerprinting. Sends 10 TLS probes with varying protocol versions, cipher preferences, and ALPN settings to generate a server fingerprint. Useful for identifying C2 frameworks, web servers, and CDN infrastructure.",
398
+ schema: {
399
+ host: z.string().describe("Target hostname or IP"),
400
+ port: z.number().optional().default(443).describe("TLS port"),
401
+ },
402
+ async execute(args) {
403
+ const host = args.host;
404
+ const port = args.port ?? 443;
405
+ const cacheKey = `tls_jarm:${host}:${port}`;
406
+ const cached = cache.get(cacheKey);
407
+ if (cached)
408
+ return json(cached);
409
+ await limiter.acquire();
410
+ // Run all 10 probes
411
+ const probeResults = [];
412
+ const probeDetails = [];
413
+ for (const probe of JARM_PROBES) {
414
+ await limiter.acquire();
415
+ const response = await jarmProbe(host, port, probe);
416
+ probeResults.push(response);
417
+ probeDetails.push({ label: probe.label, response });
418
+ }
419
+ // Build raw fingerprint from concatenated probe results
420
+ const rawFingerprint = probeResults.join("");
421
+ // Hash the raw fingerprint to produce the JARM hash (62 chars)
422
+ const jarmHash = createHash("sha256").update(rawFingerprint).digest("hex").slice(0, 62);
423
+ // Try to match against known JARM signatures
424
+ let matchedSignature = null;
425
+ try {
426
+ const { JARM_SIGNATURES } = await import("../data/jarm-signatures.js");
427
+ const match = JARM_SIGNATURES.find((sig) => sig.hash === jarmHash);
428
+ if (match) {
429
+ matchedSignature = {
430
+ name: match.name,
431
+ category: match.category,
432
+ confidence: match.confidence,
433
+ };
434
+ }
435
+ }
436
+ catch {
437
+ // Signature database not available
438
+ }
439
+ const result = {
440
+ host,
441
+ port,
442
+ jarm: {
443
+ hash: jarmHash,
444
+ rawFingerprint,
445
+ probeCount: probeResults.length,
446
+ probes: probeDetails,
447
+ match: matchedSignature,
448
+ note: "Simplified JARM implementation — probes TLS server responses with varying parameters. For full accuracy, use the official Salesforce JARM tool.",
449
+ },
450
+ };
451
+ cache.set(cacheKey, result);
452
+ return json(result);
453
+ },
454
+ };
455
+ // ──────────────────────────────────────────────────────────────────────────────
456
+ // Tool 3: tls_cert — X.509 Certificate Deep Analysis
457
+ // ──────────────────────────────────────────────────────────────────────────────
458
+ const tlsCert = {
459
+ name: "tls_cert",
460
+ description: "X.509 certificate deep analysis. Extracts subject, issuer, SAN list, validity, key type/size, signature algorithm, fingerprint, full chain, self-signed detection, malware cert patterns, CT/SCT compliance, and wildcard detection.",
461
+ schema: {
462
+ host: z.string().describe("Target hostname or IP"),
463
+ port: z.number().optional().default(443).describe("TLS port"),
464
+ },
465
+ async execute(args) {
466
+ const host = args.host;
467
+ const port = args.port ?? 443;
468
+ const cacheKey = `tls_cert:${host}:${port}`;
469
+ const cached = cache.get(cacheKey);
470
+ if (cached)
471
+ return json(cached);
472
+ await limiter.acquire();
473
+ const { socket, cert } = await tlsConnect(host, port);
474
+ try {
475
+ const subject = parseDN(cert.subject);
476
+ const issuer = parseDN(cert.issuer);
477
+ const sans = extractSANs(cert);
478
+ const keyInfo = detectKeyInfo(cert);
479
+ const malwareWarnings = detectMalwareCertPatterns(cert);
480
+ const chain = walkCertChain(cert);
481
+ const sctPresent = hasSCTExtension(cert);
482
+ // Self-signed detection
483
+ const isSelfSigned = subject.CN === issuer.CN &&
484
+ subject.O === issuer.O &&
485
+ (subject.C ?? "") === (issuer.C ?? "");
486
+ // Wildcard detection
487
+ const wildcardDomains = sans.dns.filter((d) => d.startsWith("*."));
488
+ const isWildcard = wildcardDomains.length > 0 || (subject.CN ?? "").startsWith("*.");
489
+ // Validity period
490
+ const validFrom = cert.valid_from ? new Date(cert.valid_from) : null;
491
+ const validTo = cert.valid_to ? new Date(cert.valid_to) : null;
492
+ const now = new Date();
493
+ const isExpired = validTo ? now > validTo : false;
494
+ const isNotYetValid = validFrom ? now < validFrom : false;
495
+ const daysRemaining = validTo
496
+ ? Math.floor((validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
497
+ : null;
498
+ // Signature algorithm from X509Certificate
499
+ let signatureAlgorithm = "unknown";
500
+ try {
501
+ if (cert.raw) {
502
+ const x509 = new X509Certificate(cert.raw);
503
+ // The sigAlgName is not directly exposed; try fingerprint256 approach
504
+ // Node's X509Certificate doesn't expose sigAlg directly, but we can detect from raw
505
+ const rawHex = cert.raw.toString("hex");
506
+ if (rawHex.includes("2a864886f70d01010b"))
507
+ signatureAlgorithm = "sha256WithRSAEncryption";
508
+ else if (rawHex.includes("2a864886f70d010105"))
509
+ signatureAlgorithm = "sha1WithRSAEncryption";
510
+ else if (rawHex.includes("2a864886f70d01010c"))
511
+ signatureAlgorithm = "sha384WithRSAEncryption";
512
+ else if (rawHex.includes("2a864886f70d01010d"))
513
+ signatureAlgorithm = "sha512WithRSAEncryption";
514
+ else if (rawHex.includes("2a8648ce3d040302"))
515
+ signatureAlgorithm = "ecdsa-with-SHA256";
516
+ else if (rawHex.includes("2a8648ce3d040303"))
517
+ signatureAlgorithm = "ecdsa-with-SHA384";
518
+ else if (rawHex.includes("2b6570"))
519
+ signatureAlgorithm = "Ed25519";
520
+ }
521
+ }
522
+ catch {
523
+ // ignore
524
+ }
525
+ const fingerprint256 = cert.fingerprint256 ?? sha256Fingerprint(cert.raw);
526
+ const result = {
527
+ host,
528
+ port,
529
+ certificate: {
530
+ subject,
531
+ issuer,
532
+ san: {
533
+ dns: sans.dns,
534
+ ips: sans.ips,
535
+ total: sans.dns.length + sans.ips.length,
536
+ },
537
+ validity: {
538
+ notBefore: cert.valid_from ?? "N/A",
539
+ notAfter: cert.valid_to ?? "N/A",
540
+ isExpired,
541
+ isNotYetValid,
542
+ daysRemaining,
543
+ },
544
+ serialNumber: cert.serialNumber ?? "N/A",
545
+ key: {
546
+ type: keyInfo.type,
547
+ size: keyInfo.size,
548
+ },
549
+ signatureAlgorithm,
550
+ fingerprint: {
551
+ sha256: fingerprint256,
552
+ },
553
+ chain: {
554
+ depth: chain.length,
555
+ certificates: chain,
556
+ },
557
+ analysis: {
558
+ isSelfSigned,
559
+ isWildcard,
560
+ wildcardDomains,
561
+ ctCompliance: sctPresent,
562
+ sctExtensionPresent: sctPresent,
563
+ malwareIndicators: malwareWarnings,
564
+ malwareScore: malwareWarnings.length === 0
565
+ ? "clean"
566
+ : malwareWarnings.length <= 2
567
+ ? "suspicious"
568
+ : "likely_malicious",
569
+ },
570
+ },
571
+ };
572
+ cache.set(cacheKey, result);
573
+ return json(result);
574
+ }
575
+ finally {
576
+ safeClose(socket);
577
+ }
578
+ },
579
+ };
580
+ // ──────────────────────────────────────────────────────────────────────────────
581
+ // Tool 4: tls_ja4x — JA4X Certificate Generation Fingerprint
582
+ // ──────────────────────────────────────────────────────────────────────────────
583
+ // Known JA4X patterns for certificate generation tools
584
+ const JA4X_KNOWN_TOOLS = {
585
+ "CN,O,L,ST,C": "OpenSSL (default)",
586
+ "CN,O,OU,L,ST,C": "OpenSSL (full subject)",
587
+ CN: "mkcert / simple tool",
588
+ "C,ST,L,O,OU,CN": "Java keytool",
589
+ "C,O,CN": "Let's Encrypt / ACME",
590
+ "O,CN": "Cobalt Strike (common pattern)",
591
+ "C,ST,O,CN": "Microsoft PKI / Active Directory CS",
592
+ };
593
+ const tlsJa4x = {
594
+ name: "tls_ja4x",
595
+ description: "JA4X certificate generation fingerprint. Fingerprints HOW a certificate was generated by analyzing issuer/subject RDN ordering and extension OID list. Identifies the tool that generated the cert (Cobalt Strike, mkcert, OpenSSL, Let's Encrypt, etc.).",
596
+ schema: {
597
+ host: z.string().describe("Target hostname or IP"),
598
+ port: z.number().optional().default(443).describe("TLS port"),
599
+ },
600
+ async execute(args) {
601
+ const host = args.host;
602
+ const port = args.port ?? 443;
603
+ const cacheKey = `tls_ja4x:${host}:${port}`;
604
+ const cached = cache.get(cacheKey);
605
+ if (cached)
606
+ return json(cached);
607
+ await limiter.acquire();
608
+ const { socket, cert } = await tlsConnect(host, port);
609
+ try {
610
+ const issuerDN = parseDN(cert.issuer);
611
+ const subjectDN = parseDN(cert.subject);
612
+ // RDN ordering
613
+ const issuerRdnOrder = rdnOrder(issuerDN);
614
+ const subjectRdnOrder = rdnOrder(subjectDN);
615
+ // Extension OIDs
616
+ const extensionOIDs = extractExtensionOIDs(cert);
617
+ const extensionStr = extensionOIDs.join(",");
618
+ // Hash each section
619
+ const issuerHash = createHash("sha256").update(issuerRdnOrder).digest("hex").slice(0, 12);
620
+ const subjectHash = createHash("sha256").update(subjectRdnOrder).digest("hex").slice(0, 12);
621
+ const extensionHash = createHash("sha256").update(extensionStr).digest("hex").slice(0, 12);
622
+ // JA4X format: {issuer_hash}_{subject_hash}_{extension_hash}
623
+ const ja4xFingerprint = `${issuerHash}_${subjectHash}_${extensionHash}`;
624
+ // Attempt tool identification
625
+ let toolGuess = null;
626
+ for (const [pattern, tool] of Object.entries(JA4X_KNOWN_TOOLS)) {
627
+ if (subjectRdnOrder === pattern || issuerRdnOrder === pattern) {
628
+ toolGuess = tool;
629
+ break;
630
+ }
631
+ }
632
+ const result = {
633
+ host,
634
+ port,
635
+ ja4x: {
636
+ fingerprint: ja4xFingerprint,
637
+ components: {
638
+ issuer: {
639
+ rdnOrder: issuerRdnOrder,
640
+ hash: issuerHash,
641
+ fields: issuerDN,
642
+ },
643
+ subject: {
644
+ rdnOrder: subjectRdnOrder,
645
+ hash: subjectHash,
646
+ fields: subjectDN,
647
+ },
648
+ extensions: {
649
+ oids: extensionOIDs,
650
+ hash: extensionHash,
651
+ },
652
+ },
653
+ toolIdentification: toolGuess
654
+ ? {
655
+ tool: toolGuess,
656
+ matchedOn: subjectRdnOrder,
657
+ confidence: "medium",
658
+ note: "Based on RDN ordering pattern match against known certificate generation tools",
659
+ }
660
+ : {
661
+ tool: "unknown",
662
+ note: "RDN ordering does not match any known certificate generation tool pattern",
663
+ },
664
+ },
665
+ };
666
+ cache.set(cacheKey, result);
667
+ return json(result);
668
+ }
669
+ finally {
670
+ safeClose(socket);
671
+ }
672
+ },
673
+ };
674
+ // ──────────────────────────────────────────────────────────────────────────────
675
+ // Tool 5: tls_cert_cross_ref — Certificate Cross-Reference
676
+ // ──────────────────────────────────────────────────────────────────────────────
677
+ const tlsCertCrossRef = {
678
+ name: "tls_cert_cross_ref",
679
+ description: "Cross-reference certificate SAN/fingerprint for infrastructure mapping. Extracts all SANs, resolves each domain to IP, and generates Shodan/Censys queries for the cert fingerprint to discover shared infrastructure.",
680
+ schema: {
681
+ host: z.string().describe("Target hostname or IP"),
682
+ port: z.number().optional().default(443).describe("TLS port"),
683
+ },
684
+ async execute(args) {
685
+ const host = args.host;
686
+ const port = args.port ?? 443;
687
+ const cacheKey = `tls_cert_cross_ref:${host}:${port}`;
688
+ const cached = cache.get(cacheKey);
689
+ if (cached)
690
+ return json(cached);
691
+ await limiter.acquire();
692
+ const { socket, cert } = await tlsConnect(host, port);
693
+ try {
694
+ const sans = extractSANs(cert);
695
+ const fingerprint256 = cert.fingerprint256 ?? sha256Fingerprint(cert.raw);
696
+ const serialNumber = cert.serialNumber ?? "N/A";
697
+ // Resolve each SAN domain to IP addresses
698
+ const domainResolutions = [];
699
+ const allIps = new Set();
700
+ // Add IPs from SAN directly
701
+ for (const ip of sans.ips) {
702
+ allIps.add(ip);
703
+ }
704
+ // Resolve DNS SANs (limit to avoid excessive DNS queries)
705
+ const domainsToResolve = sans.dns.slice(0, 50);
706
+ for (const domain of domainsToResolve) {
707
+ // Skip wildcard domains for resolution
708
+ if (domain.startsWith("*.")) {
709
+ domainResolutions.push({ domain, ips: ["(wildcard — not resolved)"] });
710
+ continue;
711
+ }
712
+ try {
713
+ const ips = await resolveHostname(domain);
714
+ domainResolutions.push({ domain, ips });
715
+ for (const ip of ips)
716
+ allIps.add(ip);
717
+ }
718
+ catch {
719
+ domainResolutions.push({ domain, ips: ["(resolution failed)"] });
720
+ }
721
+ }
722
+ // Identify interesting subdomains
723
+ const internalKeywords = [
724
+ "staging",
725
+ "stage",
726
+ "dev",
727
+ "test",
728
+ "internal",
729
+ "admin",
730
+ "jenkins",
731
+ "vault",
732
+ "gitlab",
733
+ "jira",
734
+ "confluence",
735
+ "grafana",
736
+ "kibana",
737
+ "prometheus",
738
+ "api",
739
+ "vpn",
740
+ "mail",
741
+ "mx",
742
+ "smtp",
743
+ "imap",
744
+ "pop",
745
+ "ftp",
746
+ "ssh",
747
+ "rdp",
748
+ "db",
749
+ "database",
750
+ "mongo",
751
+ "redis",
752
+ "elastic",
753
+ "kafka",
754
+ ];
755
+ const interestingDomains = sans.dns.filter((d) => internalKeywords.some((kw) => d.toLowerCase().includes(kw)));
756
+ // Clean fingerprint for search queries (remove colons)
757
+ const cleanFingerprint = fingerprint256.replace(/:/g, "");
758
+ const result = {
759
+ host,
760
+ port,
761
+ crossReference: {
762
+ certificate: {
763
+ fingerprint256: cleanFingerprint,
764
+ serialNumber,
765
+ subject: parseDN(cert.subject),
766
+ },
767
+ san: {
768
+ totalDomains: sans.dns.length,
769
+ totalIPs: sans.ips.length,
770
+ domains: sans.dns,
771
+ ips: sans.ips,
772
+ },
773
+ dnsResolution: {
774
+ resolved: domainResolutions,
775
+ uniqueIPs: Array.from(allIps),
776
+ totalUniqueIPs: allIps.size,
777
+ },
778
+ interestingFindings: {
779
+ internalDomains: interestingDomains,
780
+ wildcardDomains: sans.dns.filter((d) => d.startsWith("*.")),
781
+ },
782
+ searchQueries: {
783
+ shodan: {
784
+ byFingerprint: `ssl.cert.fingerprint:"${cleanFingerprint}"`,
785
+ bySerial: `ssl.cert.serial:"${serialNumber}"`,
786
+ byIssuerOrg: cert.issuer?.O
787
+ ? `ssl.cert.issuer.o:"${cert.issuer.O}"`
788
+ : null,
789
+ bySubjectCN: cert.subject?.CN
790
+ ? `ssl.cert.subject.cn:"${cert.subject.CN}"`
791
+ : null,
792
+ },
793
+ censys: {
794
+ byFingerprint: `services.tls.certificates.leaf.fingerprint_sha256:"${cleanFingerprint}"`,
795
+ bySerial: `services.tls.certificates.leaf.tbs_certificate.serial_number:"${serialNumber}"`,
796
+ bySubjectCN: cert.subject?.CN
797
+ ? `services.tls.certificates.leaf.tbs_certificate.subject.common_name:"${cert.subject.CN}"`
798
+ : null,
799
+ },
800
+ },
801
+ },
802
+ };
803
+ cache.set(cacheKey, result);
804
+ return json(result);
805
+ }
806
+ finally {
807
+ safeClose(socket);
808
+ }
809
+ },
810
+ };
811
+ // ──────────────────────────────────────────────────────────────────────────────
812
+ // Tool 6: tls_ct_subdomains — Certificate Transparency Subdomain Discovery
813
+ // ──────────────────────────────────────────────────────────────────────────────
814
+ const tlsCtSubdomains = {
815
+ name: "tls_ct_subdomains",
816
+ description: "Certificate Transparency subdomain discovery via crt.sh. Queries CT logs for all certificates issued for a domain, extracts and deduplicates subdomain names, and highlights internal/interesting hostnames (staging, jenkins, vault, etc.).",
817
+ schema: {
818
+ domain: z.string().describe("Domain for CT log subdomain discovery"),
819
+ },
820
+ async execute(args) {
821
+ const domain = args.domain;
822
+ const cacheKey = `tls_ct_subdomains:${domain}`;
823
+ const cached = cache.get(cacheKey);
824
+ if (cached)
825
+ return json(cached);
826
+ await limiter.acquire();
827
+ // Query crt.sh API
828
+ const url = `https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`;
829
+ const response = await fetch(url, {
830
+ headers: { "User-Agent": "fingerprint-mcp/0.1.0" },
831
+ signal: AbortSignal.timeout(30000),
832
+ });
833
+ if (!response.ok) {
834
+ throw new Error(`crt.sh API returned HTTP ${response.status}`);
835
+ }
836
+ const data = await response.json();
837
+ // Extract and deduplicate all subdomains
838
+ const subdomainSet = new Set();
839
+ for (const entry of data) {
840
+ // name_value can contain multiple domains separated by newlines
841
+ const names = (entry.name_value ?? "").split("\n");
842
+ for (const name of names) {
843
+ const cleaned = name.trim().toLowerCase();
844
+ if (cleaned && cleaned.endsWith(`.${domain.toLowerCase()}`)) {
845
+ // Remove leading wildcard if present
846
+ const normalized = cleaned.startsWith("*.") ? cleaned.slice(2) : cleaned;
847
+ subdomainSet.add(normalized);
848
+ }
849
+ // Also include exact domain match
850
+ if (cleaned === domain.toLowerCase()) {
851
+ subdomainSet.add(cleaned);
852
+ }
853
+ }
854
+ // Also check common_name
855
+ const cn = (entry.common_name ?? "").trim().toLowerCase();
856
+ if (cn && (cn.endsWith(`.${domain.toLowerCase()}`) || cn === domain.toLowerCase())) {
857
+ const normalized = cn.startsWith("*.") ? cn.slice(2) : cn;
858
+ subdomainSet.add(normalized);
859
+ }
860
+ }
861
+ const subdomains = Array.from(subdomainSet).sort();
862
+ // Categorize interesting subdomains
863
+ const internalKeywords = {
864
+ staging: "Staging environment",
865
+ stage: "Staging environment",
866
+ dev: "Development environment",
867
+ test: "Test environment",
868
+ uat: "User acceptance testing",
869
+ qa: "Quality assurance",
870
+ sandbox: "Sandbox environment",
871
+ internal: "Internal service",
872
+ admin: "Admin panel",
873
+ jenkins: "CI/CD (Jenkins)",
874
+ gitlab: "GitLab instance",
875
+ github: "GitHub Enterprise",
876
+ jira: "Issue tracker (Jira)",
877
+ confluence: "Wiki/Docs (Confluence)",
878
+ vault: "Secret management (Vault)",
879
+ grafana: "Monitoring (Grafana)",
880
+ kibana: "Log analysis (Kibana)",
881
+ prometheus: "Monitoring (Prometheus)",
882
+ elastic: "Elasticsearch",
883
+ api: "API endpoint",
884
+ vpn: "VPN gateway",
885
+ mail: "Mail server",
886
+ smtp: "SMTP server",
887
+ imap: "IMAP server",
888
+ webmail: "Webmail interface",
889
+ ftp: "FTP server",
890
+ sftp: "SFTP server",
891
+ ssh: "SSH access",
892
+ rdp: "Remote desktop",
893
+ db: "Database server",
894
+ mysql: "MySQL database",
895
+ postgres: "PostgreSQL database",
896
+ mongo: "MongoDB database",
897
+ redis: "Redis cache",
898
+ kafka: "Message broker (Kafka)",
899
+ rabbitmq: "Message broker (RabbitMQ)",
900
+ docker: "Docker registry",
901
+ k8s: "Kubernetes",
902
+ kubernetes: "Kubernetes",
903
+ cdn: "CDN endpoint",
904
+ static: "Static assets",
905
+ assets: "Static assets",
906
+ s3: "S3/Object storage",
907
+ minio: "MinIO storage",
908
+ sentry: "Error tracking (Sentry)",
909
+ sonar: "Code quality (SonarQube)",
910
+ nexus: "Artifact repository",
911
+ artifactory: "Artifact repository",
912
+ backup: "Backup service",
913
+ };
914
+ const interestingSubdomains = [];
915
+ for (const sub of subdomains) {
916
+ const parts = sub.split(".");
917
+ for (const part of parts) {
918
+ for (const [keyword, classification] of Object.entries(internalKeywords)) {
919
+ if (part.includes(keyword)) {
920
+ interestingSubdomains.push({ subdomain: sub, classification });
921
+ break;
922
+ }
923
+ }
924
+ // Only match the first keyword per subdomain
925
+ if (interestingSubdomains.length > 0 && interestingSubdomains.at(-1)?.subdomain === sub) {
926
+ break;
927
+ }
928
+ }
929
+ }
930
+ // Wildcard domains found in raw data
931
+ const wildcards = new Set();
932
+ for (const entry of data) {
933
+ const names = (entry.name_value ?? "").split("\n");
934
+ for (const name of names) {
935
+ const cleaned = name.trim().toLowerCase();
936
+ if (cleaned.startsWith("*.")) {
937
+ wildcards.add(cleaned);
938
+ }
939
+ }
940
+ }
941
+ // Unique issuers
942
+ const issuers = new Set();
943
+ for (const entry of data) {
944
+ if (entry.issuer_name)
945
+ issuers.add(entry.issuer_name);
946
+ }
947
+ const result = {
948
+ domain,
949
+ ctLogs: {
950
+ source: "crt.sh (Certificate Transparency)",
951
+ totalCertificatesFound: data.length,
952
+ subdomains: {
953
+ total: subdomains.length,
954
+ list: subdomains,
955
+ },
956
+ interestingSubdomains,
957
+ wildcardCertificates: Array.from(wildcards),
958
+ certificateIssuers: Array.from(issuers).slice(0, 20),
959
+ },
960
+ };
961
+ cache.set(cacheKey, result);
962
+ return json(result);
963
+ },
964
+ };
965
+ // ──────────────────────────────────────────────────────────────────────────────
966
+ // Tool 7: tls_ciphers — Cipher Suite Enumeration
967
+ // ──────────────────────────────────────────────────────────────────────────────
968
+ // Cipher suites to test, grouped by security level
969
+ const CIPHER_SUITES_TO_TEST = [
970
+ // Critical — should never be accepted
971
+ { name: "NULL", openssl: "NULL", security: "critical" },
972
+ { name: "EXPORT", openssl: "EXPORT", security: "critical" },
973
+ { name: "eNULL", openssl: "eNULL", security: "critical" },
974
+ { name: "aNULL", openssl: "aNULL", security: "critical" },
975
+ // Weak — should be disabled
976
+ { name: "RC4", openssl: "RC4", security: "weak" },
977
+ { name: "DES-CBC3-SHA", openssl: "DES-CBC3-SHA", security: "weak" },
978
+ { name: "DES-CBC-SHA", openssl: "DES-CBC-SHA", security: "weak" },
979
+ { name: "SEED-SHA", openssl: "SEED-SHA", security: "weak" },
980
+ { name: "IDEA-CBC-SHA", openssl: "IDEA-CBC-SHA", security: "weak" },
981
+ // Deprecated — should be migrated away from
982
+ { name: "AES128-SHA", openssl: "AES128-SHA", security: "deprecated" },
983
+ { name: "AES256-SHA", openssl: "AES256-SHA", security: "deprecated" },
984
+ { name: "AES128-SHA256", openssl: "AES128-SHA256", security: "deprecated" },
985
+ { name: "AES256-SHA256", openssl: "AES256-SHA256", security: "deprecated" },
986
+ // Acceptable — forward-secrecy with CBC
987
+ { name: "ECDHE-RSA-AES128-SHA", openssl: "ECDHE-RSA-AES128-SHA", security: "acceptable" },
988
+ { name: "ECDHE-RSA-AES256-SHA", openssl: "ECDHE-RSA-AES256-SHA", security: "acceptable" },
989
+ { name: "ECDHE-RSA-AES128-SHA256", openssl: "ECDHE-RSA-AES128-SHA256", security: "acceptable" },
990
+ { name: "ECDHE-RSA-AES256-SHA384", openssl: "ECDHE-RSA-AES256-SHA384", security: "acceptable" },
991
+ { name: "DHE-RSA-AES128-SHA", openssl: "DHE-RSA-AES128-SHA", security: "acceptable" },
992
+ { name: "DHE-RSA-AES256-SHA", openssl: "DHE-RSA-AES256-SHA", security: "acceptable" },
993
+ { name: "DHE-RSA-AES128-SHA256", openssl: "DHE-RSA-AES128-SHA256", security: "acceptable" },
994
+ { name: "DHE-RSA-AES256-SHA256", openssl: "DHE-RSA-AES256-SHA256", security: "acceptable" },
995
+ // Strong — forward-secrecy with AEAD
996
+ { name: "ECDHE-RSA-AES128-GCM-SHA256", openssl: "ECDHE-RSA-AES128-GCM-SHA256", security: "strong" },
997
+ { name: "ECDHE-RSA-AES256-GCM-SHA384", openssl: "ECDHE-RSA-AES256-GCM-SHA384", security: "strong" },
998
+ { name: "ECDHE-ECDSA-AES128-GCM-SHA256", openssl: "ECDHE-ECDSA-AES128-GCM-SHA256", security: "strong" },
999
+ { name: "ECDHE-ECDSA-AES256-GCM-SHA384", openssl: "ECDHE-ECDSA-AES256-GCM-SHA384", security: "strong" },
1000
+ { name: "ECDHE-RSA-CHACHA20-POLY1305", openssl: "ECDHE-RSA-CHACHA20-POLY1305", security: "strong" },
1001
+ { name: "ECDHE-ECDSA-CHACHA20-POLY1305", openssl: "ECDHE-ECDSA-CHACHA20-POLY1305", security: "strong" },
1002
+ { name: "DHE-RSA-AES128-GCM-SHA256", openssl: "DHE-RSA-AES128-GCM-SHA256", security: "strong" },
1003
+ { name: "DHE-RSA-AES256-GCM-SHA384", openssl: "DHE-RSA-AES256-GCM-SHA384", security: "strong" },
1004
+ { name: "DHE-RSA-CHACHA20-POLY1305", openssl: "DHE-RSA-CHACHA20-POLY1305", security: "strong" },
1005
+ // Modern — TLS 1.3 cipher suites
1006
+ { name: "TLS_AES_128_GCM_SHA256", openssl: "TLS_AES_128_GCM_SHA256", security: "modern", tlsVersion: "tls13" },
1007
+ { name: "TLS_AES_256_GCM_SHA384", openssl: "TLS_AES_256_GCM_SHA384", security: "modern", tlsVersion: "tls13" },
1008
+ {
1009
+ name: "TLS_CHACHA20_POLY1305_SHA256",
1010
+ openssl: "TLS_CHACHA20_POLY1305_SHA256",
1011
+ security: "modern",
1012
+ tlsVersion: "tls13",
1013
+ },
1014
+ ];
1015
+ async function testCipher(host, port, cipher) {
1016
+ try {
1017
+ const opts = {};
1018
+ if (cipher.tlsVersion === "tls13") {
1019
+ opts.minVersion = "TLSv1.3";
1020
+ opts.maxVersion = "TLSv1.3";
1021
+ opts.ciphers = undefined; // TLS 1.3 ciphers are controlled differently
1022
+ // For TLS 1.3, we use cipherSuites instead of ciphers
1023
+ opts.cipherSuites = cipher.openssl;
1024
+ }
1025
+ else {
1026
+ opts.ciphers = cipher.openssl;
1027
+ opts.maxVersion = "TLSv1.2";
1028
+ }
1029
+ const { socket } = await tlsConnect(host, port, opts);
1030
+ const negotiated = socket.getCipher();
1031
+ safeClose(socket);
1032
+ return {
1033
+ name: cipher.name,
1034
+ accepted: true,
1035
+ security: cipher.security,
1036
+ negotiatedCipher: negotiated?.name,
1037
+ };
1038
+ }
1039
+ catch {
1040
+ return {
1041
+ name: cipher.name,
1042
+ accepted: false,
1043
+ security: cipher.security,
1044
+ };
1045
+ }
1046
+ }
1047
+ const tlsCiphers = {
1048
+ name: "tls_ciphers",
1049
+ description: "Enumerate accepted TLS cipher suites by testing each cipher individually. Identifies weak ciphers (RC4, DES, NULL, EXPORT), acceptable ciphers, strong ciphers (AES-GCM, ChaCha20), and TLS 1.3 suites. Provides a security grade.",
1050
+ schema: {
1051
+ host: z.string().describe("Target hostname or IP"),
1052
+ port: z.number().optional().default(443).describe("TLS port"),
1053
+ },
1054
+ async execute(args) {
1055
+ const host = args.host;
1056
+ const port = args.port ?? 443;
1057
+ const cacheKey = `tls_ciphers:${host}:${port}`;
1058
+ const cached = cache.get(cacheKey);
1059
+ if (cached)
1060
+ return json(cached);
1061
+ await limiter.acquire();
1062
+ // Test all cipher suites
1063
+ const results = [];
1064
+ // Test in small batches to avoid overwhelming the target
1065
+ for (const cipher of CIPHER_SUITES_TO_TEST) {
1066
+ await limiter.acquire();
1067
+ const result = await testCipher(host, port, cipher);
1068
+ results.push(result);
1069
+ }
1070
+ // Categorize results
1071
+ const accepted = results.filter((r) => r.accepted);
1072
+ const rejected = results.filter((r) => !r.accepted);
1073
+ const bySecurity = {
1074
+ critical: [],
1075
+ weak: [],
1076
+ deprecated: [],
1077
+ acceptable: [],
1078
+ strong: [],
1079
+ modern: [],
1080
+ };
1081
+ for (const r of accepted) {
1082
+ bySecurity[r.security]?.push(r.name);
1083
+ }
1084
+ // Calculate security grade
1085
+ let grade;
1086
+ if (bySecurity.critical.length > 0) {
1087
+ grade = "F";
1088
+ }
1089
+ else if (bySecurity.weak.length > 0) {
1090
+ grade = "C";
1091
+ }
1092
+ else if (bySecurity.deprecated.length > 0 && bySecurity.strong.length === 0) {
1093
+ grade = "D";
1094
+ }
1095
+ else if (bySecurity.deprecated.length > 0) {
1096
+ grade = "B-";
1097
+ }
1098
+ else if (bySecurity.strong.length > 0 || bySecurity.modern.length > 0) {
1099
+ if (bySecurity.acceptable.length === 0) {
1100
+ grade = "A+";
1101
+ }
1102
+ else {
1103
+ grade = "A";
1104
+ }
1105
+ }
1106
+ else if (bySecurity.acceptable.length > 0) {
1107
+ grade = "B";
1108
+ }
1109
+ else {
1110
+ grade = "N/A";
1111
+ }
1112
+ const vulnerabilities = [];
1113
+ if (bySecurity.critical.length > 0) {
1114
+ vulnerabilities.push(`CRITICAL: Accepts NULL/EXPORT ciphers: ${bySecurity.critical.join(", ")}`);
1115
+ }
1116
+ if (bySecurity.weak.length > 0) {
1117
+ vulnerabilities.push(`WEAK: Accepts insecure ciphers: ${bySecurity.weak.join(", ")}`);
1118
+ }
1119
+ if (accepted.length === 0) {
1120
+ vulnerabilities.push("No cipher suites could be negotiated — server may be unreachable or using unusual configuration");
1121
+ }
1122
+ const result = {
1123
+ host,
1124
+ port,
1125
+ cipherEnumeration: {
1126
+ totalTested: CIPHER_SUITES_TO_TEST.length,
1127
+ totalAccepted: accepted.length,
1128
+ totalRejected: rejected.length,
1129
+ grade,
1130
+ accepted: accepted.map((r) => ({
1131
+ name: r.name,
1132
+ security: r.security,
1133
+ negotiated: r.negotiatedCipher,
1134
+ })),
1135
+ bySecurity: {
1136
+ critical: bySecurity.critical,
1137
+ weak: bySecurity.weak,
1138
+ deprecated: bySecurity.deprecated,
1139
+ acceptable: bySecurity.acceptable,
1140
+ strong: bySecurity.strong,
1141
+ modern: bySecurity.modern,
1142
+ },
1143
+ vulnerabilities,
1144
+ recommendations: grade === "A+" || grade === "A"
1145
+ ? ["Cipher configuration looks good"]
1146
+ : [
1147
+ bySecurity.critical.length > 0
1148
+ ? "URGENT: Disable NULL and EXPORT cipher suites immediately"
1149
+ : null,
1150
+ bySecurity.weak.length > 0 ? "Disable RC4, DES, and 3DES cipher suites" : null,
1151
+ bySecurity.deprecated.length > 0
1152
+ ? "Migrate from non-FS cipher suites to ECDHE/DHE variants"
1153
+ : null,
1154
+ bySecurity.modern.length === 0 ? "Enable TLS 1.3 cipher suites for best performance" : null,
1155
+ ].filter(Boolean),
1156
+ },
1157
+ };
1158
+ cache.set(cacheKey, result);
1159
+ return json(result);
1160
+ },
1161
+ };
1162
+ // ──────────────────────────────────────────────────────────────────────────────
1163
+ // Tool 8: tls_sni — SNI Probing
1164
+ // ──────────────────────────────────────────────────────────────────────────────
1165
+ async function sniProbe(host, port, sniValue, label) {
1166
+ try {
1167
+ const opts = {};
1168
+ if (sniValue === undefined) {
1169
+ // No SNI — remove servername
1170
+ opts.servername = "";
1171
+ }
1172
+ else {
1173
+ opts.servername = sniValue;
1174
+ }
1175
+ const { socket, cert } = await tlsConnect(host, port, opts);
1176
+ try {
1177
+ const sans = extractSANs(cert);
1178
+ const subject = parseDN(cert.subject);
1179
+ const issuer = parseDN(cert.issuer);
1180
+ return {
1181
+ label,
1182
+ sni: sniValue ?? null,
1183
+ success: true,
1184
+ cert: {
1185
+ cn: subject.CN ?? "N/A",
1186
+ san: sans.dns,
1187
+ issuer: issuer.CN ?? issuer.O ?? "N/A",
1188
+ fingerprint256: cert.fingerprint256 ?? sha256Fingerprint(cert.raw),
1189
+ },
1190
+ };
1191
+ }
1192
+ finally {
1193
+ safeClose(socket);
1194
+ }
1195
+ }
1196
+ catch (err) {
1197
+ return {
1198
+ label,
1199
+ sni: sniValue ?? null,
1200
+ success: false,
1201
+ error: err.message,
1202
+ };
1203
+ }
1204
+ }
1205
+ const tlsSni = {
1206
+ name: "tls_sni",
1207
+ description: "SNI (Server Name Indication) probing. Connects with various SNI values and compares returned certificates. Detects reverse proxies with multiple backends, reveals default/backend certificates, and analyzes wildcard scope.",
1208
+ schema: {
1209
+ host: z.string().describe("Target hostname or IP"),
1210
+ port: z.number().optional().default(443).describe("TLS port"),
1211
+ },
1212
+ async execute(args) {
1213
+ const host = args.host;
1214
+ const port = args.port ?? 443;
1215
+ const cacheKey = `tls_sni:${host}:${port}`;
1216
+ const cached = cache.get(cacheKey);
1217
+ if (cached)
1218
+ return json(cached);
1219
+ await limiter.acquire();
1220
+ // Define SNI values to test
1221
+ const sniTests = [
1222
+ { sni: host, label: "target_domain" },
1223
+ { sni: undefined, label: "no_sni" },
1224
+ { sni: "", label: "empty_sni" },
1225
+ { sni: "localhost", label: "localhost" },
1226
+ { sni: "internal", label: "internal" },
1227
+ { sni: `nonexistent-${Date.now()}.example.com`, label: "random_domain" },
1228
+ ];
1229
+ // If host looks like an IP, also test it as SNI
1230
+ const isIP = net.isIP(host);
1231
+ if (isIP) {
1232
+ sniTests.push({ sni: host, label: "ip_as_sni" });
1233
+ }
1234
+ else {
1235
+ // Try the IP address as SNI too
1236
+ try {
1237
+ const dns = await import("node:dns/promises");
1238
+ const ips = await dns.resolve4(host);
1239
+ if (ips.length > 0) {
1240
+ sniTests.push({ sni: ips[0], label: "resolved_ip_as_sni" });
1241
+ }
1242
+ }
1243
+ catch {
1244
+ // ignore DNS failure
1245
+ }
1246
+ }
1247
+ // Run all SNI probes
1248
+ const probeResults = [];
1249
+ for (const test of sniTests) {
1250
+ await limiter.acquire();
1251
+ const result = await sniProbe(host, port, test.sni, test.label);
1252
+ probeResults.push(result);
1253
+ }
1254
+ // Analyze results
1255
+ const successfulProbes = probeResults.filter((p) => p.success);
1256
+ const uniqueFingerprints = new Set(successfulProbes
1257
+ .filter((p) => p.cert)
1258
+ .map((p) => p.cert.fingerprint256));
1259
+ // Compare target domain cert with no-SNI cert
1260
+ const targetProbe = probeResults.find((p) => p.label === "target_domain" && p.success);
1261
+ const noSniProbe = probeResults.find((p) => (p.label === "no_sni" || p.label === "empty_sni") && p.success);
1262
+ const analysis = [];
1263
+ if (uniqueFingerprints.size > 1) {
1264
+ analysis.push(`Multiple certificates detected (${uniqueFingerprints.size} unique) — indicates reverse proxy or multi-tenant hosting`);
1265
+ }
1266
+ else if (uniqueFingerprints.size === 1) {
1267
+ analysis.push("Same certificate returned for all SNI values — single-site or wildcard configuration");
1268
+ }
1269
+ if (targetProbe?.cert && noSniProbe?.cert) {
1270
+ if (targetProbe.cert.fingerprint256 !== noSniProbe.cert.fingerprint256) {
1271
+ analysis.push(`Different cert on no-SNI (CN=${noSniProbe.cert.cn}) vs target (CN=${targetProbe.cert.cn}) — reverse proxy with SNI routing`);
1272
+ analysis.push(`Default certificate (no-SNI) reveals: CN=${noSniProbe.cert.cn}, Issuer=${noSniProbe.cert.issuer}`);
1273
+ }
1274
+ }
1275
+ // Wildcard analysis
1276
+ const wildcardCerts = successfulProbes.filter((p) => p.cert && p.cert.san.some((s) => s.startsWith("*.")));
1277
+ if (wildcardCerts.length > 0) {
1278
+ const wildcardSans = new Set(wildcardCerts.flatMap((p) => p.cert.san.filter((s) => s.startsWith("*."))));
1279
+ analysis.push(`Wildcard certificate scope: ${Array.from(wildcardSans).join(", ")}`);
1280
+ }
1281
+ // Random domain test
1282
+ const randomProbe = probeResults.find((p) => p.label === "random_domain");
1283
+ if (randomProbe?.success && targetProbe?.success) {
1284
+ if (randomProbe.cert?.fingerprint256 === targetProbe.cert?.fingerprint256) {
1285
+ analysis.push("Server returns same cert for unknown domains — catch-all/default vhost configuration");
1286
+ }
1287
+ else if (randomProbe.success) {
1288
+ analysis.push(`Server returns different cert for unknown domain (CN=${randomProbe.cert?.cn}) — may reveal default backend`);
1289
+ }
1290
+ }
1291
+ const result = {
1292
+ host,
1293
+ port,
1294
+ sniAnalysis: {
1295
+ probeCount: probeResults.length,
1296
+ successfulProbes: successfulProbes.length,
1297
+ uniqueCertificates: uniqueFingerprints.size,
1298
+ probes: probeResults,
1299
+ analysis,
1300
+ findings: {
1301
+ multipleBackends: uniqueFingerprints.size > 1,
1302
+ defaultCertificate: noSniProbe?.cert
1303
+ ? {
1304
+ cn: noSniProbe.cert.cn,
1305
+ issuer: noSniProbe.cert.issuer,
1306
+ san: noSniProbe.cert.san,
1307
+ }
1308
+ : null,
1309
+ wildcardScope: wildcardCerts.length > 0
1310
+ ? Array.from(new Set(wildcardCerts.flatMap((p) => p.cert.san.filter((s) => s.startsWith("*.")))))
1311
+ : [],
1312
+ },
1313
+ },
1314
+ };
1315
+ cache.set(cacheKey, result);
1316
+ return json(result);
1317
+ },
1318
+ };
1319
+ // ──────────────────────────────────────────────────────────────────────────────
1320
+ // Export
1321
+ // ──────────────────────────────────────────────────────────────────────────────
1322
+ export const tlsTools = [
1323
+ tlsProbe,
1324
+ tlsJarm,
1325
+ tlsCert,
1326
+ tlsJa4x,
1327
+ tlsCertCrossRef,
1328
+ tlsCtSubdomains,
1329
+ tlsCiphers,
1330
+ tlsSni,
1331
+ ];
1332
+ //# sourceMappingURL=index.js.map