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.
- package/CHANGELOG.md +471 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/build/core/auto-installer.d.ts +102 -0
- package/build/core/auto-installer.d.ts.map +1 -0
- package/build/core/auto-installer.js +833 -0
- package/build/core/backup-manager.d.ts +63 -0
- package/build/core/backup-manager.d.ts.map +1 -0
- package/build/core/backup-manager.js +189 -0
- package/build/core/changelog.d.ts +75 -0
- package/build/core/changelog.d.ts.map +1 -0
- package/build/core/changelog.js +123 -0
- package/build/core/command-allowlist.d.ts +129 -0
- package/build/core/command-allowlist.d.ts.map +1 -0
- package/build/core/command-allowlist.js +849 -0
- package/build/core/config.d.ts +79 -0
- package/build/core/config.d.ts.map +1 -0
- package/build/core/config.js +193 -0
- package/build/core/dependency-validator.d.ts +106 -0
- package/build/core/dependency-validator.d.ts.map +1 -0
- package/build/core/dependency-validator.js +405 -0
- package/build/core/distro-adapter.d.ts +177 -0
- package/build/core/distro-adapter.d.ts.map +1 -0
- package/build/core/distro-adapter.js +481 -0
- package/build/core/distro.d.ts +68 -0
- package/build/core/distro.d.ts.map +1 -0
- package/build/core/distro.js +457 -0
- package/build/core/encrypted-state.d.ts +76 -0
- package/build/core/encrypted-state.d.ts.map +1 -0
- package/build/core/encrypted-state.js +209 -0
- package/build/core/executor.d.ts +56 -0
- package/build/core/executor.d.ts.map +1 -0
- package/build/core/executor.js +350 -0
- package/build/core/installer.d.ts +92 -0
- package/build/core/installer.d.ts.map +1 -0
- package/build/core/installer.js +1072 -0
- package/build/core/logger.d.ts +102 -0
- package/build/core/logger.d.ts.map +1 -0
- package/build/core/logger.js +132 -0
- package/build/core/parsers.d.ts +151 -0
- package/build/core/parsers.d.ts.map +1 -0
- package/build/core/parsers.js +479 -0
- package/build/core/policy-engine.d.ts +170 -0
- package/build/core/policy-engine.d.ts.map +1 -0
- package/build/core/policy-engine.js +656 -0
- package/build/core/preflight.d.ts +157 -0
- package/build/core/preflight.d.ts.map +1 -0
- package/build/core/preflight.js +638 -0
- package/build/core/privilege-manager.d.ts +108 -0
- package/build/core/privilege-manager.d.ts.map +1 -0
- package/build/core/privilege-manager.js +363 -0
- package/build/core/rate-limiter.d.ts +67 -0
- package/build/core/rate-limiter.d.ts.map +1 -0
- package/build/core/rate-limiter.js +129 -0
- package/build/core/rollback.d.ts +73 -0
- package/build/core/rollback.d.ts.map +1 -0
- package/build/core/rollback.js +278 -0
- package/build/core/safeguards.d.ts +58 -0
- package/build/core/safeguards.d.ts.map +1 -0
- package/build/core/safeguards.js +448 -0
- package/build/core/sanitizer.d.ts +118 -0
- package/build/core/sanitizer.d.ts.map +1 -0
- package/build/core/sanitizer.js +459 -0
- package/build/core/secure-fs.d.ts +67 -0
- package/build/core/secure-fs.d.ts.map +1 -0
- package/build/core/secure-fs.js +143 -0
- package/build/core/spawn-safe.d.ts +55 -0
- package/build/core/spawn-safe.d.ts.map +1 -0
- package/build/core/spawn-safe.js +146 -0
- package/build/core/sudo-guard.d.ts +145 -0
- package/build/core/sudo-guard.d.ts.map +1 -0
- package/build/core/sudo-guard.js +349 -0
- package/build/core/sudo-session.d.ts +100 -0
- package/build/core/sudo-session.d.ts.map +1 -0
- package/build/core/sudo-session.js +319 -0
- package/build/core/tool-dependencies.d.ts +61 -0
- package/build/core/tool-dependencies.d.ts.map +1 -0
- package/build/core/tool-dependencies.js +571 -0
- package/build/core/tool-registry.d.ts +111 -0
- package/build/core/tool-registry.d.ts.map +1 -0
- package/build/core/tool-registry.js +656 -0
- package/build/core/tool-wrapper.d.ts +73 -0
- package/build/core/tool-wrapper.d.ts.map +1 -0
- package/build/core/tool-wrapper.js +296 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +247 -0
- package/build/tools/access-control.d.ts +9 -0
- package/build/tools/access-control.d.ts.map +1 -0
- package/build/tools/access-control.js +1818 -0
- package/build/tools/api-security.d.ts +12 -0
- package/build/tools/api-security.d.ts.map +1 -0
- package/build/tools/api-security.js +901 -0
- package/build/tools/app-hardening.d.ts +11 -0
- package/build/tools/app-hardening.d.ts.map +1 -0
- package/build/tools/app-hardening.js +768 -0
- package/build/tools/backup.d.ts +8 -0
- package/build/tools/backup.d.ts.map +1 -0
- package/build/tools/backup.js +381 -0
- package/build/tools/cloud-security.d.ts +17 -0
- package/build/tools/cloud-security.d.ts.map +1 -0
- package/build/tools/cloud-security.js +739 -0
- package/build/tools/compliance.d.ts +10 -0
- package/build/tools/compliance.d.ts.map +1 -0
- package/build/tools/compliance.js +1225 -0
- package/build/tools/container-security.d.ts +14 -0
- package/build/tools/container-security.d.ts.map +1 -0
- package/build/tools/container-security.js +788 -0
- package/build/tools/deception.d.ts +13 -0
- package/build/tools/deception.d.ts.map +1 -0
- package/build/tools/deception.js +763 -0
- package/build/tools/dns-security.d.ts +93 -0
- package/build/tools/dns-security.d.ts.map +1 -0
- package/build/tools/dns-security.js +745 -0
- package/build/tools/drift-detection.d.ts +8 -0
- package/build/tools/drift-detection.d.ts.map +1 -0
- package/build/tools/drift-detection.js +326 -0
- package/build/tools/ebpf-security.d.ts +15 -0
- package/build/tools/ebpf-security.d.ts.map +1 -0
- package/build/tools/ebpf-security.js +294 -0
- package/build/tools/encryption.d.ts +9 -0
- package/build/tools/encryption.d.ts.map +1 -0
- package/build/tools/encryption.js +1667 -0
- package/build/tools/firewall.d.ts +9 -0
- package/build/tools/firewall.d.ts.map +1 -0
- package/build/tools/firewall.js +1398 -0
- package/build/tools/hardening.d.ts +10 -0
- package/build/tools/hardening.d.ts.map +1 -0
- package/build/tools/hardening.js +2654 -0
- package/build/tools/ids.d.ts +9 -0
- package/build/tools/ids.d.ts.map +1 -0
- package/build/tools/ids.js +624 -0
- package/build/tools/incident-response.d.ts +10 -0
- package/build/tools/incident-response.d.ts.map +1 -0
- package/build/tools/incident-response.js +1180 -0
- package/build/tools/logging.d.ts +12 -0
- package/build/tools/logging.d.ts.map +1 -0
- package/build/tools/logging.js +454 -0
- package/build/tools/malware.d.ts +10 -0
- package/build/tools/malware.d.ts.map +1 -0
- package/build/tools/malware.js +532 -0
- package/build/tools/meta.d.ts +11 -0
- package/build/tools/meta.d.ts.map +1 -0
- package/build/tools/meta.js +2278 -0
- package/build/tools/network-defense.d.ts +12 -0
- package/build/tools/network-defense.d.ts.map +1 -0
- package/build/tools/network-defense.js +760 -0
- package/build/tools/patch-management.d.ts +3 -0
- package/build/tools/patch-management.d.ts.map +1 -0
- package/build/tools/patch-management.js +708 -0
- package/build/tools/process-security.d.ts +12 -0
- package/build/tools/process-security.d.ts.map +1 -0
- package/build/tools/process-security.js +784 -0
- package/build/tools/reporting.d.ts +11 -0
- package/build/tools/reporting.d.ts.map +1 -0
- package/build/tools/reporting.js +559 -0
- package/build/tools/secrets.d.ts +9 -0
- package/build/tools/secrets.d.ts.map +1 -0
- package/build/tools/secrets.js +596 -0
- package/build/tools/siem-integration.d.ts +18 -0
- package/build/tools/siem-integration.d.ts.map +1 -0
- package/build/tools/siem-integration.js +754 -0
- package/build/tools/sudo-management.d.ts +18 -0
- package/build/tools/sudo-management.d.ts.map +1 -0
- package/build/tools/sudo-management.js +737 -0
- package/build/tools/supply-chain-security.d.ts +8 -0
- package/build/tools/supply-chain-security.d.ts.map +1 -0
- package/build/tools/supply-chain-security.js +256 -0
- package/build/tools/threat-intel.d.ts +22 -0
- package/build/tools/threat-intel.d.ts.map +1 -0
- package/build/tools/threat-intel.js +749 -0
- package/build/tools/vulnerability-management.d.ts +11 -0
- package/build/tools/vulnerability-management.d.ts.map +1 -0
- package/build/tools/vulnerability-management.js +667 -0
- package/build/tools/waf.d.ts +12 -0
- package/build/tools/waf.d.ts.map +1 -0
- package/build/tools/waf.js +843 -0
- package/build/tools/wireless-security.d.ts +19 -0
- package/build/tools/wireless-security.d.ts.map +1 -0
- package/build/tools/wireless-security.js +826 -0
- package/build/tools/zero-trust-network.d.ts +8 -0
- package/build/tools/zero-trust-network.d.ts.map +1 -0
- package/build/tools/zero-trust-network.js +367 -0
- package/docs/SAFEGUARDS.md +518 -0
- package/docs/TOOLS-REFERENCE.md +665 -0
- 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
|
+
}
|