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,1180 @@
1
+ /**
2
+ * Incident response tools for Kali Defense MCP Server.
3
+ *
4
+ * Registers 2 tools:
5
+ * - incident_response (actions: collect, ioc_scan, timeline)
6
+ * - ir_forensics (actions: memory_dump, disk_image, network_capture_forensic, evidence_bag, chain_of_custody)
7
+ */
8
+ import { z } from "zod";
9
+ import { executeCommand } from "../core/executor.js";
10
+ import { getConfig } from "../core/config.js";
11
+ import { createTextContent, createErrorContent, } from "../core/parsers.js";
12
+ import { spawnSafe } from "../core/spawn-safe.js";
13
+ import { secureWriteFileSync } from "../core/secure-fs.js";
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ // ── Suspicious port list for IOC scanning ──────────────────────────────────
16
+ const SUSPICIOUS_PORTS = [4444, 5555, 6666, 8888, 9999, 1337, 31337];
17
+ const CRYPTO_MINER_NAMES = [
18
+ "xmrig", "minerd", "minergate", "cpuminer", "cgminer",
19
+ "bfgminer", "ethminer", "claymore", "nicehash", "kthreaddi",
20
+ ];
21
+ // ── Forensics constants ────────────────────────────────────────────────────
22
+ const DEFAULT_FORENSICS_DIR = "/var/lib/kali-defense/forensics/";
23
+ /**
24
+ * Run a command via spawnSafe and collect output as a promise.
25
+ * Similar to the helper in reporting.ts — returns error info instead of throwing.
26
+ */
27
+ async function runForensicCommand(command, args, timeoutMs = 30_000) {
28
+ return new Promise((resolve) => {
29
+ let child;
30
+ try {
31
+ child = spawnSafe(command, args);
32
+ }
33
+ catch (err) {
34
+ const msg = err instanceof Error ? err.message : String(err);
35
+ resolve({ stdout: "", stderr: msg, exitCode: -1 });
36
+ return;
37
+ }
38
+ let stdout = "";
39
+ let stderr = "";
40
+ let resolved = false;
41
+ const timer = setTimeout(() => {
42
+ if (!resolved) {
43
+ resolved = true;
44
+ child.kill("SIGTERM");
45
+ resolve({ stdout, stderr: stderr + "\n[TIMEOUT]", exitCode: -1 });
46
+ }
47
+ }, timeoutMs);
48
+ child.stdout?.on("data", (data) => {
49
+ stdout += data.toString();
50
+ });
51
+ child.stderr?.on("data", (data) => {
52
+ stderr += data.toString();
53
+ });
54
+ child.on("close", (code) => {
55
+ if (!resolved) {
56
+ resolved = true;
57
+ clearTimeout(timer);
58
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
59
+ }
60
+ });
61
+ child.on("error", (err) => {
62
+ if (!resolved) {
63
+ resolved = true;
64
+ clearTimeout(timer);
65
+ resolve({ stdout, stderr: err.message, exitCode: -1 });
66
+ }
67
+ });
68
+ });
69
+ }
70
+ // ── Registration entry point ───────────────────────────────────────────────
71
+ export function registerIncidentResponseTools(server) {
72
+ server.tool("incident_response", "Incident response: collect volatile data (RFC 3227), scan for IOCs, or generate filesystem timelines.", {
73
+ action: z.enum(["collect", "ioc_scan", "timeline"]).describe("Action: collect=volatile data collection, ioc_scan=scan for indicators of compromise, timeline=filesystem timeline"),
74
+ // collect params
75
+ output_dir: z.string().optional().default("/tmp/ir-collection").describe("Directory to save collected volatile data (collect action)"),
76
+ // ioc_scan params
77
+ check_type: z.enum(["processes", "connections", "persistence", "all"]).optional().default("all").describe("Type of IOC check to perform (ioc_scan action)"),
78
+ // timeline params
79
+ path: z.string().optional().default("/").describe("Root path to search for modified files (timeline action)"),
80
+ hours: z.number().optional().default(24).describe("Look back this many hours for modifications (timeline action)"),
81
+ exclude_paths: z.string().optional().default("/proc,/sys,/dev,/run").describe("Comma-separated paths to exclude from search (timeline action)"),
82
+ file_types: z.enum(["all", "executables", "configs", "scripts"]).optional().default("all").describe("Type of files to include in the timeline (timeline action)"),
83
+ // shared
84
+ dry_run: z.boolean().optional().describe("Preview what would be done without executing (defaults to KALI_DEFENSE_DRY_RUN env var)"),
85
+ }, async (params) => {
86
+ const { action } = params;
87
+ switch (action) {
88
+ // ── collect ──────────────────────────────────────────────────
89
+ case "collect": {
90
+ const { output_dir, dry_run } = params;
91
+ try {
92
+ const collectionSteps = [
93
+ { name: "01-processes", command: "ps", args: ["auxwww"], desc: "Running processes" },
94
+ { name: "02-network-connections", command: "ss", args: ["-tulnpea"], desc: "Network connections" },
95
+ { name: "03-ip-addresses", command: "ip", args: ["addr", "show"], desc: "IP addresses" },
96
+ { name: "04-routes", command: "ip", args: ["route", "show"], desc: "Routing table" },
97
+ { name: "05-arp-cache", command: "arp", args: ["-an"], desc: "ARP cache" },
98
+ { name: "06-logged-in-users-who", command: "who", args: [], desc: "Logged in users (who)" },
99
+ { name: "07-logged-in-users-w", command: "w", args: [], desc: "Logged in users (w)" },
100
+ { name: "08-recent-logins", command: "last", args: ["-n", "20"], desc: "Recent logins" },
101
+ { name: "09-open-files", command: "lsof", args: ["-n"], desc: "Open files", maxLines: 500 },
102
+ { name: "10-kernel-modules", command: "lsmod", args: [], desc: "Loaded kernel modules" },
103
+ { name: "11-mounts", command: "mount", args: [], desc: "Mounted filesystems" },
104
+ { name: "12-disk-usage", command: "df", args: ["-h"], desc: "Disk usage" },
105
+ { name: "13-environment", command: "env", args: [], desc: "Environment variables" },
106
+ { name: "14-uptime", command: "uptime", args: [], desc: "System uptime" },
107
+ { name: "15-hostname", command: "hostname", args: [], desc: "Hostname" },
108
+ { name: "16-utc-time", command: "date", args: ["-u"], desc: "Current UTC time" },
109
+ ];
110
+ if (dry_run ?? getConfig().dryRun) {
111
+ const lines = [
112
+ `[DRY-RUN] Volatile Data Collection Plan (RFC 3227)`,
113
+ `Output directory: ${output_dir}/<timestamp>/`,
114
+ ``,
115
+ `The following data would be collected in order of volatility:`,
116
+ ``,
117
+ ];
118
+ for (const step of collectionSteps) {
119
+ lines.push(` ${step.name}: ${step.desc}`);
120
+ lines.push(` Command: ${step.command} ${step.args.join(" ")}`);
121
+ }
122
+ lines.push(``);
123
+ lines.push(`Total: ${collectionSteps.length} collection steps`);
124
+ lines.push(`Set dry_run=false to execute collection.`);
125
+ return { content: [createTextContent(lines.join("\n"))] };
126
+ }
127
+ // Create timestamped output directory
128
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
129
+ const collectionDir = `${output_dir}/${timestamp}`;
130
+ const mkdirResult = await executeCommand({
131
+ command: "mkdir",
132
+ args: ["-p", collectionDir],
133
+ toolName: "incident_response",
134
+ timeout: 5000,
135
+ });
136
+ if (mkdirResult.exitCode !== 0) {
137
+ return {
138
+ content: [
139
+ createErrorContent(`Failed to create output directory ${collectionDir}: ${mkdirResult.stderr}`),
140
+ ],
141
+ isError: true,
142
+ };
143
+ }
144
+ const lines = [
145
+ `=== Volatile Data Collection Report (RFC 3227) ===`,
146
+ `Collection Directory: ${collectionDir}`,
147
+ `Started: ${new Date().toISOString()}`,
148
+ ``,
149
+ ];
150
+ let successCount = 0;
151
+ let failCount = 0;
152
+ for (const step of collectionSteps) {
153
+ // Use pre-defined command and args directly — no shell string parsing
154
+ const result = await executeCommand({
155
+ command: step.command,
156
+ args: step.args,
157
+ toolName: "incident_response",
158
+ timeout: 30000,
159
+ });
160
+ // Truncate output if maxLines is set (replaces shell piping to head)
161
+ let stdout = result.stdout;
162
+ if ('maxLines' in step && step.maxLines) {
163
+ const allLines = stdout.split("\n");
164
+ if (allLines.length > step.maxLines) {
165
+ stdout = allLines.slice(0, step.maxLines).join("\n");
166
+ }
167
+ }
168
+ // Write output to file using tee
169
+ const outputPath = `${collectionDir}/${step.name}.txt`;
170
+ const outputContent = stdout + (result.stderr ? "\n" + result.stderr : "");
171
+ await executeCommand({
172
+ command: "tee",
173
+ args: [outputPath],
174
+ stdin: outputContent,
175
+ toolName: "incident_response",
176
+ timeout: 5000,
177
+ });
178
+ if (result.exitCode === 0) {
179
+ // Get file size
180
+ const sizeResult = await executeCommand({
181
+ command: "stat",
182
+ args: ["-c", "%s", `${collectionDir}/${step.name}.txt`],
183
+ toolName: "incident_response",
184
+ timeout: 5000,
185
+ });
186
+ const size = sizeResult.stdout.trim();
187
+ lines.push(` ✓ ${step.name}: ${step.desc} (${size} bytes)`);
188
+ successCount++;
189
+ }
190
+ else {
191
+ lines.push(` ✗ ${step.name}: ${step.desc} [FAILED: ${result.stderr.trim()}]`);
192
+ failCount++;
193
+ }
194
+ }
195
+ lines.push(``);
196
+ lines.push(`── Collection Summary ──`);
197
+ lines.push(`Successful: ${successCount}/${collectionSteps.length}`);
198
+ lines.push(`Failed: ${failCount}/${collectionSteps.length}`);
199
+ lines.push(`Output: ${collectionDir}/`);
200
+ lines.push(`Completed: ${new Date().toISOString()}`);
201
+ return { content: [createTextContent(lines.join("\n"))] };
202
+ }
203
+ catch (err) {
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ return { content: [createErrorContent(msg)], isError: true };
206
+ }
207
+ }
208
+ // ── ioc_scan ────────────────────────────────────────────────
209
+ case "ioc_scan": {
210
+ const { check_type } = params;
211
+ try {
212
+ const lines = [
213
+ `=== Indicator of Compromise (IOC) Scan ===`,
214
+ `Check Type: ${check_type}`,
215
+ `Scan Time: ${new Date().toISOString()}`,
216
+ ``,
217
+ ];
218
+ let totalFindings = 0;
219
+ // ── Process IOCs ───────────────────────────────────────────────
220
+ if (check_type === "all" || check_type === "processes") {
221
+ lines.push(`── PROCESS IOCs ──`);
222
+ const psResult = await executeCommand({
223
+ command: "ps",
224
+ args: ["aux"],
225
+ toolName: "incident_response",
226
+ timeout: 15000,
227
+ });
228
+ const psLines = psResult.stdout.split("\n").filter((l) => l.trim());
229
+ const suspiciousPaths = ["/tmp/", "/dev/shm/", "/var/tmp/"];
230
+ const tmpProcesses = psLines.filter((line) => suspiciousPaths.some((p) => line.includes(p)));
231
+ if (tmpProcesses.length > 0) {
232
+ lines.push(` [HIGH] Processes running from suspicious paths:`);
233
+ for (const proc of tmpProcesses) {
234
+ lines.push(` ${proc.trim()}`);
235
+ }
236
+ totalFindings += tmpProcesses.length;
237
+ }
238
+ const deletedResult = await executeCommand({
239
+ command: "ls",
240
+ args: ["-la", "/proc/self/exe"],
241
+ toolName: "incident_response",
242
+ timeout: 15000,
243
+ });
244
+ // Use find to locate deleted exe symlinks instead of globbing with shell
245
+ const deletedFindResult = await executeCommand({
246
+ command: "find",
247
+ args: ["/proc", "-maxdepth", "2", "-name", "exe", "-type", "l"],
248
+ toolName: "incident_response",
249
+ timeout: 15000,
250
+ });
251
+ // Filter for "deleted" in TypeScript by checking each symlink
252
+ const exeLinks = deletedFindResult.stdout.split("\n").filter((l) => l.trim());
253
+ const deletedChecks = [];
254
+ for (const link of exeLinks.slice(0, 50)) {
255
+ const lsResult = await executeCommand({
256
+ command: "ls",
257
+ args: ["-la", link],
258
+ toolName: "incident_response",
259
+ timeout: 2000,
260
+ });
261
+ if (lsResult.exitCode === 0 && lsResult.stdout.includes("deleted")) {
262
+ deletedChecks.push(lsResult.stdout.trim());
263
+ }
264
+ }
265
+ const deletedProcs = deletedChecks.slice(0, 20);
266
+ if (deletedProcs.length > 0) {
267
+ lines.push(` [HIGH] Processes with deleted executables:`);
268
+ for (const proc of deletedProcs) {
269
+ lines.push(` ${proc.trim()}`);
270
+ }
271
+ totalFindings += deletedProcs.length;
272
+ }
273
+ const minerProcs = psLines.filter((line) => CRYPTO_MINER_NAMES.some((name) => line.toLowerCase().includes(name)));
274
+ if (minerProcs.length > 0) {
275
+ lines.push(` [CRITICAL] Potential crypto mining processes:`);
276
+ for (const proc of minerProcs) {
277
+ lines.push(` ${proc.trim()}`);
278
+ }
279
+ totalFindings += minerProcs.length;
280
+ }
281
+ if (tmpProcesses.length === 0 &&
282
+ deletedProcs.length === 0 &&
283
+ minerProcs.length === 0) {
284
+ lines.push(` No suspicious processes detected.`);
285
+ }
286
+ lines.push(``);
287
+ }
288
+ // ── Connection IOCs ────────────────────────────────────────────
289
+ if (check_type === "all" || check_type === "connections") {
290
+ lines.push(`── CONNECTION IOCs ──`);
291
+ const ssResult = await executeCommand({
292
+ command: "ss",
293
+ args: ["-tulnpea"],
294
+ toolName: "incident_response",
295
+ timeout: 15000,
296
+ });
297
+ const ssLines = ssResult.stdout.split("\n").filter((l) => l.trim());
298
+ const suspiciousPortConns = ssLines.filter((line) => SUSPICIOUS_PORTS.some((port) => line.includes(`:${port} `) || line.includes(`:${port}\t`)));
299
+ if (suspiciousPortConns.length > 0) {
300
+ lines.push(` [HIGH] Connections on suspicious ports (${SUSPICIOUS_PORTS.join(", ")}):`);
301
+ for (const conn of suspiciousPortConns) {
302
+ lines.push(` ${conn.trim()}`);
303
+ }
304
+ totalFindings += suspiciousPortConns.length;
305
+ }
306
+ const establishedHighPort = ssLines.filter((line) => {
307
+ if (!line.includes("ESTAB"))
308
+ return false;
309
+ const peerMatch = line.match(/\s+(\d+\.\d+\.\d+\.\d+):(\d+)\s*$/);
310
+ if (peerMatch) {
311
+ const port = parseInt(peerMatch[2], 10);
312
+ return port > 1024 && port < 65535;
313
+ }
314
+ return false;
315
+ });
316
+ if (establishedHighPort.length > 0) {
317
+ lines.push(` [MEDIUM] ESTABLISHED connections to high ports (review needed):`);
318
+ for (const conn of establishedHighPort.slice(0, 20)) {
319
+ lines.push(` ${conn.trim()}`);
320
+ }
321
+ if (establishedHighPort.length > 20) {
322
+ lines.push(` ... and ${establishedHighPort.length - 20} more`);
323
+ }
324
+ totalFindings += establishedHighPort.length;
325
+ }
326
+ const ipCounts = {};
327
+ for (const line of ssLines) {
328
+ const peerMatch = line.match(/\s+(\d+\.\d+\.\d+\.\d+):\d+\s*$/);
329
+ if (peerMatch &&
330
+ !peerMatch[1].startsWith("127.") &&
331
+ !peerMatch[1].startsWith("0.")) {
332
+ ipCounts[peerMatch[1]] = (ipCounts[peerMatch[1]] ?? 0) + 1;
333
+ }
334
+ }
335
+ const multiConns = Object.entries(ipCounts)
336
+ .filter(([, count]) => count >= 5)
337
+ .sort(([, a], [, b]) => b - a);
338
+ if (multiConns.length > 0) {
339
+ lines.push(` [MEDIUM] IPs with multiple connections (≥5):`);
340
+ for (const [ip, count] of multiConns) {
341
+ lines.push(` ${ip}: ${count} connections`);
342
+ }
343
+ totalFindings += multiConns.length;
344
+ }
345
+ if (suspiciousPortConns.length === 0 &&
346
+ establishedHighPort.length === 0 &&
347
+ multiConns.length === 0) {
348
+ lines.push(` No suspicious connections detected.`);
349
+ }
350
+ lines.push(``);
351
+ }
352
+ // ── Persistence IOCs ───────────────────────────────────────────
353
+ if (check_type === "all" || check_type === "persistence") {
354
+ lines.push(`── PERSISTENCE IOCs ──`);
355
+ lines.push(` ─ Cron Jobs ─`);
356
+ // Get user crontab
357
+ const crontabResult = await executeCommand({
358
+ command: "crontab",
359
+ args: ["-l"],
360
+ toolName: "incident_response",
361
+ timeout: 10000,
362
+ });
363
+ // Get system crontab
364
+ const cronTabFileResult = await executeCommand({
365
+ command: "cat",
366
+ args: ["/etc/crontab"],
367
+ toolName: "incident_response",
368
+ timeout: 5000,
369
+ });
370
+ // Find cron files in standard directories
371
+ const cronDirs = ["/etc/cron.d", "/etc/cron.daily", "/etc/cron.hourly", "/etc/cron.weekly", "/etc/cron.monthly"];
372
+ const cronFileEntries = [];
373
+ for (const dir of cronDirs) {
374
+ const lsResult = await executeCommand({
375
+ command: "find",
376
+ args: [dir, "-maxdepth", "1", "-type", "f"],
377
+ toolName: "incident_response",
378
+ timeout: 5000,
379
+ });
380
+ if (lsResult.exitCode === 0 && lsResult.stdout.trim()) {
381
+ for (const f of lsResult.stdout.trim().split("\n").filter((l) => l.trim())) {
382
+ const headResult = await executeCommand({
383
+ command: "head",
384
+ args: ["-5", f],
385
+ toolName: "incident_response",
386
+ timeout: 2000,
387
+ });
388
+ cronFileEntries.push(`FILE: ${f}\n${headResult.stdout.trim()}`);
389
+ }
390
+ }
391
+ }
392
+ // Combine all cron data
393
+ const cronParts = [];
394
+ if (crontabResult.exitCode === 0 && crontabResult.stdout.trim()) {
395
+ cronParts.push(crontabResult.stdout.trim());
396
+ }
397
+ for (const entry of cronFileEntries) {
398
+ cronParts.push(entry);
399
+ }
400
+ const cronResult = {
401
+ stdout: cronParts.join("\n---\n"),
402
+ exitCode: 0,
403
+ };
404
+ if (cronResult.stdout.trim() && cronResult.stdout.trim() !== "---") {
405
+ const cronEntries = cronResult.stdout
406
+ .split("---")
407
+ .filter((s) => s.trim());
408
+ lines.push(` Found ${cronEntries.length} cron source(s):`);
409
+ for (const entry of cronEntries.slice(0, 10)) {
410
+ const trimmed = entry.trim().split("\n").slice(0, 3).join("\n ");
411
+ lines.push(` ${trimmed}`);
412
+ }
413
+ const suspiciousCron = cronResult.stdout
414
+ .split("\n")
415
+ .filter((l) => l.includes("/tmp/") ||
416
+ l.includes("/dev/shm/") ||
417
+ l.includes("curl ") ||
418
+ l.includes("wget ") ||
419
+ l.includes("base64"));
420
+ if (suspiciousCron.length > 0) {
421
+ lines.push(` [HIGH] Suspicious cron entries:`);
422
+ for (const s of suspiciousCron) {
423
+ lines.push(` ${s.trim()}`);
424
+ }
425
+ totalFindings += suspiciousCron.length;
426
+ }
427
+ }
428
+ else {
429
+ lines.push(` No user cron entries found.`);
430
+ }
431
+ lines.push(` ─ Systemd Services ─`);
432
+ const systemdResult = await executeCommand({
433
+ command: "find",
434
+ args: [
435
+ "/etc/systemd/system",
436
+ "-maxdepth", "1",
437
+ "-name", "*.service",
438
+ "-mtime", "-7",
439
+ "-type", "f",
440
+ ],
441
+ toolName: "incident_response",
442
+ timeout: 15000,
443
+ });
444
+ const recentServices = systemdResult.stdout
445
+ .split("\n")
446
+ .filter((l) => l.trim());
447
+ if (recentServices.length > 0) {
448
+ lines.push(` [MEDIUM] Recently created service files (last 7 days):`);
449
+ for (const svc of recentServices) {
450
+ lines.push(` ${svc.trim()}`);
451
+ }
452
+ totalFindings += recentServices.length;
453
+ }
454
+ else {
455
+ lines.push(` No recently created systemd services found.`);
456
+ }
457
+ lines.push(` ─ rc.local ─`);
458
+ const rcCatResult = await executeCommand({
459
+ command: "cat",
460
+ args: ["/etc/rc.local"],
461
+ toolName: "incident_response",
462
+ timeout: 10000,
463
+ });
464
+ // Filter out comments, empty lines, and "exit 0" in TypeScript
465
+ const rcContent = rcCatResult.exitCode === 0
466
+ ? rcCatResult.stdout
467
+ .split("\n")
468
+ .filter((l) => l.trim() && !l.trim().startsWith("#") && l.trim() !== "exit 0")
469
+ .join("\n")
470
+ .trim()
471
+ : "";
472
+ if (rcContent) {
473
+ lines.push(` [MEDIUM] Non-standard rc.local entries:`);
474
+ for (const entry of rcContent.split("\n")) {
475
+ lines.push(` ${entry.trim()}`);
476
+ }
477
+ totalFindings += rcContent.split("\n").length;
478
+ }
479
+ else {
480
+ lines.push(` rc.local is clean or not present.`);
481
+ }
482
+ lines.push(` ─ Shell Profile Checks ─`);
483
+ // Search shell profiles for suspicious patterns
484
+ const profileFiles = ["/root/.bashrc", "/root/.profile"];
485
+ // Find user profile files
486
+ const homeFind = await executeCommand({
487
+ command: "find",
488
+ args: ["/home", "-maxdepth", "2", "(", "-name", ".bashrc", "-o", "-name", ".profile", ")", "-type", "f"],
489
+ toolName: "incident_response",
490
+ timeout: 5000,
491
+ });
492
+ if (homeFind.exitCode === 0 && homeFind.stdout.trim()) {
493
+ profileFiles.push(...homeFind.stdout.trim().split("\n").filter((l) => l.trim()));
494
+ }
495
+ const bashrcResult = await executeCommand({
496
+ command: "grep",
497
+ args: ["-rnH", "curl\\|wget\\|base64\\|/dev/tcp\\|nc -e\\|ncat\\|python.*-c.*import", ...profileFiles],
498
+ toolName: "incident_response",
499
+ timeout: 15000,
500
+ });
501
+ const suspiciousShell = bashrcResult.stdout
502
+ .split("\n")
503
+ .filter((l) => l.trim());
504
+ if (suspiciousShell.length > 0) {
505
+ lines.push(` [HIGH] Suspicious shell profile entries:`);
506
+ for (const entry of suspiciousShell) {
507
+ lines.push(` ${entry.trim()}`);
508
+ }
509
+ totalFindings += suspiciousShell.length;
510
+ }
511
+ else {
512
+ lines.push(` No suspicious shell profile entries found.`);
513
+ }
514
+ lines.push(` ─ SSH Authorized Keys ─`);
515
+ const akResult = await executeCommand({
516
+ command: "find",
517
+ args: [
518
+ "/home", "/root",
519
+ "-name", "authorized_keys",
520
+ "-mtime", "-7",
521
+ "-type", "f",
522
+ ],
523
+ toolName: "incident_response",
524
+ timeout: 15000,
525
+ });
526
+ const recentAK = akResult.stdout.split("\n").filter((l) => l.trim());
527
+ if (recentAK.length > 0) {
528
+ lines.push(` [MEDIUM] Recently modified authorized_keys (last 7 days):`);
529
+ for (const ak of recentAK) {
530
+ lines.push(` ${ak.trim()}`);
531
+ }
532
+ totalFindings += recentAK.length;
533
+ }
534
+ else {
535
+ lines.push(` No recently modified authorized_keys files.`);
536
+ }
537
+ lines.push(``);
538
+ }
539
+ // ── Summary ────────────────────────────────────────────────────
540
+ lines.push(`── IOC SCAN SUMMARY ──`);
541
+ lines.push(`Total Findings: ${totalFindings}`);
542
+ if (totalFindings === 0) {
543
+ lines.push(`Status: No indicators of compromise detected.`);
544
+ }
545
+ else if (totalFindings < 5) {
546
+ lines.push(`Status: Minor findings — review recommended.`);
547
+ }
548
+ else {
549
+ lines.push(`Status: MULTIPLE IOCs DETECTED — immediate investigation recommended!`);
550
+ }
551
+ return { content: [createTextContent(lines.join("\n"))] };
552
+ }
553
+ catch (err) {
554
+ const msg = err instanceof Error ? err.message : String(err);
555
+ return { content: [createErrorContent(msg)], isError: true };
556
+ }
557
+ }
558
+ // ── timeline ────────────────────────────────────────────────
559
+ case "timeline": {
560
+ const { path: searchPath, hours, exclude_paths, file_types } = params;
561
+ try {
562
+ const minutes = hours * 60;
563
+ const excludes = exclude_paths
564
+ .split(",")
565
+ .map((p) => p.trim())
566
+ .filter((p) => p);
567
+ const excludeArgs = [];
568
+ for (const ex of excludes) {
569
+ excludeArgs.push("-not", "-path", `${ex}/*`);
570
+ }
571
+ let typeArgs = [];
572
+ switch (file_types) {
573
+ case "executables":
574
+ typeArgs = ["-executable"];
575
+ break;
576
+ case "configs":
577
+ typeArgs = [
578
+ "(",
579
+ "-name", "*.conf",
580
+ "-o", "-name", "*.cfg",
581
+ "-o", "-name", "*.ini",
582
+ "-o", "-name", "*.yaml",
583
+ "-o", "-name", "*.yml",
584
+ "-o", "-name", "*.json",
585
+ ")",
586
+ ];
587
+ break;
588
+ case "scripts":
589
+ typeArgs = [
590
+ "(",
591
+ "-name", "*.sh",
592
+ "-o", "-name", "*.py",
593
+ "-o", "-name", "*.pl",
594
+ "-o", "-name", "*.rb",
595
+ ")",
596
+ ];
597
+ break;
598
+ default:
599
+ break;
600
+ }
601
+ const findArgs = [
602
+ searchPath,
603
+ "-mmin", `-${minutes}`,
604
+ "-type", "f",
605
+ ...excludeArgs,
606
+ ...typeArgs,
607
+ "-printf", "%T@ %m %u:%g %p\\n",
608
+ ];
609
+ const result = await executeCommand({
610
+ command: "find",
611
+ args: findArgs,
612
+ toolName: "incident_response",
613
+ timeout: 60000,
614
+ });
615
+ const fileEntries = result.stdout
616
+ .split("\n")
617
+ .filter((l) => l.trim())
618
+ .map((line) => {
619
+ const parts = line.split(" ");
620
+ const epochStr = parts[0];
621
+ const perms = parts[1];
622
+ const owner = parts[2];
623
+ const filePath = parts.slice(3).join(" ");
624
+ const epoch = parseFloat(epochStr);
625
+ const date = new Date(epoch * 1000);
626
+ return {
627
+ timestamp: date.toISOString(),
628
+ epoch,
629
+ permissions: perms,
630
+ owner,
631
+ path: filePath,
632
+ };
633
+ })
634
+ .filter((e) => !isNaN(e.epoch))
635
+ .sort((a, b) => b.epoch - a.epoch)
636
+ .slice(0, 200);
637
+ const lines = [
638
+ `=== Filesystem Timeline ===`,
639
+ `Search Path: ${searchPath}`,
640
+ `Timeframe: Last ${hours} hour(s)`,
641
+ `File Types: ${file_types}`,
642
+ `Excluded: ${exclude_paths}`,
643
+ `Results: ${fileEntries.length} file(s) (max 200)`,
644
+ ``,
645
+ `── TIMELINE (newest first) ──`,
646
+ ``,
647
+ ];
648
+ if (fileEntries.length === 0) {
649
+ lines.push(`No modified files found matching criteria.`);
650
+ }
651
+ else {
652
+ let lastHour = "";
653
+ for (const entry of fileEntries) {
654
+ const hourKey = entry.timestamp.substring(0, 13);
655
+ if (hourKey !== lastHour) {
656
+ lines.push(``);
657
+ lines.push(` ── ${hourKey}:00Z ──`);
658
+ lastHour = hourKey;
659
+ }
660
+ lines.push(` ${entry.timestamp} ${entry.permissions} ${entry.owner} ${entry.path}`);
661
+ }
662
+ }
663
+ lines.push(``);
664
+ lines.push(`── STATISTICS ──`);
665
+ lines.push(`Total files found: ${fileEntries.length}`);
666
+ const ownerCounts = {};
667
+ for (const entry of fileEntries) {
668
+ ownerCounts[entry.owner] = (ownerCounts[entry.owner] ?? 0) + 1;
669
+ }
670
+ lines.push(`Files by owner:`);
671
+ for (const [owner, count] of Object.entries(ownerCounts).sort(([, a], [, b]) => b - a)) {
672
+ lines.push(` ${owner}: ${count}`);
673
+ }
674
+ const dirCounts = {};
675
+ for (const entry of fileEntries) {
676
+ const dir = entry.path.substring(0, entry.path.lastIndexOf("/"));
677
+ dirCounts[dir] = (dirCounts[dir] ?? 0) + 1;
678
+ }
679
+ const topDirs = Object.entries(dirCounts)
680
+ .sort(([, a], [, b]) => b - a)
681
+ .slice(0, 10);
682
+ lines.push(`Top directories with changes:`);
683
+ for (const [dir, count] of topDirs) {
684
+ lines.push(` ${dir}: ${count}`);
685
+ }
686
+ return { content: [createTextContent(lines.join("\n"))] };
687
+ }
688
+ catch (err) {
689
+ const msg = err instanceof Error ? err.message : String(err);
690
+ return { content: [createErrorContent(msg)], isError: true };
691
+ }
692
+ }
693
+ default:
694
+ return { content: [createErrorContent(`Unknown action: ${action}`)], isError: true };
695
+ }
696
+ });
697
+ // ── ir_forensics tool ──────────────────────────────────────────────────
698
+ server.tool("ir_forensics", "Digital forensics: acquire memory dumps, create forensic disk images, capture network traffic, bag evidence, and manage chain-of-custody logs.", {
699
+ action: z
700
+ .enum([
701
+ "memory_dump",
702
+ "disk_image",
703
+ "network_capture_forensic",
704
+ "evidence_bag",
705
+ "chain_of_custody",
706
+ ])
707
+ .describe("Action: memory_dump=acquire RAM, disk_image=forensic disk copy, network_capture_forensic=full packet capture, evidence_bag=bag+hash artifact, chain_of_custody=manage custody log"),
708
+ output_dir: z
709
+ .string()
710
+ .optional()
711
+ .default(DEFAULT_FORENSICS_DIR)
712
+ .describe("Directory to store forensic artifacts"),
713
+ case_id: z
714
+ .string()
715
+ .optional()
716
+ .describe("Case identifier for chain-of-custody tracking"),
717
+ device: z
718
+ .string()
719
+ .optional()
720
+ .describe("Device path for disk imaging (e.g. /dev/sda1)"),
721
+ interface: z
722
+ .string()
723
+ .optional()
724
+ .default("any")
725
+ .describe("Network interface for capture (default: any)"),
726
+ duration: z
727
+ .number()
728
+ .optional()
729
+ .default(60)
730
+ .describe("Capture duration in seconds (default 60, max 300)"),
731
+ evidence_path: z
732
+ .string()
733
+ .optional()
734
+ .describe("Path to evidence file (used with evidence_bag, chain_of_custody)"),
735
+ description: z
736
+ .string()
737
+ .optional()
738
+ .describe("Description of the evidence item"),
739
+ examiner: z
740
+ .string()
741
+ .optional()
742
+ .describe("Name of the forensic examiner"),
743
+ custody_action: z
744
+ .enum(["add", "view", "verify"])
745
+ .optional()
746
+ .default("view")
747
+ .describe("Chain-of-custody sub-action: add, view, or verify"),
748
+ }, async (params) => {
749
+ const { action } = params;
750
+ switch (action) {
751
+ // ── memory_dump ────────────────────────────────────────────────
752
+ case "memory_dump": {
753
+ const { output_dir } = params;
754
+ try {
755
+ // Create output directory
756
+ const mkdirResult = await runForensicCommand("mkdir", ["-p", output_dir]);
757
+ if (mkdirResult.exitCode !== 0) {
758
+ return {
759
+ content: [createErrorContent(`Failed to create output directory: ${mkdirResult.stderr}`)],
760
+ isError: true,
761
+ };
762
+ }
763
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
764
+ const dumpPath = `${output_dir}/memory-dump-${timestamp}.raw`;
765
+ // Try avml first (preferred)
766
+ const avmlResult = await runForensicCommand("avml", [dumpPath], 120_000);
767
+ let toolUsed;
768
+ if (avmlResult.exitCode === 0) {
769
+ toolUsed = "avml";
770
+ }
771
+ else {
772
+ // Fallback to dd from /proc/kcore
773
+ const ddResult = await runForensicCommand("dd", ["if=/proc/kcore", `of=${dumpPath}`, "bs=1M", "status=progress"], 120_000);
774
+ if (ddResult.exitCode !== 0) {
775
+ return {
776
+ content: [createErrorContent(`Memory dump failed: avml: ${avmlResult.stderr}; dd: ${ddResult.stderr}`)],
777
+ isError: true,
778
+ };
779
+ }
780
+ toolUsed = "dd (fallback from /proc/kcore)";
781
+ }
782
+ // Calculate SHA-256 hash
783
+ const hashResult = await runForensicCommand("sha256sum", [dumpPath]);
784
+ const hash = hashResult.exitCode === 0
785
+ ? hashResult.stdout.trim().split(/\s+/)[0]
786
+ : "hash-calculation-failed";
787
+ // Get file size
788
+ const statResult = await runForensicCommand("stat", ["-c", "%s", dumpPath]);
789
+ const size = statResult.exitCode === 0 ? statResult.stdout.trim() : "unknown";
790
+ const lines = [
791
+ `=== Memory Dump Acquisition ===`,
792
+ `Tool Used: ${toolUsed}`,
793
+ `Dump Path: ${dumpPath}`,
794
+ `Size: ${size} bytes`,
795
+ `SHA-256: ${hash}`,
796
+ `Timestamp: ${new Date().toISOString()}`,
797
+ ];
798
+ return { content: [createTextContent(lines.join("\n"))] };
799
+ }
800
+ catch (err) {
801
+ const msg = err instanceof Error ? err.message : String(err);
802
+ return { content: [createErrorContent(msg)], isError: true };
803
+ }
804
+ }
805
+ // ── disk_image ─────────────────────────────────────────────────
806
+ case "disk_image": {
807
+ const { output_dir, device } = params;
808
+ try {
809
+ if (!device) {
810
+ return {
811
+ content: [createErrorContent("device parameter is required for disk_image action")],
812
+ isError: true,
813
+ };
814
+ }
815
+ // Validate device path
816
+ if (!device.startsWith("/dev/")) {
817
+ return {
818
+ content: [createErrorContent(`Invalid device path: ${device}. Must start with /dev/`)],
819
+ isError: true,
820
+ };
821
+ }
822
+ // Block root device imaging
823
+ if (device === "/dev/sda" || device === "/dev/vda" || device === "/dev/nvme0n1" || device === "/dev/xvda") {
824
+ return {
825
+ content: [createErrorContent(`Refusing to image root device: ${device}. Specify a partition (e.g. /dev/sda1)`)],
826
+ isError: true,
827
+ };
828
+ }
829
+ // Create output directory
830
+ const mkdirResult = await runForensicCommand("mkdir", ["-p", output_dir]);
831
+ if (mkdirResult.exitCode !== 0) {
832
+ return {
833
+ content: [createErrorContent(`Failed to create output directory: ${mkdirResult.stderr}`)],
834
+ isError: true,
835
+ };
836
+ }
837
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
838
+ const imagePath = `${output_dir}/disk-image-${timestamp}.raw`;
839
+ // Create forensic disk image
840
+ const ddResult = await runForensicCommand("dd", [`if=${device}`, `of=${imagePath}`, "bs=4M", "conv=noerror,sync", "status=progress"], 600_000);
841
+ if (ddResult.exitCode !== 0) {
842
+ return {
843
+ content: [createErrorContent(`Disk imaging failed: ${ddResult.stderr}`)],
844
+ isError: true,
845
+ };
846
+ }
847
+ // Calculate SHA-256 hash
848
+ const hashResult = await runForensicCommand("sha256sum", [imagePath], 300_000);
849
+ const hash = hashResult.exitCode === 0
850
+ ? hashResult.stdout.trim().split(/\s+/)[0]
851
+ : "hash-calculation-failed";
852
+ // Get file size
853
+ const statResult = await runForensicCommand("stat", ["-c", "%s", imagePath]);
854
+ const size = statResult.exitCode === 0 ? statResult.stdout.trim() : "unknown";
855
+ // Capture partition info
856
+ const fdiskResult = await runForensicCommand("fdisk", ["-l", device]);
857
+ const partitionInfo = fdiskResult.exitCode === 0
858
+ ? fdiskResult.stdout.trim()
859
+ : `fdisk failed: ${fdiskResult.stderr}`;
860
+ const lines = [
861
+ `=== Forensic Disk Image ===`,
862
+ `Source Device: ${device}`,
863
+ `Image Path: ${imagePath}`,
864
+ `Size: ${size} bytes`,
865
+ `SHA-256: ${hash}`,
866
+ `Timestamp: ${new Date().toISOString()}`,
867
+ ``,
868
+ `── Partition Info ──`,
869
+ partitionInfo,
870
+ ];
871
+ return { content: [createTextContent(lines.join("\n"))] };
872
+ }
873
+ catch (err) {
874
+ const msg = err instanceof Error ? err.message : String(err);
875
+ return { content: [createErrorContent(msg)], isError: true };
876
+ }
877
+ }
878
+ // ── network_capture_forensic ───────────────────────────────────
879
+ case "network_capture_forensic": {
880
+ const { output_dir, duration } = params;
881
+ const iface = params.interface ?? "any";
882
+ try {
883
+ // Cap duration at 300 seconds
884
+ const cappedDuration = Math.min(duration ?? 60, 300);
885
+ // Create output directory
886
+ const mkdirResult = await runForensicCommand("mkdir", ["-p", output_dir]);
887
+ if (mkdirResult.exitCode !== 0) {
888
+ return {
889
+ content: [createErrorContent(`Failed to create output directory: ${mkdirResult.stderr}`)],
890
+ isError: true,
891
+ };
892
+ }
893
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
894
+ const capturePath = `${output_dir}/capture-${timestamp}.pcap`;
895
+ const startTime = new Date().toISOString();
896
+ // Full packet capture with tcpdump
897
+ const tcpdumpResult = await runForensicCommand("tcpdump", [
898
+ "-i", iface,
899
+ "-s", "0",
900
+ "-w", capturePath,
901
+ "-c", "0",
902
+ "-G", String(cappedDuration),
903
+ "-W", "1",
904
+ ], (cappedDuration + 10) * 1000);
905
+ const endTime = new Date().toISOString();
906
+ // Even if tcpdump exits non-zero (e.g. due to timeout/signal), the file may exist
907
+ // Calculate SHA-256 hash
908
+ const hashResult = await runForensicCommand("sha256sum", [capturePath]);
909
+ const hash = hashResult.exitCode === 0
910
+ ? hashResult.stdout.trim().split(/\s+/)[0]
911
+ : "hash-calculation-failed";
912
+ // Get file size
913
+ const statResult = await runForensicCommand("stat", ["-c", "%s", capturePath]);
914
+ const size = statResult.exitCode === 0 ? statResult.stdout.trim() : "unknown";
915
+ // Get packet count using tcpdump -r
916
+ const countResult = await runForensicCommand("tcpdump", ["-r", capturePath, "--count"], 30_000);
917
+ // tcpdump --count outputs like "N packets" on stderr
918
+ const packetCountMatch = (countResult.stdout + countResult.stderr).match(/(\d+)\s+packet/);
919
+ const packetCount = packetCountMatch ? packetCountMatch[1] : "unknown";
920
+ const lines = [
921
+ `=== Forensic Network Capture ===`,
922
+ `Interface: ${iface}`,
923
+ `Capture Path: ${capturePath}`,
924
+ `Duration: ${cappedDuration}s`,
925
+ `Start Time: ${startTime}`,
926
+ `End Time: ${endTime}`,
927
+ `Packets Captured: ${packetCount}`,
928
+ `Size: ${size} bytes`,
929
+ `SHA-256: ${hash}`,
930
+ ];
931
+ return { content: [createTextContent(lines.join("\n"))] };
932
+ }
933
+ catch (err) {
934
+ const msg = err instanceof Error ? err.message : String(err);
935
+ return { content: [createErrorContent(msg)], isError: true };
936
+ }
937
+ }
938
+ // ── evidence_bag ───────────────────────────────────────────────
939
+ case "evidence_bag": {
940
+ const { output_dir, case_id, evidence_path, description, examiner } = params;
941
+ try {
942
+ if (!evidence_path) {
943
+ return {
944
+ content: [createErrorContent("evidence_path parameter is required for evidence_bag action")],
945
+ isError: true,
946
+ };
947
+ }
948
+ const effectiveCaseId = case_id ?? "default";
949
+ const evidenceDir = `${output_dir}/${effectiveCaseId}/evidence`;
950
+ // Create evidence directory
951
+ const mkdirResult = await runForensicCommand("mkdir", ["-p", evidenceDir]);
952
+ if (mkdirResult.exitCode !== 0) {
953
+ return {
954
+ content: [createErrorContent(`Failed to create evidence directory: ${mkdirResult.stderr}`)],
955
+ isError: true,
956
+ };
957
+ }
958
+ // Copy file to evidence directory
959
+ const basename = evidence_path.split("/").pop() ?? "evidence";
960
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
961
+ const destPath = `${evidenceDir}/${timestamp}-${basename}`;
962
+ const cpResult = await runForensicCommand("cp", ["-p", evidence_path, destPath]);
963
+ if (cpResult.exitCode !== 0) {
964
+ return {
965
+ content: [createErrorContent(`Failed to copy evidence: ${cpResult.stderr}`)],
966
+ isError: true,
967
+ };
968
+ }
969
+ // Calculate SHA-256 hash
970
+ const hashResult = await runForensicCommand("sha256sum", [destPath]);
971
+ const hash = hashResult.exitCode === 0
972
+ ? hashResult.stdout.trim().split(/\s+/)[0]
973
+ : "hash-calculation-failed";
974
+ // Get file size
975
+ const statResult = await runForensicCommand("stat", ["-c", "%s", destPath]);
976
+ const fileSize = statResult.exitCode === 0 ? statResult.stdout.trim() : "unknown";
977
+ // Create metadata sidecar file
978
+ const metadata = {
979
+ original_path: evidence_path,
980
+ hash,
981
+ timestamp: new Date().toISOString(),
982
+ case_id: effectiveCaseId,
983
+ description: description ?? "",
984
+ examiner: examiner ?? "unknown",
985
+ file_size: fileSize,
986
+ };
987
+ const metadataPath = `${destPath}.metadata.json`;
988
+ secureWriteFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
989
+ const lines = [
990
+ `=== Evidence Bagged ===`,
991
+ `Original Path: ${evidence_path}`,
992
+ `Evidence Path: ${destPath}`,
993
+ `Metadata: ${metadataPath}`,
994
+ `Case ID: ${effectiveCaseId}`,
995
+ `SHA-256: ${hash}`,
996
+ `Size: ${fileSize} bytes`,
997
+ `Description: ${description ?? "N/A"}`,
998
+ `Examiner: ${examiner ?? "unknown"}`,
999
+ `Timestamp: ${metadata.timestamp}`,
1000
+ ];
1001
+ return { content: [createTextContent(lines.join("\n"))] };
1002
+ }
1003
+ catch (err) {
1004
+ const msg = err instanceof Error ? err.message : String(err);
1005
+ return { content: [createErrorContent(msg)], isError: true };
1006
+ }
1007
+ }
1008
+ // ── chain_of_custody ───────────────────────────────────────────
1009
+ case "chain_of_custody": {
1010
+ const { output_dir, case_id, evidence_path, description, examiner, custody_action } = params;
1011
+ try {
1012
+ if (!case_id) {
1013
+ return {
1014
+ content: [createErrorContent("case_id parameter is required for chain_of_custody action")],
1015
+ isError: true,
1016
+ };
1017
+ }
1018
+ const caseDir = `${output_dir}/${case_id}`;
1019
+ const custodyLogPath = `${caseDir}/custody-log.json`;
1020
+ // Ensure case directory exists
1021
+ const mkdirResult = await runForensicCommand("mkdir", ["-p", caseDir]);
1022
+ if (mkdirResult.exitCode !== 0) {
1023
+ return {
1024
+ content: [createErrorContent(`Failed to create case directory: ${mkdirResult.stderr}`)],
1025
+ isError: true,
1026
+ };
1027
+ }
1028
+ const effectiveAction = custody_action ?? "view";
1029
+ switch (effectiveAction) {
1030
+ case "add": {
1031
+ // Read existing log or create new
1032
+ let custodyLog = [];
1033
+ if (existsSync(custodyLogPath)) {
1034
+ try {
1035
+ const raw = readFileSync(custodyLogPath, "utf-8");
1036
+ custodyLog = JSON.parse(raw);
1037
+ }
1038
+ catch {
1039
+ custodyLog = [];
1040
+ }
1041
+ }
1042
+ // Calculate hash if evidence_path provided
1043
+ let evidenceHash = "N/A";
1044
+ if (evidence_path) {
1045
+ const hashResult = await runForensicCommand("sha256sum", [evidence_path]);
1046
+ evidenceHash = hashResult.exitCode === 0
1047
+ ? hashResult.stdout.trim().split(/\s+/)[0]
1048
+ : "hash-calculation-failed";
1049
+ }
1050
+ const entry = {
1051
+ timestamp: new Date().toISOString(),
1052
+ action: "collected",
1053
+ examiner: examiner ?? "unknown",
1054
+ description: description ?? "",
1055
+ evidence_hash: evidenceHash,
1056
+ evidence_path: evidence_path ?? "N/A",
1057
+ };
1058
+ custodyLog.push(entry);
1059
+ // Write log using secureFsWrite
1060
+ secureWriteFileSync(custodyLogPath, JSON.stringify(custodyLog, null, 2), "utf-8");
1061
+ const lines = [
1062
+ `=== Chain of Custody — Entry Added ===`,
1063
+ `Case ID: ${case_id}`,
1064
+ `Log Path: ${custodyLogPath}`,
1065
+ `Entry #${custodyLog.length}:`,
1066
+ ` Timestamp: ${entry.timestamp}`,
1067
+ ` Action: ${entry.action}`,
1068
+ ` Examiner: ${entry.examiner}`,
1069
+ ` Description: ${entry.description}`,
1070
+ ` Evidence Hash: ${entry.evidence_hash}`,
1071
+ ` Evidence Path: ${entry.evidence_path}`,
1072
+ `Total Entries: ${custodyLog.length}`,
1073
+ ];
1074
+ return { content: [createTextContent(lines.join("\n"))] };
1075
+ }
1076
+ case "view": {
1077
+ if (!existsSync(custodyLogPath)) {
1078
+ return {
1079
+ content: [createTextContent(`=== Chain of Custody — ${case_id} ===\nNo custody log found for case ${case_id}.\nLog path: ${custodyLogPath}`)],
1080
+ };
1081
+ }
1082
+ let custodyLog = [];
1083
+ try {
1084
+ const raw = readFileSync(custodyLogPath, "utf-8");
1085
+ custodyLog = JSON.parse(raw);
1086
+ }
1087
+ catch {
1088
+ return {
1089
+ content: [createErrorContent(`Failed to parse custody log at ${custodyLogPath}`)],
1090
+ isError: true,
1091
+ };
1092
+ }
1093
+ const lines = [
1094
+ `=== Chain of Custody — ${case_id} ===`,
1095
+ `Log Path: ${custodyLogPath}`,
1096
+ `Total Entries: ${custodyLog.length}`,
1097
+ ``,
1098
+ ];
1099
+ for (let i = 0; i < custodyLog.length; i++) {
1100
+ const entry = custodyLog[i];
1101
+ lines.push(`── Entry #${i + 1} ──`);
1102
+ lines.push(` Timestamp: ${entry.timestamp ?? "N/A"}`);
1103
+ lines.push(` Action: ${entry.action ?? "N/A"}`);
1104
+ lines.push(` Examiner: ${entry.examiner ?? "N/A"}`);
1105
+ lines.push(` Description: ${entry.description ?? "N/A"}`);
1106
+ lines.push(` Evidence Hash: ${entry.evidence_hash ?? "N/A"}`);
1107
+ lines.push(` Evidence Path: ${entry.evidence_path ?? "N/A"}`);
1108
+ lines.push(``);
1109
+ }
1110
+ return { content: [createTextContent(lines.join("\n"))] };
1111
+ }
1112
+ case "verify": {
1113
+ if (!evidence_path) {
1114
+ return {
1115
+ content: [createErrorContent("evidence_path parameter is required for chain_of_custody verify action")],
1116
+ isError: true,
1117
+ };
1118
+ }
1119
+ if (!existsSync(custodyLogPath)) {
1120
+ return {
1121
+ content: [createErrorContent(`No custody log found for case ${case_id}`)],
1122
+ isError: true,
1123
+ };
1124
+ }
1125
+ let custodyLog = [];
1126
+ try {
1127
+ const raw = readFileSync(custodyLogPath, "utf-8");
1128
+ custodyLog = JSON.parse(raw);
1129
+ }
1130
+ catch {
1131
+ return {
1132
+ content: [createErrorContent(`Failed to parse custody log at ${custodyLogPath}`)],
1133
+ isError: true,
1134
+ };
1135
+ }
1136
+ // Re-hash evidence
1137
+ const hashResult = await runForensicCommand("sha256sum", [evidence_path]);
1138
+ const currentHash = hashResult.exitCode === 0
1139
+ ? hashResult.stdout.trim().split(/\s+/)[0]
1140
+ : null;
1141
+ if (!currentHash) {
1142
+ return {
1143
+ content: [createErrorContent(`Failed to hash evidence file: ${hashResult.stderr}`)],
1144
+ isError: true,
1145
+ };
1146
+ }
1147
+ // Find matching entry in custody log
1148
+ const matchingEntry = custodyLog.find((entry) => entry.evidence_path === evidence_path || entry.evidence_hash === currentHash);
1149
+ const recordedHash = matchingEntry?.evidence_hash;
1150
+ const hashMatch = recordedHash === currentHash;
1151
+ const lines = [
1152
+ `=== Chain of Custody — Verification ===`,
1153
+ `Case ID: ${case_id}`,
1154
+ `Evidence Path: ${evidence_path}`,
1155
+ `Current SHA-256: ${currentHash}`,
1156
+ `Recorded SHA-256: ${recordedHash ?? "NOT FOUND"}`,
1157
+ `Integrity: ${hashMatch ? "✓ VERIFIED — hashes match" : "✗ MISMATCH — evidence may be tampered"}`,
1158
+ ];
1159
+ return { content: [createTextContent(lines.join("\n"))] };
1160
+ }
1161
+ default:
1162
+ return {
1163
+ content: [createErrorContent(`Unknown custody_action: ${effectiveAction}`)],
1164
+ isError: true,
1165
+ };
1166
+ }
1167
+ }
1168
+ catch (err) {
1169
+ const msg = err instanceof Error ? err.message : String(err);
1170
+ return { content: [createErrorContent(msg)], isError: true };
1171
+ }
1172
+ }
1173
+ default:
1174
+ return {
1175
+ content: [createErrorContent(`Unknown ir_forensics action: ${action}`)],
1176
+ isError: true,
1177
+ };
1178
+ }
1179
+ });
1180
+ }