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,2278 @@
1
+ /**
2
+ * Meta/utility tools for Defense MCP Server.
3
+ *
4
+ * Registers 6 tools: defense_check_tools, defense_workflow (actions: suggest, run),
5
+ * defense_change_history, security_posture (actions: score, trend, dashboard),
6
+ * scheduled_audit (actions: create, list, remove, history),
7
+ * auto_remediate (actions: plan, apply, rollback_session, status).
8
+ */
9
+ import { z } from "zod";
10
+ import { executeCommand } from "../core/executor.js";
11
+ import { resolveCommand, isAllowlisted } from "../core/command-allowlist.js";
12
+ import { getConfig } from "../core/config.js";
13
+ import { createTextContent, createErrorContent, formatToolOutput, } from "../core/parsers.js";
14
+ import { logChange, createChangeEntry, getChangelog } from "../core/changelog.js";
15
+ import { checkAllTools, installMissing, } from "../core/installer.js";
16
+ import { SafeguardRegistry } from "../core/safeguards.js";
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { homedir } from "node:os";
20
+ import { spawnSafe } from "../core/spawn-safe.js";
21
+ import { secureWriteFileSync } from "../core/secure-fs.js";
22
+ // ── Security Posture Helpers ───────────────────────────────────────────────
23
+ const POSTURE_DIR = join(homedir(), ".kali-mcp-posture");
24
+ function ensurePostureDir() {
25
+ if (!existsSync(POSTURE_DIR)) {
26
+ mkdirSync(POSTURE_DIR, { recursive: true });
27
+ }
28
+ }
29
+ async function checkSysctl(key, expected) {
30
+ const r = await executeCommand({ command: "sysctl", args: ["-n", key], timeout: 5000 });
31
+ if (r.exitCode !== 0) {
32
+ return { passed: false, assessable: false, actual: r.stderr.trim() || "command failed" };
33
+ }
34
+ const actual = r.stdout.trim();
35
+ return { passed: actual === expected, assessable: true, actual };
36
+ }
37
+ // ── Automation Workflow Helpers ─────────────────────────────────────────────
38
+ const AUDIT_LOG_DIR = join(homedir(), ".kali-defense", "audit-logs");
39
+ function ensureAuditLogDir() {
40
+ if (!existsSync(AUDIT_LOG_DIR)) {
41
+ mkdirSync(AUDIT_LOG_DIR, { recursive: true });
42
+ }
43
+ }
44
+ // ── Scheduled Audit Command Allowlist (TOOL-004 remediation) ───────────────
45
+ /**
46
+ * Strict allowlist of audit commands permitted for scheduled execution.
47
+ * Maps human-readable command strings to their executable binary and arguments.
48
+ * Commands are validated against the command allowlist via resolveCommand().
49
+ * NO arbitrary commands are permitted — only these pre-approved security audits.
50
+ */
51
+ const ALLOWED_AUDIT_COMMANDS = {
52
+ "lynis audit system": { command: "lynis", args: ["audit", "system"] },
53
+ "rkhunter --check --skip-keypress": { command: "rkhunter", args: ["--check", "--skip-keypress"] },
54
+ "aide --check": { command: "aide", args: ["--check"] },
55
+ "clamscan -r /home": { command: "clamscan", args: ["-r", "/home"] },
56
+ "chkrootkit": { command: "chkrootkit", args: [] },
57
+ "freshclam": { command: "freshclam", args: [] },
58
+ "tiger": { command: "tiger", args: [] },
59
+ };
60
+ /** Valid characters for audit job names (used in file paths and systemd unit names) */
61
+ const AUDIT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
62
+ /**
63
+ * TOOL-004 remediation: Validate schedule format to prevent injection.
64
+ * Allows:
65
+ * - Cron format: 5 fields of [0-9*,/-] (e.g., "0 2 * * *")
66
+ * - Systemd calendar format: alphanumeric with *:-/, space, and common calendar specifiers
67
+ */
68
+ const CRON_FIELD_RE = /^[0-9*,\/-]+$/;
69
+ const SYSTEMD_CALENDAR_RE = /^[a-zA-Z0-9*:,\-\/. ]+$/;
70
+ function validateSchedule(schedule, isSystemd) {
71
+ if (!schedule || typeof schedule !== "string") {
72
+ throw new Error("Schedule must be a non-empty string");
73
+ }
74
+ const trimmed = schedule.trim();
75
+ if (trimmed.length > 256) {
76
+ throw new Error("Schedule string too long (max 256 characters)");
77
+ }
78
+ // Reject shell metacharacters
79
+ if (/[;|&$`(){}<>\n\r\\]/.test(trimmed)) {
80
+ throw new Error(`Schedule contains forbidden characters: ${trimmed}`);
81
+ }
82
+ if (isSystemd) {
83
+ if (!SYSTEMD_CALENDAR_RE.test(trimmed)) {
84
+ throw new Error(`Invalid systemd calendar format: '${trimmed}'. Only alphanumeric, spaces, and *:-/., allowed.`);
85
+ }
86
+ }
87
+ else {
88
+ // Validate cron: should be 5 space-separated fields
89
+ const fields = trimmed.split(/\s+/);
90
+ if (fields.length !== 5) {
91
+ throw new Error(`Invalid cron schedule: '${trimmed}'. Expected 5 fields (minute hour day month weekday).`);
92
+ }
93
+ for (const field of fields) {
94
+ if (!CRON_FIELD_RE.test(field)) {
95
+ throw new Error(`Invalid cron field: '${field}' in schedule '${trimmed}'.`);
96
+ }
97
+ }
98
+ }
99
+ return trimmed;
100
+ }
101
+ const WORKFLOWS = {
102
+ quick_harden: [
103
+ {
104
+ tool: "sysctl",
105
+ description: "Audit kernel security parameters",
106
+ command: "sudo",
107
+ args: ["sysctl", "-a"],
108
+ estimatedSeconds: 5,
109
+ },
110
+ {
111
+ tool: "ssh",
112
+ description: "Check SSH configuration",
113
+ command: "cat",
114
+ args: ["/etc/ssh/sshd_config"],
115
+ estimatedSeconds: 2,
116
+ },
117
+ {
118
+ tool: "systemctl",
119
+ description: "Audit running services",
120
+ command: "systemctl",
121
+ args: ["list-units", "--type=service", "--state=running", "--no-pager"],
122
+ estimatedSeconds: 5,
123
+ },
124
+ {
125
+ tool: "ufw/iptables",
126
+ description: "Check firewall status",
127
+ command: "sudo",
128
+ args: ["iptables", "-L", "-n", "--line-numbers"],
129
+ estimatedSeconds: 3,
130
+ },
131
+ {
132
+ tool: "find",
133
+ description: "Audit world-writable files in /etc",
134
+ command: "find",
135
+ args: ["/etc", "-type", "f", "-perm", "-002", "-ls"],
136
+ estimatedSeconds: 10,
137
+ },
138
+ ],
139
+ full_audit: [
140
+ {
141
+ tool: "lynis",
142
+ description: "Run Lynis security audit",
143
+ command: "sudo",
144
+ args: ["lynis", "audit", "system", "--quick", "--no-colors"],
145
+ estimatedSeconds: 120,
146
+ },
147
+ {
148
+ tool: "ssh",
149
+ description: "Audit SSH configuration",
150
+ command: "cat",
151
+ args: ["/etc/ssh/sshd_config"],
152
+ estimatedSeconds: 2,
153
+ },
154
+ {
155
+ tool: "passwd",
156
+ description: "Audit user accounts",
157
+ command: "cat",
158
+ args: ["/etc/passwd"],
159
+ estimatedSeconds: 2,
160
+ },
161
+ {
162
+ tool: "ss",
163
+ description: "Audit listening ports",
164
+ command: "ss",
165
+ args: ["-tulnp"],
166
+ estimatedSeconds: 3,
167
+ },
168
+ {
169
+ tool: "clamscan",
170
+ description: "Quick malware scan of /tmp",
171
+ command: "clamscan",
172
+ args: ["--recursive", "--infected", "/tmp"],
173
+ estimatedSeconds: 60,
174
+ },
175
+ ],
176
+ incident_prep: [
177
+ {
178
+ tool: "tar",
179
+ description: "Backup critical configurations",
180
+ command: "sudo",
181
+ args: [
182
+ "tar",
183
+ "-czf",
184
+ "/tmp/incident-config-backup.tar.gz",
185
+ "/etc/ssh/sshd_config",
186
+ "/etc/passwd",
187
+ "/etc/shadow",
188
+ "/etc/group",
189
+ ],
190
+ estimatedSeconds: 5,
191
+ },
192
+ {
193
+ tool: "ps",
194
+ description: "Snapshot running processes",
195
+ command: "ps",
196
+ args: ["auxf"],
197
+ estimatedSeconds: 2,
198
+ },
199
+ {
200
+ tool: "ss",
201
+ description: "Snapshot network connections",
202
+ command: "ss",
203
+ args: ["-tulnp"],
204
+ estimatedSeconds: 2,
205
+ },
206
+ {
207
+ tool: "auditctl",
208
+ description: "Enable auditd if not running",
209
+ command: "sudo",
210
+ args: ["systemctl", "start", "auditd"],
211
+ estimatedSeconds: 5,
212
+ },
213
+ ],
214
+ backup_all: [
215
+ {
216
+ tool: "tar",
217
+ description: "Backup /etc configuration",
218
+ command: "sudo",
219
+ args: [
220
+ "tar",
221
+ "-czf",
222
+ "/tmp/etc-backup.tar.gz",
223
+ "/etc/",
224
+ ],
225
+ estimatedSeconds: 30,
226
+ },
227
+ {
228
+ tool: "iptables-save",
229
+ description: "Backup firewall rules",
230
+ command: "sudo",
231
+ args: ["iptables-save"],
232
+ estimatedSeconds: 2,
233
+ },
234
+ {
235
+ tool: "dpkg",
236
+ description: "List installed packages",
237
+ command: "dpkg",
238
+ args: ["--get-selections"],
239
+ estimatedSeconds: 5,
240
+ },
241
+ ],
242
+ network_lockdown: [
243
+ {
244
+ tool: "iptables-save",
245
+ description: "Save current firewall state",
246
+ command: "sudo",
247
+ args: ["iptables-save"],
248
+ estimatedSeconds: 2,
249
+ },
250
+ {
251
+ tool: "ss",
252
+ description: "Identify unnecessary listening ports",
253
+ command: "ss",
254
+ args: ["-tulnp"],
255
+ estimatedSeconds: 2,
256
+ },
257
+ {
258
+ tool: "fail2ban",
259
+ description: "Check fail2ban status",
260
+ command: "sudo",
261
+ args: ["fail2ban-client", "status"],
262
+ estimatedSeconds: 3,
263
+ },
264
+ {
265
+ tool: "sysctl",
266
+ description: "Disable IP forwarding",
267
+ command: "sudo",
268
+ args: ["sysctl", "net.ipv4.ip_forward"],
269
+ estimatedSeconds: 2,
270
+ },
271
+ ],
272
+ };
273
+ const WORKFLOW_SUGGESTIONS = {
274
+ initial_hardening: {
275
+ server: [
276
+ {
277
+ tool: "hardening_sysctl_audit",
278
+ description: "Audit kernel security parameters",
279
+ suggestedParams: "category: 'security'",
280
+ estimatedMinutes: 1,
281
+ },
282
+ {
283
+ tool: "hardening_ssh_audit",
284
+ description: "Audit and harden SSH configuration",
285
+ suggestedParams: "action: 'audit'",
286
+ estimatedMinutes: 1,
287
+ },
288
+ {
289
+ tool: "hardening_service_audit",
290
+ description: "Audit running services, disable unnecessary ones",
291
+ suggestedParams: "action: 'list'",
292
+ estimatedMinutes: 2,
293
+ },
294
+ {
295
+ tool: "firewall_iptables_list",
296
+ description: "Review current firewall rules",
297
+ suggestedParams: "table: 'filter'",
298
+ estimatedMinutes: 1,
299
+ },
300
+ {
301
+ tool: "access_user_audit",
302
+ description: "Audit user accounts and privileges",
303
+ suggestedParams: "check_type: 'all'",
304
+ estimatedMinutes: 1,
305
+ },
306
+ {
307
+ tool: "hardening_file_perms",
308
+ description: "Audit file permissions on critical paths",
309
+ suggestedParams: "path: '/etc', check_type: 'world_writable'",
310
+ estimatedMinutes: 3,
311
+ },
312
+ {
313
+ tool: "crypto_tls_config_audit",
314
+ description: "Audit TLS/SSL configuration",
315
+ suggestedParams: "service: 'all'",
316
+ estimatedMinutes: 2,
317
+ },
318
+ ],
319
+ desktop: [
320
+ {
321
+ tool: "hardening_sysctl_audit",
322
+ description: "Audit kernel parameters",
323
+ suggestedParams: "category: 'security'",
324
+ estimatedMinutes: 1,
325
+ },
326
+ {
327
+ tool: "firewall_ufw_status",
328
+ description: "Check UFW firewall status",
329
+ suggestedParams: "",
330
+ estimatedMinutes: 1,
331
+ },
332
+ {
333
+ tool: "hardening_service_audit",
334
+ description: "Disable unnecessary services",
335
+ suggestedParams: "action: 'list'",
336
+ estimatedMinutes: 2,
337
+ },
338
+ {
339
+ tool: "malware_clamscan",
340
+ description: "Scan home directory",
341
+ suggestedParams: "path: '/home', quick: true",
342
+ estimatedMinutes: 10,
343
+ },
344
+ ],
345
+ container: [
346
+ {
347
+ tool: "container_docker_audit",
348
+ description: "Full Docker security audit",
349
+ suggestedParams: "check_type: 'all'",
350
+ estimatedMinutes: 3,
351
+ },
352
+ {
353
+ tool: "container_docker_bench",
354
+ description: "Run CIS Docker Benchmark",
355
+ suggestedParams: "log_level: 'WARN'",
356
+ estimatedMinutes: 5,
357
+ },
358
+ {
359
+ tool: "container_apparmor_manage",
360
+ description: "Check AppArmor status",
361
+ suggestedParams: "action: 'status'",
362
+ estimatedMinutes: 1,
363
+ },
364
+ {
365
+ tool: "container_namespace_check",
366
+ description: "Verify namespace isolation",
367
+ suggestedParams: "check_type: 'all'",
368
+ estimatedMinutes: 1,
369
+ },
370
+ ],
371
+ cloud: [
372
+ {
373
+ tool: "hardening_sysctl_audit",
374
+ description: "Audit kernel parameters",
375
+ suggestedParams: "category: 'security'",
376
+ estimatedMinutes: 1,
377
+ },
378
+ {
379
+ tool: "hardening_ssh_audit",
380
+ description: "Harden SSH (critical for cloud instances)",
381
+ suggestedParams: "action: 'audit'",
382
+ estimatedMinutes: 1,
383
+ },
384
+ {
385
+ tool: "firewall_iptables_list",
386
+ description: "Verify firewall rules",
387
+ suggestedParams: "table: 'filter'",
388
+ estimatedMinutes: 1,
389
+ },
390
+ {
391
+ tool: "network_port_audit",
392
+ description: "Audit exposed ports",
393
+ suggestedParams: "",
394
+ estimatedMinutes: 2,
395
+ },
396
+ {
397
+ tool: "crypto_tls_config_audit",
398
+ description: "Audit TLS configuration",
399
+ suggestedParams: "service: 'all'",
400
+ estimatedMinutes: 2,
401
+ },
402
+ ],
403
+ },
404
+ incident_response: {
405
+ server: [
406
+ {
407
+ tool: "logging_journald_query",
408
+ description: "Check recent system logs for anomalies",
409
+ suggestedParams: "priority: 'err', lines: 100",
410
+ estimatedMinutes: 1,
411
+ },
412
+ {
413
+ tool: "network_connections",
414
+ description: "Check active network connections",
415
+ suggestedParams: "",
416
+ estimatedMinutes: 1,
417
+ },
418
+ {
419
+ tool: "access_user_audit",
420
+ description: "Check for unauthorized user accounts",
421
+ suggestedParams: "check_type: 'all'",
422
+ estimatedMinutes: 1,
423
+ },
424
+ {
425
+ tool: "malware_rkhunter",
426
+ description: "Scan for rootkits",
427
+ suggestedParams: "",
428
+ estimatedMinutes: 5,
429
+ },
430
+ {
431
+ tool: "logging_auth_analyze",
432
+ description: "Analyze authentication logs",
433
+ suggestedParams: "",
434
+ estimatedMinutes: 2,
435
+ },
436
+ {
437
+ tool: "backup_system_state",
438
+ description: "Preserve current system state for forensics",
439
+ suggestedParams: "",
440
+ estimatedMinutes: 5,
441
+ },
442
+ ],
443
+ desktop: [
444
+ {
445
+ tool: "logging_journald_query",
446
+ description: "Check system logs",
447
+ suggestedParams: "priority: 'err'",
448
+ estimatedMinutes: 1,
449
+ },
450
+ {
451
+ tool: "malware_clamscan",
452
+ description: "Full malware scan",
453
+ suggestedParams: "path: '/'",
454
+ estimatedMinutes: 30,
455
+ },
456
+ {
457
+ tool: "network_connections",
458
+ description: "Check for suspicious connections",
459
+ suggestedParams: "",
460
+ estimatedMinutes: 1,
461
+ },
462
+ ],
463
+ container: [
464
+ {
465
+ tool: "container_docker_audit",
466
+ description: "Audit container security",
467
+ suggestedParams: "check_type: 'containers'",
468
+ estimatedMinutes: 2,
469
+ },
470
+ {
471
+ tool: "logging_journald_query",
472
+ description: "Check container logs",
473
+ suggestedParams: "priority: 'err'",
474
+ estimatedMinutes: 1,
475
+ },
476
+ ],
477
+ cloud: [
478
+ {
479
+ tool: "logging_auth_analyze",
480
+ description: "Analyze authentication attempts",
481
+ suggestedParams: "",
482
+ estimatedMinutes: 2,
483
+ },
484
+ {
485
+ tool: "network_connections",
486
+ description: "Check for anomalous connections",
487
+ suggestedParams: "",
488
+ estimatedMinutes: 1,
489
+ },
490
+ {
491
+ tool: "access_user_audit",
492
+ description: "Audit user access",
493
+ suggestedParams: "check_type: 'all'",
494
+ estimatedMinutes: 1,
495
+ },
496
+ ],
497
+ },
498
+ compliance_audit: {
499
+ server: [
500
+ {
501
+ tool: "compliance_lynis",
502
+ description: "Run Lynis compliance audit",
503
+ suggestedParams: "",
504
+ estimatedMinutes: 5,
505
+ },
506
+ {
507
+ tool: "compliance_cis_check",
508
+ description: "Run CIS benchmark checks",
509
+ suggestedParams: "",
510
+ estimatedMinutes: 10,
511
+ },
512
+ {
513
+ tool: "hardening_ssh_audit",
514
+ description: "Audit SSH compliance",
515
+ suggestedParams: "action: 'audit'",
516
+ estimatedMinutes: 1,
517
+ },
518
+ {
519
+ tool: "crypto_tls_config_audit",
520
+ description: "Audit crypto compliance",
521
+ suggestedParams: "service: 'all'",
522
+ estimatedMinutes: 2,
523
+ },
524
+ ],
525
+ desktop: [
526
+ {
527
+ tool: "compliance_lynis",
528
+ description: "Run Lynis audit",
529
+ suggestedParams: "",
530
+ estimatedMinutes: 5,
531
+ },
532
+ ],
533
+ container: [
534
+ {
535
+ tool: "container_docker_bench",
536
+ description: "Docker CIS benchmark",
537
+ suggestedParams: "",
538
+ estimatedMinutes: 5,
539
+ },
540
+ {
541
+ tool: "compliance_lynis",
542
+ description: "Lynis audit of host",
543
+ suggestedParams: "",
544
+ estimatedMinutes: 5,
545
+ },
546
+ ],
547
+ cloud: [
548
+ {
549
+ tool: "compliance_lynis",
550
+ description: "Lynis audit",
551
+ suggestedParams: "",
552
+ estimatedMinutes: 5,
553
+ },
554
+ {
555
+ tool: "compliance_cis_check",
556
+ description: "CIS benchmark",
557
+ suggestedParams: "",
558
+ estimatedMinutes: 10,
559
+ },
560
+ ],
561
+ },
562
+ malware_investigation: {
563
+ server: [
564
+ {
565
+ tool: "malware_clamscan",
566
+ description: "ClamAV scan",
567
+ suggestedParams: "path: '/', quick: true",
568
+ estimatedMinutes: 15,
569
+ },
570
+ {
571
+ tool: "malware_rkhunter",
572
+ description: "Rootkit scan",
573
+ suggestedParams: "",
574
+ estimatedMinutes: 5,
575
+ },
576
+ {
577
+ tool: "malware_chkrootkit",
578
+ description: "Secondary rootkit check",
579
+ suggestedParams: "",
580
+ estimatedMinutes: 5,
581
+ },
582
+ {
583
+ tool: "crypto_file_hash",
584
+ description: "Hash critical binaries for verification",
585
+ suggestedParams: "path: '/usr/bin', algorithm: 'sha256', recursive: true",
586
+ estimatedMinutes: 10,
587
+ },
588
+ ],
589
+ desktop: [
590
+ {
591
+ tool: "malware_clamscan",
592
+ description: "Full ClamAV scan",
593
+ suggestedParams: "path: '/'",
594
+ estimatedMinutes: 30,
595
+ },
596
+ {
597
+ tool: "malware_rkhunter",
598
+ description: "Rootkit scan",
599
+ suggestedParams: "",
600
+ estimatedMinutes: 5,
601
+ },
602
+ ],
603
+ container: [
604
+ {
605
+ tool: "container_docker_audit",
606
+ description: "Audit container images",
607
+ suggestedParams: "check_type: 'images'",
608
+ estimatedMinutes: 2,
609
+ },
610
+ ],
611
+ cloud: [
612
+ {
613
+ tool: "malware_clamscan",
614
+ description: "ClamAV scan",
615
+ suggestedParams: "path: '/'",
616
+ estimatedMinutes: 15,
617
+ },
618
+ {
619
+ tool: "malware_rkhunter",
620
+ description: "Rootkit scan",
621
+ suggestedParams: "",
622
+ estimatedMinutes: 5,
623
+ },
624
+ ],
625
+ },
626
+ network_monitoring: {
627
+ server: [
628
+ {
629
+ tool: "network_port_audit",
630
+ description: "Audit listening ports",
631
+ suggestedParams: "",
632
+ estimatedMinutes: 1,
633
+ },
634
+ {
635
+ tool: "network_connections",
636
+ description: "Monitor active connections",
637
+ suggestedParams: "",
638
+ estimatedMinutes: 1,
639
+ },
640
+ {
641
+ tool: "ids_snort_manage",
642
+ description: "Check IDS status",
643
+ suggestedParams: "action: 'status'",
644
+ estimatedMinutes: 1,
645
+ },
646
+ {
647
+ tool: "firewall_iptables_list",
648
+ description: "Review firewall rules",
649
+ suggestedParams: "",
650
+ estimatedMinutes: 1,
651
+ },
652
+ ],
653
+ desktop: [
654
+ {
655
+ tool: "network_connections",
656
+ description: "Check connections",
657
+ suggestedParams: "",
658
+ estimatedMinutes: 1,
659
+ },
660
+ {
661
+ tool: "firewall_ufw_status",
662
+ description: "Check UFW",
663
+ suggestedParams: "",
664
+ estimatedMinutes: 1,
665
+ },
666
+ ],
667
+ container: [
668
+ {
669
+ tool: "container_docker_audit",
670
+ description: "Audit Docker network",
671
+ suggestedParams: "check_type: 'network'",
672
+ estimatedMinutes: 1,
673
+ },
674
+ {
675
+ tool: "container_namespace_check",
676
+ description: "Check network namespace isolation",
677
+ suggestedParams: "check_type: 'network'",
678
+ estimatedMinutes: 1,
679
+ },
680
+ ],
681
+ cloud: [
682
+ {
683
+ tool: "network_port_audit",
684
+ description: "Audit exposed ports",
685
+ suggestedParams: "",
686
+ estimatedMinutes: 1,
687
+ },
688
+ {
689
+ tool: "network_connections",
690
+ description: "Monitor connections",
691
+ suggestedParams: "",
692
+ estimatedMinutes: 1,
693
+ },
694
+ ],
695
+ },
696
+ full_assessment: {
697
+ server: [
698
+ {
699
+ tool: "compliance_lynis",
700
+ description: "Full Lynis audit",
701
+ suggestedParams: "",
702
+ estimatedMinutes: 5,
703
+ },
704
+ {
705
+ tool: "hardening_sysctl_audit",
706
+ description: "Kernel audit",
707
+ suggestedParams: "category: 'all'",
708
+ estimatedMinutes: 1,
709
+ },
710
+ {
711
+ tool: "hardening_ssh_audit",
712
+ description: "SSH audit",
713
+ suggestedParams: "action: 'audit'",
714
+ estimatedMinutes: 1,
715
+ },
716
+ {
717
+ tool: "firewall_iptables_list",
718
+ description: "Firewall audit",
719
+ suggestedParams: "",
720
+ estimatedMinutes: 1,
721
+ },
722
+ {
723
+ tool: "access_user_audit",
724
+ description: "User audit",
725
+ suggestedParams: "check_type: 'all'",
726
+ estimatedMinutes: 1,
727
+ },
728
+ {
729
+ tool: "network_port_audit",
730
+ description: "Port audit",
731
+ suggestedParams: "",
732
+ estimatedMinutes: 1,
733
+ },
734
+ {
735
+ tool: "malware_clamscan",
736
+ description: "Malware scan",
737
+ suggestedParams: "path: '/', quick: true",
738
+ estimatedMinutes: 15,
739
+ },
740
+ {
741
+ tool: "crypto_tls_config_audit",
742
+ description: "Crypto audit",
743
+ suggestedParams: "service: 'all'",
744
+ estimatedMinutes: 2,
745
+ },
746
+ ],
747
+ desktop: [
748
+ {
749
+ tool: "compliance_lynis",
750
+ description: "Lynis audit",
751
+ suggestedParams: "",
752
+ estimatedMinutes: 5,
753
+ },
754
+ {
755
+ tool: "firewall_ufw_status",
756
+ description: "Firewall check",
757
+ suggestedParams: "",
758
+ estimatedMinutes: 1,
759
+ },
760
+ {
761
+ tool: "malware_clamscan",
762
+ description: "Malware scan",
763
+ suggestedParams: "path: '/'",
764
+ estimatedMinutes: 30,
765
+ },
766
+ ],
767
+ container: [
768
+ {
769
+ tool: "container_docker_audit",
770
+ description: "Docker audit",
771
+ suggestedParams: "check_type: 'all'",
772
+ estimatedMinutes: 3,
773
+ },
774
+ {
775
+ tool: "container_docker_bench",
776
+ description: "CIS benchmark",
777
+ suggestedParams: "",
778
+ estimatedMinutes: 5,
779
+ },
780
+ {
781
+ tool: "container_apparmor_manage",
782
+ description: "AppArmor status",
783
+ suggestedParams: "action: 'status'",
784
+ estimatedMinutes: 1,
785
+ },
786
+ {
787
+ tool: "compliance_lynis",
788
+ description: "Host Lynis audit",
789
+ suggestedParams: "",
790
+ estimatedMinutes: 5,
791
+ },
792
+ ],
793
+ cloud: [
794
+ {
795
+ tool: "compliance_lynis",
796
+ description: "Lynis audit",
797
+ suggestedParams: "",
798
+ estimatedMinutes: 5,
799
+ },
800
+ {
801
+ tool: "hardening_ssh_audit",
802
+ description: "SSH audit",
803
+ suggestedParams: "action: 'audit'",
804
+ estimatedMinutes: 1,
805
+ },
806
+ {
807
+ tool: "firewall_iptables_list",
808
+ description: "Firewall review",
809
+ suggestedParams: "",
810
+ estimatedMinutes: 1,
811
+ },
812
+ {
813
+ tool: "crypto_tls_config_audit",
814
+ description: "Crypto audit",
815
+ suggestedParams: "service: 'all'",
816
+ estimatedMinutes: 2,
817
+ },
818
+ ],
819
+ },
820
+ };
821
+ // ── Auto-Remediate Helpers ─────────────────────────────────────────────────
822
+ const REMEDIATION_SESSIONS_DIR = "/var/lib/kali-defense/remediation-sessions";
823
+ const SEVERITY_LEVELS_ORDER = ["critical", "high", "medium", "low"];
824
+ /** Hardcoded set of known, safe remediation mappings */
825
+ const KNOWN_REMEDIATIONS = [
826
+ // ── Hardening: kernel sysctl parameters ──
827
+ {
828
+ finding_id: "HARD-001",
829
+ description: "ASLR not fully enabled (kernel.randomize_va_space != 2)",
830
+ severity: "high",
831
+ remediation_command: "sysctl",
832
+ remediation_args: ["-w", "kernel.randomize_va_space=2"],
833
+ rollback_command: "sysctl",
834
+ rollback_args: ["-w", "kernel.randomize_va_space=0"],
835
+ risk_level: "safe",
836
+ category: "hardening",
837
+ },
838
+ {
839
+ finding_id: "HARD-002",
840
+ description: "IP forwarding enabled (net.ipv4.ip_forward = 1)",
841
+ severity: "medium",
842
+ remediation_command: "sysctl",
843
+ remediation_args: ["-w", "net.ipv4.ip_forward=0"],
844
+ rollback_command: "sysctl",
845
+ rollback_args: ["-w", "net.ipv4.ip_forward=1"],
846
+ risk_level: "moderate",
847
+ category: "hardening",
848
+ },
849
+ {
850
+ finding_id: "HARD-003",
851
+ description: "SYN cookies not enabled (net.ipv4.tcp_syncookies != 1)",
852
+ severity: "medium",
853
+ remediation_command: "sysctl",
854
+ remediation_args: ["-w", "net.ipv4.tcp_syncookies=1"],
855
+ rollback_command: "sysctl",
856
+ rollback_args: ["-w", "net.ipv4.tcp_syncookies=0"],
857
+ risk_level: "safe",
858
+ category: "hardening",
859
+ },
860
+ {
861
+ finding_id: "HARD-004",
862
+ description: "Reverse path filtering not enabled (net.ipv4.conf.all.rp_filter != 1)",
863
+ severity: "medium",
864
+ remediation_command: "sysctl",
865
+ remediation_args: ["-w", "net.ipv4.conf.all.rp_filter=1"],
866
+ rollback_command: "sysctl",
867
+ rollback_args: ["-w", "net.ipv4.conf.all.rp_filter=0"],
868
+ risk_level: "safe",
869
+ category: "hardening",
870
+ },
871
+ {
872
+ finding_id: "HARD-005",
873
+ description: "ICMP redirects accepted (net.ipv4.conf.all.accept_redirects != 0)",
874
+ severity: "medium",
875
+ remediation_command: "sysctl",
876
+ remediation_args: ["-w", "net.ipv4.conf.all.accept_redirects=0"],
877
+ rollback_command: "sysctl",
878
+ rollback_args: ["-w", "net.ipv4.conf.all.accept_redirects=1"],
879
+ risk_level: "safe",
880
+ category: "hardening",
881
+ },
882
+ {
883
+ finding_id: "HARD-006",
884
+ description: "Source routing accepted (net.ipv4.conf.all.accept_source_route != 0)",
885
+ severity: "high",
886
+ remediation_command: "sysctl",
887
+ remediation_args: ["-w", "net.ipv4.conf.all.accept_source_route=0"],
888
+ rollback_command: "sysctl",
889
+ rollback_args: ["-w", "net.ipv4.conf.all.accept_source_route=1"],
890
+ risk_level: "safe",
891
+ category: "hardening",
892
+ },
893
+ // ── Access control ──
894
+ {
895
+ finding_id: "ACCESS-001",
896
+ description: "SSH PermitRootLogin is enabled",
897
+ severity: "critical",
898
+ remediation_command: "sed",
899
+ remediation_args: ["-i", "s/^PermitRootLogin yes/PermitRootLogin no/", "/etc/ssh/sshd_config"],
900
+ rollback_command: "sed",
901
+ rollback_args: ["-i", "s/^PermitRootLogin no/PermitRootLogin yes/", "/etc/ssh/sshd_config"],
902
+ risk_level: "moderate",
903
+ category: "access_control",
904
+ },
905
+ {
906
+ finding_id: "ACCESS-002",
907
+ description: "SSH PermitEmptyPasswords is enabled",
908
+ severity: "critical",
909
+ remediation_command: "sed",
910
+ remediation_args: ["-i", "s/^PermitEmptyPasswords yes/PermitEmptyPasswords no/", "/etc/ssh/sshd_config"],
911
+ rollback_command: "sed",
912
+ rollback_args: ["-i", "s/^PermitEmptyPasswords no/PermitEmptyPasswords yes/", "/etc/ssh/sshd_config"],
913
+ risk_level: "moderate",
914
+ category: "access_control",
915
+ },
916
+ // ── Firewall ──
917
+ {
918
+ finding_id: "FW-001",
919
+ description: "Firewall INPUT chain has default ACCEPT policy",
920
+ severity: "high",
921
+ remediation_command: "iptables",
922
+ remediation_args: ["-P", "INPUT", "DROP"],
923
+ rollback_command: "iptables",
924
+ rollback_args: ["-P", "INPUT", "ACCEPT"],
925
+ risk_level: "risky",
926
+ category: "firewall",
927
+ },
928
+ {
929
+ finding_id: "FW-002",
930
+ description: "Firewall FORWARD chain has default ACCEPT policy",
931
+ severity: "high",
932
+ remediation_command: "iptables",
933
+ remediation_args: ["-P", "FORWARD", "DROP"],
934
+ rollback_command: "iptables",
935
+ rollback_args: ["-P", "FORWARD", "ACCEPT"],
936
+ risk_level: "risky",
937
+ category: "firewall",
938
+ },
939
+ ];
940
+ /** Allowed commands for auto-remediation execution */
941
+ const REMEDIATION_ALLOWLIST = new Set(["sysctl", "sed", "iptables"]);
942
+ function generateSessionId() {
943
+ const ts = Date.now();
944
+ const rand = Math.random().toString(36).substring(2, 8);
945
+ return `rem-${ts}-${rand}`;
946
+ }
947
+ function severityAtOrAbove(finding, threshold) {
948
+ return SEVERITY_LEVELS_ORDER.indexOf(finding) <= SEVERITY_LEVELS_ORDER.indexOf(threshold);
949
+ }
950
+ function riskSortValue(risk) {
951
+ switch (risk) {
952
+ case "safe": return 0;
953
+ case "moderate": return 1;
954
+ case "risky": return 2;
955
+ default: return 3;
956
+ }
957
+ }
958
+ /**
959
+ * Run a command via spawnSafe and collect output as a promise.
960
+ * Handles errors gracefully — returns error info instead of throwing.
961
+ */
962
+ async function runRemediateCmd(command, args, timeoutMs = 30_000) {
963
+ return new Promise((resolve) => {
964
+ let child;
965
+ try {
966
+ child = spawnSafe(command, args);
967
+ }
968
+ catch (err) {
969
+ const msg = err instanceof Error ? err.message : String(err);
970
+ resolve({ stdout: "", stderr: msg, exitCode: -1 });
971
+ return;
972
+ }
973
+ let stdout = "";
974
+ let stderr = "";
975
+ let resolved = false;
976
+ const timer = setTimeout(() => {
977
+ if (!resolved) {
978
+ resolved = true;
979
+ child.kill("SIGTERM");
980
+ resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
981
+ }
982
+ }, timeoutMs);
983
+ child.stdout?.on("data", (data) => {
984
+ stdout += data.toString();
985
+ });
986
+ child.stderr?.on("data", (data) => {
987
+ stderr += data.toString();
988
+ });
989
+ child.on("close", (code) => {
990
+ if (!resolved) {
991
+ resolved = true;
992
+ clearTimeout(timer);
993
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
994
+ }
995
+ });
996
+ child.on("error", (err) => {
997
+ if (!resolved) {
998
+ resolved = true;
999
+ clearTimeout(timer);
1000
+ resolve({ stdout, stderr: err.message, exitCode: -1 });
1001
+ }
1002
+ });
1003
+ });
1004
+ }
1005
+ /**
1006
+ * Gather applicable remediation findings by running audit commands and matching
1007
+ * against known remediation mappings.
1008
+ */
1009
+ async function gatherRemediationFindings(source, severityFilter) {
1010
+ const findings = [];
1011
+ const shouldInclude = (cat) => source === "all" || source === cat;
1012
+ // ── Hardening: check sysctl params ──
1013
+ if (shouldInclude("hardening")) {
1014
+ const sysctlResult = await runRemediateCmd("sysctl", ["-a"]);
1015
+ if (sysctlResult.exitCode === 0) {
1016
+ const sysctlValues = new Map();
1017
+ for (const line of sysctlResult.stdout.split("\n")) {
1018
+ const match = line.match(/^([^=]+?)\s*=\s*(.+)$/);
1019
+ if (match)
1020
+ sysctlValues.set(match[1].trim(), match[2].trim());
1021
+ }
1022
+ for (const f of KNOWN_REMEDIATIONS.filter(r => r.category === "hardening")) {
1023
+ const setArg = f.remediation_args.find(a => a.includes("="));
1024
+ if (setArg) {
1025
+ const eqIdx = setArg.indexOf("=");
1026
+ const key = setArg.substring(0, eqIdx);
1027
+ const expected = setArg.substring(eqIdx + 1);
1028
+ const actual = sysctlValues.get(key);
1029
+ if (actual !== undefined && actual !== expected) {
1030
+ findings.push(f);
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ // ── Access control: check SSH config ──
1037
+ if (shouldInclude("access_control")) {
1038
+ const sshResult = await runRemediateCmd("grep", ["-E", "^PermitRootLogin|^PermitEmptyPasswords", "/etc/ssh/sshd_config"]);
1039
+ if (sshResult.exitCode === 0 || sshResult.stdout.length > 0) {
1040
+ if (sshResult.stdout.includes("PermitRootLogin yes")) {
1041
+ const f = KNOWN_REMEDIATIONS.find(r => r.finding_id === "ACCESS-001");
1042
+ if (f)
1043
+ findings.push(f);
1044
+ }
1045
+ if (sshResult.stdout.includes("PermitEmptyPasswords yes")) {
1046
+ const f = KNOWN_REMEDIATIONS.find(r => r.finding_id === "ACCESS-002");
1047
+ if (f)
1048
+ findings.push(f);
1049
+ }
1050
+ }
1051
+ }
1052
+ // ── Firewall: check iptables policies ──
1053
+ if (shouldInclude("firewall")) {
1054
+ const fwResult = await runRemediateCmd("iptables", ["-L", "-n"]);
1055
+ if (fwResult.exitCode === 0) {
1056
+ if (fwResult.stdout.includes("Chain INPUT (policy ACCEPT)")) {
1057
+ const f = KNOWN_REMEDIATIONS.find(r => r.finding_id === "FW-001");
1058
+ if (f)
1059
+ findings.push(f);
1060
+ }
1061
+ if (fwResult.stdout.includes("Chain FORWARD (policy ACCEPT)")) {
1062
+ const f = KNOWN_REMEDIATIONS.find(r => r.finding_id === "FW-002");
1063
+ if (f)
1064
+ findings.push(f);
1065
+ }
1066
+ }
1067
+ }
1068
+ // ── Compliance: run lynis and cross-reference known remediations ──
1069
+ if (shouldInclude("compliance")) {
1070
+ const lynisResult = await runRemediateCmd("lynis", ["audit", "system", "--quick", "--no-colors"], 120_000);
1071
+ if (lynisResult.exitCode === 0 || lynisResult.stdout.length > 0) {
1072
+ // Cross-reference lynis output with known remediations
1073
+ for (const f of KNOWN_REMEDIATIONS) {
1074
+ if (findings.some(e => e.finding_id === f.finding_id))
1075
+ continue;
1076
+ // Check if lynis mentions the relevant sysctl key or SSH setting
1077
+ const setArg = f.remediation_args.find(a => a.includes("="));
1078
+ if (setArg) {
1079
+ const key = setArg.substring(0, setArg.indexOf("="));
1080
+ if (lynisResult.stdout.includes(key)) {
1081
+ findings.push(f);
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ // Apply severity filter
1088
+ const filtered = findings.filter(f => severityAtOrAbove(f.severity, severityFilter));
1089
+ // Sort by severity (critical first) then risk level (safe first)
1090
+ filtered.sort((a, b) => {
1091
+ const sevDiff = SEVERITY_LEVELS_ORDER.indexOf(a.severity) - SEVERITY_LEVELS_ORDER.indexOf(b.severity);
1092
+ if (sevDiff !== 0)
1093
+ return sevDiff;
1094
+ return riskSortValue(a.risk_level) - riskSortValue(b.risk_level);
1095
+ });
1096
+ return filtered;
1097
+ }
1098
+ // ── Registration entry point ───────────────────────────────────────────────
1099
+ export function registerMetaTools(server) {
1100
+ // ── 1. defense_check_tools ───────────────────────────────────────────────
1101
+ server.tool("defense_check_tools", "Check availability and versions of all defensive security tools, optionally install missing ones", {
1102
+ category: z
1103
+ .string()
1104
+ .optional()
1105
+ .describe("Filter by category: hardening, firewall, monitoring, assessment, network, access, encryption, container"),
1106
+ install_missing: z
1107
+ .boolean()
1108
+ .optional()
1109
+ .default(false)
1110
+ .describe("Attempt to install missing tools"),
1111
+ dry_run: z
1112
+ .boolean()
1113
+ .optional()
1114
+ .describe("Preview installations without executing (defaults to KALI_DEFENSE_DRY_RUN env var)"),
1115
+ }, async ({ category, install_missing, dry_run }) => {
1116
+ try {
1117
+ const sections = [];
1118
+ sections.push("🔧 Defensive Tool Availability Check");
1119
+ sections.push("=".repeat(50));
1120
+ const validCategories = [
1121
+ "hardening",
1122
+ "firewall",
1123
+ "monitoring",
1124
+ "assessment",
1125
+ "network",
1126
+ "access",
1127
+ "encryption",
1128
+ "container",
1129
+ "malware",
1130
+ "forensics",
1131
+ ];
1132
+ const filterCategory = category && validCategories.includes(category)
1133
+ ? category
1134
+ : undefined;
1135
+ if (category && !filterCategory) {
1136
+ sections.push(`\n⚠️ Unknown category '${category}'. Valid: ${validCategories.join(", ")}`);
1137
+ sections.push("Showing all categories.\n");
1138
+ }
1139
+ const results = await checkAllTools(filterCategory);
1140
+ // Group by category
1141
+ const grouped = new Map();
1142
+ for (const r of results) {
1143
+ const cat = r.tool.category;
1144
+ if (!grouped.has(cat))
1145
+ grouped.set(cat, []);
1146
+ grouped.get(cat).push(r);
1147
+ }
1148
+ let installed = 0;
1149
+ let missing = 0;
1150
+ for (const [cat, tools] of grouped) {
1151
+ sections.push(`\n── ${cat.charAt(0).toUpperCase() + cat.slice(1)} ──`);
1152
+ for (const t of tools) {
1153
+ if (t.installed) {
1154
+ installed++;
1155
+ const version = t.version
1156
+ ? ` (${t.version.substring(0, 60)})`
1157
+ : "";
1158
+ sections.push(` ✅ ${t.tool.name}${version}`);
1159
+ if (t.path)
1160
+ sections.push(` Path: ${t.path}`);
1161
+ }
1162
+ else {
1163
+ missing++;
1164
+ const req = t.tool.required ? " [REQUIRED]" : " [optional]";
1165
+ sections.push(` ❌ ${t.tool.name}${req}`);
1166
+ }
1167
+ }
1168
+ }
1169
+ sections.push(`\n── Summary ──`);
1170
+ sections.push(` Installed: ${installed} | Missing: ${missing} | Total: ${installed + missing}`);
1171
+ // Install missing tools if requested
1172
+ if (install_missing && missing > 0) {
1173
+ sections.push("\n── Installation ──");
1174
+ if (dry_run ?? getConfig().dryRun) {
1175
+ const installResults = await installMissing(filterCategory, true);
1176
+ for (const r of installResults) {
1177
+ sections.push(` ${r.message}`);
1178
+ }
1179
+ }
1180
+ else {
1181
+ sections.push(" Installing missing tools...\n");
1182
+ const installResults = await installMissing(filterCategory, false);
1183
+ for (const r of installResults) {
1184
+ const icon = r.success ? "✅" : "❌";
1185
+ sections.push(` ${icon} ${r.message}`);
1186
+ }
1187
+ logChange(createChangeEntry({
1188
+ tool: "defense_check_tools",
1189
+ action: "install_missing",
1190
+ target: filterCategory || "all",
1191
+ after: `Attempted to install ${installResults.length} tools`,
1192
+ dryRun: false,
1193
+ success: installResults.every((r) => r.success),
1194
+ }));
1195
+ }
1196
+ }
1197
+ return { content: [createTextContent(sections.join("\n"))] };
1198
+ }
1199
+ catch (err) {
1200
+ const msg = err instanceof Error ? err.message : String(err);
1201
+ return { content: [createErrorContent(msg)], isError: true };
1202
+ }
1203
+ });
1204
+ // ── 2. defense_workflow (merged: suggest + run) ─────────────────────────
1205
+ server.tool("defense_workflow", "Defense workflows: suggest a workflow based on objectives, or run a predefined multi-step workflow.", {
1206
+ action: z.enum(["suggest", "run"]).describe("Action: suggest=recommend workflow, run=execute predefined workflow"),
1207
+ // suggest params
1208
+ objective: z.enum(["initial_hardening", "incident_response", "compliance_audit", "malware_investigation", "network_monitoring", "full_assessment"]).optional().describe("Security objective (suggest action)"),
1209
+ system_type: z.enum(["server", "desktop", "container", "cloud"]).optional().default("server").describe("System type (suggest action)"),
1210
+ // run params
1211
+ workflow: z.enum(["quick_harden", "full_audit", "incident_prep", "backup_all", "network_lockdown"]).optional().describe("Workflow to execute (run action)"),
1212
+ // shared
1213
+ dry_run: z.boolean().optional().default(true).describe("Preview without executing (run action, defaults to true for safety)"),
1214
+ }, async (params) => {
1215
+ const { action } = params;
1216
+ switch (action) {
1217
+ case "suggest": {
1218
+ const { objective, system_type } = params;
1219
+ try {
1220
+ if (!objective)
1221
+ return { content: [createErrorContent("objective is required for suggest action")], isError: true };
1222
+ const sections = [];
1223
+ sections.push(`📋 Recommended Workflow: ${objective.replace(/_/g, " ").toUpperCase()}`);
1224
+ sections.push(`System type: ${system_type}`);
1225
+ sections.push("=".repeat(50));
1226
+ const suggestions = WORKFLOW_SUGGESTIONS[objective]?.[system_type] || [];
1227
+ if (suggestions.length === 0) {
1228
+ sections.push("\nNo specific workflow available for this combination.");
1229
+ return { content: [createTextContent(sections.join("\n"))] };
1230
+ }
1231
+ let totalMinutes = 0;
1232
+ for (let i = 0; i < suggestions.length; i++) {
1233
+ const step = suggestions[i];
1234
+ totalMinutes += step.estimatedMinutes;
1235
+ sections.push(`\n Step ${i + 1}: ${step.description}`);
1236
+ sections.push(` Tool: ${step.tool}`);
1237
+ if (step.suggestedParams)
1238
+ sections.push(` Suggested params: { ${step.suggestedParams} }`);
1239
+ sections.push(` Estimated time: ~${step.estimatedMinutes} min`);
1240
+ }
1241
+ sections.push(`\n── Workflow Summary ──`);
1242
+ sections.push(` Total steps: ${suggestions.length}`);
1243
+ sections.push(` Estimated total time: ~${totalMinutes} minutes`);
1244
+ return { content: [createTextContent(sections.join("\n"))] };
1245
+ }
1246
+ catch (err) {
1247
+ return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
1248
+ }
1249
+ }
1250
+ case "run": {
1251
+ const { workflow, dry_run } = params;
1252
+ try {
1253
+ if (!workflow)
1254
+ return { content: [createErrorContent("workflow is required for run action")], isError: true };
1255
+ const sections = [];
1256
+ sections.push(`🚀 Workflow: ${workflow.replace(/_/g, " ").toUpperCase()}`);
1257
+ sections.push("=".repeat(50));
1258
+ const steps = WORKFLOWS[workflow];
1259
+ if (!steps || steps.length === 0)
1260
+ return { content: [createErrorContent(`Unknown workflow: ${workflow}`)], isError: true };
1261
+ // Pre-validate all workflow commands against the command allowlist
1262
+ // before executing any steps (fail-fast)
1263
+ const invalidSteps = [];
1264
+ for (const step of steps) {
1265
+ const cmdToCheck = step.command;
1266
+ if (!isAllowlisted(cmdToCheck)) {
1267
+ invalidSteps.push(`${step.description}: '${cmdToCheck}' not in command allowlist`);
1268
+ }
1269
+ // For sudo commands, also validate the target binary
1270
+ if (cmdToCheck === "sudo" && step.args.length > 0) {
1271
+ const targetCmd = step.args.find(a => !a.startsWith("-"));
1272
+ if (targetCmd && !isAllowlisted(targetCmd)) {
1273
+ invalidSteps.push(`${step.description}: sudo target '${targetCmd}' not in command allowlist`);
1274
+ }
1275
+ }
1276
+ }
1277
+ if (invalidSteps.length > 0) {
1278
+ return { content: [createErrorContent(`Workflow contains commands not in the allowlist:\n${invalidSteps.join("\n")}`)], isError: true };
1279
+ }
1280
+ const totalEstimate = steps.reduce((sum, s) => sum + s.estimatedSeconds, 0);
1281
+ sections.push(`Steps: ${steps.length} | Estimated time: ~${Math.ceil(totalEstimate / 60)} min`);
1282
+ const effectiveDryRun = dry_run ?? getConfig().dryRun;
1283
+ if (effectiveDryRun) {
1284
+ sections.push("\n[DRY RUN] Workflow steps that would be executed:\n");
1285
+ for (let i = 0; i < steps.length; i++) {
1286
+ const step = steps[i];
1287
+ sections.push(` Step ${i + 1}: ${step.description}`);
1288
+ sections.push(` Tool: ${step.tool}`);
1289
+ sections.push(` Command: ${step.command} ${step.args.join(" ")}`);
1290
+ sections.push(` Est. time: ~${step.estimatedSeconds}s`);
1291
+ sections.push("");
1292
+ }
1293
+ sections.push("To execute, set dry_run: false");
1294
+ logChange(createChangeEntry({ tool: "defense_workflow", action: `${workflow}_dry_run`, target: workflow, after: `Previewed ${steps.length} workflow steps`, dryRun: true, success: true }));
1295
+ }
1296
+ else {
1297
+ sections.push("\nExecuting workflow...\n");
1298
+ let successCount = 0, failCount = 0;
1299
+ for (let i = 0; i < steps.length; i++) {
1300
+ const step = steps[i];
1301
+ sections.push(`── Step ${i + 1}/${steps.length}: ${step.description} ──`);
1302
+ // TOOL-005 remediation: check safeguards before EVERY workflow step
1303
+ const stepSafety = await SafeguardRegistry.getInstance().checkSafety(`defense_workflow_${workflow}_step_${i + 1}`, { command: step.command, args: step.args, description: step.description });
1304
+ if (stepSafety.warnings.length > 0) {
1305
+ sections.push(` ⚠️ Safety warnings: ${stepSafety.warnings.join("; ")}`);
1306
+ }
1307
+ if (!stepSafety.safe) {
1308
+ sections.push(` 🛑 Step blocked by safeguards: ${stepSafety.blockers.join("; ")}`);
1309
+ sections.push(` Impacted: ${stepSafety.impactedApps.join(", ")}`);
1310
+ failCount++;
1311
+ logChange(createChangeEntry({ tool: "defense_workflow", action: `${workflow}_step_${i + 1}`, target: step.description, after: `blocked by safeguards: ${stepSafety.blockers.join("; ")}`, dryRun: false, success: false, error: "Blocked by safeguard checks" }));
1312
+ sections.push("");
1313
+ continue;
1314
+ }
1315
+ const startTime = Date.now();
1316
+ const result = await executeCommand({ command: step.command, args: step.args, toolName: `defense_workflow_${workflow}`, timeout: Math.max(step.estimatedSeconds * 3 * 1000, 30000) });
1317
+ const duration = Math.round((Date.now() - startTime) / 1000);
1318
+ if (result.exitCode === 0) {
1319
+ successCount++;
1320
+ sections.push(` ✅ Completed in ${duration}s`);
1321
+ const output = result.stdout.trim();
1322
+ if (output) {
1323
+ const outputLines = output.split("\n");
1324
+ if (outputLines.length > 20) {
1325
+ sections.push(` Output (${outputLines.length} lines, showing first 20):`);
1326
+ for (const line of outputLines.slice(0, 20))
1327
+ sections.push(` ${line}`);
1328
+ sections.push(" ...");
1329
+ }
1330
+ else {
1331
+ sections.push(" Output:");
1332
+ for (const line of outputLines)
1333
+ sections.push(` ${line}`);
1334
+ }
1335
+ }
1336
+ }
1337
+ else {
1338
+ failCount++;
1339
+ sections.push(` ❌ Failed (exit ${result.exitCode}) in ${duration}s`);
1340
+ if (result.stderr)
1341
+ sections.push(` Error: ${result.stderr.substring(0, 200)}`);
1342
+ }
1343
+ sections.push("");
1344
+ logChange(createChangeEntry({ tool: "defense_workflow", action: `${workflow}_step_${i + 1}`, target: step.description, after: `exit=${result.exitCode} duration=${duration}s`, dryRun: false, success: result.exitCode === 0, error: result.exitCode !== 0 ? result.stderr.substring(0, 200) : undefined }));
1345
+ }
1346
+ sections.push("── Workflow Summary ──");
1347
+ sections.push(` Completed: ${successCount}/${steps.length}`);
1348
+ sections.push(` Failed: ${failCount}/${steps.length}`);
1349
+ sections.push(failCount === 0 ? " ✅ All steps completed successfully" : " ⚠️ Some steps failed");
1350
+ }
1351
+ return { content: [createTextContent(sections.join("\n"))] };
1352
+ }
1353
+ catch (err) {
1354
+ return { content: [createErrorContent(err instanceof Error ? err.message : String(err))], isError: true };
1355
+ }
1356
+ }
1357
+ default:
1358
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
1359
+ }
1360
+ });
1361
+ // ── 3. defense_change_history (kept as-is) ──────────────────────────────
1362
+ server.tool("defense_change_history", "View the audit trail of all defensive changes made by this server", {
1363
+ limit: z
1364
+ .number()
1365
+ .optional()
1366
+ .default(20)
1367
+ .describe("Maximum number of entries to return (default: 20)"),
1368
+ tool: z
1369
+ .string()
1370
+ .optional()
1371
+ .describe("Filter by tool name (e.g. 'firewall_ufw_rule')"),
1372
+ since: z
1373
+ .string()
1374
+ .optional()
1375
+ .describe("Filter by date, e.g. 'today', '2024-01-01'"),
1376
+ }, async ({ limit, tool, since }) => {
1377
+ try {
1378
+ const sections = [];
1379
+ sections.push("📜 Defense Change History");
1380
+ sections.push("=".repeat(50));
1381
+ let entries = getChangelog(limit * 5); // Get more than needed to allow filtering
1382
+ // Filter by tool name
1383
+ if (tool) {
1384
+ entries = entries.filter((e) => e.tool.toLowerCase().includes(tool.toLowerCase()));
1385
+ }
1386
+ // Filter by date
1387
+ if (since) {
1388
+ let sinceDate;
1389
+ if (since.toLowerCase() === "today") {
1390
+ sinceDate = new Date();
1391
+ sinceDate.setHours(0, 0, 0, 0);
1392
+ }
1393
+ else {
1394
+ sinceDate = new Date(since);
1395
+ }
1396
+ if (!isNaN(sinceDate.getTime())) {
1397
+ entries = entries.filter((e) => new Date(e.timestamp) >= sinceDate);
1398
+ }
1399
+ }
1400
+ // Apply limit after filtering
1401
+ entries = entries.slice(0, limit);
1402
+ if (entries.length === 0) {
1403
+ sections.push("\nNo changes recorded");
1404
+ if (tool)
1405
+ sections.push(` (filtered by tool: ${tool})`);
1406
+ if (since)
1407
+ sections.push(` (filtered by since: ${since})`);
1408
+ return { content: [createTextContent(sections.join("\n"))] };
1409
+ }
1410
+ sections.push(`\nShowing ${entries.length} entries (newest first):`);
1411
+ if (tool)
1412
+ sections.push(` Filter: tool contains '${tool}'`);
1413
+ if (since)
1414
+ sections.push(` Filter: since '${since}'`);
1415
+ for (const entry of entries) {
1416
+ sections.push("\n " + "─".repeat(40));
1417
+ sections.push(` ID: ${entry.id}`);
1418
+ sections.push(` Time: ${entry.timestamp}`);
1419
+ sections.push(` Tool: ${entry.tool}`);
1420
+ sections.push(` Action: ${entry.action}`);
1421
+ sections.push(` Target: ${entry.target}`);
1422
+ sections.push(` Dry Run: ${entry.dryRun ? "Yes" : "No"}`);
1423
+ sections.push(` Success: ${entry.success ? "✅" : "❌"}`);
1424
+ if (entry.error)
1425
+ sections.push(` Error: ${entry.error}`);
1426
+ if (entry.before)
1427
+ sections.push(` Before: ${entry.before.substring(0, 100)}`);
1428
+ if (entry.after)
1429
+ sections.push(` After: ${entry.after.substring(0, 100)}`);
1430
+ if (entry.backupPath)
1431
+ sections.push(` Backup: ${entry.backupPath}`);
1432
+ if (entry.rollbackCommand)
1433
+ sections.push(` Rollback: ${entry.rollbackCommand}`);
1434
+ }
1435
+ return { content: [createTextContent(sections.join("\n"))] };
1436
+ }
1437
+ catch (err) {
1438
+ const msg = err instanceof Error ? err.message : String(err);
1439
+ return { content: [createErrorContent(msg)], isError: true };
1440
+ }
1441
+ });
1442
+ // ── 4. security_posture (merged: score + trend + dashboard) ─────────────
1443
+ server.tool("defense_security_posture", "Security posture: calculate security score, view historical trends, or generate a posture dashboard.", {
1444
+ action: z.enum(["score", "trend", "dashboard"]).describe("Action: score=calculate score, trend=view history, dashboard=generate dashboard"),
1445
+ // trend params
1446
+ limit: z.number().optional().default(10).describe("Number of historical entries (trend action)"),
1447
+ // shared
1448
+ dryRun: z.boolean().optional().default(true).describe("Preview only"),
1449
+ }, async (params) => {
1450
+ const { action } = params;
1451
+ switch (action) {
1452
+ case "score": {
1453
+ const { dryRun } = params;
1454
+ try {
1455
+ const domains = [];
1456
+ // ── Kernel hardening (weight: 20) ──
1457
+ const kernelChecks = [
1458
+ { name: "ASLR full", key: "kernel.randomize_va_space", expected: "2" },
1459
+ { name: "dmesg restricted", key: "kernel.dmesg_restrict", expected: "1" },
1460
+ { name: "kptr restricted", key: "kernel.kptr_restrict", expected: "2" },
1461
+ { name: "SysRq disabled", key: "kernel.sysrq", expected: "0" },
1462
+ { name: "ptrace restricted", key: "kernel.yama.ptrace_scope", expected: "1" },
1463
+ { name: "IP forwarding disabled", key: "net.ipv4.ip_forward", expected: "0" },
1464
+ { name: "SYN cookies enabled", key: "net.ipv4.tcp_syncookies", expected: "1" },
1465
+ { name: "ICMP redirects disabled", key: "net.ipv4.conf.all.accept_redirects", expected: "0" },
1466
+ { name: "Source routing disabled", key: "net.ipv4.conf.all.accept_source_route", expected: "0" },
1467
+ { name: "Core dumps restricted", key: "fs.suid_dumpable", expected: "0" },
1468
+ ];
1469
+ const kernelResults = await Promise.all(kernelChecks.map(async (c) => {
1470
+ const result = await checkSysctl(c.key, c.expected);
1471
+ return {
1472
+ name: c.name,
1473
+ passed: result.passed,
1474
+ assessable: result.assessable,
1475
+ detail: result.assessable ? c.key : `${c.key} (unable to assess)`,
1476
+ };
1477
+ }));
1478
+ const assessableKernelCount = kernelResults.filter((r) => r.assessable).length;
1479
+ const kernelPassed = kernelResults.filter((r) => r.passed).length;
1480
+ const kernelScore = assessableKernelCount > 0
1481
+ ? Math.round((kernelPassed / assessableKernelCount) * 100)
1482
+ : -1;
1483
+ domains.push({
1484
+ domain: "kernel-hardening",
1485
+ score: kernelScore,
1486
+ maxScore: 100,
1487
+ checks: kernelResults.map((r) => ({ name: r.name, passed: r.passed, detail: r.detail })),
1488
+ });
1489
+ // ── Firewall (weight: 15) ──
1490
+ const fwChecks = [];
1491
+ const iptResult = await executeCommand({ command: "iptables", args: ["-L", "-n"], timeout: 10000 });
1492
+ const hasRules = iptResult.exitCode === 0 && iptResult.stdout.split("\n").length > 8;
1493
+ fwChecks.push({ name: "iptables rules present", passed: hasRules, detail: `${iptResult.stdout.split("\n").length} lines` });
1494
+ const ufwResult = await executeCommand({ command: "ufw", args: ["status"], timeout: 5000 });
1495
+ const ufwActive = ufwResult.exitCode === 0 && ufwResult.stdout.includes("active");
1496
+ fwChecks.push({ name: "UFW active", passed: ufwActive, detail: ufwResult.stdout.slice(0, 100) });
1497
+ const fwPassed = fwChecks.filter((c) => c.passed).length;
1498
+ domains.push({
1499
+ domain: "firewall",
1500
+ score: Math.round((fwPassed / fwChecks.length) * 100),
1501
+ maxScore: 100,
1502
+ checks: fwChecks,
1503
+ });
1504
+ // ── Services (weight: 15) ──
1505
+ const dangerousServices = ["telnet.socket", "rsh.socket", "rlogin.socket", "tftp.socket", "xinetd.service"];
1506
+ const svcChecks = [];
1507
+ for (const svc of dangerousServices) {
1508
+ const r = await executeCommand({ command: "systemctl", args: ["is-active", svc], timeout: 5000 });
1509
+ const inactive = r.exitCode !== 0 || r.stdout.trim() !== "active";
1510
+ svcChecks.push({ name: `${svc} disabled`, passed: inactive, detail: r.stdout.trim() });
1511
+ }
1512
+ const svcPassed = svcChecks.filter((c) => c.passed).length;
1513
+ domains.push({
1514
+ domain: "services",
1515
+ score: Math.round((svcPassed / svcChecks.length) * 100),
1516
+ maxScore: 100,
1517
+ checks: svcChecks,
1518
+ });
1519
+ // ── Users (weight: 15) ──
1520
+ const userChecks = [];
1521
+ const rootLogin = await executeCommand({ command: "passwd", args: ["-S", "root"], timeout: 5000 });
1522
+ const rootLocked = rootLogin.stdout.includes(" L ") || rootLogin.stdout.includes(" LK ");
1523
+ userChecks.push({ name: "Root account locked", passed: rootLocked, detail: rootLogin.stdout.trim().slice(0, 100) });
1524
+ const noPasswd = await executeCommand({ command: "awk", args: ["-F:", '($2 == "" ) { print $1 }', "/etc/shadow"], timeout: 5000 });
1525
+ const noEmptyPasswd = noPasswd.stdout.trim().length === 0;
1526
+ userChecks.push({ name: "No empty passwords", passed: noEmptyPasswd, detail: noPasswd.stdout.trim() || "none" });
1527
+ const uidZero = await executeCommand({ command: "awk", args: ["-F:", '($3 == 0) { print $1 }', "/etc/passwd"], timeout: 5000 });
1528
+ const onlyRoot = uidZero.stdout.trim() === "root";
1529
+ userChecks.push({ name: "Only root has UID 0", passed: onlyRoot, detail: uidZero.stdout.trim() });
1530
+ const userPassed = userChecks.filter((c) => c.passed).length;
1531
+ domains.push({
1532
+ domain: "users",
1533
+ score: Math.round((userPassed / userChecks.length) * 100),
1534
+ maxScore: 100,
1535
+ checks: userChecks,
1536
+ });
1537
+ // ── Filesystem (weight: 15) ──
1538
+ const fsChecks = [];
1539
+ const criticalFiles = [
1540
+ ["/etc/passwd", "644"],
1541
+ ["/etc/shadow", "640"],
1542
+ ["/etc/ssh/sshd_config", "600"],
1543
+ ];
1544
+ for (const [fp, expected] of criticalFiles) {
1545
+ const r = await executeCommand({ command: "stat", args: ["-c", "%a", fp], timeout: 5000 });
1546
+ const actual = r.stdout.trim();
1547
+ const ok = r.exitCode === 0 && parseInt(actual, 8) <= parseInt(expected, 8);
1548
+ fsChecks.push({ name: `${fp} permissions`, passed: ok, detail: `${actual} (expected \u2264${expected})` });
1549
+ }
1550
+ const fsPassed = fsChecks.filter((c) => c.passed).length;
1551
+ domains.push({
1552
+ domain: "filesystem",
1553
+ score: Math.round((fsPassed / fsChecks.length) * 100),
1554
+ maxScore: 100,
1555
+ checks: fsChecks,
1556
+ });
1557
+ // ── Overall score ──
1558
+ const weights = {
1559
+ "kernel-hardening": 25,
1560
+ "firewall": 20,
1561
+ "services": 15,
1562
+ "users": 20,
1563
+ "filesystem": 20,
1564
+ };
1565
+ let weightedSum = 0;
1566
+ let totalWeight = 0;
1567
+ for (const d of domains) {
1568
+ if (d.score < 0)
1569
+ continue;
1570
+ const w = weights[d.domain] ?? 10;
1571
+ weightedSum += d.score * w;
1572
+ totalWeight += w;
1573
+ }
1574
+ const overallScore = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0;
1575
+ // Save score history
1576
+ ensurePostureDir();
1577
+ const historyPath = join(POSTURE_DIR, "history.json");
1578
+ let history = [];
1579
+ try {
1580
+ if (existsSync(historyPath)) {
1581
+ history = JSON.parse(readFileSync(historyPath, "utf-8"));
1582
+ }
1583
+ }
1584
+ catch { /* start fresh */ }
1585
+ const domainScores = {};
1586
+ for (const d of domains)
1587
+ domainScores[d.domain] = d.score;
1588
+ history.push({ timestamp: new Date().toISOString(), score: overallScore, domains: domainScores });
1589
+ if (history.length > 1000)
1590
+ history = history.slice(-1000);
1591
+ writeFileSync(historyPath, JSON.stringify(history, null, 2), "utf-8");
1592
+ return {
1593
+ content: [formatToolOutput({
1594
+ overallScore,
1595
+ rating: overallScore >= 80 ? "GOOD" : overallScore >= 60 ? "FAIR" : overallScore >= 40 ? "POOR" : "CRITICAL",
1596
+ domains,
1597
+ })],
1598
+ };
1599
+ }
1600
+ catch (err) {
1601
+ const msg = err instanceof Error ? err.message : String(err);
1602
+ return { content: [createErrorContent(`Security score calculation failed: ${msg}`)], isError: true };
1603
+ }
1604
+ }
1605
+ case "trend": {
1606
+ const { limit } = params;
1607
+ try {
1608
+ ensurePostureDir();
1609
+ const historyPath = join(POSTURE_DIR, "history.json");
1610
+ if (!existsSync(historyPath)) {
1611
+ return { content: [formatToolOutput({ message: "No posture history found. Run security_posture action=score first." })] };
1612
+ }
1613
+ const history = JSON.parse(readFileSync(historyPath, "utf-8"));
1614
+ const recent = history.slice(-limit);
1615
+ return {
1616
+ content: [formatToolOutput({
1617
+ entries: recent.length,
1618
+ trend: recent,
1619
+ latestScore: recent.length > 0 ? recent[recent.length - 1].score : null,
1620
+ })],
1621
+ };
1622
+ }
1623
+ catch (err) {
1624
+ const msg = err instanceof Error ? err.message : String(err);
1625
+ return { content: [createErrorContent(`Posture trend failed: ${msg}`)], isError: true };
1626
+ }
1627
+ }
1628
+ case "dashboard": {
1629
+ try {
1630
+ ensurePostureDir();
1631
+ const historyPath = join(POSTURE_DIR, "history.json");
1632
+ let latestEntry = null;
1633
+ try {
1634
+ if (existsSync(historyPath)) {
1635
+ const history = JSON.parse(readFileSync(historyPath, "utf-8"));
1636
+ if (history.length > 0)
1637
+ latestEntry = history[history.length - 1];
1638
+ }
1639
+ }
1640
+ catch { /* no history */ }
1641
+ if (!latestEntry) {
1642
+ return { content: [formatToolOutput({ message: "No posture data available. Run security_posture action=score first." })] };
1643
+ }
1644
+ const recommendations = [];
1645
+ for (const [domain, score] of Object.entries(latestEntry.domains)) {
1646
+ if (score < 0)
1647
+ recommendations.push(`INFO: ${domain} could not be assessed`);
1648
+ else if (score < 50)
1649
+ recommendations.push(`CRITICAL: ${domain} score is ${score}/100`);
1650
+ else if (score < 80)
1651
+ recommendations.push(`MODERATE: ${domain} score is ${score}/100`);
1652
+ }
1653
+ if (recommendations.length === 0)
1654
+ recommendations.push("All domains scoring above 80.");
1655
+ const displayDomainScores = {};
1656
+ for (const [domain, score] of Object.entries(latestEntry.domains))
1657
+ displayDomainScores[domain] = score < 0 ? "N/A" : score;
1658
+ let weightedSum = 0;
1659
+ let totalWeight = 0;
1660
+ const weights = { "kernel-hardening": 25, "firewall": 20, "services": 15, "users": 20, "filesystem": 20 };
1661
+ for (const [domain, score] of Object.entries(latestEntry.domains)) {
1662
+ if (score < 0)
1663
+ continue;
1664
+ const w = weights[domain] ?? 10;
1665
+ weightedSum += score * w;
1666
+ totalWeight += w;
1667
+ }
1668
+ const overallScore = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0;
1669
+ return {
1670
+ content: [formatToolOutput({
1671
+ dashboard: {
1672
+ timestamp: latestEntry.timestamp,
1673
+ overallScore,
1674
+ rating: overallScore >= 80 ? "GOOD" : overallScore >= 60 ? "FAIR" : overallScore >= 40 ? "POOR" : "CRITICAL",
1675
+ domainScores: displayDomainScores,
1676
+ recommendations,
1677
+ nextSteps: ["Run security_posture action=score for detailed breakdown", "Address CRITICAL domains first", "Re-run periodically"],
1678
+ },
1679
+ })],
1680
+ };
1681
+ }
1682
+ catch (err) {
1683
+ const msg = err instanceof Error ? err.message : String(err);
1684
+ return { content: [createErrorContent(`Dashboard generation failed: ${msg}`)], isError: true };
1685
+ }
1686
+ }
1687
+ default:
1688
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
1689
+ }
1690
+ });
1691
+ // ── 5. scheduled_audit (merged: setup + list + remove + history) ────────
1692
+ server.tool("defense_scheduled_audit", "Scheduled security audits: create, list, remove, or read audit history.", {
1693
+ action: z.enum(["create", "list", "remove", "history"]).describe("Action: create, list, remove, history"),
1694
+ name: z.string().optional().describe("Audit job name (create/remove/history). Only [a-zA-Z0-9_-] allowed."),
1695
+ command: z.enum(["lynis audit system", "rkhunter --check --skip-keypress", "aide --check", "clamscan -r /home", "chkrootkit", "freshclam", "tiger"]).optional().describe("Audit command to schedule (create action). Must be from the approved allowlist."),
1696
+ schedule: z.string().optional().describe("Schedule cron format or systemd calendar (create action)"),
1697
+ useSystemd: z.boolean().optional().default(true).describe("Use systemd timer vs cron (create action)"),
1698
+ lines: z.number().optional().default(100).describe("Number of recent lines (history action)"),
1699
+ dryRun: z.boolean().optional().default(true).describe("Preview only"),
1700
+ }, async (params) => {
1701
+ const { action } = params;
1702
+ switch (action) {
1703
+ case "create": {
1704
+ const { name, command: auditCommand, schedule, useSystemd, dryRun } = params;
1705
+ if (!name)
1706
+ return { content: [createErrorContent("name is required for create action")], isError: true };
1707
+ if (!auditCommand)
1708
+ return { content: [createErrorContent("command is required for create action")], isError: true };
1709
+ if (!schedule)
1710
+ return { content: [createErrorContent("schedule is required for create action")], isError: true };
1711
+ try {
1712
+ // Validate audit job name — used in file paths and systemd unit names
1713
+ if (!AUDIT_NAME_RE.test(name)) {
1714
+ return { content: [createErrorContent(`Invalid audit name: '${name}'. Only [a-zA-Z0-9_-] allowed.`)], isError: true };
1715
+ }
1716
+ // TOOL-004 remediation: validate schedule format to prevent injection
1717
+ const validatedSchedule = validateSchedule(schedule, useSystemd);
1718
+ // Validate command against the strict audit command allowlist
1719
+ const allowedEntry = ALLOWED_AUDIT_COMMANDS[auditCommand];
1720
+ if (!allowedEntry) {
1721
+ return { content: [createErrorContent(`Command not in scheduled audit allowlist: '${auditCommand}'. Allowed: ${Object.keys(ALLOWED_AUDIT_COMMANDS).join(", ")}`)], isError: true };
1722
+ }
1723
+ // Resolve the command binary to its absolute path via the command allowlist
1724
+ let resolvedBinaryPath;
1725
+ try {
1726
+ resolvedBinaryPath = resolveCommand(allowedEntry.command);
1727
+ }
1728
+ catch {
1729
+ return { content: [createErrorContent(`Audit command binary '${allowedEntry.command}' not found on this system. Install it first.`)], isError: true };
1730
+ }
1731
+ // Build the resolved command line with absolute path (no bash -c)
1732
+ const resolvedCommandLine = [resolvedBinaryPath, ...allowedEntry.args].join(" ");
1733
+ const safety = await SafeguardRegistry.getInstance().checkSafety("setup_scheduled_audit", { name });
1734
+ ensureAuditLogDir();
1735
+ const logFile = join(AUDIT_LOG_DIR, `${name}.log`);
1736
+ if (useSystemd) {
1737
+ // Use direct ExecStart with StandardOutput/StandardError — no bash -c shell wrapper
1738
+ const serviceContent = `[Unit]\nDescription=Kali Defense Scheduled Audit: ${name}\n\n[Service]\nType=oneshot\nExecStart=${resolvedCommandLine}\nStandardOutput=append:${logFile}\nStandardError=append:${logFile}\n`;
1739
+ const timerContent = `[Unit]\nDescription=Timer for ${name} audit\n\n[Timer]\nOnCalendar=${validatedSchedule}\nPersistent=true\n\n[Install]\nWantedBy=timers.target\n`;
1740
+ const servicePath = `/etc/systemd/system/kali-audit-${name}.service`;
1741
+ const timerPath = `/etc/systemd/system/kali-audit-${name}.timer`;
1742
+ if (dryRun) {
1743
+ return { content: [formatToolOutput({ dryRun: true, type: "systemd", servicePath, timerPath, serviceContent, timerContent, warnings: safety.warnings, enableCommand: `systemctl enable --now kali-audit-${name}.timer` })] };
1744
+ }
1745
+ writeFileSync(servicePath, serviceContent, "utf-8");
1746
+ writeFileSync(timerPath, timerContent, "utf-8");
1747
+ await executeCommand({ command: "systemctl", args: ["daemon-reload"], timeout: 10000 });
1748
+ const enable = await executeCommand({ command: "systemctl", args: ["enable", "--now", `kali-audit-${name}.timer`], timeout: 10000 });
1749
+ logChange(createChangeEntry({ tool: "defense_scheduled_audit", action: `Create systemd timer for ${name}`, target: timerPath, dryRun: false, success: enable.exitCode === 0, rollbackCommand: `systemctl disable --now kali-audit-${name}.timer && rm ${servicePath} ${timerPath}` }));
1750
+ return { content: [formatToolOutput({ success: enable.exitCode === 0, type: "systemd", name, servicePath, timerPath, enabled: enable.exitCode === 0 })] };
1751
+ }
1752
+ // Cron approach — uses resolved absolute path and validated schedule, not raw user input
1753
+ const cronLine = `${validatedSchedule} ${resolvedCommandLine} >> ${logFile} 2>&1 # kali-audit-${name}`;
1754
+ if (dryRun) {
1755
+ return { content: [formatToolOutput({ dryRun: true, type: "cron", cronLine, warnings: safety.warnings })] };
1756
+ }
1757
+ const currentCron = await executeCommand({ command: "crontab", args: ["-l"], timeout: 5000 });
1758
+ const existing = currentCron.exitCode === 0 ? currentCron.stdout : "";
1759
+ if (existing.includes(`kali-audit-${name}`)) {
1760
+ return { content: [createErrorContent(`Cron job 'kali-audit-${name}' already exists. Remove it first.`)], isError: true };
1761
+ }
1762
+ const newCron = existing.trimEnd() + "\n" + cronLine + "\n";
1763
+ const install = await executeCommand({ command: "crontab", args: ["-"], stdin: newCron, timeout: 5000 });
1764
+ logChange(createChangeEntry({ tool: "defense_scheduled_audit", action: `Create cron job for ${name}`, target: "crontab", dryRun: false, success: install.exitCode === 0 }));
1765
+ return { content: [formatToolOutput({ success: install.exitCode === 0, type: "cron", name, cronLine })] };
1766
+ }
1767
+ catch (err) {
1768
+ const msg = err instanceof Error ? err.message : String(err);
1769
+ return { content: [createErrorContent(`Scheduled audit setup failed: ${msg}`)], isError: true };
1770
+ }
1771
+ }
1772
+ case "list": {
1773
+ try {
1774
+ const audits = [];
1775
+ const timers = await executeCommand({ command: "systemctl", args: ["list-timers", "--no-pager", "--plain"], timeout: 10000 });
1776
+ if (timers.exitCode === 0) {
1777
+ for (const line of timers.stdout.split("\n")) {
1778
+ if (line.includes("kali-audit-")) {
1779
+ const match = line.match(/kali-audit-(\S+)/);
1780
+ if (match)
1781
+ audits.push({ name: match[1].replace(".timer", ""), type: "systemd", schedule: line.trim(), status: "active" });
1782
+ }
1783
+ }
1784
+ }
1785
+ const cron = await executeCommand({ command: "crontab", args: ["-l"], timeout: 5000 });
1786
+ if (cron.exitCode === 0) {
1787
+ for (const line of cron.stdout.split("\n")) {
1788
+ if (line.includes("kali-audit-")) {
1789
+ const match = line.match(/# kali-audit-(\S+)/);
1790
+ if (match)
1791
+ audits.push({ name: match[1], type: "cron", schedule: line.split("#")[0].trim(), status: "active" });
1792
+ }
1793
+ }
1794
+ }
1795
+ return { content: [formatToolOutput({ totalAudits: audits.length, audits })] };
1796
+ }
1797
+ catch (err) {
1798
+ const msg = err instanceof Error ? err.message : String(err);
1799
+ return { content: [createErrorContent(`List audits failed: ${msg}`)], isError: true };
1800
+ }
1801
+ }
1802
+ case "remove": {
1803
+ const { name, dryRun } = params;
1804
+ try {
1805
+ if (!name)
1806
+ return { content: [createErrorContent("name is required for remove action")], isError: true };
1807
+ const actions = [];
1808
+ const timerPath = `/etc/systemd/system/kali-audit-${name}.timer`;
1809
+ const servicePath = `/etc/systemd/system/kali-audit-${name}.service`;
1810
+ const hasTimer = existsSync(timerPath);
1811
+ const cron = await executeCommand({ command: "crontab", args: ["-l"], timeout: 5000 });
1812
+ const hasCron = cron.exitCode === 0 && cron.stdout.includes(`kali-audit-${name}`);
1813
+ if (!hasTimer && !hasCron) {
1814
+ return { content: [createErrorContent(`No scheduled audit found with name: ${name}`)], isError: true };
1815
+ }
1816
+ if (dryRun) {
1817
+ return { content: [formatToolOutput({ dryRun: true, name, hasSystemdTimer: hasTimer, hasCronJob: hasCron, actions: [hasTimer ? `systemctl disable --now kali-audit-${name}.timer && rm ${timerPath} ${servicePath}` : null, hasCron ? `Remove cron line containing kali-audit-${name}` : null].filter(Boolean) })] };
1818
+ }
1819
+ if (hasTimer) {
1820
+ await executeCommand({ command: "systemctl", args: ["disable", "--now", `kali-audit-${name}.timer`], timeout: 10000 });
1821
+ await executeCommand({ command: "rm", args: ["-f", timerPath, servicePath], timeout: 5000 });
1822
+ await executeCommand({ command: "systemctl", args: ["daemon-reload"], timeout: 10000 });
1823
+ actions.push({ action: "Removed systemd timer", success: true });
1824
+ }
1825
+ if (hasCron) {
1826
+ const lines = cron.stdout.split("\n").filter((l) => !l.includes(`kali-audit-${name}`));
1827
+ await executeCommand({ command: "crontab", args: ["-"], stdin: lines.join("\n") + "\n", timeout: 5000 });
1828
+ actions.push({ action: "Removed cron job", success: true });
1829
+ }
1830
+ logChange(createChangeEntry({ tool: "defense_scheduled_audit", action: `Remove scheduled audit ${name}`, target: name, dryRun: false, success: true }));
1831
+ return { content: [formatToolOutput({ name, actions })] };
1832
+ }
1833
+ catch (err) {
1834
+ const msg = err instanceof Error ? err.message : String(err);
1835
+ return { content: [createErrorContent(`Remove audit failed: ${msg}`)], isError: true };
1836
+ }
1837
+ }
1838
+ case "history": {
1839
+ const { name, lines } = params;
1840
+ try {
1841
+ if (!name)
1842
+ return { content: [createErrorContent("name is required for history action")], isError: true };
1843
+ ensureAuditLogDir();
1844
+ const logFile = join(AUDIT_LOG_DIR, `${name}.log`);
1845
+ if (!existsSync(logFile)) {
1846
+ return { content: [formatToolOutput({ name, message: `No audit log found at ${logFile}` })] };
1847
+ }
1848
+ const result = await executeCommand({ command: "tail", args: ["-n", String(lines), logFile], timeout: 10000 });
1849
+ return { content: [formatToolOutput({ name, logFile, lines: result.stdout.trim().split("\n"), totalLines: result.stdout.trim().split("\n").length })] };
1850
+ }
1851
+ catch (err) {
1852
+ const msg = err instanceof Error ? err.message : String(err);
1853
+ return { content: [createErrorContent(`Audit history failed: ${msg}`)], isError: true };
1854
+ }
1855
+ }
1856
+ default:
1857
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
1858
+ }
1859
+ });
1860
+ // ── 6. auto_remediate ─────────────────────────────────────────────────────
1861
+ server.tool("auto_remediate", "Automated remediation: plan findings, apply fixes (with dry-run), rollback sessions, and check status.", {
1862
+ action: z.enum(["plan", "apply", "rollback_session", "status"]).describe("Action: plan=generate remediation plan, apply=execute remediations, rollback_session=undo a session, status=view sessions"),
1863
+ source: z.enum(["compliance", "hardening", "access_control", "firewall", "all"]).optional().default("all").describe("Source of findings to remediate (plan/apply)"),
1864
+ severity_filter: z.enum(["critical", "high", "medium", "low"]).optional().default("medium").describe("Minimum severity level to include (plan/apply)"),
1865
+ dry_run: z.boolean().optional().default(true).describe("If true, only show what would be done without executing (apply action)"),
1866
+ session_id: z.string().optional().describe("Remediation session ID (rollback_session/status)"),
1867
+ output_format: z.enum(["text", "json"]).optional().default("text").describe("Output format"),
1868
+ }, async (params) => {
1869
+ const { action, source, severity_filter, dry_run, session_id, output_format } = params;
1870
+ switch (action) {
1871
+ // ── plan ──────────────────────────────────────────────────────────
1872
+ case "plan": {
1873
+ try {
1874
+ const effectiveSource = source ?? "all";
1875
+ const effectiveSeverity = (severity_filter ?? "medium");
1876
+ const findings = await gatherRemediationFindings(effectiveSource, effectiveSeverity);
1877
+ if (output_format === "json") {
1878
+ return {
1879
+ content: [formatToolOutput({
1880
+ action: "plan",
1881
+ source: effectiveSource,
1882
+ severity_filter: effectiveSeverity,
1883
+ total_findings: findings.length,
1884
+ findings: findings.map(f => ({
1885
+ finding_id: f.finding_id,
1886
+ description: f.description,
1887
+ severity: f.severity,
1888
+ remediation_command: `${f.remediation_command} ${f.remediation_args.join(" ")}`,
1889
+ risk_level: f.risk_level,
1890
+ category: f.category,
1891
+ })),
1892
+ })],
1893
+ };
1894
+ }
1895
+ const sections = [];
1896
+ sections.push("🔍 Auto-Remediation Plan");
1897
+ sections.push("=".repeat(50));
1898
+ sections.push(`Source: ${effectiveSource} | Severity filter: >= ${effectiveSeverity}`);
1899
+ sections.push(`Total findings: ${findings.length}`);
1900
+ if (findings.length === 0) {
1901
+ sections.push("\n✅ No findings match the current filters. System looks good!");
1902
+ return { content: [createTextContent(sections.join("\n"))] };
1903
+ }
1904
+ for (const f of findings) {
1905
+ sections.push("");
1906
+ sections.push(` [${f.severity.toUpperCase()}] ${f.finding_id}: ${f.description}`);
1907
+ sections.push(` Category: ${f.category}`);
1908
+ sections.push(` Fix: ${f.remediation_command} ${f.remediation_args.join(" ")}`);
1909
+ sections.push(` Risk: ${f.risk_level}`);
1910
+ }
1911
+ const safeCount = findings.filter(f => f.risk_level === "safe").length;
1912
+ const moderateCount = findings.filter(f => f.risk_level === "moderate").length;
1913
+ const riskyCount = findings.filter(f => f.risk_level === "risky").length;
1914
+ sections.push("\n── Plan Summary ──");
1915
+ sections.push(` Safe: ${safeCount} | Moderate: ${moderateCount} | Risky: ${riskyCount}`);
1916
+ sections.push(" Use action=apply with dry_run=false to execute safe remediations.");
1917
+ return { content: [createTextContent(sections.join("\n"))] };
1918
+ }
1919
+ catch (err) {
1920
+ const msg = err instanceof Error ? err.message : String(err);
1921
+ return { content: [createErrorContent(`Remediation plan failed: ${msg}`)], isError: true };
1922
+ }
1923
+ }
1924
+ // ── apply ─────────────────────────────────────────────────────────
1925
+ case "apply": {
1926
+ try {
1927
+ const effectiveSource = source ?? "all";
1928
+ const effectiveSeverity = (severity_filter ?? "medium");
1929
+ const effectiveDryRun = dry_run ?? true;
1930
+ const findings = await gatherRemediationFindings(effectiveSource, effectiveSeverity);
1931
+ if (findings.length === 0) {
1932
+ const msg = "No findings match the current filters. Nothing to remediate.";
1933
+ if (output_format === "json") {
1934
+ return { content: [formatToolOutput({ action: "apply", dry_run: effectiveDryRun, message: msg, actions_taken: 0 })] };
1935
+ }
1936
+ return { content: [createTextContent(`✅ ${msg}`)] };
1937
+ }
1938
+ if (effectiveDryRun) {
1939
+ // Dry run — show what would be done
1940
+ if (output_format === "json") {
1941
+ return {
1942
+ content: [formatToolOutput({
1943
+ action: "apply",
1944
+ dry_run: true,
1945
+ total_findings: findings.length,
1946
+ would_execute: findings.filter(f => f.risk_level === "safe").map(f => ({
1947
+ finding_id: f.finding_id,
1948
+ description: f.description,
1949
+ command: `${f.remediation_command} ${f.remediation_args.join(" ")}`,
1950
+ risk_level: f.risk_level,
1951
+ })),
1952
+ would_skip: findings.filter(f => f.risk_level !== "safe").map(f => ({
1953
+ finding_id: f.finding_id,
1954
+ description: f.description,
1955
+ risk_level: f.risk_level,
1956
+ reason: `risk_level is ${f.risk_level} (only safe actions auto-executed)`,
1957
+ })),
1958
+ })],
1959
+ };
1960
+ }
1961
+ const sections = [];
1962
+ sections.push("🔒 Auto-Remediation — DRY RUN");
1963
+ sections.push("=".repeat(50));
1964
+ sections.push("[DRY RUN] No changes will be made.\n");
1965
+ const safeFindings = findings.filter(f => f.risk_level === "safe");
1966
+ const skippedFindings = findings.filter(f => f.risk_level !== "safe");
1967
+ if (safeFindings.length > 0) {
1968
+ sections.push("Would execute:");
1969
+ for (const f of safeFindings) {
1970
+ sections.push(` ✅ ${f.finding_id}: ${f.remediation_command} ${f.remediation_args.join(" ")}`);
1971
+ sections.push(` ${f.description}`);
1972
+ }
1973
+ }
1974
+ if (skippedFindings.length > 0) {
1975
+ sections.push("\nWould skip (too risky for auto-execution):");
1976
+ for (const f of skippedFindings) {
1977
+ sections.push(` ⏭️ ${f.finding_id}: ${f.description} [${f.risk_level}]`);
1978
+ }
1979
+ }
1980
+ sections.push("\nSet dry_run=false to execute safe remediations.");
1981
+ return { content: [createTextContent(sections.join("\n"))] };
1982
+ }
1983
+ // ── Live execution (dry_run=false) ──
1984
+ const sessionId = generateSessionId();
1985
+ const session = {
1986
+ session_id: sessionId,
1987
+ created_at: new Date().toISOString(),
1988
+ status: "in_progress",
1989
+ actions: [],
1990
+ summary: { total: 0, successful: 0, failed: 0, skipped: 0, rolled_back: 0 },
1991
+ };
1992
+ const sections = [];
1993
+ sections.push("🔧 Auto-Remediation — LIVE EXECUTION");
1994
+ sections.push("=".repeat(50));
1995
+ sections.push(`Session ID: ${sessionId}\n`);
1996
+ for (const f of findings) {
1997
+ session.summary.total++;
1998
+ // Only execute safe remediations
1999
+ if (f.risk_level !== "safe") {
2000
+ session.actions.push({
2001
+ finding_id: f.finding_id,
2002
+ description: f.description,
2003
+ remediation_command: f.remediation_command,
2004
+ remediation_args: f.remediation_args,
2005
+ rollback_command: f.rollback_command,
2006
+ rollback_args: f.rollback_args,
2007
+ before_state: "",
2008
+ after_state: "",
2009
+ status: "skipped",
2010
+ error: `risk_level is ${f.risk_level} (only safe actions auto-executed)`,
2011
+ timestamp: new Date().toISOString(),
2012
+ });
2013
+ session.summary.skipped++;
2014
+ sections.push(` ⏭️ ${f.finding_id}: SKIPPED (${f.risk_level} risk)`);
2015
+ continue;
2016
+ }
2017
+ // Verify command is in our remediation allowlist
2018
+ if (!REMEDIATION_ALLOWLIST.has(f.remediation_command)) {
2019
+ session.actions.push({
2020
+ finding_id: f.finding_id,
2021
+ description: f.description,
2022
+ remediation_command: f.remediation_command,
2023
+ remediation_args: f.remediation_args,
2024
+ rollback_command: f.rollback_command,
2025
+ rollback_args: f.rollback_args,
2026
+ before_state: "",
2027
+ after_state: "",
2028
+ status: "skipped",
2029
+ error: `Command '${f.remediation_command}' not in remediation allowlist`,
2030
+ timestamp: new Date().toISOString(),
2031
+ });
2032
+ session.summary.skipped++;
2033
+ sections.push(` ⏭️ ${f.finding_id}: SKIPPED (command not in remediation allowlist)`);
2034
+ continue;
2035
+ }
2036
+ // Capture before state
2037
+ let beforeState = "";
2038
+ const setArg = f.remediation_args.find(a => a.includes("="));
2039
+ if (setArg && f.remediation_command === "sysctl") {
2040
+ const key = setArg.substring(0, setArg.indexOf("="));
2041
+ const beforeResult = await runRemediateCmd("sysctl", ["-n", key]);
2042
+ beforeState = beforeResult.stdout.trim();
2043
+ }
2044
+ // Execute remediation
2045
+ const result = await runRemediateCmd(f.remediation_command, f.remediation_args);
2046
+ // Capture after state
2047
+ let afterState = "";
2048
+ if (setArg && f.remediation_command === "sysctl") {
2049
+ const key = setArg.substring(0, setArg.indexOf("="));
2050
+ const afterResult = await runRemediateCmd("sysctl", ["-n", key]);
2051
+ afterState = afterResult.stdout.trim();
2052
+ }
2053
+ if (result.exitCode === 0) {
2054
+ session.actions.push({
2055
+ finding_id: f.finding_id,
2056
+ description: f.description,
2057
+ remediation_command: f.remediation_command,
2058
+ remediation_args: f.remediation_args,
2059
+ rollback_command: f.rollback_command,
2060
+ rollback_args: f.rollback_args,
2061
+ before_state: beforeState,
2062
+ after_state: afterState,
2063
+ status: "success",
2064
+ timestamp: new Date().toISOString(),
2065
+ });
2066
+ session.summary.successful++;
2067
+ sections.push(` ✅ ${f.finding_id}: ${f.description}`);
2068
+ }
2069
+ else {
2070
+ session.actions.push({
2071
+ finding_id: f.finding_id,
2072
+ description: f.description,
2073
+ remediation_command: f.remediation_command,
2074
+ remediation_args: f.remediation_args,
2075
+ rollback_command: f.rollback_command,
2076
+ rollback_args: f.rollback_args,
2077
+ before_state: beforeState,
2078
+ after_state: afterState,
2079
+ status: "failed",
2080
+ error: result.stderr.substring(0, 200),
2081
+ timestamp: new Date().toISOString(),
2082
+ });
2083
+ session.summary.failed++;
2084
+ sections.push(` ❌ ${f.finding_id}: FAILED — ${result.stderr.substring(0, 100)}`);
2085
+ }
2086
+ }
2087
+ // Determine final session status
2088
+ if (session.summary.failed === 0 && session.summary.skipped === 0) {
2089
+ session.status = "completed";
2090
+ }
2091
+ else if (session.summary.successful > 0) {
2092
+ session.status = "partial";
2093
+ }
2094
+ else {
2095
+ session.status = "completed";
2096
+ }
2097
+ // Save session file
2098
+ try {
2099
+ const sessionPath = join(REMEDIATION_SESSIONS_DIR, `${sessionId}.json`);
2100
+ secureWriteFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
2101
+ sections.push(`\nSession saved: ${sessionPath}`);
2102
+ }
2103
+ catch (writeErr) {
2104
+ const msg = writeErr instanceof Error ? writeErr.message : String(writeErr);
2105
+ sections.push(`\n⚠️ Failed to save session: ${msg}`);
2106
+ }
2107
+ sections.push("\n── Summary ──");
2108
+ sections.push(` Total: ${session.summary.total} | Success: ${session.summary.successful} | Failed: ${session.summary.failed} | Skipped: ${session.summary.skipped}`);
2109
+ sections.push(` Session ID: ${sessionId} (use with rollback_session to undo)`);
2110
+ if (output_format === "json") {
2111
+ return { content: [formatToolOutput(session)] };
2112
+ }
2113
+ return { content: [createTextContent(sections.join("\n"))] };
2114
+ }
2115
+ catch (err) {
2116
+ const msg = err instanceof Error ? err.message : String(err);
2117
+ return { content: [createErrorContent(`Remediation apply failed: ${msg}`)], isError: true };
2118
+ }
2119
+ }
2120
+ // ── rollback_session ──────────────────────────────────────────────
2121
+ case "rollback_session": {
2122
+ try {
2123
+ if (!session_id) {
2124
+ return { content: [createErrorContent("session_id is required for rollback_session action")], isError: true };
2125
+ }
2126
+ const sessionPath = join(REMEDIATION_SESSIONS_DIR, `${session_id}.json`);
2127
+ if (!existsSync(sessionPath)) {
2128
+ return { content: [createErrorContent(`Session not found: ${session_id}`)], isError: true };
2129
+ }
2130
+ let session;
2131
+ try {
2132
+ session = JSON.parse(readFileSync(sessionPath, "utf-8"));
2133
+ }
2134
+ catch {
2135
+ return { content: [createErrorContent(`Failed to parse session file: ${sessionPath}`)], isError: true };
2136
+ }
2137
+ const sections = [];
2138
+ sections.push("⏪ Rollback Session");
2139
+ sections.push("=".repeat(50));
2140
+ sections.push(`Session: ${session_id}`);
2141
+ // Get successful actions to rollback (in reverse order)
2142
+ const actionsToRollback = session.actions
2143
+ .filter(a => a.status === "success")
2144
+ .reverse();
2145
+ if (actionsToRollback.length === 0) {
2146
+ sections.push("\nNo successful actions to roll back.");
2147
+ if (output_format === "json") {
2148
+ return { content: [formatToolOutput({ session_id, actions_rolled_back: 0, message: "No successful actions to roll back" })] };
2149
+ }
2150
+ return { content: [createTextContent(sections.join("\n"))] };
2151
+ }
2152
+ let rolledBack = 0;
2153
+ let errors = 0;
2154
+ for (const action of actionsToRollback) {
2155
+ const result = await runRemediateCmd(action.rollback_command, action.rollback_args);
2156
+ if (result.exitCode === 0) {
2157
+ action.status = "rolled_back";
2158
+ rolledBack++;
2159
+ sections.push(` ✅ Rolled back: ${action.finding_id} — ${action.description}`);
2160
+ }
2161
+ else {
2162
+ errors++;
2163
+ sections.push(` ❌ Rollback failed: ${action.finding_id} — ${result.stderr.substring(0, 100)}`);
2164
+ }
2165
+ }
2166
+ // Update session status and save
2167
+ session.status = "rolled_back";
2168
+ session.summary.rolled_back = rolledBack;
2169
+ try {
2170
+ secureWriteFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
2171
+ }
2172
+ catch { /* best effort */ }
2173
+ sections.push(`\n── Rollback Summary ──`);
2174
+ sections.push(` Rolled back: ${rolledBack} | Errors: ${errors}`);
2175
+ if (output_format === "json") {
2176
+ return { content: [formatToolOutput({ session_id, actions_rolled_back: rolledBack, errors, session })] };
2177
+ }
2178
+ return { content: [createTextContent(sections.join("\n"))] };
2179
+ }
2180
+ catch (err) {
2181
+ const msg = err instanceof Error ? err.message : String(err);
2182
+ return { content: [createErrorContent(`Rollback failed: ${msg}`)], isError: true };
2183
+ }
2184
+ }
2185
+ // ── status ────────────────────────────────────────────────────────
2186
+ case "status": {
2187
+ try {
2188
+ // Specific session detail
2189
+ if (session_id) {
2190
+ const sessionPath = join(REMEDIATION_SESSIONS_DIR, `${session_id}.json`);
2191
+ if (!existsSync(sessionPath)) {
2192
+ return { content: [createErrorContent(`Session not found: ${session_id}`)], isError: true };
2193
+ }
2194
+ let session;
2195
+ try {
2196
+ session = JSON.parse(readFileSync(sessionPath, "utf-8"));
2197
+ }
2198
+ catch {
2199
+ return { content: [createErrorContent(`Failed to parse session file: ${sessionPath}`)], isError: true };
2200
+ }
2201
+ if (output_format === "json") {
2202
+ return { content: [formatToolOutput(session)] };
2203
+ }
2204
+ const sections = [];
2205
+ sections.push("📊 Remediation Session Detail");
2206
+ sections.push("=".repeat(50));
2207
+ sections.push(`Session: ${session.session_id}`);
2208
+ sections.push(`Created: ${session.created_at}`);
2209
+ sections.push(`Status: ${session.status}`);
2210
+ sections.push(`Total: ${session.summary.total} | Success: ${session.summary.successful} | Failed: ${session.summary.failed} | Skipped: ${session.summary.skipped} | Rolled back: ${session.summary.rolled_back}`);
2211
+ for (const a of session.actions) {
2212
+ sections.push(`\n ${a.finding_id}: ${a.description}`);
2213
+ sections.push(` Status: ${a.status}`);
2214
+ if (a.error)
2215
+ sections.push(` Error: ${a.error}`);
2216
+ if (a.before_state)
2217
+ sections.push(` Before: ${a.before_state}`);
2218
+ if (a.after_state)
2219
+ sections.push(` After: ${a.after_state}`);
2220
+ }
2221
+ return { content: [createTextContent(sections.join("\n"))] };
2222
+ }
2223
+ // List all sessions
2224
+ if (!existsSync(REMEDIATION_SESSIONS_DIR)) {
2225
+ const msg = "No remediation sessions found. Run auto_remediate action=apply first.";
2226
+ if (output_format === "json") {
2227
+ return { content: [formatToolOutput({ sessions: [], message: msg })] };
2228
+ }
2229
+ return { content: [createTextContent(msg)] };
2230
+ }
2231
+ const files = readdirSync(REMEDIATION_SESSIONS_DIR).filter((f) => f.endsWith(".json"));
2232
+ if (files.length === 0) {
2233
+ const msg = "No remediation sessions found.";
2234
+ if (output_format === "json") {
2235
+ return { content: [formatToolOutput({ sessions: [], message: msg })] };
2236
+ }
2237
+ return { content: [createTextContent(msg)] };
2238
+ }
2239
+ const sessionSummaries = [];
2240
+ for (const file of files) {
2241
+ try {
2242
+ const data = JSON.parse(readFileSync(join(REMEDIATION_SESSIONS_DIR, file), "utf-8"));
2243
+ sessionSummaries.push({
2244
+ session_id: data.session_id,
2245
+ created_at: data.created_at,
2246
+ status: data.status,
2247
+ total: data.summary.total,
2248
+ successful: data.summary.successful,
2249
+ failed: data.summary.failed,
2250
+ rolled_back: data.summary.rolled_back,
2251
+ });
2252
+ }
2253
+ catch { /* skip unparseable files */ }
2254
+ }
2255
+ if (output_format === "json") {
2256
+ return { content: [formatToolOutput({ total_sessions: sessionSummaries.length, sessions: sessionSummaries })] };
2257
+ }
2258
+ const sections = [];
2259
+ sections.push("📊 Remediation Sessions");
2260
+ sections.push("=".repeat(50));
2261
+ sections.push(`Total sessions: ${sessionSummaries.length}\n`);
2262
+ for (const s of sessionSummaries) {
2263
+ sections.push(` ${s.session_id}`);
2264
+ sections.push(` Created: ${s.created_at} | Status: ${s.status}`);
2265
+ sections.push(` Actions: ${s.total} total, ${s.successful} success, ${s.failed} failed, ${s.rolled_back} rolled back`);
2266
+ }
2267
+ return { content: [createTextContent(sections.join("\n"))] };
2268
+ }
2269
+ catch (err) {
2270
+ const msg = err instanceof Error ? err.message : String(err);
2271
+ return { content: [createErrorContent(`Status check failed: ${msg}`)], isError: true };
2272
+ }
2273
+ }
2274
+ default:
2275
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
2276
+ }
2277
+ });
2278
+ }