@eyeauras/insight-cli 0.0.0-stage → 0.2.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 (174) hide show
  1. package/.cli-package-build.json +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +60 -2
  4. package/SKILL.md +58 -0
  5. package/dist/src/administration-results.d.ts +1 -0
  6. package/dist/src/administration-results.js +84 -0
  7. package/dist/src/administration.d.ts +14 -0
  8. package/dist/src/administration.js +134 -0
  9. package/dist/src/archive-contract.d.ts +6 -0
  10. package/dist/src/archive-contract.js +108 -0
  11. package/dist/src/archive.d.ts +12 -0
  12. package/dist/src/archive.js +116 -0
  13. package/dist/src/arguments.d.ts +14 -0
  14. package/dist/src/arguments.js +62 -0
  15. package/dist/src/artifacts.d.ts +17 -0
  16. package/dist/src/artifacts.js +115 -0
  17. package/dist/src/bin.d.ts +2 -0
  18. package/dist/src/bin.js +4 -0
  19. package/dist/src/cli.d.ts +10 -0
  20. package/dist/src/cli.js +419 -0
  21. package/dist/src/comment-effects.d.ts +3 -0
  22. package/dist/src/comment-effects.js +55 -0
  23. package/dist/src/contexts.d.ts +115 -0
  24. package/dist/src/contexts.js +1078 -0
  25. package/dist/src/coordinator.d.ts +41 -0
  26. package/dist/src/coordinator.js +160 -0
  27. package/dist/src/cross-results.d.ts +14 -0
  28. package/dist/src/cross-results.js +71 -0
  29. package/dist/src/cross.d.ts +3 -0
  30. package/dist/src/cross.js +43 -0
  31. package/dist/src/data-effects.d.ts +3 -0
  32. package/dist/src/data-effects.js +103 -0
  33. package/dist/src/debugger-results.d.ts +4 -0
  34. package/dist/src/debugger-results.js +151 -0
  35. package/dist/src/debugger-supervisor.d.ts +1 -0
  36. package/dist/src/debugger-supervisor.js +136 -0
  37. package/dist/src/debugger.d.ts +21 -0
  38. package/dist/src/debugger.js +267 -0
  39. package/dist/src/documentation-effects.d.ts +5 -0
  40. package/dist/src/documentation-effects.js +56 -0
  41. package/dist/src/extensions.d.ts +64 -0
  42. package/dist/src/extensions.js +289 -0
  43. package/dist/src/function-effects.d.ts +4 -0
  44. package/dist/src/function-effects.js +112 -0
  45. package/dist/src/ghidra.d.ts +50 -0
  46. package/dist/src/ghidra.js +468 -0
  47. package/dist/src/index.d.ts +2 -0
  48. package/dist/src/index.js +1 -0
  49. package/dist/src/initial-model.d.ts +6 -0
  50. package/dist/src/initial-model.js +85 -0
  51. package/dist/src/json.d.ts +2 -0
  52. package/dist/src/json.js +8 -0
  53. package/dist/src/lifecycle.d.ts +49 -0
  54. package/dist/src/lifecycle.js +154 -0
  55. package/dist/src/memory-inputs.d.ts +8 -0
  56. package/dist/src/memory-inputs.js +56 -0
  57. package/dist/src/merge.d.ts +29 -0
  58. package/dist/src/merge.js +66 -0
  59. package/dist/src/model.d.ts +74 -0
  60. package/dist/src/model.js +67 -0
  61. package/dist/src/mutation-effects.d.ts +4 -0
  62. package/dist/src/mutation-effects.js +321 -0
  63. package/dist/src/mutation-inputs.d.ts +2 -0
  64. package/dist/src/mutation-inputs.js +71 -0
  65. package/dist/src/mutations.d.ts +4 -0
  66. package/dist/src/mutations.js +53 -0
  67. package/dist/src/native-inputs.d.ts +28 -0
  68. package/dist/src/native-inputs.js +182 -0
  69. package/dist/src/native-queries.d.ts +2 -0
  70. package/dist/src/native-queries.js +29 -0
  71. package/dist/src/projects.d.ts +31 -0
  72. package/dist/src/projects.js +301 -0
  73. package/dist/src/prototype-cli.d.ts +8 -0
  74. package/dist/src/prototype-cli.js +155 -0
  75. package/dist/src/queries.d.ts +5 -0
  76. package/dist/src/queries.js +84 -0
  77. package/dist/src/query-policy.d.ts +6 -0
  78. package/dist/src/query-policy.js +72 -0
  79. package/dist/src/query-results.d.ts +2 -0
  80. package/dist/src/query-results.js +224 -0
  81. package/dist/src/research.d.ts +184 -0
  82. package/dist/src/research.js +808 -0
  83. package/dist/src/runtime-archives.d.ts +5 -0
  84. package/dist/src/runtime-archives.js +127 -0
  85. package/dist/src/runtime-catalog.d.ts +18 -0
  86. package/dist/src/runtime-catalog.js +27 -0
  87. package/dist/src/runtime-manager.d.ts +101 -0
  88. package/dist/src/runtime-manager.js +444 -0
  89. package/dist/src/runtime-selection.d.ts +13 -0
  90. package/dist/src/runtime-selection.js +25 -0
  91. package/dist/src/saved-materials.d.ts +12 -0
  92. package/dist/src/saved-materials.js +196 -0
  93. package/dist/src/scripts.d.ts +19 -0
  94. package/dist/src/scripts.js +79 -0
  95. package/dist/src/sdk-contracts.d.ts +13 -0
  96. package/dist/src/sdk-contracts.js +37 -0
  97. package/dist/src/sdk-files.d.ts +29 -0
  98. package/dist/src/sdk-files.js +249 -0
  99. package/dist/src/sdk.d.ts +4 -0
  100. package/dist/src/sdk.js +125 -0
  101. package/dist/src/server.d.ts +46 -0
  102. package/dist/src/server.js +460 -0
  103. package/dist/src/store.d.ts +33 -0
  104. package/dist/src/store.js +178 -0
  105. package/dist/src/sync-policy.d.ts +22 -0
  106. package/dist/src/sync-policy.js +123 -0
  107. package/dist/src/transport.d.ts +23 -0
  108. package/dist/src/transport.js +135 -0
  109. package/dist/src/ui-results.d.ts +2 -0
  110. package/dist/src/ui-results.js +80 -0
  111. package/dist/src/ui.d.ts +17 -0
  112. package/dist/src/ui.js +88 -0
  113. package/dist/src/utility.d.ts +14 -0
  114. package/dist/src/utility.js +43 -0
  115. package/dist/src/version-tracking.d.ts +57 -0
  116. package/dist/src/version-tracking.js +200 -0
  117. package/dist/src/workspace.d.ts +199 -0
  118. package/dist/src/workspace.js +564 -0
  119. package/package.json +47 -4
  120. package/references/InsightRepositoryProbe.java +55 -0
  121. package/references/administration.md +34 -0
  122. package/references/archive.md +32 -0
  123. package/references/artifacts.md +47 -0
  124. package/references/debugger.md +40 -0
  125. package/references/extensions-and-sync.md +124 -0
  126. package/references/managed-runtime.md +116 -0
  127. package/references/mutations.json +1424 -0
  128. package/references/native/InsightApplyDocumentation.java +48 -0
  129. package/references/native/InsightArchivePayloads.java +36 -0
  130. package/references/native/InsightBlockTransfer.java +57 -0
  131. package/references/native/InsightEmulation.java +93 -0
  132. package/references/native/InsightExtensions.java +36 -0
  133. package/references/native/InsightFunctionDatabases.java +98 -0
  134. package/references/native/InsightGuiMain.java +32 -0
  135. package/references/native/InsightHeadlessMain.java +10 -0
  136. package/references/native/InsightInitialImport.java +42 -0
  137. package/references/native/InsightInitialJournal.java +48 -0
  138. package/references/native/InsightInitialPublication.java +142 -0
  139. package/references/native/InsightKeytool.java +16 -0
  140. package/references/native/InsightNativeReceipt.java +31 -0
  141. package/references/native/InsightPlugins.java +34 -0
  142. package/references/native/InsightProjectFiles.java +92 -0
  143. package/references/native/InsightPublicationRecovery.java +63 -0
  144. package/references/native/InsightPythonWheels.java +31 -0
  145. package/references/native/InsightResearchQueries.java +188 -0
  146. package/references/native/InsightRuntimeDoctor.java +33 -0
  147. package/references/native/InsightSdkFiles.java +142 -0
  148. package/references/native/InsightSdkScript.java +31 -0
  149. package/references/native/InsightSdkTools.java +338 -0
  150. package/references/native/InsightSdkTypes.java +132 -0
  151. package/references/native/InsightServerAdministration.java +82 -0
  152. package/references/native/InsightServerInit.java +62 -0
  153. package/references/native/InsightServerMain.java +43 -0
  154. package/references/native/InsightServerProbe.java +29 -0
  155. package/references/native/InsightSnapshotFields.java +68 -0
  156. package/references/native/InsightSnapshotInspect.java +189 -0
  157. package/references/native/InsightSnapshotPublisher.java +123 -0
  158. package/references/native/InsightStartDebugger.java +28 -0
  159. package/references/native/InsightStopDebugger.java +20 -0
  160. package/references/native/InsightTraceInspect.java +24 -0
  161. package/references/native/InsightTraceState.java +130 -0
  162. package/references/native/InsightTraceTools.java +114 -0
  163. package/references/native/InsightVersionTracking.java +91 -0
  164. package/references/native-workflows.md +112 -0
  165. package/references/queries.json +1761 -0
  166. package/references/query-semantics.md +96 -0
  167. package/references/recovery.md +53 -0
  168. package/references/research.md +101 -0
  169. package/references/setup.md +49 -0
  170. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  171. package/runtime/GhidraMCP-LICENSE +202 -0
  172. package/runtime/GhidraMCP-NOTICE +10 -0
  173. package/runtime/provenance.json +11 -0
  174. package/skills/insight-re/SKILL.md +108 -0
@@ -0,0 +1,4 @@
1
+ import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
2
+ import { Research } from './research.js';
3
+ export declare function sdkInputs(operation: string, p: Record<string, unknown>): void;
4
+ export declare function sdkCommands(research: (c: CommandContext) => Research): CommandDefinition[];
@@ -0,0 +1,125 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { sdkOperations } from './sdk-contracts.js';
3
+ import { option, key, parsed, selectedOptions } from './arguments.js';
4
+ import { scope, address, number } from './research.js';
5
+ import { id, fail, object, text } from './model.js';
6
+ import { verifyMutation } from './mutation-effects.js';
7
+ import { resolve } from 'node:path';
8
+ export function sdkInputs(operation, p) {
9
+ if (['type-structure', 'type-layout', 'type-settings'].includes(operation) && !/^\/(?:[^/\0]+\/)*[^/\0]+$/.test(String(p.path)))
10
+ fail('invalid', 'Types require an absolute native type path.');
11
+ for (const name of ['packing', 'alignment'])
12
+ if (p[name] !== undefined) {
13
+ const n = number(Number(p[name]) + 1, 65) - 1;
14
+ if (n > 0 && (n & (n - 1)) !== 0)
15
+ fail('invalid', 'Packing/alignment must be -1, zero or a power of two through 64.');
16
+ }
17
+ if (p.types !== undefined && (!Array.isArray(p.types) || !p.types.length || p.types.length > 256 || p.types.some(v => typeof v !== 'string' || !v.startsWith('/'))))
18
+ fail('invalid', 'Select 1–256 explicit type paths.');
19
+ if (operation === 'type-structure' && (!Array.isArray(p.fields) || !p.fields.length || p.fields.length > 256))
20
+ fail('invalid', 'A structure needs 1–256 field objects.');
21
+ if (operation === 'type-structure') {
22
+ const names = new Set();
23
+ for (const item of p.fields) {
24
+ const field = object(item), name = text(field.name, 512);
25
+ text(field.type, 512);
26
+ if (names.has(name) || Object.keys(field).some(key => !['name', 'type', 'comment', 'offset', 'bit_size', 'bit_offset', 'byte_width'].includes(key)))
27
+ fail('invalid', 'Fields require unique names and known layout properties.');
28
+ names.add(name);
29
+ if (field.comment !== undefined && (typeof field.comment !== 'string' || field.comment.length > 4096 || field.comment.includes('\0')))
30
+ fail('invalid', 'Field comment exceeds its text bound.');
31
+ for (const [key, max, min] of [['offset', 1048576, 0], ['bit_size', 512, 1], ['bit_offset', 511, 0], ['byte_width', 64, 1]])
32
+ if (field[key] !== undefined) {
33
+ if (typeof field[key] !== 'number')
34
+ fail('invalid', 'Field numeric values must be exact JSON integers.');
35
+ number(field[key], max, min);
36
+ }
37
+ if (field.offset !== undefined && Number(p.packing ?? 0) >= 0)
38
+ fail('invalid', 'Explicit field offsets require disabled packing (-1).');
39
+ if ((field.bit_offset !== undefined || field.byte_width !== undefined) && (field.bit_size === undefined || field.offset === undefined))
40
+ fail('invalid', 'Explicit bit placement requires a bitfield and byte offset.');
41
+ }
42
+ }
43
+ if (operation === 'type-settings')
44
+ for (const value of Object.values(object(p.settings))) {
45
+ if (value !== null && (!['string', 'number', 'boolean'].includes(typeof value) || typeof value === 'number' && !Number.isSafeInteger(value) || typeof value === 'string' && (value.length > 4096 || value.includes('\0'))))
46
+ fail('invalid', 'Type settings require bounded primitive values or null.');
47
+ }
48
+ if (operation === 'decompiler-ir')
49
+ for (const [key, value] of Object.entries(object(p.options ?? {})))
50
+ if (!['eliminate_unreachable', 'respect_read_only', 'infer_constant_pointers', 'split_structures', 'split_arrays'].includes(key) || typeof value !== 'boolean')
51
+ fail('invalid', 'Decompiler options require known Boolean properties.');
52
+ if (operation === 'changes') {
53
+ const categories = p.categories;
54
+ if (!Array.isArray(categories) || !categories.length || categories.length > 8 || new Set(categories).size !== categories.length || categories.some(value => !['bytes', 'code_units', 'comments', 'symbols', 'functions', 'references', 'context', 'properties'].includes(String(value))))
55
+ fail('invalid', 'Select 1–8 distinct supported diff categories.');
56
+ }
57
+ for (const name of ['expect_hex', 'bytes_hex'])
58
+ if (p[name] !== undefined &&
59
+ (typeof p[name] !== 'string' || !/^(?:[a-fA-F0-9]{2}){1,4096}$/.test(p[name])))
60
+ fail('invalid', name + ' needs 1–4096 complete hex bytes.');
61
+ if (operation === 'patch-bytes' && String(p.expect_hex).length !== String(p.bytes_hex).length)
62
+ fail('invalid', 'Expected and replacement lengths must match.');
63
+ for (const name of ['expect_sha256', 'second_sha256'])
64
+ if (p[name] !== undefined && !/^[a-f0-9]{64}$/.test(String(p[name])))
65
+ fail('invalid', 'Memory preconditions require SHA256.');
66
+ for (const name of ['expect_size', 'second_size'])
67
+ if (p[name] !== undefined)
68
+ p[name] = number(p[name], 16777216, 1);
69
+ for (const [name, max] of [['max_ranges', 1000], ['max_ops', 10000], ['timeout_seconds', 120]])
70
+ if (p[name] !== undefined)
71
+ p[name] = number(p[name], max, 1);
72
+ if (p.instructions !== undefined && (!Array.isArray(p.instructions) || !p.instructions.length || p.instructions.length > 64 || p.instructions.some(v => typeof v !== 'string' || v.length > 512 || /[\r\n\0]/.test(v))))
73
+ fail('invalid', 'Assembly requires 1–64 bounded instruction strings.');
74
+ if (operation === 'library-update' && p.new_name === undefined && p.path === undefined)
75
+ fail('invalid', 'Supply a library name or path update.');
76
+ if (operation.startsWith('context-') && p.end_address !== undefined) {
77
+ const start = address(p.address), end = address(p.end_address), parts = (v) => v.split(':');
78
+ const a = parts(start), b = parts(end);
79
+ if (a.length !== b.length || a.length === 2 && a[0] !== b[0])
80
+ fail('invalid', 'Processor context needs one address space.');
81
+ const length = BigInt('0x' + b.at(-1)) - BigInt('0x' + a.at(-1)) + 1n;
82
+ if (length < 1n || length > 65536n)
83
+ fail('invalid', 'Processor context range is 1–65536 bytes.');
84
+ }
85
+ if (operation === 'context-set' && !/^(?:0x)?[a-f0-9]{1,128}$/i.test(String(p.value)))
86
+ fail('invalid', 'Context value must be an unsigned hex integer.');
87
+ if (operation === 'context-set')
88
+ p.value = String(p.value).toLowerCase();
89
+ }
90
+ export function sdkCommands(research) {
91
+ const groups = new Map();
92
+ for (const entry of sdkOperations) {
93
+ const children = groups.get(entry.group) ?? [];
94
+ groups.set(entry.group, children);
95
+ children.push(command(entry.name, entry.description, async ({ options: o }, c) => {
96
+ const selected = scope(o), p = {};
97
+ for (const parameter of entry.params) {
98
+ const value = o[key(parameter.name)];
99
+ if (value !== undefined)
100
+ p[parameter.name] = parsed(parameter, value);
101
+ else if (parameter.required)
102
+ fail('invalid', 'Missing ' + parameter.name);
103
+ }
104
+ sdkInputs(entry.operation, p);
105
+ const api = research(c), operation = '/sdk/' + entry.operation;
106
+ if (entry.operation === 'type-archive-import') {
107
+ const archive = await api.workspace.artifacts.capture(resolve(c.cwd, String(p.input)), 'input');
108
+ if (archive.size > 67108864)
109
+ fail('limit', 'Type archive exceeds 64 MiB.');
110
+ delete p.input;
111
+ p.archive = archive;
112
+ }
113
+ if (entry.write)
114
+ return api.mutate(selected, operation, p, async () => ({}), o.requestId === undefined ? undefined : id(o.requestId), (backend, snapshot, response) => verifyMutation(backend, snapshot, operation, p, response));
115
+ const other = entry.pair ? { session: o.otherSession === undefined ? selected.session : id(o.otherSession), target: id(o.otherTarget) } : undefined;
116
+ const value = object(other ? await api.crossRead(selected, other, operation, p) : await api.read(selected, operation, p));
117
+ if (Buffer.byteLength(JSON.stringify(value)) > 1048576)
118
+ fail('limit', 'SDK result exceeds 1 MiB; narrow the operation.');
119
+ return { ...selected, ...(other ? { other } : {}), value };
120
+ }, { permission: entry.write ? 'Destructive' : Permission.ReadOnly, options: [...selectedOptions, ...entry.params.map(option),
121
+ ...(entry.pair ? [{ flags: '--other-target <id>', description: 'Second immutable target', required: true }, { flags: '--other-session <id>', description: 'Second session (default selected session)' }] : []),
122
+ ...(entry.write ? [{ flags: '--request-id <id>', description: 'Retrieve this changing intent without repeating it' }] : [])] }));
123
+ }
124
+ return [...groups].map(([name, children]) => command(name, name + ' native SDK workflows', children));
125
+ }
@@ -0,0 +1,46 @@
1
+ import type { CommandContext } from '@eyeauras/cli-factory';
2
+ import { type ChildProcess } from 'node:child_process';
3
+ import { type NativeInputs } from './native-inputs.js';
4
+ export interface ServerBinding {
5
+ mode: 'local' | 'external';
6
+ origin: string;
7
+ repository: string;
8
+ readerUser: string;
9
+ publisherUser: string;
10
+ readerSecret: string;
11
+ publisherSecret: string;
12
+ }
13
+ /** Own local startup; external profiles connect without taking ownership of their server. */
14
+ export declare class GhidraServer {
15
+ readonly directory: string;
16
+ private readonly inputs;
17
+ private readonly c;
18
+ private readonly state;
19
+ constructor(directory: string, inputs: NativeInputs, c: CommandContext, state?: {
20
+ initializing?: Promise<ServerBinding>;
21
+ child?: ChildProcess;
22
+ });
23
+ fork(c: CommandContext): GhidraServer;
24
+ private root;
25
+ private run;
26
+ credentials(binding: ServerBinding, role: 'reader' | 'publisher'): Promise<Record<string, string>>;
27
+ fields(snapshot: string, selectors: string): Promise<Record<string, unknown>>;
28
+ inspect(snapshot: string, request: string): Promise<Record<string, unknown>>;
29
+ sdkFile(operation: string, p: Record<string, unknown>): Promise<Record<string, unknown>>;
30
+ traceInspect(snapshot: string): Promise<Record<string, unknown>>;
31
+ project(operation: string, parameters: Record<string, unknown>): Promise<Record<string, unknown>>;
32
+ projectOutcome(requestId: string, expected: Record<string, unknown>): Promise<Record<string, unknown> | undefined>;
33
+ administration(operation: string, parameters: Record<string, unknown>): Promise<Record<string, unknown>>;
34
+ programStatus(binding: ServerBinding, path: string): Promise<Record<string, unknown>>;
35
+ initialImport(root: string): Promise<Record<string, unknown>>;
36
+ initialPublication(binding: ServerBinding, root: string, operation?: 'stage' | 'verify-stage' | 'promote' | 'verify'): Promise<Record<string, unknown>>;
37
+ publish(binding: ServerBinding, path: string, snapshot: string, expected: number, request: string, journal: string, sourceSha: string): Promise<Record<string, unknown>>;
38
+ recoverPublication(binding: ServerBinding, path: string, snapshot: string, expected: number, request: string, journal: string, sourceSha: string): Promise<Record<string, unknown>>;
39
+ private authority;
40
+ ensure(): Promise<ServerBinding>;
41
+ private initialize;
42
+ private identity;
43
+ stop(): Promise<void>;
44
+ }
45
+ /** Stop only the authenticated owned server recorded below this workspace/probe root. */
46
+ export declare function stopLocalServer(directory: string, c: CommandContext): Promise<void>;
@@ -0,0 +1,460 @@
1
+ import { randomUUID, randomBytes, createHash } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { readFile, mkdir, open, rename, readdir, stat, chmod, realpath } from 'node:fs/promises';
4
+ import { delimiter, dirname, join, resolve } from 'node:path';
5
+ import { createServer } from 'node:net';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { fail, object, stable, text, digest } from './model.js';
8
+ import { nativeHelpers, nativeJvmDirectories, nativeExtensionArguments } from './native-inputs.js';
9
+ import { parseDomainJson } from './json.js';
10
+ const live = (pid) => {
11
+ try {
12
+ process.kill(pid, 0);
13
+ return true;
14
+ }
15
+ catch (error) {
16
+ if (error.code === 'ESRCH')
17
+ return false;
18
+ throw error;
19
+ }
20
+ };
21
+ async function durable(path, value) {
22
+ const temporary = path + '.new-' + randomUUID(), handle = await open(temporary, 'wx', 0o600);
23
+ try {
24
+ await handle.writeFile(stable(value));
25
+ await handle.sync();
26
+ }
27
+ finally {
28
+ await handle.close();
29
+ }
30
+ await rename(temporary, path);
31
+ if (process.platform !== 'win32') {
32
+ const directory = await open(dirname(path), 'r');
33
+ try {
34
+ await directory.sync();
35
+ }
36
+ finally {
37
+ await directory.close();
38
+ }
39
+ }
40
+ }
41
+ async function ports(count) {
42
+ for (let attempt = 0; attempt < 100; attempt++) {
43
+ const base = 20000 + Math.floor(Math.random() * 30000), held = [];
44
+ try {
45
+ for (let index = 0; index < count; index++) {
46
+ const listener = createServer();
47
+ held.push(listener);
48
+ await new Promise((resolve, reject) => { listener.once('error', reject); listener.listen(base + index, '127.0.0.1', resolve); });
49
+ }
50
+ return base;
51
+ }
52
+ catch { /* Collision is retried before any resource or process is published. */ }
53
+ finally {
54
+ await Promise.all(held.filter(listener => listener.listening).map(listener => new Promise(resolve => listener.close(() => resolve()))));
55
+ }
56
+ }
57
+ fail('launch', 'Could not reserve loopback ports for the owned server.');
58
+ }
59
+ /** Own local startup; external profiles connect without taking ownership of their server. */
60
+ export class GhidraServer {
61
+ directory;
62
+ inputs;
63
+ c;
64
+ state;
65
+ constructor(directory, inputs, c, state = {}) {
66
+ this.directory = directory;
67
+ this.inputs = inputs;
68
+ this.c = c;
69
+ this.state = state;
70
+ }
71
+ fork(c) { return new GhidraServer(this.directory, this.inputs, c, this.state); }
72
+ root() { return join(this.directory, 'server'); }
73
+ async run(label, main, args, environment = {}, input, acceptedCodes = [0], timeoutMilliseconds = 120000) {
74
+ const root = join(this.root(), 'calls', label + '-' + randomUUID());
75
+ await mkdir(root, { recursive: true, mode: 0o700 });
76
+ const classes = await nativeHelpers(this.directory, this.inputs);
77
+ const log = await open(join(root, 'diagnostic.log'), 'a', 0o600);
78
+ const child = spawn(this.inputs.java, ['-Djava.awt.headless=true', '-Dghidra.home=' + this.inputs.ghidraHome,
79
+ ...nativeJvmDirectories(root), ...nativeExtensionArguments(this.inputs), '-cp', classes + delimiter + this.inputs.classpath, main, ...args], { cwd: root, windowsHide: true, env: { PATH: this.c.environment.PATH ?? process.env.PATH, HOME: root, TMPDIR: root, ...environment }, stdio: ['pipe', 'pipe', log.fd] });
80
+ const exited = new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', resolve); });
81
+ exited.catch(() => { });
82
+ await log.close();
83
+ let output = '', overflow = false;
84
+ child.stdout.on('data', chunk => { if (!overflow)
85
+ output += chunk.toString(); if (Buffer.byteLength(output) > 1024 * 1024) {
86
+ overflow = true;
87
+ child.kill('SIGTERM');
88
+ } });
89
+ const abort = () => child.kill('SIGTERM');
90
+ this.c.signal.addEventListener('abort', abort, { once: true });
91
+ const timer = setTimeout(abort, timeoutMilliseconds);
92
+ const force = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
93
+ child.kill('SIGKILL'); }, timeoutMilliseconds + 5000);
94
+ try {
95
+ child.stdin.on('error', () => { });
96
+ child.stdin.end(input);
97
+ if (this.c.signal.aborted)
98
+ abort();
99
+ const code = await exited;
100
+ if (code === null || !acceptedCodes.includes(code) || overflow)
101
+ fail('authorization', 'Native server operation could not be verified; its owned diagnostic stage is retained.');
102
+ let result;
103
+ try {
104
+ result = object(parseDomainJson(output));
105
+ }
106
+ catch {
107
+ fail('malformed', 'Native server operation returned malformed content.');
108
+ }
109
+ if (result.error || result.state === 'failed')
110
+ fail('authorization', 'Native server operation did not establish its outcome.');
111
+ return result;
112
+ }
113
+ finally {
114
+ clearTimeout(timer);
115
+ clearTimeout(force);
116
+ this.c.signal.removeEventListener('abort', abort);
117
+ }
118
+ }
119
+ async credentials(binding, role) {
120
+ const url = new URL(binding.origin);
121
+ return { GHIDRA_SERVER_HOST: url.hostname, GHIDRA_SERVER_PORT: url.port || '13100',
122
+ GHIDRA_SERVER_USER: role === 'reader' ? binding.readerUser : binding.publisherUser,
123
+ GHIDRA_SERVER_PASSWORD: await this.c.secrets.require(role === 'reader' ? binding.readerSecret : binding.publisherSecret) };
124
+ }
125
+ async fields(snapshot, selectors) {
126
+ return this.run('snapshot-fields', 'InsightSnapshotFields', [snapshot, selectors]);
127
+ }
128
+ async inspect(snapshot, request) { return this.run('snapshot-inspect', 'InsightSnapshotInspect', [snapshot, request]); }
129
+ async sdkFile(operation, p) {
130
+ const root = join(this.root(), 'sdk-files', randomUUID());
131
+ await mkdir(root, { recursive: true, mode: 0o700 });
132
+ const tracking = operation.startsWith('vt.');
133
+ const request = join(root, 'request.json');
134
+ await durable(request, { ...p, operation: tracking ? operation.slice(3) : operation });
135
+ const main = tracking ? 'InsightVersionTracking' : /^(fid|bsim)-(build|query)$/.test(operation) ? 'InsightFunctionDatabases' : 'InsightSdkFiles';
136
+ const result = await this.run('sdk-file', main, [request]);
137
+ return object(result.value);
138
+ }
139
+ async traceInspect(snapshot) { return this.run('trace-inspect', 'InsightTraceInspect', [snapshot]); }
140
+ async project(operation, parameters) {
141
+ const requestId = text(parameters.request_id, 128);
142
+ if (!/^(?:a|read)_[a-zA-Z0-9-]+$/.test(requestId))
143
+ fail('binding', 'Invalid internal project request identity.');
144
+ const parent = join(this.directory, 'project-calls');
145
+ await mkdir(parent, { recursive: true, mode: 0o700 });
146
+ if (await realpath(parent) !== resolve(parent))
147
+ fail('binding', 'Project call container moved outside owned storage.');
148
+ const root = join(parent, requestId);
149
+ await mkdir(root, { mode: 0o700 });
150
+ const request = join(root, 'request.json');
151
+ await durable(request, { ...parameters, operation });
152
+ return this.run('project-files', 'InsightProjectFiles', [request]);
153
+ }
154
+ async projectOutcome(requestId, expected) {
155
+ if (!/^a_[a-zA-Z0-9-]+$/.test(requestId))
156
+ fail('binding', 'Invalid retained project request identity.');
157
+ const root = join(this.directory, 'project-calls', requestId);
158
+ try {
159
+ if (await realpath(root) !== resolve(root))
160
+ fail('binding', 'Retained project call moved outside owned storage.');
161
+ const raw = await readFile(join(root, 'request.json'), 'utf8'), request = object(parseDomainJson(raw)), journal = object(parseDomainJson(await readFile(join(root, 'result.json'), 'utf8')));
162
+ if (journal.schema !== 1 || journal.request !== requestId || journal.hash !== createHash('sha256').update(raw).digest('hex') ||
163
+ Object.entries(expected).some(([key, value]) => stable(request[key]) !== stable(value)))
164
+ fail('binding', 'Project completion journal differs from the retained request.');
165
+ return object(journal.result);
166
+ }
167
+ catch (error) {
168
+ if (error.code === 'ENOENT')
169
+ return;
170
+ throw error;
171
+ }
172
+ }
173
+ async administration(operation, parameters) {
174
+ const connection = join(this.root(), 'connection.json');
175
+ if (operation === 'status') {
176
+ try {
177
+ if (object(JSON.parse(await readFile(connection, 'utf8'))).enabled === false)
178
+ return { connected: false, mode: this.c.profile.values.server ? 'external' : 'local', state: 'disconnected' };
179
+ }
180
+ catch (error) {
181
+ if (error.code !== 'ENOENT')
182
+ throw error;
183
+ }
184
+ }
185
+ if (operation === 'connect') {
186
+ await mkdir(this.root(), { recursive: true, mode: 0o700 });
187
+ await durable(connection, { enabled: true });
188
+ }
189
+ if (operation === 'stop') {
190
+ await this.stop();
191
+ return { connected: false, mode: this.c.profile.values.server ? 'external' : 'local', state: 'stopped' };
192
+ }
193
+ if (operation === 'disconnect') {
194
+ await this.stop();
195
+ await mkdir(this.root(), { recursive: true, mode: 0o700 });
196
+ await durable(connection, { enabled: false });
197
+ return { connected: false, mode: this.c.profile.values.server ? 'external' : 'local' };
198
+ }
199
+ if (operation === 'status' && !this.c.profile.values.server) {
200
+ let local;
201
+ try {
202
+ local = JSON.parse(await readFile(join(this.root(), 'local.json'), 'utf8'));
203
+ }
204
+ catch (error) {
205
+ if (error.code === 'ENOENT')
206
+ return { connected: false, mode: 'local', state: 'not_started' };
207
+ throw error;
208
+ }
209
+ if (!local.pid || !live(local.pid))
210
+ return { connected: false, mode: 'local', repository: local.repository };
211
+ await this.identity(local);
212
+ await this.authority(local);
213
+ return { connected: true, mode: 'local', repository: local.repository };
214
+ }
215
+ const binding = await this.ensure();
216
+ if (operation === 'connect' || operation === 'status')
217
+ return { connected: true, mode: binding.mode, repository: binding.repository };
218
+ if (operation === 'set_permissions' && parameters.repository === binding.repository &&
219
+ (parameters.user === binding.readerUser && parameters.access !== 0 || parameters.user === binding.publisherUser && ![1, 2].includes(Number(parameters.access))))
220
+ fail('authorization', 'Canonical reader/publisher authority cannot be changed by repository administration.');
221
+ const root = join(this.root(), 'administration', randomUUID());
222
+ await mkdir(root, { recursive: true, mode: 0o700 });
223
+ const request = join(root, 'request.json');
224
+ await durable(request, { operation, ...parameters, protected_repository: binding.repository });
225
+ return this.run('administration', 'InsightServerAdministration', [request], await this.credentials(binding, 'publisher'));
226
+ }
227
+ async programStatus(binding, path) {
228
+ return this.run('program-status', 'InsightRepositoryProbe', [binding.repository, path, 'reader'], await this.credentials(binding, 'reader'));
229
+ }
230
+ async initialImport(root) { return this.run('initial-import', 'InsightInitialImport', [root], {}, undefined, [0], 600000); }
231
+ async initialPublication(binding, root, operation = 'stage') {
232
+ return this.run('initial-publication', 'db.InsightInitialPublication', [root, operation], await this.credentials(binding, operation.startsWith('verify') ? 'reader' : 'publisher'));
233
+ }
234
+ async publish(binding, path, snapshot, expected, request, journal, sourceSha) {
235
+ return this.run('publish', 'db.InsightSnapshotPublisher', [binding.repository, path, snapshot, String(expected), 'insight merge ' + request, journal, sourceSha], await this.credentials(binding, 'publisher'), undefined, [0, 2]);
236
+ }
237
+ async recoverPublication(binding, path, snapshot, expected, request, journal, sourceSha) {
238
+ return this.run('recover-publication', 'db.InsightPublicationRecovery', [binding.repository, path, snapshot, String(expected), 'insight merge ' + request, journal, sourceSha], { ...await this.credentials(binding, 'reader'), INSIGHT_PUBLISHER_USER: binding.publisherUser,
239
+ INSIGHT_PUBLISHER_PASSWORD: await this.c.secrets.require(binding.publisherSecret) });
240
+ }
241
+ async authority(binding) {
242
+ for (const role of ['reader', 'publisher']) {
243
+ const result = await this.run('verify-' + role, 'InsightServerProbe', [binding.repository, role], await this.credentials(binding, role));
244
+ if (result.repository !== binding.repository || result.role !== role || (role === 'reader' ? result.permission !== 0 : ![1, 2].includes(Number(result.permission))))
245
+ fail('authorization', 'Actual repository permissions do not match the selected server profile.');
246
+ }
247
+ }
248
+ async ensure() {
249
+ try {
250
+ if (object(JSON.parse(await readFile(join(this.root(), 'connection.json'), 'utf8'))).enabled === false)
251
+ fail('blocked', 'Configured server is disconnected; use ghidra server connect before shared operations.');
252
+ }
253
+ catch (error) {
254
+ if (error.code !== 'ENOENT')
255
+ throw error;
256
+ }
257
+ if (!this.state.initializing)
258
+ this.state.initializing = this.initialize().catch(error => { this.state.initializing = undefined; throw error; });
259
+ const binding = await this.state.initializing;
260
+ if (binding.mode === 'local') {
261
+ const local = binding;
262
+ if (local.pid && !live(local.pid)) {
263
+ this.state.initializing = undefined;
264
+ return this.ensure();
265
+ }
266
+ await this.identity(local);
267
+ }
268
+ return binding;
269
+ }
270
+ async initialize() {
271
+ const root = this.root();
272
+ await mkdir(root, { recursive: true, mode: 0o700 });
273
+ const values = this.c.profile.values;
274
+ const mode = values.server ? 'external' : 'local';
275
+ const effective = mode === 'external' ? { mode, origin: text(values.server), repository: text(values.repository),
276
+ readerUser: text(values.readerUser), publisherUser: text(values.publisherUser) } : { mode };
277
+ if (mode === 'external' && (!/^ghidra:\/\/[^/?#@]+$/.test(text(values.server)) || /[\\/]/.test(text(values.repository)) ||
278
+ text(values.repository).startsWith('.') || values.readerUser === values.publisherUser))
279
+ fail('configuration', 'External server requires an origin, repository and distinct reader/publisher accounts.');
280
+ const configuration = digest(effective), configured = join(root, 'configuration.json');
281
+ try {
282
+ if (object(JSON.parse(await readFile(configured, 'utf8'))).hash !== configuration)
283
+ fail('binding', 'Server profile changed while its durable workspace remains; restore the original binding.');
284
+ }
285
+ catch (error) {
286
+ if (error.code !== 'ENOENT')
287
+ throw error;
288
+ await durable(configured, { hash: configuration });
289
+ }
290
+ if (mode === 'external') {
291
+ const origin = text(values.server), repository = text(values.repository), readerUser = text(values.readerUser), publisherUser = text(values.publisherUser);
292
+ if (!/^ghidra:\/\/[^/?#@]+$/.test(origin) || new URL(origin).protocol !== 'ghidra:' || /[\\/]/.test(repository) || repository.startsWith('.') || readerUser === publisherUser)
293
+ fail('configuration', 'External server requires an origin, repository and distinct reader/publisher accounts.');
294
+ const binding = { mode, origin, repository, readerUser, publisherUser, readerSecret: 'readerPassword', publisherSecret: 'publisherPassword' };
295
+ await this.authority(binding);
296
+ return binding;
297
+ }
298
+ const saved = join(root, 'local.json'), data = join(root, 'data');
299
+ let local;
300
+ try {
301
+ local = JSON.parse(await readFile(saved, 'utf8'));
302
+ }
303
+ catch (error) {
304
+ if (error.code !== 'ENOENT')
305
+ throw error;
306
+ await mkdir(data, { recursive: true, mode: 0o700 });
307
+ if ((await readdir(data)).length)
308
+ fail('binding', 'Unidentified server storage exists; it cannot be adopted.');
309
+ const instance = 'server_' + randomUUID(), base = await ports(4);
310
+ local = { mode: 'local', instance, port: base, identityPort: base + 3, origin: `ghidra://127.0.0.1:${base}`,
311
+ repository: 'insight_' + randomUUID().replaceAll('-', ''), readerUser: 'reader', publisherUser: 'publisher',
312
+ readerSecret: instance + '-readerPassword', publisherSecret: instance + '-publisherPassword',
313
+ keySecret: instance + '-keystorePassword', tokenSecret: instance + '-identityToken', phase: 'seeded' };
314
+ await durable(saved, local);
315
+ }
316
+ if (local.mode !== 'local' || !/^server_[a-f0-9-]{36}$/.test(local.instance) || local.origin !== `ghidra://127.0.0.1:${local.port}` ||
317
+ local.identityPort !== local.port + 3 || !local.repository.startsWith('insight_') || local.readerUser !== 'reader' || local.publisherUser !== 'publisher')
318
+ fail('binding', 'Owned local server receipt is inconsistent.');
319
+ for (const key of [local.readerSecret, local.publisherSecret, local.keySecret, local.tokenSecret]) {
320
+ if (!key.startsWith(local.instance + '-'))
321
+ fail('binding', 'Owned local server secret reference is inconsistent.');
322
+ if (!await this.c.secrets.get(key)) {
323
+ if (local.phase !== 'seeded')
324
+ fail('credentials', 'Owned local server credentials are missing; retain its storage and restore its credential store.');
325
+ await this.c.secrets.set(key, randomBytes(32).toString('hex'));
326
+ }
327
+ }
328
+ if (local.pid && live(local.pid)) {
329
+ await this.identity(local);
330
+ await this.authority(local);
331
+ return local;
332
+ }
333
+ // A prior owner may have died after spawn and before recording its PID.
334
+ if (!local.pid && await this.identity(local).then(() => true, () => false)) {
335
+ const identity = await this.identity(local);
336
+ local.pid = Number(identity.pid);
337
+ local.phase = 'running';
338
+ await durable(saved, local);
339
+ await this.authority(local);
340
+ return local;
341
+ }
342
+ const initialized = await this.run('initialize', 'ghidra.server.InsightServerInit', [data, local.repository, local.publisherUser, local.readerUser], {
343
+ INSIGHT_PUBLISHER_PASSWORD: await this.c.secrets.require(local.publisherSecret), INSIGHT_READER_PASSWORD: await this.c.secrets.require(local.readerSecret)
344
+ });
345
+ if (initialized.state !== 'ready' || initialized.repository !== local.repository || initialized.readerReadOnly !== true || initialized.publisherAdmin !== true)
346
+ fail('authorization', 'Owned local server initialization did not establish its permission boundary.');
347
+ local.phase = 'initialized';
348
+ delete local.pid;
349
+ await durable(saved, local);
350
+ const keystore = join(root, 'owned-cert.p12');
351
+ try {
352
+ await stat(keystore);
353
+ }
354
+ catch (error) {
355
+ if (error.code !== 'ENOENT')
356
+ throw error;
357
+ // keytool gets the random password through its documented environment input, never argv.
358
+ await this.run('certificate', 'InsightKeytool', [keystore], { INSIGHT_KEYSTORE_PASSWORD: await this.c.secrets.require(local.keySecret) });
359
+ await chmod(keystore, 0o600);
360
+ }
361
+ const classes = await nativeHelpers(this.directory, this.inputs);
362
+ const processRoot = join(root, 'processes', randomUUID());
363
+ await mkdir(processRoot, { recursive: true, mode: 0o700 });
364
+ const log = await open(join(processRoot, 'runtime.log'), 'a', 0o600);
365
+ const child = spawn(this.inputs.java, ['-Djava.awt.headless=true', '-Dghidra.home=' + this.inputs.ghidraHome,
366
+ ...nativeJvmDirectories(processRoot), '-cp', classes + delimiter + (this.inputs.serverClasspath ?? this.inputs.classpath),
367
+ 'InsightServerMain', data, String(local.port), String(local.identityPort), local.instance, keystore], { cwd: processRoot, detached: true, windowsHide: true, env: { PATH: this.c.environment.PATH ?? process.env.PATH, HOME: processRoot, TMPDIR: processRoot,
368
+ INSIGHT_KEYSTORE_PASSWORD: await this.c.secrets.require(local.keySecret), INSIGHT_SERVER_TOKEN: await this.c.secrets.require(local.tokenSecret) }, stdio: ['ignore', log.fd, log.fd] });
369
+ child.on('error', () => { });
370
+ child.unref();
371
+ await log.close();
372
+ if (!child.pid)
373
+ fail('launch', 'Owned local Ghidra server could not be started.');
374
+ this.state.child = child;
375
+ local.pid = child.pid;
376
+ local.phase = 'running';
377
+ await durable(saved, local);
378
+ try {
379
+ const deadline = Date.now() + 60000;
380
+ while (Date.now() < deadline && child.exitCode === null && child.signalCode === null && !this.c.signal.aborted) {
381
+ try {
382
+ await this.identity(local);
383
+ await this.authority(local);
384
+ return local;
385
+ }
386
+ catch {
387
+ await delay(100);
388
+ }
389
+ }
390
+ fail('launch', 'Owned local Ghidra server did not become ready; its storage and diagnostic stage are retained.');
391
+ }
392
+ catch (error) {
393
+ // Only this newly-created child handle can be cleaned without a ready identity probe.
394
+ if (child.exitCode === null && child.signalCode === null)
395
+ child.kill('SIGTERM');
396
+ for (let i = 0; i < 100 && child.exitCode === null && child.signalCode === null; i++)
397
+ await delay(50);
398
+ throw error;
399
+ }
400
+ }
401
+ identity(local) { return serverIdentity(local, this.c); }
402
+ async stop() { await stopLocalServer(this.directory, this.c); this.state.child = undefined; this.state.initializing = undefined; }
403
+ }
404
+ async function serverIdentity(local, c) {
405
+ const token = await c.secrets.require(local.tokenSecret);
406
+ const response = await c.fetch(`http://127.0.0.1:${local.identityPort}/identity`, { redirect: 'error',
407
+ signal: AbortSignal.timeout(1000), headers: { Authorization: 'Bearer ' + token } });
408
+ if (!response.ok || Number(response.headers.get('content-length')) > 4096)
409
+ fail('binding', 'Owned server identity was not verified.');
410
+ const reader = response.body?.getReader();
411
+ if (!reader)
412
+ fail('binding', 'Owned server identity has no body.');
413
+ const chunks = [];
414
+ let size = 0;
415
+ try {
416
+ for (;;) {
417
+ const next = await reader.read();
418
+ if (next.done)
419
+ break;
420
+ size += next.value.byteLength;
421
+ if (size > 4096)
422
+ fail('binding', 'Owned server identity exceeded its bound.');
423
+ chunks.push(next.value);
424
+ }
425
+ }
426
+ finally {
427
+ await reader.cancel().catch(() => { });
428
+ }
429
+ const value = Buffer.concat(chunks).toString('utf8');
430
+ const result = object(JSON.parse(value));
431
+ if (result.instance !== local.instance || result.port !== local.port || !Number.isSafeInteger(result.pid) || Number(result.pid) < 1 ||
432
+ local.pid !== undefined && result.pid !== local.pid)
433
+ fail('binding', 'Server endpoint no longer belongs to the recorded owned process.');
434
+ return result;
435
+ }
436
+ /** Stop only the authenticated owned server recorded below this workspace/probe root. */
437
+ export async function stopLocalServer(directory, c) {
438
+ let local;
439
+ try {
440
+ local = JSON.parse(await readFile(join(directory, 'server', 'local.json'), 'utf8'));
441
+ }
442
+ catch (error) {
443
+ if (error.code === 'ENOENT')
444
+ return;
445
+ throw error;
446
+ }
447
+ if (!local.pid)
448
+ return;
449
+ if (live(local.pid)) {
450
+ await serverIdentity(local, c);
451
+ process.kill(local.pid, 'SIGTERM');
452
+ for (let i = 0; i < 100 && live(local.pid); i++)
453
+ await delay(50);
454
+ if (live(local.pid))
455
+ fail('busy', 'Owned server did not stop; its receipt is retained.');
456
+ }
457
+ local.phase = 'stopped';
458
+ delete local.pid;
459
+ await durable(join(directory, 'server', 'local.json'), local);
460
+ }