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.
Files changed (190) hide show
  1. package/CODEX_INSTALLATION.md +69 -0
  2. package/COPILOT_INSTALLATION.md +77 -0
  3. package/LICENSE +21 -0
  4. package/README.md +314 -0
  5. package/bin/windows-exe-decompiler-mcp-server.js +3 -0
  6. package/dist/analysis-provenance.d.ts +184 -0
  7. package/dist/analysis-provenance.js +74 -0
  8. package/dist/analysis-task-runner.d.ts +31 -0
  9. package/dist/analysis-task-runner.js +160 -0
  10. package/dist/artifact-inventory.d.ts +23 -0
  11. package/dist/artifact-inventory.js +175 -0
  12. package/dist/cache-manager.d.ts +128 -0
  13. package/dist/cache-manager.js +454 -0
  14. package/dist/confidence-semantics.d.ts +66 -0
  15. package/dist/confidence-semantics.js +122 -0
  16. package/dist/config.d.ts +335 -0
  17. package/dist/config.js +193 -0
  18. package/dist/database.d.ts +227 -0
  19. package/dist/database.js +601 -0
  20. package/dist/decompiler-worker.d.ts +441 -0
  21. package/dist/decompiler-worker.js +1962 -0
  22. package/dist/dynamic-trace.d.ts +95 -0
  23. package/dist/dynamic-trace.js +629 -0
  24. package/dist/env-validator.d.ts +15 -0
  25. package/dist/env-validator.js +249 -0
  26. package/dist/error-handler.d.ts +28 -0
  27. package/dist/error-handler.example.d.ts +22 -0
  28. package/dist/error-handler.example.js +141 -0
  29. package/dist/error-handler.js +139 -0
  30. package/dist/ghidra-analysis-status.d.ts +49 -0
  31. package/dist/ghidra-analysis-status.js +178 -0
  32. package/dist/ghidra-config.d.ts +134 -0
  33. package/dist/ghidra-config.js +464 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +200 -0
  36. package/dist/job-queue.d.ts +169 -0
  37. package/dist/job-queue.js +407 -0
  38. package/dist/logger.d.ts +106 -0
  39. package/dist/logger.js +176 -0
  40. package/dist/policy-guard.d.ts +115 -0
  41. package/dist/policy-guard.js +243 -0
  42. package/dist/process-output.d.ts +15 -0
  43. package/dist/process-output.js +90 -0
  44. package/dist/prompts/function-explanation-review.d.ts +5 -0
  45. package/dist/prompts/function-explanation-review.js +64 -0
  46. package/dist/prompts/semantic-name-review.d.ts +5 -0
  47. package/dist/prompts/semantic-name-review.js +63 -0
  48. package/dist/runtime-correlation.d.ts +34 -0
  49. package/dist/runtime-correlation.js +279 -0
  50. package/dist/runtime-paths.d.ts +3 -0
  51. package/dist/runtime-paths.js +11 -0
  52. package/dist/selection-diff.d.ts +667 -0
  53. package/dist/selection-diff.js +53 -0
  54. package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
  55. package/dist/semantic-name-suggestion-artifacts.js +314 -0
  56. package/dist/server.d.ts +129 -0
  57. package/dist/server.js +578 -0
  58. package/dist/tools/artifact-read.d.ts +235 -0
  59. package/dist/tools/artifact-read.js +317 -0
  60. package/dist/tools/artifacts-diff.d.ts +728 -0
  61. package/dist/tools/artifacts-diff.js +304 -0
  62. package/dist/tools/artifacts-list.d.ts +515 -0
  63. package/dist/tools/artifacts-list.js +389 -0
  64. package/dist/tools/attack-map.d.ts +290 -0
  65. package/dist/tools/attack-map.js +519 -0
  66. package/dist/tools/cache-observability.d.ts +4 -0
  67. package/dist/tools/cache-observability.js +36 -0
  68. package/dist/tools/code-function-cfg.d.ts +50 -0
  69. package/dist/tools/code-function-cfg.js +102 -0
  70. package/dist/tools/code-function-decompile.d.ts +55 -0
  71. package/dist/tools/code-function-decompile.js +103 -0
  72. package/dist/tools/code-function-disassemble.d.ts +43 -0
  73. package/dist/tools/code-function-disassemble.js +185 -0
  74. package/dist/tools/code-function-explain-apply.d.ts +255 -0
  75. package/dist/tools/code-function-explain-apply.js +225 -0
  76. package/dist/tools/code-function-explain-prepare.d.ts +535 -0
  77. package/dist/tools/code-function-explain-prepare.js +276 -0
  78. package/dist/tools/code-function-explain-review.d.ts +397 -0
  79. package/dist/tools/code-function-explain-review.js +589 -0
  80. package/dist/tools/code-function-rename-apply.d.ts +248 -0
  81. package/dist/tools/code-function-rename-apply.js +220 -0
  82. package/dist/tools/code-function-rename-prepare.d.ts +506 -0
  83. package/dist/tools/code-function-rename-prepare.js +279 -0
  84. package/dist/tools/code-function-rename-review.d.ts +574 -0
  85. package/dist/tools/code-function-rename-review.js +761 -0
  86. package/dist/tools/code-functions-list.d.ts +37 -0
  87. package/dist/tools/code-functions-list.js +91 -0
  88. package/dist/tools/code-functions-rank.d.ts +34 -0
  89. package/dist/tools/code-functions-rank.js +90 -0
  90. package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
  91. package/dist/tools/code-functions-reconstruct.js +2807 -0
  92. package/dist/tools/code-functions-search.d.ts +39 -0
  93. package/dist/tools/code-functions-search.js +90 -0
  94. package/dist/tools/code-reconstruct-export.d.ts +1212 -0
  95. package/dist/tools/code-reconstruct-export.js +4002 -0
  96. package/dist/tools/code-reconstruct-plan.d.ts +274 -0
  97. package/dist/tools/code-reconstruct-plan.js +342 -0
  98. package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
  99. package/dist/tools/dotnet-metadata-extract.js +355 -0
  100. package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
  101. package/dist/tools/dotnet-reconstruct-export.js +1151 -0
  102. package/dist/tools/dotnet-types-list.d.ts +325 -0
  103. package/dist/tools/dotnet-types-list.js +201 -0
  104. package/dist/tools/dynamic-dependencies.d.ts +115 -0
  105. package/dist/tools/dynamic-dependencies.js +213 -0
  106. package/dist/tools/dynamic-memory-import.d.ts +10 -0
  107. package/dist/tools/dynamic-memory-import.js +567 -0
  108. package/dist/tools/dynamic-trace-import.d.ts +10 -0
  109. package/dist/tools/dynamic-trace-import.js +235 -0
  110. package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
  111. package/dist/tools/entrypoint-fallback-disasm.js +89 -0
  112. package/dist/tools/ghidra-analyze.d.ts +88 -0
  113. package/dist/tools/ghidra-analyze.js +208 -0
  114. package/dist/tools/ghidra-health.d.ts +37 -0
  115. package/dist/tools/ghidra-health.js +212 -0
  116. package/dist/tools/ioc-export.d.ts +209 -0
  117. package/dist/tools/ioc-export.js +542 -0
  118. package/dist/tools/packer-detect.d.ts +165 -0
  119. package/dist/tools/packer-detect.js +284 -0
  120. package/dist/tools/pe-exports-extract.d.ts +175 -0
  121. package/dist/tools/pe-exports-extract.js +253 -0
  122. package/dist/tools/pe-fingerprint.d.ts +234 -0
  123. package/dist/tools/pe-fingerprint.js +269 -0
  124. package/dist/tools/pe-imports-extract.d.ts +105 -0
  125. package/dist/tools/pe-imports-extract.js +245 -0
  126. package/dist/tools/report-generate.d.ts +157 -0
  127. package/dist/tools/report-generate.js +457 -0
  128. package/dist/tools/report-summarize.d.ts +2131 -0
  129. package/dist/tools/report-summarize.js +596 -0
  130. package/dist/tools/runtime-detect.d.ts +135 -0
  131. package/dist/tools/runtime-detect.js +247 -0
  132. package/dist/tools/sample-ingest.d.ts +94 -0
  133. package/dist/tools/sample-ingest.js +327 -0
  134. package/dist/tools/sample-profile-get.d.ts +183 -0
  135. package/dist/tools/sample-profile-get.js +121 -0
  136. package/dist/tools/sandbox-execute.d.ts +441 -0
  137. package/dist/tools/sandbox-execute.js +392 -0
  138. package/dist/tools/strings-extract.d.ts +375 -0
  139. package/dist/tools/strings-extract.js +314 -0
  140. package/dist/tools/strings-floss-decode.d.ts +143 -0
  141. package/dist/tools/strings-floss-decode.js +259 -0
  142. package/dist/tools/system-health.d.ts +434 -0
  143. package/dist/tools/system-health.js +446 -0
  144. package/dist/tools/task-cancel.d.ts +21 -0
  145. package/dist/tools/task-cancel.js +70 -0
  146. package/dist/tools/task-status.d.ts +27 -0
  147. package/dist/tools/task-status.js +106 -0
  148. package/dist/tools/task-sweep.d.ts +22 -0
  149. package/dist/tools/task-sweep.js +77 -0
  150. package/dist/tools/tool-help.d.ts +340 -0
  151. package/dist/tools/tool-help.js +261 -0
  152. package/dist/tools/yara-scan.d.ts +554 -0
  153. package/dist/tools/yara-scan.js +313 -0
  154. package/dist/types.d.ts +266 -0
  155. package/dist/types.js +41 -0
  156. package/dist/worker-pool.d.ts +204 -0
  157. package/dist/worker-pool.js +650 -0
  158. package/dist/workflows/deep-static.d.ts +104 -0
  159. package/dist/workflows/deep-static.js +276 -0
  160. package/dist/workflows/function-explanation-review.d.ts +655 -0
  161. package/dist/workflows/function-explanation-review.js +440 -0
  162. package/dist/workflows/reconstruct.d.ts +2053 -0
  163. package/dist/workflows/reconstruct.js +666 -0
  164. package/dist/workflows/semantic-name-review.d.ts +2418 -0
  165. package/dist/workflows/semantic-name-review.js +521 -0
  166. package/dist/workflows/triage.d.ts +659 -0
  167. package/dist/workflows/triage.js +1374 -0
  168. package/dist/workspace-manager.d.ts +150 -0
  169. package/dist/workspace-manager.js +411 -0
  170. package/ghidra_scripts/DecompileFunction.java +487 -0
  171. package/ghidra_scripts/DecompileFunction.py +150 -0
  172. package/ghidra_scripts/ExtractCFG.java +256 -0
  173. package/ghidra_scripts/ExtractCFG.py +233 -0
  174. package/ghidra_scripts/ExtractFunctions.java +442 -0
  175. package/ghidra_scripts/ExtractFunctions.py +101 -0
  176. package/ghidra_scripts/README.md +125 -0
  177. package/ghidra_scripts/SearchFunctionReferences.java +380 -0
  178. package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
  179. package/helpers/DotNetMetadataProbe/Program.cs +566 -0
  180. package/install-to-codex.ps1 +178 -0
  181. package/install-to-copilot.ps1 +303 -0
  182. package/package.json +101 -0
  183. package/requirements.txt +9 -0
  184. package/workers/requirements-dynamic.txt +11 -0
  185. package/workers/requirements.txt +8 -0
  186. package/workers/speakeasy_compat.py +175 -0
  187. package/workers/static_worker.py +5183 -0
  188. package/workers/yara_rules/default.yar +33 -0
  189. package/workers/yara_rules/malware_families.yar +93 -0
  190. package/workers/yara_rules/packers.yar +80 -0
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ export const ArtifactSelectionProvenanceSchema = z.object({
3
+ scope: z.enum(['all', 'latest', 'session']),
4
+ session_selector: z.string().nullable(),
5
+ artifact_count: z.number().int().nonnegative(),
6
+ artifact_ids: z.array(z.string()),
7
+ session_tags: z.array(z.string()),
8
+ earliest_artifact_at: z.string().nullable(),
9
+ latest_artifact_at: z.string().nullable(),
10
+ scope_note: z.string(),
11
+ });
12
+ export const AnalysisProvenanceSchema = z.object({
13
+ runtime: ArtifactSelectionProvenanceSchema,
14
+ semantic_names: ArtifactSelectionProvenanceSchema.optional(),
15
+ semantic_explanations: ArtifactSelectionProvenanceSchema.optional(),
16
+ });
17
+ function emptyScopeNote(label, scope, sessionTag) {
18
+ if (scope === 'session' && sessionTag?.trim()) {
19
+ return `No ${label} matched session selector "${sessionTag.trim()}".`;
20
+ }
21
+ if (scope === 'latest') {
22
+ return `No ${label} matched the latest selection window.`;
23
+ }
24
+ return `No ${label} were selected.`;
25
+ }
26
+ export function buildRuntimeArtifactProvenance(dynamicEvidence, scope, sessionTag) {
27
+ if (!dynamicEvidence) {
28
+ return {
29
+ scope,
30
+ session_selector: sessionTag?.trim() || null,
31
+ artifact_count: 0,
32
+ artifact_ids: [],
33
+ session_tags: [],
34
+ earliest_artifact_at: null,
35
+ latest_artifact_at: null,
36
+ scope_note: emptyScopeNote('runtime evidence artifacts', scope, sessionTag),
37
+ };
38
+ }
39
+ return {
40
+ scope,
41
+ session_selector: dynamicEvidence.session_selector || sessionTag?.trim() || null,
42
+ artifact_count: dynamicEvidence.artifact_count,
43
+ artifact_ids: dynamicEvidence.artifact_ids || [],
44
+ session_tags: dynamicEvidence.session_tags || [],
45
+ earliest_artifact_at: dynamicEvidence.earliest_imported_at || null,
46
+ latest_artifact_at: dynamicEvidence.latest_imported_at || null,
47
+ scope_note: dynamicEvidence.scope_note || emptyScopeNote('runtime evidence artifacts', scope, sessionTag),
48
+ };
49
+ }
50
+ export function buildSemanticArtifactProvenance(label, index, scope, sessionTag) {
51
+ if (!index) {
52
+ return {
53
+ scope,
54
+ session_selector: sessionTag?.trim() || null,
55
+ artifact_count: 0,
56
+ artifact_ids: [],
57
+ session_tags: [],
58
+ earliest_artifact_at: null,
59
+ latest_artifact_at: null,
60
+ scope_note: emptyScopeNote(label, scope, sessionTag),
61
+ };
62
+ }
63
+ return {
64
+ scope,
65
+ session_selector: sessionTag?.trim() || null,
66
+ artifact_count: index.artifact_ids.length,
67
+ artifact_ids: index.artifact_ids,
68
+ session_tags: index.session_tags,
69
+ earliest_artifact_at: index.earliest_created_at,
70
+ latest_artifact_at: index.latest_created_at,
71
+ scope_note: index.scope_note || emptyScopeNote(label, scope, sessionTag),
72
+ };
73
+ }
74
+ //# sourceMappingURL=analysis-provenance.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Background runner for queued analysis jobs.
3
+ * Current scope: execute ghidra.analyze jobs with cancellation/timeouts and stale-job reaping.
4
+ */
5
+ import type { DatabaseManager } from './database.js';
6
+ import type { WorkspaceManager } from './workspace-manager.js';
7
+ import type { JobQueue } from './job-queue.js';
8
+ import type { CacheManager } from './cache-manager.js';
9
+ export interface AnalysisTaskRunnerOptions {
10
+ pollIntervalMs?: number;
11
+ staleRunningMs?: number | null;
12
+ }
13
+ export declare class AnalysisTaskRunner {
14
+ private readonly jobQueue;
15
+ private readonly decompilerWorker;
16
+ private readonly pollIntervalMs;
17
+ private readonly staleRunningMs?;
18
+ private readonly database;
19
+ private readonly workspaceManager;
20
+ private readonly cacheManager?;
21
+ private timer?;
22
+ private processing;
23
+ private activeControllers;
24
+ constructor(jobQueue: JobQueue, database: DatabaseManager, workspaceManager: WorkspaceManager, cacheManager?: CacheManager, options?: AnalysisTaskRunnerOptions);
25
+ start(): void;
26
+ stop(): void;
27
+ private reapStaleRunning;
28
+ private processNext;
29
+ private executeJob;
30
+ }
31
+ //# sourceMappingURL=analysis-task-runner.d.ts.map
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Background runner for queued analysis jobs.
3
+ * Current scope: execute ghidra.analyze jobs with cancellation/timeouts and stale-job reaping.
4
+ */
5
+ import { DecompilerWorker, GhidraProcessError } from './decompiler-worker.js';
6
+ import { logger } from './logger.js';
7
+ import { deepStaticWorkflow } from './workflows/deep-static.js';
8
+ export class AnalysisTaskRunner {
9
+ jobQueue;
10
+ decompilerWorker;
11
+ pollIntervalMs;
12
+ staleRunningMs;
13
+ database;
14
+ workspaceManager;
15
+ cacheManager;
16
+ timer;
17
+ processing = false;
18
+ activeControllers = new Map();
19
+ constructor(jobQueue, database, workspaceManager, cacheManager, options = {}) {
20
+ this.jobQueue = jobQueue;
21
+ this.database = database;
22
+ this.workspaceManager = workspaceManager;
23
+ this.cacheManager = cacheManager;
24
+ this.decompilerWorker = new DecompilerWorker(database, workspaceManager);
25
+ this.pollIntervalMs = options.pollIntervalMs ?? 500;
26
+ this.staleRunningMs = options.staleRunningMs;
27
+ this.jobQueue.on('job:cancelled', (jobId) => {
28
+ const controller = this.activeControllers.get(jobId);
29
+ if (controller) {
30
+ controller.abort();
31
+ }
32
+ });
33
+ }
34
+ start() {
35
+ if (this.timer) {
36
+ return;
37
+ }
38
+ this.timer = setInterval(() => {
39
+ this.reapStaleRunning();
40
+ void this.processNext();
41
+ }, this.pollIntervalMs);
42
+ }
43
+ stop() {
44
+ if (this.timer) {
45
+ clearInterval(this.timer);
46
+ this.timer = undefined;
47
+ }
48
+ for (const controller of this.activeControllers.values()) {
49
+ controller.abort();
50
+ }
51
+ this.activeControllers.clear();
52
+ }
53
+ reapStaleRunning() {
54
+ if (typeof this.staleRunningMs !== 'number' ||
55
+ !Number.isFinite(this.staleRunningMs) ||
56
+ this.staleRunningMs <= 0) {
57
+ return;
58
+ }
59
+ const queue = this.jobQueue;
60
+ if (typeof queue.reapStaleRunningJobs !== 'function') {
61
+ return;
62
+ }
63
+ const reaped = queue.reapStaleRunningJobs(this.staleRunningMs);
64
+ for (const jobId of reaped) {
65
+ const controller = this.activeControllers.get(jobId);
66
+ if (controller) {
67
+ controller.abort();
68
+ }
69
+ }
70
+ if (reaped.length > 0) {
71
+ logger.warn({
72
+ reaped_count: reaped.length,
73
+ stale_running_ms: this.staleRunningMs,
74
+ jobs: reaped,
75
+ }, 'Reaped stale running analysis jobs');
76
+ }
77
+ }
78
+ async processNext() {
79
+ if (this.processing) {
80
+ return;
81
+ }
82
+ const job = this.jobQueue.dequeue();
83
+ if (!job) {
84
+ return;
85
+ }
86
+ this.processing = true;
87
+ const startTime = Date.now();
88
+ const controller = new AbortController();
89
+ this.activeControllers.set(job.id, controller);
90
+ try {
91
+ const result = await this.executeJob(job, controller.signal);
92
+ if (typeof result.metrics.elapsedMs !== 'number' || result.metrics.elapsedMs <= 0) {
93
+ result.metrics.elapsedMs = Date.now() - startTime;
94
+ }
95
+ this.jobQueue.complete(job.id, result);
96
+ }
97
+ catch (error) {
98
+ const elapsedMs = Date.now() - startTime;
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ logger.error({
101
+ job_id: job.id,
102
+ tool: job.tool,
103
+ sample_id: job.sampleId,
104
+ error: message,
105
+ }, 'Analysis task failed');
106
+ const normalizedError = error instanceof Error ? error : new Error(message);
107
+ this.jobQueue.complete(job.id, this.decompilerWorker.createErrorJobResult(job.id, normalizedError, elapsedMs));
108
+ }
109
+ finally {
110
+ this.activeControllers.delete(job.id);
111
+ this.processing = false;
112
+ }
113
+ }
114
+ async executeJob(job, abortSignal) {
115
+ if (job.tool === 'ghidra.analyze') {
116
+ const options = (job.args?.options || {});
117
+ try {
118
+ const analysisResult = await this.decompilerWorker.analyze(job.sampleId, {
119
+ analysisId: job.id,
120
+ timeout: job.timeout,
121
+ maxCpu: options.max_cpu || '4',
122
+ projectKey: options.project_key,
123
+ abortSignal,
124
+ });
125
+ return this.decompilerWorker.createJobResult(analysisResult, 0);
126
+ }
127
+ catch (error) {
128
+ if (error instanceof GhidraProcessError && error.errorCode === 'E_CANCELLED') {
129
+ throw new Error('E_CANCELLED: analysis task cancelled');
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+ if (job.tool === 'workflow.deep_static') {
135
+ if (!this.cacheManager) {
136
+ throw new Error('workflow.deep_static requires cache manager for queued execution');
137
+ }
138
+ const options = (job.args?.options || {});
139
+ const result = await deepStaticWorkflow(job.sampleId, this.workspaceManager, this.database, this.cacheManager, options, {
140
+ onProgress: (progress) => {
141
+ this.jobQueue.updateProgress(job.id, progress);
142
+ },
143
+ });
144
+ return {
145
+ jobId: job.id,
146
+ ok: result.ok,
147
+ data: result.data,
148
+ errors: result.errors || [],
149
+ warnings: result.warnings || [],
150
+ artifacts: [],
151
+ metrics: {
152
+ elapsedMs: 0,
153
+ peakRssMb: 0,
154
+ },
155
+ };
156
+ }
157
+ throw new Error(`Unsupported queued tool: ${job.tool}`);
158
+ }
159
+ }
160
+ //# sourceMappingURL=analysis-task-runner.js.map
@@ -0,0 +1,23 @@
1
+ import type { WorkspaceManager } from './workspace-manager.js';
2
+ import type { DatabaseManager, Artifact } from './database.js';
3
+ export interface ArtifactInventoryItem extends Artifact {
4
+ exists: boolean;
5
+ size_bytes: number | null;
6
+ modified_at: string | null;
7
+ tracked: boolean;
8
+ session_tag: string | null;
9
+ retention_bucket: 'active' | 'recent' | 'archive';
10
+ age_days: number;
11
+ }
12
+ export interface ArtifactInventoryOptions {
13
+ artifactTypes?: Iterable<string>;
14
+ includeMissing?: boolean;
15
+ includeUntrackedFiles?: boolean;
16
+ recursive?: boolean;
17
+ scanRoots?: string[];
18
+ }
19
+ export declare function normalizeRelativeArtifactPath(p: string): string;
20
+ export declare function inferUntrackedArtifactType(relativePath: string): string;
21
+ export declare function deriveArtifactSessionTag(relativePath: string): string | null;
22
+ export declare function listArtifactInventory(workspaceManager: WorkspaceManager, database: DatabaseManager, sampleId: string, options?: ArtifactInventoryOptions): Promise<ArtifactInventoryItem[]>;
23
+ //# sourceMappingURL=artifact-inventory.d.ts.map
@@ -0,0 +1,175 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { createHash } from 'crypto';
4
+ export function normalizeRelativeArtifactPath(p) {
5
+ return p.replace(/\\/g, '/').replace(/^\.?\//, '');
6
+ }
7
+ export function inferUntrackedArtifactType(relativePath) {
8
+ const normalized = relativePath.toLowerCase();
9
+ if (normalized.endsWith('.pseudo.c') || normalized.includes('pseudo')) {
10
+ return 'ghidra_pseudocode';
11
+ }
12
+ if (normalized.includes('manifest')) {
13
+ return 'manifest';
14
+ }
15
+ if (normalized.includes('gaps')) {
16
+ return 'gaps';
17
+ }
18
+ if (normalized.includes('report')) {
19
+ return 'report';
20
+ }
21
+ if (normalized.includes('ioc_export')) {
22
+ return 'ioc_export';
23
+ }
24
+ return 'filesystem_untracked';
25
+ }
26
+ export function deriveArtifactSessionTag(relativePath) {
27
+ const normalized = normalizeRelativeArtifactPath(relativePath);
28
+ const segments = normalized.split('/').filter((item) => item.length > 0);
29
+ if (segments.length < 2) {
30
+ return null;
31
+ }
32
+ if (segments[0] === 'reports' && segments.length >= 4) {
33
+ return `${segments[0]}/${segments[1]}/${segments[2]}`;
34
+ }
35
+ if (segments[0] === 'reports' && segments.length >= 2) {
36
+ return `${segments[0]}/${segments[1]}`;
37
+ }
38
+ return `${segments[0]}/${segments[1]}`;
39
+ }
40
+ function deriveArtifactAgeDays(referenceIso) {
41
+ const timestamp = referenceIso ? new Date(referenceIso).getTime() : Number.NaN;
42
+ if (!Number.isFinite(timestamp)) {
43
+ return 0;
44
+ }
45
+ return Math.max(0, Math.floor((Date.now() - timestamp) / (24 * 60 * 60 * 1000)));
46
+ }
47
+ function deriveRetentionBucket(ageDays) {
48
+ if (ageDays <= 3) {
49
+ return 'active';
50
+ }
51
+ if (ageDays <= 14) {
52
+ return 'recent';
53
+ }
54
+ return 'archive';
55
+ }
56
+ async function collectFiles(rootPath, recursive) {
57
+ const items = [];
58
+ let entries = [];
59
+ try {
60
+ entries = (await fs.readdir(rootPath, {
61
+ withFileTypes: true,
62
+ encoding: 'utf8',
63
+ }));
64
+ }
65
+ catch {
66
+ return items;
67
+ }
68
+ for (const entry of entries) {
69
+ const absolute = path.join(rootPath, String(entry.name));
70
+ if (entry.isFile()) {
71
+ try {
72
+ const stat = await fs.stat(absolute);
73
+ items.push({ absolute, stat: { size: stat.size, mtime: stat.mtime } });
74
+ }
75
+ catch {
76
+ // Ignore transient file errors.
77
+ }
78
+ continue;
79
+ }
80
+ if (recursive && entry.isDirectory()) {
81
+ const nested = await collectFiles(absolute, recursive);
82
+ items.push(...nested);
83
+ }
84
+ }
85
+ return items;
86
+ }
87
+ async function enrichTrackedArtifact(workspaceManager, workspaceRoot, artifact) {
88
+ const absolutePath = workspaceManager.normalizePath(workspaceRoot, artifact.path);
89
+ const sessionTag = deriveArtifactSessionTag(artifact.path);
90
+ try {
91
+ const stat = await fs.stat(absolutePath);
92
+ const ageDays = deriveArtifactAgeDays(artifact.created_at || stat.mtime.toISOString());
93
+ return {
94
+ ...artifact,
95
+ exists: true,
96
+ size_bytes: stat.size,
97
+ modified_at: stat.mtime.toISOString(),
98
+ tracked: true,
99
+ session_tag: sessionTag,
100
+ retention_bucket: deriveRetentionBucket(ageDays),
101
+ age_days: ageDays,
102
+ };
103
+ }
104
+ catch {
105
+ const ageDays = deriveArtifactAgeDays(artifact.created_at);
106
+ return {
107
+ ...artifact,
108
+ exists: false,
109
+ size_bytes: null,
110
+ modified_at: null,
111
+ tracked: true,
112
+ session_tag: sessionTag,
113
+ retention_bucket: deriveRetentionBucket(ageDays),
114
+ age_days: ageDays,
115
+ };
116
+ }
117
+ }
118
+ export async function listArtifactInventory(workspaceManager, database, sampleId, options = {}) {
119
+ const includeMissing = options.includeMissing !== false;
120
+ const includeUntrackedFiles = options.includeUntrackedFiles !== false;
121
+ const recursive = options.recursive !== false;
122
+ const scanRoots = options.scanRoots || ['reports', 'ghidra', 'dotnet'];
123
+ const typeFilter = new Set();
124
+ for (const item of options.artifactTypes || []) {
125
+ if (typeof item === 'string' && item.length > 0) {
126
+ typeFilter.add(item);
127
+ }
128
+ }
129
+ const trackedArtifacts = database.findArtifacts(sampleId);
130
+ const filteredTracked = typeFilter.size > 0
131
+ ? trackedArtifacts.filter((item) => typeFilter.has(item.type))
132
+ : trackedArtifacts;
133
+ const workspace = await workspaceManager.getWorkspace(sampleId);
134
+ const tracked = await Promise.all(filteredTracked.map((artifact) => enrichTrackedArtifact(workspaceManager, workspace.root, artifact)));
135
+ const trackedPathSet = new Set(tracked.map((item) => normalizeRelativeArtifactPath(item.path).toLowerCase()));
136
+ const untracked = [];
137
+ if (includeUntrackedFiles) {
138
+ for (const scanRoot of scanRoots) {
139
+ const rootAbsolute = path.join(workspace.root, scanRoot);
140
+ const files = await collectFiles(rootAbsolute, recursive);
141
+ for (const file of files) {
142
+ const relative = normalizeRelativeArtifactPath(path.relative(workspace.root, file.absolute));
143
+ const key = relative.toLowerCase();
144
+ if (trackedPathSet.has(key)) {
145
+ continue;
146
+ }
147
+ trackedPathSet.add(key);
148
+ const inferredType = inferUntrackedArtifactType(relative);
149
+ const ageDays = deriveArtifactAgeDays(file.stat.mtime.toISOString());
150
+ if (typeFilter.size > 0 && !typeFilter.has(inferredType)) {
151
+ continue;
152
+ }
153
+ untracked.push({
154
+ id: `fs:${createHash('sha1').update(relative).digest('hex')}`,
155
+ sample_id: sampleId,
156
+ type: inferredType,
157
+ path: relative,
158
+ sha256: '',
159
+ mime: null,
160
+ created_at: file.stat.mtime.toISOString(),
161
+ exists: true,
162
+ size_bytes: file.stat.size,
163
+ modified_at: file.stat.mtime.toISOString(),
164
+ tracked: false,
165
+ session_tag: deriveArtifactSessionTag(relative),
166
+ retention_bucket: deriveRetentionBucket(ageDays),
167
+ age_days: ageDays,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ const merged = [...tracked, ...untracked];
173
+ return includeMissing ? merged : merged.filter((item) => item.exists);
174
+ }
175
+ //# sourceMappingURL=artifact-inventory.js.map
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Cache Manager
3
+ * Implements cache key generation and three-tier caching architecture
4
+ * Requirements: 20.1, 20.2, 20.3, 20.4, 20.5
5
+ */
6
+ import type { CacheKeyParams } from './types.js';
7
+ import type { DatabaseManager } from './database.js';
8
+ export type CacheTier = 'memory' | 'filesystem' | 'database' | 'unknown';
9
+ export interface CacheHitMetadata {
10
+ key: string;
11
+ tier: CacheTier;
12
+ createdAt?: string;
13
+ expiresAt?: string;
14
+ fetchedAt: string;
15
+ sampleSha256?: string;
16
+ }
17
+ export interface CacheHitLookup {
18
+ data: unknown;
19
+ metadata: CacheHitMetadata;
20
+ }
21
+ /**
22
+ * Cache Manager with three-tier architecture
23
+ *
24
+ * Requirements: 20.3, 20.4, 20.5, 26.1 (cache prewarming)
25
+ *
26
+ * Architecture:
27
+ * - L1: Memory cache (LRU, 5 minutes TTL)
28
+ * - L2: File system cache (30 days TTL)
29
+ * - L3: Database cache
30
+ */
31
+ export declare class CacheManager {
32
+ private memoryCache;
33
+ private fsCache;
34
+ private db;
35
+ private prewarmInProgress;
36
+ constructor(cacheDir: string, db?: DatabaseManager);
37
+ /**
38
+ * Get cached result from three-tier cache
39
+ *
40
+ * Requirements: 20.3
41
+ *
42
+ * Algorithm:
43
+ * 1. Check L1 (memory cache)
44
+ * 2. If miss, check L2 (file system cache) and populate L1
45
+ * 3. If miss, check L3 (database cache) and populate L1 and L2
46
+ * 4. Return null if not found in any layer
47
+ *
48
+ * @param key - Cache key
49
+ * @returns Cached data or null if not found
50
+ */
51
+ getCachedResult(key: string): Promise<unknown | null>;
52
+ /**
53
+ * Get cached result with hit metadata for observability.
54
+ */
55
+ getCachedResultWithMetadata(key: string): Promise<CacheHitLookup | null>;
56
+ /**
57
+ * Set cached result in all three tiers
58
+ *
59
+ * Requirements: 20.4
60
+ *
61
+ * Algorithm:
62
+ * 1. Store in L1 (memory cache)
63
+ * 2. Store in L2 (file system cache)
64
+ * 3. Store in L3 (database cache) if available
65
+ *
66
+ * @param key - Cache key
67
+ * @param data - Data to cache
68
+ * @param ttl - Time to live in milliseconds (optional)
69
+ */
70
+ setCachedResult(key: string, data: unknown, ttl?: number, sampleSha256?: string): Promise<void>;
71
+ /**
72
+ * Clear all caches
73
+ */
74
+ clearAll(): void;
75
+ /**
76
+ * Prewarm cache by loading frequently accessed data into memory
77
+ *
78
+ * Requirements: 26.1 (cache prewarming)
79
+ *
80
+ * Strategy:
81
+ * 1. Load recent cache entries from database
82
+ * 2. Populate L1 (memory) and L2 (filesystem) caches
83
+ * 3. Prioritize entries with high access frequency
84
+ *
85
+ * @param maxEntries - Maximum number of entries to prewarm (default: 100)
86
+ */
87
+ prewarmCache(maxEntries?: number): Promise<number>;
88
+ /**
89
+ * Prewarm cache for a specific sample
90
+ * Loads all cached results for a sample into memory
91
+ *
92
+ * Requirements: 26.1 (cache prewarming)
93
+ *
94
+ * @param sampleSha256 - SHA256 hash of the sample
95
+ * @returns Number of entries prewarmed
96
+ */
97
+ prewarmSampleCache(sampleSha256: string): Promise<number>;
98
+ }
99
+ /**
100
+ * Generate deterministic cache key from parameters
101
+ *
102
+ * Requirements: 20.1, 20.2
103
+ *
104
+ * Algorithm:
105
+ * 1. Normalize arguments (sort keys, remove defaults)
106
+ * 2. Create canonical representation
107
+ * 3. Generate SHA256 hash
108
+ *
109
+ * @param params - Cache key parameters
110
+ * @returns Cache key string in format "cache:<sha256>"
111
+ */
112
+ export declare function generateCacheKey(params: CacheKeyParams): string;
113
+ /**
114
+ * Normalize arguments for cache key generation
115
+ *
116
+ * Requirements: 20.2
117
+ *
118
+ * Normalization rules:
119
+ * 1. Sort object keys recursively
120
+ * 2. Remove null and undefined values
121
+ * 3. Recursively normalize nested objects
122
+ * 4. Preserve arrays as-is (order matters)
123
+ *
124
+ * @param args - Arguments object to normalize
125
+ * @returns Normalized arguments object
126
+ */
127
+ export declare function normalizeArgs(args: Record<string, unknown>): Record<string, unknown>;
128
+ //# sourceMappingURL=cache-manager.d.ts.map