@eyeauras/insight-cli 0.1.0 → 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.
- package/.cli-package-build.json +1 -1
- package/README.md +8 -2
- package/SKILL.md +4 -1
- package/dist/src/administration-results.js +1 -1
- package/dist/src/administration.d.ts +1 -1
- package/dist/src/administration.js +16 -2
- package/dist/src/cli.js +37 -5
- package/dist/src/contexts.js +12 -2
- package/dist/src/lifecycle.d.ts +1 -1
- package/dist/src/lifecycle.js +8 -2
- package/dist/src/native-inputs.js +12 -1
- package/dist/src/runtime-archives.d.ts +5 -0
- package/dist/src/runtime-archives.js +127 -0
- package/dist/src/runtime-catalog.d.ts +18 -0
- package/dist/src/runtime-catalog.js +27 -0
- package/dist/src/runtime-manager.d.ts +101 -0
- package/dist/src/runtime-manager.js +444 -0
- package/dist/src/runtime-selection.d.ts +13 -0
- package/dist/src/runtime-selection.js +25 -0
- package/dist/src/server.d.ts +2 -0
- package/dist/src/server.js +59 -54
- package/package.json +4 -2
- package/references/administration.md +2 -1
- package/references/managed-runtime.md +116 -0
- package/references/native/InsightRuntimeDoctor.java +33 -0
- package/references/setup.md +4 -2
- package/skills/insight-re/SKILL.md +1 -1
package/.cli-package-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":1,"artifact":"
|
|
1
|
+
{"version":1,"artifact":"e6afb01edf053fe9b2087f5cc2ca38dd13cb0587a804ebbb1d39c4e5f433c6a3"}
|
package/README.md
CHANGED
|
@@ -17,14 +17,17 @@ The executable is `insight-cli`. Its global npm executable directory must be on
|
|
|
17
17
|
No source checkout, TypeScript build or separate Factory installation is required.
|
|
18
18
|
The pinned Factory IPC runtime is a normal npm dependency fetched from an immutable
|
|
19
19
|
GitHub tarball URL; installation therefore needs npm registry and GitHub access.
|
|
20
|
-
|
|
20
|
+
Use `insight-cli ghidra install` to deliver the pinned Ghidra/JDK/native/server runtime.
|
|
21
|
+
External Ghidra launchers and Java paths are also supported.
|
|
21
22
|
Help, skill discovery and saving ordinary files do not need Ghidra or Java.
|
|
22
23
|
|
|
23
24
|
## Start
|
|
24
25
|
|
|
25
26
|
```sh
|
|
26
|
-
insight-cli profile create local
|
|
27
|
+
insight-cli profile create local
|
|
27
28
|
insight-cli --profile local permissions grant Update
|
|
29
|
+
insight-cli --profile local ghidra install
|
|
30
|
+
insight-cli --profile local ghidra status
|
|
28
31
|
insight-cli --profile local ghidra open ./client.exe --session new --json
|
|
29
32
|
```
|
|
30
33
|
|
|
@@ -53,3 +56,6 @@ and `insight-cli skill insight-re` to locate packaged instructions and reference
|
|
|
53
56
|
|
|
54
57
|
Full documentation and verification: [GitHub](https://github.com/iXab3r/insight-cli).
|
|
55
58
|
License: MIT. The bundled GhidraMCP bridge has its own Apache-2.0 license and notice.
|
|
59
|
+
|
|
60
|
+
Runtime management, local archives and external paths: [managed runtime](references/managed-runtime.md).
|
|
61
|
+
The host debugger and OS permissions are checked separately by `ghidra doctor`.
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,10 @@ name: insight-cli
|
|
|
3
3
|
description: Research several executable inputs through durable independent sessions and isolated Ghidra contexts using the matching insight-cli facade. Agents supply the investigation and interpret results.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# insight-cli 0.
|
|
6
|
+
# insight-cli 0.2.0
|
|
7
|
+
|
|
8
|
+
Use `ghidra status` to inspect runtime readiness; `ghidra install` delivers the pinned runtime.
|
|
9
|
+
For archives, external launchers, server lifecycle or removal, read [managed runtime](references/managed-runtime.md).
|
|
7
10
|
|
|
8
11
|
This skill and its executable helper are one npm package. Run `insight-cli skill` to locate
|
|
9
12
|
these installed resources. Use generated `--help` for syntax; read the references as needed.
|
|
@@ -12,7 +12,7 @@ export function administrationResult(operation, p, raw) {
|
|
|
12
12
|
const value = object(raw);
|
|
13
13
|
if (JSON.stringify(value).length > 1048576)
|
|
14
14
|
fail('limit', 'Administration result exceeds one MiB.');
|
|
15
|
-
if (['status', 'connect', 'disconnect'].includes(operation)) {
|
|
15
|
+
if (['status', 'connect', 'disconnect', 'stop'].includes(operation)) {
|
|
16
16
|
if (typeof value.connected !== 'boolean' || !['local', 'external'].includes(String(value.mode)))
|
|
17
17
|
fail('partial', 'Server status is incomplete.');
|
|
18
18
|
if (value.connected && typeof value.repository !== 'string')
|
|
@@ -11,4 +11,4 @@ export declare class Administration {
|
|
|
11
11
|
read(operation: string, parameters: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
12
12
|
change(operation: string, parameters: Record<string, unknown>, callerId?: string): Promise<unknown>;
|
|
13
13
|
}
|
|
14
|
-
export declare function administrationCommands(service: (context: CommandContext) => Administration): CommandDefinition[];
|
|
14
|
+
export declare function administrationCommands(service: (context: CommandContext) => Administration, unconfiguredStatus?: (context: CommandContext) => Promise<Record<string, unknown> | undefined>): CommandDefinition[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
2
|
import { fail, id, text } from './model.js';
|
|
3
3
|
import { option, parsed, key } from './arguments.js';
|
|
4
|
+
import { runtimeProcesses } from './runtime-manager.js';
|
|
4
5
|
import { administrationResult } from './administration-results.js';
|
|
5
6
|
const repository = (value) => { const name = text(value, 128); if (name !== name.trim() || /[\\/\0]/.test(name) || name.startsWith('.'))
|
|
6
7
|
fail('invalid', 'Repository name must be a single bounded name.'); return name; };
|
|
@@ -67,6 +68,8 @@ export class Administration {
|
|
|
67
68
|
const repo = { name: 'repository', type: 'string', required: true }, path = { name: 'path', type: 'string', required: false, default: '/' };
|
|
68
69
|
const leaves = [
|
|
69
70
|
{ names: ['status'], operation: 'status', gate: Permission.ReadOnly, params: [] },
|
|
71
|
+
{ names: ['start'], operation: 'connect', gate: Permission.Update, params: [] },
|
|
72
|
+
{ names: ['stop'], operation: 'stop', gate: Permission.Update, params: [] },
|
|
70
73
|
{ names: ['connect'], operation: 'connect', gate: Permission.Update, params: [] },
|
|
71
74
|
{ names: ['disconnect'], operation: 'disconnect', gate: Permission.Update, params: [] },
|
|
72
75
|
{ names: ['repositories', 'list'], operation: 'repositories', gate: Permission.ReadOnly, params: [] },
|
|
@@ -81,11 +84,22 @@ const leaves = [
|
|
|
81
84
|
{ names: ['admin', 'terminate-checkout'], operation: 'terminate_checkout', gate: 'Destructive', params: [repo, { ...path, required: true, default: undefined }, { name: 'checkout_id', type: 'string', required: false }] },
|
|
82
85
|
{ names: ['admin', 'terminate-all-checkouts'], operation: 'terminate_all_checkouts', gate: 'Destructive', params: [repo, path] },
|
|
83
86
|
];
|
|
84
|
-
export function administrationCommands(service) {
|
|
87
|
+
export function administrationCommands(service, unconfiguredStatus) {
|
|
85
88
|
const children = [];
|
|
86
89
|
for (const leaf of leaves) {
|
|
87
90
|
const changing = leaf.gate !== Permission.ReadOnly;
|
|
88
|
-
const definition = command(leaf.names.at(-1), 'Operate on the configured server and explicit repository', ({ options: o }, c) => {
|
|
91
|
+
const definition = command(leaf.names.at(-1), 'Operate on the configured server and explicit repository', async ({ options: o }, c) => {
|
|
92
|
+
if (leaf.operation === 'stop') {
|
|
93
|
+
if (c.profile.values.server)
|
|
94
|
+
fail('authorization', 'External server lifecycle is owned by its operator. Use server disconnect.');
|
|
95
|
+
if ((await runtimeProcesses(c.appArguments.AppDataDirectory)).some(p => p.kind !== 'server'))
|
|
96
|
+
fail('busy', 'Close active Ghidra contexts before stopping their server.');
|
|
97
|
+
}
|
|
98
|
+
if (leaf.operation === 'status' && unconfiguredStatus) {
|
|
99
|
+
const passive = await unconfiguredStatus(c);
|
|
100
|
+
if (passive)
|
|
101
|
+
return passive;
|
|
102
|
+
}
|
|
89
103
|
const parameters = {};
|
|
90
104
|
for (const p of leaf.params) {
|
|
91
105
|
const value = o[key(p.name)];
|
package/dist/src/cli.js
CHANGED
|
@@ -27,16 +27,46 @@ import { VersionTracking, trackingCommands } from './version-tracking.js';
|
|
|
27
27
|
import { Extensions, extensionCommands, extensionState, extensionInventoryCommands, managedExtensions } from './extensions.js';
|
|
28
28
|
import { Synchronization, synchronizationCommands } from './sync-policy.js';
|
|
29
29
|
import { artifactsCommands } from './saved-materials.js';
|
|
30
|
+
import { RuntimeManager, runtimeCommands, runtimeProcesses } from './runtime-manager.js';
|
|
31
|
+
import { runtimeSelection } from './runtime-selection.js';
|
|
30
32
|
const bindingFields = ['java', 'ghidraHome', 'mcpJar', 'mcpJarSha256', 'server', 'repository', 'readerUser', 'publisherUser', 'maxContexts', 'debuggerPython', 'windbgDir', 'archiveUrl'];
|
|
31
33
|
class Owner {
|
|
32
34
|
options;
|
|
33
35
|
profiles = new Map();
|
|
36
|
+
runtimeChanging = new Set();
|
|
37
|
+
async runtime(c, action, quiescent = true) {
|
|
38
|
+
const directory = c.appArguments.AppDataDirectory;
|
|
39
|
+
if (this.runtimeChanging.has(directory))
|
|
40
|
+
fail('busy', 'Another runtime operation is in progress.');
|
|
41
|
+
const current = this.profiles.get(directory);
|
|
42
|
+
if (quiescent && current?.tasks.size)
|
|
43
|
+
fail('busy', 'Research operations are in progress; finish them before managing the runtime.');
|
|
44
|
+
this.runtimeChanging.add(directory);
|
|
45
|
+
try {
|
|
46
|
+
if (!quiescent)
|
|
47
|
+
return await action();
|
|
48
|
+
const processes = await runtimeProcesses(directory);
|
|
49
|
+
if (processes.length)
|
|
50
|
+
fail('busy', 'Close Ghidra contexts and stop the owned server before changing or checking the runtime.', { processes });
|
|
51
|
+
if (current) {
|
|
52
|
+
await (await current.backend)?.dispose();
|
|
53
|
+
current.workspace.close();
|
|
54
|
+
this.profiles.delete(directory);
|
|
55
|
+
}
|
|
56
|
+
return await action();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
this.runtimeChanging.delete(directory);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
34
62
|
constructor(options) {
|
|
35
63
|
this.options = options;
|
|
36
64
|
}
|
|
37
65
|
selected(c) {
|
|
38
66
|
const directory = c.appArguments.AppDataDirectory;
|
|
39
|
-
|
|
67
|
+
if (this.runtimeChanging.has(directory))
|
|
68
|
+
fail('busy', 'Runtime preparation is in progress.');
|
|
69
|
+
const configuration = digest({ runtime: runtimeSelection(directory), profile: Object.fromEntries(bindingFields.map(key => [key, c.profile.values[key]])), extensions: extensionState(join(directory, 'workspace')).modules.filter(m => m.enabled).map(m => [m.name, m.sha256]) });
|
|
40
70
|
let profile = this.profiles.get(directory);
|
|
41
71
|
if (!profile) {
|
|
42
72
|
profile = { workspace: new Workspace(join(directory, 'workspace'), c.profile.name), tasks: new Map(), configuration };
|
|
@@ -64,6 +94,8 @@ class Owner {
|
|
|
64
94
|
}
|
|
65
95
|
async invalidateProfile(app) {
|
|
66
96
|
const directory = app.AppDataDirectory, profile = this.profiles.get(directory);
|
|
97
|
+
if (this.runtimeChanging.has(directory))
|
|
98
|
+
fail('busy', 'Runtime preparation is in progress; finish it before changing or deleting the profile.');
|
|
67
99
|
const materials = await readdir(join(directory, 'materials')).catch(error => { if (error.code === 'ENOENT')
|
|
68
100
|
return []; throw error; });
|
|
69
101
|
if (existsSync(join(directory, 'workspace', 'workspace.sqlite')) || materials.length)
|
|
@@ -190,7 +222,7 @@ function scopedFailures(entry, owner, parents = []) {
|
|
|
190
222
|
export function createDefinition(options = {}) {
|
|
191
223
|
const owner = new Owner(options);
|
|
192
224
|
const definition = {
|
|
193
|
-
name: 'insight-cli', applicationId: 'insight-cli', version: '0.
|
|
225
|
+
name: 'insight-cli', applicationId: 'insight-cli', version: '0.2.0', description: 'Isolated research contexts and coordinated Ghidra analysis',
|
|
194
226
|
resources: [owner], permissions: { categories: [{ name: 'Destructive', description: 'Explicit deletion or replacement of retained research data', enabledByDefault: false }] }, ...(options.runtime ? { runtime: options.runtime } : {}),
|
|
195
227
|
profile: { fields: fields.map(([name, flags, description]) => ({ name: name, flags: flags, description: description,
|
|
196
228
|
...(name === 'maxContexts' ? { normalize: (value) => number(value, 16, 1) } : {}) })) },
|
|
@@ -249,7 +281,7 @@ export function createDefinition(options = {}) {
|
|
|
249
281
|
const name = args.name === undefined ? 'insight-cli' : text(args.name);
|
|
250
282
|
if (!['insight-cli', 'insight-re'].includes(name))
|
|
251
283
|
fail('notFound', 'Unknown skill; choose insight-cli or insight-re.');
|
|
252
|
-
return { version: '0.
|
|
284
|
+
return { version: '0.2.0', name, path: fileURLToPath(new URL(name === 'insight-re' ? '../../skills/insight-re/SKILL.md' : '../../SKILL.md', import.meta.url)),
|
|
253
285
|
skills: ['insight-cli', 'insight-re'] };
|
|
254
286
|
})],
|
|
255
287
|
commands: [
|
|
@@ -381,7 +413,7 @@ export function createDefinition(options = {}) {
|
|
|
381
413
|
],
|
|
382
414
|
};
|
|
383
415
|
return { ...definition, commands: definition.commands?.map(entry => entry.name === 'ghidra' ?
|
|
384
|
-
scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...sdkCommands(c => owner.selected(c).research), ...sdkFileCommands(c => owner.selected(c).sdkFiles), ...trackingCommands(c => owner.selected(c).tracking), ...crossCommands(c => owner.selected(c).research), ...administrationCommands(c => owner.selected(c).administration),
|
|
385
|
-
...extensionCommands(c => new Extensions(join(c.appArguments.AppDataDirectory, 'workspace'))), ...extensionInventoryCommands(c => owner.selected(c).backend), ...pluginCommands(c => owner.selected(c).ui), ...synchronizationCommands(c => owner.selected(c).synchronization), ...utilityCommands(), ...lifecycleCommands(c => owner.selected(c).lifecycle), ...projectCommands(c => owner.selected(c).projects), ...uiCommands(c => owner.selected(c).ui), ...debuggerCommands(c => owner.selected(c).debugger), ...archiveCommands(c => owner.selected(c).archive)]) }, owner) : entry) };
|
|
416
|
+
scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...sdkCommands(c => owner.selected(c).research), ...sdkFileCommands(c => owner.selected(c).sdkFiles), ...trackingCommands(c => owner.selected(c).tracking), ...crossCommands(c => owner.selected(c).research), ...administrationCommands(c => owner.selected(c).administration, options.backend ? undefined : async (c) => { const status = await new RuntimeManager(c).status(); return status.state === 'not_ready' ? { ...status.server, connected: false, state: 'runtime_not_ready', missing: status.missing } : undefined; }),
|
|
417
|
+
...extensionCommands(c => new Extensions(join(c.appArguments.AppDataDirectory, 'workspace'))), ...extensionInventoryCommands(c => owner.selected(c).backend), ...pluginCommands(c => owner.selected(c).ui), ...synchronizationCommands(c => owner.selected(c).synchronization), ...utilityCommands(), ...runtimeCommands((c, action, quiescent) => owner.runtime(c, action, quiescent)), ...lifecycleCommands(c => owner.selected(c).lifecycle, c => new RuntimeManager(c).status()), ...projectCommands(c => owner.selected(c).projects), ...uiCommands(c => owner.selected(c).ui), ...debuggerCommands(c => owner.selected(c).debugger), ...archiveCommands(c => owner.selected(c).archive)]) }, owner) : entry) };
|
|
386
418
|
}
|
|
387
419
|
export const createInsightCli = (options = {}) => createCli(createDefinition(options));
|
package/dist/src/contexts.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { runtimeProcesses } from './runtime-manager.js';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import { randomUUID, randomBytes } from 'node:crypto';
|
|
3
4
|
import { mkdir, open, readFile, readdir, rename, link, realpath, copyFile, chmod, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { constants } from 'node:fs';
|
|
5
|
-
import { join, delimiter, sep, resolve } from 'node:path';
|
|
6
|
+
import { join, delimiter, sep, resolve, dirname } from 'node:path';
|
|
6
7
|
import { createServer } from 'node:net';
|
|
7
8
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
9
|
import { nativeHelpers, nativeInputs, nativeJvmDirectories, nativeExtensionArguments } from './native-inputs.js';
|
|
@@ -73,7 +74,16 @@ export class GhidraContexts {
|
|
|
73
74
|
fork(c) { return new GhidraContexts(this.workspace, this.inputs, c, this.running, this.modelTasks, this.ownedServer.fork(c), this.capacity); }
|
|
74
75
|
directory() { return join(this.workspace.directory, 'ghidra-native'); }
|
|
75
76
|
server() { return this.ownedServer; }
|
|
76
|
-
async administration(operation, parameters) {
|
|
77
|
+
async administration(operation, parameters) {
|
|
78
|
+
if (operation === 'stop') {
|
|
79
|
+
if (this.c.profile.values.server)
|
|
80
|
+
fail('authorization', 'External server lifecycle is owned by its operator. Use server disconnect.');
|
|
81
|
+
const processes = await runtimeProcesses(dirname(this.workspace.directory));
|
|
82
|
+
if (processes.some(p => p.kind !== 'server'))
|
|
83
|
+
fail('busy', 'Close active Ghidra contexts before stopping their server.');
|
|
84
|
+
}
|
|
85
|
+
return this.server().administration(operation, parameters);
|
|
86
|
+
}
|
|
77
87
|
async sdkFile(operation, parameters) { return this.server().sdkFile(operation, parameters); }
|
|
78
88
|
async prepareRoot(kind, target, context) {
|
|
79
89
|
const root = join(this.directory(), kind, context ?? target, randomUUID());
|
package/dist/src/lifecycle.d.ts
CHANGED
|
@@ -46,4 +46,4 @@ export declare class Lifecycle {
|
|
|
46
46
|
discard(selected: Scope, callerId?: string): Promise<unknown>;
|
|
47
47
|
saveAll(session: string, callerId?: string): Promise<unknown>;
|
|
48
48
|
}
|
|
49
|
-
export declare function lifecycleCommands(service: (context: CommandContext) => Lifecycle): CommandDefinition[];
|
|
49
|
+
export declare function lifecycleCommands(service: (context: CommandContext) => Lifecycle, runtimeStatus?: (context: CommandContext) => Promise<unknown>): CommandDefinition[];
|
package/dist/src/lifecycle.js
CHANGED
|
@@ -138,11 +138,17 @@ export class Lifecycle {
|
|
|
138
138
|
return result;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
export function lifecycleCommands(service) {
|
|
141
|
+
export function lifecycleCommands(service, runtimeStatus) {
|
|
142
142
|
return [command('changes', 'Manage unpublished edits in the selected context', [
|
|
143
143
|
command('discard', 'Save a retained copy, then restore the opening base without publication', ({ options: o }, c) => service(c).discard(scope(o), o.requestId === undefined ? undefined : id(o.requestId)), { permission: 'Destructive', options: [...selectedOptions, { flags: '--request-id <id>', description: 'Retrieve the same local discard result' }] })
|
|
144
144
|
]),
|
|
145
145
|
command('save-all', 'Save every Ghidra context in the explicit session; outcomes are per context', ({ options: o }, c) => service(c).saveAll(id(o.session), o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [{ flags: '--session <id>', description: 'Explicit session whose contexts are saved', required: true }, { flags: '--request-id <id>', description: 'Retrieve the recorded session save outcomes' }] }),
|
|
146
|
-
command('status', '
|
|
146
|
+
command('status', 'Inspect the installed runtime, or verify a selected context', ({ options: o }, c) => {
|
|
147
|
+
if (o.session === undefined && o.target === undefined && runtimeStatus)
|
|
148
|
+
return runtimeStatus(c);
|
|
149
|
+
if (o.session === undefined || o.target === undefined)
|
|
150
|
+
fail('invalid', 'Context status requires both --session and --target.');
|
|
151
|
+
return service(c).health(scope(o));
|
|
152
|
+
}, { permission: Permission.ReadOnly, options: selectedOptions.map(option => ({ ...option, required: false })) }),
|
|
147
153
|
...['save', 'close'].map(name => command(name, name === 'save' ? 'Save private work without publication' : 'Save and release the owned context; reopen retains local work', ({ options: o }, c) => service(c).change(scope(o), name, o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [...selectedOptions, { flags: '--request-id <id>', description: 'Retrieve the lifecycle outcome after a lost response' }] }))];
|
|
148
154
|
}
|
|
@@ -6,6 +6,7 @@ import { execFile } from 'node:child_process';
|
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { fail, stable, text } from './model.js';
|
|
8
8
|
import { managedExtensions, extensionJvmArguments } from './extensions.js';
|
|
9
|
+
import { runtimeValues, runtimeSelection } from './runtime-selection.js';
|
|
9
10
|
export const pinnedMcpJar = '7084e2fda16dc779dd05398e48821ff3f9ace4d32757ea143b257b39689e5fbd';
|
|
10
11
|
export const packagedMcpJar = fileURLToPath(new URL('../../runtime/GhidraMCP-7.0.0.jar', import.meta.url));
|
|
11
12
|
/** Ghidra has its own cache/settings overrides in addition to Java's home/tmp properties. */
|
|
@@ -78,6 +79,10 @@ export async function nativeRuntimeFingerprint(ghidraVersion, files) {
|
|
|
78
79
|
return createHash('sha256').update(stable({ schema: 2, ghidraVersion, hashes })).digest('hex');
|
|
79
80
|
}
|
|
80
81
|
export async function nativeInputs(values, directory) {
|
|
82
|
+
if (directory)
|
|
83
|
+
values = runtimeValues(dirname(directory), values);
|
|
84
|
+
if (!values.java || !values.ghidraHome)
|
|
85
|
+
fail('configuration', 'Ghidra is not installed. Run insight-cli ghidra install, or supply external runtime paths.');
|
|
81
86
|
const requested = [text(values.java, 4096), text(values.ghidraHome, 4096), text(values.mcpJar ?? packagedMcpJar, 4096)];
|
|
82
87
|
if (!requested.every(isAbsolute))
|
|
83
88
|
fail('configuration', 'Configure absolute Java, Ghidra home and GhidraMCP jar paths.');
|
|
@@ -110,12 +115,18 @@ export async function nativeInputs(values, directory) {
|
|
|
110
115
|
join(debug, 'Debugger-agent-lldb/pypkg/dist/ghidralldb-12.1-py3-none-any.whl')
|
|
111
116
|
];
|
|
112
117
|
const files = await nativeRuntimeFiles(ghidraHome, java);
|
|
118
|
+
const baseFingerprint = await nativeRuntimeFingerprint(ghidraVersion, [...files, { name: 'bridge/GhidraMCP-7.0.0.jar', path: mcpJar }]);
|
|
119
|
+
if (directory) {
|
|
120
|
+
const selected = runtimeSelection(dirname(directory));
|
|
121
|
+
if (selected && selected.fingerprint !== baseFingerprint)
|
|
122
|
+
fail('binding', 'Selected runtime inputs differ from their installation receipt; reinstall or restore the verified files.');
|
|
123
|
+
}
|
|
113
124
|
for (const module of modules)
|
|
114
125
|
for (const file of module.files)
|
|
115
126
|
files.push({ name: 'extension/' + module.name + '/' + file.name, path: join(module.root, file.name) });
|
|
116
127
|
// Logical names bind locations without embedding the author's installation path.
|
|
117
128
|
files.push({ name: 'bridge/GhidraMCP-7.0.0.jar', path: mcpJar });
|
|
118
|
-
const fingerprint = await nativeRuntimeFingerprint(ghidraVersion, files);
|
|
129
|
+
const fingerprint = modules.length ? await nativeRuntimeFingerprint(ghidraVersion, files) : baseFingerprint;
|
|
119
130
|
return { java, ghidraHome, mcpJar, jars, classpath: jars.join(delimiter), ghidraVersion, fingerprint, debuggerWheels, extensionRoots: modules.map(m => m.root), serverClasspath: baseJars.join(delimiter) };
|
|
120
131
|
}
|
|
121
132
|
/** Compile packaged helper sources against the selected installation, never an author checkout. */
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RuntimeArtifact } from './runtime-catalog.js';
|
|
2
|
+
export declare function fileSha256(path: string): Promise<string>;
|
|
3
|
+
export declare function archiveName(name: string): string;
|
|
4
|
+
export declare function extractArchive(file: string, destination: string, format: 'tgz' | 'zip'): Promise<void>;
|
|
5
|
+
export declare function cacheArtifact(artifact: RuntimeArtifact, cache: string, fetcher: typeof fetch, signal: AbortSignal, offline?: boolean): Promise<string>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir, open, rename, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, posix } from 'node:path';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import * as tar from 'tar';
|
|
7
|
+
import * as zip from 'yauzl';
|
|
8
|
+
import { fail } from './model.js';
|
|
9
|
+
export async function fileSha256(path) { const hash = createHash('sha256'); for await (const chunk of createReadStream(path))
|
|
10
|
+
hash.update(chunk); return hash.digest('hex'); }
|
|
11
|
+
export function archiveName(name) {
|
|
12
|
+
if (!name || name.includes('\\') || name.includes('\0') || name.startsWith('/') || /^[a-zA-Z]:/.test(name) || name.split('/').includes('..'))
|
|
13
|
+
fail('invalid', 'Runtime archive contains an unsafe path.');
|
|
14
|
+
return name.replace(/^(\.\/)+/, '');
|
|
15
|
+
}
|
|
16
|
+
export async function extractArchive(file, destination, format) {
|
|
17
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
18
|
+
let count = 0, bytes = 0;
|
|
19
|
+
const bound = (name, size) => { archiveName(name); if (++count > 100000 || !Number.isSafeInteger(size) || size < 0 || (bytes += size) > 8 * 1024 ** 3)
|
|
20
|
+
fail('limit', 'Runtime archive exceeds extraction bounds.'); };
|
|
21
|
+
if (format === 'tgz') {
|
|
22
|
+
let unsafe;
|
|
23
|
+
await tar.t({ file, strict: true, filter: (path, entry) => {
|
|
24
|
+
try {
|
|
25
|
+
if (!('type' in entry))
|
|
26
|
+
fail('invalid', 'Unsupported runtime archive metadata.');
|
|
27
|
+
bound(path, entry.size);
|
|
28
|
+
if (!['File', 'Directory', 'SymbolicLink', 'Link', 'OldFile', 'ContiguousFile'].includes(entry.type))
|
|
29
|
+
fail('invalid', 'Unsupported runtime archive entry.');
|
|
30
|
+
if (entry.type === 'SymbolicLink' || entry.type === 'Link') {
|
|
31
|
+
const target = entry.linkpath;
|
|
32
|
+
if (typeof target !== 'string')
|
|
33
|
+
fail('invalid', 'Runtime archive link omitted its target.');
|
|
34
|
+
if (target.startsWith('/') || target.includes('\\') || /^[a-zA-Z]:/.test(target))
|
|
35
|
+
fail('invalid', 'Runtime archive link escapes extraction storage.');
|
|
36
|
+
const joined = posix.normalize(entry.type === 'Link' ? target : posix.join(posix.dirname(path), target));
|
|
37
|
+
archiveName(joined);
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
unsafe ??= error;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
} });
|
|
46
|
+
if (unsafe)
|
|
47
|
+
throw unsafe;
|
|
48
|
+
await tar.x({ file, cwd: destination, strict: true, preserveOwner: false, noChmod: false });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const archive = await new Promise((ok, bad) => zip.open(file, { lazyEntries: true, strictFileNames: true }, (e, z) => e ? bad(e) : ok(z)));
|
|
52
|
+
await new Promise((ok, bad) => {
|
|
53
|
+
const failed = (e) => { archive.close(); bad(e); };
|
|
54
|
+
archive.on('error', failed);
|
|
55
|
+
archive.on('end', ok);
|
|
56
|
+
archive.on('entry', (entry) => {
|
|
57
|
+
void (async () => {
|
|
58
|
+
bound(entry.fileName, entry.uncompressedSize);
|
|
59
|
+
const mode = entry.externalFileAttributes >>> 16, type = mode & 0o170000;
|
|
60
|
+
if (type && type !== 0o100000 && type !== 0o040000)
|
|
61
|
+
fail('invalid', 'Runtime zip may contain only files and directories.');
|
|
62
|
+
const path = join(destination, entry.fileName);
|
|
63
|
+
if (entry.fileName.endsWith('/'))
|
|
64
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
65
|
+
else {
|
|
66
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
67
|
+
const stream = await new Promise((ok, bad) => archive.openReadStream(entry, (e, s) => e ? bad(e) : ok(s)));
|
|
68
|
+
await pipeline(stream, createWriteStream(path, { flags: 'wx', mode: mode & 0o111 ? 0o755 : 0o644 }));
|
|
69
|
+
}
|
|
70
|
+
archive.readEntry();
|
|
71
|
+
})().catch(failed);
|
|
72
|
+
});
|
|
73
|
+
archive.readEntry();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export async function cacheArtifact(artifact, cache, fetcher, signal, offline = false) {
|
|
77
|
+
await mkdir(cache, { recursive: true, mode: 0o700 });
|
|
78
|
+
const path = join(cache, artifact.sha256 + '.' + artifact.format);
|
|
79
|
+
try {
|
|
80
|
+
if ((await stat(path)).isFile() && await fileSha256(path) === artifact.sha256)
|
|
81
|
+
return path;
|
|
82
|
+
fail('binding', 'Cached runtime archive failed its SHA256 check; remove the corrupt cache entry before retrying.');
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== 'ENOENT')
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
if (offline)
|
|
89
|
+
fail('configuration', 'Required runtime archive is not cached; run ghidra download before using --offline.', { artifact: artifact.name, sha256: artifact.sha256 });
|
|
90
|
+
const temporary = path + '.partial-' + randomUUID();
|
|
91
|
+
try {
|
|
92
|
+
let headers = {};
|
|
93
|
+
if (artifact.url.startsWith('https://ghcr.io/v2/homebrew/core/ghidra/')) {
|
|
94
|
+
const response = await fetcher('https://ghcr.io/token?service=ghcr.io&scope=repository:homebrew/core/ghidra:pull', { signal, redirect: 'error' });
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
fail('download', 'Anonymous runtime registry authorization failed.');
|
|
97
|
+
const value = await response.json();
|
|
98
|
+
if (!value.token || value.token.length > 8192)
|
|
99
|
+
fail('download', 'Invalid anonymous runtime registry token.');
|
|
100
|
+
headers = { Authorization: 'Bearer ' + value.token };
|
|
101
|
+
}
|
|
102
|
+
const response = await fetcher(artifact.url, { signal, headers });
|
|
103
|
+
if (!response.ok || !response.body)
|
|
104
|
+
fail('download', 'Pinned runtime download failed.', { artifact: artifact.name, status: response.status });
|
|
105
|
+
let size = 0;
|
|
106
|
+
const hash = createHash('sha256'), handle = await open(temporary, 'wx', 0o600);
|
|
107
|
+
try {
|
|
108
|
+
for await (const chunk of response.body) {
|
|
109
|
+
if ((size += chunk.byteLength) > 2 * 1024 ** 3)
|
|
110
|
+
fail('limit', 'Runtime download exceeds two GiB.');
|
|
111
|
+
hash.update(chunk);
|
|
112
|
+
await handle.writeFile(chunk);
|
|
113
|
+
}
|
|
114
|
+
await handle.sync();
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
await handle.close();
|
|
118
|
+
}
|
|
119
|
+
if (hash.digest('hex') !== artifact.sha256)
|
|
120
|
+
fail('binding', 'Downloaded runtime archive failed its pinned SHA256 check.');
|
|
121
|
+
await rename(temporary, path);
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await rm(temporary, { force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RuntimeArtifact {
|
|
2
|
+
name: string;
|
|
3
|
+
url: string;
|
|
4
|
+
sha256: string;
|
|
5
|
+
format: 'zip' | 'tgz';
|
|
6
|
+
root: string;
|
|
7
|
+
license: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const ghidraArchive: RuntimeArtifact;
|
|
10
|
+
export declare function runtimeCatalog(platform?: string, arch?: string, kernel?: string): {
|
|
11
|
+
schema: number;
|
|
12
|
+
ghidraVersion: string;
|
|
13
|
+
javaVersion: string;
|
|
14
|
+
platform: string;
|
|
15
|
+
architecture: string;
|
|
16
|
+
ghidra: RuntimeArtifact;
|
|
17
|
+
java: RuntimeArtifact;
|
|
18
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { release } from 'node:os';
|
|
2
|
+
import { fail } from './model.js';
|
|
3
|
+
const github = 'https://github.com/';
|
|
4
|
+
export const ghidraArchive = { name: 'ghidra-12.1.2', url: github + 'NationalSecurityAgency/ghidra/releases/download/Ghidra_12.1.2_build/ghidra_12.1.2_PUBLIC_20260605.zip', sha256: 'b62e81a0390618466c019c60d8c2f796ced2509c4c1aea4a37644a77272cf99d', format: 'zip', root: 'ghidra_12.1.2_PUBLIC', license: 'Apache-2.0 and included third-party licenses' };
|
|
5
|
+
const bottles = { arm64_sonoma: '40f68357d6b62a08429c36228907c995e3449283b6d2e08ba5a4e30b53a09725', arm64_sequoia: '90aa942184fca2337ad0a123f818f37372ea642e8e69e969d45cc144a6f26fdf', arm64_tahoe: '079007d8a3a477c8d5901e6596947612d86c8641956424a2bcb71ecd798bedcc', sonoma: 'e30f8c84b64331f87f6ef1890cbdded6672bb7fcc8b674794ac93e4b3e62ad2b' };
|
|
6
|
+
const jdks = {
|
|
7
|
+
'darwin-arm64': ['aarch64_mac', '6ebcf221c9b41507b14c098e93c6ead6440b8d9bd154f8ec666c4c73abbdb201'],
|
|
8
|
+
'darwin-x64': ['x64_mac', '34180eb03e6d207c388cce3da668f6cc7cd7508c185c24782fadac2c9c0e66f9'],
|
|
9
|
+
'linux-x64': ['x64_linux', '4b2220e232a97997b436ca6ab15cbf70171ecff52958a46159dfa5a8c44ca4de'],
|
|
10
|
+
'win32-x64': ['x64_windows', 'd3625e7cadf23787ea540229544b6e2ab494b3b54da1801879e583e1dfee0a64'],
|
|
11
|
+
};
|
|
12
|
+
export function runtimeCatalog(platform = process.platform, arch = process.arch, kernel = release()) {
|
|
13
|
+
const jdk = jdks[platform + '-' + arch];
|
|
14
|
+
if (!jdk)
|
|
15
|
+
fail('configuration', 'No verified managed runtime for this OS/architecture; use an external compatible Ghidra installation.');
|
|
16
|
+
const windows = platform === 'win32', format = windows ? 'zip' : 'tgz', extension = windows ? 'zip' : 'tar.gz';
|
|
17
|
+
const java = { name: 'temurin-21.0.11+10-' + platform + '-' + arch, url: github + 'adoptium/temurin21-binaries/releases/download/jdk-21.0.11%2B10/OpenJDK21U-jdk_' + jdk[0] + '_hotspot_21.0.11_10.' + extension, sha256: jdk[1], format, root: 'jdk-21.0.11+10' + (platform === 'darwin' ? '.jdk/Contents/Home' : ''), license: 'GPL-2.0 WITH Classpath-exception-2.0; included legal notices' };
|
|
18
|
+
let ghidra = ghidraArchive;
|
|
19
|
+
if (platform === 'darwin') {
|
|
20
|
+
const major = Number(kernel.split('.')[0]);
|
|
21
|
+
if (major < 23)
|
|
22
|
+
fail('configuration', 'Managed macOS runtimes require macOS 14 or newer.');
|
|
23
|
+
const tag = arch === 'x64' ? 'sonoma' : major >= 25 ? 'arm64_tahoe' : major >= 24 ? 'arm64_sequoia' : 'arm64_sonoma', hash = bottles[tag];
|
|
24
|
+
ghidra = { name: 'ghidra-12.1.2-' + tag, url: 'https://ghcr.io/v2/homebrew/core/ghidra/blobs/sha256:' + hash, sha256: hash, format: 'tgz', root: 'ghidra/12.1.2/libexec', license: ghidraArchive.license };
|
|
25
|
+
}
|
|
26
|
+
return { schema: 1, ghidraVersion: '12.1.2', javaVersion: '21.0.11+10', platform, architecture: arch, ghidra, java };
|
|
27
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
export declare function runtimeWrite(path: string, value: unknown): Promise<void>;
|
|
3
|
+
/** Passive, conservative inventory: a reused PID blocks deletion; it never grants kill authority. */
|
|
4
|
+
export declare function runtimeProcesses(directory: string): Promise<Record<string, unknown>[]>;
|
|
5
|
+
export declare function externalGhidra(path: string): Promise<string>;
|
|
6
|
+
export declare class RuntimeManager {
|
|
7
|
+
private readonly c;
|
|
8
|
+
readonly directory: string;
|
|
9
|
+
readonly root: string;
|
|
10
|
+
constructor(c: CommandContext);
|
|
11
|
+
status(): Promise<{
|
|
12
|
+
missing: string[];
|
|
13
|
+
capabilities: {
|
|
14
|
+
headless: boolean;
|
|
15
|
+
decompiler: boolean;
|
|
16
|
+
gui: boolean;
|
|
17
|
+
debugger: {
|
|
18
|
+
backend: string;
|
|
19
|
+
requires: string;
|
|
20
|
+
checkedBy: string;
|
|
21
|
+
} | {
|
|
22
|
+
backend: string;
|
|
23
|
+
checkedBy: string;
|
|
24
|
+
requires?: undefined;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
server: {
|
|
28
|
+
pid?: unknown;
|
|
29
|
+
mode: string;
|
|
30
|
+
state: string;
|
|
31
|
+
};
|
|
32
|
+
processes: Record<string, unknown>[];
|
|
33
|
+
installations: {
|
|
34
|
+
id: string;
|
|
35
|
+
selected: boolean;
|
|
36
|
+
ghidraVersion: unknown;
|
|
37
|
+
verifiedAt: unknown;
|
|
38
|
+
}[];
|
|
39
|
+
retainedStages: string[];
|
|
40
|
+
lastCheck: any;
|
|
41
|
+
storage: {
|
|
42
|
+
runtime: string;
|
|
43
|
+
research: string;
|
|
44
|
+
materials: string;
|
|
45
|
+
};
|
|
46
|
+
installation?: string | undefined;
|
|
47
|
+
fingerprint?: string | undefined;
|
|
48
|
+
state: string;
|
|
49
|
+
mode: string;
|
|
50
|
+
recommended: string;
|
|
51
|
+
ghidraVersion: string | undefined;
|
|
52
|
+
java: string | undefined;
|
|
53
|
+
ghidraHome: string | undefined;
|
|
54
|
+
}>;
|
|
55
|
+
download(offline?: boolean): Promise<{
|
|
56
|
+
state: string;
|
|
57
|
+
ghidraVersion: string;
|
|
58
|
+
javaVersion: string;
|
|
59
|
+
items: {
|
|
60
|
+
name: string;
|
|
61
|
+
path: string;
|
|
62
|
+
sha256: string;
|
|
63
|
+
license: string;
|
|
64
|
+
}[];
|
|
65
|
+
}>;
|
|
66
|
+
private archive;
|
|
67
|
+
private cleanupProbes;
|
|
68
|
+
install(o: Record<string, unknown>): Promise<{
|
|
69
|
+
checks: string[];
|
|
70
|
+
schema: 1;
|
|
71
|
+
mode: "managed" | "external";
|
|
72
|
+
id?: string;
|
|
73
|
+
ghidraHome: string;
|
|
74
|
+
java: string;
|
|
75
|
+
fingerprint: string;
|
|
76
|
+
selectedAt: string;
|
|
77
|
+
uninstalledAt?: string;
|
|
78
|
+
state: string;
|
|
79
|
+
}>;
|
|
80
|
+
uninstall(purgeCache?: boolean): Promise<{
|
|
81
|
+
state: string;
|
|
82
|
+
mode: "external" | "managed";
|
|
83
|
+
externalInstallationPreserved: boolean;
|
|
84
|
+
researchPreserved: boolean;
|
|
85
|
+
materialsPreserved: boolean;
|
|
86
|
+
cachePreserved: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
private probe;
|
|
89
|
+
doctor(): Promise<{
|
|
90
|
+
state: string;
|
|
91
|
+
fingerprint: string;
|
|
92
|
+
ghidraVersion: string;
|
|
93
|
+
checkedAt: string;
|
|
94
|
+
headless: unknown;
|
|
95
|
+
decompiler: unknown;
|
|
96
|
+
server: boolean;
|
|
97
|
+
debugger: Record<string, unknown>;
|
|
98
|
+
diagnostics: string;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
export declare function runtimeCommands(exclusive: <T>(c: CommandContext, action: () => Promise<T>, quiescent?: boolean) => Promise<T>): CommandDefinition[];
|