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,1398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firewall management tools for Kali Defense MCP Server.
|
|
3
|
+
*
|
|
4
|
+
* Registers 5 tools: firewall_iptables, firewall_ufw, firewall_persist,
|
|
5
|
+
* firewall_nftables_list, firewall_policy_audit.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { executeCommand } from "../core/executor.js";
|
|
9
|
+
import { getConfig, getToolTimeout } from "../core/config.js";
|
|
10
|
+
import { getDistroAdapter } from "../core/distro-adapter.js";
|
|
11
|
+
import { createTextContent, createErrorContent, parseIptablesOutput, formatToolOutput, } from "../core/parsers.js";
|
|
12
|
+
import { logChange, createChangeEntry, backupFile, } from "../core/changelog.js";
|
|
13
|
+
import { validateIptablesChain, validateTarget, sanitizeArgs, validateToolPath, } from "../core/sanitizer.js";
|
|
14
|
+
// ── TOOL-017 remediation: allowed directories for firewall rule file paths ──
|
|
15
|
+
const ALLOWED_FIREWALL_DIRS = ["/etc/iptables", "/etc/nftables", "/etc/ufw", "/tmp", "/var/lib", "/root", "/home"];
|
|
16
|
+
// ── TOOL-003 remediation: strict input validation helpers ──────────────────
|
|
17
|
+
/** Validate a port or port range string for iptables --dport: "80", "8080:8090" */
|
|
18
|
+
const PORT_SPEC_RE = /^\d{1,5}(:\d{1,5})?$/;
|
|
19
|
+
function validatePortSpec(port) {
|
|
20
|
+
if (!PORT_SPEC_RE.test(port)) {
|
|
21
|
+
throw new Error(`Invalid port specification: '${port}'. Must be a number or range (e.g., '80', '8080:8090').`);
|
|
22
|
+
}
|
|
23
|
+
const parts = port.split(":").map(Number);
|
|
24
|
+
for (const p of parts) {
|
|
25
|
+
if (p < 1 || p > 65535) {
|
|
26
|
+
throw new Error(`Port out of range: ${p}. Must be 1-65535.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return port;
|
|
30
|
+
}
|
|
31
|
+
/** Validate an interface name */
|
|
32
|
+
const IFACE_NAME_RE = /^[a-zA-Z0-9._-]+$/;
|
|
33
|
+
function validateInterfaceName(name) {
|
|
34
|
+
if (!name || !IFACE_NAME_RE.test(name)) {
|
|
35
|
+
throw new Error(`Invalid interface name: '${name}'. Only [a-zA-Z0-9._-] allowed.`);
|
|
36
|
+
}
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
/** Allowed match module names for iptables -m */
|
|
40
|
+
const ALLOWED_MATCH_MODULES = new Set([
|
|
41
|
+
"limit", "conntrack", "state", "recent", "multiport", "tcp", "udp",
|
|
42
|
+
"icmp", "comment", "connlimit", "hashlimit", "iprange", "mark",
|
|
43
|
+
"time", "addrtype", "geoip", "string", "owner", "set", "mac",
|
|
44
|
+
]);
|
|
45
|
+
function validateMatchModule(mod) {
|
|
46
|
+
if (!ALLOWED_MATCH_MODULES.has(mod)) {
|
|
47
|
+
throw new Error(`Unknown match module: '${mod}'. Allowed: ${[...ALLOWED_MATCH_MODULES].join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
return mod;
|
|
50
|
+
}
|
|
51
|
+
// ── Table enum shared across iptables tools ────────────────────────────────
|
|
52
|
+
const TABLE_ENUM = z
|
|
53
|
+
.enum(["filter", "nat", "mangle", "raw"])
|
|
54
|
+
.optional()
|
|
55
|
+
.default("filter")
|
|
56
|
+
.describe("Iptables table (default: filter)");
|
|
57
|
+
// ── Custom chain name regex ────────────────────────────────────────────────
|
|
58
|
+
const CHAIN_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_-]{0,28}$/;
|
|
59
|
+
// ── Registration entry point ───────────────────────────────────────────────
|
|
60
|
+
export function registerFirewallTools(server) {
|
|
61
|
+
// ── 1. firewall_iptables (merged: firewall_iptables_list, firewall_iptables_add, firewall_iptables_delete, firewall_set_policy, firewall_create_chain) ──
|
|
62
|
+
server.tool("firewall_iptables", "Manage iptables rules and chains. Actions: list=show rules, add=insert rule, delete=remove rule by number, set_policy=set chain default policy, create_chain=create custom chain", {
|
|
63
|
+
action: z
|
|
64
|
+
.enum(["list", "add", "delete", "set_policy", "create_chain"])
|
|
65
|
+
.describe("Action: list=show rules, add=insert rule, delete=remove rule by number, set_policy=set chain default, create_chain=create custom chain"),
|
|
66
|
+
// Shared params
|
|
67
|
+
table: TABLE_ENUM,
|
|
68
|
+
chain: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Target chain (e.g., INPUT, OUTPUT, FORWARD) — required for list (optional), add, delete"),
|
|
72
|
+
dry_run: z
|
|
73
|
+
.boolean()
|
|
74
|
+
.optional()
|
|
75
|
+
.default(true)
|
|
76
|
+
.describe("Preview changes (for add/delete/set_policy/create_chain)"),
|
|
77
|
+
ipv6: z
|
|
78
|
+
.boolean()
|
|
79
|
+
.optional()
|
|
80
|
+
.default(false)
|
|
81
|
+
.describe("Also apply to ip6tables (for set_policy/create_chain)"),
|
|
82
|
+
// list params
|
|
83
|
+
verbose: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.default(false)
|
|
87
|
+
.describe("Show verbose output with packet/byte counters (for list)"),
|
|
88
|
+
// add params
|
|
89
|
+
protocol: z
|
|
90
|
+
.enum(["tcp", "udp", "icmp", "all"])
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Protocol to match (for add)"),
|
|
93
|
+
source: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Source IP/CIDR to match (for add)"),
|
|
97
|
+
destination: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Destination IP/CIDR to match (for add)"),
|
|
101
|
+
port: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Destination port or port range, e.g. '80', '8080:8090' (for add)"),
|
|
105
|
+
target_action: z
|
|
106
|
+
.enum(["ACCEPT", "DROP", "REJECT", "LOG"])
|
|
107
|
+
.optional()
|
|
108
|
+
.default("DROP")
|
|
109
|
+
.describe("Rule target action (for add, default: DROP)"),
|
|
110
|
+
position: z
|
|
111
|
+
.number()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("Position to insert rule (for add)"),
|
|
114
|
+
match_module: z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("Match module to load, e.g. 'limit', 'conntrack' (for add)"),
|
|
118
|
+
match_options: z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("Options for match module (for add)"),
|
|
122
|
+
tcp_flags: z
|
|
123
|
+
.string()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("TCP flags to match, e.g. '--syn' (for add)"),
|
|
126
|
+
custom_chain: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("Custom chain for -j target, overrides target_action (for add)"),
|
|
130
|
+
// delete params
|
|
131
|
+
rule_number: z
|
|
132
|
+
.number()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Rule number to delete (for delete)"),
|
|
135
|
+
// set_policy params
|
|
136
|
+
policy: z
|
|
137
|
+
.enum(["ACCEPT", "DROP"])
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Default policy to set (for set_policy)"),
|
|
140
|
+
// create_chain params
|
|
141
|
+
chain_name: z
|
|
142
|
+
.string()
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Name of custom chain to create (for create_chain)"),
|
|
145
|
+
}, async (params) => {
|
|
146
|
+
const { action, table, dry_run } = params;
|
|
147
|
+
switch (action) {
|
|
148
|
+
// ── list ──────────────────────────────────────────────────────
|
|
149
|
+
case "list": {
|
|
150
|
+
try {
|
|
151
|
+
const args = ["-t", table, "-L"];
|
|
152
|
+
if (params.chain) {
|
|
153
|
+
const validatedChain = validateIptablesChain(params.chain);
|
|
154
|
+
args.push(validatedChain);
|
|
155
|
+
}
|
|
156
|
+
args.push("-n", "--line-numbers");
|
|
157
|
+
if (params.verbose) {
|
|
158
|
+
args.push("-v");
|
|
159
|
+
}
|
|
160
|
+
sanitizeArgs(args);
|
|
161
|
+
const result = await executeCommand({
|
|
162
|
+
command: "sudo",
|
|
163
|
+
args: ["iptables", ...args],
|
|
164
|
+
toolName: "firewall_iptables",
|
|
165
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
166
|
+
});
|
|
167
|
+
if (result.exitCode !== 0) {
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
createErrorContent(`iptables list failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
171
|
+
],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const parsed = parseIptablesOutput(result.stdout);
|
|
176
|
+
const output = {
|
|
177
|
+
table,
|
|
178
|
+
chain: params.chain ?? "all",
|
|
179
|
+
rules: parsed,
|
|
180
|
+
ruleCount: parsed.length,
|
|
181
|
+
raw: result.stdout,
|
|
182
|
+
};
|
|
183
|
+
return { content: [formatToolOutput(output)] };
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ── add ──────────────────────────────────────────────────────
|
|
191
|
+
case "add": {
|
|
192
|
+
try {
|
|
193
|
+
if (!params.chain) {
|
|
194
|
+
return { content: [createErrorContent("Error: 'chain' is required for add action")], isError: true };
|
|
195
|
+
}
|
|
196
|
+
const validatedChain = validateIptablesChain(params.chain);
|
|
197
|
+
if (params.source)
|
|
198
|
+
validateTarget(params.source);
|
|
199
|
+
if (params.destination)
|
|
200
|
+
validateTarget(params.destination);
|
|
201
|
+
// Validate match_options: only allow alphanumeric, commas, slashes, hyphens, spaces, equals signs
|
|
202
|
+
if (params.match_options && !/^[A-Za-z0-9,\/\-\s=]+$/.test(params.match_options)) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
createErrorContent("match_options contains invalid characters. Only alphanumeric, commas, slashes, hyphens, spaces, and equals signs are allowed."),
|
|
206
|
+
],
|
|
207
|
+
isError: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Validate tcp_flags: only allow --syn or --tcp-flags [A-Z,]+ [A-Z,]+
|
|
211
|
+
if (params.tcp_flags) {
|
|
212
|
+
const isSyn = params.tcp_flags === "--syn";
|
|
213
|
+
const isTcpFlags = /^--tcp-flags\s+[A-Z,]+\s+[A-Z,]+$/.test(params.tcp_flags);
|
|
214
|
+
if (!isSyn && !isTcpFlags) {
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
createErrorContent("tcp_flags must be '--syn' or '--tcp-flags <mask> <comp>' (e.g., '--tcp-flags SYN,ACK SYN')"),
|
|
218
|
+
],
|
|
219
|
+
isError: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Validate custom_chain name
|
|
224
|
+
if (params.custom_chain && !CHAIN_NAME_REGEX.test(params.custom_chain)) {
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
createErrorContent("custom_chain name is invalid. Must match /^[A-Za-z_][A-Za-z0-9_-]{0,28}$/"),
|
|
228
|
+
],
|
|
229
|
+
isError: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const ruleAction = params.target_action ?? "DROP";
|
|
233
|
+
const args = ["-t", table, "-I", validatedChain];
|
|
234
|
+
if (params.position !== undefined) {
|
|
235
|
+
args.push(String(params.position));
|
|
236
|
+
}
|
|
237
|
+
if (params.protocol) {
|
|
238
|
+
args.push("-p", params.protocol);
|
|
239
|
+
}
|
|
240
|
+
if (params.source) {
|
|
241
|
+
args.push("-s", validateTarget(params.source));
|
|
242
|
+
}
|
|
243
|
+
if (params.destination) {
|
|
244
|
+
args.push("-d", validateTarget(params.destination));
|
|
245
|
+
}
|
|
246
|
+
if (params.port) {
|
|
247
|
+
if (!params.protocol || params.protocol === "all") {
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
createErrorContent("Protocol (tcp or udp) must be specified when using --dport"),
|
|
251
|
+
],
|
|
252
|
+
isError: true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// TOOL-003: validate port specification before use
|
|
256
|
+
args.push("--dport", validatePortSpec(params.port));
|
|
257
|
+
}
|
|
258
|
+
// Add match module and options
|
|
259
|
+
if (params.match_module) {
|
|
260
|
+
// TOOL-003: validate match module against whitelist
|
|
261
|
+
args.push("-m", validateMatchModule(params.match_module));
|
|
262
|
+
if (params.match_options) {
|
|
263
|
+
const optTokens = params.match_options.trim().split(/\s+/);
|
|
264
|
+
args.push(...optTokens);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Add TCP flags
|
|
268
|
+
if (params.tcp_flags) {
|
|
269
|
+
const flagTokens = params.tcp_flags.trim().split(/\s+/);
|
|
270
|
+
args.push(...flagTokens);
|
|
271
|
+
}
|
|
272
|
+
// Determine jump target: custom_chain overrides target_action
|
|
273
|
+
const jumpTarget = params.custom_chain ?? ruleAction;
|
|
274
|
+
args.push("-j", jumpTarget);
|
|
275
|
+
sanitizeArgs(args);
|
|
276
|
+
// Build rollback command (delete rule)
|
|
277
|
+
const deleteArgs = ["-t", table, "-D", validatedChain];
|
|
278
|
+
if (params.protocol)
|
|
279
|
+
deleteArgs.push("-p", params.protocol);
|
|
280
|
+
if (params.source)
|
|
281
|
+
deleteArgs.push("-s", params.source);
|
|
282
|
+
if (params.destination)
|
|
283
|
+
deleteArgs.push("-d", params.destination);
|
|
284
|
+
if (params.port)
|
|
285
|
+
deleteArgs.push("--dport", params.port);
|
|
286
|
+
if (params.match_module) {
|
|
287
|
+
deleteArgs.push("-m", params.match_module);
|
|
288
|
+
if (params.match_options) {
|
|
289
|
+
deleteArgs.push(...params.match_options.trim().split(/\s+/));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (params.tcp_flags) {
|
|
293
|
+
deleteArgs.push(...params.tcp_flags.trim().split(/\s+/));
|
|
294
|
+
}
|
|
295
|
+
deleteArgs.push("-j", jumpTarget);
|
|
296
|
+
const rollbackCmd = `sudo iptables ${deleteArgs.join(" ")}`;
|
|
297
|
+
const fullCmd = `sudo iptables ${args.join(" ")}`;
|
|
298
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
299
|
+
const entry = createChangeEntry({
|
|
300
|
+
tool: "firewall_iptables",
|
|
301
|
+
action: `[DRY-RUN] Add iptables rule`,
|
|
302
|
+
target: `${table}/${validatedChain}`,
|
|
303
|
+
after: fullCmd,
|
|
304
|
+
dryRun: true,
|
|
305
|
+
success: true,
|
|
306
|
+
rollbackCommand: rollbackCmd,
|
|
307
|
+
});
|
|
308
|
+
logChange(entry);
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}\n\nRollback command:\n ${rollbackCmd}`),
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const result = await executeCommand({
|
|
316
|
+
command: "sudo",
|
|
317
|
+
args: ["iptables", ...args],
|
|
318
|
+
toolName: "firewall_iptables",
|
|
319
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
320
|
+
});
|
|
321
|
+
const success = result.exitCode === 0;
|
|
322
|
+
const entry = createChangeEntry({
|
|
323
|
+
tool: "firewall_iptables",
|
|
324
|
+
action: `Add iptables rule`,
|
|
325
|
+
target: `${table}/${validatedChain}`,
|
|
326
|
+
after: fullCmd,
|
|
327
|
+
dryRun: false,
|
|
328
|
+
success,
|
|
329
|
+
error: success ? undefined : result.stderr,
|
|
330
|
+
rollbackCommand: rollbackCmd,
|
|
331
|
+
});
|
|
332
|
+
logChange(entry);
|
|
333
|
+
if (!success) {
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
createErrorContent(`iptables add failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
337
|
+
],
|
|
338
|
+
isError: true,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
content: [
|
|
343
|
+
createTextContent(`Rule added successfully.\nCommand: ${fullCmd}\nRollback: ${rollbackCmd}`),
|
|
344
|
+
],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
349
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ── delete ───────────────────────────────────────────────────
|
|
353
|
+
case "delete": {
|
|
354
|
+
try {
|
|
355
|
+
if (!params.chain) {
|
|
356
|
+
return { content: [createErrorContent("Error: 'chain' is required for delete action")], isError: true };
|
|
357
|
+
}
|
|
358
|
+
if (params.rule_number === undefined) {
|
|
359
|
+
return { content: [createErrorContent("Error: 'rule_number' is required for delete action")], isError: true };
|
|
360
|
+
}
|
|
361
|
+
const validatedChain = validateIptablesChain(params.chain);
|
|
362
|
+
const args = ["-t", table, "-D", validatedChain, String(params.rule_number)];
|
|
363
|
+
sanitizeArgs(args);
|
|
364
|
+
const fullCmd = `sudo iptables ${args.join(" ")}`;
|
|
365
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
366
|
+
const entry = createChangeEntry({
|
|
367
|
+
tool: "firewall_iptables",
|
|
368
|
+
action: `[DRY-RUN] Delete iptables rule #${params.rule_number}`,
|
|
369
|
+
target: `${table}/${validatedChain}`,
|
|
370
|
+
dryRun: true,
|
|
371
|
+
success: true,
|
|
372
|
+
});
|
|
373
|
+
logChange(entry);
|
|
374
|
+
return {
|
|
375
|
+
content: [
|
|
376
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}\n\nNote: List rules first with firewall_iptables action=list to confirm rule number.`),
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// Get the rule details before deleting (for changelog)
|
|
381
|
+
const listResult = await executeCommand({
|
|
382
|
+
command: "sudo",
|
|
383
|
+
args: ["iptables", "-t", table, "-L", validatedChain, "-n", "--line-numbers", "-v"],
|
|
384
|
+
toolName: "firewall_iptables",
|
|
385
|
+
});
|
|
386
|
+
const beforeState = listResult.stdout;
|
|
387
|
+
const result = await executeCommand({
|
|
388
|
+
command: "sudo",
|
|
389
|
+
args: ["iptables", ...args],
|
|
390
|
+
toolName: "firewall_iptables",
|
|
391
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
392
|
+
});
|
|
393
|
+
const success = result.exitCode === 0;
|
|
394
|
+
const entry = createChangeEntry({
|
|
395
|
+
tool: "firewall_iptables",
|
|
396
|
+
action: `Delete iptables rule #${params.rule_number}`,
|
|
397
|
+
target: `${table}/${validatedChain}`,
|
|
398
|
+
before: beforeState,
|
|
399
|
+
dryRun: false,
|
|
400
|
+
success,
|
|
401
|
+
error: success ? undefined : result.stderr,
|
|
402
|
+
});
|
|
403
|
+
logChange(entry);
|
|
404
|
+
if (!success) {
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
createErrorContent(`iptables delete failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
408
|
+
],
|
|
409
|
+
isError: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
content: [
|
|
414
|
+
createTextContent(`Rule #${params.rule_number} deleted from ${validatedChain} in ${table} table.`),
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
420
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// ── set_policy ───────────────────────────────────────────────
|
|
424
|
+
case "set_policy": {
|
|
425
|
+
try {
|
|
426
|
+
if (!params.chain) {
|
|
427
|
+
return { content: [createErrorContent("Error: 'chain' is required for set_policy action (INPUT, FORWARD, or OUTPUT)")], isError: true };
|
|
428
|
+
}
|
|
429
|
+
if (!params.policy) {
|
|
430
|
+
return { content: [createErrorContent("Error: 'policy' is required for set_policy action (ACCEPT or DROP)")], isError: true };
|
|
431
|
+
}
|
|
432
|
+
const chain = params.chain;
|
|
433
|
+
const policy = params.policy;
|
|
434
|
+
const ipv6 = params.ipv6 ?? false;
|
|
435
|
+
// Validate chain is a built-in chain
|
|
436
|
+
if (chain !== "INPUT" && chain !== "FORWARD" && chain !== "OUTPUT") {
|
|
437
|
+
return {
|
|
438
|
+
content: [createErrorContent("Error: chain must be INPUT, FORWARD, or OUTPUT for set_policy action")],
|
|
439
|
+
isError: true,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const fullCmd = `sudo iptables -P ${chain} ${policy}`;
|
|
443
|
+
const ipv6Cmd = `sudo ip6tables -P ${chain} ${policy}`;
|
|
444
|
+
// ── SAFETY CHECK: Prevent DROP policy without essential allow rules ──
|
|
445
|
+
if (policy === "DROP" && (chain === "INPUT" || chain === "FORWARD")) {
|
|
446
|
+
const safetyRules = [];
|
|
447
|
+
if (chain === "INPUT") {
|
|
448
|
+
safetyRules.push({
|
|
449
|
+
description: "Allow loopback (lo) traffic",
|
|
450
|
+
checkArgs: ["-C", "INPUT", "-i", "lo", "-j", "ACCEPT"],
|
|
451
|
+
addArgs: ["-I", "INPUT", "1", "-i", "lo", "-j", "ACCEPT"],
|
|
452
|
+
addArgs6: ["-I", "INPUT", "1", "-i", "lo", "-j", "ACCEPT"],
|
|
453
|
+
}, {
|
|
454
|
+
description: "Allow established/related connections",
|
|
455
|
+
checkArgs: ["-C", "INPUT", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"],
|
|
456
|
+
addArgs: ["-I", "INPUT", "2", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"],
|
|
457
|
+
addArgs6: ["-I", "INPUT", "2", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else if (chain === "FORWARD") {
|
|
461
|
+
safetyRules.push({
|
|
462
|
+
description: "Allow established/related forwarded connections",
|
|
463
|
+
checkArgs: ["-C", "FORWARD", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"],
|
|
464
|
+
addArgs: ["-I", "FORWARD", "1", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"],
|
|
465
|
+
addArgs6: ["-I", "FORWARD", "1", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"],
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const injectedRules = [];
|
|
469
|
+
for (const rule of safetyRules) {
|
|
470
|
+
const checkResult = await executeCommand({
|
|
471
|
+
command: "sudo",
|
|
472
|
+
args: ["iptables", ...rule.checkArgs],
|
|
473
|
+
toolName: "firewall_iptables",
|
|
474
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
475
|
+
});
|
|
476
|
+
if (checkResult.exitCode !== 0) {
|
|
477
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
478
|
+
injectedRules.push(`[DRY-RUN] Would add: ${rule.description}`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const addResult = await executeCommand({
|
|
482
|
+
command: "sudo",
|
|
483
|
+
args: ["iptables", ...rule.addArgs],
|
|
484
|
+
toolName: "firewall_iptables",
|
|
485
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
486
|
+
});
|
|
487
|
+
if (addResult.exitCode !== 0) {
|
|
488
|
+
return {
|
|
489
|
+
content: [
|
|
490
|
+
createErrorContent(`SAFETY: Failed to add prerequisite rule "${rule.description}" before setting DROP policy. ` +
|
|
491
|
+
`Aborting to prevent network lockout. Error: ${addResult.stderr}`),
|
|
492
|
+
],
|
|
493
|
+
isError: true,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
injectedRules.push(`✅ Auto-added: ${rule.description}`);
|
|
497
|
+
if (ipv6 && rule.addArgs6) {
|
|
498
|
+
const add6Result = await executeCommand({
|
|
499
|
+
command: "sudo",
|
|
500
|
+
args: ["ip6tables", ...rule.addArgs6],
|
|
501
|
+
toolName: "firewall_iptables",
|
|
502
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
503
|
+
});
|
|
504
|
+
if (add6Result.exitCode !== 0) {
|
|
505
|
+
injectedRules.push(`⚠️ IPv6: Failed to add "${rule.description}": ${add6Result.stderr}`);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
injectedRules.push(`✅ Auto-added (IPv6): ${rule.description}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (injectedRules.length > 0) {
|
|
515
|
+
const safetyEntry = createChangeEntry({
|
|
516
|
+
tool: "firewall_iptables",
|
|
517
|
+
action: `Safety: auto-injected ${injectedRules.length} prerequisite rules before ${chain} DROP`,
|
|
518
|
+
target: chain,
|
|
519
|
+
after: injectedRules.join("; "),
|
|
520
|
+
dryRun: !!(dry_run ?? getConfig().dryRun),
|
|
521
|
+
success: true,
|
|
522
|
+
});
|
|
523
|
+
logChange(safetyEntry);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
527
|
+
const cmds = [fullCmd];
|
|
528
|
+
if (ipv6)
|
|
529
|
+
cmds.push(ipv6Cmd);
|
|
530
|
+
const entry = createChangeEntry({
|
|
531
|
+
tool: "firewall_iptables",
|
|
532
|
+
action: `[DRY-RUN] Set ${chain} policy to ${policy}`,
|
|
533
|
+
target: chain,
|
|
534
|
+
after: cmds.join(" && "),
|
|
535
|
+
dryRun: true,
|
|
536
|
+
success: true,
|
|
537
|
+
});
|
|
538
|
+
logChange(entry);
|
|
539
|
+
return {
|
|
540
|
+
content: [
|
|
541
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${cmds.join("\n ")}`),
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
// Get current policy for rollback
|
|
546
|
+
const listResult = await executeCommand({
|
|
547
|
+
command: "sudo",
|
|
548
|
+
args: ["iptables", "-L", chain, "-n"],
|
|
549
|
+
toolName: "firewall_iptables",
|
|
550
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
551
|
+
});
|
|
552
|
+
const currentPolicyMatch = listResult.stdout.match(/Chain \w+ \(policy (\w+)\)/);
|
|
553
|
+
const currentPolicy = currentPolicyMatch ? currentPolicyMatch[1] : "ACCEPT";
|
|
554
|
+
const rollbackCmd = `sudo iptables -P ${chain} ${currentPolicy}`;
|
|
555
|
+
const result = await executeCommand({
|
|
556
|
+
command: "sudo",
|
|
557
|
+
args: ["iptables", "-P", chain, policy],
|
|
558
|
+
toolName: "firewall_iptables",
|
|
559
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
560
|
+
});
|
|
561
|
+
if (result.exitCode !== 0) {
|
|
562
|
+
const entry = createChangeEntry({
|
|
563
|
+
tool: "firewall_iptables",
|
|
564
|
+
action: `Set ${chain} policy to ${policy}`,
|
|
565
|
+
target: chain,
|
|
566
|
+
dryRun: false,
|
|
567
|
+
success: false,
|
|
568
|
+
error: result.stderr,
|
|
569
|
+
});
|
|
570
|
+
logChange(entry);
|
|
571
|
+
return {
|
|
572
|
+
content: [
|
|
573
|
+
createErrorContent(`iptables set policy failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
574
|
+
],
|
|
575
|
+
isError: true,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const messages = [`IPv4: ${chain} policy set to ${policy}`];
|
|
579
|
+
if (ipv6) {
|
|
580
|
+
const ip6Result = await executeCommand({
|
|
581
|
+
command: "sudo",
|
|
582
|
+
args: ["ip6tables", "-P", chain, policy],
|
|
583
|
+
toolName: "firewall_iptables",
|
|
584
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
585
|
+
});
|
|
586
|
+
if (ip6Result.exitCode !== 0) {
|
|
587
|
+
messages.push(`IPv6: FAILED - ${ip6Result.stderr}`);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
messages.push(`IPv6: ${chain} policy set to ${policy}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const entry = createChangeEntry({
|
|
594
|
+
tool: "firewall_iptables",
|
|
595
|
+
action: `Set ${chain} policy to ${policy}`,
|
|
596
|
+
target: chain,
|
|
597
|
+
before: `policy ${currentPolicy}`,
|
|
598
|
+
after: `policy ${policy}`,
|
|
599
|
+
dryRun: false,
|
|
600
|
+
success: true,
|
|
601
|
+
rollbackCommand: rollbackCmd,
|
|
602
|
+
});
|
|
603
|
+
logChange(entry);
|
|
604
|
+
return {
|
|
605
|
+
content: [
|
|
606
|
+
createTextContent(`Policy updated successfully.\n${messages.join("\n")}\nRollback: ${rollbackCmd}`),
|
|
607
|
+
],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
612
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// ── create_chain ─────────────────────────────────────────────
|
|
616
|
+
case "create_chain": {
|
|
617
|
+
try {
|
|
618
|
+
if (!params.chain_name) {
|
|
619
|
+
return { content: [createErrorContent("Error: 'chain_name' is required for create_chain action")], isError: true };
|
|
620
|
+
}
|
|
621
|
+
const chain_name = params.chain_name;
|
|
622
|
+
const ipv6 = params.ipv6 ?? false;
|
|
623
|
+
if (!CHAIN_NAME_REGEX.test(chain_name)) {
|
|
624
|
+
return {
|
|
625
|
+
content: [
|
|
626
|
+
createErrorContent(`Invalid chain name '${chain_name}'. Must match /^[A-Za-z_][A-Za-z0-9_-]{0,28}$/`),
|
|
627
|
+
],
|
|
628
|
+
isError: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const fullCmd = `sudo iptables -N ${chain_name}`;
|
|
632
|
+
const ipv6Cmd = `sudo ip6tables -N ${chain_name}`;
|
|
633
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
634
|
+
const cmds = [fullCmd];
|
|
635
|
+
if (ipv6)
|
|
636
|
+
cmds.push(ipv6Cmd);
|
|
637
|
+
const entry = createChangeEntry({
|
|
638
|
+
tool: "firewall_iptables",
|
|
639
|
+
action: `[DRY-RUN] Create custom chain ${chain_name}`,
|
|
640
|
+
target: chain_name,
|
|
641
|
+
after: cmds.join(" && "),
|
|
642
|
+
dryRun: true,
|
|
643
|
+
success: true,
|
|
644
|
+
});
|
|
645
|
+
logChange(entry);
|
|
646
|
+
return {
|
|
647
|
+
content: [
|
|
648
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${cmds.join("\n ")}`),
|
|
649
|
+
],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const result = await executeCommand({
|
|
653
|
+
command: "sudo",
|
|
654
|
+
args: ["iptables", "-N", chain_name],
|
|
655
|
+
toolName: "firewall_iptables",
|
|
656
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
657
|
+
});
|
|
658
|
+
if (result.exitCode !== 0) {
|
|
659
|
+
const entry = createChangeEntry({
|
|
660
|
+
tool: "firewall_iptables",
|
|
661
|
+
action: `Create custom chain ${chain_name}`,
|
|
662
|
+
target: chain_name,
|
|
663
|
+
dryRun: false,
|
|
664
|
+
success: false,
|
|
665
|
+
error: result.stderr,
|
|
666
|
+
});
|
|
667
|
+
logChange(entry);
|
|
668
|
+
return {
|
|
669
|
+
content: [
|
|
670
|
+
createErrorContent(`iptables create chain failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
671
|
+
],
|
|
672
|
+
isError: true,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const rollbackCmd = `sudo iptables -X ${chain_name}`;
|
|
676
|
+
const messages = [`IPv4: Chain '${chain_name}' created`];
|
|
677
|
+
if (ipv6) {
|
|
678
|
+
const ip6Result = await executeCommand({
|
|
679
|
+
command: "sudo",
|
|
680
|
+
args: ["ip6tables", "-N", chain_name],
|
|
681
|
+
toolName: "firewall_iptables",
|
|
682
|
+
timeout: getToolTimeout("firewall_iptables"),
|
|
683
|
+
});
|
|
684
|
+
if (ip6Result.exitCode !== 0) {
|
|
685
|
+
messages.push(`IPv6: FAILED - ${ip6Result.stderr}`);
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
messages.push(`IPv6: Chain '${chain_name}' created`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const entry = createChangeEntry({
|
|
692
|
+
tool: "firewall_iptables",
|
|
693
|
+
action: `Create custom chain ${chain_name}`,
|
|
694
|
+
target: chain_name,
|
|
695
|
+
dryRun: false,
|
|
696
|
+
success: true,
|
|
697
|
+
rollbackCommand: rollbackCmd,
|
|
698
|
+
});
|
|
699
|
+
logChange(entry);
|
|
700
|
+
return {
|
|
701
|
+
content: [
|
|
702
|
+
createTextContent(`Chain created successfully.\n${messages.join("\n")}\nRollback: ${rollbackCmd}`),
|
|
703
|
+
],
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
708
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
default:
|
|
712
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
// ── 2. firewall_ufw (merged: firewall_ufw_status, firewall_ufw_rule) ────
|
|
716
|
+
server.tool("firewall_ufw", "Manage UFW (Uncomplicated Firewall). Actions: status=show current status/rules, add=add a rule, delete=delete a rule", {
|
|
717
|
+
action: z
|
|
718
|
+
.enum(["status", "add", "delete"])
|
|
719
|
+
.describe("Action: status=show status, add=add rule, delete=delete rule"),
|
|
720
|
+
// status params
|
|
721
|
+
verbose: z
|
|
722
|
+
.boolean()
|
|
723
|
+
.optional()
|
|
724
|
+
.default(false)
|
|
725
|
+
.describe("Show verbose status including logging and default policies (for status)"),
|
|
726
|
+
// add/delete params
|
|
727
|
+
rule_action: z
|
|
728
|
+
.enum(["allow", "deny", "reject", "limit"])
|
|
729
|
+
.optional()
|
|
730
|
+
.describe("Rule action (required for add/delete)"),
|
|
731
|
+
direction: z
|
|
732
|
+
.enum(["in", "out"])
|
|
733
|
+
.optional()
|
|
734
|
+
.default("in")
|
|
735
|
+
.describe("Traffic direction (for add/delete, default: in)"),
|
|
736
|
+
port: z
|
|
737
|
+
.string()
|
|
738
|
+
.optional()
|
|
739
|
+
.describe("Port number or range, e.g. '22', '8000:9000' (for add/delete)"),
|
|
740
|
+
protocol: z
|
|
741
|
+
.enum(["tcp", "udp", "any"])
|
|
742
|
+
.optional()
|
|
743
|
+
.describe("Protocol (for add/delete)"),
|
|
744
|
+
from_addr: z
|
|
745
|
+
.string()
|
|
746
|
+
.optional()
|
|
747
|
+
.describe("Source address or 'any' (for add/delete)"),
|
|
748
|
+
to_addr: z
|
|
749
|
+
.string()
|
|
750
|
+
.optional()
|
|
751
|
+
.describe("Destination address or 'any' (for add/delete)"),
|
|
752
|
+
dry_run: z
|
|
753
|
+
.boolean()
|
|
754
|
+
.optional()
|
|
755
|
+
.default(true)
|
|
756
|
+
.describe("Preview the command without executing (for add/delete)"),
|
|
757
|
+
}, async (params) => {
|
|
758
|
+
const { action } = params;
|
|
759
|
+
switch (action) {
|
|
760
|
+
// ── status ───────────────────────────────────────────────────
|
|
761
|
+
case "status": {
|
|
762
|
+
try {
|
|
763
|
+
const args = ["ufw", "status"];
|
|
764
|
+
if (params.verbose) {
|
|
765
|
+
args.push("verbose");
|
|
766
|
+
}
|
|
767
|
+
const result = await executeCommand({
|
|
768
|
+
command: "sudo",
|
|
769
|
+
args,
|
|
770
|
+
toolName: "firewall_ufw",
|
|
771
|
+
timeout: getToolTimeout("firewall_ufw"),
|
|
772
|
+
});
|
|
773
|
+
if (result.exitCode !== 0) {
|
|
774
|
+
return {
|
|
775
|
+
content: [
|
|
776
|
+
createErrorContent(`ufw status failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
777
|
+
],
|
|
778
|
+
isError: true,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
return { content: [createTextContent(result.stdout)] };
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
785
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// ── add / delete ─────────────────────────────────────────────
|
|
789
|
+
case "add":
|
|
790
|
+
case "delete": {
|
|
791
|
+
try {
|
|
792
|
+
if (!params.rule_action) {
|
|
793
|
+
return { content: [createErrorContent("Error: 'rule_action' is required for add/delete actions (allow, deny, reject, limit)")], isError: true };
|
|
794
|
+
}
|
|
795
|
+
const ruleAction = params.rule_action;
|
|
796
|
+
const direction = params.direction ?? "in";
|
|
797
|
+
const deleteRule = action === "delete";
|
|
798
|
+
if (params.from_addr && params.from_addr !== "any")
|
|
799
|
+
validateTarget(params.from_addr);
|
|
800
|
+
if (params.to_addr && params.to_addr !== "any")
|
|
801
|
+
validateTarget(params.to_addr);
|
|
802
|
+
const args = ["ufw"];
|
|
803
|
+
if (deleteRule) {
|
|
804
|
+
args.push("delete");
|
|
805
|
+
}
|
|
806
|
+
args.push(ruleAction, direction);
|
|
807
|
+
if (params.protocol && params.protocol !== "any") {
|
|
808
|
+
args.push("proto", params.protocol);
|
|
809
|
+
}
|
|
810
|
+
if (params.from_addr) {
|
|
811
|
+
args.push("from", params.from_addr);
|
|
812
|
+
}
|
|
813
|
+
if (params.to_addr) {
|
|
814
|
+
args.push("to", params.to_addr);
|
|
815
|
+
}
|
|
816
|
+
if (params.port) {
|
|
817
|
+
args.push("port", params.port);
|
|
818
|
+
}
|
|
819
|
+
sanitizeArgs(args);
|
|
820
|
+
const fullCmd = `sudo ${args.join(" ")}`;
|
|
821
|
+
if (params.dry_run ?? getConfig().dryRun) {
|
|
822
|
+
const entry = createChangeEntry({
|
|
823
|
+
tool: "firewall_ufw",
|
|
824
|
+
action: `[DRY-RUN] ${deleteRule ? "Delete" : "Add"} UFW rule`,
|
|
825
|
+
target: `ufw/${ruleAction}/${direction}`,
|
|
826
|
+
after: fullCmd,
|
|
827
|
+
dryRun: true,
|
|
828
|
+
success: true,
|
|
829
|
+
});
|
|
830
|
+
logChange(entry);
|
|
831
|
+
return {
|
|
832
|
+
content: [
|
|
833
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}`),
|
|
834
|
+
],
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// Use --force to avoid interactive prompt
|
|
838
|
+
const execArgs = [...args];
|
|
839
|
+
if (!deleteRule) {
|
|
840
|
+
execArgs.splice(1, 0, "--force");
|
|
841
|
+
}
|
|
842
|
+
const result = await executeCommand({
|
|
843
|
+
command: "sudo",
|
|
844
|
+
args: execArgs,
|
|
845
|
+
toolName: "firewall_ufw",
|
|
846
|
+
timeout: getToolTimeout("firewall_ufw"),
|
|
847
|
+
});
|
|
848
|
+
const success = result.exitCode === 0;
|
|
849
|
+
// Build rollback: invert the operation
|
|
850
|
+
const rollbackArgs = ["sudo", "ufw"];
|
|
851
|
+
if (!deleteRule) {
|
|
852
|
+
rollbackArgs.push("delete");
|
|
853
|
+
}
|
|
854
|
+
rollbackArgs.push(ruleAction, direction);
|
|
855
|
+
if (params.protocol && params.protocol !== "any")
|
|
856
|
+
rollbackArgs.push("proto", params.protocol);
|
|
857
|
+
if (params.from_addr)
|
|
858
|
+
rollbackArgs.push("from", params.from_addr);
|
|
859
|
+
if (params.to_addr)
|
|
860
|
+
rollbackArgs.push("to", params.to_addr);
|
|
861
|
+
if (params.port)
|
|
862
|
+
rollbackArgs.push("port", params.port);
|
|
863
|
+
const rollbackCmd = rollbackArgs.join(" ");
|
|
864
|
+
const entry = createChangeEntry({
|
|
865
|
+
tool: "firewall_ufw",
|
|
866
|
+
action: `${deleteRule ? "Delete" : "Add"} UFW rule`,
|
|
867
|
+
target: `ufw/${ruleAction}/${direction}`,
|
|
868
|
+
after: fullCmd,
|
|
869
|
+
dryRun: false,
|
|
870
|
+
success,
|
|
871
|
+
error: success ? undefined : result.stderr,
|
|
872
|
+
rollbackCommand: rollbackCmd,
|
|
873
|
+
});
|
|
874
|
+
logChange(entry);
|
|
875
|
+
if (!success) {
|
|
876
|
+
return {
|
|
877
|
+
content: [
|
|
878
|
+
createErrorContent(`UFW rule failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
879
|
+
],
|
|
880
|
+
isError: true,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
content: [
|
|
885
|
+
createTextContent(`UFW rule ${deleteRule ? "deleted" : "added"} successfully.\nCommand: ${fullCmd}\nRollback: ${rollbackCmd}\n\n${result.stdout}`),
|
|
886
|
+
],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
891
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
default:
|
|
895
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
// ── 3. firewall_persist (merged: firewall_save, firewall_restore, firewall_persistence) ──
|
|
899
|
+
server.tool("firewall_persist", "Manage firewall rule persistence. Actions: save=save iptables rules to file, restore=restore rules from file, enable=install persistence package, status=check persistence status", {
|
|
900
|
+
action: z
|
|
901
|
+
.enum(["save", "restore", "enable", "status"])
|
|
902
|
+
.describe("Action: save=save rules to file, restore=restore from file, enable=install persistence, status=check status"),
|
|
903
|
+
// save params
|
|
904
|
+
output_path: z
|
|
905
|
+
.string()
|
|
906
|
+
.optional()
|
|
907
|
+
.default("/etc/iptables/rules.v4")
|
|
908
|
+
.describe("Output file path for save (default: /etc/iptables/rules.v4)"),
|
|
909
|
+
// save/restore params
|
|
910
|
+
ipv6: z
|
|
911
|
+
.boolean()
|
|
912
|
+
.optional()
|
|
913
|
+
.default(false)
|
|
914
|
+
.describe("Use ip6tables instead of iptables (for save/restore)"),
|
|
915
|
+
// restore params
|
|
916
|
+
input_path: z
|
|
917
|
+
.string()
|
|
918
|
+
.optional()
|
|
919
|
+
.describe("Path to rules file to restore from (required for restore)"),
|
|
920
|
+
test_only: z
|
|
921
|
+
.boolean()
|
|
922
|
+
.optional()
|
|
923
|
+
.default(true)
|
|
924
|
+
.describe("Only test/validate rules file without applying (for restore, default: true)"),
|
|
925
|
+
// shared
|
|
926
|
+
dry_run: z
|
|
927
|
+
.boolean()
|
|
928
|
+
.optional()
|
|
929
|
+
.default(true)
|
|
930
|
+
.describe("Preview the command without executing"),
|
|
931
|
+
}, async (params) => {
|
|
932
|
+
const { action, dry_run } = params;
|
|
933
|
+
switch (action) {
|
|
934
|
+
// ── save ─────────────────────────────────────────────────────
|
|
935
|
+
case "save": {
|
|
936
|
+
try {
|
|
937
|
+
const output_path = params.output_path ?? "/etc/iptables/rules.v4";
|
|
938
|
+
// TOOL-017: Validate output path against traversal and allowed dirs
|
|
939
|
+
validateToolPath(output_path, ALLOWED_FIREWALL_DIRS, "Firewall rules output path");
|
|
940
|
+
const ipv6 = params.ipv6 ?? false;
|
|
941
|
+
const saveCmd = ipv6 ? "ip6tables-save" : "iptables-save";
|
|
942
|
+
const fullCmd = `sudo ${saveCmd} > ${output_path}`;
|
|
943
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
944
|
+
const entry = createChangeEntry({
|
|
945
|
+
tool: "firewall_persist",
|
|
946
|
+
action: `[DRY-RUN] Save firewall rules`,
|
|
947
|
+
target: output_path,
|
|
948
|
+
after: fullCmd,
|
|
949
|
+
dryRun: true,
|
|
950
|
+
success: true,
|
|
951
|
+
});
|
|
952
|
+
logChange(entry);
|
|
953
|
+
return {
|
|
954
|
+
content: [
|
|
955
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}\n\nThis would save current ${ipv6 ? "ip6tables" : "iptables"} rules to ${output_path}`),
|
|
956
|
+
],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
// Backup existing file if it exists
|
|
960
|
+
let backupPath;
|
|
961
|
+
try {
|
|
962
|
+
backupPath = backupFile(output_path);
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
// File may not exist yet, that's fine
|
|
966
|
+
}
|
|
967
|
+
// Get current rules
|
|
968
|
+
const result = await executeCommand({
|
|
969
|
+
command: "sudo",
|
|
970
|
+
args: [saveCmd],
|
|
971
|
+
toolName: "firewall_persist",
|
|
972
|
+
timeout: getToolTimeout("firewall_persist"),
|
|
973
|
+
});
|
|
974
|
+
if (result.exitCode !== 0) {
|
|
975
|
+
return {
|
|
976
|
+
content: [
|
|
977
|
+
createErrorContent(`${saveCmd} failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
978
|
+
],
|
|
979
|
+
isError: true,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
// Write rules to file using tee (handles permissions)
|
|
983
|
+
const writeResult = await executeCommand({
|
|
984
|
+
command: "sudo",
|
|
985
|
+
args: ["tee", output_path],
|
|
986
|
+
stdin: result.stdout,
|
|
987
|
+
toolName: "firewall_persist",
|
|
988
|
+
timeout: getToolTimeout("firewall_persist"),
|
|
989
|
+
});
|
|
990
|
+
const success = writeResult.exitCode === 0;
|
|
991
|
+
const entry = createChangeEntry({
|
|
992
|
+
tool: "firewall_persist",
|
|
993
|
+
action: `Save firewall rules`,
|
|
994
|
+
target: output_path,
|
|
995
|
+
after: fullCmd,
|
|
996
|
+
backupPath,
|
|
997
|
+
dryRun: false,
|
|
998
|
+
success,
|
|
999
|
+
error: success ? undefined : writeResult.stderr,
|
|
1000
|
+
rollbackCommand: backupPath
|
|
1001
|
+
? `sudo cp ${backupPath} ${output_path}`
|
|
1002
|
+
: undefined,
|
|
1003
|
+
});
|
|
1004
|
+
logChange(entry);
|
|
1005
|
+
if (!success) {
|
|
1006
|
+
return {
|
|
1007
|
+
content: [
|
|
1008
|
+
createErrorContent(`Failed to write rules to ${output_path}: ${writeResult.stderr}`),
|
|
1009
|
+
],
|
|
1010
|
+
isError: true,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
content: [
|
|
1015
|
+
createTextContent(`Firewall rules saved to ${output_path}.${backupPath ? `\nBackup: ${backupPath}` : ""}\nRules:\n${result.stdout}`),
|
|
1016
|
+
],
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
catch (err) {
|
|
1020
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1021
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// ── restore ──────────────────────────────────────────────────
|
|
1025
|
+
case "restore": {
|
|
1026
|
+
try {
|
|
1027
|
+
if (!params.input_path) {
|
|
1028
|
+
return { content: [createErrorContent("Error: 'input_path' is required for restore action")], isError: true };
|
|
1029
|
+
}
|
|
1030
|
+
const input_path = params.input_path;
|
|
1031
|
+
// TOOL-017: Validate input path against traversal and allowed dirs
|
|
1032
|
+
validateToolPath(input_path, ALLOWED_FIREWALL_DIRS, "Firewall rules input path");
|
|
1033
|
+
const ipv6 = params.ipv6 ?? false;
|
|
1034
|
+
const test_only = params.test_only ?? true;
|
|
1035
|
+
const restoreCmd = ipv6 ? "ip6tables-restore" : "iptables-restore";
|
|
1036
|
+
const args = [restoreCmd];
|
|
1037
|
+
if (test_only) {
|
|
1038
|
+
args.push("--test");
|
|
1039
|
+
}
|
|
1040
|
+
const fullCmd = `sudo ${args.join(" ")} < ${input_path}`;
|
|
1041
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
1042
|
+
const entry = createChangeEntry({
|
|
1043
|
+
tool: "firewall_persist",
|
|
1044
|
+
action: `[DRY-RUN] Restore firewall rules`,
|
|
1045
|
+
target: input_path,
|
|
1046
|
+
after: fullCmd,
|
|
1047
|
+
dryRun: true,
|
|
1048
|
+
success: true,
|
|
1049
|
+
});
|
|
1050
|
+
logChange(entry);
|
|
1051
|
+
return {
|
|
1052
|
+
content: [
|
|
1053
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}\n\n${test_only ? "This would only validate the rules file." : "This would apply all rules from the file."}`),
|
|
1054
|
+
],
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
// Read the rules file content first
|
|
1058
|
+
const catResult = await executeCommand({
|
|
1059
|
+
command: "sudo",
|
|
1060
|
+
args: ["cat", input_path],
|
|
1061
|
+
toolName: "firewall_persist",
|
|
1062
|
+
});
|
|
1063
|
+
if (catResult.exitCode !== 0) {
|
|
1064
|
+
return {
|
|
1065
|
+
content: [
|
|
1066
|
+
createErrorContent(`Cannot read rules file ${input_path}: ${catResult.stderr}`),
|
|
1067
|
+
],
|
|
1068
|
+
isError: true,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
// Save current rules before restoring (for rollback)
|
|
1072
|
+
let beforeState;
|
|
1073
|
+
if (!test_only) {
|
|
1074
|
+
const saveCmdStr = ipv6 ? "ip6tables-save" : "iptables-save";
|
|
1075
|
+
const saveResult = await executeCommand({
|
|
1076
|
+
command: "sudo",
|
|
1077
|
+
args: [saveCmdStr],
|
|
1078
|
+
toolName: "firewall_persist",
|
|
1079
|
+
});
|
|
1080
|
+
beforeState = saveResult.stdout;
|
|
1081
|
+
}
|
|
1082
|
+
const result = await executeCommand({
|
|
1083
|
+
command: "sudo",
|
|
1084
|
+
args,
|
|
1085
|
+
stdin: catResult.stdout,
|
|
1086
|
+
toolName: "firewall_persist",
|
|
1087
|
+
timeout: getToolTimeout("firewall_persist"),
|
|
1088
|
+
});
|
|
1089
|
+
const success = result.exitCode === 0;
|
|
1090
|
+
const entry = createChangeEntry({
|
|
1091
|
+
tool: "firewall_persist",
|
|
1092
|
+
action: `${test_only ? "Test" : "Restore"} firewall rules`,
|
|
1093
|
+
target: input_path,
|
|
1094
|
+
before: beforeState,
|
|
1095
|
+
dryRun: false,
|
|
1096
|
+
success,
|
|
1097
|
+
error: success ? undefined : result.stderr,
|
|
1098
|
+
});
|
|
1099
|
+
logChange(entry);
|
|
1100
|
+
if (!success) {
|
|
1101
|
+
return {
|
|
1102
|
+
content: [
|
|
1103
|
+
createErrorContent(`${restoreCmd} failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
1104
|
+
],
|
|
1105
|
+
isError: true,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
content: [
|
|
1110
|
+
createTextContent(test_only
|
|
1111
|
+
? `Rules file ${input_path} validated successfully.`
|
|
1112
|
+
: `Firewall rules restored from ${input_path}.\n${result.stdout}`),
|
|
1113
|
+
],
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1118
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// ── enable ───────────────────────────────────────────────────
|
|
1122
|
+
case "enable": {
|
|
1123
|
+
try {
|
|
1124
|
+
const da = await getDistroAdapter();
|
|
1125
|
+
const fwp = da.fwPersistence;
|
|
1126
|
+
const installDesc = `sudo ${fwp.installCmd.join(" ")}`;
|
|
1127
|
+
const enableDesc = `sudo ${fwp.enableCmd.join(" ")}`;
|
|
1128
|
+
const cmds = [installDesc, enableDesc];
|
|
1129
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
1130
|
+
const entry = createChangeEntry({
|
|
1131
|
+
tool: "firewall_persist",
|
|
1132
|
+
action: `[DRY-RUN] Enable ${fwp.packageName}`,
|
|
1133
|
+
target: fwp.packageName,
|
|
1134
|
+
after: cmds.join(" && "),
|
|
1135
|
+
dryRun: true,
|
|
1136
|
+
success: true,
|
|
1137
|
+
});
|
|
1138
|
+
logChange(entry);
|
|
1139
|
+
return {
|
|
1140
|
+
content: [
|
|
1141
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${cmds.join("\n ")}`),
|
|
1142
|
+
],
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
// Install the persistence package (distro-aware)
|
|
1146
|
+
const installResult = await executeCommand({
|
|
1147
|
+
command: "sudo",
|
|
1148
|
+
args: fwp.installCmd,
|
|
1149
|
+
toolName: "firewall_persist",
|
|
1150
|
+
timeout: 120000,
|
|
1151
|
+
env: da.isDebian ? { DEBIAN_FRONTEND: "noninteractive" } : undefined,
|
|
1152
|
+
});
|
|
1153
|
+
let installSuccess = installResult.exitCode === 0;
|
|
1154
|
+
if (!installSuccess && da.isDebian) {
|
|
1155
|
+
const installResult2 = await executeCommand({
|
|
1156
|
+
command: "sudo",
|
|
1157
|
+
args: fwp.installCmd,
|
|
1158
|
+
env: { DEBIAN_FRONTEND: "noninteractive" },
|
|
1159
|
+
toolName: "firewall_persist",
|
|
1160
|
+
timeout: 120000,
|
|
1161
|
+
});
|
|
1162
|
+
installSuccess = installResult2.exitCode === 0;
|
|
1163
|
+
}
|
|
1164
|
+
if (!installSuccess) {
|
|
1165
|
+
const entry = createChangeEntry({
|
|
1166
|
+
tool: "firewall_persist",
|
|
1167
|
+
action: `Enable ${fwp.packageName}`,
|
|
1168
|
+
target: fwp.packageName,
|
|
1169
|
+
dryRun: false,
|
|
1170
|
+
success: false,
|
|
1171
|
+
error: installResult.stderr,
|
|
1172
|
+
});
|
|
1173
|
+
logChange(entry);
|
|
1174
|
+
return {
|
|
1175
|
+
content: [
|
|
1176
|
+
createErrorContent(`Failed to install ${fwp.packageName}: ${installResult.stderr}`),
|
|
1177
|
+
],
|
|
1178
|
+
isError: true,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
// Enable the service
|
|
1182
|
+
const enableResult = await executeCommand({
|
|
1183
|
+
command: "sudo",
|
|
1184
|
+
args: fwp.enableCmd,
|
|
1185
|
+
toolName: "firewall_persist",
|
|
1186
|
+
timeout: 15000,
|
|
1187
|
+
});
|
|
1188
|
+
const entry = createChangeEntry({
|
|
1189
|
+
tool: "firewall_persist",
|
|
1190
|
+
action: `Enable ${fwp.packageName}`,
|
|
1191
|
+
target: fwp.packageName,
|
|
1192
|
+
dryRun: false,
|
|
1193
|
+
success: true,
|
|
1194
|
+
rollbackCommand: fwp.uninstallHint,
|
|
1195
|
+
});
|
|
1196
|
+
logChange(entry);
|
|
1197
|
+
return {
|
|
1198
|
+
content: [
|
|
1199
|
+
createTextContent(`${fwp.packageName} installed and ${fwp.serviceName} service enabled.\n` +
|
|
1200
|
+
`Service status: ${enableResult.exitCode === 0 ? "enabled" : "enable may have failed: " + enableResult.stderr}\n` +
|
|
1201
|
+
`Use firewall_persist with action='save' to persist current rules.`),
|
|
1202
|
+
],
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
catch (err) {
|
|
1206
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1207
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// ── status ───────────────────────────────────────────────────
|
|
1211
|
+
case "status": {
|
|
1212
|
+
try {
|
|
1213
|
+
const da = await getDistroAdapter();
|
|
1214
|
+
const fwp = da.fwPersistence;
|
|
1215
|
+
// Check if persistence package is installed (distro-aware)
|
|
1216
|
+
const pkgCheckResult = await executeCommand({
|
|
1217
|
+
command: fwp.checkInstalledCmd[0],
|
|
1218
|
+
args: fwp.checkInstalledCmd.slice(1),
|
|
1219
|
+
toolName: "firewall_persist",
|
|
1220
|
+
timeout: 5000,
|
|
1221
|
+
});
|
|
1222
|
+
const installed = da.isDebian
|
|
1223
|
+
? pkgCheckResult.stdout.includes("ii")
|
|
1224
|
+
: pkgCheckResult.exitCode === 0;
|
|
1225
|
+
// Check if persistence service is enabled
|
|
1226
|
+
const svcResult = await executeCommand({
|
|
1227
|
+
command: "systemctl",
|
|
1228
|
+
args: ["is-enabled", fwp.serviceName],
|
|
1229
|
+
toolName: "firewall_persist",
|
|
1230
|
+
timeout: 5000,
|
|
1231
|
+
});
|
|
1232
|
+
const enabled = svcResult.stdout.trim() === "enabled";
|
|
1233
|
+
// Check if rules file exists
|
|
1234
|
+
const rulesResult = await executeCommand({
|
|
1235
|
+
command: "test",
|
|
1236
|
+
args: ["-f", da.paths.firewallPersistenceConfig],
|
|
1237
|
+
toolName: "firewall_persist",
|
|
1238
|
+
timeout: 3000,
|
|
1239
|
+
});
|
|
1240
|
+
const status = {
|
|
1241
|
+
distro: da.summary,
|
|
1242
|
+
persistence_package: fwp.packageName,
|
|
1243
|
+
package_installed: installed,
|
|
1244
|
+
service_enabled: enabled,
|
|
1245
|
+
service_name: fwp.serviceName,
|
|
1246
|
+
rules_file_exists: rulesResult.exitCode === 0,
|
|
1247
|
+
rules_file_path: da.paths.firewallPersistenceConfig,
|
|
1248
|
+
recommendation: !installed
|
|
1249
|
+
? `Use firewall_persist with action='enable' to install ${fwp.packageName}`
|
|
1250
|
+
: !enabled
|
|
1251
|
+
? `Run: sudo systemctl enable ${fwp.serviceName}`
|
|
1252
|
+
: "Persistence is properly configured",
|
|
1253
|
+
};
|
|
1254
|
+
return {
|
|
1255
|
+
content: [createTextContent(JSON.stringify(status, null, 2))],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
catch (err) {
|
|
1259
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1260
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
default:
|
|
1264
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
// ── 4. firewall_nftables_list ──────────────────────────────────────────────
|
|
1268
|
+
// TOOL-008: Added table name validation
|
|
1269
|
+
const NFTABLES_TABLE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
1270
|
+
server.tool("firewall_nftables_list", "List nftables ruleset. nftables is the modern replacement for iptables on Linux systems.", {
|
|
1271
|
+
table: z.string().optional().describe("Specific table name to list"),
|
|
1272
|
+
family: z.enum(["ip", "ip6", "inet", "arp", "bridge", "netdev"]).optional().describe("Address family"),
|
|
1273
|
+
}, async (params) => {
|
|
1274
|
+
try {
|
|
1275
|
+
const args = ["list", "ruleset"];
|
|
1276
|
+
if (params.table && params.family) {
|
|
1277
|
+
// TOOL-008: Validate nftables table name
|
|
1278
|
+
if (!NFTABLES_TABLE_NAME_RE.test(params.table)) {
|
|
1279
|
+
return { content: [createErrorContent(`Invalid nftables table name: '${params.table}'. Must match /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/ (start with letter, no whitespace or special characters).`)], isError: true };
|
|
1280
|
+
}
|
|
1281
|
+
args.length = 0;
|
|
1282
|
+
args.push("list", "table", params.family, params.table);
|
|
1283
|
+
}
|
|
1284
|
+
const result = await executeCommand({ command: "sudo", args: ["nft", ...args], timeout: 15000, toolName: "firewall_nftables_list" });
|
|
1285
|
+
if (result.exitCode !== 0) {
|
|
1286
|
+
if (result.stderr.includes("not found")) {
|
|
1287
|
+
return { content: [createErrorContent("nftables (nft) is not installed. Install with: sudo apt install nftables")], isError: true };
|
|
1288
|
+
}
|
|
1289
|
+
return { content: [createErrorContent(`nft list failed (exit ${result.exitCode}): ${result.stderr}`)], isError: true };
|
|
1290
|
+
}
|
|
1291
|
+
return { content: [createTextContent(result.stdout || "No nftables rules configured")] };
|
|
1292
|
+
}
|
|
1293
|
+
catch (error) {
|
|
1294
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
// ── 5. firewall_policy_audit (kept as-is) ─────────────────────────────────
|
|
1298
|
+
server.tool("firewall_policy_audit", "Audit firewall configuration for security issues: default chain policies, missing rules, and common misconfigurations.", {}, async () => {
|
|
1299
|
+
try {
|
|
1300
|
+
const findings = [];
|
|
1301
|
+
// Check iptables default policies
|
|
1302
|
+
const iptResult = await executeCommand({ command: "sudo", args: ["iptables", "-L", "-n"], timeout: 10000, toolName: "firewall_policy_audit" });
|
|
1303
|
+
if (iptResult.exitCode === 0) {
|
|
1304
|
+
const output = iptResult.stdout;
|
|
1305
|
+
// Check INPUT policy
|
|
1306
|
+
const inputMatch = output.match(/Chain INPUT \(policy (\w+)\)/);
|
|
1307
|
+
if (inputMatch) {
|
|
1308
|
+
const isSecure = inputMatch[1] === "DROP" || inputMatch[1] === "REJECT";
|
|
1309
|
+
findings.push({
|
|
1310
|
+
check: "iptables_input_policy",
|
|
1311
|
+
status: isSecure ? "PASS" : "FAIL",
|
|
1312
|
+
value: inputMatch[1],
|
|
1313
|
+
description: "INPUT chain default policy (should be DROP)",
|
|
1314
|
+
recommendation: isSecure
|
|
1315
|
+
? undefined
|
|
1316
|
+
: "Use firewall_iptables action=set_policy chain=INPUT policy=DROP ipv6=true",
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
// Check FORWARD policy
|
|
1320
|
+
const fwdMatch = output.match(/Chain FORWARD \(policy (\w+)\)/);
|
|
1321
|
+
if (fwdMatch) {
|
|
1322
|
+
const isSecure = fwdMatch[1] === "DROP" || fwdMatch[1] === "REJECT";
|
|
1323
|
+
findings.push({
|
|
1324
|
+
check: "iptables_forward_policy",
|
|
1325
|
+
status: isSecure ? "PASS" : "FAIL",
|
|
1326
|
+
value: fwdMatch[1],
|
|
1327
|
+
description: "FORWARD chain default policy (should be DROP)",
|
|
1328
|
+
recommendation: isSecure
|
|
1329
|
+
? undefined
|
|
1330
|
+
: "Use firewall_iptables action=set_policy chain=FORWARD policy=DROP ipv6=true",
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
// Check OUTPUT policy
|
|
1334
|
+
const outMatch = output.match(/Chain OUTPUT \(policy (\w+)\)/);
|
|
1335
|
+
if (outMatch) {
|
|
1336
|
+
findings.push({
|
|
1337
|
+
check: "iptables_output_policy",
|
|
1338
|
+
status: "INFO",
|
|
1339
|
+
value: outMatch[1],
|
|
1340
|
+
description: "OUTPUT chain policy (DROP recommended for high security)",
|
|
1341
|
+
recommendation: outMatch[1] !== "DROP"
|
|
1342
|
+
? "Use firewall_iptables action=set_policy chain=OUTPUT policy=DROP for high-security environments"
|
|
1343
|
+
: undefined,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
// Count rules
|
|
1347
|
+
const ruleCount = (output.match(/^[A-Z]+\s/gm) || []).length;
|
|
1348
|
+
findings.push({ check: "iptables_rule_count", status: ruleCount > 0 ? "INFO" : "WARN", value: String(ruleCount), description: "Total iptables rules" });
|
|
1349
|
+
}
|
|
1350
|
+
// Check UFW status
|
|
1351
|
+
const ufwResult = await executeCommand({ command: "sudo", args: ["ufw", "status"], timeout: 10000, toolName: "firewall_policy_audit" });
|
|
1352
|
+
if (ufwResult.exitCode === 0) {
|
|
1353
|
+
const active = ufwResult.stdout.includes("Status: active");
|
|
1354
|
+
findings.push({ check: "ufw_active", status: active ? "PASS" : "FAIL", value: active ? "active" : "inactive", description: "UFW firewall status" });
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
findings.push({ check: "ufw_installed", status: "FAIL", value: "not installed", description: "UFW firewall availability" });
|
|
1358
|
+
}
|
|
1359
|
+
// Check ip6tables
|
|
1360
|
+
const ip6Result = await executeCommand({ command: "sudo", args: ["ip6tables", "-L", "-n"], timeout: 10000, toolName: "firewall_policy_audit" });
|
|
1361
|
+
if (ip6Result.exitCode === 0) {
|
|
1362
|
+
const ip6InputMatch = ip6Result.stdout.match(/Chain INPUT \(policy (\w+)\)/);
|
|
1363
|
+
if (ip6InputMatch) {
|
|
1364
|
+
const isSecure = ip6InputMatch[1] === "DROP";
|
|
1365
|
+
findings.push({
|
|
1366
|
+
check: "ip6tables_input_policy",
|
|
1367
|
+
status: isSecure ? "PASS" : "FAIL",
|
|
1368
|
+
value: ip6InputMatch[1],
|
|
1369
|
+
description: "IPv6 INPUT chain policy (should be DROP)",
|
|
1370
|
+
recommendation: isSecure
|
|
1371
|
+
? undefined
|
|
1372
|
+
: "Use firewall_iptables action=set_policy chain=INPUT policy=DROP ipv6=true",
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
// Check for firewall persistence (distro-aware)
|
|
1377
|
+
const daPolicy = await getDistroAdapter();
|
|
1378
|
+
const fwpPolicy = daPolicy.fwPersistence;
|
|
1379
|
+
const persistResult = await executeCommand({ command: fwpPolicy.checkInstalledCmd[0], args: fwpPolicy.checkInstalledCmd.slice(1), timeout: 5000, toolName: "firewall_policy_audit" });
|
|
1380
|
+
const persistInstalled = daPolicy.isDebian ? persistResult.stdout.includes("ii") : persistResult.exitCode === 0;
|
|
1381
|
+
findings.push({
|
|
1382
|
+
check: "firewall_persistence",
|
|
1383
|
+
status: persistInstalled ? "PASS" : "WARN",
|
|
1384
|
+
value: persistInstalled ? "installed" : "not installed",
|
|
1385
|
+
description: `${fwpPolicy.packageName} (rules survive reboot)`,
|
|
1386
|
+
recommendation: persistInstalled
|
|
1387
|
+
? undefined
|
|
1388
|
+
: "Use firewall_persist with action='enable' to install and activate persistence, then action='save' to persist current rules",
|
|
1389
|
+
});
|
|
1390
|
+
const passCount = findings.filter(f => f.status === "PASS").length;
|
|
1391
|
+
const failCount = findings.filter(f => f.status === "FAIL").length;
|
|
1392
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: passCount, fail: failCount, warn: findings.filter(f => f.status === "WARN").length }, findings }, null, 2))] };
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
}
|