defense-mcp-server 0.6.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 (186) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/build/core/auto-installer.d.ts +102 -0
  5. package/build/core/auto-installer.d.ts.map +1 -0
  6. package/build/core/auto-installer.js +833 -0
  7. package/build/core/backup-manager.d.ts +63 -0
  8. package/build/core/backup-manager.d.ts.map +1 -0
  9. package/build/core/backup-manager.js +189 -0
  10. package/build/core/changelog.d.ts +75 -0
  11. package/build/core/changelog.d.ts.map +1 -0
  12. package/build/core/changelog.js +123 -0
  13. package/build/core/command-allowlist.d.ts +129 -0
  14. package/build/core/command-allowlist.d.ts.map +1 -0
  15. package/build/core/command-allowlist.js +849 -0
  16. package/build/core/config.d.ts +79 -0
  17. package/build/core/config.d.ts.map +1 -0
  18. package/build/core/config.js +193 -0
  19. package/build/core/dependency-validator.d.ts +106 -0
  20. package/build/core/dependency-validator.d.ts.map +1 -0
  21. package/build/core/dependency-validator.js +405 -0
  22. package/build/core/distro-adapter.d.ts +177 -0
  23. package/build/core/distro-adapter.d.ts.map +1 -0
  24. package/build/core/distro-adapter.js +481 -0
  25. package/build/core/distro.d.ts +68 -0
  26. package/build/core/distro.d.ts.map +1 -0
  27. package/build/core/distro.js +457 -0
  28. package/build/core/encrypted-state.d.ts +76 -0
  29. package/build/core/encrypted-state.d.ts.map +1 -0
  30. package/build/core/encrypted-state.js +209 -0
  31. package/build/core/executor.d.ts +56 -0
  32. package/build/core/executor.d.ts.map +1 -0
  33. package/build/core/executor.js +350 -0
  34. package/build/core/installer.d.ts +92 -0
  35. package/build/core/installer.d.ts.map +1 -0
  36. package/build/core/installer.js +1072 -0
  37. package/build/core/logger.d.ts +102 -0
  38. package/build/core/logger.d.ts.map +1 -0
  39. package/build/core/logger.js +132 -0
  40. package/build/core/parsers.d.ts +151 -0
  41. package/build/core/parsers.d.ts.map +1 -0
  42. package/build/core/parsers.js +479 -0
  43. package/build/core/policy-engine.d.ts +170 -0
  44. package/build/core/policy-engine.d.ts.map +1 -0
  45. package/build/core/policy-engine.js +656 -0
  46. package/build/core/preflight.d.ts +157 -0
  47. package/build/core/preflight.d.ts.map +1 -0
  48. package/build/core/preflight.js +638 -0
  49. package/build/core/privilege-manager.d.ts +108 -0
  50. package/build/core/privilege-manager.d.ts.map +1 -0
  51. package/build/core/privilege-manager.js +363 -0
  52. package/build/core/rate-limiter.d.ts +67 -0
  53. package/build/core/rate-limiter.d.ts.map +1 -0
  54. package/build/core/rate-limiter.js +129 -0
  55. package/build/core/rollback.d.ts +73 -0
  56. package/build/core/rollback.d.ts.map +1 -0
  57. package/build/core/rollback.js +278 -0
  58. package/build/core/safeguards.d.ts +58 -0
  59. package/build/core/safeguards.d.ts.map +1 -0
  60. package/build/core/safeguards.js +448 -0
  61. package/build/core/sanitizer.d.ts +118 -0
  62. package/build/core/sanitizer.d.ts.map +1 -0
  63. package/build/core/sanitizer.js +459 -0
  64. package/build/core/secure-fs.d.ts +67 -0
  65. package/build/core/secure-fs.d.ts.map +1 -0
  66. package/build/core/secure-fs.js +143 -0
  67. package/build/core/spawn-safe.d.ts +55 -0
  68. package/build/core/spawn-safe.d.ts.map +1 -0
  69. package/build/core/spawn-safe.js +146 -0
  70. package/build/core/sudo-guard.d.ts +145 -0
  71. package/build/core/sudo-guard.d.ts.map +1 -0
  72. package/build/core/sudo-guard.js +349 -0
  73. package/build/core/sudo-session.d.ts +100 -0
  74. package/build/core/sudo-session.d.ts.map +1 -0
  75. package/build/core/sudo-session.js +319 -0
  76. package/build/core/tool-dependencies.d.ts +61 -0
  77. package/build/core/tool-dependencies.d.ts.map +1 -0
  78. package/build/core/tool-dependencies.js +571 -0
  79. package/build/core/tool-registry.d.ts +111 -0
  80. package/build/core/tool-registry.d.ts.map +1 -0
  81. package/build/core/tool-registry.js +656 -0
  82. package/build/core/tool-wrapper.d.ts +73 -0
  83. package/build/core/tool-wrapper.d.ts.map +1 -0
  84. package/build/core/tool-wrapper.js +296 -0
  85. package/build/index.d.ts +3 -0
  86. package/build/index.d.ts.map +1 -0
  87. package/build/index.js +247 -0
  88. package/build/tools/access-control.d.ts +9 -0
  89. package/build/tools/access-control.d.ts.map +1 -0
  90. package/build/tools/access-control.js +1818 -0
  91. package/build/tools/api-security.d.ts +12 -0
  92. package/build/tools/api-security.d.ts.map +1 -0
  93. package/build/tools/api-security.js +901 -0
  94. package/build/tools/app-hardening.d.ts +11 -0
  95. package/build/tools/app-hardening.d.ts.map +1 -0
  96. package/build/tools/app-hardening.js +768 -0
  97. package/build/tools/backup.d.ts +8 -0
  98. package/build/tools/backup.d.ts.map +1 -0
  99. package/build/tools/backup.js +381 -0
  100. package/build/tools/cloud-security.d.ts +17 -0
  101. package/build/tools/cloud-security.d.ts.map +1 -0
  102. package/build/tools/cloud-security.js +739 -0
  103. package/build/tools/compliance.d.ts +10 -0
  104. package/build/tools/compliance.d.ts.map +1 -0
  105. package/build/tools/compliance.js +1225 -0
  106. package/build/tools/container-security.d.ts +14 -0
  107. package/build/tools/container-security.d.ts.map +1 -0
  108. package/build/tools/container-security.js +788 -0
  109. package/build/tools/deception.d.ts +13 -0
  110. package/build/tools/deception.d.ts.map +1 -0
  111. package/build/tools/deception.js +763 -0
  112. package/build/tools/dns-security.d.ts +93 -0
  113. package/build/tools/dns-security.d.ts.map +1 -0
  114. package/build/tools/dns-security.js +745 -0
  115. package/build/tools/drift-detection.d.ts +8 -0
  116. package/build/tools/drift-detection.d.ts.map +1 -0
  117. package/build/tools/drift-detection.js +326 -0
  118. package/build/tools/ebpf-security.d.ts +15 -0
  119. package/build/tools/ebpf-security.d.ts.map +1 -0
  120. package/build/tools/ebpf-security.js +294 -0
  121. package/build/tools/encryption.d.ts +9 -0
  122. package/build/tools/encryption.d.ts.map +1 -0
  123. package/build/tools/encryption.js +1667 -0
  124. package/build/tools/firewall.d.ts +9 -0
  125. package/build/tools/firewall.d.ts.map +1 -0
  126. package/build/tools/firewall.js +1398 -0
  127. package/build/tools/hardening.d.ts +10 -0
  128. package/build/tools/hardening.d.ts.map +1 -0
  129. package/build/tools/hardening.js +2654 -0
  130. package/build/tools/ids.d.ts +9 -0
  131. package/build/tools/ids.d.ts.map +1 -0
  132. package/build/tools/ids.js +624 -0
  133. package/build/tools/incident-response.d.ts +10 -0
  134. package/build/tools/incident-response.d.ts.map +1 -0
  135. package/build/tools/incident-response.js +1180 -0
  136. package/build/tools/logging.d.ts +12 -0
  137. package/build/tools/logging.d.ts.map +1 -0
  138. package/build/tools/logging.js +454 -0
  139. package/build/tools/malware.d.ts +10 -0
  140. package/build/tools/malware.d.ts.map +1 -0
  141. package/build/tools/malware.js +532 -0
  142. package/build/tools/meta.d.ts +11 -0
  143. package/build/tools/meta.d.ts.map +1 -0
  144. package/build/tools/meta.js +2278 -0
  145. package/build/tools/network-defense.d.ts +12 -0
  146. package/build/tools/network-defense.d.ts.map +1 -0
  147. package/build/tools/network-defense.js +760 -0
  148. package/build/tools/patch-management.d.ts +3 -0
  149. package/build/tools/patch-management.d.ts.map +1 -0
  150. package/build/tools/patch-management.js +708 -0
  151. package/build/tools/process-security.d.ts +12 -0
  152. package/build/tools/process-security.d.ts.map +1 -0
  153. package/build/tools/process-security.js +784 -0
  154. package/build/tools/reporting.d.ts +11 -0
  155. package/build/tools/reporting.d.ts.map +1 -0
  156. package/build/tools/reporting.js +559 -0
  157. package/build/tools/secrets.d.ts +9 -0
  158. package/build/tools/secrets.d.ts.map +1 -0
  159. package/build/tools/secrets.js +596 -0
  160. package/build/tools/siem-integration.d.ts +18 -0
  161. package/build/tools/siem-integration.d.ts.map +1 -0
  162. package/build/tools/siem-integration.js +754 -0
  163. package/build/tools/sudo-management.d.ts +18 -0
  164. package/build/tools/sudo-management.d.ts.map +1 -0
  165. package/build/tools/sudo-management.js +737 -0
  166. package/build/tools/supply-chain-security.d.ts +8 -0
  167. package/build/tools/supply-chain-security.d.ts.map +1 -0
  168. package/build/tools/supply-chain-security.js +256 -0
  169. package/build/tools/threat-intel.d.ts +22 -0
  170. package/build/tools/threat-intel.d.ts.map +1 -0
  171. package/build/tools/threat-intel.js +749 -0
  172. package/build/tools/vulnerability-management.d.ts +11 -0
  173. package/build/tools/vulnerability-management.d.ts.map +1 -0
  174. package/build/tools/vulnerability-management.js +667 -0
  175. package/build/tools/waf.d.ts +12 -0
  176. package/build/tools/waf.d.ts.map +1 -0
  177. package/build/tools/waf.js +843 -0
  178. package/build/tools/wireless-security.d.ts +19 -0
  179. package/build/tools/wireless-security.d.ts.map +1 -0
  180. package/build/tools/wireless-security.js +826 -0
  181. package/build/tools/zero-trust-network.d.ts +8 -0
  182. package/build/tools/zero-trust-network.d.ts.map +1 -0
  183. package/build/tools/zero-trust-network.js +367 -0
  184. package/docs/SAFEGUARDS.md +518 -0
  185. package/docs/TOOLS-REFERENCE.md +665 -0
  186. package/package.json +87 -0
@@ -0,0 +1,745 @@
1
+ /**
2
+ * DNS security tools for Kali Defense MCP Server.
3
+ *
4
+ * Registers 1 tool: dns_security (actions: audit_resolv, check_dnssec,
5
+ * detect_tunneling, block_domains, query_log_audit)
6
+ *
7
+ * Provides DNS configuration auditing, DNSSEC validation checking,
8
+ * DNS tunneling detection, domain blocking, and query log analysis.
9
+ */
10
+ import { z } from "zod";
11
+ import { spawnSafe } from "../core/spawn-safe.js";
12
+ import { secureWriteFileSync, secureCopyFileSync } from "../core/secure-fs.js";
13
+ import { createErrorContent, formatToolOutput, } from "../core/parsers.js";
14
+ import { validateInterface } from "../core/sanitizer.js";
15
+ // ── Constants ──────────────────────────────────────────────────────────────────
16
+ /** Well-known public DNS resolvers */
17
+ const PUBLIC_RESOLVERS = {
18
+ "8.8.8.8": "Google Public DNS",
19
+ "8.8.4.4": "Google Public DNS (secondary)",
20
+ "1.1.1.1": "Cloudflare DNS",
21
+ "1.0.0.1": "Cloudflare DNS (secondary)",
22
+ "9.9.9.9": "Quad9 DNS",
23
+ "149.112.112.112": "Quad9 DNS (secondary)",
24
+ "208.67.222.222": "OpenDNS",
25
+ "208.67.220.220": "OpenDNS (secondary)",
26
+ };
27
+ /** Suspicious TLDs commonly associated with malware/phishing */
28
+ const SUSPICIOUS_TLDS = new Set([
29
+ ".top", ".xyz", ".buzz", ".club", ".work", ".date", ".loan",
30
+ ".click", ".gdn", ".racing", ".win", ".bid", ".stream", ".download",
31
+ ".review", ".accountant", ".science", ".party", ".trade",
32
+ ]);
33
+ /** Maximum capture duration in seconds */
34
+ const MAX_CAPTURE_DURATION = 120;
35
+ /** Default entropy threshold for tunneling detection */
36
+ const DEFAULT_ENTROPY_THRESHOLD = 3.5;
37
+ /**
38
+ * Run a command via spawnSafe and collect output as a promise.
39
+ * Handles errors gracefully — returns error info instead of throwing.
40
+ */
41
+ async function runCommand(command, args, timeoutMs = 30_000) {
42
+ return new Promise((resolve) => {
43
+ let child;
44
+ try {
45
+ child = spawnSafe(command, args);
46
+ }
47
+ catch (err) {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ resolve({ stdout: "", stderr: msg, exitCode: -1 });
50
+ return;
51
+ }
52
+ let stdout = "";
53
+ let stderr = "";
54
+ let resolved = false;
55
+ const timer = setTimeout(() => {
56
+ if (!resolved) {
57
+ resolved = true;
58
+ child.kill("SIGTERM");
59
+ resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
60
+ }
61
+ }, timeoutMs);
62
+ child.stdout?.on("data", (data) => {
63
+ stdout += data.toString();
64
+ });
65
+ child.stderr?.on("data", (data) => {
66
+ stderr += data.toString();
67
+ });
68
+ child.on("close", (code) => {
69
+ if (!resolved) {
70
+ resolved = true;
71
+ clearTimeout(timer);
72
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
73
+ }
74
+ });
75
+ child.on("error", (err) => {
76
+ if (!resolved) {
77
+ resolved = true;
78
+ clearTimeout(timer);
79
+ resolve({ stdout, stderr: err.message, exitCode: -1 });
80
+ }
81
+ });
82
+ });
83
+ }
84
+ /**
85
+ * Run a command via sudo through spawnSafe.
86
+ */
87
+ async function runSudoCommand(command, args, timeoutMs = 30_000) {
88
+ return runCommand("sudo", [command, ...args], timeoutMs);
89
+ }
90
+ // ── Shannon Entropy ────────────────────────────────────────────────────────────
91
+ /**
92
+ * Calculate Shannon entropy of a string.
93
+ * Pure function — no external dependencies.
94
+ *
95
+ * Higher entropy values indicate more randomness, which is characteristic
96
+ * of DNS tunneling and DGA (Domain Generation Algorithm) domains.
97
+ *
98
+ * @param str - Input string to calculate entropy for
99
+ * @returns Entropy value in bits per character
100
+ */
101
+ export function calculateShannonEntropy(str) {
102
+ if (!str || str.length === 0)
103
+ return 0;
104
+ const freq = {};
105
+ for (const ch of str) {
106
+ freq[ch] = (freq[ch] ?? 0) + 1;
107
+ }
108
+ let entropy = 0;
109
+ const len = str.length;
110
+ for (const count of Object.values(freq)) {
111
+ const p = count / len;
112
+ if (p > 0) {
113
+ entropy -= p * Math.log2(p);
114
+ }
115
+ }
116
+ return entropy;
117
+ }
118
+ /**
119
+ * Parse dig +dnssec output and determine DNSSEC status.
120
+ */
121
+ export function parseDnssecOutput(domain, digOutput) {
122
+ const result = {
123
+ domain,
124
+ dnssecEnabled: false,
125
+ hasRRSIG: false,
126
+ hasDNSKEY: false,
127
+ hasDS: false,
128
+ adFlag: false,
129
+ chainValid: false,
130
+ records: [],
131
+ issues: [],
132
+ };
133
+ const lines = digOutput.split("\n");
134
+ for (const line of lines) {
135
+ const trimmed = line.trim();
136
+ // Check for AD (Authenticated Data) flag in header
137
+ if (trimmed.includes("flags:") && trimmed.includes("ad")) {
138
+ result.adFlag = true;
139
+ }
140
+ // Check for RRSIG records
141
+ if (trimmed.includes("RRSIG")) {
142
+ result.hasRRSIG = true;
143
+ result.records.push(trimmed);
144
+ }
145
+ // Check for DNSKEY records
146
+ if (trimmed.includes("DNSKEY")) {
147
+ result.hasDNSKEY = true;
148
+ result.records.push(trimmed);
149
+ }
150
+ // Check for DS records
151
+ if (trimmed.includes("\tDS\t") || trimmed.includes(" DS ")) {
152
+ result.hasDS = true;
153
+ result.records.push(trimmed);
154
+ }
155
+ // Check for SERVFAIL (may indicate DNSSEC validation failure)
156
+ if (trimmed.includes("SERVFAIL")) {
157
+ result.issues.push("SERVFAIL response — DNSSEC validation may have failed");
158
+ }
159
+ }
160
+ // Determine DNSSEC status
161
+ result.dnssecEnabled = result.hasRRSIG || result.hasDNSKEY;
162
+ result.chainValid = result.adFlag && result.hasRRSIG;
163
+ // Add issues if DNSSEC is incomplete
164
+ if (!result.hasRRSIG) {
165
+ result.issues.push("No RRSIG records found — domain may not be DNSSEC-signed");
166
+ }
167
+ if (!result.adFlag && result.hasRRSIG) {
168
+ result.issues.push("RRSIG present but AD flag not set — chain of trust may be broken");
169
+ }
170
+ return result;
171
+ }
172
+ /**
173
+ * Analyze captured DNS queries for tunneling indicators.
174
+ */
175
+ export function analyzeDnsQueries(capturedOutput, entropyThreshold) {
176
+ const lines = capturedOutput.split("\n").filter((l) => l.trim().length > 0);
177
+ const suspicious = [];
178
+ const domainCounts = {};
179
+ let txtQueries = 0;
180
+ let nullQueries = 0;
181
+ let totalQueries = 0;
182
+ // Pattern to extract domain from tcpdump DNS output
183
+ // e.g., "12:00:00.000000 IP 192.168.1.1.12345 > 8.8.8.8.53: 12345+ A? example.com. (30)"
184
+ const dnsQueryRe = /\s(?:A\?|AAAA\?|TXT\?|NULL\?|MX\?|CNAME\?|ANY\?)\s+(\S+?)\.?\s/;
185
+ const txtRe = /\sTXT\?\s/;
186
+ const nullRe = /\sNULL\?\s/;
187
+ for (const line of lines) {
188
+ const match = dnsQueryRe.exec(line);
189
+ if (!match)
190
+ continue;
191
+ totalQueries++;
192
+ const domain = match[1].replace(/\.$/, "");
193
+ // Count domain frequencies
194
+ const baseDomain = domain.split(".").slice(-2).join(".");
195
+ domainCounts[baseDomain] = (domainCounts[baseDomain] ?? 0) + 1;
196
+ // Count TXT/NULL queries
197
+ if (txtRe.test(line))
198
+ txtQueries++;
199
+ if (nullRe.test(line))
200
+ nullQueries++;
201
+ // Analyze subdomain labels for tunneling indicators
202
+ const labels = domain.split(".");
203
+ const subdomain = labels.slice(0, -2).join(".");
204
+ if (subdomain.length > 0) {
205
+ const entropy = calculateShannonEntropy(subdomain);
206
+ const longestLabel = Math.max(...labels.map((l) => l.length));
207
+ const reasons = [];
208
+ if (entropy > entropyThreshold) {
209
+ reasons.push(`high entropy (${entropy.toFixed(2)})`);
210
+ }
211
+ if (longestLabel > 50) {
212
+ reasons.push(`long label (${longestLabel} chars)`);
213
+ }
214
+ if (subdomain.length > 100) {
215
+ reasons.push(`very long subdomain (${subdomain.length} chars)`);
216
+ }
217
+ if (reasons.length > 0) {
218
+ suspicious.push({
219
+ domain,
220
+ entropy,
221
+ labelLength: longestLabel,
222
+ reason: reasons.join(", "),
223
+ });
224
+ }
225
+ }
226
+ }
227
+ return { suspicious, totalQueries, txtQueries, nullQueries, domainCounts };
228
+ }
229
+ /**
230
+ * Audit resolv.conf content.
231
+ */
232
+ export function auditResolvConf(resolvContent, resolvedStatus) {
233
+ const result = {
234
+ nameservers: [],
235
+ searchDomains: [],
236
+ options: [],
237
+ findings: [],
238
+ recommendations: [],
239
+ };
240
+ // Parse resolv.conf
241
+ const lines = resolvContent.split("\n");
242
+ for (const line of lines) {
243
+ const trimmed = line.trim();
244
+ if (trimmed.startsWith("#") || trimmed.length === 0)
245
+ continue;
246
+ if (trimmed.startsWith("nameserver ")) {
247
+ const ip = trimmed.replace("nameserver ", "").trim();
248
+ const provider = PUBLIC_RESOLVERS[ip];
249
+ let type = "internal";
250
+ if (provider)
251
+ type = "public";
252
+ else if (ip === "127.0.0.1" || ip === "::1" || ip.startsWith("127.0.0."))
253
+ type = "loopback";
254
+ result.nameservers.push({ ip, type, provider });
255
+ }
256
+ else if (trimmed.startsWith("search ")) {
257
+ result.searchDomains = trimmed.replace("search ", "").trim().split(/\s+/);
258
+ }
259
+ else if (trimmed.startsWith("options ")) {
260
+ result.options = trimmed.replace("options ", "").trim().split(/\s+/);
261
+ }
262
+ }
263
+ // Check number of nameservers
264
+ if (result.nameservers.length === 0) {
265
+ result.findings.push({ check: "nameserver_count", status: "FAIL", detail: "No nameservers configured" });
266
+ result.recommendations.push("Configure at least 2 DNS nameservers for redundancy");
267
+ }
268
+ else if (result.nameservers.length === 1) {
269
+ result.findings.push({ check: "nameserver_count", status: "WARN", detail: `Only ${result.nameservers.length} nameserver configured` });
270
+ result.recommendations.push("Add a secondary nameserver for redundancy");
271
+ }
272
+ else {
273
+ result.findings.push({ check: "nameserver_count", status: "PASS", detail: `${result.nameservers.length} nameservers configured` });
274
+ }
275
+ // Check for public vs internal resolvers
276
+ const publicNs = result.nameservers.filter((ns) => ns.type === "public");
277
+ const internalNs = result.nameservers.filter((ns) => ns.type === "internal");
278
+ if (publicNs.length > 0 && internalNs.length === 0) {
279
+ result.findings.push({ check: "resolver_type", status: "INFO", detail: `Using public resolvers: ${publicNs.map((n) => n.provider ?? n.ip).join(", ")}` });
280
+ }
281
+ else if (internalNs.length > 0) {
282
+ result.findings.push({ check: "resolver_type", status: "INFO", detail: `Using internal resolvers: ${internalNs.map((n) => n.ip).join(", ")}` });
283
+ }
284
+ // Check systemd-resolved for DoT/DNSSEC
285
+ const hasDot = resolvedStatus.includes("DNSOverTLS") && !resolvedStatus.includes("DNSOverTLS: no");
286
+ const hasDnssec = resolvedStatus.includes("DNSSEC") && !resolvedStatus.includes("DNSSEC: no") && !resolvedStatus.includes("DNSSEC setting: no");
287
+ result.findings.push({
288
+ check: "dns_over_tls",
289
+ status: hasDot ? "PASS" : "FAIL",
290
+ detail: hasDot ? "DNS over TLS is enabled" : "DNS over TLS is not enabled",
291
+ });
292
+ if (!hasDot) {
293
+ result.recommendations.push("Enable DNS over TLS in systemd-resolved for encrypted DNS queries");
294
+ }
295
+ result.findings.push({
296
+ check: "dnssec_validation",
297
+ status: hasDnssec ? "PASS" : "FAIL",
298
+ detail: hasDnssec ? "DNSSEC validation is enabled" : "DNSSEC validation is not enabled",
299
+ });
300
+ if (!hasDnssec) {
301
+ result.recommendations.push("Enable DNSSEC validation in systemd-resolved to prevent DNS spoofing");
302
+ }
303
+ return result;
304
+ }
305
+ /**
306
+ * Analyze DNS query log content.
307
+ */
308
+ export function analyzeQueryLog(logContent) {
309
+ const lines = logContent.split("\n").filter((l) => l.trim().length > 0);
310
+ const domainCounts = {};
311
+ const suspiciousTldQueries = [];
312
+ const queryTimeline = {};
313
+ let nxdomainCount = 0;
314
+ let totalEntries = 0;
315
+ // Patterns for different DNS log formats
316
+ // dnsmasq: "Jan 01 10:00:00 host dnsmasq[1234]: query[A] example.com from 192.168.1.1"
317
+ // systemd-resolved: "... lookup example.com ..."
318
+ const dnsmasqRe = /query\[(?:A|AAAA|TXT|MX|CNAME|ANY)\]\s+(\S+)/;
319
+ const resolvedRe = /(?:resolved|query)\S*\s+(\S+\.(?:[a-z]{2,}))/i;
320
+ const nxdomainRe = /NXDOMAIN|nxdomain|status: NXDOMAIN/i;
321
+ const hourRe = /\b(\d{2}):\d{2}:\d{2}\b/;
322
+ for (const line of lines) {
323
+ totalEntries++;
324
+ // Extract domain
325
+ let domain = null;
326
+ const dnsmasqMatch = dnsmasqRe.exec(line);
327
+ if (dnsmasqMatch) {
328
+ domain = dnsmasqMatch[1];
329
+ }
330
+ else {
331
+ const resolvedMatch = resolvedRe.exec(line);
332
+ if (resolvedMatch) {
333
+ domain = resolvedMatch[1];
334
+ }
335
+ }
336
+ if (domain) {
337
+ domain = domain.replace(/\.$/, "").toLowerCase();
338
+ domainCounts[domain] = (domainCounts[domain] ?? 0) + 1;
339
+ // Check for suspicious TLDs
340
+ for (const tld of SUSPICIOUS_TLDS) {
341
+ if (domain.endsWith(tld)) {
342
+ suspiciousTldQueries.push({ domain, tld });
343
+ break;
344
+ }
345
+ }
346
+ }
347
+ // Count NXDOMAIN responses
348
+ if (nxdomainRe.test(line)) {
349
+ nxdomainCount++;
350
+ }
351
+ // Build timeline by hour
352
+ const hourMatch = hourRe.exec(line);
353
+ if (hourMatch) {
354
+ const hour = `${hourMatch[1]}:00`;
355
+ queryTimeline[hour] = (queryTimeline[hour] ?? 0) + 1;
356
+ }
357
+ }
358
+ // Sort top domains
359
+ const topDomains = Object.entries(domainCounts)
360
+ .sort(([, a], [, b]) => b - a)
361
+ .slice(0, 20)
362
+ .map(([domain, count]) => ({ domain, count }));
363
+ const nxdomainRate = totalEntries > 0 ? nxdomainCount / totalEntries : 0;
364
+ // Generate findings
365
+ const findings = [];
366
+ if (nxdomainRate > 0.3) {
367
+ findings.push(`High NXDOMAIN rate (${(nxdomainRate * 100).toFixed(1)}%) — may indicate DGA activity`);
368
+ }
369
+ if (suspiciousTldQueries.length > 0) {
370
+ findings.push(`${suspiciousTldQueries.length} queries to suspicious TLDs detected`);
371
+ }
372
+ if (topDomains.length > 0 && topDomains[0].count > 100) {
373
+ findings.push(`Unusually high query volume for ${topDomains[0].domain} (${topDomains[0].count} queries)`);
374
+ }
375
+ if (findings.length === 0) {
376
+ findings.push("No suspicious DNS activity detected in the analyzed logs");
377
+ }
378
+ return {
379
+ totalEntries,
380
+ topDomains,
381
+ suspiciousTldQueries,
382
+ nxdomainCount,
383
+ nxdomainRate,
384
+ queryTimeline,
385
+ findings,
386
+ };
387
+ }
388
+ // ── Registration entry point ───────────────────────────────────────────────
389
+ export function registerDnsSecurityTools(server) {
390
+ server.tool("dns_security", "DNS security: audit resolver config, check DNSSEC, detect DNS tunneling, manage domain blocklists, and audit query logs.", {
391
+ action: z
392
+ .enum(["audit_resolv", "check_dnssec", "detect_tunneling", "block_domains", "query_log_audit"])
393
+ .describe("Action to perform"),
394
+ domain: z
395
+ .string()
396
+ .optional()
397
+ .describe("Domain to check (used with check_dnssec)"),
398
+ interface: z
399
+ .string()
400
+ .min(1)
401
+ .optional()
402
+ .default("any")
403
+ .describe("Network interface for capture (used with detect_tunneling)"),
404
+ duration: z
405
+ .number()
406
+ .optional()
407
+ .default(30)
408
+ .describe("Capture duration in seconds (used with detect_tunneling, max 120)"),
409
+ blocklist_path: z
410
+ .string()
411
+ .optional()
412
+ .describe("Path to blocklist file (used with block_domains)"),
413
+ domains_to_block: z
414
+ .array(z.string())
415
+ .optional()
416
+ .describe("Array of domains to add to blocklist (used with block_domains)"),
417
+ log_path: z
418
+ .string()
419
+ .optional()
420
+ .describe("Path to DNS query log (used with query_log_audit)"),
421
+ threshold: z
422
+ .number()
423
+ .optional()
424
+ .default(3.5)
425
+ .describe("Entropy threshold for tunneling detection (default 3.5)"),
426
+ }, async (params) => {
427
+ const { action } = params;
428
+ switch (action) {
429
+ // ── audit_resolv ────────────────────────────────────────────────
430
+ case "audit_resolv": {
431
+ try {
432
+ // Read /etc/resolv.conf
433
+ const resolvResult = await runCommand("cat", ["/etc/resolv.conf"]);
434
+ // Try systemd-resolve --status first, fall back to resolvectl
435
+ let resolvedResult = await runCommand("systemd-resolve", ["--status"], 10_000);
436
+ if (resolvedResult.exitCode !== 0) {
437
+ resolvedResult = await runCommand("resolvectl", ["status"], 10_000);
438
+ }
439
+ const audit = auditResolvConf(resolvResult.exitCode === 0 ? resolvResult.stdout : "", resolvedResult.exitCode === 0 ? resolvedResult.stdout : "");
440
+ return {
441
+ content: [
442
+ formatToolOutput({
443
+ action: "audit_resolv",
444
+ nameservers: audit.nameservers,
445
+ searchDomains: audit.searchDomains,
446
+ options: audit.options,
447
+ findings: audit.findings,
448
+ recommendations: audit.recommendations,
449
+ rawResolvConf: resolvResult.exitCode === 0 ? resolvResult.stdout.trim() : "[could not read /etc/resolv.conf]",
450
+ }),
451
+ ],
452
+ };
453
+ }
454
+ catch (err) {
455
+ const msg = err instanceof Error ? err.message : String(err);
456
+ return { content: [createErrorContent(`audit_resolv failed: ${msg}`)], isError: true };
457
+ }
458
+ }
459
+ // ── check_dnssec ────────────────────────────────────────────────
460
+ case "check_dnssec": {
461
+ const { domain } = params;
462
+ if (!domain) {
463
+ return { content: [createErrorContent("check_dnssec requires a 'domain' parameter")], isError: true };
464
+ }
465
+ try {
466
+ // Run dig +dnssec for the domain
467
+ const digResult = await runCommand("dig", ["+dnssec", "+multi", domain], 15_000);
468
+ if (digResult.exitCode !== 0) {
469
+ return { content: [createErrorContent(`dig command failed: ${digResult.stderr}`)], isError: true };
470
+ }
471
+ const dnssec = parseDnssecOutput(domain, digResult.stdout);
472
+ // Also check the DS record at the parent zone
473
+ const dsResult = await runCommand("dig", ["+short", "DS", domain], 15_000);
474
+ if (dsResult.exitCode === 0 && dsResult.stdout.trim().length > 0) {
475
+ dnssec.hasDS = true;
476
+ dnssec.records.push(...dsResult.stdout.trim().split("\n"));
477
+ }
478
+ return {
479
+ content: [
480
+ formatToolOutput({
481
+ action: "check_dnssec",
482
+ domain,
483
+ dnssecEnabled: dnssec.dnssecEnabled,
484
+ hasRRSIG: dnssec.hasRRSIG,
485
+ hasDNSKEY: dnssec.hasDNSKEY,
486
+ hasDS: dnssec.hasDS,
487
+ adFlag: dnssec.adFlag,
488
+ chainOfTrustValid: dnssec.chainValid,
489
+ issues: dnssec.issues,
490
+ recordCount: dnssec.records.length,
491
+ records: dnssec.records.slice(0, 20),
492
+ }),
493
+ ],
494
+ };
495
+ }
496
+ catch (err) {
497
+ const msg = err instanceof Error ? err.message : String(err);
498
+ return { content: [createErrorContent(`check_dnssec failed: ${msg}`)], isError: true };
499
+ }
500
+ }
501
+ // ── detect_tunneling ────────────────────────────────────────────
502
+ case "detect_tunneling": {
503
+ const iface = params.interface ?? "any";
504
+ const duration = Math.min(params.duration ?? 30, MAX_CAPTURE_DURATION);
505
+ const entropyThreshold = params.threshold ?? DEFAULT_ENTROPY_THRESHOLD;
506
+ try {
507
+ if (iface !== "any")
508
+ validateInterface(iface);
509
+ const captureTimeout = duration * 1000 + 5000;
510
+ const captureResult = await runSudoCommand("tcpdump", ["-i", iface, "-c", "1000", "-n", "port", "53", "-l"], captureTimeout);
511
+ const analysis = analyzeDnsQueries(captureResult.stdout, entropyThreshold);
512
+ // Check for high TXT/NULL query ratios
513
+ const txtRatio = analysis.totalQueries > 0
514
+ ? analysis.txtQueries / analysis.totalQueries
515
+ : 0;
516
+ const findings = [];
517
+ if (analysis.suspicious.length > 0) {
518
+ findings.push(`${analysis.suspicious.length} suspicious queries detected with high entropy`);
519
+ }
520
+ if (txtRatio > 0.3) {
521
+ findings.push(`High TXT query ratio (${(txtRatio * 100).toFixed(1)}%) — may indicate tunneling`);
522
+ }
523
+ if (analysis.nullQueries > 0) {
524
+ findings.push(`${analysis.nullQueries} NULL record queries detected — unusual for normal traffic`);
525
+ }
526
+ // Check for single-domain concentration
527
+ const domainEntries = Object.entries(analysis.domainCounts)
528
+ .sort(([, a], [, b]) => b - a);
529
+ if (domainEntries.length > 0 && domainEntries[0][1] > analysis.totalQueries * 0.5) {
530
+ findings.push(`High concentration to ${domainEntries[0][0]} (${domainEntries[0][1]}/${analysis.totalQueries} queries)`);
531
+ }
532
+ if (findings.length === 0) {
533
+ findings.push("No DNS tunneling indicators detected");
534
+ }
535
+ return {
536
+ content: [
537
+ formatToolOutput({
538
+ action: "detect_tunneling",
539
+ interface: iface,
540
+ duration,
541
+ entropyThreshold,
542
+ totalQueries: analysis.totalQueries,
543
+ txtQueries: analysis.txtQueries,
544
+ nullQueries: analysis.nullQueries,
545
+ suspiciousQueries: analysis.suspicious,
546
+ domainDistribution: domainEntries.slice(0, 10).map(([d, c]) => ({ domain: d, count: c })),
547
+ findings,
548
+ timedOut: captureResult.stderr.includes("[TIMEOUT]"),
549
+ }),
550
+ ],
551
+ };
552
+ }
553
+ catch (err) {
554
+ const msg = err instanceof Error ? err.message : String(err);
555
+ return { content: [createErrorContent(`detect_tunneling failed: ${msg}`)], isError: true };
556
+ }
557
+ }
558
+ // ── block_domains ───────────────────────────────────────────────
559
+ case "block_domains": {
560
+ const { blocklist_path, domains_to_block } = params;
561
+ if (!domains_to_block && !blocklist_path) {
562
+ return {
563
+ content: [createErrorContent("block_domains requires either 'domains_to_block' or 'blocklist_path'")],
564
+ isError: true,
565
+ };
566
+ }
567
+ try {
568
+ // Collect domains to block
569
+ let domainsToAdd = [];
570
+ if (domains_to_block && domains_to_block.length > 0) {
571
+ domainsToAdd.push(...domains_to_block);
572
+ }
573
+ if (blocklist_path) {
574
+ const fileResult = await runCommand("cat", [blocklist_path]);
575
+ if (fileResult.exitCode !== 0) {
576
+ return {
577
+ content: [createErrorContent(`Failed to read blocklist file: ${fileResult.stderr}`)],
578
+ isError: true,
579
+ };
580
+ }
581
+ const fileDomains = fileResult.stdout
582
+ .split("\n")
583
+ .map((l) => l.trim())
584
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
585
+ domainsToAdd.push(...fileDomains);
586
+ }
587
+ // Deduplicate
588
+ domainsToAdd = [...new Set(domainsToAdd)];
589
+ if (domainsToAdd.length === 0) {
590
+ return {
591
+ content: [createErrorContent("No domains to block — empty list provided")],
592
+ isError: true,
593
+ };
594
+ }
595
+ // Read current /etc/hosts
596
+ const hostsResult = await runSudoCommand("cat", ["/etc/hosts"]);
597
+ const currentHosts = hostsResult.exitCode === 0 ? hostsResult.stdout : "";
598
+ // Backup /etc/hosts before modifying
599
+ const backupPath = `/etc/hosts.bak.${Date.now()}`;
600
+ try {
601
+ secureCopyFileSync("/etc/hosts", backupPath);
602
+ }
603
+ catch {
604
+ // If secureCopyFileSync fails, try via sudo cp
605
+ await runSudoCommand("cp", ["/etc/hosts", backupPath]);
606
+ }
607
+ // Build new entries (avoid duplicates)
608
+ const existingBlocked = new Set();
609
+ for (const line of currentHosts.split("\n")) {
610
+ const match = /^0\.0\.0\.0\s+(\S+)/.exec(line.trim());
611
+ if (match)
612
+ existingBlocked.add(match[1].toLowerCase());
613
+ }
614
+ const newEntries = [];
615
+ const alreadyBlocked = [];
616
+ for (const domain of domainsToAdd) {
617
+ const lower = domain.toLowerCase().replace(/\.$/, "");
618
+ if (existingBlocked.has(lower)) {
619
+ alreadyBlocked.push(lower);
620
+ }
621
+ else {
622
+ newEntries.push(`0.0.0.0 ${lower}`);
623
+ }
624
+ }
625
+ if (newEntries.length === 0) {
626
+ return {
627
+ content: [
628
+ formatToolOutput({
629
+ action: "block_domains",
630
+ domainsAdded: 0,
631
+ alreadyBlocked: alreadyBlocked.length,
632
+ message: "All specified domains are already blocked",
633
+ }),
634
+ ],
635
+ };
636
+ }
637
+ // Append new entries to /etc/hosts
638
+ const separator = "\n# ── Kali Defense DNS Blocklist ──\n";
639
+ const newContent = currentHosts.trimEnd() +
640
+ (currentHosts.includes("Kali Defense DNS Blocklist") ? "\n" : separator) +
641
+ newEntries.join("\n") + "\n";
642
+ try {
643
+ secureWriteFileSync("/etc/hosts", newContent, "utf-8");
644
+ }
645
+ catch {
646
+ // If direct write fails, try via sudo tee
647
+ const teeResult = await runCommand("sudo", ["tee", "/etc/hosts"], 10_000);
648
+ if (teeResult.exitCode !== 0) {
649
+ return {
650
+ content: [createErrorContent(`Failed to write to /etc/hosts: ${teeResult.stderr}`)],
651
+ isError: true,
652
+ };
653
+ }
654
+ }
655
+ return {
656
+ content: [
657
+ formatToolOutput({
658
+ action: "block_domains",
659
+ domainsAdded: newEntries.length,
660
+ alreadyBlocked: alreadyBlocked.length,
661
+ backupPath,
662
+ addedDomains: newEntries.map((e) => e.replace("0.0.0.0 ", "")),
663
+ skippedDomains: alreadyBlocked,
664
+ }),
665
+ ],
666
+ };
667
+ }
668
+ catch (err) {
669
+ const msg = err instanceof Error ? err.message : String(err);
670
+ return { content: [createErrorContent(`block_domains failed: ${msg}`)], isError: true };
671
+ }
672
+ }
673
+ // ── query_log_audit ─────────────────────────────────────────────
674
+ case "query_log_audit": {
675
+ const { log_path } = params;
676
+ try {
677
+ let logContent = "";
678
+ if (log_path) {
679
+ // Read specified log file
680
+ const fileResult = await runSudoCommand("cat", [log_path]);
681
+ if (fileResult.exitCode !== 0) {
682
+ return {
683
+ content: [createErrorContent(`Failed to read log file ${log_path}: ${fileResult.stderr}`)],
684
+ isError: true,
685
+ };
686
+ }
687
+ logContent = fileResult.stdout;
688
+ }
689
+ else {
690
+ // Try journalctl for systemd-resolved first
691
+ const journalResult = await runCommand("journalctl", ["-u", "systemd-resolved", "-n", "500", "--no-pager"], 15_000);
692
+ if (journalResult.exitCode === 0 && journalResult.stdout.trim().length > 0) {
693
+ logContent = journalResult.stdout;
694
+ }
695
+ else {
696
+ // Fall back to dnsmasq log
697
+ const dnsmasqResult = await runSudoCommand("grep", ["-i", "dnsmasq", "/var/log/syslog"], 15_000);
698
+ if (dnsmasqResult.exitCode === 0) {
699
+ logContent = dnsmasqResult.stdout;
700
+ }
701
+ else {
702
+ // Try /var/log/messages as last resort
703
+ const messagesResult = await runSudoCommand("grep", ["-i", "dns\\|query\\|named", "/var/log/messages"], 15_000);
704
+ logContent = messagesResult.exitCode === 0 ? messagesResult.stdout : "";
705
+ }
706
+ }
707
+ }
708
+ if (logContent.trim().length === 0) {
709
+ return {
710
+ content: [
711
+ formatToolOutput({
712
+ action: "query_log_audit",
713
+ message: "No DNS query logs found. Enable DNS query logging in systemd-resolved or dnsmasq.",
714
+ checkedSources: log_path ? [log_path] : ["journalctl (systemd-resolved)", "/var/log/syslog (dnsmasq)", "/var/log/messages"],
715
+ }),
716
+ ],
717
+ };
718
+ }
719
+ const analysis = analyzeQueryLog(logContent);
720
+ return {
721
+ content: [
722
+ formatToolOutput({
723
+ action: "query_log_audit",
724
+ logSource: log_path ?? "system logs",
725
+ totalEntries: analysis.totalEntries,
726
+ topQueriedDomains: analysis.topDomains,
727
+ suspiciousTldQueries: analysis.suspiciousTldQueries.slice(0, 50),
728
+ nxdomainCount: analysis.nxdomainCount,
729
+ nxdomainRate: `${(analysis.nxdomainRate * 100).toFixed(1)}%`,
730
+ queryTimeline: analysis.queryTimeline,
731
+ findings: analysis.findings,
732
+ }),
733
+ ],
734
+ };
735
+ }
736
+ catch (err) {
737
+ const msg = err instanceof Error ? err.message : String(err);
738
+ return { content: [createErrorContent(`query_log_audit failed: ${msg}`)], isError: true };
739
+ }
740
+ }
741
+ default:
742
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
743
+ }
744
+ });
745
+ }