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.
Files changed (186) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/build/core/auto-installer.d.ts +102 -0
  5. package/build/core/auto-installer.d.ts.map +1 -0
  6. package/build/core/auto-installer.js +833 -0
  7. package/build/core/backup-manager.d.ts +63 -0
  8. package/build/core/backup-manager.d.ts.map +1 -0
  9. package/build/core/backup-manager.js +189 -0
  10. package/build/core/changelog.d.ts +75 -0
  11. package/build/core/changelog.d.ts.map +1 -0
  12. package/build/core/changelog.js +123 -0
  13. package/build/core/command-allowlist.d.ts +129 -0
  14. package/build/core/command-allowlist.d.ts.map +1 -0
  15. package/build/core/command-allowlist.js +849 -0
  16. package/build/core/config.d.ts +79 -0
  17. package/build/core/config.d.ts.map +1 -0
  18. package/build/core/config.js +193 -0
  19. package/build/core/dependency-validator.d.ts +106 -0
  20. package/build/core/dependency-validator.d.ts.map +1 -0
  21. package/build/core/dependency-validator.js +405 -0
  22. package/build/core/distro-adapter.d.ts +177 -0
  23. package/build/core/distro-adapter.d.ts.map +1 -0
  24. package/build/core/distro-adapter.js +481 -0
  25. package/build/core/distro.d.ts +68 -0
  26. package/build/core/distro.d.ts.map +1 -0
  27. package/build/core/distro.js +457 -0
  28. package/build/core/encrypted-state.d.ts +76 -0
  29. package/build/core/encrypted-state.d.ts.map +1 -0
  30. package/build/core/encrypted-state.js +209 -0
  31. package/build/core/executor.d.ts +56 -0
  32. package/build/core/executor.d.ts.map +1 -0
  33. package/build/core/executor.js +350 -0
  34. package/build/core/installer.d.ts +92 -0
  35. package/build/core/installer.d.ts.map +1 -0
  36. package/build/core/installer.js +1072 -0
  37. package/build/core/logger.d.ts +102 -0
  38. package/build/core/logger.d.ts.map +1 -0
  39. package/build/core/logger.js +132 -0
  40. package/build/core/parsers.d.ts +151 -0
  41. package/build/core/parsers.d.ts.map +1 -0
  42. package/build/core/parsers.js +479 -0
  43. package/build/core/policy-engine.d.ts +170 -0
  44. package/build/core/policy-engine.d.ts.map +1 -0
  45. package/build/core/policy-engine.js +656 -0
  46. package/build/core/preflight.d.ts +157 -0
  47. package/build/core/preflight.d.ts.map +1 -0
  48. package/build/core/preflight.js +638 -0
  49. package/build/core/privilege-manager.d.ts +108 -0
  50. package/build/core/privilege-manager.d.ts.map +1 -0
  51. package/build/core/privilege-manager.js +363 -0
  52. package/build/core/rate-limiter.d.ts +67 -0
  53. package/build/core/rate-limiter.d.ts.map +1 -0
  54. package/build/core/rate-limiter.js +129 -0
  55. package/build/core/rollback.d.ts +73 -0
  56. package/build/core/rollback.d.ts.map +1 -0
  57. package/build/core/rollback.js +278 -0
  58. package/build/core/safeguards.d.ts +58 -0
  59. package/build/core/safeguards.d.ts.map +1 -0
  60. package/build/core/safeguards.js +448 -0
  61. package/build/core/sanitizer.d.ts +118 -0
  62. package/build/core/sanitizer.d.ts.map +1 -0
  63. package/build/core/sanitizer.js +459 -0
  64. package/build/core/secure-fs.d.ts +67 -0
  65. package/build/core/secure-fs.d.ts.map +1 -0
  66. package/build/core/secure-fs.js +143 -0
  67. package/build/core/spawn-safe.d.ts +55 -0
  68. package/build/core/spawn-safe.d.ts.map +1 -0
  69. package/build/core/spawn-safe.js +146 -0
  70. package/build/core/sudo-guard.d.ts +145 -0
  71. package/build/core/sudo-guard.d.ts.map +1 -0
  72. package/build/core/sudo-guard.js +349 -0
  73. package/build/core/sudo-session.d.ts +100 -0
  74. package/build/core/sudo-session.d.ts.map +1 -0
  75. package/build/core/sudo-session.js +319 -0
  76. package/build/core/tool-dependencies.d.ts +61 -0
  77. package/build/core/tool-dependencies.d.ts.map +1 -0
  78. package/build/core/tool-dependencies.js +571 -0
  79. package/build/core/tool-registry.d.ts +111 -0
  80. package/build/core/tool-registry.d.ts.map +1 -0
  81. package/build/core/tool-registry.js +656 -0
  82. package/build/core/tool-wrapper.d.ts +73 -0
  83. package/build/core/tool-wrapper.d.ts.map +1 -0
  84. package/build/core/tool-wrapper.js +296 -0
  85. package/build/index.d.ts +3 -0
  86. package/build/index.d.ts.map +1 -0
  87. package/build/index.js +247 -0
  88. package/build/tools/access-control.d.ts +9 -0
  89. package/build/tools/access-control.d.ts.map +1 -0
  90. package/build/tools/access-control.js +1818 -0
  91. package/build/tools/api-security.d.ts +12 -0
  92. package/build/tools/api-security.d.ts.map +1 -0
  93. package/build/tools/api-security.js +901 -0
  94. package/build/tools/app-hardening.d.ts +11 -0
  95. package/build/tools/app-hardening.d.ts.map +1 -0
  96. package/build/tools/app-hardening.js +768 -0
  97. package/build/tools/backup.d.ts +8 -0
  98. package/build/tools/backup.d.ts.map +1 -0
  99. package/build/tools/backup.js +381 -0
  100. package/build/tools/cloud-security.d.ts +17 -0
  101. package/build/tools/cloud-security.d.ts.map +1 -0
  102. package/build/tools/cloud-security.js +739 -0
  103. package/build/tools/compliance.d.ts +10 -0
  104. package/build/tools/compliance.d.ts.map +1 -0
  105. package/build/tools/compliance.js +1225 -0
  106. package/build/tools/container-security.d.ts +14 -0
  107. package/build/tools/container-security.d.ts.map +1 -0
  108. package/build/tools/container-security.js +788 -0
  109. package/build/tools/deception.d.ts +13 -0
  110. package/build/tools/deception.d.ts.map +1 -0
  111. package/build/tools/deception.js +763 -0
  112. package/build/tools/dns-security.d.ts +93 -0
  113. package/build/tools/dns-security.d.ts.map +1 -0
  114. package/build/tools/dns-security.js +745 -0
  115. package/build/tools/drift-detection.d.ts +8 -0
  116. package/build/tools/drift-detection.d.ts.map +1 -0
  117. package/build/tools/drift-detection.js +326 -0
  118. package/build/tools/ebpf-security.d.ts +15 -0
  119. package/build/tools/ebpf-security.d.ts.map +1 -0
  120. package/build/tools/ebpf-security.js +294 -0
  121. package/build/tools/encryption.d.ts +9 -0
  122. package/build/tools/encryption.d.ts.map +1 -0
  123. package/build/tools/encryption.js +1667 -0
  124. package/build/tools/firewall.d.ts +9 -0
  125. package/build/tools/firewall.d.ts.map +1 -0
  126. package/build/tools/firewall.js +1398 -0
  127. package/build/tools/hardening.d.ts +10 -0
  128. package/build/tools/hardening.d.ts.map +1 -0
  129. package/build/tools/hardening.js +2654 -0
  130. package/build/tools/ids.d.ts +9 -0
  131. package/build/tools/ids.d.ts.map +1 -0
  132. package/build/tools/ids.js +624 -0
  133. package/build/tools/incident-response.d.ts +10 -0
  134. package/build/tools/incident-response.d.ts.map +1 -0
  135. package/build/tools/incident-response.js +1180 -0
  136. package/build/tools/logging.d.ts +12 -0
  137. package/build/tools/logging.d.ts.map +1 -0
  138. package/build/tools/logging.js +454 -0
  139. package/build/tools/malware.d.ts +10 -0
  140. package/build/tools/malware.d.ts.map +1 -0
  141. package/build/tools/malware.js +532 -0
  142. package/build/tools/meta.d.ts +11 -0
  143. package/build/tools/meta.d.ts.map +1 -0
  144. package/build/tools/meta.js +2278 -0
  145. package/build/tools/network-defense.d.ts +12 -0
  146. package/build/tools/network-defense.d.ts.map +1 -0
  147. package/build/tools/network-defense.js +760 -0
  148. package/build/tools/patch-management.d.ts +3 -0
  149. package/build/tools/patch-management.d.ts.map +1 -0
  150. package/build/tools/patch-management.js +708 -0
  151. package/build/tools/process-security.d.ts +12 -0
  152. package/build/tools/process-security.d.ts.map +1 -0
  153. package/build/tools/process-security.js +784 -0
  154. package/build/tools/reporting.d.ts +11 -0
  155. package/build/tools/reporting.d.ts.map +1 -0
  156. package/build/tools/reporting.js +559 -0
  157. package/build/tools/secrets.d.ts +9 -0
  158. package/build/tools/secrets.d.ts.map +1 -0
  159. package/build/tools/secrets.js +596 -0
  160. package/build/tools/siem-integration.d.ts +18 -0
  161. package/build/tools/siem-integration.d.ts.map +1 -0
  162. package/build/tools/siem-integration.js +754 -0
  163. package/build/tools/sudo-management.d.ts +18 -0
  164. package/build/tools/sudo-management.d.ts.map +1 -0
  165. package/build/tools/sudo-management.js +737 -0
  166. package/build/tools/supply-chain-security.d.ts +8 -0
  167. package/build/tools/supply-chain-security.d.ts.map +1 -0
  168. package/build/tools/supply-chain-security.js +256 -0
  169. package/build/tools/threat-intel.d.ts +22 -0
  170. package/build/tools/threat-intel.d.ts.map +1 -0
  171. package/build/tools/threat-intel.js +749 -0
  172. package/build/tools/vulnerability-management.d.ts +11 -0
  173. package/build/tools/vulnerability-management.d.ts.map +1 -0
  174. package/build/tools/vulnerability-management.js +667 -0
  175. package/build/tools/waf.d.ts +12 -0
  176. package/build/tools/waf.d.ts.map +1 -0
  177. package/build/tools/waf.js +843 -0
  178. package/build/tools/wireless-security.d.ts +19 -0
  179. package/build/tools/wireless-security.d.ts.map +1 -0
  180. package/build/tools/wireless-security.js +826 -0
  181. package/build/tools/zero-trust-network.d.ts +8 -0
  182. package/build/tools/zero-trust-network.d.ts.map +1 -0
  183. package/build/tools/zero-trust-network.js +367 -0
  184. package/docs/SAFEGUARDS.md +518 -0
  185. package/docs/TOOLS-REFERENCE.md +665 -0
  186. 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
+ }