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,1225 @@
1
+ /**
2
+ * Compliance and audit tools for Kali Defense MCP Server.
3
+ *
4
+ * Registers 6 tools: compliance_lynis_audit, compliance_oscap_scan,
5
+ * compliance_check (actions: cis, framework), compliance_policy_evaluate,
6
+ * compliance_report, compliance_cron_restrict, compliance_tmp_hardening.
7
+ */
8
+ import { z } from "zod";
9
+ import { executeCommand } from "../core/executor.js";
10
+ import { getConfig, getToolTimeout } from "../core/config.js";
11
+ import { createTextContent, createErrorContent, parseLynisOutput, parseOscapOutput, formatToolOutput, } from "../core/parsers.js";
12
+ import { logChange, createChangeEntry } from "../core/changelog.js";
13
+ import { getDistroAdapter } from "../core/distro-adapter.js";
14
+ import { sanitizeArgs } from "../core/sanitizer.js";
15
+ import { readFileSync } from "node:fs";
16
+ import { loadPolicy, evaluatePolicy, getBuiltinPolicies, } from "../core/policy-engine.js";
17
+ async function runCisCheck(command, args, id, title, level, expectPattern) {
18
+ try {
19
+ const result = await executeCommand({
20
+ command,
21
+ args,
22
+ timeout: 30_000,
23
+ });
24
+ if (expectPattern) {
25
+ const passed = expectPattern.test(result.stdout.trim());
26
+ return {
27
+ id,
28
+ title,
29
+ status: passed ? "pass" : "fail",
30
+ detail: passed
31
+ ? "Check passed"
32
+ : `Expected pattern not found. Output: ${result.stdout.trim().slice(0, 200)}`,
33
+ level,
34
+ };
35
+ }
36
+ return {
37
+ id,
38
+ title,
39
+ status: result.exitCode === 0 ? "pass" : "fail",
40
+ detail: result.exitCode === 0
41
+ ? "Check passed"
42
+ : `Exit code ${result.exitCode}: ${result.stderr.trim().slice(0, 200)}`,
43
+ level,
44
+ };
45
+ }
46
+ catch (err) {
47
+ const msg = err instanceof Error ? err.message : String(err);
48
+ return { id, title, status: "error", detail: msg, level };
49
+ }
50
+ }
51
+ async function cisFilesystemChecks(level) {
52
+ const results = [];
53
+ // CIS 1.1.2 - /tmp is a separate mount
54
+ results.push(await runCisCheck("findmnt", ["-n", "/tmp"], "CIS-1.1.2", "/tmp is a separate partition", level));
55
+ // CIS 1.1.4 - /tmp has noexec (check mount output AND fstab)
56
+ {
57
+ const mountCheck = await executeCommand({
58
+ command: "findmnt", args: ["-n", "-o", "OPTIONS", "/tmp"], timeout: 10_000,
59
+ });
60
+ const fstabCheck = await executeCommand({
61
+ command: "sudo", args: ["grep", "-E", "^[^#].*\\s/tmp\\s.*noexec", "/etc/fstab"], timeout: 10_000,
62
+ });
63
+ const mountHasNoexec = /noexec/.test(mountCheck.stdout.trim());
64
+ const fstabHasNoexec = fstabCheck.exitCode === 0;
65
+ results.push({
66
+ id: "CIS-1.1.4",
67
+ title: "/tmp has noexec mount option",
68
+ status: (mountHasNoexec || fstabHasNoexec) ? "pass" : "fail",
69
+ detail: mountHasNoexec
70
+ ? "Check passed"
71
+ : fstabHasNoexec
72
+ ? "noexec configured in fstab (will apply on next mount/reboot)"
73
+ : `Expected pattern not found. Output: ${mountCheck.stdout.trim().slice(0, 200)}`,
74
+ level,
75
+ });
76
+ }
77
+ // CIS 1.1.21 - Sticky bit on world-writable dirs
78
+ results.push(await runCisCheck("find", ["/", "-xdev", "-type", "d", "-perm", "-0002", "!", "-perm", "-1000", "-print"], "CIS-1.1.21", "Sticky bit set on world-writable directories", level, /^$/));
79
+ // CIS 1.4.1 - ASLR enabled
80
+ results.push(await runCisCheck("sysctl", ["-n", "kernel.randomize_va_space"], "CIS-1.4.1", "ASLR enabled", level, /^2$/));
81
+ // CIS 1.1.22 - Automounting disabled (autofs should NOT be active)
82
+ {
83
+ const autofs = await executeCommand({
84
+ command: "systemctl",
85
+ args: ["is-active", "autofs"],
86
+ timeout: 10_000,
87
+ });
88
+ results.push({
89
+ id: "CIS-1.1.22",
90
+ title: "Automounting (autofs) is disabled",
91
+ status: autofs.stdout.trim() !== "active" ? "pass" : "fail",
92
+ detail: autofs.stdout.trim() === "active"
93
+ ? "autofs service is running — should be disabled"
94
+ : "autofs service is not active",
95
+ level,
96
+ });
97
+ }
98
+ // CIS 1.5.1 - Core dump limits (hard core 0 in limits.conf or limits.d/)
99
+ {
100
+ const limitsCheck = await executeCommand({
101
+ command: "sudo",
102
+ args: ["grep", "-rE", "\\*\\s+hard\\s+core\\s+0", "/etc/security/limits.conf", "/etc/security/limits.d/"],
103
+ timeout: 10_000,
104
+ });
105
+ results.push({
106
+ id: "CIS-1.5.1-limits",
107
+ title: "Core dumps restricted via limits.conf (hard core 0)",
108
+ status: limitsCheck.exitCode === 0 ? "pass" : "fail",
109
+ detail: limitsCheck.exitCode === 0
110
+ ? "Check passed"
111
+ : `hard core 0 not found in limits.conf or limits.d/`,
112
+ level,
113
+ });
114
+ }
115
+ return results;
116
+ }
117
+ async function cisServicesChecks(level) {
118
+ const results = [];
119
+ // Check unnecessary services are not running
120
+ const unnecessaryServices = ["avahi-daemon", "cups", "rpcbind", "telnet.socket"];
121
+ for (const svc of unnecessaryServices) {
122
+ const result = await executeCommand({
123
+ command: "systemctl",
124
+ args: ["is-active", svc],
125
+ timeout: 10_000,
126
+ });
127
+ results.push({
128
+ id: `CIS-2.x-${svc}`,
129
+ title: `Unnecessary service '${svc}' is disabled`,
130
+ status: result.stdout.trim() === "inactive" || result.exitCode !== 0 ? "pass" : "fail",
131
+ detail: result.stdout.trim() === "active" ? `Service ${svc} is running` : `Service ${svc} is not active`,
132
+ level,
133
+ });
134
+ }
135
+ // CIS 2.2.1 - NTP/Chrony time synchronization is active
136
+ {
137
+ const chronyd = await executeCommand({
138
+ command: "systemctl",
139
+ args: ["is-active", "chronyd"],
140
+ timeout: 10_000,
141
+ });
142
+ const ntpSvc = await executeCommand({
143
+ command: "systemctl",
144
+ args: ["is-active", "ntp"],
145
+ timeout: 10_000,
146
+ });
147
+ const isActive = chronyd.stdout.trim() === "active" || ntpSvc.stdout.trim() === "active";
148
+ results.push({
149
+ id: "CIS-2.2.1",
150
+ title: "Time synchronization (chrony/ntp) is active",
151
+ status: isActive ? "pass" : "fail",
152
+ detail: isActive
153
+ ? "Time synchronization service is running"
154
+ : "Neither chronyd nor ntp service is active",
155
+ level,
156
+ });
157
+ }
158
+ return results;
159
+ }
160
+ async function cisNetworkChecks(level) {
161
+ const results = [];
162
+ // CIS 3.1.1 - IP forwarding disabled
163
+ results.push(await runCisCheck("sysctl", ["-n", "net.ipv4.ip_forward"], "CIS-3.1.1", "IP forwarding disabled", level, /^0$/));
164
+ // CIS 3.2.2 - ICMP redirects disabled
165
+ results.push(await runCisCheck("sysctl", ["-n", "net.ipv4.conf.all.accept_redirects"], "CIS-3.2.2", "ICMP redirects not accepted", level, /^0$/));
166
+ // CIS 3.2.1 - Source routing disabled
167
+ results.push(await runCisCheck("sysctl", ["-n", "net.ipv4.conf.all.accept_source_route"], "CIS-3.2.1", "Source routed packets not accepted", level, /^0$/));
168
+ // CIS 3.2.8 - TCP SYN cookies
169
+ results.push(await runCisCheck("sysctl", ["-n", "net.ipv4.tcp_syncookies"], "CIS-3.2.8", "TCP SYN cookies enabled", level, /^1$/));
170
+ return results;
171
+ }
172
+ async function cisLoggingChecks(level) {
173
+ const results = [];
174
+ // CIS 4.1.1.1 - auditd active
175
+ results.push(await runCisCheck("systemctl", ["is-active", "auditd"], "CIS-4.1.1.1", "auditd service is active", level, /^active$/));
176
+ // CIS 4.2.1.1 - rsyslog active
177
+ results.push(await runCisCheck("systemctl", ["is-active", "rsyslog"], "CIS-4.2.1.1", "rsyslog service is active", level, /^active$/));
178
+ // Check log file permissions (distro-aware path)
179
+ const daComp = await getDistroAdapter();
180
+ results.push(await runCisCheck("stat", ["-c", "%a", daComp.paths.syslog], "CIS-4.2.4", "Syslog file has restrictive permissions", level, /^(640|600)$/));
181
+ // CIS 4.1.1.3 - GRUB audit param (audit=1 in kernel cmdline)
182
+ results.push(await runCisCheck("grep", ["audit=1", "/proc/cmdline"], "CIS-4.1.1.3", "Kernel boot parameter audit=1 is set", level));
183
+ return results;
184
+ }
185
+ async function cisAccessChecks(level) {
186
+ const results = [];
187
+ // CIS 5.2.10 - SSH PermitRootLogin
188
+ results.push(await runCisCheck("grep", ["-i", "^PermitRootLogin", "/etc/ssh/sshd_config"], "CIS-5.2.10", "SSH root login disabled", level, /PermitRootLogin\s+no/i));
189
+ // CIS 5.2.4 - SSH Protocol 2
190
+ // GAP-20 FIX: Modern OpenSSH (>= 7.6) removed the Protocol directive and
191
+ // always uses Protocol 2. Instead of grepping for a deprecated directive,
192
+ // verify OpenSSH version >= 7.6 and mark as pass.
193
+ {
194
+ let sshStatus = "pass";
195
+ let sshDetail = "Protocol 2 is enforced by default in OpenSSH 7.6+; directive deprecated";
196
+ try {
197
+ const sshVersionResult = await executeCommand({
198
+ command: "ssh",
199
+ args: ["-V"],
200
+ timeout: 10_000,
201
+ });
202
+ // ssh -V outputs to stderr typically
203
+ const versionOutput = (sshVersionResult.stderr + " " + sshVersionResult.stdout).trim();
204
+ const versionMatch = versionOutput.match(/OpenSSH[_\s](\d+)\.(\d+)/i);
205
+ if (versionMatch) {
206
+ const major = parseInt(versionMatch[1], 10);
207
+ const minor = parseInt(versionMatch[2], 10);
208
+ if (major > 7 || (major === 7 && minor >= 6)) {
209
+ sshStatus = "pass";
210
+ sshDetail = `PASS (deprecated — Protocol 2 enforced by default in OpenSSH ${major}.${minor})`;
211
+ }
212
+ else {
213
+ // Old OpenSSH — need the explicit directive
214
+ const protoCheck = await executeCommand({
215
+ command: "grep",
216
+ args: ["-i", "^Protocol", "/etc/ssh/sshd_config"],
217
+ timeout: 10_000,
218
+ });
219
+ if (/Protocol\s+2/i.test(protoCheck.stdout.trim())) {
220
+ sshStatus = "pass";
221
+ sshDetail = `Protocol 2 explicitly configured (OpenSSH ${major}.${minor})`;
222
+ }
223
+ else {
224
+ sshStatus = "fail";
225
+ sshDetail = `OpenSSH ${major}.${minor} requires explicit 'Protocol 2' in sshd_config`;
226
+ }
227
+ }
228
+ }
229
+ else {
230
+ sshStatus = "warn";
231
+ sshDetail = `Could not parse OpenSSH version from: ${versionOutput.slice(0, 100)}`;
232
+ }
233
+ }
234
+ catch {
235
+ sshStatus = "error";
236
+ sshDetail = "Failed to determine SSH version";
237
+ }
238
+ results.push({
239
+ id: "CIS-5.2.4",
240
+ title: "SSH uses Protocol 2",
241
+ status: sshStatus,
242
+ detail: sshDetail,
243
+ level,
244
+ });
245
+ }
246
+ // CIS 6.1.2 - /etc/passwd permissions
247
+ results.push(await runCisCheck("stat", ["-c", "%a", "/etc/passwd"], "CIS-6.1.2", "/etc/passwd permissions (644)", level, /^644$/));
248
+ // CIS 6.1.3 - /etc/shadow permissions
249
+ results.push(await runCisCheck("stat", ["-c", "%a", "/etc/shadow"], "CIS-6.1.3", "/etc/shadow permissions (640 or 600)", level, /^(0|600|640)$/));
250
+ // CIS 5.4.1 - PAM password quality (minlen >= 14)
251
+ results.push(await runCisCheck("grep", ["-E", "^minlen", "/etc/security/pwquality.conf"], "CIS-5.4.1", "PAM password quality - minlen >= 14", level, /minlen\s*=\s*(1[4-9]|[2-9]\d|\d{3,})/));
252
+ // CIS 5.4.2 - PAM account lockout (pam_faillock configured)
253
+ results.push(await runCisCheck("grep", ["pam_faillock", (await getDistroAdapter()).paths.pamAuth], "CIS-5.4.2", "PAM account lockout (pam_faillock) is configured", level));
254
+ // CIS 5.5.5 - Default umask 027 or more restrictive
255
+ // Check login.defs, /etc/profile, and /etc/bash.bashrc for umask 027 or 077
256
+ {
257
+ const umaskCheck = await executeCommand({
258
+ command: "sudo",
259
+ args: ["grep", "-rEh", "(^UMASK\\s+0[2-7]7|^umask\\s+0[2-7]7)", "/etc/login.defs", "/etc/profile", "/etc/bash.bashrc"],
260
+ timeout: 10_000,
261
+ });
262
+ const hasRestrictive = /0[2-7]7/.test(umaskCheck.stdout.trim());
263
+ results.push({
264
+ id: "CIS-5.5.5",
265
+ title: "Default umask is 027 or more restrictive",
266
+ status: hasRestrictive ? "pass" : "fail",
267
+ detail: hasRestrictive
268
+ ? "Check passed"
269
+ : `Restrictive umask not found in login.defs, profile, or bash.bashrc`,
270
+ level,
271
+ });
272
+ }
273
+ // CIS 5.1.8 - /etc/cron.allow exists
274
+ {
275
+ const cronCheck = await executeCommand({
276
+ command: "sudo", args: ["test", "-f", "/etc/cron.allow"], timeout: 10_000,
277
+ });
278
+ results.push({
279
+ id: "CIS-5.1.8",
280
+ title: "/etc/cron.allow exists",
281
+ status: cronCheck.exitCode === 0 ? "pass" : "fail",
282
+ detail: cronCheck.exitCode === 0 ? "Check passed" : "/etc/cron.allow not found",
283
+ level,
284
+ });
285
+ }
286
+ // CIS 5.1.9 - /etc/at.allow exists
287
+ {
288
+ const atCheck = await executeCommand({
289
+ command: "sudo", args: ["test", "-f", "/etc/at.allow"], timeout: 10_000,
290
+ });
291
+ results.push({
292
+ id: "CIS-5.1.9",
293
+ title: "/etc/at.allow exists",
294
+ status: atCheck.exitCode === 0 ? "pass" : "fail",
295
+ detail: atCheck.exitCode === 0 ? "Check passed" : "/etc/at.allow not found",
296
+ level,
297
+ });
298
+ }
299
+ return results;
300
+ }
301
+ async function cisSystemChecks(level) {
302
+ const results = [];
303
+ // CIS 1.5.1 - Core dumps restricted
304
+ results.push(await runCisCheck("sysctl", ["-n", "fs.suid_dumpable"], "CIS-1.5.1", "Core dumps restricted for SUID programs", level, /^0$/));
305
+ // CIS 1.4.1 - ASLR
306
+ results.push(await runCisCheck("sysctl", ["-n", "kernel.randomize_va_space"], "CIS-1.6.2", "ASLR enabled", level, /^2$/));
307
+ // Check for login banner
308
+ const bannerResult = await executeCommand({
309
+ command: "cat",
310
+ args: ["/etc/issue"],
311
+ timeout: 10_000,
312
+ });
313
+ results.push({
314
+ id: "CIS-1.7.1",
315
+ title: "Login warning banner configured",
316
+ status: bannerResult.stdout.trim().length > 0 ? "pass" : "warn",
317
+ detail: bannerResult.stdout.trim().length > 0
318
+ ? "Login banner is configured"
319
+ : "No login banner found in /etc/issue",
320
+ level,
321
+ });
322
+ return results;
323
+ }
324
+ // ── Registration entry point ───────────────────────────────────────────────
325
+ export function registerComplianceTools(server) {
326
+ // ── 1. compliance_lynis_audit ───────────────────────────────────────
327
+ server.tool("compliance_lynis_audit", "Run Lynis security audit for comprehensive system hardening assessment", {
328
+ profile: z.string().optional().describe("Lynis profile file path"),
329
+ test_group: z
330
+ .string()
331
+ .optional()
332
+ .describe("Specific test group like 'firewall', 'ssh', 'kernel'"),
333
+ pentest: z
334
+ .boolean()
335
+ .optional()
336
+ .default(false)
337
+ .describe("Enable pentest mode for more aggressive checks"),
338
+ quick: z
339
+ .boolean()
340
+ .optional()
341
+ .default(false)
342
+ .describe("Run in quick mode (skip some long-running tests)"),
343
+ }, async ({ profile, test_group, pentest, quick }) => {
344
+ try {
345
+ const args = ["lynis", "audit", "system"];
346
+ if (profile) {
347
+ sanitizeArgs([profile]);
348
+ args.push("--profile", profile);
349
+ }
350
+ if (test_group) {
351
+ sanitizeArgs([test_group]);
352
+ args.push("--tests-from-group", test_group);
353
+ }
354
+ if (pentest) {
355
+ args.push("--pentest");
356
+ }
357
+ if (quick) {
358
+ args.push("--quick");
359
+ }
360
+ args.push("--no-colors");
361
+ sanitizeArgs(args);
362
+ const result = await executeCommand({
363
+ command: "sudo",
364
+ args,
365
+ toolName: "compliance_lynis_audit",
366
+ timeout: getToolTimeout("lynis"),
367
+ });
368
+ // Lynis may exit with non-zero for findings, which is normal
369
+ const findings = parseLynisOutput(result.stdout);
370
+ // Extract hardening index
371
+ const hardeningMatch = result.stdout.match(/Hardening index\s*:\s*(\d+)/);
372
+ const hardeningIndex = hardeningMatch ? parseInt(hardeningMatch[1], 10) : null;
373
+ const warnings = findings.filter((f) => f.severity === "warning");
374
+ const suggestions = findings.filter((f) => f.severity === "suggestion");
375
+ const output = {
376
+ hardeningIndex,
377
+ totalFindings: findings.length,
378
+ warnings: warnings.length,
379
+ suggestions: suggestions.length,
380
+ warningList: warnings,
381
+ suggestionList: suggestions,
382
+ raw: result.stdout,
383
+ };
384
+ return { content: [formatToolOutput(output)] };
385
+ }
386
+ catch (err) {
387
+ const msg = err instanceof Error ? err.message : String(err);
388
+ return { content: [createErrorContent(msg)], isError: true };
389
+ }
390
+ });
391
+ // ── 2. compliance_oscap_scan ────────────────────────────────────────
392
+ server.tool("compliance_oscap_scan", "Run OpenSCAP compliance scan against XCCDF security profiles", {
393
+ profile: z
394
+ .string()
395
+ .optional()
396
+ .default("xccdf_org.ssgproject.content_profile_standard")
397
+ .describe("XCCDF profile ID for the scan"),
398
+ content: z
399
+ .string()
400
+ .optional()
401
+ .describe("Path to SCAP content DS file (auto-detected if omitted)"),
402
+ results_file: z
403
+ .string()
404
+ .optional()
405
+ .describe("Path to save XML results"),
406
+ report_file: z
407
+ .string()
408
+ .optional()
409
+ .describe("Path to save HTML report"),
410
+ }, async ({ profile, content, results_file, report_file }) => {
411
+ try {
412
+ // Auto-detect SCAP content if not provided
413
+ let contentPath = content;
414
+ if (!contentPath) {
415
+ const candidates = [
416
+ "/usr/share/xml/scap/ssg/content/ssg-debian12-ds.xml",
417
+ "/usr/share/xml/scap/ssg/content/ssg-debian11-ds.xml",
418
+ "/usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml",
419
+ "/usr/share/xml/scap/ssg/content/ssg-ubuntu2004-ds.xml",
420
+ "/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml",
421
+ "/usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml",
422
+ ];
423
+ for (const candidate of candidates) {
424
+ const checkResult = await executeCommand({
425
+ command: "test",
426
+ args: ["-f", candidate],
427
+ timeout: 5_000,
428
+ });
429
+ if (checkResult.exitCode === 0) {
430
+ contentPath = candidate;
431
+ break;
432
+ }
433
+ }
434
+ if (!contentPath) {
435
+ return {
436
+ content: [createErrorContent("No SCAP content file found. Install scap-security-guide: sudo apt install ssg-debian\n" +
437
+ "Or specify the content path explicitly.")],
438
+ isError: true,
439
+ };
440
+ }
441
+ }
442
+ const args = ["oscap", "xccdf", "eval", "--profile", profile];
443
+ if (results_file) {
444
+ sanitizeArgs([results_file]);
445
+ args.push("--results", results_file);
446
+ }
447
+ if (report_file) {
448
+ sanitizeArgs([report_file]);
449
+ args.push("--report", report_file);
450
+ }
451
+ args.push(contentPath);
452
+ sanitizeArgs(args);
453
+ const result = await executeCommand({
454
+ command: "sudo",
455
+ args,
456
+ toolName: "compliance_oscap_scan",
457
+ timeout: getToolTimeout("oscap"),
458
+ });
459
+ // oscap exits with 2 for failures found (not actual error)
460
+ if (result.exitCode !== 0 && result.exitCode !== 2) {
461
+ return {
462
+ content: [createErrorContent(`oscap scan failed (exit ${result.exitCode}): ${result.stderr}`)],
463
+ isError: true,
464
+ };
465
+ }
466
+ const parsed = parseOscapOutput(result.stdout);
467
+ const passed = parsed.filter((r) => r.result === "pass").length;
468
+ const failed = parsed.filter((r) => r.result === "fail").length;
469
+ const notApplicable = parsed.filter((r) => r.result.includes("notapplicable")).length;
470
+ const output = {
471
+ profile,
472
+ contentFile: contentPath,
473
+ totalRules: parsed.length,
474
+ passed,
475
+ failed,
476
+ notApplicable,
477
+ compliancePercent: parsed.length > 0 ? Math.round((passed / parsed.length) * 100) : 0,
478
+ resultsFile: results_file ?? "not saved",
479
+ reportFile: report_file ?? "not saved",
480
+ failedRules: parsed.filter((r) => r.result === "fail"),
481
+ raw: result.stdout,
482
+ };
483
+ return { content: [formatToolOutput(output)] };
484
+ }
485
+ catch (err) {
486
+ const msg = err instanceof Error ? err.message : String(err);
487
+ return { content: [createErrorContent(msg)], isError: true };
488
+ }
489
+ });
490
+ // ── 3. compliance_check (merged: cis + framework) ───────────────────
491
+ server.tool("compliance_check", "Run compliance checks: CIS benchmark checks or framework-specific checks (PCI-DSS, HIPAA, SOC2, ISO 27001, GDPR).", {
492
+ action: z.enum(["cis", "framework"]).describe("Action: cis=CIS benchmark checks, framework=compliance framework check"),
493
+ // cis params
494
+ section: z.enum(["filesystem", "services", "network", "logging", "access", "system", "all"]).optional().default("all").describe("CIS benchmark section to check (cis action)"),
495
+ level: z.enum(["1", "2"]).optional().default("1").describe("CIS benchmark level (cis action)"),
496
+ // framework params
497
+ framework: z.enum(["pci-dss-v4", "hipaa", "soc2", "iso27001", "gdpr"]).optional().describe("Compliance framework (framework action)"),
498
+ dryRun: z.boolean().optional().default(true).describe("Preview only (framework action)"),
499
+ }, async (params) => {
500
+ const { action } = params;
501
+ switch (action) {
502
+ case "cis": {
503
+ const { section, level } = params;
504
+ try {
505
+ let results = [];
506
+ const sections = section === "all"
507
+ ? ["filesystem", "services", "network", "logging", "access", "system"]
508
+ : [section];
509
+ for (const sec of sections) {
510
+ switch (sec) {
511
+ case "filesystem":
512
+ results = results.concat(await cisFilesystemChecks(level));
513
+ break;
514
+ case "services":
515
+ results = results.concat(await cisServicesChecks(level));
516
+ break;
517
+ case "network":
518
+ results = results.concat(await cisNetworkChecks(level));
519
+ break;
520
+ case "logging":
521
+ results = results.concat(await cisLoggingChecks(level));
522
+ break;
523
+ case "access":
524
+ results = results.concat(await cisAccessChecks(level));
525
+ break;
526
+ case "system":
527
+ results = results.concat(await cisSystemChecks(level));
528
+ break;
529
+ }
530
+ }
531
+ const passCount = results.filter((r) => r.status === "pass").length;
532
+ const failCount = results.filter((r) => r.status === "fail").length;
533
+ const warnCount = results.filter((r) => r.status === "warn").length;
534
+ const errorCount = results.filter((r) => r.status === "error").length;
535
+ const output = {
536
+ cisLevel: level,
537
+ sections: sections,
538
+ totalChecks: results.length,
539
+ summary: { pass: passCount, fail: failCount, warn: warnCount, error: errorCount },
540
+ compliancePercent: results.length > 0 ? Math.round((passCount / results.length) * 100) : 0,
541
+ results,
542
+ };
543
+ return { content: [formatToolOutput(output)] };
544
+ }
545
+ catch (err) {
546
+ const msg = err instanceof Error ? err.message : String(err);
547
+ return { content: [createErrorContent(msg)], isError: true };
548
+ }
549
+ }
550
+ case "framework": {
551
+ const { framework, dryRun } = params;
552
+ try {
553
+ if (!framework) {
554
+ return { content: [createErrorContent("framework is required for framework action")], isError: true };
555
+ }
556
+ async function runSysctlCheckLocal(key, expected) {
557
+ const r = await executeCommand({ command: "sysctl", args: ["-n", key], timeout: 5000 });
558
+ const actual = r.stdout.trim();
559
+ return { passed: actual === expected, detail: `${key} = ${actual} (expected: ${expected})` };
560
+ }
561
+ async function serviceInactiveLocal(svc) {
562
+ const r = await executeCommand({ command: "systemctl", args: ["is-active", svc], timeout: 5000 });
563
+ const active = r.stdout.trim() === "active";
564
+ return { passed: !active, detail: `${svc}: ${r.stdout.trim()}` };
565
+ }
566
+ function filePermCheckLocal(filePath, maxPerm) {
567
+ try {
568
+ const { statSync } = require("node:fs");
569
+ const stat = statSync(filePath);
570
+ const mode = (stat.mode & 0o777).toString(8);
571
+ return { passed: parseInt(mode, 8) <= parseInt(maxPerm, 8), detail: `${filePath}: ${mode} (max: ${maxPerm})` };
572
+ }
573
+ catch {
574
+ return { passed: false, detail: `${filePath}: unable to check` };
575
+ }
576
+ }
577
+ function getFrameworkChecks(fw) {
578
+ const commonChecks = [
579
+ { id: "AUTH-001", description: "Ensure no empty passwords in /etc/shadow", check: async () => { const r = await executeCommand({ command: "awk", args: ["-F:", '($2 == "" ) { print $1 }', "/etc/shadow"], timeout: 5000 }); return { passed: r.stdout.trim().length === 0, detail: r.stdout.trim() || "No empty passwords" }; } },
580
+ { id: "NET-001", description: "IP forwarding disabled", check: async () => runSysctlCheckLocal("net.ipv4.ip_forward", "0") },
581
+ { id: "NET-002", description: "SYN cookies enabled", check: async () => runSysctlCheckLocal("net.ipv4.tcp_syncookies", "1") },
582
+ { id: "KERN-001", description: "ASLR fully enabled", check: async () => runSysctlCheckLocal("kernel.randomize_va_space", "2") },
583
+ { id: "KERN-002", description: "dmesg access restricted", check: async () => runSysctlCheckLocal("kernel.dmesg_restrict", "1") },
584
+ { id: "FS-001", description: "/etc/passwd permissions ≤ 644", check: async () => filePermCheckLocal("/etc/passwd", "644") },
585
+ { id: "FS-002", description: "/etc/shadow permissions ≤ 640", check: async () => filePermCheckLocal("/etc/shadow", "640") },
586
+ { id: "SVC-001", description: "Telnet service disabled", check: async () => serviceInactiveLocal("telnet.socket") },
587
+ { id: "SSH-001", description: "SSH root login disabled", check: async () => { try {
588
+ const content = readFileSync("/etc/ssh/sshd_config", "utf-8");
589
+ const match = content.match(/^\s*PermitRootLogin\s+(\S+)/m);
590
+ const value = match?.[1] ?? "not set";
591
+ return { passed: value === "no" || value === "prohibit-password", detail: `PermitRootLogin: ${value}` };
592
+ }
593
+ catch {
594
+ return { passed: false, detail: "Unable to read sshd_config" };
595
+ } } },
596
+ ];
597
+ const frameworkSpecific = {
598
+ "pci-dss-v4": [
599
+ { id: "PCI-1.1", description: "Firewall rules present", check: async () => { const r = await executeCommand({ command: "iptables", args: ["-L", "-n"], timeout: 10000 }); const hasRules = r.stdout.split("\n").length > 8; return { passed: hasRules, detail: `${r.stdout.split("\n").length} iptables lines` }; } },
600
+ { id: "PCI-8.2", description: "Password minimum length configured", check: async () => { try {
601
+ const content = readFileSync("/etc/security/pwquality.conf", "utf-8");
602
+ const match = content.match(/minlen\s*=\s*(\d+)/);
603
+ const len = match ? parseInt(match[1]) : 0;
604
+ return { passed: len >= 12, detail: `minlen = ${len} (required: ≥12)` };
605
+ }
606
+ catch {
607
+ return { passed: false, detail: "pwquality.conf not found" };
608
+ } } },
609
+ ],
610
+ hipaa: [
611
+ { id: "HIPAA-164.312a", description: "Audit logging enabled (auditd)", check: async () => { const r = await executeCommand({ command: "systemctl", args: ["is-active", "auditd"], timeout: 5000 }); return { passed: r.stdout.trim() === "active", detail: `auditd: ${r.stdout.trim()}` }; } },
612
+ ],
613
+ soc2: [
614
+ { id: "SOC2-CC6.1", description: "System monitoring enabled", check: async () => { const r = await executeCommand({ command: "systemctl", args: ["is-active", "auditd"], timeout: 5000 }); return { passed: r.stdout.trim() === "active", detail: `auditd: ${r.stdout.trim()}` }; } },
615
+ ],
616
+ iso27001: [
617
+ { id: "ISO-A.12.4.1", description: "Event logging active", check: async () => { const rsyslog = await executeCommand({ command: "systemctl", args: ["is-active", "rsyslog"], timeout: 5000 }); const journald = await executeCommand({ command: "systemctl", args: ["is-active", "systemd-journald"], timeout: 5000 }); const active = rsyslog.stdout.trim() === "active" || journald.stdout.trim() === "active"; return { passed: active, detail: `rsyslog: ${rsyslog.stdout.trim()}, journald: ${journald.stdout.trim()}` }; } },
618
+ ],
619
+ gdpr: [
620
+ { id: "GDPR-Art32", description: "Encryption capabilities available", check: async () => { const r = await executeCommand({ command: "which", args: ["openssl"], timeout: 5000 }); return { passed: r.exitCode === 0, detail: r.exitCode === 0 ? "openssl available" : "openssl not found" }; } },
621
+ ],
622
+ };
623
+ return [...commonChecks, ...(frameworkSpecific[fw] ?? [])];
624
+ }
625
+ if (dryRun) {
626
+ const checks = getFrameworkChecks(framework);
627
+ return {
628
+ content: [formatToolOutput({
629
+ dryRun: true,
630
+ framework,
631
+ checksCount: checks.length,
632
+ checkIds: checks.map((c) => ({ id: c.id, description: c.description })),
633
+ })],
634
+ };
635
+ }
636
+ const checks = getFrameworkChecks(framework);
637
+ const results = [];
638
+ for (const check of checks) {
639
+ try {
640
+ const result = await check.check();
641
+ results.push({ id: check.id, description: check.description, ...result });
642
+ }
643
+ catch (err) {
644
+ results.push({ id: check.id, description: check.description, passed: false, detail: `Check error: ${err instanceof Error ? err.message : String(err)}` });
645
+ }
646
+ }
647
+ const passed = results.filter((r) => r.passed).length;
648
+ const failed = results.filter((r) => !r.passed).length;
649
+ const score = Math.round((passed / results.length) * 100);
650
+ return {
651
+ content: [formatToolOutput({
652
+ framework,
653
+ totalChecks: results.length,
654
+ passed,
655
+ failed,
656
+ score,
657
+ rating: score >= 80 ? "COMPLIANT" : score >= 60 ? "PARTIALLY_COMPLIANT" : "NON_COMPLIANT",
658
+ results,
659
+ })],
660
+ };
661
+ }
662
+ catch (err) {
663
+ const msg = err instanceof Error ? err.message : String(err);
664
+ return { content: [createErrorContent(`Compliance check failed: ${msg}`)], isError: true };
665
+ }
666
+ }
667
+ default:
668
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
669
+ }
670
+ });
671
+ // ── 4. compliance_policy_evaluate ───────────────────────────────────
672
+ server.tool("compliance_policy_evaluate", "Evaluate a compliance policy set (built-in or custom) against the current system", {
673
+ policy_name: z
674
+ .string()
675
+ .optional()
676
+ .describe("Built-in policy name (use without policy_path)"),
677
+ policy_path: z
678
+ .string()
679
+ .optional()
680
+ .describe("Path to custom policy JSON file"),
681
+ }, async ({ policy_name, policy_path }) => {
682
+ try {
683
+ if (!policy_name && !policy_path) {
684
+ // List available policies
685
+ const builtins = getBuiltinPolicies();
686
+ return {
687
+ content: [createTextContent(`No policy specified. Available built-in policies:\n${builtins.length > 0 ? builtins.map((p) => ` - ${p}`).join("\n") : " (none found)"}\n\nProvide policy_name or policy_path to evaluate.`)],
688
+ };
689
+ }
690
+ let policyFilePath;
691
+ if (policy_path) {
692
+ sanitizeArgs([policy_path]);
693
+ policyFilePath = policy_path;
694
+ }
695
+ else {
696
+ const config = getConfig();
697
+ policyFilePath = `${config.policyDir}/${policy_name}.json`;
698
+ }
699
+ const policySet = loadPolicy(policyFilePath);
700
+ const evaluation = await evaluatePolicy(policySet);
701
+ const output = {
702
+ policyName: evaluation.policyName,
703
+ totalRules: evaluation.totalRules,
704
+ passed: evaluation.passed,
705
+ failed: evaluation.failed,
706
+ errors: evaluation.errors,
707
+ compliancePercent: evaluation.compliancePercent,
708
+ results: evaluation.results.map((r) => ({
709
+ id: r.rule.id,
710
+ title: r.rule.title,
711
+ severity: r.rule.severity,
712
+ passed: r.passed,
713
+ message: r.message,
714
+ actual: r.actual.slice(0, 200),
715
+ })),
716
+ };
717
+ return { content: [formatToolOutput(output)] };
718
+ }
719
+ catch (err) {
720
+ const msg = err instanceof Error ? err.message : String(err);
721
+ return { content: [createErrorContent(msg)], isError: true };
722
+ }
723
+ });
724
+ // ── 5. compliance_report ────────────────────────────────────────────
725
+ server.tool("compliance_report", "Generate a comprehensive compliance summary report combining multiple check sources", {
726
+ format: z
727
+ .enum(["text", "json", "markdown"])
728
+ .optional()
729
+ .default("text")
730
+ .describe("Output format for the report"),
731
+ include_lynis: z
732
+ .boolean()
733
+ .optional()
734
+ .default(true)
735
+ .describe("Include Lynis quick scan results"),
736
+ include_cis: z
737
+ .boolean()
738
+ .optional()
739
+ .default(true)
740
+ .describe("Include CIS benchmark check results"),
741
+ include_policy: z
742
+ .boolean()
743
+ .optional()
744
+ .default(false)
745
+ .describe("Include custom policy evaluation results"),
746
+ policy_name: z
747
+ .string()
748
+ .optional()
749
+ .describe("Policy name to include (required if include_policy is true)"),
750
+ }, async ({ format, include_lynis, include_cis, include_policy, policy_name }) => {
751
+ try {
752
+ const report = {
753
+ timestamp: new Date().toISOString(),
754
+ overallScore: 0,
755
+ sections: [],
756
+ };
757
+ let totalScore = 0;
758
+ let sectionCount = 0;
759
+ // ── Lynis section ──
760
+ if (include_lynis) {
761
+ try {
762
+ const lynisResult = await executeCommand({
763
+ command: "sudo",
764
+ args: ["lynis", "audit", "system", "--quick", "--no-colors"],
765
+ toolName: "compliance_report",
766
+ timeout: getToolTimeout("lynis"),
767
+ });
768
+ const findings = parseLynisOutput(lynisResult.stdout);
769
+ const hardeningMatch = lynisResult.stdout.match(/Hardening index\s*:\s*(\d+)/);
770
+ const hardeningIndex = hardeningMatch ? parseInt(hardeningMatch[1], 10) : 0;
771
+ report.sections.push({
772
+ name: "Lynis Security Audit",
773
+ score: hardeningIndex,
774
+ details: {
775
+ hardeningIndex,
776
+ warnings: findings.filter((f) => f.severity === "warning").length,
777
+ suggestions: findings.filter((f) => f.severity === "suggestion").length,
778
+ topWarnings: findings.filter((f) => f.severity === "warning").slice(0, 5),
779
+ },
780
+ });
781
+ totalScore += hardeningIndex;
782
+ sectionCount++;
783
+ }
784
+ catch {
785
+ report.sections.push({
786
+ name: "Lynis Security Audit",
787
+ score: 0,
788
+ details: { error: "Lynis not available or failed to run. Install with: sudo apt install lynis" },
789
+ });
790
+ }
791
+ }
792
+ // ── CIS section ──
793
+ if (include_cis) {
794
+ try {
795
+ let cisResults = [];
796
+ cisResults = cisResults.concat(await cisFilesystemChecks("1"));
797
+ cisResults = cisResults.concat(await cisNetworkChecks("1"));
798
+ cisResults = cisResults.concat(await cisLoggingChecks("1"));
799
+ cisResults = cisResults.concat(await cisAccessChecks("1"));
800
+ cisResults = cisResults.concat(await cisSystemChecks("1"));
801
+ cisResults = cisResults.concat(await cisServicesChecks("1"));
802
+ const passCount = cisResults.filter((r) => r.status === "pass").length;
803
+ const cisScore = cisResults.length > 0
804
+ ? Math.round((passCount / cisResults.length) * 100)
805
+ : 0;
806
+ report.sections.push({
807
+ name: "CIS Benchmark Checks",
808
+ score: cisScore,
809
+ details: {
810
+ totalChecks: cisResults.length,
811
+ passed: passCount,
812
+ failed: cisResults.filter((r) => r.status === "fail").length,
813
+ warned: cisResults.filter((r) => r.status === "warn").length,
814
+ errors: cisResults.filter((r) => r.status === "error").length,
815
+ failedChecks: cisResults.filter((r) => r.status === "fail"),
816
+ },
817
+ });
818
+ totalScore += cisScore;
819
+ sectionCount++;
820
+ }
821
+ catch {
822
+ report.sections.push({
823
+ name: "CIS Benchmark Checks",
824
+ score: 0,
825
+ details: { error: "CIS checks failed to run" },
826
+ });
827
+ }
828
+ }
829
+ // ── Policy section ──
830
+ if (include_policy && policy_name) {
831
+ try {
832
+ const config = getConfig();
833
+ const policyPath = `${config.policyDir}/${policy_name}.json`;
834
+ const policySet = loadPolicy(policyPath);
835
+ const evaluation = await evaluatePolicy(policySet);
836
+ report.sections.push({
837
+ name: `Policy: ${evaluation.policyName}`,
838
+ score: evaluation.compliancePercent,
839
+ details: {
840
+ totalRules: evaluation.totalRules,
841
+ passed: evaluation.passed,
842
+ failed: evaluation.failed,
843
+ errors: evaluation.errors,
844
+ failedRules: evaluation.results
845
+ .filter((r) => !r.passed)
846
+ .map((r) => ({
847
+ id: r.rule.id,
848
+ title: r.rule.title,
849
+ severity: r.rule.severity,
850
+ message: r.message,
851
+ })),
852
+ },
853
+ });
854
+ totalScore += evaluation.compliancePercent;
855
+ sectionCount++;
856
+ }
857
+ catch {
858
+ report.sections.push({
859
+ name: `Policy: ${policy_name}`,
860
+ score: 0,
861
+ details: { error: `Policy '${policy_name}' not found or failed to evaluate` },
862
+ });
863
+ }
864
+ }
865
+ // Calculate overall score
866
+ report.overallScore = sectionCount > 0
867
+ ? Math.round(totalScore / sectionCount)
868
+ : 0;
869
+ // Format output
870
+ if (format === "json") {
871
+ return { content: [formatToolOutput(report)] };
872
+ }
873
+ if (format === "markdown") {
874
+ let md = `# Compliance Report\n\n`;
875
+ md += `**Generated:** ${report.timestamp}\n`;
876
+ md += `**Overall Score:** ${report.overallScore}/100\n\n`;
877
+ for (const section of report.sections) {
878
+ md += `## ${section.name}\n\n`;
879
+ md += `**Score:** ${section.score}/100\n\n`;
880
+ md += `\`\`\`json\n${JSON.stringify(section.details, null, 2)}\n\`\`\`\n\n`;
881
+ }
882
+ return { content: [createTextContent(md)] };
883
+ }
884
+ // Text format
885
+ let text = `${"=".repeat(60)}\n`;
886
+ text += ` COMPLIANCE REPORT\n`;
887
+ text += ` Generated: ${report.timestamp}\n`;
888
+ text += ` Overall Score: ${report.overallScore}/100\n`;
889
+ text += `${"=".repeat(60)}\n\n`;
890
+ for (const section of report.sections) {
891
+ text += `${"─".repeat(50)}\n`;
892
+ text += ` ${section.name} — Score: ${section.score}/100\n`;
893
+ text += `${"─".repeat(50)}\n`;
894
+ text += `${JSON.stringify(section.details, null, 2)}\n\n`;
895
+ }
896
+ return { content: [createTextContent(text)] };
897
+ }
898
+ catch (err) {
899
+ const msg = err instanceof Error ? err.message : String(err);
900
+ return { content: [createErrorContent(msg)], isError: true };
901
+ }
902
+ });
903
+ // ── 6. compliance_cron_restrict ─────────────────────────────────────
904
+ // GAP-21: Tool to create/manage cron.allow and at.allow files
905
+ server.tool("compliance_cron_restrict", "Create and manage /etc/cron.allow and /etc/at.allow to restrict cron/at access (CIS 5.1.8, 5.1.9)", {
906
+ action: z
907
+ .enum(["create_allow_files", "status"])
908
+ .describe("Action: create_allow_files to create allow lists, status to check current state"),
909
+ allowed_users: z
910
+ .array(z.string())
911
+ .optional()
912
+ .default(["root"])
913
+ .describe("Users to include in cron.allow and at.allow (default: ['root'])"),
914
+ dry_run: z
915
+ .boolean()
916
+ .optional()
917
+ .default(true)
918
+ .describe("Preview changes without applying them"),
919
+ }, async ({ action, allowed_users, dry_run }) => {
920
+ try {
921
+ const usernamePattern = /^[a-z_][a-z0-9_-]{0,31}$/;
922
+ const changes = [];
923
+ if (action === "status") {
924
+ // Check existence and contents of all 4 files
925
+ const files = ["/etc/cron.allow", "/etc/cron.deny", "/etc/at.allow", "/etc/at.deny"];
926
+ const statusResults = [];
927
+ for (const f of files) {
928
+ const existCheck = await executeCommand({
929
+ command: "test",
930
+ args: ["-f", f],
931
+ timeout: 5_000,
932
+ });
933
+ let contents = "";
934
+ if (existCheck.exitCode === 0) {
935
+ const catResult = await executeCommand({
936
+ command: "sudo",
937
+ args: ["cat", f],
938
+ timeout: 5_000,
939
+ });
940
+ contents = catResult.stdout.trim();
941
+ }
942
+ statusResults.push({
943
+ file: f,
944
+ exists: existCheck.exitCode === 0,
945
+ contents,
946
+ });
947
+ }
948
+ const output = {
949
+ action: "status",
950
+ files: statusResults,
951
+ recommendation: "cron.allow and at.allow should exist with only authorized users. cron.deny and at.deny should be removed when allow files are present.",
952
+ };
953
+ return { content: [formatToolOutput(output)] };
954
+ }
955
+ // action === "create_allow_files"
956
+ // Validate usernames
957
+ for (const user of allowed_users) {
958
+ if (!usernamePattern.test(user)) {
959
+ return {
960
+ content: [createErrorContent(`Invalid username '${user}': must match ${usernamePattern}`)],
961
+ isError: true,
962
+ };
963
+ }
964
+ }
965
+ const userListContent = allowed_users.join("\\n");
966
+ if (dry_run) {
967
+ const output = {
968
+ action: "create_allow_files",
969
+ dry_run: true,
970
+ planned_changes: [
971
+ `Create /etc/cron.allow with users: ${allowed_users.join(", ")}`,
972
+ `Create /etc/at.allow with users: ${allowed_users.join(", ")}`,
973
+ "Set permissions 600 on both files",
974
+ "Set ownership root:root on both files",
975
+ "Remove /etc/cron.deny if it exists",
976
+ "Remove /etc/at.deny if it exists",
977
+ ],
978
+ allowed_users,
979
+ };
980
+ return { content: [formatToolOutput(output)] };
981
+ }
982
+ // Create /etc/cron.allow
983
+ await executeCommand({
984
+ command: "sudo",
985
+ args: ["tee", "/etc/cron.allow"],
986
+ stdin: allowed_users.join("\n") + "\n",
987
+ timeout: 10_000,
988
+ });
989
+ changes.push("Created /etc/cron.allow");
990
+ // Create /etc/at.allow
991
+ await executeCommand({
992
+ command: "sudo",
993
+ args: ["tee", "/etc/at.allow"],
994
+ stdin: allowed_users.join("\n") + "\n",
995
+ timeout: 10_000,
996
+ });
997
+ changes.push("Created /etc/at.allow");
998
+ // Set permissions
999
+ await executeCommand({
1000
+ command: "sudo",
1001
+ args: ["chmod", "600", "/etc/cron.allow", "/etc/at.allow"],
1002
+ timeout: 10_000,
1003
+ });
1004
+ changes.push("Set permissions 600 on /etc/cron.allow and /etc/at.allow");
1005
+ // Set ownership
1006
+ await executeCommand({
1007
+ command: "sudo",
1008
+ args: ["chown", "root:root", "/etc/cron.allow", "/etc/at.allow"],
1009
+ timeout: 10_000,
1010
+ });
1011
+ changes.push("Set ownership root:root on /etc/cron.allow and /etc/at.allow");
1012
+ // Remove deny files if they exist
1013
+ const cronDenyExists = await executeCommand({
1014
+ command: "test",
1015
+ args: ["-f", "/etc/cron.deny"],
1016
+ timeout: 5_000,
1017
+ });
1018
+ if (cronDenyExists.exitCode === 0) {
1019
+ await executeCommand({
1020
+ command: "sudo",
1021
+ args: ["rm", "-f", "/etc/cron.deny"],
1022
+ timeout: 10_000,
1023
+ });
1024
+ changes.push("Removed /etc/cron.deny (allow-list supersedes)");
1025
+ }
1026
+ const atDenyExists = await executeCommand({
1027
+ command: "test",
1028
+ args: ["-f", "/etc/at.deny"],
1029
+ timeout: 5_000,
1030
+ });
1031
+ if (atDenyExists.exitCode === 0) {
1032
+ await executeCommand({
1033
+ command: "sudo",
1034
+ args: ["rm", "-f", "/etc/at.deny"],
1035
+ timeout: 10_000,
1036
+ });
1037
+ changes.push("Removed /etc/at.deny (allow-list supersedes)");
1038
+ }
1039
+ // Log changes
1040
+ logChange(createChangeEntry({
1041
+ tool: "compliance_cron_restrict",
1042
+ action: "create_allow_files",
1043
+ target: "/etc/cron.allow, /etc/at.allow",
1044
+ after: changes.join("; "),
1045
+ dryRun: false,
1046
+ success: true,
1047
+ }));
1048
+ const output = {
1049
+ action: "create_allow_files",
1050
+ dry_run: false,
1051
+ allowed_users,
1052
+ changes,
1053
+ cis_checks_addressed: ["CIS-5.1.8 (cron.allow)", "CIS-5.1.9 (at.allow)"],
1054
+ };
1055
+ return { content: [formatToolOutput(output)] };
1056
+ }
1057
+ catch (err) {
1058
+ const msg = err instanceof Error ? err.message : String(err);
1059
+ return { content: [createErrorContent(msg)], isError: true };
1060
+ }
1061
+ });
1062
+ // ── 7. compliance_tmp_hardening ─────────────────────────────────────
1063
+ // GAP-31: Tool to audit and apply /tmp mount hardening (CIS 1.1.4)
1064
+ // GAP-31: Tool to audit and apply /tmp mount hardening (CIS 1.1.4)
1065
+ server.tool("compliance_tmp_hardening", "Audit and apply /tmp mount hardening with nodev,nosuid,noexec options (CIS 1.1.4)", {
1066
+ action: z
1067
+ .enum(["audit", "apply"])
1068
+ .describe("Action: audit to check current /tmp mount options, apply to harden"),
1069
+ mount_options: z
1070
+ .string()
1071
+ .optional()
1072
+ .default("nodev,nosuid,noexec")
1073
+ .describe("Mount options to apply (default: 'nodev,nosuid,noexec')"),
1074
+ dry_run: z
1075
+ .boolean()
1076
+ .optional()
1077
+ .default(true)
1078
+ .describe("Preview changes without applying them"),
1079
+ }, async ({ action, mount_options, dry_run }) => {
1080
+ try {
1081
+ if (action === "audit") {
1082
+ // Check current mount options for /tmp
1083
+ const mountResult = await executeCommand({
1084
+ command: "findmnt",
1085
+ args: ["-n", "-o", "SOURCE,TARGET,FSTYPE,OPTIONS", "/tmp"],
1086
+ timeout: 10_000,
1087
+ });
1088
+ // Check /etc/fstab for /tmp entry
1089
+ const fstabResult = await executeCommand({
1090
+ command: "grep",
1091
+ args: ["/tmp", "/etc/fstab"],
1092
+ timeout: 10_000,
1093
+ });
1094
+ const currentOptions = mountResult.stdout.trim();
1095
+ const fstabEntry = fstabResult.stdout.trim();
1096
+ const hasNodev = /nodev/.test(currentOptions);
1097
+ const hasNosuid = /nosuid/.test(currentOptions);
1098
+ const hasNoexec = /noexec/.test(currentOptions);
1099
+ const output = {
1100
+ action: "audit",
1101
+ tmp_mounted: mountResult.exitCode === 0,
1102
+ current_mount_info: currentOptions || "Not mounted or not found",
1103
+ fstab_entry: fstabEntry || "No /tmp entry in /etc/fstab",
1104
+ options_present: {
1105
+ nodev: hasNodev,
1106
+ nosuid: hasNosuid,
1107
+ noexec: hasNoexec,
1108
+ },
1109
+ compliant: hasNodev && hasNosuid && hasNoexec,
1110
+ cis_check: "CIS-1.1.4",
1111
+ };
1112
+ return { content: [formatToolOutput(output)] };
1113
+ }
1114
+ // action === "apply"
1115
+ // Validate mount_options
1116
+ if (!/^[a-z,]+$/.test(mount_options)) {
1117
+ return {
1118
+ content: [createErrorContent(`Invalid mount_options '${mount_options}': must contain only lowercase letters and commas`)],
1119
+ isError: true,
1120
+ };
1121
+ }
1122
+ const changes = [];
1123
+ if (dry_run) {
1124
+ // Check if /tmp line exists in fstab
1125
+ const fstabCheck = await executeCommand({
1126
+ command: "grep",
1127
+ args: ["-c", "/tmp", "/etc/fstab"],
1128
+ timeout: 5_000,
1129
+ });
1130
+ const hasFstabEntry = fstabCheck.exitCode === 0 && parseInt(fstabCheck.stdout.trim(), 10) > 0;
1131
+ const output = {
1132
+ action: "apply",
1133
+ dry_run: true,
1134
+ planned_changes: [
1135
+ "Backup /etc/fstab to /etc/fstab.bak.compliance",
1136
+ hasFstabEntry
1137
+ ? `Update existing /tmp entry in /etc/fstab with options: defaults,${mount_options}`
1138
+ : `Add new /tmp entry to /etc/fstab: tmpfs /tmp tmpfs defaults,${mount_options} 0 0`,
1139
+ "Remount /tmp with new options",
1140
+ ],
1141
+ mount_options,
1142
+ };
1143
+ return { content: [formatToolOutput(output)] };
1144
+ }
1145
+ // Backup /etc/fstab
1146
+ await executeCommand({
1147
+ command: "sudo",
1148
+ args: ["cp", "-p", "/etc/fstab", "/etc/fstab.bak.compliance"],
1149
+ timeout: 10_000,
1150
+ });
1151
+ changes.push("Backed up /etc/fstab to /etc/fstab.bak.compliance");
1152
+ // Check if /tmp line exists in fstab
1153
+ const fstabCheck = await executeCommand({
1154
+ command: "grep",
1155
+ args: ["-c", "/tmp", "/etc/fstab"],
1156
+ timeout: 5_000,
1157
+ });
1158
+ const hasFstabEntry = fstabCheck.exitCode === 0 && parseInt(fstabCheck.stdout.trim(), 10) > 0;
1159
+ if (hasFstabEntry) {
1160
+ // Update existing /tmp entry — replace its options
1161
+ await executeCommand({
1162
+ command: "sudo",
1163
+ args: [
1164
+ "sed", "-i",
1165
+ `s|^\\([^#]*\\s\\+/tmp\\s\\+\\S\\+\\s\\+\\)\\S\\+|\\1defaults,${mount_options}|`,
1166
+ "/etc/fstab",
1167
+ ],
1168
+ timeout: 10_000,
1169
+ });
1170
+ changes.push(`Updated /tmp mount options in /etc/fstab to: defaults,${mount_options}`);
1171
+ }
1172
+ else {
1173
+ // Add new /tmp line
1174
+ const fstabLine = `tmpfs /tmp tmpfs defaults,${mount_options} 0 0`;
1175
+ await executeCommand({
1176
+ command: "sudo",
1177
+ args: ["tee", "-a", "/etc/fstab"],
1178
+ stdin: fstabLine + "\n",
1179
+ timeout: 10_000,
1180
+ });
1181
+ changes.push(`Added /tmp entry to /etc/fstab: ${fstabLine}`);
1182
+ }
1183
+ // Remount /tmp
1184
+ const remountResult = await executeCommand({
1185
+ command: "sudo",
1186
+ args: ["mount", "-o", "remount", "/tmp"],
1187
+ timeout: 15_000,
1188
+ });
1189
+ if (remountResult.exitCode === 0) {
1190
+ changes.push("Remounted /tmp with new options");
1191
+ }
1192
+ else {
1193
+ changes.push(`Warning: remount /tmp returned exit code ${remountResult.exitCode}: ${remountResult.stderr.trim().slice(0, 200)}`);
1194
+ }
1195
+ // Verify
1196
+ const verifyResult = await executeCommand({
1197
+ command: "findmnt",
1198
+ args: ["-n", "-o", "OPTIONS", "/tmp"],
1199
+ timeout: 10_000,
1200
+ });
1201
+ // Log changes
1202
+ logChange(createChangeEntry({
1203
+ tool: "compliance_tmp_hardening",
1204
+ action: "apply",
1205
+ target: "/tmp mount options, /etc/fstab",
1206
+ after: changes.join("; "),
1207
+ dryRun: false,
1208
+ success: true,
1209
+ }));
1210
+ const output = {
1211
+ action: "apply",
1212
+ dry_run: false,
1213
+ mount_options,
1214
+ changes,
1215
+ current_mount_options: verifyResult.stdout.trim(),
1216
+ cis_check: "CIS-1.1.4",
1217
+ };
1218
+ return { content: [formatToolOutput(output)] };
1219
+ }
1220
+ catch (err) {
1221
+ const msg = err instanceof Error ? err.message : String(err);
1222
+ return { content: [createErrorContent(msg)], isError: true };
1223
+ }
1224
+ });
1225
+ }