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.
- package/LICENSE +663 -0
- package/README.ar.md +767 -0
- package/README.bn.md +767 -0
- package/README.bs.md +767 -0
- package/README.da.md +767 -0
- package/README.de.md +770 -0
- package/README.el.md +767 -0
- package/README.es.md +769 -0
- package/README.fr.md +769 -0
- package/README.hi.md +767 -0
- package/README.it.md +767 -0
- package/README.ja.md +767 -0
- package/README.ko.md +772 -0
- package/README.md +767 -0
- package/README.no.md +767 -0
- package/README.pl.md +767 -0
- package/README.pt-BR.md +767 -0
- package/README.ru.md +767 -0
- package/README.th.md +767 -0
- package/README.tr.md +767 -0
- package/README.uk.md +767 -0
- package/README.vi.md +767 -0
- package/README.zh-TW.md +768 -0
- package/README.zh.md +768 -0
- package/dist/app/index.d.ts +3 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +614 -0
- package/dist/app/index.js.map +1 -0
- package/dist/composite/analyze.d.ts +3 -0
- package/dist/composite/analyze.d.ts.map +1 -0
- package/dist/composite/analyze.js +59 -0
- package/dist/composite/analyze.js.map +1 -0
- package/dist/composite/correlate.d.ts +3 -0
- package/dist/composite/correlate.d.ts.map +1 -0
- package/dist/composite/correlate.js +184 -0
- package/dist/composite/correlate.js.map +1 -0
- package/dist/composite/enumerate.d.ts +3 -0
- package/dist/composite/enumerate.d.ts.map +1 -0
- package/dist/composite/enumerate.js +94 -0
- package/dist/composite/enumerate.js.map +1 -0
- package/dist/composite/helpers.d.ts +22 -0
- package/dist/composite/helpers.d.ts.map +1 -0
- package/dist/composite/helpers.js +111 -0
- package/dist/composite/helpers.js.map +1 -0
- package/dist/composite/index.d.ts +3 -0
- package/dist/composite/index.d.ts.map +1 -0
- package/dist/composite/index.js +29 -0
- package/dist/composite/index.js.map +1 -0
- package/dist/composite/meta.d.ts +3 -0
- package/dist/composite/meta.d.ts.map +1 -0
- package/dist/composite/meta.js +23 -0
- package/dist/composite/meta.js.map +1 -0
- package/dist/composite/osint.d.ts +3 -0
- package/dist/composite/osint.d.ts.map +1 -0
- package/dist/composite/osint.js +40 -0
- package/dist/composite/osint.js.map +1 -0
- package/dist/composite/recon.d.ts +3 -0
- package/dist/composite/recon.d.ts.map +1 -0
- package/dist/composite/recon.js +131 -0
- package/dist/composite/recon.js.map +1 -0
- package/dist/composite/scan-dns.d.ts +3 -0
- package/dist/composite/scan-dns.d.ts.map +1 -0
- package/dist/composite/scan-dns.js +44 -0
- package/dist/composite/scan-dns.js.map +1 -0
- package/dist/composite/scan-http.d.ts +3 -0
- package/dist/composite/scan-http.d.ts.map +1 -0
- package/dist/composite/scan-http.js +68 -0
- package/dist/composite/scan-http.js.map +1 -0
- package/dist/composite/scan-paths.d.ts +3 -0
- package/dist/composite/scan-paths.d.ts.map +1 -0
- package/dist/composite/scan-paths.js +32 -0
- package/dist/composite/scan-paths.js.map +1 -0
- package/dist/composite/scan-ports.d.ts +3 -0
- package/dist/composite/scan-ports.d.ts.map +1 -0
- package/dist/composite/scan-ports.js +79 -0
- package/dist/composite/scan-ports.js.map +1 -0
- package/dist/composite/scan-services.d.ts +3 -0
- package/dist/composite/scan-services.d.ts.map +1 -0
- package/dist/composite/scan-services.js +111 -0
- package/dist/composite/scan-services.js.map +1 -0
- package/dist/composite/scan-tls.d.ts +3 -0
- package/dist/composite/scan-tls.d.ts.map +1 -0
- package/dist/composite/scan-tls.js +32 -0
- package/dist/composite/scan-tls.js.map +1 -0
- package/dist/composite/scan-waf.d.ts +3 -0
- package/dist/composite/scan-waf.d.ts.map +1 -0
- package/dist/composite/scan-waf.js +35 -0
- package/dist/composite/scan-waf.js.map +1 -0
- package/dist/correlation/index.d.ts +3 -0
- package/dist/correlation/index.d.ts.map +1 -0
- package/dist/correlation/index.js +1044 -0
- package/dist/correlation/index.js.map +1 -0
- package/dist/data/analytics-patterns.d.ts +21 -0
- package/dist/data/analytics-patterns.d.ts.map +1 -0
- package/dist/data/analytics-patterns.js +449 -0
- package/dist/data/analytics-patterns.js.map +1 -0
- package/dist/data/app-signatures.d.ts +52 -0
- package/dist/data/app-signatures.d.ts.map +1 -0
- package/dist/data/app-signatures.js +1143 -0
- package/dist/data/app-signatures.js.map +1 -0
- package/dist/data/c2-signatures.d.ts +54 -0
- package/dist/data/c2-signatures.d.ts.map +1 -0
- package/dist/data/c2-signatures.js +256 -0
- package/dist/data/c2-signatures.js.map +1 -0
- package/dist/data/cloud-ranges.d.ts +34 -0
- package/dist/data/cloud-ranges.d.ts.map +1 -0
- package/dist/data/cloud-ranges.js +628 -0
- package/dist/data/cloud-ranges.js.map +1 -0
- package/dist/data/cookie-patterns.d.ts +28 -0
- package/dist/data/cookie-patterns.d.ts.map +1 -0
- package/dist/data/cookie-patterns.js +225 -0
- package/dist/data/cookie-patterns.js.map +1 -0
- package/dist/data/error-signatures.d.ts +19 -0
- package/dist/data/error-signatures.d.ts.map +1 -0
- package/dist/data/error-signatures.js +321 -0
- package/dist/data/error-signatures.js.map +1 -0
- package/dist/data/favicon-hashes.d.ts +25 -0
- package/dist/data/favicon-hashes.d.ts.map +1 -0
- package/dist/data/favicon-hashes.js +249 -0
- package/dist/data/favicon-hashes.js.map +1 -0
- package/dist/data/h2-signatures.d.ts +50 -0
- package/dist/data/h2-signatures.d.ts.map +1 -0
- package/dist/data/h2-signatures.js +211 -0
- package/dist/data/h2-signatures.js.map +1 -0
- package/dist/data/header-order.d.ts +38 -0
- package/dist/data/header-order.d.ts.map +1 -0
- package/dist/data/header-order.js +185 -0
- package/dist/data/header-order.js.map +1 -0
- package/dist/data/jarm-signatures.d.ts +25 -0
- package/dist/data/jarm-signatures.d.ts.map +1 -0
- package/dist/data/jarm-signatures.js +213 -0
- package/dist/data/jarm-signatures.js.map +1 -0
- package/dist/data/saas-patterns.d.ts +23 -0
- package/dist/data/saas-patterns.d.ts.map +1 -0
- package/dist/data/saas-patterns.js +139 -0
- package/dist/data/saas-patterns.js.map +1 -0
- package/dist/data/sensitive-paths.d.ts +12 -0
- package/dist/data/sensitive-paths.d.ts.map +1 -0
- package/dist/data/sensitive-paths.js +1539 -0
- package/dist/data/sensitive-paths.js.map +1 -0
- package/dist/data/service-banners.d.ts +59 -0
- package/dist/data/service-banners.d.ts.map +1 -0
- package/dist/data/service-banners.js +323 -0
- package/dist/data/service-banners.js.map +1 -0
- package/dist/data/smtp-signatures.d.ts +12 -0
- package/dist/data/smtp-signatures.d.ts.map +1 -0
- package/dist/data/smtp-signatures.js +350 -0
- package/dist/data/smtp-signatures.js.map +1 -0
- package/dist/data/takeover-patterns.d.ts +37 -0
- package/dist/data/takeover-patterns.d.ts.map +1 -0
- package/dist/data/takeover-patterns.js +249 -0
- package/dist/data/takeover-patterns.js.map +1 -0
- package/dist/data/tech-patterns.d.ts +44 -0
- package/dist/data/tech-patterns.d.ts.map +1 -0
- package/dist/data/tech-patterns.js +690 -0
- package/dist/data/tech-patterns.js.map +1 -0
- package/dist/data/waf-signatures.d.ts +21 -0
- package/dist/data/waf-signatures.d.ts.map +1 -0
- package/dist/data/waf-signatures.js +318 -0
- package/dist/data/waf-signatures.js.map +1 -0
- package/dist/dns/index.d.ts +3 -0
- package/dist/dns/index.d.ts.map +1 -0
- package/dist/dns/index.js +1067 -0
- package/dist/dns/index.js.map +1 -0
- package/dist/enum/index.d.ts +3 -0
- package/dist/enum/index.d.ts.map +1 -0
- package/dist/enum/index.js +818 -0
- package/dist/enum/index.js.map +1 -0
- package/dist/h2/index.d.ts +3 -0
- package/dist/h2/index.d.ts.map +1 -0
- package/dist/h2/index.js +414 -0
- package/dist/h2/index.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +2444 -0
- package/dist/http/index.js.map +1 -0
- package/dist/identify/index.d.ts +3 -0
- package/dist/identify/index.d.ts.map +1 -0
- package/dist/identify/index.js +447 -0
- package/dist/identify/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/index.d.ts +3 -0
- package/dist/infra/index.d.ts.map +1 -0
- package/dist/infra/index.js +989 -0
- package/dist/infra/index.js.map +1 -0
- package/dist/iot/index.d.ts +3 -0
- package/dist/iot/index.d.ts.map +1 -0
- package/dist/iot/index.js +610 -0
- package/dist/iot/index.js.map +1 -0
- package/dist/meta/index.d.ts +3 -0
- package/dist/meta/index.d.ts.map +1 -0
- package/dist/meta/index.js +442 -0
- package/dist/meta/index.js.map +1 -0
- package/dist/osint/index.d.ts +3 -0
- package/dist/osint/index.d.ts.map +1 -0
- package/dist/osint/index.js +687 -0
- package/dist/osint/index.js.map +1 -0
- package/dist/passive/index.d.ts +3 -0
- package/dist/passive/index.d.ts.map +1 -0
- package/dist/passive/index.js +944 -0
- package/dist/passive/index.js.map +1 -0
- package/dist/path/index.d.ts +3 -0
- package/dist/path/index.d.ts.map +1 -0
- package/dist/path/index.js +878 -0
- package/dist/path/index.js.map +1 -0
- package/dist/protocol/mcp-server.d.ts +4 -0
- package/dist/protocol/mcp-server.d.ts.map +1 -0
- package/dist/protocol/mcp-server.js +32 -0
- package/dist/protocol/mcp-server.js.map +1 -0
- package/dist/protocol/tools.d.ts +3 -0
- package/dist/protocol/tools.d.ts.map +1 -0
- package/dist/protocol/tools.js +5 -0
- package/dist/protocol/tools.js.map +1 -0
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +1053 -0
- package/dist/service/index.js.map +1 -0
- package/dist/smtp/index.d.ts +3 -0
- package/dist/smtp/index.d.ts.map +1 -0
- package/dist/smtp/index.js +437 -0
- package/dist/smtp/index.js.map +1 -0
- package/dist/ssh/index.d.ts +3 -0
- package/dist/ssh/index.d.ts.map +1 -0
- package/dist/ssh/index.js +676 -0
- package/dist/ssh/index.js.map +1 -0
- package/dist/tcp/index.d.ts +3 -0
- package/dist/tcp/index.d.ts.map +1 -0
- package/dist/tcp/index.js +369 -0
- package/dist/tcp/index.js.map +1 -0
- package/dist/timing/index.d.ts +3 -0
- package/dist/timing/index.d.ts.map +1 -0
- package/dist/timing/index.js +425 -0
- package/dist/timing/index.js.map +1 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/tls/index.d.ts.map +1 -0
- package/dist/tls/index.js +1332 -0
- package/dist/tls/index.js.map +1 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cache.d.ts +11 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +35 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/murmurhash3.d.ts +3 -0
- package/dist/utils/murmurhash3.d.ts.map +1 -0
- package/dist/utils/murmurhash3.js +57 -0
- package/dist/utils/murmurhash3.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +10 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +35 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/require-key.d.ts +2 -0
- package/dist/utils/require-key.d.ts.map +1 -0
- package/dist/utils/require-key.js +8 -0
- package/dist/utils/require-key.js.map +1 -0
- package/dist/waf/index.d.ts +3 -0
- package/dist/waf/index.d.ts.map +1 -0
- package/dist/waf/index.js +767 -0
- package/dist/waf/index.js.map +1 -0
- package/dist/web/index.d.ts +3 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +1366 -0
- package/dist/web/index.js.map +1 -0
- 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
|