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,2444 @@
1
+ import { z } from "zod";
2
+ import { createHash } from "node:crypto";
3
+ import * as cheerio from "cheerio";
4
+ import { json } from "../types/index.js";
5
+ import { RateLimiter } from "../utils/rate-limiter.js";
6
+ import { TTLCache } from "../utils/cache.js";
7
+ import { shodanFaviconHash } from "../utils/murmurhash3.js";
8
+ import { computeHeaderOrderHash, HEADER_ORDER_SIGNATURES, } from "../data/header-order.js";
9
+ // ─── Module-Level Setup ───
10
+ const limiter = new RateLimiter(200); // 200ms between requests
11
+ const cache = new TTLCache(300_000); // 5min cache
12
+ const USER_AGENT = "fingerprint-mcp/0.1";
13
+ const DEFAULT_TIMEOUT = 10_000; // 10 seconds
14
+ // ─── Known Header Order Patterns (inline for spoofing detection) ───
15
+ const KNOWN_SERVER_HEADER_ORDERS = {
16
+ apache: [
17
+ ["date", "server", "last-modified", "etag", "accept-ranges", "content-length", "content-type"],
18
+ ["date", "server", "content-type", "content-length", "connection"],
19
+ ["date", "server", "last-modified", "etag", "accept-ranges", "content-length", "vary", "content-type"],
20
+ ],
21
+ nginx: [
22
+ ["server", "date", "content-type", "content-length", "connection", "last-modified", "etag", "accept-ranges"],
23
+ ["server", "date", "content-type", "transfer-encoding", "connection"],
24
+ ["server", "date", "content-type", "content-length", "connection"],
25
+ ],
26
+ iis: [
27
+ ["content-type", "server", "x-powered-by", "date", "content-length"],
28
+ ["content-type", "server", "date", "content-length"],
29
+ ],
30
+ express: [
31
+ ["x-powered-by", "content-type", "content-length", "etag", "date", "connection"],
32
+ ["x-powered-by", "content-type", "date", "connection", "transfer-encoding"],
33
+ ],
34
+ litespeed: [
35
+ ["date", "content-type", "transfer-encoding", "connection", "x-powered-by"],
36
+ ["date", "server", "content-type", "content-length", "connection"],
37
+ ],
38
+ caddy: [
39
+ ["server", "content-type", "date"],
40
+ ["server", "content-type", "content-length", "date"],
41
+ ],
42
+ };
43
+ const ETAG_PATTERNS = [
44
+ {
45
+ server: "Apache",
46
+ regex: /^"([0-9a-f]+)-([0-9a-f]+)-([0-9a-f]+)"$/i,
47
+ description: "Apache inode-size-timestamp format (hex triplet)",
48
+ extract: (m) => ({
49
+ inode: parseInt(m[1], 16),
50
+ fileSize: parseInt(m[2], 16),
51
+ timestamp: parseInt(m[3], 16),
52
+ timestampDate: new Date(parseInt(m[3], 16) * 1000).toISOString(),
53
+ }),
54
+ },
55
+ {
56
+ server: "Nginx",
57
+ regex: /^"([0-9a-f]+)-([0-9a-f]+)"$/i,
58
+ description: "Nginx timestamp-size format (hex pair)",
59
+ extract: (m) => ({
60
+ timestamp: parseInt(m[1], 16),
61
+ timestampDate: new Date(parseInt(m[1], 16) * 1000).toISOString(),
62
+ fileSize: parseInt(m[2], 16),
63
+ }),
64
+ },
65
+ {
66
+ server: "IIS",
67
+ regex: /^"[0-9a-f]+:[0-9a-f]+"$/i,
68
+ description: "IIS opaque hash format (colon-separated)",
69
+ },
70
+ {
71
+ server: "LiteSpeed",
72
+ regex: /^"([0-9a-f]+)-([0-9a-f]+)-([0-9a-f]+)"$/i,
73
+ description: "LiteSpeed size-timestamp-gzip format",
74
+ extract: (m) => ({
75
+ fileSize: parseInt(m[1], 16),
76
+ timestamp: parseInt(m[2], 16),
77
+ gzipFlag: parseInt(m[3], 16),
78
+ }),
79
+ },
80
+ {
81
+ server: "Cloudflare",
82
+ regex: /^W\/"[0-9a-f]+-[0-9a-f]+"$/i,
83
+ description: "Cloudflare weak ETag (W/ prefix)",
84
+ },
85
+ {
86
+ server: "Cloudflare",
87
+ regex: /^W\//,
88
+ description: "Weak ETag (generic, possibly Cloudflare or CDN)",
89
+ },
90
+ ];
91
+ // ─── Session Cookie Patterns (inline) ───
92
+ const SESSION_COOKIE_MAP = {
93
+ PHPSESSID: "PHP",
94
+ JSESSIONID: "Java (Servlet/JSP)",
95
+ "ASP.NET_SessionId": "ASP.NET",
96
+ "connect.sid": "Express.js (Node.js)",
97
+ laravel_session: "Laravel (PHP)",
98
+ "rack.session": "Ruby on Rails / Sinatra",
99
+ _session_id: "Ruby on Rails",
100
+ csrftoken: "Django (Python)",
101
+ sessionid: "Django (Python)",
102
+ ci_session: "CodeIgniter (PHP)",
103
+ _gorilla_csrf: "Go (Gorilla toolkit)",
104
+ };
105
+ // ─── Known Services for Resource Hints ───
106
+ const KNOWN_SERVICES = {
107
+ "fonts.googleapis.com": "Google Fonts",
108
+ "fonts.gstatic.com": "Google Fonts (static)",
109
+ "www.googletagmanager.com": "Google Tag Manager",
110
+ "www.google-analytics.com": "Google Analytics",
111
+ "analytics.google.com": "Google Analytics",
112
+ "cdn.segment.com": "Segment Analytics",
113
+ "js.stripe.com": "Stripe Payments",
114
+ "m.stripe.com": "Stripe Mobile",
115
+ "api.stripe.com": "Stripe API",
116
+ "js.braintreegateway.com": "Braintree Payments",
117
+ "cdn.auth0.com": "Auth0 Authentication",
118
+ "browser.sentry-cdn.com": "Sentry Error Tracking",
119
+ "js.sentry-cdn.com": "Sentry Error Tracking",
120
+ "cdn.heapanalytics.com": "Heap Analytics",
121
+ "cdn.amplitude.com": "Amplitude Analytics",
122
+ "cdn.mxpnl.com": "Mixpanel Analytics",
123
+ "static.hotjar.com": "Hotjar Heatmaps",
124
+ "script.hotjar.com": "Hotjar Heatmaps",
125
+ "connect.facebook.net": "Facebook SDK",
126
+ "platform.twitter.com": "Twitter/X Platform",
127
+ "cdn.jsdelivr.net": "jsDelivr CDN",
128
+ "cdnjs.cloudflare.com": "Cloudflare cdnjs",
129
+ "unpkg.com": "unpkg CDN",
130
+ "ajax.googleapis.com": "Google Hosted Libraries",
131
+ "cdn.shopify.com": "Shopify CDN",
132
+ "static.cloudflareinsights.com": "Cloudflare Web Analytics",
133
+ "widget.intercom.io": "Intercom",
134
+ "js.intercomcdn.com": "Intercom CDN",
135
+ "fast.wistia.com": "Wistia Video",
136
+ "player.vimeo.com": "Vimeo Player",
137
+ "www.youtube.com": "YouTube",
138
+ "maps.googleapis.com": "Google Maps",
139
+ "api.mapbox.com": "Mapbox Maps",
140
+ "kit.fontawesome.com": "Font Awesome",
141
+ "use.typekit.net": "Adobe Fonts (Typekit)",
142
+ "snap.licdn.com": "LinkedIn Insight Tag",
143
+ "bat.bing.com": "Bing Ads UET",
144
+ "sc-static.net": "Snapchat Pixel",
145
+ "px.ads.linkedin.com": "LinkedIn Ads",
146
+ "cdn.cookielaw.org": "OneTrust Cookie Consent",
147
+ };
148
+ // ─── Shared Helpers ───
149
+ async function safeFetch(url, init) {
150
+ await limiter.acquire();
151
+ return fetch(url, {
152
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT),
153
+ headers: { "User-Agent": USER_AGENT },
154
+ ...init,
155
+ });
156
+ }
157
+ function getHeaderOrder(response) {
158
+ const order = [];
159
+ response.headers.forEach((_value, key) => {
160
+ order.push(key);
161
+ });
162
+ return order;
163
+ }
164
+ function matchServerFromOrder(headerOrder) {
165
+ const lowerOrder = headerOrder.map((h) => h.toLowerCase());
166
+ const hash = computeHeaderOrderHash(headerOrder);
167
+ // Check against known signatures from data file
168
+ for (const sig of HEADER_ORDER_SIGNATURES) {
169
+ if (sig.hash === hash) {
170
+ return { detected: sig.server, confidence: 0.9 };
171
+ }
172
+ }
173
+ // Fuzzy match against inline patterns
174
+ let bestMatch = null;
175
+ let bestScore = 0;
176
+ for (const [server, patterns] of Object.entries(KNOWN_SERVER_HEADER_ORDERS)) {
177
+ for (const pattern of patterns) {
178
+ // Count how many headers appear in the same relative order
179
+ let matchCount = 0;
180
+ let lastIdx = -1;
181
+ for (const hdr of pattern) {
182
+ const idx = lowerOrder.indexOf(hdr);
183
+ if (idx > lastIdx) {
184
+ matchCount++;
185
+ lastIdx = idx;
186
+ }
187
+ }
188
+ const score = matchCount / pattern.length;
189
+ if (score > bestScore) {
190
+ bestScore = score;
191
+ bestMatch = server;
192
+ }
193
+ }
194
+ }
195
+ if (bestScore >= 0.6) {
196
+ return { detected: bestMatch, confidence: bestScore };
197
+ }
198
+ return { detected: null, confidence: 0 };
199
+ }
200
+ function detectSpoofing(serverHeader, orderDetection) {
201
+ if (!serverHeader || !orderDetection) {
202
+ return { spoofed: false, details: "Insufficient data for spoofing detection" };
203
+ }
204
+ const serverLower = serverHeader.toLowerCase();
205
+ const orderLower = orderDetection.toLowerCase();
206
+ // Map server header values to canonical names
207
+ const serverCanonical = serverLower.includes("apache") ? "apache"
208
+ : serverLower.includes("nginx") ? "nginx"
209
+ : serverLower.includes("iis") || serverLower.includes("microsoft") ? "iis"
210
+ : serverLower.includes("express") ? "express"
211
+ : serverLower.includes("litespeed") ? "litespeed"
212
+ : serverLower.includes("caddy") ? "caddy"
213
+ : serverLower.includes("cloudflare") ? "cloudflare"
214
+ : null;
215
+ if (!serverCanonical) {
216
+ return { spoofed: false, details: "Server header value not in spoofing detection database" };
217
+ }
218
+ if (!orderLower.includes(serverCanonical)) {
219
+ return {
220
+ spoofed: true,
221
+ details: `Server header claims "${serverHeader}" but header ordering pattern matches "${orderDetection}"`,
222
+ };
223
+ }
224
+ return { spoofed: false, details: "Server header consistent with header ordering pattern" };
225
+ }
226
+ function parseCookieString(setCookieHeader) {
227
+ const parts = setCookieHeader.split(";").map((p) => p.trim());
228
+ const [nameValue, ...attrs] = parts;
229
+ const eqIdx = nameValue.indexOf("=");
230
+ const name = eqIdx > 0 ? nameValue.slice(0, eqIdx) : nameValue;
231
+ const value = eqIdx > 0 ? nameValue.slice(eqIdx + 1) : "";
232
+ const attributes = {};
233
+ for (const attr of attrs) {
234
+ const aEqIdx = attr.indexOf("=");
235
+ if (aEqIdx > 0) {
236
+ attributes[attr.slice(0, aEqIdx).toLowerCase()] = attr.slice(aEqIdx + 1);
237
+ }
238
+ else {
239
+ attributes[attr.toLowerCase()] = true;
240
+ }
241
+ }
242
+ return { name, value, attributes };
243
+ }
244
+ function decodeBigIPCookie(cookieValue) {
245
+ try {
246
+ // Simple format: NNNNN.NNNNN.0000
247
+ const simpleParts = cookieValue.split(".");
248
+ if (simpleParts.length >= 2) {
249
+ const ipEncoded = parseInt(simpleParts[0], 10);
250
+ const portEncoded = parseInt(simpleParts[1], 10);
251
+ if (!isNaN(ipEncoded) && !isNaN(portEncoded)) {
252
+ // Decode IP: convert to hex, reverse byte pairs, convert each pair to decimal
253
+ const ipHex = (ipEncoded >>> 0).toString(16).padStart(8, "0");
254
+ const ip = [
255
+ parseInt(ipHex.slice(6, 8), 16),
256
+ parseInt(ipHex.slice(4, 6), 16),
257
+ parseInt(ipHex.slice(2, 4), 16),
258
+ parseInt(ipHex.slice(0, 2), 16),
259
+ ].join(".");
260
+ // Decode port: convert to hex, reverse byte pair, convert to decimal
261
+ const portHex = (portEncoded >>> 0).toString(16).padStart(4, "0");
262
+ const port = parseInt(portHex.slice(2, 4) + portHex.slice(0, 2), 16);
263
+ return { ip, port };
264
+ }
265
+ }
266
+ // Extended format: rd<N>o00000000000000000000ffff<hex-ip>o<port>
267
+ const extMatch = cookieValue.match(/rd\d+o0+ffff([0-9a-f]{8})o(\d+)/i);
268
+ if (extMatch) {
269
+ const hexIp = extMatch[1];
270
+ const ip = [
271
+ parseInt(hexIp.slice(0, 2), 16),
272
+ parseInt(hexIp.slice(2, 4), 16),
273
+ parseInt(hexIp.slice(4, 6), 16),
274
+ parseInt(hexIp.slice(6, 8), 16),
275
+ ].join(".");
276
+ const port = parseInt(extMatch[2], 10);
277
+ return { ip, port };
278
+ }
279
+ return null;
280
+ }
281
+ catch {
282
+ return null;
283
+ }
284
+ }
285
+ function decodeNetScalerCookie(cookieName, cookieValue) {
286
+ // Cookie name after NSC_ is the vserver name with char substitution
287
+ const encodedName = cookieName.replace(/^NSC_/, "");
288
+ // Reverse the character mapping (lowercase letters shifted by a fixed offset)
289
+ const vserverName = encodedName
290
+ .split("")
291
+ .map((c) => {
292
+ const code = c.charCodeAt(0);
293
+ if (code >= 97 && code <= 122) {
294
+ // a-z: reverse rotate
295
+ return String.fromCharCode(((code - 97 + 13) % 26) + 97);
296
+ }
297
+ if (code >= 65 && code <= 90) {
298
+ return String.fromCharCode(((code - 65 + 13) % 26) + 65);
299
+ }
300
+ return c;
301
+ })
302
+ .join("");
303
+ // Cookie value: hex-encoded, first 8 hex = XOR-decoded VIP, next 4 = XOR-decoded port
304
+ let ip = null;
305
+ let port = null;
306
+ try {
307
+ const hexMatch = cookieValue.match(/^([0-9a-f]{8})([0-9a-f]{4})/i);
308
+ if (hexMatch) {
309
+ const ipEncoded = parseInt(hexMatch[1], 16);
310
+ const portEncoded = parseInt(hexMatch[2], 16);
311
+ // XOR with 0x03030303 for IP
312
+ const ipDecoded = ipEncoded ^ 0x03030303;
313
+ ip = [
314
+ (ipDecoded >>> 24) & 0xff,
315
+ (ipDecoded >>> 16) & 0xff,
316
+ (ipDecoded >>> 8) & 0xff,
317
+ ipDecoded & 0xff,
318
+ ].join(".");
319
+ // XOR with 0x0303 for port
320
+ port = portEncoded ^ 0x0303;
321
+ }
322
+ }
323
+ catch {
324
+ // Decoding failed — return what we have
325
+ }
326
+ return { vserverName, ip, port };
327
+ }
328
+ // ──────────────────────────────────────────────────────────────────────────────
329
+ // Tool 1: http_headers
330
+ // ──────────────────────────────────────────────────────────────────────────────
331
+ const httpHeaders = {
332
+ name: "http_headers",
333
+ description: "HTTP header ordering fingerprint. Sends a GET request, captures response headers in order, " +
334
+ "compares ordering against known server patterns (Apache, Nginx, IIS, Express, LiteSpeed, Caddy). " +
335
+ "Detects spoofing when Server header claims one server but header ordering follows another pattern. " +
336
+ "Computes header order hash for correlation.",
337
+ schema: {
338
+ url: z.string().url().describe("Target URL to fingerprint via header ordering"),
339
+ },
340
+ async execute(args) {
341
+ const url = args.url;
342
+ const cacheKey = `http_headers:${url}`;
343
+ const cached = cache.get(cacheKey);
344
+ if (cached)
345
+ return json(cached);
346
+ try {
347
+ const res = await safeFetch(url, { redirect: "follow" });
348
+ const headerOrder = getHeaderOrder(res);
349
+ const hash = computeHeaderOrderHash(headerOrder);
350
+ const serverHeader = res.headers.get("server");
351
+ const orderMatch = matchServerFromOrder(headerOrder);
352
+ const spoofCheck = detectSpoofing(serverHeader, orderMatch.detected);
353
+ const result = {
354
+ url,
355
+ status: res.status,
356
+ serverHeader,
357
+ headerOrder,
358
+ headerOrderHash: hash,
359
+ headerCount: headerOrder.length,
360
+ detection: {
361
+ serverFromOrder: orderMatch.detected,
362
+ confidence: orderMatch.confidence,
363
+ },
364
+ spoofing: spoofCheck,
365
+ rawHeaders: Object.fromEntries(headerOrder.map((h) => [h, res.headers.get(h)])),
366
+ };
367
+ cache.set(cacheKey, result);
368
+ return json(result);
369
+ }
370
+ catch (err) {
371
+ return json({ url, error: err.message });
372
+ }
373
+ },
374
+ };
375
+ // ──────────────────────────────────────────────────────────────────────────────
376
+ // Tool 2: http_multi_fingerprint
377
+ // ──────────────────────────────────────────────────────────────────────────────
378
+ const httpMultiFingerprint = {
379
+ name: "http_multi_fingerprint",
380
+ description: "Multi-request header ordering comparison. Sends GET /, GET /nonexistent404, POST /, HEAD / and compares " +
381
+ "header ordering across request types. Ordering differences between methods provide additional fingerprint " +
382
+ "signal for server identification.",
383
+ schema: {
384
+ url: z.string().url().describe("Target URL for multi-method header fingerprinting"),
385
+ },
386
+ async execute(args) {
387
+ const url = args.url;
388
+ const cacheKey = `http_multi_fingerprint:${url}`;
389
+ const cached = cache.get(cacheKey);
390
+ if (cached)
391
+ return json(cached);
392
+ const methods = [
393
+ { label: "GET /", init: { method: "GET" }, path: "" },
394
+ {
395
+ label: "GET /nonexistent404",
396
+ init: { method: "GET" },
397
+ path: "/nonexistent-path-fingerprint-mcp-404",
398
+ },
399
+ { label: "POST /", init: { method: "POST", body: "" }, path: "" },
400
+ { label: "HEAD /", init: { method: "HEAD" }, path: "" },
401
+ ];
402
+ const results = {};
403
+ const baseUrl = new URL(url);
404
+ for (const m of methods) {
405
+ try {
406
+ const targetUrl = new URL(m.path, baseUrl).toString();
407
+ const res = await safeFetch(targetUrl, {
408
+ ...m.init,
409
+ redirect: "follow",
410
+ });
411
+ const headerOrder = getHeaderOrder(res);
412
+ results[m.label] = {
413
+ status: res.status,
414
+ headerOrder,
415
+ hash: computeHeaderOrderHash(headerOrder),
416
+ serverHeader: res.headers.get("server"),
417
+ };
418
+ }
419
+ catch (err) {
420
+ results[m.label] = {
421
+ status: 0,
422
+ headerOrder: [],
423
+ hash: "",
424
+ serverHeader: null,
425
+ };
426
+ }
427
+ }
428
+ // Analyze ordering differences
429
+ const hashes = Object.values(results).map((r) => r.hash).filter(Boolean);
430
+ const uniqueHashes = [...new Set(hashes)];
431
+ const orderingConsistent = uniqueHashes.length <= 1;
432
+ // Detect server from the GET / response
433
+ const getResult = results["GET /"];
434
+ const detection = getResult
435
+ ? matchServerFromOrder(getResult.headerOrder)
436
+ : { detected: null, confidence: 0 };
437
+ const result = {
438
+ url,
439
+ requests: results,
440
+ analysis: {
441
+ uniqueOrderHashes: uniqueHashes.length,
442
+ orderingConsistent,
443
+ serverDetection: detection.detected,
444
+ confidence: detection.confidence,
445
+ insight: orderingConsistent
446
+ ? "Header ordering is consistent across methods — typical for single-server deployments"
447
+ : "Header ordering varies across methods — may indicate reverse proxy, load balancer, or WAF in front of origin",
448
+ },
449
+ };
450
+ cache.set(cacheKey, result);
451
+ return json(result);
452
+ },
453
+ };
454
+ // ──────────────────────────────────────────────────────────────────────────────
455
+ // Tool 3: http_favicon
456
+ // ──────────────────────────────────────────────────────────────────────────────
457
+ const httpFavicon = {
458
+ name: "http_favicon",
459
+ description: "Favicon detection and hashing. Checks 4 sources: /favicon.ico, HTML <link rel=\"icon\">, " +
460
+ "/apple-touch-icon.png, /apple-touch-icon-precomposed.png. Calculates Shodan MurmurHash3, MD5, " +
461
+ "and SHA256. Matches against known application favicon hashes (Jenkins, Grafana, Kibana, etc.).",
462
+ schema: {
463
+ url: z.string().url().describe("Target URL for favicon detection and hash fingerprinting"),
464
+ },
465
+ async execute(args) {
466
+ const url = args.url;
467
+ const cacheKey = `http_favicon:${url}`;
468
+ const cached = cache.get(cacheKey);
469
+ if (cached)
470
+ return json(cached);
471
+ const baseUrl = new URL(url);
472
+ const faviconSources = [];
473
+ // Helper to fetch and hash a favicon URL
474
+ async function fetchAndHash(faviconUrl, source) {
475
+ try {
476
+ const res = await safeFetch(faviconUrl, { redirect: "follow" });
477
+ if (!res.ok) {
478
+ faviconSources.push({ source, url: faviconUrl, error: `HTTP ${res.status}` });
479
+ return;
480
+ }
481
+ const contentType = res.headers.get("content-type") || "";
482
+ // Skip HTML responses (error pages returned as 200)
483
+ if (contentType.includes("text/html") && source !== "html-link") {
484
+ faviconSources.push({ source, url: faviconUrl, error: "Returned HTML instead of icon" });
485
+ return;
486
+ }
487
+ const buffer = Buffer.from(await res.arrayBuffer());
488
+ if (buffer.length === 0) {
489
+ faviconSources.push({ source, url: faviconUrl, error: "Empty response" });
490
+ return;
491
+ }
492
+ const mmh3 = shodanFaviconHash(buffer);
493
+ const md5 = createHash("md5").update(buffer).digest("hex");
494
+ const sha256 = createHash("sha256").update(buffer).digest("hex");
495
+ // Match against known favicon hashes
496
+ const { FAVICON_HASHES } = await import("../data/favicon-hashes.js");
497
+ const knownMatch = FAVICON_HASHES.find((f) => f.hash === mmh3);
498
+ faviconSources.push({
499
+ source,
500
+ url: faviconUrl,
501
+ hash: mmh3,
502
+ md5,
503
+ sha256,
504
+ size: buffer.length,
505
+ contentType,
506
+ match: knownMatch?.name ?? undefined,
507
+ });
508
+ }
509
+ catch (err) {
510
+ faviconSources.push({ source, url: faviconUrl, error: err.message });
511
+ }
512
+ }
513
+ // Source 1: /favicon.ico
514
+ await fetchAndHash(new URL("/favicon.ico", baseUrl).toString(), "favicon.ico");
515
+ // Source 2: HTML <link rel="icon">
516
+ try {
517
+ const htmlRes = await safeFetch(url, { redirect: "follow" });
518
+ if (htmlRes.ok) {
519
+ const html = await htmlRes.text();
520
+ const $ = cheerio.load(html);
521
+ const iconLink = $('link[rel="icon"], link[rel="shortcut icon"]').first();
522
+ if (iconLink.length) {
523
+ const href = iconLink.attr("href");
524
+ if (href) {
525
+ const iconUrl = new URL(href, url).toString();
526
+ await fetchAndHash(iconUrl, "html-link");
527
+ }
528
+ }
529
+ }
530
+ }
531
+ catch {
532
+ // HTML parsing failed — skip
533
+ }
534
+ // Source 3: /apple-touch-icon.png
535
+ await fetchAndHash(new URL("/apple-touch-icon.png", baseUrl).toString(), "apple-touch-icon");
536
+ // Source 4: /apple-touch-icon-precomposed.png
537
+ await fetchAndHash(new URL("/apple-touch-icon-precomposed.png", baseUrl).toString(), "apple-touch-icon-precomposed");
538
+ const foundIcons = faviconSources.filter((f) => f.hash !== undefined);
539
+ const matches = faviconSources.filter((f) => f.match);
540
+ const result = {
541
+ url,
542
+ sources: faviconSources,
543
+ summary: {
544
+ iconsFound: foundIcons.length,
545
+ knownApplicationMatches: matches.map((m) => ({
546
+ source: m.source,
547
+ application: m.match,
548
+ shodanHash: m.hash,
549
+ })),
550
+ shodanSearchQuery: foundIcons.length > 0
551
+ ? `http.favicon.hash:${foundIcons[0].hash}`
552
+ : null,
553
+ },
554
+ };
555
+ cache.set(cacheKey, result);
556
+ return json(result);
557
+ },
558
+ };
559
+ // ──────────────────────────────────────────────────────────────────────────────
560
+ // Tool 4: http_etag
561
+ // ──────────────────────────────────────────────────────────────────────────────
562
+ const httpEtag = {
563
+ name: "http_etag",
564
+ description: "ETag format analysis for server fingerprinting. Detects server type from ETag format patterns: " +
565
+ "Apache (inode-size-timestamp hex), Nginx (timestamp-size hex), IIS (opaque), LiteSpeed (size-timestamp-gzip), " +
566
+ "Cloudflare (weak ETag W/). Extracts Apache inode number, file size, and timestamp from ETag values.",
567
+ schema: {
568
+ url: z.string().url().describe("Target URL for ETag format analysis"),
569
+ },
570
+ async execute(args) {
571
+ const url = args.url;
572
+ const cacheKey = `http_etag:${url}`;
573
+ const cached = cache.get(cacheKey);
574
+ if (cached)
575
+ return json(cached);
576
+ try {
577
+ const res = await safeFetch(url, { redirect: "follow" });
578
+ const etag = res.headers.get("etag");
579
+ if (!etag) {
580
+ const result = {
581
+ url,
582
+ etag: null,
583
+ serverDetected: null,
584
+ analysis: "No ETag header present — server may disable ETags or use different caching strategy",
585
+ };
586
+ cache.set(cacheKey, result);
587
+ return json(result);
588
+ }
589
+ let serverDetected = null;
590
+ let formatDescription = null;
591
+ let extractedData = null;
592
+ const isWeak = etag.startsWith("W/");
593
+ for (const pattern of ETAG_PATTERNS) {
594
+ const match = etag.match(pattern.regex);
595
+ if (match) {
596
+ serverDetected = pattern.server;
597
+ formatDescription = pattern.description;
598
+ if (pattern.extract) {
599
+ extractedData = pattern.extract(match);
600
+ }
601
+ break;
602
+ }
603
+ }
604
+ // Check for Apache inode leak specifically
605
+ let inodeLeak = false;
606
+ if (serverDetected === "Apache" && extractedData && "inode" in extractedData) {
607
+ inodeLeak = true;
608
+ }
609
+ const result = {
610
+ url,
611
+ etag,
612
+ isWeakETag: isWeak,
613
+ serverDetected,
614
+ formatDescription,
615
+ extractedData,
616
+ securityFindings: {
617
+ inodeLeak,
618
+ inodeLeakDetails: inodeLeak
619
+ ? `Apache inode ${extractedData?.inode} leaked — may aid in identifying specific files or filesystem layout`
620
+ : null,
621
+ },
622
+ serverHeader: res.headers.get("server"),
623
+ };
624
+ cache.set(cacheKey, result);
625
+ return json(result);
626
+ }
627
+ catch (err) {
628
+ return json({ url, error: err.message });
629
+ }
630
+ },
631
+ };
632
+ // ──────────────────────────────────────────────────────────────────────────────
633
+ // Tool 5: http_errors
634
+ // ──────────────────────────────────────────────────────────────────────────────
635
+ const httpErrors = {
636
+ name: "http_errors",
637
+ description: "Error page fingerprinting. Triggers 400, 404, 405, 500 errors plus special triggers " +
638
+ "(null byte, oversized header, invalid method). Each error response is matched against " +
639
+ "framework signatures (Apache, Nginx, IIS, Express, Django, Laravel, Spring Boot, etc.). " +
640
+ "Reveals server software, version, and potentially debug information.",
641
+ schema: {
642
+ url: z.string().url().describe("Target URL for error page fingerprinting"),
643
+ },
644
+ async execute(args) {
645
+ const url = args.url;
646
+ const cacheKey = `http_errors:${url}`;
647
+ const cached = cache.get(cacheKey);
648
+ if (cached)
649
+ return json(cached);
650
+ const baseUrl = new URL(url);
651
+ const errorTests = [
652
+ {
653
+ name: "404 — Non-existent path",
654
+ expectedStatus: 404,
655
+ fetch: () => safeFetch(new URL("/nonexistent-path-fpmcp-" + Date.now(), baseUrl).toString(), { redirect: "follow" }),
656
+ },
657
+ {
658
+ name: "405 — Method Not Allowed (DELETE on root)",
659
+ expectedStatus: 405,
660
+ fetch: () => safeFetch(url, { method: "DELETE", redirect: "follow" }),
661
+ },
662
+ {
663
+ name: "400 — Null byte in URL",
664
+ expectedStatus: 400,
665
+ fetch: () => safeFetch(new URL("/test%00null", baseUrl).toString(), { redirect: "follow" }),
666
+ },
667
+ {
668
+ name: "400 — Oversized header",
669
+ expectedStatus: 400,
670
+ fetch: () => safeFetch(url, {
671
+ redirect: "follow",
672
+ headers: {
673
+ "User-Agent": USER_AGENT,
674
+ "X-Oversized-Test": "A".repeat(16384),
675
+ },
676
+ }),
677
+ },
678
+ {
679
+ name: "405 — Invalid method (FAKEVERB)",
680
+ expectedStatus: 405,
681
+ fetch: () => safeFetch(url, { method: "FAKEVERB", redirect: "follow" }),
682
+ },
683
+ ];
684
+ const { ERROR_SIGNATURES } = await import("../data/error-signatures.js");
685
+ const errorResults = [];
686
+ for (const test of errorTests) {
687
+ try {
688
+ const res = await test.fetch();
689
+ const body = await res.text();
690
+ const bodySnippet = body.slice(0, 2000);
691
+ const serverHeader = res.headers.get("server");
692
+ const matchedSignatures = [];
693
+ for (const sig of ERROR_SIGNATURES) {
694
+ const matchedPatterns = [];
695
+ // Check body patterns
696
+ if (sig.patterns.body) {
697
+ for (const pattern of sig.patterns.body) {
698
+ try {
699
+ if (new RegExp(pattern, "is").test(body)) {
700
+ matchedPatterns.push(`body: ${pattern}`);
701
+ }
702
+ }
703
+ catch {
704
+ // Invalid regex — skip
705
+ }
706
+ }
707
+ }
708
+ // Check header patterns
709
+ if (sig.patterns.headers) {
710
+ for (const [headerName, headerPattern] of Object.entries(sig.patterns.headers)) {
711
+ const headerValue = res.headers.get(headerName);
712
+ if (headerValue) {
713
+ try {
714
+ if (new RegExp(headerPattern, "i").test(headerValue)) {
715
+ matchedPatterns.push(`header ${headerName}: ${headerPattern}`);
716
+ }
717
+ }
718
+ catch {
719
+ // Invalid regex — skip
720
+ }
721
+ }
722
+ }
723
+ }
724
+ if (matchedPatterns.length > 0) {
725
+ matchedSignatures.push({
726
+ server: sig.server,
727
+ confidence: sig.confidence,
728
+ matchedPatterns,
729
+ });
730
+ }
731
+ }
732
+ errorResults.push({
733
+ name: test.name,
734
+ status: res.status,
735
+ serverHeader,
736
+ bodySnippet,
737
+ matchedSignatures,
738
+ });
739
+ }
740
+ catch (err) {
741
+ errorResults.push({
742
+ name: test.name,
743
+ status: 0,
744
+ serverHeader: null,
745
+ bodySnippet: "",
746
+ matchedSignatures: [],
747
+ error: err.message,
748
+ });
749
+ }
750
+ }
751
+ // Aggregate the most confident detections
752
+ const allDetections = errorResults.flatMap((r) => r.matchedSignatures);
753
+ const serverCounts = {};
754
+ for (const d of allDetections) {
755
+ if (!serverCounts[d.server]) {
756
+ serverCounts[d.server] = { count: 0, maxConfidence: 0 };
757
+ }
758
+ serverCounts[d.server].count++;
759
+ serverCounts[d.server].maxConfidence = Math.max(serverCounts[d.server].maxConfidence, d.confidence);
760
+ }
761
+ const topDetection = Object.entries(serverCounts)
762
+ .sort((a, b) => b[1].maxConfidence - a[1].maxConfidence || b[1].count - a[1].count)
763
+ .map(([server, info]) => ({ server, ...info }));
764
+ const result = {
765
+ url,
766
+ errorTests: errorResults,
767
+ summary: {
768
+ detectedServers: topDetection,
769
+ primaryDetection: topDetection[0]?.server ?? null,
770
+ debugInfoExposed: errorResults.some((r) => r.bodySnippet.includes("Traceback") ||
771
+ r.bodySnippet.includes("stack trace") ||
772
+ r.bodySnippet.includes("DEBUG = True") ||
773
+ r.bodySnippet.includes("Exception")),
774
+ },
775
+ };
776
+ cache.set(cacheKey, result);
777
+ return json(result);
778
+ },
779
+ };
780
+ // ──────────────────────────────────────────────────────────────────────────────
781
+ // Tool 6: http_cookies
782
+ // ──────────────────────────────────────────────────────────────────────────────
783
+ const httpCookies = {
784
+ name: "http_cookies",
785
+ description: "Cookie analysis for technology and infrastructure detection. Identifies session technology " +
786
+ "(PHPSESSID, JSESSIONID, connect.sid, etc.), decodes infrastructure cookies (F5 BIG-IP " +
787
+ "backend IP:port, Citrix NetScaler VIP:port), and assesses cookie security (HttpOnly, Secure, " +
788
+ "SameSite, domain scope, expiry).",
789
+ schema: {
790
+ url: z.string().url().describe("Target URL for cookie analysis"),
791
+ },
792
+ async execute(args) {
793
+ const url = args.url;
794
+ const cacheKey = `http_cookies:${url}`;
795
+ const cached = cache.get(cacheKey);
796
+ if (cached)
797
+ return json(cached);
798
+ try {
799
+ const res = await safeFetch(url, { redirect: "manual" });
800
+ // Collect all Set-Cookie headers
801
+ const setCookieHeaders = [];
802
+ res.headers.forEach((value, key) => {
803
+ if (key.toLowerCase() === "set-cookie") {
804
+ setCookieHeaders.push(value);
805
+ }
806
+ });
807
+ // Some environments merge set-cookie — also try splitting on comma+space pattern
808
+ // But first try the raw headers approach
809
+ if (setCookieHeaders.length === 0) {
810
+ const raw = res.headers.get("set-cookie");
811
+ if (raw) {
812
+ // Set-Cookie headers can contain commas in dates, so splitting is complex
813
+ // Just treat the whole thing as one cookie for now
814
+ setCookieHeaders.push(raw);
815
+ }
816
+ }
817
+ if (setCookieHeaders.length === 0) {
818
+ const result = {
819
+ url,
820
+ cookies: [],
821
+ technologies: [],
822
+ infrastructure: [],
823
+ securityAssessment: { findings: ["No cookies set by server"] },
824
+ };
825
+ cache.set(cacheKey, result);
826
+ return json(result);
827
+ }
828
+ const cookies = setCookieHeaders.map(parseCookieString);
829
+ // Session technology detection
830
+ const technologies = [];
831
+ for (const cookie of cookies) {
832
+ for (const [pattern, tech] of Object.entries(SESSION_COOKIE_MAP)) {
833
+ if (cookie.name === pattern || cookie.name.startsWith(pattern)) {
834
+ technologies.push({ cookieName: cookie.name, technology: tech });
835
+ break;
836
+ }
837
+ }
838
+ // WordPress-specific pattern checks
839
+ if (cookie.name.startsWith("wordpress_logged_in_")) {
840
+ technologies.push({ cookieName: cookie.name, technology: "WordPress" });
841
+ }
842
+ if (cookie.name.startsWith("wp-settings-")) {
843
+ technologies.push({ cookieName: cookie.name, technology: "WordPress" });
844
+ }
845
+ // Drupal pattern
846
+ if (/^SESS[0-9a-f]{32}$/.test(cookie.name)) {
847
+ technologies.push({ cookieName: cookie.name, technology: "Drupal" });
848
+ }
849
+ }
850
+ // Infrastructure decode
851
+ const infrastructure = [];
852
+ for (const cookie of cookies) {
853
+ // F5 BIG-IP
854
+ if (cookie.name.startsWith("BIGipServer")) {
855
+ const decoded = decodeBigIPCookie(cookie.value);
856
+ infrastructure.push({
857
+ cookieName: cookie.name,
858
+ technology: "F5 BIG-IP",
859
+ decoded: decoded
860
+ ? { backendIP: decoded.ip, backendPort: decoded.port, poolName: cookie.name.replace("BIGipServer", "") }
861
+ : { raw: cookie.value, decodeFailed: true },
862
+ });
863
+ }
864
+ // Citrix NetScaler
865
+ if (cookie.name.startsWith("NSC_")) {
866
+ const decoded = decodeNetScalerCookie(cookie.name, cookie.value);
867
+ infrastructure.push({
868
+ cookieName: cookie.name,
869
+ technology: "Citrix NetScaler",
870
+ decoded,
871
+ });
872
+ }
873
+ // AWS ALB
874
+ if (cookie.name === "AWSALB" || cookie.name === "AWSALBCORS") {
875
+ infrastructure.push({
876
+ cookieName: cookie.name,
877
+ technology: "AWS ALB",
878
+ decoded: { note: "ALB routing cookie — base64-encoded target group info" },
879
+ });
880
+ }
881
+ // Google Cloud LB
882
+ if (cookie.name === "GCLB") {
883
+ infrastructure.push({
884
+ cookieName: cookie.name,
885
+ technology: "Google Cloud Load Balancer",
886
+ decoded: { note: "GCLB backend routing cookie" },
887
+ });
888
+ }
889
+ // HAProxy
890
+ if (cookie.name === "ROUTEID" || cookie.name === "SERVERID") {
891
+ infrastructure.push({
892
+ cookieName: cookie.name,
893
+ technology: "HAProxy",
894
+ decoded: { backendServer: cookie.value },
895
+ });
896
+ }
897
+ // Cloudflare
898
+ if (cookie.name === "__cf_bm" ||
899
+ cookie.name === "_cfuvid" ||
900
+ cookie.name === "cf_clearance") {
901
+ infrastructure.push({
902
+ cookieName: cookie.name,
903
+ technology: "Cloudflare",
904
+ decoded: { note: "Cloudflare bot management / security cookie" },
905
+ });
906
+ }
907
+ }
908
+ // Security assessment
909
+ const securityFindings = [];
910
+ for (const cookie of cookies) {
911
+ const prefix = `Cookie "${cookie.name}":`;
912
+ if (!cookie.attributes.httponly) {
913
+ securityFindings.push(`${prefix} missing HttpOnly flag — accessible via JavaScript`);
914
+ }
915
+ if (!cookie.attributes.secure) {
916
+ securityFindings.push(`${prefix} missing Secure flag — may be sent over HTTP`);
917
+ }
918
+ if (!cookie.attributes.samesite) {
919
+ securityFindings.push(`${prefix} missing SameSite attribute — defaults to Lax in modern browsers`);
920
+ }
921
+ if (cookie.attributes.domain) {
922
+ const domain = cookie.attributes.domain;
923
+ if (domain.startsWith(".")) {
924
+ securityFindings.push(`${prefix} wide domain scope (${domain}) — shared across subdomains`);
925
+ }
926
+ }
927
+ if (cookie.attributes.expires) {
928
+ const expiryDate = new Date(cookie.attributes.expires);
929
+ const daysUntilExpiry = Math.floor((expiryDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
930
+ if (daysUntilExpiry > 365) {
931
+ securityFindings.push(`${prefix} long expiry (${daysUntilExpiry} days) — consider shorter session lifetime`);
932
+ }
933
+ }
934
+ }
935
+ const result = {
936
+ url,
937
+ cookies: cookies.map((c) => ({
938
+ name: c.name,
939
+ valueLength: c.value.length,
940
+ attributes: c.attributes,
941
+ })),
942
+ technologies,
943
+ infrastructure,
944
+ securityAssessment: {
945
+ totalCookies: cookies.length,
946
+ findings: securityFindings.length > 0 ? securityFindings : ["All cookies have proper security attributes"],
947
+ },
948
+ };
949
+ cache.set(cacheKey, result);
950
+ return json(result);
951
+ }
952
+ catch (err) {
953
+ return json({ url, error: err.message });
954
+ }
955
+ },
956
+ };
957
+ // ──────────────────────────────────────────────────────────────────────────────
958
+ // Tool 7: http_methods
959
+ // ──────────────────────────────────────────────────────────────────────────────
960
+ const httpMethods = {
961
+ name: "http_methods",
962
+ description: "HTTP method probing. Tests GET, POST, PUT, DELETE, PATCH, OPTIONS, TRACE, CONNECT, PROPFIND, " +
963
+ "MKCOL and method override headers (X-HTTP-Method-Override). Identifies TRACE 200 (XST risk), " +
964
+ "PROPFIND 207 (WebDAV), verb tampering (FAKEVERB), and method override support.",
965
+ schema: {
966
+ url: z.string().url().describe("Target URL for HTTP method probing"),
967
+ },
968
+ async execute(args) {
969
+ const url = args.url;
970
+ const cacheKey = `http_methods:${url}`;
971
+ const cached = cache.get(cacheKey);
972
+ if (cached)
973
+ return json(cached);
974
+ const methodsToTest = [
975
+ "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS",
976
+ "TRACE", "PROPFIND", "MKCOL", "FAKEVERB",
977
+ ];
978
+ const methodResults = {};
979
+ for (const method of methodsToTest) {
980
+ try {
981
+ const res = await safeFetch(url, {
982
+ method: method,
983
+ redirect: "follow",
984
+ });
985
+ const status = res.status;
986
+ const allowed = status < 400 || status === 401 || status === 403;
987
+ let note;
988
+ if (method === "TRACE" && status === 200) {
989
+ note = "SECURITY: TRACE enabled — Cross-Site Tracing (XST) risk";
990
+ }
991
+ if (method === "PROPFIND" && status === 207) {
992
+ note = "WebDAV enabled (207 Multi-Status response)";
993
+ }
994
+ if (method === "FAKEVERB" && status < 400) {
995
+ note = "Verb tampering possible — server accepts unknown HTTP methods";
996
+ }
997
+ if (method === "OPTIONS") {
998
+ const allow = res.headers.get("allow");
999
+ if (allow) {
1000
+ note = `Allowed methods: ${allow}`;
1001
+ }
1002
+ }
1003
+ // Consume body to avoid connection issues
1004
+ await res.text().catch(() => { });
1005
+ methodResults[method] = { status, allowed, note };
1006
+ }
1007
+ catch (err) {
1008
+ methodResults[method] = { status: 0, allowed: false, error: err.message };
1009
+ }
1010
+ }
1011
+ // Test method override headers
1012
+ const overrideResults = {};
1013
+ const overrideHeaders = [
1014
+ "X-HTTP-Method-Override",
1015
+ "X-HTTP-Method",
1016
+ "X-Method-Override",
1017
+ ];
1018
+ for (const header of overrideHeaders) {
1019
+ try {
1020
+ const res = await safeFetch(url, {
1021
+ method: "POST",
1022
+ headers: {
1023
+ "User-Agent": USER_AGENT,
1024
+ [header]: "DELETE",
1025
+ "Content-Length": "0",
1026
+ },
1027
+ redirect: "follow",
1028
+ });
1029
+ // Compare against normal POST status
1030
+ const normalPostStatus = methodResults["POST"]?.status ?? 0;
1031
+ const overrideWorked = res.status !== normalPostStatus && res.status < 400;
1032
+ await res.text().catch(() => { });
1033
+ overrideResults[header] = {
1034
+ status: res.status,
1035
+ overrideWorked,
1036
+ };
1037
+ }
1038
+ catch (err) {
1039
+ overrideResults[header] = { status: 0, overrideWorked: false };
1040
+ }
1041
+ }
1042
+ const findings = [];
1043
+ if (methodResults["TRACE"]?.status === 200) {
1044
+ findings.push("TRACE method enabled — XST (Cross-Site Tracing) vulnerability risk");
1045
+ }
1046
+ if (methodResults["PROPFIND"]?.status === 207) {
1047
+ findings.push("WebDAV enabled — PROPFIND returns 207 Multi-Status");
1048
+ }
1049
+ if (methodResults["PUT"]?.allowed) {
1050
+ findings.push("PUT method allowed — check for arbitrary file upload");
1051
+ }
1052
+ if (methodResults["DELETE"]?.allowed) {
1053
+ findings.push("DELETE method allowed — check for unauthorized deletion");
1054
+ }
1055
+ if (methodResults["FAKEVERB"]?.allowed) {
1056
+ findings.push("Server accepts unknown HTTP verbs — potential verb tampering");
1057
+ }
1058
+ for (const [header, r] of Object.entries(overrideResults)) {
1059
+ if (r.overrideWorked) {
1060
+ findings.push(`Method override via ${header} header is supported`);
1061
+ }
1062
+ }
1063
+ const result = {
1064
+ url,
1065
+ methods: methodResults,
1066
+ methodOverride: overrideResults,
1067
+ securityFindings: findings.length > 0 ? findings : ["No concerning method configurations found"],
1068
+ allowedMethods: Object.entries(methodResults)
1069
+ .filter(([, r]) => r.allowed)
1070
+ .map(([m]) => m),
1071
+ };
1072
+ cache.set(cacheKey, result);
1073
+ return json(result);
1074
+ },
1075
+ };
1076
+ // ──────────────────────────────────────────────────────────────────────────────
1077
+ // Tool 8: http_cors
1078
+ // ──────────────────────────────────────────────────────────────────────────────
1079
+ const httpCors = {
1080
+ name: "http_cors",
1081
+ description: "CORS (Cross-Origin Resource Sharing) configuration analysis. Tests origin reflection " +
1082
+ "(evil.com), null origin, subdomain trust, prefix/suffix regex bypass. Extracts " +
1083
+ "Access-Control-Allow-Methods/Headers/Expose-Headers to map API surface and discover " +
1084
+ "internal header names.",
1085
+ schema: {
1086
+ url: z.string().url().describe("Target URL for CORS configuration analysis"),
1087
+ },
1088
+ async execute(args) {
1089
+ const url = args.url;
1090
+ const cacheKey = `http_cors:${url}`;
1091
+ const cached = cache.get(cacheKey);
1092
+ if (cached)
1093
+ return json(cached);
1094
+ const targetHost = new URL(url).hostname;
1095
+ const originTests = [
1096
+ {
1097
+ name: "Arbitrary origin",
1098
+ origin: "https://evil.com",
1099
+ expectReflect: false,
1100
+ risk: "HIGH: Server reflects arbitrary origins — full CORS bypass",
1101
+ },
1102
+ {
1103
+ name: "Null origin",
1104
+ origin: "null",
1105
+ expectReflect: false,
1106
+ risk: "HIGH: Null origin accepted — exploitable via sandboxed iframes, data: URIs",
1107
+ },
1108
+ {
1109
+ name: "Subdomain",
1110
+ origin: `https://test.${targetHost}`,
1111
+ expectReflect: false,
1112
+ risk: "MEDIUM: Subdomain trusted — subdomain takeover could lead to CORS bypass",
1113
+ },
1114
+ {
1115
+ name: "Prefix bypass",
1116
+ origin: `https://${targetHost}.evil.com`,
1117
+ expectReflect: false,
1118
+ risk: "HIGH: Origin prefix bypass — regex/string matching flaw",
1119
+ },
1120
+ {
1121
+ name: "Suffix bypass",
1122
+ origin: `https://evil${targetHost}`,
1123
+ expectReflect: false,
1124
+ risk: "HIGH: Origin suffix bypass — regex/string matching flaw",
1125
+ },
1126
+ {
1127
+ name: "Scheme mismatch (HTTP)",
1128
+ origin: `http://${targetHost}`,
1129
+ expectReflect: false,
1130
+ risk: "MEDIUM: HTTP origin accepted on HTTPS site — downgrade risk",
1131
+ },
1132
+ ];
1133
+ const corsResults = [];
1134
+ for (const test of originTests) {
1135
+ try {
1136
+ const res = await safeFetch(url, {
1137
+ method: "OPTIONS",
1138
+ headers: {
1139
+ "User-Agent": USER_AGENT,
1140
+ Origin: test.origin,
1141
+ "Access-Control-Request-Method": "GET",
1142
+ },
1143
+ redirect: "follow",
1144
+ });
1145
+ const acao = res.headers.get("access-control-allow-origin");
1146
+ const reflected = acao === test.origin || acao === "*";
1147
+ const allowCredentials = res.headers.get("access-control-allow-credentials") === "true";
1148
+ const allowMethods = res.headers.get("access-control-allow-methods");
1149
+ const allowHeaders = res.headers.get("access-control-allow-headers");
1150
+ const exposeHeaders = res.headers.get("access-control-expose-headers");
1151
+ await res.text().catch(() => { });
1152
+ corsResults.push({
1153
+ name: test.name,
1154
+ origin: test.origin,
1155
+ reflected,
1156
+ allowCredentials,
1157
+ allowMethods,
1158
+ allowHeaders,
1159
+ exposeHeaders,
1160
+ risk: reflected ? test.risk : null,
1161
+ });
1162
+ }
1163
+ catch (err) {
1164
+ corsResults.push({
1165
+ name: test.name,
1166
+ origin: test.origin,
1167
+ reflected: false,
1168
+ allowCredentials: false,
1169
+ allowMethods: null,
1170
+ allowHeaders: null,
1171
+ exposeHeaders: null,
1172
+ risk: null,
1173
+ error: err.message,
1174
+ });
1175
+ }
1176
+ }
1177
+ // Also do a simple GET with Origin to check non-preflight behavior
1178
+ let getReflection = null;
1179
+ try {
1180
+ const res = await safeFetch(url, {
1181
+ headers: {
1182
+ "User-Agent": USER_AGENT,
1183
+ Origin: "https://evil.com",
1184
+ },
1185
+ redirect: "follow",
1186
+ });
1187
+ const acao = res.headers.get("access-control-allow-origin");
1188
+ getReflection = {
1189
+ reflected: acao === "https://evil.com" || acao === "*",
1190
+ acao,
1191
+ allowCredentials: res.headers.get("access-control-allow-credentials") === "true",
1192
+ };
1193
+ await res.text().catch(() => { });
1194
+ }
1195
+ catch {
1196
+ // Skip
1197
+ }
1198
+ const vulnerabilities = corsResults.filter((r) => r.risk !== null);
1199
+ const wildcardWithCredentials = corsResults.some((r) => r.reflected && r.allowCredentials);
1200
+ // Extract API surface from allowed methods/headers
1201
+ const allMethods = new Set();
1202
+ const allHeaders = new Set();
1203
+ const allExposedHeaders = new Set();
1204
+ for (const r of corsResults) {
1205
+ if (r.allowMethods) {
1206
+ r.allowMethods.split(",").map((m) => m.trim()).forEach((m) => allMethods.add(m));
1207
+ }
1208
+ if (r.allowHeaders) {
1209
+ r.allowHeaders.split(",").map((h) => h.trim()).forEach((h) => allHeaders.add(h));
1210
+ }
1211
+ if (r.exposeHeaders) {
1212
+ r.exposeHeaders.split(",").map((h) => h.trim()).forEach((h) => allExposedHeaders.add(h));
1213
+ }
1214
+ }
1215
+ const result = {
1216
+ url,
1217
+ tests: corsResults,
1218
+ getReflection,
1219
+ apiSurface: {
1220
+ allowedMethods: [...allMethods],
1221
+ allowedHeaders: [...allHeaders],
1222
+ exposedHeaders: [...allExposedHeaders],
1223
+ },
1224
+ summary: {
1225
+ vulnerabilities: vulnerabilities.map((v) => v.risk),
1226
+ wildcardWithCredentials,
1227
+ wildcardWithCredentialsNote: wildcardWithCredentials
1228
+ ? "CRITICAL: Origin reflected with credentials — allows full cross-origin data theft"
1229
+ : null,
1230
+ corsConfigured: corsResults.some((r) => r.reflected || r.allowMethods !== null),
1231
+ },
1232
+ };
1233
+ cache.set(cacheKey, result);
1234
+ return json(result);
1235
+ },
1236
+ };
1237
+ // ──────────────────────────────────────────────────────────────────────────────
1238
+ // Tool 9: http_compression
1239
+ // ──────────────────────────────────────────────────────────────────────────────
1240
+ const httpCompression = {
1241
+ name: "http_compression",
1242
+ description: "Compression support probing. Tests gzip, deflate, br (Brotli), and zstd encoding support. " +
1243
+ "Intelligence: Brotli + modern stack = modern infrastructure. Only gzip = legacy. " +
1244
+ "No compression = embedded/IoT device.",
1245
+ schema: {
1246
+ url: z.string().url().describe("Target URL for compression support probing"),
1247
+ },
1248
+ async execute(args) {
1249
+ const url = args.url;
1250
+ const cacheKey = `http_compression:${url}`;
1251
+ const cached = cache.get(cacheKey);
1252
+ if (cached)
1253
+ return json(cached);
1254
+ const encodings = ["gzip", "deflate", "br", "zstd"];
1255
+ const compressionResults = {};
1256
+ // Also fetch without compression for baseline
1257
+ let baselineSize = null;
1258
+ try {
1259
+ const baseRes = await safeFetch(url, {
1260
+ headers: {
1261
+ "User-Agent": USER_AGENT,
1262
+ "Accept-Encoding": "identity",
1263
+ },
1264
+ redirect: "follow",
1265
+ });
1266
+ const baseBody = await baseRes.arrayBuffer();
1267
+ baselineSize = baseBody.byteLength;
1268
+ }
1269
+ catch {
1270
+ // Skip baseline
1271
+ }
1272
+ for (const encoding of encodings) {
1273
+ try {
1274
+ const res = await safeFetch(url, {
1275
+ headers: {
1276
+ "User-Agent": USER_AGENT,
1277
+ "Accept-Encoding": encoding,
1278
+ },
1279
+ redirect: "follow",
1280
+ });
1281
+ const contentEncoding = res.headers.get("content-encoding");
1282
+ const contentLength = res.headers.get("content-length");
1283
+ const vary = res.headers.get("vary");
1284
+ // Consume body
1285
+ await res.arrayBuffer().catch(() => { });
1286
+ compressionResults[encoding] = {
1287
+ supported: contentEncoding?.toLowerCase().includes(encoding) ?? false,
1288
+ contentEncoding,
1289
+ contentLength,
1290
+ vary,
1291
+ };
1292
+ }
1293
+ catch (err) {
1294
+ compressionResults[encoding] = {
1295
+ supported: false,
1296
+ contentEncoding: null,
1297
+ contentLength: null,
1298
+ vary: null,
1299
+ error: err.message,
1300
+ };
1301
+ }
1302
+ }
1303
+ const supportedEncodings = Object.entries(compressionResults)
1304
+ .filter(([, r]) => r.supported)
1305
+ .map(([enc]) => enc);
1306
+ let stackAssessment;
1307
+ if (supportedEncodings.includes("br") && supportedEncodings.includes("zstd")) {
1308
+ stackAssessment = "Cutting-edge stack — Brotli and Zstd support indicates modern infrastructure (likely CDN or recent server)";
1309
+ }
1310
+ else if (supportedEncodings.includes("br")) {
1311
+ stackAssessment = "Modern stack — Brotli support indicates recent server software or CDN";
1312
+ }
1313
+ else if (supportedEncodings.includes("gzip") && !supportedEncodings.includes("br")) {
1314
+ stackAssessment = "Legacy stack — only gzip compression, no Brotli support";
1315
+ }
1316
+ else if (supportedEncodings.length === 0) {
1317
+ stackAssessment = "No compression — may indicate embedded device, IoT, or minimal HTTP server";
1318
+ }
1319
+ else {
1320
+ stackAssessment = "Standard compression support";
1321
+ }
1322
+ const result = {
1323
+ url,
1324
+ baselineSizeBytes: baselineSize,
1325
+ encodings: compressionResults,
1326
+ supported: supportedEncodings,
1327
+ intelligence: {
1328
+ stackAssessment,
1329
+ compressionRatio: baselineSize && compressionResults.gzip?.contentLength
1330
+ ? (parseInt(compressionResults.gzip.contentLength, 10) / baselineSize).toFixed(3)
1331
+ : null,
1332
+ },
1333
+ };
1334
+ cache.set(cacheKey, result);
1335
+ return json(result);
1336
+ },
1337
+ };
1338
+ // ──────────────────────────────────────────────────────────────────────────────
1339
+ // Tool 10: http_cache
1340
+ // ──────────────────────────────────────────────────────────────────────────────
1341
+ const httpCache = {
1342
+ name: "http_cache",
1343
+ description: "Cache behavior analysis. Detects hit/miss via X-Cache, CF-Cache-Status headers. " +
1344
+ "Discovers cache key components by varying query string, headers, and cookies. " +
1345
+ "Tests cache poisoning potential via unkeyed headers.",
1346
+ schema: {
1347
+ url: z.string().url().describe("Target URL for cache behavior analysis"),
1348
+ },
1349
+ async execute(args) {
1350
+ const url = args.url;
1351
+ const cacheKey = `http_cache:${url}`;
1352
+ const cached = cache.get(cacheKey);
1353
+ if (cached)
1354
+ return json(cached);
1355
+ // First request — establish baseline
1356
+ let baseline = null;
1357
+ const cacheHeaderNames = [
1358
+ "x-cache", "x-cache-status", "cf-cache-status",
1359
+ "x-varnish", "x-fastly-request-id", "x-served-by",
1360
+ "x-cache-hits", "cache-control", "expires",
1361
+ "pragma", "age", "via",
1362
+ ];
1363
+ try {
1364
+ const res = await safeFetch(url, { redirect: "follow" });
1365
+ const headers = {};
1366
+ for (const h of cacheHeaderNames) {
1367
+ headers[h] = res.headers.get(h);
1368
+ }
1369
+ baseline = {
1370
+ status: res.status,
1371
+ cacheHeaders: headers,
1372
+ age: res.headers.get("age"),
1373
+ };
1374
+ await res.text().catch(() => { });
1375
+ }
1376
+ catch (err) {
1377
+ return json({ url, error: err.message });
1378
+ }
1379
+ // Second request — check if cache warms
1380
+ let secondRequest = null;
1381
+ try {
1382
+ const res2 = await safeFetch(url, { redirect: "follow" });
1383
+ secondRequest = {};
1384
+ for (const h of cacheHeaderNames) {
1385
+ secondRequest[h] = res2.headers.get(h);
1386
+ }
1387
+ await res2.text().catch(() => { });
1388
+ }
1389
+ catch {
1390
+ // Skip
1391
+ }
1392
+ // Cache key discovery: vary query string
1393
+ let queryStringKeyed = false;
1394
+ try {
1395
+ const bustUrl = new URL(url);
1396
+ bustUrl.searchParams.set("_fpmcp_bust", String(Date.now()));
1397
+ const res = await safeFetch(bustUrl.toString(), { redirect: "follow" });
1398
+ const xcache = res.headers.get("x-cache") || res.headers.get("cf-cache-status") || "";
1399
+ queryStringKeyed = xcache.toLowerCase().includes("miss") || xcache.toLowerCase().includes("dynamic");
1400
+ await res.text().catch(() => { });
1401
+ }
1402
+ catch {
1403
+ // Skip
1404
+ }
1405
+ // Test unkeyed headers for cache poisoning
1406
+ const poisonTests = [];
1407
+ const unkeyedHeaders = [
1408
+ { header: "X-Forwarded-Host", value: "evil.com" },
1409
+ { header: "X-Original-URL", value: "/admin" },
1410
+ { header: "X-Rewrite-URL", value: "/admin" },
1411
+ { header: "X-Forwarded-Scheme", value: "nothttps" },
1412
+ ];
1413
+ for (const test of unkeyedHeaders) {
1414
+ try {
1415
+ const res = await safeFetch(url, {
1416
+ headers: {
1417
+ "User-Agent": USER_AGENT,
1418
+ [test.header]: test.value,
1419
+ },
1420
+ redirect: "follow",
1421
+ });
1422
+ const xc = res.headers.get("x-cache") || res.headers.get("cf-cache-status") || "";
1423
+ // If still a cache HIT, the header is unkeyed
1424
+ const isHit = xc.toLowerCase().includes("hit");
1425
+ poisonTests.push({
1426
+ header: test.header,
1427
+ value: test.value,
1428
+ keyed: !isHit,
1429
+ });
1430
+ await res.text().catch(() => { });
1431
+ }
1432
+ catch (err) {
1433
+ poisonTests.push({
1434
+ header: test.header,
1435
+ value: test.value,
1436
+ keyed: true,
1437
+ error: err.message,
1438
+ });
1439
+ }
1440
+ }
1441
+ // Detect CDN / cache provider
1442
+ let cacheProvider = null;
1443
+ if (baseline.cacheHeaders["cf-cache-status"])
1444
+ cacheProvider = "Cloudflare";
1445
+ else if (baseline.cacheHeaders["x-varnish"])
1446
+ cacheProvider = "Varnish";
1447
+ else if (baseline.cacheHeaders["x-fastly-request-id"])
1448
+ cacheProvider = "Fastly";
1449
+ else if (baseline.cacheHeaders["x-served-by"]?.includes("cache-"))
1450
+ cacheProvider = "Fastly";
1451
+ else if (baseline.cacheHeaders["via"]?.includes("cloudfront"))
1452
+ cacheProvider = "CloudFront";
1453
+ else if (baseline.cacheHeaders["x-cache"])
1454
+ cacheProvider = "CDN/Cache (unknown vendor)";
1455
+ const poisonable = poisonTests.filter((p) => !p.keyed && !p.error);
1456
+ const result = {
1457
+ url,
1458
+ cacheProvider,
1459
+ baseline: baseline.cacheHeaders,
1460
+ secondRequest,
1461
+ cacheKeyAnalysis: {
1462
+ queryStringKeyed,
1463
+ unkeyedHeaders: poisonTests,
1464
+ },
1465
+ summary: {
1466
+ cachingDetected: cacheProvider !== null || baseline.age !== null,
1467
+ provider: cacheProvider,
1468
+ poisoningRisk: poisonable.length > 0,
1469
+ poisonableHeaders: poisonable.map((p) => p.header),
1470
+ cacheControl: baseline.cacheHeaders["cache-control"],
1471
+ },
1472
+ };
1473
+ cache.set(cacheKey, result);
1474
+ return json(result);
1475
+ },
1476
+ };
1477
+ // ──────────────────────────────────────────────────────────────────────────────
1478
+ // Tool 11: http_security
1479
+ // ──────────────────────────────────────────────────────────────────────────────
1480
+ const httpSecurity = {
1481
+ name: "http_security",
1482
+ description: "Security header assessment. Evaluates HSTS, CSP, X-Frame-Options, X-Content-Type-Options, " +
1483
+ "Permissions-Policy, Referrer-Policy, COOP, CORP, and other security headers. " +
1484
+ "Missing headers are reported as findings with severity ratings.",
1485
+ schema: {
1486
+ url: z.string().url().describe("Target URL for security header assessment"),
1487
+ },
1488
+ async execute(args) {
1489
+ const url = args.url;
1490
+ const cacheKey = `http_security:${url}`;
1491
+ const cached = cache.get(cacheKey);
1492
+ if (cached)
1493
+ return json(cached);
1494
+ try {
1495
+ const res = await safeFetch(url, { redirect: "follow" });
1496
+ const securityHeaders = [
1497
+ {
1498
+ name: "Strict-Transport-Security",
1499
+ value: res.headers.get("strict-transport-security"),
1500
+ present: !!res.headers.get("strict-transport-security"),
1501
+ severity: "high",
1502
+ description: "HSTS — forces HTTPS connections, prevents SSL stripping attacks",
1503
+ recommendation: "max-age=31536000; includeSubDomains; preload",
1504
+ },
1505
+ {
1506
+ name: "Content-Security-Policy",
1507
+ value: res.headers.get("content-security-policy"),
1508
+ present: !!res.headers.get("content-security-policy"),
1509
+ severity: "high",
1510
+ description: "CSP — mitigates XSS, data injection, and clickjacking attacks",
1511
+ recommendation: "Define strict policy with nonces or hashes",
1512
+ },
1513
+ {
1514
+ name: "X-Frame-Options",
1515
+ value: res.headers.get("x-frame-options"),
1516
+ present: !!res.headers.get("x-frame-options"),
1517
+ severity: "medium",
1518
+ description: "Clickjacking protection — prevents page from being embedded in iframes",
1519
+ recommendation: "DENY or SAMEORIGIN",
1520
+ },
1521
+ {
1522
+ name: "X-Content-Type-Options",
1523
+ value: res.headers.get("x-content-type-options"),
1524
+ present: !!res.headers.get("x-content-type-options"),
1525
+ severity: "medium",
1526
+ description: "MIME type sniffing prevention",
1527
+ recommendation: "nosniff",
1528
+ },
1529
+ {
1530
+ name: "Permissions-Policy",
1531
+ value: res.headers.get("permissions-policy"),
1532
+ present: !!res.headers.get("permissions-policy"),
1533
+ severity: "medium",
1534
+ description: "Controls browser feature access (camera, microphone, geolocation, etc.)",
1535
+ recommendation: "Restrict unused features: camera=(), microphone=(), geolocation=()",
1536
+ },
1537
+ {
1538
+ name: "Referrer-Policy",
1539
+ value: res.headers.get("referrer-policy"),
1540
+ present: !!res.headers.get("referrer-policy"),
1541
+ severity: "low",
1542
+ description: "Controls how much referrer information is sent with requests",
1543
+ recommendation: "strict-origin-when-cross-origin or no-referrer",
1544
+ },
1545
+ {
1546
+ name: "Cross-Origin-Opener-Policy",
1547
+ value: res.headers.get("cross-origin-opener-policy"),
1548
+ present: !!res.headers.get("cross-origin-opener-policy"),
1549
+ severity: "low",
1550
+ description: "COOP — isolates browsing context to prevent Spectre-like attacks",
1551
+ recommendation: "same-origin",
1552
+ },
1553
+ {
1554
+ name: "Cross-Origin-Resource-Policy",
1555
+ value: res.headers.get("cross-origin-resource-policy"),
1556
+ present: !!res.headers.get("cross-origin-resource-policy"),
1557
+ severity: "low",
1558
+ description: "CORP — controls which origins can load resources",
1559
+ recommendation: "same-origin or cross-origin (if CDN)",
1560
+ },
1561
+ {
1562
+ name: "Cross-Origin-Embedder-Policy",
1563
+ value: res.headers.get("cross-origin-embedder-policy"),
1564
+ present: !!res.headers.get("cross-origin-embedder-policy"),
1565
+ severity: "low",
1566
+ description: "COEP — prevents loading cross-origin resources without explicit opt-in",
1567
+ recommendation: "require-corp",
1568
+ },
1569
+ {
1570
+ name: "X-XSS-Protection",
1571
+ value: res.headers.get("x-xss-protection"),
1572
+ present: !!res.headers.get("x-xss-protection"),
1573
+ severity: "info",
1574
+ description: "Legacy XSS filter (deprecated in modern browsers, CSP is preferred)",
1575
+ },
1576
+ ];
1577
+ // Analyze HSTS strength
1578
+ let hstsAnalysis = null;
1579
+ const hstsValue = res.headers.get("strict-transport-security");
1580
+ if (hstsValue) {
1581
+ const maxAgeMatch = hstsValue.match(/max-age=(\d+)/i);
1582
+ const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 0;
1583
+ hstsAnalysis = {
1584
+ maxAge,
1585
+ maxAgeDays: Math.floor(maxAge / 86400),
1586
+ includeSubDomains: /includeSubDomains/i.test(hstsValue),
1587
+ preload: /preload/i.test(hstsValue),
1588
+ adequate: maxAge >= 31536000,
1589
+ };
1590
+ }
1591
+ // Analyze CSP
1592
+ let cspAnalysis = null;
1593
+ const cspValue = res.headers.get("content-security-policy");
1594
+ if (cspValue) {
1595
+ const hasUnsafeInline = cspValue.includes("'unsafe-inline'");
1596
+ const hasUnsafeEval = cspValue.includes("'unsafe-eval'");
1597
+ const hasWildcard = /\s\*\s/.test(cspValue) || cspValue.includes(" * ");
1598
+ const directives = cspValue.split(";").map((d) => d.trim()).filter(Boolean);
1599
+ cspAnalysis = {
1600
+ directiveCount: directives.length,
1601
+ unsafeInline: hasUnsafeInline,
1602
+ unsafeEval: hasUnsafeEval,
1603
+ wildcardSource: hasWildcard,
1604
+ weaknesses: [
1605
+ ...(hasUnsafeInline ? ["unsafe-inline allows inline scripts/styles"] : []),
1606
+ ...(hasUnsafeEval ? ["unsafe-eval allows eval() and similar"] : []),
1607
+ ...(hasWildcard ? ["Wildcard (*) source allows loading from any origin"] : []),
1608
+ ],
1609
+ };
1610
+ }
1611
+ // Information disclosure headers
1612
+ const infoDisclosure = [];
1613
+ const disclosureHeaders = [
1614
+ "server", "x-powered-by", "x-aspnet-version",
1615
+ "x-runtime", "x-generator", "x-drupal-cache",
1616
+ "x-drupal-dynamic-cache",
1617
+ ];
1618
+ for (const h of disclosureHeaders) {
1619
+ const val = res.headers.get(h);
1620
+ if (val) {
1621
+ infoDisclosure.push({ header: h, value: val });
1622
+ }
1623
+ }
1624
+ const missing = securityHeaders.filter((h) => !h.present);
1625
+ const present = securityHeaders.filter((h) => h.present);
1626
+ // Score (0-100)
1627
+ const totalWeight = securityHeaders.reduce((sum, h) => {
1628
+ const w = h.severity === "high" ? 25 : h.severity === "medium" ? 15 : h.severity === "low" ? 10 : 5;
1629
+ return sum + w;
1630
+ }, 0);
1631
+ const earnedWeight = present.reduce((sum, h) => {
1632
+ const w = h.severity === "high" ? 25 : h.severity === "medium" ? 15 : h.severity === "low" ? 10 : 5;
1633
+ return sum + w;
1634
+ }, 0);
1635
+ const score = Math.round((earnedWeight / totalWeight) * 100);
1636
+ const result = {
1637
+ url,
1638
+ score,
1639
+ grade: score >= 90 ? "A" : score >= 75 ? "B" : score >= 60 ? "C" : score >= 40 ? "D" : "F",
1640
+ presentHeaders: present.map((h) => ({ name: h.name, value: h.value })),
1641
+ missingHeaders: missing.map((h) => ({
1642
+ name: h.name,
1643
+ severity: h.severity,
1644
+ description: h.description,
1645
+ recommendation: h.recommendation,
1646
+ })),
1647
+ hstsAnalysis,
1648
+ cspAnalysis,
1649
+ informationDisclosure: infoDisclosure,
1650
+ };
1651
+ cache.set(cacheKey, result);
1652
+ return json(result);
1653
+ }
1654
+ catch (err) {
1655
+ return json({ url, error: err.message });
1656
+ }
1657
+ },
1658
+ };
1659
+ // ──────────────────────────────────────────────────────────────────────────────
1660
+ // Tool 12: http_timing
1661
+ // ──────────────────────────────────────────────────────────────────────────────
1662
+ const httpTiming = {
1663
+ name: "http_timing",
1664
+ description: "Response timing analysis. Sends 10 identical GET requests and measures response times. " +
1665
+ "Computes mean and standard deviation. Low stddev = bare metal / dedicated. High stddev = " +
1666
+ "shared / cloud. High first request, low subsequent = serverless cold start pattern.",
1667
+ schema: {
1668
+ url: z.string().url().describe("Target URL for response timing analysis"),
1669
+ },
1670
+ async execute(args) {
1671
+ const url = args.url;
1672
+ const cacheKey = `http_timing:${url}`;
1673
+ const cached = cache.get(cacheKey);
1674
+ if (cached)
1675
+ return json(cached);
1676
+ const timings = [];
1677
+ const requestCount = 10;
1678
+ for (let i = 0; i < requestCount; i++) {
1679
+ try {
1680
+ const start = performance.now();
1681
+ const res = await safeFetch(url, { redirect: "follow" });
1682
+ await res.text().catch(() => { });
1683
+ const elapsed = performance.now() - start;
1684
+ timings.push(Math.round(elapsed * 100) / 100);
1685
+ }
1686
+ catch {
1687
+ timings.push(-1); // Mark failed requests
1688
+ }
1689
+ }
1690
+ const validTimings = timings.filter((t) => t >= 0);
1691
+ if (validTimings.length === 0) {
1692
+ return json({ url, error: "All requests failed", timings });
1693
+ }
1694
+ const mean = validTimings.reduce((a, b) => a + b, 0) / validTimings.length;
1695
+ const variance = validTimings.reduce((sum, t) => sum + Math.pow(t - mean, 2), 0) /
1696
+ validTimings.length;
1697
+ const stddev = Math.sqrt(variance);
1698
+ const min = Math.min(...validTimings);
1699
+ const max = Math.max(...validTimings);
1700
+ const coefficientOfVariation = mean > 0 ? (stddev / mean) * 100 : 0;
1701
+ // Detect cold start pattern: first request significantly slower than rest
1702
+ const firstTiming = validTimings[0];
1703
+ const restMean = validTimings.length > 1
1704
+ ? validTimings.slice(1).reduce((a, b) => a + b, 0) /
1705
+ (validTimings.length - 1)
1706
+ : mean;
1707
+ const coldStartRatio = firstTiming / restMean;
1708
+ const coldStartDetected = coldStartRatio > 2.0 && validTimings.length > 2;
1709
+ let infrastructureAssessment;
1710
+ if (coldStartDetected) {
1711
+ infrastructureAssessment =
1712
+ "Serverless / Lambda pattern detected — first request significantly slower (cold start), subsequent requests fast";
1713
+ }
1714
+ else if (coefficientOfVariation < 10) {
1715
+ infrastructureAssessment =
1716
+ "Consistent timing (low CV) — likely bare metal, dedicated server, or well-provisioned infrastructure";
1717
+ }
1718
+ else if (coefficientOfVariation < 30) {
1719
+ infrastructureAssessment =
1720
+ "Moderate variance — likely cloud VM or containerized deployment";
1721
+ }
1722
+ else {
1723
+ infrastructureAssessment =
1724
+ "High variance — shared hosting, heavily loaded server, or multi-region CDN with variable latency";
1725
+ }
1726
+ const result = {
1727
+ url,
1728
+ requestCount,
1729
+ successfulRequests: validTimings.length,
1730
+ timingsMs: timings,
1731
+ statistics: {
1732
+ meanMs: Math.round(mean * 100) / 100,
1733
+ stddevMs: Math.round(stddev * 100) / 100,
1734
+ minMs: min,
1735
+ maxMs: max,
1736
+ coefficientOfVariation: Math.round(coefficientOfVariation * 100) / 100,
1737
+ },
1738
+ coldStartAnalysis: {
1739
+ firstRequestMs: firstTiming,
1740
+ subsequentMeanMs: Math.round(restMean * 100) / 100,
1741
+ ratio: Math.round(coldStartRatio * 100) / 100,
1742
+ coldStartDetected,
1743
+ },
1744
+ intelligence: {
1745
+ infrastructureAssessment,
1746
+ },
1747
+ };
1748
+ cache.set(cacheKey, result);
1749
+ return json(result);
1750
+ },
1751
+ };
1752
+ // ──────────────────────────────────────────────────────────────────────────────
1753
+ // Tool 13: http_server_timing
1754
+ // ──────────────────────────────────────────────────────────────────────────────
1755
+ const httpServerTiming = {
1756
+ name: "http_server_timing",
1757
+ description: "Server-Timing header intelligence. Parses Server-Timing for CDN layer information, " +
1758
+ "backend service names, cache architecture details. Reveals internal service topology " +
1759
+ "and processing pipeline stages.",
1760
+ schema: {
1761
+ url: z.string().url().describe("Target URL for Server-Timing header analysis"),
1762
+ },
1763
+ async execute(args) {
1764
+ const url = args.url;
1765
+ const cacheKey = `http_server_timing:${url}`;
1766
+ const cached = cache.get(cacheKey);
1767
+ if (cached)
1768
+ return json(cached);
1769
+ try {
1770
+ const res = await safeFetch(url, { redirect: "follow" });
1771
+ const serverTiming = res.headers.get("server-timing");
1772
+ if (!serverTiming) {
1773
+ const result = {
1774
+ url,
1775
+ serverTiming: null,
1776
+ analysis: "No Server-Timing header present",
1777
+ };
1778
+ cache.set(cacheKey, result);
1779
+ return json(result);
1780
+ }
1781
+ // Parse Server-Timing: metric-name;desc="description";dur=123.4, metric2;dur=56
1782
+ const metrics = [];
1783
+ const entries = serverTiming.split(",").map((e) => e.trim());
1784
+ for (const entry of entries) {
1785
+ const parts = entry.split(";").map((p) => p.trim());
1786
+ const name = parts[0];
1787
+ let description = null;
1788
+ let durationMs = null;
1789
+ for (let i = 1; i < parts.length; i++) {
1790
+ const part = parts[i];
1791
+ if (part.startsWith("desc=")) {
1792
+ description = part.slice(5).replace(/^"|"$/g, "");
1793
+ }
1794
+ else if (part.startsWith("dur=")) {
1795
+ durationMs = parseFloat(part.slice(4));
1796
+ }
1797
+ }
1798
+ metrics.push({ name, description, durationMs, raw: entry });
1799
+ }
1800
+ // Intelligence extraction
1801
+ const serviceNames = metrics.map((m) => m.name);
1802
+ const totalDuration = metrics
1803
+ .filter((m) => m.durationMs !== null)
1804
+ .reduce((sum, m) => sum + (m.durationMs ?? 0), 0);
1805
+ // Detect CDN indicators
1806
+ const cdnIndicators = [];
1807
+ for (const m of metrics) {
1808
+ const lower = (m.name + " " + (m.description || "")).toLowerCase();
1809
+ if (lower.includes("cdn") || lower.includes("edge") || lower.includes("pop")) {
1810
+ cdnIndicators.push(`${m.name}: CDN/edge processing`);
1811
+ }
1812
+ if (lower.includes("cache") || lower.includes("hit") || lower.includes("miss")) {
1813
+ cdnIndicators.push(`${m.name}: Cache layer`);
1814
+ }
1815
+ if (lower.includes("origin") || lower.includes("backend") || lower.includes("upstream")) {
1816
+ cdnIndicators.push(`${m.name}: Origin/backend fetch`);
1817
+ }
1818
+ if (lower.includes("waf") || lower.includes("firewall") || lower.includes("security")) {
1819
+ cdnIndicators.push(`${m.name}: WAF/security processing`);
1820
+ }
1821
+ if (lower.includes("db") || lower.includes("database") || lower.includes("sql") || lower.includes("redis")) {
1822
+ cdnIndicators.push(`${m.name}: Database layer`);
1823
+ }
1824
+ }
1825
+ const result = {
1826
+ url,
1827
+ serverTiming,
1828
+ metrics,
1829
+ intelligence: {
1830
+ totalProcessingMs: Math.round(totalDuration * 100) / 100,
1831
+ serviceNames,
1832
+ cdnIndicators,
1833
+ internalTopology: cdnIndicators.length > 0
1834
+ ? "Server-Timing reveals internal processing pipeline and service names"
1835
+ : "No obvious infrastructure indicators in metric names",
1836
+ },
1837
+ };
1838
+ cache.set(cacheKey, result);
1839
+ return json(result);
1840
+ }
1841
+ catch (err) {
1842
+ return json({ url, error: err.message });
1843
+ }
1844
+ },
1845
+ };
1846
+ // ──────────────────────────────────────────────────────────────────────────────
1847
+ // Tool 14: http_resource_hints
1848
+ // ──────────────────────────────────────────────────────────────────────────────
1849
+ const httpResourceHints = {
1850
+ name: "http_resource_hints",
1851
+ description: "Resource hint analysis. Parses <link rel=\"preconnect\">, <link rel=\"dns-prefetch\">, " +
1852
+ "<link rel=\"preload\"> and Link HTTP headers. Maps third-party dependency graph and " +
1853
+ "identifies known services (Google Fonts, Stripe, Sentry, Auth0, Segment, etc.).",
1854
+ schema: {
1855
+ url: z.string().url().describe("Target URL for resource hint and third-party dependency analysis"),
1856
+ },
1857
+ async execute(args) {
1858
+ const url = args.url;
1859
+ const cacheKey = `http_resource_hints:${url}`;
1860
+ const cached = cache.get(cacheKey);
1861
+ if (cached)
1862
+ return json(cached);
1863
+ try {
1864
+ const res = await safeFetch(url, { redirect: "follow" });
1865
+ const html = await res.text();
1866
+ const linkHeader = res.headers.get("link");
1867
+ const $ = cheerio.load(html);
1868
+ const hints = [];
1869
+ // Parse HTML link elements
1870
+ const hintTypes = ["preconnect", "dns-prefetch", "preload", "prefetch", "modulepreload"];
1871
+ for (const rel of hintTypes) {
1872
+ $(`link[rel="${rel}"]`).each((_i, el) => {
1873
+ const href = $(el).attr("href");
1874
+ if (!href)
1875
+ return;
1876
+ let domain = null;
1877
+ try {
1878
+ domain = new URL(href, url).hostname;
1879
+ }
1880
+ catch {
1881
+ // Relative URL or invalid
1882
+ }
1883
+ hints.push({
1884
+ type: rel,
1885
+ href,
1886
+ source: "html",
1887
+ domain,
1888
+ knownService: domain ? (KNOWN_SERVICES[domain] ?? null) : null,
1889
+ as: $(el).attr("as") || undefined,
1890
+ crossorigin: $(el).attr("crossorigin") || undefined,
1891
+ });
1892
+ });
1893
+ }
1894
+ // Parse Link HTTP header
1895
+ if (linkHeader) {
1896
+ const linkParts = linkHeader.split(",").map((p) => p.trim());
1897
+ for (const part of linkParts) {
1898
+ const urlMatch = part.match(/<([^>]+)>/);
1899
+ const relMatch = part.match(/rel="([^"]+)"/);
1900
+ if (urlMatch && relMatch) {
1901
+ const href = urlMatch[1];
1902
+ const rel = relMatch[1];
1903
+ let domain = null;
1904
+ try {
1905
+ domain = new URL(href, url).hostname;
1906
+ }
1907
+ catch {
1908
+ // Invalid URL
1909
+ }
1910
+ hints.push({
1911
+ type: rel,
1912
+ href,
1913
+ source: "header",
1914
+ domain,
1915
+ knownService: domain ? (KNOWN_SERVICES[domain] ?? null) : null,
1916
+ });
1917
+ }
1918
+ }
1919
+ }
1920
+ // Build dependency graph
1921
+ const thirdPartyDomains = [
1922
+ ...new Set(hints
1923
+ .filter((h) => h.domain && h.domain !== new URL(url).hostname)
1924
+ .map((h) => h.domain)),
1925
+ ];
1926
+ const identifiedServices = hints
1927
+ .filter((h) => h.knownService)
1928
+ .map((h) => ({
1929
+ service: h.knownService,
1930
+ domain: h.domain,
1931
+ hintType: h.type,
1932
+ }));
1933
+ // Deduplicate identified services by service name
1934
+ const uniqueServices = [
1935
+ ...new Map(identifiedServices.map((s) => [s.service, s])).values(),
1936
+ ];
1937
+ const result = {
1938
+ url,
1939
+ hints,
1940
+ dependencyGraph: {
1941
+ thirdPartyDomains,
1942
+ thirdPartyCount: thirdPartyDomains.length,
1943
+ identifiedServices: uniqueServices,
1944
+ unidentifiedDomains: thirdPartyDomains.filter((d) => !KNOWN_SERVICES[d]),
1945
+ },
1946
+ summary: {
1947
+ totalHints: hints.length,
1948
+ preconnect: hints.filter((h) => h.type === "preconnect").length,
1949
+ dnsPrefetch: hints.filter((h) => h.type === "dns-prefetch").length,
1950
+ preload: hints.filter((h) => h.type === "preload").length,
1951
+ prefetch: hints.filter((h) => h.type === "prefetch").length,
1952
+ },
1953
+ };
1954
+ cache.set(cacheKey, result);
1955
+ return json(result);
1956
+ }
1957
+ catch (err) {
1958
+ return json({ url, error: err.message });
1959
+ }
1960
+ },
1961
+ };
1962
+ // ──────────────────────────────────────────────────────────────────────────────
1963
+ // Tool 15: http_keepalive
1964
+ // ──────────────────────────────────────────────────────────────────────────────
1965
+ const httpKeepalive = {
1966
+ name: "http_keepalive",
1967
+ description: "Connection keep-alive behavior analysis. Measures server idle timeout, max requests " +
1968
+ "per connection (via Keep-Alive header parsing). Detects HTTP/2 behavior and connection " +
1969
+ "management strategies.",
1970
+ schema: {
1971
+ url: z.string().url().describe("Target URL for keep-alive behavior analysis"),
1972
+ },
1973
+ async execute(args) {
1974
+ const url = args.url;
1975
+ const cacheKey = `http_keepalive:${url}`;
1976
+ const cached = cache.get(cacheKey);
1977
+ if (cached)
1978
+ return json(cached);
1979
+ try {
1980
+ // Send request with Connection: keep-alive
1981
+ const res = await safeFetch(url, {
1982
+ headers: {
1983
+ "User-Agent": USER_AGENT,
1984
+ Connection: "keep-alive",
1985
+ },
1986
+ redirect: "follow",
1987
+ });
1988
+ const connectionHeader = res.headers.get("connection");
1989
+ const keepAliveHeader = res.headers.get("keep-alive");
1990
+ const altSvc = res.headers.get("alt-svc");
1991
+ // Parse Keep-Alive header: timeout=5, max=100
1992
+ let timeout = null;
1993
+ let maxRequests = null;
1994
+ if (keepAliveHeader) {
1995
+ const timeoutMatch = keepAliveHeader.match(/timeout=(\d+)/i);
1996
+ const maxMatch = keepAliveHeader.match(/max=(\d+)/i);
1997
+ if (timeoutMatch)
1998
+ timeout = parseInt(timeoutMatch[1], 10);
1999
+ if (maxMatch)
2000
+ maxRequests = parseInt(maxMatch[1], 10);
2001
+ }
2002
+ await res.text().catch(() => { });
2003
+ // Also test with Connection: close
2004
+ let closeResponse = null;
2005
+ try {
2006
+ const closeRes = await safeFetch(url, {
2007
+ headers: {
2008
+ "User-Agent": USER_AGENT,
2009
+ Connection: "close",
2010
+ },
2011
+ redirect: "follow",
2012
+ });
2013
+ const closeConn = closeRes.headers.get("connection");
2014
+ await closeRes.text().catch(() => { });
2015
+ closeResponse = {
2016
+ connectionHeader: closeConn,
2017
+ serverRespected: closeConn?.toLowerCase() === "close",
2018
+ };
2019
+ }
2020
+ catch {
2021
+ // Skip
2022
+ }
2023
+ // HTTP/2 detection from Alt-Svc
2024
+ let http2Indicators = [];
2025
+ if (altSvc) {
2026
+ if (altSvc.includes("h2"))
2027
+ http2Indicators.push("Alt-Svc advertises HTTP/2 (h2)");
2028
+ if (altSvc.includes("h3"))
2029
+ http2Indicators.push("Alt-Svc advertises HTTP/3 (h3/QUIC)");
2030
+ }
2031
+ let assessment;
2032
+ if (keepAliveHeader) {
2033
+ assessment = `Keep-Alive configured: timeout=${timeout ?? "unknown"}s, max=${maxRequests ?? "unknown"} requests`;
2034
+ }
2035
+ else if (connectionHeader?.toLowerCase() === "keep-alive") {
2036
+ assessment =
2037
+ "Server supports keep-alive but does not expose timeout/max parameters";
2038
+ }
2039
+ else if (connectionHeader?.toLowerCase() === "close") {
2040
+ assessment =
2041
+ "Server closes connections after each request — may be HTTP/1.0 compatible or behind a proxy that disables keep-alive";
2042
+ }
2043
+ else {
2044
+ assessment =
2045
+ "No explicit Connection header — default behavior applies (HTTP/1.1 defaults to keep-alive)";
2046
+ }
2047
+ const result = {
2048
+ url,
2049
+ connection: connectionHeader,
2050
+ keepAlive: {
2051
+ header: keepAliveHeader,
2052
+ timeout,
2053
+ maxRequests,
2054
+ },
2055
+ altSvc,
2056
+ http2Indicators,
2057
+ closeTest: closeResponse,
2058
+ assessment,
2059
+ };
2060
+ cache.set(cacheKey, result);
2061
+ return json(result);
2062
+ }
2063
+ catch (err) {
2064
+ return json({ url, error: err.message });
2065
+ }
2066
+ },
2067
+ };
2068
+ // ──────────────────────────────────────────────────────────────────────────────
2069
+ // Tool 16: http_nel
2070
+ // ──────────────────────────────────────────────────────────────────────────────
2071
+ const httpNel = {
2072
+ name: "http_nel",
2073
+ description: "NEL (Network Error Logging), Report-To, Reporting-Endpoints, and Expect-CT header analysis. " +
2074
+ "Extracts monitoring provider information, CDN/security vendor confirmation from reporting " +
2075
+ "endpoints and error logging configuration.",
2076
+ schema: {
2077
+ url: z.string().url().describe("Target URL for NEL / reporting header analysis"),
2078
+ },
2079
+ async execute(args) {
2080
+ const url = args.url;
2081
+ const cacheKey = `http_nel:${url}`;
2082
+ const cached = cache.get(cacheKey);
2083
+ if (cached)
2084
+ return json(cached);
2085
+ try {
2086
+ const res = await safeFetch(url, { redirect: "follow" });
2087
+ const nel = res.headers.get("nel");
2088
+ const reportTo = res.headers.get("report-to");
2089
+ const reportingEndpoints = res.headers.get("reporting-endpoints");
2090
+ const expectCt = res.headers.get("expect-ct");
2091
+ const analysis = {
2092
+ nel: null,
2093
+ reportTo: null,
2094
+ reportingEndpoints: null,
2095
+ expectCt: null,
2096
+ providers: [],
2097
+ endpoints: [],
2098
+ };
2099
+ // Parse NEL header (JSON)
2100
+ if (nel) {
2101
+ try {
2102
+ analysis.nel = JSON.parse(nel);
2103
+ }
2104
+ catch {
2105
+ analysis.nel = { raw: nel, parseError: true };
2106
+ }
2107
+ }
2108
+ // Parse Report-To header (JSON, may be array)
2109
+ if (reportTo) {
2110
+ try {
2111
+ // Report-To can be multiple JSON objects separated by commas
2112
+ // Each object is a separate group
2113
+ const groups = reportTo.split(/(?<=\})\s*,\s*(?=\{)/).map((g) => {
2114
+ try {
2115
+ return JSON.parse(g.trim());
2116
+ }
2117
+ catch {
2118
+ return { raw: g.trim(), parseError: true };
2119
+ }
2120
+ });
2121
+ analysis.reportTo = groups;
2122
+ // Extract endpoint URLs
2123
+ for (const group of groups) {
2124
+ if (group.endpoints && Array.isArray(group.endpoints)) {
2125
+ for (const ep of group.endpoints) {
2126
+ if (ep.url) {
2127
+ analysis.endpoints.push(ep.url);
2128
+ try {
2129
+ const domain = new URL(ep.url).hostname;
2130
+ if (domain.includes("cloudflare"))
2131
+ analysis.providers.push("Cloudflare");
2132
+ else if (domain.includes("sentry"))
2133
+ analysis.providers.push("Sentry");
2134
+ else if (domain.includes("report-uri"))
2135
+ analysis.providers.push("Report URI");
2136
+ else if (domain.includes("datadog"))
2137
+ analysis.providers.push("Datadog");
2138
+ else if (domain.includes("newrelic"))
2139
+ analysis.providers.push("New Relic");
2140
+ else
2141
+ analysis.providers.push(domain);
2142
+ }
2143
+ catch {
2144
+ // Invalid URL
2145
+ }
2146
+ }
2147
+ }
2148
+ }
2149
+ }
2150
+ }
2151
+ catch {
2152
+ analysis.reportTo = { raw: reportTo, parseError: true };
2153
+ }
2154
+ }
2155
+ // Parse Reporting-Endpoints header (key=value pairs)
2156
+ if (reportingEndpoints) {
2157
+ const endpoints = {};
2158
+ const parts = reportingEndpoints.split(",").map((p) => p.trim());
2159
+ for (const part of parts) {
2160
+ const eqIdx = part.indexOf("=");
2161
+ if (eqIdx > 0) {
2162
+ const key = part.slice(0, eqIdx).trim();
2163
+ const value = part.slice(eqIdx + 1).trim().replace(/^"|"$/g, "");
2164
+ endpoints[key] = value;
2165
+ analysis.endpoints.push(value);
2166
+ try {
2167
+ const domain = new URL(value).hostname;
2168
+ if (domain.includes("cloudflare"))
2169
+ analysis.providers.push("Cloudflare");
2170
+ else if (domain.includes("sentry"))
2171
+ analysis.providers.push("Sentry");
2172
+ else if (domain.includes("report-uri"))
2173
+ analysis.providers.push("Report URI");
2174
+ else if (domain.includes("datadog"))
2175
+ analysis.providers.push("Datadog");
2176
+ else
2177
+ analysis.providers.push(domain);
2178
+ }
2179
+ catch {
2180
+ // Invalid URL
2181
+ }
2182
+ }
2183
+ }
2184
+ analysis.reportingEndpoints = endpoints;
2185
+ }
2186
+ // Parse Expect-CT header
2187
+ if (expectCt) {
2188
+ const parts = {};
2189
+ for (const part of expectCt.split(",").map((p) => p.trim())) {
2190
+ if (part.startsWith("max-age=")) {
2191
+ parts["max-age"] = part.split("=")[1];
2192
+ }
2193
+ else if (part.startsWith("report-uri=")) {
2194
+ const uri = part.split("=").slice(1).join("=").replace(/^"|"$/g, "");
2195
+ parts["report-uri"] = uri;
2196
+ analysis.endpoints.push(uri);
2197
+ }
2198
+ else if (part === "enforce") {
2199
+ parts.enforce = true;
2200
+ }
2201
+ }
2202
+ analysis.expectCt = parts;
2203
+ }
2204
+ // Deduplicate providers
2205
+ analysis.providers = [...new Set(analysis.providers)];
2206
+ analysis.endpoints = [...new Set(analysis.endpoints)];
2207
+ const hasReporting = nel !== null ||
2208
+ reportTo !== null ||
2209
+ reportingEndpoints !== null ||
2210
+ expectCt !== null;
2211
+ const result = {
2212
+ url,
2213
+ headers: {
2214
+ nel: nel ?? null,
2215
+ reportTo: reportTo ?? null,
2216
+ reportingEndpoints: reportingEndpoints ?? null,
2217
+ expectCt: expectCt ?? null,
2218
+ },
2219
+ analysis,
2220
+ summary: {
2221
+ reportingConfigured: hasReporting,
2222
+ monitoringProviders: analysis.providers,
2223
+ reportingEndpoints: analysis.endpoints,
2224
+ insight: hasReporting
2225
+ ? `Reporting configured — monitored by: ${analysis.providers.join(", ") || "unknown provider"}`
2226
+ : "No NEL/Report-To/Reporting-Endpoints/Expect-CT headers present",
2227
+ },
2228
+ };
2229
+ cache.set(cacheKey, result);
2230
+ return json(result);
2231
+ }
2232
+ catch (err) {
2233
+ return json({ url, error: err.message });
2234
+ }
2235
+ },
2236
+ };
2237
+ // ──────────────────────────────────────────────────────────────────────────────
2238
+ // Tool 17: http_redirect
2239
+ // ──────────────────────────────────────────────────────────────────────────────
2240
+ const httpRedirect = {
2241
+ name: "http_redirect",
2242
+ description: "Full redirect chain analysis. Follows all 3xx responses manually (no auto-follow). " +
2243
+ "Maps complete redirection path. Detects CDN redirect patterns, WAF challenge redirects, " +
2244
+ "and authentication redirects. Extracts intermediate servers from Via/Server headers at each hop.",
2245
+ schema: {
2246
+ url: z.string().url().describe("Target URL for redirect chain analysis"),
2247
+ },
2248
+ async execute(args) {
2249
+ const url = args.url;
2250
+ const cacheKey = `http_redirect:${url}`;
2251
+ const cached = cache.get(cacheKey);
2252
+ if (cached)
2253
+ return json(cached);
2254
+ const chain = [];
2255
+ let currentUrl = url;
2256
+ const maxRedirects = 20;
2257
+ const visitedUrls = new Set();
2258
+ for (let step = 0; step < maxRedirects; step++) {
2259
+ if (visitedUrls.has(currentUrl)) {
2260
+ chain.push({
2261
+ step,
2262
+ url: currentUrl,
2263
+ status: 0,
2264
+ statusText: "REDIRECT LOOP DETECTED",
2265
+ location: null,
2266
+ serverHeader: null,
2267
+ viaHeader: null,
2268
+ setCookies: [],
2269
+ headers: {},
2270
+ pattern: "redirect-loop",
2271
+ });
2272
+ break;
2273
+ }
2274
+ visitedUrls.add(currentUrl);
2275
+ try {
2276
+ const res = await safeFetch(currentUrl, { redirect: "manual" });
2277
+ const location = res.headers.get("location");
2278
+ const serverHeader = res.headers.get("server");
2279
+ const viaHeader = res.headers.get("via");
2280
+ // Collect Set-Cookie headers
2281
+ const setCookies = [];
2282
+ res.headers.forEach((value, key) => {
2283
+ if (key.toLowerCase() === "set-cookie") {
2284
+ setCookies.push(value);
2285
+ }
2286
+ });
2287
+ if (setCookies.length === 0) {
2288
+ const raw = res.headers.get("set-cookie");
2289
+ if (raw)
2290
+ setCookies.push(raw);
2291
+ }
2292
+ // Collect interesting headers
2293
+ const interestingHeaders = {};
2294
+ const headerNames = [
2295
+ "server", "via", "x-powered-by", "x-forwarded-for",
2296
+ "cf-ray", "x-cache", "x-amz-cf-id", "x-served-by",
2297
+ ];
2298
+ for (const h of headerNames) {
2299
+ const val = res.headers.get(h);
2300
+ if (val)
2301
+ interestingHeaders[h] = val;
2302
+ }
2303
+ // Detect redirect patterns
2304
+ let pattern;
2305
+ if (res.status === 301)
2306
+ pattern = "permanent-redirect";
2307
+ else if (res.status === 302)
2308
+ pattern = "temporary-redirect";
2309
+ else if (res.status === 303)
2310
+ pattern = "see-other";
2311
+ else if (res.status === 307)
2312
+ pattern = "temporary-redirect-preserving-method";
2313
+ else if (res.status === 308)
2314
+ pattern = "permanent-redirect-preserving-method";
2315
+ // Detect specific redirect patterns
2316
+ if (location) {
2317
+ if (location.includes("/login") || location.includes("/auth") || location.includes("/signin")) {
2318
+ pattern = "authentication-redirect";
2319
+ }
2320
+ else if (location.includes("challenge") || location.includes("captcha")) {
2321
+ pattern = "waf-challenge-redirect";
2322
+ }
2323
+ else if (location.startsWith("https://") &&
2324
+ currentUrl.startsWith("http://") &&
2325
+ new URL(location).hostname === new URL(currentUrl).hostname) {
2326
+ pattern = "http-to-https-upgrade";
2327
+ }
2328
+ else if (location.includes("www.") && !currentUrl.includes("www.")) {
2329
+ pattern = "www-redirect";
2330
+ }
2331
+ else if (!location.includes("www.") && currentUrl.includes("www.")) {
2332
+ pattern = "www-to-bare-redirect";
2333
+ }
2334
+ }
2335
+ await res.text().catch(() => { });
2336
+ chain.push({
2337
+ step,
2338
+ url: currentUrl,
2339
+ status: res.status,
2340
+ statusText: res.statusText,
2341
+ location,
2342
+ serverHeader,
2343
+ viaHeader,
2344
+ setCookies,
2345
+ headers: interestingHeaders,
2346
+ pattern,
2347
+ });
2348
+ // Follow redirect
2349
+ if (res.status >= 300 && res.status < 400 && location) {
2350
+ try {
2351
+ currentUrl = new URL(location, currentUrl).toString();
2352
+ }
2353
+ catch {
2354
+ // Invalid location header
2355
+ break;
2356
+ }
2357
+ }
2358
+ else {
2359
+ // Not a redirect — we've reached the final destination
2360
+ break;
2361
+ }
2362
+ }
2363
+ catch (err) {
2364
+ chain.push({
2365
+ step,
2366
+ url: currentUrl,
2367
+ status: 0,
2368
+ statusText: err.message,
2369
+ location: null,
2370
+ serverHeader: null,
2371
+ viaHeader: null,
2372
+ setCookies: [],
2373
+ headers: {},
2374
+ pattern: "error",
2375
+ });
2376
+ break;
2377
+ }
2378
+ }
2379
+ // Analyze the chain
2380
+ const redirectCount = chain.filter((c) => c.status >= 300 && c.status < 400).length;
2381
+ const finalHop = chain[chain.length - 1];
2382
+ const intermediateServers = [
2383
+ ...new Set(chain.map((c) => c.serverHeader).filter(Boolean)),
2384
+ ];
2385
+ const viaChain = [
2386
+ ...new Set(chain.map((c) => c.viaHeader).filter(Boolean)),
2387
+ ];
2388
+ const patterns = [
2389
+ ...new Set(chain.map((c) => c.pattern).filter(Boolean)),
2390
+ ];
2391
+ const hostsInChain = [
2392
+ ...new Set(chain.map((c) => {
2393
+ try {
2394
+ return new URL(c.url).hostname;
2395
+ }
2396
+ catch {
2397
+ return null;
2398
+ }
2399
+ }).filter(Boolean)),
2400
+ ];
2401
+ const result = {
2402
+ url,
2403
+ chain,
2404
+ summary: {
2405
+ totalHops: chain.length,
2406
+ redirectCount,
2407
+ finalUrl: finalHop?.url ?? url,
2408
+ finalStatus: finalHop?.status ?? 0,
2409
+ intermediateServers,
2410
+ viaChain,
2411
+ redirectPatterns: patterns,
2412
+ hostsInChain,
2413
+ crossDomain: hostsInChain.length > 1,
2414
+ hasLoop: chain.some((c) => c.pattern === "redirect-loop"),
2415
+ excessiveRedirects: redirectCount > 5,
2416
+ },
2417
+ };
2418
+ cache.set(cacheKey, result);
2419
+ return json(result);
2420
+ },
2421
+ };
2422
+ // ──────────────────────────────────────────────────────────────────────────────
2423
+ // Export All HTTP Tools
2424
+ // ──────────────────────────────────────────────────────────────────────────────
2425
+ export const httpTools = [
2426
+ httpHeaders,
2427
+ httpMultiFingerprint,
2428
+ httpFavicon,
2429
+ httpEtag,
2430
+ httpErrors,
2431
+ httpCookies,
2432
+ httpMethods,
2433
+ httpCors,
2434
+ httpCompression,
2435
+ httpCache,
2436
+ httpSecurity,
2437
+ httpTiming,
2438
+ httpServerTiming,
2439
+ httpResourceHints,
2440
+ httpKeepalive,
2441
+ httpNel,
2442
+ httpRedirect,
2443
+ ];
2444
+ //# sourceMappingURL=index.js.map