windows-exe-decompiler-mcp-server 0.1.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/CODEX_INSTALLATION.md +69 -0
- package/COPILOT_INSTALLATION.md +77 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/bin/windows-exe-decompiler-mcp-server.js +3 -0
- package/dist/analysis-provenance.d.ts +184 -0
- package/dist/analysis-provenance.js +74 -0
- package/dist/analysis-task-runner.d.ts +31 -0
- package/dist/analysis-task-runner.js +160 -0
- package/dist/artifact-inventory.d.ts +23 -0
- package/dist/artifact-inventory.js +175 -0
- package/dist/cache-manager.d.ts +128 -0
- package/dist/cache-manager.js +454 -0
- package/dist/confidence-semantics.d.ts +66 -0
- package/dist/confidence-semantics.js +122 -0
- package/dist/config.d.ts +335 -0
- package/dist/config.js +193 -0
- package/dist/database.d.ts +227 -0
- package/dist/database.js +601 -0
- package/dist/decompiler-worker.d.ts +441 -0
- package/dist/decompiler-worker.js +1962 -0
- package/dist/dynamic-trace.d.ts +95 -0
- package/dist/dynamic-trace.js +629 -0
- package/dist/env-validator.d.ts +15 -0
- package/dist/env-validator.js +249 -0
- package/dist/error-handler.d.ts +28 -0
- package/dist/error-handler.example.d.ts +22 -0
- package/dist/error-handler.example.js +141 -0
- package/dist/error-handler.js +139 -0
- package/dist/ghidra-analysis-status.d.ts +49 -0
- package/dist/ghidra-analysis-status.js +178 -0
- package/dist/ghidra-config.d.ts +134 -0
- package/dist/ghidra-config.js +464 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +200 -0
- package/dist/job-queue.d.ts +169 -0
- package/dist/job-queue.js +407 -0
- package/dist/logger.d.ts +106 -0
- package/dist/logger.js +176 -0
- package/dist/policy-guard.d.ts +115 -0
- package/dist/policy-guard.js +243 -0
- package/dist/process-output.d.ts +15 -0
- package/dist/process-output.js +90 -0
- package/dist/prompts/function-explanation-review.d.ts +5 -0
- package/dist/prompts/function-explanation-review.js +64 -0
- package/dist/prompts/semantic-name-review.d.ts +5 -0
- package/dist/prompts/semantic-name-review.js +63 -0
- package/dist/runtime-correlation.d.ts +34 -0
- package/dist/runtime-correlation.js +279 -0
- package/dist/runtime-paths.d.ts +3 -0
- package/dist/runtime-paths.js +11 -0
- package/dist/selection-diff.d.ts +667 -0
- package/dist/selection-diff.js +53 -0
- package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
- package/dist/semantic-name-suggestion-artifacts.js +314 -0
- package/dist/server.d.ts +129 -0
- package/dist/server.js +578 -0
- package/dist/tools/artifact-read.d.ts +235 -0
- package/dist/tools/artifact-read.js +317 -0
- package/dist/tools/artifacts-diff.d.ts +728 -0
- package/dist/tools/artifacts-diff.js +304 -0
- package/dist/tools/artifacts-list.d.ts +515 -0
- package/dist/tools/artifacts-list.js +389 -0
- package/dist/tools/attack-map.d.ts +290 -0
- package/dist/tools/attack-map.js +519 -0
- package/dist/tools/cache-observability.d.ts +4 -0
- package/dist/tools/cache-observability.js +36 -0
- package/dist/tools/code-function-cfg.d.ts +50 -0
- package/dist/tools/code-function-cfg.js +102 -0
- package/dist/tools/code-function-decompile.d.ts +55 -0
- package/dist/tools/code-function-decompile.js +103 -0
- package/dist/tools/code-function-disassemble.d.ts +43 -0
- package/dist/tools/code-function-disassemble.js +185 -0
- package/dist/tools/code-function-explain-apply.d.ts +255 -0
- package/dist/tools/code-function-explain-apply.js +225 -0
- package/dist/tools/code-function-explain-prepare.d.ts +535 -0
- package/dist/tools/code-function-explain-prepare.js +276 -0
- package/dist/tools/code-function-explain-review.d.ts +397 -0
- package/dist/tools/code-function-explain-review.js +589 -0
- package/dist/tools/code-function-rename-apply.d.ts +248 -0
- package/dist/tools/code-function-rename-apply.js +220 -0
- package/dist/tools/code-function-rename-prepare.d.ts +506 -0
- package/dist/tools/code-function-rename-prepare.js +279 -0
- package/dist/tools/code-function-rename-review.d.ts +574 -0
- package/dist/tools/code-function-rename-review.js +761 -0
- package/dist/tools/code-functions-list.d.ts +37 -0
- package/dist/tools/code-functions-list.js +91 -0
- package/dist/tools/code-functions-rank.d.ts +34 -0
- package/dist/tools/code-functions-rank.js +90 -0
- package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
- package/dist/tools/code-functions-reconstruct.js +2807 -0
- package/dist/tools/code-functions-search.d.ts +39 -0
- package/dist/tools/code-functions-search.js +90 -0
- package/dist/tools/code-reconstruct-export.d.ts +1212 -0
- package/dist/tools/code-reconstruct-export.js +4002 -0
- package/dist/tools/code-reconstruct-plan.d.ts +274 -0
- package/dist/tools/code-reconstruct-plan.js +342 -0
- package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
- package/dist/tools/dotnet-metadata-extract.js +355 -0
- package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
- package/dist/tools/dotnet-reconstruct-export.js +1151 -0
- package/dist/tools/dotnet-types-list.d.ts +325 -0
- package/dist/tools/dotnet-types-list.js +201 -0
- package/dist/tools/dynamic-dependencies.d.ts +115 -0
- package/dist/tools/dynamic-dependencies.js +213 -0
- package/dist/tools/dynamic-memory-import.d.ts +10 -0
- package/dist/tools/dynamic-memory-import.js +567 -0
- package/dist/tools/dynamic-trace-import.d.ts +10 -0
- package/dist/tools/dynamic-trace-import.js +235 -0
- package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
- package/dist/tools/entrypoint-fallback-disasm.js +89 -0
- package/dist/tools/ghidra-analyze.d.ts +88 -0
- package/dist/tools/ghidra-analyze.js +208 -0
- package/dist/tools/ghidra-health.d.ts +37 -0
- package/dist/tools/ghidra-health.js +212 -0
- package/dist/tools/ioc-export.d.ts +209 -0
- package/dist/tools/ioc-export.js +542 -0
- package/dist/tools/packer-detect.d.ts +165 -0
- package/dist/tools/packer-detect.js +284 -0
- package/dist/tools/pe-exports-extract.d.ts +175 -0
- package/dist/tools/pe-exports-extract.js +253 -0
- package/dist/tools/pe-fingerprint.d.ts +234 -0
- package/dist/tools/pe-fingerprint.js +269 -0
- package/dist/tools/pe-imports-extract.d.ts +105 -0
- package/dist/tools/pe-imports-extract.js +245 -0
- package/dist/tools/report-generate.d.ts +157 -0
- package/dist/tools/report-generate.js +457 -0
- package/dist/tools/report-summarize.d.ts +2131 -0
- package/dist/tools/report-summarize.js +596 -0
- package/dist/tools/runtime-detect.d.ts +135 -0
- package/dist/tools/runtime-detect.js +247 -0
- package/dist/tools/sample-ingest.d.ts +94 -0
- package/dist/tools/sample-ingest.js +327 -0
- package/dist/tools/sample-profile-get.d.ts +183 -0
- package/dist/tools/sample-profile-get.js +121 -0
- package/dist/tools/sandbox-execute.d.ts +441 -0
- package/dist/tools/sandbox-execute.js +392 -0
- package/dist/tools/strings-extract.d.ts +375 -0
- package/dist/tools/strings-extract.js +314 -0
- package/dist/tools/strings-floss-decode.d.ts +143 -0
- package/dist/tools/strings-floss-decode.js +259 -0
- package/dist/tools/system-health.d.ts +434 -0
- package/dist/tools/system-health.js +446 -0
- package/dist/tools/task-cancel.d.ts +21 -0
- package/dist/tools/task-cancel.js +70 -0
- package/dist/tools/task-status.d.ts +27 -0
- package/dist/tools/task-status.js +106 -0
- package/dist/tools/task-sweep.d.ts +22 -0
- package/dist/tools/task-sweep.js +77 -0
- package/dist/tools/tool-help.d.ts +340 -0
- package/dist/tools/tool-help.js +261 -0
- package/dist/tools/yara-scan.d.ts +554 -0
- package/dist/tools/yara-scan.js +313 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.js +41 -0
- package/dist/worker-pool.d.ts +204 -0
- package/dist/worker-pool.js +650 -0
- package/dist/workflows/deep-static.d.ts +104 -0
- package/dist/workflows/deep-static.js +276 -0
- package/dist/workflows/function-explanation-review.d.ts +655 -0
- package/dist/workflows/function-explanation-review.js +440 -0
- package/dist/workflows/reconstruct.d.ts +2053 -0
- package/dist/workflows/reconstruct.js +666 -0
- package/dist/workflows/semantic-name-review.d.ts +2418 -0
- package/dist/workflows/semantic-name-review.js +521 -0
- package/dist/workflows/triage.d.ts +659 -0
- package/dist/workflows/triage.js +1374 -0
- package/dist/workspace-manager.d.ts +150 -0
- package/dist/workspace-manager.js +411 -0
- package/ghidra_scripts/DecompileFunction.java +487 -0
- package/ghidra_scripts/DecompileFunction.py +150 -0
- package/ghidra_scripts/ExtractCFG.java +256 -0
- package/ghidra_scripts/ExtractCFG.py +233 -0
- package/ghidra_scripts/ExtractFunctions.java +442 -0
- package/ghidra_scripts/ExtractFunctions.py +101 -0
- package/ghidra_scripts/README.md +125 -0
- package/ghidra_scripts/SearchFunctionReferences.java +380 -0
- package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
- package/helpers/DotNetMetadataProbe/Program.cs +566 -0
- package/install-to-codex.ps1 +178 -0
- package/install-to-copilot.ps1 +303 -0
- package/package.json +101 -0
- package/requirements.txt +9 -0
- package/workers/requirements-dynamic.txt +11 -0
- package/workers/requirements.txt +8 -0
- package/workers/speakeasy_compat.py +175 -0
- package/workers/static_worker.py +5183 -0
- package/workers/yara_rules/default.yar +33 -0
- package/workers/yara_rules/malware_families.yar +93 -0
- package/workers/yara_rules/packers.yar +80 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sample.ingest tool implementation
|
|
3
|
+
* Uploads and registers new samples to the system
|
|
4
|
+
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import { withLogging, logError, logWarning } from '../logger.js';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// ============================================================================
|
|
13
|
+
const MAX_SAMPLE_SIZE = 500 * 1024 * 1024; // 500MB
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Input/Output Schemas
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Input schema for sample.ingest tool
|
|
19
|
+
* Requirements: 1.1
|
|
20
|
+
*/
|
|
21
|
+
export const SampleIngestInputSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
path: z
|
|
24
|
+
.string()
|
|
25
|
+
.trim()
|
|
26
|
+
.min(1)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Preferred for local files. Pass an absolute local file path when the MCP client can access the file system.'),
|
|
29
|
+
bytes_b64: z
|
|
30
|
+
.string()
|
|
31
|
+
.trim()
|
|
32
|
+
.min(1)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Fallback only. Use Base64 file bytes when the MCP client cannot access the local file path. Ignored when `path` is provided.'),
|
|
35
|
+
filename: z.string().optional().describe('Optional display/original filename'),
|
|
36
|
+
source: z.string().optional().describe('Optional source tag, e.g. upload/email/sandbox'),
|
|
37
|
+
})
|
|
38
|
+
.superRefine((value, ctx) => {
|
|
39
|
+
const hasPath = typeof value.path === 'string' && value.path.length > 0;
|
|
40
|
+
const hasBytes = typeof value.bytes_b64 === 'string' && value.bytes_b64.length > 0;
|
|
41
|
+
if (!hasPath && !hasBytes) {
|
|
42
|
+
ctx.addIssue({
|
|
43
|
+
code: z.ZodIssueCode.custom,
|
|
44
|
+
path: ['path'],
|
|
45
|
+
message: 'Provide either `path` (preferred for local files) or `bytes_b64` (fallback when the client cannot read the file path).',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.describe('Ingest a sample from a local file path or Base64 bytes. Prefer `path` whenever the MCP client can access the file directly.');
|
|
50
|
+
/**
|
|
51
|
+
* Output schema for sample.ingest tool
|
|
52
|
+
* Requirements: 1.5
|
|
53
|
+
*/
|
|
54
|
+
export const SampleIngestOutputSchema = z.object({
|
|
55
|
+
ok: z.boolean(),
|
|
56
|
+
data: z.object({
|
|
57
|
+
sample_id: z.string(),
|
|
58
|
+
size: z.number(),
|
|
59
|
+
file_type: z.string().optional(),
|
|
60
|
+
existed: z.boolean().optional(),
|
|
61
|
+
}).optional(),
|
|
62
|
+
errors: z.array(z.string()).optional(),
|
|
63
|
+
});
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Tool Definition
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* Tool definition for sample.ingest
|
|
69
|
+
*/
|
|
70
|
+
export const sampleIngestToolDefinition = {
|
|
71
|
+
name: 'sample.ingest',
|
|
72
|
+
description: 'Register a new sample from a local file path or Base64 bytes. Prefer `path` for local files; use `bytes_b64` only when the MCP client cannot read local disk.',
|
|
73
|
+
inputSchema: SampleIngestInputSchema,
|
|
74
|
+
outputSchema: SampleIngestOutputSchema,
|
|
75
|
+
};
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Helper Functions
|
|
78
|
+
// ============================================================================
|
|
79
|
+
/**
|
|
80
|
+
* Compute SHA256 hash of data
|
|
81
|
+
* Requirement: 1.1
|
|
82
|
+
*/
|
|
83
|
+
function computeSHA256(data) {
|
|
84
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compute MD5 hash of data
|
|
88
|
+
* Requirement: 1.1
|
|
89
|
+
*/
|
|
90
|
+
function computeMD5(data) {
|
|
91
|
+
return crypto.createHash('md5').update(data).digest('hex');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Detect file type from data
|
|
95
|
+
* Basic implementation - can be enhanced with magic number detection
|
|
96
|
+
*/
|
|
97
|
+
function detectFileType(data) {
|
|
98
|
+
// Check for PE signature (MZ header)
|
|
99
|
+
if (data.length >= 2 && data[0] === 0x4D && data[1] === 0x5A) {
|
|
100
|
+
return 'PE';
|
|
101
|
+
}
|
|
102
|
+
// Check for ELF signature
|
|
103
|
+
if (data.length >= 4 &&
|
|
104
|
+
data[0] === 0x7F &&
|
|
105
|
+
data[1] === 0x45 &&
|
|
106
|
+
data[2] === 0x4C &&
|
|
107
|
+
data[3] === 0x46) {
|
|
108
|
+
return 'ELF';
|
|
109
|
+
}
|
|
110
|
+
return 'unknown';
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Tool Handler
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Create sample.ingest tool handler
|
|
117
|
+
* Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6
|
|
118
|
+
*/
|
|
119
|
+
export function createSampleIngestHandler(workspaceManager, database, policyGuard) {
|
|
120
|
+
return async (args) => {
|
|
121
|
+
const input = args;
|
|
122
|
+
return withLogging({
|
|
123
|
+
operation: 'sample.ingest',
|
|
124
|
+
toolName: 'sample.ingest',
|
|
125
|
+
source: input.source,
|
|
126
|
+
}, async () => {
|
|
127
|
+
try {
|
|
128
|
+
// 1. Read sample data
|
|
129
|
+
let data;
|
|
130
|
+
let originalFilename;
|
|
131
|
+
if (input.path) {
|
|
132
|
+
// Read from file path
|
|
133
|
+
if (!fs.existsSync(input.path)) {
|
|
134
|
+
logWarning('File not found', { path: input.path });
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
errors: [`File not found: ${input.path}`],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
data = fs.readFileSync(input.path);
|
|
141
|
+
// Extract just the filename, not the full path
|
|
142
|
+
const pathParts = input.path.replace(/\\/g, '/').split('/');
|
|
143
|
+
originalFilename = input.filename || pathParts[pathParts.length - 1] || 'sample.bin';
|
|
144
|
+
}
|
|
145
|
+
else if (input.bytes_b64) {
|
|
146
|
+
// Decode from Base64
|
|
147
|
+
// Validate Base64 format first
|
|
148
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
149
|
+
if (!base64Regex.test(input.bytes_b64)) {
|
|
150
|
+
logWarning('Invalid Base64 encoding', { length: input.bytes_b64.length });
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
errors: ['Invalid Base64 encoding: contains invalid characters'],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
data = Buffer.from(input.bytes_b64, 'base64');
|
|
158
|
+
// Verify the decoded data is not empty and makes sense
|
|
159
|
+
if (data.length === 0 && input.bytes_b64.length > 0) {
|
|
160
|
+
throw new Error('Base64 decoding resulted in empty buffer');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
logError(error, { operation: 'base64_decode' });
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
errors: [`Invalid Base64 encoding: ${error.message}`],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
originalFilename = input.filename || 'sample.bin';
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
errors: [
|
|
176
|
+
'Missing input: provide `path` (preferred local file path) or `bytes_b64` (Base64 bytes fallback when the client cannot access the file path).',
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// 2. Check file size limit
|
|
181
|
+
// Requirement: 1.3
|
|
182
|
+
if (data.length > MAX_SAMPLE_SIZE) {
|
|
183
|
+
logWarning('Sample size exceeds limit', {
|
|
184
|
+
size: data.length,
|
|
185
|
+
maxSize: MAX_SAMPLE_SIZE,
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
errors: [
|
|
190
|
+
`Sample size ${data.length} bytes exceeds maximum limit of ${MAX_SAMPLE_SIZE} bytes (500MB)`
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// 3. Compute hashes
|
|
195
|
+
// Requirement: 1.1
|
|
196
|
+
const sha256 = computeSHA256(data);
|
|
197
|
+
const md5 = computeMD5(data);
|
|
198
|
+
const sampleId = `sha256:${sha256}`;
|
|
199
|
+
// 4. Check if sample already exists
|
|
200
|
+
// Requirement: 1.2
|
|
201
|
+
const existingSample = database.findSampleBySha256(sha256);
|
|
202
|
+
if (existingSample) {
|
|
203
|
+
// Sample already exists, return existing sample_id
|
|
204
|
+
await policyGuard.auditLog({
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
operation: 'sample.ingest',
|
|
207
|
+
sampleId: existingSample.id,
|
|
208
|
+
decision: 'allow',
|
|
209
|
+
reason: 'Sample already exists (SHA256 match)',
|
|
210
|
+
metadata: {
|
|
211
|
+
size: data.length,
|
|
212
|
+
source: input.source || 'upload',
|
|
213
|
+
existed: true,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
data: {
|
|
219
|
+
sample_id: existingSample.id,
|
|
220
|
+
size: existingSample.size,
|
|
221
|
+
file_type: existingSample.file_type || undefined,
|
|
222
|
+
existed: true,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// 5. Create workspace
|
|
227
|
+
// Requirement: 1.4
|
|
228
|
+
const workspace = await workspaceManager.createWorkspace(sampleId);
|
|
229
|
+
// 6. Store sample file
|
|
230
|
+
// Requirement: 1.4
|
|
231
|
+
const samplePath = `${workspace.original}/${originalFilename}`;
|
|
232
|
+
fs.writeFileSync(samplePath, data);
|
|
233
|
+
// Mark file as non-executable (security measure)
|
|
234
|
+
// Requirement: 29.3
|
|
235
|
+
try {
|
|
236
|
+
fs.chmodSync(samplePath, 0o444); // Read-only
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// Ignore chmod errors on Windows
|
|
240
|
+
logWarning('Failed to set file permissions', {
|
|
241
|
+
path: samplePath,
|
|
242
|
+
error: error.message,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// 7. Detect file type
|
|
246
|
+
const fileType = detectFileType(data);
|
|
247
|
+
// 8. Insert into database
|
|
248
|
+
// Requirement: 1.5
|
|
249
|
+
const sample = {
|
|
250
|
+
id: sampleId,
|
|
251
|
+
sha256,
|
|
252
|
+
md5,
|
|
253
|
+
size: data.length,
|
|
254
|
+
file_type: fileType,
|
|
255
|
+
created_at: new Date().toISOString(),
|
|
256
|
+
source: input.source || 'upload',
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
database.insertSample(sample);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
// Handle race condition: if another concurrent request already inserted this sample
|
|
263
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE' || error.message?.includes('UNIQUE constraint')) {
|
|
264
|
+
// Sample was inserted by another concurrent request
|
|
265
|
+
// Query it again and return
|
|
266
|
+
const existingSample = database.findSampleBySha256(sha256);
|
|
267
|
+
if (existingSample) {
|
|
268
|
+
await policyGuard.auditLog({
|
|
269
|
+
timestamp: new Date().toISOString(),
|
|
270
|
+
operation: 'sample.ingest',
|
|
271
|
+
sampleId: existingSample.id,
|
|
272
|
+
decision: 'allow',
|
|
273
|
+
reason: 'Sample already exists (concurrent insert race condition)',
|
|
274
|
+
metadata: {
|
|
275
|
+
size: data.length,
|
|
276
|
+
source: input.source || 'upload',
|
|
277
|
+
existed: true,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
data: {
|
|
283
|
+
sample_id: existingSample.id,
|
|
284
|
+
size: existingSample.size,
|
|
285
|
+
file_type: existingSample.file_type || undefined,
|
|
286
|
+
existed: true,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Re-throw if it's a different error
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
// 9. Audit log
|
|
295
|
+
// Requirement: 1.6
|
|
296
|
+
await policyGuard.auditLog({
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
298
|
+
operation: 'sample.ingest',
|
|
299
|
+
sampleId: sample.id,
|
|
300
|
+
decision: 'allow',
|
|
301
|
+
metadata: {
|
|
302
|
+
size: data.length,
|
|
303
|
+
source: sample.source,
|
|
304
|
+
file_type: fileType,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
// 10. Return result
|
|
308
|
+
return {
|
|
309
|
+
ok: true,
|
|
310
|
+
data: {
|
|
311
|
+
sample_id: sampleId,
|
|
312
|
+
size: data.length,
|
|
313
|
+
file_type: fileType,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
logError(error, { operation: 'sample.ingest' });
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
errors: [error.message],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=sample-ingest.js.map
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sample.profile.get tool implementation
|
|
3
|
+
* Retrieves sample profile including basic information and completed analyses
|
|
4
|
+
* Requirements: Data Model
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { ToolDefinition, ToolArgs, WorkerResult } from '../types.js';
|
|
8
|
+
import type { DatabaseManager } from '../database.js';
|
|
9
|
+
/**
|
|
10
|
+
* Input schema for sample.profile.get tool
|
|
11
|
+
*/
|
|
12
|
+
export declare const SampleProfileGetInputSchema: z.ZodObject<{
|
|
13
|
+
sample_id: z.ZodString;
|
|
14
|
+
stale_running_ms: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
sample_id: string;
|
|
17
|
+
stale_running_ms?: number | null | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
sample_id: string;
|
|
20
|
+
stale_running_ms?: number | null | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
export type SampleProfileGetInput = z.infer<typeof SampleProfileGetInputSchema>;
|
|
23
|
+
/**
|
|
24
|
+
* Output schema for sample.profile.get tool
|
|
25
|
+
*/
|
|
26
|
+
export declare const SampleProfileGetOutputSchema: z.ZodObject<{
|
|
27
|
+
ok: z.ZodBoolean;
|
|
28
|
+
data: z.ZodOptional<z.ZodObject<{
|
|
29
|
+
sample: z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
sha256: z.ZodString;
|
|
32
|
+
md5: z.ZodString;
|
|
33
|
+
size: z.ZodNumber;
|
|
34
|
+
file_type: z.ZodOptional<z.ZodString>;
|
|
35
|
+
created_at: z.ZodString;
|
|
36
|
+
source: z.ZodString;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
id: string;
|
|
39
|
+
size: number;
|
|
40
|
+
sha256: string;
|
|
41
|
+
created_at: string;
|
|
42
|
+
source: string;
|
|
43
|
+
md5: string;
|
|
44
|
+
file_type?: string | undefined;
|
|
45
|
+
}, {
|
|
46
|
+
id: string;
|
|
47
|
+
size: number;
|
|
48
|
+
sha256: string;
|
|
49
|
+
created_at: string;
|
|
50
|
+
source: string;
|
|
51
|
+
md5: string;
|
|
52
|
+
file_type?: string | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
analyses: z.ZodArray<z.ZodObject<{
|
|
55
|
+
id: z.ZodString;
|
|
56
|
+
stage: z.ZodString;
|
|
57
|
+
backend: z.ZodString;
|
|
58
|
+
status: z.ZodString;
|
|
59
|
+
started_at: z.ZodOptional<z.ZodString>;
|
|
60
|
+
finished_at: z.ZodOptional<z.ZodString>;
|
|
61
|
+
output_json: z.ZodOptional<z.ZodString>;
|
|
62
|
+
metrics_json: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
status: string;
|
|
65
|
+
id: string;
|
|
66
|
+
stage: string;
|
|
67
|
+
backend: string;
|
|
68
|
+
started_at?: string | undefined;
|
|
69
|
+
finished_at?: string | undefined;
|
|
70
|
+
output_json?: string | undefined;
|
|
71
|
+
metrics_json?: string | undefined;
|
|
72
|
+
}, {
|
|
73
|
+
status: string;
|
|
74
|
+
id: string;
|
|
75
|
+
stage: string;
|
|
76
|
+
backend: string;
|
|
77
|
+
started_at?: string | undefined;
|
|
78
|
+
finished_at?: string | undefined;
|
|
79
|
+
output_json?: string | undefined;
|
|
80
|
+
metrics_json?: string | undefined;
|
|
81
|
+
}>, "many">;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
sample: {
|
|
84
|
+
id: string;
|
|
85
|
+
size: number;
|
|
86
|
+
sha256: string;
|
|
87
|
+
created_at: string;
|
|
88
|
+
source: string;
|
|
89
|
+
md5: string;
|
|
90
|
+
file_type?: string | undefined;
|
|
91
|
+
};
|
|
92
|
+
analyses: {
|
|
93
|
+
status: string;
|
|
94
|
+
id: string;
|
|
95
|
+
stage: string;
|
|
96
|
+
backend: string;
|
|
97
|
+
started_at?: string | undefined;
|
|
98
|
+
finished_at?: string | undefined;
|
|
99
|
+
output_json?: string | undefined;
|
|
100
|
+
metrics_json?: string | undefined;
|
|
101
|
+
}[];
|
|
102
|
+
}, {
|
|
103
|
+
sample: {
|
|
104
|
+
id: string;
|
|
105
|
+
size: number;
|
|
106
|
+
sha256: string;
|
|
107
|
+
created_at: string;
|
|
108
|
+
source: string;
|
|
109
|
+
md5: string;
|
|
110
|
+
file_type?: string | undefined;
|
|
111
|
+
};
|
|
112
|
+
analyses: {
|
|
113
|
+
status: string;
|
|
114
|
+
id: string;
|
|
115
|
+
stage: string;
|
|
116
|
+
backend: string;
|
|
117
|
+
started_at?: string | undefined;
|
|
118
|
+
finished_at?: string | undefined;
|
|
119
|
+
output_json?: string | undefined;
|
|
120
|
+
metrics_json?: string | undefined;
|
|
121
|
+
}[];
|
|
122
|
+
}>>;
|
|
123
|
+
errors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
124
|
+
}, "strip", z.ZodTypeAny, {
|
|
125
|
+
ok: boolean;
|
|
126
|
+
data?: {
|
|
127
|
+
sample: {
|
|
128
|
+
id: string;
|
|
129
|
+
size: number;
|
|
130
|
+
sha256: string;
|
|
131
|
+
created_at: string;
|
|
132
|
+
source: string;
|
|
133
|
+
md5: string;
|
|
134
|
+
file_type?: string | undefined;
|
|
135
|
+
};
|
|
136
|
+
analyses: {
|
|
137
|
+
status: string;
|
|
138
|
+
id: string;
|
|
139
|
+
stage: string;
|
|
140
|
+
backend: string;
|
|
141
|
+
started_at?: string | undefined;
|
|
142
|
+
finished_at?: string | undefined;
|
|
143
|
+
output_json?: string | undefined;
|
|
144
|
+
metrics_json?: string | undefined;
|
|
145
|
+
}[];
|
|
146
|
+
} | undefined;
|
|
147
|
+
errors?: string[] | undefined;
|
|
148
|
+
}, {
|
|
149
|
+
ok: boolean;
|
|
150
|
+
data?: {
|
|
151
|
+
sample: {
|
|
152
|
+
id: string;
|
|
153
|
+
size: number;
|
|
154
|
+
sha256: string;
|
|
155
|
+
created_at: string;
|
|
156
|
+
source: string;
|
|
157
|
+
md5: string;
|
|
158
|
+
file_type?: string | undefined;
|
|
159
|
+
};
|
|
160
|
+
analyses: {
|
|
161
|
+
status: string;
|
|
162
|
+
id: string;
|
|
163
|
+
stage: string;
|
|
164
|
+
backend: string;
|
|
165
|
+
started_at?: string | undefined;
|
|
166
|
+
finished_at?: string | undefined;
|
|
167
|
+
output_json?: string | undefined;
|
|
168
|
+
metrics_json?: string | undefined;
|
|
169
|
+
}[];
|
|
170
|
+
} | undefined;
|
|
171
|
+
errors?: string[] | undefined;
|
|
172
|
+
}>;
|
|
173
|
+
export type SampleProfileGetOutput = z.infer<typeof SampleProfileGetOutputSchema>;
|
|
174
|
+
/**
|
|
175
|
+
* Tool definition for sample.profile.get
|
|
176
|
+
*/
|
|
177
|
+
export declare const sampleProfileGetToolDefinition: ToolDefinition;
|
|
178
|
+
/**
|
|
179
|
+
* Create sample.profile.get tool handler
|
|
180
|
+
* Requirements: Data Model
|
|
181
|
+
*/
|
|
182
|
+
export declare function createSampleProfileGetHandler(database: DatabaseManager): (args: ToolArgs) => Promise<WorkerResult>;
|
|
183
|
+
//# sourceMappingURL=sample-profile-get.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sample.profile.get tool implementation
|
|
3
|
+
* Retrieves sample profile including basic information and completed analyses
|
|
4
|
+
* Requirements: Data Model
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Input/Output Schemas
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Input schema for sample.profile.get tool
|
|
12
|
+
*/
|
|
13
|
+
export const SampleProfileGetInputSchema = z.object({
|
|
14
|
+
sample_id: z.string().describe('Sample ID (format: sha256:<hex>)'),
|
|
15
|
+
stale_running_ms: z
|
|
16
|
+
.number()
|
|
17
|
+
.int()
|
|
18
|
+
.min(1000)
|
|
19
|
+
.nullable()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Optional stale-analysis reap threshold in milliseconds. Omit or null to disable auto-reaping.'),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Output schema for sample.profile.get tool
|
|
25
|
+
*/
|
|
26
|
+
export const SampleProfileGetOutputSchema = z.object({
|
|
27
|
+
ok: z.boolean(),
|
|
28
|
+
data: z.object({
|
|
29
|
+
sample: z.object({
|
|
30
|
+
id: z.string(),
|
|
31
|
+
sha256: z.string(),
|
|
32
|
+
md5: z.string(),
|
|
33
|
+
size: z.number(),
|
|
34
|
+
file_type: z.string().optional(),
|
|
35
|
+
created_at: z.string(),
|
|
36
|
+
source: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
analyses: z.array(z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
stage: z.string(),
|
|
41
|
+
backend: z.string(),
|
|
42
|
+
status: z.string(),
|
|
43
|
+
started_at: z.string().optional(),
|
|
44
|
+
finished_at: z.string().optional(),
|
|
45
|
+
output_json: z.string().optional(),
|
|
46
|
+
metrics_json: z.string().optional(),
|
|
47
|
+
})),
|
|
48
|
+
}).optional(),
|
|
49
|
+
errors: z.array(z.string()).optional(),
|
|
50
|
+
});
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Tool Definition
|
|
53
|
+
// ============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Tool definition for sample.profile.get
|
|
56
|
+
*/
|
|
57
|
+
export const sampleProfileGetToolDefinition = {
|
|
58
|
+
name: 'sample.profile.get',
|
|
59
|
+
description: '查询样本基础信息和已完成的分析',
|
|
60
|
+
inputSchema: SampleProfileGetInputSchema,
|
|
61
|
+
outputSchema: SampleProfileGetOutputSchema,
|
|
62
|
+
};
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Tool Handler
|
|
65
|
+
// ============================================================================
|
|
66
|
+
/**
|
|
67
|
+
* Create sample.profile.get tool handler
|
|
68
|
+
* Requirements: Data Model
|
|
69
|
+
*/
|
|
70
|
+
export function createSampleProfileGetHandler(database) {
|
|
71
|
+
return async (args) => {
|
|
72
|
+
try {
|
|
73
|
+
const input = SampleProfileGetInputSchema.parse(args);
|
|
74
|
+
// 1. Query sample from database
|
|
75
|
+
const sample = database.findSample(input.sample_id);
|
|
76
|
+
if (!sample) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
errors: [`Sample not found: ${input.sample_id}`],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// 2. Query analyses for this sample
|
|
83
|
+
if (typeof input.stale_running_ms === 'number') {
|
|
84
|
+
database.reapStaleAnalyses(input.stale_running_ms, input.sample_id);
|
|
85
|
+
}
|
|
86
|
+
const analyses = database.findAnalysesBySample(input.sample_id);
|
|
87
|
+
// 3. Return profile data
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
data: {
|
|
91
|
+
sample: {
|
|
92
|
+
id: sample.id,
|
|
93
|
+
sha256: sample.sha256,
|
|
94
|
+
md5: sample.md5,
|
|
95
|
+
size: sample.size,
|
|
96
|
+
file_type: sample.file_type || undefined,
|
|
97
|
+
created_at: sample.created_at,
|
|
98
|
+
source: sample.source,
|
|
99
|
+
},
|
|
100
|
+
analyses: analyses.map(analysis => ({
|
|
101
|
+
id: analysis.id,
|
|
102
|
+
stage: analysis.stage,
|
|
103
|
+
backend: analysis.backend,
|
|
104
|
+
status: analysis.status,
|
|
105
|
+
started_at: analysis.started_at || undefined,
|
|
106
|
+
finished_at: analysis.finished_at || undefined,
|
|
107
|
+
output_json: analysis.output_json || undefined,
|
|
108
|
+
metrics_json: analysis.metrics_json || undefined,
|
|
109
|
+
})),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
errors: [error.message],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=sample-profile-get.js.map
|