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,760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network defense tools for Kali Defense MCP Server.
|
|
3
|
+
*
|
|
4
|
+
* Registers 4 tools:
|
|
5
|
+
* netdef_connections (actions: list, audit)
|
|
6
|
+
* netdef_capture (actions: custom, dns, arp)
|
|
7
|
+
* netdef_security_audit (actions: scan_detect, ipv6, self_scan)
|
|
8
|
+
* network_segmentation_audit (actions: map_zones, verify_isolation, test_paths, audit_vlans)
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { executeCommand } from "../core/executor.js";
|
|
12
|
+
import { getConfig, getToolTimeout } from "../core/config.js";
|
|
13
|
+
import { createTextContent, createErrorContent, parseSsOutput, formatToolOutput, } from "../core/parsers.js";
|
|
14
|
+
import { logChange, createChangeEntry } from "../core/changelog.js";
|
|
15
|
+
import { validateInterface, sanitizeArgs, validateToolPath } from "../core/sanitizer.js";
|
|
16
|
+
import { validateBpfFilter } from "./ebpf-security.js";
|
|
17
|
+
import * as net from "node:net";
|
|
18
|
+
import { spawnSafe } from "../core/spawn-safe.js";
|
|
19
|
+
// ── TOOL-022 remediation: strict network parameter validation helpers ───────
|
|
20
|
+
/** Allowed protocol names for network operations */
|
|
21
|
+
const ALLOWED_PROTOCOLS = new Set(["tcp", "udp", "icmp", "sctp"]);
|
|
22
|
+
/** Validate an IP address strictly using net.isIP() */
|
|
23
|
+
function validateIPAddress(ip, label = "IP address") {
|
|
24
|
+
const trimmed = ip.trim();
|
|
25
|
+
// Could be CIDR
|
|
26
|
+
if (trimmed.includes("/")) {
|
|
27
|
+
return validateCIDR(trimmed, label);
|
|
28
|
+
}
|
|
29
|
+
if (net.isIP(trimmed) === 0) {
|
|
30
|
+
throw new Error(`${label} is not a valid IP address: '${trimmed}'`);
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
/** Validate a CIDR range (e.g., 192.168.1.0/24) */
|
|
35
|
+
function validateCIDR(cidr, label = "CIDR range") {
|
|
36
|
+
const trimmed = cidr.trim();
|
|
37
|
+
const cidrRe = /^([0-9a-fA-F.:]+)\/(\d{1,3})$/;
|
|
38
|
+
const match = cidrRe.exec(trimmed);
|
|
39
|
+
if (!match) {
|
|
40
|
+
throw new Error(`${label} is not a valid CIDR notation: '${trimmed}'`);
|
|
41
|
+
}
|
|
42
|
+
const ip = match[1];
|
|
43
|
+
const prefix = parseInt(match[2], 10);
|
|
44
|
+
const ipVersion = net.isIP(ip);
|
|
45
|
+
if (ipVersion === 0) {
|
|
46
|
+
throw new Error(`${label} contains invalid IP address: '${ip}'`);
|
|
47
|
+
}
|
|
48
|
+
const maxPrefix = ipVersion === 4 ? 32 : 128;
|
|
49
|
+
if (prefix < 0 || prefix > maxPrefix) {
|
|
50
|
+
throw new Error(`${label} has invalid prefix length: /${prefix} (must be 0-${maxPrefix})`);
|
|
51
|
+
}
|
|
52
|
+
return trimmed;
|
|
53
|
+
}
|
|
54
|
+
/** Validate port number is in range 1-65535 */
|
|
55
|
+
function validatePortNumber(port, label = "Port") {
|
|
56
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
57
|
+
throw new Error(`${label} must be an integer between 1 and 65535, got: ${port}`);
|
|
58
|
+
}
|
|
59
|
+
return port;
|
|
60
|
+
}
|
|
61
|
+
/** Validate protocol name against whitelist */
|
|
62
|
+
function validateProtocol(proto, label = "Protocol") {
|
|
63
|
+
const lower = proto.trim().toLowerCase();
|
|
64
|
+
if (!ALLOWED_PROTOCOLS.has(lower)) {
|
|
65
|
+
throw new Error(`${label} '${proto}' is not allowed. Allowed: ${[...ALLOWED_PROTOCOLS].join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
return lower;
|
|
68
|
+
}
|
|
69
|
+
// ── TOOL-022: Allowed directories for log/output paths ─────────────────────
|
|
70
|
+
const ALLOWED_CAPTURE_DIRS = ["/tmp", "/var/log", "/home", "/root", "/opt"];
|
|
71
|
+
// ── Known safe ports for audit reference ───────────────────────────────────
|
|
72
|
+
const KNOWN_SAFE_PORTS = {
|
|
73
|
+
22: "SSH",
|
|
74
|
+
53: "DNS",
|
|
75
|
+
80: "HTTP",
|
|
76
|
+
443: "HTTPS",
|
|
77
|
+
123: "NTP",
|
|
78
|
+
323: "Chrony NTP",
|
|
79
|
+
631: "CUPS (printing)",
|
|
80
|
+
5353: "mDNS",
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Run a command via spawnSafe for segmentation audit operations.
|
|
84
|
+
* Returns collected stdout/stderr and exit code — never throws.
|
|
85
|
+
*/
|
|
86
|
+
async function runSegmentCommand(command, args, timeoutMs = 30_000) {
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
let child;
|
|
89
|
+
try {
|
|
90
|
+
child = spawnSafe(command, args);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
resolve({ stdout: "", stderr: msg, exitCode: -1 });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let stdout = "";
|
|
98
|
+
let stderr = "";
|
|
99
|
+
let settled = false;
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
if (!settled) {
|
|
102
|
+
settled = true;
|
|
103
|
+
child.kill("SIGTERM");
|
|
104
|
+
resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
|
|
105
|
+
}
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
child.stdout?.on("data", (data) => {
|
|
108
|
+
stdout += data.toString();
|
|
109
|
+
});
|
|
110
|
+
child.stderr?.on("data", (data) => {
|
|
111
|
+
stderr += data.toString();
|
|
112
|
+
});
|
|
113
|
+
child.on("close", (code) => {
|
|
114
|
+
if (!settled) {
|
|
115
|
+
settled = true;
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
resolve({ stdout, stderr, exitCode: code ?? -1 });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
if (!settled) {
|
|
122
|
+
settled = true;
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
resolve({ stdout, stderr: err.message, exitCode: -1 });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async function runSudoSegmentCommand(command, args, timeoutMs = 30_000) {
|
|
130
|
+
return runSegmentCommand("sudo", [command, ...args], timeoutMs);
|
|
131
|
+
}
|
|
132
|
+
// ── Registration entry point ───────────────────────────────────────────────
|
|
133
|
+
export function registerNetworkDefenseTools(server) {
|
|
134
|
+
// ── 1. netdef_connections (merged: connections + open_ports_audit) ─────
|
|
135
|
+
server.tool("netdef_connections", "Network connections: list active connections or audit listening ports for suspicious services.", {
|
|
136
|
+
action: z.enum(["list", "audit"]).describe("Action: list=list active connections, audit=audit listening ports"),
|
|
137
|
+
// list params
|
|
138
|
+
protocol: z.enum(["tcp", "udp", "all"]).optional().default("all").describe("Protocol filter (list action)"),
|
|
139
|
+
listening: z.boolean().optional().default(false).describe("Show only listening sockets (list action)"),
|
|
140
|
+
process: z.boolean().optional().default(true).describe("Show process information (list action)"),
|
|
141
|
+
// audit params
|
|
142
|
+
include_loopback: z.boolean().optional().default(false).describe("Include services listening only on loopback (audit action)"),
|
|
143
|
+
}, async (params) => {
|
|
144
|
+
const { action } = params;
|
|
145
|
+
switch (action) {
|
|
146
|
+
case "list": {
|
|
147
|
+
const { protocol, listening, process: showProcess } = params;
|
|
148
|
+
try {
|
|
149
|
+
const args = [];
|
|
150
|
+
if (protocol === "tcp")
|
|
151
|
+
args.push("-t");
|
|
152
|
+
else if (protocol === "udp")
|
|
153
|
+
args.push("-u");
|
|
154
|
+
else
|
|
155
|
+
args.push("-t", "-u");
|
|
156
|
+
if (listening)
|
|
157
|
+
args.push("-l");
|
|
158
|
+
args.push("-n");
|
|
159
|
+
if (showProcess)
|
|
160
|
+
args.push("-p");
|
|
161
|
+
const result = await executeCommand({
|
|
162
|
+
command: "ss", args, toolName: "netdef_connections",
|
|
163
|
+
timeout: getToolTimeout("tcpdump"),
|
|
164
|
+
});
|
|
165
|
+
if (result.exitCode !== 0) {
|
|
166
|
+
return { content: [createErrorContent(`ss command failed (exit ${result.exitCode}): ${result.stderr}`)], isError: true };
|
|
167
|
+
}
|
|
168
|
+
const parsed = parseSsOutput(result.stdout);
|
|
169
|
+
return { content: [formatToolOutput({ protocol, listening, connectionCount: parsed.length, connections: parsed, raw: result.stdout })] };
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
173
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
case "audit": {
|
|
177
|
+
const { include_loopback } = params;
|
|
178
|
+
try {
|
|
179
|
+
const result = await executeCommand({
|
|
180
|
+
command: "ss", args: ["-tulnp"], toolName: "netdef_connections",
|
|
181
|
+
timeout: getToolTimeout("tcpdump"),
|
|
182
|
+
});
|
|
183
|
+
if (result.exitCode !== 0) {
|
|
184
|
+
return { content: [createErrorContent(`ss command failed (exit ${result.exitCode}): ${result.stderr}`)], isError: true };
|
|
185
|
+
}
|
|
186
|
+
const parsed = parseSsOutput(result.stdout);
|
|
187
|
+
const auditEntries = [];
|
|
188
|
+
for (const entry of parsed) {
|
|
189
|
+
const localParts = entry.local.split(":");
|
|
190
|
+
const portStr = localParts[localParts.length - 1];
|
|
191
|
+
const port = parseInt(portStr, 10);
|
|
192
|
+
if (isNaN(port))
|
|
193
|
+
continue;
|
|
194
|
+
const isLoopback = entry.local.startsWith("127.") || entry.local.startsWith("[::1]") || entry.local.startsWith("localhost");
|
|
195
|
+
if (!include_loopback && isLoopback)
|
|
196
|
+
continue;
|
|
197
|
+
const knownService = KNOWN_SAFE_PORTS[port];
|
|
198
|
+
let status;
|
|
199
|
+
let note;
|
|
200
|
+
if (knownService) {
|
|
201
|
+
status = "safe";
|
|
202
|
+
note = `Known service: ${knownService}`;
|
|
203
|
+
}
|
|
204
|
+
else if (port < 1024) {
|
|
205
|
+
status = "review";
|
|
206
|
+
note = "Privileged port - verify this service is expected";
|
|
207
|
+
}
|
|
208
|
+
else if (port < 49152) {
|
|
209
|
+
status = "unknown";
|
|
210
|
+
note = "Registered port - verify this service is authorized";
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
status = "unknown";
|
|
214
|
+
note = "Ephemeral/dynamic port range";
|
|
215
|
+
}
|
|
216
|
+
if (isLoopback)
|
|
217
|
+
note += " (loopback only)";
|
|
218
|
+
const proto = entry.state.toLowerCase().includes("udp") ? "udp" : "tcp";
|
|
219
|
+
auditEntries.push({ protocol: proto, localAddress: entry.local, port, process: entry.process || "unknown", status, note });
|
|
220
|
+
}
|
|
221
|
+
const safeCount = auditEntries.filter((e) => e.status === "safe").length;
|
|
222
|
+
const reviewCount = auditEntries.filter((e) => e.status === "review").length;
|
|
223
|
+
const unknownCount = auditEntries.filter((e) => e.status === "unknown").length;
|
|
224
|
+
return { content: [formatToolOutput({ totalListeners: auditEntries.length, summary: { safe: safeCount, needsReview: reviewCount, unknown: unknownCount }, listeners: auditEntries, raw: result.stdout })] };
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
228
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
default:
|
|
232
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// ── 2. netdef_capture (merged: tcpdump + dns_monitor + arp_monitor) ────
|
|
236
|
+
server.tool("netdef_capture", "Network capture: custom tcpdump capture, DNS query monitoring, or ARP traffic monitoring.", {
|
|
237
|
+
action: z.enum(["custom", "dns", "arp"]).describe("Action: custom=tcpdump capture, dns=DNS monitoring, arp=ARP monitoring"),
|
|
238
|
+
interface: z.string().min(1).optional().default("any").describe("Network interface to capture on"),
|
|
239
|
+
count: z.number().optional().default(100).describe("Number of packets to capture"),
|
|
240
|
+
duration: z.number().optional().default(30).describe("Capture duration in seconds"),
|
|
241
|
+
// custom params
|
|
242
|
+
filter: z.string().optional().describe("BPF filter expression (custom action)"),
|
|
243
|
+
output_file: z.string().optional().describe("Path to save pcap file (custom action)"),
|
|
244
|
+
dry_run: z.boolean().optional().describe("Preview the command without executing"),
|
|
245
|
+
}, async (params) => {
|
|
246
|
+
const { action } = params;
|
|
247
|
+
switch (action) {
|
|
248
|
+
case "custom": {
|
|
249
|
+
const { interface: iface, filter, count, output_file, duration, dry_run } = params;
|
|
250
|
+
try {
|
|
251
|
+
if (iface !== "any")
|
|
252
|
+
validateInterface(iface);
|
|
253
|
+
const args = ["-i", iface, "-c", String(count), "-n"];
|
|
254
|
+
if (output_file) {
|
|
255
|
+
// TOOL-022: Validate output file path for traversal
|
|
256
|
+
validateToolPath(output_file, ALLOWED_CAPTURE_DIRS, "Capture output file path");
|
|
257
|
+
args.push("-w", output_file);
|
|
258
|
+
}
|
|
259
|
+
if (filter) {
|
|
260
|
+
// TOOL-018/022: Validate BPF filter expression
|
|
261
|
+
const validatedFilter = validateBpfFilter(filter);
|
|
262
|
+
args.push(...validatedFilter.split(/\s+/));
|
|
263
|
+
}
|
|
264
|
+
const fullCmd = `sudo tcpdump ${args.join(" ")}`;
|
|
265
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
266
|
+
logChange(createChangeEntry({ tool: "netdef_capture", action: "[DRY-RUN] Capture network traffic", target: iface, after: fullCmd, dryRun: true, success: true }));
|
|
267
|
+
return { content: [createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}\n\nDuration: ${duration}s, Packets: ${count}`)] };
|
|
268
|
+
}
|
|
269
|
+
const captureTimeout = duration * 1000 + 5000;
|
|
270
|
+
const result = await executeCommand({ command: "sudo", args: ["tcpdump", ...args], toolName: "netdef_capture", timeout: captureTimeout });
|
|
271
|
+
logChange(createChangeEntry({ tool: "netdef_capture", action: "Capture network traffic", target: iface, after: fullCmd, dryRun: false, success: result.exitCode === 0 || result.timedOut }));
|
|
272
|
+
return { content: [formatToolOutput({ interface: iface, filter: filter ?? "none", packetCount: count, duration, timedOut: result.timedOut, outputFile: output_file ?? "none (stdout)", captured: result.stdout, stats: result.stderr })] };
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
276
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
case "dns": {
|
|
280
|
+
const { interface: iface, count, duration } = params;
|
|
281
|
+
try {
|
|
282
|
+
if (iface !== "any")
|
|
283
|
+
validateInterface(iface);
|
|
284
|
+
const args = ["-i", iface, "-c", String(count), "port", "53", "-n"];
|
|
285
|
+
const captureTimeout = duration * 1000 + 5000;
|
|
286
|
+
const result = await executeCommand({ command: "sudo", args: ["tcpdump", ...args], toolName: "netdef_capture", timeout: captureTimeout });
|
|
287
|
+
const dnsLines = result.stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
288
|
+
const queries = [];
|
|
289
|
+
const responses = [];
|
|
290
|
+
for (const line of dnsLines) {
|
|
291
|
+
if (line.includes("A?") || line.includes("AAAA?") || line.includes("PTR?") || line.includes("MX?"))
|
|
292
|
+
queries.push(line.trim());
|
|
293
|
+
else if (line.includes("A ") || line.includes("CNAME"))
|
|
294
|
+
responses.push(line.trim());
|
|
295
|
+
}
|
|
296
|
+
return { content: [formatToolOutput({ interface: iface, duration, timedOut: result.timedOut, totalPackets: dnsLines.length, queries: queries.length, responses: responses.length, raw: result.stdout, stats: result.stderr })] };
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
300
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
case "arp": {
|
|
304
|
+
const { interface: iface, count, duration } = params;
|
|
305
|
+
try {
|
|
306
|
+
if (iface !== "any")
|
|
307
|
+
validateInterface(iface);
|
|
308
|
+
const args = ["-i", iface, "-c", String(count), "arp", "-n"];
|
|
309
|
+
const captureTimeout = duration * 1000 + 5000;
|
|
310
|
+
const result = await executeCommand({ command: "sudo", args: ["tcpdump", ...args], toolName: "netdef_capture", timeout: captureTimeout });
|
|
311
|
+
const arpLines = result.stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
312
|
+
const ipToMac = {};
|
|
313
|
+
const arpRe = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+is-at\s+([0-9a-f:]+)/i;
|
|
314
|
+
for (const line of arpLines) {
|
|
315
|
+
const m = arpRe.exec(line);
|
|
316
|
+
if (m) {
|
|
317
|
+
const ip = m[1];
|
|
318
|
+
const mac = m[2];
|
|
319
|
+
if (!ipToMac[ip])
|
|
320
|
+
ipToMac[ip] = new Set();
|
|
321
|
+
ipToMac[ip].add(mac);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const duplicates = Object.entries(ipToMac)
|
|
325
|
+
.filter(([, macs]) => macs.size > 1)
|
|
326
|
+
.map(([ip, macs]) => ({ ip, macAddresses: Array.from(macs), warning: "Multiple MAC addresses detected - possible ARP poisoning!" }));
|
|
327
|
+
return { content: [formatToolOutput({
|
|
328
|
+
interface: iface, duration, timedOut: result.timedOut, totalArpPackets: arpLines.length,
|
|
329
|
+
uniqueIPs: Object.keys(ipToMac).length, arpPoisoningDetected: duplicates.length > 0, duplicateMappings: duplicates,
|
|
330
|
+
ipToMacMap: Object.fromEntries(Object.entries(ipToMac).map(([ip, macs]) => [ip, Array.from(macs)])),
|
|
331
|
+
raw: result.stdout, stats: result.stderr,
|
|
332
|
+
})] };
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
336
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
// ── 3. netdef_security_audit (merged: port_scan_detect + ipv6 + self_scan) ──
|
|
344
|
+
server.tool("netdef_security_audit", "Network security audit: detect port scanning, audit IPv6 configuration, or run nmap self-scan.", {
|
|
345
|
+
action: z.enum(["scan_detect", "ipv6", "self_scan"]).describe("Action: scan_detect=detect port scans, ipv6=audit IPv6, self_scan=nmap self-scan"),
|
|
346
|
+
// scan_detect params
|
|
347
|
+
log_file: z.string().optional().describe("Log file to analyze (scan_detect action)"),
|
|
348
|
+
threshold: z.number().optional().default(10).describe("Connection attempts threshold (scan_detect action)"),
|
|
349
|
+
timeframe: z.number().optional().default(60).describe("Seconds to look back (scan_detect action)"),
|
|
350
|
+
// self_scan params
|
|
351
|
+
target: z.enum(["localhost", "external"]).optional().default("localhost").describe("Scan target (self_scan action)"),
|
|
352
|
+
scan_type: z.enum(["quick", "full", "service"]).optional().default("quick").describe("Scan type (self_scan action)"),
|
|
353
|
+
}, async (params) => {
|
|
354
|
+
const { action } = params;
|
|
355
|
+
switch (action) {
|
|
356
|
+
case "scan_detect": {
|
|
357
|
+
const { log_file, threshold, timeframe } = params;
|
|
358
|
+
try {
|
|
359
|
+
const sinceStr = `${String(timeframe)} seconds ago`;
|
|
360
|
+
sanitizeArgs(["--since", sinceStr, "--no-pager"]);
|
|
361
|
+
const journalArgs = ["--since", sinceStr, "--no-pager", "-g", "SYN|refused connection|connection attempt|UFW BLOCK"];
|
|
362
|
+
const journalResult = await executeCommand({ command: "journalctl", args: journalArgs, toolName: "netdef_security_audit", timeout: getToolTimeout("tcpdump") });
|
|
363
|
+
const dmesgArgs = ["-T", "--level=warn,err"];
|
|
364
|
+
const dmesgResult = await executeCommand({ command: "dmesg", args: dmesgArgs, toolName: "netdef_security_audit", timeout: getToolTimeout("tcpdump") });
|
|
365
|
+
const ipCounts = {};
|
|
366
|
+
const ipRe = /SRC=(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g;
|
|
367
|
+
const combinedOutput = journalResult.stdout + "\n" + dmesgResult.stdout;
|
|
368
|
+
let match;
|
|
369
|
+
while ((match = ipRe.exec(combinedOutput)) !== null) {
|
|
370
|
+
const ip = match[1];
|
|
371
|
+
ipCounts[ip] = (ipCounts[ip] ?? 0) + 1;
|
|
372
|
+
}
|
|
373
|
+
const flaggedIPs = Object.entries(ipCounts)
|
|
374
|
+
.filter(([, count]) => count >= threshold)
|
|
375
|
+
.sort(([, a], [, b]) => b - a)
|
|
376
|
+
.map(([ip, count]) => ({ ip, attempts: count }));
|
|
377
|
+
return { content: [formatToolOutput({
|
|
378
|
+
timeframe, threshold, totalUniqueSourceIPs: Object.keys(ipCounts).length,
|
|
379
|
+
flaggedIPs, suspectedScan: flaggedIPs.length > 0,
|
|
380
|
+
summary: flaggedIPs.length > 0 ? `${flaggedIPs.length} IP(s) exceeded the threshold of ${threshold} attempts` : "No port scanning activity detected above threshold",
|
|
381
|
+
journalEntries: journalResult.stdout.split("\n").filter((l) => l.trim()).length,
|
|
382
|
+
})] };
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
386
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
case "ipv6": {
|
|
390
|
+
try {
|
|
391
|
+
const findings = [];
|
|
392
|
+
const ipv6All = await executeCommand({ command: "sysctl", args: ["net.ipv6.conf.all.disable_ipv6"], timeout: 5000, toolName: "netdef_security_audit" });
|
|
393
|
+
const disabled = ipv6All.stdout.includes("= 1");
|
|
394
|
+
findings.push({ check: "ipv6_disabled", status: "INFO", value: disabled ? "disabled" : "enabled", description: "IPv6 status (disable if not needed)" });
|
|
395
|
+
if (!disabled) {
|
|
396
|
+
const addrResult = await executeCommand({ command: "ip", args: ["-6", "addr", "show"], timeout: 5000, toolName: "netdef_security_audit" });
|
|
397
|
+
const hasGlobal = addrResult.stdout.includes("scope global");
|
|
398
|
+
findings.push({ check: "ipv6_global_address", status: hasGlobal ? "INFO" : "PASS", value: hasGlobal ? "has global IPv6 address" : "link-local only", description: "IPv6 global address presence" });
|
|
399
|
+
const ip6tables = await executeCommand({ command: "sudo", args: ["ip6tables", "-L", "-n"], timeout: 10000, toolName: "netdef_security_audit" });
|
|
400
|
+
const inputMatch = ip6tables.stdout.match(/Chain INPUT \(policy (\w+)\)/);
|
|
401
|
+
findings.push({ check: "ipv6_firewall_input", status: inputMatch && (inputMatch[1] === "DROP" || inputMatch[1] === "REJECT") ? "PASS" : "FAIL", value: inputMatch ? inputMatch[1] : "unknown", description: "IPv6 INPUT chain policy (should be DROP if IPv6 enabled)" });
|
|
402
|
+
const sysctlChecks = [
|
|
403
|
+
{ key: "net.ipv6.conf.all.accept_ra", expected: "0", desc: "Accept router advertisements" },
|
|
404
|
+
{ key: "net.ipv6.conf.all.accept_redirects", expected: "0", desc: "Accept redirects" },
|
|
405
|
+
{ key: "net.ipv6.conf.all.accept_source_route", expected: "0", desc: "Accept source route" },
|
|
406
|
+
];
|
|
407
|
+
for (const sc of sysctlChecks) {
|
|
408
|
+
const val = await executeCommand({ command: "sysctl", args: [sc.key], timeout: 5000, toolName: "netdef_security_audit" });
|
|
409
|
+
const current = val.stdout.split("=").pop()?.trim() || "unknown";
|
|
410
|
+
findings.push({ check: sc.key, status: current === sc.expected ? "PASS" : "FAIL", value: current, description: `${sc.desc} (should be ${sc.expected})` });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, fail: findings.filter(f => f.status === "FAIL").length }, ipv6Enabled: !disabled, findings }, null, 2))] };
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
case "self_scan": {
|
|
420
|
+
try {
|
|
421
|
+
let scanTarget = "127.0.0.1";
|
|
422
|
+
if (params.target === "external") {
|
|
423
|
+
const ipResult = await executeCommand({ command: "hostname", args: ["-I"], timeout: 5000, toolName: "netdef_security_audit" });
|
|
424
|
+
scanTarget = ipResult.stdout.trim().split(" ")[0] || "127.0.0.1";
|
|
425
|
+
}
|
|
426
|
+
const args = [scanTarget];
|
|
427
|
+
if (params.scan_type === "quick")
|
|
428
|
+
args.unshift("-F");
|
|
429
|
+
else if (params.scan_type === "full")
|
|
430
|
+
args.unshift("-p-");
|
|
431
|
+
else if (params.scan_type === "service")
|
|
432
|
+
args.unshift("-sV", "-F");
|
|
433
|
+
args.push("--open", "-n");
|
|
434
|
+
const result = await executeCommand({ command: "nmap", args, timeout: 120000, toolName: "netdef_security_audit" });
|
|
435
|
+
return { content: [createTextContent(`Self-Scan Results (target: ${scanTarget}, type: ${params.scan_type}):\n\n${result.stdout}\n${result.stderr ? `\nWarnings:\n${result.stderr}` : ""}`)] };
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
default:
|
|
442
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// ── 4. network_segmentation_audit ────────────────────────────────────────
|
|
446
|
+
server.tool("network_segmentation_audit", "Network segmentation audit: map network zones, verify isolation enforcement, test paths between zones, audit VLAN configurations.", {
|
|
447
|
+
action: z.enum(["map_zones", "verify_isolation", "test_paths", "audit_vlans"]).describe("Action: map_zones=map network zones, verify_isolation=check segmentation enforcement, test_paths=test connectivity between zones, audit_vlans=audit VLAN configs"),
|
|
448
|
+
source_zone: z.string().optional().describe("Source network zone/subnet in CIDR notation (test_paths action)"),
|
|
449
|
+
dest_zone: z.string().optional().describe("Destination network zone/subnet in CIDR notation (test_paths action)"),
|
|
450
|
+
interface: z.string().optional().describe("Specific network interface to audit"),
|
|
451
|
+
output_format: z.enum(["text", "json"]).optional().default("text").describe("Output format: text or json"),
|
|
452
|
+
}, async (params) => {
|
|
453
|
+
const { action, output_format } = params;
|
|
454
|
+
switch (action) {
|
|
455
|
+
case "map_zones": {
|
|
456
|
+
try {
|
|
457
|
+
const ifaceFilter = params.interface;
|
|
458
|
+
// Get network interfaces and subnets
|
|
459
|
+
const addrResult = await runSegmentCommand("ip", ["addr", "show"]);
|
|
460
|
+
// Get routing table
|
|
461
|
+
const routeResult = await runSegmentCommand("ip", ["route", "show"]);
|
|
462
|
+
// Get firewall rules
|
|
463
|
+
const fwResult = await runSudoSegmentCommand("iptables", ["-L", "-n", "-v"]);
|
|
464
|
+
// Get FORWARD chain specifically
|
|
465
|
+
const fwdResult = await runSudoSegmentCommand("iptables", ["-L", "FORWARD", "-n", "-v"]);
|
|
466
|
+
// Get bridge interfaces
|
|
467
|
+
const bridgeResult = await runSegmentCommand("bridge", ["link", "show"]);
|
|
468
|
+
const zones = [];
|
|
469
|
+
const ifaceBlocks = addrResult.stdout.split(/(?=^\d+:)/m).filter(b => b.trim());
|
|
470
|
+
for (const block of ifaceBlocks) {
|
|
471
|
+
const ifaceMatch = /^\d+:\s+(\S+?)(?:@\S+)?:/.exec(block);
|
|
472
|
+
if (!ifaceMatch)
|
|
473
|
+
continue;
|
|
474
|
+
const ifaceName = ifaceMatch[1];
|
|
475
|
+
if (ifaceName === "lo")
|
|
476
|
+
continue;
|
|
477
|
+
if (ifaceFilter && ifaceName !== ifaceFilter)
|
|
478
|
+
continue;
|
|
479
|
+
const inetMatches = [...block.matchAll(/inet\s+(\S+)/g)];
|
|
480
|
+
for (const m of inetMatches) {
|
|
481
|
+
const subnet = m[1];
|
|
482
|
+
// Find gateway from route table
|
|
483
|
+
let gateway = null;
|
|
484
|
+
const routeLines = routeResult.stdout.split("\n");
|
|
485
|
+
for (const line of routeLines) {
|
|
486
|
+
if (line.includes(ifaceName) && line.includes("via")) {
|
|
487
|
+
const gwMatch = /via\s+(\S+)/.exec(line);
|
|
488
|
+
if (gwMatch) {
|
|
489
|
+
gateway = gwMatch[1];
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Find associated firewall rules
|
|
495
|
+
const firewallRules = [];
|
|
496
|
+
if (fwResult.exitCode === 0) {
|
|
497
|
+
const fwLines = fwResult.stdout.split("\n");
|
|
498
|
+
for (const line of fwLines) {
|
|
499
|
+
if (line.includes(subnet.split("/")[0]) || line.includes(ifaceName)) {
|
|
500
|
+
firewallRules.push(line.trim());
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
zones.push({ interface: ifaceName, subnet, gateway, firewallRules });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const bridgeInterfaces = [];
|
|
508
|
+
if (bridgeResult.exitCode === 0 && bridgeResult.stdout.trim()) {
|
|
509
|
+
for (const line of bridgeResult.stdout.split("\n")) {
|
|
510
|
+
if (line.trim())
|
|
511
|
+
bridgeInterfaces.push(line.trim());
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const zoneMap = {
|
|
515
|
+
zones,
|
|
516
|
+
totalZones: zones.length,
|
|
517
|
+
forwardChain: fwdResult.exitCode === 0 ? fwdResult.stdout.trim() : "Unable to read FORWARD chain",
|
|
518
|
+
bridgeInterfaces,
|
|
519
|
+
};
|
|
520
|
+
if (output_format === "json") {
|
|
521
|
+
return { content: [formatToolOutput(zoneMap)] };
|
|
522
|
+
}
|
|
523
|
+
let text = `=== Network Zone Map ===\n\nTotal zones detected: ${zones.length}\n\n`;
|
|
524
|
+
for (const zone of zones) {
|
|
525
|
+
text += `Interface: ${zone.interface}\n`;
|
|
526
|
+
text += ` Subnet: ${zone.subnet}\n`;
|
|
527
|
+
text += ` Gateway: ${zone.gateway ?? "none"}\n`;
|
|
528
|
+
text += ` Firewall rules: ${zone.firewallRules.length > 0 ? "\n " + zone.firewallRules.join("\n ") : "none"}\n\n`;
|
|
529
|
+
}
|
|
530
|
+
if (fwdResult.exitCode === 0) {
|
|
531
|
+
text += `--- FORWARD Chain ---\n${fwdResult.stdout}\n`;
|
|
532
|
+
}
|
|
533
|
+
if (bridgeInterfaces.length > 0) {
|
|
534
|
+
text += `--- Bridge Interfaces ---\n${bridgeInterfaces.join("\n")}\n`;
|
|
535
|
+
}
|
|
536
|
+
return { content: [createTextContent(text)] };
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
540
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
case "verify_isolation": {
|
|
544
|
+
try {
|
|
545
|
+
const violations = [];
|
|
546
|
+
let score = 100;
|
|
547
|
+
// Check FORWARD chain default policy
|
|
548
|
+
const fwdResult = await runSudoSegmentCommand("iptables", ["-L", "FORWARD", "-n"]);
|
|
549
|
+
let forwardPolicy = "UNKNOWN";
|
|
550
|
+
if (fwdResult.exitCode === 0) {
|
|
551
|
+
const policyMatch = /Chain FORWARD \(policy (\w+)\)/.exec(fwdResult.stdout);
|
|
552
|
+
if (policyMatch) {
|
|
553
|
+
forwardPolicy = policyMatch[1];
|
|
554
|
+
if (forwardPolicy !== "DROP" && forwardPolicy !== "REJECT") {
|
|
555
|
+
violations.push(`FORWARD chain default policy is ${forwardPolicy} (should be DROP or REJECT)`);
|
|
556
|
+
score -= 30;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Check for overly permissive rules (ACCEPT all from any to any)
|
|
560
|
+
const fwdLines = fwdResult.stdout.split("\n");
|
|
561
|
+
for (const line of fwdLines) {
|
|
562
|
+
if (line.includes("ACCEPT") && line.includes("0.0.0.0/0") &&
|
|
563
|
+
(line.match(/0\.0\.0\.0\/0/g) || []).length >= 2) {
|
|
564
|
+
violations.push(`Overly permissive FORWARD rule: ${line.trim()}`);
|
|
565
|
+
score -= 20;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
violations.push("Unable to read FORWARD chain - iptables may not be available");
|
|
571
|
+
score -= 40;
|
|
572
|
+
}
|
|
573
|
+
// Check for NAT/masquerade rules that might bypass segmentation
|
|
574
|
+
const natResult = await runSudoSegmentCommand("iptables", ["-t", "nat", "-L", "-n"]);
|
|
575
|
+
const natBypasses = [];
|
|
576
|
+
if (natResult.exitCode === 0) {
|
|
577
|
+
const natLines = natResult.stdout.split("\n");
|
|
578
|
+
for (const line of natLines) {
|
|
579
|
+
if (line.includes("MASQUERADE") || line.includes("SNAT")) {
|
|
580
|
+
natBypasses.push(line.trim());
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (natBypasses.length > 0) {
|
|
584
|
+
violations.push(`NAT/masquerade rules detected that may bypass segmentation: ${natBypasses.length} rule(s)`);
|
|
585
|
+
score -= 10;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
score = Math.max(0, score);
|
|
589
|
+
const isolation = {
|
|
590
|
+
forwardPolicy,
|
|
591
|
+
violations,
|
|
592
|
+
natBypasses,
|
|
593
|
+
segmentationScore: score,
|
|
594
|
+
segmentationStatus: score >= 80 ? "GOOD" : score >= 50 ? "FAIR" : "POOR",
|
|
595
|
+
violationCount: violations.length,
|
|
596
|
+
};
|
|
597
|
+
if (output_format === "json") {
|
|
598
|
+
return { content: [formatToolOutput(isolation)] };
|
|
599
|
+
}
|
|
600
|
+
let text = `=== Segmentation Isolation Verification ===\n\n`;
|
|
601
|
+
text += `FORWARD Chain Policy: ${forwardPolicy}\n`;
|
|
602
|
+
text += `Segmentation Score: ${score}/100 (${isolation.segmentationStatus})\n`;
|
|
603
|
+
text += `Violations: ${violations.length}\n\n`;
|
|
604
|
+
if (violations.length > 0) {
|
|
605
|
+
text += `--- Violations ---\n`;
|
|
606
|
+
for (const v of violations) {
|
|
607
|
+
text += ` ⚠ ${v}\n`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (natBypasses.length > 0) {
|
|
611
|
+
text += `\n--- NAT/Masquerade Rules ---\n`;
|
|
612
|
+
for (const n of natBypasses) {
|
|
613
|
+
text += ` ${n}\n`;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { content: [createTextContent(text)] };
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
620
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
case "test_paths": {
|
|
624
|
+
const { source_zone, dest_zone } = params;
|
|
625
|
+
if (!source_zone || !dest_zone) {
|
|
626
|
+
return { content: [createErrorContent("test_paths requires both source_zone and dest_zone parameters (CIDR notation)")], isError: true };
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
validateCIDR(source_zone, "source_zone");
|
|
630
|
+
validateCIDR(dest_zone, "dest_zone");
|
|
631
|
+
const destIP = dest_zone.split("/")[0];
|
|
632
|
+
// Try traceroute first, fall back to tracepath
|
|
633
|
+
let traceResult = await runSegmentCommand("traceroute", ["-n", "-m", "15", destIP], 30_000);
|
|
634
|
+
if (traceResult.exitCode !== 0) {
|
|
635
|
+
traceResult = await runSegmentCommand("tracepath", ["-n", destIP], 30_000);
|
|
636
|
+
}
|
|
637
|
+
// Host discovery in target zone
|
|
638
|
+
const nmapResult = await runSudoSegmentCommand("nmap", ["-sn", dest_zone], 60_000);
|
|
639
|
+
const reachableHosts = [];
|
|
640
|
+
if (nmapResult.exitCode === 0) {
|
|
641
|
+
const hostRe = /Nmap scan report for (\S+)/g;
|
|
642
|
+
let hm;
|
|
643
|
+
while ((hm = hostRe.exec(nmapResult.stdout)) !== null) {
|
|
644
|
+
reachableHosts.push(hm[1]);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const pathResult = {
|
|
648
|
+
sourceZone: source_zone,
|
|
649
|
+
destZone: dest_zone,
|
|
650
|
+
traceroute: traceResult.exitCode === 0 ? traceResult.stdout.trim() : `Trace failed: ${traceResult.stderr}`,
|
|
651
|
+
reachableHosts,
|
|
652
|
+
reachableHostCount: reachableHosts.length,
|
|
653
|
+
pathExists: traceResult.exitCode === 0,
|
|
654
|
+
};
|
|
655
|
+
if (output_format === "json") {
|
|
656
|
+
return { content: [formatToolOutput(pathResult)] };
|
|
657
|
+
}
|
|
658
|
+
let text = `=== Path Test: ${source_zone} → ${dest_zone} ===\n\n`;
|
|
659
|
+
text += `--- Traceroute ---\n${traceResult.exitCode === 0 ? traceResult.stdout : `Failed: ${traceResult.stderr}`}\n\n`;
|
|
660
|
+
text += `--- Host Discovery (${dest_zone}) ---\n`;
|
|
661
|
+
text += `Reachable hosts: ${reachableHosts.length}\n`;
|
|
662
|
+
for (const h of reachableHosts) {
|
|
663
|
+
text += ` ${h}\n`;
|
|
664
|
+
}
|
|
665
|
+
return { content: [createTextContent(text)] };
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
669
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
case "audit_vlans": {
|
|
673
|
+
try {
|
|
674
|
+
// List VLAN interfaces
|
|
675
|
+
const linkResult = await runSegmentCommand("ip", ["-d", "link", "show"]);
|
|
676
|
+
const vlans = [];
|
|
677
|
+
if (linkResult.exitCode === 0) {
|
|
678
|
+
const blocks = linkResult.stdout.split(/(?=^\d+:)/m).filter(b => b.trim());
|
|
679
|
+
for (const block of blocks) {
|
|
680
|
+
if (block.includes("vlan")) {
|
|
681
|
+
const ifMatch = /^\d+:\s+(\S+?)(?:@(\S+))?:/.exec(block);
|
|
682
|
+
const vlanIdMatch = /vlan.*?id\s+(\d+)/i.exec(block);
|
|
683
|
+
if (ifMatch && vlanIdMatch) {
|
|
684
|
+
vlans.push({
|
|
685
|
+
interface: ifMatch[1],
|
|
686
|
+
vlanId: vlanIdMatch[1],
|
|
687
|
+
parentInterface: ifMatch[2] || "unknown",
|
|
688
|
+
flags: (block.match(/<([^>]+)>/) || ["", ""])[1],
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Check VLAN tagging config
|
|
695
|
+
let vlanConfig = "";
|
|
696
|
+
const vlanConfigResult = await runSegmentCommand("cat", ["/proc/net/vlan/config"]);
|
|
697
|
+
if (vlanConfigResult.exitCode === 0) {
|
|
698
|
+
vlanConfig = vlanConfigResult.stdout.trim();
|
|
699
|
+
}
|
|
700
|
+
// Check 802.1Q support
|
|
701
|
+
let dot1qSupported = vlanConfigResult.exitCode === 0;
|
|
702
|
+
if (!dot1qSupported) {
|
|
703
|
+
const lsmodResult = await runSegmentCommand("lsmod", []);
|
|
704
|
+
if (lsmodResult.exitCode === 0 && lsmodResult.stdout.includes("8021q")) {
|
|
705
|
+
dot1qSupported = true;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// Security concerns
|
|
709
|
+
const concerns = [];
|
|
710
|
+
if (vlans.length === 0) {
|
|
711
|
+
concerns.push("No VLAN interfaces detected - network may lack segmentation");
|
|
712
|
+
}
|
|
713
|
+
if (!dot1qSupported) {
|
|
714
|
+
concerns.push("802.1Q VLAN support not detected");
|
|
715
|
+
}
|
|
716
|
+
// Check for promiscuous interfaces (possible trunk port exposure)
|
|
717
|
+
if (linkResult.exitCode === 0 && linkResult.stdout.includes("PROMISC")) {
|
|
718
|
+
concerns.push("Promiscuous interface detected - possible trunk port exposure");
|
|
719
|
+
}
|
|
720
|
+
const vlanAudit = {
|
|
721
|
+
vlans,
|
|
722
|
+
vlanCount: vlans.length,
|
|
723
|
+
vlanConfig: vlanConfig || "Not available",
|
|
724
|
+
dot1qSupported,
|
|
725
|
+
securityConcerns: concerns,
|
|
726
|
+
};
|
|
727
|
+
if (output_format === "json") {
|
|
728
|
+
return { content: [formatToolOutput(vlanAudit)] };
|
|
729
|
+
}
|
|
730
|
+
let text = `=== VLAN Audit ===\n\n`;
|
|
731
|
+
text += `VLANs detected: ${vlans.length}\n`;
|
|
732
|
+
text += `802.1Q support: ${dot1qSupported ? "yes" : "no"}\n\n`;
|
|
733
|
+
if (vlans.length > 0) {
|
|
734
|
+
text += `--- VLAN Interfaces ---\n`;
|
|
735
|
+
for (const v of vlans) {
|
|
736
|
+
text += ` ${v.interface}: VLAN ID ${v.vlanId} (parent: ${v.parentInterface})\n`;
|
|
737
|
+
}
|
|
738
|
+
text += "\n";
|
|
739
|
+
}
|
|
740
|
+
if (vlanConfig) {
|
|
741
|
+
text += `--- VLAN Config ---\n${vlanConfig}\n\n`;
|
|
742
|
+
}
|
|
743
|
+
if (concerns.length > 0) {
|
|
744
|
+
text += `--- Security Concerns ---\n`;
|
|
745
|
+
for (const c of concerns) {
|
|
746
|
+
text += ` ⚠ ${c}\n`;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return { content: [createTextContent(text)] };
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
753
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
default:
|
|
757
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|