dns-security-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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +723 -0
  3. package/dist/blocklist/index.d.ts +3 -0
  4. package/dist/blocklist/index.d.ts.map +1 -0
  5. package/dist/blocklist/index.js +596 -0
  6. package/dist/blocklist/index.js.map +1 -0
  7. package/dist/ct/index.d.ts +3 -0
  8. package/dist/ct/index.d.ts.map +1 -0
  9. package/dist/ct/index.js +534 -0
  10. package/dist/ct/index.js.map +1 -0
  11. package/dist/data/dkim-selectors.d.ts +2 -0
  12. package/dist/data/dkim-selectors.d.ts.map +1 -0
  13. package/dist/data/dkim-selectors.js +60 -0
  14. package/dist/data/dkim-selectors.js.map +1 -0
  15. package/dist/data/dnsbl-lists.d.ts +8 -0
  16. package/dist/data/dnsbl-lists.d.ts.map +1 -0
  17. package/dist/data/dnsbl-lists.js +54 -0
  18. package/dist/data/dnsbl-lists.js.map +1 -0
  19. package/dist/data/takeover-fingerprints.d.ts +8 -0
  20. package/dist/data/takeover-fingerprints.d.ts.map +1 -0
  21. package/dist/data/takeover-fingerprints.js +84 -0
  22. package/dist/data/takeover-fingerprints.js.map +1 -0
  23. package/dist/data/tunneling-signatures.d.ts +17 -0
  24. package/dist/data/tunneling-signatures.d.ts.map +1 -0
  25. package/dist/data/tunneling-signatures.js +85 -0
  26. package/dist/data/tunneling-signatures.js.map +1 -0
  27. package/dist/dns/index.d.ts +3 -0
  28. package/dist/dns/index.d.ts.map +1 -0
  29. package/dist/dns/index.js +1211 -0
  30. package/dist/dns/index.js.map +1 -0
  31. package/dist/dnssec/index.d.ts +3 -0
  32. package/dist/dnssec/index.d.ts.map +1 -0
  33. package/dist/dnssec/index.js +1377 -0
  34. package/dist/dnssec/index.js.map +1 -0
  35. package/dist/domain/index.d.ts +3 -0
  36. package/dist/domain/index.d.ts.map +1 -0
  37. package/dist/domain/index.js +938 -0
  38. package/dist/domain/index.js.map +1 -0
  39. package/dist/email/index.d.ts +3 -0
  40. package/dist/email/index.d.ts.map +1 -0
  41. package/dist/email/index.js +1188 -0
  42. package/dist/email/index.js.map +1 -0
  43. package/dist/hijack/index.d.ts +3 -0
  44. package/dist/hijack/index.d.ts.map +1 -0
  45. package/dist/hijack/index.js +1117 -0
  46. package/dist/hijack/index.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +151 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/infra/index.d.ts +3 -0
  52. package/dist/infra/index.d.ts.map +1 -0
  53. package/dist/infra/index.js +797 -0
  54. package/dist/infra/index.js.map +1 -0
  55. package/dist/privacy/index.d.ts +3 -0
  56. package/dist/privacy/index.d.ts.map +1 -0
  57. package/dist/privacy/index.js +772 -0
  58. package/dist/privacy/index.js.map +1 -0
  59. package/dist/protocol/mcp-server.d.ts +4 -0
  60. package/dist/protocol/mcp-server.d.ts.map +1 -0
  61. package/dist/protocol/mcp-server.js +32 -0
  62. package/dist/protocol/mcp-server.js.map +1 -0
  63. package/dist/protocol/tools.d.ts +3 -0
  64. package/dist/protocol/tools.d.ts.map +1 -0
  65. package/dist/protocol/tools.js +29 -0
  66. package/dist/protocol/tools.js.map +1 -0
  67. package/dist/report/index.d.ts +3 -0
  68. package/dist/report/index.d.ts.map +1 -0
  69. package/dist/report/index.js +1167 -0
  70. package/dist/report/index.js.map +1 -0
  71. package/dist/threat/index.d.ts +3 -0
  72. package/dist/threat/index.d.ts.map +1 -0
  73. package/dist/threat/index.js +999 -0
  74. package/dist/threat/index.js.map +1 -0
  75. package/dist/tunnel/index.d.ts +3 -0
  76. package/dist/tunnel/index.d.ts.map +1 -0
  77. package/dist/tunnel/index.js +688 -0
  78. package/dist/tunnel/index.js.map +1 -0
  79. package/dist/types/index.d.ts +52 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/index.js +8 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/typo/index.d.ts +3 -0
  84. package/dist/typo/index.d.ts.map +1 -0
  85. package/dist/typo/index.js +625 -0
  86. package/dist/typo/index.js.map +1 -0
  87. package/dist/utils/cache.d.ts +11 -0
  88. package/dist/utils/cache.d.ts.map +1 -0
  89. package/dist/utils/cache.js +35 -0
  90. package/dist/utils/cache.js.map +1 -0
  91. package/dist/utils/dns-client.d.ts +37 -0
  92. package/dist/utils/dns-client.d.ts.map +1 -0
  93. package/dist/utils/dns-client.js +359 -0
  94. package/dist/utils/dns-client.js.map +1 -0
  95. package/dist/utils/rate-limiter.d.ts +10 -0
  96. package/dist/utils/rate-limiter.d.ts.map +1 -0
  97. package/dist/utils/rate-limiter.js +35 -0
  98. package/dist/utils/rate-limiter.js.map +1 -0
  99. package/package.json +63 -0
@@ -0,0 +1,797 @@
1
+ import { z } from "zod";
2
+ import { json } from "../types/index.js";
3
+ import { queryRaw, queryTcp, createResolver } from "../utils/dns-client.js";
4
+ import * as dnsPacket from "dns-packet";
5
+ import * as dgram from "node:dgram";
6
+ // ─── Known CVE Patterns ───
7
+ const BIND_CVES = [
8
+ { version: /^9\.16\.[0-7]$/, cves: ["CVE-2020-8617", "CVE-2020-8616"] },
9
+ { version: /^9\.16\.[0-9]$/, cves: ["CVE-2020-8622", "CVE-2020-8623"] },
10
+ { version: /^9\.11\.[0-9]$/, cves: ["CVE-2020-8616", "CVE-2019-6465"] },
11
+ { version: /^9\.11\.([0-4]|5$)/, cves: ["CVE-2018-5743", "CVE-2018-5740"] },
12
+ { version: /^9\.9\./, cves: ["CVE-2016-2776", "CVE-2016-1286"] },
13
+ { version: /^9\.10\./, cves: ["CVE-2016-2776", "CVE-2016-8864"] },
14
+ { version: /^9\.18\.[0-3]$/, cves: ["CVE-2022-2795", "CVE-2022-2881"] },
15
+ ];
16
+ const POWERDNS_CVES = [
17
+ { version: /^4\.0\./, cves: ["CVE-2016-7068", "CVE-2016-7072"] },
18
+ { version: /^4\.1\.[0-7]$/, cves: ["CVE-2019-3871", "CVE-2018-10851"] },
19
+ { version: /^4\.2\.[0-1]$/, cves: ["CVE-2020-17482"] },
20
+ { version: /^4\.3\.[0-1]$/, cves: ["CVE-2021-36754"] },
21
+ ];
22
+ const UNBOUND_CVES = [
23
+ { version: /^1\.[0-5]\./, cves: ["CVE-2017-15105", "CVE-2019-18934"] },
24
+ { version: /^1\.6\.[0-7]$/, cves: ["CVE-2019-18934", "CVE-2017-15105"] },
25
+ { version: /^1\.7\.[0-2]$/, cves: ["CVE-2019-18934"] },
26
+ { version: /^1\.(9|10|11|12)\.[0-1]$/, cves: ["CVE-2022-30698", "CVE-2022-30699"] },
27
+ ];
28
+ // ─── Helpers ───
29
+ function parseVersionString(txt) {
30
+ const bindMatch = txt.match(/BIND\s+([\d.]+[a-zA-Z0-9-]*)/i);
31
+ if (bindMatch)
32
+ return { software: "BIND", version: bindMatch[1] };
33
+ const pdnsMatch = txt.match(/PowerDNS.*?([\d.]+)/i);
34
+ if (pdnsMatch)
35
+ return { software: "PowerDNS", version: pdnsMatch[1] };
36
+ const unboundMatch = txt.match(/unbound\s+([\d.]+)/i);
37
+ if (unboundMatch)
38
+ return { software: "Unbound", version: unboundMatch[1] };
39
+ const nsdMatch = txt.match(/NSD\s+([\d.]+)/i);
40
+ if (nsdMatch)
41
+ return { software: "NSD", version: nsdMatch[1] };
42
+ const knMatch = txt.match(/Knot DNS\s+([\d.]+)/i);
43
+ if (knMatch)
44
+ return { software: "Knot DNS", version: knMatch[1] };
45
+ return null;
46
+ }
47
+ function lookupCves(software, version) {
48
+ const cves = [];
49
+ const entries = software === "BIND" ? BIND_CVES :
50
+ software === "PowerDNS" ? POWERDNS_CVES :
51
+ software === "Unbound" ? UNBOUND_CVES :
52
+ [];
53
+ for (const entry of entries) {
54
+ if (entry.version.test(version)) {
55
+ cves.push(...entry.cves);
56
+ }
57
+ }
58
+ return [...new Set(cves)];
59
+ }
60
+ async function sendChaosQuery(nameserver, qname, port = 53, timeout = 5000) {
61
+ const buf = dnsPacket.encode({
62
+ type: "query",
63
+ id: Math.floor(Math.random() * 65535),
64
+ flags: dnsPacket.RECURSION_DESIRED,
65
+ questions: [
66
+ {
67
+ type: "TXT",
68
+ name: qname,
69
+ class: "CH",
70
+ },
71
+ ],
72
+ });
73
+ return new Promise((resolve) => {
74
+ const socket = dgram.createSocket("udp4");
75
+ const timer = setTimeout(() => {
76
+ socket.close();
77
+ resolve(null);
78
+ }, timeout);
79
+ socket.on("message", (msg) => {
80
+ clearTimeout(timer);
81
+ socket.close();
82
+ try {
83
+ const res = dnsPacket.decode(msg);
84
+ for (const answer of res.answers ?? []) {
85
+ if (answer.type === "TXT" && answer.data) {
86
+ const data = answer.data;
87
+ if (Buffer.isBuffer(data))
88
+ return resolve(data.toString("utf-8"));
89
+ if (Array.isArray(data))
90
+ return resolve(data.map((d) => d.toString("utf-8")).join(""));
91
+ if (typeof data === "string")
92
+ return resolve(data);
93
+ }
94
+ }
95
+ resolve(null);
96
+ }
97
+ catch {
98
+ resolve(null);
99
+ }
100
+ });
101
+ socket.on("error", () => {
102
+ clearTimeout(timer);
103
+ socket.close();
104
+ resolve(null);
105
+ });
106
+ socket.send(buf, 0, buf.length, port, nameserver);
107
+ });
108
+ }
109
+ // ─── Tool 1: infra_open_resolver ───
110
+ const infraOpenResolver = {
111
+ name: "infra_open_resolver",
112
+ description: "Test if a DNS nameserver is an open resolver (accepts recursive queries from any source). Open resolvers can be abused for DDoS amplification attacks. Also checks EDNS0 buffer size from OPT record.",
113
+ schema: {
114
+ nameserver: z
115
+ .string()
116
+ .describe("IP address or hostname of the DNS nameserver to test"),
117
+ },
118
+ async execute(args) {
119
+ const nameserver = args.nameserver;
120
+ try {
121
+ const result = await queryRaw("google.com", "A", nameserver, 53, { rd: true, dnssec: true });
122
+ const isOpen = result.answers.length > 0 && result.rcode === "NOERROR";
123
+ const recursionAvailable = result.flags.recursionAvailable;
124
+ let ednsBufferSize = null;
125
+ for (const add of result.additionals) {
126
+ if (add.type === "OPT") {
127
+ ednsBufferSize = add.udpPayloadSize ?? null;
128
+ }
129
+ }
130
+ const findings = [];
131
+ if (isOpen && recursionAvailable) {
132
+ findings.push("CRITICAL: Server is an open resolver (responds to recursive queries from external sources)");
133
+ findings.push("Risk: Can be abused for DNS amplification DDoS attacks");
134
+ findings.push("Remediation: Restrict recursion to trusted networks using ACLs");
135
+ }
136
+ if (ednsBufferSize && ednsBufferSize > 4096) {
137
+ findings.push(`WARNING: Large EDNS0 buffer size (${ednsBufferSize} bytes) increases amplification potential`);
138
+ }
139
+ return json({
140
+ nameserver,
141
+ is_open_resolver: isOpen && recursionAvailable,
142
+ recursion_available: recursionAvailable,
143
+ rcode: result.rcode,
144
+ answers_count: result.answers.length,
145
+ edns0_buffer_size: ednsBufferSize,
146
+ findings: findings.length > 0 ? findings : ["OK: Server does not appear to be an open resolver"],
147
+ });
148
+ }
149
+ catch (err) {
150
+ return json({
151
+ nameserver,
152
+ is_open_resolver: false,
153
+ error: err.message,
154
+ findings: ["Server did not respond to recursive query (may be filtered or non-existent)"],
155
+ });
156
+ }
157
+ },
158
+ };
159
+ // ─── Tool 2: infra_amplification ───
160
+ const infraAmplification = {
161
+ name: "infra_amplification",
162
+ description: "Measure DNS amplification factor of a nameserver. Sends a small query (ANY for root) and measures the response size ratio. Amplification factor > 10x indicates significant DDoS risk.",
163
+ schema: {
164
+ nameserver: z
165
+ .string()
166
+ .describe("IP address or hostname of the DNS nameserver to test"),
167
+ },
168
+ async execute(args) {
169
+ const nameserver = args.nameserver;
170
+ try {
171
+ const queryBuf = dnsPacket.encode({
172
+ type: "query",
173
+ id: Math.floor(Math.random() * 65535),
174
+ flags: dnsPacket.RECURSION_DESIRED,
175
+ questions: [{ type: "ANY", name: ".", class: "IN" }],
176
+ });
177
+ const queryBytes = queryBuf.length;
178
+ const responseBytes = await new Promise((resolve, reject) => {
179
+ const socket = dgram.createSocket("udp4");
180
+ const timer = setTimeout(() => {
181
+ socket.close();
182
+ reject(new Error("Timeout waiting for DNS response"));
183
+ }, 5000);
184
+ socket.on("message", (msg) => {
185
+ clearTimeout(timer);
186
+ socket.close();
187
+ resolve(msg.length);
188
+ });
189
+ socket.on("error", (err) => {
190
+ clearTimeout(timer);
191
+ socket.close();
192
+ reject(err);
193
+ });
194
+ socket.send(queryBuf, 0, queryBuf.length, 53, nameserver);
195
+ });
196
+ const amplificationFactor = parseFloat((responseBytes / queryBytes).toFixed(2));
197
+ const findings = [];
198
+ if (amplificationFactor > 50) {
199
+ findings.push(`CRITICAL: Extreme amplification factor (${amplificationFactor}x)`);
200
+ }
201
+ else if (amplificationFactor > 10) {
202
+ findings.push(`HIGH: Significant amplification factor (${amplificationFactor}x)`);
203
+ }
204
+ else if (amplificationFactor > 5) {
205
+ findings.push(`MEDIUM: Moderate amplification factor (${amplificationFactor}x)`);
206
+ }
207
+ else {
208
+ findings.push(`LOW: Minimal amplification factor (${amplificationFactor}x)`);
209
+ }
210
+ return json({
211
+ nameserver,
212
+ query_bytes: queryBytes,
213
+ response_bytes: responseBytes,
214
+ amplification_factor: amplificationFactor,
215
+ risk_level: amplificationFactor > 50 ? "critical" : amplificationFactor > 10 ? "high" : amplificationFactor > 5 ? "medium" : "low",
216
+ findings,
217
+ });
218
+ }
219
+ catch (err) {
220
+ return json({
221
+ nameserver,
222
+ error: err.message,
223
+ findings: ["Could not measure amplification factor (server may not respond to ANY queries)"],
224
+ });
225
+ }
226
+ },
227
+ };
228
+ // ─── Tool 3: infra_rate_limiting ───
229
+ const infraRateLimiting = {
230
+ name: "infra_rate_limiting",
231
+ description: "Test if a DNS nameserver has Response Rate Limiting (RRL) enabled. Sends a burst of identical queries and checks for REFUSED responses or dropped packets, indicating active rate limiting.",
232
+ schema: {
233
+ nameserver: z
234
+ .string()
235
+ .describe("IP address or hostname of the DNS nameserver to test"),
236
+ queries: z
237
+ .number()
238
+ .optional()
239
+ .describe("Number of burst queries to send (default: 20)"),
240
+ },
241
+ async execute(args) {
242
+ const nameserver = args.nameserver;
243
+ const queryCount = args.queries ?? 20;
244
+ const results = [];
245
+ const startTime = Date.now();
246
+ const promises = Array.from({ length: queryCount }, async (_, i) => {
247
+ const queryStart = Date.now();
248
+ try {
249
+ const res = await queryRaw("example.com", "A", nameserver, 53, { rd: true });
250
+ results.push({
251
+ status: res.rcode === "REFUSED" ? "refused" : "success",
252
+ rcode: res.rcode,
253
+ time_ms: Date.now() - queryStart,
254
+ });
255
+ }
256
+ catch (err) {
257
+ const errMsg = err.message;
258
+ results.push({
259
+ status: errMsg.includes("timeout") ? "timeout" : "error",
260
+ time_ms: Date.now() - queryStart,
261
+ });
262
+ }
263
+ });
264
+ await Promise.allSettled(promises);
265
+ const totalTime = Date.now() - startTime;
266
+ const successCount = results.filter((r) => r.status === "success").length;
267
+ const refusedCount = results.filter((r) => r.status === "refused").length;
268
+ const timeoutCount = results.filter((r) => r.status === "timeout").length;
269
+ const errorCount = results.filter((r) => r.status === "error").length;
270
+ const avgResponseTime = results.length > 0
271
+ ? parseFloat((results.reduce((s, r) => s + r.time_ms, 0) / results.length).toFixed(1))
272
+ : 0;
273
+ const rrlDetected = refusedCount > 0 || timeoutCount > queryCount * 0.3;
274
+ const findings = [];
275
+ if (rrlDetected) {
276
+ findings.push("Rate limiting detected: server actively limits response rate");
277
+ if (refusedCount > 0)
278
+ findings.push(`${refusedCount}/${queryCount} queries received REFUSED response`);
279
+ if (timeoutCount > 0)
280
+ findings.push(`${timeoutCount}/${queryCount} queries timed out (possible drop)`);
281
+ }
282
+ else {
283
+ findings.push("WARNING: No rate limiting detected. Server responds to all burst queries.");
284
+ findings.push("Remediation: Enable Response Rate Limiting (RRL) to mitigate amplification attacks");
285
+ }
286
+ return json({
287
+ nameserver,
288
+ queries_sent: queryCount,
289
+ successful: successCount,
290
+ refused: refusedCount,
291
+ timed_out: timeoutCount,
292
+ errors: errorCount,
293
+ total_time_ms: totalTime,
294
+ avg_response_time_ms: avgResponseTime,
295
+ rrl_detected: rrlDetected,
296
+ findings,
297
+ });
298
+ },
299
+ };
300
+ // ─── Tool 4: infra_software_cve ───
301
+ const infraSoftwareCve = {
302
+ name: "infra_software_cve",
303
+ description: "Fingerprint DNS server software via CHAOS class version.bind TXT query and map to known CVEs. Identifies BIND, PowerDNS, Unbound, NSD, and Knot DNS versions.",
304
+ schema: {
305
+ nameserver: z
306
+ .string()
307
+ .describe("IP address or hostname of the DNS nameserver to fingerprint"),
308
+ },
309
+ async execute(args) {
310
+ const nameserver = args.nameserver;
311
+ const versionBind = await sendChaosQuery(nameserver, "version.bind");
312
+ const versionServer = await sendChaosQuery(nameserver, "version.server");
313
+ const hostnameBind = await sendChaosQuery(nameserver, "hostname.bind");
314
+ const versionString = versionBind ?? versionServer ?? null;
315
+ const hostname = hostnameBind ?? null;
316
+ const findings = [];
317
+ let software = null;
318
+ let version = null;
319
+ let cves = [];
320
+ if (versionString) {
321
+ const parsed = parseVersionString(versionString);
322
+ if (parsed) {
323
+ software = parsed.software;
324
+ version = parsed.version;
325
+ cves = lookupCves(software, version);
326
+ if (cves.length > 0) {
327
+ findings.push(`CRITICAL: ${software} ${version} has ${cves.length} known CVE(s): ${cves.join(", ")}`);
328
+ findings.push("Remediation: Update DNS software to the latest stable version");
329
+ }
330
+ else {
331
+ findings.push(`INFO: ${software} ${version} — no known CVEs matched in our database`);
332
+ }
333
+ }
334
+ else {
335
+ findings.push(`INFO: Version string detected but not recognized: "${versionString}"`);
336
+ }
337
+ findings.push("WARNING: CHAOS class version disclosure is enabled. Consider disabling it to reduce fingerprinting surface.");
338
+ findings.push("Remediation: In BIND add 'version none;' to options block");
339
+ }
340
+ else {
341
+ findings.push("OK: Server does not disclose version information via CHAOS class queries");
342
+ }
343
+ return json({
344
+ nameserver,
345
+ version_bind: versionBind,
346
+ version_server: versionServer,
347
+ hostname_bind: hostname,
348
+ software,
349
+ version,
350
+ known_cves: cves,
351
+ findings,
352
+ });
353
+ },
354
+ };
355
+ // ─── Tool 5: infra_edns_compliance ───
356
+ const infraEdnsCompliance = {
357
+ name: "infra_edns_compliance",
358
+ description: "Test EDNS0 compliance of a DNS nameserver. Checks EDNS version, UDP buffer size, DO (DNSSEC OK) flag, and NSID option. Flags issues like missing EDNS, small buffer size, or no DNSSEC support.",
359
+ schema: {
360
+ nameserver: z
361
+ .string()
362
+ .describe("IP address or hostname of the DNS nameserver to test"),
363
+ },
364
+ async execute(args) {
365
+ const nameserver = args.nameserver;
366
+ try {
367
+ const result = await queryRaw("example.com", "A", nameserver, 53, { rd: true, dnssec: true });
368
+ let ednsFound = false;
369
+ let ednsVersion = null;
370
+ let udpBufferSize = null;
371
+ let doFlag = false;
372
+ let nsid = null;
373
+ const optionCodes = [];
374
+ for (const add of result.additionals) {
375
+ if (add.type === "OPT") {
376
+ ednsFound = true;
377
+ ednsVersion = add.version ?? 0;
378
+ udpBufferSize = add.udpPayloadSize ?? null;
379
+ doFlag = !!(add.flags & dnsPacket.DNSSEC_OK);
380
+ const options = add.options ?? [];
381
+ for (const opt of options) {
382
+ optionCodes.push(opt.code?.toString() ?? opt.type ?? "unknown");
383
+ if (opt.code === 3 || opt.type === "NSID") {
384
+ nsid = Buffer.isBuffer(opt.data) ? opt.data.toString("utf-8") : String(opt.data ?? "");
385
+ }
386
+ }
387
+ }
388
+ }
389
+ const findings = [];
390
+ if (!ednsFound) {
391
+ findings.push("CRITICAL: Server does not support EDNS0. This breaks modern DNS functionality.");
392
+ findings.push("Impact: No DNSSEC, limited UDP response size (512 bytes), no DNS cookies");
393
+ }
394
+ else {
395
+ findings.push(`OK: EDNS0 supported (version ${ednsVersion})`);
396
+ if (udpBufferSize !== null && udpBufferSize < 1232) {
397
+ findings.push(`WARNING: UDP buffer size (${udpBufferSize}) below recommended minimum of 1232 bytes`);
398
+ findings.push("Impact: May cause fragmentation issues. RFC 6891 recommends >= 1232 bytes.");
399
+ }
400
+ else if (udpBufferSize !== null) {
401
+ findings.push(`OK: UDP buffer size ${udpBufferSize} bytes`);
402
+ }
403
+ if (!doFlag) {
404
+ findings.push("WARNING: DNSSEC OK (DO) flag not set in response — server may not support DNSSEC");
405
+ }
406
+ else {
407
+ findings.push("OK: DNSSEC OK (DO) flag present");
408
+ }
409
+ if (nsid) {
410
+ findings.push(`INFO: NSID present: "${nsid}" (useful for debugging, but may disclose server identity)`);
411
+ }
412
+ }
413
+ return json({
414
+ nameserver,
415
+ edns_supported: ednsFound,
416
+ edns_version: ednsVersion,
417
+ udp_buffer_size: udpBufferSize,
418
+ dnssec_ok_flag: doFlag,
419
+ nsid,
420
+ option_codes: optionCodes,
421
+ authenticated_data: result.flags.authenticatedData,
422
+ findings,
423
+ });
424
+ }
425
+ catch (err) {
426
+ return json({
427
+ nameserver,
428
+ edns_supported: false,
429
+ error: err.message,
430
+ findings: ["Could not test EDNS compliance (server may be unreachable)"],
431
+ });
432
+ }
433
+ },
434
+ };
435
+ // ─── Tool 6: infra_tcp_fallback ───
436
+ const infraTcpFallback = {
437
+ name: "infra_tcp_fallback",
438
+ description: "Test if a DNS nameserver supports TCP fallback for large responses. Checks the TC (truncated) flag on UDP responses and verifies TCP port 53 connectivity. DNS over TCP is required by RFC 7766.",
439
+ schema: {
440
+ nameserver: z
441
+ .string()
442
+ .describe("IP address or hostname of the DNS nameserver to test"),
443
+ },
444
+ async execute(args) {
445
+ const nameserver = args.nameserver;
446
+ let udpWorks = false;
447
+ let tcpWorks = false;
448
+ let truncatedFlag = false;
449
+ // Test UDP first
450
+ try {
451
+ const udpResult = await queryRaw("example.com", "A", nameserver, 53, { rd: true });
452
+ udpWorks = true;
453
+ truncatedFlag = udpResult.flags.truncated;
454
+ }
455
+ catch {
456
+ // UDP failed
457
+ }
458
+ // Test TCP
459
+ try {
460
+ const tcpAnswers = await queryTcp("example.com", "A", nameserver, 53);
461
+ tcpWorks = tcpAnswers.length >= 0; // Even empty is OK — connection succeeded
462
+ tcpWorks = true;
463
+ }
464
+ catch {
465
+ // TCP failed
466
+ }
467
+ const findings = [];
468
+ if (!udpWorks && !tcpWorks) {
469
+ findings.push("CRITICAL: Server does not respond on either UDP or TCP port 53");
470
+ }
471
+ else if (udpWorks && !tcpWorks) {
472
+ findings.push("HIGH: TCP port 53 is blocked or not supported");
473
+ findings.push("Impact: Large DNS responses (>512 bytes) cannot be delivered. DNSSEC, DKIM, and large TXT records may fail.");
474
+ findings.push("Remediation: Ensure TCP port 53 is open on the nameserver (required by RFC 7766)");
475
+ }
476
+ else if (!udpWorks && tcpWorks) {
477
+ findings.push("WARNING: UDP port 53 not responding, but TCP works");
478
+ findings.push("Impact: Most DNS clients try UDP first — performance degradation expected");
479
+ }
480
+ else {
481
+ findings.push("OK: Both UDP and TCP port 53 are operational");
482
+ }
483
+ if (truncatedFlag) {
484
+ findings.push("INFO: TC (truncated) flag was set on UDP response, indicating large response that needs TCP");
485
+ }
486
+ return json({
487
+ nameserver,
488
+ udp_operational: udpWorks,
489
+ tcp_operational: tcpWorks,
490
+ truncated_flag: truncatedFlag,
491
+ findings,
492
+ });
493
+ },
494
+ };
495
+ // ─── Tool 7: infra_dns_cookie ───
496
+ const infraDnsCookie = {
497
+ name: "infra_dns_cookie",
498
+ description: "Test DNS Cookie support (RFC 7873) on a nameserver. DNS cookies protect against cache poisoning, off-path attacks, and amplification. Sends a query with a client cookie and checks if the server returns a server cookie.",
499
+ schema: {
500
+ nameserver: z
501
+ .string()
502
+ .describe("IP address or hostname of the DNS nameserver to test"),
503
+ },
504
+ async execute(args) {
505
+ const nameserver = args.nameserver;
506
+ try {
507
+ // Generate an 8-byte random client cookie
508
+ const clientCookie = Buffer.alloc(8);
509
+ for (let i = 0; i < 8; i++)
510
+ clientCookie[i] = Math.floor(Math.random() * 256);
511
+ const buf = dnsPacket.encode({
512
+ type: "query",
513
+ id: Math.floor(Math.random() * 65535),
514
+ flags: dnsPacket.RECURSION_DESIRED,
515
+ questions: [{ type: "A", name: "example.com", class: "IN" }],
516
+ additionals: [
517
+ {
518
+ type: "OPT",
519
+ name: ".",
520
+ udpPayloadSize: 4096,
521
+ flags: 0,
522
+ options: [
523
+ {
524
+ code: 10, // COOKIE option code
525
+ data: clientCookie,
526
+ },
527
+ ],
528
+ },
529
+ ],
530
+ });
531
+ const response = await new Promise((resolve, reject) => {
532
+ const socket = dgram.createSocket("udp4");
533
+ const timer = setTimeout(() => {
534
+ socket.close();
535
+ reject(new Error("Timeout waiting for DNS response"));
536
+ }, 5000);
537
+ socket.on("message", (msg) => {
538
+ clearTimeout(timer);
539
+ socket.close();
540
+ resolve(msg);
541
+ });
542
+ socket.on("error", (err) => {
543
+ clearTimeout(timer);
544
+ socket.close();
545
+ reject(err);
546
+ });
547
+ socket.send(buf, 0, buf.length, 53, nameserver);
548
+ });
549
+ const decoded = dnsPacket.decode(response);
550
+ let cookieSupported = false;
551
+ let serverCookieLength = 0;
552
+ for (const add of decoded.additionals ?? []) {
553
+ if (add.type === "OPT") {
554
+ const options = add.options ?? [];
555
+ for (const opt of options) {
556
+ if (opt.code === 10) {
557
+ cookieSupported = true;
558
+ const cookieData = opt.data;
559
+ // Full cookie: 8 bytes client + 8-32 bytes server
560
+ serverCookieLength = cookieData.length > 8 ? cookieData.length - 8 : 0;
561
+ }
562
+ }
563
+ }
564
+ }
565
+ const findings = [];
566
+ if (cookieSupported && serverCookieLength > 0) {
567
+ findings.push("OK: DNS Cookie support detected. Server returned a server cookie.");
568
+ findings.push(`Server cookie length: ${serverCookieLength} bytes`);
569
+ }
570
+ else if (cookieSupported && serverCookieLength === 0) {
571
+ findings.push("WARNING: Server echoed cookie option but did not include a server cookie");
572
+ }
573
+ else {
574
+ findings.push("WARNING: DNS Cookie not supported by this server");
575
+ findings.push("Impact: Vulnerable to off-path cache poisoning and forgery attacks (RFC 7873)");
576
+ findings.push("Remediation: Enable DNS Cookie support in server configuration");
577
+ }
578
+ return json({
579
+ nameserver,
580
+ cookie_supported: cookieSupported,
581
+ server_cookie_present: serverCookieLength > 0,
582
+ server_cookie_length: serverCookieLength,
583
+ client_cookie_hex: clientCookie.toString("hex"),
584
+ findings,
585
+ });
586
+ }
587
+ catch (err) {
588
+ return json({
589
+ nameserver,
590
+ cookie_supported: false,
591
+ error: err.message,
592
+ findings: ["Could not test DNS Cookie support (server may be unreachable)"],
593
+ });
594
+ }
595
+ },
596
+ };
597
+ // ─── Tool 8: infra_axfr_protection ───
598
+ const infraAxfrProtection = {
599
+ name: "infra_axfr_protection",
600
+ description: "Test if a DNS nameserver allows unauthorized AXFR (zone transfer) for a domain. Successful zone transfer is a critical misconfiguration that exposes all DNS records to attackers.",
601
+ schema: {
602
+ nameserver: z
603
+ .string()
604
+ .describe("IP address or hostname of the DNS nameserver to test"),
605
+ domain: z
606
+ .string()
607
+ .describe("Domain name to attempt zone transfer for (e.g. 'example.com')"),
608
+ },
609
+ async execute(args) {
610
+ const nameserver = args.nameserver;
611
+ const domain = args.domain;
612
+ try {
613
+ const answers = await queryTcp(domain, "AXFR", nameserver, 53);
614
+ const recordTypes = new Map();
615
+ for (const answer of answers) {
616
+ const t = answer.type ?? "UNKNOWN";
617
+ recordTypes.set(t, (recordTypes.get(t) ?? 0) + 1);
618
+ }
619
+ const findings = [];
620
+ if (answers.length > 0) {
621
+ findings.push(`CRITICAL: Zone transfer (AXFR) succeeded! ${answers.length} records exposed.`);
622
+ findings.push("Impact: Full zone data leaked — subdomains, mail servers, internal hosts all visible to attackers");
623
+ findings.push("Remediation: Restrict AXFR to authorized secondary nameservers using allow-transfer ACLs");
624
+ const typeBreakdown = Array.from(recordTypes.entries())
625
+ .map(([type, count]) => `${type}: ${count}`)
626
+ .join(", ");
627
+ findings.push(`Record types exposed: ${typeBreakdown}`);
628
+ }
629
+ else {
630
+ findings.push("OK: Zone transfer (AXFR) was refused or returned no records");
631
+ }
632
+ return json({
633
+ nameserver,
634
+ domain,
635
+ axfr_allowed: answers.length > 0,
636
+ records_exposed: answers.length,
637
+ record_type_breakdown: Object.fromEntries(recordTypes),
638
+ sample_records: answers.slice(0, 20).map((a) => ({
639
+ name: a.name,
640
+ type: a.type,
641
+ data: a.data ?? a.address ?? a.target ?? String(a),
642
+ })),
643
+ findings,
644
+ });
645
+ }
646
+ catch (err) {
647
+ const errMsg = err.message;
648
+ const isRefused = errMsg.includes("REFUSED") || errMsg.includes("refused") || errMsg.includes("ECONNRESET");
649
+ return json({
650
+ nameserver,
651
+ domain,
652
+ axfr_allowed: false,
653
+ records_exposed: 0,
654
+ error: errMsg,
655
+ findings: isRefused
656
+ ? ["OK: Zone transfer was explicitly refused by the server"]
657
+ : [`Could not complete AXFR test: ${errMsg}`],
658
+ });
659
+ }
660
+ },
661
+ };
662
+ // ─── Tool 9: infra_ns_diversity ───
663
+ const infraNsDiversity = {
664
+ name: "infra_ns_diversity",
665
+ description: "Analyze nameserver diversity for a domain. Checks if NS records resolve to IPs in different ASNs, different /24 subnets, and different providers. Single point of failure = high risk if one provider goes down.",
666
+ schema: {
667
+ domain: z
668
+ .string()
669
+ .describe("Domain name to check NS diversity for (e.g. 'example.com')"),
670
+ },
671
+ async execute(args) {
672
+ const domain = args.domain;
673
+ try {
674
+ const resolver = createResolver();
675
+ const nsRecords = await resolver.resolveNs(domain);
676
+ if (nsRecords.length === 0) {
677
+ return json({
678
+ domain,
679
+ ns_count: 0,
680
+ findings: ["CRITICAL: No NS records found for this domain"],
681
+ });
682
+ }
683
+ const nsDetails = [];
684
+ for (const ns of nsRecords) {
685
+ let ips = [];
686
+ try {
687
+ ips = await resolver.resolve4(ns);
688
+ }
689
+ catch {
690
+ // Could not resolve NS hostname
691
+ }
692
+ let asn = null;
693
+ let asnName = null;
694
+ let subnet24 = null;
695
+ if (ips.length > 0) {
696
+ const ip = ips[0];
697
+ subnet24 = ip.split(".").slice(0, 3).join(".") + ".0/24";
698
+ // ASN lookup via Cymru DNS
699
+ try {
700
+ const reversed = ip.split(".").reverse().join(".");
701
+ const asnLookup = `${reversed}.origin.asn.cymru.com`;
702
+ const asnResolver = createResolver("8.8.8.8");
703
+ const txtRecords = await asnResolver.resolveTxt(asnLookup);
704
+ if (txtRecords.length > 0) {
705
+ const parts = txtRecords[0].join("").split("|").map((s) => s.trim());
706
+ asn = parts[0] ?? null;
707
+ // Get ASN name
708
+ if (asn) {
709
+ try {
710
+ const asnNameRecords = await asnResolver.resolveTxt(`AS${asn}.asn.cymru.com`);
711
+ if (asnNameRecords.length > 0) {
712
+ const nameParts = asnNameRecords[0].join("").split("|").map((s) => s.trim());
713
+ asnName = nameParts[nameParts.length - 1] ?? null;
714
+ }
715
+ }
716
+ catch {
717
+ // ASN name lookup failed
718
+ }
719
+ }
720
+ }
721
+ }
722
+ catch {
723
+ // ASN lookup failed
724
+ }
725
+ }
726
+ nsDetails.push({
727
+ hostname: ns,
728
+ ips,
729
+ asn,
730
+ asn_name: asnName,
731
+ subnet_24: subnet24,
732
+ });
733
+ }
734
+ // Analysis
735
+ const uniqueAsns = new Set(nsDetails.map((n) => n.asn).filter(Boolean));
736
+ const uniqueSubnets = new Set(nsDetails.map((n) => n.subnet_24).filter(Boolean));
737
+ const allSameAsn = uniqueAsns.size <= 1 && nsDetails.every((n) => n.asn !== null);
738
+ const allSameSubnet = uniqueSubnets.size <= 1 && nsDetails.every((n) => n.subnet_24 !== null);
739
+ const findings = [];
740
+ if (nsRecords.length < 2) {
741
+ findings.push("CRITICAL: Only 1 nameserver — no redundancy at all");
742
+ }
743
+ else if (nsRecords.length === 2) {
744
+ findings.push("WARNING: Only 2 nameservers — minimal redundancy (recommend 3+)");
745
+ }
746
+ else {
747
+ findings.push(`OK: ${nsRecords.length} nameservers configured`);
748
+ }
749
+ if (allSameAsn && uniqueAsns.size === 1) {
750
+ const asnVal = [...uniqueAsns][0];
751
+ const asnNameVal = nsDetails.find((n) => n.asn === asnVal)?.asn_name ?? "unknown";
752
+ findings.push(`HIGH: All nameservers in the same ASN (AS${asnVal} — ${asnNameVal}). Single provider failure = total DNS outage.`);
753
+ findings.push("Remediation: Use nameservers from at least 2 different providers/ASNs");
754
+ }
755
+ else if (uniqueAsns.size > 1) {
756
+ findings.push(`OK: Nameservers distributed across ${uniqueAsns.size} ASN(s)`);
757
+ }
758
+ if (allSameSubnet && uniqueSubnets.size === 1) {
759
+ findings.push(`HIGH: All nameservers in the same /24 subnet (${[...uniqueSubnets][0]}). Network-level failure = total DNS outage.`);
760
+ }
761
+ else if (uniqueSubnets.size > 1) {
762
+ findings.push(`OK: Nameservers distributed across ${uniqueSubnets.size} subnet(s)`);
763
+ }
764
+ return json({
765
+ domain,
766
+ ns_count: nsRecords.length,
767
+ nameservers: nsDetails,
768
+ unique_asns: [...uniqueAsns],
769
+ unique_subnets: [...uniqueSubnets],
770
+ all_same_asn: allSameAsn,
771
+ all_same_subnet: allSameSubnet,
772
+ diversity_score: Math.min(100, (uniqueAsns.size * 30) + (uniqueSubnets.size * 20) + (Math.min(nsRecords.length, 4) * 10)),
773
+ findings,
774
+ });
775
+ }
776
+ catch (err) {
777
+ return json({
778
+ domain,
779
+ error: err.message,
780
+ findings: [`Could not analyze NS diversity: ${err.message}`],
781
+ });
782
+ }
783
+ },
784
+ };
785
+ // ─── Export ───
786
+ export const infraTools = [
787
+ infraOpenResolver,
788
+ infraAmplification,
789
+ infraRateLimiting,
790
+ infraSoftwareCve,
791
+ infraEdnsCompliance,
792
+ infraTcpFallback,
793
+ infraDnsCookie,
794
+ infraAxfrProtection,
795
+ infraNsDiversity,
796
+ ];
797
+ //# sourceMappingURL=index.js.map