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,788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container and mandatory access control security tools for Kali Defense MCP Server.
|
|
3
|
+
*
|
|
4
|
+
* Registers 6 tools:
|
|
5
|
+
* container_docker (actions: audit, bench, seccomp, daemon)
|
|
6
|
+
* container_apparmor (actions: status, list, enforce, complain, disable, install, apply_container)
|
|
7
|
+
* container_security_config (actions: seccomp_profile, rootless)
|
|
8
|
+
* container_selinux_manage (kept as-is)
|
|
9
|
+
* container_namespace_check (kept as-is)
|
|
10
|
+
* container_image_scan (kept as-is)
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { executeCommand } from "../core/executor.js";
|
|
14
|
+
import { getConfig, getToolTimeout } from "../core/config.js";
|
|
15
|
+
import { createTextContent, createErrorContent, formatToolOutput, parseJsonSafe, } from "../core/parsers.js";
|
|
16
|
+
import { logChange, createChangeEntry, backupFile } from "../core/changelog.js";
|
|
17
|
+
import { sanitizeArgs } from "../core/sanitizer.js";
|
|
18
|
+
import { SafeguardRegistry } from "../core/safeguards.js";
|
|
19
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { resolve } from "node:path";
|
|
21
|
+
import { secureWriteFileSync } from "../core/secure-fs.js";
|
|
22
|
+
// ── TOOL-011 remediation: safe directory for seccomp profiles ──────────────
|
|
23
|
+
const SECCOMP_PROFILE_DIR = "/tmp/kali-defense/seccomp";
|
|
24
|
+
// ── Registration entry point ───────────────────────────────────────────────
|
|
25
|
+
export function registerContainerSecurityTools(server) {
|
|
26
|
+
// ── 1. container_docker (merged: audit + bench + seccomp + daemon) ─────
|
|
27
|
+
server.tool("container_docker", "Docker security: audit configuration, run CIS benchmarks, audit seccomp profiles, or configure daemon settings.", {
|
|
28
|
+
action: z.enum(["audit", "bench", "seccomp", "daemon"]).describe("Action: audit=security audit, bench=CIS benchmark, seccomp=seccomp audit, daemon=configure daemon"),
|
|
29
|
+
// audit params
|
|
30
|
+
check_type: z.enum(["daemon", "images", "containers", "network", "all"]).optional().default("all").describe("Docker check type (audit action)"),
|
|
31
|
+
// bench params
|
|
32
|
+
checks: z.string().optional().describe("Specific check sections e.g. '1,2,4' (bench action)"),
|
|
33
|
+
log_level: z.enum(["INFO", "WARN", "NOTE", "PASS"]).optional().default("WARN").describe("Min log level (bench action)"),
|
|
34
|
+
// daemon params
|
|
35
|
+
daemon_action: z.enum(["audit", "apply"]).optional().describe("Whether to audit or apply daemon settings (daemon action)"),
|
|
36
|
+
settings: z.object({
|
|
37
|
+
userns_remap: z.boolean().optional(),
|
|
38
|
+
no_new_privileges: z.boolean().optional(),
|
|
39
|
+
icc: z.boolean().optional(),
|
|
40
|
+
live_restore: z.boolean().optional(),
|
|
41
|
+
log_driver: z.enum(["json-file", "journald"]).optional(),
|
|
42
|
+
log_max_size: z.string().optional().default("10m"),
|
|
43
|
+
log_max_file: z.string().optional().default("3"),
|
|
44
|
+
}).optional().describe("Settings to apply (daemon action with daemon_action=apply)"),
|
|
45
|
+
// shared
|
|
46
|
+
dry_run: z.boolean().optional().describe("Preview changes without executing"),
|
|
47
|
+
}, async (params) => {
|
|
48
|
+
const { action } = params;
|
|
49
|
+
switch (action) {
|
|
50
|
+
// ── audit ───────────────────────────────────────────────────
|
|
51
|
+
case "audit": {
|
|
52
|
+
const { check_type } = params;
|
|
53
|
+
try {
|
|
54
|
+
const sections = [];
|
|
55
|
+
sections.push("🐳 Docker Security Audit");
|
|
56
|
+
sections.push("=".repeat(50));
|
|
57
|
+
const findings = [];
|
|
58
|
+
const dockerCheck = await executeCommand({ command: "which", args: ["docker"], toolName: "container_docker", timeout: 5000 });
|
|
59
|
+
if (dockerCheck.exitCode !== 0) {
|
|
60
|
+
return { content: [createTextContent("Docker is not installed or not in PATH. No audit possible.")] };
|
|
61
|
+
}
|
|
62
|
+
if (check_type === "daemon" || check_type === "all") {
|
|
63
|
+
sections.push("\n── Docker Daemon Configuration ──");
|
|
64
|
+
const infoResult = await executeCommand({ command: "docker", args: ["info", "--format", "{{json .}}"], toolName: "container_docker", timeout: getToolTimeout("container_docker_audit") });
|
|
65
|
+
if (infoResult.exitCode === 0) {
|
|
66
|
+
const info = parseJsonSafe(infoResult.stdout);
|
|
67
|
+
if (info) {
|
|
68
|
+
sections.push(` Server Version: ${info["ServerVersion"] ?? "unknown"}`);
|
|
69
|
+
sections.push(` Storage Driver: ${info["Driver"] ?? "unknown"}`);
|
|
70
|
+
sections.push(` Logging Driver: ${info["LoggingDriver"] ?? "unknown"}`);
|
|
71
|
+
sections.push(` Live Restore: ${info["LiveRestoreEnabled"] ?? "unknown"}`);
|
|
72
|
+
const securityOptions = info["SecurityOptions"];
|
|
73
|
+
if (securityOptions) {
|
|
74
|
+
sections.push(` Security Options: ${securityOptions.join(", ")}`);
|
|
75
|
+
if (!securityOptions.some((o) => String(o).includes("userns")))
|
|
76
|
+
findings.push({ level: "WARNING", msg: "User namespaces not enabled" });
|
|
77
|
+
}
|
|
78
|
+
if (info["LiveRestoreEnabled"] !== true)
|
|
79
|
+
findings.push({ level: "INFO", msg: "Live restore is not enabled" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const daemonResult = await executeCommand({ command: "cat", args: ["/etc/docker/daemon.json"], toolName: "container_docker", timeout: 5000 });
|
|
83
|
+
if (daemonResult.exitCode === 0) {
|
|
84
|
+
const daemonConfig = parseJsonSafe(daemonResult.stdout);
|
|
85
|
+
if (daemonConfig) {
|
|
86
|
+
if (!daemonConfig["userns-remap"])
|
|
87
|
+
findings.push({ level: "WARNING", msg: "userns-remap not configured" });
|
|
88
|
+
if (!daemonConfig["no-new-privileges"])
|
|
89
|
+
findings.push({ level: "INFO", msg: "no-new-privileges not set" });
|
|
90
|
+
if (!daemonConfig["icc"] || daemonConfig["icc"] === true)
|
|
91
|
+
findings.push({ level: "WARNING", msg: "Inter-container communication not disabled" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
findings.push({ level: "WARNING", msg: "No custom Docker daemon configuration" });
|
|
96
|
+
}
|
|
97
|
+
const socketResult = await executeCommand({ command: "ls", args: ["-la", "/var/run/docker.sock"], toolName: "container_docker", timeout: 5000 });
|
|
98
|
+
if (socketResult.exitCode === 0 && socketResult.stdout.includes("rw-rw-rw")) {
|
|
99
|
+
findings.push({ level: "CRITICAL", msg: "Docker socket is world-writable!" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (check_type === "images" || check_type === "all") {
|
|
103
|
+
sections.push("\n── Docker Images ──");
|
|
104
|
+
const imagesResult = await executeCommand({ command: "docker", args: ["images", "--format", "{{.Repository}}:{{.Tag}} | {{.Size}} | {{.CreatedSince}} | {{.ID}}"], toolName: "container_docker", timeout: getToolTimeout("container_docker_audit") });
|
|
105
|
+
if (imagesResult.exitCode === 0 && imagesResult.stdout.trim()) {
|
|
106
|
+
const imageLines = imagesResult.stdout.trim().split("\n").filter((l) => l.trim());
|
|
107
|
+
sections.push(` Total images: ${imageLines.length}`);
|
|
108
|
+
let latestCount = 0;
|
|
109
|
+
for (const line of imageLines) {
|
|
110
|
+
sections.push(` ${line}`);
|
|
111
|
+
if (line.includes(":latest ") || line.endsWith(":latest"))
|
|
112
|
+
latestCount++;
|
|
113
|
+
}
|
|
114
|
+
if (latestCount > 0)
|
|
115
|
+
findings.push({ level: "WARNING", msg: `${latestCount} image(s) using 'latest' tag` });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
sections.push(" No Docker images found.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (check_type === "containers" || check_type === "all") {
|
|
122
|
+
sections.push("\n── Running Containers ──");
|
|
123
|
+
const psResult = await executeCommand({ command: "docker", args: ["ps", "--format", "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}"], toolName: "container_docker", timeout: getToolTimeout("container_docker_audit") });
|
|
124
|
+
if (psResult.exitCode === 0 && psResult.stdout.trim()) {
|
|
125
|
+
const containerLines = psResult.stdout.trim().split("\n").filter((l) => l.trim());
|
|
126
|
+
sections.push(` Running containers: ${containerLines.length}`);
|
|
127
|
+
for (const line of containerLines) {
|
|
128
|
+
const parts = line.split("|");
|
|
129
|
+
const containerId = parts[0] || "unknown";
|
|
130
|
+
const containerName = parts[1] || "unknown";
|
|
131
|
+
sections.push(`\n Container: ${containerName} (${containerId})`);
|
|
132
|
+
const inspectResult = await executeCommand({ command: "docker", args: ["inspect", "--format", "{{.HostConfig.Privileged}}|{{.HostConfig.NetworkMode}}|{{.HostConfig.PidMode}}|{{.HostConfig.ReadonlyRootfs}}", containerId], toolName: "container_docker", timeout: 10000 });
|
|
133
|
+
if (inspectResult.exitCode === 0) {
|
|
134
|
+
const inspParts = inspectResult.stdout.trim().split("|");
|
|
135
|
+
if (inspParts[0] === "true")
|
|
136
|
+
findings.push({ level: "CRITICAL", msg: `Container '${containerName}' is running in privileged mode!` });
|
|
137
|
+
if (inspParts[1] === "host")
|
|
138
|
+
findings.push({ level: "WARNING", msg: `Container '${containerName}' uses host networking` });
|
|
139
|
+
if (inspParts[2] === "host")
|
|
140
|
+
findings.push({ level: "WARNING", msg: `Container '${containerName}' shares host PID namespace` });
|
|
141
|
+
}
|
|
142
|
+
const mountInspect = await executeCommand({ command: "docker", args: ["inspect", "--format", "{{json .Mounts}}", containerId], toolName: "container_docker", timeout: 10000 });
|
|
143
|
+
if (mountInspect.exitCode === 0 && mountInspect.stdout.trim()) {
|
|
144
|
+
const mounts = parseJsonSafe(mountInspect.stdout.trim());
|
|
145
|
+
if (mounts) {
|
|
146
|
+
for (const mount of mounts) {
|
|
147
|
+
const src = mount.Source || "";
|
|
148
|
+
if (src === "/var/run/docker.sock" || mount.Destination === "/var/run/docker.sock")
|
|
149
|
+
findings.push({ level: "CRITICAL", msg: `Container '${containerName}': Docker socket mounted` });
|
|
150
|
+
if (src === "/")
|
|
151
|
+
findings.push({ level: "CRITICAL", msg: `Container '${containerName}': Root filesystem '/' mounted` });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (check_type === "network" || check_type === "all") {
|
|
159
|
+
sections.push("\n── Docker Networks ──");
|
|
160
|
+
const netResult = await executeCommand({ command: "docker", args: ["network", "ls", "--format", "{{.Name}} | {{.Driver}} | {{.Scope}}"], toolName: "container_docker", timeout: getToolTimeout("container_docker_audit") });
|
|
161
|
+
if (netResult.exitCode === 0 && netResult.stdout.trim()) {
|
|
162
|
+
for (const line of netResult.stdout.trim().split("\n"))
|
|
163
|
+
sections.push(` ${line}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
sections.push("\n── Security Findings Summary ──");
|
|
167
|
+
if (findings.length === 0) {
|
|
168
|
+
sections.push(" ✅ No significant security issues found.");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
for (const lvl of ["CRITICAL", "WARNING", "INFO"]) {
|
|
172
|
+
const items = findings.filter((f) => f.level === lvl);
|
|
173
|
+
if (items.length > 0) {
|
|
174
|
+
const icon = lvl === "CRITICAL" ? "⛔" : lvl === "WARNING" ? "⚠️" : "ℹ️";
|
|
175
|
+
sections.push(`\n ${icon} ${lvl} (${items.length}):`);
|
|
176
|
+
for (const f of items)
|
|
177
|
+
sections.push(` - ${f.msg}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ── bench ───────────────────────────────────────────────────
|
|
188
|
+
case "bench": {
|
|
189
|
+
const { checks: benchChecks, log_level } = params;
|
|
190
|
+
try {
|
|
191
|
+
const sections = ["🔒 Docker Bench for Security", "=".repeat(50)];
|
|
192
|
+
const dockerCheck = await executeCommand({ command: "which", args: ["docker"], toolName: "container_docker", timeout: 5000 });
|
|
193
|
+
if (dockerCheck.exitCode !== 0)
|
|
194
|
+
return { content: [createTextContent("Docker is not installed.")] };
|
|
195
|
+
const benchArgs = ["run", "--rm", "--net", "host", "--pid", "host", "--userns", "host", "--cap-add", "audit_control", "-v", "/etc:/etc:ro", "-v", "/var/lib:/var/lib:ro", "-v", "/var/run/docker.sock:/var/run/docker.sock:ro", "-v", "/usr/lib/systemd:/usr/lib/systemd:ro", "-v", "/usr/bin/containerd:/usr/bin/containerd:ro", "-v", "/usr/bin/runc:/usr/bin/runc:ro", "docker/docker-bench-security"];
|
|
196
|
+
if (benchChecks) {
|
|
197
|
+
sanitizeArgs([benchChecks]);
|
|
198
|
+
benchArgs.push("-c", benchChecks);
|
|
199
|
+
}
|
|
200
|
+
const result = await executeCommand({ command: "docker", args: benchArgs, toolName: "container_docker", timeout: 300000 });
|
|
201
|
+
const output = result.stdout || result.stderr;
|
|
202
|
+
if (result.exitCode !== 0 && !output) {
|
|
203
|
+
sections.push("⚠️ Docker Bench could not run.");
|
|
204
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
205
|
+
}
|
|
206
|
+
const levelPriority = { PASS: 0, INFO: 1, NOTE: 2, WARN: 3 };
|
|
207
|
+
const minLevel = levelPriority[log_level] ?? 0;
|
|
208
|
+
let passCount = 0, warnCount = 0, infoCount = 0, noteCount = 0;
|
|
209
|
+
const filteredLines = [];
|
|
210
|
+
for (const line of output.split("\n")) {
|
|
211
|
+
if (line.includes("[PASS]"))
|
|
212
|
+
passCount++;
|
|
213
|
+
if (line.includes("[WARN]"))
|
|
214
|
+
warnCount++;
|
|
215
|
+
if (line.includes("[INFO]"))
|
|
216
|
+
infoCount++;
|
|
217
|
+
if (line.includes("[NOTE]"))
|
|
218
|
+
noteCount++;
|
|
219
|
+
if (line.match(/^\[INFO\]\s+\d+\s+-\s+/) || line.startsWith("# ")) {
|
|
220
|
+
filteredLines.push(line);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
let lineLevel = -1;
|
|
224
|
+
if (line.includes("[PASS]"))
|
|
225
|
+
lineLevel = 0;
|
|
226
|
+
if (line.includes("[INFO]"))
|
|
227
|
+
lineLevel = 1;
|
|
228
|
+
if (line.includes("[NOTE]"))
|
|
229
|
+
lineLevel = 2;
|
|
230
|
+
if (line.includes("[WARN]"))
|
|
231
|
+
lineLevel = 3;
|
|
232
|
+
if (lineLevel >= minLevel)
|
|
233
|
+
filteredLines.push(line);
|
|
234
|
+
}
|
|
235
|
+
sections.push("── Results ──", filteredLines.join("\n"), "\n── Summary ──", ` [PASS]: ${passCount}`, ` [WARN]: ${warnCount}`, ` [INFO]: ${infoCount}`, ` [NOTE]: ${noteCount}`);
|
|
236
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// ── seccomp ─────────────────────────────────────────────────
|
|
243
|
+
case "seccomp": {
|
|
244
|
+
try {
|
|
245
|
+
const psResult = await executeCommand({ command: "docker", args: ["ps", "--format", "{{.ID}} {{.Names}} {{.Image}}"], timeout: 10000, toolName: "container_docker" });
|
|
246
|
+
if (psResult.exitCode !== 0)
|
|
247
|
+
return { content: [createTextContent("Docker is not available or not running")] };
|
|
248
|
+
const containers = psResult.stdout.trim().split("\n").filter((l) => l.trim());
|
|
249
|
+
const results = [];
|
|
250
|
+
for (const line of containers) {
|
|
251
|
+
const [id, name, image] = line.split(" ");
|
|
252
|
+
if (!id)
|
|
253
|
+
continue;
|
|
254
|
+
const inspectResult = await executeCommand({ command: "docker", args: ["inspect", "--format", '{{.HostConfig.SecurityOpt}}', id], timeout: 10000, toolName: "container_docker" });
|
|
255
|
+
const secOpt = inspectResult.stdout.trim();
|
|
256
|
+
const hasSeccomp = secOpt.includes("seccomp");
|
|
257
|
+
const unconfined = secOpt.includes("seccomp=unconfined");
|
|
258
|
+
results.push({
|
|
259
|
+
container: name || id, image: image || "unknown", securityOpt: secOpt,
|
|
260
|
+
seccompEnabled: hasSeccomp && !unconfined,
|
|
261
|
+
status: unconfined ? "FAIL" : hasSeccomp ? "PASS" : secOpt === "[]" ? "WARN" : "PASS",
|
|
262
|
+
note: unconfined ? "seccomp explicitly disabled — HIGH RISK" : !hasSeccomp && secOpt === "[]" ? "Using Docker default seccomp (acceptable)" : "seccomp configured",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: results.length, pass: results.filter(r => r.status === "PASS").length, warn: results.filter(r => r.status === "WARN").length, fail: results.filter(r => r.status === "FAIL").length }, containers: results }, null, 2))] };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// ── daemon ──────────────────────────────────────────────────
|
|
272
|
+
case "daemon": {
|
|
273
|
+
const { daemon_action, settings, dry_run } = params;
|
|
274
|
+
try {
|
|
275
|
+
if (!daemon_action)
|
|
276
|
+
return { content: [createErrorContent("daemon_action is required (audit or apply)")], isError: true };
|
|
277
|
+
const sections = ["🐳 Docker Daemon Configuration", "=".repeat(50)];
|
|
278
|
+
const daemonPath = "/etc/docker/daemon.json";
|
|
279
|
+
const readResult = await executeCommand({ command: "cat", args: [daemonPath], toolName: "container_docker", timeout: 5000 });
|
|
280
|
+
const existingConfig = readResult.exitCode === 0 ? parseJsonSafe(readResult.stdout) || {} : {};
|
|
281
|
+
if (daemon_action === "audit") {
|
|
282
|
+
if (readResult.exitCode !== 0)
|
|
283
|
+
sections.push(" ⚠️ No /etc/docker/daemon.json found");
|
|
284
|
+
else
|
|
285
|
+
sections.push(` ${JSON.stringify(existingConfig, null, 4).replace(/\n/g, "\n ")}`);
|
|
286
|
+
sections.push("\n── Security Settings Audit ──");
|
|
287
|
+
const checks = [
|
|
288
|
+
{ key: "userns-remap", present: !!existingConfig["userns-remap"], recommended: '"default"', severity: "HIGH" },
|
|
289
|
+
{ key: "no-new-privileges", present: !!existingConfig["no-new-privileges"], recommended: "true", severity: "MEDIUM" },
|
|
290
|
+
{ key: "icc", present: existingConfig["icc"] === false, recommended: "false", severity: "HIGH" },
|
|
291
|
+
{ key: "live-restore", present: !!existingConfig["live-restore"], recommended: "true", severity: "LOW" },
|
|
292
|
+
{ key: "log-driver", present: !!existingConfig["log-driver"], recommended: '"json-file"', severity: "LOW" },
|
|
293
|
+
];
|
|
294
|
+
let missingCount = 0;
|
|
295
|
+
for (const c of checks) {
|
|
296
|
+
if (!c.present)
|
|
297
|
+
missingCount++;
|
|
298
|
+
sections.push(` ${c.present ? "✅ Present" : "❌ Missing"}: ${c.key} [${c.severity}]`);
|
|
299
|
+
}
|
|
300
|
+
sections.push(`\n Summary: ${checks.length - missingCount}/${checks.length} configured`);
|
|
301
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
302
|
+
}
|
|
303
|
+
// apply
|
|
304
|
+
if (!settings)
|
|
305
|
+
return { content: [createErrorContent("settings parameter is required for daemon_action=apply")], isError: true };
|
|
306
|
+
const isDryRun = dry_run ?? getConfig().dryRun;
|
|
307
|
+
const changes = [];
|
|
308
|
+
const newConfig = { ...existingConfig };
|
|
309
|
+
if (settings.userns_remap !== undefined) {
|
|
310
|
+
if (settings.userns_remap) {
|
|
311
|
+
newConfig["userns-remap"] = "default";
|
|
312
|
+
changes.push('userns-remap: "default"');
|
|
313
|
+
}
|
|
314
|
+
else if (newConfig["userns-remap"]) {
|
|
315
|
+
delete newConfig["userns-remap"];
|
|
316
|
+
changes.push("userns-remap: removed");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (settings.no_new_privileges !== undefined) {
|
|
320
|
+
newConfig["no-new-privileges"] = settings.no_new_privileges;
|
|
321
|
+
changes.push(`no-new-privileges: ${settings.no_new_privileges}`);
|
|
322
|
+
}
|
|
323
|
+
if (settings.icc !== undefined) {
|
|
324
|
+
newConfig["icc"] = settings.icc;
|
|
325
|
+
changes.push(`icc: ${settings.icc}`);
|
|
326
|
+
}
|
|
327
|
+
if (settings.live_restore !== undefined) {
|
|
328
|
+
newConfig["live-restore"] = settings.live_restore;
|
|
329
|
+
changes.push(`live-restore: ${settings.live_restore}`);
|
|
330
|
+
}
|
|
331
|
+
if (settings.log_driver) {
|
|
332
|
+
newConfig["log-driver"] = settings.log_driver;
|
|
333
|
+
changes.push(`log-driver: "${settings.log_driver}"`);
|
|
334
|
+
}
|
|
335
|
+
if (settings.log_driver || settings.log_max_size || settings.log_max_file) {
|
|
336
|
+
const logOpts = newConfig["log-opts"] || {};
|
|
337
|
+
if (settings.log_max_size)
|
|
338
|
+
logOpts["max-size"] = settings.log_max_size;
|
|
339
|
+
if (settings.log_max_file)
|
|
340
|
+
logOpts["max-file"] = settings.log_max_file;
|
|
341
|
+
newConfig["log-opts"] = logOpts;
|
|
342
|
+
}
|
|
343
|
+
if (changes.length === 0) {
|
|
344
|
+
sections.push("\n No changes to apply.");
|
|
345
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
346
|
+
}
|
|
347
|
+
const newJson = JSON.stringify(newConfig, null, 2);
|
|
348
|
+
sections.push("\n── Changes ──");
|
|
349
|
+
for (const c of changes)
|
|
350
|
+
sections.push(` • ${c}`);
|
|
351
|
+
if (isDryRun) {
|
|
352
|
+
sections.push("\n[DRY RUN] No changes written.");
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
if (readResult.exitCode === 0) {
|
|
356
|
+
await backupFile(daemonPath);
|
|
357
|
+
sections.push(`\n ✅ Backed up ${daemonPath}`);
|
|
358
|
+
}
|
|
359
|
+
const writeResult = await executeCommand({ command: "sudo", args: ["tee", daemonPath], stdin: newJson, toolName: "container_docker", timeout: 10000 });
|
|
360
|
+
if (writeResult.exitCode !== 0)
|
|
361
|
+
return { content: [createErrorContent(`Failed to write ${daemonPath}: ${writeResult.stderr}`)], isError: true };
|
|
362
|
+
sections.push(` ✅ Written to ${daemonPath}`);
|
|
363
|
+
sections.push("\n ⚠️ Restart Docker: sudo systemctl restart docker");
|
|
364
|
+
logChange(createChangeEntry({ tool: "container_docker", action: "apply daemon config", target: daemonPath, before: JSON.stringify(existingConfig), after: newJson, dryRun: false, success: true }));
|
|
365
|
+
}
|
|
366
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
default:
|
|
373
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// ── 2. container_apparmor (merged: manage + install + apply_container) ──
|
|
377
|
+
server.tool("container_apparmor", "AppArmor management: check status, list/enforce/complain/disable profiles, install profile packages, or generate container profiles.", {
|
|
378
|
+
action: z.enum(["status", "list", "enforce", "complain", "disable", "install", "apply_container"]).describe("Action"),
|
|
379
|
+
// enforce/complain/disable params
|
|
380
|
+
profile: z.string().optional().describe("Profile name (enforce/complain/disable action)"),
|
|
381
|
+
// apply_container params
|
|
382
|
+
profileName: z.string().optional().describe("AppArmor profile name (apply_container action)"),
|
|
383
|
+
containerName: z.string().optional().describe("Container name for context (apply_container action)"),
|
|
384
|
+
allowNetwork: z.boolean().optional().default(true).describe("Allow network access (apply_container action)"),
|
|
385
|
+
allowWrite: z.array(z.string()).optional().default([]).describe("Writable paths (apply_container action)"),
|
|
386
|
+
// shared
|
|
387
|
+
dry_run: z.boolean().optional().describe("Preview changes without executing"),
|
|
388
|
+
}, async (params) => {
|
|
389
|
+
const { action } = params;
|
|
390
|
+
switch (action) {
|
|
391
|
+
case "status": {
|
|
392
|
+
try {
|
|
393
|
+
const sections = ["🛡️ AppArmor System Status", "=".repeat(40)];
|
|
394
|
+
const enabledResult = await executeCommand({ command: "aa-enabled", args: [], toolName: "container_apparmor", timeout: 5000 });
|
|
395
|
+
const aaEnabled = enabledResult.exitCode === 0 && enabledResult.stdout.trim() === "Yes";
|
|
396
|
+
sections.push(`\n AppArmor enabled: ${aaEnabled ? "✅ Yes" : "❌ No"}`);
|
|
397
|
+
const moduleResult = await executeCommand({ command: "cat", args: ["/sys/module/apparmor/parameters/enabled"], toolName: "container_apparmor", timeout: 5000 });
|
|
398
|
+
if (moduleResult.exitCode === 0)
|
|
399
|
+
sections.push(` Kernel module: ${moduleResult.stdout.trim() === "Y" ? "✅ Loaded" : "❌ Not loaded"}`);
|
|
400
|
+
const pkgChecks = ["apparmor-profiles", "apparmor-profiles-extra", "apparmor-utils"];
|
|
401
|
+
sections.push("\n Profile Packages:");
|
|
402
|
+
for (const pkg of pkgChecks) {
|
|
403
|
+
const dpkgResult = await executeCommand({ command: "dpkg", args: ["-s", pkg], toolName: "container_apparmor", timeout: 5000 });
|
|
404
|
+
const installed = dpkgResult.exitCode === 0 && dpkgResult.stdout.includes("Status: install ok installed");
|
|
405
|
+
sections.push(` ${pkg}: ${installed ? "✅ Installed" : "❌ Not installed"}`);
|
|
406
|
+
}
|
|
407
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
case "list": {
|
|
414
|
+
try {
|
|
415
|
+
const sections = ["🛡️ AppArmor Profiles", "=".repeat(40)];
|
|
416
|
+
let result = await executeCommand({ command: "sudo", args: ["aa-status"], toolName: "container_apparmor", timeout: getToolTimeout("container_apparmor_manage") });
|
|
417
|
+
if (result.exitCode !== 0)
|
|
418
|
+
result = await executeCommand({ command: "sudo", args: ["apparmor_status"], toolName: "container_apparmor", timeout: getToolTimeout("container_apparmor_manage") });
|
|
419
|
+
if (result.exitCode !== 0) {
|
|
420
|
+
sections.push("\n⚠️ Cannot list AppArmor profiles.");
|
|
421
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
422
|
+
}
|
|
423
|
+
const output = result.stdout;
|
|
424
|
+
const lines = output.split("\n");
|
|
425
|
+
let currentSection = "";
|
|
426
|
+
for (const line of lines) {
|
|
427
|
+
const trimmed = line.trim();
|
|
428
|
+
if (trimmed.includes("enforce mode")) {
|
|
429
|
+
currentSection = "enforce";
|
|
430
|
+
sections.push("\n 🔒 Enforce Mode:");
|
|
431
|
+
}
|
|
432
|
+
else if (trimmed.includes("complain mode")) {
|
|
433
|
+
currentSection = "complain";
|
|
434
|
+
sections.push("\n 📝 Complain Mode:");
|
|
435
|
+
}
|
|
436
|
+
else if (trimmed.includes("unconfined")) {
|
|
437
|
+
currentSection = "unconfined";
|
|
438
|
+
sections.push("\n ⚠️ Unconfined:");
|
|
439
|
+
}
|
|
440
|
+
else if (currentSection && trimmed && !trimmed.match(/^\d+\s+processes?/)) {
|
|
441
|
+
sections.push(` ${trimmed}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
case "enforce":
|
|
451
|
+
case "complain":
|
|
452
|
+
case "disable": {
|
|
453
|
+
const { profile, dry_run } = params;
|
|
454
|
+
try {
|
|
455
|
+
if (!profile)
|
|
456
|
+
return { content: [createErrorContent(`profile name is required for '${action}' action`)], isError: true };
|
|
457
|
+
sanitizeArgs([profile]);
|
|
458
|
+
const cmdMap = { enforce: "aa-enforce", complain: "aa-complain", disable: "aa-disable" };
|
|
459
|
+
const cmd = cmdMap[action];
|
|
460
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
461
|
+
return { content: [createTextContent(`[DRY RUN] Would set profile '${profile}' to ${action} mode.\n Command: sudo ${cmd} ${profile}`)] };
|
|
462
|
+
}
|
|
463
|
+
const result = await executeCommand({ command: "sudo", args: [cmd, profile], toolName: "container_apparmor", timeout: getToolTimeout("container_apparmor_manage") });
|
|
464
|
+
if (result.exitCode !== 0)
|
|
465
|
+
return { content: [createErrorContent(`Failed to ${action} profile '${profile}': ${result.stderr}`)], isError: true };
|
|
466
|
+
logChange(createChangeEntry({ tool: "container_apparmor", action, target: profile, after: `${action} mode`, dryRun: false, success: true, rollbackCommand: action === "disable" ? `sudo aa-enforce ${profile}` : undefined }));
|
|
467
|
+
return { content: [createTextContent(`✅ Profile '${profile}' set to ${action} mode.\n${result.stdout || result.stderr}`)] };
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
case "install": {
|
|
474
|
+
const { dry_run } = params;
|
|
475
|
+
try {
|
|
476
|
+
const isDryRun = dry_run ?? getConfig().dryRun;
|
|
477
|
+
const packages = ["apparmor-profiles", "apparmor-profiles-extra"];
|
|
478
|
+
if (isDryRun) {
|
|
479
|
+
return { content: [createTextContent(`[DRY RUN] Would install: ${packages.join(", ")}\n Command: sudo apt-get install -y ${packages.join(" ")}`)] };
|
|
480
|
+
}
|
|
481
|
+
const installResult = await executeCommand({ command: "sudo", args: ["apt-get", "install", "-y", ...packages], toolName: "container_apparmor", timeout: 120000 });
|
|
482
|
+
if (installResult.exitCode !== 0)
|
|
483
|
+
return { content: [createErrorContent(`Failed to install: ${installResult.stderr}`)], isError: true };
|
|
484
|
+
logChange(createChangeEntry({ tool: "container_apparmor", action: "install_profiles", target: packages.join(", "), after: "installed", dryRun: false, success: true, rollbackCommand: `sudo apt-get remove -y ${packages.join(" ")}` }));
|
|
485
|
+
return { content: [createTextContent(`✅ Successfully installed: ${packages.join(", ")}`)] };
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
case "apply_container": {
|
|
492
|
+
const { profileName, containerName, allowNetwork, allowWrite, dry_run } = params;
|
|
493
|
+
try {
|
|
494
|
+
if (!profileName)
|
|
495
|
+
return { content: [createErrorContent("profileName is required for apply_container action")], isError: true };
|
|
496
|
+
const writeRules = (allowWrite ?? []).map((p) => ` ${p} rw,`).join("\n");
|
|
497
|
+
const networkRule = allowNetwork ? " network,\n" : " deny network,\n";
|
|
498
|
+
const profileContent = `#include <tunables/global>\n\nprofile ${profileName} flags=(attach_disconnected,mediate_deleted) {\n #include <abstractions/base>\n\n${networkRule}\n /usr/** r,\n /etc/** r,\n /proc/** r,\n /sys/** r,\n /tmp/** rw,\n${writeRules}\n\n deny /etc/shadow r,\n deny /etc/gshadow r,\n\n capability net_bind_service,\n capability setuid,\n capability setgid,\n}\n`;
|
|
499
|
+
const isDryRun = dry_run ?? getConfig().dryRun;
|
|
500
|
+
if (isDryRun) {
|
|
501
|
+
return { content: [formatToolOutput({ dryRun: true, profileName, profile: profileContent, loadCommand: `apparmor_parser -r /etc/apparmor.d/${profileName}` })] };
|
|
502
|
+
}
|
|
503
|
+
const profilePath = `/etc/apparmor.d/${profileName}`;
|
|
504
|
+
// TOOL-009: Use secure-fs instead of direct writeFileSync
|
|
505
|
+
secureWriteFileSync(profilePath, profileContent, "utf-8");
|
|
506
|
+
const result = await executeCommand({ command: "apparmor_parser", args: ["-r", profilePath], timeout: 15000 });
|
|
507
|
+
logChange(createChangeEntry({ tool: "container_apparmor", action: `Create AppArmor profile ${profileName}`, target: profilePath, dryRun: false, success: result.exitCode === 0, rollbackCommand: `apparmor_parser -R ${profilePath} && rm ${profilePath}` }));
|
|
508
|
+
return { content: [formatToolOutput({ success: result.exitCode === 0, profilePath, loaded: result.exitCode === 0, output: result.stdout || result.stderr })] };
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
return { content: [createErrorContent(`AppArmor profile failed: ${err instanceof Error ? err.message : String(err)}`)], isError: true };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
default:
|
|
515
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
// ── 3. container_security_config (merged: seccomp_profile + rootless) ──
|
|
519
|
+
server.tool("container_security_config", "Container security configuration: generate seccomp profiles or set up rootless container support.", {
|
|
520
|
+
action: z.enum(["seccomp_profile", "rootless"]).describe("Action: seccomp_profile=generate seccomp profile, rootless=setup rootless containers"),
|
|
521
|
+
// seccomp_profile params
|
|
522
|
+
allowedSyscalls: z.array(z.string()).optional().describe("List of syscall names to allow (seccomp_profile action)"),
|
|
523
|
+
defaultAction: z.enum(["SCMP_ACT_ERRNO", "SCMP_ACT_KILL", "SCMP_ACT_LOG"]).optional().default("SCMP_ACT_ERRNO").describe("Default action for unlisted syscalls (seccomp_profile action)"),
|
|
524
|
+
outputPath: z.string().optional().describe("Path to write the profile (seccomp_profile action)"),
|
|
525
|
+
// rootless params
|
|
526
|
+
username: z.string().optional().describe("Username to configure (rootless action)"),
|
|
527
|
+
subuidCount: z.number().optional().default(65536).describe("Number of subordinate UIDs (rootless action)"),
|
|
528
|
+
// shared
|
|
529
|
+
dryRun: z.boolean().optional().default(true).describe("Preview only"),
|
|
530
|
+
}, async (params) => {
|
|
531
|
+
const { action } = params;
|
|
532
|
+
switch (action) {
|
|
533
|
+
case "seccomp_profile": {
|
|
534
|
+
const { allowedSyscalls, defaultAction, outputPath, dryRun } = params;
|
|
535
|
+
try {
|
|
536
|
+
if (!allowedSyscalls || allowedSyscalls.length === 0) {
|
|
537
|
+
return { content: [createErrorContent("allowedSyscalls is required for seccomp_profile action")], isError: true };
|
|
538
|
+
}
|
|
539
|
+
const profile = {
|
|
540
|
+
defaultAction,
|
|
541
|
+
architectures: ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_AARCH64"],
|
|
542
|
+
syscalls: [{ names: allowedSyscalls, action: "SCMP_ACT_ALLOW" }],
|
|
543
|
+
};
|
|
544
|
+
// TOOL-011: Validate the profile content is valid JSON (it's constructed above, so this verifies serialization)
|
|
545
|
+
const json = JSON.stringify(profile, null, 2);
|
|
546
|
+
if (dryRun || !outputPath) {
|
|
547
|
+
return { content: [formatToolOutput({ dryRun: dryRun || !outputPath, profile, syscallCount: allowedSyscalls.length, outputPath: outputPath ?? "(stdout)" })] };
|
|
548
|
+
}
|
|
549
|
+
// TOOL-011: Restrict seccomp profile output to safe directory
|
|
550
|
+
const safeBaseDir = resolve(SECCOMP_PROFILE_DIR);
|
|
551
|
+
const resolvedOutput = resolve(outputPath);
|
|
552
|
+
if (!resolvedOutput.startsWith(safeBaseDir + "/") && resolvedOutput !== safeBaseDir) {
|
|
553
|
+
// If the user-provided path isn't within the safe dir, place it there instead
|
|
554
|
+
const filename = outputPath.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
555
|
+
const safePath = resolve(safeBaseDir, filename);
|
|
556
|
+
if (!existsSync(safeBaseDir))
|
|
557
|
+
mkdirSync(safeBaseDir, { recursive: true });
|
|
558
|
+
secureWriteFileSync(safePath, json, "utf-8");
|
|
559
|
+
logChange(createChangeEntry({ tool: "container_security_config", action: "Create seccomp profile (path restricted to safe directory)", target: safePath, dryRun: false, success: true }));
|
|
560
|
+
return { content: [formatToolOutput({ success: true, outputPath: safePath, note: `Output path was restricted to safe directory: ${SECCOMP_PROFILE_DIR}`, syscallCount: allowedSyscalls.length, defaultAction })] };
|
|
561
|
+
}
|
|
562
|
+
if (!existsSync(safeBaseDir))
|
|
563
|
+
mkdirSync(safeBaseDir, { recursive: true });
|
|
564
|
+
// TOOL-011: Use secure-fs for the write operation
|
|
565
|
+
secureWriteFileSync(resolvedOutput, json, "utf-8");
|
|
566
|
+
logChange(createChangeEntry({ tool: "container_security_config", action: "Create seccomp profile", target: resolvedOutput, dryRun: false, success: true }));
|
|
567
|
+
return { content: [formatToolOutput({ success: true, outputPath: resolvedOutput, syscallCount: allowedSyscalls.length, defaultAction })] };
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
return { content: [createErrorContent(`Seccomp profile generation failed: ${err instanceof Error ? err.message : String(err)}`)], isError: true };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
case "rootless": {
|
|
574
|
+
const { username, subuidCount, dryRun } = params;
|
|
575
|
+
try {
|
|
576
|
+
if (!username)
|
|
577
|
+
return { content: [createErrorContent("username is required for rootless action")], isError: true };
|
|
578
|
+
const safety = await SafeguardRegistry.getInstance().checkSafety("setup_rootless_containers", { username });
|
|
579
|
+
const checks = {};
|
|
580
|
+
const newuidmap = await executeCommand({ command: "which", args: ["newuidmap"], timeout: 5000 });
|
|
581
|
+
checks.newuidmap = newuidmap.exitCode === 0;
|
|
582
|
+
const newgidmap = await executeCommand({ command: "which", args: ["newgidmap"], timeout: 5000 });
|
|
583
|
+
checks.newgidmap = newgidmap.exitCode === 0;
|
|
584
|
+
const ns = await executeCommand({ command: "sysctl", args: ["-n", "kernel.unprivileged_userns_clone"], timeout: 5000 });
|
|
585
|
+
checks.userNamespacesEnabled = ns.exitCode === 0 && ns.stdout.trim() === "1";
|
|
586
|
+
const subuidCheck = await executeCommand({ command: "grep", args: [username, "/etc/subuid"], timeout: 5000 });
|
|
587
|
+
checks.subuidConfigured = subuidCheck.exitCode === 0;
|
|
588
|
+
if (dryRun) {
|
|
589
|
+
return { content: [formatToolOutput({ dryRun: true, username, currentState: checks, commands: [`usermod --add-subuids 100000-${100000 + subuidCount - 1} --add-subgids 100000-${100000 + subuidCount - 1} ${username}`, "sysctl -w kernel.unprivileged_userns_clone=1"], warnings: safety.warnings })] };
|
|
590
|
+
}
|
|
591
|
+
const results = [];
|
|
592
|
+
if (!checks.subuidConfigured) {
|
|
593
|
+
const r = await executeCommand({ command: "usermod", args: ["--add-subuids", `100000-${100000 + subuidCount - 1}`, "--add-subgids", `100000-${100000 + subuidCount - 1}`, username], timeout: 10000 });
|
|
594
|
+
results.push({ step: "Configure subuid/subgid", success: r.exitCode === 0, output: r.stderr || r.stdout });
|
|
595
|
+
}
|
|
596
|
+
if (!checks.userNamespacesEnabled) {
|
|
597
|
+
const r = await executeCommand({ command: "sysctl", args: ["-w", "kernel.unprivileged_userns_clone=1"], timeout: 10000 });
|
|
598
|
+
results.push({ step: "Enable user namespaces", success: r.exitCode === 0, output: r.stdout });
|
|
599
|
+
}
|
|
600
|
+
logChange(createChangeEntry({ tool: "container_security_config", action: `Configure rootless containers for ${username}`, target: username, dryRun: false, success: results.every((r) => r.success) }));
|
|
601
|
+
return { content: [formatToolOutput({ username, results, checks })] };
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return { content: [createErrorContent(`Rootless setup failed: ${err instanceof Error ? err.message : String(err)}`)], isError: true };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
default:
|
|
608
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
// ── 4. container_selinux_manage (kept as-is) ─────────────────────────────
|
|
612
|
+
server.tool("container_selinux_manage", "Manage SELinux settings: check status, get/set enforcement mode, manage booleans, audit denials", {
|
|
613
|
+
action: z.enum(["status", "getenforce", "setenforce", "booleans", "audit"]).describe("SELinux management action"),
|
|
614
|
+
mode: z.enum(["enforcing", "permissive", "disabled"]).optional().describe("SELinux mode (for setenforce)"),
|
|
615
|
+
boolean_name: z.string().optional().describe("SELinux boolean name"),
|
|
616
|
+
boolean_value: z.enum(["on", "off"]).optional().describe("SELinux boolean value"),
|
|
617
|
+
dry_run: z.boolean().optional().describe("Preview changes"),
|
|
618
|
+
}, async ({ action, mode, boolean_name, boolean_value, dry_run }) => {
|
|
619
|
+
try {
|
|
620
|
+
const sections = [`🛡️ SELinux Management: ${action}`, "=".repeat(40)];
|
|
621
|
+
switch (action) {
|
|
622
|
+
case "status": {
|
|
623
|
+
const result = await executeCommand({ command: "sestatus", args: [], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
624
|
+
if (result.exitCode !== 0) {
|
|
625
|
+
sections.push("\n⚠️ SELinux may not be installed.");
|
|
626
|
+
sections.push(result.stderr || result.stdout);
|
|
627
|
+
}
|
|
628
|
+
else
|
|
629
|
+
sections.push("\n" + result.stdout);
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case "getenforce": {
|
|
633
|
+
const result = await executeCommand({ command: "getenforce", args: [], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
634
|
+
if (result.exitCode !== 0)
|
|
635
|
+
sections.push("\n⚠️ getenforce not available.");
|
|
636
|
+
else
|
|
637
|
+
sections.push(`\nCurrent SELinux mode: ${result.stdout.trim()}`);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case "setenforce": {
|
|
641
|
+
if (!mode)
|
|
642
|
+
return { content: [createErrorContent("mode is required for setenforce")], isError: true };
|
|
643
|
+
if (mode === "disabled") {
|
|
644
|
+
sections.push("\n⚠️ Cannot disable SELinux at runtime.");
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
const modeValue = mode === "enforcing" ? "1" : "0";
|
|
648
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
649
|
+
sections.push(`\n[DRY RUN] Would set SELinux to ${mode}.`);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
const result = await executeCommand({ command: "sudo", args: ["setenforce", modeValue], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
653
|
+
if (result.exitCode !== 0)
|
|
654
|
+
return { content: [createErrorContent(`Failed: ${result.stderr}`)], isError: true };
|
|
655
|
+
sections.push(`\n✅ SELinux mode set to ${mode}.`);
|
|
656
|
+
logChange(createChangeEntry({ tool: "container_selinux_manage", action: "setenforce", target: "SELinux", after: mode, dryRun: false, success: true, rollbackCommand: `sudo setenforce ${mode === "enforcing" ? "0" : "1"}` }));
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "booleans": {
|
|
660
|
+
if (boolean_name && boolean_value) {
|
|
661
|
+
sanitizeArgs([boolean_name]);
|
|
662
|
+
if (dry_run ?? getConfig().dryRun) {
|
|
663
|
+
sections.push(`\n[DRY RUN] Would set '${boolean_name}' to ${boolean_value}.`);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
const result = await executeCommand({ command: "sudo", args: ["setsebool", "-P", boolean_name, boolean_value], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
667
|
+
if (result.exitCode !== 0)
|
|
668
|
+
return { content: [createErrorContent(`Failed: ${result.stderr}`)], isError: true };
|
|
669
|
+
sections.push(`\n✅ Boolean '${boolean_name}' set to ${boolean_value}.`);
|
|
670
|
+
logChange(createChangeEntry({ tool: "container_selinux_manage", action: "set_boolean", target: boolean_name, after: boolean_value, dryRun: false, success: true, rollbackCommand: `sudo setsebool -P ${boolean_name} ${boolean_value === "on" ? "off" : "on"}` }));
|
|
671
|
+
}
|
|
672
|
+
else if (boolean_name) {
|
|
673
|
+
sanitizeArgs([boolean_name]);
|
|
674
|
+
const result = await executeCommand({ command: "getsebool", args: [boolean_name], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
675
|
+
if (result.exitCode !== 0)
|
|
676
|
+
return { content: [createErrorContent(`Failed: ${result.stderr}`)], isError: true };
|
|
677
|
+
sections.push(`\n${result.stdout.trim()}`);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
const result = await executeCommand({ command: "getsebool", args: ["-a"], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
681
|
+
if (result.exitCode !== 0)
|
|
682
|
+
sections.push("\n⚠️ Cannot list booleans.");
|
|
683
|
+
else
|
|
684
|
+
sections.push(result.stdout);
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
case "audit": {
|
|
689
|
+
const result = await executeCommand({ command: "sudo", args: ["ausearch", "-m", "AVC", "-ts", "recent"], toolName: "container_selinux_manage", timeout: getToolTimeout("container_selinux_manage") });
|
|
690
|
+
if (result.exitCode !== 0 && (result.stderr.includes("no matches") || result.stdout.includes("no matches")))
|
|
691
|
+
sections.push("\n✅ No recent SELinux AVC denials.");
|
|
692
|
+
else if (result.exitCode !== 0)
|
|
693
|
+
sections.push("\n⚠️ Could not search audit logs.");
|
|
694
|
+
else
|
|
695
|
+
sections.push(`\n⚠️ Recent SELinux AVC Denials:\n${result.stdout}`);
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
// ── 5. container_namespace_check (kept as-is) ────────────────────────────
|
|
706
|
+
server.tool("container_namespace_check", "Check Linux namespace isolation for processes and system-wide namespace configuration", {
|
|
707
|
+
pid: z.number().optional().describe("Process ID to inspect namespaces for"),
|
|
708
|
+
check_type: z.enum(["user", "network", "pid", "mount", "all"]).optional().default("all").describe("Type of namespace check"),
|
|
709
|
+
}, async ({ pid, check_type }) => {
|
|
710
|
+
try {
|
|
711
|
+
const sections = ["📦 Namespace Isolation Check", "=".repeat(40)];
|
|
712
|
+
if (pid !== undefined) {
|
|
713
|
+
sections.push(`\nProcess PID: ${pid}`);
|
|
714
|
+
const nsResult = await executeCommand({ command: "ls", args: ["-la", `/proc/${pid}/ns/`], toolName: "container_namespace_check", timeout: getToolTimeout("container_namespace_check") });
|
|
715
|
+
if (nsResult.exitCode !== 0)
|
|
716
|
+
return { content: [createErrorContent(`Cannot read namespaces for PID ${pid}: ${nsResult.stderr}`)], isError: true };
|
|
717
|
+
sections.push("\nNamespace symlinks:");
|
|
718
|
+
sections.push(nsResult.stdout);
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
sections.push("\n── System Namespace Configuration ──");
|
|
722
|
+
if (check_type === "user" || check_type === "all") {
|
|
723
|
+
sections.push("\n🔑 User Namespaces:");
|
|
724
|
+
const maxNsResult = await executeCommand({ command: "cat", args: ["/proc/sys/user/max_user_namespaces"], toolName: "container_namespace_check", timeout: 5000 });
|
|
725
|
+
if (maxNsResult.exitCode === 0) {
|
|
726
|
+
const maxNs = maxNsResult.stdout.trim();
|
|
727
|
+
sections.push(` max_user_namespaces: ${maxNs}`);
|
|
728
|
+
sections.push(maxNs === "0" ? " ⚠️ User namespaces are disabled" : " ✅ User namespaces are enabled");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (check_type === "network" || check_type === "all") {
|
|
732
|
+
sections.push("\n🌐 Network Namespaces:");
|
|
733
|
+
const netnsResult = await executeCommand({ command: "ip", args: ["netns", "list"], toolName: "container_namespace_check", timeout: getToolTimeout("container_namespace_check") });
|
|
734
|
+
if (netnsResult.exitCode === 0 && netnsResult.stdout.trim()) {
|
|
735
|
+
const namespaces = netnsResult.stdout.trim().split("\n").filter((l) => l.trim());
|
|
736
|
+
sections.push(` Named network namespaces: ${namespaces.length}`);
|
|
737
|
+
for (const ns of namespaces)
|
|
738
|
+
sections.push(` - ${ns.trim()}`);
|
|
739
|
+
}
|
|
740
|
+
else
|
|
741
|
+
sections.push(" No named network namespaces found.");
|
|
742
|
+
}
|
|
743
|
+
if (check_type === "all" || check_type === "pid") {
|
|
744
|
+
sections.push("\n📋 All Active Namespaces (lsns):");
|
|
745
|
+
let lsnsResult = await executeCommand({ command: "lsns", args: [], toolName: "container_namespace_check", timeout: getToolTimeout("container_namespace_check") });
|
|
746
|
+
if (lsnsResult.exitCode !== 0)
|
|
747
|
+
lsnsResult = await executeCommand({ command: "sudo", args: ["lsns"], toolName: "container_namespace_check", timeout: getToolTimeout("container_namespace_check") });
|
|
748
|
+
if (lsnsResult.exitCode === 0)
|
|
749
|
+
sections.push(lsnsResult.stdout);
|
|
750
|
+
else
|
|
751
|
+
sections.push(" ⚠️ Cannot list namespaces.");
|
|
752
|
+
}
|
|
753
|
+
if (check_type === "mount" || check_type === "all") {
|
|
754
|
+
sections.push("\n📁 Mount Namespace Info:");
|
|
755
|
+
const mountInfoResult = await executeCommand({ command: "cat", args: ["/proc/self/mountinfo"], toolName: "container_namespace_check", timeout: 5000 });
|
|
756
|
+
if (mountInfoResult.exitCode === 0)
|
|
757
|
+
sections.push(` Current mount namespace has ${mountInfoResult.stdout.trim().split("\n").length} mount points`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return { content: [createTextContent(sections.join("\n"))] };
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
// ── 6. container_image_scan (kept as-is) ─────────────────────────────────
|
|
767
|
+
server.tool("container_image_scan", "Scan Docker container images for known vulnerabilities using Trivy or Grype (if installed).", {
|
|
768
|
+
image: z.string().describe("Docker image name/ID to scan, e.g. 'nginx:latest'"),
|
|
769
|
+
severity: z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "ALL"]).optional().default("HIGH").describe("Minimum severity to report"),
|
|
770
|
+
}, async (params) => {
|
|
771
|
+
try {
|
|
772
|
+
const trivyResult = await executeCommand({
|
|
773
|
+
command: "trivy",
|
|
774
|
+
args: ["image", "--severity", params.severity === "ALL" ? "CRITICAL,HIGH,MEDIUM,LOW" : `CRITICAL${params.severity !== "CRITICAL" ? ",HIGH" : ""}${params.severity === "MEDIUM" || params.severity === "LOW" ? ",MEDIUM" : ""}${params.severity === "LOW" ? ",LOW" : ""}`, "--format", "json", params.image],
|
|
775
|
+
timeout: 300000, toolName: "container_image_scan",
|
|
776
|
+
});
|
|
777
|
+
if (trivyResult.exitCode === 0)
|
|
778
|
+
return { content: [createTextContent(`Trivy scan results for ${params.image}:\n${trivyResult.stdout.substring(0, 8000)}`)] };
|
|
779
|
+
const grypeResult = await executeCommand({ command: "grype", args: [params.image, "-o", "json"], timeout: 300000, toolName: "container_image_scan" });
|
|
780
|
+
if (grypeResult.exitCode === 0)
|
|
781
|
+
return { content: [createTextContent(`Grype scan results for ${params.image}:\n${grypeResult.stdout.substring(0, 8000)}`)] };
|
|
782
|
+
return { content: [createTextContent(JSON.stringify({ error: "Neither Trivy nor Grype is installed", recommendation: "Install Trivy or Grype" }, null, 2))] };
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|