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,1377 @@
1
+ import { z } from "zod";
2
+ import { text } from "../types/index.js";
3
+ import { queryRaw } from "../utils/dns-client.js";
4
+ // ─── Constants ───
5
+ const DEFAULT_RESOLVER = "8.8.8.8";
6
+ const ALGORITHM_NAMES = {
7
+ 1: "RSA/MD5",
8
+ 3: "DSA",
9
+ 5: "RSASHA1",
10
+ 6: "DSA-NSEC3-SHA1",
11
+ 7: "RSASHA1-NSEC3-SHA1",
12
+ 8: "RSASHA256",
13
+ 10: "RSASHA512",
14
+ 12: "ECC-GOST",
15
+ 13: "ECDSAP256SHA256",
16
+ 14: "ECDSAP384SHA384",
17
+ 15: "ED25519",
18
+ 16: "ED448",
19
+ };
20
+ const DEPRECATED_ALGORITHMS = new Set([1, 3, 5]);
21
+ const WEAK_ALGORITHMS = new Set([1, 3, 5, 6, 7]);
22
+ const STRONG_ALGORITHMS = new Set([13, 14, 15, 16]);
23
+ const DIGEST_TYPE_NAMES = {
24
+ 1: "SHA-1",
25
+ 2: "SHA-256",
26
+ 3: "GOST R 34.11-94",
27
+ 4: "SHA-384",
28
+ };
29
+ const WEAK_DIGEST_TYPES = new Set([1]); // SHA-1
30
+ const STRONG_DIGEST_TYPES = new Set([2, 4]); // SHA-256, SHA-384
31
+ // ─── Helpers ───
32
+ function getAlgorithmName(alg) {
33
+ return ALGORITHM_NAMES[alg] ?? `UNKNOWN(${alg})`;
34
+ }
35
+ function getDigestTypeName(dt) {
36
+ return DIGEST_TYPE_NAMES[dt] ?? `UNKNOWN(${dt})`;
37
+ }
38
+ function getParentZone(domain) {
39
+ const parts = domain.split(".");
40
+ if (parts.length <= 1)
41
+ return ".";
42
+ return parts.slice(1).join(".");
43
+ }
44
+ function getTldZone(domain) {
45
+ const parts = domain.split(".");
46
+ if (parts.length <= 1)
47
+ return domain;
48
+ return parts[parts.length - 1];
49
+ }
50
+ function severityScore(severity) {
51
+ const scores = {
52
+ critical: 5,
53
+ high: 4,
54
+ medium: 3,
55
+ low: 2,
56
+ info: 1,
57
+ };
58
+ return scores[severity] ?? 0;
59
+ }
60
+ // ─── Core Logic Functions ───
61
+ async function queryDnskeys(domain) {
62
+ const result = await queryRaw(domain, "DNSKEY", DEFAULT_RESOLVER, 53, { dnssec: true });
63
+ const keys = [];
64
+ for (const answer of result.answers) {
65
+ if (answer.type !== "DNSKEY")
66
+ continue;
67
+ const data = answer.data;
68
+ const flags = data.flags;
69
+ const protocol = data.protocol;
70
+ const algorithm = data.algorithm;
71
+ keys.push({
72
+ flags,
73
+ protocol,
74
+ algorithm,
75
+ algorithmName: getAlgorithmName(algorithm),
76
+ keyType: flags === 257 ? "KSK" : flags === 256 ? "ZSK" : `OTHER(${flags})`,
77
+ key: data.key ? Buffer.from(data.key).toString("base64") : "",
78
+ });
79
+ }
80
+ return keys;
81
+ }
82
+ async function queryDsRecords(domain) {
83
+ const result = await queryRaw(domain, "DS", DEFAULT_RESOLVER, 53, { dnssec: true });
84
+ const records = [];
85
+ for (const answer of result.answers) {
86
+ if (answer.type !== "DS")
87
+ continue;
88
+ const data = answer.data;
89
+ records.push({
90
+ keyTag: data.keyTag,
91
+ algorithm: data.algorithm,
92
+ algorithmName: getAlgorithmName(data.algorithm),
93
+ digestType: data.digestType,
94
+ digestTypeName: getDigestTypeName(data.digestType),
95
+ digest: data.digest ? Buffer.from(data.digest).toString("hex") : "",
96
+ });
97
+ }
98
+ return records;
99
+ }
100
+ async function queryRrsigs(domain) {
101
+ const result = await queryRaw(domain, "A", DEFAULT_RESOLVER, 53, { dnssec: true });
102
+ const sigs = [];
103
+ const now = Math.floor(Date.now() / 1000);
104
+ const allRecords = [...result.answers, ...result.authorities, ...result.additionals];
105
+ for (const answer of allRecords) {
106
+ if (answer.type !== "RRSIG")
107
+ continue;
108
+ const data = answer.data;
109
+ const expiration = data.expiration;
110
+ const inception = data.inception;
111
+ const daysUntilExpiry = Math.floor((expiration - now) / 86400);
112
+ sigs.push({
113
+ typeCovered: data.typeCovered,
114
+ algorithm: data.algorithm,
115
+ algorithmName: getAlgorithmName(data.algorithm),
116
+ labels: data.labels,
117
+ originalTtl: data.originalTTL,
118
+ expiration,
119
+ inception,
120
+ keyTag: data.keyTag,
121
+ signerName: data.signerName,
122
+ daysUntilExpiry,
123
+ expiringWithin7Days: daysUntilExpiry >= 0 && daysUntilExpiry <= 7,
124
+ expired: daysUntilExpiry < 0,
125
+ });
126
+ }
127
+ return sigs;
128
+ }
129
+ async function queryNsecRecords(domain) {
130
+ // Query for a non-existent subdomain to trigger NSEC/NSEC3 in authorities
131
+ const nxName = `_dnssec-probe-${Date.now()}.${domain}`;
132
+ const result = await queryRaw(nxName, "A", DEFAULT_RESOLVER, 53, { dnssec: true, cd: true });
133
+ const nsecRecords = [];
134
+ let foundType = "NONE";
135
+ let nsec3params;
136
+ const allRecords = [...result.authorities, ...result.additionals];
137
+ for (const record of allRecords) {
138
+ if (record.type === "NSEC") {
139
+ foundType = "NSEC";
140
+ nsecRecords.push({
141
+ name: record.name,
142
+ type: "NSEC",
143
+ data: record.data,
144
+ });
145
+ }
146
+ else if (record.type === "NSEC3") {
147
+ foundType = "NSEC3";
148
+ const data = record.data;
149
+ nsecRecords.push({
150
+ name: record.name,
151
+ type: "NSEC3",
152
+ data,
153
+ });
154
+ if (!nsec3params) {
155
+ nsec3params = {
156
+ hashAlgorithm: data.algorithm ?? data.hashAlgorithm ?? 1,
157
+ iterations: data.iterations ?? 0,
158
+ salt: data.salt ? Buffer.from(data.salt).toString("hex") : "(empty)",
159
+ };
160
+ }
161
+ }
162
+ else if (record.type === "NSEC3PARAM") {
163
+ const data = record.data;
164
+ nsec3params = {
165
+ hashAlgorithm: data.algorithm ?? data.hashAlgorithm ?? 1,
166
+ iterations: data.iterations ?? 0,
167
+ salt: data.salt ? Buffer.from(data.salt).toString("hex") : "(empty)",
168
+ };
169
+ }
170
+ }
171
+ // Also try querying NSEC3PARAM directly for the zone
172
+ if (foundType === "NONE") {
173
+ try {
174
+ const nsec3Result = await queryRaw(domain, "NSEC3PARAM", DEFAULT_RESOLVER, 53, { dnssec: true });
175
+ for (const record of nsec3Result.answers) {
176
+ if (record.type === "NSEC3PARAM") {
177
+ foundType = "NSEC3";
178
+ const data = record.data;
179
+ nsec3params = {
180
+ hashAlgorithm: data.algorithm ?? data.hashAlgorithm ?? 1,
181
+ iterations: data.iterations ?? 0,
182
+ salt: data.salt ? Buffer.from(data.salt).toString("hex") : "(empty)",
183
+ };
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ // NSEC3PARAM query failed, continue
189
+ }
190
+ }
191
+ return { type: foundType, records: nsecRecords, nsec3params };
192
+ }
193
+ async function performChainOfTrustValidation(domain) {
194
+ const parts = domain.split(".");
195
+ const levels = [];
196
+ // Check root -> TLD -> domain
197
+ const zones = [
198
+ domain,
199
+ parts.length > 1 ? parts.slice(1).join(".") : null, // TLD or parent
200
+ ".",
201
+ ].filter(Boolean);
202
+ // Reverse to go root -> TLD -> domain
203
+ zones.reverse();
204
+ let chainIntact = true;
205
+ let domainSigned = false;
206
+ for (const zone of zones) {
207
+ const zoneResult = {
208
+ zone,
209
+ hasDnskey: false,
210
+ hasDs: false,
211
+ hasRrsig: false,
212
+ authenticatedData: false,
213
+ issues: [],
214
+ };
215
+ try {
216
+ // Check DNSKEY
217
+ const dnskeyResult = await queryRaw(zone, "DNSKEY", DEFAULT_RESOLVER, 53, { dnssec: true });
218
+ const dnskeys = dnskeyResult.answers.filter((a) => a.type === "DNSKEY");
219
+ zoneResult.hasDnskey = dnskeys.length > 0;
220
+ zoneResult.authenticatedData = dnskeyResult.flags.authenticatedData;
221
+ if (!zoneResult.hasDnskey && zone !== ".") {
222
+ zoneResult.issues.push("No DNSKEY records found");
223
+ chainIntact = false;
224
+ }
225
+ // Check for RRSIG on DNSKEY
226
+ const rrsigs = dnskeyResult.answers.filter((a) => a.type === "RRSIG");
227
+ zoneResult.hasRrsig = rrsigs.length > 0;
228
+ if (zoneResult.hasDnskey && !zoneResult.hasRrsig) {
229
+ zoneResult.issues.push("DNSKEY present but no RRSIG covering it");
230
+ chainIntact = false;
231
+ }
232
+ }
233
+ catch {
234
+ zoneResult.issues.push("Failed to query DNSKEY");
235
+ chainIntact = false;
236
+ }
237
+ // Check DS record (not for root)
238
+ if (zone !== "." && zone !== domain) {
239
+ try {
240
+ // DS for the next zone down
241
+ const nextZoneIndex = zones.indexOf(zone) + 1;
242
+ if (nextZoneIndex < zones.length) {
243
+ const childZone = zones[nextZoneIndex];
244
+ const dsResult = await queryRaw(childZone, "DS", DEFAULT_RESOLVER, 53, { dnssec: true });
245
+ const dsRecords = dsResult.answers.filter((a) => a.type === "DS");
246
+ zoneResult.hasDs = dsRecords.length > 0;
247
+ }
248
+ }
249
+ catch {
250
+ zoneResult.issues.push("Failed to query DS");
251
+ }
252
+ }
253
+ if (zone === domain) {
254
+ domainSigned = zoneResult.hasDnskey;
255
+ }
256
+ levels.push(zoneResult);
257
+ }
258
+ // Also check DS for the domain itself at the parent
259
+ try {
260
+ const dsResult = await queryRaw(domain, "DS", DEFAULT_RESOLVER, 53, { dnssec: true });
261
+ const dsRecords = dsResult.answers.filter((a) => a.type === "DS");
262
+ if (dsRecords.length > 0) {
263
+ const domainLevel = levels.find((l) => l.zone === domain);
264
+ if (domainLevel)
265
+ domainLevel.hasDs = true;
266
+ }
267
+ else if (domainSigned) {
268
+ const domainLevel = levels.find((l) => l.zone === domain);
269
+ if (domainLevel) {
270
+ domainLevel.issues.push("DNSKEY present but no DS record at parent zone");
271
+ chainIntact = false;
272
+ }
273
+ }
274
+ }
275
+ catch {
276
+ // DS query failed
277
+ }
278
+ let status;
279
+ if (!domainSigned) {
280
+ status = "not_signed";
281
+ }
282
+ else if (chainIntact) {
283
+ status = "valid";
284
+ }
285
+ else {
286
+ status = "broken";
287
+ }
288
+ return { status, levels };
289
+ }
290
+ async function performAlgorithmAudit(domain) {
291
+ const algorithms = [];
292
+ const findings = [];
293
+ const seenAlgorithms = new Set();
294
+ function classifyAlgorithm(alg) {
295
+ if (DEPRECATED_ALGORITHMS.has(alg))
296
+ return "deprecated";
297
+ if (WEAK_ALGORITHMS.has(alg))
298
+ return "weak";
299
+ if (STRONG_ALGORITHMS.has(alg))
300
+ return "strong";
301
+ return "acceptable";
302
+ }
303
+ // Check DNSKEY algorithms
304
+ try {
305
+ const keys = await queryDnskeys(domain);
306
+ for (const key of keys) {
307
+ seenAlgorithms.add(key.algorithm);
308
+ algorithms.push({
309
+ source: `DNSKEY (${key.keyType})`,
310
+ algorithmNumber: key.algorithm,
311
+ algorithmName: key.algorithmName,
312
+ strength: classifyAlgorithm(key.algorithm),
313
+ });
314
+ }
315
+ }
316
+ catch {
317
+ findings.push({
318
+ severity: "info",
319
+ title: "DNSKEY query failed",
320
+ description: "Could not retrieve DNSKEY records for algorithm audit.",
321
+ });
322
+ }
323
+ // Check DS algorithms
324
+ try {
325
+ const dsRecords = await queryDsRecords(domain);
326
+ for (const ds of dsRecords) {
327
+ seenAlgorithms.add(ds.algorithm);
328
+ algorithms.push({
329
+ source: "DS",
330
+ algorithmNumber: ds.algorithm,
331
+ algorithmName: ds.algorithmName,
332
+ strength: classifyAlgorithm(ds.algorithm),
333
+ });
334
+ }
335
+ }
336
+ catch {
337
+ findings.push({
338
+ severity: "info",
339
+ title: "DS query failed",
340
+ description: "Could not retrieve DS records for algorithm audit.",
341
+ });
342
+ }
343
+ // Check RRSIG algorithms
344
+ try {
345
+ const rrsigs = await queryRrsigs(domain);
346
+ for (const sig of rrsigs) {
347
+ seenAlgorithms.add(sig.algorithm);
348
+ algorithms.push({
349
+ source: `RRSIG (covers ${sig.typeCovered})`,
350
+ algorithmNumber: sig.algorithm,
351
+ algorithmName: sig.algorithmName,
352
+ strength: classifyAlgorithm(sig.algorithm),
353
+ });
354
+ }
355
+ }
356
+ catch {
357
+ findings.push({
358
+ severity: "info",
359
+ title: "RRSIG query failed",
360
+ description: "Could not retrieve RRSIG records for algorithm audit.",
361
+ });
362
+ }
363
+ // Generate findings for deprecated/weak algorithms
364
+ for (const alg of seenAlgorithms) {
365
+ if (DEPRECATED_ALGORITHMS.has(alg)) {
366
+ findings.push({
367
+ severity: "critical",
368
+ title: `Deprecated DNSSEC algorithm: ${getAlgorithmName(alg)} (${alg})`,
369
+ description: `Algorithm ${getAlgorithmName(alg)} is deprecated and considered insecure. It should be replaced immediately.`,
370
+ remediation: `Migrate to ECDSAP256SHA256 (13) or ED25519 (15).`,
371
+ });
372
+ }
373
+ else if (WEAK_ALGORITHMS.has(alg)) {
374
+ findings.push({
375
+ severity: "high",
376
+ title: `Weak DNSSEC algorithm: ${getAlgorithmName(alg)} (${alg})`,
377
+ description: `Algorithm ${getAlgorithmName(alg)} is considered weak and may be vulnerable.`,
378
+ remediation: `Consider migrating to ECDSAP256SHA256 (13) or ED25519 (15).`,
379
+ });
380
+ }
381
+ }
382
+ if (seenAlgorithms.size > 0 && ![...seenAlgorithms].some((a) => STRONG_ALGORITHMS.has(a))) {
383
+ findings.push({
384
+ severity: "medium",
385
+ title: "No strong DNSSEC algorithms in use",
386
+ description: "The domain does not use any of the recommended strong algorithms (ECDSAP256SHA256, ECDSAP384SHA384, ED25519, ED448).",
387
+ remediation: "Consider migrating to ECDSAP256SHA256 (13) or ED25519 (15) for improved security and performance.",
388
+ });
389
+ }
390
+ return { algorithms, findings };
391
+ }
392
+ async function performKeyRolloverCheck(domain) {
393
+ const findings = [];
394
+ let keys = [];
395
+ let dsRecords = [];
396
+ try {
397
+ keys = await queryDnskeys(domain);
398
+ }
399
+ catch {
400
+ findings.push({
401
+ severity: "info",
402
+ title: "DNSKEY query failed",
403
+ description: "Could not retrieve DNSKEY records for key rollover check.",
404
+ });
405
+ return { rolloverDetected: false, method: "unknown", keys, dsRecords, findings };
406
+ }
407
+ try {
408
+ dsRecords = await queryDsRecords(domain);
409
+ }
410
+ catch {
411
+ findings.push({
412
+ severity: "info",
413
+ title: "DS query failed",
414
+ description: "Could not retrieve DS records for key rollover check.",
415
+ });
416
+ }
417
+ const ksks = keys.filter((k) => k.keyType === "KSK");
418
+ const zsks = keys.filter((k) => k.keyType === "ZSK");
419
+ let rolloverDetected = false;
420
+ let method = "none";
421
+ // Multiple KSKs indicate KSK rollover
422
+ if (ksks.length > 1) {
423
+ rolloverDetected = true;
424
+ // Check if DS records match all KSKs
425
+ // If DS count < KSK count, it's likely pre-publish method
426
+ // If DS count >= KSK count, it's likely double-signature method
427
+ if (dsRecords.length > 0 && dsRecords.length < ksks.length) {
428
+ method = "pre-publish";
429
+ findings.push({
430
+ severity: "info",
431
+ title: "KSK rollover in progress (pre-publish method)",
432
+ description: `Detected ${ksks.length} KSKs but only ${dsRecords.length} DS record(s). The new KSK is published in the zone but not yet in the parent DS. This is consistent with pre-publish key rollover.`,
433
+ });
434
+ }
435
+ else if (dsRecords.length >= ksks.length) {
436
+ method = "double-signature";
437
+ findings.push({
438
+ severity: "info",
439
+ title: "KSK rollover in progress (double-signature method)",
440
+ description: `Detected ${ksks.length} KSKs with ${dsRecords.length} DS record(s). Both old and new keys have corresponding DS records. This is consistent with double-signature key rollover.`,
441
+ });
442
+ }
443
+ else {
444
+ method = "ksk-rollover-detected";
445
+ findings.push({
446
+ severity: "medium",
447
+ title: "Multiple KSKs without matching DS",
448
+ description: `Detected ${ksks.length} KSKs but no DS records at parent. This may indicate a misconfigured KSK rollover or the domain's parent has not published the DS record.`,
449
+ remediation: "Ensure the parent zone has the correct DS record(s) for the active KSK(s).",
450
+ });
451
+ }
452
+ }
453
+ // Multiple ZSKs indicate ZSK rollover
454
+ if (zsks.length > 1) {
455
+ rolloverDetected = true;
456
+ if (method === "none")
457
+ method = "zsk-pre-publish";
458
+ findings.push({
459
+ severity: "info",
460
+ title: "ZSK rollover in progress",
461
+ description: `Detected ${zsks.length} ZSKs. This is typical of a pre-publish ZSK rollover where both old and new ZSK are published simultaneously.`,
462
+ });
463
+ }
464
+ // Check for algorithm mismatch between KSKs
465
+ if (ksks.length > 1) {
466
+ const algorithms = new Set(ksks.map((k) => k.algorithm));
467
+ if (algorithms.size > 1) {
468
+ findings.push({
469
+ severity: "medium",
470
+ title: "Algorithm rollover detected",
471
+ description: `KSKs use different algorithms: ${[...algorithms].map((a) => `${getAlgorithmName(a)} (${a})`).join(", ")}. This may indicate an algorithm rollover.`,
472
+ remediation: "Ensure the algorithm rollover completes properly and old algorithms are retired.",
473
+ });
474
+ }
475
+ }
476
+ if (!rolloverDetected) {
477
+ findings.push({
478
+ severity: "info",
479
+ title: "No key rollover detected",
480
+ description: `Found ${ksks.length} KSK(s) and ${zsks.length} ZSK(s). No active key rollover detected.`,
481
+ });
482
+ }
483
+ return { rolloverDetected, method, keys, dsRecords, findings };
484
+ }
485
+ // ─── Tool 1: dnssec_validate ───
486
+ const dnssecValidate = {
487
+ name: "dnssec_validate",
488
+ description: "Full DNSSEC chain of trust validation from root to TLD to domain. Queries DNSKEY, DS, and RRSIG records at each level. Reports whether the chain is valid, broken, or the domain is not signed.",
489
+ schema: {
490
+ domain: z
491
+ .string()
492
+ .describe("The domain name to validate DNSSEC chain of trust for (e.g. 'example.com')"),
493
+ },
494
+ async execute(args) {
495
+ const domain = args.domain;
496
+ try {
497
+ const result = await performChainOfTrustValidation(domain);
498
+ const lines = [
499
+ `DNSSEC Chain of Trust Validation: ${domain}`,
500
+ `${"=".repeat(60)}`,
501
+ `Status: ${result.status.toUpperCase()}`,
502
+ "",
503
+ ];
504
+ if (result.status === "valid") {
505
+ lines.push("The DNSSEC chain of trust is intact from root to this domain.");
506
+ }
507
+ else if (result.status === "broken") {
508
+ lines.push("WARNING: The DNSSEC chain of trust is BROKEN. DNS responses may not be authenticated.");
509
+ }
510
+ else {
511
+ lines.push("This domain is NOT signed with DNSSEC. DNS responses are not authenticated.");
512
+ }
513
+ lines.push("");
514
+ lines.push("Chain Details:");
515
+ lines.push("-".repeat(40));
516
+ for (const level of result.levels) {
517
+ lines.push(`\nZone: ${level.zone}`);
518
+ lines.push(` DNSKEY: ${level.hasDnskey ? "YES" : "NO"}`);
519
+ lines.push(` DS: ${level.hasDs ? "YES" : "NO"}`);
520
+ lines.push(` RRSIG: ${level.hasRrsig ? "YES" : "NO"}`);
521
+ lines.push(` AD bit: ${level.authenticatedData ? "SET" : "NOT SET"}`);
522
+ if (level.issues.length > 0) {
523
+ lines.push(` Issues:`);
524
+ for (const issue of level.issues) {
525
+ lines.push(` - ${issue}`);
526
+ }
527
+ }
528
+ }
529
+ return text(lines.join("\n"));
530
+ }
531
+ catch (err) {
532
+ return text(`Error validating DNSSEC for ${domain}: ${err.message}`);
533
+ }
534
+ },
535
+ };
536
+ // ─── Tool 2: dnssec_check_ds ───
537
+ const dnssecCheckDs = {
538
+ name: "dnssec_check_ds",
539
+ description: "Check DS (Delegation Signer) records for a domain. Queries DS from parent zone and DNSKEY from child zone. Verifies DS existence and reports algorithm strength and digest type.",
540
+ schema: {
541
+ domain: z
542
+ .string()
543
+ .describe("The domain name to check DS records for (e.g. 'example.com')"),
544
+ },
545
+ async execute(args) {
546
+ const domain = args.domain;
547
+ try {
548
+ const dsRecords = await queryDsRecords(domain);
549
+ let dnskeys = [];
550
+ try {
551
+ dnskeys = await queryDnskeys(domain);
552
+ }
553
+ catch {
554
+ // DNSKEY query may fail
555
+ }
556
+ const lines = [
557
+ `DS Record Check: ${domain}`,
558
+ `${"=".repeat(60)}`,
559
+ "",
560
+ ];
561
+ if (dsRecords.length === 0) {
562
+ lines.push("No DS records found at parent zone.");
563
+ lines.push("This domain either does not use DNSSEC or the parent zone has not published the DS record.");
564
+ return text(lines.join("\n"));
565
+ }
566
+ lines.push(`Found ${dsRecords.length} DS record(s):`);
567
+ lines.push("");
568
+ for (const ds of dsRecords) {
569
+ lines.push(` Key Tag: ${ds.keyTag}`);
570
+ lines.push(` Algorithm: ${ds.algorithmName} (${ds.algorithm})`);
571
+ // Algorithm strength assessment
572
+ if (DEPRECATED_ALGORITHMS.has(ds.algorithm)) {
573
+ lines.push(` Algorithm Strength: DEPRECATED - INSECURE`);
574
+ }
575
+ else if (WEAK_ALGORITHMS.has(ds.algorithm)) {
576
+ lines.push(` Algorithm Strength: WEAK`);
577
+ }
578
+ else if (STRONG_ALGORITHMS.has(ds.algorithm)) {
579
+ lines.push(` Algorithm Strength: STRONG`);
580
+ }
581
+ else {
582
+ lines.push(` Algorithm Strength: ACCEPTABLE`);
583
+ }
584
+ lines.push(` Digest Type: ${ds.digestTypeName} (${ds.digestType})`);
585
+ if (WEAK_DIGEST_TYPES.has(ds.digestType)) {
586
+ lines.push(` Digest Strength: WEAK - SHA-1 is deprecated for DNSSEC`);
587
+ }
588
+ else if (STRONG_DIGEST_TYPES.has(ds.digestType)) {
589
+ lines.push(` Digest Strength: STRONG`);
590
+ }
591
+ else {
592
+ lines.push(` Digest Strength: ACCEPTABLE`);
593
+ }
594
+ lines.push(` Digest: ${ds.digest}`);
595
+ lines.push("");
596
+ }
597
+ // Cross-reference with DNSKEY
598
+ if (dnskeys.length > 0) {
599
+ const ksks = dnskeys.filter((k) => k.keyType === "KSK");
600
+ lines.push(`Child zone has ${dnskeys.length} DNSKEY(s) (${ksks.length} KSK, ${dnskeys.length - ksks.length} ZSK).`);
601
+ if (ksks.length === 0) {
602
+ lines.push("WARNING: No KSK found in child zone. DS record may reference a missing key.");
603
+ }
604
+ }
605
+ else {
606
+ lines.push("WARNING: Could not retrieve DNSKEY records from child zone for cross-reference.");
607
+ }
608
+ return text(lines.join("\n"));
609
+ }
610
+ catch (err) {
611
+ return text(`Error checking DS for ${domain}: ${err.message}`);
612
+ }
613
+ },
614
+ };
615
+ // ─── Tool 3: dnssec_check_dnskey ───
616
+ const dnssecCheckDnskey = {
617
+ name: "dnssec_check_dnskey",
618
+ description: "List all DNSKEY records for a domain. Distinguishes KSK (flag 257) vs ZSK (flag 256), reports algorithm and protocol. Flags weak algorithms and identifies strong ones.",
619
+ schema: {
620
+ domain: z
621
+ .string()
622
+ .describe("The domain name to check DNSKEY records for (e.g. 'example.com')"),
623
+ },
624
+ async execute(args) {
625
+ const domain = args.domain;
626
+ try {
627
+ const keys = await queryDnskeys(domain);
628
+ const lines = [
629
+ `DNSKEY Record Check: ${domain}`,
630
+ `${"=".repeat(60)}`,
631
+ "",
632
+ ];
633
+ if (keys.length === 0) {
634
+ lines.push("No DNSKEY records found. This domain does not appear to use DNSSEC.");
635
+ return text(lines.join("\n"));
636
+ }
637
+ const ksks = keys.filter((k) => k.keyType === "KSK");
638
+ const zsks = keys.filter((k) => k.keyType === "ZSK");
639
+ const others = keys.filter((k) => k.keyType !== "KSK" && k.keyType !== "ZSK");
640
+ lines.push(`Found ${keys.length} DNSKEY(s): ${ksks.length} KSK, ${zsks.length} ZSK${others.length > 0 ? `, ${others.length} other` : ""}`);
641
+ lines.push("");
642
+ for (const key of keys) {
643
+ lines.push(` Type: ${key.keyType} (flags=${key.flags})`);
644
+ lines.push(` Algorithm: ${key.algorithmName} (${key.algorithm})`);
645
+ lines.push(` Protocol: ${key.protocol}`);
646
+ if (DEPRECATED_ALGORITHMS.has(key.algorithm)) {
647
+ lines.push(` ** CRITICAL: Deprecated algorithm. ${key.algorithmName} is insecure and should be replaced immediately.`);
648
+ }
649
+ else if (WEAK_ALGORITHMS.has(key.algorithm)) {
650
+ lines.push(` ** WARNING: Weak algorithm. ${key.algorithmName} is considered weak.`);
651
+ }
652
+ else if (STRONG_ALGORITHMS.has(key.algorithm)) {
653
+ lines.push(` Strength: STRONG - ${key.algorithmName} is a recommended algorithm.`);
654
+ }
655
+ else {
656
+ lines.push(` Strength: ACCEPTABLE`);
657
+ }
658
+ lines.push(` Key (base64): ${key.key.substring(0, 40)}${key.key.length > 40 ? "..." : ""}`);
659
+ lines.push("");
660
+ }
661
+ // Summary recommendations
662
+ const allAlgorithms = new Set(keys.map((k) => k.algorithm));
663
+ if ([...allAlgorithms].some((a) => DEPRECATED_ALGORITHMS.has(a))) {
664
+ lines.push("RECOMMENDATION: Replace deprecated algorithms with ECDSAP256SHA256 (13) or ED25519 (15).");
665
+ }
666
+ else if (![...allAlgorithms].some((a) => STRONG_ALGORITHMS.has(a))) {
667
+ lines.push("RECOMMENDATION: Consider upgrading to ECDSAP256SHA256 (13) or ED25519 (15) for better security and smaller key sizes.");
668
+ }
669
+ return text(lines.join("\n"));
670
+ }
671
+ catch (err) {
672
+ return text(`Error checking DNSKEY for ${domain}: ${err.message}`);
673
+ }
674
+ },
675
+ };
676
+ // ─── Tool 4: dnssec_check_rrsig ───
677
+ const dnssecCheckRrsig = {
678
+ name: "dnssec_check_rrsig",
679
+ description: "Check RRSIG (Resource Record Signature) records for a domain. Extracts type covered, algorithm, labels, original TTL, signature expiry/inception, key tag, and signer. Reports days until expiry and flags signatures expiring within 7 days.",
680
+ schema: {
681
+ domain: z
682
+ .string()
683
+ .describe("The domain name to check RRSIG records for (e.g. 'example.com')"),
684
+ },
685
+ async execute(args) {
686
+ const domain = args.domain;
687
+ try {
688
+ // Query multiple record types to get more RRSIGs
689
+ const rrsigsByType = [];
690
+ const seenKeys = new Set();
691
+ const typesToQuery = ["A", "AAAA", "MX", "NS", "SOA", "DNSKEY"];
692
+ for (const qtype of typesToQuery) {
693
+ try {
694
+ const result = await queryRaw(domain, qtype, DEFAULT_RESOLVER, 53, { dnssec: true });
695
+ const allRecords = [...result.answers, ...result.authorities, ...result.additionals];
696
+ const now = Math.floor(Date.now() / 1000);
697
+ for (const answer of allRecords) {
698
+ if (answer.type !== "RRSIG")
699
+ continue;
700
+ const data = answer.data;
701
+ const typeCovered = data.typeCovered;
702
+ const keyTag = data.keyTag;
703
+ const uniqueKey = `${typeCovered}:${keyTag}:${data.expiration}`;
704
+ if (seenKeys.has(uniqueKey))
705
+ continue;
706
+ seenKeys.add(uniqueKey);
707
+ const expiration = data.expiration;
708
+ const inception = data.inception;
709
+ const daysUntilExpiry = Math.floor((expiration - now) / 86400);
710
+ rrsigsByType.push({
711
+ typeCovered,
712
+ algorithm: data.algorithm,
713
+ algorithmName: getAlgorithmName(data.algorithm),
714
+ labels: data.labels,
715
+ originalTtl: data.originalTTL,
716
+ expiration,
717
+ inception,
718
+ keyTag,
719
+ signerName: data.signerName,
720
+ daysUntilExpiry,
721
+ expiringWithin7Days: daysUntilExpiry >= 0 && daysUntilExpiry <= 7,
722
+ expired: daysUntilExpiry < 0,
723
+ });
724
+ }
725
+ }
726
+ catch {
727
+ // Some query types may fail, continue
728
+ }
729
+ }
730
+ const lines = [
731
+ `RRSIG Record Check: ${domain}`,
732
+ `${"=".repeat(60)}`,
733
+ "",
734
+ ];
735
+ if (rrsigsByType.length === 0) {
736
+ lines.push("No RRSIG records found. This domain does not appear to use DNSSEC.");
737
+ return text(lines.join("\n"));
738
+ }
739
+ lines.push(`Found ${rrsigsByType.length} unique RRSIG(s):`);
740
+ lines.push("");
741
+ const expiredSigs = [];
742
+ const expiringSoonSigs = [];
743
+ for (const sig of rrsigsByType) {
744
+ lines.push(` Type Covered: ${sig.typeCovered}`);
745
+ lines.push(` Algorithm: ${sig.algorithmName} (${sig.algorithm})`);
746
+ lines.push(` Labels: ${sig.labels}`);
747
+ lines.push(` Original TTL: ${sig.originalTtl}`);
748
+ lines.push(` Expiration: ${new Date(sig.expiration * 1000).toISOString()}`);
749
+ lines.push(` Inception: ${new Date(sig.inception * 1000).toISOString()}`);
750
+ lines.push(` Key Tag: ${sig.keyTag}`);
751
+ lines.push(` Signer: ${sig.signerName}`);
752
+ if (sig.expired) {
753
+ lines.push(` ** EXPIRED: Signature expired ${Math.abs(sig.daysUntilExpiry)} day(s) ago!`);
754
+ expiredSigs.push(sig);
755
+ }
756
+ else if (sig.expiringWithin7Days) {
757
+ lines.push(` ** WARNING: Expires in ${sig.daysUntilExpiry} day(s)!`);
758
+ expiringSoonSigs.push(sig);
759
+ }
760
+ else {
761
+ lines.push(` Days until expiry: ${sig.daysUntilExpiry}`);
762
+ }
763
+ lines.push("");
764
+ }
765
+ // Summary
766
+ lines.push("-".repeat(40));
767
+ lines.push("Summary:");
768
+ if (expiredSigs.length > 0) {
769
+ lines.push(` CRITICAL: ${expiredSigs.length} expired signature(s) found. DNSSEC validation will fail.`);
770
+ }
771
+ if (expiringSoonSigs.length > 0) {
772
+ lines.push(` WARNING: ${expiringSoonSigs.length} signature(s) expiring within 7 days. Ensure re-signing is scheduled.`);
773
+ }
774
+ if (expiredSigs.length === 0 && expiringSoonSigs.length === 0) {
775
+ lines.push(" All signatures are valid and not expiring soon.");
776
+ }
777
+ return text(lines.join("\n"));
778
+ }
779
+ catch (err) {
780
+ return text(`Error checking RRSIG for ${domain}: ${err.message}`);
781
+ }
782
+ },
783
+ };
784
+ // ─── Tool 5: dnssec_check_nsec ───
785
+ const dnssecCheckNsec = {
786
+ name: "dnssec_check_nsec",
787
+ description: "Check NSEC/NSEC3 records for a domain. Determines if the zone uses NSEC (enumerable via zone walking) or NSEC3 (hashed names). For NSEC3, reports hash algorithm, iterations, and salt. Flags NSEC as informational risk allowing zone walking.",
788
+ schema: {
789
+ domain: z
790
+ .string()
791
+ .describe("The domain name to check NSEC/NSEC3 records for (e.g. 'example.com')"),
792
+ },
793
+ async execute(args) {
794
+ const domain = args.domain;
795
+ try {
796
+ const nsecResult = await queryNsecRecords(domain);
797
+ const lines = [
798
+ `NSEC/NSEC3 Record Check: ${domain}`,
799
+ `${"=".repeat(60)}`,
800
+ "",
801
+ ];
802
+ if (nsecResult.type === "NONE") {
803
+ lines.push("No NSEC or NSEC3 records detected.");
804
+ lines.push("The zone may not use DNSSEC, or the resolver may not return denial-of-existence records.");
805
+ return text(lines.join("\n"));
806
+ }
807
+ lines.push(`Denial of Existence: ${nsecResult.type}`);
808
+ lines.push("");
809
+ if (nsecResult.type === "NSEC") {
810
+ lines.push("Type: NSEC (plain)");
811
+ lines.push("");
812
+ lines.push("** INFORMATIONAL RISK: NSEC allows zone walking.");
813
+ lines.push(" An attacker can enumerate all record names in the zone by");
814
+ lines.push(" following NSEC next-name chains. This reveals the full zone contents.");
815
+ lines.push("");
816
+ lines.push(" Remediation: Consider migrating to NSEC3 to prevent zone enumeration.");
817
+ lines.push("");
818
+ lines.push(`Found ${nsecResult.records.length} NSEC record(s):`);
819
+ for (const record of nsecResult.records) {
820
+ lines.push(` Name: ${record.name}`);
821
+ const data = record.data;
822
+ if (data.nextDomain) {
823
+ lines.push(` Next Domain: ${data.nextDomain}`);
824
+ }
825
+ if (data.rrtypes) {
826
+ lines.push(` RR Types: ${data.rrtypes.join(", ")}`);
827
+ }
828
+ lines.push("");
829
+ }
830
+ }
831
+ else if (nsecResult.type === "NSEC3") {
832
+ lines.push("Type: NSEC3 (hashed)");
833
+ lines.push("Zone names are hashed, preventing direct zone walking.");
834
+ lines.push("");
835
+ if (nsecResult.nsec3params) {
836
+ lines.push("NSEC3 Parameters:");
837
+ lines.push(` Hash Algorithm: ${nsecResult.nsec3params.hashAlgorithm} (${nsecResult.nsec3params.hashAlgorithm === 1 ? "SHA-1" : "UNKNOWN"})`);
838
+ lines.push(` Iterations: ${nsecResult.nsec3params.iterations}`);
839
+ lines.push(` Salt: ${nsecResult.nsec3params.salt}`);
840
+ lines.push("");
841
+ // Check iteration count
842
+ if (nsecResult.nsec3params.iterations > 100) {
843
+ lines.push("** WARNING: High iteration count (>100).");
844
+ lines.push(" RFC 9276 recommends 0 iterations. High iterations increase CPU cost");
845
+ lines.push(" for resolvers without significantly improving security.");
846
+ }
847
+ else if (nsecResult.nsec3params.iterations > 0) {
848
+ lines.push("** NOTE: RFC 9276 recommends 0 iterations for best performance.");
849
+ }
850
+ else {
851
+ lines.push("Iterations: 0 (optimal per RFC 9276).");
852
+ }
853
+ lines.push("");
854
+ // Check salt
855
+ if (nsecResult.nsec3params.salt === "(empty)" || nsecResult.nsec3params.salt === "") {
856
+ lines.push("Salt: Empty (recommended per RFC 9276 for simplified key rollover).");
857
+ }
858
+ else {
859
+ lines.push("** NOTE: RFC 9276 recommends an empty salt for simplified key rollover.");
860
+ }
861
+ }
862
+ if (nsecResult.records.length > 0) {
863
+ lines.push("");
864
+ lines.push(`Found ${nsecResult.records.length} NSEC3 record(s).`);
865
+ }
866
+ }
867
+ return text(lines.join("\n"));
868
+ }
869
+ catch (err) {
870
+ return text(`Error checking NSEC/NSEC3 for ${domain}: ${err.message}`);
871
+ }
872
+ },
873
+ };
874
+ // ─── Tool 6: dnssec_algorithm_audit ───
875
+ const dnssecAlgorithmAudit = {
876
+ name: "dnssec_algorithm_audit",
877
+ description: "Inventory all DNSSEC algorithms used in DS, DNSKEY, and RRSIG records. Maps algorithm numbers to names. Flags deprecated algorithms (RSA/MD5, DSA, RSASHA1) and recommends ECDSAP256SHA256 or ED25519.",
878
+ schema: {
879
+ domain: z
880
+ .string()
881
+ .describe("The domain name to audit DNSSEC algorithms for (e.g. 'example.com')"),
882
+ },
883
+ async execute(args) {
884
+ const domain = args.domain;
885
+ try {
886
+ const audit = await performAlgorithmAudit(domain);
887
+ const lines = [
888
+ `DNSSEC Algorithm Audit: ${domain}`,
889
+ `${"=".repeat(60)}`,
890
+ "",
891
+ ];
892
+ if (audit.algorithms.length === 0) {
893
+ lines.push("No DNSSEC algorithms found. The domain may not use DNSSEC.");
894
+ return text(lines.join("\n"));
895
+ }
896
+ lines.push("Algorithm Inventory:");
897
+ lines.push("-".repeat(40));
898
+ for (const alg of audit.algorithms) {
899
+ lines.push(` Source: ${alg.source}`);
900
+ lines.push(` Algorithm: ${alg.algorithmName} (${alg.algorithmNumber})`);
901
+ lines.push(` Strength: ${alg.strength.toUpperCase()}`);
902
+ lines.push("");
903
+ }
904
+ // Unique algorithms summary
905
+ const unique = new Map();
906
+ for (const alg of audit.algorithms) {
907
+ unique.set(alg.algorithmNumber, alg.algorithmName);
908
+ }
909
+ lines.push("Unique Algorithms:");
910
+ lines.push("-".repeat(40));
911
+ for (const [num, name] of unique) {
912
+ const strength = DEPRECATED_ALGORITHMS.has(num)
913
+ ? "DEPRECATED"
914
+ : WEAK_ALGORITHMS.has(num)
915
+ ? "WEAK"
916
+ : STRONG_ALGORITHMS.has(num)
917
+ ? "STRONG"
918
+ : "ACCEPTABLE";
919
+ lines.push(` ${name} (${num}): ${strength}`);
920
+ }
921
+ lines.push("");
922
+ // Algorithm reference table
923
+ lines.push("Algorithm Reference:");
924
+ lines.push("-".repeat(40));
925
+ for (const [num, name] of Object.entries(ALGORITHM_NAMES)) {
926
+ const status = DEPRECATED_ALGORITHMS.has(Number(num))
927
+ ? "DEPRECATED"
928
+ : WEAK_ALGORITHMS.has(Number(num))
929
+ ? "WEAK"
930
+ : STRONG_ALGORITHMS.has(Number(num))
931
+ ? "STRONG"
932
+ : "ACCEPTABLE";
933
+ const inUse = unique.has(Number(num)) ? " [IN USE]" : "";
934
+ lines.push(` ${num}: ${name} - ${status}${inUse}`);
935
+ }
936
+ lines.push("");
937
+ // Findings
938
+ if (audit.findings.length > 0) {
939
+ lines.push("Findings:");
940
+ lines.push("-".repeat(40));
941
+ for (const finding of audit.findings) {
942
+ lines.push(` [${finding.severity.toUpperCase()}] ${finding.title}`);
943
+ lines.push(` ${finding.description}`);
944
+ if (finding.remediation) {
945
+ lines.push(` Remediation: ${finding.remediation}`);
946
+ }
947
+ lines.push("");
948
+ }
949
+ }
950
+ return text(lines.join("\n"));
951
+ }
952
+ catch (err) {
953
+ return text(`Error auditing algorithms for ${domain}: ${err.message}`);
954
+ }
955
+ },
956
+ };
957
+ // ─── Tool 7: dnssec_key_rollover ───
958
+ const dnssecKeyRollover = {
959
+ name: "dnssec_key_rollover",
960
+ description: "Check for DNSSEC key rollover. Detects multiple DNSKEYs indicating rollover in progress. Reports pre-publish vs double-signature rollover method. Flags if both old and new KSK present without matching DS.",
961
+ schema: {
962
+ domain: z
963
+ .string()
964
+ .describe("The domain name to check key rollover for (e.g. 'example.com')"),
965
+ },
966
+ async execute(args) {
967
+ const domain = args.domain;
968
+ try {
969
+ const result = await performKeyRolloverCheck(domain);
970
+ const lines = [
971
+ `DNSSEC Key Rollover Check: ${domain}`,
972
+ `${"=".repeat(60)}`,
973
+ "",
974
+ ];
975
+ lines.push(`Rollover Detected: ${result.rolloverDetected ? "YES" : "NO"}`);
976
+ if (result.rolloverDetected) {
977
+ lines.push(`Method: ${result.method}`);
978
+ }
979
+ lines.push("");
980
+ // DNSKEY listing
981
+ lines.push("DNSKEY Records:");
982
+ lines.push("-".repeat(40));
983
+ if (result.keys.length === 0) {
984
+ lines.push(" No DNSKEY records found.");
985
+ }
986
+ else {
987
+ for (const key of result.keys) {
988
+ lines.push(` ${key.keyType} (flags=${key.flags}): ${key.algorithmName} (${key.algorithm})`);
989
+ }
990
+ }
991
+ lines.push("");
992
+ // DS listing
993
+ lines.push("DS Records (at parent):");
994
+ lines.push("-".repeat(40));
995
+ if (result.dsRecords.length === 0) {
996
+ lines.push(" No DS records found.");
997
+ }
998
+ else {
999
+ for (const ds of result.dsRecords) {
1000
+ lines.push(` Key Tag ${ds.keyTag}: ${ds.algorithmName} (${ds.algorithm}), Digest: ${ds.digestTypeName} (${ds.digestType})`);
1001
+ }
1002
+ }
1003
+ lines.push("");
1004
+ // Findings
1005
+ if (result.findings.length > 0) {
1006
+ lines.push("Analysis:");
1007
+ lines.push("-".repeat(40));
1008
+ for (const finding of result.findings) {
1009
+ lines.push(` [${finding.severity.toUpperCase()}] ${finding.title}`);
1010
+ lines.push(` ${finding.description}`);
1011
+ if (finding.remediation) {
1012
+ lines.push(` Remediation: ${finding.remediation}`);
1013
+ }
1014
+ lines.push("");
1015
+ }
1016
+ }
1017
+ return text(lines.join("\n"));
1018
+ }
1019
+ catch (err) {
1020
+ return text(`Error checking key rollover for ${domain}: ${err.message}`);
1021
+ }
1022
+ },
1023
+ };
1024
+ // ─── Tool 8: dnssec_full_audit ───
1025
+ const dnssecFullAudit = {
1026
+ name: "dnssec_full_audit",
1027
+ description: "Comprehensive DNSSEC audit that runs all 7 DNSSEC checks: chain of trust validation, DS verification, DNSKEY analysis, RRSIG expiry check, NSEC/NSEC3 analysis, algorithm audit, and key rollover detection. Aggregates findings into a severity-scored report with remediation guidance.",
1028
+ schema: {
1029
+ domain: z
1030
+ .string()
1031
+ .describe("The domain name to perform a full DNSSEC audit on (e.g. 'example.com')"),
1032
+ },
1033
+ async execute(args) {
1034
+ const domain = args.domain;
1035
+ const allFindings = [];
1036
+ const sections = [];
1037
+ sections.push(`DNSSEC Full Audit Report: ${domain}`);
1038
+ sections.push(`${"=".repeat(60)}`);
1039
+ sections.push(`Audit Date: ${new Date().toISOString()}`);
1040
+ sections.push("");
1041
+ // ── 1. Chain of Trust Validation ──
1042
+ sections.push("1. Chain of Trust Validation");
1043
+ sections.push("-".repeat(40));
1044
+ try {
1045
+ const chainResult = await performChainOfTrustValidation(domain);
1046
+ sections.push(`Status: ${chainResult.status.toUpperCase()}`);
1047
+ if (chainResult.status === "not_signed") {
1048
+ allFindings.push({
1049
+ severity: "high",
1050
+ title: "Domain is not DNSSEC signed",
1051
+ description: "No DNSKEY records found. DNS responses are not cryptographically authenticated, making the domain vulnerable to DNS spoofing and cache poisoning.",
1052
+ remediation: "Enable DNSSEC signing for this domain at your DNS provider or registrar.",
1053
+ });
1054
+ sections.push("Domain is NOT signed with DNSSEC.");
1055
+ }
1056
+ else if (chainResult.status === "broken") {
1057
+ allFindings.push({
1058
+ severity: "critical",
1059
+ title: "DNSSEC chain of trust is broken",
1060
+ description: "The chain of trust from root to this domain is broken. DNSSEC-validating resolvers may reject responses for this domain.",
1061
+ remediation: "Investigate and fix the broken link in the DNSSEC chain. Ensure DS records at parent match DNSKEY at child.",
1062
+ });
1063
+ sections.push("Chain of trust is BROKEN.");
1064
+ }
1065
+ else {
1066
+ sections.push("Chain of trust is VALID.");
1067
+ }
1068
+ for (const level of chainResult.levels) {
1069
+ if (level.issues.length > 0) {
1070
+ sections.push(` Zone ${level.zone}: ${level.issues.join("; ")}`);
1071
+ }
1072
+ }
1073
+ }
1074
+ catch (err) {
1075
+ sections.push(` Error: ${err.message}`);
1076
+ allFindings.push({
1077
+ severity: "info",
1078
+ title: "Chain of trust validation failed",
1079
+ description: `Could not complete chain validation: ${err.message}`,
1080
+ });
1081
+ }
1082
+ sections.push("");
1083
+ // ── 2. DS Record Check ──
1084
+ sections.push("2. DS Record Check");
1085
+ sections.push("-".repeat(40));
1086
+ try {
1087
+ const dsRecords = await queryDsRecords(domain);
1088
+ if (dsRecords.length === 0) {
1089
+ sections.push("No DS records found at parent zone.");
1090
+ }
1091
+ else {
1092
+ sections.push(`Found ${dsRecords.length} DS record(s).`);
1093
+ for (const ds of dsRecords) {
1094
+ sections.push(` Key Tag ${ds.keyTag}: ${ds.algorithmName} (${ds.algorithm}), Digest: ${ds.digestTypeName} (${ds.digestType})`);
1095
+ if (WEAK_DIGEST_TYPES.has(ds.digestType)) {
1096
+ allFindings.push({
1097
+ severity: "medium",
1098
+ title: `Weak DS digest type: ${ds.digestTypeName}`,
1099
+ description: `DS record for key tag ${ds.keyTag} uses ${ds.digestTypeName} which is deprecated.`,
1100
+ remediation: "Add a DS record with SHA-256 (digest type 2) or SHA-384 (digest type 4).",
1101
+ });
1102
+ }
1103
+ if (DEPRECATED_ALGORITHMS.has(ds.algorithm)) {
1104
+ allFindings.push({
1105
+ severity: "critical",
1106
+ title: `Deprecated algorithm in DS: ${ds.algorithmName}`,
1107
+ description: `DS record for key tag ${ds.keyTag} uses deprecated algorithm ${ds.algorithmName} (${ds.algorithm}).`,
1108
+ remediation: "Generate new keys using ECDSAP256SHA256 (13) or ED25519 (15) and update DS at parent.",
1109
+ });
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ catch (err) {
1115
+ sections.push(` Error: ${err.message}`);
1116
+ }
1117
+ sections.push("");
1118
+ // ── 3. DNSKEY Check ──
1119
+ sections.push("3. DNSKEY Check");
1120
+ sections.push("-".repeat(40));
1121
+ try {
1122
+ const keys = await queryDnskeys(domain);
1123
+ if (keys.length === 0) {
1124
+ sections.push("No DNSKEY records found.");
1125
+ }
1126
+ else {
1127
+ const ksks = keys.filter((k) => k.keyType === "KSK");
1128
+ const zsks = keys.filter((k) => k.keyType === "ZSK");
1129
+ sections.push(`Found ${keys.length} DNSKEY(s): ${ksks.length} KSK, ${zsks.length} ZSK`);
1130
+ for (const key of keys) {
1131
+ sections.push(` ${key.keyType}: ${key.algorithmName} (${key.algorithm})`);
1132
+ if (DEPRECATED_ALGORITHMS.has(key.algorithm)) {
1133
+ allFindings.push({
1134
+ severity: "critical",
1135
+ title: `Deprecated DNSKEY algorithm: ${key.algorithmName} (${key.keyType})`,
1136
+ description: `${key.keyType} uses deprecated algorithm ${key.algorithmName}. This is insecure.`,
1137
+ remediation: "Replace with ECDSAP256SHA256 (13) or ED25519 (15).",
1138
+ });
1139
+ }
1140
+ else if (WEAK_ALGORITHMS.has(key.algorithm)) {
1141
+ allFindings.push({
1142
+ severity: "high",
1143
+ title: `Weak DNSKEY algorithm: ${key.algorithmName} (${key.keyType})`,
1144
+ description: `${key.keyType} uses weak algorithm ${key.algorithmName}.`,
1145
+ remediation: "Consider migrating to ECDSAP256SHA256 (13) or ED25519 (15).",
1146
+ });
1147
+ }
1148
+ }
1149
+ if (ksks.length === 0 && keys.length > 0) {
1150
+ allFindings.push({
1151
+ severity: "medium",
1152
+ title: "No KSK found",
1153
+ description: "DNSKEY records exist but no Key Signing Key (flag 257) was found. This is unusual and may indicate a configuration issue.",
1154
+ remediation: "Verify DNSSEC configuration includes a proper KSK.",
1155
+ });
1156
+ }
1157
+ }
1158
+ }
1159
+ catch (err) {
1160
+ sections.push(` Error: ${err.message}`);
1161
+ }
1162
+ sections.push("");
1163
+ // ── 4. RRSIG Check ──
1164
+ sections.push("4. RRSIG Expiry Check");
1165
+ sections.push("-".repeat(40));
1166
+ try {
1167
+ const rrsigs = await queryRrsigs(domain);
1168
+ if (rrsigs.length === 0) {
1169
+ sections.push("No RRSIG records found.");
1170
+ }
1171
+ else {
1172
+ sections.push(`Found ${rrsigs.length} RRSIG(s).`);
1173
+ const expired = rrsigs.filter((r) => r.expired);
1174
+ const expiringSoon = rrsigs.filter((r) => r.expiringWithin7Days);
1175
+ if (expired.length > 0) {
1176
+ sections.push(` CRITICAL: ${expired.length} expired signature(s)!`);
1177
+ for (const sig of expired) {
1178
+ sections.push(` - ${sig.typeCovered} (key tag ${sig.keyTag}): expired ${Math.abs(sig.daysUntilExpiry)} day(s) ago`);
1179
+ }
1180
+ allFindings.push({
1181
+ severity: "critical",
1182
+ title: `${expired.length} expired RRSIG signature(s)`,
1183
+ description: "Expired signatures will cause DNSSEC validation failures. DNSSEC-validating resolvers will return SERVFAIL.",
1184
+ remediation: "Immediately re-sign the zone. Check that automatic re-signing is configured and functioning.",
1185
+ });
1186
+ }
1187
+ if (expiringSoon.length > 0) {
1188
+ sections.push(` WARNING: ${expiringSoon.length} signature(s) expiring within 7 days.`);
1189
+ for (const sig of expiringSoon) {
1190
+ sections.push(` - ${sig.typeCovered} (key tag ${sig.keyTag}): ${sig.daysUntilExpiry} day(s) remaining`);
1191
+ }
1192
+ allFindings.push({
1193
+ severity: "high",
1194
+ title: `${expiringSoon.length} RRSIG signature(s) expiring within 7 days`,
1195
+ description: "Signatures approaching expiration. If not re-signed in time, DNSSEC validation will fail.",
1196
+ remediation: "Verify that automatic zone re-signing is scheduled and operational.",
1197
+ });
1198
+ }
1199
+ if (expired.length === 0 && expiringSoon.length === 0) {
1200
+ sections.push(" All signatures are valid and not expiring soon.");
1201
+ }
1202
+ }
1203
+ }
1204
+ catch (err) {
1205
+ sections.push(` Error: ${err.message}`);
1206
+ }
1207
+ sections.push("");
1208
+ // ── 5. NSEC/NSEC3 Check ──
1209
+ sections.push("5. NSEC/NSEC3 Analysis");
1210
+ sections.push("-".repeat(40));
1211
+ try {
1212
+ const nsecResult = await queryNsecRecords(domain);
1213
+ if (nsecResult.type === "NONE") {
1214
+ sections.push("No NSEC or NSEC3 records detected.");
1215
+ }
1216
+ else if (nsecResult.type === "NSEC") {
1217
+ sections.push("Uses: NSEC (plain)");
1218
+ allFindings.push({
1219
+ severity: "low",
1220
+ title: "Zone uses NSEC (allows zone walking)",
1221
+ description: "NSEC records allow an attacker to enumerate all names in the zone by following the chain of next-domain-name fields.",
1222
+ remediation: "Consider migrating to NSEC3 to prevent zone enumeration. Note: NSEC3 has its own trade-offs (CPU cost, complexity).",
1223
+ });
1224
+ }
1225
+ else if (nsecResult.type === "NSEC3") {
1226
+ sections.push("Uses: NSEC3 (hashed names)");
1227
+ if (nsecResult.nsec3params) {
1228
+ sections.push(` Hash Algorithm: ${nsecResult.nsec3params.hashAlgorithm}`);
1229
+ sections.push(` Iterations: ${nsecResult.nsec3params.iterations}`);
1230
+ sections.push(` Salt: ${nsecResult.nsec3params.salt}`);
1231
+ if (nsecResult.nsec3params.iterations > 100) {
1232
+ allFindings.push({
1233
+ severity: "medium",
1234
+ title: "High NSEC3 iteration count",
1235
+ description: `NSEC3 iterations set to ${nsecResult.nsec3params.iterations}. RFC 9276 recommends 0 iterations.`,
1236
+ remediation: "Reduce NSEC3 iterations to 0 per RFC 9276 for optimal performance.",
1237
+ });
1238
+ }
1239
+ else if (nsecResult.nsec3params.iterations > 0) {
1240
+ allFindings.push({
1241
+ severity: "low",
1242
+ title: "Non-zero NSEC3 iterations",
1243
+ description: `NSEC3 iterations set to ${nsecResult.nsec3params.iterations}. RFC 9276 recommends 0.`,
1244
+ remediation: "Consider reducing iterations to 0 per RFC 9276.",
1245
+ });
1246
+ }
1247
+ }
1248
+ }
1249
+ }
1250
+ catch (err) {
1251
+ sections.push(` Error: ${err.message}`);
1252
+ }
1253
+ sections.push("");
1254
+ // ── 6. Algorithm Audit ──
1255
+ sections.push("6. Algorithm Audit");
1256
+ sections.push("-".repeat(40));
1257
+ try {
1258
+ const audit = await performAlgorithmAudit(domain);
1259
+ if (audit.algorithms.length === 0) {
1260
+ sections.push("No DNSSEC algorithms found.");
1261
+ }
1262
+ else {
1263
+ const unique = new Map();
1264
+ for (const alg of audit.algorithms) {
1265
+ unique.set(alg.algorithmNumber, alg.algorithmName);
1266
+ }
1267
+ for (const [num, name] of unique) {
1268
+ const strength = DEPRECATED_ALGORITHMS.has(num)
1269
+ ? "DEPRECATED"
1270
+ : WEAK_ALGORITHMS.has(num)
1271
+ ? "WEAK"
1272
+ : STRONG_ALGORITHMS.has(num)
1273
+ ? "STRONG"
1274
+ : "ACCEPTABLE";
1275
+ sections.push(` ${name} (${num}): ${strength}`);
1276
+ }
1277
+ // Add algorithm findings (deduplicate with existing findings by title)
1278
+ const existingTitles = new Set(allFindings.map((f) => f.title));
1279
+ for (const finding of audit.findings) {
1280
+ if (!existingTitles.has(finding.title)) {
1281
+ allFindings.push(finding);
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+ catch (err) {
1287
+ sections.push(` Error: ${err.message}`);
1288
+ }
1289
+ sections.push("");
1290
+ // ── 7. Key Rollover Detection ──
1291
+ sections.push("7. Key Rollover Detection");
1292
+ sections.push("-".repeat(40));
1293
+ try {
1294
+ const rollover = await performKeyRolloverCheck(domain);
1295
+ if (rollover.rolloverDetected) {
1296
+ sections.push(`Rollover detected: ${rollover.method}`);
1297
+ }
1298
+ else {
1299
+ sections.push("No key rollover in progress.");
1300
+ }
1301
+ // Add rollover findings (deduplicate)
1302
+ const existingTitles = new Set(allFindings.map((f) => f.title));
1303
+ for (const finding of rollover.findings) {
1304
+ if (!existingTitles.has(finding.title)) {
1305
+ allFindings.push(finding);
1306
+ }
1307
+ }
1308
+ }
1309
+ catch (err) {
1310
+ sections.push(` Error: ${err.message}`);
1311
+ }
1312
+ sections.push("");
1313
+ // ── Aggregated Findings Report ──
1314
+ sections.push("=".repeat(60));
1315
+ sections.push("FINDINGS SUMMARY");
1316
+ sections.push("=".repeat(60));
1317
+ sections.push("");
1318
+ // Sort findings by severity
1319
+ allFindings.sort((a, b) => severityScore(b.severity) - severityScore(a.severity));
1320
+ const countBySeverity = {
1321
+ critical: 0,
1322
+ high: 0,
1323
+ medium: 0,
1324
+ low: 0,
1325
+ info: 0,
1326
+ };
1327
+ for (const finding of allFindings) {
1328
+ countBySeverity[finding.severity]++;
1329
+ }
1330
+ sections.push(`Total Findings: ${allFindings.length}`);
1331
+ sections.push(` Critical: ${countBySeverity.critical}`);
1332
+ sections.push(` High: ${countBySeverity.high}`);
1333
+ sections.push(` Medium: ${countBySeverity.medium}`);
1334
+ sections.push(` Low: ${countBySeverity.low}`);
1335
+ sections.push(` Info: ${countBySeverity.info}`);
1336
+ sections.push("");
1337
+ // Overall score (0-100, higher is better)
1338
+ const maxPenalty = allFindings.length * 5; // worst case all critical
1339
+ const actualPenalty = allFindings.reduce((sum, f) => sum + severityScore(f.severity), 0);
1340
+ const score = maxPenalty > 0 ? Math.max(0, Math.round(100 - (actualPenalty / maxPenalty) * 100)) : 100;
1341
+ // Adjust score: if not signed, cap at 20
1342
+ const isNotSigned = allFindings.some((f) => f.title.includes("not DNSSEC signed"));
1343
+ const finalScore = isNotSigned ? Math.min(score, 20) : score;
1344
+ sections.push(`DNSSEC Security Score: ${finalScore}/100`);
1345
+ sections.push("");
1346
+ // Detailed findings
1347
+ if (allFindings.length > 0) {
1348
+ sections.push("Detailed Findings:");
1349
+ sections.push("-".repeat(40));
1350
+ for (let i = 0; i < allFindings.length; i++) {
1351
+ const f = allFindings[i];
1352
+ sections.push(`${i + 1}. [${f.severity.toUpperCase()}] ${f.title}`);
1353
+ sections.push(` ${f.description}`);
1354
+ if (f.remediation) {
1355
+ sections.push(` Remediation: ${f.remediation}`);
1356
+ }
1357
+ sections.push("");
1358
+ }
1359
+ }
1360
+ else {
1361
+ sections.push("No issues found. DNSSEC configuration appears healthy.");
1362
+ }
1363
+ return text(sections.join("\n"));
1364
+ },
1365
+ };
1366
+ // ─── Export ───
1367
+ export const dnssecTools = [
1368
+ dnssecValidate,
1369
+ dnssecCheckDs,
1370
+ dnssecCheckDnskey,
1371
+ dnssecCheckRrsig,
1372
+ dnssecCheckNsec,
1373
+ dnssecAlgorithmAudit,
1374
+ dnssecKeyRollover,
1375
+ dnssecFullAudit,
1376
+ ];
1377
+ //# sourceMappingURL=index.js.map