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,1667 @@
1
+ /**
2
+ * Encryption and cryptography tools for Kali Defense MCP Server.
3
+ *
4
+ * Registers 4 tools: crypto_tls (actions: remote_audit, cert_expiry, config_audit),
5
+ * crypto_gpg_keys, crypto_luks_manage, crypto_file_hash.
6
+ */
7
+ import { z } from "zod";
8
+ import { executeCommand } from "../core/executor.js";
9
+ import { getConfig, getToolTimeout } from "../core/config.js";
10
+ import { createTextContent, createErrorContent, formatToolOutput, } from "../core/parsers.js";
11
+ import { logChange, createChangeEntry } from "../core/changelog.js";
12
+ import { validateTarget, sanitizeArgs, validateCertPath, } from "../core/sanitizer.js";
13
+ import { validateToolPath } from "../core/sanitizer.js";
14
+ import { spawnSafe } from "../core/spawn-safe.js";
15
+ // ── Helpers ────────────────────────────────────────────────────────────────
16
+ /** Reject paths containing `..` directory-traversal sequences. */
17
+ const PATH_TRAVERSAL_RE = /(^|[\/\\])\.\.([\/\\]|$)/;
18
+ function assertNoTraversal(p) {
19
+ if (PATH_TRAVERSAL_RE.test(p)) {
20
+ throw new Error("Path contains forbidden directory traversal (..)");
21
+ }
22
+ }
23
+ // ── TOOL-023 remediation: encryption parameter validation ──────────────────
24
+ /** Allowed encryption algorithms (explicit allowlist) */
25
+ const ALLOWED_ALGORITHMS = new Set([
26
+ "aes-256-gcm",
27
+ "aes-256-cbc",
28
+ "aes-128-gcm",
29
+ "aes-128-cbc",
30
+ "chacha20-poly1305",
31
+ ]);
32
+ /** Allowed directories for key file paths */
33
+ const ALLOWED_KEY_DIRS = ["/etc/ssl", "/etc/pki", "/home", "/root", "/tmp", "/var/lib", "/opt"];
34
+ /**
35
+ * Validate an encryption algorithm against the explicit allowlist.
36
+ */
37
+ function validateAlgorithm(algorithm) {
38
+ const lower = algorithm.trim().toLowerCase();
39
+ if (!ALLOWED_ALGORITHMS.has(lower)) {
40
+ throw new Error(`Algorithm '${algorithm}' is not allowed. Allowed algorithms: ${[...ALLOWED_ALGORITHMS].join(", ")}`);
41
+ }
42
+ return lower;
43
+ }
44
+ /**
45
+ * Validate a key file path for traversal and containment within allowed directories.
46
+ */
47
+ function validateKeyPath(keyPath) {
48
+ return validateToolPath(keyPath, ALLOWED_KEY_DIRS, "Key file path");
49
+ }
50
+ const WEAK_CIPHERS = [
51
+ "RC4",
52
+ "DES",
53
+ "NULL",
54
+ "EXPORT",
55
+ "anon",
56
+ "MD5",
57
+ "RC2",
58
+ "SEED",
59
+ "IDEA",
60
+ ];
61
+ const WEAK_PROTOCOLS = ["SSLv2", "SSLv3", "TLSv1.0", "TLSv1.1", "TLSv1 "];
62
+ function checkWeakCiphers(output) {
63
+ const found = [];
64
+ for (const cipher of WEAK_CIPHERS) {
65
+ if (output.toUpperCase().includes(cipher.toUpperCase())) {
66
+ found.push(cipher);
67
+ }
68
+ }
69
+ return found;
70
+ }
71
+ function checkWeakProtocols(output) {
72
+ const found = [];
73
+ for (const proto of WEAK_PROTOCOLS) {
74
+ if (output.includes(proto)) {
75
+ found.push(proto.trim());
76
+ }
77
+ }
78
+ return found;
79
+ }
80
+ /**
81
+ * Run a command via spawnSafe and collect output as a promise.
82
+ * Handles errors gracefully — returns error info instead of throwing.
83
+ */
84
+ async function runCertCommand(command, args, timeoutMs = 30_000, stdinData) {
85
+ return new Promise((resolve) => {
86
+ let child;
87
+ try {
88
+ child = spawnSafe(command, args);
89
+ }
90
+ catch (err) {
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ resolve({ stdout: "", stderr: msg, exitCode: -1 });
93
+ return;
94
+ }
95
+ let stdout = "";
96
+ let stderr = "";
97
+ let resolved = false;
98
+ const timer = setTimeout(() => {
99
+ if (!resolved) {
100
+ resolved = true;
101
+ child.kill("SIGTERM");
102
+ resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
103
+ }
104
+ }, timeoutMs);
105
+ child.stdout?.on("data", (data) => {
106
+ stdout += data.toString();
107
+ });
108
+ child.stderr?.on("data", (data) => {
109
+ stderr += data.toString();
110
+ });
111
+ child.on("close", (code) => {
112
+ if (!resolved) {
113
+ resolved = true;
114
+ clearTimeout(timer);
115
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
116
+ }
117
+ });
118
+ child.on("error", (err) => {
119
+ if (!resolved) {
120
+ resolved = true;
121
+ clearTimeout(timer);
122
+ resolve({ stdout, stderr: err.message, exitCode: -1 });
123
+ }
124
+ });
125
+ // Write stdin data if provided, then close
126
+ if (stdinData !== undefined && child.stdin) {
127
+ child.stdin.write(stdinData);
128
+ child.stdin.end();
129
+ }
130
+ });
131
+ }
132
+ // ── Registration entry point ───────────────────────────────────────────────
133
+ export function registerEncryptionTools(server) {
134
+ // ── 1. crypto_tls (merged: remote_audit, cert_expiry, config_audit) ───
135
+ server.tool("crypto_tls", "TLS/SSL security: audit remote host TLS config, check certificate expiry, or audit local web server TLS configuration.", {
136
+ action: z.enum(["remote_audit", "cert_expiry", "config_audit"]).describe("Action: remote_audit=audit remote TLS, cert_expiry=check cert expiry, config_audit=audit local TLS config"),
137
+ // remote_audit params
138
+ host: z.string().optional().describe("Target hostname or IP address (remote_audit/cert_expiry)"),
139
+ port: z.number().optional().default(443).describe("Target port (remote_audit/cert_expiry)"),
140
+ check_ciphers: z.boolean().optional().default(true).describe("Check for weak cipher suites (remote_audit)"),
141
+ check_protocols: z.boolean().optional().default(true).describe("Check for weak protocol versions (remote_audit)"),
142
+ check_certificate: z.boolean().optional().default(true).describe("Check certificate details (remote_audit)"),
143
+ // cert_expiry params
144
+ cert_path: z.string().optional().describe("Local certificate file path to check (cert_expiry)"),
145
+ warn_days: z.number().optional().default(30).describe("Days before expiry to issue a warning (cert_expiry)"),
146
+ // config_audit params
147
+ service: z.enum(["apache", "nginx", "system", "all"]).optional().default("all").describe("Service to audit TLS config for (config_audit)"),
148
+ }, async (params) => {
149
+ const { action } = params;
150
+ switch (action) {
151
+ // ── remote_audit ────────────────────────────────────────────
152
+ case "remote_audit": {
153
+ const { host, port, check_ciphers, check_protocols, check_certificate } = params;
154
+ if (!host) {
155
+ return { content: [createErrorContent("host is required for remote_audit action")], isError: true };
156
+ }
157
+ try {
158
+ const validHost = validateTarget(host);
159
+ const sections = [];
160
+ sections.push(`🔐 TLS/SSL Audit: ${validHost}:${port}`);
161
+ sections.push("=".repeat(50));
162
+ // Basic connection test
163
+ const connResult = await executeCommand({
164
+ command: "openssl",
165
+ args: [
166
+ "s_client",
167
+ "-connect",
168
+ `${validHost}:${port}`,
169
+ "-servername",
170
+ validHost,
171
+ "-brief",
172
+ ],
173
+ stdin: "",
174
+ toolName: "crypto_tls_audit",
175
+ timeout: getToolTimeout("crypto_tls_audit"),
176
+ });
177
+ const fullOutput = connResult.stdout + "\n" + connResult.stderr;
178
+ if (connResult.exitCode !== 0 && !fullOutput.includes("Protocol")) {
179
+ return {
180
+ content: [
181
+ createErrorContent(`Failed to connect to ${validHost}:${port}: ${connResult.stderr}`),
182
+ ],
183
+ isError: true,
184
+ };
185
+ }
186
+ sections.push("\n📡 Connection Info:");
187
+ // Extract protocol and cipher from output
188
+ const protocolMatch = fullOutput.match(/Protocol\s*:\s*(\S+)/);
189
+ const cipherMatch = fullOutput.match(/Cipher\s*:\s*(\S+)/);
190
+ if (protocolMatch)
191
+ sections.push(` Protocol: ${protocolMatch[1]}`);
192
+ if (cipherMatch)
193
+ sections.push(` Cipher: ${cipherMatch[1]}`);
194
+ // Detailed connection for more info
195
+ const detailResult = await executeCommand({
196
+ command: "openssl",
197
+ args: [
198
+ "s_client",
199
+ "-connect",
200
+ `${validHost}:${port}`,
201
+ "-servername",
202
+ validHost,
203
+ ],
204
+ stdin: "",
205
+ toolName: "crypto_tls_audit",
206
+ timeout: getToolTimeout("crypto_tls_audit"),
207
+ });
208
+ const detailOutput = detailResult.stdout + "\n" + detailResult.stderr;
209
+ // Check certificate details
210
+ if (check_certificate) {
211
+ sections.push("\n📜 Certificate Details:");
212
+ const subjectMatch = detailOutput.match(/subject=([^\n]+)/);
213
+ const issuerMatch = detailOutput.match(/issuer=([^\n]+)/);
214
+ const datesMatch = detailOutput.match(/Not Before:\s*([^\n]+)[\s\S]*?Not After\s*:\s*([^\n]+)/);
215
+ const verifyMatch = detailOutput.match(/Verify return code:\s*(\d+)\s*\(([^)]+)\)/);
216
+ if (subjectMatch)
217
+ sections.push(` Subject: ${subjectMatch[1].trim()}`);
218
+ if (issuerMatch)
219
+ sections.push(` Issuer: ${issuerMatch[1].trim()}`);
220
+ if (datesMatch) {
221
+ sections.push(` Not Before: ${datesMatch[1].trim()}`);
222
+ sections.push(` Not After: ${datesMatch[2].trim()}`);
223
+ // Check expiry
224
+ const expiryDate = new Date(datesMatch[2].trim());
225
+ const now = new Date();
226
+ const daysLeft = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
227
+ if (daysLeft < 0) {
228
+ sections.push(` ⛔ EXPIRED ${Math.abs(daysLeft)} days ago`);
229
+ }
230
+ else if (daysLeft < 30) {
231
+ sections.push(` ⚠️ WARNING: Expires in ${daysLeft} days`);
232
+ }
233
+ else {
234
+ sections.push(` ✅ Valid for ${daysLeft} more days`);
235
+ }
236
+ }
237
+ if (verifyMatch) {
238
+ const code = parseInt(verifyMatch[1], 10);
239
+ const reason = verifyMatch[2];
240
+ if (code === 0) {
241
+ sections.push(` ✅ Verification: OK`);
242
+ }
243
+ else {
244
+ sections.push(` ⛔ Verification FAILED: ${reason} (code ${code})`);
245
+ }
246
+ }
247
+ // Check for self-signed
248
+ if (detailOutput.includes("self signed certificate") ||
249
+ detailOutput.includes("self-signed")) {
250
+ sections.push(` ⚠️ Self-signed certificate detected`);
251
+ }
252
+ }
253
+ // Check for weak ciphers
254
+ if (check_ciphers) {
255
+ sections.push("\n🔑 Cipher Analysis:");
256
+ const weakFound = checkWeakCiphers(detailOutput);
257
+ if (weakFound.length > 0) {
258
+ sections.push(` ⛔ Weak ciphers detected: ${weakFound.join(", ")}`);
259
+ }
260
+ else {
261
+ sections.push(` ✅ No known weak ciphers detected in connection`);
262
+ }
263
+ }
264
+ // Check for weak protocols
265
+ if (check_protocols) {
266
+ sections.push("\n🔒 Protocol Analysis:");
267
+ const weakProtos = checkWeakProtocols(detailOutput);
268
+ if (weakProtos.length > 0) {
269
+ sections.push(` ⛔ Weak protocols detected: ${weakProtos.join(", ")}`);
270
+ }
271
+ else {
272
+ sections.push(` ✅ No weak protocols detected in connection`);
273
+ }
274
+ // Test specific weak protocols
275
+ const testProtocols = [
276
+ { name: "TLSv1", arg: "-tls1" },
277
+ { name: "TLSv1.1", arg: "-tls1_1" },
278
+ { name: "TLSv1.2", arg: "-tls1_2" },
279
+ ];
280
+ for (const proto of testProtocols) {
281
+ const protoResult = await executeCommand({
282
+ command: "openssl",
283
+ args: [
284
+ "s_client",
285
+ "-connect",
286
+ `${validHost}:${port}`,
287
+ "-servername",
288
+ validHost,
289
+ proto.arg,
290
+ ],
291
+ stdin: "",
292
+ toolName: "crypto_tls_audit",
293
+ timeout: 10000,
294
+ });
295
+ const protoOutput = protoResult.stdout + protoResult.stderr;
296
+ const connected = protoOutput.includes("Protocol :") ||
297
+ protoOutput.includes("Cipher :") ||
298
+ (protoResult.exitCode === 0 &&
299
+ !protoOutput.includes("no protocols available"));
300
+ if (proto.name === "TLSv1" ||
301
+ proto.name === "TLSv1.1") {
302
+ if (connected) {
303
+ sections.push(` ⚠️ ${proto.name}: Supported (deprecated, should be disabled)`);
304
+ }
305
+ else {
306
+ sections.push(` ✅ ${proto.name}: Not supported (good)`);
307
+ }
308
+ }
309
+ else {
310
+ if (connected) {
311
+ sections.push(` ✅ ${proto.name}: Supported`);
312
+ }
313
+ else {
314
+ sections.push(` ℹ️ ${proto.name}: Not supported`);
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return { content: [createTextContent(sections.join("\n"))] };
320
+ }
321
+ catch (err) {
322
+ const msg = err instanceof Error ? err.message : String(err);
323
+ return { content: [createErrorContent(msg)], isError: true };
324
+ }
325
+ }
326
+ // ── cert_expiry ─────────────────────────────────────────────
327
+ case "cert_expiry": {
328
+ const { cert_path, host, port, warn_days } = params;
329
+ try {
330
+ if (!cert_path && !host) {
331
+ return {
332
+ content: [
333
+ createErrorContent("Must specify either cert_path (local file) or host (remote check)"),
334
+ ],
335
+ isError: true,
336
+ };
337
+ }
338
+ const sections = [];
339
+ sections.push("📅 Certificate Expiry Check");
340
+ sections.push("=".repeat(40));
341
+ let endDate = "";
342
+ let subject = "";
343
+ let issuer = "";
344
+ if (cert_path) {
345
+ const validPath = validateCertPath(cert_path);
346
+ sections.push(`\nLocal certificate: ${validPath}`);
347
+ const result = await executeCommand({
348
+ command: "openssl",
349
+ args: [
350
+ "x509",
351
+ "-in",
352
+ validPath,
353
+ "-noout",
354
+ "-enddate",
355
+ "-subject",
356
+ "-issuer",
357
+ ],
358
+ toolName: "crypto_cert_expiry",
359
+ timeout: getToolTimeout("crypto_cert_expiry"),
360
+ });
361
+ if (result.exitCode !== 0) {
362
+ return {
363
+ content: [
364
+ createErrorContent(`Failed to read certificate: ${result.stderr}`),
365
+ ],
366
+ isError: true,
367
+ };
368
+ }
369
+ const endMatch = result.stdout.match(/notAfter=(.+)/);
370
+ const subjectMatch = result.stdout.match(/subject=(.+)/);
371
+ const issuerMatch = result.stdout.match(/issuer=(.+)/);
372
+ if (endMatch)
373
+ endDate = endMatch[1].trim();
374
+ if (subjectMatch)
375
+ subject = subjectMatch[1].trim();
376
+ if (issuerMatch)
377
+ issuer = issuerMatch[1].trim();
378
+ }
379
+ else if (host) {
380
+ const validHost = validateTarget(host);
381
+ sections.push(`\nRemote host: ${validHost}:${port}`);
382
+ const result = await executeCommand({
383
+ command: "openssl",
384
+ args: [
385
+ "s_client",
386
+ "-connect",
387
+ `${validHost}:${port}`,
388
+ "-servername",
389
+ validHost,
390
+ ],
391
+ stdin: "",
392
+ toolName: "crypto_cert_expiry",
393
+ timeout: getToolTimeout("crypto_cert_expiry"),
394
+ });
395
+ const fullOutput = result.stdout + "\n" + result.stderr;
396
+ // Extract dates from the connection output
397
+ const notAfterMatch = fullOutput.match(/Not After\s*:\s*([^\n]+)/);
398
+ const subjectMatch = fullOutput.match(/subject=([^\n]+)/);
399
+ const issuerMatch = fullOutput.match(/issuer=([^\n]+)/);
400
+ if (notAfterMatch)
401
+ endDate = notAfterMatch[1].trim();
402
+ if (subjectMatch)
403
+ subject = subjectMatch[1].trim();
404
+ if (issuerMatch)
405
+ issuer = issuerMatch[1].trim();
406
+ if (!endDate) {
407
+ // Try parsing cert separately via pipe
408
+ const certMatch = fullOutput.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/);
409
+ if (certMatch) {
410
+ const dateResult = await executeCommand({
411
+ command: "openssl",
412
+ args: ["x509", "-noout", "-enddate", "-subject", "-issuer"],
413
+ stdin: certMatch[0],
414
+ toolName: "crypto_cert_expiry",
415
+ timeout: 10000,
416
+ });
417
+ const endM = dateResult.stdout.match(/notAfter=(.+)/);
418
+ const subM = dateResult.stdout.match(/subject=(.+)/);
419
+ const issM = dateResult.stdout.match(/issuer=(.+)/);
420
+ if (endM)
421
+ endDate = endM[1].trim();
422
+ if (subM)
423
+ subject = subM[1].trim();
424
+ if (issM)
425
+ issuer = issM[1].trim();
426
+ }
427
+ }
428
+ }
429
+ if (!endDate) {
430
+ return {
431
+ content: [
432
+ createErrorContent("Could not determine certificate expiry date"),
433
+ ],
434
+ isError: true,
435
+ };
436
+ }
437
+ if (subject)
438
+ sections.push(` Subject: ${subject}`);
439
+ if (issuer)
440
+ sections.push(` Issuer: ${issuer}`);
441
+ sections.push(` Expiry: ${endDate}`);
442
+ // Calculate days until expiry
443
+ const expiryDate = new Date(endDate);
444
+ const now = new Date();
445
+ const daysLeft = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
446
+ let status;
447
+ if (daysLeft < 0) {
448
+ status = "CRITICAL";
449
+ sections.push(`\n⛔ Status: ${status} - Certificate EXPIRED ${Math.abs(daysLeft)} days ago`);
450
+ }
451
+ else if (daysLeft <= warn_days) {
452
+ status = "WARNING";
453
+ sections.push(`\n⚠️ Status: ${status} - Certificate expires in ${daysLeft} days (threshold: ${warn_days})`);
454
+ }
455
+ else {
456
+ status = "OK";
457
+ sections.push(`\n✅ Status: ${status} - Certificate valid for ${daysLeft} more days`);
458
+ }
459
+ return { content: [createTextContent(sections.join("\n"))] };
460
+ }
461
+ catch (err) {
462
+ const msg = err instanceof Error ? err.message : String(err);
463
+ return { content: [createErrorContent(msg)], isError: true };
464
+ }
465
+ }
466
+ // ── config_audit ────────────────────────────────────────────
467
+ case "config_audit": {
468
+ const { service } = params;
469
+ try {
470
+ const sections = [];
471
+ sections.push("🔍 TLS Configuration Audit");
472
+ sections.push("=".repeat(40));
473
+ const findings = [];
474
+ // Apache audit
475
+ if (service === "apache" || service === "all") {
476
+ sections.push("\n── Apache TLS Configuration ──");
477
+ const apacheResult = await executeCommand({
478
+ command: "find",
479
+ args: ["/etc/apache2/sites-enabled/", "-type", "f", "-name", "*.conf"],
480
+ toolName: "crypto_tls",
481
+ timeout: 10000,
482
+ });
483
+ if (apacheResult.exitCode === 0 && apacheResult.stdout.trim()) {
484
+ const confFiles = apacheResult.stdout.trim().split("\n").filter((f) => f.trim());
485
+ for (const confFile of confFiles) {
486
+ const catResult = await executeCommand({
487
+ command: "cat",
488
+ args: [confFile.trim()],
489
+ toolName: "crypto_tls",
490
+ timeout: 5000,
491
+ });
492
+ if (catResult.exitCode === 0) {
493
+ const content = catResult.stdout;
494
+ sections.push(`\n File: ${confFile.trim()}`);
495
+ const protoMatch = content.match(/SSLProtocol\s+(.+)/);
496
+ if (protoMatch) {
497
+ sections.push(` SSLProtocol: ${protoMatch[1].trim()}`);
498
+ const proto = protoMatch[1];
499
+ if (proto.includes("SSLv3") || proto.includes("TLSv1 ") || proto.includes("TLSv1.0") || proto.includes("TLSv1.1")) {
500
+ findings.push({ level: "CRITICAL", msg: `${confFile}: Weak protocol in SSLProtocol: ${protoMatch[1].trim()}` });
501
+ }
502
+ }
503
+ const cipherMatch = content.match(/SSLCipherSuite\s+(.+)/);
504
+ if (cipherMatch) {
505
+ sections.push(` SSLCipherSuite: ${cipherMatch[1].trim().substring(0, 80)}...`);
506
+ const weakCiphers = checkWeakCiphers(cipherMatch[1]);
507
+ if (weakCiphers.length > 0) {
508
+ findings.push({ level: "WARNING", msg: `${confFile}: Weak ciphers found: ${weakCiphers.join(", ")}` });
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ else {
515
+ sections.push(" Apache not installed or no sites-enabled configuration found.");
516
+ }
517
+ }
518
+ // Nginx audit
519
+ if (service === "nginx" || service === "all") {
520
+ sections.push("\n── Nginx TLS Configuration ──");
521
+ const nginxResult = await executeCommand({
522
+ command: "find",
523
+ args: ["/etc/nginx/", "-type", "f", "-name", "*.conf"],
524
+ toolName: "crypto_tls",
525
+ timeout: 10000,
526
+ });
527
+ if (nginxResult.exitCode === 0 && nginxResult.stdout.trim()) {
528
+ const confFiles = nginxResult.stdout.trim().split("\n").filter((f) => f.trim());
529
+ for (const confFile of confFiles) {
530
+ const catResult = await executeCommand({
531
+ command: "cat",
532
+ args: [confFile.trim()],
533
+ toolName: "crypto_tls",
534
+ timeout: 5000,
535
+ });
536
+ if (catResult.exitCode === 0) {
537
+ const content = catResult.stdout;
538
+ if (content.includes("ssl_protocols") || content.includes("ssl_ciphers")) {
539
+ sections.push(`\n File: ${confFile.trim()}`);
540
+ const protoMatch = content.match(/ssl_protocols\s+([^;]+)/);
541
+ if (protoMatch) {
542
+ sections.push(` ssl_protocols: ${protoMatch[1].trim()}`);
543
+ const proto = protoMatch[1];
544
+ if (proto.includes("SSLv3") || proto.includes("TLSv1 ") || proto.includes("TLSv1.0") || proto.includes("TLSv1.1")) {
545
+ findings.push({ level: "CRITICAL", msg: `${confFile}: Weak protocol in ssl_protocols: ${protoMatch[1].trim()}` });
546
+ }
547
+ }
548
+ const cipherMatch = content.match(/ssl_ciphers\s+['"]?([^;'"]+)/);
549
+ if (cipherMatch) {
550
+ sections.push(` ssl_ciphers: ${cipherMatch[1].trim().substring(0, 80)}...`);
551
+ const weakCiphers = checkWeakCiphers(cipherMatch[1]);
552
+ if (weakCiphers.length > 0) {
553
+ findings.push({ level: "WARNING", msg: `${confFile}: Weak ciphers: ${weakCiphers.join(", ")}` });
554
+ }
555
+ }
556
+ }
557
+ }
558
+ }
559
+ }
560
+ else {
561
+ sections.push(" Nginx not installed or no configuration found.");
562
+ }
563
+ }
564
+ // System-wide crypto audit
565
+ if (service === "system" || service === "all") {
566
+ sections.push("\n── System-Wide Crypto Configuration ──");
567
+ const opensslResult = await executeCommand({
568
+ command: "cat",
569
+ args: ["/etc/ssl/openssl.cnf"],
570
+ toolName: "crypto_tls",
571
+ timeout: 5000,
572
+ });
573
+ if (opensslResult.exitCode === 0) {
574
+ sections.push("\n OpenSSL config (/etc/ssl/openssl.cnf): Found");
575
+ const minProtoMatch = opensslResult.stdout.match(/MinProtocol\s*=\s*(\S+)/);
576
+ if (minProtoMatch)
577
+ sections.push(` MinProtocol: ${minProtoMatch[1]}`);
578
+ const cipherStringMatch = opensslResult.stdout.match(/CipherString\s*=\s*(\S+)/);
579
+ if (cipherStringMatch)
580
+ sections.push(` CipherString: ${cipherStringMatch[1]}`);
581
+ }
582
+ else {
583
+ sections.push(" OpenSSL config: Not found at /etc/ssl/openssl.cnf");
584
+ }
585
+ const policyResult = await executeCommand({
586
+ command: "cat",
587
+ args: ["/etc/crypto-policies/config"],
588
+ toolName: "crypto_tls",
589
+ timeout: 5000,
590
+ });
591
+ if (policyResult.exitCode === 0 && policyResult.stdout.trim()) {
592
+ sections.push(`\n System crypto policy: ${policyResult.stdout.trim()}`);
593
+ const policy = policyResult.stdout.trim().toUpperCase();
594
+ if (policy === "LEGACY" || policy === "DEFAULT") {
595
+ findings.push({ level: "WARNING", msg: `System crypto policy is '${policyResult.stdout.trim()}' - consider using FUTURE or FIPS` });
596
+ }
597
+ }
598
+ const versionResult = await executeCommand({
599
+ command: "openssl",
600
+ args: ["version"],
601
+ toolName: "crypto_tls",
602
+ timeout: 5000,
603
+ });
604
+ if (versionResult.exitCode === 0) {
605
+ sections.push(`\n OpenSSL version: ${versionResult.stdout.trim()}`);
606
+ }
607
+ }
608
+ // Summary
609
+ sections.push("\n── Findings Summary ──");
610
+ if (findings.length === 0) {
611
+ sections.push(" ✅ No critical TLS configuration issues found.");
612
+ }
613
+ else {
614
+ const criticals = findings.filter((f) => f.level === "CRITICAL");
615
+ const warnings = findings.filter((f) => f.level === "WARNING");
616
+ if (criticals.length > 0) {
617
+ sections.push(`\n ⛔ Critical (${criticals.length}):`);
618
+ for (const f of criticals)
619
+ sections.push(` - ${f.msg}`);
620
+ }
621
+ if (warnings.length > 0) {
622
+ sections.push(`\n ⚠️ Warnings (${warnings.length}):`);
623
+ for (const f of warnings)
624
+ sections.push(` - ${f.msg}`);
625
+ }
626
+ }
627
+ return { content: [createTextContent(sections.join("\n"))] };
628
+ }
629
+ catch (err) {
630
+ const msg = err instanceof Error ? err.message : String(err);
631
+ return { content: [createErrorContent(msg)], isError: true };
632
+ }
633
+ }
634
+ default:
635
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
636
+ }
637
+ });
638
+ // ── 2. crypto_gpg_keys (kept as-is) ──────────────────────────────────────
639
+ server.tool("crypto_gpg_keys", "Manage GPG keys: list, generate, export, import, or verify signatures", {
640
+ action: z
641
+ .enum(["list", "generate", "export", "import", "verify"])
642
+ .describe("GPG action to perform"),
643
+ key_id: z
644
+ .string()
645
+ .optional()
646
+ .describe("GPG key ID (for export/verify)"),
647
+ file_path: z
648
+ .string()
649
+ .optional()
650
+ .describe("File path (for import/verify)"),
651
+ dry_run: z
652
+ .boolean()
653
+ .optional()
654
+ .describe("Preview changes without executing (defaults to KALI_DEFENSE_DRY_RUN env var)"),
655
+ }, async ({ action, key_id, file_path, dry_run }) => {
656
+ try {
657
+ const sections = [];
658
+ sections.push(`🔑 GPG Key Management: ${action}`);
659
+ sections.push("=".repeat(40));
660
+ switch (action) {
661
+ case "list": {
662
+ const result = await executeCommand({
663
+ command: "gpg",
664
+ args: ["--list-keys", "--keyid-format", "long"],
665
+ toolName: "crypto_gpg_keys",
666
+ timeout: getToolTimeout("crypto_gpg_keys"),
667
+ });
668
+ if (result.exitCode !== 0 && !result.stdout) {
669
+ sections.push("\nNo GPG keys found or GPG not configured.");
670
+ sections.push(`stderr: ${result.stderr}`);
671
+ }
672
+ else {
673
+ sections.push("\nPublic Keys:");
674
+ sections.push(result.stdout || "No keys found");
675
+ }
676
+ // Also list secret keys
677
+ const secretResult = await executeCommand({
678
+ command: "gpg",
679
+ args: ["--list-secret-keys", "--keyid-format", "long"],
680
+ toolName: "crypto_gpg_keys",
681
+ timeout: getToolTimeout("crypto_gpg_keys"),
682
+ });
683
+ if (secretResult.stdout.trim()) {
684
+ sections.push("\nSecret Keys:");
685
+ sections.push(secretResult.stdout);
686
+ }
687
+ break;
688
+ }
689
+ case "generate": {
690
+ if (dry_run ?? getConfig().dryRun) {
691
+ sections.push("\n[DRY RUN] Would generate a new GPG key pair.");
692
+ sections.push("Command: gpg --full-generate-key");
693
+ sections.push("\nNote: Key generation is interactive and requires user input.");
694
+ sections.push("To generate non-interactively, create a batch file with parameters.");
695
+ sections.push("\nExample batch file content:");
696
+ sections.push(" %no-protection");
697
+ sections.push(" Key-Type: RSA");
698
+ sections.push(" Key-Length: 4096");
699
+ sections.push(" Subkey-Type: RSA");
700
+ sections.push(" Subkey-Length: 4096");
701
+ sections.push(" Name-Real: Your Name");
702
+ sections.push(" Name-Email: your@email.com");
703
+ sections.push(" Expire-Date: 1y");
704
+ sections.push(" %commit");
705
+ }
706
+ else {
707
+ sections.push("⚠️ Interactive GPG key generation cannot be run in non-interactive mode.");
708
+ sections.push("Use 'gpg --batch --gen-key <batch_file>' for non-interactive generation.");
709
+ }
710
+ break;
711
+ }
712
+ case "export": {
713
+ if (!key_id) {
714
+ return {
715
+ content: [
716
+ createErrorContent("key_id is required for GPG key export"),
717
+ ],
718
+ isError: true,
719
+ };
720
+ }
721
+ sanitizeArgs([key_id]);
722
+ const result = await executeCommand({
723
+ command: "gpg",
724
+ args: ["--export", "--armor", key_id],
725
+ toolName: "crypto_gpg_keys",
726
+ timeout: getToolTimeout("crypto_gpg_keys"),
727
+ });
728
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
729
+ return {
730
+ content: [
731
+ createErrorContent(`Failed to export key ${key_id}: ${result.stderr}`),
732
+ ],
733
+ isError: true,
734
+ };
735
+ }
736
+ sections.push(`\nExported public key for: ${key_id}`);
737
+ sections.push(result.stdout);
738
+ break;
739
+ }
740
+ case "import": {
741
+ if (!file_path) {
742
+ return {
743
+ content: [
744
+ createErrorContent("file_path is required for GPG key import"),
745
+ ],
746
+ isError: true,
747
+ };
748
+ }
749
+ sanitizeArgs([file_path]);
750
+ // TOOL-023: Validate key file path with containment check
751
+ validateKeyPath(file_path);
752
+ if (dry_run ?? getConfig().dryRun) {
753
+ sections.push(`\n[DRY RUN] Would import GPG key from: ${file_path}`);
754
+ sections.push(`Command: gpg --import ${file_path}`);
755
+ }
756
+ else {
757
+ const result = await executeCommand({
758
+ command: "gpg",
759
+ args: ["--import", file_path],
760
+ toolName: "crypto_gpg_keys",
761
+ timeout: getToolTimeout("crypto_gpg_keys"),
762
+ });
763
+ if (result.exitCode !== 0) {
764
+ return {
765
+ content: [
766
+ createErrorContent(`Failed to import key: ${result.stderr}`),
767
+ ],
768
+ isError: true,
769
+ };
770
+ }
771
+ sections.push(`\n✅ Key imported from: ${file_path}`);
772
+ sections.push(result.stderr || result.stdout);
773
+ logChange(createChangeEntry({
774
+ tool: "crypto_gpg_keys",
775
+ action: "import",
776
+ target: file_path,
777
+ after: result.stderr || result.stdout,
778
+ dryRun: false,
779
+ success: true,
780
+ }));
781
+ }
782
+ break;
783
+ }
784
+ case "verify": {
785
+ if (!file_path) {
786
+ return {
787
+ content: [
788
+ createErrorContent("file_path is required for GPG signature verification"),
789
+ ],
790
+ isError: true,
791
+ };
792
+ }
793
+ sanitizeArgs([file_path]);
794
+ // TOOL-023: Validate key file path with containment check
795
+ validateKeyPath(file_path);
796
+ const result = await executeCommand({
797
+ command: "gpg",
798
+ args: ["--verify", file_path],
799
+ toolName: "crypto_gpg_keys",
800
+ timeout: getToolTimeout("crypto_gpg_keys"),
801
+ });
802
+ const output = result.stderr || result.stdout;
803
+ if (result.exitCode !== 0) {
804
+ sections.push(`\n⛔ Signature verification FAILED for: ${file_path}`);
805
+ }
806
+ else {
807
+ sections.push(`\n✅ Signature verification PASSED for: ${file_path}`);
808
+ }
809
+ sections.push(output);
810
+ break;
811
+ }
812
+ }
813
+ return { content: [createTextContent(sections.join("\n"))] };
814
+ }
815
+ catch (err) {
816
+ const msg = err instanceof Error ? err.message : String(err);
817
+ return { content: [createErrorContent(msg)], isError: true };
818
+ }
819
+ });
820
+ // ── 3. crypto_luks_manage (kept as-is) ───────────────────────────────────
821
+ server.tool("crypto_luks_manage", "Manage LUKS encrypted volumes: check status, dump headers, open/close, or list encrypted devices", {
822
+ action: z
823
+ .enum(["status", "dump", "open", "close", "list"])
824
+ .describe("LUKS management action"),
825
+ device: z
826
+ .string()
827
+ .optional()
828
+ .describe("Block device path (e.g., /dev/sda2) for status/dump/open"),
829
+ name: z
830
+ .string()
831
+ .optional()
832
+ .describe("Mapper name for open/close operations"),
833
+ dry_run: z
834
+ .boolean()
835
+ .optional()
836
+ .describe("Preview changes without executing (defaults to KALI_DEFENSE_DRY_RUN env var)"),
837
+ }, async ({ action, device, name, dry_run }) => {
838
+ try {
839
+ const sections = [];
840
+ sections.push(`🔐 LUKS Volume Management: ${action}`);
841
+ sections.push("=".repeat(40));
842
+ switch (action) {
843
+ case "status": {
844
+ if (!name) {
845
+ return {
846
+ content: [
847
+ createErrorContent("name (mapper name) is required for status check"),
848
+ ],
849
+ isError: true,
850
+ };
851
+ }
852
+ sanitizeArgs([name]);
853
+ const result = await executeCommand({
854
+ command: "sudo",
855
+ args: ["cryptsetup", "status", name],
856
+ toolName: "crypto_luks_manage",
857
+ timeout: getToolTimeout("crypto_luks_manage"),
858
+ });
859
+ if (result.exitCode !== 0) {
860
+ sections.push(`\n⚠️ Device mapper '${name}' not found or not active.`);
861
+ sections.push(result.stderr || result.stdout);
862
+ }
863
+ else {
864
+ sections.push(`\nStatus for /dev/mapper/${name}:`);
865
+ sections.push(result.stdout);
866
+ }
867
+ break;
868
+ }
869
+ case "dump": {
870
+ if (!device) {
871
+ return {
872
+ content: [
873
+ createErrorContent("device path is required for LUKS header dump"),
874
+ ],
875
+ isError: true,
876
+ };
877
+ }
878
+ sanitizeArgs([device]);
879
+ assertNoTraversal(device);
880
+ const result = await executeCommand({
881
+ command: "sudo",
882
+ args: ["cryptsetup", "luksDump", device],
883
+ toolName: "crypto_luks_manage",
884
+ timeout: getToolTimeout("crypto_luks_manage"),
885
+ });
886
+ if (result.exitCode !== 0) {
887
+ return {
888
+ content: [
889
+ createErrorContent(`Failed to dump LUKS header for ${device}: ${result.stderr}`),
890
+ ],
891
+ isError: true,
892
+ };
893
+ }
894
+ sections.push(`\nLUKS Header Dump for ${device}:`);
895
+ sections.push(result.stdout);
896
+ break;
897
+ }
898
+ case "open": {
899
+ if (!device || !name) {
900
+ return {
901
+ content: [
902
+ createErrorContent("Both device and name are required for LUKS open"),
903
+ ],
904
+ isError: true,
905
+ };
906
+ }
907
+ sanitizeArgs([device, name]);
908
+ assertNoTraversal(device);
909
+ if (dry_run ?? getConfig().dryRun) {
910
+ sections.push(`\n[DRY RUN] Would open LUKS volume:`);
911
+ sections.push(` Device: ${device}`);
912
+ sections.push(` Mapper name: ${name}`);
913
+ sections.push(` Command: sudo cryptsetup luksOpen ${device} ${name}`);
914
+ sections.push("\nNote: This operation requires a passphrase and cannot be run non-interactively without a key file.");
915
+ }
916
+ else {
917
+ sections.push("⚠️ Interactive LUKS open requires a passphrase.");
918
+ sections.push("Use a key file with: sudo cryptsetup luksOpen --key-file <keyfile> <device> <name>");
919
+ logChange(createChangeEntry({
920
+ tool: "crypto_luks_manage",
921
+ action: "open_attempted",
922
+ target: device,
923
+ dryRun: false,
924
+ success: false,
925
+ error: "Interactive passphrase required, cannot run non-interactively",
926
+ }));
927
+ }
928
+ break;
929
+ }
930
+ case "close": {
931
+ if (!name) {
932
+ return {
933
+ content: [
934
+ createErrorContent("name (mapper name) is required for LUKS close"),
935
+ ],
936
+ isError: true,
937
+ };
938
+ }
939
+ sanitizeArgs([name]);
940
+ if (dry_run ?? getConfig().dryRun) {
941
+ sections.push(`\n[DRY RUN] Would close LUKS volume: /dev/mapper/${name}`);
942
+ sections.push(` Command: sudo cryptsetup luksClose ${name}`);
943
+ }
944
+ else {
945
+ const result = await executeCommand({
946
+ command: "sudo",
947
+ args: ["cryptsetup", "luksClose", name],
948
+ toolName: "crypto_luks_manage",
949
+ timeout: getToolTimeout("crypto_luks_manage"),
950
+ });
951
+ if (result.exitCode !== 0) {
952
+ return {
953
+ content: [
954
+ createErrorContent(`Failed to close LUKS volume ${name}: ${result.stderr}`),
955
+ ],
956
+ isError: true,
957
+ };
958
+ }
959
+ sections.push(`\n✅ LUKS volume '${name}' closed successfully.`);
960
+ logChange(createChangeEntry({
961
+ tool: "crypto_luks_manage",
962
+ action: "close",
963
+ target: name,
964
+ dryRun: false,
965
+ success: true,
966
+ rollbackCommand: `sudo cryptsetup luksOpen <device> ${name}`,
967
+ }));
968
+ }
969
+ break;
970
+ }
971
+ case "list": {
972
+ // List device mapper entries
973
+ const mapperResult = await executeCommand({
974
+ command: "ls",
975
+ args: ["-la", "/dev/mapper/"],
976
+ toolName: "crypto_luks_manage",
977
+ timeout: getToolTimeout("crypto_luks_manage"),
978
+ });
979
+ sections.push("\n📁 Device Mapper Entries:");
980
+ sections.push(mapperResult.stdout || "No entries found");
981
+ // List block devices with filesystem info
982
+ const lsblkResult = await executeCommand({
983
+ command: "lsblk",
984
+ args: ["--fs", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,UUID"],
985
+ toolName: "crypto_luks_manage",
986
+ timeout: getToolTimeout("crypto_luks_manage"),
987
+ });
988
+ sections.push("\n💾 Block Devices (with filesystem info):");
989
+ sections.push(lsblkResult.stdout || "No block devices found");
990
+ // Filter for crypto entries
991
+ const cryptoLines = (lsblkResult.stdout || "")
992
+ .split("\n")
993
+ .filter((l) => l.includes("crypto_LUKS") || l.includes("crypt"));
994
+ if (cryptoLines.length > 0) {
995
+ sections.push("\n🔐 LUKS Encrypted Devices:");
996
+ for (const line of cryptoLines) {
997
+ sections.push(` ${line.trim()}`);
998
+ }
999
+ }
1000
+ else {
1001
+ sections.push("\nNo LUKS encrypted devices detected.");
1002
+ }
1003
+ break;
1004
+ }
1005
+ }
1006
+ return { content: [createTextContent(sections.join("\n"))] };
1007
+ }
1008
+ catch (err) {
1009
+ const msg = err instanceof Error ? err.message : String(err);
1010
+ return { content: [createErrorContent(msg)], isError: true };
1011
+ }
1012
+ });
1013
+ // ── 4. crypto_file_hash (kept as-is) ─────────────────────────────────────
1014
+ server.tool("crypto_file_hash", "Calculate cryptographic hashes of files for integrity verification", {
1015
+ path: z.string().describe("File or directory path to hash"),
1016
+ algorithm: z
1017
+ .enum(["sha256", "sha512", "sha1", "md5"])
1018
+ .optional()
1019
+ .default("sha256")
1020
+ .describe("Hash algorithm to use (default: sha256)"),
1021
+ recursive: z
1022
+ .boolean()
1023
+ .optional()
1024
+ .default(false)
1025
+ .describe("Recursively hash all files in a directory"),
1026
+ }, async ({ path, algorithm, recursive }) => {
1027
+ try {
1028
+ sanitizeArgs([path]);
1029
+ assertNoTraversal(path);
1030
+ const sections = [];
1031
+ const hashCmd = `${algorithm}sum`;
1032
+ sections.push(`#️⃣ File Integrity Hash (${algorithm.toUpperCase()})`);
1033
+ sections.push("=".repeat(40));
1034
+ if (recursive) {
1035
+ // Hash all files in directory recursively
1036
+ const result = await executeCommand({
1037
+ command: "find",
1038
+ args: [
1039
+ path,
1040
+ "-type",
1041
+ "f",
1042
+ "-exec",
1043
+ hashCmd,
1044
+ "{}",
1045
+ "+",
1046
+ ],
1047
+ toolName: "crypto_file_hash",
1048
+ timeout: getToolTimeout("crypto_file_hash"),
1049
+ });
1050
+ if (result.exitCode !== 0 && !result.stdout) {
1051
+ return {
1052
+ content: [
1053
+ createErrorContent(`Failed to hash files in ${path}: ${result.stderr}`),
1054
+ ],
1055
+ isError: true,
1056
+ };
1057
+ }
1058
+ const lines = result.stdout
1059
+ .trim()
1060
+ .split("\n")
1061
+ .filter((l) => l.trim());
1062
+ sections.push(`\nDirectory: ${path}`);
1063
+ sections.push(`Files hashed: ${lines.length}`);
1064
+ sections.push(`\nResults:`);
1065
+ sections.push(result.stdout);
1066
+ }
1067
+ else {
1068
+ // Hash a single file
1069
+ const result = await executeCommand({
1070
+ command: hashCmd,
1071
+ args: [path],
1072
+ toolName: "crypto_file_hash",
1073
+ timeout: getToolTimeout("crypto_file_hash"),
1074
+ });
1075
+ if (result.exitCode !== 0) {
1076
+ return {
1077
+ content: [
1078
+ createErrorContent(`Failed to hash ${path}: ${result.stderr}`),
1079
+ ],
1080
+ isError: true,
1081
+ };
1082
+ }
1083
+ sections.push(`\nFile: ${path}`);
1084
+ sections.push(`Algorithm: ${algorithm.toUpperCase()}`);
1085
+ const hashValue = result.stdout.trim().split(/\s+/)[0];
1086
+ sections.push(`Hash: ${hashValue}`);
1087
+ sections.push(`\nFull output: ${result.stdout.trim()}`);
1088
+ }
1089
+ return { content: [createTextContent(sections.join("\n"))] };
1090
+ }
1091
+ catch (err) {
1092
+ const msg = err instanceof Error ? err.message : String(err);
1093
+ return { content: [createErrorContent(msg)], isError: true };
1094
+ }
1095
+ });
1096
+ // ── 5. certificate_lifecycle ──────────────────────────────────────────────
1097
+ server.tool("certificate_lifecycle", "Certificate lifecycle management: inventory system certificates, check auto-renewal, audit CA trust store, verify OCSP revocation status, and monitor Certificate Transparency logs.", {
1098
+ action: z
1099
+ .enum(["inventory", "auto_renew_check", "ca_audit", "ocsp_check", "ct_log_monitor"])
1100
+ .describe("Action: inventory=scan system certs, auto_renew_check=check certbot/ACME renewal, ca_audit=audit CA trust store, ocsp_check=check OCSP revocation, ct_log_monitor=check CT logs"),
1101
+ domain: z
1102
+ .string()
1103
+ .optional()
1104
+ .describe("Domain for OCSP/CT checks"),
1105
+ cert_path: z
1106
+ .string()
1107
+ .optional()
1108
+ .describe("Path to specific certificate file"),
1109
+ search_paths: z
1110
+ .array(z.string())
1111
+ .optional()
1112
+ .describe("Additional paths to search for certificates"),
1113
+ output_format: z
1114
+ .enum(["text", "json"])
1115
+ .optional()
1116
+ .default("text")
1117
+ .describe("Output format (default text)"),
1118
+ }, async (params) => {
1119
+ const { action } = params;
1120
+ switch (action) {
1121
+ // ── inventory ──────────────────────────────────────────────────
1122
+ case "inventory": {
1123
+ try {
1124
+ const defaultPaths = [
1125
+ "/etc/ssl/certs/",
1126
+ "/etc/pki/tls/certs/",
1127
+ "/etc/letsencrypt/live/",
1128
+ "/usr/local/share/ca-certificates/",
1129
+ ];
1130
+ const searchPaths = [...defaultPaths, ...(params.search_paths ?? [])];
1131
+ for (const p of searchPaths) {
1132
+ assertNoTraversal(p);
1133
+ }
1134
+ const allCerts = [];
1135
+ for (const searchPath of searchPaths) {
1136
+ const findResult = await runCertCommand("find", [
1137
+ searchPath, "-name", "*.pem", "-o", "-name", "*.crt", "-o", "-name", "*.cer",
1138
+ ], 15_000);
1139
+ if (findResult.exitCode === 0 && findResult.stdout.trim()) {
1140
+ const certs = findResult.stdout.trim().split("\n").filter((c) => c.trim());
1141
+ allCerts.push(...certs);
1142
+ }
1143
+ }
1144
+ const certDetails = [];
1145
+ let expiredCount = 0;
1146
+ let expiringSoonCount = 0;
1147
+ let validCount = 0;
1148
+ const certsToCheck = allCerts.slice(0, 100);
1149
+ for (const certFile of certsToCheck) {
1150
+ const certResult = await runCertCommand("openssl", [
1151
+ "x509", "-in", certFile.trim(), "-noout",
1152
+ "-subject", "-issuer", "-dates", "-serial",
1153
+ ], 5_000);
1154
+ if (certResult.exitCode === 0) {
1155
+ const out = certResult.stdout;
1156
+ const subjectMatch = out.match(/subject=(.+)/);
1157
+ const issuerMatch = out.match(/issuer=(.+)/);
1158
+ const notBeforeMatch = out.match(/notBefore=(.+)/);
1159
+ const notAfterMatch = out.match(/notAfter=(.+)/);
1160
+ const serialMatch = out.match(/serial=(.+)/);
1161
+ const notAfter = notAfterMatch ? notAfterMatch[1].trim() : "";
1162
+ let daysLeft = 0;
1163
+ let status = "valid";
1164
+ if (notAfter) {
1165
+ const expiryDate = new Date(notAfter);
1166
+ const now = new Date();
1167
+ daysLeft = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
1168
+ if (daysLeft < 0) {
1169
+ status = "expired";
1170
+ expiredCount++;
1171
+ }
1172
+ else if (daysLeft < 30) {
1173
+ status = "expiring_soon";
1174
+ expiringSoonCount++;
1175
+ }
1176
+ else {
1177
+ validCount++;
1178
+ }
1179
+ }
1180
+ certDetails.push({
1181
+ path: certFile.trim(),
1182
+ subject: subjectMatch ? subjectMatch[1].trim() : "unknown",
1183
+ issuer: issuerMatch ? issuerMatch[1].trim() : "unknown",
1184
+ notBefore: notBeforeMatch ? notBeforeMatch[1].trim() : "unknown",
1185
+ notAfter: notAfter || "unknown",
1186
+ serial: serialMatch ? serialMatch[1].trim() : "unknown",
1187
+ status,
1188
+ daysLeft,
1189
+ });
1190
+ }
1191
+ }
1192
+ const output = {
1193
+ action: "inventory",
1194
+ totalCerts: certDetails.length,
1195
+ expired: expiredCount,
1196
+ expiringSoon: expiringSoonCount,
1197
+ valid: validCount,
1198
+ certificates: certDetails,
1199
+ searchPaths,
1200
+ };
1201
+ if (params.output_format === "json") {
1202
+ return { content: [formatToolOutput(output)] };
1203
+ }
1204
+ const sections = [];
1205
+ sections.push("📜 Certificate Inventory");
1206
+ sections.push("=".repeat(50));
1207
+ sections.push(`\nTotal certificates found: ${certDetails.length}`);
1208
+ sections.push(` ✅ Valid: ${validCount}`);
1209
+ sections.push(` ⚠️ Expiring soon (< 30 days): ${expiringSoonCount}`);
1210
+ sections.push(` ⛔ Expired: ${expiredCount}`);
1211
+ if (expiredCount > 0) {
1212
+ sections.push("\n── Expired Certificates ──");
1213
+ for (const cert of certDetails.filter((c) => c.status === "expired")) {
1214
+ sections.push(` ${cert.path}`);
1215
+ sections.push(` Subject: ${cert.subject}`);
1216
+ sections.push(` Expired: ${Math.abs(cert.daysLeft)} days ago`);
1217
+ }
1218
+ }
1219
+ if (expiringSoonCount > 0) {
1220
+ sections.push("\n── Expiring Soon ──");
1221
+ for (const cert of certDetails.filter((c) => c.status === "expiring_soon")) {
1222
+ sections.push(` ${cert.path}`);
1223
+ sections.push(` Subject: ${cert.subject}`);
1224
+ sections.push(` Expires in: ${cert.daysLeft} days`);
1225
+ }
1226
+ }
1227
+ return { content: [createTextContent(sections.join("\n"))] };
1228
+ }
1229
+ catch (err) {
1230
+ const msg = err instanceof Error ? err.message : String(err);
1231
+ return { content: [createErrorContent(`inventory failed: ${msg}`)], isError: true };
1232
+ }
1233
+ }
1234
+ // ── auto_renew_check ───────────────────────────────────────────
1235
+ case "auto_renew_check": {
1236
+ try {
1237
+ const findings = { action: "auto_renew_check" };
1238
+ const certbotCheck = await runCertCommand("which", ["certbot"], 5_000);
1239
+ const certbotInstalled = certbotCheck.exitCode === 0;
1240
+ findings.certbotInstalled = certbotInstalled;
1241
+ if (!certbotInstalled) {
1242
+ findings.status = "certbot_not_found";
1243
+ findings.recommendations = [
1244
+ "Certbot is not installed. Install with: apt install certbot",
1245
+ ];
1246
+ if (params.output_format === "json") {
1247
+ return { content: [formatToolOutput(findings)] };
1248
+ }
1249
+ const sections = [];
1250
+ sections.push("🔄 Auto-Renewal Check");
1251
+ sections.push("=".repeat(50));
1252
+ sections.push("\n⚠️ Certbot is not installed.");
1253
+ sections.push(" Install with: apt install certbot");
1254
+ return { content: [createTextContent(sections.join("\n"))] };
1255
+ }
1256
+ // Check certbot timer
1257
+ const timerResult = await runCertCommand("systemctl", ["status", "certbot.timer"], 10_000);
1258
+ const timerActive = timerResult.exitCode === 0 &&
1259
+ timerResult.stdout.includes("active");
1260
+ findings.timerActive = timerActive;
1261
+ findings.timerOutput = timerResult.stdout.trim();
1262
+ // List certbot certificates
1263
+ const certsResult = await runCertCommand("certbot", ["certificates"], 15_000);
1264
+ findings.certificates = certsResult.stdout.trim();
1265
+ findings.certbotExitCode = certsResult.exitCode;
1266
+ // Check renewal configs
1267
+ const renewalResult = await runCertCommand("find", [
1268
+ "/etc/letsencrypt/renewal/", "-name", "*.conf",
1269
+ ], 5_000);
1270
+ const renewalConfigs = renewalResult.exitCode === 0 && renewalResult.stdout.trim()
1271
+ ? renewalResult.stdout.trim().split("\n").filter((l) => l.trim())
1272
+ : [];
1273
+ findings.renewalConfigs = renewalConfigs;
1274
+ // Check cron for renewal jobs
1275
+ const cronResult = await runCertCommand("grep", [
1276
+ "-r", "certbot", "/etc/cron.d/", "/etc/cron.daily/", "/etc/crontab",
1277
+ ], 5_000);
1278
+ const cronJobs = cronResult.exitCode === 0 && cronResult.stdout.trim()
1279
+ ? cronResult.stdout.trim().split("\n").filter((l) => l.trim())
1280
+ : [];
1281
+ findings.cronJobs = cronJobs;
1282
+ findings.status = "checked";
1283
+ if (params.output_format === "json") {
1284
+ return { content: [formatToolOutput(findings)] };
1285
+ }
1286
+ const sections = [];
1287
+ sections.push("🔄 Auto-Renewal Check");
1288
+ sections.push("=".repeat(50));
1289
+ sections.push(`\nCertbot: installed at ${certbotCheck.stdout.trim()}`);
1290
+ sections.push(`Timer: ${timerActive ? "✅ Active" : "⚠️ Not active"}`);
1291
+ sections.push("\nManaged Certificates:");
1292
+ sections.push(certsResult.stdout.trim() || " No certificates found");
1293
+ sections.push(`\nRenewal Configs (${renewalConfigs.length}):`);
1294
+ for (const conf of renewalConfigs) {
1295
+ sections.push(` ${conf}`);
1296
+ }
1297
+ if (cronJobs.length > 0) {
1298
+ sections.push("\nCron Jobs:");
1299
+ for (const job of cronJobs) {
1300
+ sections.push(` ${job}`);
1301
+ }
1302
+ }
1303
+ return { content: [createTextContent(sections.join("\n"))] };
1304
+ }
1305
+ catch (err) {
1306
+ const msg = err instanceof Error ? err.message : String(err);
1307
+ return {
1308
+ content: [createErrorContent(`auto_renew_check failed: ${msg}`)],
1309
+ isError: true,
1310
+ };
1311
+ }
1312
+ }
1313
+ // ── ca_audit ───────────────────────────────────────────────────
1314
+ case "ca_audit": {
1315
+ try {
1316
+ const findings = { action: "ca_audit" };
1317
+ let trustStorePath = "/etc/ssl/certs/";
1318
+ const sslCheck = await runCertCommand("ls", ["/etc/ssl/certs/"], 5_000);
1319
+ if (sslCheck.exitCode !== 0) {
1320
+ const pkiCheck = await runCertCommand("ls", ["/etc/pki/tls/certs/"], 5_000);
1321
+ if (pkiCheck.exitCode === 0) {
1322
+ trustStorePath = "/etc/pki/tls/certs/";
1323
+ }
1324
+ }
1325
+ findings.trustStorePath = trustStorePath;
1326
+ const caListResult = await runCertCommand("find", [
1327
+ trustStorePath,
1328
+ "-name", "*.pem", "-o", "-name", "*.crt",
1329
+ ], 10_000);
1330
+ const caFiles = caListResult.exitCode === 0 && caListResult.stdout.trim()
1331
+ ? caListResult.stdout.trim().split("\n").filter((l) => l.trim())
1332
+ : [];
1333
+ findings.totalCAs = caFiles.length;
1334
+ const recentResult = await runCertCommand("find", [
1335
+ trustStorePath,
1336
+ "-mtime", "-30",
1337
+ "-name", "*.pem",
1338
+ "-o",
1339
+ "-mtime", "-30",
1340
+ "-name", "*.crt",
1341
+ ], 10_000);
1342
+ const recentlyAdded = recentResult.exitCode === 0 && recentResult.stdout.trim()
1343
+ ? recentResult.stdout.trim().split("\n").filter((l) => l.trim())
1344
+ : [];
1345
+ findings.recentlyAdded = recentlyAdded;
1346
+ findings.recentlyAddedCount = recentlyAdded.length;
1347
+ const suspiciousPatterns = [
1348
+ "test", "debug", "fake", "temporary", "tmp",
1349
+ "self-signed", "localhost", "example",
1350
+ ];
1351
+ const suspiciousFindings = [];
1352
+ for (const caFile of caFiles.slice(0, 200)) {
1353
+ const lower = caFile.toLowerCase();
1354
+ for (const pattern of suspiciousPatterns) {
1355
+ if (lower.includes(pattern)) {
1356
+ suspiciousFindings.push(caFile);
1357
+ break;
1358
+ }
1359
+ }
1360
+ }
1361
+ findings.suspiciousFindings = suspiciousFindings;
1362
+ findings.suspiciousCount = suspiciousFindings.length;
1363
+ const updateCheck = await runCertCommand("which", ["update-ca-certificates"], 5_000);
1364
+ findings.updateCaCertificatesAvailable =
1365
+ updateCheck.exitCode === 0;
1366
+ if (params.output_format === "json") {
1367
+ return { content: [formatToolOutput(findings)] };
1368
+ }
1369
+ const sections = [];
1370
+ sections.push("🏛️ CA Trust Store Audit");
1371
+ sections.push("=".repeat(50));
1372
+ sections.push(`\nTrust store path: ${trustStorePath}`);
1373
+ sections.push(`Total trusted CAs: ${caFiles.length}`);
1374
+ sections.push(`Recently added (last 30 days): ${recentlyAdded.length}`);
1375
+ if (recentlyAdded.length > 0) {
1376
+ sections.push("\n── Recently Added CAs ──");
1377
+ for (const ca of recentlyAdded.slice(0, 20)) {
1378
+ sections.push(` ${ca}`);
1379
+ }
1380
+ }
1381
+ if (suspiciousFindings.length > 0) {
1382
+ sections.push(`\n⚠️ Suspicious CAs Found (${suspiciousFindings.length}):`);
1383
+ for (const ca of suspiciousFindings.slice(0, 20)) {
1384
+ sections.push(` ${ca}`);
1385
+ }
1386
+ }
1387
+ else {
1388
+ sections.push("\n✅ No suspicious CA names detected.");
1389
+ }
1390
+ return { content: [createTextContent(sections.join("\n"))] };
1391
+ }
1392
+ catch (err) {
1393
+ const msg = err instanceof Error ? err.message : String(err);
1394
+ return {
1395
+ content: [createErrorContent(`ca_audit failed: ${msg}`)],
1396
+ isError: true,
1397
+ };
1398
+ }
1399
+ }
1400
+ // ── ocsp_check ─────────────────────────────────────────────────
1401
+ case "ocsp_check": {
1402
+ try {
1403
+ if (!params.domain && !params.cert_path) {
1404
+ return {
1405
+ content: [
1406
+ createErrorContent("domain or cert_path is required for ocsp_check"),
1407
+ ],
1408
+ isError: true,
1409
+ };
1410
+ }
1411
+ const findings = {
1412
+ action: "ocsp_check",
1413
+ };
1414
+ let certPem = "";
1415
+ if (params.domain) {
1416
+ const validDomain = validateTarget(params.domain);
1417
+ findings.domain = validDomain;
1418
+ // Get certificate chain from domain
1419
+ const sClientResult = await runCertCommand("openssl", [
1420
+ "s_client", "-connect", `${validDomain}:443`,
1421
+ "-servername", validDomain, "-showcerts",
1422
+ ], 10_000, "");
1423
+ const fullOutput = sClientResult.stdout + "\n" + sClientResult.stderr;
1424
+ const certMatches = fullOutput.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
1425
+ if (!certMatches || certMatches.length === 0) {
1426
+ findings.error =
1427
+ "Could not retrieve certificate from domain";
1428
+ if (params.output_format === "json") {
1429
+ return { content: [formatToolOutput(findings)] };
1430
+ }
1431
+ return {
1432
+ content: [
1433
+ createErrorContent(`Could not retrieve certificate from ${validDomain}`),
1434
+ ],
1435
+ isError: true,
1436
+ };
1437
+ }
1438
+ certPem = certMatches[0];
1439
+ }
1440
+ else if (params.cert_path) {
1441
+ assertNoTraversal(params.cert_path);
1442
+ const validPath = validateCertPath(params.cert_path);
1443
+ findings.certPath = validPath;
1444
+ const readResult = await runCertCommand("openssl", ["x509", "-in", validPath], 5_000);
1445
+ if (readResult.exitCode !== 0) {
1446
+ return {
1447
+ content: [
1448
+ createErrorContent(`Failed to read certificate: ${readResult.stderr}`),
1449
+ ],
1450
+ isError: true,
1451
+ };
1452
+ }
1453
+ certPem = readResult.stdout;
1454
+ }
1455
+ // Extract OCSP URI from certificate via stdin
1456
+ const ocspUriResult = await runCertCommand("openssl", ["x509", "-noout", "-ocsp_uri"], 5_000, certPem);
1457
+ const ocspUri = ocspUriResult.stdout.trim();
1458
+ findings.ocspUri = ocspUri || "not found";
1459
+ if (!ocspUri) {
1460
+ findings.status = "no_ocsp_uri";
1461
+ findings.message =
1462
+ "Certificate does not contain an OCSP responder URI";
1463
+ if (params.output_format === "json") {
1464
+ return { content: [formatToolOutput(findings)] };
1465
+ }
1466
+ return {
1467
+ content: [
1468
+ createTextContent("🔍 OCSP Check\n" +
1469
+ "=".repeat(50) +
1470
+ "\n\n⚠️ Certificate does not contain an OCSP responder URI."),
1471
+ ],
1472
+ };
1473
+ }
1474
+ // Check OCSP stapling via s_client -status
1475
+ if (params.domain) {
1476
+ const validDomain = validateTarget(params.domain);
1477
+ const staplingResult = await runCertCommand("openssl", [
1478
+ "s_client", "-connect", `${validDomain}:443`,
1479
+ "-servername", validDomain, "-status",
1480
+ ], 10_000, "");
1481
+ const staplingOutput = staplingResult.stdout + staplingResult.stderr;
1482
+ const hasStapling = staplingOutput.includes("OCSP Response Status: successful");
1483
+ findings.ocspStapling = hasStapling;
1484
+ if (hasStapling) {
1485
+ if (staplingOutput.includes("Cert Status: good")) {
1486
+ findings.revocationStatus = "good";
1487
+ }
1488
+ else if (staplingOutput.includes("Cert Status: revoked")) {
1489
+ findings.revocationStatus = "revoked";
1490
+ }
1491
+ else {
1492
+ findings.revocationStatus = "unknown";
1493
+ }
1494
+ }
1495
+ else {
1496
+ findings.revocationStatus = "unknown";
1497
+ findings.message =
1498
+ "OCSP stapling not available; direct OCSP query may be needed";
1499
+ }
1500
+ }
1501
+ else {
1502
+ findings.revocationStatus = "unknown";
1503
+ findings.message =
1504
+ "Direct OCSP query requires domain; use domain parameter for full check";
1505
+ }
1506
+ if (params.output_format === "json") {
1507
+ return { content: [formatToolOutput(findings)] };
1508
+ }
1509
+ const sections = [];
1510
+ sections.push("🔍 OCSP Check");
1511
+ sections.push("=".repeat(50));
1512
+ sections.push(`\nOCSP Responder: ${ocspUri}`);
1513
+ sections.push(`Revocation Status: ${String(findings.revocationStatus)}`);
1514
+ if (findings.ocspStapling !== undefined) {
1515
+ sections.push(`OCSP Stapling: ${findings.ocspStapling ? "✅ Supported" : "⚠️ Not supported"}`);
1516
+ }
1517
+ if (findings.message) {
1518
+ sections.push(`\nNote: ${String(findings.message)}`);
1519
+ }
1520
+ return { content: [createTextContent(sections.join("\n"))] };
1521
+ }
1522
+ catch (err) {
1523
+ const msg = err instanceof Error ? err.message : String(err);
1524
+ return {
1525
+ content: [createErrorContent(`ocsp_check failed: ${msg}`)],
1526
+ isError: true,
1527
+ };
1528
+ }
1529
+ }
1530
+ // ── ct_log_monitor ─────────────────────────────────────────────
1531
+ case "ct_log_monitor": {
1532
+ try {
1533
+ if (!params.domain) {
1534
+ return {
1535
+ content: [
1536
+ createErrorContent("domain is required for ct_log_monitor"),
1537
+ ],
1538
+ isError: true,
1539
+ };
1540
+ }
1541
+ const validDomain = validateTarget(params.domain);
1542
+ const findings = {
1543
+ action: "ct_log_monitor",
1544
+ domain: validDomain,
1545
+ };
1546
+ // Query crt.sh for certificates
1547
+ const crtshResult = await runCertCommand("curl", [
1548
+ "-s", "-m", "15",
1549
+ `https://crt.sh/?q=${encodeURIComponent(validDomain)}&output=json`,
1550
+ ], 20_000);
1551
+ if (crtshResult.exitCode !== 0 || !crtshResult.stdout.trim()) {
1552
+ findings.error = "Failed to query crt.sh";
1553
+ findings.stderr = crtshResult.stderr;
1554
+ if (params.output_format === "json") {
1555
+ return { content: [formatToolOutput(findings)] };
1556
+ }
1557
+ return {
1558
+ content: [
1559
+ createTextContent("🔍 CT Log Monitor\n" +
1560
+ "=".repeat(50) +
1561
+ `\n\n⚠️ Failed to query crt.sh for ${validDomain}.\n` +
1562
+ `Error: ${crtshResult.stderr}`),
1563
+ ],
1564
+ };
1565
+ }
1566
+ let ctEntries = [];
1567
+ try {
1568
+ ctEntries = JSON.parse(crtshResult.stdout);
1569
+ }
1570
+ catch {
1571
+ findings.error = "Failed to parse crt.sh response";
1572
+ if (params.output_format === "json") {
1573
+ return { content: [formatToolOutput(findings)] };
1574
+ }
1575
+ return {
1576
+ content: [
1577
+ createErrorContent("Failed to parse crt.sh JSON response"),
1578
+ ],
1579
+ isError: true,
1580
+ };
1581
+ }
1582
+ findings.totalCerts = ctEntries.length;
1583
+ // Analyze certificates
1584
+ const issuers = new Set();
1585
+ const wildcardCerts = [];
1586
+ const recentCerts = [];
1587
+ const thirtyDaysAgo = new Date();
1588
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
1589
+ for (const entry of ctEntries.slice(0, 200)) {
1590
+ const issuerName = String(entry.issuer_name ?? "unknown");
1591
+ issuers.add(issuerName);
1592
+ const commonName = String(entry.common_name ?? "");
1593
+ if (commonName.startsWith("*.")) {
1594
+ wildcardCerts.push(entry);
1595
+ }
1596
+ const notBefore = entry.not_before
1597
+ ? new Date(String(entry.not_before))
1598
+ : null;
1599
+ if (notBefore && notBefore > thirtyDaysAgo) {
1600
+ recentCerts.push(entry);
1601
+ }
1602
+ }
1603
+ findings.issuers = [...issuers];
1604
+ findings.wildcardCount = wildcardCerts.length;
1605
+ findings.recentCount = recentCerts.length;
1606
+ findings.recentCerts = recentCerts.slice(0, 20).map((e) => ({
1607
+ commonName: e.common_name,
1608
+ issuer: e.issuer_name,
1609
+ notBefore: e.not_before,
1610
+ notAfter: e.not_after,
1611
+ }));
1612
+ // Flag unexpected issuers
1613
+ const unexpectedFindings = [];
1614
+ if (issuers.size > 3) {
1615
+ unexpectedFindings.push(`Multiple issuers detected (${issuers.size}) — review for unauthorized certificate issuance`);
1616
+ }
1617
+ if (wildcardCerts.length > 0) {
1618
+ unexpectedFindings.push(`${wildcardCerts.length} wildcard certificate(s) found`);
1619
+ }
1620
+ findings.unexpectedFindings = unexpectedFindings;
1621
+ if (params.output_format === "json") {
1622
+ return { content: [formatToolOutput(findings)] };
1623
+ }
1624
+ const sections = [];
1625
+ sections.push("🔍 CT Log Monitor");
1626
+ sections.push("=".repeat(50));
1627
+ sections.push(`\nDomain: ${validDomain}`);
1628
+ sections.push(`Total certificates in CT logs: ${ctEntries.length}`);
1629
+ sections.push(`Unique issuers: ${issuers.size}`);
1630
+ sections.push(`Wildcard certificates: ${wildcardCerts.length}`);
1631
+ sections.push(`Recently issued (last 30 days): ${recentCerts.length}`);
1632
+ if (issuers.size > 0) {
1633
+ sections.push("\n── Issuers ──");
1634
+ for (const issuer of issuers) {
1635
+ sections.push(` ${issuer}`);
1636
+ }
1637
+ }
1638
+ if (recentCerts.length > 0) {
1639
+ sections.push("\n── Recently Issued ──");
1640
+ for (const cert of recentCerts.slice(0, 10)) {
1641
+ sections.push(` ${cert.common_name} (issued: ${cert.not_before}, by: ${cert.issuer_name})`);
1642
+ }
1643
+ }
1644
+ if (unexpectedFindings.length > 0) {
1645
+ sections.push("\n⚠️ Findings:");
1646
+ for (const finding of unexpectedFindings) {
1647
+ sections.push(` ${finding}`);
1648
+ }
1649
+ }
1650
+ return { content: [createTextContent(sections.join("\n"))] };
1651
+ }
1652
+ catch (err) {
1653
+ const msg = err instanceof Error ? err.message : String(err);
1654
+ return {
1655
+ content: [createErrorContent(`ct_log_monitor failed: ${msg}`)],
1656
+ isError: true,
1657
+ };
1658
+ }
1659
+ }
1660
+ default:
1661
+ return {
1662
+ content: [createErrorContent(`Unknown action: ${action}`)],
1663
+ isError: true,
1664
+ };
1665
+ }
1666
+ });
1667
+ }