@eyeauras/insight-cli 0.3.1 → 0.4.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/SKILL.md +4 -1
- package/dist/src/analysis-default.d.ts +26 -0
- package/dist/src/analysis-default.js +149 -0
- package/dist/src/analysis-storage.d.ts +39 -20
- package/dist/src/analysis-storage.js +112 -16
- package/dist/src/arguments.js +9 -4
- package/dist/src/cli.js +45 -37
- package/dist/src/code-references.d.ts +48 -0
- package/dist/src/code-references.js +154 -0
- package/dist/src/context-diagnostics.d.ts +25 -0
- package/dist/src/context-diagnostics.js +82 -0
- package/dist/src/contexts.d.ts +13 -1
- package/dist/src/contexts.js +38 -9
- package/dist/src/function-boundaries.d.ts +9 -0
- package/dist/src/function-boundaries.js +68 -0
- package/dist/src/function-entries.d.ts +33 -0
- package/dist/src/function-entries.js +84 -0
- package/dist/src/function-inspection.d.ts +4 -2
- package/dist/src/function-inspection.js +94 -59
- package/dist/src/function-results.d.ts +3 -1
- package/dist/src/function-results.js +9 -3
- package/dist/src/inline-script.d.ts +15 -0
- package/dist/src/inline-script.js +62 -0
- package/dist/src/internal-timing.d.ts +3 -0
- package/dist/src/internal-timing.js +52 -0
- package/dist/src/lifecycle.js +14 -4
- package/dist/src/native-inputs.d.ts +1 -1
- package/dist/src/native-inputs.js +1 -1
- package/dist/src/native-queries.js +1 -1
- package/dist/src/persistent-client.d.ts +30 -0
- package/dist/src/persistent-client.js +125 -0
- package/dist/src/queries.js +4 -1
- package/dist/src/query-policy.js +9 -1
- package/dist/src/research-view.d.ts +6 -0
- package/dist/src/research-view.js +9 -0
- package/dist/src/research.d.ts +38 -0
- package/dist/src/research.js +78 -8
- package/dist/src/string-anchors.d.ts +43 -0
- package/dist/src/string-anchors.js +176 -0
- package/dist/src/sync-policy.js +3 -0
- package/dist/src/timed-backend.d.ts +3 -0
- package/dist/src/timed-backend.js +16 -0
- package/dist/src/transport.d.ts +1 -0
- package/dist/src/transport.js +4 -0
- package/dist/src/workspace.js +5 -2
- package/package.json +12 -1
- package/references/native/InsightAnchorScan.java +135 -0
- package/references/native/InsightCodeReferences.java +129 -0
- package/references/native/InsightFunctionBoundaries.java +56 -0
- package/references/native/InsightInlineExecutor.java +143 -0
- package/references/native/InsightInlineScript.java +14 -0
- package/references/native/InsightPeRanges.java +92 -0
- package/references/native/InsightResearchQueries.java +22 -1
- package/references/native/InsightResearchView.java +69 -0
- package/references/native/InsightStringAnchors.java +49 -0
- package/references/persistent-client.mjs +125 -0
- package/references/persistent.md +50 -0
- package/references/query-semantics.md +12 -0
- package/references/recovery.md +36 -0
- package/references/research.md +87 -0
- package/references/setup.md +1 -1
- package/references/sparse-navigation.md +173 -0
- package/references/storage.md +35 -7
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-NOTICE +5 -0
- package/runtime/provenance.json +17 -3
- package/runtime/research-view.provenance.json +28 -0
- package/runtime/toolbox.json +10 -3
- package/skill-bundle/insight-artifacts/SKILL.md +1 -1
- package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
- package/skill-bundle/insight-artifacts/references/storage.md +35 -7
- package/skill-bundle/insight-binary/SKILL.md +1 -1
- package/skill-bundle/insight-capture/SKILL.md +1 -1
- package/skill-bundle/insight-cli/SKILL.md +4 -1
- package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-cli/references/persistent.md +50 -0
- package/skill-bundle/insight-cli/references/recovery.md +36 -0
- package/skill-bundle/insight-cli/references/setup.md +1 -1
- package/skill-bundle/insight-cli/references/storage.md +35 -7
- package/skill-bundle/insight-die/SKILL.md +1 -1
- package/skill-bundle/insight-dnspy/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
- package/skill-bundle/insight-frida/SKILL.md +1 -1
- package/skill-bundle/insight-ghidra/SKILL.md +14 -1
- package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
- package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
- package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
- package/skill-bundle/insight-ghidra/references/research.md +87 -0
- package/skill-bundle/insight-ghidra/references/setup.md +1 -1
- package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skill-bundle/insight-ghidra/references/storage.md +35 -7
- package/skill-bundle/insight-ilspy/SKILL.md +1 -1
- package/skill-bundle/insight-interop/SKILL.md +1 -1
- package/skill-bundle/insight-lldb/SKILL.md +1 -1
- package/skill-bundle/insight-llvm/SKILL.md +1 -1
- package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
- package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
- package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
- package/skill-bundle/insight-overview/SKILL.md +1 -1
- package/skill-bundle/insight-procmon/SKILL.md +1 -1
- package/skill-bundle/insight-re/SKILL.md +4 -1
- package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-re/references/persistent.md +50 -0
- package/skill-bundle/insight-re/references/recovery.md +36 -0
- package/skill-bundle/insight-re/references/storage.md +35 -7
- package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
- package/skill-bundle/insight-strace/SKILL.md +1 -1
- package/skill-bundle/insight-tshark/SKILL.md +1 -1
- package/skill-bundle/insight-windbg/SKILL.md +1 -1
- package/skill-bundle/manifest.json +1 -1
- package/skills/insight-artifacts/SKILL.md +1 -1
- package/skills/insight-artifacts/references/recovery.md +36 -0
- package/skills/insight-artifacts/references/storage.md +35 -7
- package/skills/insight-binary/SKILL.md +1 -1
- package/skills/insight-capture/SKILL.md +1 -1
- package/skills/insight-die/SKILL.md +1 -1
- package/skills/insight-dnspy/SKILL.md +1 -1
- package/skills/insight-dotnet-dump/SKILL.md +1 -1
- package/skills/insight-dotnet-stack/SKILL.md +1 -1
- package/skills/insight-dotnet-trace/SKILL.md +1 -1
- package/skills/insight-frida/SKILL.md +1 -1
- package/skills/insight-ghidra/SKILL.md +14 -1
- package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skills/insight-ghidra/references/persistent.md +50 -0
- package/skills/insight-ghidra/references/query-semantics.md +12 -0
- package/skills/insight-ghidra/references/recovery.md +36 -0
- package/skills/insight-ghidra/references/research.md +87 -0
- package/skills/insight-ghidra/references/setup.md +1 -1
- package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skills/insight-ghidra/references/storage.md +35 -7
- package/skills/insight-ilspy/SKILL.md +1 -1
- package/skills/insight-interop/SKILL.md +1 -1
- package/skills/insight-lldb/SKILL.md +1 -1
- package/skills/insight-llvm/SKILL.md +1 -1
- package/skills/insight-managed-heap/SKILL.md +1 -1
- package/skills/insight-managed-metadata/SKILL.md +1 -1
- package/skills/insight-mitmproxy/SKILL.md +1 -1
- package/skills/insight-overview/SKILL.md +1 -1
- package/skills/insight-procmon/SKILL.md +1 -1
- package/skills/insight-re/SKILL.md +4 -1
- package/skills/insight-re/references/persistent-client.mjs +125 -0
- package/skills/insight-re/references/persistent.md +50 -0
- package/skills/insight-re/references/recovery.md +36 -0
- package/skills/insight-re/references/storage.md +35 -7
- package/skills/insight-sevenzip/SKILL.md +1 -1
- package/skills/insight-strace/SKILL.md +1 -1
- package/skills/insight-tshark/SKILL.md +1 -1
- package/skills/insight-windbg/SKILL.md +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -52,6 +52,11 @@ import { RuntimeManager, runtimeCommands, runtimeProcesses } from './runtime-man
|
|
|
52
52
|
import { packageVersion } from './package-version.js';
|
|
53
53
|
import { runtimeSelection } from './runtime-selection.js';
|
|
54
54
|
import { AnalysisStorage, analysisDirectory } from './analysis-storage.js';
|
|
55
|
+
import { codeReferenceCommands } from './code-references.js';
|
|
56
|
+
import { stringAnchorCommands } from './string-anchors.js';
|
|
57
|
+
import { contextDiagnosticCommands } from './context-diagnostics.js';
|
|
58
|
+
import { traceCommand } from './internal-timing.js';
|
|
59
|
+
import { timedBackend } from './timed-backend.js';
|
|
55
60
|
import { readInitialProgress } from './initial-progress.js';
|
|
56
61
|
const bindingFields = ['java', 'ghidraHome', 'mcpJar', 'mcpJarSha256', 'server', 'repository', 'readerUser', 'publisherUser', 'maxContexts', 'debuggerPython', 'windbgDir', 'archiveUrl'];
|
|
57
62
|
class Owner {
|
|
@@ -109,7 +114,7 @@ class Owner {
|
|
|
109
114
|
const result = await selected.backend;
|
|
110
115
|
if (result instanceof GhidraContexts)
|
|
111
116
|
await managedExtensions(selected.workspace.directory);
|
|
112
|
-
return result instanceof GhidraContexts ? result.fork(c) : result;
|
|
117
|
+
return timedBackend(result instanceof GhidraContexts ? result.fork(c) : result);
|
|
113
118
|
};
|
|
114
119
|
const research = new Research(profile.workspace, backend, profile.tasks);
|
|
115
120
|
return { workspace: profile.workspace, research, synchronization: new Synchronization(profile.workspace, research, async () => {
|
|
@@ -231,47 +236,50 @@ function scopedFailures(entry, owner, parents = []) {
|
|
|
231
236
|
const path = [...parents, entry.name.split(' ')[0]], name = path.join(' ');
|
|
232
237
|
const run = entry.run;
|
|
233
238
|
return { ...entry, ...(entry.children ? { children: entry.children.map(child => scopedFailures(child, owner, path)) } : {}), ...(run ? { run: async (input, c) => {
|
|
234
|
-
|
|
235
|
-
let selected;
|
|
239
|
+
const execute = async () => {
|
|
236
240
|
try {
|
|
237
|
-
selected
|
|
238
|
-
|
|
239
|
-
selected =
|
|
241
|
+
let selected;
|
|
242
|
+
try {
|
|
243
|
+
selected = scope(input.options);
|
|
244
|
+
if (selected.session === 'new')
|
|
245
|
+
selected = undefined;
|
|
246
|
+
}
|
|
247
|
+
catch { }
|
|
248
|
+
if (!selected)
|
|
249
|
+
return await run(input, c);
|
|
250
|
+
const services = owner.selected(c);
|
|
251
|
+
// First open of an already known target reserves its context inside Research.open.
|
|
252
|
+
// Existing contexts still use the command boundary to order automatic sync phases.
|
|
253
|
+
if (path[1] === 'open')
|
|
254
|
+
try {
|
|
255
|
+
services.workspace.context(selected);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error instanceof CliError && error.code === 'coordinator.notFound')
|
|
259
|
+
return await run(input, c);
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
return await services.research.boundary(selected, async () => {
|
|
263
|
+
const excluded = ['open', 'merge', 'refresh', 'sync-policy', 'close', 'changes', 'debugger', 'ui', 'version-tracking', 'archive', 'projects', 'repository', 'server', 'formats', 'libraries', 'plugins'];
|
|
264
|
+
const eligible = entry.permission !== Permission.ReadOnly && entry.permission !== undefined && !excluded.includes(path[1]) && !['ghidra types archive-export', 'ghidra documentation transfer'].includes(name);
|
|
265
|
+
return eligible ? services.synchronization.execute(selected, name, { ...input.options, commandArguments: input.args }, async (requestId) => run({ ...input, options: { ...input.options, ...(requestId ? { requestId } : {}) } }, c)) : run(input, c);
|
|
266
|
+
});
|
|
240
267
|
}
|
|
241
|
-
catch {
|
|
242
|
-
|
|
243
|
-
return await run(input, c);
|
|
244
|
-
const services = owner.selected(c);
|
|
245
|
-
// First open of an already known target reserves its context inside Research.open.
|
|
246
|
-
// Existing contexts still use the command boundary to order automatic sync phases.
|
|
247
|
-
if (path[1] === 'open')
|
|
268
|
+
catch (error) {
|
|
269
|
+
let selected;
|
|
248
270
|
try {
|
|
249
|
-
|
|
271
|
+
selected = scope(input.options);
|
|
250
272
|
}
|
|
251
|
-
catch
|
|
252
|
-
if (error instanceof CliError && error.code === 'coordinator.notFound')
|
|
253
|
-
return await run(input, c);
|
|
273
|
+
catch {
|
|
254
274
|
throw error;
|
|
255
275
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
let selected;
|
|
264
|
-
try {
|
|
265
|
-
selected = scope(input.options);
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
throw error;
|
|
276
|
+
if (!(error instanceof CliError))
|
|
277
|
+
throw error;
|
|
278
|
+
const result = error.result && typeof error.result === 'object' && !Array.isArray(error.result) ? error.result : {};
|
|
279
|
+
throw new CliError(error.message, { code: error.code, exitCode: error.exitCode, next: error.next, result: { ...result, ...selected } });
|
|
269
280
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const result = error.result && typeof error.result === 'object' && !Array.isArray(error.result) ? error.result : {};
|
|
273
|
-
throw new CliError(error.message, { code: error.code, exitCode: error.exitCode, next: error.next, result: { ...result, ...selected } });
|
|
274
|
-
}
|
|
281
|
+
};
|
|
282
|
+
return path[1] === 'open' || input.options.session && input.options.target ? traceCommand(join(analysisDirectory(c), 'workspace'), name, execute) : execute();
|
|
275
283
|
} } : {}) };
|
|
276
284
|
}
|
|
277
285
|
export function createDefinition(options = {}) {
|
|
@@ -424,7 +432,7 @@ export function createDefinition(options = {}) {
|
|
|
424
432
|
{ flags: '--loader <name>', description: 'Explicit installed loader name/ID from formats loaders' },
|
|
425
433
|
{ flags: '--loader-options <json>', description: 'Named primitive SDK loader options, requiring --loader' }] }),
|
|
426
434
|
command('strings', 'Read defined strings', [
|
|
427
|
-
command('search', 'Find literal text
|
|
435
|
+
command('search', 'Find literal text in defined string data; undefined literals: analysis search-byte-patterns', ({ options: o }, c) => owner.selected(c).research.strings(scope(o), text(o.contains, 4096), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, { flags: '--contains <text>', description: 'Literal case-insensitive substring', required: true }, ...paged] }),
|
|
428
436
|
]),
|
|
429
437
|
command('xrefs', 'Follow cross references', [
|
|
430
438
|
command('to', 'Find incoming references and their functions', ({ options: o }, c) => owner.selected(c).research.xrefs(scope(o), address(o.address), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, at, ...paged] }),
|
|
@@ -508,7 +516,7 @@ export function createDefinition(options = {}) {
|
|
|
508
516
|
],
|
|
509
517
|
};
|
|
510
518
|
const commands = definition.commands?.map(entry => entry.name === 'ghidra' ?
|
|
511
|
-
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; }),
|
|
519
|
+
scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...codeReferenceCommands(c => owner.selected(c).research), ...stringAnchorCommands(c => owner.selected(c).research), ...contextDiagnosticCommands(c => owner.selected(c).research, c => owner.selected(c).workspace), ...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; }),
|
|
512
520
|
...extensionCommands(c => new Extensions(join(analysisDirectory(c), '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);
|
|
513
521
|
return { ...definition, commands: [...(commands ?? []).map(e => storage.wrap(e)), storage.commands()] };
|
|
514
522
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CommandContext } from '@eyeauras/cli-factory';
|
|
2
|
+
import { type Research, type ResearchBackend } from './research.js';
|
|
3
|
+
import type { Scope } from './workspace.js';
|
|
4
|
+
export declare const referenceAlgorithm = "x86-64-code-reference-candidates-v1";
|
|
5
|
+
export declare const referenceForms: readonly ["call-rel32", "lea-rip", "mov-rip", "call-rip-slot"];
|
|
6
|
+
type Form = typeof referenceForms[number];
|
|
7
|
+
export interface ReferenceQuery {
|
|
8
|
+
targets: string[];
|
|
9
|
+
forms: Form[];
|
|
10
|
+
start: string;
|
|
11
|
+
end: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ReferenceBudgets {
|
|
14
|
+
max_bytes: number;
|
|
15
|
+
max_candidates: number;
|
|
16
|
+
timeout_seconds: number;
|
|
17
|
+
limit: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ReferenceRequest {
|
|
20
|
+
query: ReferenceQuery;
|
|
21
|
+
from: string;
|
|
22
|
+
budgets: ReferenceBudgets;
|
|
23
|
+
expectedView?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function referenceRequest(o: Record<string, unknown>, who: Scope): ReferenceRequest;
|
|
26
|
+
/** The deliberately small encoding contract, independently rechecked in the facade. */
|
|
27
|
+
export declare function referenceEncoding(input: Buffer): {
|
|
28
|
+
form: "call-rel32" | "lea-rip" | "mov-rip" | "call-rip-slot";
|
|
29
|
+
length: number;
|
|
30
|
+
displacement: number;
|
|
31
|
+
operand: number;
|
|
32
|
+
bits: number;
|
|
33
|
+
delta: number;
|
|
34
|
+
role: string;
|
|
35
|
+
} | undefined;
|
|
36
|
+
export declare function referenceResult(raw: unknown, r: ReferenceRequest): Record<string, unknown>;
|
|
37
|
+
export declare function inspectCodeReferences(backend: ResearchBackend, who: Scope, r: ReferenceRequest, view: string, observation: unknown): Promise<{
|
|
38
|
+
value: {
|
|
39
|
+
view: {
|
|
40
|
+
identity: string;
|
|
41
|
+
};
|
|
42
|
+
cursor: string | null;
|
|
43
|
+
};
|
|
44
|
+
session: string;
|
|
45
|
+
target: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function codeReferenceCommands(research: (c: CommandContext) => Research): import("@eyeauras/cli-factory").CommandDefinition[];
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { scope, number, address } from './research.js';
|
|
3
|
+
import { fail, object, digest, stable } from './model.js';
|
|
4
|
+
import { parseDomainJson } from './json.js';
|
|
5
|
+
import { selectedOptions } from './arguments.js';
|
|
6
|
+
export const referenceAlgorithm = 'x86-64-code-reference-candidates-v1';
|
|
7
|
+
export const referenceForms = ['call-rel32', 'lea-rip', 'mov-rip', 'call-rip-slot'];
|
|
8
|
+
const defaults = { max_bytes: 4194304, max_candidates: 200000, timeout_seconds: 5, limit: 100 };
|
|
9
|
+
const hex = (v) => { const s = address(v); if (!/^[a-f0-9]{1,16}$/.test(s))
|
|
10
|
+
fail('invalid', 'Code-reference V1 requires default-space unsigned 64-bit addresses.'); return s; };
|
|
11
|
+
const big = (s) => BigInt('0x' + s);
|
|
12
|
+
function query(v) {
|
|
13
|
+
const q = object(v);
|
|
14
|
+
if (!Array.isArray(q.targets) || q.targets.length < 1 || q.targets.length > 64 || !Array.isArray(q.forms) || !q.forms.length || q.forms.length > 4)
|
|
15
|
+
fail('invalid', 'Select 1–64 targets and 1–4 reference forms.');
|
|
16
|
+
const targets = q.targets.map(hex).sort((a, b) => big(a) < big(b) ? -1 : big(a) > big(b) ? 1 : 0), forms = referenceForms.filter(f => q.forms.includes(f));
|
|
17
|
+
if (new Set(targets).size !== targets.length || forms.length !== q.forms.length)
|
|
18
|
+
fail('invalid', 'Targets/forms must be distinct supported values.');
|
|
19
|
+
const start = hex(q.start), end = hex(q.end);
|
|
20
|
+
if (big(start) > big(end))
|
|
21
|
+
fail('invalid', 'scan-through must not precede scan-from.');
|
|
22
|
+
return { targets, forms, start, end };
|
|
23
|
+
}
|
|
24
|
+
function budgets(v, base = defaults) { return { max_bytes: number(v.maxBytes ?? base.max_bytes, 16777216, 15), max_candidates: number(v.maxCandidates ?? base.max_candidates, 1000000, 1), timeout_seconds: number(v.timeoutSeconds ?? base.timeout_seconds, 30, 1), limit: number(v.limit ?? base.limit, 1000, 1) }; }
|
|
25
|
+
export function referenceRequest(o, who) {
|
|
26
|
+
if (o.cursor !== undefined) {
|
|
27
|
+
if (['to', 'forms', 'scanFrom', 'scanThrough'].some(k => o[k] !== undefined))
|
|
28
|
+
fail('invalid', 'Cursor continuation cannot change targets, forms or scan bounds.');
|
|
29
|
+
if (typeof o.cursor !== 'string' || o.cursor.length > 16384 || !/^cr1\.[A-Za-z0-9_-]+$/.test(o.cursor))
|
|
30
|
+
fail('invalid', 'Malformed code-reference cursor.');
|
|
31
|
+
let c;
|
|
32
|
+
try {
|
|
33
|
+
const encoded = o.cursor.slice(4), bytes = Buffer.from(encoded, 'base64url');
|
|
34
|
+
if (bytes.toString('base64url') !== encoded)
|
|
35
|
+
throw Error();
|
|
36
|
+
c = object(parseDomainJson(bytes.toString('utf8')));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
fail('invalid', 'Malformed code-reference cursor.');
|
|
40
|
+
}
|
|
41
|
+
if (c.version !== 1 || c.algorithm !== referenceAlgorithm || stable(c.scope) !== stable(who) || typeof c.view !== 'string' || !/^[a-f0-9]{64}$/.test(c.view))
|
|
42
|
+
fail('invalid', 'Cursor version, scope or view is incompatible.');
|
|
43
|
+
const q = query(c.query), from = hex(c.next), b = object(c.budgets);
|
|
44
|
+
if (big(from) < big(q.start) || big(from) > big(q.end))
|
|
45
|
+
fail('invalid', 'Cursor frontier is outside its range.');
|
|
46
|
+
if (['max_bytes', 'max_candidates', 'timeout_seconds', 'limit'].some(k => !Number.isSafeInteger(b[k])))
|
|
47
|
+
fail('invalid', 'Malformed cursor budgets.');
|
|
48
|
+
const inherited = budgets({ maxBytes: b.max_bytes, maxCandidates: b.max_candidates, timeoutSeconds: b.timeout_seconds, limit: b.limit });
|
|
49
|
+
return { query: q, from, budgets: budgets(o, inherited), expectedView: c.view };
|
|
50
|
+
}
|
|
51
|
+
if (typeof o.to !== 'string' || typeof o.scanFrom !== 'string' || typeof o.scanThrough !== 'string')
|
|
52
|
+
fail('invalid', 'Supply --to, --scan-from and --scan-through, or --cursor.');
|
|
53
|
+
const q = query({ targets: o.to.split(',').map(x => x.trim()), forms: o.forms === undefined ? [...referenceForms] : String(o.forms).split(',').map(x => x.trim()), start: o.scanFrom, end: o.scanThrough });
|
|
54
|
+
return { query: q, from: q.start, budgets: budgets(o) };
|
|
55
|
+
}
|
|
56
|
+
/** The deliberately small encoding contract, independently rechecked in the facade. */
|
|
57
|
+
export function referenceEncoding(input) {
|
|
58
|
+
let p = 0, rex = 0, word = false;
|
|
59
|
+
while (p < input.length && p < 15) {
|
|
60
|
+
const b = input[p];
|
|
61
|
+
if ([0x66, 0x26, 0x2e, 0x36, 0x3e].includes(b)) {
|
|
62
|
+
word ||= b === 0x66;
|
|
63
|
+
rex = 0;
|
|
64
|
+
p++;
|
|
65
|
+
}
|
|
66
|
+
else if (b >= 0x40 && b <= 0x4f) {
|
|
67
|
+
rex = b;
|
|
68
|
+
p++;
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
const op = input[p], m = input[p + 1];
|
|
74
|
+
let form, displacement, length, operand, bits;
|
|
75
|
+
if (op === 0xe8) {
|
|
76
|
+
form = 'call-rel32';
|
|
77
|
+
length = p + 5;
|
|
78
|
+
displacement = p + 1;
|
|
79
|
+
operand = 0;
|
|
80
|
+
bits = 64;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
if (m === undefined || (m & 0xc7) !== 5)
|
|
84
|
+
return;
|
|
85
|
+
if (op === 0x8d) {
|
|
86
|
+
form = 'lea-rip';
|
|
87
|
+
operand = 1;
|
|
88
|
+
}
|
|
89
|
+
else if (op !== undefined && [0x88, 0x89, 0x8a, 0x8b].includes(op)) {
|
|
90
|
+
form = 'mov-rip';
|
|
91
|
+
operand = op === 0x88 || op === 0x89 ? 0 : 1;
|
|
92
|
+
}
|
|
93
|
+
else if (op === 0xff && ((m >> 3) & 7) === 2) {
|
|
94
|
+
form = 'call-rip-slot';
|
|
95
|
+
operand = 0;
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
return;
|
|
99
|
+
length = p + 6;
|
|
100
|
+
displacement = p + 2;
|
|
101
|
+
bits = form === 'call-rip-slot' ? 64 : op === 0x88 || op === 0x8a ? 8 : rex & 8 ? 64 : word ? 16 : 32;
|
|
102
|
+
}
|
|
103
|
+
if (length > 15 || input.length < length)
|
|
104
|
+
return;
|
|
105
|
+
return { form, length, displacement, operand, bits, delta: input.readInt32LE(displacement), role: form === 'call-rel32' ? 'direct-call-target' : form === 'call-rip-slot' ? 'pointer-slot' : 'effective-address' };
|
|
106
|
+
}
|
|
107
|
+
export function referenceResult(raw, r) {
|
|
108
|
+
const v = object(raw), s = object(v.scan), q = r.query, b = r.budgets;
|
|
109
|
+
if (v.algorithm !== referenceAlgorithm || typeof v.language !== 'string' || !v.language.startsWith('x86:LE:64:') || stable(v.forms) !== stable(q.forms) || !Array.isArray(v.matches) || v.matches.length > b.limit || object(v.read_only ?? {}).modification_unchanged !== true)
|
|
110
|
+
fail('partial', 'Malformed code-reference result.');
|
|
111
|
+
for (const k of ['bytes_read', 'tested_starts', 'visited_candidates'])
|
|
112
|
+
if (!Number.isSafeInteger(s[k]) || Number(s[k]) < 0)
|
|
113
|
+
fail('partial', 'Malformed reference scan counters.');
|
|
114
|
+
const complete = s.complete;
|
|
115
|
+
if (typeof complete !== 'boolean' || Number(s.bytes_read) > b.max_bytes || Number(s.tested_starts) > Number(s.bytes_read) || Number(s.visited_candidates) > b.max_candidates || Number(s.visited_candidates) < v.matches.length || Number(s.visited_candidates) > Number(s.tested_starts) || !['exhausted', 'byte_limit', 'candidate_limit', 'timeout', 'result_limit'].includes(String(s.reason)) || complete !== (s.reason === 'exhausted') || !Number.isFinite(s.elapsed_ms) || Number(s.elapsed_ms) < 0 || hex(s.from) !== r.from || hex(s.through) !== q.end)
|
|
116
|
+
fail('partial', 'Inconsistent code-reference coverage.');
|
|
117
|
+
const next = complete ? null : hex(s.next_start);
|
|
118
|
+
if (complete && s.next_start !== null || next !== null && (big(next) < big(r.from) || big(next) > big(q.end)) || next !== null && BigInt(Number(s.tested_starts)) > big(next) - big(r.from))
|
|
119
|
+
fail('partial', 'Reference frontier changed.');
|
|
120
|
+
let previous = -1n;
|
|
121
|
+
for (const rawRow of v.matches) {
|
|
122
|
+
const row = object(rawRow), from = hex(row.from), to = hex(row.to), bytes = row.bytes;
|
|
123
|
+
if (typeof bytes !== 'string' || !/^(?:[a-f0-9]{2}){1,15}$/.test(bytes))
|
|
124
|
+
fail('partial', 'Reference omitted its instruction bytes.');
|
|
125
|
+
const encoding = referenceEncoding(Buffer.from(bytes, 'hex'));
|
|
126
|
+
if (!encoding || row.form !== encoding.form || !q.forms.includes(encoding.form) || row.length !== encoding.length || bytes.length !== encoding.length * 2 || row.operand_index !== encoding.operand || row.operand_bits !== encoding.bits || row.target_role !== encoding.role || !q.targets.includes(to) || big(to) !== BigInt.asUintN(64, big(from) + BigInt(encoding.length + encoding.delta)) || big(from) <= previous || big(from) < big(r.from) || big(from) + BigInt(encoding.length - 1) > big(q.end) || next !== null && big(from) >= big(next) || !['listing-confirmed', 'listing-conflict', 'unconfirmed'].includes(String(row.boundary)) || row.validation !== 'decoded' || typeof row.instruction !== 'string' || row.instruction.length > 512)
|
|
127
|
+
fail('partial', 'Reference bytes, target, form or frontier are inconsistent.');
|
|
128
|
+
previous = big(from);
|
|
129
|
+
}
|
|
130
|
+
if (Buffer.byteLength(JSON.stringify(v)) > 1048576)
|
|
131
|
+
fail('limit', 'Code-reference response exceeds one MiB.');
|
|
132
|
+
return v;
|
|
133
|
+
}
|
|
134
|
+
export async function inspectCodeReferences(backend, who, r, view, observation) {
|
|
135
|
+
if (r.expectedView !== undefined && r.expectedView !== view)
|
|
136
|
+
fail('binding', 'Code-reference continuation view changed.', { reason: 'changed_view' });
|
|
137
|
+
const q = r.query, result = await backend.call(who, '/code_references', { targets: q.targets, forms: q.forms, scan_from: r.from, scan_through: q.end, ...r.budgets });
|
|
138
|
+
if (digest(observation) !== digest(await backend.observation(who)))
|
|
139
|
+
fail('binding', 'Native view changed during code-reference scan.', { reason: 'changed_view' });
|
|
140
|
+
const value = referenceResult(result, r), s = object(value.scan);
|
|
141
|
+
const cursor = s.complete ? null : 'cr1.' + Buffer.from(stable({ version: 1, algorithm: referenceAlgorithm, scope: who, query: q, next: hex(s.next_start), view, budgets: r.budgets })).toString('base64url');
|
|
142
|
+
return { ...who, value: { ...value, view: { identity: view }, cursor } };
|
|
143
|
+
}
|
|
144
|
+
export function codeReferenceCommands(research) {
|
|
145
|
+
return [command('analysis', 'Static analysis', [command('code-references', 'Find bounded x86-64 byte reference candidates without changing Listing or xrefs', ({ options: o }, c) => { const who = scope(o); return research(c).codeReferences(who, referenceRequest(o, who)); }, { permission: Permission.ReadOnly, options: [...selectedOptions,
|
|
146
|
+
{ flags: '--to <addresses>', description: '1–64 distinct exact default-space targets, comma separated' },
|
|
147
|
+
{ flags: '--forms <forms>', description: 'call-rel32,lea-rip,mov-rip,call-rip-slot; defaults to all four' },
|
|
148
|
+
{ flags: '--scan-from <address>', description: 'First candidate start (initial request)' }, { flags: '--scan-through <address>', description: 'Inclusive final byte; matches must fit completely' },
|
|
149
|
+
{ flags: '--cursor <token>', description: 'Stateless continuation; excludes targets/forms/range selectors' },
|
|
150
|
+
{ flags: '--max-bytes <count>', description: '15–16777216 bytes read including lookahead; default 4194304' },
|
|
151
|
+
{ flags: '--max-candidates <count>', description: '1–1000000 prefiltered starts including rejected candidates; default 200000' },
|
|
152
|
+
{ flags: '--timeout-seconds <count>', description: '1–30 second cooperative native scan deadline; default 5' },
|
|
153
|
+
{ flags: '--limit <count>', description: '1–1000 matches per call; default 100' }] })])];
|
|
154
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { type Research } from './research.js';
|
|
3
|
+
import type { Workspace, Scope } from './workspace.js';
|
|
4
|
+
export type StopReason = 'explicit_close' | 'view_replaced' | 'mode_changed' | 'owner_dispose' | 'startup_failed' | 'temporary_complete' | 'legacy_cleanup' | 'unknown';
|
|
5
|
+
type Event = {
|
|
6
|
+
at: string;
|
|
7
|
+
event: 'start' | 'restore' | 'stop';
|
|
8
|
+
reason: string;
|
|
9
|
+
pid: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function recordLifecycle(workspace: Workspace, context: string, event: Event): Promise<void>;
|
|
12
|
+
export declare function lifecycleDiagnostic(workspace: Workspace, selected: Scope): Promise<{
|
|
13
|
+
lifecycle: {
|
|
14
|
+
ownerShutdownCause: string;
|
|
15
|
+
coverage: string;
|
|
16
|
+
latest?: Record<string, unknown> | undefined;
|
|
17
|
+
events: Event[];
|
|
18
|
+
maximumEvents: number;
|
|
19
|
+
};
|
|
20
|
+
session: string;
|
|
21
|
+
target: string;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function navigationResult(raw: unknown, maximum: number): Record<string, unknown>;
|
|
24
|
+
export declare function contextDiagnosticCommands(research: (c: CommandContext) => Research, workspace: (c: CommandContext) => Workspace): CommandDefinition[];
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { mkdir, readFile, writeFile, rename, realpath } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve, sep } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { scope, number } from './research.js';
|
|
6
|
+
import { object, fail, stable } from './model.js';
|
|
7
|
+
import { selectedOptions } from './arguments.js';
|
|
8
|
+
const writes = new Map();
|
|
9
|
+
const journal = (workspace, context) => join(workspace.directory, 'diagnostics', 'lifecycle', context + '.json');
|
|
10
|
+
const reasons = new Set(['explicit_close', 'view_replaced', 'mode_changed', 'owner_dispose', 'startup_failed', 'temporary_complete', 'legacy_cleanup', 'unknown', 'native_launch', 'saved_snapshot']);
|
|
11
|
+
async function events(path) {
|
|
12
|
+
try {
|
|
13
|
+
const rows = JSON.parse(await readFile(path, 'utf8'));
|
|
14
|
+
if (!Array.isArray(rows) || rows.length > 128 || rows.some(row => !row || typeof row.at !== 'string' || row.at.length > 64 || !Number.isFinite(Date.parse(row.at)) || !['start', 'restore', 'stop'].includes(row.event) || !reasons.has(row.reason) || !Number.isSafeInteger(row.pid) || row.pid < 1))
|
|
15
|
+
fail('binding', 'Retained lifecycle observations are malformed.');
|
|
16
|
+
return rows.map(({ at, event, reason, pid }) => ({ at, event, reason, pid }));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return [];
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function recordLifecycle(workspace, context, event) {
|
|
25
|
+
const path = journal(workspace, context), previous = writes.get(path) ?? Promise.resolve();
|
|
26
|
+
const pending = previous.catch(() => { }).then(async () => { const rows = [...await events(path), event].slice(-128), directory = join(workspace.directory, 'diagnostics', 'lifecycle'); await mkdir(directory, { recursive: true, mode: 0o700 }); const temporary = path + '.new-' + randomUUID(); await writeFile(temporary, JSON.stringify(rows) + '\n', { mode: 0o600, flag: 'wx' }); await rename(temporary, path); });
|
|
27
|
+
writes.set(path, pending);
|
|
28
|
+
try {
|
|
29
|
+
await pending;
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
finally {
|
|
33
|
+
if (writes.get(path) === pending)
|
|
34
|
+
writes.delete(path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function lifecycleDiagnostic(workspace, selected) {
|
|
38
|
+
const context = workspace.context(selected), root = join(workspace.directory, 'ghidra-native', 'context', context.id);
|
|
39
|
+
let latest;
|
|
40
|
+
try {
|
|
41
|
+
const binding = object(JSON.parse(await readFile(join(root, 'current.json'), 'utf8')));
|
|
42
|
+
if (binding.context !== context.id || binding.target !== selected.target || binding.session !== undefined && binding.session !== selected.session || typeof binding.root !== 'string' || !Number.isSafeInteger(binding.pid) || typeof binding.tokenSecret !== 'string')
|
|
43
|
+
fail('binding', 'Lifecycle receipt belongs to another context.');
|
|
44
|
+
const physical = await realpath(binding.root);
|
|
45
|
+
if (physical !== resolve(binding.root) || !physical.startsWith(resolve(root) + sep))
|
|
46
|
+
fail('binding', 'Lifecycle receipt moved outside its context.');
|
|
47
|
+
const persisted = object(JSON.parse(await readFile(join(physical, 'process.json'), 'utf8')));
|
|
48
|
+
if (stable(persisted) !== stable(binding))
|
|
49
|
+
fail('binding', 'Lifecycle process receipt changed.');
|
|
50
|
+
latest = { pid: binding.pid, state: 'unobserved', reason: 'unknown' };
|
|
51
|
+
try {
|
|
52
|
+
const stopped = object(JSON.parse(await readFile(join(physical, 'stopped.json'), 'utf8')));
|
|
53
|
+
if (stopped.pid === binding.pid && stopped.tokenSecret === binding.tokenSecret)
|
|
54
|
+
latest = { pid: binding.pid, state: 'stopped', reason: reasons.has(String(stopped.reason)) ? stopped.reason : 'unknown', ...(typeof stopped.at === 'string' ? { at: stopped.at } : {}) };
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code !== 'ENOENT')
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code !== 'ENOENT')
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return { ...selected, lifecycle: { events: await events(journal(workspace, context.id)), maximumEvents: 128, ...(latest ? { latest } : {}), ownerShutdownCause: 'unknown', coverage: 'Retained facade observations, at most the latest 128 events; absent events and historical causes remain unknown. This read neither starts nor probes a native process.' } };
|
|
66
|
+
}
|
|
67
|
+
export function navigationResult(raw, maximum) {
|
|
68
|
+
const value = object(raw), scan = object(value.scan), functions = object(value.functions);
|
|
69
|
+
const samples = ['functions', 'instructions', 'references', 'definedData'].map(key => object(value[key]));
|
|
70
|
+
if (samples.some(row => !Number.isSafeInteger(row.sampled) || Number(row.sampled) < 0 || Number(row.sampled) > 128 || typeof row.complete !== 'boolean') ||
|
|
71
|
+
!Number.isSafeInteger(scan.visited) || Number(scan.visited) !== samples.reduce((n, row) => n + Number(row.sampled), 0) || Number(scan.visited) > maximum || scan.maximum !== maximum || scan.sampleLimitPerCategory !== 128 || typeof scan.complete !== 'boolean' || scan.complete !== samples.every(row => row.complete) || !['budget', 'sample_limit', 'exhausted'].includes(String(scan.reason)) ||
|
|
72
|
+
!Number.isSafeInteger(functions.oneByteBodies) || Number(functions.oneByteBodies) < 0 || Number(functions.oneByteBodies) > Number(functions.sampled) || !Number.isSafeInteger(functions.entriesWithInstructions) || Number(functions.entriesWithInstructions) < 0 || Number(functions.entriesWithInstructions) > Number(functions.sampled) || !Number.isSafeInteger(functions.bodyBytes) || Number(functions.bodyBytes) < 0 ||
|
|
73
|
+
!Number.isSafeInteger(samples[3].strings) || Number(samples[3].strings) < 0 || Number(samples[3].strings) > Number(samples[3].sampled) || !['sparse-metadata', 'no-code-observed', 'observed-metadata'].includes(String(value.assessment)) || !Array.isArray(value.recommendations) || value.recommendations.some(v => typeof v !== 'string') || object(value.read_only ?? {}).modification_unchanged !== true)
|
|
74
|
+
fail('partial', 'Malformed navigation diagnostic.');
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
export function contextDiagnosticCommands(research, workspace) {
|
|
78
|
+
return [
|
|
79
|
+
command('analysis', 'Analysis observations', [command('navigation', 'Inspect bounded navigation metadata; ready is not analysis completeness', ({ options: o }, c) => research(c).navigation(scope(o), number(o.maxScan ?? 10000, 200000, 1)), { permission: Permission.ReadOnly, options: [...selectedOptions, { flags: '--max-scan <count>', description: 'Maximum visited metadata inputs across all samples, 1–200000', defaultValue: 10000 }] })]),
|
|
80
|
+
command('diagnostics', 'Inspect retained context diagnostics', [command('lifecycle', 'Read retained start/restore/stop reasons without starting Ghidra', ({ options: o }, c) => lifecycleDiagnostic(workspace(c), scope(o)), { permission: Permission.ReadOnly, options: selectedOptions })]),
|
|
81
|
+
];
|
|
82
|
+
}
|
package/dist/src/contexts.d.ts
CHANGED
|
@@ -62,7 +62,19 @@ export declare class GhidraContexts {
|
|
|
62
62
|
private privateCopy;
|
|
63
63
|
open(scope: Scope, request?: string): Promise<GhidraContext>;
|
|
64
64
|
private selected;
|
|
65
|
-
observation(scope: Scope): Promise<
|
|
65
|
+
observation(scope: Scope): Promise<{
|
|
66
|
+
incarnation: string;
|
|
67
|
+
}>;
|
|
68
|
+
inspectObservation(scope: Scope): Promise<{
|
|
69
|
+
incarnation: string;
|
|
70
|
+
modification: string;
|
|
71
|
+
language: unknown;
|
|
72
|
+
compiler: unknown;
|
|
73
|
+
}>;
|
|
74
|
+
inlineScript(scope: Scope, parameters: Record<string, unknown>, requestId: string): Promise<{
|
|
75
|
+
response: Record<string, unknown>;
|
|
76
|
+
safe: boolean;
|
|
77
|
+
}>;
|
|
66
78
|
call(scope: Scope, operation: string, parameters?: Record<string, unknown>): Promise<unknown>;
|
|
67
79
|
saved(scope: Scope): Promise<Artifact>;
|
|
68
80
|
scripts(scope: Scope, filter: string, limit: number, offset: number): Promise<import("./scripts.js").ScriptPage>;
|
package/dist/src/contexts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordLifecycle } from './context-diagnostics.js';
|
|
1
2
|
import { ghidraBudget } from './budgets.js';
|
|
2
3
|
import { launchProcess, terminateProcess } from './tool-runtime/supervisor.js';
|
|
3
4
|
import { runTool } from './tool-runtime/run.js';
|
|
@@ -11,7 +12,7 @@ import { createServer } from 'node:net';
|
|
|
11
12
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
12
13
|
import { nativeHelpers, nativeInputs, nativeJvmDirectories, nativeExtensionArguments } from './native-inputs.js';
|
|
13
14
|
import { GhidraServer } from './server.js';
|
|
14
|
-
import { object, fail, text, version, stable } from './model.js';
|
|
15
|
+
import { object, fail, text, version, stable, digest } from './model.js';
|
|
15
16
|
import { HttpBridge } from './transport.js';
|
|
16
17
|
import { scriptInventory, scriptPath } from './scripts.js';
|
|
17
18
|
import { logicalPrograms } from './cross-results.js';
|
|
@@ -21,6 +22,8 @@ import { readCallerGoal, awaitCaller } from './debugger-caller.js';
|
|
|
21
22
|
import { debuggerProgramMetadata } from './debugger-results.js';
|
|
22
23
|
import { nativeQueries } from './query-policy.js';
|
|
23
24
|
import { nativeQuery } from './native-queries.js';
|
|
25
|
+
import { runInline } from './inline-script.js';
|
|
26
|
+
import { researchView } from './research-view.js';
|
|
24
27
|
import { sdkWrites } from './sdk-contracts.js';
|
|
25
28
|
import { initialIntent, initializeModel } from './initial-model.js';
|
|
26
29
|
import { managedGhidraDebugger, prepareGhidraDebugger } from './ghidra-debugger-runtime.js';
|
|
@@ -149,6 +152,8 @@ export class GhidraContexts {
|
|
|
149
152
|
const info = await record.http.instance();
|
|
150
153
|
if (info.pid !== binding.pid || info.tcp_port !== endpointPort)
|
|
151
154
|
fail('binding', 'Context endpoint belongs to a different native process.');
|
|
155
|
+
if (context)
|
|
156
|
+
await recordLifecycle(this.workspace, context, { at: new Date().toISOString(), event: 'start', reason: 'native_launch', pid: binding.pid });
|
|
152
157
|
return record;
|
|
153
158
|
}
|
|
154
159
|
catch {
|
|
@@ -162,6 +167,11 @@ export class GhidraContexts {
|
|
|
162
167
|
child.kill('SIGTERM');
|
|
163
168
|
for (let i = 0; i < 100 && child.exitCode === null && child.signalCode === null; i++)
|
|
164
169
|
await delay(50);
|
|
170
|
+
if (context && (child.exitCode !== null || child.signalCode !== null)) {
|
|
171
|
+
const at = new Date().toISOString();
|
|
172
|
+
await durable(join(root, 'stopped.json'), { pid: binding.pid, tokenSecret: binding.tokenSecret, at, reason: 'startup_failed' });
|
|
173
|
+
await recordLifecycle(this.workspace, context, { at, event: 'stop', reason: 'startup_failed', pid: binding.pid });
|
|
174
|
+
}
|
|
165
175
|
this.running.delete(root);
|
|
166
176
|
throw error;
|
|
167
177
|
}
|
|
@@ -190,7 +200,7 @@ export class GhidraContexts {
|
|
|
190
200
|
fail('binding', 'Opened native program does not match the target input bytes.');
|
|
191
201
|
return object(await record.http.call('/get_metadata', { program: record.binding.program }));
|
|
192
202
|
}
|
|
193
|
-
async stopRecord(record, ready = true, releaseCapacity = true) {
|
|
203
|
+
async stopRecord(record, ready = true, releaseCapacity = true, reason = 'temporary_complete') {
|
|
194
204
|
const { binding, child } = record;
|
|
195
205
|
const childFinished = child && child.pid === binding.pid && (child.exitCode !== null || child.signalCode !== null);
|
|
196
206
|
if (!childFinished && alive(binding.pid)) {
|
|
@@ -213,7 +223,10 @@ export class GhidraContexts {
|
|
|
213
223
|
}
|
|
214
224
|
if (binding.mode === 'gui')
|
|
215
225
|
await this.debuggerReleased(binding);
|
|
216
|
-
|
|
226
|
+
const at = new Date().toISOString();
|
|
227
|
+
await durable(join(binding.root, 'stopped.json'), { pid: binding.pid, tokenSecret: binding.tokenSecret, at, reason });
|
|
228
|
+
if (binding.context)
|
|
229
|
+
await recordLifecycle(this.workspace, binding.context, { at, event: 'stop', reason, pid: binding.pid });
|
|
217
230
|
if (releaseCapacity && binding.context)
|
|
218
231
|
this.capacity.slots.delete(binding.context);
|
|
219
232
|
this.running.delete(binding.root);
|
|
@@ -378,11 +391,13 @@ export class GhidraContexts {
|
|
|
378
391
|
record.binding.viewEpoch = context.viewEpoch ?? 0;
|
|
379
392
|
await durable(join(root, 'process.json'), record.binding);
|
|
380
393
|
await durable(join(this.directory(), 'context', context.id, 'current.json'), record.binding);
|
|
394
|
+
if (context.workingSnapshot)
|
|
395
|
+
await recordLifecycle(this.workspace, context.id, { at: new Date().toISOString(), event: 'restore', reason: 'saved_snapshot', pid: record.binding.pid });
|
|
381
396
|
return record;
|
|
382
397
|
}
|
|
383
398
|
catch (error) {
|
|
384
399
|
if (record)
|
|
385
|
-
await this.stopRecord(record, false);
|
|
400
|
+
await this.stopRecord(record, false, true, 'startup_failed');
|
|
386
401
|
else
|
|
387
402
|
this.capacity.slots.delete(context.id);
|
|
388
403
|
throw error;
|
|
@@ -413,7 +428,7 @@ export class GhidraContexts {
|
|
|
413
428
|
}
|
|
414
429
|
if (binding && !stopped && alive(binding.pid) && ((binding.viewEpoch ?? 0) !== (context.viewEpoch ?? 0) ||
|
|
415
430
|
binding.baseRevision !== context.baseRevision && context.baseRevision !== undefined)) {
|
|
416
|
-
await this.stopRecord(await this.verified(binding, target), true, false);
|
|
431
|
+
await this.stopRecord(await this.verified(binding, target), true, false, 'view_replaced');
|
|
417
432
|
stopped = true;
|
|
418
433
|
}
|
|
419
434
|
if (binding && !stopped && alive(binding.pid)) {
|
|
@@ -434,6 +449,8 @@ export class GhidraContexts {
|
|
|
434
449
|
const snapshot = context.workingSnapshot ?? model.sharedSnapshot;
|
|
435
450
|
if (!snapshot || !model.sharedRevision)
|
|
436
451
|
fail('binding', 'Canonical target has no saved base.');
|
|
452
|
+
if (binding && !stopped && !alive(binding.pid))
|
|
453
|
+
await recordLifecycle(this.workspace, context.id, { at: new Date().toISOString(), event: 'stop', reason: 'unknown', pid: binding.pid });
|
|
437
454
|
await this.privateCopy(context, snapshot);
|
|
438
455
|
if (context.workingSnapshot)
|
|
439
456
|
return this.workspace.saveContext(scope, context.generation, { state: 'ready' });
|
|
@@ -447,7 +464,19 @@ export class GhidraContexts {
|
|
|
447
464
|
fail('binding', 'Context process binding changed.');
|
|
448
465
|
return this.verified(binding, this.workspace.target(scope.target));
|
|
449
466
|
}
|
|
450
|
-
async observation(scope) {
|
|
467
|
+
async observation(scope) {
|
|
468
|
+
const record = await this.selected(scope);
|
|
469
|
+
return { ...object(await this.call(scope, '/research_view')), incarnation: digest({ pid: record.binding.pid, root: record.binding.root }) };
|
|
470
|
+
}
|
|
471
|
+
async inspectObservation(scope) {
|
|
472
|
+
const record = await this.selected(scope);
|
|
473
|
+
const view = researchView(await record.http.call('/insight/research-view/v1', { program: record.binding.program }));
|
|
474
|
+
return { ...view, incarnation: digest({ pid: record.binding.pid, root: record.binding.root }) };
|
|
475
|
+
}
|
|
476
|
+
async inlineScript(scope, parameters, requestId) {
|
|
477
|
+
const record = await this.selected(scope);
|
|
478
|
+
return runInline(record.http, record.binding.program, record.binding.root, parameters, requestId);
|
|
479
|
+
}
|
|
451
480
|
async call(scope, operation, parameters = {}) {
|
|
452
481
|
if (Object.hasOwn(parameters, 'program'))
|
|
453
482
|
fail('invalid', 'Program routing is owned by the session and target selectors.');
|
|
@@ -652,7 +681,7 @@ public class ${name} extends GhidraScript {
|
|
|
652
681
|
const debuggerState = record.binding.mode === 'gui' && context.debugger?.traces.length ? await this.debuggerSaved(scope) : context.debugger;
|
|
653
682
|
const complexChanges = await this.debuggerMetadataChanges(context, snapshot);
|
|
654
683
|
this.workspace.saveContext(scope, context.generation, { workingSnapshot: snapshot, complexChanges, ...(debuggerState ? { debugger: { ...debuggerState, connected: false, traces: debuggerState.traces.map(trace => ({ ...trace, connected: false })) } } : {}) });
|
|
655
|
-
await this.stopRecord(record, true, false);
|
|
684
|
+
await this.stopRecord(record, true, false, 'mode_changed');
|
|
656
685
|
if (mode === 'headless') {
|
|
657
686
|
await this.privateCopy(this.workspace.context(scope), snapshot);
|
|
658
687
|
return;
|
|
@@ -868,7 +897,7 @@ public class ${name} extends GhidraScript {
|
|
|
868
897
|
const debuggerState = record.binding.mode === 'gui' && context.debugger?.traces.length ? await this.debuggerSaved(scope) : context.debugger;
|
|
869
898
|
const complexChanges = await this.debuggerMetadataChanges(context, snapshot);
|
|
870
899
|
this.workspace.saveContext(scope, context.generation, { state: 'stopped', workingSnapshot: snapshot, complexChanges, ...(debuggerState ? { debugger: { ...debuggerState, connected: false, traces: debuggerState.traces.map(trace => ({ ...trace, connected: false })) } } : {}) });
|
|
871
|
-
await this.stopRecord(record);
|
|
900
|
+
await this.stopRecord(record, true, true, 'explicit_close');
|
|
872
901
|
}
|
|
873
902
|
async closed(scope) {
|
|
874
903
|
const context = this.workspace.context(scope), path = join(this.directory(), 'context', context.id, 'current.json');
|
|
@@ -1050,7 +1079,7 @@ public class ${name} extends GhidraScript {
|
|
|
1050
1079
|
for (const record of [...this.running.values()]) {
|
|
1051
1080
|
try {
|
|
1052
1081
|
// Original child handles also permit cleanup of a failed, unready owned stage.
|
|
1053
|
-
await cleanup.stopRecord(record, !record.child);
|
|
1082
|
+
await cleanup.stopRecord(record, !record.child, true, 'owner_dispose');
|
|
1054
1083
|
}
|
|
1055
1084
|
catch {
|
|
1056
1085
|
failures++;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LocatedEntry } from './function-entries.js';
|
|
2
|
+
/** Validate evidence and selection policy independently, before any function inspection/save. */
|
|
3
|
+
export declare function locatedBoundaries(raw: unknown, request: {
|
|
4
|
+
addresses: string[];
|
|
5
|
+
unwindMaximum: number;
|
|
6
|
+
}, modification: string): {
|
|
7
|
+
entries: Map<string, LocatedEntry>;
|
|
8
|
+
boundaryLookup: Record<string, unknown>;
|
|
9
|
+
};
|