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,2654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System hardening tools for Kali Defense MCP Server.
|
|
3
|
+
*
|
|
4
|
+
* Registers 8 tools: harden_sysctl, harden_service, harden_permissions,
|
|
5
|
+
* harden_systemd, harden_kernel, harden_bootloader, harden_misc,
|
|
6
|
+
* memory_protection.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import * as nodePath from "node:path";
|
|
10
|
+
import { executeCommand } from "../core/executor.js";
|
|
11
|
+
import { getConfig, getToolTimeout } from "../core/config.js";
|
|
12
|
+
import { createTextContent, createErrorContent, parseSysctlOutput, parseSystemctlOutput, formatToolOutput, } from "../core/parsers.js";
|
|
13
|
+
import { logChange, createChangeEntry, backupFile, } from "../core/changelog.js";
|
|
14
|
+
import { validateServiceName, sanitizeArgs, validateSysctlKey, } from "../core/sanitizer.js";
|
|
15
|
+
import { SafeguardRegistry } from "../core/safeguards.js";
|
|
16
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { spawnSafe } from "../core/spawn-safe.js";
|
|
19
|
+
import { secureWriteFileSync } from "../core/secure-fs.js";
|
|
20
|
+
// ── TOOL-019 remediation: privilege check helper ───────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Check whether the current process has sufficient privileges for
|
|
23
|
+
* operations that modify system files (e.g., files in /etc/).
|
|
24
|
+
* Returns { ok: true } if privileged, or { ok: false, message: string } if not.
|
|
25
|
+
*/
|
|
26
|
+
function checkPrivileges() {
|
|
27
|
+
// Check if running as root
|
|
28
|
+
if (typeof process.getuid === "function" && process.getuid() === 0) {
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
// Check if sudo is available and the user has passwordless sudo
|
|
32
|
+
try {
|
|
33
|
+
execSync("sudo -n true 2>/dev/null", { timeout: 3000, stdio: "ignore" });
|
|
34
|
+
return { ok: true };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
message: "Insufficient privileges. This operation requires root access or sudo. " +
|
|
40
|
+
"Run this tool as root or ensure sudo is available without a password prompt.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── TOOL-007 remediation: path validation for harden_permissions ───────────
|
|
45
|
+
/** Allowed root directories for harden_permissions fix/check actions */
|
|
46
|
+
const ALLOWED_PERMISSION_DIRS = [
|
|
47
|
+
"/etc",
|
|
48
|
+
"/boot",
|
|
49
|
+
"/var",
|
|
50
|
+
"/usr",
|
|
51
|
+
"/home",
|
|
52
|
+
"/root",
|
|
53
|
+
"/tmp",
|
|
54
|
+
"/opt",
|
|
55
|
+
"/srv",
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Validate that a path is within allowed directories and doesn't use traversal.
|
|
59
|
+
* Defense-in-depth: reject `..` before resolution, then verify resolved path.
|
|
60
|
+
*/
|
|
61
|
+
function validatePathWithinAllowed(inputPath) {
|
|
62
|
+
// Defense-in-depth: reject any path containing `..` sequences
|
|
63
|
+
if (inputPath.includes("..")) {
|
|
64
|
+
throw new Error(`Path traversal detected: '..' sequences are not allowed in path '${inputPath}'`);
|
|
65
|
+
}
|
|
66
|
+
const resolved = nodePath.resolve(inputPath);
|
|
67
|
+
const isAllowed = ALLOWED_PERMISSION_DIRS.some((dir) => resolved === dir || resolved.startsWith(dir + "/"));
|
|
68
|
+
if (!isAllowed) {
|
|
69
|
+
throw new Error(`Path '${resolved}' is outside allowed directories. Allowed roots: ${ALLOWED_PERMISSION_DIRS.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
const SYSCTL_RECOMMENDATIONS = [
|
|
74
|
+
// Network hardening
|
|
75
|
+
{ key: "net.ipv4.ip_forward", recommended: "0", description: "Disable IP forwarding", category: "network" },
|
|
76
|
+
{ key: "net.ipv4.conf.all.rp_filter", recommended: "1", description: "Enable reverse path filtering", category: "network" },
|
|
77
|
+
{ key: "net.ipv4.conf.default.rp_filter", recommended: "1", description: "Enable default reverse path filtering", category: "network" },
|
|
78
|
+
{ key: "net.ipv4.conf.all.accept_redirects", recommended: "0", description: "Disable ICMP redirects", category: "network" },
|
|
79
|
+
{ key: "net.ipv4.conf.default.accept_redirects", recommended: "0", description: "Disable default ICMP redirects", category: "network" },
|
|
80
|
+
{ key: "net.ipv4.conf.all.send_redirects", recommended: "0", description: "Disable sending ICMP redirects", category: "network" },
|
|
81
|
+
{ key: "net.ipv4.conf.default.send_redirects", recommended: "0", description: "Disable default sending ICMP redirects", category: "network" },
|
|
82
|
+
{ key: "net.ipv4.conf.all.accept_source_route", recommended: "0", description: "Disable source routing", category: "network" },
|
|
83
|
+
{ key: "net.ipv4.conf.default.accept_source_route", recommended: "0", description: "Disable default source routing", category: "network" },
|
|
84
|
+
{ key: "net.ipv4.conf.all.log_martians", recommended: "1", description: "Log martian packets", category: "network" },
|
|
85
|
+
{ key: "net.ipv4.conf.default.log_martians", recommended: "1", description: "Log default martian packets", category: "network" },
|
|
86
|
+
{ key: "net.ipv4.conf.all.secure_redirects", recommended: "0", description: "Disable secure ICMP redirects", category: "network" },
|
|
87
|
+
{ key: "net.ipv4.conf.default.secure_redirects", recommended: "0", description: "Disable default secure ICMP redirects", category: "network" },
|
|
88
|
+
{ key: "net.ipv4.icmp_echo_ignore_broadcasts", recommended: "1", description: "Ignore ICMP broadcast requests", category: "network" },
|
|
89
|
+
{ key: "net.ipv4.icmp_ignore_bogus_error_responses", recommended: "1", description: "Ignore bogus ICMP error responses", category: "network" },
|
|
90
|
+
{ key: "net.ipv4.tcp_syncookies", recommended: "1", description: "Enable TCP SYN cookies", category: "network" },
|
|
91
|
+
{ key: "net.ipv6.conf.all.accept_redirects", recommended: "0", description: "Disable IPv6 ICMP redirects", category: "network" },
|
|
92
|
+
{ key: "net.ipv6.conf.default.accept_redirects", recommended: "0", description: "Disable default IPv6 ICMP redirects", category: "network" },
|
|
93
|
+
{ key: "net.ipv6.conf.all.accept_source_route", recommended: "0", description: "Disable IPv6 source routing", category: "network" },
|
|
94
|
+
{ key: "net.ipv6.conf.all.accept_ra", recommended: "0", description: "Disable IPv6 router advertisements", category: "network" },
|
|
95
|
+
{ key: "net.ipv6.conf.default.accept_ra", recommended: "0", description: "Disable default IPv6 router advertisements", category: "network" },
|
|
96
|
+
// Kernel hardening
|
|
97
|
+
{ key: "kernel.randomize_va_space", recommended: "2", description: "Full ASLR randomization", category: "kernel" },
|
|
98
|
+
{ key: "kernel.sysrq", recommended: "0", description: "Disable SysRq key", category: "kernel" },
|
|
99
|
+
{ key: "kernel.core_uses_pid", recommended: "1", description: "Append PID to core dumps", category: "kernel" },
|
|
100
|
+
{ key: "kernel.dmesg_restrict", recommended: "1", description: "Restrict dmesg access", category: "kernel" },
|
|
101
|
+
{ key: "kernel.kptr_restrict", recommended: "2", description: "Restrict kernel pointer access", category: "kernel" },
|
|
102
|
+
{ key: "kernel.yama.ptrace_scope", recommended: "1", description: "Restrict ptrace scope", category: "kernel" },
|
|
103
|
+
{ key: "kernel.unprivileged_bpf_disabled", recommended: "1", description: "Disable unprivileged BPF", category: "kernel" },
|
|
104
|
+
{ key: "net.core.bpf_jit_harden", recommended: "2", description: "Harden BPF JIT compiler", category: "kernel" },
|
|
105
|
+
{ key: "kernel.kexec_load_disabled", recommended: "1", description: "Disable kexec loading", category: "kernel" },
|
|
106
|
+
{ key: "kernel.perf_event_paranoid", recommended: "3", description: "Restrict perf events", category: "kernel" },
|
|
107
|
+
{ key: "kernel.modules_disabled", recommended: "1", description: "Disable kernel module loading", category: "kernel" },
|
|
108
|
+
{ key: "vm.unprivileged_userfaultfd", recommended: "0", description: "Disable unprivileged userfaultfd", category: "kernel" },
|
|
109
|
+
{ key: "kernel.io_uring_disabled", recommended: "2", description: "Disable io_uring for unprivileged users", category: "kernel" },
|
|
110
|
+
// Filesystem hardening
|
|
111
|
+
{ key: "fs.suid_dumpable", recommended: "0", description: "Disable SUID core dumps", category: "fs" },
|
|
112
|
+
{ key: "fs.protected_hardlinks", recommended: "1", description: "Protect hardlinks", category: "fs" },
|
|
113
|
+
{ key: "fs.protected_symlinks", recommended: "1", description: "Protect symlinks", category: "fs" },
|
|
114
|
+
{ key: "fs.protected_fifos", recommended: "2", description: "Protect FIFO files", category: "fs" },
|
|
115
|
+
{ key: "fs.protected_regular", recommended: "2", description: "Protect regular files", category: "fs" },
|
|
116
|
+
// SYN flood tuning parameters
|
|
117
|
+
{ key: "net.ipv4.tcp_max_syn_backlog", recommended: "2048", description: "Increase SYN backlog queue to resist SYN flood attacks", category: "network" },
|
|
118
|
+
{ key: "net.ipv4.tcp_synack_retries", recommended: "2", description: "Reduce SYN-ACK retries to limit half-open connections", category: "network" },
|
|
119
|
+
{ key: "net.ipv4.tcp_syn_retries", recommended: "5", description: "Limit SYN retry attempts", category: "network" },
|
|
120
|
+
{ key: "net.ipv4.tcp_timestamps", recommended: "0", description: "Disable TCP timestamps to prevent uptime fingerprinting", category: "network" },
|
|
121
|
+
];
|
|
122
|
+
// ── Known unnecessary/dangerous services ───────────────────────────────────
|
|
123
|
+
const UNNECESSARY_SERVICES = [
|
|
124
|
+
{ name: "telnet.socket", reason: "Telnet is unencrypted and insecure" },
|
|
125
|
+
{ name: "rsh.socket", reason: "Remote shell is unencrypted and insecure" },
|
|
126
|
+
{ name: "rlogin.socket", reason: "Remote login is unencrypted and insecure" },
|
|
127
|
+
{ name: "finger.socket", reason: "Finger leaks user information" },
|
|
128
|
+
{ name: "talk.socket", reason: "Talk is unencrypted" },
|
|
129
|
+
{ name: "ntalk.socket", reason: "Ntalk is unencrypted" },
|
|
130
|
+
{ name: "tftp.socket", reason: "TFTP is unencrypted and unauthenticated" },
|
|
131
|
+
{ name: "xinetd.service", reason: "Legacy super-server, usually unnecessary" },
|
|
132
|
+
{ name: "avahi-daemon.service", reason: "mDNS may expose services unnecessarily" },
|
|
133
|
+
{ name: "cups.service", reason: "Print server often unnecessary on servers" },
|
|
134
|
+
{ name: "bluetooth.service", reason: "Bluetooth often unnecessary on servers" },
|
|
135
|
+
{ name: "rpcbind.service", reason: "RPC portmapper often unnecessary" },
|
|
136
|
+
{ name: "nfs-server.service", reason: "NFS server if not needed" },
|
|
137
|
+
{ name: "vsftpd.service", reason: "FTP is insecure, prefer SFTP" },
|
|
138
|
+
{ name: "snmpd.service", reason: "SNMP may expose system information" },
|
|
139
|
+
{ name: "isc-dhcp-server.service", reason: "DHCP server often unnecessary" },
|
|
140
|
+
{ name: "slapd.service", reason: "LDAP server if not needed" },
|
|
141
|
+
{ name: "named.service", reason: "DNS server if not needed" },
|
|
142
|
+
{ name: "dovecot.service", reason: "Mail delivery agent if not needed" },
|
|
143
|
+
{ name: "smbd.service", reason: "Samba file sharing may expose data" },
|
|
144
|
+
{ name: "nmbd.service", reason: "Samba NetBIOS name service may expose data" },
|
|
145
|
+
{ name: "squid.service", reason: "Proxy server if not needed" },
|
|
146
|
+
{ name: "apache2.service", reason: "Web server if not needed" },
|
|
147
|
+
{ name: "nginx.service", reason: "Web server if not needed" },
|
|
148
|
+
];
|
|
149
|
+
const PERMISSION_CHECKS = [
|
|
150
|
+
{ path: "/etc/passwd", expectedMode: "644", expectedOwner: "root", expectedGroup: "root", scope: "passwd", description: "User account database" },
|
|
151
|
+
{ path: "/etc/group", expectedMode: "644", expectedOwner: "root", expectedGroup: "root", scope: "passwd", description: "Group database" },
|
|
152
|
+
{ path: "/etc/shadow", expectedMode: "640", expectedOwner: "root", expectedGroup: "shadow", scope: "shadow", description: "Password hash database" },
|
|
153
|
+
{ path: "/etc/gshadow", expectedMode: "640", expectedOwner: "root", expectedGroup: "shadow", scope: "shadow", description: "Group password database" },
|
|
154
|
+
{ path: "/etc/ssh/sshd_config", expectedMode: "600", expectedOwner: "root", expectedGroup: "root", scope: "ssh", description: "SSH server configuration" },
|
|
155
|
+
{ path: "/etc/crontab", expectedMode: "600", expectedOwner: "root", expectedGroup: "root", scope: "cron", description: "System crontab" },
|
|
156
|
+
{ path: "/etc/cron.d", expectedMode: "700", expectedOwner: "root", expectedGroup: "root", scope: "cron", description: "Cron drop-in directory" },
|
|
157
|
+
{ path: "/etc/cron.daily", expectedMode: "700", expectedOwner: "root", expectedGroup: "root", scope: "cron", description: "Daily cron directory" },
|
|
158
|
+
{ path: "/etc/cron.hourly", expectedMode: "700", expectedOwner: "root", expectedGroup: "root", scope: "cron", description: "Hourly cron directory" },
|
|
159
|
+
{ path: "/etc/cron.weekly", expectedMode: "700", expectedOwner: "root", expectedGroup: "root", scope: "cron", description: "Weekly cron directory" },
|
|
160
|
+
{ path: "/etc/cron.monthly", expectedMode: "700", expectedOwner: "root", expectedGroup: "root", scope: "cron", description: "Monthly cron directory" },
|
|
161
|
+
{ path: "/etc/sudoers", expectedMode: "440", expectedOwner: "root", expectedGroup: "root", scope: "critical", description: "Sudoers configuration" },
|
|
162
|
+
{ path: "/etc/sudoers.d", expectedMode: "750", expectedOwner: "root", expectedGroup: "root", scope: "critical", description: "Sudoers drop-in directory" },
|
|
163
|
+
{ path: "/boot/grub/grub.cfg", expectedMode: "600", expectedOwner: "root", expectedGroup: "root", scope: "critical", description: "GRUB configuration" },
|
|
164
|
+
];
|
|
165
|
+
// ── USB device control constants and helpers ───────────────────────────────
|
|
166
|
+
/** Path to USB device whitelist file */
|
|
167
|
+
const USB_WHITELIST_PATH = "/var/lib/kali-defense/usb/whitelist.json";
|
|
168
|
+
/**
|
|
169
|
+
* Run a command via spawnSafe and collect output as a promise.
|
|
170
|
+
* Handles errors gracefully — returns error info instead of throwing.
|
|
171
|
+
*/
|
|
172
|
+
async function runUsbCommand(command, args, timeoutMs = 30_000) {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
let child;
|
|
175
|
+
try {
|
|
176
|
+
child = spawnSafe(command, args);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
180
|
+
resolve({ stdout: "", stderr: msg, exitCode: -1 });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let stdout = "";
|
|
184
|
+
let stderr = "";
|
|
185
|
+
let resolved = false;
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
if (!resolved) {
|
|
188
|
+
resolved = true;
|
|
189
|
+
child.kill("SIGTERM");
|
|
190
|
+
resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
|
|
191
|
+
}
|
|
192
|
+
}, timeoutMs);
|
|
193
|
+
child.stdout?.on("data", (data) => {
|
|
194
|
+
stdout += data.toString();
|
|
195
|
+
});
|
|
196
|
+
child.stderr?.on("data", (data) => {
|
|
197
|
+
stderr += data.toString();
|
|
198
|
+
});
|
|
199
|
+
child.on("close", (code) => {
|
|
200
|
+
if (!resolved) {
|
|
201
|
+
resolved = true;
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
resolve({ stdout, stderr, exitCode: code ?? -1 });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
child.on("error", (err) => {
|
|
207
|
+
if (!resolved) {
|
|
208
|
+
resolved = true;
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
resolve({ stdout, stderr: err.message, exitCode: -1 });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Load USB device whitelist from disk.
|
|
217
|
+
* Returns empty array if file doesn't exist or is invalid.
|
|
218
|
+
*/
|
|
219
|
+
function loadUsbWhitelist() {
|
|
220
|
+
try {
|
|
221
|
+
if (existsSync(USB_WHITELIST_PATH)) {
|
|
222
|
+
const data = readFileSync(USB_WHITELIST_PATH, "utf-8");
|
|
223
|
+
const parsed = JSON.parse(data);
|
|
224
|
+
if (Array.isArray(parsed))
|
|
225
|
+
return parsed;
|
|
226
|
+
if (parsed && Array.isArray(parsed.devices))
|
|
227
|
+
return parsed.devices;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Fall through to default
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Save USB device whitelist to disk using secure file write.
|
|
237
|
+
*/
|
|
238
|
+
function saveUsbWhitelist(entries) {
|
|
239
|
+
secureWriteFileSync(USB_WHITELIST_PATH, JSON.stringify({ devices: entries }, null, 2));
|
|
240
|
+
}
|
|
241
|
+
// ── Registration entry point ───────────────────────────────────────────────
|
|
242
|
+
export function registerHardeningTools(server) {
|
|
243
|
+
// ── 1. harden_sysctl (merged: harden_sysctl_get, harden_sysctl_set, harden_sysctl_audit) ──
|
|
244
|
+
server.tool("harden_sysctl", "Manage sysctl kernel parameters. Actions: get=read value(s), set=write value, audit=security check against CIS/STIG", {
|
|
245
|
+
action: z
|
|
246
|
+
.enum(["get", "set", "audit"])
|
|
247
|
+
.describe("Action: get=read value, set=write value, audit=security check"),
|
|
248
|
+
// get params
|
|
249
|
+
key: z.string().optional().describe("Sysctl key (required for get single/set)"),
|
|
250
|
+
all: z.boolean().optional().default(false).describe("Return all sysctl values (for get)"),
|
|
251
|
+
pattern: z.string().optional().describe("Filter keys matching substring (for get)"),
|
|
252
|
+
// set params
|
|
253
|
+
value: z.string().optional().describe("Value to set (required for set)"),
|
|
254
|
+
persistent: z.boolean().optional().default(false).describe("Write to /etc/sysctl.d/99-kali-defense.conf (for set)"),
|
|
255
|
+
// audit params
|
|
256
|
+
category: z.enum(["network", "kernel", "fs", "all"]).optional().default("all").describe("Category of settings to audit (for audit)"),
|
|
257
|
+
// shared
|
|
258
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for set)"),
|
|
259
|
+
}, async (params) => {
|
|
260
|
+
const { action } = params;
|
|
261
|
+
switch (action) {
|
|
262
|
+
// ── get ──────────────────────────────────────────────────────
|
|
263
|
+
case "get": {
|
|
264
|
+
try {
|
|
265
|
+
if (params.key) {
|
|
266
|
+
const validatedKey = validateSysctlKey(params.key);
|
|
267
|
+
const result = await executeCommand({
|
|
268
|
+
command: "sysctl",
|
|
269
|
+
args: [validatedKey],
|
|
270
|
+
toolName: "harden_sysctl",
|
|
271
|
+
timeout: getToolTimeout("harden_sysctl"),
|
|
272
|
+
});
|
|
273
|
+
if (result.exitCode !== 0) {
|
|
274
|
+
return {
|
|
275
|
+
content: [
|
|
276
|
+
createErrorContent(`sysctl get failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
277
|
+
],
|
|
278
|
+
isError: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const entries = parseSysctlOutput(result.stdout);
|
|
282
|
+
return { content: [formatToolOutput({ key: validatedKey, entries })] };
|
|
283
|
+
}
|
|
284
|
+
if (params.all || params.pattern) {
|
|
285
|
+
const result = await executeCommand({
|
|
286
|
+
command: "sysctl",
|
|
287
|
+
args: ["-a"],
|
|
288
|
+
toolName: "harden_sysctl",
|
|
289
|
+
timeout: getToolTimeout("harden_sysctl"),
|
|
290
|
+
});
|
|
291
|
+
if (result.exitCode !== 0) {
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
createErrorContent(`sysctl -a failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
295
|
+
],
|
|
296
|
+
isError: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
let entries = parseSysctlOutput(result.stdout);
|
|
300
|
+
if (params.pattern) {
|
|
301
|
+
const lowerPattern = params.pattern.toLowerCase();
|
|
302
|
+
entries = entries.filter((e) => e.key.toLowerCase().includes(lowerPattern));
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
formatToolOutput({
|
|
307
|
+
count: entries.length,
|
|
308
|
+
pattern: params.pattern ?? "all",
|
|
309
|
+
entries,
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
createErrorContent("Specify either 'key', 'all: true', or 'pattern' to query sysctl values"),
|
|
317
|
+
],
|
|
318
|
+
isError: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
323
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── set ──────────────────────────────────────────────────────
|
|
327
|
+
case "set": {
|
|
328
|
+
try {
|
|
329
|
+
if (!params.key) {
|
|
330
|
+
return { content: [createErrorContent("Error: 'key' is required for set action")], isError: true };
|
|
331
|
+
}
|
|
332
|
+
if (!params.value) {
|
|
333
|
+
return { content: [createErrorContent("Error: 'value' is required for set action")], isError: true };
|
|
334
|
+
}
|
|
335
|
+
// TOOL-019: Pre-execution privilege check for sysctl set
|
|
336
|
+
const privCheck = checkPrivileges();
|
|
337
|
+
if (!privCheck.ok) {
|
|
338
|
+
return { content: [createErrorContent(`Cannot set sysctl value: ${privCheck.message}`)], isError: true };
|
|
339
|
+
}
|
|
340
|
+
const validatedKey = validateSysctlKey(params.key);
|
|
341
|
+
sanitizeArgs([params.value]);
|
|
342
|
+
// Get current value for before state
|
|
343
|
+
const currentResult = await executeCommand({
|
|
344
|
+
command: "sysctl",
|
|
345
|
+
args: [validatedKey],
|
|
346
|
+
toolName: "harden_sysctl",
|
|
347
|
+
});
|
|
348
|
+
const beforeValue = currentResult.stdout.trim();
|
|
349
|
+
const fullCmd = `sudo sysctl -w ${validatedKey}=${params.value}`;
|
|
350
|
+
const persistPath = "/etc/sysctl.d/99-kali-defense.conf";
|
|
351
|
+
if (params.dry_run ?? getConfig().dryRun) {
|
|
352
|
+
let preview = `[DRY-RUN] Would execute:\n ${fullCmd}\n\nCurrent value: ${beforeValue}`;
|
|
353
|
+
if (params.persistent) {
|
|
354
|
+
preview += `\n\nWould also append to ${persistPath}:\n ${validatedKey} = ${params.value}`;
|
|
355
|
+
}
|
|
356
|
+
const entry = createChangeEntry({
|
|
357
|
+
tool: "harden_sysctl",
|
|
358
|
+
action: `[DRY-RUN] Set sysctl ${validatedKey}`,
|
|
359
|
+
target: validatedKey,
|
|
360
|
+
before: beforeValue,
|
|
361
|
+
after: `${validatedKey} = ${params.value}`,
|
|
362
|
+
dryRun: true,
|
|
363
|
+
success: true,
|
|
364
|
+
rollbackCommand: beforeValue
|
|
365
|
+
? `sudo sysctl -w ${beforeValue}`
|
|
366
|
+
: undefined,
|
|
367
|
+
});
|
|
368
|
+
logChange(entry);
|
|
369
|
+
return { content: [createTextContent(preview)] };
|
|
370
|
+
}
|
|
371
|
+
// Set the value
|
|
372
|
+
const result = await executeCommand({
|
|
373
|
+
command: "sudo",
|
|
374
|
+
args: ["sysctl", "-w", `${validatedKey}=${params.value}`],
|
|
375
|
+
toolName: "harden_sysctl",
|
|
376
|
+
timeout: getToolTimeout("harden_sysctl"),
|
|
377
|
+
});
|
|
378
|
+
const success = result.exitCode === 0;
|
|
379
|
+
// Persist if requested
|
|
380
|
+
let persistResult = "";
|
|
381
|
+
if (success && params.persistent) {
|
|
382
|
+
try {
|
|
383
|
+
backupFile(persistPath);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// File may not exist yet
|
|
387
|
+
}
|
|
388
|
+
await executeCommand({
|
|
389
|
+
command: "sudo",
|
|
390
|
+
args: ["mkdir", "-p", "/etc/sysctl.d"],
|
|
391
|
+
toolName: "harden_sysctl",
|
|
392
|
+
});
|
|
393
|
+
await executeCommand({
|
|
394
|
+
command: "sudo",
|
|
395
|
+
args: ["sed", "-i", `/^${validatedKey}\\s*=/d`, persistPath],
|
|
396
|
+
toolName: "harden_sysctl",
|
|
397
|
+
});
|
|
398
|
+
const appendResult = await executeCommand({
|
|
399
|
+
command: "sudo",
|
|
400
|
+
args: ["tee", "-a", persistPath],
|
|
401
|
+
stdin: `${validatedKey} = ${params.value}\n`,
|
|
402
|
+
toolName: "harden_sysctl",
|
|
403
|
+
});
|
|
404
|
+
persistResult = appendResult.exitCode === 0
|
|
405
|
+
? `\nPersisted to ${persistPath}`
|
|
406
|
+
: `\nWarning: Failed to persist: ${appendResult.stderr}`;
|
|
407
|
+
}
|
|
408
|
+
const rollbackCmd = beforeValue
|
|
409
|
+
? `sudo sysctl -w ${beforeValue}`
|
|
410
|
+
: undefined;
|
|
411
|
+
const entry = createChangeEntry({
|
|
412
|
+
tool: "harden_sysctl",
|
|
413
|
+
action: `Set sysctl ${validatedKey}=${params.value}`,
|
|
414
|
+
target: validatedKey,
|
|
415
|
+
before: beforeValue,
|
|
416
|
+
after: `${validatedKey} = ${params.value}`,
|
|
417
|
+
dryRun: false,
|
|
418
|
+
success,
|
|
419
|
+
error: success ? undefined : result.stderr,
|
|
420
|
+
rollbackCommand: rollbackCmd,
|
|
421
|
+
});
|
|
422
|
+
logChange(entry);
|
|
423
|
+
if (!success) {
|
|
424
|
+
return {
|
|
425
|
+
content: [
|
|
426
|
+
createErrorContent(`sysctl set failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
427
|
+
],
|
|
428
|
+
isError: true,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
createTextContent(`Set ${validatedKey} = ${params.value}\nPrevious: ${beforeValue}${persistResult}\nRollback: ${rollbackCmd ?? "N/A"}`),
|
|
434
|
+
],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
439
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ── audit ────────────────────────────────────────────────────
|
|
443
|
+
case "audit": {
|
|
444
|
+
try {
|
|
445
|
+
const result = await executeCommand({
|
|
446
|
+
command: "sysctl",
|
|
447
|
+
args: ["-a"],
|
|
448
|
+
toolName: "harden_sysctl",
|
|
449
|
+
timeout: getToolTimeout("harden_sysctl"),
|
|
450
|
+
});
|
|
451
|
+
if (result.exitCode !== 0) {
|
|
452
|
+
return {
|
|
453
|
+
content: [
|
|
454
|
+
createErrorContent(`sysctl -a failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
455
|
+
],
|
|
456
|
+
isError: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const entries = parseSysctlOutput(result.stdout);
|
|
460
|
+
const currentValues = new Map(entries.map((e) => [e.key, e.value]));
|
|
461
|
+
const category = params.category ?? "all";
|
|
462
|
+
const checks = category === "all"
|
|
463
|
+
? SYSCTL_RECOMMENDATIONS
|
|
464
|
+
: SYSCTL_RECOMMENDATIONS.filter((r) => r.category === category);
|
|
465
|
+
const findings = [];
|
|
466
|
+
let compliantCount = 0;
|
|
467
|
+
let nonCompliantCount = 0;
|
|
468
|
+
let unknownCount = 0;
|
|
469
|
+
for (const check of checks) {
|
|
470
|
+
const current = currentValues.get(check.key);
|
|
471
|
+
if (current === undefined) {
|
|
472
|
+
unknownCount++;
|
|
473
|
+
findings.push({
|
|
474
|
+
key: check.key,
|
|
475
|
+
current: "NOT SET",
|
|
476
|
+
recommended: check.recommended,
|
|
477
|
+
description: check.description,
|
|
478
|
+
compliant: false,
|
|
479
|
+
category: check.category,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
else if (current === check.recommended) {
|
|
483
|
+
compliantCount++;
|
|
484
|
+
findings.push({
|
|
485
|
+
key: check.key,
|
|
486
|
+
current,
|
|
487
|
+
recommended: check.recommended,
|
|
488
|
+
description: check.description,
|
|
489
|
+
compliant: true,
|
|
490
|
+
category: check.category,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
nonCompliantCount++;
|
|
495
|
+
findings.push({
|
|
496
|
+
key: check.key,
|
|
497
|
+
current,
|
|
498
|
+
recommended: check.recommended,
|
|
499
|
+
description: check.description,
|
|
500
|
+
compliant: false,
|
|
501
|
+
category: check.category,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const output = {
|
|
506
|
+
category,
|
|
507
|
+
summary: {
|
|
508
|
+
total: checks.length,
|
|
509
|
+
compliant: compliantCount,
|
|
510
|
+
nonCompliant: nonCompliantCount,
|
|
511
|
+
unknown: unknownCount,
|
|
512
|
+
compliancePercent: checks.length > 0
|
|
513
|
+
? Math.round((compliantCount / checks.length) * 100)
|
|
514
|
+
: 0,
|
|
515
|
+
},
|
|
516
|
+
findings,
|
|
517
|
+
};
|
|
518
|
+
return { content: [formatToolOutput(output)] };
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
522
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// ── 2. harden_service (merged: harden_service_manage, harden_service_audit) ──
|
|
530
|
+
server.tool("harden_service", "Manage and audit systemd services. Actions: manage=enable/disable/start/stop/restart/mask/unmask/status, audit=check for unnecessary services", {
|
|
531
|
+
action: z
|
|
532
|
+
.enum(["manage", "audit"])
|
|
533
|
+
.describe("Action: manage=control service, audit=find unnecessary services"),
|
|
534
|
+
// manage params
|
|
535
|
+
service: z.string().optional().describe("Service name, e.g. 'ssh.service' (required for manage)"),
|
|
536
|
+
service_action: z
|
|
537
|
+
.enum(["enable", "disable", "stop", "start", "restart", "mask", "unmask", "status"])
|
|
538
|
+
.optional()
|
|
539
|
+
.describe("Service action (required for manage)"),
|
|
540
|
+
// audit params
|
|
541
|
+
show_all: z.boolean().optional().default(false).describe("Show all running services, not just flagged (for audit)"),
|
|
542
|
+
// shared
|
|
543
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for manage)"),
|
|
544
|
+
}, async (params) => {
|
|
545
|
+
const { action } = params;
|
|
546
|
+
switch (action) {
|
|
547
|
+
// ── manage ───────────────────────────────────────────────────
|
|
548
|
+
case "manage": {
|
|
549
|
+
try {
|
|
550
|
+
if (!params.service) {
|
|
551
|
+
return { content: [createErrorContent("Error: 'service' is required for manage action")], isError: true };
|
|
552
|
+
}
|
|
553
|
+
if (!params.service_action) {
|
|
554
|
+
return { content: [createErrorContent("Error: 'service_action' is required for manage action")], isError: true };
|
|
555
|
+
}
|
|
556
|
+
const validatedService = validateServiceName(params.service);
|
|
557
|
+
const svcAction = params.service_action;
|
|
558
|
+
const fullCmd = `sudo systemctl ${svcAction} ${validatedService}`;
|
|
559
|
+
// Status is always read-only, skip dry_run check
|
|
560
|
+
if (svcAction === "status") {
|
|
561
|
+
const result = await executeCommand({
|
|
562
|
+
command: "systemctl",
|
|
563
|
+
args: ["status", validatedService],
|
|
564
|
+
toolName: "harden_service",
|
|
565
|
+
timeout: getToolTimeout("harden_service"),
|
|
566
|
+
});
|
|
567
|
+
return {
|
|
568
|
+
content: [
|
|
569
|
+
createTextContent(`Service: ${validatedService}\n\n${result.stdout}${result.stderr ? `\n${result.stderr}` : ""}`),
|
|
570
|
+
],
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
// Determine rollback action
|
|
574
|
+
const rollbackActions = {
|
|
575
|
+
enable: "disable",
|
|
576
|
+
disable: "enable",
|
|
577
|
+
stop: "start",
|
|
578
|
+
start: "stop",
|
|
579
|
+
restart: "restart",
|
|
580
|
+
mask: "unmask",
|
|
581
|
+
unmask: "mask",
|
|
582
|
+
};
|
|
583
|
+
const rollbackCmd = `sudo systemctl ${rollbackActions[svcAction]} ${validatedService}`;
|
|
584
|
+
if (params.dry_run ?? getConfig().dryRun) {
|
|
585
|
+
const entry = createChangeEntry({
|
|
586
|
+
tool: "harden_service",
|
|
587
|
+
action: `[DRY-RUN] ${svcAction} service`,
|
|
588
|
+
target: validatedService,
|
|
589
|
+
dryRun: true,
|
|
590
|
+
success: true,
|
|
591
|
+
rollbackCommand: rollbackCmd,
|
|
592
|
+
});
|
|
593
|
+
logChange(entry);
|
|
594
|
+
return {
|
|
595
|
+
content: [
|
|
596
|
+
createTextContent(`[DRY-RUN] Would execute:\n ${fullCmd}\n\nRollback:\n ${rollbackCmd}`),
|
|
597
|
+
],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// Get before state
|
|
601
|
+
const beforeResult = await executeCommand({
|
|
602
|
+
command: "systemctl",
|
|
603
|
+
args: ["is-active", validatedService],
|
|
604
|
+
toolName: "harden_service",
|
|
605
|
+
});
|
|
606
|
+
const beforeState = beforeResult.stdout.trim();
|
|
607
|
+
const result = await executeCommand({
|
|
608
|
+
command: "sudo",
|
|
609
|
+
args: ["systemctl", svcAction, validatedService],
|
|
610
|
+
toolName: "harden_service",
|
|
611
|
+
timeout: getToolTimeout("harden_service"),
|
|
612
|
+
});
|
|
613
|
+
const success = result.exitCode === 0;
|
|
614
|
+
// Get after state
|
|
615
|
+
const afterResult = await executeCommand({
|
|
616
|
+
command: "systemctl",
|
|
617
|
+
args: ["is-active", validatedService],
|
|
618
|
+
toolName: "harden_service",
|
|
619
|
+
});
|
|
620
|
+
const afterState = afterResult.stdout.trim();
|
|
621
|
+
const entry = createChangeEntry({
|
|
622
|
+
tool: "harden_service",
|
|
623
|
+
action: `${svcAction} service`,
|
|
624
|
+
target: validatedService,
|
|
625
|
+
before: beforeState,
|
|
626
|
+
after: afterState,
|
|
627
|
+
dryRun: false,
|
|
628
|
+
success,
|
|
629
|
+
error: success ? undefined : result.stderr,
|
|
630
|
+
rollbackCommand: rollbackCmd,
|
|
631
|
+
});
|
|
632
|
+
logChange(entry);
|
|
633
|
+
if (!success) {
|
|
634
|
+
return {
|
|
635
|
+
content: [
|
|
636
|
+
createErrorContent(`systemctl ${svcAction} failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
637
|
+
],
|
|
638
|
+
isError: true,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
content: [
|
|
643
|
+
createTextContent(`Service ${validatedService}: ${svcAction} completed.\nBefore: ${beforeState}\nAfter: ${afterState}\nRollback: ${rollbackCmd}\n\n${result.stdout}`),
|
|
644
|
+
],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
649
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// ── audit ────────────────────────────────────────────────────
|
|
653
|
+
case "audit": {
|
|
654
|
+
try {
|
|
655
|
+
const result = await executeCommand({
|
|
656
|
+
command: "systemctl",
|
|
657
|
+
args: [
|
|
658
|
+
"list-units",
|
|
659
|
+
"--type=service",
|
|
660
|
+
"--state=running",
|
|
661
|
+
"--no-pager",
|
|
662
|
+
"--no-legend",
|
|
663
|
+
],
|
|
664
|
+
toolName: "harden_service",
|
|
665
|
+
timeout: getToolTimeout("harden_service"),
|
|
666
|
+
});
|
|
667
|
+
if (result.exitCode !== 0) {
|
|
668
|
+
return {
|
|
669
|
+
content: [
|
|
670
|
+
createErrorContent(`systemctl list-units failed (exit ${result.exitCode}): ${result.stderr}`),
|
|
671
|
+
],
|
|
672
|
+
isError: true,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const units = parseSystemctlOutput(result.stdout);
|
|
676
|
+
const runningServices = units.map((u) => u.unit);
|
|
677
|
+
const flagged = [];
|
|
678
|
+
for (const check of UNNECESSARY_SERVICES) {
|
|
679
|
+
const isRunning = runningServices.some((s) => s === check.name ||
|
|
680
|
+
s.startsWith(check.name.replace(".service", "").replace(".socket", "")));
|
|
681
|
+
if (isRunning) {
|
|
682
|
+
flagged.push({
|
|
683
|
+
service: check.name,
|
|
684
|
+
reason: check.reason,
|
|
685
|
+
running: true,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const output = {
|
|
690
|
+
totalRunning: runningServices.length,
|
|
691
|
+
flaggedCount: flagged.length,
|
|
692
|
+
flaggedServices: flagged,
|
|
693
|
+
};
|
|
694
|
+
if (params.show_all) {
|
|
695
|
+
output.allRunningServices = units;
|
|
696
|
+
}
|
|
697
|
+
if (flagged.length === 0) {
|
|
698
|
+
output.assessment =
|
|
699
|
+
"No known unnecessary or dangerous services detected running.";
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
output.assessment = `Found ${flagged.length} potentially unnecessary service(s). Review and consider disabling with harden_service action=manage.`;
|
|
703
|
+
}
|
|
704
|
+
return { content: [formatToolOutput(output)] };
|
|
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
|
+
// ── 3. harden_permissions (merged: harden_file_permissions, harden_permissions_audit) ──
|
|
716
|
+
server.tool("harden_permissions", "Manage file permissions. Actions: check=audit a specific path, fix=change permissions/ownership, audit=CIS benchmark audit of critical system files", {
|
|
717
|
+
action: z
|
|
718
|
+
.enum(["check", "fix", "audit"])
|
|
719
|
+
.describe("Action: check=audit path, fix=change perms/owner, audit=CIS benchmark check"),
|
|
720
|
+
// check/fix params
|
|
721
|
+
path: z.string().optional().describe("File or directory path (required for check/fix)"),
|
|
722
|
+
mode: z.string().optional().describe("Desired octal permissions, e.g. '600' (for fix)"),
|
|
723
|
+
owner: z.string().optional().describe("Desired owner, e.g. 'root' (for fix)"),
|
|
724
|
+
group: z.string().optional().describe("Desired group, e.g. 'root' (for fix)"),
|
|
725
|
+
recursive: z.boolean().optional().default(false).describe("Apply changes recursively (for fix)"),
|
|
726
|
+
// audit params
|
|
727
|
+
scope: z
|
|
728
|
+
.enum(["passwd", "shadow", "ssh", "cron", "critical", "all"])
|
|
729
|
+
.optional()
|
|
730
|
+
.default("all")
|
|
731
|
+
.describe("Scope of files to audit (for audit)"),
|
|
732
|
+
// shared
|
|
733
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for fix)"),
|
|
734
|
+
}, async (params) => {
|
|
735
|
+
const { action } = params;
|
|
736
|
+
switch (action) {
|
|
737
|
+
// ── check ────────────────────────────────────────────────────
|
|
738
|
+
case "check": {
|
|
739
|
+
try {
|
|
740
|
+
if (!params.path) {
|
|
741
|
+
return { content: [createErrorContent("Error: 'path' is required for check action")], isError: true };
|
|
742
|
+
}
|
|
743
|
+
// TOOL-007: Validate path is within allowed directories
|
|
744
|
+
const validatedCheckPath = validatePathWithinAllowed(params.path);
|
|
745
|
+
const statResult = await executeCommand({
|
|
746
|
+
command: "stat",
|
|
747
|
+
args: ["-c", "%a %U %G %n", validatedCheckPath],
|
|
748
|
+
toolName: "harden_permissions",
|
|
749
|
+
});
|
|
750
|
+
const beforeState = statResult.stdout.trim();
|
|
751
|
+
const lsResult = await executeCommand({
|
|
752
|
+
command: "ls",
|
|
753
|
+
args: ["-la", validatedCheckPath],
|
|
754
|
+
toolName: "harden_permissions",
|
|
755
|
+
});
|
|
756
|
+
return {
|
|
757
|
+
content: [
|
|
758
|
+
createTextContent(`File permissions audit for: ${validatedCheckPath}\n\nstat: ${beforeState}\n\n${lsResult.stdout}`),
|
|
759
|
+
],
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
764
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// ── fix ──────────────────────────────────────────────────────
|
|
768
|
+
case "fix": {
|
|
769
|
+
try {
|
|
770
|
+
if (!params.path) {
|
|
771
|
+
return { content: [createErrorContent("Error: 'path' is required for fix action")], isError: true };
|
|
772
|
+
}
|
|
773
|
+
// TOOL-007: Validate path is within allowed directories (before privilege check)
|
|
774
|
+
const validatedFixPath = validatePathWithinAllowed(params.path);
|
|
775
|
+
if (!params.mode && !params.owner && !params.group) {
|
|
776
|
+
return { content: [createErrorContent("Error: at least one of 'mode', 'owner', or 'group' is required for fix action")], isError: true };
|
|
777
|
+
}
|
|
778
|
+
// TOOL-019: Pre-execution privilege check for chmod/chown on system files
|
|
779
|
+
if (!(params.dry_run ?? getConfig().dryRun)) {
|
|
780
|
+
const privCheckFix = checkPrivileges();
|
|
781
|
+
if (!privCheckFix.ok) {
|
|
782
|
+
return { content: [createErrorContent(`Cannot change permissions: ${privCheckFix.message}`)], isError: true };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Get current permissions
|
|
786
|
+
const statResult = await executeCommand({
|
|
787
|
+
command: "stat",
|
|
788
|
+
args: ["-c", "%a %U %G %n", validatedFixPath],
|
|
789
|
+
toolName: "harden_permissions",
|
|
790
|
+
});
|
|
791
|
+
const beforeState = statResult.stdout.trim();
|
|
792
|
+
const commands = [];
|
|
793
|
+
if (params.mode) {
|
|
794
|
+
sanitizeArgs([params.mode]);
|
|
795
|
+
const chmodArgs = params.recursive ? ["-R", params.mode, validatedFixPath] : [params.mode, validatedFixPath];
|
|
796
|
+
commands.push(`sudo chmod ${chmodArgs.join(" ")}`);
|
|
797
|
+
}
|
|
798
|
+
if (params.owner || params.group) {
|
|
799
|
+
const ownerGroup = `${params.owner ?? ""}${params.group ? `:${params.group}` : ""}`;
|
|
800
|
+
sanitizeArgs([ownerGroup]);
|
|
801
|
+
const chownArgs = params.recursive
|
|
802
|
+
? ["-R", ownerGroup, validatedFixPath]
|
|
803
|
+
: [ownerGroup, validatedFixPath];
|
|
804
|
+
commands.push(`sudo chown ${chownArgs.join(" ")}`);
|
|
805
|
+
}
|
|
806
|
+
if (params.dry_run ?? getConfig().dryRun) {
|
|
807
|
+
const entry = createChangeEntry({
|
|
808
|
+
tool: "harden_permissions",
|
|
809
|
+
action: `[DRY-RUN] Change permissions`,
|
|
810
|
+
target: validatedFixPath,
|
|
811
|
+
before: beforeState,
|
|
812
|
+
dryRun: true,
|
|
813
|
+
success: true,
|
|
814
|
+
});
|
|
815
|
+
logChange(entry);
|
|
816
|
+
return {
|
|
817
|
+
content: [
|
|
818
|
+
createTextContent(`[DRY-RUN] Current: ${beforeState}\n\nWould execute:\n${commands.map((c) => ` ${c}`).join("\n")}`),
|
|
819
|
+
],
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
// Execute chmod if needed
|
|
823
|
+
if (params.mode) {
|
|
824
|
+
const chmodArgs = params.recursive
|
|
825
|
+
? ["chmod", "-R", params.mode, validatedFixPath]
|
|
826
|
+
: ["chmod", params.mode, validatedFixPath];
|
|
827
|
+
const chmodResult = await executeCommand({
|
|
828
|
+
command: "sudo",
|
|
829
|
+
args: chmodArgs,
|
|
830
|
+
toolName: "harden_permissions",
|
|
831
|
+
timeout: getToolTimeout("harden_permissions"),
|
|
832
|
+
});
|
|
833
|
+
if (chmodResult.exitCode !== 0) {
|
|
834
|
+
return {
|
|
835
|
+
content: [
|
|
836
|
+
createErrorContent(`chmod failed (exit ${chmodResult.exitCode}): ${chmodResult.stderr}`),
|
|
837
|
+
],
|
|
838
|
+
isError: true,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Execute chown if needed
|
|
843
|
+
if (params.owner || params.group) {
|
|
844
|
+
const ownerGroup = `${params.owner ?? ""}${params.group ? `:${params.group}` : ""}`;
|
|
845
|
+
const chownArgs = params.recursive
|
|
846
|
+
? ["chown", "-R", ownerGroup, validatedFixPath]
|
|
847
|
+
: ["chown", ownerGroup, validatedFixPath];
|
|
848
|
+
const chownResult = await executeCommand({
|
|
849
|
+
command: "sudo",
|
|
850
|
+
args: chownArgs,
|
|
851
|
+
toolName: "harden_permissions",
|
|
852
|
+
timeout: getToolTimeout("harden_permissions"),
|
|
853
|
+
});
|
|
854
|
+
if (chownResult.exitCode !== 0) {
|
|
855
|
+
return {
|
|
856
|
+
content: [
|
|
857
|
+
createErrorContent(`chown failed (exit ${chownResult.exitCode}): ${chownResult.stderr}`),
|
|
858
|
+
],
|
|
859
|
+
isError: true,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// Get after state
|
|
864
|
+
const afterStatResult = await executeCommand({
|
|
865
|
+
command: "stat",
|
|
866
|
+
args: ["-c", "%a %U %G %n", validatedFixPath],
|
|
867
|
+
toolName: "harden_permissions",
|
|
868
|
+
});
|
|
869
|
+
const afterState = afterStatResult.stdout.trim();
|
|
870
|
+
const entry = createChangeEntry({
|
|
871
|
+
tool: "harden_permissions",
|
|
872
|
+
action: `Change permissions`,
|
|
873
|
+
target: validatedFixPath,
|
|
874
|
+
before: beforeState,
|
|
875
|
+
after: afterState,
|
|
876
|
+
dryRun: false,
|
|
877
|
+
success: true,
|
|
878
|
+
});
|
|
879
|
+
logChange(entry);
|
|
880
|
+
return {
|
|
881
|
+
content: [
|
|
882
|
+
createTextContent(`Permissions updated for ${validatedFixPath}\nBefore: ${beforeState}\nAfter: ${afterState}\nCommands: ${commands.join("; ")}`),
|
|
883
|
+
],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
888
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// ── audit ────────────────────────────────────────────────────
|
|
892
|
+
case "audit": {
|
|
893
|
+
try {
|
|
894
|
+
const scope = params.scope ?? "all";
|
|
895
|
+
const checks = scope === "all"
|
|
896
|
+
? PERMISSION_CHECKS
|
|
897
|
+
: PERMISSION_CHECKS.filter((c) => c.scope === scope);
|
|
898
|
+
const results = [];
|
|
899
|
+
let compliantCount = 0;
|
|
900
|
+
let nonCompliantCount = 0;
|
|
901
|
+
let missingCount = 0;
|
|
902
|
+
for (const check of checks) {
|
|
903
|
+
const statResult = await executeCommand({
|
|
904
|
+
command: "stat",
|
|
905
|
+
args: ["-c", "%a %U %G", check.path],
|
|
906
|
+
toolName: "harden_permissions",
|
|
907
|
+
});
|
|
908
|
+
if (statResult.exitCode !== 0) {
|
|
909
|
+
missingCount++;
|
|
910
|
+
results.push({
|
|
911
|
+
path: check.path,
|
|
912
|
+
description: check.description,
|
|
913
|
+
expected: `${check.expectedMode} ${check.expectedOwner}:${check.expectedGroup}`,
|
|
914
|
+
actual: "FILE NOT FOUND",
|
|
915
|
+
compliant: false,
|
|
916
|
+
exists: false,
|
|
917
|
+
});
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
const parts = statResult.stdout.trim().split(" ");
|
|
921
|
+
const actualMode = parts[0] ?? "";
|
|
922
|
+
const actualOwner = parts[1] ?? "";
|
|
923
|
+
const actualGroup = parts[2] ?? "";
|
|
924
|
+
const modeOk = actualMode === check.expectedMode;
|
|
925
|
+
const ownerOk = actualOwner === check.expectedOwner;
|
|
926
|
+
const groupOk = actualGroup === check.expectedGroup;
|
|
927
|
+
const compliant = modeOk && ownerOk && groupOk;
|
|
928
|
+
if (compliant) {
|
|
929
|
+
compliantCount++;
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
nonCompliantCount++;
|
|
933
|
+
}
|
|
934
|
+
results.push({
|
|
935
|
+
path: check.path,
|
|
936
|
+
description: check.description,
|
|
937
|
+
expected: `${check.expectedMode} ${check.expectedOwner}:${check.expectedGroup}`,
|
|
938
|
+
actual: `${actualMode} ${actualOwner}:${actualGroup}`,
|
|
939
|
+
compliant,
|
|
940
|
+
exists: true,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
const total = checks.length;
|
|
944
|
+
const output = {
|
|
945
|
+
scope,
|
|
946
|
+
summary: {
|
|
947
|
+
total,
|
|
948
|
+
compliant: compliantCount,
|
|
949
|
+
nonCompliant: nonCompliantCount,
|
|
950
|
+
missing: missingCount,
|
|
951
|
+
compliancePercent: total > 0
|
|
952
|
+
? Math.round((compliantCount / total) * 100)
|
|
953
|
+
: 0,
|
|
954
|
+
},
|
|
955
|
+
results,
|
|
956
|
+
};
|
|
957
|
+
return { content: [formatToolOutput(output)] };
|
|
958
|
+
}
|
|
959
|
+
catch (err) {
|
|
960
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
961
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
default:
|
|
965
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// ── 4. harden_systemd (merged: harden_systemd_audit, harden_systemd_apply) ──
|
|
969
|
+
server.tool("harden_systemd", "Audit or apply systemd service security hardening. Actions: audit=score security properties, apply=create hardening overrides", {
|
|
970
|
+
action: z
|
|
971
|
+
.enum(["audit", "apply"])
|
|
972
|
+
.describe("Action: audit=security analysis, apply=create hardening overrides"),
|
|
973
|
+
// audit params
|
|
974
|
+
service: z.string().optional().describe("Service to audit/apply, e.g. 'sshd' or 'sshd.service'"),
|
|
975
|
+
threshold: z.number().optional().default(5).describe("Exposure score threshold 0-10 (for audit)"),
|
|
976
|
+
// apply params
|
|
977
|
+
hardening_level: z.enum(["basic", "strict"]).optional().describe("Preset hardening level (required for apply)"),
|
|
978
|
+
// shared
|
|
979
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for apply)"),
|
|
980
|
+
}, async (params) => {
|
|
981
|
+
const { action } = params;
|
|
982
|
+
switch (action) {
|
|
983
|
+
// ── audit ────────────────────────────────────────────────────
|
|
984
|
+
case "audit": {
|
|
985
|
+
try {
|
|
986
|
+
if (params.service) {
|
|
987
|
+
const result = await executeCommand({
|
|
988
|
+
command: "systemd-analyze",
|
|
989
|
+
args: ["security", params.service],
|
|
990
|
+
timeout: 30000,
|
|
991
|
+
toolName: "harden_systemd",
|
|
992
|
+
});
|
|
993
|
+
const lines = (result.stdout + result.stderr).split("\n");
|
|
994
|
+
const exposureLine = lines.find(l => l.includes("EXPOSURE"));
|
|
995
|
+
let score = "unknown";
|
|
996
|
+
let rating = "unknown";
|
|
997
|
+
if (exposureLine) {
|
|
998
|
+
const match = exposureLine.match(/(\d+\.?\d*)\s+(OK|EXPOSED|MEDIUM|UNSAFE)/i);
|
|
999
|
+
if (match) {
|
|
1000
|
+
score = match[1];
|
|
1001
|
+
rating = match[2];
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const findings = [];
|
|
1005
|
+
for (const line of lines) {
|
|
1006
|
+
const checkMatch = line.match(/^[✓✗→◌]\s+(\S+)=?\s+(.*)/);
|
|
1007
|
+
if (checkMatch && (line.startsWith("✗") || line.startsWith("→"))) {
|
|
1008
|
+
findings.push({
|
|
1009
|
+
property: checkMatch[1].replace(/=$/, ""),
|
|
1010
|
+
status: line.startsWith("✗") ? "FAIL" : "WARNING",
|
|
1011
|
+
description: checkMatch[2].trim(),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
content: [createTextContent(JSON.stringify({
|
|
1017
|
+
service: params.service,
|
|
1018
|
+
exposureScore: parseFloat(score) || 0,
|
|
1019
|
+
rating,
|
|
1020
|
+
totalFindings: findings.length,
|
|
1021
|
+
findings: findings.slice(0, 50),
|
|
1022
|
+
rawExposureLine: exposureLine || "Not found",
|
|
1023
|
+
}, null, 2))],
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
const result = await executeCommand({
|
|
1028
|
+
command: "systemd-analyze",
|
|
1029
|
+
args: ["security", "--no-pager"],
|
|
1030
|
+
timeout: 60000,
|
|
1031
|
+
toolName: "harden_systemd",
|
|
1032
|
+
});
|
|
1033
|
+
const lines = (result.stdout + result.stderr).split("\n").filter(l => l.trim());
|
|
1034
|
+
const services = [];
|
|
1035
|
+
for (const line of lines) {
|
|
1036
|
+
const match = line.match(/^(\S+\.service)\s+(\d+\.?\d*)\s+(\S+)\s+/);
|
|
1037
|
+
if (match) {
|
|
1038
|
+
const exposure = parseFloat(match[2]);
|
|
1039
|
+
services.push({
|
|
1040
|
+
unit: match[1],
|
|
1041
|
+
exposure,
|
|
1042
|
+
rating: match[3],
|
|
1043
|
+
flagged: exposure > params.threshold,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
services.sort((a, b) => b.exposure - a.exposure);
|
|
1048
|
+
const flagged = services.filter(s => s.flagged);
|
|
1049
|
+
return {
|
|
1050
|
+
content: [createTextContent(JSON.stringify({
|
|
1051
|
+
summary: {
|
|
1052
|
+
totalServices: services.length,
|
|
1053
|
+
flaggedAboveThreshold: flagged.length,
|
|
1054
|
+
threshold: params.threshold,
|
|
1055
|
+
averageExposure: services.length > 0
|
|
1056
|
+
? (services.reduce((sum, s) => sum + s.exposure, 0) / services.length).toFixed(1)
|
|
1057
|
+
: 0,
|
|
1058
|
+
},
|
|
1059
|
+
flaggedServices: flagged,
|
|
1060
|
+
allServices: services,
|
|
1061
|
+
}, null, 2))],
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
return {
|
|
1067
|
+
content: [createErrorContent(error instanceof Error ? error.message : String(error))],
|
|
1068
|
+
isError: true,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// ── apply ────────────────────────────────────────────────────
|
|
1073
|
+
case "apply": {
|
|
1074
|
+
try {
|
|
1075
|
+
if (!params.service) {
|
|
1076
|
+
return { content: [createErrorContent("Error: 'service' is required for apply action")], isError: true };
|
|
1077
|
+
}
|
|
1078
|
+
if (!params.hardening_level) {
|
|
1079
|
+
return { content: [createErrorContent("Error: 'hardening_level' is required for apply action")], isError: true };
|
|
1080
|
+
}
|
|
1081
|
+
const service = params.service;
|
|
1082
|
+
const hardening_level = params.hardening_level;
|
|
1083
|
+
const isDryRun = params.dry_run ?? getConfig().dryRun;
|
|
1084
|
+
// Validate service name format
|
|
1085
|
+
if (!/^[a-zA-Z0-9_-]+\.service$/.test(service)) {
|
|
1086
|
+
return { content: [createErrorContent("Invalid service name. Must match format: name.service")], isError: true };
|
|
1087
|
+
}
|
|
1088
|
+
const basicDirectives = [
|
|
1089
|
+
"ProtectSystem=full",
|
|
1090
|
+
"ProtectHome=yes",
|
|
1091
|
+
"PrivateTmp=yes",
|
|
1092
|
+
"NoNewPrivileges=yes",
|
|
1093
|
+
];
|
|
1094
|
+
const strictDirectives = [
|
|
1095
|
+
"ProtectSystem=strict",
|
|
1096
|
+
"ProtectHome=yes",
|
|
1097
|
+
"PrivateTmp=yes",
|
|
1098
|
+
"NoNewPrivileges=yes",
|
|
1099
|
+
"ProtectKernelTunables=yes",
|
|
1100
|
+
"ProtectKernelModules=yes",
|
|
1101
|
+
"ProtectControlGroups=yes",
|
|
1102
|
+
"RestrictSUIDSGID=yes",
|
|
1103
|
+
"MemoryDenyWriteExecute=yes",
|
|
1104
|
+
];
|
|
1105
|
+
const directives = hardening_level === "strict" ? strictDirectives : basicDirectives;
|
|
1106
|
+
const overrideDir = `/etc/systemd/system/${service}.d`;
|
|
1107
|
+
const overrideFile = `${overrideDir}/security.conf`;
|
|
1108
|
+
const overrideContent = `[Service]\n${directives.join("\n")}`;
|
|
1109
|
+
if (isDryRun) {
|
|
1110
|
+
const entry = createChangeEntry({
|
|
1111
|
+
tool: "harden_systemd",
|
|
1112
|
+
action: `[DRY-RUN] Apply ${hardening_level} hardening to ${service}`,
|
|
1113
|
+
target: service,
|
|
1114
|
+
after: overrideContent,
|
|
1115
|
+
dryRun: true,
|
|
1116
|
+
success: true,
|
|
1117
|
+
});
|
|
1118
|
+
logChange(entry);
|
|
1119
|
+
return {
|
|
1120
|
+
content: [formatToolOutput({
|
|
1121
|
+
service,
|
|
1122
|
+
hardening_level,
|
|
1123
|
+
dryRun: true,
|
|
1124
|
+
overrideFile,
|
|
1125
|
+
content: overrideContent,
|
|
1126
|
+
directives,
|
|
1127
|
+
})],
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
// Create override directory
|
|
1131
|
+
await executeCommand({
|
|
1132
|
+
command: "sudo",
|
|
1133
|
+
args: ["mkdir", "-p", overrideDir],
|
|
1134
|
+
toolName: "harden_systemd",
|
|
1135
|
+
});
|
|
1136
|
+
// Backup existing override if present
|
|
1137
|
+
try {
|
|
1138
|
+
backupFile(overrideFile);
|
|
1139
|
+
}
|
|
1140
|
+
catch { /* may not exist */ }
|
|
1141
|
+
// Write security.conf using sudo tee with stdin
|
|
1142
|
+
const writeResult = await executeCommand({
|
|
1143
|
+
command: "sudo",
|
|
1144
|
+
args: ["tee", overrideFile],
|
|
1145
|
+
stdin: overrideContent + "\n",
|
|
1146
|
+
toolName: "harden_systemd",
|
|
1147
|
+
});
|
|
1148
|
+
if (writeResult.exitCode !== 0) {
|
|
1149
|
+
return { content: [createErrorContent(`Failed to write override: ${writeResult.stderr}`)], isError: true };
|
|
1150
|
+
}
|
|
1151
|
+
// Reload systemd daemon to pick up changes
|
|
1152
|
+
const reloadResult = await executeCommand({
|
|
1153
|
+
command: "sudo",
|
|
1154
|
+
args: ["systemctl", "daemon-reload"],
|
|
1155
|
+
toolName: "harden_systemd",
|
|
1156
|
+
timeout: 15000,
|
|
1157
|
+
});
|
|
1158
|
+
const entry = createChangeEntry({
|
|
1159
|
+
tool: "harden_systemd",
|
|
1160
|
+
action: `Apply ${hardening_level} hardening to ${service}`,
|
|
1161
|
+
target: service,
|
|
1162
|
+
after: overrideContent,
|
|
1163
|
+
dryRun: false,
|
|
1164
|
+
success: reloadResult.exitCode === 0,
|
|
1165
|
+
error: reloadResult.exitCode !== 0 ? reloadResult.stderr : undefined,
|
|
1166
|
+
rollbackCommand: `sudo rm ${overrideFile} && sudo systemctl daemon-reload`,
|
|
1167
|
+
});
|
|
1168
|
+
logChange(entry);
|
|
1169
|
+
return {
|
|
1170
|
+
content: [formatToolOutput({
|
|
1171
|
+
service,
|
|
1172
|
+
hardening_level,
|
|
1173
|
+
dryRun: false,
|
|
1174
|
+
overrideFile,
|
|
1175
|
+
directives,
|
|
1176
|
+
daemonReload: reloadResult.exitCode === 0 ? "success" : "failed",
|
|
1177
|
+
})],
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
catch (err) {
|
|
1181
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1182
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
default:
|
|
1186
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
// ── 5. harden_kernel (merged: harden_kernel_security_audit, harden_module_audit, harden_coredump_disable) ──
|
|
1190
|
+
server.tool("harden_kernel", "Kernel security hardening. Actions: audit=CPU vulns/LSM/lockdown/features, modules=audit kernel module blacklisting, coredump=disable core dumps", {
|
|
1191
|
+
action: z
|
|
1192
|
+
.enum(["audit", "modules", "coredump"])
|
|
1193
|
+
.describe("Action: audit=kernel security features, modules=module blacklist audit, coredump=disable core dumps"),
|
|
1194
|
+
// audit params
|
|
1195
|
+
check_type: z
|
|
1196
|
+
.enum(["cpu_vulns", "lsm", "lockdown", "features", "all"])
|
|
1197
|
+
.optional()
|
|
1198
|
+
.default("all")
|
|
1199
|
+
.describe("Type of kernel security check (for audit)"),
|
|
1200
|
+
// shared
|
|
1201
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for coredump)"),
|
|
1202
|
+
}, async (params) => {
|
|
1203
|
+
const { action } = params;
|
|
1204
|
+
switch (action) {
|
|
1205
|
+
// ── audit ────────────────────────────────────────────────────
|
|
1206
|
+
case "audit": {
|
|
1207
|
+
try {
|
|
1208
|
+
const findings = [];
|
|
1209
|
+
const checkType = params.check_type ?? "all";
|
|
1210
|
+
// CPU Vulnerability Mitigations
|
|
1211
|
+
if (checkType === "cpu_vulns" || checkType === "all") {
|
|
1212
|
+
const vulnDir = "/sys/devices/system/cpu/vulnerabilities";
|
|
1213
|
+
const lsResult = await executeCommand({
|
|
1214
|
+
command: "ls",
|
|
1215
|
+
args: [vulnDir],
|
|
1216
|
+
timeout: 5000,
|
|
1217
|
+
toolName: "harden_kernel",
|
|
1218
|
+
});
|
|
1219
|
+
if (lsResult.exitCode === 0) {
|
|
1220
|
+
for (const vuln of lsResult.stdout.trim().split("\n").filter(v => v.trim())) {
|
|
1221
|
+
const catResult = await executeCommand({
|
|
1222
|
+
command: "cat",
|
|
1223
|
+
args: [`${vulnDir}/${vuln.trim()}`],
|
|
1224
|
+
timeout: 5000,
|
|
1225
|
+
toolName: "harden_kernel",
|
|
1226
|
+
});
|
|
1227
|
+
const value = catResult.stdout.trim();
|
|
1228
|
+
const mitigated = value.toLowerCase().includes("not affected") || value.toLowerCase().includes("mitigat");
|
|
1229
|
+
findings.push({
|
|
1230
|
+
check: `cpu_vuln_${vuln.trim()}`,
|
|
1231
|
+
status: mitigated ? "PASS" : (value.toLowerCase().includes("vulnerable") ? "FAIL" : "INFO"),
|
|
1232
|
+
value,
|
|
1233
|
+
description: `CPU vulnerability: ${vuln.trim()}`,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
// Linux Security Modules
|
|
1239
|
+
if (checkType === "lsm" || checkType === "all") {
|
|
1240
|
+
const lsmResult = await executeCommand({
|
|
1241
|
+
command: "cat",
|
|
1242
|
+
args: ["/sys/kernel/security/lsm"],
|
|
1243
|
+
timeout: 5000,
|
|
1244
|
+
toolName: "harden_kernel",
|
|
1245
|
+
});
|
|
1246
|
+
findings.push({
|
|
1247
|
+
check: "active_lsms",
|
|
1248
|
+
status: lsmResult.stdout.includes("apparmor") || lsmResult.stdout.includes("selinux") ? "PASS" : "WARN",
|
|
1249
|
+
value: lsmResult.stdout.trim(),
|
|
1250
|
+
description: "Active Linux Security Modules",
|
|
1251
|
+
});
|
|
1252
|
+
findings.push({
|
|
1253
|
+
check: "landlock_available",
|
|
1254
|
+
status: lsmResult.stdout.includes("landlock") ? "PASS" : "INFO",
|
|
1255
|
+
value: lsmResult.stdout.includes("landlock") ? "enabled" : "not in LSM list",
|
|
1256
|
+
description: "Landlock LSM for filesystem sandboxing",
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
// Lockdown mode
|
|
1260
|
+
if (checkType === "lockdown" || checkType === "all") {
|
|
1261
|
+
const lockdownResult = await executeCommand({
|
|
1262
|
+
command: "cat",
|
|
1263
|
+
args: ["/sys/kernel/security/lockdown"],
|
|
1264
|
+
timeout: 5000,
|
|
1265
|
+
toolName: "harden_kernel",
|
|
1266
|
+
});
|
|
1267
|
+
const lockdownValue = lockdownResult.stdout.trim();
|
|
1268
|
+
const isLocked = lockdownValue.includes("[integrity]") || lockdownValue.includes("[confidentiality]");
|
|
1269
|
+
findings.push({
|
|
1270
|
+
check: "kernel_lockdown",
|
|
1271
|
+
status: isLocked ? "PASS" : "WARN",
|
|
1272
|
+
value: lockdownValue,
|
|
1273
|
+
description: "Kernel lockdown mode (integrity/confidentiality)",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
// General features
|
|
1277
|
+
if (checkType === "features" || checkType === "all") {
|
|
1278
|
+
const kaslrResult = await executeCommand({
|
|
1279
|
+
command: "cat",
|
|
1280
|
+
args: ["/proc/cmdline"],
|
|
1281
|
+
timeout: 5000,
|
|
1282
|
+
toolName: "harden_kernel",
|
|
1283
|
+
});
|
|
1284
|
+
const cmdline = kaslrResult.stdout.trim();
|
|
1285
|
+
findings.push({
|
|
1286
|
+
check: "kaslr",
|
|
1287
|
+
status: cmdline.includes("nokaslr") ? "FAIL" : "PASS",
|
|
1288
|
+
value: cmdline.includes("nokaslr") ? "disabled" : "enabled",
|
|
1289
|
+
description: "Kernel Address Space Layout Randomization",
|
|
1290
|
+
});
|
|
1291
|
+
const secbootResult = await executeCommand({
|
|
1292
|
+
command: "mokutil",
|
|
1293
|
+
args: ["--sb-state"],
|
|
1294
|
+
timeout: 5000,
|
|
1295
|
+
toolName: "harden_kernel",
|
|
1296
|
+
});
|
|
1297
|
+
const secBoot = secbootResult.stdout.trim() || secbootResult.stderr.trim();
|
|
1298
|
+
findings.push({
|
|
1299
|
+
check: "secure_boot",
|
|
1300
|
+
status: secBoot.toLowerCase().includes("secureboot enabled") ? "PASS" : "WARN",
|
|
1301
|
+
value: secBoot || "unknown",
|
|
1302
|
+
description: "UEFI Secure Boot status",
|
|
1303
|
+
});
|
|
1304
|
+
const configResult = await executeCommand({
|
|
1305
|
+
command: "zgrep",
|
|
1306
|
+
args: ["CONFIG_STACKPROTECTOR", "/proc/config.gz"],
|
|
1307
|
+
timeout: 5000,
|
|
1308
|
+
toolName: "harden_kernel",
|
|
1309
|
+
});
|
|
1310
|
+
if (configResult.exitCode === 0) {
|
|
1311
|
+
findings.push({
|
|
1312
|
+
check: "stack_protector",
|
|
1313
|
+
status: configResult.stdout.includes("=y") ? "PASS" : "WARN",
|
|
1314
|
+
value: configResult.stdout.trim(),
|
|
1315
|
+
description: "Kernel stack protector",
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const passCount = findings.filter(f => f.status === "PASS").length;
|
|
1320
|
+
const failCount = findings.filter(f => f.status === "FAIL").length;
|
|
1321
|
+
const warnCount = findings.filter(f => f.status === "WARN").length;
|
|
1322
|
+
return {
|
|
1323
|
+
content: [createTextContent(JSON.stringify({
|
|
1324
|
+
summary: {
|
|
1325
|
+
total: findings.length,
|
|
1326
|
+
pass: passCount,
|
|
1327
|
+
fail: failCount,
|
|
1328
|
+
warn: warnCount,
|
|
1329
|
+
info: findings.length - passCount - failCount - warnCount,
|
|
1330
|
+
},
|
|
1331
|
+
findings,
|
|
1332
|
+
}, null, 2))],
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
catch (error) {
|
|
1336
|
+
return {
|
|
1337
|
+
content: [createErrorContent(error instanceof Error ? error.message : String(error))],
|
|
1338
|
+
isError: true,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
// ── modules ──────────────────────────────────────────────────
|
|
1343
|
+
case "modules": {
|
|
1344
|
+
try {
|
|
1345
|
+
const MODULES_TO_DISABLE = [
|
|
1346
|
+
{ name: "cramfs", description: "CramFS filesystem", cis: "1.1.1.1" },
|
|
1347
|
+
{ name: "squashfs", description: "SquashFS filesystem", cis: "1.1.1.2" },
|
|
1348
|
+
{ name: "udf", description: "UDF filesystem", cis: "1.1.1.3" },
|
|
1349
|
+
{ name: "freevxfs", description: "FreeVXFS filesystem", cis: "1.1.1.1" },
|
|
1350
|
+
{ name: "jffs2", description: "JFFS2 filesystem", cis: "1.1.1.1" },
|
|
1351
|
+
{ name: "hfs", description: "HFS filesystem", cis: "1.1.1.1" },
|
|
1352
|
+
{ name: "hfsplus", description: "HFS+ filesystem", cis: "1.1.1.1" },
|
|
1353
|
+
{ name: "usb-storage", description: "USB storage", cis: "1.1.1.4" },
|
|
1354
|
+
{ name: "dccp", description: "DCCP protocol", cis: "3.4.1" },
|
|
1355
|
+
{ name: "sctp", description: "SCTP protocol", cis: "3.4.2" },
|
|
1356
|
+
{ name: "rds", description: "RDS protocol", cis: "3.4.3" },
|
|
1357
|
+
{ name: "tipc", description: "TIPC protocol", cis: "3.4.4" },
|
|
1358
|
+
];
|
|
1359
|
+
const results = [];
|
|
1360
|
+
for (const mod of MODULES_TO_DISABLE) {
|
|
1361
|
+
const blacklistResult = await executeCommand({ command: "grep", args: ["-r", `install ${mod.name} /bin/true`, "/etc/modprobe.d/"], timeout: 5000, toolName: "harden_kernel" });
|
|
1362
|
+
const blacklisted = blacklistResult.exitCode === 0 && blacklistResult.stdout.trim().length > 0;
|
|
1363
|
+
const loadedResult = await executeCommand({ command: "lsmod", args: [], timeout: 5000, toolName: "harden_kernel" });
|
|
1364
|
+
const loaded = loadedResult.stdout.includes(mod.name);
|
|
1365
|
+
results.push({ module: mod.name, description: mod.description, cis: mod.cis, blacklisted, loaded, status: blacklisted && !loaded ? "PASS" : loaded ? "FAIL" : "WARN" });
|
|
1366
|
+
}
|
|
1367
|
+
const passCount = results.filter(r => r.status === "PASS").length;
|
|
1368
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: results.length, pass: passCount, fail: results.filter(r => r.status === "FAIL").length, warn: results.filter(r => r.status === "WARN").length }, results }, null, 2))] };
|
|
1369
|
+
}
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
// ── coredump ─────────────────────────────────────────────────
|
|
1375
|
+
case "coredump": {
|
|
1376
|
+
try {
|
|
1377
|
+
const isDryRun = params.dry_run ?? getConfig().dryRun;
|
|
1378
|
+
const actions = [];
|
|
1379
|
+
// 1. Check/add "* hard core 0" to /etc/security/limits.conf
|
|
1380
|
+
const limitsPath = "/etc/security/limits.conf";
|
|
1381
|
+
const limitsCheck = await executeCommand({
|
|
1382
|
+
command: "grep",
|
|
1383
|
+
args: ["-c", "\\* hard core 0", limitsPath],
|
|
1384
|
+
toolName: "harden_kernel",
|
|
1385
|
+
});
|
|
1386
|
+
const limitsHasEntry = parseInt(limitsCheck.stdout.trim()) > 0;
|
|
1387
|
+
if (isDryRun) {
|
|
1388
|
+
actions.push({
|
|
1389
|
+
target: limitsPath,
|
|
1390
|
+
action: limitsHasEntry ? "Already present" : "[DRY-RUN] Would add '* hard core 0'",
|
|
1391
|
+
status: limitsHasEntry ? "ok" : "preview",
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
else if (!limitsHasEntry) {
|
|
1395
|
+
try {
|
|
1396
|
+
backupFile(limitsPath);
|
|
1397
|
+
}
|
|
1398
|
+
catch { /* may not exist */ }
|
|
1399
|
+
await executeCommand({
|
|
1400
|
+
command: "sudo",
|
|
1401
|
+
args: ["tee", "-a", limitsPath],
|
|
1402
|
+
stdin: `* hard core 0\n`,
|
|
1403
|
+
toolName: "harden_kernel",
|
|
1404
|
+
});
|
|
1405
|
+
actions.push({ target: limitsPath, action: "Added '* hard core 0'", status: "applied" });
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
actions.push({ target: limitsPath, action: "Already present", status: "ok" });
|
|
1409
|
+
}
|
|
1410
|
+
// 2. Check/create /etc/systemd/coredump.conf with Storage=none, ProcessSizeMax=0
|
|
1411
|
+
const coredumpPath = "/etc/systemd/coredump.conf";
|
|
1412
|
+
if (isDryRun) {
|
|
1413
|
+
actions.push({
|
|
1414
|
+
target: coredumpPath,
|
|
1415
|
+
action: "[DRY-RUN] Would write coredump.conf with Storage=none, ProcessSizeMax=0",
|
|
1416
|
+
status: "preview",
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
try {
|
|
1421
|
+
backupFile(coredumpPath);
|
|
1422
|
+
}
|
|
1423
|
+
catch { /* may not exist */ }
|
|
1424
|
+
await executeCommand({
|
|
1425
|
+
command: "sudo",
|
|
1426
|
+
args: ["tee", coredumpPath],
|
|
1427
|
+
stdin: `[Coredump]\nStorage=none\nProcessSizeMax=0\n`,
|
|
1428
|
+
toolName: "harden_kernel",
|
|
1429
|
+
});
|
|
1430
|
+
actions.push({ target: coredumpPath, action: "Written with Storage=none, ProcessSizeMax=0", status: "applied" });
|
|
1431
|
+
}
|
|
1432
|
+
// 3. Set fs.suid_dumpable = 0 via sysctl
|
|
1433
|
+
if (isDryRun) {
|
|
1434
|
+
actions.push({
|
|
1435
|
+
target: "fs.suid_dumpable",
|
|
1436
|
+
action: "[DRY-RUN] Would set fs.suid_dumpable=0",
|
|
1437
|
+
status: "preview",
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
const sysctlResult = await executeCommand({
|
|
1442
|
+
command: "sudo",
|
|
1443
|
+
args: ["sysctl", "-w", "fs.suid_dumpable=0"],
|
|
1444
|
+
toolName: "harden_kernel",
|
|
1445
|
+
});
|
|
1446
|
+
actions.push({
|
|
1447
|
+
target: "fs.suid_dumpable",
|
|
1448
|
+
action: sysctlResult.exitCode === 0 ? "Set to 0" : `Failed: ${sysctlResult.stderr}`,
|
|
1449
|
+
status: sysctlResult.exitCode === 0 ? "applied" : "error",
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
const entry = createChangeEntry({
|
|
1453
|
+
tool: "harden_kernel",
|
|
1454
|
+
action: isDryRun ? "[DRY-RUN] Disable core dumps" : "Disable core dumps",
|
|
1455
|
+
target: "system",
|
|
1456
|
+
dryRun: isDryRun,
|
|
1457
|
+
success: true,
|
|
1458
|
+
});
|
|
1459
|
+
logChange(entry);
|
|
1460
|
+
return { content: [formatToolOutput({ actions, dryRun: isDryRun })] };
|
|
1461
|
+
}
|
|
1462
|
+
catch (err) {
|
|
1463
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1464
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
default:
|
|
1468
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
// ── 6. harden_bootloader (merged: harden_bootloader_audit, harden_bootloader_configure) ──
|
|
1472
|
+
server.tool("harden_bootloader", "Bootloader security. Actions: audit=check GRUB security config, configure=add kernel security parameters", {
|
|
1473
|
+
action: z
|
|
1474
|
+
.enum(["audit", "configure"])
|
|
1475
|
+
.describe("Action: audit=check GRUB security, configure=add kernel params"),
|
|
1476
|
+
// configure params
|
|
1477
|
+
configure_action: z
|
|
1478
|
+
.enum(["add_kernel_params", "status"])
|
|
1479
|
+
.optional()
|
|
1480
|
+
.describe("Configure sub-action (required for configure)"),
|
|
1481
|
+
kernel_params: z
|
|
1482
|
+
.string()
|
|
1483
|
+
.optional()
|
|
1484
|
+
.describe("Space-separated kernel parameters to add (for configure action=add_kernel_params)"),
|
|
1485
|
+
// shared
|
|
1486
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for configure)"),
|
|
1487
|
+
}, async (params) => {
|
|
1488
|
+
const { action } = params;
|
|
1489
|
+
switch (action) {
|
|
1490
|
+
// ── audit ────────────────────────────────────────────────────
|
|
1491
|
+
case "audit": {
|
|
1492
|
+
try {
|
|
1493
|
+
const findings = [];
|
|
1494
|
+
// Check GRUB password protection
|
|
1495
|
+
const grubCfg = await executeCommand({
|
|
1496
|
+
command: "sudo",
|
|
1497
|
+
args: ["grep", "-r", "password", "/etc/grub.d/", "/boot/grub/grub.cfg"],
|
|
1498
|
+
timeout: 10000,
|
|
1499
|
+
toolName: "harden_bootloader",
|
|
1500
|
+
});
|
|
1501
|
+
findings.push({
|
|
1502
|
+
check: "grub_password",
|
|
1503
|
+
status: grubCfg.stdout.includes("password") ? "PASS" : "FAIL",
|
|
1504
|
+
value: grubCfg.stdout.includes("password") ? "configured" : "not configured",
|
|
1505
|
+
description: "GRUB bootloader password protection",
|
|
1506
|
+
});
|
|
1507
|
+
// Check GRUB config permissions
|
|
1508
|
+
const grubPerms = await executeCommand({
|
|
1509
|
+
command: "stat",
|
|
1510
|
+
args: ["-c", "%a %U:%G", "/boot/grub/grub.cfg"],
|
|
1511
|
+
timeout: 5000,
|
|
1512
|
+
toolName: "harden_bootloader",
|
|
1513
|
+
});
|
|
1514
|
+
const permsValue = grubPerms.stdout.trim();
|
|
1515
|
+
const permOk = permsValue.startsWith("400") || permsValue.startsWith("600");
|
|
1516
|
+
findings.push({
|
|
1517
|
+
check: "grub_config_perms",
|
|
1518
|
+
status: permOk ? "PASS" : "FAIL",
|
|
1519
|
+
value: permsValue,
|
|
1520
|
+
description: "GRUB config file permissions (should be 400/600 root:root)",
|
|
1521
|
+
});
|
|
1522
|
+
// Check kernel command line for security params
|
|
1523
|
+
const cmdline = await executeCommand({
|
|
1524
|
+
command: "cat",
|
|
1525
|
+
args: ["/proc/cmdline"],
|
|
1526
|
+
timeout: 5000,
|
|
1527
|
+
toolName: "harden_bootloader",
|
|
1528
|
+
});
|
|
1529
|
+
const cmd = cmdline.stdout.trim();
|
|
1530
|
+
const kernelParams = [
|
|
1531
|
+
{ param: "nokaslr", bad: true, desc: "KASLR disabled" },
|
|
1532
|
+
{ param: "init_on_alloc=1", bad: false, desc: "Zero memory on allocation" },
|
|
1533
|
+
{ param: "init_on_free=1", bad: false, desc: "Zero memory on free" },
|
|
1534
|
+
{ param: "slab_nomerge", bad: false, desc: "Disable SLAB merging" },
|
|
1535
|
+
{ param: "page_alloc.shuffle=1", bad: false, desc: "Page allocator randomization" },
|
|
1536
|
+
{ param: "randomize_kstack_offset=on", bad: false, desc: "Kernel stack offset randomization" },
|
|
1537
|
+
{ param: "vsyscall=none", bad: false, desc: "Disable vsyscall" },
|
|
1538
|
+
{ param: "lockdown=integrity", bad: false, desc: "Kernel lockdown integrity mode" },
|
|
1539
|
+
{ param: "lockdown=confidentiality", bad: false, desc: "Kernel lockdown confidentiality mode" },
|
|
1540
|
+
];
|
|
1541
|
+
for (const kp of kernelParams) {
|
|
1542
|
+
const present = cmd.includes(kp.param.split("=")[0]);
|
|
1543
|
+
if (kp.bad) {
|
|
1544
|
+
findings.push({
|
|
1545
|
+
check: `cmdline_${kp.param.replace(/[=]/g, "_")}`,
|
|
1546
|
+
status: present ? "FAIL" : "PASS",
|
|
1547
|
+
value: present ? "present (BAD)" : "not present (GOOD)",
|
|
1548
|
+
description: kp.desc,
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
findings.push({
|
|
1553
|
+
check: `cmdline_${kp.param.replace(/[=]/g, "_")}`,
|
|
1554
|
+
status: present ? "PASS" : "INFO",
|
|
1555
|
+
value: present ? "present" : "not set (recommended)",
|
|
1556
|
+
description: kp.desc,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
// Secure Boot
|
|
1561
|
+
const secboot = await executeCommand({
|
|
1562
|
+
command: "mokutil",
|
|
1563
|
+
args: ["--sb-state"],
|
|
1564
|
+
timeout: 5000,
|
|
1565
|
+
toolName: "harden_bootloader",
|
|
1566
|
+
});
|
|
1567
|
+
const sbState = (secboot.stdout + secboot.stderr).trim();
|
|
1568
|
+
findings.push({
|
|
1569
|
+
check: "secure_boot",
|
|
1570
|
+
status: sbState.toLowerCase().includes("secureboot enabled") ? "PASS" : "WARN",
|
|
1571
|
+
value: sbState || "unknown",
|
|
1572
|
+
description: "UEFI Secure Boot status",
|
|
1573
|
+
});
|
|
1574
|
+
const passCount = findings.filter(f => f.status === "PASS").length;
|
|
1575
|
+
const failCount = findings.filter(f => f.status === "FAIL").length;
|
|
1576
|
+
return {
|
|
1577
|
+
content: [createTextContent(JSON.stringify({
|
|
1578
|
+
summary: {
|
|
1579
|
+
total: findings.length,
|
|
1580
|
+
pass: passCount,
|
|
1581
|
+
fail: failCount,
|
|
1582
|
+
warn: findings.filter(f => f.status === "WARN").length,
|
|
1583
|
+
info: findings.filter(f => f.status === "INFO").length,
|
|
1584
|
+
},
|
|
1585
|
+
kernelCommandLine: cmd,
|
|
1586
|
+
findings,
|
|
1587
|
+
}, null, 2))],
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
return {
|
|
1592
|
+
content: [createErrorContent(error instanceof Error ? error.message : String(error))],
|
|
1593
|
+
isError: true,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// ── configure ────────────────────────────────────────────────
|
|
1598
|
+
case "configure": {
|
|
1599
|
+
try {
|
|
1600
|
+
if (!params.configure_action) {
|
|
1601
|
+
return { content: [createErrorContent("Error: 'configure_action' is required for configure (add_kernel_params or status)")], isError: true };
|
|
1602
|
+
}
|
|
1603
|
+
const isDryRun = params.dry_run ?? getConfig().dryRun;
|
|
1604
|
+
const grubFile = "/etc/default/grub";
|
|
1605
|
+
const SAFE_PARAMS = [
|
|
1606
|
+
"audit=1", "init_on_alloc=1", "init_on_free=1", "page_poison=1",
|
|
1607
|
+
"slab_nomerge", "pti=on", "vsyscall=none", "debugfs=off",
|
|
1608
|
+
"oops=panic", "module.sig_enforce=1",
|
|
1609
|
+
];
|
|
1610
|
+
if (params.configure_action === "status") {
|
|
1611
|
+
const readResult = await executeCommand({
|
|
1612
|
+
command: "grep",
|
|
1613
|
+
args: ["^GRUB_CMDLINE_LINUX_DEFAULT", grubFile],
|
|
1614
|
+
toolName: "harden_bootloader",
|
|
1615
|
+
});
|
|
1616
|
+
const currentLine = readResult.stdout.trim();
|
|
1617
|
+
const currentParams = currentLine
|
|
1618
|
+
.replace(/^GRUB_CMDLINE_LINUX_DEFAULT="/, "")
|
|
1619
|
+
.replace(/"$/, "")
|
|
1620
|
+
.split(/\s+/)
|
|
1621
|
+
.filter(Boolean);
|
|
1622
|
+
const paramStatus = SAFE_PARAMS.map((p) => ({
|
|
1623
|
+
param: p,
|
|
1624
|
+
present: currentParams.some((cp) => cp === p || cp.startsWith(p.split("=")[0] + "=")),
|
|
1625
|
+
}));
|
|
1626
|
+
return {
|
|
1627
|
+
content: [formatToolOutput({
|
|
1628
|
+
action: "status",
|
|
1629
|
+
grubFile,
|
|
1630
|
+
currentLine,
|
|
1631
|
+
currentParams,
|
|
1632
|
+
safeParamStatus: paramStatus,
|
|
1633
|
+
missingCount: paramStatus.filter((p) => !p.present).length,
|
|
1634
|
+
})],
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
// add_kernel_params action
|
|
1638
|
+
if (!params.kernel_params) {
|
|
1639
|
+
return { content: [createErrorContent("kernel_params is required for add_kernel_params action")], isError: true };
|
|
1640
|
+
}
|
|
1641
|
+
const requestedParams = params.kernel_params.split(/\s+/).filter(Boolean);
|
|
1642
|
+
// Validate each param format
|
|
1643
|
+
const paramRegex = /^[a-z_][a-z0-9_.=]+$/i;
|
|
1644
|
+
for (const p of requestedParams) {
|
|
1645
|
+
if (!paramRegex.test(p)) {
|
|
1646
|
+
return { content: [createErrorContent(`Invalid kernel parameter format: ${p}`)], isError: true };
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
// Only allow params from the predefined safe list
|
|
1650
|
+
const disallowed = requestedParams.filter((p) => !SAFE_PARAMS.includes(p));
|
|
1651
|
+
if (disallowed.length > 0) {
|
|
1652
|
+
return {
|
|
1653
|
+
content: [createErrorContent(`Disallowed kernel parameters: ${disallowed.join(", ")}. Allowed: ${SAFE_PARAMS.join(", ")}`)],
|
|
1654
|
+
isError: true,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
// Read current GRUB config
|
|
1658
|
+
const readResult = await executeCommand({
|
|
1659
|
+
command: "grep",
|
|
1660
|
+
args: ["^GRUB_CMDLINE_LINUX_DEFAULT", grubFile],
|
|
1661
|
+
toolName: "harden_bootloader",
|
|
1662
|
+
});
|
|
1663
|
+
const currentLine = readResult.stdout.trim();
|
|
1664
|
+
const currentParams = currentLine
|
|
1665
|
+
.replace(/^GRUB_CMDLINE_LINUX_DEFAULT="/, "")
|
|
1666
|
+
.replace(/"$/, "")
|
|
1667
|
+
.split(/\s+/)
|
|
1668
|
+
.filter(Boolean);
|
|
1669
|
+
// Find which params need to be added
|
|
1670
|
+
const toAdd = requestedParams.filter((p) => !currentParams.some((cp) => cp === p || cp.startsWith(p.split("=")[0] + "=")));
|
|
1671
|
+
if (toAdd.length === 0) {
|
|
1672
|
+
return { content: [createTextContent("All requested kernel parameters are already present.")] };
|
|
1673
|
+
}
|
|
1674
|
+
const newParams = [...currentParams, ...toAdd].join(" ");
|
|
1675
|
+
const newLine = `GRUB_CMDLINE_LINUX_DEFAULT="${newParams}"`;
|
|
1676
|
+
if (isDryRun) {
|
|
1677
|
+
const entry = createChangeEntry({
|
|
1678
|
+
tool: "harden_bootloader",
|
|
1679
|
+
action: "[DRY-RUN] Add kernel params",
|
|
1680
|
+
target: grubFile,
|
|
1681
|
+
before: currentLine,
|
|
1682
|
+
after: newLine,
|
|
1683
|
+
dryRun: true,
|
|
1684
|
+
success: true,
|
|
1685
|
+
});
|
|
1686
|
+
logChange(entry);
|
|
1687
|
+
return {
|
|
1688
|
+
content: [formatToolOutput({
|
|
1689
|
+
action: "add_kernel_params",
|
|
1690
|
+
dryRun: true,
|
|
1691
|
+
paramsToAdd: toAdd,
|
|
1692
|
+
currentLine,
|
|
1693
|
+
newLine,
|
|
1694
|
+
})],
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
// Backup GRUB config before modification
|
|
1698
|
+
try {
|
|
1699
|
+
backupFile(grubFile);
|
|
1700
|
+
}
|
|
1701
|
+
catch { /* may not exist */ }
|
|
1702
|
+
// Update GRUB config using sed
|
|
1703
|
+
const escapedCurrent = currentLine.replace(/[/\\.*+?^${}()|[\]]/g, "\\$&");
|
|
1704
|
+
const escapedNew = newLine.replace(/[/\\&]/g, "\\$&");
|
|
1705
|
+
await executeCommand({
|
|
1706
|
+
command: "sudo",
|
|
1707
|
+
args: ["sed", "-i", `s/${escapedCurrent}/${escapedNew}/`, grubFile],
|
|
1708
|
+
toolName: "harden_bootloader",
|
|
1709
|
+
});
|
|
1710
|
+
// Run update-grub to apply changes
|
|
1711
|
+
const updateResult = await executeCommand({
|
|
1712
|
+
command: "sudo",
|
|
1713
|
+
args: ["update-grub"],
|
|
1714
|
+
toolName: "harden_bootloader",
|
|
1715
|
+
timeout: 30000,
|
|
1716
|
+
});
|
|
1717
|
+
const entry = createChangeEntry({
|
|
1718
|
+
tool: "harden_bootloader",
|
|
1719
|
+
action: "Add kernel params",
|
|
1720
|
+
target: grubFile,
|
|
1721
|
+
before: currentLine,
|
|
1722
|
+
after: newLine,
|
|
1723
|
+
dryRun: false,
|
|
1724
|
+
success: updateResult.exitCode === 0,
|
|
1725
|
+
error: updateResult.exitCode !== 0 ? updateResult.stderr : undefined,
|
|
1726
|
+
rollbackCommand: `sudo sed -i 's/${escapedNew}/${escapedCurrent}/' ${grubFile} && sudo update-grub`,
|
|
1727
|
+
});
|
|
1728
|
+
logChange(entry);
|
|
1729
|
+
return {
|
|
1730
|
+
content: [formatToolOutput({
|
|
1731
|
+
action: "add_kernel_params",
|
|
1732
|
+
dryRun: false,
|
|
1733
|
+
paramsAdded: toAdd,
|
|
1734
|
+
updateGrubSuccess: updateResult.exitCode === 0,
|
|
1735
|
+
newLine,
|
|
1736
|
+
})],
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
catch (err) {
|
|
1740
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1741
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
default:
|
|
1745
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
// ── 7. harden_misc (merged: harden_cron_audit, harden_umask_audit, harden_umask_set, harden_banner_audit, harden_banner_set) ──
|
|
1749
|
+
server.tool("harden_misc", "Miscellaneous hardening. Actions: cron_audit=audit cron/at access, umask_audit=check umask config, umask_set=set umask, banner_audit=check login banners, banner_set=set login banners", {
|
|
1750
|
+
action: z
|
|
1751
|
+
.enum(["cron_audit", "umask_audit", "umask_set", "banner_audit", "banner_set"])
|
|
1752
|
+
.describe("Action to perform"),
|
|
1753
|
+
// umask_set params
|
|
1754
|
+
umask_value: z.enum(["027", "077"]).optional().describe("Umask value (required for umask_set)"),
|
|
1755
|
+
targets: z
|
|
1756
|
+
.array(z.enum(["login.defs", "profile", "bashrc"]))
|
|
1757
|
+
.optional()
|
|
1758
|
+
.describe("Files to update for umask_set (default: all)"),
|
|
1759
|
+
// banner_set params
|
|
1760
|
+
banner_text: z.string().optional().describe("Custom banner text (for banner_set; uses CIS default if omitted)"),
|
|
1761
|
+
banner_targets: z
|
|
1762
|
+
.array(z.enum(["issue", "issue.net", "motd"]))
|
|
1763
|
+
.optional()
|
|
1764
|
+
.describe("Banner files to update (for banner_set; default: all)"),
|
|
1765
|
+
// shared
|
|
1766
|
+
dry_run: z.boolean().optional().default(true).describe("Preview changes (for umask_set/banner_set)"),
|
|
1767
|
+
}, async (params) => {
|
|
1768
|
+
const { action } = params;
|
|
1769
|
+
switch (action) {
|
|
1770
|
+
// ── cron_audit ───────────────────────────────────────────────
|
|
1771
|
+
case "cron_audit": {
|
|
1772
|
+
try {
|
|
1773
|
+
const findings = [];
|
|
1774
|
+
const cronDeny = await executeCommand({ command: "ls", args: ["-la", "/etc/cron.deny"], timeout: 5000, toolName: "harden_misc" });
|
|
1775
|
+
const cronAllow = await executeCommand({ command: "ls", args: ["-la", "/etc/cron.allow"], timeout: 5000, toolName: "harden_misc" });
|
|
1776
|
+
findings.push({ check: "cron_deny", status: cronDeny.exitCode !== 0 ? "PASS" : "WARN", value: cronDeny.exitCode === 0 ? "exists" : "not present", description: "CIS: /etc/cron.deny should not exist (use cron.allow instead)" });
|
|
1777
|
+
findings.push({ check: "cron_allow", status: cronAllow.exitCode === 0 ? "PASS" : "WARN", value: cronAllow.exitCode === 0 ? "exists" : "not present", description: "CIS: /etc/cron.allow should exist to restrict cron access" });
|
|
1778
|
+
if (cronAllow.exitCode === 0) {
|
|
1779
|
+
const permsResult = await executeCommand({ command: "stat", args: ["-c", "%a %U:%G", "/etc/cron.allow"], timeout: 5000, toolName: "harden_misc" });
|
|
1780
|
+
findings.push({ check: "cron_allow_perms", status: permsResult.stdout.trim().startsWith("600") ? "PASS" : "WARN", value: permsResult.stdout.trim(), description: "cron.allow permissions (should be 600 root:root)" });
|
|
1781
|
+
}
|
|
1782
|
+
const atDeny = await executeCommand({ command: "ls", args: ["-la", "/etc/at.deny"], timeout: 5000, toolName: "harden_misc" });
|
|
1783
|
+
const atAllow = await executeCommand({ command: "ls", args: ["-la", "/etc/at.allow"], timeout: 5000, toolName: "harden_misc" });
|
|
1784
|
+
findings.push({ check: "at_deny", status: atDeny.exitCode !== 0 ? "PASS" : "WARN", value: atDeny.exitCode === 0 ? "exists" : "not present", description: "CIS: /etc/at.deny should not exist" });
|
|
1785
|
+
findings.push({ check: "at_allow", status: atAllow.exitCode === 0 ? "PASS" : "WARN", value: atAllow.exitCode === 0 ? "exists" : "not present", description: "CIS: /etc/at.allow should exist" });
|
|
1786
|
+
const cronDirs = ["/etc/crontab", "/etc/cron.hourly", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly", "/etc/cron.d"];
|
|
1787
|
+
for (const dir of cronDirs) {
|
|
1788
|
+
const perms = await executeCommand({ command: "stat", args: ["-c", "%a %U:%G", dir], timeout: 5000, toolName: "harden_misc" });
|
|
1789
|
+
if (perms.exitCode === 0) {
|
|
1790
|
+
const perm = perms.stdout.trim().split(" ")[0];
|
|
1791
|
+
const isFile = dir === "/etc/crontab";
|
|
1792
|
+
const expected = isFile ? "600" : "700";
|
|
1793
|
+
findings.push({ check: `perms_${dir.replace(/\//g, "_")}`, status: perm === expected ? "PASS" : "WARN", value: perms.stdout.trim(), description: `${dir} permissions (should be ${expected} root:root)` });
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
const passCount = findings.filter(f => f.status === "PASS").length;
|
|
1797
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: passCount, fail: findings.filter(f => f.status === "FAIL").length, warn: findings.filter(f => f.status === "WARN").length }, findings }, null, 2))] };
|
|
1798
|
+
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
// ── umask_audit ──────────────────────────────────────────────
|
|
1804
|
+
case "umask_audit": {
|
|
1805
|
+
try {
|
|
1806
|
+
const findings = [];
|
|
1807
|
+
const files = [
|
|
1808
|
+
{ path: "/etc/login.defs", pattern: "UMASK" },
|
|
1809
|
+
{ path: "/etc/profile", pattern: "umask" },
|
|
1810
|
+
{ path: "/etc/bash.bashrc", pattern: "umask" },
|
|
1811
|
+
{ path: "/etc/profile.d/", pattern: "umask" },
|
|
1812
|
+
];
|
|
1813
|
+
for (const file of files) {
|
|
1814
|
+
if (file.path.endsWith("/")) {
|
|
1815
|
+
const grepResult = await executeCommand({ command: "grep", args: ["-r", file.pattern, file.path], timeout: 5000, toolName: "harden_misc" });
|
|
1816
|
+
findings.push({ check: `umask_${file.path.replace(/\//g, "_")}`, status: grepResult.stdout.includes("027") || grepResult.stdout.includes("077") ? "PASS" : "WARN", value: grepResult.stdout.trim().substring(0, 200) || "not set", description: `umask in ${file.path} (should be 027 or more restrictive)` });
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
const grepResult = await executeCommand({ command: "grep", args: ["-i", file.pattern, file.path], timeout: 5000, toolName: "harden_misc" });
|
|
1820
|
+
const lines = grepResult.stdout.split("\n").filter((l) => !l.trim().startsWith("#") && l.includes("mask"));
|
|
1821
|
+
const hasSecure = lines.some((l) => l.includes("027") || l.includes("077"));
|
|
1822
|
+
findings.push({ check: `umask_${file.path.replace(/\//g, "_")}`, status: hasSecure ? "PASS" : "WARN", value: lines.join("; ").substring(0, 200) || "not explicitly set", description: `umask in ${file.path} (should be 027 or 077)` });
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, warn: findings.filter(f => f.status === "WARN").length }, findings, recommendation: "Set umask 027 in /etc/profile and /etc/login.defs for CIS compliance" }, null, 2))] };
|
|
1826
|
+
}
|
|
1827
|
+
catch (error) {
|
|
1828
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// ── umask_set ────────────────────────────────────────────────
|
|
1832
|
+
case "umask_set": {
|
|
1833
|
+
try {
|
|
1834
|
+
if (!params.umask_value) {
|
|
1835
|
+
return { content: [createErrorContent("Error: 'umask_value' is required for umask_set action")], isError: true };
|
|
1836
|
+
}
|
|
1837
|
+
const umask_value = params.umask_value;
|
|
1838
|
+
const allTargets = params.targets ?? ["login.defs", "profile", "bashrc"];
|
|
1839
|
+
const results = [];
|
|
1840
|
+
const isDryRun = params.dry_run ?? getConfig().dryRun;
|
|
1841
|
+
const targetMap = {
|
|
1842
|
+
"login.defs": "/etc/login.defs",
|
|
1843
|
+
"profile": "/etc/profile",
|
|
1844
|
+
"bashrc": "/etc/bash.bashrc",
|
|
1845
|
+
};
|
|
1846
|
+
for (const target of allTargets) {
|
|
1847
|
+
const filePath = targetMap[target];
|
|
1848
|
+
if (!filePath)
|
|
1849
|
+
continue;
|
|
1850
|
+
if (isDryRun) {
|
|
1851
|
+
results.push({ target: filePath, action: `[DRY-RUN] Would set umask to ${umask_value}`, status: "preview" });
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
backupFile(filePath);
|
|
1856
|
+
}
|
|
1857
|
+
catch { /* file may not exist */ }
|
|
1858
|
+
if (target === "login.defs") {
|
|
1859
|
+
const grepResult = await executeCommand({
|
|
1860
|
+
command: "grep",
|
|
1861
|
+
args: ["-c", "^UMASK", filePath],
|
|
1862
|
+
toolName: "harden_misc",
|
|
1863
|
+
});
|
|
1864
|
+
if (parseInt(grepResult.stdout.trim()) > 0) {
|
|
1865
|
+
await executeCommand({
|
|
1866
|
+
command: "sudo",
|
|
1867
|
+
args: ["sed", "-i", `s/^UMASK.*/UMASK\t\t${umask_value}/`, filePath],
|
|
1868
|
+
toolName: "harden_misc",
|
|
1869
|
+
});
|
|
1870
|
+
results.push({ target: filePath, action: `Updated UMASK to ${umask_value}`, status: "updated" });
|
|
1871
|
+
}
|
|
1872
|
+
else {
|
|
1873
|
+
await executeCommand({
|
|
1874
|
+
command: "sudo",
|
|
1875
|
+
args: ["tee", "-a", filePath],
|
|
1876
|
+
stdin: `UMASK\t\t${umask_value}\n`,
|
|
1877
|
+
toolName: "harden_misc",
|
|
1878
|
+
});
|
|
1879
|
+
results.push({ target: filePath, action: `Appended UMASK ${umask_value}`, status: "appended" });
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
const grepResult = await executeCommand({
|
|
1884
|
+
command: "grep",
|
|
1885
|
+
args: ["-c", "^umask [0-9]", filePath],
|
|
1886
|
+
toolName: "harden_misc",
|
|
1887
|
+
});
|
|
1888
|
+
if (parseInt(grepResult.stdout.trim()) > 0) {
|
|
1889
|
+
await executeCommand({
|
|
1890
|
+
command: "sudo",
|
|
1891
|
+
args: ["sed", "-i", `s/^umask [0-9]*/umask ${umask_value}/`, filePath],
|
|
1892
|
+
toolName: "harden_misc",
|
|
1893
|
+
});
|
|
1894
|
+
results.push({ target: filePath, action: `Updated umask to ${umask_value}`, status: "updated" });
|
|
1895
|
+
}
|
|
1896
|
+
else {
|
|
1897
|
+
await executeCommand({
|
|
1898
|
+
command: "sudo",
|
|
1899
|
+
args: ["tee", "-a", filePath],
|
|
1900
|
+
stdin: `umask ${umask_value}\n`,
|
|
1901
|
+
toolName: "harden_misc",
|
|
1902
|
+
});
|
|
1903
|
+
results.push({ target: filePath, action: `Appended umask ${umask_value}`, status: "appended" });
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
const entry = createChangeEntry({
|
|
1907
|
+
tool: "harden_misc",
|
|
1908
|
+
action: `Set umask ${umask_value} in ${filePath}`,
|
|
1909
|
+
target: filePath,
|
|
1910
|
+
dryRun: false,
|
|
1911
|
+
success: true,
|
|
1912
|
+
});
|
|
1913
|
+
logChange(entry);
|
|
1914
|
+
}
|
|
1915
|
+
if (isDryRun) {
|
|
1916
|
+
const entry = createChangeEntry({
|
|
1917
|
+
tool: "harden_misc",
|
|
1918
|
+
action: `[DRY-RUN] Set umask ${umask_value}`,
|
|
1919
|
+
target: allTargets.join(", "),
|
|
1920
|
+
dryRun: true,
|
|
1921
|
+
success: true,
|
|
1922
|
+
});
|
|
1923
|
+
logChange(entry);
|
|
1924
|
+
}
|
|
1925
|
+
return { content: [formatToolOutput({ umask_value, results, dryRun: isDryRun })] };
|
|
1926
|
+
}
|
|
1927
|
+
catch (err) {
|
|
1928
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1929
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// ── banner_audit ─────────────────────────────────────────────
|
|
1933
|
+
case "banner_audit": {
|
|
1934
|
+
try {
|
|
1935
|
+
const findings = [];
|
|
1936
|
+
const bannerFiles = [
|
|
1937
|
+
{ path: "/etc/issue", description: "Local login banner" },
|
|
1938
|
+
{ path: "/etc/issue.net", description: "Remote login banner" },
|
|
1939
|
+
{ path: "/etc/motd", description: "Message of the day" },
|
|
1940
|
+
];
|
|
1941
|
+
for (const banner of bannerFiles) {
|
|
1942
|
+
const result = await executeCommand({ command: "cat", args: [banner.path], timeout: 5000, toolName: "harden_misc" });
|
|
1943
|
+
const content = result.stdout.trim();
|
|
1944
|
+
const hasContent = content.length > 10;
|
|
1945
|
+
const hasOsInfo = /\\[mrsv]/.test(content);
|
|
1946
|
+
findings.push({ check: `${banner.path.replace(/\//g, "_")}_exists`, status: hasContent ? "PASS" : "WARN", value: hasContent ? `${content.length} chars` : "empty or missing", description: `${banner.description} should contain a warning` });
|
|
1947
|
+
if (hasContent) {
|
|
1948
|
+
findings.push({ check: `${banner.path.replace(/\//g, "_")}_no_os_info`, status: hasOsInfo ? "FAIL" : "PASS", value: hasOsInfo ? "contains OS info" : "clean", description: `${banner.description} should NOT contain OS version info (\\m, \\r, \\s, \\v)` });
|
|
1949
|
+
}
|
|
1950
|
+
const perms = await executeCommand({ command: "stat", args: ["-c", "%a %U:%G", banner.path], timeout: 5000, toolName: "harden_misc" });
|
|
1951
|
+
if (perms.exitCode === 0) {
|
|
1952
|
+
findings.push({ check: `${banner.path.replace(/\//g, "_")}_perms`, status: perms.stdout.trim().startsWith("644") ? "PASS" : "WARN", value: perms.stdout.trim(), description: `${banner.description} permissions (should be 644 root:root)` });
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return { content: [createTextContent(JSON.stringify({ summary: { total: findings.length, pass: findings.filter(f => f.status === "PASS").length, fail: findings.filter(f => f.status === "FAIL").length, warn: findings.filter(f => f.status === "WARN").length }, findings }, null, 2))] };
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
return { content: [createErrorContent(error instanceof Error ? error.message : String(error))], isError: true };
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// ── banner_set ───────────────────────────────────────────────
|
|
1962
|
+
case "banner_set": {
|
|
1963
|
+
try {
|
|
1964
|
+
const isDryRun = params.dry_run ?? getConfig().dryRun;
|
|
1965
|
+
const allTargets = params.banner_targets ?? ["issue", "issue.net", "motd"];
|
|
1966
|
+
const defaultBanner = "Authorized uses only. All activity may be monitored and reported.";
|
|
1967
|
+
const text = params.banner_text ?? defaultBanner;
|
|
1968
|
+
if (text.length > 2000) {
|
|
1969
|
+
return { content: [createErrorContent("Banner text exceeds maximum length of 2000 characters")], isError: true };
|
|
1970
|
+
}
|
|
1971
|
+
if (!/^[\x20-\x7E\n\r\t]*$/.test(text)) {
|
|
1972
|
+
return { content: [createErrorContent("Banner text contains invalid characters. Only printable ASCII, newlines, and common punctuation are allowed.")], isError: true };
|
|
1973
|
+
}
|
|
1974
|
+
const results = [];
|
|
1975
|
+
for (const target of allTargets) {
|
|
1976
|
+
const filePath = `/etc/${target}`;
|
|
1977
|
+
if (isDryRun) {
|
|
1978
|
+
results.push({ target: filePath, action: `[DRY-RUN] Would write banner text (${text.length} chars)`, status: "preview" });
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
backupFile(filePath);
|
|
1983
|
+
}
|
|
1984
|
+
catch { /* may not exist */ }
|
|
1985
|
+
const writeResult = await executeCommand({
|
|
1986
|
+
command: "sudo",
|
|
1987
|
+
args: ["tee", filePath],
|
|
1988
|
+
stdin: text + "\n",
|
|
1989
|
+
toolName: "harden_misc",
|
|
1990
|
+
});
|
|
1991
|
+
if (writeResult.exitCode === 0) {
|
|
1992
|
+
results.push({ target: filePath, action: `Written banner (${text.length} chars)`, status: "applied" });
|
|
1993
|
+
const entry = createChangeEntry({
|
|
1994
|
+
tool: "harden_misc",
|
|
1995
|
+
action: `Set login banner`,
|
|
1996
|
+
target: filePath,
|
|
1997
|
+
after: text.substring(0, 100),
|
|
1998
|
+
dryRun: false,
|
|
1999
|
+
success: true,
|
|
2000
|
+
});
|
|
2001
|
+
logChange(entry);
|
|
2002
|
+
}
|
|
2003
|
+
else {
|
|
2004
|
+
results.push({ target: filePath, action: `Failed: ${writeResult.stderr}`, status: "error" });
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (isDryRun) {
|
|
2008
|
+
const entry = createChangeEntry({
|
|
2009
|
+
tool: "harden_misc",
|
|
2010
|
+
action: "[DRY-RUN] Set login banners",
|
|
2011
|
+
target: allTargets.join(", "),
|
|
2012
|
+
dryRun: true,
|
|
2013
|
+
success: true,
|
|
2014
|
+
});
|
|
2015
|
+
logChange(entry);
|
|
2016
|
+
}
|
|
2017
|
+
return { content: [formatToolOutput({ targets: allTargets, bannerLength: text.length, results, dryRun: isDryRun })] };
|
|
2018
|
+
}
|
|
2019
|
+
catch (err) {
|
|
2020
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2021
|
+
return { content: [createErrorContent(msg)], isError: true };
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
default:
|
|
2025
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
// ── 8. memory_protection (merged: audit_memory_protections, enforce_aslr, report_exploit_mitigations) ──
|
|
2029
|
+
server.tool("harden_memory", "Memory and exploit mitigations. Actions: audit=check ASLR/PIE/RELRO/NX/canary on binaries, enforce_aslr=enable full ASLR, report=system-wide mitigation status", {
|
|
2030
|
+
action: z
|
|
2031
|
+
.enum(["audit", "enforce_aslr", "report"])
|
|
2032
|
+
.describe("Action: audit=binary protections, enforce_aslr=enable ASLR, report=system mitigations"),
|
|
2033
|
+
// audit params
|
|
2034
|
+
binaries: z.array(z.string()).optional().describe("Binary paths to check (for audit)"),
|
|
2035
|
+
// shared
|
|
2036
|
+
dry_run: z.boolean().optional().default(true).describe("Preview only (for enforce_aslr)"),
|
|
2037
|
+
}, async (params) => {
|
|
2038
|
+
const { action } = params;
|
|
2039
|
+
switch (action) {
|
|
2040
|
+
// ── audit ────────────────────────────────────────────────────
|
|
2041
|
+
case "audit": {
|
|
2042
|
+
try {
|
|
2043
|
+
const findings = [];
|
|
2044
|
+
// Check ASLR
|
|
2045
|
+
let aslrStatus = "unknown";
|
|
2046
|
+
try {
|
|
2047
|
+
if (existsSync("/proc/sys/kernel/randomize_va_space")) {
|
|
2048
|
+
const val = readFileSync("/proc/sys/kernel/randomize_va_space", "utf-8").trim();
|
|
2049
|
+
aslrStatus = val === "2" ? "full" : val === "1" ? "partial" : val === "0" ? "disabled" : val;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
catch { /* not readable */ }
|
|
2053
|
+
const targets = params.binaries ?? [
|
|
2054
|
+
"/usr/bin/ssh", "/usr/sbin/sshd", "/usr/bin/sudo",
|
|
2055
|
+
"/usr/bin/passwd", "/usr/sbin/nginx", "/usr/sbin/apache2",
|
|
2056
|
+
];
|
|
2057
|
+
for (const binary of targets) {
|
|
2058
|
+
if (!existsSync(binary)) {
|
|
2059
|
+
findings.push({ binary, status: "not found" });
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
const result = await executeCommand({
|
|
2063
|
+
command: "readelf",
|
|
2064
|
+
args: ["-h", "-Wl", "-Wd", binary],
|
|
2065
|
+
timeout: 10000,
|
|
2066
|
+
});
|
|
2067
|
+
if (result.exitCode !== 0) {
|
|
2068
|
+
findings.push({ binary, status: "readelf failed", error: result.stderr.slice(0, 200) });
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
const output = result.stdout;
|
|
2072
|
+
let pie = "unknown";
|
|
2073
|
+
const typeMatch = output.match(/^\s*Type:\s+(EXEC|DYN)\b/m);
|
|
2074
|
+
if (typeMatch) {
|
|
2075
|
+
pie = typeMatch[1] === "DYN" ? "enabled" : "disabled";
|
|
2076
|
+
}
|
|
2077
|
+
else {
|
|
2078
|
+
const fileResult = await executeCommand({
|
|
2079
|
+
command: "file",
|
|
2080
|
+
args: [binary],
|
|
2081
|
+
timeout: 5000,
|
|
2082
|
+
});
|
|
2083
|
+
if (fileResult.exitCode === 0) {
|
|
2084
|
+
const fileOut = fileResult.stdout;
|
|
2085
|
+
if (fileOut.includes("pie executable") || fileOut.includes("shared object")) {
|
|
2086
|
+
pie = "enabled";
|
|
2087
|
+
}
|
|
2088
|
+
else if (fileOut.includes("executable")) {
|
|
2089
|
+
pie = "disabled";
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
const relro = output.includes("GNU_RELRO")
|
|
2094
|
+
? (output.includes("BIND_NOW") ? "full" : "partial")
|
|
2095
|
+
: "disabled";
|
|
2096
|
+
const nx = output.includes("GNU_STACK") && !output.includes("RWE") ? "enabled" : "disabled";
|
|
2097
|
+
const symResult = await executeCommand({
|
|
2098
|
+
command: "readelf",
|
|
2099
|
+
args: ["-Ws", binary],
|
|
2100
|
+
timeout: 10000,
|
|
2101
|
+
});
|
|
2102
|
+
const canary = symResult.stdout.includes("__stack_chk_fail") ? "enabled" : "not detected";
|
|
2103
|
+
findings.push({
|
|
2104
|
+
binary,
|
|
2105
|
+
pie,
|
|
2106
|
+
relro,
|
|
2107
|
+
nx,
|
|
2108
|
+
stackCanary: canary,
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
return {
|
|
2112
|
+
content: [formatToolOutput({
|
|
2113
|
+
aslr: aslrStatus,
|
|
2114
|
+
binariesChecked: findings.length,
|
|
2115
|
+
findings,
|
|
2116
|
+
})],
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
catch (err) {
|
|
2120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2121
|
+
return { content: [createErrorContent(`Memory audit failed: ${msg}`)], isError: true };
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
// ── enforce_aslr ─────────────────────────────────────────────
|
|
2125
|
+
case "enforce_aslr": {
|
|
2126
|
+
try {
|
|
2127
|
+
const safety = await SafeguardRegistry.getInstance().checkSafety("harden_memory", {});
|
|
2128
|
+
let currentValue = "unknown";
|
|
2129
|
+
try {
|
|
2130
|
+
currentValue = readFileSync("/proc/sys/kernel/randomize_va_space", "utf-8").trim();
|
|
2131
|
+
}
|
|
2132
|
+
catch { /* not readable */ }
|
|
2133
|
+
if (currentValue === "2") {
|
|
2134
|
+
return { content: [formatToolOutput({ status: "already_enabled", currentValue: "2 (full ASLR)" })] };
|
|
2135
|
+
}
|
|
2136
|
+
if (params.dry_run) {
|
|
2137
|
+
return {
|
|
2138
|
+
content: [formatToolOutput({
|
|
2139
|
+
dryRun: true,
|
|
2140
|
+
currentValue,
|
|
2141
|
+
command: "sysctl -w kernel.randomize_va_space=2",
|
|
2142
|
+
warnings: safety.warnings,
|
|
2143
|
+
})],
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
const result = await executeCommand({
|
|
2147
|
+
command: "sysctl",
|
|
2148
|
+
args: ["-w", "kernel.randomize_va_space=2"],
|
|
2149
|
+
timeout: 10000,
|
|
2150
|
+
});
|
|
2151
|
+
const entry = createChangeEntry({
|
|
2152
|
+
tool: "harden_memory",
|
|
2153
|
+
action: "Set ASLR to full (2)",
|
|
2154
|
+
target: "kernel.randomize_va_space",
|
|
2155
|
+
before: currentValue,
|
|
2156
|
+
after: "2",
|
|
2157
|
+
dryRun: false,
|
|
2158
|
+
success: result.exitCode === 0,
|
|
2159
|
+
rollbackCommand: `sysctl -w kernel.randomize_va_space=${currentValue}`,
|
|
2160
|
+
error: result.exitCode !== 0 ? result.stderr : undefined,
|
|
2161
|
+
});
|
|
2162
|
+
logChange(entry);
|
|
2163
|
+
return {
|
|
2164
|
+
content: [formatToolOutput({
|
|
2165
|
+
success: result.exitCode === 0,
|
|
2166
|
+
before: currentValue,
|
|
2167
|
+
after: "2",
|
|
2168
|
+
output: result.stdout,
|
|
2169
|
+
})],
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
catch (err) {
|
|
2173
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2174
|
+
return { content: [createErrorContent(`ASLR enforcement failed: ${msg}`)], isError: true };
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
// ── report ───────────────────────────────────────────────────
|
|
2178
|
+
case "report": {
|
|
2179
|
+
try {
|
|
2180
|
+
const mitigations = {};
|
|
2181
|
+
// ASLR
|
|
2182
|
+
try {
|
|
2183
|
+
const val = readFileSync("/proc/sys/kernel/randomize_va_space", "utf-8").trim();
|
|
2184
|
+
mitigations["ASLR"] = val === "2" ? "Full (2)" : val === "1" ? "Partial (1)" : `Disabled (${val})`;
|
|
2185
|
+
}
|
|
2186
|
+
catch {
|
|
2187
|
+
mitigations["ASLR"] = "Unable to read";
|
|
2188
|
+
}
|
|
2189
|
+
// Kernel mitigations from /proc/cmdline
|
|
2190
|
+
try {
|
|
2191
|
+
const cmdline = readFileSync("/proc/cmdline", "utf-8").trim();
|
|
2192
|
+
mitigations["KASLR"] = cmdline.includes("nokaslr") ? "Disabled" : "Enabled (default)";
|
|
2193
|
+
mitigations["PTI"] = cmdline.includes("nopti") ? "Disabled" : "Enabled (default)";
|
|
2194
|
+
mitigations["Spectre v2"] = cmdline.includes("nospectre_v2") ? "Disabled" : "Enabled (default)";
|
|
2195
|
+
}
|
|
2196
|
+
catch {
|
|
2197
|
+
mitigations["Kernel cmdline"] = "Unable to read";
|
|
2198
|
+
}
|
|
2199
|
+
// CPU flags for hardware mitigations
|
|
2200
|
+
try {
|
|
2201
|
+
const cpuinfo = readFileSync("/proc/cpuinfo", "utf-8");
|
|
2202
|
+
const flags = cpuinfo.match(/^flags\s*:\s*(.*)$/m)?.[1] ?? "";
|
|
2203
|
+
mitigations["SMEP"] = flags.includes("smep") ? "Supported" : "Not available";
|
|
2204
|
+
mitigations["SMAP"] = flags.includes("smap") ? "Supported" : "Not available";
|
|
2205
|
+
mitigations["NX/XD"] = flags.includes("nx") ? "Supported" : "Not available";
|
|
2206
|
+
}
|
|
2207
|
+
catch {
|
|
2208
|
+
mitigations["CPU flags"] = "Unable to read";
|
|
2209
|
+
}
|
|
2210
|
+
// Kernel hardening sysctls
|
|
2211
|
+
const sysctlChecks = [
|
|
2212
|
+
["kernel.dmesg_restrict", "dmesg_restrict"],
|
|
2213
|
+
["kernel.kptr_restrict", "kptr_restrict"],
|
|
2214
|
+
["kernel.yama.ptrace_scope", "ptrace_scope"],
|
|
2215
|
+
["kernel.unprivileged_bpf_disabled", "unprivileged_bpf"],
|
|
2216
|
+
["kernel.kexec_load_disabled", "kexec_disabled"],
|
|
2217
|
+
];
|
|
2218
|
+
for (const [key, label] of sysctlChecks) {
|
|
2219
|
+
const r = await executeCommand({
|
|
2220
|
+
command: "sysctl",
|
|
2221
|
+
args: ["-n", key],
|
|
2222
|
+
timeout: 5000,
|
|
2223
|
+
});
|
|
2224
|
+
mitigations[label] = r.exitCode === 0 ? r.stdout.trim() : "unavailable";
|
|
2225
|
+
}
|
|
2226
|
+
return { content: [formatToolOutput({ mitigations })] };
|
|
2227
|
+
}
|
|
2228
|
+
catch (err) {
|
|
2229
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2230
|
+
return { content: [createErrorContent(`Mitigation report failed: ${msg}`)], isError: true };
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
default:
|
|
2234
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
// ── 9. usb_device_control ────────────────────────────────────────────────
|
|
2238
|
+
server.tool("usb_device_control", "USB device security control. Actions: audit_devices=audit connected USB devices, block_storage=block USB mass storage, whitelist=manage USB device whitelist, monitor=check recent USB events", {
|
|
2239
|
+
action: z
|
|
2240
|
+
.enum(["audit_devices", "block_storage", "whitelist", "monitor"])
|
|
2241
|
+
.describe("Action: audit_devices=list/audit USB, block_storage=block USB storage, whitelist=manage allowed devices, monitor=check USB events"),
|
|
2242
|
+
device_id: z
|
|
2243
|
+
.string()
|
|
2244
|
+
.optional()
|
|
2245
|
+
.describe("USB device ID in vendor:product format (for whitelist action)"),
|
|
2246
|
+
block_method: z
|
|
2247
|
+
.enum(["modprobe", "udev"])
|
|
2248
|
+
.optional()
|
|
2249
|
+
.default("modprobe")
|
|
2250
|
+
.describe("Method to block USB storage: modprobe=blacklist kernel module, udev=udev rule (default modprobe)"),
|
|
2251
|
+
output_format: z
|
|
2252
|
+
.enum(["text", "json"])
|
|
2253
|
+
.optional()
|
|
2254
|
+
.default("text")
|
|
2255
|
+
.describe("Output format (default text)"),
|
|
2256
|
+
}, async (params) => {
|
|
2257
|
+
const { action } = params;
|
|
2258
|
+
const outputFormat = params.output_format ?? "text";
|
|
2259
|
+
switch (action) {
|
|
2260
|
+
// ── audit_devices ────────────────────────────────────────────
|
|
2261
|
+
case "audit_devices": {
|
|
2262
|
+
try {
|
|
2263
|
+
const findings = {};
|
|
2264
|
+
// List connected USB devices via lsusb
|
|
2265
|
+
const lsusbResult = await runUsbCommand("lsusb", [], 10_000);
|
|
2266
|
+
if (lsusbResult.exitCode === 0 && lsusbResult.stdout.trim().length > 0) {
|
|
2267
|
+
const devices = lsusbResult.stdout.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
2268
|
+
findings.connectedDevices = devices;
|
|
2269
|
+
findings.connectedDeviceCount = devices.length;
|
|
2270
|
+
}
|
|
2271
|
+
else if (lsusbResult.stderr.includes("not found") || lsusbResult.exitCode === -1) {
|
|
2272
|
+
findings.connectedDevices = [];
|
|
2273
|
+
findings.connectedDeviceCount = 0;
|
|
2274
|
+
findings.lsusbAvailable = false;
|
|
2275
|
+
findings.lsusbNote = "lsusb not found — install usbutils package to audit USB devices";
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
findings.connectedDevices = [];
|
|
2279
|
+
findings.connectedDeviceCount = 0;
|
|
2280
|
+
findings.lsusbError = lsusbResult.stderr;
|
|
2281
|
+
}
|
|
2282
|
+
// Check for USB storage devices via lsblk
|
|
2283
|
+
const lsblkResult = await runUsbCommand("lsblk", ["-S", "-o", "NAME,TRAN,TYPE"], 10_000);
|
|
2284
|
+
if (lsblkResult.exitCode === 0) {
|
|
2285
|
+
const lines = lsblkResult.stdout.trim().split("\n").filter((l) => l.toLowerCase().includes("usb"));
|
|
2286
|
+
findings.storageDevices = lines;
|
|
2287
|
+
findings.storageDeviceCount = lines.length;
|
|
2288
|
+
}
|
|
2289
|
+
else {
|
|
2290
|
+
findings.storageDevices = [];
|
|
2291
|
+
findings.storageDeviceCount = 0;
|
|
2292
|
+
}
|
|
2293
|
+
// Check if usb_storage kernel module is loaded
|
|
2294
|
+
const lsmodResult = await runUsbCommand("lsmod", [], 10_000);
|
|
2295
|
+
if (lsmodResult.exitCode === 0) {
|
|
2296
|
+
const moduleLoaded = lsmodResult.stdout.split("\n").some((l) => l.startsWith("usb_storage"));
|
|
2297
|
+
findings.usbStorageModuleLoaded = moduleLoaded;
|
|
2298
|
+
}
|
|
2299
|
+
else {
|
|
2300
|
+
findings.usbStorageModuleLoaded = "unknown";
|
|
2301
|
+
}
|
|
2302
|
+
// Check existing udev rules for USB
|
|
2303
|
+
const udevLsResult = await runUsbCommand("ls", ["/etc/udev/rules.d/"], 5_000);
|
|
2304
|
+
if (udevLsResult.exitCode === 0) {
|
|
2305
|
+
const usbRuleFiles = udevLsResult.stdout.trim().split("\n").filter((f) => f.toLowerCase().includes("usb"));
|
|
2306
|
+
findings.existingUdevRules = [];
|
|
2307
|
+
for (const ruleFile of usbRuleFiles) {
|
|
2308
|
+
if (ruleFile.trim().length > 0) {
|
|
2309
|
+
const catResult = await runUsbCommand("cat", [`/etc/udev/rules.d/${ruleFile.trim()}`], 5_000);
|
|
2310
|
+
if (catResult.exitCode === 0) {
|
|
2311
|
+
findings.existingUdevRules.push(`${ruleFile}: ${catResult.stdout.trim()}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
else {
|
|
2317
|
+
findings.existingUdevRules = [];
|
|
2318
|
+
}
|
|
2319
|
+
// Check if USBGuard is installed and running
|
|
2320
|
+
const usbguardResult = await runUsbCommand("systemctl", ["status", "usbguard"], 10_000);
|
|
2321
|
+
if (usbguardResult.exitCode === 0 || usbguardResult.exitCode === 3) {
|
|
2322
|
+
findings.usbguardInstalled = true;
|
|
2323
|
+
findings.usbguardRunning = usbguardResult.stdout.includes("active (running)");
|
|
2324
|
+
findings.usbguardStatus = usbguardResult.stdout.trim().split("\n").slice(0, 5).join("\n");
|
|
2325
|
+
}
|
|
2326
|
+
else {
|
|
2327
|
+
findings.usbguardInstalled = false;
|
|
2328
|
+
findings.usbguardRunning = false;
|
|
2329
|
+
}
|
|
2330
|
+
const output = {
|
|
2331
|
+
action: "audit_devices",
|
|
2332
|
+
...findings,
|
|
2333
|
+
};
|
|
2334
|
+
if (outputFormat === "json") {
|
|
2335
|
+
return { content: [formatToolOutput(output)] };
|
|
2336
|
+
}
|
|
2337
|
+
let text = "USB Device Control — Audit Devices\n\n";
|
|
2338
|
+
text += `Connected USB Devices: ${findings.connectedDeviceCount ?? 0}\n`;
|
|
2339
|
+
if (Array.isArray(findings.connectedDevices) && findings.connectedDevices.length > 0) {
|
|
2340
|
+
for (const dev of findings.connectedDevices) {
|
|
2341
|
+
text += ` • ${dev}\n`;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
if (findings.lsusbNote) {
|
|
2345
|
+
text += `\n⚠ ${findings.lsusbNote}\n`;
|
|
2346
|
+
}
|
|
2347
|
+
text += `\nUSB Storage Devices: ${findings.storageDeviceCount ?? 0}\n`;
|
|
2348
|
+
if (Array.isArray(findings.storageDevices) && findings.storageDevices.length > 0) {
|
|
2349
|
+
for (const dev of findings.storageDevices) {
|
|
2350
|
+
text += ` • ${dev}\n`;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
text += `\nUSB Storage Kernel Module Loaded: ${findings.usbStorageModuleLoaded}\n`;
|
|
2354
|
+
text += `USBGuard Installed: ${findings.usbguardInstalled ? "yes" : "no"}\n`;
|
|
2355
|
+
text += `USBGuard Running: ${findings.usbguardRunning ? "yes" : "no"}\n`;
|
|
2356
|
+
if (Array.isArray(findings.existingUdevRules) && findings.existingUdevRules.length > 0) {
|
|
2357
|
+
text += `\nExisting USB udev Rules:\n`;
|
|
2358
|
+
for (const rule of findings.existingUdevRules) {
|
|
2359
|
+
text += ` • ${rule}\n`;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
return { content: [createTextContent(text)] };
|
|
2363
|
+
}
|
|
2364
|
+
catch (err) {
|
|
2365
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2366
|
+
return { content: [createErrorContent(`audit_devices failed: ${msg}`)], isError: true };
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
// ── block_storage ────────────────────────────────────────────
|
|
2370
|
+
case "block_storage": {
|
|
2371
|
+
try {
|
|
2372
|
+
const method = params.block_method ?? "modprobe";
|
|
2373
|
+
const results = [];
|
|
2374
|
+
// Privilege check
|
|
2375
|
+
const privCheck = checkPrivileges();
|
|
2376
|
+
if (!privCheck.ok) {
|
|
2377
|
+
return { content: [createErrorContent(`Cannot block USB storage: ${privCheck.message}`)], isError: true };
|
|
2378
|
+
}
|
|
2379
|
+
if (method === "modprobe") {
|
|
2380
|
+
// Blacklist usb-storage module via modprobe config
|
|
2381
|
+
const modprobeConfPath = "/etc/modprobe.d/usb-storage-block.conf";
|
|
2382
|
+
const modprobeContent = 'blacklist usb-storage\ninstall usb-storage /bin/true\n';
|
|
2383
|
+
try {
|
|
2384
|
+
secureWriteFileSync(modprobeConfPath, modprobeContent);
|
|
2385
|
+
results.push({ target: modprobeConfPath, action: "Created modprobe blacklist config", status: "applied" });
|
|
2386
|
+
}
|
|
2387
|
+
catch (writeErr) {
|
|
2388
|
+
const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
2389
|
+
results.push({ target: modprobeConfPath, action: `Failed to write: ${writeMsg}`, status: "error" });
|
|
2390
|
+
}
|
|
2391
|
+
// Attempt to unload the module
|
|
2392
|
+
const rmmodResult = await runUsbCommand("modprobe", ["-r", "usb-storage"], 10_000);
|
|
2393
|
+
if (rmmodResult.exitCode === 0) {
|
|
2394
|
+
results.push({ target: "usb-storage module", action: "Module unloaded successfully", status: "applied" });
|
|
2395
|
+
}
|
|
2396
|
+
else {
|
|
2397
|
+
results.push({ target: "usb-storage module", action: `Module unload: ${rmmodResult.stderr || "may not be loaded"}`, status: "info" });
|
|
2398
|
+
}
|
|
2399
|
+
// Verify module status after
|
|
2400
|
+
const lsmodAfter = await runUsbCommand("lsmod", [], 10_000);
|
|
2401
|
+
const moduleStillLoaded = lsmodAfter.stdout.split("\n").some((l) => l.startsWith("usb_storage"));
|
|
2402
|
+
const output = {
|
|
2403
|
+
action: "block_storage",
|
|
2404
|
+
method: "modprobe",
|
|
2405
|
+
filesCreated: [modprobeConfPath],
|
|
2406
|
+
moduleLoadedAfter: moduleStillLoaded,
|
|
2407
|
+
cisBenchmark: "CIS Benchmark 1.1.10 — Disable USB Storage",
|
|
2408
|
+
results,
|
|
2409
|
+
};
|
|
2410
|
+
if (outputFormat === "json") {
|
|
2411
|
+
return { content: [formatToolOutput(output)] };
|
|
2412
|
+
}
|
|
2413
|
+
let text = "USB Device Control — Block Storage (modprobe method)\n\n";
|
|
2414
|
+
text += `CIS Reference: CIS Benchmark 1.1.10\n\n`;
|
|
2415
|
+
for (const r of results) {
|
|
2416
|
+
text += ` • ${r.target}: ${r.action} [${r.status}]\n`;
|
|
2417
|
+
}
|
|
2418
|
+
text += `\nUSB Storage Module Still Loaded: ${moduleStillLoaded ? "yes ⚠" : "no ✓"}\n`;
|
|
2419
|
+
return { content: [createTextContent(text)] };
|
|
2420
|
+
}
|
|
2421
|
+
else {
|
|
2422
|
+
// udev method
|
|
2423
|
+
const udevRulePath = "/etc/udev/rules.d/99-usb-storage-block.rules";
|
|
2424
|
+
const udevContent = 'ACTION=="add", SUBSYSTEMS=="usb", DRIVERS=="usb-storage", ATTR{authorized}="0"\n';
|
|
2425
|
+
try {
|
|
2426
|
+
secureWriteFileSync(udevRulePath, udevContent);
|
|
2427
|
+
results.push({ target: udevRulePath, action: "Created udev rule to block USB storage", status: "applied" });
|
|
2428
|
+
}
|
|
2429
|
+
catch (writeErr) {
|
|
2430
|
+
const writeMsg = writeErr instanceof Error ? writeErr.message : String(writeErr);
|
|
2431
|
+
results.push({ target: udevRulePath, action: `Failed to write: ${writeMsg}`, status: "error" });
|
|
2432
|
+
}
|
|
2433
|
+
// Reload udev rules
|
|
2434
|
+
const reloadResult = await runUsbCommand("udevadm", ["control", "--reload-rules"], 10_000);
|
|
2435
|
+
if (reloadResult.exitCode === 0) {
|
|
2436
|
+
results.push({ target: "udev", action: "Rules reloaded successfully", status: "applied" });
|
|
2437
|
+
}
|
|
2438
|
+
else {
|
|
2439
|
+
results.push({ target: "udev", action: `Reload failed: ${reloadResult.stderr}`, status: "error" });
|
|
2440
|
+
}
|
|
2441
|
+
const output = {
|
|
2442
|
+
action: "block_storage",
|
|
2443
|
+
method: "udev",
|
|
2444
|
+
filesCreated: [udevRulePath],
|
|
2445
|
+
cisBenchmark: "CIS Benchmark 1.1.10 — Disable USB Storage",
|
|
2446
|
+
results,
|
|
2447
|
+
};
|
|
2448
|
+
if (outputFormat === "json") {
|
|
2449
|
+
return { content: [formatToolOutput(output)] };
|
|
2450
|
+
}
|
|
2451
|
+
let text = "USB Device Control — Block Storage (udev method)\n\n";
|
|
2452
|
+
text += `CIS Reference: CIS Benchmark 1.1.10\n\n`;
|
|
2453
|
+
for (const r of results) {
|
|
2454
|
+
text += ` • ${r.target}: ${r.action} [${r.status}]\n`;
|
|
2455
|
+
}
|
|
2456
|
+
return { content: [createTextContent(text)] };
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
catch (err) {
|
|
2460
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2461
|
+
return { content: [createErrorContent(`block_storage failed: ${msg}`)], isError: true };
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
// ── whitelist ────────────────────────────────────────────────
|
|
2465
|
+
case "whitelist": {
|
|
2466
|
+
try {
|
|
2467
|
+
const whitelist = loadUsbWhitelist();
|
|
2468
|
+
if (params.device_id) {
|
|
2469
|
+
// Validate device_id format (vendor:product)
|
|
2470
|
+
if (!/^[0-9a-fA-F]{4}:[0-9a-fA-F]{4}$/.test(params.device_id)) {
|
|
2471
|
+
return { content: [createErrorContent("Invalid device_id format. Expected vendor:product format (e.g., 1234:5678)")], isError: true };
|
|
2472
|
+
}
|
|
2473
|
+
// Check for duplicate
|
|
2474
|
+
const existing = whitelist.find((e) => e.device_id === params.device_id);
|
|
2475
|
+
if (existing) {
|
|
2476
|
+
const output = {
|
|
2477
|
+
action: "whitelist",
|
|
2478
|
+
status: "duplicate",
|
|
2479
|
+
message: `Device ${params.device_id} is already whitelisted`,
|
|
2480
|
+
device: existing,
|
|
2481
|
+
totalDevices: whitelist.length,
|
|
2482
|
+
};
|
|
2483
|
+
if (outputFormat === "json") {
|
|
2484
|
+
return { content: [formatToolOutput(output)] };
|
|
2485
|
+
}
|
|
2486
|
+
return { content: [createTextContent(`Device ${params.device_id} is already in the whitelist (added ${existing.added_date})`)] };
|
|
2487
|
+
}
|
|
2488
|
+
// Get description from lsusb
|
|
2489
|
+
let description = "Unknown device";
|
|
2490
|
+
const lsusbResult = await runUsbCommand("lsusb", [], 10_000);
|
|
2491
|
+
if (lsusbResult.exitCode === 0) {
|
|
2492
|
+
const vendorProduct = params.device_id.toLowerCase();
|
|
2493
|
+
const matchLine = lsusbResult.stdout.split("\n").find((l) => l.toLowerCase().includes(vendorProduct));
|
|
2494
|
+
if (matchLine) {
|
|
2495
|
+
// Extract description after the ID
|
|
2496
|
+
const idIdx = matchLine.toLowerCase().indexOf(vendorProduct);
|
|
2497
|
+
if (idIdx >= 0) {
|
|
2498
|
+
description = matchLine.substring(idIdx + vendorProduct.length).trim() || description;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const newEntry = {
|
|
2503
|
+
device_id: params.device_id,
|
|
2504
|
+
description,
|
|
2505
|
+
added_date: new Date().toISOString(),
|
|
2506
|
+
};
|
|
2507
|
+
whitelist.push(newEntry);
|
|
2508
|
+
saveUsbWhitelist(whitelist);
|
|
2509
|
+
const output = {
|
|
2510
|
+
action: "whitelist",
|
|
2511
|
+
status: "added",
|
|
2512
|
+
device: newEntry,
|
|
2513
|
+
totalDevices: whitelist.length,
|
|
2514
|
+
};
|
|
2515
|
+
if (outputFormat === "json") {
|
|
2516
|
+
return { content: [formatToolOutput(output)] };
|
|
2517
|
+
}
|
|
2518
|
+
let text = "USB Device Control — Whitelist\n\n";
|
|
2519
|
+
text += `Added device: ${newEntry.device_id}\n`;
|
|
2520
|
+
text += `Description: ${newEntry.description}\n`;
|
|
2521
|
+
text += `Added: ${newEntry.added_date}\n`;
|
|
2522
|
+
text += `Total whitelisted devices: ${whitelist.length}\n`;
|
|
2523
|
+
return { content: [createTextContent(text)] };
|
|
2524
|
+
}
|
|
2525
|
+
else {
|
|
2526
|
+
// List current whitelist
|
|
2527
|
+
const output = {
|
|
2528
|
+
action: "whitelist",
|
|
2529
|
+
status: "list",
|
|
2530
|
+
totalDevices: whitelist.length,
|
|
2531
|
+
devices: whitelist,
|
|
2532
|
+
};
|
|
2533
|
+
if (outputFormat === "json") {
|
|
2534
|
+
return { content: [formatToolOutput(output)] };
|
|
2535
|
+
}
|
|
2536
|
+
let text = "USB Device Control — Whitelist\n\n";
|
|
2537
|
+
if (whitelist.length === 0) {
|
|
2538
|
+
text += "No devices in whitelist.\n";
|
|
2539
|
+
text += `\nWhitelist file: ${USB_WHITELIST_PATH}\n`;
|
|
2540
|
+
text += "Use device_id parameter to add devices (e.g., device_id: \"1234:5678\")\n";
|
|
2541
|
+
}
|
|
2542
|
+
else {
|
|
2543
|
+
text += `Total whitelisted devices: ${whitelist.length}\n\n`;
|
|
2544
|
+
for (const entry of whitelist) {
|
|
2545
|
+
text += ` • ${entry.device_id} — ${entry.description} (added ${entry.added_date})\n`;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return { content: [createTextContent(text)] };
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
catch (err) {
|
|
2552
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2553
|
+
return { content: [createErrorContent(`whitelist failed: ${msg}`)], isError: true };
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
// ── monitor ──────────────────────────────────────────────────
|
|
2557
|
+
case "monitor": {
|
|
2558
|
+
try {
|
|
2559
|
+
const events = {};
|
|
2560
|
+
// Read dmesg for USB events
|
|
2561
|
+
const dmesgResult = await runUsbCommand("dmesg", [], 15_000);
|
|
2562
|
+
if (dmesgResult.exitCode === 0) {
|
|
2563
|
+
const usbLines = dmesgResult.stdout.split("\n").filter((l) => l.toLowerCase().includes("usb"));
|
|
2564
|
+
// Take last 50 lines to avoid overwhelming output
|
|
2565
|
+
events.dmesgEvents = usbLines.slice(-50);
|
|
2566
|
+
events.dmesgEventCount = usbLines.length;
|
|
2567
|
+
}
|
|
2568
|
+
else {
|
|
2569
|
+
events.dmesgEvents = [];
|
|
2570
|
+
events.dmesgEventCount = 0;
|
|
2571
|
+
events.dmesgError = dmesgResult.stderr;
|
|
2572
|
+
}
|
|
2573
|
+
// Check journalctl for USB events
|
|
2574
|
+
const journalResult = await runUsbCommand("journalctl", ["-k", "--grep=usb", "--since=1 hour ago", "--no-pager"], 15_000);
|
|
2575
|
+
if (journalResult.exitCode === 0) {
|
|
2576
|
+
const journalLines = journalResult.stdout.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
2577
|
+
events.journalctlEvents = journalLines.slice(-50);
|
|
2578
|
+
events.journalctlEventCount = journalLines.length;
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
events.journalctlEvents = [];
|
|
2582
|
+
events.journalctlEventCount = 0;
|
|
2583
|
+
}
|
|
2584
|
+
// Check audit log for USB events
|
|
2585
|
+
const auditResult = await runUsbCommand("grep", ["-i", "usb", "/var/log/audit/audit.log"], 10_000);
|
|
2586
|
+
if (auditResult.exitCode === 0 && auditResult.stdout.trim().length > 0) {
|
|
2587
|
+
const auditLines = auditResult.stdout.trim().split("\n");
|
|
2588
|
+
events.auditEvents = auditLines.slice(-20);
|
|
2589
|
+
events.auditEventCount = auditLines.length;
|
|
2590
|
+
}
|
|
2591
|
+
else {
|
|
2592
|
+
events.auditEvents = [];
|
|
2593
|
+
events.auditEventCount = 0;
|
|
2594
|
+
}
|
|
2595
|
+
// List recently connected USB devices
|
|
2596
|
+
const lsusbResult = await runUsbCommand("lsusb", [], 10_000);
|
|
2597
|
+
if (lsusbResult.exitCode === 0 && lsusbResult.stdout.trim().length > 0) {
|
|
2598
|
+
events.currentDevices = lsusbResult.stdout.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
2599
|
+
}
|
|
2600
|
+
else {
|
|
2601
|
+
events.currentDevices = [];
|
|
2602
|
+
}
|
|
2603
|
+
const totalEvents = (events.dmesgEventCount ?? 0) +
|
|
2604
|
+
(events.journalctlEventCount ?? 0) +
|
|
2605
|
+
(events.auditEventCount ?? 0);
|
|
2606
|
+
const output = {
|
|
2607
|
+
action: "monitor",
|
|
2608
|
+
totalUsbEvents: totalEvents,
|
|
2609
|
+
...events,
|
|
2610
|
+
};
|
|
2611
|
+
if (outputFormat === "json") {
|
|
2612
|
+
return { content: [formatToolOutput(output)] };
|
|
2613
|
+
}
|
|
2614
|
+
let text = "USB Device Control — Monitor\n\n";
|
|
2615
|
+
text += `Total USB Events Found: ${totalEvents}\n\n`;
|
|
2616
|
+
text += `dmesg USB Events: ${events.dmesgEventCount}\n`;
|
|
2617
|
+
if (Array.isArray(events.dmesgEvents) && events.dmesgEvents.length > 0) {
|
|
2618
|
+
const recentDmesg = events.dmesgEvents.slice(-10);
|
|
2619
|
+
for (const line of recentDmesg) {
|
|
2620
|
+
text += ` ${line}\n`;
|
|
2621
|
+
}
|
|
2622
|
+
if (events.dmesgEventCount > 10) {
|
|
2623
|
+
text += ` ... and ${events.dmesgEventCount - 10} more\n`;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
text += `\njournalctl USB Events (last hour): ${events.journalctlEventCount}\n`;
|
|
2627
|
+
if (Array.isArray(events.journalctlEvents) && events.journalctlEvents.length > 0) {
|
|
2628
|
+
const recentJournal = events.journalctlEvents.slice(-10);
|
|
2629
|
+
for (const line of recentJournal) {
|
|
2630
|
+
text += ` ${line}\n`;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
text += `\nAudit Log USB Events: ${events.auditEventCount}\n`;
|
|
2634
|
+
if (Array.isArray(events.currentDevices) && events.currentDevices.length > 0) {
|
|
2635
|
+
text += `\nCurrently Connected USB Devices:\n`;
|
|
2636
|
+
for (const dev of events.currentDevices) {
|
|
2637
|
+
text += ` • ${dev}\n`;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (totalEvents === 0) {
|
|
2641
|
+
text += "\nNo recent USB events found.\n";
|
|
2642
|
+
}
|
|
2643
|
+
return { content: [createTextContent(text)] };
|
|
2644
|
+
}
|
|
2645
|
+
catch (err) {
|
|
2646
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2647
|
+
return { content: [createErrorContent(`monitor failed: ${msg}`)], isError: true };
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
default:
|
|
2651
|
+
return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
}
|