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,1044 @@
1
+ import { z } from "zod";
2
+ import { json } from "../types/index.js";
3
+ // ─── Known Patterns for Correlation Analysis ───
4
+ /** Header ordering fingerprints for common web servers */
5
+ const HEADER_ORDER_PATTERNS = {
6
+ nginx: ["server", "date", "content-type", "content-length", "connection"],
7
+ apache: ["date", "server", "content-type", "content-length"],
8
+ iis: ["content-type", "server", "x-powered-by", "date", "content-length"],
9
+ caddy: ["server", "content-type", "date"],
10
+ litespeed: ["date", "content-type", "content-length", "server"],
11
+ };
12
+ /** Cookie names associated with specific technologies */
13
+ const COOKIE_TECH_MAP = {
14
+ PHPSESSID: "PHP",
15
+ JSESSIONID: "Java (Servlet/JSP)",
16
+ "ASP.NET_SessionId": "ASP.NET",
17
+ "connect.sid": "Node.js (Express/Connect)",
18
+ _csrf: "Node.js (csurf/Express)",
19
+ laravel_session: "PHP (Laravel)",
20
+ ci_session: "PHP (CodeIgniter)",
21
+ PLAY_SESSION: "Scala (Play Framework)",
22
+ rack_session: "Ruby (Rack/Rails)",
23
+ _session_id: "Ruby on Rails",
24
+ wp_settings: "PHP (WordPress)",
25
+ CRAFT_CSRF_TOKEN: "PHP (Craft CMS)",
26
+ "django-session": "Python (Django)",
27
+ beaker_session_id: "Python (Pylons/TurboGears)",
28
+ };
29
+ /** Server headers that imply specific technologies */
30
+ const SERVER_TECH_MAP = {
31
+ nginx: "Nginx",
32
+ apache: "Apache",
33
+ "microsoft-iis": "IIS",
34
+ litespeed: "LiteSpeed",
35
+ caddy: "Caddy",
36
+ openresty: "OpenResty (Nginx)",
37
+ gunicorn: "Python (Gunicorn)",
38
+ uvicorn: "Python (Uvicorn)",
39
+ "cowboy": "Erlang (Cowboy)",
40
+ jetty: "Java (Jetty)",
41
+ tomcat: "Java (Tomcat)",
42
+ kestrel: "ASP.NET (Kestrel)",
43
+ };
44
+ /** TLS version compatibility reference */
45
+ const TLS_COMPATIBILITY = {
46
+ "TLSv1.3": { minYear: 2018, maxYear: 9999 },
47
+ "TLSv1.2": { minYear: 2008, maxYear: 9999 },
48
+ "TLSv1.1": { minYear: 2006, maxYear: 2021 },
49
+ "TLSv1.0": { minYear: 1999, maxYear: 2020 },
50
+ SSLv3: { minYear: 1996, maxYear: 2015 },
51
+ };
52
+ // ─── Tool 1: fp_correlate ───
53
+ const fpCorrelate = {
54
+ name: "fp_correlate",
55
+ description: "Cross-layer consistency validation. Takes fingerprint data from multiple tools and checks for contradictions " +
56
+ "between server header, header ordering, TLS version, error page signature, SSH banner, cookies, and JARM. " +
57
+ "Returns a consistency score (0.0-1.0) and a list of contradictions with explanations.",
58
+ schema: {
59
+ serverHeader: z.string().optional().describe("Server header value"),
60
+ headerOrderHash: z.string().optional().describe("Header ordering hash"),
61
+ tlsVersion: z.string().optional().describe("Negotiated TLS version"),
62
+ errorSignature: z.string().optional().describe("Detected error page framework"),
63
+ cookies: z.array(z.string()).optional().describe("Cookie names found"),
64
+ sshBanner: z.string().optional().describe("SSH banner string"),
65
+ jarm: z.string().optional().describe("JARM fingerprint hash"),
66
+ },
67
+ async execute(args) {
68
+ const serverHeader = args.serverHeader?.toLowerCase();
69
+ const headerOrderHash = args.headerOrderHash;
70
+ const tlsVersion = args.tlsVersion;
71
+ const errorSignature = args.errorSignature?.toLowerCase();
72
+ const cookies = args.cookies;
73
+ const sshBanner = args.sshBanner;
74
+ const jarm = args.jarm;
75
+ const contradictions = [];
76
+ let totalChecks = 0;
77
+ let passedChecks = 0;
78
+ // Detect server technology from header
79
+ let detectedServerTech = null;
80
+ if (serverHeader) {
81
+ for (const [pattern, tech] of Object.entries(SERVER_TECH_MAP)) {
82
+ if (serverHeader.includes(pattern)) {
83
+ detectedServerTech = tech;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ // Check 1: Server header vs header ordering
89
+ if (serverHeader && headerOrderHash) {
90
+ totalChecks++;
91
+ // If server says Apache but ordering matches Nginx (or vice versa), flag it
92
+ const isApache = serverHeader.includes("apache");
93
+ const isNginx = serverHeader.includes("nginx");
94
+ const isIIS = serverHeader.includes("microsoft-iis");
95
+ // We cannot reverse a hash, but if we know the hash came from a known pattern
96
+ // we can check consistency. For now, just log the relationship.
97
+ // In practice, compare against known hash databases.
98
+ if ((isApache && headerOrderHash.startsWith("nginx")) ||
99
+ (isNginx && headerOrderHash.startsWith("apache")) ||
100
+ (isIIS && headerOrderHash.startsWith("nginx"))) {
101
+ contradictions.push({
102
+ check: "server_vs_header_order",
103
+ severity: "HIGH",
104
+ explanation: `Server header claims "${serverHeader}" but header ordering pattern matches a different server. ` +
105
+ `This strongly indicates server header spoofing or a reverse proxy.`,
106
+ });
107
+ }
108
+ else {
109
+ passedChecks++;
110
+ }
111
+ }
112
+ // Check 2: TLS version vs server version claim
113
+ if (tlsVersion && serverHeader) {
114
+ totalChecks++;
115
+ const versionMatch = serverHeader.match(/(\d+\.\d+(\.\d+)?)/);
116
+ if (versionMatch) {
117
+ const claimedVersion = versionMatch[1];
118
+ const majorMinor = claimedVersion.split(".").slice(0, 2).join(".");
119
+ // Apache 2.2 typically cannot do TLS 1.3 natively (requires OpenSSL 1.1.1+)
120
+ if (serverHeader.includes("apache") && majorMinor === "2.2" && tlsVersion === "TLSv1.3") {
121
+ contradictions.push({
122
+ check: "tls_vs_server_version",
123
+ severity: "HIGH",
124
+ explanation: `Server claims "Apache/2.2" but negotiated TLS 1.3. Apache 2.2 uses OpenSSL < 1.1.1 which ` +
125
+ `does not support TLS 1.3. This is likely a reverse proxy (Cloudflare, Nginx) in front, or server header spoofing.`,
126
+ });
127
+ }
128
+ // IIS 7.x and below cannot do TLS 1.2+ natively
129
+ else if (serverHeader.includes("microsoft-iis") && parseFloat(majorMinor) <= 7.5 &&
130
+ (tlsVersion === "TLSv1.2" || tlsVersion === "TLSv1.3")) {
131
+ contradictions.push({
132
+ check: "tls_vs_server_version",
133
+ severity: "MEDIUM",
134
+ explanation: `Server claims "IIS/${claimedVersion}" but negotiated ${tlsVersion}. IIS 7.x only supports ` +
135
+ `TLS 1.0 natively. A load balancer or reverse proxy likely handles TLS termination.`,
136
+ });
137
+ }
138
+ // Very old Nginx with TLS 1.3
139
+ else if (serverHeader.includes("nginx") && parseFloat(majorMinor) < 1.13 && tlsVersion === "TLSv1.3") {
140
+ contradictions.push({
141
+ check: "tls_vs_server_version",
142
+ severity: "LOW",
143
+ explanation: `Server claims "nginx/${claimedVersion}" which predates TLS 1.3 support (requires nginx 1.13+). ` +
144
+ `Likely version spoofing or a CDN/proxy terminating TLS.`,
145
+ });
146
+ }
147
+ else {
148
+ passedChecks++;
149
+ }
150
+ }
151
+ else {
152
+ passedChecks++;
153
+ }
154
+ }
155
+ // Check 3: Error page vs server header
156
+ if (errorSignature && serverHeader) {
157
+ totalChecks++;
158
+ const errorToServer = {
159
+ nginx: ["nginx"],
160
+ apache: ["apache"],
161
+ iis: ["microsoft-iis", "iis"],
162
+ tomcat: ["tomcat", "apache-coyote"],
163
+ "spring boot": ["apache", "tomcat", "jetty"],
164
+ django: ["gunicorn", "uvicorn", "apache", "nginx"],
165
+ express: ["express", "nginx"],
166
+ flask: ["gunicorn", "uvicorn", "werkzeug"],
167
+ laravel: ["apache", "nginx"],
168
+ };
169
+ const expectedServers = errorToServer[errorSignature];
170
+ if (expectedServers) {
171
+ const matches = expectedServers.some((s) => serverHeader.includes(s));
172
+ if (!matches) {
173
+ contradictions.push({
174
+ check: "error_page_vs_server",
175
+ severity: "MEDIUM",
176
+ explanation: `Error page signature indicates "${errorSignature}" but server header says "${serverHeader}". ` +
177
+ `The error page reveals the actual backend framework while the server header may be spoofed or from a proxy.`,
178
+ });
179
+ }
180
+ else {
181
+ passedChecks++;
182
+ }
183
+ }
184
+ else {
185
+ passedChecks++;
186
+ }
187
+ }
188
+ // Check 4: Cookie technology vs server header
189
+ if (cookies && cookies.length > 0 && serverHeader) {
190
+ totalChecks++;
191
+ const cookieTechs = [];
192
+ for (const cookie of cookies) {
193
+ const tech = COOKIE_TECH_MAP[cookie];
194
+ if (tech)
195
+ cookieTechs.push(tech);
196
+ }
197
+ if (cookieTechs.length > 0) {
198
+ // JSESSIONID + "Apache" without AJP is suspicious
199
+ const hasJava = cookieTechs.some((t) => t.includes("Java"));
200
+ const hasPHP = cookieTechs.some((t) => t.includes("PHP"));
201
+ const hasNet = cookieTechs.some((t) => t.includes("ASP.NET"));
202
+ const hasNode = cookieTechs.some((t) => t.includes("Node"));
203
+ const hasPython = cookieTechs.some((t) => t.includes("Python"));
204
+ const hasRuby = cookieTechs.some((t) => t.includes("Ruby"));
205
+ if (hasJava && serverHeader.includes("apache") && !serverHeader.includes("tomcat")) {
206
+ // Apache fronting Tomcat via mod_jk/AJP is normal, but pure Apache + JSESSIONID is odd
207
+ contradictions.push({
208
+ check: "cookie_tech_vs_server",
209
+ severity: "LOW",
210
+ explanation: `JSESSIONID cookie (Java) found behind Apache without Tomcat server header. ` +
211
+ `Likely Apache reverse proxying to Tomcat/Jetty — normal architecture, but reveals backend technology.`,
212
+ });
213
+ }
214
+ else if (hasNet && !serverHeader.includes("microsoft-iis") && !serverHeader.includes("kestrel")) {
215
+ contradictions.push({
216
+ check: "cookie_tech_vs_server",
217
+ severity: "MEDIUM",
218
+ explanation: `ASP.NET session cookie found but server header says "${serverHeader}" instead of IIS/Kestrel. ` +
219
+ `A reverse proxy is hiding the actual ASP.NET backend.`,
220
+ });
221
+ }
222
+ else if (hasPHP && serverHeader.includes("microsoft-iis")) {
223
+ // PHP on IIS is possible but uncommon
224
+ contradictions.push({
225
+ check: "cookie_tech_vs_server",
226
+ severity: "LOW",
227
+ explanation: `PHP session cookie (PHPSESSID) found on IIS server. PHP on IIS via FastCGI is possible ` +
228
+ `but uncommon — verify if this is intentional.`,
229
+ });
230
+ }
231
+ else {
232
+ passedChecks++;
233
+ }
234
+ }
235
+ else {
236
+ passedChecks++;
237
+ }
238
+ }
239
+ // Check 5: SSH banner vs server context
240
+ if (sshBanner && serverHeader) {
241
+ totalChecks++;
242
+ // If SSH banner claims ancient version but web server is modern, note it
243
+ const sshVersionMatch = sshBanner.match(/OpenSSH[_\s](\d+\.\d+)/i);
244
+ if (sshVersionMatch) {
245
+ const sshVersion = parseFloat(sshVersionMatch[1]);
246
+ // OpenSSH < 7.0 is very old (2015)
247
+ if (sshVersion < 7.0 && tlsVersion === "TLSv1.3") {
248
+ contradictions.push({
249
+ check: "ssh_vs_tls_modernity",
250
+ severity: "MEDIUM",
251
+ explanation: `SSH claims OpenSSH ${sshVersion} (pre-2015) but TLS 1.3 is negotiated (2018+). ` +
252
+ `The SSH banner may be spoofed, or the system is severely misconfigured.`,
253
+ });
254
+ }
255
+ else {
256
+ passedChecks++;
257
+ }
258
+ }
259
+ else {
260
+ passedChecks++;
261
+ }
262
+ }
263
+ // Calculate consistency score
264
+ const consistencyScore = totalChecks > 0 ? passedChecks / totalChecks : 1.0;
265
+ const result = {
266
+ consistencyScore: Math.round(consistencyScore * 100) / 100,
267
+ totalChecks,
268
+ passedChecks,
269
+ failedChecks: contradictions.length,
270
+ contradictions,
271
+ inputSignals: {
272
+ serverHeader: serverHeader ?? null,
273
+ headerOrderHash: headerOrderHash ?? null,
274
+ tlsVersion: tlsVersion ?? null,
275
+ errorSignature: errorSignature ?? null,
276
+ cookies: cookies ?? [],
277
+ sshBanner: sshBanner ?? null,
278
+ jarm: jarm ?? null,
279
+ },
280
+ detectedServerTech,
281
+ assessment: consistencyScore >= 0.9
282
+ ? "CONSISTENT — Signals align. Server identity appears genuine."
283
+ : consistencyScore >= 0.6
284
+ ? "PARTIAL_MISMATCH — Some signals contradict. Possible reverse proxy, CDN, or partial spoofing."
285
+ : "INCONSISTENT — Multiple contradictions detected. Server identity is likely spoofed or heavily proxied.",
286
+ };
287
+ return json(result);
288
+ },
289
+ };
290
+ // ─── Tool 2: fp_honeypot ───
291
+ const fpHoneypot = {
292
+ name: "fp_honeypot",
293
+ description: "Honeypot probability scoring. Analyzes service scan results for honeypot indicators: old version claims with " +
294
+ "modern protocols, identical sub-millisecond response times, too many open ports with perfect banners, TLS " +
295
+ "contradictions, ancient SSH with modern KEX, and default credential acceptance. Returns a score (0.0-1.0) " +
296
+ "with detailed reasoning for each signal.",
297
+ schema: {
298
+ services: z
299
+ .array(z.object({
300
+ port: z.number().describe("Service port number"),
301
+ banner: z.string().optional().describe("Service banner string"),
302
+ responseTimeMs: z.number().optional().describe("Response time in milliseconds"),
303
+ tlsVersion: z.string().optional().describe("Negotiated TLS version"),
304
+ }))
305
+ .describe("Service scan results"),
306
+ openPortCount: z.number().optional().describe("Total open ports found"),
307
+ sshBanner: z.string().optional().describe("SSH banner"),
308
+ sshAlgorithms: z.array(z.string()).optional().describe("SSH KEX algorithms"),
309
+ },
310
+ async execute(args) {
311
+ const services = args.services;
312
+ const openPortCount = args.openPortCount ?? services.length;
313
+ const sshBanner = args.sshBanner;
314
+ const sshAlgorithms = args.sshAlgorithms;
315
+ const signals = [];
316
+ let totalWeight = 0;
317
+ let honeypotWeight = 0;
318
+ // Signal 1: Too many open ports
319
+ totalWeight += 1.0;
320
+ if (openPortCount > 50) {
321
+ const weight = 0.9;
322
+ honeypotWeight += weight;
323
+ signals.push({
324
+ indicator: "excessive_open_ports",
325
+ weight,
326
+ reasoning: `${openPortCount} open ports detected. Real servers typically have 3-15 open ports. ` +
327
+ `Honeypots like Cowrie and Dionaea open many ports to attract scanners.`,
328
+ });
329
+ }
330
+ else if (openPortCount > 30) {
331
+ const weight = 0.5;
332
+ honeypotWeight += weight;
333
+ signals.push({
334
+ indicator: "many_open_ports",
335
+ weight,
336
+ reasoning: `${openPortCount} open ports — more than typical for production servers. Possible honeypot.`,
337
+ });
338
+ }
339
+ // Signal 2: Identical response times (single-process emulation)
340
+ totalWeight += 1.0;
341
+ const responseTimes = services.filter((s) => s.responseTimeMs !== undefined).map((s) => s.responseTimeMs);
342
+ if (responseTimes.length >= 3) {
343
+ const allSame = responseTimes.every((t) => Math.abs(t - responseTimes[0]) < 0.5);
344
+ const allSubMs = responseTimes.every((t) => t < 1.0);
345
+ if (allSame && allSubMs) {
346
+ const weight = 0.85;
347
+ honeypotWeight += weight;
348
+ signals.push({
349
+ indicator: "identical_sub_ms_response",
350
+ weight,
351
+ reasoning: `All ${responseTimes.length} services respond in identical sub-millisecond times ` +
352
+ `(~${responseTimes[0].toFixed(2)}ms). This suggests a single process emulating multiple services — ` +
353
+ `classic honeypot behavior.`,
354
+ });
355
+ }
356
+ else if (allSame) {
357
+ const weight = 0.4;
358
+ honeypotWeight += weight;
359
+ signals.push({
360
+ indicator: "identical_response_times",
361
+ weight,
362
+ reasoning: `All services respond in nearly identical times. Unusual for independent real services.`,
363
+ });
364
+ }
365
+ }
366
+ // Signal 3: Old version banners with modern TLS
367
+ totalWeight += 1.0;
368
+ const oldVersionModernTls = [];
369
+ for (const svc of services) {
370
+ if (!svc.banner || !svc.tlsVersion)
371
+ continue;
372
+ const bannerLower = svc.banner.toLowerCase();
373
+ // Check for old Apache + TLS 1.3
374
+ if (bannerLower.includes("apache/2.2") && svc.tlsVersion === "TLSv1.3") {
375
+ oldVersionModernTls.push(`port ${svc.port}: Apache/2.2 + TLS 1.3`);
376
+ }
377
+ // Old Nginx + TLS 1.3
378
+ if (bannerLower.match(/nginx\/1\.[0-9]\./) && svc.tlsVersion === "TLSv1.3") {
379
+ oldVersionModernTls.push(`port ${svc.port}: old Nginx + TLS 1.3`);
380
+ }
381
+ // Old OpenSSH + modern TLS on adjacent ports
382
+ if (bannerLower.match(/openssh[_\s][45]\./i) && svc.tlsVersion === "TLSv1.3") {
383
+ oldVersionModernTls.push(`port ${svc.port}: ancient OpenSSH + TLS 1.3`);
384
+ }
385
+ }
386
+ if (oldVersionModernTls.length >= 2) {
387
+ const weight = 0.8;
388
+ honeypotWeight += weight;
389
+ signals.push({
390
+ indicator: "old_versions_modern_tls",
391
+ weight,
392
+ reasoning: `Multiple services claim old versions but support modern TLS: ${oldVersionModernTls.join("; ")}. ` +
393
+ `Real old software cannot negotiate TLS 1.3.`,
394
+ });
395
+ }
396
+ else if (oldVersionModernTls.length === 1) {
397
+ const weight = 0.4;
398
+ honeypotWeight += weight;
399
+ signals.push({
400
+ indicator: "old_version_modern_tls",
401
+ weight,
402
+ reasoning: `${oldVersionModernTls[0]} — version claim contradicts TLS capability.`,
403
+ });
404
+ }
405
+ // Signal 4: Perfect banners on every port
406
+ totalWeight += 1.0;
407
+ const banneredServices = services.filter((s) => s.banner && s.banner.length > 0);
408
+ if (banneredServices.length === services.length && services.length > 10) {
409
+ const weight = 0.6;
410
+ honeypotWeight += weight;
411
+ signals.push({
412
+ indicator: "perfect_banners_all_ports",
413
+ weight,
414
+ reasoning: `Every single port (${services.length}) returns a clean banner. Real servers often have ports ` +
415
+ `that respond with empty or malformed banners.`,
416
+ });
417
+ }
418
+ // Signal 5: SSH banner vs KEX algorithms mismatch
419
+ totalWeight += 1.0;
420
+ if (sshBanner && sshAlgorithms && sshAlgorithms.length > 0) {
421
+ const sshVersionMatch = sshBanner.match(/OpenSSH[_\s](\d+\.\d+)/i);
422
+ if (sshVersionMatch) {
423
+ const sshMajor = parseFloat(sshVersionMatch[1]);
424
+ // Modern KEX algorithms that old OpenSSH cannot support
425
+ const modernKex = [
426
+ "curve25519-sha256",
427
+ "curve25519-sha256@libssh.org",
428
+ "sntrup761x25519-sha512@openssh.com",
429
+ ];
430
+ const hasModernKex = sshAlgorithms.some((a) => modernKex.includes(a));
431
+ if (sshMajor < 6.5 && hasModernKex) {
432
+ const weight = 0.85;
433
+ honeypotWeight += weight;
434
+ signals.push({
435
+ indicator: "ssh_version_kex_mismatch",
436
+ weight,
437
+ reasoning: `SSH banner claims OpenSSH ${sshMajor} but supports modern KEX algorithms ` +
438
+ `(curve25519-sha256 requires OpenSSH 6.5+). Banner is spoofed — likely a honeypot.`,
439
+ });
440
+ }
441
+ else {
442
+ // Consistent
443
+ }
444
+ }
445
+ }
446
+ // Signal 6: Every port responds (typical honeypot behavior)
447
+ totalWeight += 1.0;
448
+ if (services.length >= 20) {
449
+ const commonPorts = [21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 993, 995, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 9200];
450
+ const matchedCommon = commonPorts.filter((p) => services.some((s) => s.port === p));
451
+ if (matchedCommon.length >= 15) {
452
+ const weight = 0.75;
453
+ honeypotWeight += weight;
454
+ signals.push({
455
+ indicator: "too_many_common_ports",
456
+ weight,
457
+ reasoning: `${matchedCommon.length} of ${commonPorts.length} well-known ports are open. ` +
458
+ `Production servers focus on a few services. Honeypots maximize attack surface to collect data.`,
459
+ });
460
+ }
461
+ }
462
+ const score = totalWeight > 0 ? Math.min(honeypotWeight / totalWeight, 1.0) : 0;
463
+ const roundedScore = Math.round(score * 100) / 100;
464
+ const result = {
465
+ honeypotScore: roundedScore,
466
+ assessment: roundedScore >= 0.8
467
+ ? "HIGH_PROBABILITY — Strong honeypot indicators detected. Treat with extreme caution."
468
+ : roundedScore >= 0.5
469
+ ? "MODERATE_PROBABILITY — Several suspicious signals. Could be a honeypot or heavily emulated environment."
470
+ : roundedScore >= 0.2
471
+ ? "LOW_PROBABILITY — Minor anomalies but likely a real server."
472
+ : "UNLIKELY — No significant honeypot indicators found.",
473
+ signals,
474
+ inputSummary: {
475
+ servicesAnalyzed: services.length,
476
+ openPortCount,
477
+ sshBanner: sshBanner ?? null,
478
+ sshAlgorithmCount: sshAlgorithms?.length ?? 0,
479
+ },
480
+ };
481
+ return json(result);
482
+ },
483
+ };
484
+ // ─── Tool 3: fp_spoofing ───
485
+ const fpSpoofing = {
486
+ name: "fp_spoofing",
487
+ description: "Spoofing detection. Identifies when services misrepresent their identity by comparing server header vs " +
488
+ "header ordering, SSH banner vs HASSH fingerprint, and claimed version vs actual capabilities. Returns " +
489
+ "actual estimate vs claimed identity with confidence.",
490
+ schema: {
491
+ serverHeader: z.string().optional().describe("Server header value"),
492
+ headerOrder: z.array(z.string()).optional().describe("Response header names in order"),
493
+ sshBanner: z.string().optional().describe("SSH banner"),
494
+ hassh: z.string().optional().describe("HASSH fingerprint"),
495
+ sshAlgorithms: z.array(z.string()).optional().describe("SSH supported algorithms"),
496
+ claimedVersion: z.string().optional().describe("Claimed software version"),
497
+ },
498
+ async execute(args) {
499
+ const serverHeader = args.serverHeader?.toLowerCase();
500
+ const headerOrder = args.headerOrder;
501
+ const sshBanner = args.sshBanner;
502
+ const hassh = args.hassh;
503
+ const sshAlgorithms = args.sshAlgorithms;
504
+ const claimedVersion = args.claimedVersion;
505
+ const findings = [];
506
+ // Check 1: Server header vs header ordering
507
+ if (serverHeader && headerOrder && headerOrder.length > 0) {
508
+ const normalizedOrder = headerOrder.map((h) => h.toLowerCase());
509
+ // Nginx typically sends: server, date, content-type, ...
510
+ const looksLikeNginx = normalizedOrder[0] === "server" &&
511
+ normalizedOrder.indexOf("date") < normalizedOrder.indexOf("content-length");
512
+ // Apache typically sends: date, server, ...
513
+ const looksLikeApache = normalizedOrder[0] === "date" && normalizedOrder[1] === "server";
514
+ // IIS typically sends: content-type first
515
+ const looksLikeIIS = normalizedOrder[0] === "content-type" &&
516
+ normalizedOrder.includes("x-powered-by");
517
+ let claimedServer = "unknown";
518
+ let detectedServer = "unknown";
519
+ if (serverHeader.includes("nginx"))
520
+ claimedServer = "nginx";
521
+ else if (serverHeader.includes("apache"))
522
+ claimedServer = "apache";
523
+ else if (serverHeader.includes("microsoft-iis"))
524
+ claimedServer = "iis";
525
+ else if (serverHeader.includes("litespeed"))
526
+ claimedServer = "litespeed";
527
+ else if (serverHeader.includes("caddy"))
528
+ claimedServer = "caddy";
529
+ if (looksLikeNginx)
530
+ detectedServer = "nginx";
531
+ else if (looksLikeApache)
532
+ detectedServer = "apache";
533
+ else if (looksLikeIIS)
534
+ detectedServer = "iis";
535
+ if (claimedServer !== "unknown" && detectedServer !== "unknown" && claimedServer !== detectedServer) {
536
+ findings.push({
537
+ check: "server_header_vs_header_order",
538
+ spoofingDetected: true,
539
+ claimedIdentity: `Server header: ${serverHeader}`,
540
+ actualEstimate: `Header ordering matches ${detectedServer}`,
541
+ confidence: 0.8,
542
+ explanation: `Server claims to be ${claimedServer} but response header ordering pattern ` +
543
+ `(${normalizedOrder.slice(0, 4).join(", ")}) is characteristic of ${detectedServer}. ` +
544
+ `Header ordering is hard to fake without rewriting the entire HTTP stack.`,
545
+ });
546
+ }
547
+ else {
548
+ findings.push({
549
+ check: "server_header_vs_header_order",
550
+ spoofingDetected: false,
551
+ claimedIdentity: claimedServer,
552
+ actualEstimate: detectedServer !== "unknown" ? detectedServer : "matches claim",
553
+ confidence: 0.7,
554
+ explanation: "Server header and header ordering are consistent.",
555
+ });
556
+ }
557
+ }
558
+ // Check 2: SSH banner vs HASSH
559
+ if (sshBanner && hassh) {
560
+ // Known HASSH fingerprints
561
+ const KNOWN_HASSH = {
562
+ "ec7378c1a92f5a8dde7e8b7a1ddf33d1": "OpenSSH 7.x-8.x",
563
+ "b12d2871a1571f57b5f7e53e3e7d2b97": "OpenSSH 6.x",
564
+ "06046964c022c6407d15a27b12a6a4fb": "Dropbear SSH",
565
+ "92674389fa1e47a27ddd8d9b63ecd42b": "PuTTY / libssh",
566
+ "c8a9b0c5c7e4d8e1f2a3b4c5d6e7f8a9": "Paramiko (Python)",
567
+ "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6": "libssh2",
568
+ };
569
+ const sshBannerLower = sshBanner.toLowerCase();
570
+ const hasshMatch = KNOWN_HASSH[hassh];
571
+ let spoofed = false;
572
+ let claimedSsh = "unknown";
573
+ let actualSsh = hasshMatch ?? "unknown (not in database)";
574
+ if (sshBannerLower.includes("openssh"))
575
+ claimedSsh = "OpenSSH";
576
+ else if (sshBannerLower.includes("dropbear"))
577
+ claimedSsh = "Dropbear";
578
+ else if (sshBannerLower.includes("putty"))
579
+ claimedSsh = "PuTTY";
580
+ else if (sshBannerLower.includes("libssh"))
581
+ claimedSsh = "libssh";
582
+ if (hasshMatch) {
583
+ // Banner says OpenSSH but HASSH matches Dropbear
584
+ if (claimedSsh === "OpenSSH" && hasshMatch.includes("Dropbear")) {
585
+ spoofed = true;
586
+ actualSsh = "Dropbear SSH (masquerading as OpenSSH)";
587
+ }
588
+ else if (claimedSsh === "Dropbear" && hasshMatch.includes("OpenSSH")) {
589
+ spoofed = true;
590
+ actualSsh = "OpenSSH (masquerading as Dropbear)";
591
+ }
592
+ else if (claimedSsh === "OpenSSH" && hasshMatch.includes("Paramiko")) {
593
+ spoofed = true;
594
+ actualSsh = "Paramiko Python SSH (masquerading as OpenSSH)";
595
+ }
596
+ }
597
+ findings.push({
598
+ check: "ssh_banner_vs_hassh",
599
+ spoofingDetected: spoofed,
600
+ claimedIdentity: `SSH banner: ${sshBanner}`,
601
+ actualEstimate: actualSsh,
602
+ confidence: spoofed ? 0.9 : 0.6,
603
+ explanation: spoofed
604
+ ? `SSH banner claims ${claimedSsh} but HASSH fingerprint (${hassh}) matches ${actualSsh}. ` +
605
+ `HASSH is computed from the SSH algorithm negotiation, which differs between implementations.`
606
+ : "SSH banner and HASSH fingerprint are consistent (or HASSH not in database).",
607
+ });
608
+ }
609
+ // Check 3: Claimed version vs SSH algorithms
610
+ if (sshBanner && sshAlgorithms && sshAlgorithms.length > 0) {
611
+ const versionMatch = sshBanner.match(/OpenSSH[_\s](\d+\.\d+)/i);
612
+ if (versionMatch) {
613
+ const version = parseFloat(versionMatch[1]);
614
+ // Check for algorithm support that contradicts version
615
+ const hasCurve25519 = sshAlgorithms.some((a) => a.includes("curve25519"));
616
+ const hasEd25519 = sshAlgorithms.some((a) => a.includes("ed25519"));
617
+ const hasChaCha = sshAlgorithms.some((a) => a.includes("chacha20"));
618
+ // curve25519 requires OpenSSH 6.5+, ed25519 requires 6.5+, chacha20 requires 6.5+
619
+ if (version < 6.5 && (hasCurve25519 || hasEd25519 || hasChaCha)) {
620
+ findings.push({
621
+ check: "ssh_version_vs_algorithms",
622
+ spoofingDetected: true,
623
+ claimedIdentity: `OpenSSH ${version}`,
624
+ actualEstimate: `OpenSSH 6.5+ (supports modern algorithms not available in ${version})`,
625
+ confidence: 0.92,
626
+ explanation: `Banner claims OpenSSH ${version} but supports ${[
627
+ hasCurve25519 ? "curve25519" : "",
628
+ hasEd25519 ? "ed25519" : "",
629
+ hasChaCha ? "chacha20-poly1305" : "",
630
+ ].filter(Boolean).join(", ")} which require OpenSSH 6.5+. The version in the banner is spoofed.`,
631
+ });
632
+ }
633
+ // DSA key algorithm was deprecated in OpenSSH 7.0+
634
+ const hasDSA = sshAlgorithms.some((a) => a.includes("ssh-dss"));
635
+ if (version >= 8.0 && hasDSA) {
636
+ findings.push({
637
+ check: "ssh_version_vs_dsa",
638
+ spoofingDetected: true,
639
+ claimedIdentity: `OpenSSH ${version}`,
640
+ actualEstimate: "OpenSSH < 7.0 or manually re-enabled DSA",
641
+ confidence: 0.5,
642
+ explanation: `Banner claims OpenSSH ${version} but still offers ssh-dss (DSA). ` +
643
+ `DSA was disabled by default in OpenSSH 7.0 and removed in 8.8. Could be re-enabled, ` +
644
+ `but combined with other signals may indicate spoofing.`,
645
+ });
646
+ }
647
+ }
648
+ }
649
+ // Check 4: Claimed version string validation
650
+ if (claimedVersion && serverHeader) {
651
+ const versionMatch = claimedVersion.match(/^(\d+)\.(\d+)(?:\.(\d+))?/);
652
+ if (versionMatch) {
653
+ const major = parseInt(versionMatch[1], 10);
654
+ const minor = parseInt(versionMatch[2], 10);
655
+ // Check for impossible version numbers
656
+ if (serverHeader.includes("nginx") && major > 1) {
657
+ findings.push({
658
+ check: "impossible_version",
659
+ spoofingDetected: true,
660
+ claimedIdentity: `nginx/${claimedVersion}`,
661
+ actualEstimate: "Unknown (nginx major version has never exceeded 1.x)",
662
+ confidence: 0.95,
663
+ explanation: `Nginx version ${claimedVersion} does not exist. Nginx has never released a 2.x version. ` +
664
+ `This is clearly a spoofed or fabricated version string.`,
665
+ });
666
+ }
667
+ if (serverHeader.includes("apache") && major === 2 && minor > 4) {
668
+ findings.push({
669
+ check: "impossible_version",
670
+ spoofingDetected: true,
671
+ claimedIdentity: `Apache/${claimedVersion}`,
672
+ actualEstimate: "Unknown (Apache 2.x has never exceeded 2.4.x)",
673
+ confidence: 0.95,
674
+ explanation: `Apache version 2.${minor} does not exist. Apache httpd has stayed within 2.4.x releases. ` +
675
+ `This version string is fabricated.`,
676
+ });
677
+ }
678
+ }
679
+ }
680
+ const spoofingDetected = findings.some((f) => f.spoofingDetected);
681
+ const maxConfidence = findings
682
+ .filter((f) => f.spoofingDetected)
683
+ .reduce((max, f) => Math.max(max, f.confidence), 0);
684
+ const result = {
685
+ spoofingDetected,
686
+ overallConfidence: Math.round(maxConfidence * 100) / 100,
687
+ totalChecks: findings.length,
688
+ spoofedChecks: findings.filter((f) => f.spoofingDetected).length,
689
+ findings,
690
+ assessment: spoofingDetected
691
+ ? `SPOOFING DETECTED — ${findings.filter((f) => f.spoofingDetected).length} signal(s) indicate ` +
692
+ `the server is misrepresenting its identity.`
693
+ : "NO SPOOFING DETECTED — All checked signals are consistent with claimed identity.",
694
+ };
695
+ return json(result);
696
+ },
697
+ };
698
+ // ─── Tool 4: fp_compare ───
699
+ const fpCompare = {
700
+ name: "fp_compare",
701
+ description: "Compare two fingerprint profiles. Takes two sets of fingerprint data, diffs them, and reports significance " +
702
+ "of each difference (e.g. 'WAF deployed', 'Server restarted with new config', 'Backend OS changed', " +
703
+ "'CDN added'). Returns a similarity score (0.0-1.0) and a list of changes.",
704
+ schema: {
705
+ profile1: z.record(z.unknown()).describe("First fingerprint profile (JSON)"),
706
+ profile2: z.record(z.unknown()).describe("Second fingerprint profile (JSON)"),
707
+ },
708
+ async execute(args) {
709
+ const profile1 = args.profile1;
710
+ const profile2 = args.profile2;
711
+ const differences = [];
712
+ // Get all unique keys from both profiles
713
+ const allKeys = new Set([...Object.keys(profile1), ...Object.keys(profile2)]);
714
+ let matchedKeys = 0;
715
+ let totalKeys = 0;
716
+ /** Interpret what a change in a specific field means */
717
+ function interpretChange(key, val1, val2) {
718
+ const keyLower = key.toLowerCase();
719
+ if (keyLower.includes("server") || keyLower === "serverheader") {
720
+ const v1 = String(val1 ?? "").toLowerCase();
721
+ const v2 = String(val2 ?? "").toLowerCase();
722
+ if ((v1.includes("nginx") && v2.includes("cloudflare")) ||
723
+ (v1.includes("apache") && v2.includes("cloudflare"))) {
724
+ return { significance: "CDN/WAF deployed — origin server now behind Cloudflare", changeType: "infrastructure" };
725
+ }
726
+ if (v1 !== v2) {
727
+ return { significance: "Web server software changed", changeType: "configuration" };
728
+ }
729
+ }
730
+ if (keyLower.includes("tls") || keyLower.includes("ssl")) {
731
+ return { significance: "TLS configuration changed — may indicate certificate rotation or security hardening", changeType: "security" };
732
+ }
733
+ if (keyLower.includes("jarm")) {
734
+ return { significance: "JARM fingerprint changed — TLS stack or configuration modified. Could indicate server software change, config update, or C2 profile switch", changeType: "infrastructure" };
735
+ }
736
+ if (keyLower.includes("ssh") || keyLower.includes("hassh")) {
737
+ return { significance: "SSH configuration changed — possible server rebuild, OS update, or key rotation", changeType: "configuration" };
738
+ }
739
+ if (keyLower.includes("waf") || keyLower.includes("cdn")) {
740
+ return { significance: "WAF/CDN configuration changed", changeType: "security" };
741
+ }
742
+ if (keyLower.includes("cookie")) {
743
+ return { significance: "Cookie configuration changed — possible framework update or session management change", changeType: "application" };
744
+ }
745
+ if (keyLower.includes("header")) {
746
+ return { significance: "HTTP header configuration changed", changeType: "configuration" };
747
+ }
748
+ if (keyLower.includes("cert") || keyLower.includes("certificate")) {
749
+ return { significance: "TLS certificate changed — rotation, renewal, or issuer switch", changeType: "security" };
750
+ }
751
+ if (keyLower.includes("os") || keyLower.includes("platform")) {
752
+ return { significance: "Operating system or platform changed — possible server migration", changeType: "infrastructure" };
753
+ }
754
+ if (keyLower.includes("port")) {
755
+ return { significance: "Port configuration changed — new services added or removed", changeType: "infrastructure" };
756
+ }
757
+ if (keyLower.includes("origin") || keyLower.includes("ip")) {
758
+ return { significance: "Origin IP changed — possible server migration, CDN change, or failover", changeType: "infrastructure" };
759
+ }
760
+ return { significance: "Configuration value changed", changeType: "general" };
761
+ }
762
+ for (const key of allKeys) {
763
+ totalKeys++;
764
+ const val1 = profile1[key];
765
+ const val2 = profile2[key];
766
+ const str1 = JSON.stringify(val1);
767
+ const str2 = JSON.stringify(val2);
768
+ if (str1 === str2) {
769
+ matchedKeys++;
770
+ }
771
+ else {
772
+ const { significance, changeType } = interpretChange(key, val1, val2);
773
+ differences.push({
774
+ field: key,
775
+ value1: val1 ?? "(absent)",
776
+ value2: val2 ?? "(absent)",
777
+ significance,
778
+ changeType,
779
+ });
780
+ }
781
+ }
782
+ const similarityScore = totalKeys > 0 ? matchedKeys / totalKeys : 1.0;
783
+ const roundedScore = Math.round(similarityScore * 100) / 100;
784
+ // Group changes by type
785
+ const changesByType = {};
786
+ for (const diff of differences) {
787
+ if (!changesByType[diff.changeType])
788
+ changesByType[diff.changeType] = [];
789
+ changesByType[diff.changeType].push(diff.field);
790
+ }
791
+ const result = {
792
+ similarityScore: roundedScore,
793
+ totalFields: totalKeys,
794
+ matchedFields: matchedKeys,
795
+ differentFields: differences.length,
796
+ differences,
797
+ changesByType,
798
+ assessment: roundedScore >= 0.95
799
+ ? "IDENTICAL — Profiles are effectively the same. Minor differences may be from timing or cache."
800
+ : roundedScore >= 0.8
801
+ ? "SIMILAR — Most signals match. Changes suggest minor configuration updates."
802
+ : roundedScore >= 0.5
803
+ ? "CHANGED — Significant differences detected. Major configuration or infrastructure change."
804
+ : roundedScore >= 0.2
805
+ ? "SUBSTANTIALLY_DIFFERENT — Most signals differ. Server rebuild, migration, or completely different host."
806
+ : "COMPLETELY_DIFFERENT — Profiles share almost nothing. Likely different servers entirely.",
807
+ };
808
+ return json(result);
809
+ },
810
+ };
811
+ // ─── Tool 5: fp_topology ───
812
+ const fpTopology = {
813
+ name: "fp_topology",
814
+ description: "Infrastructure topology reconstruction. Takes collected data from CDN detection, origin IP discovery, " +
815
+ "F5 cookie decode, certificate SANs, DNS records, and builds a layered map: " +
816
+ "Internet -> CDN/WAF -> Origin -> Load Balancer -> Backend(s) -> Internal hostnames.",
817
+ schema: {
818
+ cdnProvider: z.string().optional().describe("Detected CDN provider"),
819
+ originIp: z.string().optional().describe("Discovered origin IP"),
820
+ lbCookies: z.array(z.string()).optional().describe("Load balancer cookie values"),
821
+ certSans: z.array(z.string()).optional().describe("Certificate SAN hostnames"),
822
+ dnsRecords: z.record(z.unknown()).optional().describe("DNS records"),
823
+ serverHeader: z.string().optional().describe("Server header"),
824
+ internalHostnames: z.array(z.string()).optional().describe("Discovered internal hostnames"),
825
+ },
826
+ async execute(args) {
827
+ const cdnProvider = args.cdnProvider;
828
+ const originIp = args.originIp;
829
+ const lbCookies = args.lbCookies;
830
+ const certSans = args.certSans;
831
+ const dnsRecords = args.dnsRecords;
832
+ const serverHeader = args.serverHeader;
833
+ const internalHostnames = args.internalHostnames;
834
+ // Build topology layers
835
+ const layers = [];
836
+ // Layer 1: Internet (always present)
837
+ layers.push({
838
+ layer: 1,
839
+ name: "Internet",
840
+ type: "network",
841
+ details: { description: "Public internet / client" },
842
+ });
843
+ // Layer 2: CDN/WAF (if detected)
844
+ if (cdnProvider) {
845
+ layers.push({
846
+ layer: 2,
847
+ name: cdnProvider,
848
+ type: "cdn_waf",
849
+ details: {
850
+ provider: cdnProvider,
851
+ role: "CDN / WAF / DDoS protection",
852
+ tlsTermination: true,
853
+ note: "CDN terminates TLS and proxies to origin. JARM fingerprint reflects CDN, not origin server.",
854
+ },
855
+ });
856
+ }
857
+ // Layer 3: Origin Server
858
+ const originDetails = {};
859
+ if (originIp) {
860
+ originDetails.ip = originIp;
861
+ // Classify IP
862
+ if (originIp.match(/^10\.|^172\.(1[6-9]|2[0-9]|3[01])\.|^192\.168\./)) {
863
+ originDetails.networkType = "private (RFC 1918)";
864
+ originDetails.note = "Origin IP is in a private range — likely behind NAT or in a cloud VPC";
865
+ }
866
+ else {
867
+ originDetails.networkType = "public";
868
+ }
869
+ }
870
+ if (serverHeader) {
871
+ originDetails.serverSoftware = serverHeader;
872
+ }
873
+ if (dnsRecords) {
874
+ const aRecords = dnsRecords["A"] ?? dnsRecords["a"];
875
+ if (Array.isArray(aRecords)) {
876
+ originDetails.dnsARecords = aRecords;
877
+ if (aRecords.length > 1) {
878
+ originDetails.dnsRoundRobin = true;
879
+ originDetails.note = `${aRecords.length} A records suggest DNS-based load balancing or multi-region deployment`;
880
+ }
881
+ }
882
+ }
883
+ layers.push({
884
+ layer: cdnProvider ? 3 : 2,
885
+ name: "Origin Server",
886
+ type: "origin",
887
+ details: originDetails,
888
+ });
889
+ // Layer 4: Load Balancer (if cookies indicate)
890
+ let lbDetected = false;
891
+ const lbDetails = {};
892
+ if (lbCookies && lbCookies.length > 0) {
893
+ lbDetected = true;
894
+ const parsedCookies = [];
895
+ for (const cookie of lbCookies) {
896
+ const parsed = { raw: cookie };
897
+ // F5 BIG-IP cookie decode
898
+ // F5 cookies look like: BIGipServer<pool_name>=<encoded_ip>.<encoded_port>.0000
899
+ if (cookie.startsWith("BIGipServer")) {
900
+ parsed.type = "F5 BIG-IP";
901
+ const valueMatch = cookie.match(/=(\d+)\.(\d+)\./);
902
+ if (valueMatch) {
903
+ const encodedIp = parseInt(valueMatch[1], 10);
904
+ const encodedPort = parseInt(valueMatch[2], 10);
905
+ // Decode F5 IP: reverse byte order
906
+ const ip = [
907
+ encodedIp & 0xff,
908
+ (encodedIp >> 8) & 0xff,
909
+ (encodedIp >> 16) & 0xff,
910
+ (encodedIp >> 24) & 0xff,
911
+ ].join(".");
912
+ // Decode F5 port: swap bytes
913
+ const port = ((encodedPort & 0xff) << 8) | ((encodedPort >> 8) & 0xff);
914
+ parsed.decodedBackendIp = ip;
915
+ parsed.decodedBackendPort = port;
916
+ }
917
+ }
918
+ // AWS ALB cookie
919
+ else if (cookie.includes("AWSALB") || cookie.includes("AWSELB")) {
920
+ parsed.type = "AWS ELB/ALB";
921
+ }
922
+ // HAProxy
923
+ else if (cookie.includes("SERVERID") || cookie.includes("SRV")) {
924
+ parsed.type = "HAProxy";
925
+ }
926
+ parsedCookies.push(parsed);
927
+ }
928
+ lbDetails.cookies = parsedCookies;
929
+ lbDetails.type = parsedCookies[0]?.type ?? "Unknown load balancer";
930
+ layers.push({
931
+ layer: cdnProvider ? 4 : 3,
932
+ name: "Load Balancer",
933
+ type: "load_balancer",
934
+ details: lbDetails,
935
+ });
936
+ }
937
+ // Layer 5: Backends (from LB cookie decode, internal hostnames, SANs)
938
+ const backends = [];
939
+ // From F5 decoded IPs
940
+ if (lbCookies) {
941
+ for (const cookie of lbCookies) {
942
+ const valueMatch = cookie.match(/BIGipServer.*?=(\d+)\.(\d+)\./);
943
+ if (valueMatch) {
944
+ const encodedIp = parseInt(valueMatch[1], 10);
945
+ const encodedPort = parseInt(valueMatch[2], 10);
946
+ const ip = [
947
+ encodedIp & 0xff,
948
+ (encodedIp >> 8) & 0xff,
949
+ (encodedIp >> 16) & 0xff,
950
+ (encodedIp >> 24) & 0xff,
951
+ ].join(".");
952
+ const port = ((encodedPort & 0xff) << 8) | ((encodedPort >> 8) & 0xff);
953
+ backends.push({
954
+ source: "F5 BIG-IP cookie",
955
+ ip,
956
+ port,
957
+ note: "Backend server IP decoded from F5 persistence cookie",
958
+ });
959
+ }
960
+ }
961
+ }
962
+ // From internal hostnames
963
+ if (internalHostnames && internalHostnames.length > 0) {
964
+ for (const hostname of internalHostnames) {
965
+ backends.push({
966
+ source: "internal_hostname",
967
+ hostname,
968
+ note: "Internal hostname discovered via headers, error pages, or certificate SANs",
969
+ });
970
+ }
971
+ }
972
+ if (backends.length > 0) {
973
+ const backendLayer = (cdnProvider ? 4 : 3) + (lbDetected ? 1 : 0);
974
+ layers.push({
975
+ layer: backendLayer,
976
+ name: "Backend Servers",
977
+ type: "backends",
978
+ details: {
979
+ count: backends.length,
980
+ servers: backends,
981
+ },
982
+ });
983
+ }
984
+ // Certificate SAN analysis
985
+ const sanAnalysis = {};
986
+ if (certSans && certSans.length > 0) {
987
+ const wildcards = certSans.filter((s) => s.startsWith("*."));
988
+ const specifics = certSans.filter((s) => !s.startsWith("*."));
989
+ const uniqueDomains = [...new Set(certSans.map((s) => {
990
+ const parts = s.replace("*.", "").split(".");
991
+ return parts.slice(-2).join(".");
992
+ }))];
993
+ sanAnalysis.totalSans = certSans.length;
994
+ sanAnalysis.wildcards = wildcards;
995
+ sanAnalysis.specificHosts = specifics;
996
+ sanAnalysis.uniqueRootDomains = uniqueDomains;
997
+ if (uniqueDomains.length > 1) {
998
+ sanAnalysis.multiTenant = true;
999
+ sanAnalysis.note = "Multiple root domains on one certificate suggest shared hosting or multi-tenant infrastructure";
1000
+ }
1001
+ }
1002
+ // Build ASCII topology diagram
1003
+ const diagramLines = [];
1004
+ diagramLines.push("=== Infrastructure Topology ===");
1005
+ diagramLines.push("");
1006
+ for (const layer of layers) {
1007
+ const indent = " ".repeat(layer.layer - 1);
1008
+ const arrow = layer.layer > 1 ? `${indent}|` : "";
1009
+ if (arrow)
1010
+ diagramLines.push(arrow);
1011
+ diagramLines.push(`${indent}[${layer.name}] (${layer.type})`);
1012
+ }
1013
+ diagramLines.push("");
1014
+ const result = {
1015
+ topology: {
1016
+ layers,
1017
+ totalLayers: layers.length,
1018
+ hasCdn: !!cdnProvider,
1019
+ hasLoadBalancer: lbDetected,
1020
+ backendCount: backends.length,
1021
+ },
1022
+ sanAnalysis,
1023
+ diagram: diagramLines.join("\n"),
1024
+ recommendations: [
1025
+ ...(cdnProvider ? [`CDN detected (${cdnProvider}). JARM and TLS fingerprints reflect CDN, not origin.`] : []),
1026
+ ...(backends.length > 0 ? ["Backend IPs discovered. These may be directly accessible, bypassing WAF/CDN."] : []),
1027
+ ...(lbDetected ? ["Load balancer detected. Multiple requests may hit different backends."] : []),
1028
+ ...(certSans && certSans.length > 5
1029
+ ? ["Certificate has many SANs — review for related domains and potential attack surface expansion."]
1030
+ : []),
1031
+ ],
1032
+ };
1033
+ return json(result);
1034
+ },
1035
+ };
1036
+ // ─── Export All Correlation Tools ───
1037
+ export const correlationTools = [
1038
+ fpCorrelate,
1039
+ fpHoneypot,
1040
+ fpSpoofing,
1041
+ fpCompare,
1042
+ fpTopology,
1043
+ ];
1044
+ //# sourceMappingURL=index.js.map