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,767 @@
1
+ import { z } from "zod";
2
+ import { resolve4, resolveCname } from "node:dns/promises";
3
+ import { json } from "../types/index.js";
4
+ import { RateLimiter } from "../utils/rate-limiter.js";
5
+ import { TTLCache } from "../utils/cache.js";
6
+ // ─── Rate Limiter & Cache ───
7
+ const limiter = new RateLimiter(500); // slower for WAF — avoid triggering blocks
8
+ const cache = new TTLCache(300_000);
9
+ const WAF_HEADER_SIGNATURES = {
10
+ "cf-ray": { name: "Cloudflare", vendor: "Cloudflare, Inc." },
11
+ "x-sucuri-id": { name: "Sucuri WAF", vendor: "Sucuri (GoDaddy)" },
12
+ "x-amzn-requestid": { name: "AWS WAF", vendor: "Amazon Web Services" },
13
+ "x-akamai-transformed": { name: "Akamai", vendor: "Akamai Technologies" },
14
+ "x-sigsci-tags": { name: "Signal Sciences", vendor: "Signal Sciences (Fastly)" },
15
+ "x-sigsci-decisionscore": { name: "Signal Sciences", vendor: "Signal Sciences (Fastly)" },
16
+ "x-sigsci-requestid": { name: "Signal Sciences", vendor: "Signal Sciences (Fastly)" },
17
+ "x-wallarm-waf-check": { name: "Wallarm", vendor: "Wallarm" },
18
+ };
19
+ const WAF_SERVER_SIGNATURES = {
20
+ cloudflare: { name: "Cloudflare", vendor: "Cloudflare, Inc." },
21
+ akamaighost: { name: "Akamai", vendor: "Akamai Technologies" },
22
+ "ddos-guard": { name: "DDoS-Guard", vendor: "DDoS-Guard" },
23
+ "nginx-wallarm": { name: "Wallarm", vendor: "Wallarm" },
24
+ };
25
+ const WAF_COOKIE_SIGNATURES = {
26
+ "cf-chl-bypass": { name: "Cloudflare", vendor: "Cloudflare, Inc." },
27
+ ___utmvc: { name: "Imperva Incapsula", vendor: "Imperva" },
28
+ fortiwafsid: { name: "FortiWeb", vendor: "Fortinet" },
29
+ barra_counter_session: { name: "Barracuda WAF", vendor: "Barracuda Networks" },
30
+ };
31
+ const WAF_BODY_SIGNATURES = [
32
+ { pattern: /Attention Required.*Cloudflare|cf-error-details|Ray ID:/i, name: "Cloudflare", vendor: "Cloudflare, Inc." },
33
+ { pattern: /Request blocked|automated access.*denied.*AWS/i, name: "AWS WAF", vendor: "Amazon Web Services" },
34
+ { pattern: /ModSecurity|Mod_Security|mod_security module/i, name: "ModSecurity", vendor: "Trustwave / OWASP" },
35
+ { pattern: /Incapsula incident|Powered By Incapsula|_Incapsula_Resource/i, name: "Imperva Incapsula", vendor: "Imperva" },
36
+ { pattern: /Reference #18\.[0-9a-f]+|AkamaiGHost/i, name: "Akamai", vendor: "Akamai Technologies" },
37
+ { pattern: /The requested URL was rejected|Your support ID is:/i, name: "F5 BIG-IP ASM", vendor: "F5 Networks" },
38
+ { pattern: /Sucuri Website Firewall|Access Denied.*Sucuri/i, name: "Sucuri WAF", vendor: "Sucuri (GoDaddy)" },
39
+ { pattern: /Barracuda Networks|You have been blocked.*barracuda/i, name: "Barracuda WAF", vendor: "Barracuda Networks" },
40
+ { pattern: /DDoS-Guard|ddos-guard\.net/i, name: "DDoS-Guard", vendor: "DDoS-Guard" },
41
+ { pattern: /Generated by Wordfence|wordfence\.com|wfwaf-authcookie-/i, name: "Wordfence", vendor: "Defiant Inc." },
42
+ { pattern: /FortiWeb|fortigate|\.fgd_icon/i, name: "FortiWeb", vendor: "Fortinet" },
43
+ { pattern: /wallarm|nginx-wallarm/i, name: "Wallarm", vendor: "Wallarm" },
44
+ { pattern: /Signal Sciences/i, name: "Signal Sciences", vendor: "Signal Sciences (Fastly)" },
45
+ ];
46
+ // ─── CDN Signature Maps ───
47
+ const CDN_HEADER_SIGNATURES = {
48
+ "cf-ray": "Cloudflare",
49
+ "x-amz-cf-id": "Amazon CloudFront",
50
+ "x-akamai-transformed": "Akamai",
51
+ "x-fastly-request-id": "Fastly",
52
+ };
53
+ const CDN_SERVER_SIGNATURES = {
54
+ cloudflare: "Cloudflare",
55
+ amazons3: "Amazon S3",
56
+ akamaihost: "Akamai",
57
+ akamainetworkhttpmonitor: "Akamai",
58
+ varnish: "Varnish (generic CDN cache)",
59
+ };
60
+ const CDN_CNAME_PATTERNS = {
61
+ "cloudfront.net": "Amazon CloudFront",
62
+ "edgekey.net": "Akamai",
63
+ "edgesuite.net": "Akamai",
64
+ "akamaiedge.net": "Akamai",
65
+ "fastly.net": "Fastly",
66
+ "cloudflare.net": "Cloudflare",
67
+ "azureedge.net": "Azure CDN",
68
+ "stackpathdns.com": "StackPath",
69
+ "impervadns.net": "Imperva",
70
+ "sucuri.net": "Sucuri",
71
+ };
72
+ // ─── Helpers ───
73
+ function parseCookies(headers) {
74
+ const cookies = new Map();
75
+ const setCookie = headers.getSetCookie?.() ?? [];
76
+ for (const raw of setCookie) {
77
+ const name = raw.split("=")[0]?.trim();
78
+ if (name)
79
+ cookies.set(name.toLowerCase(), raw);
80
+ }
81
+ return cookies;
82
+ }
83
+ function extractHostname(url) {
84
+ try {
85
+ return new URL(url).hostname;
86
+ }
87
+ catch {
88
+ return url;
89
+ }
90
+ }
91
+ async function safeFetch(url, options) {
92
+ try {
93
+ const controller = new AbortController();
94
+ const timeout = setTimeout(() => controller.abort(), 10_000);
95
+ const res = await fetch(url, {
96
+ ...options,
97
+ signal: controller.signal,
98
+ redirect: "follow",
99
+ });
100
+ clearTimeout(timeout);
101
+ return res;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ async function safeDnsResolve4(hostname) {
108
+ try {
109
+ return await resolve4(hostname);
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ }
115
+ async function safeDnsResolveCname(hostname) {
116
+ try {
117
+ return await resolveCname(hostname);
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ }
123
+ const ATTACK_PAYLOADS = [
124
+ {
125
+ type: "xss",
126
+ label: "XSS (reflected)",
127
+ build: (base) => ({
128
+ url: `${base}?q=${encodeURIComponent("<script>alert(1)</script>")}`,
129
+ }),
130
+ },
131
+ {
132
+ type: "sqli",
133
+ label: "SQL Injection",
134
+ build: (base) => ({
135
+ url: `${base}?id=${encodeURIComponent("' OR '1'='1")}`,
136
+ }),
137
+ },
138
+ {
139
+ type: "path_traversal",
140
+ label: "Path Traversal",
141
+ build: (base) => {
142
+ const u = new URL(base);
143
+ u.pathname = "/../../../etc/passwd";
144
+ return { url: u.toString() };
145
+ },
146
+ },
147
+ {
148
+ type: "scanner_ua",
149
+ label: "Scanner User-Agent",
150
+ build: (base) => ({
151
+ url: base,
152
+ headers: { "User-Agent": "sqlmap/1.0" },
153
+ }),
154
+ },
155
+ {
156
+ type: "cmdi",
157
+ label: "Command Injection",
158
+ build: (base) => ({
159
+ url: `${base}?cmd=${encodeURIComponent("; cat /etc/passwd")}`,
160
+ }),
161
+ },
162
+ ];
163
+ // ─── Tool 1: waf_detect ───
164
+ const wafDetect = {
165
+ name: "waf_detect",
166
+ description: "Identify Web Application Firewalls by probing with common attack payloads (XSS, SQLi, path traversal, scanner UA, command injection) " +
167
+ "and matching block responses against known WAF signatures. Detects Cloudflare, AWS WAF, ModSecurity, Imperva, Akamai, F5 ASM, Fortinet, " +
168
+ "Sucuri, Barracuda, Wallarm, Signal Sciences, DDoS-Guard, Wordfence, and more.",
169
+ schema: {
170
+ url: z.string().url().describe("URL to probe for WAF detection"),
171
+ },
172
+ async execute(args) {
173
+ const url = args.url;
174
+ const cacheKey = `waf_detect:${url}`;
175
+ const cached = cache.get(cacheKey);
176
+ if (cached)
177
+ return json(cached);
178
+ await limiter.acquire();
179
+ const detections = new Map();
180
+ // Send each attack payload and analyze responses
181
+ for (const payload of ATTACK_PAYLOADS) {
182
+ const { url: probeUrl, headers: extraHeaders } = payload.build(url);
183
+ const res = await safeFetch(probeUrl, {
184
+ headers: {
185
+ "User-Agent": extraHeaders?.["User-Agent"] ??
186
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
187
+ Accept: "text/html,application/xhtml+xml,*/*",
188
+ ...extraHeaders,
189
+ },
190
+ });
191
+ if (!res)
192
+ continue;
193
+ const status = res.status;
194
+ const body = await res.text().catch(() => "");
195
+ const cookies = parseCookies(res.headers);
196
+ // Check response headers
197
+ for (const [header, sig] of Object.entries(WAF_HEADER_SIGNATURES)) {
198
+ if (res.headers.has(header)) {
199
+ if (!detections.has(sig.name)) {
200
+ detections.set(sig.name, {
201
+ name: sig.name,
202
+ vendor: sig.vendor,
203
+ confidence: 0.8,
204
+ indicators: [],
205
+ });
206
+ }
207
+ detections.get(sig.name).indicators.push(`Header '${header}' present (${payload.type})`);
208
+ }
209
+ }
210
+ // Check Server header
211
+ const server = res.headers.get("server")?.toLowerCase() ?? "";
212
+ for (const [pattern, sig] of Object.entries(WAF_SERVER_SIGNATURES)) {
213
+ if (server.includes(pattern)) {
214
+ if (!detections.has(sig.name)) {
215
+ detections.set(sig.name, {
216
+ name: sig.name,
217
+ vendor: sig.vendor,
218
+ confidence: 0.85,
219
+ indicators: [],
220
+ });
221
+ }
222
+ detections.get(sig.name).indicators.push(`Server header matches '${pattern}' (${payload.type})`);
223
+ }
224
+ }
225
+ // Check cookies
226
+ for (const [cookiePattern, sig] of Object.entries(WAF_COOKIE_SIGNATURES)) {
227
+ for (const [cookieName] of cookies) {
228
+ if (cookieName.includes(cookiePattern.toLowerCase())) {
229
+ if (!detections.has(sig.name)) {
230
+ detections.set(sig.name, {
231
+ name: sig.name,
232
+ vendor: sig.vendor,
233
+ confidence: 0.85,
234
+ indicators: [],
235
+ });
236
+ }
237
+ detections.get(sig.name).indicators.push(`Cookie '${cookieName}' matches '${cookiePattern}' (${payload.type})`);
238
+ }
239
+ }
240
+ }
241
+ // Check for incap_ses_* cookie pattern (Imperva)
242
+ for (const [cookieName] of cookies) {
243
+ if (/^incap_ses_/i.test(cookieName)) {
244
+ if (!detections.has("Imperva Incapsula")) {
245
+ detections.set("Imperva Incapsula", {
246
+ name: "Imperva Incapsula",
247
+ vendor: "Imperva",
248
+ confidence: 0.9,
249
+ indicators: [],
250
+ });
251
+ }
252
+ detections.get("Imperva Incapsula").indicators.push(`Cookie '${cookieName}' matches Imperva pattern (${payload.type})`);
253
+ }
254
+ }
255
+ // Check for TS* cookie prefix (F5 ASM)
256
+ for (const [cookieName] of cookies) {
257
+ if (/^TS[0-9a-f]{6,8}$/i.test(cookieName)) {
258
+ if (!detections.has("F5 BIG-IP ASM")) {
259
+ detections.set("F5 BIG-IP ASM", {
260
+ name: "F5 BIG-IP ASM",
261
+ vendor: "F5 Networks",
262
+ confidence: 0.85,
263
+ indicators: [],
264
+ });
265
+ }
266
+ detections.get("F5 BIG-IP ASM").indicators.push(`Cookie '${cookieName}' matches F5 TS* pattern (${payload.type})`);
267
+ }
268
+ }
269
+ // Check for wfwaf-authcookie pattern (Wordfence)
270
+ for (const [cookieName] of cookies) {
271
+ if (/wfwaf-authcookie/i.test(cookieName)) {
272
+ if (!detections.has("Wordfence")) {
273
+ detections.set("Wordfence", {
274
+ name: "Wordfence",
275
+ vendor: "Defiant Inc.",
276
+ confidence: 0.9,
277
+ indicators: [],
278
+ });
279
+ }
280
+ detections.get("Wordfence").indicators.push(`Cookie '${cookieName}' matches Wordfence pattern (${payload.type})`);
281
+ }
282
+ }
283
+ // Check body content
284
+ if (body && (status === 403 || status === 406 || status === 503)) {
285
+ for (const sig of WAF_BODY_SIGNATURES) {
286
+ if (sig.pattern.test(body)) {
287
+ if (!detections.has(sig.name)) {
288
+ detections.set(sig.name, {
289
+ name: sig.name,
290
+ vendor: sig.vendor,
291
+ confidence: 0.9,
292
+ indicators: [],
293
+ });
294
+ }
295
+ detections.get(sig.name).indicators.push(`Body matches '${sig.name}' pattern (status ${status}, ${payload.type})`);
296
+ }
297
+ }
298
+ }
299
+ await limiter.acquire();
300
+ }
301
+ // Deduplicate indicators per detection
302
+ for (const [, det] of detections) {
303
+ det.indicators = [...new Set(det.indicators)];
304
+ // Increase confidence based on number of indicators
305
+ det.confidence = Math.min(1.0, det.confidence + det.indicators.length * 0.02);
306
+ }
307
+ const result = {
308
+ url,
309
+ wafs_detected: Array.from(detections.values()),
310
+ total_detections: detections.size,
311
+ payloads_tested: ATTACK_PAYLOADS.length,
312
+ };
313
+ cache.set(cacheKey, result);
314
+ return json(result);
315
+ },
316
+ };
317
+ // ─── Tool 2: waf_cdn_detect ───
318
+ const wafCdnDetect = {
319
+ name: "waf_cdn_detect",
320
+ description: "Detect CDN provider for a given URL by analyzing response headers (cf-ray, x-amz-cf-id, x-akamai-transformed, x-fastly-request-id), " +
321
+ "Server header, Via header, DNS resolution (anycast detection via multiple IPs), and CNAME chain analysis " +
322
+ "(cloudfront.net, edgekey.net, fastly.net, cloudflare.net).",
323
+ schema: {
324
+ url: z.string().url().describe("URL to detect CDN provider"),
325
+ },
326
+ async execute(args) {
327
+ const url = args.url;
328
+ const cacheKey = `waf_cdn_detect:${url}`;
329
+ const cached = cache.get(cacheKey);
330
+ if (cached)
331
+ return json(cached);
332
+ await limiter.acquire();
333
+ const hostname = extractHostname(url);
334
+ const cdnIndicators = [];
335
+ // 1. HTTP response header analysis
336
+ const res = await safeFetch(url, {
337
+ headers: {
338
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
339
+ Accept: "text/html,application/xhtml+xml,*/*",
340
+ },
341
+ });
342
+ if (res) {
343
+ // Check CDN-specific headers
344
+ for (const [header, provider] of Object.entries(CDN_HEADER_SIGNATURES)) {
345
+ if (res.headers.has(header)) {
346
+ cdnIndicators.push({ provider, source: `Header: ${header}` });
347
+ }
348
+ }
349
+ // Check x-cache header
350
+ const xCache = res.headers.get("x-cache");
351
+ if (xCache && /HIT|MISS/i.test(xCache)) {
352
+ cdnIndicators.push({ provider: "CDN (generic)", source: `x-cache: ${xCache}` });
353
+ }
354
+ // Check Server header
355
+ const server = res.headers.get("server")?.toLowerCase() ?? "";
356
+ for (const [pattern, provider] of Object.entries(CDN_SERVER_SIGNATURES)) {
357
+ if (server.includes(pattern)) {
358
+ cdnIndicators.push({ provider, source: `Server: ${server}` });
359
+ }
360
+ }
361
+ // Check Via header
362
+ const via = res.headers.get("via");
363
+ if (via) {
364
+ cdnIndicators.push({ provider: "CDN (via header)", source: `Via: ${via}` });
365
+ if (/cloudfront/i.test(via))
366
+ cdnIndicators.push({ provider: "Amazon CloudFront", source: `Via contains cloudfront` });
367
+ if (/varnish/i.test(via))
368
+ cdnIndicators.push({ provider: "Varnish CDN", source: `Via contains varnish` });
369
+ if (/akamai/i.test(via))
370
+ cdnIndicators.push({ provider: "Akamai", source: `Via contains akamai` });
371
+ }
372
+ }
373
+ // 2. DNS resolution — multiple IPs = anycast = likely CDN
374
+ const ips = await safeDnsResolve4(hostname);
375
+ const isAnycast = ips.length > 2;
376
+ // 3. CNAME chain analysis
377
+ const cnames = await safeDnsResolveCname(hostname);
378
+ for (const cname of cnames) {
379
+ const cnameLower = cname.toLowerCase();
380
+ for (const [pattern, provider] of Object.entries(CDN_CNAME_PATTERNS)) {
381
+ if (cnameLower.includes(pattern)) {
382
+ cdnIndicators.push({ provider, source: `CNAME: ${cname}` });
383
+ }
384
+ }
385
+ }
386
+ // Deduplicate by provider
387
+ const providerMap = new Map();
388
+ for (const ind of cdnIndicators) {
389
+ if (!providerMap.has(ind.provider))
390
+ providerMap.set(ind.provider, []);
391
+ providerMap.get(ind.provider).push(ind.source);
392
+ }
393
+ const detectedCdns = Array.from(providerMap.entries()).map(([provider, sources]) => ({
394
+ provider,
395
+ sources: [...new Set(sources)],
396
+ confidence: Math.min(1.0, 0.6 + sources.length * 0.1),
397
+ }));
398
+ const result = {
399
+ url,
400
+ hostname,
401
+ cdns_detected: detectedCdns,
402
+ dns: {
403
+ a_records: ips,
404
+ cname_records: cnames,
405
+ is_anycast: isAnycast,
406
+ ip_count: ips.length,
407
+ },
408
+ behind_cdn: detectedCdns.length > 0 || isAnycast,
409
+ };
410
+ cache.set(cacheKey, result);
411
+ return json(result);
412
+ },
413
+ };
414
+ // ─── Tool 3: waf_origin ───
415
+ const wafOrigin = {
416
+ name: "waf_origin",
417
+ description: "Attempt to discover the origin IP behind a CDN-protected domain. Techniques: subdomain DNS enumeration " +
418
+ "(mail, ftp, direct, origin, cpanel, webmail, smtp subdomains), certificate SAN cross-reference via crt.sh, " +
419
+ "HTML/JS source analysis for hardcoded IPs, and content comparison to verify discovered IPs serve the same site.",
420
+ schema: {
421
+ domain: z.string().describe("Domain to find origin IP behind CDN"),
422
+ },
423
+ async execute(args) {
424
+ const domain = args.domain;
425
+ const cacheKey = `waf_origin:${domain}`;
426
+ const cached = cache.get(cacheKey);
427
+ if (cached)
428
+ return json(cached);
429
+ await limiter.acquire();
430
+ const candidateIps = [];
431
+ // 1. Subdomain DNS enumeration — these often bypass CDN
432
+ const subdomains = [
433
+ "mail",
434
+ "ftp",
435
+ "direct",
436
+ "origin",
437
+ "cpanel",
438
+ "webmail",
439
+ "smtp",
440
+ "admin",
441
+ "dev",
442
+ "staging",
443
+ "api",
444
+ ];
445
+ for (const sub of subdomains) {
446
+ const fqdn = `${sub}.${domain}`;
447
+ const ips = await safeDnsResolve4(fqdn);
448
+ for (const ip of ips) {
449
+ candidateIps.push({ ip, source: `DNS A record: ${fqdn}` });
450
+ }
451
+ }
452
+ // Resolve the main domain IPs for comparison
453
+ const mainIps = await safeDnsResolve4(domain);
454
+ // 2. Certificate SAN cross-reference via crt.sh
455
+ const certIps = [];
456
+ try {
457
+ await limiter.acquire();
458
+ const crtRes = await safeFetch(`https://crt.sh/?q=${encodeURIComponent(domain)}&output=json`, {
459
+ headers: { Accept: "application/json" },
460
+ });
461
+ if (crtRes && crtRes.ok) {
462
+ const crtData = (await crtRes.json());
463
+ // Collect unique hostnames from certificate SANs
464
+ const certHostnames = new Set();
465
+ for (const entry of crtData.slice(0, 50)) {
466
+ const names = entry.name_value?.split("\n") ?? [];
467
+ for (const name of names) {
468
+ const trimmed = name.trim().replace(/^\*\./, "");
469
+ if (trimmed &&
470
+ trimmed !== domain &&
471
+ !trimmed.startsWith("*") &&
472
+ trimmed.endsWith(domain)) {
473
+ certHostnames.add(trimmed);
474
+ }
475
+ }
476
+ }
477
+ // Resolve a subset of discovered hostnames
478
+ let resolved = 0;
479
+ for (const certHost of certHostnames) {
480
+ if (resolved >= 10)
481
+ break;
482
+ const ips = await safeDnsResolve4(certHost);
483
+ for (const ip of ips) {
484
+ candidateIps.push({ ip, source: `crt.sh SAN: ${certHost}` });
485
+ }
486
+ resolved++;
487
+ }
488
+ }
489
+ }
490
+ catch {
491
+ // crt.sh may be unavailable
492
+ }
493
+ // 3. HTML/JS source analysis — look for hardcoded IPs
494
+ try {
495
+ await limiter.acquire();
496
+ const pageRes = await safeFetch(`https://${domain}`, {
497
+ headers: {
498
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
499
+ Accept: "text/html,*/*",
500
+ },
501
+ });
502
+ if (pageRes && pageRes.ok) {
503
+ const html = await pageRes.text();
504
+ // Match IPv4 addresses in HTML/JS content
505
+ const ipv4Regex = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
506
+ let match;
507
+ const foundInSource = new Set();
508
+ while ((match = ipv4Regex.exec(html)) !== null) {
509
+ const ip = match[1];
510
+ // Filter out common non-routable and version-like patterns
511
+ if (!ip.startsWith("0.") &&
512
+ !ip.startsWith("127.") &&
513
+ !ip.startsWith("255.") &&
514
+ !ip.startsWith("10.") &&
515
+ !ip.startsWith("192.168.") &&
516
+ !/^172\.(1[6-9]|2\d|3[01])\./.test(ip) &&
517
+ // Skip version-like patterns (e.g., 1.0.0.0, 2.3.1.4)
518
+ !/^\d+\.\d+\.\d+\.\d+$/.test(ip) === false) {
519
+ foundInSource.add(ip);
520
+ }
521
+ }
522
+ for (const ip of foundInSource) {
523
+ candidateIps.push({ ip, source: "Hardcoded IP in HTML/JS source" });
524
+ }
525
+ }
526
+ }
527
+ catch {
528
+ // Page fetch may fail
529
+ }
530
+ // 4. Filter candidates — remove IPs that match the CDN-fronted IPs
531
+ const mainIpSet = new Set(mainIps);
532
+ const uniqueCandidates = new Map();
533
+ for (const { ip, source } of candidateIps) {
534
+ if (!mainIpSet.has(ip)) {
535
+ if (!uniqueCandidates.has(ip))
536
+ uniqueCandidates.set(ip, []);
537
+ uniqueCandidates.get(ip).push(source);
538
+ }
539
+ }
540
+ // 5. Content verification — check if candidate IPs serve the same content
541
+ const verified = [];
542
+ let checked = 0;
543
+ for (const [ip, sources] of uniqueCandidates) {
544
+ if (checked >= 5)
545
+ break; // Limit verification requests
546
+ let servesSameContent = false;
547
+ try {
548
+ await limiter.acquire();
549
+ const verifyRes = await safeFetch(`http://${ip}`, {
550
+ headers: {
551
+ Host: domain,
552
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
553
+ },
554
+ });
555
+ if (verifyRes && (verifyRes.ok || verifyRes.status === 301 || verifyRes.status === 302)) {
556
+ const body = await verifyRes.text().catch(() => "");
557
+ // Simple heuristic: check if the domain name appears in the response
558
+ servesSameContent = body.toLowerCase().includes(domain.toLowerCase());
559
+ }
560
+ }
561
+ catch {
562
+ // Connection to IP may fail
563
+ }
564
+ verified.push({
565
+ ip,
566
+ sources: [...new Set(sources)],
567
+ serves_same_content: servesSameContent,
568
+ });
569
+ checked++;
570
+ }
571
+ // Remaining unverified candidates
572
+ for (const [ip, sources] of uniqueCandidates) {
573
+ if (!verified.some((v) => v.ip === ip)) {
574
+ verified.push({
575
+ ip,
576
+ sources: [...new Set(sources)],
577
+ serves_same_content: false,
578
+ });
579
+ }
580
+ }
581
+ const result = {
582
+ domain,
583
+ main_domain_ips: mainIps,
584
+ origin_candidates: verified.sort((a, b) => a.serves_same_content === b.serves_same_content ? 0 : a.serves_same_content ? -1 : 1),
585
+ total_candidates: verified.length,
586
+ likely_origins: verified.filter((v) => v.serves_same_content).map((v) => v.ip),
587
+ };
588
+ cache.set(cacheKey, result);
589
+ return json(result);
590
+ },
591
+ };
592
+ // ─── Tool 4: waf_fingerprint ───
593
+ const wafFingerprint = {
594
+ name: "waf_fingerprint",
595
+ description: "Deep WAF technology fingerprinting from block page analysis. Sends requests that trigger WAF blocks (XSS, SQLi, path traversal, " +
596
+ "command injection), parses block page HTML for vendor indicators (support IDs, reference numbers, custom error pages), " +
597
+ "determines WAF mode (block, detect-only, or CAPTCHA challenge), and maps WAF rule coverage across payload types.",
598
+ schema: {
599
+ url: z.string().url().describe("URL to fingerprint WAF technology"),
600
+ },
601
+ async execute(args) {
602
+ const url = args.url;
603
+ const cacheKey = `waf_fingerprint:${url}`;
604
+ const cached = cache.get(cacheKey);
605
+ if (cached)
606
+ return json(cached);
607
+ await limiter.acquire();
608
+ // First, get a baseline response
609
+ const baselineRes = await safeFetch(url, {
610
+ headers: {
611
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
612
+ Accept: "text/html,*/*",
613
+ },
614
+ });
615
+ const baselineStatus = baselineRes?.status ?? 0;
616
+ const baselineBody = baselineRes ? await baselineRes.text().catch(() => "") : "";
617
+ // Test each payload type
618
+ const payloadResults = [];
619
+ for (const payload of ATTACK_PAYLOADS) {
620
+ await limiter.acquire();
621
+ const { url: probeUrl, headers: extraHeaders } = payload.build(url);
622
+ const res = await safeFetch(probeUrl, {
623
+ headers: {
624
+ "User-Agent": extraHeaders?.["User-Agent"] ??
625
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
626
+ Accept: "text/html,*/*",
627
+ ...extraHeaders,
628
+ },
629
+ });
630
+ if (!res) {
631
+ payloadResults.push({
632
+ type: payload.type,
633
+ label: payload.label,
634
+ status: 0,
635
+ blocked: false,
636
+ mode: "passthrough",
637
+ waf_indicators: ["Connection failed"],
638
+ });
639
+ continue;
640
+ }
641
+ const status = res.status;
642
+ const body = await res.text().catch(() => "");
643
+ const indicators = [];
644
+ // Detect WAF indicators in block page
645
+ for (const sig of WAF_BODY_SIGNATURES) {
646
+ if (sig.pattern.test(body)) {
647
+ indicators.push(`Body: ${sig.name}`);
648
+ }
649
+ }
650
+ // Check for support/reference IDs in block page
651
+ const supportIdPatterns = [
652
+ { pattern: /Ray ID:\s*([0-9a-f]+)/i, label: "Cloudflare Ray ID" },
653
+ { pattern: /Reference #(\S+)/i, label: "Akamai Reference" },
654
+ { pattern: /Your support ID is:\s*(\S+)/i, label: "F5 Support ID" },
655
+ { pattern: /Incident ID:\s*(\S+)/i, label: "Incapsula Incident ID" },
656
+ { pattern: /Block ID:\s*(\S+)/i, label: "Block ID" },
657
+ { pattern: /Request ID:\s*(\S+)/i, label: "Request ID" },
658
+ { pattern: /Event ID:\s*(\S+)/i, label: "Event ID" },
659
+ ];
660
+ for (const { pattern, label } of supportIdPatterns) {
661
+ const m = body.match(pattern);
662
+ if (m) {
663
+ indicators.push(`${label}: ${m[1]}`);
664
+ }
665
+ }
666
+ // Check for custom error page elements
667
+ if (/<title>/i.test(body)) {
668
+ const titleMatch = body.match(/<title[^>]*>([^<]*)<\/title>/i);
669
+ if (titleMatch && titleMatch[1]) {
670
+ indicators.push(`Block page title: ${titleMatch[1].trim()}`);
671
+ }
672
+ }
673
+ // Determine WAF mode
674
+ let mode;
675
+ if (status === 403 || status === 406 || status === 503) {
676
+ // Check for CAPTCHA challenge
677
+ if (/captcha|challenge|hcaptcha|recaptcha|turnstile|cf-chl-widget/i.test(body)) {
678
+ mode = "challenge";
679
+ }
680
+ else {
681
+ mode = "block";
682
+ }
683
+ }
684
+ else if (status === 200) {
685
+ // Check for warning headers (detect-only mode)
686
+ const wafWarningHeaders = [
687
+ "x-sigsci-tags",
688
+ "x-wallarm-waf-check",
689
+ "x-waf-status",
690
+ ];
691
+ const hasWarning = wafWarningHeaders.some((h) => res.headers.has(h));
692
+ if (hasWarning || indicators.length > 0) {
693
+ mode = "detect_only";
694
+ }
695
+ else {
696
+ mode = "passthrough";
697
+ }
698
+ }
699
+ else {
700
+ mode = status >= 400 ? "block" : "passthrough";
701
+ }
702
+ const blocked = mode === "block" || mode === "challenge";
703
+ payloadResults.push({
704
+ type: payload.type,
705
+ label: payload.label,
706
+ status,
707
+ blocked,
708
+ mode,
709
+ waf_indicators: indicators,
710
+ });
711
+ }
712
+ // Aggregate WAF identification from all probe results
713
+ const allIndicators = payloadResults.flatMap((r) => r.waf_indicators);
714
+ const wafNames = new Set();
715
+ for (const ind of allIndicators) {
716
+ for (const sig of WAF_BODY_SIGNATURES) {
717
+ if (ind.includes(sig.name)) {
718
+ wafNames.add(sig.name);
719
+ }
720
+ }
721
+ }
722
+ // Calculate coverage rating
723
+ const blockedCount = payloadResults.filter((r) => r.blocked).length;
724
+ const totalPayloads = payloadResults.length;
725
+ const coveragePercent = Math.round((blockedCount / totalPayloads) * 100);
726
+ let coverageRating;
727
+ if (coveragePercent >= 80)
728
+ coverageRating = "comprehensive";
729
+ else if (coveragePercent >= 60)
730
+ coverageRating = "good";
731
+ else if (coveragePercent >= 40)
732
+ coverageRating = "moderate";
733
+ else if (coveragePercent > 0)
734
+ coverageRating = "weak";
735
+ else
736
+ coverageRating = "none";
737
+ const result = {
738
+ url,
739
+ baseline_status: baselineStatus,
740
+ waf_identified: Array.from(wafNames),
741
+ waf_mode: payloadResults.some((r) => r.mode === "challenge")
742
+ ? "challenge"
743
+ : payloadResults.some((r) => r.mode === "block")
744
+ ? "block"
745
+ : payloadResults.some((r) => r.mode === "detect_only")
746
+ ? "detect_only"
747
+ : "none",
748
+ coverage: {
749
+ payloads_tested: totalPayloads,
750
+ payloads_blocked: blockedCount,
751
+ coverage_percent: coveragePercent,
752
+ rating: coverageRating,
753
+ },
754
+ payload_results: payloadResults,
755
+ };
756
+ cache.set(cacheKey, result);
757
+ return json(result);
758
+ },
759
+ };
760
+ // ─── Export All WAF Tools ───
761
+ export const wafTools = [
762
+ wafDetect,
763
+ wafCdnDetect,
764
+ wafOrigin,
765
+ wafFingerprint,
766
+ ];
767
+ //# sourceMappingURL=index.js.map