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,944 @@
1
+ import { z } from "zod";
2
+ import { json } from "../types/index.js";
3
+ import { CLOUD_PATTERNS } from "../data/cloud-ranges.js";
4
+ import { HEADER_ORDER_SIGNATURES, computeHeaderOrderHash } from "../data/header-order.js";
5
+ import { WAF_SIGNATURES } from "../data/waf-signatures.js";
6
+ import { COOKIE_PATTERNS } from "../data/cookie-patterns.js";
7
+ import { TECH_PATTERNS } from "../data/tech-patterns.js";
8
+ import { ANALYTICS_PATTERNS } from "../data/analytics-patterns.js";
9
+ import { SERVICE_BANNERS } from "../data/service-banners.js";
10
+ import * as cheerio from "cheerio";
11
+ // ─── Helpers ───
12
+ /** Safely test a regex pattern against a string, returning false on invalid regex. */
13
+ function safeRegexTest(pattern, value) {
14
+ try {
15
+ return new RegExp(pattern, "i").test(value);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /** Safely extract first capture group using a regex pattern. */
22
+ function safeRegexExtract(pattern, value) {
23
+ try {
24
+ const match = new RegExp(pattern, "i").exec(value);
25
+ return match?.[1] ?? match?.[2] ?? null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /**
32
+ * Decode an F5 BIG-IP persistence cookie value to extract internal IP:port.
33
+ *
34
+ * The decimal cookie value encodes backend IP and port:
35
+ * - Split by '.': first part is encoded IP, second is encoded port
36
+ * - IP: convert decimal to hex, reverse byte pairs, convert each pair to decimal octets
37
+ * - Port: convert decimal to hex, reverse byte pair, convert to decimal
38
+ */
39
+ function decodeBigIpCookie(cookieValue) {
40
+ try {
41
+ const parts = cookieValue.split(".");
42
+ if (parts.length < 2)
43
+ return null;
44
+ const encodedIp = parseInt(parts[0], 10);
45
+ const encodedPort = parseInt(parts[1], 10);
46
+ if (isNaN(encodedIp) || isNaN(encodedPort))
47
+ return null;
48
+ // Decode IP: convert to hex, reverse byte pairs
49
+ const ipHex = encodedIp.toString(16).padStart(8, "0");
50
+ const ip = [
51
+ parseInt(ipHex.substring(6, 8), 16),
52
+ parseInt(ipHex.substring(4, 6), 16),
53
+ parseInt(ipHex.substring(2, 4), 16),
54
+ parseInt(ipHex.substring(0, 2), 16),
55
+ ].join(".");
56
+ // Decode port: convert to hex, reverse byte pair
57
+ const portHex = encodedPort.toString(16).padStart(4, "0");
58
+ const port = parseInt(portHex.substring(2, 4) + portHex.substring(0, 2), 16);
59
+ return { ip, port };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ // ─── Security Header Constants ───
66
+ const SECURITY_HEADERS = [
67
+ "strict-transport-security",
68
+ "content-security-policy",
69
+ "x-frame-options",
70
+ "x-content-type-options",
71
+ "referrer-policy",
72
+ "permissions-policy",
73
+ "x-xss-protection",
74
+ "cross-origin-opener-policy",
75
+ "cross-origin-embedder-policy",
76
+ "cross-origin-resource-policy",
77
+ ];
78
+ // ─── Port-to-Protocol Map ───
79
+ const PORT_PROTOCOL_MAP = {
80
+ 21: "ftp",
81
+ 22: "ssh",
82
+ 25: "smtp",
83
+ 80: "http",
84
+ 110: "pop3",
85
+ 143: "imap",
86
+ 443: "http",
87
+ 465: "smtp",
88
+ 587: "smtp",
89
+ 3306: "mysql",
90
+ 5432: "postgresql",
91
+ 6379: "redis",
92
+ 8080: "http",
93
+ 8443: "http",
94
+ 11211: "memcached",
95
+ 27017: "mongodb",
96
+ };
97
+ // ─── Tool 1: fp_analyze_headers ───
98
+ async function analyzeHeaders(args) {
99
+ const headers = args.headers;
100
+ const url = args.url;
101
+ // Normalize header keys to lowercase for consistent matching
102
+ const normalizedHeaders = {};
103
+ const originalHeaderOrder = [];
104
+ for (const [key, value] of Object.entries(headers)) {
105
+ normalizedHeaders[key.toLowerCase()] = value;
106
+ originalHeaderOrder.push(key);
107
+ }
108
+ // 1. Server identification
109
+ const claimedServer = normalizedHeaders["server"] ?? null;
110
+ // Compute header order hash and match against known signatures
111
+ const headerOrderHash = computeHeaderOrderHash(originalHeaderOrder);
112
+ let headerOrderServer = null;
113
+ for (const sig of HEADER_ORDER_SIGNATURES) {
114
+ if (sig.hash === headerOrderHash) {
115
+ headerOrderServer = sig.server;
116
+ break;
117
+ }
118
+ }
119
+ const server = {
120
+ claimed: claimedServer,
121
+ headerOrderMatch: headerOrderServer,
122
+ headerOrderHash,
123
+ headerOrderMismatch: claimedServer && headerOrderServer
124
+ ? !claimedServer.toLowerCase().includes(headerOrderServer.toLowerCase()) &&
125
+ !headerOrderServer.toLowerCase().includes(claimedServer.toLowerCase())
126
+ : false,
127
+ };
128
+ // 2. CDN/Cloud detection
129
+ const cloudDetections = [];
130
+ for (const cloud of CLOUD_PATTERNS) {
131
+ const matchedHeaders = [];
132
+ for (const [headerName, pattern] of Object.entries(cloud.headerPatterns)) {
133
+ const headerValue = normalizedHeaders[headerName.toLowerCase()];
134
+ if (headerValue !== undefined && safeRegexTest(pattern, headerValue)) {
135
+ matchedHeaders.push({ header: headerName, pattern });
136
+ }
137
+ }
138
+ if (matchedHeaders.length > 0) {
139
+ cloudDetections.push({
140
+ provider: cloud.provider,
141
+ matchedHeaders,
142
+ });
143
+ }
144
+ }
145
+ // 3. WAF detection
146
+ const wafDetections = [];
147
+ // Extract cookie names from set-cookie headers
148
+ const setCookieValue = normalizedHeaders["set-cookie"] ?? "";
149
+ const cookieHeader = normalizedHeaders["cookie"] ?? "";
150
+ const allCookieText = setCookieValue + " " + cookieHeader;
151
+ // Extract cookie names from Set-Cookie (name=value; ...) patterns
152
+ const cookieNames = [];
153
+ const cookieNameMatches = allCookieText.match(/(?:^|[,;]\s*)([^=\s]+)=/g);
154
+ if (cookieNameMatches) {
155
+ for (const match of cookieNameMatches) {
156
+ const name = match.replace(/^[,;]\s*/, "").replace(/=$/, "");
157
+ if (name)
158
+ cookieNames.push(name);
159
+ }
160
+ }
161
+ for (const waf of WAF_SIGNATURES) {
162
+ const matchedHeaders = [];
163
+ const matchedCookies = [];
164
+ // Check header patterns
165
+ if (waf.patterns.headers) {
166
+ for (const [headerName, pattern] of Object.entries(waf.patterns.headers)) {
167
+ const headerValue = normalizedHeaders[headerName.toLowerCase()];
168
+ if (headerValue !== undefined && safeRegexTest(pattern, headerValue)) {
169
+ matchedHeaders.push(headerName);
170
+ }
171
+ }
172
+ }
173
+ // Check cookie patterns
174
+ if (waf.patterns.cookies) {
175
+ for (const cookiePattern of waf.patterns.cookies) {
176
+ for (const cookieName of cookieNames) {
177
+ if (safeRegexTest(cookiePattern, cookieName)) {
178
+ matchedCookies.push(cookieName);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ if (matchedHeaders.length > 0 || matchedCookies.length > 0) {
184
+ wafDetections.push({
185
+ name: waf.name,
186
+ vendor: waf.vendor,
187
+ confidence: waf.confidence,
188
+ matchedHeaders,
189
+ matchedCookies,
190
+ });
191
+ }
192
+ }
193
+ // 4. Security header audit
194
+ const securityHeaderResults = [];
195
+ let securityHeaderCount = 0;
196
+ let securityScore = 0;
197
+ for (const header of SECURITY_HEADERS) {
198
+ const value = normalizedHeaders[header] ?? null;
199
+ const present = value !== null;
200
+ let assessment = "Missing";
201
+ if (present) {
202
+ securityHeaderCount++;
203
+ securityScore += 10;
204
+ assessment = "Present";
205
+ // Additional quality checks
206
+ if (header === "strict-transport-security") {
207
+ const maxAgeMatch = value.match(/max-age=(\d+)/i);
208
+ const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 0;
209
+ if (maxAge >= 31536000) {
210
+ assessment = "Strong (max-age >= 1 year)";
211
+ }
212
+ else if (maxAge >= 2592000) {
213
+ assessment = "Moderate (max-age >= 30 days)";
214
+ }
215
+ else {
216
+ assessment = "Weak (max-age < 30 days)";
217
+ securityScore -= 3;
218
+ }
219
+ if (/includesubdomains/i.test(value))
220
+ assessment += ", includeSubDomains";
221
+ if (/preload/i.test(value))
222
+ assessment += ", preload";
223
+ }
224
+ else if (header === "x-content-type-options") {
225
+ assessment = value.toLowerCase() === "nosniff" ? "Correct (nosniff)" : `Unexpected value: ${value}`;
226
+ }
227
+ else if (header === "x-frame-options") {
228
+ if (/^deny$/i.test(value)) {
229
+ assessment = "Strong (DENY)";
230
+ }
231
+ else if (/^sameorigin$/i.test(value)) {
232
+ assessment = "Good (SAMEORIGIN)";
233
+ }
234
+ else {
235
+ assessment = `Custom: ${value}`;
236
+ }
237
+ }
238
+ else if (header === "referrer-policy") {
239
+ if (/no-referrer$/i.test(value) || /strict-origin-when-cross-origin/i.test(value)) {
240
+ assessment = `Good (${value})`;
241
+ }
242
+ else {
243
+ assessment = `Present (${value})`;
244
+ }
245
+ }
246
+ }
247
+ securityHeaderResults.push({ header, present, value, assessment });
248
+ }
249
+ // Grade calculation: A-F based on count (10 headers total, 10 points each = 100 max)
250
+ const securityGrade = securityScore >= 90
251
+ ? "A"
252
+ : securityScore >= 70
253
+ ? "B"
254
+ : securityScore >= 50
255
+ ? "C"
256
+ : securityScore >= 30
257
+ ? "D"
258
+ : securityScore >= 10
259
+ ? "E"
260
+ : "F";
261
+ const securityHeaders = {
262
+ results: securityHeaderResults,
263
+ present: securityHeaderCount,
264
+ total: SECURITY_HEADERS.length,
265
+ grade: securityGrade,
266
+ score: securityScore,
267
+ };
268
+ // 5. Infrastructure cookie decode
269
+ const cookieFindings = [];
270
+ for (const cookieName of cookieNames) {
271
+ for (const pattern of COOKIE_PATTERNS) {
272
+ if (safeRegexTest(pattern.pattern, cookieName)) {
273
+ const finding = {
274
+ cookieName,
275
+ technology: pattern.technology,
276
+ category: pattern.category,
277
+ name: pattern.name,
278
+ };
279
+ // Decode BIGipServer cookies
280
+ if (cookieName.startsWith("BIGipServer")) {
281
+ // Try to extract the cookie value from the raw set-cookie header
282
+ const bigIpMatch = allCookieText.match(new RegExp(`${cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;\\s]+)`));
283
+ if (bigIpMatch) {
284
+ finding.decoded = decodeBigIpCookie(bigIpMatch[1]);
285
+ }
286
+ finding.note = "F5 BIG-IP persistence cookie - may reveal internal IP:port";
287
+ }
288
+ else if (cookieName.startsWith("AWSALB")) {
289
+ finding.note = "AWS Application Load Balancer cookie";
290
+ }
291
+ else if (cookieName.startsWith("NSC_")) {
292
+ finding.note = "Citrix NetScaler persistence cookie - may encode VIP address";
293
+ }
294
+ cookieFindings.push(finding);
295
+ break; // First match per cookie name
296
+ }
297
+ }
298
+ }
299
+ // 6. Technology detection
300
+ const technologyDetections = [];
301
+ const techHeaders = {
302
+ "x-powered-by": "Server-side technology",
303
+ "x-aspnet-version": "ASP.NET version",
304
+ "x-generator": "Site generator",
305
+ "x-drupal-cache": "Drupal CMS",
306
+ "x-drupal-dynamic-cache": "Drupal CMS (dynamic cache)",
307
+ "x-varnish": "Varnish cache",
308
+ "x-magento-vary": "Magento e-commerce",
309
+ "x-shopify-stage": "Shopify",
310
+ "x-wix-request-id": "Wix",
311
+ "x-envoy-upstream-service-time": "Envoy proxy",
312
+ "x-litespeed-cache": "LiteSpeed Cache",
313
+ "x-turbo-charged-by": "LiteSpeed (Turbo)",
314
+ "x-pingback": "WordPress (xmlrpc)",
315
+ "x-redirect-by": "WordPress redirect",
316
+ "x-runtime": "Ruby on Rails / Rack",
317
+ };
318
+ for (const [header, tech] of Object.entries(techHeaders)) {
319
+ const value = normalizedHeaders[header];
320
+ if (value !== undefined) {
321
+ technologyDetections.push({ header, value, technology: tech });
322
+ }
323
+ }
324
+ // 7. Cache strategy
325
+ const cacheStrategy = {
326
+ cacheControl: normalizedHeaders["cache-control"] ?? null,
327
+ etag: normalizedHeaders["etag"] ?? null,
328
+ age: normalizedHeaders["age"] ?? null,
329
+ xCache: normalizedHeaders["x-cache"] ?? null,
330
+ vary: normalizedHeaders["vary"] ?? null,
331
+ cdnCache: normalizedHeaders["cf-cache-status"] ??
332
+ normalizedHeaders["x-vercel-cache"] ??
333
+ normalizedHeaders["x-cache-hits"] ??
334
+ null,
335
+ strategy: "unknown",
336
+ };
337
+ // Determine caching strategy
338
+ const cc = cacheStrategy.cacheControl?.toLowerCase() ?? "";
339
+ if (cc.includes("no-store") || cc.includes("no-cache")) {
340
+ cacheStrategy.strategy = "no-cache";
341
+ }
342
+ else if (cc.includes("public") && cc.includes("max-age")) {
343
+ cacheStrategy.strategy = "aggressive-caching";
344
+ }
345
+ else if (cc.includes("private")) {
346
+ cacheStrategy.strategy = "private-caching";
347
+ }
348
+ else if (cc.includes("must-revalidate") || cacheStrategy.etag) {
349
+ cacheStrategy.strategy = "revalidation-based";
350
+ }
351
+ else if (cacheStrategy.xCache || cacheStrategy.cdnCache) {
352
+ cacheStrategy.strategy = "cdn-managed";
353
+ }
354
+ else if (cacheStrategy.cacheControl) {
355
+ cacheStrategy.strategy = "custom";
356
+ }
357
+ // Intelligence summary
358
+ const intelligence = [];
359
+ if (server.headerOrderMismatch) {
360
+ intelligence.push(`Server header claims "${server.claimed}" but header ordering matches "${server.headerOrderMatch}" - possible header spoofing`);
361
+ }
362
+ if (cloudDetections.length > 0) {
363
+ intelligence.push(`Cloud infrastructure detected: ${cloudDetections.map((c) => c.provider).join(", ")}`);
364
+ }
365
+ if (wafDetections.length > 0) {
366
+ intelligence.push(`WAF detected: ${wafDetections.map((w) => `${w.name} (${w.vendor})`).join(", ")}`);
367
+ }
368
+ if (securityGrade === "F" || securityGrade === "E") {
369
+ intelligence.push(`Poor security header posture (grade ${securityGrade}) - ${securityHeaderCount}/${SECURITY_HEADERS.length} headers present`);
370
+ }
371
+ if (cookieFindings.some((c) => c.decoded)) {
372
+ intelligence.push("Infrastructure cookies leak internal IP addresses");
373
+ }
374
+ if (technologyDetections.length > 0) {
375
+ intelligence.push(`Technologies revealed via headers: ${technologyDetections.map((t) => t.value || t.technology).join(", ")}`);
376
+ }
377
+ const result = {
378
+ url: url ?? null,
379
+ server,
380
+ cloud: cloudDetections,
381
+ waf: wafDetections,
382
+ securityHeaders,
383
+ cookies: cookieFindings,
384
+ technology: technologyDetections,
385
+ cacheStrategy,
386
+ intelligence,
387
+ };
388
+ return json(result);
389
+ }
390
+ // ─── Tool 2: fp_analyze_html ───
391
+ async function analyzeHtml(args) {
392
+ const html = args.html;
393
+ const url = args.url;
394
+ const $ = cheerio.load(html);
395
+ // 1. Technology detection
396
+ const technologies = [];
397
+ // Collect all script src values
398
+ const scriptSrcs = [];
399
+ $("script[src]").each((_i, el) => {
400
+ const src = $(el).attr("src");
401
+ if (src)
402
+ scriptSrcs.push(src);
403
+ });
404
+ // Collect all inline script content
405
+ const inlineScripts = [];
406
+ $("script:not([src])").each((_i, el) => {
407
+ const content = $(el).html();
408
+ if (content)
409
+ inlineScripts.push(content);
410
+ });
411
+ const inlineScriptText = inlineScripts.join("\n");
412
+ // Collect all meta tags as raw HTML
413
+ const metaTags = [];
414
+ $("meta").each((_i, el) => {
415
+ const outerHtml = $.html(el);
416
+ if (outerHtml)
417
+ metaTags.push(outerHtml);
418
+ });
419
+ // Collect all HTML comments
420
+ const htmlComments = [];
421
+ const commentRegex = /<!--([\s\S]*?)-->/g;
422
+ let commentMatch;
423
+ while ((commentMatch = commentRegex.exec(html)) !== null) {
424
+ htmlComments.push(commentMatch[1].trim());
425
+ }
426
+ const htmlBody = $.html();
427
+ for (const tech of TECH_PATTERNS) {
428
+ let detected = false;
429
+ let matchType = "";
430
+ // Check script patterns
431
+ if (tech.detection.script) {
432
+ for (const pattern of tech.detection.script) {
433
+ for (const src of scriptSrcs) {
434
+ if (safeRegexTest(pattern, src)) {
435
+ detected = true;
436
+ matchType = "script-src";
437
+ break;
438
+ }
439
+ }
440
+ if (detected)
441
+ break;
442
+ }
443
+ }
444
+ // Check DOM patterns
445
+ if (!detected && tech.detection.dom) {
446
+ for (const pattern of tech.detection.dom) {
447
+ if (safeRegexTest(pattern, htmlBody)) {
448
+ detected = true;
449
+ matchType = "dom";
450
+ break;
451
+ }
452
+ }
453
+ }
454
+ // Check meta patterns
455
+ if (!detected && tech.detection.meta) {
456
+ for (const pattern of tech.detection.meta) {
457
+ for (const metaTag of metaTags) {
458
+ if (safeRegexTest(pattern, metaTag)) {
459
+ detected = true;
460
+ matchType = "meta";
461
+ break;
462
+ }
463
+ }
464
+ if (detected)
465
+ break;
466
+ }
467
+ }
468
+ // Check comment patterns
469
+ if (!detected && tech.detection.comment) {
470
+ for (const pattern of tech.detection.comment) {
471
+ for (const comment of htmlComments) {
472
+ if (safeRegexTest(pattern, comment)) {
473
+ detected = true;
474
+ matchType = "comment";
475
+ break;
476
+ }
477
+ }
478
+ if (detected)
479
+ break;
480
+ }
481
+ }
482
+ if (detected) {
483
+ // Try to extract version
484
+ let version = null;
485
+ if (tech.versionExtract) {
486
+ // Search across all available text sources
487
+ version =
488
+ safeRegexExtract(tech.versionExtract, html) ??
489
+ safeRegexExtract(tech.versionExtract, inlineScriptText);
490
+ // Also check script src values for version info
491
+ if (!version) {
492
+ for (const src of scriptSrcs) {
493
+ version = safeRegexExtract(tech.versionExtract, src);
494
+ if (version)
495
+ break;
496
+ }
497
+ }
498
+ }
499
+ // Check CVEs if version found
500
+ const matchedCves = [];
501
+ if (version && tech.cves) {
502
+ for (const cveEntry of tech.cves) {
503
+ // Simple version comparison: if the CVE applies to versions below a threshold
504
+ // we include it as potentially applicable (exact semver comparison is complex,
505
+ // so we flag all known CVEs for manual review when a version is detected)
506
+ matchedCves.push({
507
+ cve: cveEntry.cve,
508
+ description: cveEntry.description,
509
+ });
510
+ }
511
+ }
512
+ else if (!version && tech.cves) {
513
+ // If no version extracted but CVEs exist, flag them as "version unknown"
514
+ for (const cveEntry of tech.cves) {
515
+ matchedCves.push({
516
+ cve: cveEntry.cve,
517
+ description: `${cveEntry.description} (version not determined - manual check needed for ${cveEntry.version})`,
518
+ });
519
+ }
520
+ }
521
+ technologies.push({
522
+ name: tech.name,
523
+ category: tech.category,
524
+ version,
525
+ cves: matchedCves,
526
+ matchType,
527
+ });
528
+ }
529
+ }
530
+ // 2. Analytics detection
531
+ const analytics = [];
532
+ for (const ap of ANALYTICS_PATTERNS) {
533
+ let detected = false;
534
+ let matchType = "";
535
+ // Check script patterns
536
+ if (ap.patterns.script) {
537
+ for (const pattern of ap.patterns.script) {
538
+ for (const src of scriptSrcs) {
539
+ if (safeRegexTest(pattern, src)) {
540
+ detected = true;
541
+ matchType = "script-src";
542
+ break;
543
+ }
544
+ }
545
+ if (detected)
546
+ break;
547
+ }
548
+ }
549
+ // Check inline patterns
550
+ if (!detected && ap.patterns.inline) {
551
+ for (const pattern of ap.patterns.inline) {
552
+ if (safeRegexTest(pattern, inlineScriptText)) {
553
+ detected = true;
554
+ matchType = "inline-script";
555
+ break;
556
+ }
557
+ }
558
+ }
559
+ if (detected) {
560
+ // Extract tracking ID
561
+ let trackingId = null;
562
+ if (ap.idExtract) {
563
+ trackingId =
564
+ safeRegexExtract(ap.idExtract, html) ??
565
+ safeRegexExtract(ap.idExtract, inlineScriptText);
566
+ if (!trackingId) {
567
+ for (const src of scriptSrcs) {
568
+ trackingId = safeRegexExtract(ap.idExtract, src);
569
+ if (trackingId)
570
+ break;
571
+ }
572
+ }
573
+ }
574
+ analytics.push({
575
+ name: ap.name,
576
+ category: ap.category,
577
+ trackingId,
578
+ crossRefValue: ap.crossRefValue,
579
+ matchType,
580
+ });
581
+ }
582
+ }
583
+ // 3. Meta generator
584
+ const generatorMeta = $('meta[name="generator"]').attr("content") ?? null;
585
+ // 4. Framework indicators
586
+ const frameworkIndicators = [];
587
+ if (html.includes("__NEXT_DATA__") || $("script#__NEXT_DATA__").length > 0) {
588
+ frameworkIndicators.push({ framework: "Next.js", indicator: "__NEXT_DATA__" });
589
+ }
590
+ if (html.includes("__NUXT__") || html.includes("$nuxt")) {
591
+ frameworkIndicators.push({ framework: "Nuxt.js", indicator: "__NUXT__" });
592
+ }
593
+ if ($("[ng-version]").length > 0) {
594
+ const ngVersion = $("[ng-version]").attr("ng-version") ?? "unknown";
595
+ frameworkIndicators.push({ framework: "Angular", indicator: `ng-version="${ngVersion}"` });
596
+ }
597
+ if ($("[data-reactroot]").length > 0 || html.includes("data-reactroot")) {
598
+ frameworkIndicators.push({ framework: "React", indicator: "data-reactroot" });
599
+ }
600
+ if (/\bdata-v-[a-f0-9]+/.test(html)) {
601
+ frameworkIndicators.push({ framework: "Vue.js", indicator: "data-v-* scoped attributes" });
602
+ }
603
+ if (/\bclass="svelte-[a-z0-9]+"/i.test(html) || html.includes("svelte-")) {
604
+ frameworkIndicators.push({ framework: "Svelte", indicator: "svelte-* class names" });
605
+ }
606
+ if ($("astro-island").length > 0 || html.includes("astro-island")) {
607
+ frameworkIndicators.push({ framework: "Astro", indicator: "astro-island element" });
608
+ }
609
+ // 5. Source maps
610
+ const sourceMaps = [];
611
+ $("script[src]").each((_i, el) => {
612
+ const src = $(el).attr("src");
613
+ if (src && src.endsWith(".map")) {
614
+ sourceMaps.push(src);
615
+ }
616
+ });
617
+ $("link[href]").each((_i, el) => {
618
+ const href = $(el).attr("href");
619
+ if (href && href.endsWith(".map")) {
620
+ sourceMaps.push(href);
621
+ }
622
+ });
623
+ // Check inline scripts for sourceMappingURL
624
+ const sourceMappingRegex = /\/\/[#@]\s*sourceMappingURL\s*=\s*(\S+)/g;
625
+ let smMatch;
626
+ while ((smMatch = sourceMappingRegex.exec(html)) !== null) {
627
+ sourceMaps.push(smMatch[1]);
628
+ }
629
+ // 6. HTML comments analysis
630
+ const interestingComments = [];
631
+ for (const comment of htmlComments) {
632
+ // Skip empty or very short comments
633
+ if (comment.length < 3)
634
+ continue;
635
+ // Version strings
636
+ if (/v?\d+\.\d+\.\d+/.test(comment)) {
637
+ interestingComments.push({ content: comment.substring(0, 200), type: "version" });
638
+ }
639
+ // TODO/FIXME markers
640
+ else if (/\b(TODO|FIXME|HACK|BUG|XXX|WARN)\b/i.test(comment)) {
641
+ interestingComments.push({ content: comment.substring(0, 200), type: "developer-note" });
642
+ }
643
+ // Internal URLs
644
+ else if (/https?:\/\/(?:localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.)/i.test(comment)) {
645
+ interestingComments.push({ content: comment.substring(0, 200), type: "internal-url" });
646
+ }
647
+ // Developer notes with useful info
648
+ else if (comment.length > 20 && !/^\s*\[if\s/.test(comment) && !/^\s*<!\[endif\]/.test(comment)) {
649
+ // Skip IE conditional comments, only flag substantive comments
650
+ if (/\b(debug|config|password|secret|key|token|api|admin|internal|staging|test)\b/i.test(comment)) {
651
+ interestingComments.push({ content: comment.substring(0, 200), type: "potentially-sensitive" });
652
+ }
653
+ }
654
+ }
655
+ // 7. Hardcoded endpoints
656
+ const endpoints = [];
657
+ // Scan inline scripts for URLs
658
+ const urlRegex = /(?:["'`])((https?:\/\/[^\s"'`<>]+))/g;
659
+ let urlMatch;
660
+ const seenUrls = new Set();
661
+ while ((urlMatch = urlRegex.exec(inlineScriptText)) !== null) {
662
+ const foundUrl = urlMatch[1];
663
+ if (!seenUrls.has(foundUrl)) {
664
+ seenUrls.add(foundUrl);
665
+ let type = "external-url";
666
+ if (/\/api\//i.test(foundUrl))
667
+ type = "api-endpoint";
668
+ else if (/\/v[1-9]\//i.test(foundUrl))
669
+ type = "versioned-api";
670
+ else if (/\/graphql/i.test(foundUrl))
671
+ type = "graphql-endpoint";
672
+ endpoints.push({ url: foundUrl, type, source: "inline-script" });
673
+ }
674
+ }
675
+ // Scan inline scripts for IP addresses
676
+ const ipRegex = /(?:["'`])(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?)/g;
677
+ let ipMatch;
678
+ const seenIps = new Set();
679
+ while ((ipMatch = ipRegex.exec(inlineScriptText)) !== null) {
680
+ const foundIp = ipMatch[1];
681
+ if (!seenIps.has(foundIp) && !/^0\.0\.0\.0$/.test(foundIp) && !/^127\.0\.0\.1$/.test(foundIp)) {
682
+ seenIps.add(foundIp);
683
+ endpoints.push({ url: foundIp, type: "ip-address", source: "inline-script" });
684
+ }
685
+ }
686
+ // Scan inline scripts for API path patterns
687
+ const apiPathRegex = /(?:["'`])(\/(?:api|v[1-9]|v\d+|graphql)\/[^\s"'`<>]*)/g;
688
+ let apiMatch;
689
+ const seenPaths = new Set();
690
+ while ((apiMatch = apiPathRegex.exec(inlineScriptText)) !== null) {
691
+ const foundPath = apiMatch[1];
692
+ if (!seenPaths.has(foundPath)) {
693
+ seenPaths.add(foundPath);
694
+ let type = "api-path";
695
+ if (/\/graphql/i.test(foundPath))
696
+ type = "graphql-path";
697
+ endpoints.push({ url: foundPath, type, source: "inline-script" });
698
+ }
699
+ }
700
+ // 8. Build tool detection
701
+ let buildTool = null;
702
+ const buildIndicators = [];
703
+ if (html.includes("webpackJsonp") || html.includes("__webpack_require__") || html.includes("webpackChunk")) {
704
+ buildIndicators.push(html.includes("webpackJsonp") ? "webpackJsonp" : "", html.includes("__webpack_require__") ? "__webpack_require__" : "", html.includes("webpackChunk") ? "webpackChunk" : "");
705
+ buildTool = { name: "Webpack", indicators: buildIndicators.filter(Boolean) };
706
+ }
707
+ else if (html.includes("/@vite/") || html.includes("__vite_")) {
708
+ buildIndicators.push(html.includes("/@vite/") ? "/@vite/ import" : "", html.includes("__vite_") ? "__vite_ global" : "");
709
+ buildTool = { name: "Vite", indicators: buildIndicators.filter(Boolean) };
710
+ }
711
+ else if (html.includes("parcelRequire")) {
712
+ buildTool = { name: "Parcel", indicators: ["parcelRequire"] };
713
+ }
714
+ // Intelligence summary
715
+ const intelligence = [];
716
+ if (technologies.length > 0) {
717
+ intelligence.push(`${technologies.length} technologies detected: ${technologies.map((t) => t.name + (t.version ? ` v${t.version}` : "")).join(", ")}`);
718
+ }
719
+ const vulnerableTechs = technologies.filter((t) => t.cves.length > 0);
720
+ if (vulnerableTechs.length > 0) {
721
+ const totalCves = vulnerableTechs.reduce((sum, t) => sum + t.cves.length, 0);
722
+ intelligence.push(`${totalCves} potential CVEs found across ${vulnerableTechs.length} technologies`);
723
+ }
724
+ if (analytics.length > 0) {
725
+ const crossRefAnalytics = analytics.filter((a) => a.trackingId && a.crossRefValue);
726
+ if (crossRefAnalytics.length > 0) {
727
+ intelligence.push(`Cross-referenceable tracking IDs: ${crossRefAnalytics.map((a) => `${a.name}=${a.trackingId}`).join(", ")}`);
728
+ }
729
+ }
730
+ if (sourceMaps.length > 0) {
731
+ intelligence.push(`${sourceMaps.length} source maps found - may expose original source code`);
732
+ }
733
+ if (endpoints.length > 0) {
734
+ intelligence.push(`${endpoints.length} hardcoded endpoints found in inline scripts`);
735
+ }
736
+ if (interestingComments.length > 0) {
737
+ intelligence.push(`${interestingComments.length} interesting HTML comments found`);
738
+ }
739
+ const result = {
740
+ url: url ?? null,
741
+ technologies,
742
+ analytics,
743
+ generator: generatorMeta,
744
+ frameworkIndicators,
745
+ sourceMaps,
746
+ comments: interestingComments,
747
+ endpoints,
748
+ buildTool,
749
+ intelligence,
750
+ };
751
+ return json(result);
752
+ }
753
+ // ─── Tool 3: fp_analyze_banner ───
754
+ async function analyzeBanner(args) {
755
+ const banner = args.banner;
756
+ const port = args.port;
757
+ // 1. Match against SERVICE_BANNERS database
758
+ let matchedService = null;
759
+ for (const entry of SERVICE_BANNERS) {
760
+ try {
761
+ const regex = new RegExp(entry.pattern, "i");
762
+ const match = regex.exec(banner);
763
+ if (match) {
764
+ let version = null;
765
+ // Extract version from capture groups
766
+ if (entry.version_extract && match.length > 1) {
767
+ // Use the first non-undefined capture group
768
+ for (let i = 1; i < match.length; i++) {
769
+ if (match[i]) {
770
+ version = match[i];
771
+ break;
772
+ }
773
+ }
774
+ }
775
+ matchedService = {
776
+ service: entry.service,
777
+ software: entry.name,
778
+ version,
779
+ matchedPattern: entry.pattern,
780
+ };
781
+ break; // Use first match
782
+ }
783
+ }
784
+ catch {
785
+ // Invalid regex in pattern DB, skip
786
+ }
787
+ }
788
+ // 2. Port-based protocol heuristic as tiebreaker
789
+ let portProtocol = null;
790
+ if (port !== undefined && PORT_PROTOCOL_MAP[port]) {
791
+ portProtocol = PORT_PROTOCOL_MAP[port];
792
+ }
793
+ // 3. Common banner patterns not in the DB
794
+ const additionalDetections = [];
795
+ if (/^RFB\s+\d+\.\d+/i.test(banner)) {
796
+ additionalDetections.push({
797
+ pattern: "RFB protocol header",
798
+ protocol: "vnc",
799
+ software: "VNC Server",
800
+ });
801
+ }
802
+ if (/^\*\s+OK/i.test(banner)) {
803
+ additionalDetections.push({
804
+ pattern: "* OK (IMAP greeting)",
805
+ protocol: "imap",
806
+ software: "IMAP Server",
807
+ });
808
+ }
809
+ if (/^\+OK/i.test(banner)) {
810
+ additionalDetections.push({
811
+ pattern: "+OK (POP3 greeting)",
812
+ protocol: "pop3",
813
+ software: "POP3 Server",
814
+ });
815
+ }
816
+ if (/^HTTP\/\d+\.\d+/i.test(banner)) {
817
+ additionalDetections.push({
818
+ pattern: "HTTP/ protocol header",
819
+ protocol: "http",
820
+ software: "HTTP Server",
821
+ });
822
+ }
823
+ // 4. OS hints from banner
824
+ const osHints = [];
825
+ if (/ubuntu/i.test(banner))
826
+ osHints.push("Ubuntu");
827
+ if (/debian/i.test(banner))
828
+ osHints.push("Debian");
829
+ if (/centos/i.test(banner))
830
+ osHints.push("CentOS");
831
+ if (/red\s*hat/i.test(banner))
832
+ osHints.push("Red Hat");
833
+ if (/fedora/i.test(banner))
834
+ osHints.push("Fedora");
835
+ if (/alpine/i.test(banner))
836
+ osHints.push("Alpine Linux");
837
+ if (/freebsd/i.test(banner))
838
+ osHints.push("FreeBSD");
839
+ if (/windows/i.test(banner))
840
+ osHints.push("Windows");
841
+ if (/win32|win64/i.test(banner))
842
+ osHints.push("Windows");
843
+ if (/darwin|macos/i.test(banner))
844
+ osHints.push("macOS");
845
+ if (/suse/i.test(banner))
846
+ osHints.push("SUSE Linux");
847
+ if (/arch\s*linux/i.test(banner))
848
+ osHints.push("Arch Linux");
849
+ if (/amazon\s*linux/i.test(banner))
850
+ osHints.push("Amazon Linux");
851
+ // Determine final service identification
852
+ const service = matchedService?.service ?? additionalDetections[0]?.protocol ?? portProtocol ?? "unknown";
853
+ const software = matchedService?.software ?? additionalDetections[0]?.software ?? "Unknown";
854
+ const version = matchedService?.version ?? null;
855
+ // Confidence scoring
856
+ let confidence;
857
+ if (matchedService) {
858
+ confidence = version ? "high" : "medium";
859
+ }
860
+ else if (additionalDetections.length > 0) {
861
+ confidence = "medium";
862
+ }
863
+ else if (portProtocol) {
864
+ confidence = "low";
865
+ }
866
+ else {
867
+ confidence = "low";
868
+ }
869
+ // Protocol determination
870
+ const protocol = matchedService?.service ?? additionalDetections[0]?.protocol ?? portProtocol ?? "unknown";
871
+ // Intelligence summary
872
+ const intelligence = [];
873
+ if (software !== "Unknown") {
874
+ intelligence.push(`Identified as ${software}${version ? ` version ${version}` : ""}`);
875
+ }
876
+ if (osHints.length > 0) {
877
+ intelligence.push(`OS hint: ${osHints.join(", ")}`);
878
+ }
879
+ if (matchedService && portProtocol && matchedService.service !== portProtocol) {
880
+ intelligence.push(`Service "${matchedService.service}" running on non-standard port ${port} (expected ${portProtocol})`);
881
+ }
882
+ if (version) {
883
+ intelligence.push("Version disclosed in banner - check for known vulnerabilities");
884
+ }
885
+ if (confidence === "low") {
886
+ intelligence.push("Low confidence identification - banner may be custom or obfuscated");
887
+ }
888
+ const result = {
889
+ banner: banner.substring(0, 500), // Truncate very long banners
890
+ port: port ?? null,
891
+ service,
892
+ software,
893
+ version,
894
+ osHint: osHints.length > 0 ? osHints.join(", ") : null,
895
+ protocol,
896
+ matchedPattern: matchedService?.matchedPattern ?? additionalDetections[0]?.pattern ?? null,
897
+ additionalDetections: additionalDetections.length > 0 && !matchedService ? additionalDetections : [],
898
+ confidence,
899
+ intelligence,
900
+ };
901
+ return json(result);
902
+ }
903
+ // ─── Tool Definitions ───
904
+ export const passiveTools = [
905
+ {
906
+ name: "fp_analyze_headers",
907
+ description: "Passive HTTP header fingerprinting. Analyzes pre-collected response headers for server identification (claimed vs header-order-based), CDN/cloud provider detection, WAF detection, security header audit with grading (A-F), infrastructure cookie decoding (F5 BIG-IP, NetScaler, AWS ALB), technology stack detection, and cache strategy analysis. No network requests are made.",
908
+ schema: {
909
+ headers: z
910
+ .record(z.string())
911
+ .describe("HTTP response headers as key-value pairs"),
912
+ url: z
913
+ .string()
914
+ .optional()
915
+ .describe("Original URL for context (not fetched)"),
916
+ },
917
+ execute: analyzeHeaders,
918
+ },
919
+ {
920
+ name: "fp_analyze_html",
921
+ description: "Passive HTML source fingerprinting. Analyzes raw HTML for client-side technology detection with version extraction and CVE cross-referencing, analytics/tracking code identification with ID extraction for cross-domain correlation, framework indicators (Next.js, Nuxt, Angular, React, Vue, Svelte, Astro), source map exposure, interesting HTML comments, hardcoded API endpoints and IP addresses, and build tool detection. No network requests are made.",
922
+ schema: {
923
+ html: z.string().describe("Raw HTML source to analyze"),
924
+ url: z
925
+ .string()
926
+ .optional()
927
+ .describe("Original URL for context (not fetched)"),
928
+ },
929
+ execute: analyzeHtml,
930
+ },
931
+ {
932
+ name: "fp_analyze_banner",
933
+ description: "Passive service banner fingerprinting. Analyzes a raw service banner string to identify the service software, extract version numbers, detect OS hints, and determine protocol. Matches against a database of known banner patterns for SSH, FTP, SMTP, HTTP, MySQL, PostgreSQL, Redis, MongoDB, Memcached, IMAP, POP3, and DNS. Uses port number as a tiebreaker for ambiguous banners. No network requests are made.",
934
+ schema: {
935
+ banner: z.string().describe("Raw service banner string to analyze"),
936
+ port: z
937
+ .number()
938
+ .optional()
939
+ .describe("Port number for protocol heuristics"),
940
+ },
941
+ execute: analyzeBanner,
942
+ },
943
+ ];
944
+ //# sourceMappingURL=index.js.map