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,1366 @@
1
+ import { z } from "zod";
2
+ import { RateLimiter } from "../utils/rate-limiter.js";
3
+ import { TTLCache } from "../utils/cache.js";
4
+ import * as cheerio from "cheerio";
5
+ // ─── Module-level Rate Limiter & Cache ───
6
+ const limiter = new RateLimiter(300);
7
+ const cache = new TTLCache(300_000);
8
+ // ─── Shared Helpers ───
9
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
10
+ async function fetchPage(url) {
11
+ const cached = cache.get(`html:${url}`);
12
+ if (cached)
13
+ return cached;
14
+ await limiter.acquire();
15
+ const res = await fetch(url, {
16
+ headers: { "User-Agent": USER_AGENT, Accept: "text/html,application/xhtml+xml,*/*" },
17
+ redirect: "follow",
18
+ signal: AbortSignal.timeout(10_000),
19
+ });
20
+ if (!res.ok)
21
+ throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
22
+ const html = await res.text();
23
+ cache.set(`html:${url}`, html);
24
+ return html;
25
+ }
26
+ async function fetchRaw(url, options) {
27
+ await limiter.acquire();
28
+ return fetch(url, {
29
+ headers: { "User-Agent": USER_AGENT },
30
+ redirect: "follow",
31
+ signal: AbortSignal.timeout(10_000),
32
+ ...options,
33
+ });
34
+ }
35
+ function resolveUrl(base, relative) {
36
+ try {
37
+ return new URL(relative, base).href;
38
+ }
39
+ catch {
40
+ return relative;
41
+ }
42
+ }
43
+ function extractVersion(filename) {
44
+ const m = filename.match(/[-@](\d+(?:\.\d+){1,3})(?:[.\-_]|min|\.js)/);
45
+ return m ? m[1] : null;
46
+ }
47
+ const KNOWN_CVES = [
48
+ { library: "jquery", maxVersion: "3.5.0", cve: "CVE-2020-11022", description: "jQuery < 3.5.0 XSS in htmlPrefilter" },
49
+ { library: "jquery", maxVersion: "3.5.0", cve: "CVE-2020-11023", description: "jQuery < 3.5.0 XSS via HTML containing <option>" },
50
+ { library: "jquery", maxVersion: "3.0.0", cve: "CVE-2015-9251", description: "jQuery < 3.0.0 XSS via cross-domain ajax request" },
51
+ { library: "jquery", maxVersion: "1.12.0", cve: "CVE-2019-11358", description: "jQuery < 1.12.0 prototype pollution in extend()" },
52
+ { library: "lodash", maxVersion: "4.17.21", cve: "CVE-2021-23337", description: "Lodash < 4.17.21 prototype pollution via template()" },
53
+ { library: "lodash", maxVersion: "4.17.12", cve: "CVE-2019-10744", description: "Lodash < 4.17.12 prototype pollution via defaultsDeep()" },
54
+ { library: "angular", maxVersion: "1.8.0", cve: "CVE-2022-25869", description: "AngularJS < 1.8.0 XSS via $sanitize" },
55
+ { library: "moment", maxVersion: "2.29.4", cve: "CVE-2022-31129", description: "Moment.js < 2.29.4 ReDoS in parsing" },
56
+ { library: "vue", maxVersion: "2.7.14", cve: "CVE-2024-6783", description: "Vue.js < 2.7.14 XSS via v-bind" },
57
+ { library: "axios", maxVersion: "1.6.0", cve: "CVE-2023-45857", description: "Axios < 1.6.0 XSRF token leak to third-party hosts" },
58
+ { library: "dompurify", maxVersion: "3.0.6", cve: "CVE-2023-48631", description: "DOMPurify < 3.0.6 mutation XSS bypass" },
59
+ ];
60
+ function compareVersions(a, b) {
61
+ const pa = a.split(".").map(Number);
62
+ const pb = b.split(".").map(Number);
63
+ const len = Math.max(pa.length, pb.length);
64
+ for (let i = 0; i < len; i++) {
65
+ const na = pa[i] ?? 0;
66
+ const nb = pb[i] ?? 0;
67
+ if (na < nb)
68
+ return -1;
69
+ if (na > nb)
70
+ return 1;
71
+ }
72
+ return 0;
73
+ }
74
+ function findCVEs(library, version) {
75
+ const normalizedLib = library.toLowerCase().replace(/\.js$/i, "");
76
+ return KNOWN_CVES.filter((cve) => cve.library === normalizedLib && compareVersions(version, cve.maxVersion) < 0);
77
+ }
78
+ // ─── JS Library Detection Patterns ───
79
+ const JS_LIB_PATTERNS = [
80
+ { pattern: /jquery[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "jQuery" },
81
+ { pattern: /react(?:\.production)?[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "React" },
82
+ { pattern: /react-dom[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "ReactDOM" },
83
+ { pattern: /angular[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Angular" },
84
+ { pattern: /vue[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Vue.js" },
85
+ { pattern: /lodash[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Lodash" },
86
+ { pattern: /moment[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Moment.js" },
87
+ { pattern: /axios[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Axios" },
88
+ { pattern: /d3[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "D3.js" },
89
+ { pattern: /three[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Three.js" },
90
+ { pattern: /backbone[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Backbone.js" },
91
+ { pattern: /ember[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Ember.js" },
92
+ { pattern: /underscore[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Underscore.js" },
93
+ { pattern: /bootstrap[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Bootstrap" },
94
+ { pattern: /tailwind[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.(?:js|css)/i, name: "Tailwind CSS" },
95
+ { pattern: /socket\.io[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Socket.IO" },
96
+ { pattern: /dompurify[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "DOMPurify" },
97
+ { pattern: /handlebars[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Handlebars" },
98
+ { pattern: /knockout[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Knockout.js" },
99
+ { pattern: /gsap[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "GSAP" },
100
+ { pattern: /alpine[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Alpine.js" },
101
+ { pattern: /htmx[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "htmx" },
102
+ { pattern: /svelte[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Svelte" },
103
+ { pattern: /preact[-.]?(\d+(?:\.\d+){0,3})?(?:\.min)?\.js/i, name: "Preact" },
104
+ ];
105
+ const FRAMEWORK_INDICATORS = [
106
+ {
107
+ name: "Next.js",
108
+ check: (html, $) => {
109
+ const nextData = $("script#__NEXT_DATA__").html();
110
+ if (nextData) {
111
+ try {
112
+ const parsed = JSON.parse(nextData);
113
+ return `Next.js (Build ID: ${parsed.buildId ?? "unknown"})`;
114
+ }
115
+ catch {
116
+ return "Next.js";
117
+ }
118
+ }
119
+ return null;
120
+ },
121
+ },
122
+ {
123
+ name: "Nuxt",
124
+ check: (html) => (html.includes("__NUXT__") || html.includes("__nuxt") ? "Nuxt.js" : null),
125
+ },
126
+ {
127
+ name: "Angular",
128
+ check: (_html, $) => {
129
+ const ngVersion = $("[ng-version]").attr("ng-version");
130
+ return ngVersion ? `Angular (v${ngVersion})` : null;
131
+ },
132
+ },
133
+ {
134
+ name: "React",
135
+ check: (_html, $) => ($("[data-reactroot]").length > 0 || $("[data-reactid]").length > 0 ? "React (DOM)" : null),
136
+ },
137
+ {
138
+ name: "Svelte",
139
+ check: (html) => (html.includes("__svelte") ? "Svelte" : null),
140
+ },
141
+ {
142
+ name: "Vue.js",
143
+ check: (html, $) => {
144
+ if (html.includes("__VUE__") || html.includes("__vue__"))
145
+ return "Vue.js (DOM)";
146
+ if ($("[data-v-]").length > 0 || $("[data-vue-app]").length > 0)
147
+ return "Vue.js (DOM)";
148
+ return null;
149
+ },
150
+ },
151
+ {
152
+ name: "Gatsby",
153
+ check: (_html, $) => ($("script#gatsby-chunk-mapping").length > 0 || $("[data-gatsby-head]").length > 0 ? "Gatsby" : null),
154
+ },
155
+ {
156
+ name: "Remix",
157
+ check: (html) => (html.includes("__remixContext") ? "Remix" : null),
158
+ },
159
+ ];
160
+ // ─── Build Tool Detection ───
161
+ const BUILD_TOOL_PATTERNS = [
162
+ { pattern: /\/_next\//i, name: "Next.js" },
163
+ { pattern: /\/@vite\//i, name: "Vite" },
164
+ { pattern: /\/webpack/i, name: "Webpack" },
165
+ { pattern: /\/parcel\//i, name: "Parcel" },
166
+ { pattern: /\/rollup\//i, name: "Rollup" },
167
+ { pattern: /\.turbopack\//i, name: "Turbopack" },
168
+ { pattern: /\/esbuild\//i, name: "esbuild" },
169
+ { pattern: /\/__snowpack__\//i, name: "Snowpack" },
170
+ ];
171
+ // ─── WebSocket Endpoints ───
172
+ const WS_ENDPOINTS = [
173
+ { path: "/ws", framework: null },
174
+ { path: "/websocket", framework: null },
175
+ { path: "/socket.io/?EIO=4&transport=polling", framework: "Socket.IO (Node.js)" },
176
+ { path: "/socket.io/", framework: "Socket.IO (Node.js)" },
177
+ { path: "/sockjs/info", framework: "SockJS" },
178
+ { path: "/cable", framework: "ActionCable (Rails)" },
179
+ { path: "/hub", framework: "SignalR (ASP.NET)" },
180
+ { path: "/signalr/negotiate", framework: "SignalR (ASP.NET)" },
181
+ { path: "/graphql-ws", framework: "GraphQL Subscriptions" },
182
+ { path: "/mqtt", framework: "MQTT" },
183
+ { path: "/stomp", framework: "STOMP" },
184
+ { path: "/realtime", framework: null },
185
+ { path: "/live", framework: null },
186
+ { path: "/stream", framework: null },
187
+ { path: "/events", framework: null },
188
+ { path: "/notifications", framework: null },
189
+ ];
190
+ // ─── API Discovery Paths ───
191
+ const API_PATHS = {
192
+ openapi: [
193
+ "/swagger.json",
194
+ "/openapi.json",
195
+ "/api-docs",
196
+ "/v1/api-docs",
197
+ "/v2/api-docs",
198
+ "/v3/api-docs",
199
+ "/swagger.yaml",
200
+ "/openapi.yaml",
201
+ "/swagger-ui.html",
202
+ "/swagger-ui/",
203
+ "/swagger-resources",
204
+ ],
205
+ graphql: ["/graphql"],
206
+ docker: ["/_catalog", "/v2/_catalog", "/v2/"],
207
+ kubernetes: ["/api/v1", "/api/v1/namespaces", "/apis", "/version", "/healthz"],
208
+ };
209
+ // ─── Dangerous GraphQL Mutations ───
210
+ const DANGEROUS_MUTATIONS = [
211
+ "deleteUser", "removeUser", "destroyUser",
212
+ "updateRole", "setRole", "assignRole",
213
+ "createAdmin", "addAdmin", "promoteUser",
214
+ "resetPassword", "changePassword", "forceResetPassword",
215
+ "deleteAccount", "destroyAccount",
216
+ "updatePermission", "grantPermission",
217
+ "executeCommand", "runCommand",
218
+ "uploadFile", "deleteFile",
219
+ "createToken", "revokeToken", "invalidateToken",
220
+ "updateConfig", "setConfig",
221
+ "sendEmail", "sendNotification",
222
+ "impersonate", "switchUser",
223
+ ];
224
+ // ─── Virtual Host Wordlist ───
225
+ const DEFAULT_VHOSTS = [
226
+ "localhost", "127.0.0.1", "internal", "admin", "test",
227
+ "dev", "staging", "default", "backend", "api",
228
+ "intranet", "management", "portal", "dashboard", "monitor",
229
+ ];
230
+ const ANALYTICS_DETECT = [
231
+ {
232
+ name: "Google Analytics (Universal)",
233
+ patterns: [/UA-\d{4,10}-\d{1,4}/i, /google-analytics\.com\/analytics\.js/i, /\bga\s*\(\s*['"]create['"]/i],
234
+ idPattern: /(UA-\d{4,10}-\d{1,4})/,
235
+ crossRef: true,
236
+ },
237
+ {
238
+ name: "Google Analytics 4",
239
+ patterns: [/G-[A-Z0-9]{4,15}/i, /gtag\s*\(\s*['"]config['"]/i, /googletagmanager\.com\/gtag\/js/i],
240
+ idPattern: /(G-[A-Z0-9]{4,15})/,
241
+ crossRef: true,
242
+ },
243
+ {
244
+ name: "Google Tag Manager",
245
+ patterns: [/GTM-[A-Z0-9]{4,10}/i, /googletagmanager\.com\/gtm\.js/i],
246
+ idPattern: /(GTM-[A-Z0-9]{4,10})/,
247
+ crossRef: true,
248
+ },
249
+ {
250
+ name: "Facebook Pixel",
251
+ patterns: [/fbq\s*\(\s*['"]init['"]/i, /connect\.facebook\.net/i],
252
+ idPattern: /fbq\s*\(\s*['"]init['"]\s*,\s*['"]?(\d{10,20})/,
253
+ crossRef: true,
254
+ },
255
+ {
256
+ name: "Hotjar",
257
+ patterns: [/hj\s*\(\s*['"]init['"]/i, /static\.hotjar\.com/i],
258
+ idPattern: /hjid\s*:\s*(\d+)/,
259
+ crossRef: true,
260
+ },
261
+ {
262
+ name: "Segment",
263
+ patterns: [/cdn\.segment\.com/i, /analytics\.identify/i],
264
+ idPattern: /analytics\.load\s*\(\s*['"]([A-Za-z0-9]+)['"]/,
265
+ crossRef: true,
266
+ },
267
+ {
268
+ name: "Mixpanel",
269
+ patterns: [/mixpanel\.init/i, /cdn\.mxpnl\.com/i],
270
+ idPattern: /mixpanel\.init\s*\(\s*['"]([a-f0-9]{32})['"]/,
271
+ crossRef: true,
272
+ },
273
+ {
274
+ name: "Amplitude",
275
+ patterns: [/amplitude\.getInstance/i, /cdn\.amplitude\.com/i],
276
+ idPattern: /amplitude\.init\s*\(\s*['"]([a-f0-9]{32})['"]/,
277
+ crossRef: true,
278
+ },
279
+ {
280
+ name: "PostHog",
281
+ patterns: [/posthog\.init/i, /posthog\.com/i],
282
+ idPattern: /posthog\.init\s*\(\s*['"]([a-zA-Z0-9_-]+)['"]/,
283
+ crossRef: true,
284
+ },
285
+ {
286
+ name: "Plausible",
287
+ patterns: [/plausible\.io/i],
288
+ idPattern: /data-domain\s*=\s*['"]([^'"]+)['"]/,
289
+ crossRef: false,
290
+ },
291
+ {
292
+ name: "Matomo",
293
+ patterns: [/_paq\.push/i, /matomo\.js/i, /piwik\.js/i],
294
+ idPattern: /setSiteId['"\\s,]+(\d+)/,
295
+ crossRef: false,
296
+ },
297
+ {
298
+ name: "Heap",
299
+ patterns: [/heap\.load/i, /heapanalytics\.com/i],
300
+ idPattern: /heap\.load\s*\(\s*['"]?(\d{8,12})/,
301
+ crossRef: true,
302
+ },
303
+ {
304
+ name: "FullStory",
305
+ patterns: [/fullstory\.com/i, /_fs_org/i],
306
+ idPattern: /_fs_org\s*[=:]\s*['"]([A-Z0-9]+)['"]/,
307
+ crossRef: true,
308
+ },
309
+ {
310
+ name: "Microsoft Clarity",
311
+ patterns: [/clarity\.ms/i, /clarity\s*\(\s*['"]set['"]/i],
312
+ idPattern: /clarity\.ms\/tag\/([a-z0-9]+)/,
313
+ crossRef: true,
314
+ },
315
+ {
316
+ name: "Google AdSense",
317
+ patterns: [/ca-pub-\d{10,16}/i, /adsbygoogle/i],
318
+ idPattern: /(ca-pub-\d{10,16})/,
319
+ crossRef: true,
320
+ },
321
+ {
322
+ name: "Pinterest Tag",
323
+ patterns: [/pintrk/i, /ct\.pinterest\.com/i],
324
+ idPattern: /pintrk\s*\(\s*['"]load['"]\s*,\s*['"]?(\d+)/,
325
+ crossRef: true,
326
+ },
327
+ {
328
+ name: "Twitter Pixel",
329
+ patterns: [/twq\s*\(\s*['"]init['"]/i, /ads-twitter\.com/i],
330
+ idPattern: /twq\s*\(\s*['"]init['"]\s*,\s*['"]([a-z0-9]+)['"]/,
331
+ crossRef: true,
332
+ },
333
+ {
334
+ name: "LinkedIn Insight Tag",
335
+ patterns: [/snap\.licdn\.com/i, /_linkedin_partner_id/i],
336
+ idPattern: /_linkedin_partner_id\s*=\s*['"]?(\d+)/,
337
+ crossRef: true,
338
+ },
339
+ {
340
+ name: "TikTok Pixel",
341
+ patterns: [/analytics\.tiktok\.com/i, /ttq\.load/i],
342
+ idPattern: /ttq\.load\s*\(\s*['"]([A-Z0-9]+)['"]/,
343
+ crossRef: true,
344
+ },
345
+ ];
346
+ // ═══════════════════════════════════════════════════════════════════════════════
347
+ // Tool 1: web_tech — Technology Stack Detection
348
+ // ═══════════════════════════════════════════════════════════════════════════════
349
+ const webTech = {
350
+ name: "web_tech",
351
+ description: "Detect the full technology stack of a web page via HTML/JS analysis. Identifies JS libraries with versions, frameworks via DOM indicators, meta generator tags, build tools, and known CVEs for detected versions.",
352
+ schema: {
353
+ url: z.string().url().describe("URL to analyze technology stack"),
354
+ },
355
+ async execute(args) {
356
+ const url = args.url;
357
+ const html = await fetchPage(url);
358
+ const $ = cheerio.load(html);
359
+ // --- JS Libraries from <script src=""> ---
360
+ const libraries = [];
361
+ const seenLibs = new Set();
362
+ $("script[src]").each((_i, el) => {
363
+ const src = $(el).attr("src") ?? "";
364
+ for (const lib of JS_LIB_PATTERNS) {
365
+ if (lib.pattern.test(src)) {
366
+ const version = extractVersion(src);
367
+ const key = `${lib.name}:${version ?? "unknown"}`;
368
+ if (!seenLibs.has(key)) {
369
+ seenLibs.add(key);
370
+ const cves = version ? findCVEs(lib.name, version) : [];
371
+ libraries.push({ name: lib.name, version, src, cves });
372
+ }
373
+ }
374
+ }
375
+ });
376
+ // --- Framework DOM Indicators ---
377
+ const frameworks = [];
378
+ for (const indicator of FRAMEWORK_INDICATORS) {
379
+ const result = indicator.check(html, $);
380
+ if (result)
381
+ frameworks.push(result);
382
+ }
383
+ // --- Meta Generator Tag ---
384
+ const generator = $('meta[name="generator"]').attr("content") ?? null;
385
+ // --- Build Tool Detection ---
386
+ const buildTools = new Set();
387
+ $("script[src]").each((_i, el) => {
388
+ const src = $(el).attr("src") ?? "";
389
+ for (const bt of BUILD_TOOL_PATTERNS) {
390
+ if (bt.pattern.test(src)) {
391
+ buildTools.add(bt.name);
392
+ }
393
+ }
394
+ });
395
+ // Also check link tags (CSS)
396
+ $("link[href]").each((_i, el) => {
397
+ const href = $(el).attr("href") ?? "";
398
+ for (const bt of BUILD_TOOL_PATTERNS) {
399
+ if (bt.pattern.test(href)) {
400
+ buildTools.add(bt.name);
401
+ }
402
+ }
403
+ });
404
+ // --- CSS Frameworks from <link> ---
405
+ const cssFrameworks = [];
406
+ $("link[rel='stylesheet'][href]").each((_i, el) => {
407
+ const href = $(el).attr("href") ?? "";
408
+ if (/bootstrap/i.test(href))
409
+ cssFrameworks.push("Bootstrap CSS");
410
+ if (/tailwind/i.test(href))
411
+ cssFrameworks.push("Tailwind CSS");
412
+ if (/bulma/i.test(href))
413
+ cssFrameworks.push("Bulma");
414
+ if (/materialize/i.test(href))
415
+ cssFrameworks.push("Materialize CSS");
416
+ if (/foundation/i.test(href))
417
+ cssFrameworks.push("Foundation");
418
+ });
419
+ // --- Aggregate all CVEs ---
420
+ const allCVEs = libraries.flatMap((lib) => lib.cves);
421
+ return {
422
+ content: [
423
+ {
424
+ type: "text",
425
+ text: JSON.stringify({
426
+ url,
427
+ libraries: libraries.map((l) => ({
428
+ name: l.name,
429
+ version: l.version,
430
+ src: l.src,
431
+ cves: l.cves.map((c) => ({ cve: c.cve, description: c.description })),
432
+ })),
433
+ frameworks,
434
+ generator,
435
+ buildTools: [...buildTools],
436
+ cssFrameworks: [...new Set(cssFrameworks)],
437
+ knownVulnerabilities: allCVEs.length,
438
+ cveDetails: allCVEs.map((c) => ({ cve: c.cve, library: c.library, maxVersion: c.maxVersion, description: c.description })),
439
+ }, null, 2),
440
+ },
441
+ ],
442
+ };
443
+ },
444
+ };
445
+ // ═══════════════════════════════════════════════════════════════════════════════
446
+ // Tool 2: web_sourcemaps — Source Map Detection
447
+ // ═══════════════════════════════════════════════════════════════════════════════
448
+ const webSourcemaps = {
449
+ name: "web_sourcemaps",
450
+ description: "Detect exposed JavaScript source maps by probing .map suffixes for each script file found on the page. Extracts internal file paths from found source maps, revealing directory structures and sensitive development information.",
451
+ schema: {
452
+ url: z.string().url().describe("URL to probe for source maps"),
453
+ },
454
+ async execute(args) {
455
+ const url = args.url;
456
+ const html = await fetchPage(url);
457
+ const $ = cheerio.load(html);
458
+ const scriptSrcs = [];
459
+ $("script[src]").each((_i, el) => {
460
+ const src = $(el).attr("src");
461
+ if (src)
462
+ scriptSrcs.push(resolveUrl(url, src));
463
+ });
464
+ const found = [];
465
+ const probed = [];
466
+ for (const scriptUrl of scriptSrcs.slice(0, 20)) {
467
+ // Method 1: Probe .map suffix
468
+ const mapUrl = scriptUrl + ".map";
469
+ try {
470
+ const res = await fetchRaw(mapUrl);
471
+ probed.push({ url: mapUrl, status: res.status });
472
+ if (res.ok) {
473
+ const contentType = res.headers.get("content-type") ?? "";
474
+ const body = await res.text();
475
+ // Validate it's actually a source map
476
+ if (body.includes('"sources"') || contentType.includes("json")) {
477
+ try {
478
+ const sourceMap = JSON.parse(body);
479
+ const sources = sourceMap.sources ?? [];
480
+ const sensitiveInfo = [];
481
+ for (const s of sources) {
482
+ if (/\/home\/|\/Users\/|\/root\/|C:\\Users/i.test(s))
483
+ sensitiveInfo.push(`Local path: ${s}`);
484
+ if (/\.env|config|secret|password|credential|private/i.test(s))
485
+ sensitiveInfo.push(`Sensitive file: ${s}`);
486
+ if (/internal|private|staging|dev/i.test(s))
487
+ sensitiveInfo.push(`Internal path: ${s}`);
488
+ }
489
+ found.push({
490
+ scriptUrl,
491
+ mapUrl,
492
+ detectedVia: ".map suffix probe",
493
+ sourceCount: sources.length,
494
+ sources: sources.slice(0, 50),
495
+ sensitiveInfo,
496
+ });
497
+ continue;
498
+ }
499
+ catch {
500
+ // Not valid JSON, skip
501
+ }
502
+ }
503
+ }
504
+ }
505
+ catch {
506
+ probed.push({ url: mapUrl, status: "error" });
507
+ }
508
+ // Method 2: Check sourceMappingURL comment in JS file
509
+ try {
510
+ const jsRes = await fetchRaw(scriptUrl);
511
+ if (jsRes.ok) {
512
+ const jsBody = await jsRes.text();
513
+ const mappingMatch = jsBody.match(/\/\/[#@]\s*sourceMappingURL\s*=\s*(\S+)/);
514
+ if (mappingMatch) {
515
+ const mappingUrl = resolveUrl(scriptUrl, mappingMatch[1]);
516
+ // If it's a data URI, skip external fetch
517
+ if (mappingUrl.startsWith("data:")) {
518
+ found.push({
519
+ scriptUrl,
520
+ mapUrl: "(inline data URI)",
521
+ detectedVia: "sourceMappingURL comment (inline)",
522
+ sourceCount: 0,
523
+ sources: [],
524
+ sensitiveInfo: ["Source map is embedded inline as data URI"],
525
+ });
526
+ continue;
527
+ }
528
+ try {
529
+ const mapRes = await fetchRaw(mappingUrl);
530
+ probed.push({ url: mappingUrl, status: mapRes.status });
531
+ if (mapRes.ok) {
532
+ const mapBody = await mapRes.text();
533
+ try {
534
+ const sourceMap = JSON.parse(mapBody);
535
+ const sources = sourceMap.sources ?? [];
536
+ const sensitiveInfo = [];
537
+ for (const s of sources) {
538
+ if (/\/home\/|\/Users\/|\/root\/|C:\\Users/i.test(s))
539
+ sensitiveInfo.push(`Local path: ${s}`);
540
+ if (/\.env|config|secret|password|credential|private/i.test(s))
541
+ sensitiveInfo.push(`Sensitive file: ${s}`);
542
+ if (/internal|private|staging|dev/i.test(s))
543
+ sensitiveInfo.push(`Internal path: ${s}`);
544
+ }
545
+ found.push({
546
+ scriptUrl,
547
+ mapUrl: mappingUrl,
548
+ detectedVia: "sourceMappingURL comment",
549
+ sourceCount: sources.length,
550
+ sources: sources.slice(0, 50),
551
+ sensitiveInfo,
552
+ });
553
+ }
554
+ catch {
555
+ // Not valid JSON
556
+ }
557
+ }
558
+ }
559
+ catch {
560
+ probed.push({ url: mappingUrl, status: "error" });
561
+ }
562
+ }
563
+ }
564
+ }
565
+ catch {
566
+ // Failed to fetch JS file, skip
567
+ }
568
+ }
569
+ return {
570
+ content: [
571
+ {
572
+ type: "text",
573
+ text: JSON.stringify({
574
+ url,
575
+ totalScripts: scriptSrcs.length,
576
+ sourceMapsFound: found.length,
577
+ sourceMaps: found,
578
+ probed: probed.slice(0, 30),
579
+ }, null, 2),
580
+ },
581
+ ],
582
+ };
583
+ },
584
+ };
585
+ // ═══════════════════════════════════════════════════════════════════════════════
586
+ // Tool 3: web_websocket — WebSocket Endpoint Discovery
587
+ // ═══════════════════════════════════════════════════════════════════════════════
588
+ const webWebsocket = {
589
+ name: "web_websocket",
590
+ description: "Discover WebSocket endpoints by probing common WebSocket paths. Tests for Socket.IO, SockJS, ActionCable, SignalR, GraphQL subscriptions, MQTT, and more. Identifies backend frameworks from response patterns.",
591
+ schema: {
592
+ url: z.string().url().describe("Base URL to discover WebSocket endpoints"),
593
+ },
594
+ async execute(args) {
595
+ const baseUrl = args.url.replace(/\/+$/, "");
596
+ const results = [];
597
+ for (const ep of WS_ENDPOINTS) {
598
+ const fullUrl = baseUrl + ep.path;
599
+ try {
600
+ const res = await fetchRaw(fullUrl, {
601
+ headers: {
602
+ "User-Agent": USER_AGENT,
603
+ Upgrade: "websocket",
604
+ Connection: "Upgrade",
605
+ "Sec-WebSocket-Version": "13",
606
+ "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
607
+ },
608
+ });
609
+ const status = res.status;
610
+ const responsive = status === 101 || status === 200 || status === 400;
611
+ let details = null;
612
+ let framework = ep.framework;
613
+ if (responsive) {
614
+ const body = await res.text().catch(() => "");
615
+ // Socket.IO detection
616
+ if (body.includes('"sid"') && body.includes('"upgrades"')) {
617
+ framework = "Socket.IO (Node.js)";
618
+ details = "Socket.IO handshake response detected";
619
+ }
620
+ // SockJS detection
621
+ if (body.includes('"websocket"') && body.includes('"entropy"')) {
622
+ framework = "SockJS";
623
+ details = "SockJS info endpoint responded";
624
+ }
625
+ // SignalR detection
626
+ if (body.includes('"connectionId"') || body.includes('"connectionToken"')) {
627
+ framework = "SignalR (ASP.NET)";
628
+ details = "SignalR negotiation response detected";
629
+ }
630
+ // ActionCable detection
631
+ if (res.headers.get("x-request-id") && ep.path === "/cable") {
632
+ framework = "ActionCable (Rails)";
633
+ }
634
+ }
635
+ results.push({
636
+ path: ep.path,
637
+ fullUrl,
638
+ status,
639
+ responsive,
640
+ framework,
641
+ details,
642
+ });
643
+ }
644
+ catch (err) {
645
+ results.push({
646
+ path: ep.path,
647
+ fullUrl,
648
+ status: err.message.includes("timeout") ? "timeout" : "error",
649
+ responsive: false,
650
+ framework: ep.framework,
651
+ details: null,
652
+ });
653
+ }
654
+ }
655
+ const activeEndpoints = results.filter((r) => r.responsive);
656
+ return {
657
+ content: [
658
+ {
659
+ type: "text",
660
+ text: JSON.stringify({
661
+ baseUrl,
662
+ totalProbed: results.length,
663
+ activeEndpoints: activeEndpoints.length,
664
+ endpoints: results,
665
+ frameworksDetected: [...new Set(activeEndpoints.map((e) => e.framework).filter(Boolean))],
666
+ }, null, 2),
667
+ },
668
+ ],
669
+ };
670
+ },
671
+ };
672
+ // ═══════════════════════════════════════════════════════════════════════════════
673
+ // Tool 4: web_api_discovery — API Documentation Auto-Discovery
674
+ // ═══════════════════════════════════════════════════════════════════════════════
675
+ const webApiDiscovery = {
676
+ name: "web_api_discovery",
677
+ description: "Auto-discover API documentation endpoints including OpenAPI/Swagger, GraphQL, Docker Registry, and Kubernetes APIs. Parses found OpenAPI specs to extract titles, versions, endpoint counts, and authentication schemes.",
678
+ schema: {
679
+ url: z.string().url().describe("Base URL to discover API documentation"),
680
+ },
681
+ async execute(args) {
682
+ const baseUrl = args.url.replace(/\/+$/, "");
683
+ const discovered = [];
684
+ // --- OpenAPI / Swagger ---
685
+ for (const path of API_PATHS.openapi) {
686
+ const fullUrl = baseUrl + path;
687
+ try {
688
+ const res = await fetchRaw(fullUrl);
689
+ if (res.ok || res.status === 401 || res.status === 403) {
690
+ let details = null;
691
+ if (res.ok) {
692
+ const body = await res.text();
693
+ try {
694
+ const spec = JSON.parse(body);
695
+ const info = spec.info ?? {};
696
+ const paths = spec.paths ? Object.keys(spec.paths) : [];
697
+ const security = spec.securityDefinitions ?? spec.components?.securitySchemes ?? {};
698
+ details = {
699
+ title: info.title ?? null,
700
+ version: info.version ?? null,
701
+ endpointCount: paths.length,
702
+ sampleEndpoints: paths.slice(0, 10),
703
+ authSchemes: Object.keys(security),
704
+ openapiVersion: spec.openapi ?? spec.swagger ?? null,
705
+ };
706
+ }
707
+ catch {
708
+ // YAML or non-JSON — still report as found
709
+ details = { note: "Response received but not parseable as JSON (may be YAML)" };
710
+ }
711
+ }
712
+ else {
713
+ details = { note: `Auth required (HTTP ${res.status}) — endpoint exists but is protected` };
714
+ }
715
+ discovered.push({ category: "OpenAPI/Swagger", path, fullUrl, status: res.status, details });
716
+ }
717
+ }
718
+ catch {
719
+ // Skip unreachable
720
+ }
721
+ }
722
+ // --- GraphQL ---
723
+ for (const path of API_PATHS.graphql) {
724
+ const fullUrl = baseUrl + path;
725
+ try {
726
+ const res = await fetchRaw(fullUrl, {
727
+ method: "POST",
728
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
729
+ body: JSON.stringify({ query: "{ __typename }" }),
730
+ });
731
+ if (res.ok || res.status === 400 || res.status === 401 || res.status === 403) {
732
+ let details = null;
733
+ if (res.ok) {
734
+ try {
735
+ const body = await res.json();
736
+ details = { response: body, introspectionHint: "Use web_graphql for full schema introspection" };
737
+ }
738
+ catch {
739
+ details = { note: "GraphQL endpoint responded but response is not JSON" };
740
+ }
741
+ }
742
+ else {
743
+ details = { note: `GraphQL endpoint exists (HTTP ${res.status})` };
744
+ }
745
+ discovered.push({ category: "GraphQL", path, fullUrl, status: res.status, details });
746
+ }
747
+ }
748
+ catch {
749
+ // Skip
750
+ }
751
+ }
752
+ // --- Docker Registry ---
753
+ for (const path of API_PATHS.docker) {
754
+ const fullUrl = baseUrl + path;
755
+ try {
756
+ const res = await fetchRaw(fullUrl);
757
+ if (res.ok || res.status === 401) {
758
+ let details = null;
759
+ if (res.ok) {
760
+ try {
761
+ const body = await res.json();
762
+ details = { response: body };
763
+ }
764
+ catch {
765
+ details = { note: "Docker Registry endpoint responded" };
766
+ }
767
+ }
768
+ else {
769
+ details = {
770
+ note: "Docker Registry requires auth",
771
+ wwwAuthenticate: res.headers.get("www-authenticate"),
772
+ };
773
+ }
774
+ discovered.push({ category: "Docker Registry", path, fullUrl, status: res.status, details });
775
+ }
776
+ }
777
+ catch {
778
+ // Skip
779
+ }
780
+ }
781
+ // --- Kubernetes ---
782
+ for (const path of API_PATHS.kubernetes) {
783
+ const fullUrl = baseUrl + path;
784
+ try {
785
+ const res = await fetchRaw(fullUrl);
786
+ if (res.ok || res.status === 401 || res.status === 403) {
787
+ let details = null;
788
+ if (res.ok) {
789
+ try {
790
+ const body = await res.json();
791
+ details = { response: body };
792
+ }
793
+ catch {
794
+ details = { note: "Kubernetes endpoint responded" };
795
+ }
796
+ }
797
+ else {
798
+ details = { note: `Kubernetes API exists but requires auth (HTTP ${res.status})` };
799
+ }
800
+ discovered.push({ category: "Kubernetes", path, fullUrl, status: res.status, details });
801
+ }
802
+ }
803
+ catch {
804
+ // Skip
805
+ }
806
+ }
807
+ return {
808
+ content: [
809
+ {
810
+ type: "text",
811
+ text: JSON.stringify({
812
+ baseUrl,
813
+ totalDiscovered: discovered.length,
814
+ byCategory: Object.fromEntries([...new Set(discovered.map((d) => d.category))].map((cat) => [
815
+ cat,
816
+ discovered.filter((d) => d.category === cat).length,
817
+ ])),
818
+ discovered,
819
+ }, null, 2),
820
+ },
821
+ ],
822
+ };
823
+ },
824
+ };
825
+ // ═══════════════════════════════════════════════════════════════════════════════
826
+ // Tool 5: web_graphql — GraphQL Introspection
827
+ // ═══════════════════════════════════════════════════════════════════════════════
828
+ const webGraphql = {
829
+ name: "web_graphql",
830
+ description: "Perform GraphQL introspection on a given endpoint. Sends __schema query to extract types, queries, mutations, and identifies dangerous mutations like deleteUser, updateRole, createAdmin, and resetPassword.",
831
+ schema: {
832
+ url: z.string().url().describe("GraphQL endpoint URL"),
833
+ },
834
+ async execute(args) {
835
+ const url = args.url;
836
+ const introspectionQuery = JSON.stringify({
837
+ query: `{
838
+ __schema {
839
+ types { name kind fields { name type { name kind } } }
840
+ mutationType { fields { name } }
841
+ queryType { fields { name } }
842
+ }
843
+ }`,
844
+ });
845
+ let schema = null;
846
+ let method = "POST";
847
+ // Try POST first
848
+ try {
849
+ const res = await fetchRaw(url, {
850
+ method: "POST",
851
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
852
+ body: introspectionQuery,
853
+ });
854
+ if (res.ok) {
855
+ const body = await res.json();
856
+ if (body.data?.__schema) {
857
+ schema = body.data.__schema;
858
+ }
859
+ else if (body.errors) {
860
+ return {
861
+ content: [
862
+ {
863
+ type: "text",
864
+ text: JSON.stringify({
865
+ url,
866
+ introspectionEnabled: false,
867
+ method: "POST",
868
+ errors: body.errors,
869
+ note: "Introspection is disabled or blocked on this endpoint",
870
+ }, null, 2),
871
+ },
872
+ ],
873
+ };
874
+ }
875
+ }
876
+ }
877
+ catch {
878
+ // POST failed, try GET
879
+ }
880
+ // Try GET if POST didn't work
881
+ if (!schema) {
882
+ method = "GET";
883
+ try {
884
+ const getUrl = `${url}?query=${encodeURIComponent('{ __schema { types { name kind fields { name type { name kind } } } mutationType { fields { name } } queryType { fields { name } } } }')}`;
885
+ const res = await fetchRaw(getUrl);
886
+ if (res.ok) {
887
+ const body = await res.json();
888
+ if (body.data?.__schema) {
889
+ schema = body.data.__schema;
890
+ }
891
+ }
892
+ }
893
+ catch {
894
+ // GET also failed
895
+ }
896
+ }
897
+ if (!schema) {
898
+ return {
899
+ content: [
900
+ {
901
+ type: "text",
902
+ text: JSON.stringify({
903
+ url,
904
+ introspectionEnabled: false,
905
+ note: "Could not retrieve schema via POST or GET. Introspection may be disabled.",
906
+ }, null, 2),
907
+ },
908
+ ],
909
+ };
910
+ }
911
+ // --- Parse schema ---
912
+ const types = (schema.types ?? [])
913
+ .filter((t) => !t.name.startsWith("__"))
914
+ .map((t) => ({
915
+ name: t.name,
916
+ kind: t.kind,
917
+ fieldCount: t.fields?.length ?? 0,
918
+ }));
919
+ const queryFields = (schema.queryType?.fields ?? []).map((f) => f.name);
920
+ const mutationFields = (schema.mutationType?.fields ?? []).map((f) => f.name);
921
+ // --- Identify dangerous mutations ---
922
+ const dangerousMutationsFound = mutationFields.filter((name) => DANGEROUS_MUTATIONS.some((d) => name.toLowerCase().includes(d.toLowerCase())));
923
+ return {
924
+ content: [
925
+ {
926
+ type: "text",
927
+ text: JSON.stringify({
928
+ url,
929
+ introspectionEnabled: true,
930
+ method,
931
+ typeCount: types.length,
932
+ types: types.slice(0, 100),
933
+ queryFields,
934
+ mutationFields,
935
+ dangerousMutations: dangerousMutationsFound,
936
+ riskLevel: dangerousMutationsFound.length > 3
937
+ ? "HIGH"
938
+ : dangerousMutationsFound.length > 0
939
+ ? "MEDIUM"
940
+ : "LOW",
941
+ }, null, 2),
942
+ },
943
+ ],
944
+ };
945
+ },
946
+ };
947
+ // ═══════════════════════════════════════════════════════════════════════════════
948
+ // Tool 6: web_vhost — Virtual Host Enumeration
949
+ // ═══════════════════════════════════════════════════════════════════════════════
950
+ const webVhost = {
951
+ name: "web_vhost",
952
+ description: "Enumerate virtual hosts by sending HTTP requests with different Host headers. Compares response status codes, content lengths, and body content to identify distinct virtual hosts behind a single IP address.",
953
+ schema: {
954
+ url: z.string().url().describe("URL to enumerate virtual hosts"),
955
+ hostnames: z
956
+ .array(z.string())
957
+ .optional()
958
+ .describe("Additional hostnames to try beyond the default wordlist"),
959
+ },
960
+ async execute(args) {
961
+ const url = args.url;
962
+ const extraHostnames = args.hostnames ?? [];
963
+ const parsedUrl = new URL(url);
964
+ const baseHost = parsedUrl.hostname;
965
+ // Get baseline response
966
+ let baselineStatus;
967
+ let baselineLength;
968
+ let baselineBody;
969
+ try {
970
+ const res = await fetchRaw(url);
971
+ baselineStatus = res.status;
972
+ baselineBody = await res.text();
973
+ baselineLength = baselineBody.length;
974
+ }
975
+ catch (err) {
976
+ return {
977
+ content: [
978
+ {
979
+ type: "text",
980
+ text: JSON.stringify({ url, error: `Failed to fetch baseline: ${err.message}` }, null, 2),
981
+ },
982
+ ],
983
+ };
984
+ }
985
+ const hostnames = [...new Set([...DEFAULT_VHOSTS, ...extraHostnames])].filter((h) => h !== baseHost);
986
+ const results = [];
987
+ for (const hostname of hostnames) {
988
+ try {
989
+ const res = await fetchRaw(url, {
990
+ headers: {
991
+ Host: hostname,
992
+ "User-Agent": USER_AGENT,
993
+ },
994
+ });
995
+ const body = await res.text();
996
+ const status = res.status;
997
+ const contentLength = body.length;
998
+ const differenceType = [];
999
+ if (status !== baselineStatus)
1000
+ differenceType.push("status_code");
1001
+ if (Math.abs(contentLength - baselineLength) > 100)
1002
+ differenceType.push("content_length");
1003
+ if (body !== baselineBody && differenceType.length === 0) {
1004
+ // Check if body differs significantly
1005
+ if (body.length > 0 && baselineBody.length > 0) {
1006
+ const similarity = body.substring(0, 500) === baselineBody.substring(0, 500);
1007
+ if (!similarity)
1008
+ differenceType.push("body_content");
1009
+ }
1010
+ }
1011
+ results.push({
1012
+ hostname,
1013
+ status,
1014
+ contentLength,
1015
+ different: differenceType.length > 0,
1016
+ differenceType,
1017
+ });
1018
+ }
1019
+ catch {
1020
+ results.push({
1021
+ hostname,
1022
+ status: "error",
1023
+ contentLength: 0,
1024
+ different: false,
1025
+ differenceType: [],
1026
+ });
1027
+ }
1028
+ }
1029
+ const distinctVhosts = results.filter((r) => r.different);
1030
+ return {
1031
+ content: [
1032
+ {
1033
+ type: "text",
1034
+ text: JSON.stringify({
1035
+ url,
1036
+ baseHost,
1037
+ baseline: {
1038
+ status: baselineStatus,
1039
+ contentLength: baselineLength,
1040
+ },
1041
+ totalTested: results.length,
1042
+ distinctVhosts: distinctVhosts.length,
1043
+ results,
1044
+ }, null, 2),
1045
+ },
1046
+ ],
1047
+ };
1048
+ },
1049
+ };
1050
+ // ═══════════════════════════════════════════════════════════════════════════════
1051
+ // Tool 7: web_sourcemap_paths — Source Map Intelligence Analysis
1052
+ // ═══════════════════════════════════════════════════════════════════════════════
1053
+ const webSourcemapPaths = {
1054
+ name: "web_sourcemap_paths",
1055
+ description: "Parse a downloaded source map for intelligence. Extracts internal file paths, analyzes directory structure patterns, identifies environment indicators (dev/staging/prod), and extracts dependency information from import paths.",
1056
+ schema: {
1057
+ url: z.string().url().describe("Source map URL to analyze"),
1058
+ },
1059
+ async execute(args) {
1060
+ const url = args.url;
1061
+ let sourceMapData;
1062
+ try {
1063
+ const res = await fetchRaw(url);
1064
+ if (!res.ok) {
1065
+ return {
1066
+ content: [
1067
+ {
1068
+ type: "text",
1069
+ text: JSON.stringify({ url, error: `HTTP ${res.status} — source map not accessible` }, null, 2),
1070
+ },
1071
+ ],
1072
+ };
1073
+ }
1074
+ const body = await res.text();
1075
+ sourceMapData = JSON.parse(body);
1076
+ }
1077
+ catch (err) {
1078
+ return {
1079
+ content: [
1080
+ {
1081
+ type: "text",
1082
+ text: JSON.stringify({ url, error: `Failed to fetch/parse source map: ${err.message}` }, null, 2),
1083
+ },
1084
+ ],
1085
+ };
1086
+ }
1087
+ const sources = sourceMapData.sources ?? [];
1088
+ const version = sourceMapData.version ?? null;
1089
+ const file = sourceMapData.file ?? null;
1090
+ // --- Directory Structure Analysis ---
1091
+ const directories = new Set();
1092
+ for (const s of sources) {
1093
+ const parts = s.split("/");
1094
+ for (let i = 1; i < parts.length; i++) {
1095
+ directories.add(parts.slice(0, i).join("/"));
1096
+ }
1097
+ }
1098
+ // --- Environment Indicators ---
1099
+ const envIndicators = [];
1100
+ for (const s of sources) {
1101
+ const lower = s.toLowerCase();
1102
+ if (/\/dev\/|\.dev\.|dev-config|development/i.test(lower))
1103
+ envIndicators.push({ path: s, environment: "development" });
1104
+ if (/\/staging\/|\.staging\.|staging-config/i.test(lower))
1105
+ envIndicators.push({ path: s, environment: "staging" });
1106
+ if (/\/prod\/|\.prod\.|production|\.production\./i.test(lower))
1107
+ envIndicators.push({ path: s, environment: "production" });
1108
+ if (/\/test\/|\.test\.|\.spec\.|__tests__|__mocks__/i.test(lower))
1109
+ envIndicators.push({ path: s, environment: "test" });
1110
+ }
1111
+ // --- Dependency Extraction ---
1112
+ const dependencies = new Set();
1113
+ for (const s of sources) {
1114
+ const nodeModulesMatch = s.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
1115
+ if (nodeModulesMatch)
1116
+ dependencies.add(nodeModulesMatch[1]);
1117
+ }
1118
+ // --- Sensitive File Detection ---
1119
+ const sensitiveFiles = [];
1120
+ for (const s of sources) {
1121
+ if (/\.env|config\.(js|ts|json)|secret|password|credential|private[-_]?key|api[-_]?key/i.test(s)) {
1122
+ sensitiveFiles.push(s);
1123
+ }
1124
+ }
1125
+ // --- Local Path Leaks ---
1126
+ const localPaths = [];
1127
+ for (const s of sources) {
1128
+ if (/^\/home\/|^\/Users\/|^\/root\/|^C:\\Users|^\/var\/www|^\/opt\/|^\/srv\//i.test(s)) {
1129
+ localPaths.push(s);
1130
+ }
1131
+ }
1132
+ // --- File Type Breakdown ---
1133
+ const fileTypes = {};
1134
+ for (const s of sources) {
1135
+ const ext = s.match(/\.([a-zA-Z0-9]+)$/)?.[1]?.toLowerCase() ?? "unknown";
1136
+ fileTypes[ext] = (fileTypes[ext] ?? 0) + 1;
1137
+ }
1138
+ // --- Top-Level Directories ---
1139
+ const topLevelDirs = {};
1140
+ for (const s of sources) {
1141
+ const clean = s.replace(/^\.\/|^webpack:\/\/\/|^\//, "");
1142
+ const topDir = clean.split("/")[0];
1143
+ if (topDir)
1144
+ topLevelDirs[topDir] = (topLevelDirs[topDir] ?? 0) + 1;
1145
+ }
1146
+ return {
1147
+ content: [
1148
+ {
1149
+ type: "text",
1150
+ text: JSON.stringify({
1151
+ url,
1152
+ version,
1153
+ file,
1154
+ totalSources: sources.length,
1155
+ totalDirectories: directories.size,
1156
+ topLevelDirectories: topLevelDirs,
1157
+ fileTypes,
1158
+ sources: sources.slice(0, 100),
1159
+ envIndicators,
1160
+ dependencies: [...dependencies].sort(),
1161
+ sensitiveFiles,
1162
+ localPaths,
1163
+ }, null, 2),
1164
+ },
1165
+ ],
1166
+ };
1167
+ },
1168
+ };
1169
+ // ═══════════════════════════════════════════════════════════════════════════════
1170
+ // Tool 8: web_analytics — Analytics & Tracking Code Detection
1171
+ // ═══════════════════════════════════════════════════════════════════════════════
1172
+ const webAnalytics = {
1173
+ name: "web_analytics",
1174
+ description: "Detect analytics and tracking codes embedded in a web page. Identifies Google Analytics, GTM, Facebook Pixel, Hotjar, Segment, Mixpanel, Amplitude, PostHog, Plausible, Matomo, Heap, FullStory, Clarity, AdSense, Pinterest, Twitter, LinkedIn, TikTok pixels. Extracts tracking IDs for cross-domain correlation.",
1175
+ schema: {
1176
+ url: z.string().url().describe("URL to detect analytics/tracking codes"),
1177
+ },
1178
+ async execute(args) {
1179
+ const url = args.url;
1180
+ const html = await fetchPage(url);
1181
+ const detections = [];
1182
+ const seenServices = new Set();
1183
+ for (const service of ANALYTICS_DETECT) {
1184
+ for (const pattern of service.patterns) {
1185
+ if (pattern.test(html)) {
1186
+ if (seenServices.has(service.name))
1187
+ continue;
1188
+ seenServices.add(service.name);
1189
+ let trackingId = null;
1190
+ if (service.idPattern) {
1191
+ const idMatch = html.match(service.idPattern);
1192
+ if (idMatch)
1193
+ trackingId = idMatch[1] ?? idMatch[0];
1194
+ }
1195
+ detections.push({
1196
+ name: service.name,
1197
+ trackingId,
1198
+ crossReferable: service.crossRef,
1199
+ matchedPattern: pattern.source,
1200
+ });
1201
+ break;
1202
+ }
1203
+ }
1204
+ }
1205
+ // --- Cross-Reference Opportunities ---
1206
+ const crossRefIds = detections
1207
+ .filter((d) => d.crossReferable && d.trackingId)
1208
+ .map((d) => ({ service: d.name, id: d.trackingId }));
1209
+ return {
1210
+ content: [
1211
+ {
1212
+ type: "text",
1213
+ text: JSON.stringify({
1214
+ url,
1215
+ totalDetected: detections.length,
1216
+ detections,
1217
+ crossReferenceOpportunities: crossRefIds,
1218
+ note: crossRefIds.length > 0
1219
+ ? "These tracking IDs can be used to find other domains owned by the same organization"
1220
+ : "No cross-referenceable tracking IDs found",
1221
+ }, null, 2),
1222
+ },
1223
+ ],
1224
+ };
1225
+ },
1226
+ };
1227
+ // ═══════════════════════════════════════════════════════════════════════════════
1228
+ // Tool 9: web_sri — Subresource Integrity Analysis
1229
+ // ═══════════════════════════════════════════════════════════════════════════════
1230
+ const webSri = {
1231
+ name: "web_sri",
1232
+ description: "Analyze Subresource Integrity (SRI) usage on a web page. Identifies scripts and stylesheets loaded with or without SRI integrity attributes, extracts SHA hashes, detects trust chain gaps from CDN-loaded resources missing SRI, and compares CDN vs self-hosted resource security posture.",
1233
+ schema: {
1234
+ url: z.string().url().describe("URL to analyze Subresource Integrity"),
1235
+ },
1236
+ async execute(args) {
1237
+ const url = args.url;
1238
+ const html = await fetchPage(url);
1239
+ const $ = cheerio.load(html);
1240
+ const parsedUrl = new URL(url);
1241
+ const withSRI = [];
1242
+ const withoutSRI = [];
1243
+ function isCDNUrl(srcUrl) {
1244
+ try {
1245
+ const parsed = new URL(srcUrl, url);
1246
+ return parsed.hostname !== parsedUrl.hostname;
1247
+ }
1248
+ catch {
1249
+ return false;
1250
+ }
1251
+ }
1252
+ function extractAlgorithm(integrity) {
1253
+ const match = integrity.match(/^(sha\d+)-/);
1254
+ return match ? match[1] : "unknown";
1255
+ }
1256
+ // --- Analyze <script> tags ---
1257
+ $("script[src]").each((_i, el) => {
1258
+ const src = $(el).attr("src") ?? "";
1259
+ const integrity = $(el).attr("integrity") ?? "";
1260
+ const crossorigin = $(el).attr("crossorigin") ?? null;
1261
+ const fullSrc = resolveUrl(url, src);
1262
+ const cdn = isCDNUrl(fullSrc);
1263
+ if (integrity) {
1264
+ withSRI.push({
1265
+ tag: "script",
1266
+ src: fullSrc,
1267
+ integrity,
1268
+ algorithm: extractAlgorithm(integrity),
1269
+ crossorigin,
1270
+ isCDN: cdn,
1271
+ });
1272
+ }
1273
+ else {
1274
+ withoutSRI.push({
1275
+ tag: "script",
1276
+ src: fullSrc,
1277
+ isCDN: cdn,
1278
+ risk: cdn
1279
+ ? "HIGH — CDN-loaded script without SRI can be tampered if CDN is compromised"
1280
+ : "LOW — Self-hosted script, SRI less critical but still recommended",
1281
+ });
1282
+ }
1283
+ });
1284
+ // --- Analyze <link rel="stylesheet"> tags ---
1285
+ $('link[rel="stylesheet"][href]').each((_i, el) => {
1286
+ const href = $(el).attr("href") ?? "";
1287
+ const integrity = $(el).attr("integrity") ?? "";
1288
+ const crossorigin = $(el).attr("crossorigin") ?? null;
1289
+ const fullHref = resolveUrl(url, href);
1290
+ const cdn = isCDNUrl(fullHref);
1291
+ if (integrity) {
1292
+ withSRI.push({
1293
+ tag: "link",
1294
+ src: fullHref,
1295
+ integrity,
1296
+ algorithm: extractAlgorithm(integrity),
1297
+ crossorigin,
1298
+ isCDN: cdn,
1299
+ });
1300
+ }
1301
+ else {
1302
+ withoutSRI.push({
1303
+ tag: "link",
1304
+ src: fullHref,
1305
+ isCDN: cdn,
1306
+ risk: cdn
1307
+ ? "MEDIUM — CDN-loaded stylesheet without SRI could be modified"
1308
+ : "LOW — Self-hosted stylesheet",
1309
+ });
1310
+ }
1311
+ });
1312
+ // --- Stats ---
1313
+ const totalResources = withSRI.length + withoutSRI.length;
1314
+ const cdnWithoutSRI = withoutSRI.filter((r) => r.isCDN);
1315
+ const algorithmBreakdown = {};
1316
+ for (const r of withSRI) {
1317
+ algorithmBreakdown[r.algorithm] = (algorithmBreakdown[r.algorithm] ?? 0) + 1;
1318
+ }
1319
+ const sriCoverage = totalResources > 0 ? Math.round((withSRI.length / totalResources) * 100) : 0;
1320
+ return {
1321
+ content: [
1322
+ {
1323
+ type: "text",
1324
+ text: JSON.stringify({
1325
+ url,
1326
+ totalExternalResources: totalResources,
1327
+ withSRI: withSRI.length,
1328
+ withoutSRI: withoutSRI.length,
1329
+ sriCoveragePercent: sriCoverage,
1330
+ cdnResourcesWithoutSRI: cdnWithoutSRI.length,
1331
+ algorithmBreakdown,
1332
+ overallRisk: cdnWithoutSRI.length > 3
1333
+ ? "HIGH"
1334
+ : cdnWithoutSRI.length > 0
1335
+ ? "MEDIUM"
1336
+ : "LOW",
1337
+ resourcesWithSRI: withSRI,
1338
+ resourcesWithoutSRI: withoutSRI,
1339
+ recommendations: cdnWithoutSRI.length > 0
1340
+ ? [
1341
+ `Add SRI hashes to ${cdnWithoutSRI.length} CDN-loaded resource(s) to prevent supply-chain attacks`,
1342
+ "Use sha384 or sha512 for stronger integrity guarantees",
1343
+ 'Ensure crossorigin="anonymous" is set on all SRI-protected resources',
1344
+ ]
1345
+ : ["SRI is properly configured for CDN resources"],
1346
+ }, null, 2),
1347
+ },
1348
+ ],
1349
+ };
1350
+ },
1351
+ };
1352
+ // ═══════════════════════════════════════════════════════════════════════════════
1353
+ // Export
1354
+ // ═══════════════════════════════════════════════════════════════════════════════
1355
+ export const webTools = [
1356
+ webTech,
1357
+ webSourcemaps,
1358
+ webWebsocket,
1359
+ webApiDiscovery,
1360
+ webGraphql,
1361
+ webVhost,
1362
+ webSourcemapPaths,
1363
+ webAnalytics,
1364
+ webSri,
1365
+ ];
1366
+ //# sourceMappingURL=index.js.map