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,1067 @@
1
+ import { z } from "zod";
2
+ import { RateLimiter } from "../utils/rate-limiter.js";
3
+ import { TTLCache } from "../utils/cache.js";
4
+ import { resolve, resolve4, resolve6, resolveMx, resolveTxt, resolveSrv, resolveSoa, resolveCaa, resolveCname, resolveNs, resolvePtr, resolveNaptr, } from "node:dns/promises";
5
+ import { json } from "../types/index.js";
6
+ const limiter = new RateLimiter(100);
7
+ const cache = new TTLCache(600_000); // 10min cache
8
+ // ─── Helpers ───
9
+ /** Safely resolve a DNS query, returning null on NXDOMAIN/SERVFAIL/etc. */
10
+ async function safeResolve(fn) {
11
+ try {
12
+ return await fn();
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /** Build in-addr.arpa name for a given IPv4 address */
19
+ function toReverseArpa(ip) {
20
+ return ip.split(".").reverse().join(".") + ".in-addr.arpa";
21
+ }
22
+ // ─── Email Provider Patterns ───
23
+ const EMAIL_PROVIDERS = [
24
+ { pattern: /aspmx\.l\.google\.com/i, provider: "Google Workspace" },
25
+ { pattern: /google(mail)?\.com/i, provider: "Google Workspace" },
26
+ { pattern: /\.mail\.protection\.outlook\.com/i, provider: "Microsoft 365 (Exchange Online)" },
27
+ { pattern: /mail\.protonmail\.ch/i, provider: "ProtonMail" },
28
+ { pattern: /protonmail\.ch/i, provider: "ProtonMail" },
29
+ { pattern: /\.zoho\.com/i, provider: "Zoho Mail" },
30
+ { pattern: /\.zohomail\.com/i, provider: "Zoho Mail" },
31
+ { pattern: /\.fastmail\.com/i, provider: "Fastmail" },
32
+ { pattern: /in[12]?\.smtp\.messagingengine\.com/i, provider: "Fastmail" },
33
+ { pattern: /\.mimecast\.com/i, provider: "Mimecast" },
34
+ { pattern: /\.pphosted\.com/i, provider: "Proofpoint" },
35
+ { pattern: /\.barracuda(networks)?\.com/i, provider: "Barracuda" },
36
+ { pattern: /\.icloud\.com/i, provider: "Apple iCloud Mail" },
37
+ { pattern: /\.yahoodns\.net/i, provider: "Yahoo Mail" },
38
+ { pattern: /\.yandex\.(ru|net)/i, provider: "Yandex Mail" },
39
+ { pattern: /\.secureserver\.net/i, provider: "GoDaddy" },
40
+ { pattern: /\.emailsrvr\.com/i, provider: "Rackspace Email" },
41
+ { pattern: /\.mailgun\.org/i, provider: "Mailgun" },
42
+ { pattern: /\.sendgrid\.net/i, provider: "SendGrid" },
43
+ { pattern: /\.amazonses\.com/i, provider: "Amazon SES" },
44
+ { pattern: /\.postmarkapp\.com/i, provider: "Postmark" },
45
+ { pattern: /\.mandrillapp\.com/i, provider: "Mandrill (Mailchimp)" },
46
+ { pattern: /\.sparkpostmail\.com/i, provider: "SparkPost" },
47
+ ];
48
+ // ─── DKIM Selector Candidates ───
49
+ const DKIM_SELECTORS = [
50
+ "google",
51
+ "selector1",
52
+ "selector2",
53
+ "default",
54
+ "dkim",
55
+ "mail",
56
+ "k1",
57
+ "k2",
58
+ "s1",
59
+ "s2",
60
+ "mandrill",
61
+ "everlytickey1",
62
+ "everlytickey2",
63
+ "mxvault",
64
+ "smtp",
65
+ ];
66
+ // ─── SaaS TXT Patterns ───
67
+ const SAAS_TXT_PATTERNS = [
68
+ { pattern: /^google-site-verification=/i, service: "Google Search Console", category: "verification" },
69
+ { pattern: /^MS=ms/i, service: "Microsoft 365", category: "productivity" },
70
+ { pattern: /^facebook-domain-verification=/i, service: "Facebook Business", category: "marketing" },
71
+ { pattern: /^atlassian-domain-verification=/i, service: "Atlassian (Jira/Confluence)", category: "productivity" },
72
+ { pattern: /^stripe-verification=/i, service: "Stripe", category: "payments" },
73
+ { pattern: /^docusign=/i, service: "DocuSign", category: "productivity" },
74
+ { pattern: /^apple-domain-verification=/i, service: "Apple", category: "verification" },
75
+ { pattern: /^hubspot-developer-verification=/i, service: "HubSpot", category: "marketing" },
76
+ { pattern: /hubspot/i, service: "HubSpot", category: "marketing" },
77
+ { pattern: /^zoom-domain-verification=/i, service: "Zoom", category: "productivity" },
78
+ { pattern: /^_github-challenge-/i, service: "GitHub Pages", category: "development" },
79
+ { pattern: /^logmein-verification-code=/i, service: "LogMeIn / GoTo", category: "productivity" },
80
+ { pattern: /^pardot_/i, service: "Pardot (Salesforce Marketing)", category: "marketing" },
81
+ { pattern: /^dynatrace-site-verification=/i, service: "Dynatrace", category: "monitoring" },
82
+ { pattern: /^adobe-sign-verification=/i, service: "Adobe Sign", category: "productivity" },
83
+ { pattern: /^postmark-verification=/i, service: "Postmark", category: "email" },
84
+ { pattern: /^mandrill-domain-verification=/i, service: "Mandrill (Mailchimp)", category: "email" },
85
+ { pattern: /^docker-verification=/i, service: "Docker Hub", category: "development" },
86
+ { pattern: /^cisco-ci-domain-verification=/i, service: "Cisco Webex", category: "productivity" },
87
+ { pattern: /^have-i-been-pwned-verification=/i, service: "Have I Been Pwned", category: "security" },
88
+ { pattern: /^dropbox-domain-verification=/i, service: "Dropbox", category: "productivity" },
89
+ { pattern: /^slack-domain-verification=/i, service: "Slack", category: "productivity" },
90
+ { pattern: /^webexdomainverification/i, service: "Cisco Webex", category: "productivity" },
91
+ { pattern: /^amazonses:/i, service: "Amazon SES", category: "email" },
92
+ { pattern: /^t-verify=/i, service: "Trend Micro", category: "security" },
93
+ { pattern: /^miro-verification=/i, service: "Miro", category: "productivity" },
94
+ { pattern: /^ahrefs-site-verification=/i, service: "Ahrefs", category: "seo" },
95
+ { pattern: /^_1password/i, service: "1Password", category: "security" },
96
+ { pattern: /^status-page-domain-verification=/i, service: "Atlassian StatusPage", category: "monitoring" },
97
+ ];
98
+ // ─── SPF Include → Provider Mapping ───
99
+ const SPF_INCLUDE_PROVIDERS = [
100
+ { pattern: /google\.com/i, provider: "Google Workspace" },
101
+ { pattern: /googlemail\.com/i, provider: "Google Workspace" },
102
+ { pattern: /outlook\.com/i, provider: "Microsoft 365" },
103
+ { pattern: /protection\.outlook\.com/i, provider: "Microsoft 365" },
104
+ { pattern: /sendgrid\.net/i, provider: "SendGrid (Twilio)" },
105
+ { pattern: /amazonses\.com/i, provider: "Amazon SES" },
106
+ { pattern: /mailgun\.org/i, provider: "Mailgun" },
107
+ { pattern: /sparkpostmail\.com/i, provider: "SparkPost" },
108
+ { pattern: /postmarkapp\.com/i, provider: "Postmark" },
109
+ { pattern: /mandrillapp\.com/i, provider: "Mandrill (Mailchimp)" },
110
+ { pattern: /zoho\.(com|eu)/i, provider: "Zoho Mail" },
111
+ { pattern: /freshdesk\.com/i, provider: "Freshdesk" },
112
+ { pattern: /zendesk\.com/i, provider: "Zendesk" },
113
+ { pattern: /hubspot\.com/i, provider: "HubSpot" },
114
+ { pattern: /salesforce\.com/i, provider: "Salesforce" },
115
+ { pattern: /mcsv\.net/i, provider: "Mailchimp" },
116
+ { pattern: /icloud\.com/i, provider: "Apple iCloud" },
117
+ { pattern: /proofpoint\.com/i, provider: "Proofpoint" },
118
+ { pattern: /pphosted\.com/i, provider: "Proofpoint" },
119
+ { pattern: /mimecast\.(com|co\.za)/i, provider: "Mimecast" },
120
+ { pattern: /barracuda(networks)?\.com/i, provider: "Barracuda" },
121
+ { pattern: /intercom\.io/i, provider: "Intercom" },
122
+ { pattern: /brevo\.com/i, provider: "Brevo (Sendinblue)" },
123
+ { pattern: /sendinblue\.com/i, provider: "Brevo (Sendinblue)" },
124
+ { pattern: /mailjet\.com/i, provider: "Mailjet" },
125
+ { pattern: /constantcontact\.com/i, provider: "Constant Contact" },
126
+ { pattern: /cust-spf\.exacttarget\.com/i, provider: "Salesforce Marketing Cloud" },
127
+ { pattern: /servers\.mcsv\.net/i, provider: "Mailchimp" },
128
+ ];
129
+ // ─── Subdomain Takeover Vulnerable Services ───
130
+ const TAKEOVER_SERVICES = [
131
+ { pattern: /\.herokuapp\.com$/i, service: "Heroku", severity: "high" },
132
+ { pattern: /\.herokussl\.com$/i, service: "Heroku", severity: "high" },
133
+ { pattern: /\.github\.io$/i, service: "GitHub Pages", severity: "high" },
134
+ { pattern: /\.s3\.amazonaws\.com$/i, service: "AWS S3", severity: "high" },
135
+ { pattern: /\.s3-website[-.].+\.amazonaws\.com$/i, service: "AWS S3", severity: "high" },
136
+ { pattern: /\.s3\..+\.amazonaws\.com$/i, service: "AWS S3", severity: "high" },
137
+ { pattern: /\.azurewebsites\.net$/i, service: "Microsoft Azure", severity: "high" },
138
+ { pattern: /\.cloudapp\.azure\.com$/i, service: "Microsoft Azure", severity: "high" },
139
+ { pattern: /\.azure-api\.net$/i, service: "Microsoft Azure", severity: "high" },
140
+ { pattern: /\.azurefd\.net$/i, service: "Microsoft Azure", severity: "high" },
141
+ { pattern: /\.trafficmanager\.net$/i, service: "Microsoft Azure", severity: "high" },
142
+ { pattern: /shops\.myshopify\.com$/i, service: "Shopify", severity: "high" },
143
+ { pattern: /\.fastly\.net$/i, service: "Fastly", severity: "high" },
144
+ { pattern: /\.fastlylb\.net$/i, service: "Fastly", severity: "high" },
145
+ { pattern: /\.ghost\.io$/i, service: "Ghost", severity: "medium" },
146
+ { pattern: /\.pantheonsite\.io$/i, service: "Pantheon", severity: "medium" },
147
+ { pattern: /\.tumblr\.com$/i, service: "Tumblr", severity: "medium" },
148
+ { pattern: /\.zendesk\.com$/i, service: "Zendesk", severity: "medium" },
149
+ { pattern: /\.surge\.sh$/i, service: "Surge.sh", severity: "high" },
150
+ { pattern: /\.fly\.dev$/i, service: "Fly.io", severity: "medium" },
151
+ { pattern: /\.netlify\.app$/i, service: "Netlify", severity: "high" },
152
+ { pattern: /\.netlify\.com$/i, service: "Netlify", severity: "high" },
153
+ { pattern: /\.vercel\.app$/i, service: "Vercel", severity: "high" },
154
+ { pattern: /cname\.vercel-dns\.com$/i, service: "Vercel", severity: "high" },
155
+ { pattern: /\.readme\.io$/i, service: "ReadMe", severity: "medium" },
156
+ { pattern: /\.gitbook\.io$/i, service: "GitBook", severity: "medium" },
157
+ { pattern: /\.wordpress\.com$/i, service: "WordPress.com", severity: "low" },
158
+ { pattern: /\.unbouncepages\.com$/i, service: "Unbounce", severity: "medium" },
159
+ { pattern: /\.statuspage\.io$/i, service: "StatusPage (Atlassian)", severity: "medium" },
160
+ { pattern: /\.cargocollective\.com$/i, service: "Cargo Collective", severity: "medium" },
161
+ { pattern: /\.webflow\.io$/i, service: "Webflow", severity: "medium" },
162
+ { pattern: /\.helpjuice\.com$/i, service: "Helpjuice", severity: "medium" },
163
+ { pattern: /\.helpscoutdocs\.com$/i, service: "HelpScout", severity: "medium" },
164
+ { pattern: /\.teamwork\.com$/i, service: "Teamwork", severity: "medium" },
165
+ { pattern: /\.tictail\.com$/i, service: "Tictail", severity: "high" },
166
+ { pattern: /\.campaignmonitor\.com$/i, service: "Campaign Monitor", severity: "medium" },
167
+ { pattern: /\.desk\.com$/i, service: "Desk.com (Salesforce)", severity: "medium" },
168
+ { pattern: /\.thinkific\.com$/i, service: "Thinkific", severity: "medium" },
169
+ { pattern: /\.amazonaws\.com$/i, service: "AWS (generic)", severity: "medium" },
170
+ { pattern: /\.elasticbeanstalk\.com$/i, service: "AWS Elastic Beanstalk", severity: "medium" },
171
+ { pattern: /\.cloudfront\.net$/i, service: "AWS CloudFront", severity: "low" },
172
+ { pattern: /\.firebaseapp\.com$/i, service: "Firebase", severity: "medium" },
173
+ { pattern: /\.web\.app$/i, service: "Firebase Hosting", severity: "medium" },
174
+ { pattern: /\.bitbucket\.io$/i, service: "Bitbucket", severity: "medium" },
175
+ ];
176
+ // ─── NS Provider Detection ───
177
+ const NS_PROVIDERS = [
178
+ { pattern: /\.cloudflare\.com$/i, provider: "Cloudflare" },
179
+ { pattern: /\.awsdns-/i, provider: "AWS Route53" },
180
+ { pattern: /\.googledomains\.com$/i, provider: "Google Domains" },
181
+ { pattern: /\.google\.com$/i, provider: "Google Cloud DNS" },
182
+ { pattern: /\.azure-dns\.(com|net|org|info)$/i, provider: "Azure DNS" },
183
+ { pattern: /\.ultradns\.(com|net|org)$/i, provider: "UltraDNS (Neustar)" },
184
+ { pattern: /\.nsone\.net$/i, provider: "NS1" },
185
+ { pattern: /\.dnsimple\.com$/i, provider: "DNSimple" },
186
+ { pattern: /\.domaincontrol\.com$/i, provider: "GoDaddy" },
187
+ { pattern: /\.registrar-servers\.com$/i, provider: "Namecheap" },
188
+ { pattern: /\.digitalocean\.com$/i, provider: "DigitalOcean" },
189
+ { pattern: /\.linode\.com$/i, provider: "Linode (Akamai)" },
190
+ { pattern: /\.akam\.net$/i, provider: "Akamai" },
191
+ { pattern: /\.dynect\.net$/i, provider: "Dyn (Oracle)" },
192
+ { pattern: /\.hetzner\.com$/i, provider: "Hetzner" },
193
+ { pattern: /\.ovh\.(net|com)$/i, provider: "OVH" },
194
+ { pattern: /\.name-services\.com$/i, provider: "Enom" },
195
+ { pattern: /\.he\.net$/i, provider: "Hurricane Electric" },
196
+ ];
197
+ // ─── Default Subdomains ───
198
+ const DEFAULT_SUBDOMAINS = [
199
+ "www", "mail", "dev", "staging", "api", "admin", "cdn", "blog", "shop",
200
+ "app", "test", "beta", "demo", "status", "docs", "portal", "webmail",
201
+ "ftp", "cpanel", "vpn", "ns1", "ns2", "mx", "smtp", "pop", "imap",
202
+ "remote", "support", "help", "store", "media", "assets", "static",
203
+ "jenkins", "ci", "git", "jira", "confluence", "grafana", "monitoring",
204
+ ];
205
+ // ─── SRV Prefixes ───
206
+ const SRV_PREFIXES = [
207
+ "_sip._tcp", "_sips._tcp", "_xmpp-client._tcp", "_xmpp-server._tcp",
208
+ "_autodiscover._tcp", "_ldap._tcp", "_kerberos._tcp", "_kerberos._udp",
209
+ "_imap._tcp", "_imaps._tcp", "_submission._tcp", "_h323cs._tcp",
210
+ "_caldavs._tcp", "_carddavs._tcp", "_turn._tcp", "_turn._udp",
211
+ "_stun._tcp", "_stun._udp", "_minecraft._tcp", "_ts3._udp",
212
+ "_vlmcs._tcp", "_mta-sts._tcp",
213
+ ];
214
+ // ─── Tool 1: dns_email ───
215
+ async function dnsEmail(args) {
216
+ const domain = args.domain;
217
+ const cacheKey = `dns_email:${domain}`;
218
+ const cached = cache.get(cacheKey);
219
+ if (cached)
220
+ return json(cached);
221
+ await limiter.acquire();
222
+ // MX records
223
+ const mxRecords = await safeResolve(() => resolveMx(domain));
224
+ const mxEntries = (mxRecords ?? [])
225
+ .sort((a, b) => a.priority - b.priority)
226
+ .map((mx) => {
227
+ const provider = EMAIL_PROVIDERS.find((p) => p.pattern.test(mx.exchange));
228
+ return {
229
+ priority: mx.priority,
230
+ exchange: mx.exchange,
231
+ provider: provider?.provider ?? "Unknown",
232
+ };
233
+ });
234
+ const detectedProviders = [...new Set(mxEntries.map((e) => e.provider).filter((p) => p !== "Unknown"))];
235
+ // SPF record
236
+ const txtRecords = await safeResolve(() => resolveTxt(domain));
237
+ const allTxt = (txtRecords ?? []).map((r) => r.join(""));
238
+ const spfRecord = allTxt.find((r) => r.startsWith("v=spf1"));
239
+ let spfAnalysis = null;
240
+ if (spfRecord) {
241
+ const includes = [];
242
+ const ip4s = [];
243
+ const ip6s = [];
244
+ const mechanisms = [];
245
+ const authorizedSenders = [];
246
+ const parts = spfRecord.split(/\s+/);
247
+ for (const part of parts) {
248
+ if (part.startsWith("include:")) {
249
+ const includeDomain = part.replace("include:", "");
250
+ includes.push(includeDomain);
251
+ const match = SPF_INCLUDE_PROVIDERS.find((p) => p.pattern.test(includeDomain));
252
+ if (match)
253
+ authorizedSenders.push(match.provider);
254
+ }
255
+ else if (part.startsWith("ip4:")) {
256
+ ip4s.push(part.replace("ip4:", ""));
257
+ }
258
+ else if (part.startsWith("ip6:")) {
259
+ ip6s.push(part.replace("ip6:", ""));
260
+ }
261
+ else if (part !== "v=spf1") {
262
+ mechanisms.push(part);
263
+ }
264
+ }
265
+ spfAnalysis = {
266
+ raw: spfRecord,
267
+ includes,
268
+ ip4: ip4s,
269
+ ip6: ip6s,
270
+ mechanisms,
271
+ authorizedSenders,
272
+ };
273
+ }
274
+ // DKIM selector probing
275
+ const dkimResults = [];
276
+ for (const selector of DKIM_SELECTORS) {
277
+ await limiter.acquire();
278
+ const dkimDomain = `${selector}._domainkey.${domain}`;
279
+ const dkimTxt = await safeResolve(() => resolveTxt(dkimDomain));
280
+ if (dkimTxt && dkimTxt.length > 0) {
281
+ dkimResults.push({
282
+ selector,
283
+ record: dkimTxt.map((r) => r.join("")).join(""),
284
+ });
285
+ }
286
+ }
287
+ // DMARC
288
+ await limiter.acquire();
289
+ const dmarcTxt = await safeResolve(() => resolveTxt(`_dmarc.${domain}`));
290
+ let dmarcAnalysis = null;
291
+ if (dmarcTxt && dmarcTxt.length > 0) {
292
+ const dmarcRaw = dmarcTxt.map((r) => r.join("")).join("");
293
+ const tags = new Map();
294
+ for (const part of dmarcRaw.split(";")) {
295
+ const [key, ...vals] = part.trim().split("=");
296
+ if (key && vals.length > 0)
297
+ tags.set(key.trim(), vals.join("=").trim());
298
+ }
299
+ dmarcAnalysis = {
300
+ raw: dmarcRaw,
301
+ policy: tags.get("p") ?? "none",
302
+ subdomain_policy: tags.get("sp") ?? null,
303
+ rua: tags.get("rua") ?? null,
304
+ ruf: tags.get("ruf") ?? null,
305
+ pct: tags.get("pct") ?? null,
306
+ adkim: tags.get("adkim") ?? null,
307
+ aspf: tags.get("aspf") ?? null,
308
+ };
309
+ }
310
+ const result = {
311
+ domain,
312
+ mx: mxEntries,
313
+ detectedProviders,
314
+ spf: spfAnalysis,
315
+ dkim: {
316
+ selectorsProbed: DKIM_SELECTORS.length,
317
+ found: dkimResults,
318
+ },
319
+ dmarc: dmarcAnalysis,
320
+ emailSecurity: {
321
+ hasMx: mxEntries.length > 0,
322
+ hasSpf: spfAnalysis !== null,
323
+ hasDkim: dkimResults.length > 0,
324
+ hasDmarc: dmarcAnalysis !== null,
325
+ dmarcPolicy: dmarcAnalysis?.policy ?? "none",
326
+ score: calculateEmailSecurityScore(mxEntries.length > 0, spfAnalysis !== null, dkimResults.length > 0, dmarcAnalysis),
327
+ },
328
+ };
329
+ cache.set(cacheKey, result);
330
+ return json(result);
331
+ }
332
+ function calculateEmailSecurityScore(hasMx, hasSpf, hasDkim, dmarc) {
333
+ const details = [];
334
+ let score = 0;
335
+ if (!hasMx) {
336
+ details.push("No MX records — domain may not receive email");
337
+ return { grade: "N/A", details };
338
+ }
339
+ if (hasSpf) {
340
+ score += 25;
341
+ details.push("SPF present (+25)");
342
+ }
343
+ else {
344
+ details.push("Missing SPF record — email spoofing possible");
345
+ }
346
+ if (hasDkim) {
347
+ score += 25;
348
+ details.push("DKIM present (+25)");
349
+ }
350
+ else {
351
+ details.push("No common DKIM selectors found — consider DKIM deployment");
352
+ }
353
+ if (dmarc) {
354
+ score += 25;
355
+ details.push("DMARC present (+25)");
356
+ if (dmarc.policy === "reject") {
357
+ score += 25;
358
+ details.push("DMARC policy=reject (+25) — strongest protection");
359
+ }
360
+ else if (dmarc.policy === "quarantine") {
361
+ score += 15;
362
+ details.push("DMARC policy=quarantine (+15) — moderate protection");
363
+ }
364
+ else {
365
+ details.push("DMARC policy=none (+0) — monitoring only, no enforcement");
366
+ }
367
+ }
368
+ else {
369
+ details.push("Missing DMARC record — no email authentication policy");
370
+ }
371
+ const grade = score >= 90 ? "A" :
372
+ score >= 75 ? "B" :
373
+ score >= 50 ? "C" :
374
+ score >= 25 ? "D" : "F";
375
+ return { grade, details };
376
+ }
377
+ // ─── Tool 2: dns_saas ───
378
+ async function dnsSaas(args) {
379
+ const domain = args.domain;
380
+ const cacheKey = `dns_saas:${domain}`;
381
+ const cached = cache.get(cacheKey);
382
+ if (cached)
383
+ return json(cached);
384
+ await limiter.acquire();
385
+ const txtRecords = await safeResolve(() => resolveTxt(domain));
386
+ const allTxt = (txtRecords ?? []).map((r) => r.join(""));
387
+ const detectedServices = [];
388
+ for (const record of allTxt) {
389
+ for (const { pattern, service, category } of SAAS_TXT_PATTERNS) {
390
+ if (pattern.test(record)) {
391
+ detectedServices.push({ service, category, record });
392
+ break; // Only match first pattern per record
393
+ }
394
+ }
395
+ }
396
+ // Extract SPF includes as email provider indicators
397
+ const spfRecord = allTxt.find((r) => r.startsWith("v=spf1"));
398
+ const spfProviders = [];
399
+ if (spfRecord) {
400
+ const includeMatches = spfRecord.match(/include:(\S+)/g);
401
+ if (includeMatches) {
402
+ for (const incl of includeMatches) {
403
+ const includeDomain = incl.replace("include:", "");
404
+ const match = SPF_INCLUDE_PROVIDERS.find((p) => p.pattern.test(includeDomain));
405
+ if (match) {
406
+ spfProviders.push({ provider: match.provider, include: includeDomain });
407
+ }
408
+ }
409
+ }
410
+ }
411
+ // Group by category
412
+ const byCategory = {};
413
+ for (const svc of detectedServices) {
414
+ if (!byCategory[svc.category])
415
+ byCategory[svc.category] = [];
416
+ if (!byCategory[svc.category].includes(svc.service)) {
417
+ byCategory[svc.category].push(svc.service);
418
+ }
419
+ }
420
+ const result = {
421
+ domain,
422
+ totalTxtRecords: allTxt.length,
423
+ detectedServices,
424
+ spfEmailProviders: spfProviders,
425
+ servicesByCategory: byCategory,
426
+ totalServicesDetected: detectedServices.length + spfProviders.length,
427
+ };
428
+ cache.set(cacheKey, result);
429
+ return json(result);
430
+ }
431
+ // ─── Tool 3: dns_takeover ───
432
+ async function dnsTakeover(args) {
433
+ const domain = args.domain;
434
+ const subdomains = args.subdomains ?? DEFAULT_SUBDOMAINS;
435
+ const cacheKey = `dns_takeover:${domain}:${subdomains.join(",")}`;
436
+ const cached = cache.get(cacheKey);
437
+ if (cached)
438
+ return json(cached);
439
+ const findings = [];
440
+ for (const sub of subdomains) {
441
+ const fqdn = `${sub}.${domain}`;
442
+ await limiter.acquire();
443
+ // Resolve CNAME
444
+ const cname = await safeResolve(() => resolveCname(fqdn));
445
+ if (!cname || cname.length === 0) {
446
+ findings.push({
447
+ subdomain: sub,
448
+ fqdn,
449
+ cname: "",
450
+ cnameResolvable: false,
451
+ vulnerableService: null,
452
+ severity: null,
453
+ status: "no_cname",
454
+ });
455
+ continue;
456
+ }
457
+ const cnameTarget = cname[0];
458
+ // Try to resolve the CNAME target to see if it exists
459
+ await limiter.acquire();
460
+ const cnameA = await safeResolve(() => resolve4(cnameTarget));
461
+ const cnameResolvable = cnameA !== null && cnameA.length > 0;
462
+ // Check if CNAME matches known vulnerable services
463
+ let vulnerableService = null;
464
+ let severity = null;
465
+ for (const svc of TAKEOVER_SERVICES) {
466
+ if (svc.pattern.test(cnameTarget)) {
467
+ vulnerableService = svc.service;
468
+ severity = svc.severity;
469
+ break;
470
+ }
471
+ }
472
+ let status;
473
+ if (vulnerableService && !cnameResolvable) {
474
+ status = "vulnerable";
475
+ }
476
+ else if (vulnerableService && cnameResolvable) {
477
+ status = "potentially_vulnerable";
478
+ }
479
+ else if (!cnameResolvable) {
480
+ status = "potentially_vulnerable";
481
+ }
482
+ else {
483
+ status = "safe";
484
+ }
485
+ findings.push({
486
+ subdomain: sub,
487
+ fqdn,
488
+ cname: cnameTarget,
489
+ cnameResolvable,
490
+ vulnerableService,
491
+ severity,
492
+ status,
493
+ });
494
+ }
495
+ const vulnerable = findings.filter((f) => f.status === "vulnerable");
496
+ const potentiallyVulnerable = findings.filter((f) => f.status === "potentially_vulnerable");
497
+ const result = {
498
+ domain,
499
+ subdomainsChecked: subdomains.length,
500
+ summary: {
501
+ vulnerable: vulnerable.length,
502
+ potentiallyVulnerable: potentiallyVulnerable.length,
503
+ safe: findings.filter((f) => f.status === "safe").length,
504
+ noCname: findings.filter((f) => f.status === "no_cname").length,
505
+ },
506
+ vulnerableFindings: vulnerable,
507
+ potentiallyVulnerableFindings: potentiallyVulnerable,
508
+ allFindings: findings,
509
+ };
510
+ cache.set(cacheKey, result);
511
+ return json(result);
512
+ }
513
+ // ─── Tool 4: dns_server_fp ───
514
+ async function dnsServerFp(args) {
515
+ const domain = args.domain;
516
+ const nameserverArg = args.nameserver;
517
+ const cacheKey = `dns_server_fp:${domain}:${nameserverArg ?? "auto"}`;
518
+ const cached = cache.get(cacheKey);
519
+ if (cached)
520
+ return json(cached);
521
+ await limiter.acquire();
522
+ // Detect nameservers
523
+ const nsRecords = await safeResolve(() => resolveNs(domain));
524
+ const nameservers = nsRecords ?? [];
525
+ const targetNs = nameserverArg ?? nameservers[0] ?? null;
526
+ if (!targetNs) {
527
+ return json({
528
+ domain,
529
+ error: "No nameservers found and none specified",
530
+ });
531
+ }
532
+ // Resolve nameserver IP
533
+ await limiter.acquire();
534
+ const nsIps = await safeResolve(() => resolve4(targetNs));
535
+ const nsIp = nsIps?.[0] ?? null;
536
+ // NS provider detection
537
+ let nsProvider = null;
538
+ for (const { pattern, provider } of NS_PROVIDERS) {
539
+ if (pattern.test(targetNs)) {
540
+ nsProvider = provider;
541
+ break;
542
+ }
543
+ }
544
+ // CHAOS TXT queries — node:dns/promises doesn't support CHAOS class directly,
545
+ // so we document what would be queried and note the limitation.
546
+ const chaosQueries = {
547
+ note: "CHAOS class TXT queries require raw DNS packets (not available via node:dns/promises)",
548
+ queriesAttempted: ["version.bind", "hostname.bind", "id.server"],
549
+ recommendation: "Use dig +short chaos txt version.bind @" + targetNs + " for manual verification",
550
+ };
551
+ // Attempt recursive query (check if server responds for external domain)
552
+ await limiter.acquire();
553
+ let openResolver = false;
554
+ try {
555
+ const { Resolver } = await import("node:dns/promises");
556
+ const resolver = new Resolver();
557
+ if (nsIp) {
558
+ resolver.setServers([nsIp]);
559
+ const testResolve = await safeResolve(() => resolver.resolve4("example.com"));
560
+ openResolver = testResolve !== null && testResolve.length > 0;
561
+ }
562
+ }
563
+ catch {
564
+ // Cannot test recursive query
565
+ }
566
+ // AXFR attempt — zone transfer requires raw TCP DNS,
567
+ // not supported via node:dns/promises
568
+ const axfrResult = {
569
+ note: "Zone transfer (AXFR) requires raw TCP DNS packets",
570
+ recommendation: `Use: dig axfr ${domain} @${targetNs} for manual verification`,
571
+ };
572
+ // Attempt EDNS0 — check via Resolver if available
573
+ const edns0 = {
574
+ note: "EDNS0 detection requires raw DNS packet inspection",
575
+ recommendation: `Use: dig +edns ${domain} @${targetNs} to verify EDNS0 support`,
576
+ };
577
+ // NSID
578
+ const nsid = {
579
+ note: "NSID option detection requires EDNS0 raw packet support",
580
+ recommendation: `Use: dig +nsid ${domain} @${targetNs} to check NSID`,
581
+ };
582
+ const result = {
583
+ domain,
584
+ targetNameserver: targetNs,
585
+ nameserverIp: nsIp,
586
+ allNameservers: nameservers,
587
+ nsProvider,
588
+ fingerprinting: {
589
+ chaosQueries,
590
+ openResolver: {
591
+ tested: nsIp !== null,
592
+ isOpen: openResolver,
593
+ risk: openResolver
594
+ ? "HIGH — Open resolver detected. Vulnerable to DNS amplification attacks."
595
+ : "Low — Server does not respond to recursive queries for external domains.",
596
+ },
597
+ axfr: axfrResult,
598
+ edns0,
599
+ nsid,
600
+ },
601
+ securityIssues: [
602
+ ...(openResolver ? ["Open DNS resolver — vulnerable to DNS amplification DDoS attacks"] : []),
603
+ ],
604
+ manualVerificationCommands: {
605
+ chaosVersion: `dig +short chaos txt version.bind @${targetNs}`,
606
+ chaosHostname: `dig +short chaos txt hostname.bind @${targetNs}`,
607
+ chaosId: `dig +short chaos txt id.server @${targetNs}`,
608
+ zoneTransfer: `dig axfr ${domain} @${targetNs}`,
609
+ edns0: `dig +edns ${domain} @${targetNs}`,
610
+ nsid: `dig +nsid ${domain} @${targetNs}`,
611
+ recursion: `dig example.com @${targetNs} +recurse`,
612
+ },
613
+ };
614
+ cache.set(cacheKey, result);
615
+ return json(result);
616
+ }
617
+ // ─── Tool 5: dns_records ───
618
+ async function dnsRecords(args) {
619
+ const domain = args.domain;
620
+ const cacheKey = `dns_records:${domain}`;
621
+ const cached = cache.get(cacheKey);
622
+ if (cached)
623
+ return json(cached);
624
+ await limiter.acquire();
625
+ // A records
626
+ const aRecords = await safeResolve(() => resolve4(domain));
627
+ // AAAA records
628
+ await limiter.acquire();
629
+ const aaaaRecords = await safeResolve(() => resolve6(domain));
630
+ // MX records
631
+ await limiter.acquire();
632
+ const mxRecords = await safeResolve(() => resolveMx(domain));
633
+ const mxEntries = (mxRecords ?? []).sort((a, b) => a.priority - b.priority);
634
+ // TXT records
635
+ await limiter.acquire();
636
+ const txtRecords = await safeResolve(() => resolveTxt(domain));
637
+ const allTxt = (txtRecords ?? []).map((r) => r.join(""));
638
+ // NS records
639
+ await limiter.acquire();
640
+ const nsRecords = await safeResolve(() => resolveNs(domain));
641
+ const nsEntries = (nsRecords ?? []).map((ns) => {
642
+ const provider = NS_PROVIDERS.find((p) => p.pattern.test(ns));
643
+ return { nameserver: ns, provider: provider?.provider ?? "Unknown" };
644
+ });
645
+ // SOA record
646
+ await limiter.acquire();
647
+ const soaRecord = await safeResolve(() => resolveSoa(domain));
648
+ let soaAnalysis = null;
649
+ if (soaRecord) {
650
+ // Convert SOA hostmaster to email: replace first '.' with '@'
651
+ const hostmaster = soaRecord.hostmaster;
652
+ const firstDot = hostmaster.indexOf(".");
653
+ const email = firstDot > 0
654
+ ? hostmaster.substring(0, firstDot) + "@" + hostmaster.substring(firstDot + 1)
655
+ : hostmaster;
656
+ soaAnalysis = {
657
+ nsname: soaRecord.nsname,
658
+ hostmaster: soaRecord.hostmaster,
659
+ hostmasterEmail: email,
660
+ serial: soaRecord.serial,
661
+ refresh: soaRecord.refresh,
662
+ retry: soaRecord.retry,
663
+ expire: soaRecord.expire,
664
+ minttl: soaRecord.minttl,
665
+ };
666
+ }
667
+ // CAA records
668
+ await limiter.acquire();
669
+ const caaRecords = await safeResolve(() => resolveCaa(domain));
670
+ const caaEntries = (caaRecords ?? []).map((caa) => {
671
+ const tag = caa.issue !== undefined
672
+ ? "issue"
673
+ : caa.issuewild !== undefined
674
+ ? "issuewild"
675
+ : caa.iodef !== undefined
676
+ ? "iodef"
677
+ : caa.contactemail !== undefined
678
+ ? "contactemail"
679
+ : "unknown";
680
+ const value = caa.issue ?? caa.issuewild ?? caa.iodef ?? caa.contactemail ?? "";
681
+ return { critical: caa.critical, tag, value };
682
+ });
683
+ // CNAME record
684
+ await limiter.acquire();
685
+ const cnameRecords = await safeResolve(() => resolveCname(domain));
686
+ // SRV records — probe common prefixes
687
+ const srvResults = [];
688
+ for (const prefix of SRV_PREFIXES) {
689
+ await limiter.acquire();
690
+ const srvName = `${prefix}.${domain}`;
691
+ const srvRecords = await safeResolve(() => resolveSrv(srvName));
692
+ if (srvRecords && srvRecords.length > 0) {
693
+ srvResults.push({
694
+ service: prefix,
695
+ records: srvRecords.map((srv) => ({
696
+ priority: srv.priority,
697
+ weight: srv.weight,
698
+ port: srv.port,
699
+ name: srv.name,
700
+ })),
701
+ });
702
+ }
703
+ }
704
+ // NAPTR records
705
+ await limiter.acquire();
706
+ const naptrRecords = await safeResolve(() => resolveNaptr(domain));
707
+ const result = {
708
+ domain,
709
+ records: {
710
+ A: aRecords ?? [],
711
+ AAAA: aaaaRecords ?? [],
712
+ MX: mxEntries,
713
+ TXT: allTxt,
714
+ NS: nsEntries,
715
+ SOA: soaAnalysis,
716
+ CAA: caaEntries,
717
+ CNAME: cnameRecords ?? [],
718
+ SRV: srvResults,
719
+ NAPTR: naptrRecords ?? [],
720
+ },
721
+ analysis: {
722
+ nsProvider: nsEntries.length > 0
723
+ ? [...new Set(nsEntries.map((e) => e.provider).filter((p) => p !== "Unknown"))]
724
+ : [],
725
+ authorizedCAs: caaEntries
726
+ .filter((c) => c.tag === "issue" || c.tag === "issuewild")
727
+ .map((c) => c.value),
728
+ discoveredServices: srvResults.map((s) => ({
729
+ service: s.service,
730
+ endpoints: s.records.map((r) => `${r.name}:${r.port}`),
731
+ })),
732
+ hasIPv6: (aaaaRecords ?? []).length > 0,
733
+ hasCaa: caaEntries.length > 0,
734
+ hasDnssec: false, // Would need raw packet inspection
735
+ },
736
+ recordCounts: {
737
+ A: (aRecords ?? []).length,
738
+ AAAA: (aaaaRecords ?? []).length,
739
+ MX: mxEntries.length,
740
+ TXT: allTxt.length,
741
+ NS: nsEntries.length,
742
+ SOA: soaAnalysis ? 1 : 0,
743
+ CAA: caaEntries.length,
744
+ CNAME: (cnameRecords ?? []).length,
745
+ SRV: srvResults.reduce((sum, s) => sum + s.records.length, 0),
746
+ NAPTR: (naptrRecords ?? []).length,
747
+ total: (aRecords ?? []).length +
748
+ (aaaaRecords ?? []).length +
749
+ mxEntries.length +
750
+ allTxt.length +
751
+ nsEntries.length +
752
+ (soaAnalysis ? 1 : 0) +
753
+ caaEntries.length +
754
+ (cnameRecords ?? []).length +
755
+ srvResults.reduce((sum, s) => sum + s.records.length, 0) +
756
+ (naptrRecords ?? []).length,
757
+ },
758
+ };
759
+ cache.set(cacheKey, result);
760
+ return json(result);
761
+ }
762
+ // ─── Tool 6: dns_reverse ───
763
+ async function dnsReverse(args) {
764
+ const ip = args.ip;
765
+ const range = args.range ?? false;
766
+ const cacheKey = `dns_reverse:${ip}:${range}`;
767
+ const cached = cache.get(cacheKey);
768
+ if (cached)
769
+ return json(cached);
770
+ if (!range) {
771
+ // Single IP PTR lookup
772
+ await limiter.acquire();
773
+ const arpaName = toReverseArpa(ip);
774
+ const ptrRecords = await safeResolve(() => resolvePtr(arpaName));
775
+ const result = {
776
+ ip,
777
+ arpaName,
778
+ ptr: ptrRecords ?? [],
779
+ hostnames: ptrRecords ?? [],
780
+ hasPtr: (ptrRecords ?? []).length > 0,
781
+ };
782
+ cache.set(cacheKey, result);
783
+ return json(result);
784
+ }
785
+ // /24 range scan
786
+ const octets = ip.split(".");
787
+ if (octets.length !== 4) {
788
+ return json({ error: "Invalid IPv4 address format" });
789
+ }
790
+ const base = octets.slice(0, 3).join(".");
791
+ const results = [];
792
+ const hostnamePatterns = {};
793
+ // Batch resolve — process in chunks to avoid overwhelming DNS
794
+ const BATCH_SIZE = 20;
795
+ for (let batch = 0; batch < 256; batch += BATCH_SIZE) {
796
+ const promises = [];
797
+ for (let i = batch; i < Math.min(batch + BATCH_SIZE, 256); i++) {
798
+ const targetIp = `${base}.${i}`;
799
+ promises.push((async () => {
800
+ await limiter.acquire();
801
+ const arpaName = toReverseArpa(targetIp);
802
+ const ptrRecords = await safeResolve(() => resolvePtr(arpaName));
803
+ if (ptrRecords && ptrRecords.length > 0) {
804
+ results.push({ ip: targetIp, ptr: ptrRecords });
805
+ // Extract hostname patterns
806
+ for (const hostname of ptrRecords) {
807
+ // Extract the parent domain pattern
808
+ const parts = hostname.split(".");
809
+ if (parts.length >= 2) {
810
+ const parentDomain = parts.slice(-2).join(".");
811
+ hostnamePatterns[parentDomain] = (hostnamePatterns[parentDomain] ?? 0) + 1;
812
+ }
813
+ }
814
+ }
815
+ })());
816
+ }
817
+ await Promise.all(promises);
818
+ }
819
+ const result = {
820
+ range: `${base}.0/24`,
821
+ scanned: 256,
822
+ found: results.length,
823
+ results: results.sort((a, b) => {
824
+ const aNum = parseInt(a.ip.split(".")[3], 10);
825
+ const bNum = parseInt(b.ip.split(".")[3], 10);
826
+ return aNum - bNum;
827
+ }),
828
+ hostnamePatterns: Object.entries(hostnamePatterns)
829
+ .sort(([, a], [, b]) => b - a)
830
+ .map(([domain, count]) => ({ domain, count })),
831
+ };
832
+ cache.set(cacheKey, result);
833
+ return json(result);
834
+ }
835
+ // ─── Tool 7: dns_mta_sts ───
836
+ async function dnsMtaSts(args) {
837
+ const domain = args.domain;
838
+ const cacheKey = `dns_mta_sts:${domain}`;
839
+ const cached = cache.get(cacheKey);
840
+ if (cached)
841
+ return json(cached);
842
+ await limiter.acquire();
843
+ // MTA-STS TXT record
844
+ const mtaStsTxt = await safeResolve(() => resolveTxt(`_mta-sts.${domain}`));
845
+ const mtaStsRecord = mtaStsTxt
846
+ ? mtaStsTxt.map((r) => r.join("")).join("")
847
+ : null;
848
+ let mtaStsVersion = null;
849
+ if (mtaStsRecord) {
850
+ const versionMatch = mtaStsRecord.match(/v=STSv1/i);
851
+ mtaStsVersion = versionMatch ? "STSv1" : null;
852
+ }
853
+ // Fetch MTA-STS policy via HTTPS
854
+ let mtaStsPolicy = null;
855
+ try {
856
+ await limiter.acquire();
857
+ const policyUrl = `https://mta-sts.${domain}/.well-known/mta-sts.txt`;
858
+ const response = await fetch(policyUrl, {
859
+ signal: AbortSignal.timeout(10_000),
860
+ });
861
+ if (response.ok) {
862
+ const policyText = await response.text();
863
+ const lines = policyText.split("\n").map((l) => l.trim()).filter(Boolean);
864
+ let mode = null;
865
+ const mxHosts = [];
866
+ let maxAge = null;
867
+ for (const line of lines) {
868
+ if (line.startsWith("mode:")) {
869
+ mode = line.replace("mode:", "").trim();
870
+ }
871
+ else if (line.startsWith("mx:")) {
872
+ mxHosts.push(line.replace("mx:", "").trim());
873
+ }
874
+ else if (line.startsWith("max_age:")) {
875
+ maxAge = parseInt(line.replace("max_age:", "").trim(), 10);
876
+ }
877
+ }
878
+ mtaStsPolicy = { raw: policyText, mode, mxHosts, maxAge };
879
+ }
880
+ }
881
+ catch {
882
+ // MTA-STS policy fetch failed — domain may not support it
883
+ }
884
+ // TLSRPT TXT record
885
+ await limiter.acquire();
886
+ const tlsrptTxt = await safeResolve(() => resolveTxt(`_smtp._tls.${domain}`));
887
+ let tlsrpt = null;
888
+ if (tlsrptTxt && tlsrptTxt.length > 0) {
889
+ const tlsrptRaw = tlsrptTxt.map((r) => r.join("")).join("");
890
+ const version = tlsrptRaw.match(/v=TLSRPTv1/i) ? "TLSRPTv1" : null;
891
+ const ruaMatches = tlsrptRaw.match(/rua=([^;]+)/i);
892
+ const rua = ruaMatches
893
+ ? ruaMatches[1].split(",").map((r) => r.trim())
894
+ : [];
895
+ tlsrpt = { raw: tlsrptRaw, version, rua };
896
+ }
897
+ // DANE — check for TLSA records at _25._tcp.<mx>
898
+ await limiter.acquire();
899
+ const mxRecords = await safeResolve(() => resolveMx(domain));
900
+ const daneResults = [];
901
+ if (mxRecords && mxRecords.length > 0) {
902
+ for (const mx of mxRecords.slice(0, 5)) {
903
+ // Limit to first 5 MX hosts
904
+ await limiter.acquire();
905
+ const tlsaDomain = `_25._tcp.${mx.exchange}`;
906
+ // node:dns/promises doesn't have a direct TLSA resolver,
907
+ // use generic resolve() with type "TLSA" — may not be supported
908
+ // on all platforms. Fall back to noting the limitation.
909
+ let hasTlsa = false;
910
+ let tlsaRecords = [];
911
+ try {
912
+ const result = await resolve(tlsaDomain, "TLSA");
913
+ if (result && result.length > 0) {
914
+ hasTlsa = true;
915
+ tlsaRecords = result.map((r) => JSON.stringify(r));
916
+ }
917
+ }
918
+ catch {
919
+ // TLSA query not supported or NXDOMAIN
920
+ }
921
+ daneResults.push({
922
+ mxHost: mx.exchange,
923
+ tlsaDomain,
924
+ hasTlsa,
925
+ records: tlsaRecords,
926
+ });
927
+ }
928
+ }
929
+ const result = {
930
+ domain,
931
+ mtaSts: {
932
+ txtRecord: mtaStsRecord,
933
+ version: mtaStsVersion,
934
+ hasRecord: mtaStsRecord !== null,
935
+ policy: mtaStsPolicy,
936
+ isEnforced: mtaStsPolicy?.mode === "enforce",
937
+ },
938
+ tlsrpt: {
939
+ hasRecord: tlsrpt !== null,
940
+ ...tlsrpt,
941
+ },
942
+ dane: {
943
+ mxHostsChecked: daneResults.length,
944
+ results: daneResults,
945
+ anyDane: daneResults.some((d) => d.hasTlsa),
946
+ },
947
+ emailTransportSecurity: {
948
+ hasMtaSts: mtaStsRecord !== null,
949
+ mtaStsMode: mtaStsPolicy?.mode ?? "none",
950
+ hasTlsrpt: tlsrpt !== null,
951
+ hasDane: daneResults.some((d) => d.hasTlsa),
952
+ grade: gradeTransportSecurity(mtaStsPolicy?.mode ?? null, tlsrpt !== null, daneResults.some((d) => d.hasTlsa)),
953
+ },
954
+ };
955
+ cache.set(cacheKey, result);
956
+ return json(result);
957
+ }
958
+ function gradeTransportSecurity(mtaStsMode, hasTlsrpt, hasDane) {
959
+ const details = [];
960
+ let score = 0;
961
+ if (mtaStsMode === "enforce") {
962
+ score += 40;
963
+ details.push("MTA-STS mode=enforce (+40) — TLS enforced for inbound SMTP");
964
+ }
965
+ else if (mtaStsMode === "testing") {
966
+ score += 20;
967
+ details.push("MTA-STS mode=testing (+20) — TLS monitoring only");
968
+ }
969
+ else if (mtaStsMode === "none") {
970
+ score += 5;
971
+ details.push("MTA-STS mode=none (+5) — policy exists but inactive");
972
+ }
973
+ else {
974
+ details.push("No MTA-STS policy — inbound SMTP may be downgraded to cleartext");
975
+ }
976
+ if (hasTlsrpt) {
977
+ score += 30;
978
+ details.push("TLSRPT present (+30) — TLS failure reporting enabled");
979
+ }
980
+ else {
981
+ details.push("No TLSRPT — no TLS failure reporting configured");
982
+ }
983
+ if (hasDane) {
984
+ score += 30;
985
+ details.push("DANE/TLSA present (+30) — certificate pinning via DNSSEC");
986
+ }
987
+ else {
988
+ details.push("No DANE/TLSA records — no DNS-based certificate pinning");
989
+ }
990
+ const grade = score >= 90 ? "A" :
991
+ score >= 70 ? "B" :
992
+ score >= 40 ? "C" :
993
+ score >= 20 ? "D" : "F";
994
+ return { grade, details };
995
+ }
996
+ // ─── Tool Definitions ───
997
+ export const dnsTools = [
998
+ {
999
+ name: "dns_email",
1000
+ description: "Email infrastructure analysis. Resolves MX records with provider detection, parses SPF authorized senders, probes common DKIM selectors, and analyzes DMARC policy. Returns a security grade based on email authentication posture.",
1001
+ schema: {
1002
+ domain: z.string().describe("Domain to analyze email infrastructure"),
1003
+ },
1004
+ execute: dnsEmail,
1005
+ },
1006
+ {
1007
+ name: "dns_saas",
1008
+ description: "SaaS inventory from DNS TXT records. Matches TXT records against known SaaS domain verification patterns (Google, Microsoft, Atlassian, Stripe, etc.) and extracts email service providers from SPF includes. Reveals third-party service subscriptions.",
1009
+ schema: {
1010
+ domain: z.string().describe("Domain to discover SaaS services"),
1011
+ },
1012
+ execute: dnsSaas,
1013
+ },
1014
+ {
1015
+ name: "dns_takeover",
1016
+ description: "Subdomain takeover detection. Resolves CNAME chains for subdomains and checks if targets are unclaimed on known vulnerable services (Heroku, GitHub Pages, S3, Azure, Shopify, Fastly, Netlify, Vercel, etc.). Flags dangling CNAMEs pointing to deprovisioned resources.",
1017
+ schema: {
1018
+ domain: z.string().describe("Domain to check for subdomain takeover"),
1019
+ subdomains: z
1020
+ .array(z.string())
1021
+ .optional()
1022
+ .describe("Subdomains to check (default: common ones like www, mail, dev, staging, api, admin, cdn, blog, shop, app, test, beta, demo, status, docs)"),
1023
+ },
1024
+ execute: dnsTakeover,
1025
+ },
1026
+ {
1027
+ name: "dns_server_fp",
1028
+ description: "DNS server fingerprinting. Identifies nameserver provider, tests for open resolver (DNS amplification risk), attempts CHAOS TXT queries for version leak, and provides manual verification commands for zone transfer, EDNS0, and NSID detection.",
1029
+ schema: {
1030
+ domain: z.string().describe("Domain whose authoritative NS to fingerprint"),
1031
+ nameserver: z
1032
+ .string()
1033
+ .optional()
1034
+ .describe("Specific nameserver to probe (default: auto-detect from NS records)"),
1035
+ },
1036
+ execute: dnsServerFp,
1037
+ },
1038
+ {
1039
+ name: "dns_records",
1040
+ description: "Full DNS enumeration. Queries A, AAAA, MX, TXT, NS, SOA, CAA, CNAME, SRV, PTR, and NAPTR records. Identifies NS providers, authorized certificate authorities from CAA, discovers services from SRV records, and extracts admin email from SOA.",
1041
+ schema: {
1042
+ domain: z.string().describe("Domain for full DNS enumeration"),
1043
+ },
1044
+ execute: dnsRecords,
1045
+ },
1046
+ {
1047
+ name: "dns_reverse",
1048
+ description: "Reverse DNS lookup. Resolves PTR records for an IP address to reveal hostnames. Supports /24 range scanning to map an entire subnet's reverse DNS, identifying hostname patterns and infrastructure ownership.",
1049
+ schema: {
1050
+ ip: z.string().describe("IP address for reverse DNS lookup"),
1051
+ range: z
1052
+ .boolean()
1053
+ .optional()
1054
+ .describe("If true, scan /24 range (e.g., x.x.x.0-255)"),
1055
+ },
1056
+ execute: dnsReverse,
1057
+ },
1058
+ {
1059
+ name: "dns_mta_sts",
1060
+ description: "MTA-STS, TLSRPT, and DANE analysis for email transport security. Checks MTA-STS TXT record and HTTPS policy (mode, mx hosts, max_age), TLSRPT reporting configuration, and DANE/TLSA records for certificate pinning. Grades overall transport security posture.",
1061
+ schema: {
1062
+ domain: z.string().describe("Domain for MTA-STS/TLSRPT/DANE analysis"),
1063
+ },
1064
+ execute: dnsMtaSts,
1065
+ },
1066
+ ];
1067
+ //# sourceMappingURL=index.js.map