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,749 @@
1
+ /**
2
+ * Threat intelligence tools for Kali Defense MCP Server.
3
+ *
4
+ * Registers 1 tool: threat_intel (actions: check_ip, check_hash, check_domain,
5
+ * update_feeds, blocklist_apply)
6
+ *
7
+ * Provides IP/hash/domain reputation checking against local threat intelligence
8
+ * feeds, feed management, and blocklist application to iptables/fail2ban/hosts.
9
+ */
10
+ import { z } from "zod";
11
+ import { spawnSafe } from "../core/spawn-safe.js";
12
+ import { createTextContent, createErrorContent, formatToolOutput, } from "../core/parsers.js";
13
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
14
+ // ── Constants ──────────────────────────────────────────────────────────────────
15
+ /** Base directory for threat intelligence feeds */
16
+ const FEED_BASE_DIR = "/var/lib/kali-defense/threat-feeds";
17
+ /** Subdirectory for hash-based feeds */
18
+ const HASH_FEED_DIR = `${FEED_BASE_DIR}/hashes`;
19
+ /** Subdirectory for domain-based feeds */
20
+ const DOMAIN_FEED_DIR = `${FEED_BASE_DIR}/domains`;
21
+ /** ClamAV signature database path */
22
+ const CLAMAV_DB_PATH = "/var/lib/clamav";
23
+ /** Maximum entries to apply in a single blocklist operation */
24
+ const MAX_BATCH_SIZE = 1000;
25
+ /** Known sinkhole IPs */
26
+ const SINKHOLE_IPS = new Set(["0.0.0.0", "127.0.0.1"]);
27
+ /** Basic IPv4 regex for validation */
28
+ const IPV4_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
29
+ /**
30
+ * Run a command via spawnSafe and collect output as a promise.
31
+ * Handles errors gracefully — returns error info instead of throwing.
32
+ */
33
+ async function runCommand(command, args, timeoutMs = 30_000) {
34
+ return new Promise((resolve) => {
35
+ let child;
36
+ try {
37
+ child = spawnSafe(command, args);
38
+ }
39
+ catch (err) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ resolve({ stdout: "", stderr: msg, exitCode: -1 });
42
+ return;
43
+ }
44
+ let stdout = "";
45
+ let stderr = "";
46
+ let resolved = false;
47
+ const timer = setTimeout(() => {
48
+ if (!resolved) {
49
+ resolved = true;
50
+ child.kill("SIGTERM");
51
+ resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
52
+ }
53
+ }, timeoutMs);
54
+ child.stdout?.on("data", (data) => {
55
+ stdout += data.toString();
56
+ });
57
+ child.stderr?.on("data", (data) => {
58
+ stderr += data.toString();
59
+ });
60
+ child.on("close", (code) => {
61
+ if (!resolved) {
62
+ resolved = true;
63
+ clearTimeout(timer);
64
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
65
+ }
66
+ });
67
+ child.on("error", (err) => {
68
+ if (!resolved) {
69
+ resolved = true;
70
+ clearTimeout(timer);
71
+ resolve({ stdout, stderr: err.message, exitCode: -1 });
72
+ }
73
+ });
74
+ });
75
+ }
76
+ // ── Validation ─────────────────────────────────────────────────────────────────
77
+ /**
78
+ * Validate an IPv4 address.
79
+ * Returns true if the address is a valid IPv4 address.
80
+ */
81
+ export function isValidIPv4(ip) {
82
+ const match = IPV4_REGEX.exec(ip);
83
+ if (!match)
84
+ return false;
85
+ for (let i = 1; i <= 4; i++) {
86
+ const octet = parseInt(match[i], 10);
87
+ if (octet < 0 || octet > 255)
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+ /**
93
+ * Auto-detect hash type based on string length.
94
+ * Returns the hash type or "unknown".
95
+ */
96
+ export function detectHashType(hash) {
97
+ const cleaned = hash.trim().toLowerCase();
98
+ // Validate hex characters only
99
+ if (!/^[0-9a-f]+$/.test(cleaned))
100
+ return "unknown";
101
+ switch (cleaned.length) {
102
+ case 32: return "MD5";
103
+ case 40: return "SHA1";
104
+ case 64: return "SHA256";
105
+ default: return "unknown";
106
+ }
107
+ }
108
+ /**
109
+ * Read feed files from a directory and return matching indicators.
110
+ * Each file is expected to have one indicator per line, with '#' comments.
111
+ */
112
+ function searchFeedDirectory(feedDir, indicator) {
113
+ const results = [];
114
+ if (!existsSync(feedDir)) {
115
+ return results;
116
+ }
117
+ try {
118
+ const files = readdirSync(feedDir);
119
+ for (const file of files) {
120
+ const filePath = `${feedDir}/${file}`;
121
+ try {
122
+ const stat = statSync(filePath);
123
+ if (!stat.isFile())
124
+ continue;
125
+ const content = readFileSync(filePath, "utf-8");
126
+ const lines = content.split("\n")
127
+ .map((l) => l.trim().toLowerCase())
128
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
129
+ const normalizedIndicator = indicator.trim().toLowerCase();
130
+ // Match exact line or lines that start with the indicator
131
+ // (feeds may include metadata after indicator, e.g. "hash:malware_name")
132
+ const matched = lines.some((l) => l === normalizedIndicator || l.startsWith(normalizedIndicator + ":") ||
133
+ l.startsWith(normalizedIndicator + ",") || l.startsWith(normalizedIndicator + "\t") ||
134
+ l.startsWith(normalizedIndicator + " "));
135
+ results.push({ feed: file, matched });
136
+ }
137
+ catch {
138
+ // Skip unreadable files
139
+ }
140
+ }
141
+ }
142
+ catch {
143
+ // Directory read failed — return empty
144
+ }
145
+ return results;
146
+ }
147
+ /**
148
+ * List available feeds with metadata.
149
+ */
150
+ function listFeeds(feedDir) {
151
+ const feeds = [];
152
+ if (!existsSync(feedDir)) {
153
+ return feeds;
154
+ }
155
+ try {
156
+ const files = readdirSync(feedDir);
157
+ for (const file of files) {
158
+ const filePath = `${feedDir}/${file}`;
159
+ try {
160
+ const stat = statSync(filePath);
161
+ if (!stat.isFile())
162
+ continue;
163
+ const content = readFileSync(filePath, "utf-8");
164
+ const indicatorCount = content.split("\n")
165
+ .filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"))
166
+ .length;
167
+ feeds.push({
168
+ name: file,
169
+ size: stat.size,
170
+ lastUpdated: stat.mtime.toISOString(),
171
+ indicatorCount,
172
+ });
173
+ }
174
+ catch {
175
+ // Skip unreadable files
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // Directory read failed
181
+ }
182
+ return feeds;
183
+ }
184
+ // ── Registration entry point ───────────────────────────────────────────────
185
+ export function registerThreatIntelTools(server) {
186
+ server.tool("threat_intel", "Threat intelligence: check IPs, hashes, and domains against local threat feeds, manage feed updates, and apply blocklists to iptables/fail2ban/hosts.", {
187
+ action: z
188
+ .enum(["check_ip", "check_hash", "check_domain", "update_feeds", "blocklist_apply"])
189
+ .describe("Action: check_ip=check IP reputation, check_hash=check file hash, check_domain=check domain reputation, update_feeds=manage threat feeds, blocklist_apply=apply blocklist to security tools"),
190
+ indicator: z
191
+ .string()
192
+ .optional()
193
+ .describe("IP address, file hash, or domain to check (used with check_ip, check_hash, check_domain)"),
194
+ feed_name: z
195
+ .string()
196
+ .optional()
197
+ .describe("Name of threat feed to update (used with update_feeds)"),
198
+ feed_url: z
199
+ .string()
200
+ .optional()
201
+ .describe("URL of threat feed to download (used with update_feeds)"),
202
+ blocklist_path: z
203
+ .string()
204
+ .optional()
205
+ .describe("Path to blocklist file (used with blocklist_apply)"),
206
+ apply_to: z
207
+ .enum(["iptables", "fail2ban", "hosts"])
208
+ .optional()
209
+ .default("iptables")
210
+ .describe("Target to apply blocklist to (used with blocklist_apply, default iptables)"),
211
+ output_format: z
212
+ .enum(["text", "json"])
213
+ .optional()
214
+ .default("text")
215
+ .describe("Output format (default text)"),
216
+ }, async (params) => {
217
+ const { action } = params;
218
+ switch (action) {
219
+ // ── check_ip ──────────────────────────────────────────────────────
220
+ case "check_ip": {
221
+ const { indicator } = params;
222
+ if (!indicator) {
223
+ return {
224
+ content: [createErrorContent("check_ip requires an 'indicator' parameter (IP address)")],
225
+ isError: true,
226
+ };
227
+ }
228
+ if (!isValidIPv4(indicator)) {
229
+ return {
230
+ content: [createErrorContent(`Invalid IP address format: ${indicator}`)],
231
+ isError: true,
232
+ };
233
+ }
234
+ try {
235
+ // Check against local threat feeds
236
+ const feedMatches = searchFeedDirectory(FEED_BASE_DIR, indicator);
237
+ const matchedFeeds = feedMatches.filter((f) => f.matched).map((f) => f.feed);
238
+ // Check fail2ban banned list
239
+ let inFail2ban = false;
240
+ const fail2banResult = await runCommand("fail2ban-client", ["status"], 10_000);
241
+ if (fail2banResult.exitCode === 0) {
242
+ // Extract jail names and check each
243
+ const jailMatch = fail2banResult.stdout.match(/Jail list:\s*(.+)/);
244
+ if (jailMatch) {
245
+ const jails = jailMatch[1].split(",").map((j) => j.trim()).filter((j) => j.length > 0);
246
+ for (const jail of jails) {
247
+ const jailStatus = await runCommand("fail2ban-client", ["status", jail], 10_000);
248
+ if (jailStatus.exitCode === 0 && jailStatus.stdout.includes(indicator)) {
249
+ inFail2ban = true;
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ }
255
+ // Check iptables DROP rules
256
+ let inIptables = false;
257
+ const iptablesResult = await runCommand("iptables", ["-L", "-n"], 10_000);
258
+ if (iptablesResult.exitCode === 0) {
259
+ const lines = iptablesResult.stdout.split("\n");
260
+ for (const line of lines) {
261
+ if (line.includes("DROP") && line.includes(indicator)) {
262
+ inIptables = true;
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ // Get geo/whois info
268
+ let whoisInfo = "";
269
+ const whoisResult = await runCommand("whois", [indicator], 15_000);
270
+ if (whoisResult.exitCode === 0) {
271
+ // Extract key fields
272
+ const lines = whoisResult.stdout.split("\n");
273
+ const relevantFields = ["country", "orgname", "org-name", "netname", "descr"];
274
+ const extracted = [];
275
+ for (const line of lines) {
276
+ const lower = line.toLowerCase();
277
+ for (const field of relevantFields) {
278
+ if (lower.startsWith(field + ":")) {
279
+ extracted.push(line.trim());
280
+ break;
281
+ }
282
+ }
283
+ }
284
+ whoisInfo = extracted.join("\n");
285
+ }
286
+ // Calculate a basic reputation score (0=clean, 100=malicious)
287
+ let reputationScore = 0;
288
+ if (matchedFeeds.length > 0)
289
+ reputationScore += 50 + (matchedFeeds.length * 10);
290
+ if (inFail2ban)
291
+ reputationScore += 20;
292
+ if (inIptables)
293
+ reputationScore += 10;
294
+ reputationScore = Math.min(reputationScore, 100);
295
+ const alreadyBlocked = inFail2ban || inIptables;
296
+ const output = {
297
+ action: "check_ip",
298
+ indicator,
299
+ feedsChecked: feedMatches.length,
300
+ feedMatches: matchedFeeds,
301
+ matchFound: matchedFeeds.length > 0,
302
+ reputationScore,
303
+ inFail2ban,
304
+ inIptables,
305
+ alreadyBlocked,
306
+ whoisInfo: whoisInfo || "No whois data available",
307
+ };
308
+ if (params.output_format === "json") {
309
+ return { content: [formatToolOutput(output)] };
310
+ }
311
+ return {
312
+ content: [createTextContent(`Threat Intel — IP Check: ${indicator}\n\n` +
313
+ `Reputation Score: ${reputationScore}/100 (${reputationScore === 0 ? "clean" : reputationScore < 50 ? "suspicious" : "malicious"})\n` +
314
+ `Feeds Checked: ${feedMatches.length}\n` +
315
+ `Feed Matches: ${matchedFeeds.length > 0 ? matchedFeeds.join(", ") : "none"}\n` +
316
+ `Fail2ban Banned: ${inFail2ban ? "YES" : "no"}\n` +
317
+ `Iptables Blocked: ${inIptables ? "YES" : "no"}\n` +
318
+ `Already Blocked: ${alreadyBlocked ? "YES" : "no"}\n` +
319
+ (whoisInfo ? `\nWhois Info:\n${whoisInfo}\n` : ""))],
320
+ };
321
+ }
322
+ catch (err) {
323
+ const msg = err instanceof Error ? err.message : String(err);
324
+ return { content: [createErrorContent(`check_ip failed: ${msg}`)], isError: true };
325
+ }
326
+ }
327
+ // ── check_hash ────────────────────────────────────────────────────
328
+ case "check_hash": {
329
+ const { indicator } = params;
330
+ if (!indicator) {
331
+ return {
332
+ content: [createErrorContent("check_hash requires an 'indicator' parameter (file hash)")],
333
+ isError: true,
334
+ };
335
+ }
336
+ const hashType = detectHashType(indicator);
337
+ if (hashType === "unknown") {
338
+ return {
339
+ content: [createErrorContent(`Unable to detect hash type for: ${indicator}. Expected MD5 (32 chars), SHA1 (40 chars), or SHA256 (64 chars) hex string.`)],
340
+ isError: true,
341
+ };
342
+ }
343
+ try {
344
+ const normalizedHash = indicator.trim().toLowerCase();
345
+ // Check against local hash feeds
346
+ const feedMatches = searchFeedDirectory(HASH_FEED_DIR, normalizedHash);
347
+ const matchedFeeds = feedMatches.filter((f) => f.matched).map((f) => f.feed);
348
+ // Check ClamAV signature databases if available
349
+ let clamavMatch = "";
350
+ if (existsSync(CLAMAV_DB_PATH)) {
351
+ const grepResult = await runCommand("grep", ["-rl", normalizedHash, CLAMAV_DB_PATH], 15_000);
352
+ if (grepResult.exitCode === 0 && grepResult.stdout.trim().length > 0) {
353
+ clamavMatch = grepResult.stdout.trim().split("\n")[0];
354
+ }
355
+ }
356
+ // Try to find associated malware name from feed files
357
+ let malwareName = "";
358
+ if (matchedFeeds.length > 0) {
359
+ // Some feeds store "hash:malware_name" or "hash malware_name"
360
+ for (const feedFile of matchedFeeds) {
361
+ try {
362
+ const content = readFileSync(`${HASH_FEED_DIR}/${feedFile}`, "utf-8");
363
+ for (const line of content.split("\n")) {
364
+ const lower = line.trim().toLowerCase();
365
+ if (lower.startsWith(normalizedHash)) {
366
+ // Check for separator after hash
367
+ const rest = line.trim().substring(normalizedHash.length).trim();
368
+ if (rest.startsWith(":") || rest.startsWith(",") || rest.startsWith("\t") || rest.startsWith(" ")) {
369
+ malwareName = rest.replace(/^[:\s,\t]+/, "").trim();
370
+ break;
371
+ }
372
+ }
373
+ }
374
+ if (malwareName)
375
+ break;
376
+ }
377
+ catch {
378
+ // Skip unreadable files
379
+ }
380
+ }
381
+ }
382
+ const output = {
383
+ action: "check_hash",
384
+ indicator: normalizedHash,
385
+ hashType,
386
+ feedsChecked: feedMatches.length,
387
+ feedMatches: matchedFeeds,
388
+ matchFound: matchedFeeds.length > 0 || clamavMatch.length > 0,
389
+ clamavMatch: clamavMatch || null,
390
+ malwareName: malwareName || null,
391
+ };
392
+ if (params.output_format === "json") {
393
+ return { content: [formatToolOutput(output)] };
394
+ }
395
+ return {
396
+ content: [createTextContent(`Threat Intel — Hash Check: ${normalizedHash}\n\n` +
397
+ `Hash Type: ${hashType}\n` +
398
+ `Feeds Checked: ${feedMatches.length}\n` +
399
+ `Feed Matches: ${matchedFeeds.length > 0 ? matchedFeeds.join(", ") : "none"}\n` +
400
+ `ClamAV Match: ${clamavMatch || "none"}\n` +
401
+ `Malware Name: ${malwareName || "unknown"}\n` +
402
+ `Match Found: ${output.matchFound ? "YES" : "no"}\n`)],
403
+ };
404
+ }
405
+ catch (err) {
406
+ const msg = err instanceof Error ? err.message : String(err);
407
+ return { content: [createErrorContent(`check_hash failed: ${msg}`)], isError: true };
408
+ }
409
+ }
410
+ // ── check_domain ──────────────────────────────────────────────────
411
+ case "check_domain": {
412
+ const { indicator } = params;
413
+ if (!indicator) {
414
+ return {
415
+ content: [createErrorContent("check_domain requires an 'indicator' parameter (domain name)")],
416
+ isError: true,
417
+ };
418
+ }
419
+ try {
420
+ const normalizedDomain = indicator.trim().toLowerCase().replace(/\.$/, "");
421
+ // Check against local domain blocklists
422
+ const feedMatches = searchFeedDirectory(DOMAIN_FEED_DIR, normalizedDomain);
423
+ const matchedFeeds = feedMatches.filter((f) => f.matched).map((f) => f.feed);
424
+ // Check /etc/hosts for existing blocks
425
+ let inHostsFile = false;
426
+ const hostsResult = await runCommand("cat", ["/etc/hosts"], 5_000);
427
+ if (hostsResult.exitCode === 0) {
428
+ const lines = hostsResult.stdout.split("\n");
429
+ for (const line of lines) {
430
+ const trimmed = line.trim().toLowerCase();
431
+ if (trimmed.startsWith("#"))
432
+ continue;
433
+ // Match "0.0.0.0 domain" or "127.0.0.1 domain"
434
+ if (trimmed.includes(normalizedDomain)) {
435
+ const parts = trimmed.split(/\s+/);
436
+ if (parts.length >= 2 && SINKHOLE_IPS.has(parts[0]) && parts[1] === normalizedDomain) {
437
+ inHostsFile = true;
438
+ break;
439
+ }
440
+ }
441
+ }
442
+ }
443
+ // Perform dig lookup to check DNS resolution
444
+ let resolvedIPs = [];
445
+ let isSinkholed = false;
446
+ const digResult = await runCommand("dig", ["+short", normalizedDomain], 10_000);
447
+ if (digResult.exitCode === 0 && digResult.stdout.trim().length > 0) {
448
+ resolvedIPs = digResult.stdout.trim().split("\n")
449
+ .map((l) => l.trim())
450
+ .filter((l) => l.length > 0);
451
+ // Check if resolves to sinkhole
452
+ for (const ip of resolvedIPs) {
453
+ if (SINKHOLE_IPS.has(ip)) {
454
+ isSinkholed = true;
455
+ break;
456
+ }
457
+ }
458
+ }
459
+ const isBlocked = inHostsFile || isSinkholed || matchedFeeds.length > 0;
460
+ const output = {
461
+ action: "check_domain",
462
+ indicator: normalizedDomain,
463
+ feedsChecked: feedMatches.length,
464
+ feedMatches: matchedFeeds,
465
+ matchFound: matchedFeeds.length > 0,
466
+ inHostsFile,
467
+ isSinkholed,
468
+ isBlocked,
469
+ resolvedIPs,
470
+ };
471
+ if (params.output_format === "json") {
472
+ return { content: [formatToolOutput(output)] };
473
+ }
474
+ return {
475
+ content: [createTextContent(`Threat Intel — Domain Check: ${normalizedDomain}\n\n` +
476
+ `Feeds Checked: ${feedMatches.length}\n` +
477
+ `Feed Matches: ${matchedFeeds.length > 0 ? matchedFeeds.join(", ") : "none"}\n` +
478
+ `In /etc/hosts: ${inHostsFile ? "YES (blocked)" : "no"}\n` +
479
+ `Sinkholed: ${isSinkholed ? "YES" : "no"}\n` +
480
+ `Blocked: ${isBlocked ? "YES" : "no"}\n` +
481
+ `Resolved IPs: ${resolvedIPs.length > 0 ? resolvedIPs.join(", ") : "no resolution"}\n`)],
482
+ };
483
+ }
484
+ catch (err) {
485
+ const msg = err instanceof Error ? err.message : String(err);
486
+ return { content: [createErrorContent(`check_domain failed: ${msg}`)], isError: true };
487
+ }
488
+ }
489
+ // ── update_feeds ──────────────────────────────────────────────────
490
+ case "update_feeds": {
491
+ const { feed_name, feed_url } = params;
492
+ try {
493
+ // If no URL provided, list available feeds
494
+ if (!feed_url) {
495
+ const ipFeeds = listFeeds(FEED_BASE_DIR);
496
+ const hashFeeds = listFeeds(HASH_FEED_DIR);
497
+ const domainFeeds = listFeeds(DOMAIN_FEED_DIR);
498
+ const output = {
499
+ action: "update_feeds",
500
+ mode: "list",
501
+ feedDirectories: {
502
+ ip: FEED_BASE_DIR,
503
+ hashes: HASH_FEED_DIR,
504
+ domains: DOMAIN_FEED_DIR,
505
+ },
506
+ ipFeeds,
507
+ hashFeeds,
508
+ domainFeeds,
509
+ totalFeeds: ipFeeds.length + hashFeeds.length + domainFeeds.length,
510
+ };
511
+ if (params.output_format === "json") {
512
+ return { content: [formatToolOutput(output)] };
513
+ }
514
+ let text = "Threat Intel — Available Feeds\n\n";
515
+ text += `Feed Directories:\n IP: ${FEED_BASE_DIR}\n Hashes: ${HASH_FEED_DIR}\n Domains: ${DOMAIN_FEED_DIR}\n\n`;
516
+ const formatFeedList = (label, feeds) => {
517
+ if (feeds.length === 0)
518
+ return `${label}: (none)\n`;
519
+ return `${label}:\n` + feeds.map((f) => ` ${f.name} — ${f.indicatorCount} indicators, updated ${f.lastUpdated}, ${f.size} bytes`).join("\n") + "\n";
520
+ };
521
+ text += formatFeedList("IP Feeds", ipFeeds);
522
+ text += formatFeedList("Hash Feeds", hashFeeds);
523
+ text += formatFeedList("Domain Feeds", domainFeeds);
524
+ text += `\nTotal: ${output.totalFeeds} feeds`;
525
+ return { content: [createTextContent(text)] };
526
+ }
527
+ // Download a feed
528
+ if (!feed_name) {
529
+ return {
530
+ content: [createErrorContent("update_feeds requires 'feed_name' when 'feed_url' is provided")],
531
+ isError: true,
532
+ };
533
+ }
534
+ // Ensure feed directory exists
535
+ await runCommand("mkdir", ["-p", FEED_BASE_DIR]);
536
+ const outputPath = `${FEED_BASE_DIR}/${feed_name}`;
537
+ // Try curl first, fall back to wget
538
+ let downloadResult = await runCommand("curl", ["-sS", "-o", outputPath, "-L", "--max-time", "60", feed_url], 65_000);
539
+ if (downloadResult.exitCode !== 0) {
540
+ // Fall back to wget
541
+ downloadResult = await runCommand("wget", ["-q", "-O", outputPath, feed_url], 65_000);
542
+ }
543
+ if (downloadResult.exitCode !== 0) {
544
+ return {
545
+ content: [createErrorContent(`Failed to download feed from ${feed_url}: ${downloadResult.stderr}`)],
546
+ isError: true,
547
+ };
548
+ }
549
+ // Read downloaded file stats
550
+ let indicatorCount = 0;
551
+ let fileSize = 0;
552
+ try {
553
+ const content = readFileSync(outputPath, "utf-8");
554
+ indicatorCount = content.split("\n")
555
+ .filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"))
556
+ .length;
557
+ const stat = statSync(outputPath);
558
+ fileSize = stat.size;
559
+ }
560
+ catch {
561
+ // Stats unavailable
562
+ }
563
+ const output = {
564
+ action: "update_feeds",
565
+ mode: "download",
566
+ feedName: feed_name,
567
+ feedUrl: feed_url,
568
+ outputPath,
569
+ indicatorCount,
570
+ fileSize,
571
+ downloadSuccess: true,
572
+ };
573
+ if (params.output_format === "json") {
574
+ return { content: [formatToolOutput(output)] };
575
+ }
576
+ return {
577
+ content: [createTextContent(`Threat Intel — Feed Updated\n\n` +
578
+ `Feed: ${feed_name}\n` +
579
+ `Source: ${feed_url}\n` +
580
+ `Saved to: ${outputPath}\n` +
581
+ `Indicators: ${indicatorCount}\n` +
582
+ `Size: ${fileSize} bytes\n`)],
583
+ };
584
+ }
585
+ catch (err) {
586
+ const msg = err instanceof Error ? err.message : String(err);
587
+ return { content: [createErrorContent(`update_feeds failed: ${msg}`)], isError: true };
588
+ }
589
+ }
590
+ // ── blocklist_apply ───────────────────────────────────────────────
591
+ case "blocklist_apply": {
592
+ const { blocklist_path, apply_to } = params;
593
+ if (!blocklist_path) {
594
+ return {
595
+ content: [createErrorContent("blocklist_apply requires a 'blocklist_path' parameter")],
596
+ isError: true,
597
+ };
598
+ }
599
+ const target = apply_to || "iptables";
600
+ try {
601
+ // Read blocklist file
602
+ const fileResult = await runCommand("cat", [blocklist_path]);
603
+ if (fileResult.exitCode !== 0) {
604
+ return {
605
+ content: [createErrorContent(`Failed to read blocklist file: ${fileResult.stderr}`)],
606
+ isError: true,
607
+ };
608
+ }
609
+ let entries = fileResult.stdout
610
+ .split("\n")
611
+ .map((l) => l.trim())
612
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
613
+ // Deduplicate
614
+ entries = [...new Set(entries)];
615
+ if (entries.length === 0) {
616
+ return {
617
+ content: [createErrorContent("Blocklist file is empty or contains only comments")],
618
+ isError: true,
619
+ };
620
+ }
621
+ // Limit batch size
622
+ const totalEntries = entries.length;
623
+ const truncated = totalEntries > MAX_BATCH_SIZE;
624
+ if (truncated) {
625
+ entries = entries.slice(0, MAX_BATCH_SIZE);
626
+ }
627
+ let applied = 0;
628
+ let skipped = 0;
629
+ const errors = [];
630
+ switch (target) {
631
+ case "iptables": {
632
+ // Get existing iptables rules to check for duplicates
633
+ const existingResult = await runCommand("iptables", ["-L", "INPUT", "-n"], 10_000);
634
+ const existingRules = existingResult.exitCode === 0 ? existingResult.stdout : "";
635
+ for (const ip of entries) {
636
+ if (!isValidIPv4(ip)) {
637
+ errors.push(`Invalid IP skipped: ${ip}`);
638
+ skipped++;
639
+ continue;
640
+ }
641
+ // Check for existing rule
642
+ if (existingRules.includes(ip)) {
643
+ skipped++;
644
+ continue;
645
+ }
646
+ const addResult = await runCommand("iptables", ["-A", "INPUT", "-s", ip, "-j", "DROP"], 5_000);
647
+ if (addResult.exitCode === 0) {
648
+ applied++;
649
+ }
650
+ else {
651
+ errors.push(`Failed to block ${ip}: ${addResult.stderr}`);
652
+ }
653
+ }
654
+ break;
655
+ }
656
+ case "fail2ban": {
657
+ // Use the default jail or 'recidive'
658
+ const jail = "recidive";
659
+ for (const ip of entries) {
660
+ if (!isValidIPv4(ip)) {
661
+ errors.push(`Invalid IP skipped: ${ip}`);
662
+ skipped++;
663
+ continue;
664
+ }
665
+ const banResult = await runCommand("fail2ban-client", ["set", jail, "banip", ip], 5_000);
666
+ if (banResult.exitCode === 0) {
667
+ applied++;
668
+ }
669
+ else if (banResult.stderr.includes("already") || banResult.stdout.includes("already")) {
670
+ skipped++;
671
+ }
672
+ else {
673
+ errors.push(`Failed to ban ${ip}: ${banResult.stderr}`);
674
+ }
675
+ }
676
+ break;
677
+ }
678
+ case "hosts": {
679
+ // Read current /etc/hosts
680
+ const hostsResult = await runCommand("cat", ["/etc/hosts"]);
681
+ const currentHosts = hostsResult.exitCode === 0 ? hostsResult.stdout : "";
682
+ // Parse existing blocked domains
683
+ const existingBlocked = new Set();
684
+ for (const line of currentHosts.split("\n")) {
685
+ const trimmed = line.trim().toLowerCase();
686
+ if (trimmed.startsWith("#"))
687
+ continue;
688
+ const match = /^0\.0\.0\.0\s+(\S+)/.exec(trimmed);
689
+ if (match)
690
+ existingBlocked.add(match[1]);
691
+ }
692
+ const newEntries = [];
693
+ for (const domain of entries) {
694
+ const normalized = domain.toLowerCase().replace(/\.$/, "");
695
+ if (existingBlocked.has(normalized)) {
696
+ skipped++;
697
+ }
698
+ else {
699
+ newEntries.push(`0.0.0.0 ${normalized}`);
700
+ applied++;
701
+ }
702
+ }
703
+ if (newEntries.length > 0) {
704
+ // Write new entries via tee
705
+ const appendContent = "\n# ── Kali Defense Threat Intel Blocklist ──\n" +
706
+ newEntries.join("\n") + "\n";
707
+ const teeResult = await runCommand("sudo", ["tee", "-a", "/etc/hosts"], 10_000);
708
+ if (teeResult.exitCode !== 0) {
709
+ errors.push(`Failed to write to /etc/hosts: ${teeResult.stderr}`);
710
+ }
711
+ }
712
+ break;
713
+ }
714
+ }
715
+ const output = {
716
+ action: "blocklist_apply",
717
+ target,
718
+ blocklistPath: blocklist_path,
719
+ totalEntries,
720
+ applied,
721
+ skipped,
722
+ errors: errors.slice(0, 20),
723
+ truncated,
724
+ maxBatchSize: MAX_BATCH_SIZE,
725
+ };
726
+ if (params.output_format === "json") {
727
+ return { content: [formatToolOutput(output)] };
728
+ }
729
+ return {
730
+ content: [createTextContent(`Threat Intel — Blocklist Applied\n\n` +
731
+ `Target: ${target}\n` +
732
+ `Blocklist: ${blocklist_path}\n` +
733
+ `Total Entries: ${totalEntries}${truncated ? ` (truncated to ${MAX_BATCH_SIZE})` : ""}\n` +
734
+ `Applied: ${applied}\n` +
735
+ `Skipped (duplicates/invalid): ${skipped}\n` +
736
+ `Errors: ${errors.length}\n` +
737
+ (errors.length > 0 ? `\nErrors:\n${errors.slice(0, 10).map((e) => ` • ${e}`).join("\n")}\n` : ""))],
738
+ };
739
+ }
740
+ catch (err) {
741
+ const msg = err instanceof Error ? err.message : String(err);
742
+ return { content: [createErrorContent(`blocklist_apply failed: ${msg}`)], isError: true };
743
+ }
744
+ }
745
+ default:
746
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
747
+ }
748
+ });
749
+ }