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.
- package/CHANGELOG.md +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- 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
|
+
}
|