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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Manager
|
|
3
|
+
* Manages sample storage with SHA256-based bucketed directories
|
|
4
|
+
* Implements path normalization and security boundary checks
|
|
5
|
+
* Optimized for file I/O performance
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkspacePath } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* WorkspaceManager handles creation and management of sample workspaces
|
|
10
|
+
* Each workspace is organized in a bucketed directory structure based on SHA256
|
|
11
|
+
*
|
|
12
|
+
* Performance optimizations (Requirement 26.3):
|
|
13
|
+
* - Async file operations where possible
|
|
14
|
+
* - Cached workspace path lookups
|
|
15
|
+
* - Batch directory creation
|
|
16
|
+
*/
|
|
17
|
+
export declare class WorkspaceManager {
|
|
18
|
+
private workspaceRoot;
|
|
19
|
+
private workspacePathCache;
|
|
20
|
+
private readonly CACHE_SIZE_LIMIT;
|
|
21
|
+
constructor(workspaceRoot: string);
|
|
22
|
+
/**
|
|
23
|
+
* Ensure workspace root directory exists
|
|
24
|
+
*/
|
|
25
|
+
private ensureWorkspaceRoot;
|
|
26
|
+
/**
|
|
27
|
+
* Create workspace directory structure for a sample
|
|
28
|
+
* Uses SHA256-based bucketing: workspaces/ab/cd/<sha256>/
|
|
29
|
+
*
|
|
30
|
+
* Requirements: 19.1, 19.2, 26.3 (file I/O optimization)
|
|
31
|
+
*
|
|
32
|
+
* Optimizations:
|
|
33
|
+
* - Async directory creation
|
|
34
|
+
* - Cached workspace paths
|
|
35
|
+
* - Batch subdirectory creation
|
|
36
|
+
*
|
|
37
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
38
|
+
* @returns WorkspacePath with all subdirectory paths
|
|
39
|
+
*/
|
|
40
|
+
createWorkspace(sampleId: string): Promise<WorkspacePath>;
|
|
41
|
+
/**
|
|
42
|
+
* Get workspace path for an existing sample
|
|
43
|
+
*
|
|
44
|
+
* Requirements: 26.3 (file I/O optimization - cached lookups)
|
|
45
|
+
*
|
|
46
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
47
|
+
* @returns WorkspacePath with all subdirectory paths
|
|
48
|
+
*/
|
|
49
|
+
getWorkspace(sampleId: string): Promise<WorkspacePath>;
|
|
50
|
+
/**
|
|
51
|
+
* Cache workspace path for faster lookups
|
|
52
|
+
* Implements LRU eviction when cache size limit is reached
|
|
53
|
+
*
|
|
54
|
+
* Requirements: 26.3 (file I/O optimization)
|
|
55
|
+
*
|
|
56
|
+
* @param sha256 - SHA256 hash
|
|
57
|
+
* @param workspacePath - Workspace path to cache
|
|
58
|
+
*/
|
|
59
|
+
private cacheWorkspacePath;
|
|
60
|
+
/**
|
|
61
|
+
* Clear workspace path cache
|
|
62
|
+
* Useful after cleanup operations
|
|
63
|
+
*/
|
|
64
|
+
clearWorkspaceCache(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Extract SHA256 hash from sample ID
|
|
67
|
+
*
|
|
68
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
69
|
+
* @returns SHA256 hash string
|
|
70
|
+
*/
|
|
71
|
+
private extractSha256;
|
|
72
|
+
/**
|
|
73
|
+
* Generate bucketed workspace path from SHA256
|
|
74
|
+
* Structure: workspaces/ab/cd/<sha256>/
|
|
75
|
+
*
|
|
76
|
+
* @param sha256 - SHA256 hash string
|
|
77
|
+
* @returns WorkspacePath object with all subdirectory paths
|
|
78
|
+
*/
|
|
79
|
+
private generateWorkspacePath;
|
|
80
|
+
/**
|
|
81
|
+
* Normalize and validate a path is within workspace boundaries
|
|
82
|
+
* Prevents path traversal attacks
|
|
83
|
+
*
|
|
84
|
+
* Requirement: 29.5
|
|
85
|
+
*
|
|
86
|
+
* @param workspacePath - Base workspace path
|
|
87
|
+
* @param relativePath - Relative path to validate
|
|
88
|
+
* @returns Normalized absolute path
|
|
89
|
+
* @throws Error if path is outside workspace boundaries
|
|
90
|
+
*/
|
|
91
|
+
normalizePath(workspacePath: string, relativePath: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Check if a path is within workspace boundaries
|
|
94
|
+
*
|
|
95
|
+
* Requirement: 29.5
|
|
96
|
+
*
|
|
97
|
+
* @param workspacePath - Base workspace path
|
|
98
|
+
* @param targetPath - Path to check
|
|
99
|
+
* @returns true if path is within boundaries
|
|
100
|
+
*/
|
|
101
|
+
isWithinBoundaries(workspacePath: string, targetPath: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Get workspace root directory
|
|
104
|
+
*/
|
|
105
|
+
getWorkspaceRoot(): string;
|
|
106
|
+
/**
|
|
107
|
+
* Clean up workspace for a sample
|
|
108
|
+
* Deletes all subdirectories and files
|
|
109
|
+
*
|
|
110
|
+
* Requirements: 19.6, 26.3 (file I/O optimization)
|
|
111
|
+
*
|
|
112
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
113
|
+
*/
|
|
114
|
+
cleanup(sampleId: string): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Clean up old workspaces based on retention policy
|
|
117
|
+
* Deletes workspaces older than the specified number of days
|
|
118
|
+
*
|
|
119
|
+
* Requirements: 操作约束 6 (30-day retention policy), 26.3 (file I/O optimization)
|
|
120
|
+
*
|
|
121
|
+
* Optimizations:
|
|
122
|
+
* - Async file operations
|
|
123
|
+
* - Parallel directory scanning
|
|
124
|
+
* - Batch deletion
|
|
125
|
+
*
|
|
126
|
+
* @param retentionDays - Number of days to retain workspaces (default: 30)
|
|
127
|
+
* @returns Number of workspaces cleaned up
|
|
128
|
+
*/
|
|
129
|
+
cleanupOldWorkspaces(retentionDays?: number): Promise<number>;
|
|
130
|
+
/**
|
|
131
|
+
* Get workspace statistics for monitoring
|
|
132
|
+
* Requirements: 26.3 (file I/O optimization)
|
|
133
|
+
*
|
|
134
|
+
* @returns Object with workspace statistics
|
|
135
|
+
*/
|
|
136
|
+
getWorkspaceStats(): Promise<{
|
|
137
|
+
totalWorkspaces: number;
|
|
138
|
+
totalSizeBytes: number;
|
|
139
|
+
oldestWorkspaceAge: number;
|
|
140
|
+
}>;
|
|
141
|
+
/**
|
|
142
|
+
* Get approximate size of a directory
|
|
143
|
+
* Requirements: 26.3 (file I/O optimization)
|
|
144
|
+
*
|
|
145
|
+
* @param dirPath - Directory path
|
|
146
|
+
* @returns Size in bytes
|
|
147
|
+
*/
|
|
148
|
+
private getDirectorySize;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=workspace-manager.d.ts.map
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Manager
|
|
3
|
+
* Manages sample storage with SHA256-based bucketed directories
|
|
4
|
+
* Implements path normalization and security boundary checks
|
|
5
|
+
* Optimized for file I/O performance
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import fsPromises from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
/**
|
|
11
|
+
* WorkspaceManager handles creation and management of sample workspaces
|
|
12
|
+
* Each workspace is organized in a bucketed directory structure based on SHA256
|
|
13
|
+
*
|
|
14
|
+
* Performance optimizations (Requirement 26.3):
|
|
15
|
+
* - Async file operations where possible
|
|
16
|
+
* - Cached workspace path lookups
|
|
17
|
+
* - Batch directory creation
|
|
18
|
+
*/
|
|
19
|
+
export class WorkspaceManager {
|
|
20
|
+
workspaceRoot;
|
|
21
|
+
workspacePathCache = new Map();
|
|
22
|
+
CACHE_SIZE_LIMIT = 1000;
|
|
23
|
+
constructor(workspaceRoot) {
|
|
24
|
+
this.workspaceRoot = path.resolve(workspaceRoot);
|
|
25
|
+
this.ensureWorkspaceRoot();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Ensure workspace root directory exists
|
|
29
|
+
*/
|
|
30
|
+
ensureWorkspaceRoot() {
|
|
31
|
+
if (!fs.existsSync(this.workspaceRoot)) {
|
|
32
|
+
fs.mkdirSync(this.workspaceRoot, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create workspace directory structure for a sample
|
|
37
|
+
* Uses SHA256-based bucketing: workspaces/ab/cd/<sha256>/
|
|
38
|
+
*
|
|
39
|
+
* Requirements: 19.1, 19.2, 26.3 (file I/O optimization)
|
|
40
|
+
*
|
|
41
|
+
* Optimizations:
|
|
42
|
+
* - Async directory creation
|
|
43
|
+
* - Cached workspace paths
|
|
44
|
+
* - Batch subdirectory creation
|
|
45
|
+
*
|
|
46
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
47
|
+
* @returns WorkspacePath with all subdirectory paths
|
|
48
|
+
*/
|
|
49
|
+
async createWorkspace(sampleId) {
|
|
50
|
+
const sha256 = this.extractSha256(sampleId);
|
|
51
|
+
// Check cache first (Requirement 26.3)
|
|
52
|
+
const cachedPath = this.workspacePathCache.get(sha256);
|
|
53
|
+
if (cachedPath && fs.existsSync(cachedPath.root)) {
|
|
54
|
+
return cachedPath;
|
|
55
|
+
}
|
|
56
|
+
const workspacePath = this.generateWorkspacePath(sha256);
|
|
57
|
+
// Create directory structure asynchronously
|
|
58
|
+
const subdirs = ['original', 'cache', 'ghidra', 'dotnet', 'reports'];
|
|
59
|
+
// Create root workspace directory
|
|
60
|
+
await fsPromises.mkdir(workspacePath.root, { recursive: true });
|
|
61
|
+
// Create all subdirectories in parallel (Requirement 26.3)
|
|
62
|
+
await Promise.all(subdirs.map(subdir => fsPromises.mkdir(path.join(workspacePath.root, subdir), { recursive: true })));
|
|
63
|
+
// Cache the workspace path
|
|
64
|
+
this.cacheWorkspacePath(sha256, workspacePath);
|
|
65
|
+
return workspacePath;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get workspace path for an existing sample
|
|
69
|
+
*
|
|
70
|
+
* Requirements: 26.3 (file I/O optimization - cached lookups)
|
|
71
|
+
*
|
|
72
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
73
|
+
* @returns WorkspacePath with all subdirectory paths
|
|
74
|
+
*/
|
|
75
|
+
async getWorkspace(sampleId) {
|
|
76
|
+
const sha256 = this.extractSha256(sampleId);
|
|
77
|
+
// Check cache first (Requirement 26.3)
|
|
78
|
+
const cachedPath = this.workspacePathCache.get(sha256);
|
|
79
|
+
if (cachedPath) {
|
|
80
|
+
return cachedPath;
|
|
81
|
+
}
|
|
82
|
+
const workspacePath = this.generateWorkspacePath(sha256);
|
|
83
|
+
// Verify workspace exists
|
|
84
|
+
if (!fs.existsSync(workspacePath.root)) {
|
|
85
|
+
throw new Error(`Workspace not found for sample: ${sampleId}`);
|
|
86
|
+
}
|
|
87
|
+
// Cache the workspace path
|
|
88
|
+
this.cacheWorkspacePath(sha256, workspacePath);
|
|
89
|
+
return workspacePath;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Cache workspace path for faster lookups
|
|
93
|
+
* Implements LRU eviction when cache size limit is reached
|
|
94
|
+
*
|
|
95
|
+
* Requirements: 26.3 (file I/O optimization)
|
|
96
|
+
*
|
|
97
|
+
* @param sha256 - SHA256 hash
|
|
98
|
+
* @param workspacePath - Workspace path to cache
|
|
99
|
+
*/
|
|
100
|
+
cacheWorkspacePath(sha256, workspacePath) {
|
|
101
|
+
// Evict oldest entry if cache is full
|
|
102
|
+
if (this.workspacePathCache.size >= this.CACHE_SIZE_LIMIT) {
|
|
103
|
+
const firstKey = this.workspacePathCache.keys().next().value;
|
|
104
|
+
if (firstKey) {
|
|
105
|
+
this.workspacePathCache.delete(firstKey);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.workspacePathCache.set(sha256, workspacePath);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clear workspace path cache
|
|
112
|
+
* Useful after cleanup operations
|
|
113
|
+
*/
|
|
114
|
+
clearWorkspaceCache() {
|
|
115
|
+
this.workspacePathCache.clear();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Extract SHA256 hash from sample ID
|
|
119
|
+
*
|
|
120
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
121
|
+
* @returns SHA256 hash string
|
|
122
|
+
*/
|
|
123
|
+
extractSha256(sampleId) {
|
|
124
|
+
if (!sampleId.startsWith('sha256:')) {
|
|
125
|
+
throw new Error(`Invalid sample ID format: ${sampleId}`);
|
|
126
|
+
}
|
|
127
|
+
const sha256 = sampleId.substring(7);
|
|
128
|
+
// Validate SHA256 format (64 hex characters)
|
|
129
|
+
if (!/^[a-f0-9]{64}$/i.test(sha256)) {
|
|
130
|
+
throw new Error(`Invalid SHA256 hash: ${sha256}`);
|
|
131
|
+
}
|
|
132
|
+
return sha256.toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Generate bucketed workspace path from SHA256
|
|
136
|
+
* Structure: workspaces/ab/cd/<sha256>/
|
|
137
|
+
*
|
|
138
|
+
* @param sha256 - SHA256 hash string
|
|
139
|
+
* @returns WorkspacePath object with all subdirectory paths
|
|
140
|
+
*/
|
|
141
|
+
generateWorkspacePath(sha256) {
|
|
142
|
+
// Use first 2 and next 2 characters for bucketing
|
|
143
|
+
const bucket1 = sha256.substring(0, 2);
|
|
144
|
+
const bucket2 = sha256.substring(2, 4);
|
|
145
|
+
const root = path.join(this.workspaceRoot, bucket1, bucket2, sha256);
|
|
146
|
+
return {
|
|
147
|
+
root,
|
|
148
|
+
original: path.join(root, 'original'),
|
|
149
|
+
cache: path.join(root, 'cache'),
|
|
150
|
+
ghidra: path.join(root, 'ghidra'),
|
|
151
|
+
reports: path.join(root, 'reports'),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Normalize and validate a path is within workspace boundaries
|
|
156
|
+
* Prevents path traversal attacks
|
|
157
|
+
*
|
|
158
|
+
* Requirement: 29.5
|
|
159
|
+
*
|
|
160
|
+
* @param workspacePath - Base workspace path
|
|
161
|
+
* @param relativePath - Relative path to validate
|
|
162
|
+
* @returns Normalized absolute path
|
|
163
|
+
* @throws Error if path is outside workspace boundaries
|
|
164
|
+
*/
|
|
165
|
+
normalizePath(workspacePath, relativePath) {
|
|
166
|
+
// Resolve to absolute path
|
|
167
|
+
const absolutePath = path.resolve(workspacePath, relativePath);
|
|
168
|
+
const normalizedPath = path.normalize(absolutePath);
|
|
169
|
+
// Check if path is within workspace boundaries
|
|
170
|
+
const workspaceAbsolute = path.resolve(workspacePath);
|
|
171
|
+
if (!normalizedPath.startsWith(workspaceAbsolute + path.sep) &&
|
|
172
|
+
normalizedPath !== workspaceAbsolute) {
|
|
173
|
+
throw new Error(`Path traversal detected: ${relativePath} is outside workspace boundaries`);
|
|
174
|
+
}
|
|
175
|
+
return normalizedPath;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if a path is within workspace boundaries
|
|
179
|
+
*
|
|
180
|
+
* Requirement: 29.5
|
|
181
|
+
*
|
|
182
|
+
* @param workspacePath - Base workspace path
|
|
183
|
+
* @param targetPath - Path to check
|
|
184
|
+
* @returns true if path is within boundaries
|
|
185
|
+
*/
|
|
186
|
+
isWithinBoundaries(workspacePath, targetPath) {
|
|
187
|
+
try {
|
|
188
|
+
this.normalizePath(workspacePath, targetPath);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get workspace root directory
|
|
197
|
+
*/
|
|
198
|
+
getWorkspaceRoot() {
|
|
199
|
+
return this.workspaceRoot;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Clean up workspace for a sample
|
|
203
|
+
* Deletes all subdirectories and files
|
|
204
|
+
*
|
|
205
|
+
* Requirements: 19.6, 26.3 (file I/O optimization)
|
|
206
|
+
*
|
|
207
|
+
* @param sampleId - Sample ID in format "sha256:<hex>"
|
|
208
|
+
*/
|
|
209
|
+
async cleanup(sampleId) {
|
|
210
|
+
const sha256 = this.extractSha256(sampleId);
|
|
211
|
+
const workspacePath = this.generateWorkspacePath(sha256);
|
|
212
|
+
// Remove from cache
|
|
213
|
+
this.workspacePathCache.delete(sha256);
|
|
214
|
+
// Check if workspace exists
|
|
215
|
+
if (!fs.existsSync(workspacePath.root)) {
|
|
216
|
+
// Workspace doesn't exist, nothing to clean up
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Delete the entire workspace directory recursively (async)
|
|
220
|
+
await fsPromises.rm(workspacePath.root, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Clean up old workspaces based on retention policy
|
|
224
|
+
* Deletes workspaces older than the specified number of days
|
|
225
|
+
*
|
|
226
|
+
* Requirements: 操作约束 6 (30-day retention policy), 26.3 (file I/O optimization)
|
|
227
|
+
*
|
|
228
|
+
* Optimizations:
|
|
229
|
+
* - Async file operations
|
|
230
|
+
* - Parallel directory scanning
|
|
231
|
+
* - Batch deletion
|
|
232
|
+
*
|
|
233
|
+
* @param retentionDays - Number of days to retain workspaces (default: 30)
|
|
234
|
+
* @returns Number of workspaces cleaned up
|
|
235
|
+
*/
|
|
236
|
+
async cleanupOldWorkspaces(retentionDays = 30) {
|
|
237
|
+
const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000);
|
|
238
|
+
let cleanedCount = 0;
|
|
239
|
+
// Traverse the bucketed directory structure
|
|
240
|
+
if (!fs.existsSync(this.workspaceRoot)) {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
const bucket1Dirs = await fsPromises.readdir(this.workspaceRoot);
|
|
244
|
+
// Process bucket1 directories in parallel (Requirement 26.3)
|
|
245
|
+
const cleanupPromises = [];
|
|
246
|
+
for (const bucket1 of bucket1Dirs) {
|
|
247
|
+
const bucket1Path = path.join(this.workspaceRoot, bucket1);
|
|
248
|
+
cleanupPromises.push((async () => {
|
|
249
|
+
let localCleanedCount = 0;
|
|
250
|
+
try {
|
|
251
|
+
// Skip if not a directory
|
|
252
|
+
const bucket1Stats = await fsPromises.stat(bucket1Path);
|
|
253
|
+
if (!bucket1Stats.isDirectory()) {
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
const bucket2Dirs = await fsPromises.readdir(bucket1Path);
|
|
257
|
+
for (const bucket2 of bucket2Dirs) {
|
|
258
|
+
const bucket2Path = path.join(bucket1Path, bucket2);
|
|
259
|
+
try {
|
|
260
|
+
// Skip if not a directory
|
|
261
|
+
const bucket2Stats = await fsPromises.stat(bucket2Path);
|
|
262
|
+
if (!bucket2Stats.isDirectory()) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const workspaceDirs = await fsPromises.readdir(bucket2Path);
|
|
266
|
+
for (const workspaceDir of workspaceDirs) {
|
|
267
|
+
const workspacePath = path.join(bucket2Path, workspaceDir);
|
|
268
|
+
try {
|
|
269
|
+
// Skip if not a directory
|
|
270
|
+
const workspaceStats = await fsPromises.stat(workspacePath);
|
|
271
|
+
if (!workspaceStats.isDirectory()) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// Check modification time
|
|
275
|
+
if (workspaceStats.mtimeMs < cutoffTime) {
|
|
276
|
+
// Delete old workspace
|
|
277
|
+
await fsPromises.rm(workspacePath, { recursive: true, force: true });
|
|
278
|
+
localCleanedCount++;
|
|
279
|
+
// Remove from cache if present
|
|
280
|
+
this.workspacePathCache.delete(workspaceDir);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Skip workspace on error
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Clean up empty bucket2 directories
|
|
289
|
+
const remainingFiles = await fsPromises.readdir(bucket2Path);
|
|
290
|
+
if (remainingFiles.length === 0) {
|
|
291
|
+
await fsPromises.rmdir(bucket2Path);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Skip bucket2 on error
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Clean up empty bucket1 directories
|
|
300
|
+
const remainingFiles = await fsPromises.readdir(bucket1Path);
|
|
301
|
+
if (remainingFiles.length === 0) {
|
|
302
|
+
await fsPromises.rmdir(bucket1Path);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Skip bucket1 on error
|
|
307
|
+
}
|
|
308
|
+
return localCleanedCount;
|
|
309
|
+
})());
|
|
310
|
+
}
|
|
311
|
+
// Wait for all cleanup operations to complete
|
|
312
|
+
const results = await Promise.all(cleanupPromises);
|
|
313
|
+
cleanedCount = results.reduce((sum, count) => sum + count, 0);
|
|
314
|
+
return cleanedCount;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get workspace statistics for monitoring
|
|
318
|
+
* Requirements: 26.3 (file I/O optimization)
|
|
319
|
+
*
|
|
320
|
+
* @returns Object with workspace statistics
|
|
321
|
+
*/
|
|
322
|
+
async getWorkspaceStats() {
|
|
323
|
+
let totalWorkspaces = 0;
|
|
324
|
+
let totalSizeBytes = 0;
|
|
325
|
+
let oldestMtime = Date.now();
|
|
326
|
+
if (!fs.existsSync(this.workspaceRoot)) {
|
|
327
|
+
return { totalWorkspaces: 0, totalSizeBytes: 0, oldestWorkspaceAge: 0 };
|
|
328
|
+
}
|
|
329
|
+
const bucket1Dirs = await fsPromises.readdir(this.workspaceRoot);
|
|
330
|
+
for (const bucket1 of bucket1Dirs) {
|
|
331
|
+
const bucket1Path = path.join(this.workspaceRoot, bucket1);
|
|
332
|
+
try {
|
|
333
|
+
const bucket1Stats = await fsPromises.stat(bucket1Path);
|
|
334
|
+
if (!bucket1Stats.isDirectory()) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const bucket2Dirs = await fsPromises.readdir(bucket1Path);
|
|
338
|
+
for (const bucket2 of bucket2Dirs) {
|
|
339
|
+
const bucket2Path = path.join(bucket1Path, bucket2);
|
|
340
|
+
try {
|
|
341
|
+
const bucket2Stats = await fsPromises.stat(bucket2Path);
|
|
342
|
+
if (!bucket2Stats.isDirectory()) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const workspaceDirs = await fsPromises.readdir(bucket2Path);
|
|
346
|
+
for (const workspaceDir of workspaceDirs) {
|
|
347
|
+
const workspacePath = path.join(bucket2Path, workspaceDir);
|
|
348
|
+
try {
|
|
349
|
+
const workspaceStats = await fsPromises.stat(workspacePath);
|
|
350
|
+
if (!workspaceStats.isDirectory()) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
totalWorkspaces++;
|
|
354
|
+
// Get directory size (approximate)
|
|
355
|
+
const size = await this.getDirectorySize(workspacePath);
|
|
356
|
+
totalSizeBytes += size;
|
|
357
|
+
// Track oldest workspace
|
|
358
|
+
if (workspaceStats.mtimeMs < oldestMtime) {
|
|
359
|
+
oldestMtime = workspaceStats.mtimeMs;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const oldestWorkspaceAge = Math.floor((Date.now() - oldestMtime) / (24 * 60 * 60 * 1000));
|
|
377
|
+
return {
|
|
378
|
+
totalWorkspaces,
|
|
379
|
+
totalSizeBytes,
|
|
380
|
+
oldestWorkspaceAge
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get approximate size of a directory
|
|
385
|
+
* Requirements: 26.3 (file I/O optimization)
|
|
386
|
+
*
|
|
387
|
+
* @param dirPath - Directory path
|
|
388
|
+
* @returns Size in bytes
|
|
389
|
+
*/
|
|
390
|
+
async getDirectorySize(dirPath) {
|
|
391
|
+
let totalSize = 0;
|
|
392
|
+
try {
|
|
393
|
+
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
394
|
+
for (const entry of entries) {
|
|
395
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
396
|
+
if (entry.isDirectory()) {
|
|
397
|
+
totalSize += await this.getDirectorySize(entryPath);
|
|
398
|
+
}
|
|
399
|
+
else if (entry.isFile()) {
|
|
400
|
+
const stats = await fsPromises.stat(entryPath);
|
|
401
|
+
totalSize += stats.size;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// Ignore errors
|
|
407
|
+
}
|
|
408
|
+
return totalSize;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=workspace-manager.js.map
|