@eyeauras/insight-cli 0.3.2 → 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/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/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-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-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-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-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-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-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-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/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
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fail, object } from './model.js';
|
|
2
|
+
const hex = (v) => { if (typeof v !== 'string' || !/^(?:0|[1-9a-f][a-f0-9]{0,15})$/.test(v))
|
|
3
|
+
fail('partial', 'Malformed runtime boundary address.'); return v; };
|
|
4
|
+
const big = (v) => BigInt('0x' + v);
|
|
5
|
+
const unavailable = new Set(['headers_not_mapped', 'unsupported_machine', 'unsupported_optional_header', 'unsupported_language', 'table_not_mapped', 'runtime_range_not_initialized_executable', 'unwind_not_mapped', 'memory_read_failed', 'short_read']);
|
|
6
|
+
const malformed = new Set(['invalid_dos_signature', 'invalid_pe_offset', 'invalid_pe_signature', 'short_optional_header', 'invalid_image_headers', 'address_overflow', 'invalid_rva', 'invalid_exception_directory', 'invalid_runtime_range', 'unordered_or_overlapping_ranges', 'invalid_unwind_rva']);
|
|
7
|
+
/** Validate evidence and selection policy independently, before any function inspection/save. */
|
|
8
|
+
export function locatedBoundaries(raw, request, modification) {
|
|
9
|
+
const v = object(raw), s = object(v.boundaryLookup), items = object(v.items);
|
|
10
|
+
if (v.protocol !== 'insight-function-boundaries-v1' || typeof v.modification !== 'string')
|
|
11
|
+
fail('partial', 'Malformed function-boundary protocol.');
|
|
12
|
+
if (v.modification !== modification)
|
|
13
|
+
fail('binding', 'Native view changed during boundary lookup.', { reason: 'changed_view' });
|
|
14
|
+
const state = String(s.state), complete = state === 'validated' || state === 'absent';
|
|
15
|
+
const reason = String(s.reason), reasonValid = state === 'validated' ? reason === 'validated' : state === 'absent' ? reason === 'no_exception_directory' : state === 'incomplete' ? ['byte_limit', 'timeout'].includes(reason) : state === 'unavailable' ? unavailable.has(reason) : state === 'malformed' && malformed.has(reason);
|
|
16
|
+
if (!reasonValid || s.complete !== complete || s.headersSource !== 'current-mapped-memory' || s.maximumBytes !== request.unwindMaximum || s.deadlineMilliseconds !== 5000)
|
|
17
|
+
fail('partial', 'Malformed function-boundary coverage.');
|
|
18
|
+
for (const key of ['recordCount', 'recordsValidated', 'bytesRead'])
|
|
19
|
+
if (!Number.isSafeInteger(s[key]) || Number(s[key]) < 0)
|
|
20
|
+
fail('partial', 'Malformed boundary counters.');
|
|
21
|
+
if (Number(s.bytesRead) > request.unwindMaximum || Number(s.recordCount) > Math.floor(0xffffffff / 12) || Number(s.recordsValidated) > Number(s.recordCount) || Number(s.recordsValidated) * 12 > Number(s.bytesRead) || state === 'validated' && (s.recordCount !== s.recordsValidated || Number(s.recordCount) < 1) || state === 'absent' && Number(s.recordCount) !== 0)
|
|
22
|
+
fail('partial', 'Inconsistent function-boundary counters.');
|
|
23
|
+
if (complete && Number(s.bytesRead) < (state === 'absent' ? 200 : 208 + 12 * Number(s.recordCount)))
|
|
24
|
+
fail('partial', 'Impossible complete boundary read accounting.');
|
|
25
|
+
if (Object.keys(items).length !== request.addresses.length || request.addresses.some(at => !Object.hasOwn(items, at)))
|
|
26
|
+
fail('binding', 'Boundary selector set changed.', { reason: 'selector_mismatch' });
|
|
27
|
+
const entries = new Map();
|
|
28
|
+
for (const at of request.addresses) {
|
|
29
|
+
const row = object(items[at]);
|
|
30
|
+
let database = null, boundary = null;
|
|
31
|
+
if (row.database !== null) {
|
|
32
|
+
database = object(row.database);
|
|
33
|
+
const entry = hex(database.entryPoint);
|
|
34
|
+
if (!['exact-entry', 'recorded-containing'].includes(String(database.kind)) || database.recordedBodyContainsAddress !== true || (database.kind === 'exact-entry') !== (entry === at))
|
|
35
|
+
fail('partial', 'Malformed recorded boundary facts.');
|
|
36
|
+
}
|
|
37
|
+
if (row.boundary !== null) {
|
|
38
|
+
boundary = object(row.boundary);
|
|
39
|
+
const lo = hex(boundary.begin), hi = boundary.endExclusive === '10000000000000000' ? boundary.endExclusive : hex(boundary.endExclusive);
|
|
40
|
+
hex(boundary.tableRecordAddress);
|
|
41
|
+
if (!complete || state !== 'validated' || Number(s.recordCount) < 1 || big(lo) > big(at) || big(at) >= big(hi) || boundary.source !== 'pe-exception-directory' || boundary.semanticMembership !== 'not-established' || boundary.instructionBoundary !== 'not-checked' || boundary.unwindInfo !== 'not-parsed')
|
|
42
|
+
fail('partial', 'Malformed or unvalidated runtime region.');
|
|
43
|
+
}
|
|
44
|
+
const conflict = boundary && database && boundary.begin !== database.entryPoint;
|
|
45
|
+
const resolution = { requestedAddress: at, kind: 'unresolved', recordedBodyContainsAddress: database !== null, database, boundary };
|
|
46
|
+
if (row.status === 'resolved') {
|
|
47
|
+
const entry = hex(row.entryPoint), kind = boundary ? 'pe-runtime-range' : database?.kind;
|
|
48
|
+
if (!complete || conflict || !kind || row.kind !== kind || entry !== (boundary?.begin ?? database?.entryPoint) || row.recordedBodyContainsAddress !== (database !== null))
|
|
49
|
+
fail('partial', 'Inconsistent runtime entry selection.');
|
|
50
|
+
entries.set(at, { entry, resolution: { ...resolution, kind: String(kind) } });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
let expected;
|
|
54
|
+
if (!complete)
|
|
55
|
+
expected = 'boundary_lookup_' + state;
|
|
56
|
+
else if (conflict)
|
|
57
|
+
expected = 'boundary_conflict';
|
|
58
|
+
else if (boundary)
|
|
59
|
+
expected = 'runtime_entry_not_defined';
|
|
60
|
+
else
|
|
61
|
+
expected = 'no_runtime_range_or_recorded_function';
|
|
62
|
+
if (row.status !== 'unresolved' || row.reason !== expected || complete && !boundary && database || expected === 'runtime_entry_not_defined' && database || row.entryPoint !== undefined || row.kind !== undefined)
|
|
63
|
+
fail('partial', 'Inconsistent unresolved runtime boundary.');
|
|
64
|
+
entries.set(at, { status: 'partial', reason: expected, resolution });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { entries, boundaryLookup: s };
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface EntryOptions {
|
|
2
|
+
resolveAddress?: string;
|
|
3
|
+
maxEntryDistance?: number;
|
|
4
|
+
maxUnwindBytes?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface LegacyEntryRequest {
|
|
7
|
+
addresses: string[];
|
|
8
|
+
maximum: number;
|
|
9
|
+
}
|
|
10
|
+
export type EntryRequest = LegacyEntryRequest | {
|
|
11
|
+
addresses: string[];
|
|
12
|
+
unwindMaximum: number;
|
|
13
|
+
};
|
|
14
|
+
export interface EntryResolution {
|
|
15
|
+
requestedAddress: string;
|
|
16
|
+
kind: string;
|
|
17
|
+
recordedBodyContainsAddress: boolean;
|
|
18
|
+
distanceBytes?: number;
|
|
19
|
+
database?: unknown;
|
|
20
|
+
boundary?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface LocatedEntry {
|
|
23
|
+
entry?: string;
|
|
24
|
+
resolution?: EntryResolution;
|
|
25
|
+
reason?: string;
|
|
26
|
+
status?: 'partial';
|
|
27
|
+
}
|
|
28
|
+
export declare function functionSelectors(parameters: Record<string, unknown>): string[];
|
|
29
|
+
export declare function entryAddress(value: unknown): string;
|
|
30
|
+
/** Validate before opening/probing native state; ordinary inspection keeps its original contract. */
|
|
31
|
+
export declare function entryRequest(parameters: Record<string, unknown>, options: EntryOptions): EntryRequest | undefined;
|
|
32
|
+
/** The authenticated native lookup supplies DB facts, never inferred semantic membership. */
|
|
33
|
+
export declare function locatedEntries(raw: unknown, request: LegacyEntryRequest, modification: string): Map<string, LocatedEntry>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { fail, object } from './model.js';
|
|
2
|
+
export function functionSelectors(parameters) {
|
|
3
|
+
const selectors = String(parameters.functions || parameters.function || '').split(',').map(s => s.trim());
|
|
4
|
+
if (!selectors.length || selectors.length > 20 || selectors.some(s => !s || s.length > 128) || new Set(selectors).size !== selectors.length)
|
|
5
|
+
fail('invalid', 'Select 1–20 distinct function names or addresses.');
|
|
6
|
+
return selectors;
|
|
7
|
+
}
|
|
8
|
+
export function entryAddress(value) {
|
|
9
|
+
if (typeof value !== 'string' || !/^(?:0x)?[a-f0-9]{1,16}$/i.test(value))
|
|
10
|
+
fail('invalid', 'Address resolution requires 1–20 default-space hexadecimal addresses, not names.');
|
|
11
|
+
return BigInt('0x' + value.replace(/^0x/i, '')).toString(16);
|
|
12
|
+
}
|
|
13
|
+
/** Validate before opening/probing native state; ordinary inspection keeps its original contract. */
|
|
14
|
+
export function entryRequest(parameters, options) {
|
|
15
|
+
if (options.resolveAddress === undefined) {
|
|
16
|
+
if (options.maxEntryDistance !== undefined || options.maxUnwindBytes !== undefined)
|
|
17
|
+
fail('invalid', 'Resolution budgets require --resolve-address.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (options.resolveAddress === 'containing-or-pe-runtime') {
|
|
21
|
+
if (options.maxEntryDistance !== undefined)
|
|
22
|
+
fail('invalid', 'PE runtime resolution excludes --max-entry-distance.');
|
|
23
|
+
const unwindMaximum = options.maxUnwindBytes ?? 4194304;
|
|
24
|
+
if (!Number.isSafeInteger(unwindMaximum) || unwindMaximum < 1 || unwindMaximum > 16777216)
|
|
25
|
+
fail('invalid', '--max-unwind-bytes must be between 1 and 16777216.');
|
|
26
|
+
if (parameters.function && parameters.functions)
|
|
27
|
+
fail('invalid', 'Supply --function or --functions, not both, for address resolution.');
|
|
28
|
+
return { addresses: [...new Set(functionSelectors(parameters).map(entryAddress))], unwindMaximum };
|
|
29
|
+
}
|
|
30
|
+
if (options.maxUnwindBytes !== undefined)
|
|
31
|
+
fail('invalid', '--max-unwind-bytes requires containing-or-pe-runtime.');
|
|
32
|
+
if (options.resolveAddress !== 'containing-or-preceding')
|
|
33
|
+
fail('invalid', '--resolve-address must be containing-or-preceding.');
|
|
34
|
+
const maximum = options.maxEntryDistance;
|
|
35
|
+
if (typeof maximum !== 'number' || !Number.isSafeInteger(maximum) || maximum < 1 || maximum > 65536)
|
|
36
|
+
fail('invalid', 'Address resolution requires --max-entry-distance between 1 and 65536 bytes.');
|
|
37
|
+
if (parameters.function && parameters.functions)
|
|
38
|
+
fail('invalid', 'Supply --function or --functions, not both, for address resolution.');
|
|
39
|
+
return { addresses: [...new Set(functionSelectors(parameters).map(entryAddress))], maximum };
|
|
40
|
+
}
|
|
41
|
+
const misses = new Set(['address_out_of_range', 'address_not_in_initialized_executable_block', 'no_preceding_entry', 'entry_outside_block', 'entry_distance_exceeded']);
|
|
42
|
+
/** The authenticated native lookup supplies DB facts, never inferred semantic membership. */
|
|
43
|
+
export function locatedEntries(raw, request, modification) {
|
|
44
|
+
const result = object(raw), items = object(result.items);
|
|
45
|
+
if (result.protocol !== 'insight-function-entries-v1' || typeof result.modification !== 'string')
|
|
46
|
+
fail('partial', 'Function-entry lookup returned malformed protocol facts.');
|
|
47
|
+
if (result.modification !== modification)
|
|
48
|
+
fail('binding', 'Native view changed during function-entry lookup.', { reason: 'changed_view' });
|
|
49
|
+
if (Object.keys(items).length !== request.addresses.length ||
|
|
50
|
+
request.addresses.some(at => !Object.hasOwn(items, at)))
|
|
51
|
+
fail('binding', 'Function-entry lookup returned another view or selector set.', { reason: 'selector_mismatch' });
|
|
52
|
+
const out = new Map();
|
|
53
|
+
for (const at of request.addresses) {
|
|
54
|
+
const row = object(items[at]);
|
|
55
|
+
if (row.status === 'not_found') {
|
|
56
|
+
if (typeof row.reason !== 'string' || !misses.has(row.reason))
|
|
57
|
+
fail('partial', 'Function-entry lookup returned an invalid policy miss.');
|
|
58
|
+
out.set(at, { reason: row.reason });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (row.status !== 'resolved' || typeof row.entryPoint !== 'string' || !/^[a-f0-9]{1,16}$/.test(row.entryPoint) ||
|
|
62
|
+
entryAddress(row.entryPoint) !== row.entryPoint || typeof row.recordedBodyContainsAddress !== 'boolean')
|
|
63
|
+
fail('partial', 'Function-entry lookup returned malformed entry facts.');
|
|
64
|
+
const entry = row.entryPoint, contains = row.recordedBodyContainsAddress, kind = row.kind;
|
|
65
|
+
// Address strings are hexadecimal even when they contain digits only.
|
|
66
|
+
const actualDistance = BigInt('0x' + at) - BigInt('0x' + entry);
|
|
67
|
+
if (kind === 'exact-entry') {
|
|
68
|
+
if (entry !== at || !contains || row.distanceBytes !== 0)
|
|
69
|
+
fail('partial', 'Function-entry lookup returned inconsistent exact-entry facts.');
|
|
70
|
+
}
|
|
71
|
+
else if (kind === 'recorded-containing') {
|
|
72
|
+
if (entry === at || !contains || row.distanceBytes !== undefined)
|
|
73
|
+
fail('partial', 'Function-entry lookup returned inconsistent recorded containment.');
|
|
74
|
+
}
|
|
75
|
+
else if (kind === 'preceding-entry-candidate') {
|
|
76
|
+
if (contains || actualDistance < 1n || actualDistance > BigInt(request.maximum) || row.distanceBytes !== Number(actualDistance))
|
|
77
|
+
fail('partial', 'Function-entry lookup returned an out-of-policy candidate.');
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
fail('partial', 'Function-entry lookup returned an unknown resolution kind.');
|
|
81
|
+
out.set(at, { entry, resolution: { requestedAddress: at, kind, recordedBodyContainsAddress: contains, ...(kind === 'recorded-containing' ? {} : { distanceBytes: row.distanceBytes }) } });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResearchBackend } from './research.js';
|
|
2
2
|
import type { Scope } from './workspace.js';
|
|
3
|
-
|
|
3
|
+
import { type EntryOptions } from './function-entries.js';
|
|
4
|
+
export interface FunctionInspectionOptions extends EntryOptions {
|
|
4
5
|
limit: number;
|
|
5
6
|
offset: number;
|
|
6
7
|
total: number;
|
|
@@ -10,7 +11,7 @@ export interface FunctionInspectionOptions {
|
|
|
10
11
|
export declare function inspectFunctions(backend: ResearchBackend, scope: Scope, parameters: Record<string, unknown>, options: FunctionInspectionOptions, view: {
|
|
11
12
|
revision?: string;
|
|
12
13
|
identity: string;
|
|
13
|
-
}, observation?: unknown): Promise<{
|
|
14
|
+
}, observation?: unknown, observe?: () => Promise<unknown>): Promise<{
|
|
14
15
|
value: {
|
|
15
16
|
save?: {
|
|
16
17
|
host: string;
|
|
@@ -25,6 +26,7 @@ export declare function inspectFunctions(backend: ResearchBackend, scope: Scope,
|
|
|
25
26
|
maximumCharacters: number;
|
|
26
27
|
returnedCharacters: number;
|
|
27
28
|
};
|
|
29
|
+
boundaryLookup?: Record<string, unknown> | undefined;
|
|
28
30
|
};
|
|
29
31
|
session: string;
|
|
30
32
|
target: string;
|
|
@@ -2,77 +2,112 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { resolve, join, isAbsolute } from 'node:path';
|
|
3
3
|
import { fail, object, digest } from './model.js';
|
|
4
4
|
import { functionResult } from './function-results.js';
|
|
5
|
+
import { timed } from './internal-timing.js';
|
|
5
6
|
import { textWindow } from './text-window.js';
|
|
6
7
|
import { CliError } from '@eyeauras/cli-factory';
|
|
8
|
+
import { entryAddress, entryRequest, functionSelectors, locatedEntries } from './function-entries.js';
|
|
9
|
+
import { locatedBoundaries } from './function-boundaries.js';
|
|
7
10
|
const fields = new Set('signature classification return_type entry_point body_start body_end decompiled_code plate_comment comments labels tags parameters locals callers call_context callees xrefs disassembly jump_targets refs'.split(' '));
|
|
8
|
-
export async function inspectFunctions(backend, scope, parameters, options, view, observation) {
|
|
11
|
+
export async function inspectFunctions(backend, scope, parameters, options, view, observation, observe) {
|
|
9
12
|
if (options.saveDirectory !== undefined && !isAbsolute(options.saveDirectory))
|
|
10
13
|
fail('invalid', 'Save directory must be an absolute path on the owner host.');
|
|
11
|
-
const selectors =
|
|
12
|
-
if (!selectors.length || selectors.length > 20 || selectors.some(s => !s || s.length > 128) || new Set(selectors).size !== selectors.length)
|
|
13
|
-
fail('invalid', 'Select 1–20 distinct function names or addresses.');
|
|
14
|
+
const selectors = functionSelectors(parameters), lookup = entryRequest(parameters, options);
|
|
14
15
|
const selectedFields = String(parameters.fields ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
15
16
|
if (selectedFields.some(s => !fields.has(s)))
|
|
16
17
|
fail('invalid', 'Unknown function field. Allowed fields: ' + [...fields].join(', '));
|
|
17
18
|
if (options.expectView !== undefined && options.expectView !== view.identity)
|
|
18
19
|
fail('binding', 'Function continuation view changed.', { reason: 'changed_view', view });
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (!(error instanceof CliError))
|
|
34
|
-
throw error;
|
|
35
|
-
const detail = object(error.result);
|
|
36
|
-
output[selector] = { error: detail.nativeDiagnostic ?? error.message, ...detail, status: detail.reason === 'function_not_found' ? 'not_found' : 'failed' };
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
const row = { ...found, status: 'success' };
|
|
40
|
-
if (typeof row.decompiled_code === 'string') {
|
|
41
|
-
const source = row.decompiled_code, window = textWindow(source, options.saveDirectory ? 0 : Math.min(options.limit, remaining), options.saveDirectory ? 0 : options.offset, Boolean(object(row.truncation ?? {}).decompiled_code));
|
|
42
|
-
const { content, ...metadata } = window;
|
|
43
|
-
remaining -= Array.from(content).length;
|
|
44
|
-
row.decompiled_code = content;
|
|
45
|
-
row.text = { ...metadata, returnedCharacters: Array.from(content).length, nextOffset: Math.min(window.totalCharacters, window.offset + Array.from(content).length) };
|
|
46
|
-
if (options.saveDirectory) {
|
|
47
|
-
if (Buffer.byteLength(source) > 4 * 1024 ** 2)
|
|
48
|
-
fail('limit', 'Native function text exceeds the four MiB saved-file bound.');
|
|
49
|
-
const name = String(index).padStart(2, '0') + '-' + window.textSha256 + '.c';
|
|
50
|
-
saved.push({ name, content: source });
|
|
51
|
-
delete row.decompiled_code;
|
|
52
|
-
row.savedText = { file: join(resolve(options.saveDirectory), name), textSha256: window.textSha256, totalCharacters: window.totalCharacters, sourceTruncated: window.sourceTruncated, coverage: 'entire returned native text' };
|
|
20
|
+
const observeAfter = observe ?? (backend.observation ? () => backend.observation(scope) : undefined);
|
|
21
|
+
let entries, boundaryLookup, raw, nativeSelectors;
|
|
22
|
+
const guard = async () => { if (observeAfter && digest(observation) !== digest(await observeAfter()))
|
|
23
|
+
fail('binding', 'Native view changed during bulk function inspection.', { reason: 'changed_view', view }); };
|
|
24
|
+
try {
|
|
25
|
+
if (lookup) {
|
|
26
|
+
if (!observeAfter || typeof object(observation ?? {}).modification !== 'string')
|
|
27
|
+
fail('unsupported', 'Address resolution requires guarded native observations.');
|
|
28
|
+
if ('unwindMaximum' in lookup) {
|
|
29
|
+
const result = locatedBoundaries(await backend.call(scope, '/insight/function-boundaries/v1', { addresses: lookup.addresses.join(','), max_unwind_bytes: lookup.unwindMaximum }), lookup, String(object(observation).modification));
|
|
30
|
+
entries = result.entries;
|
|
31
|
+
boundaryLookup = result.boundaryLookup;
|
|
53
32
|
}
|
|
33
|
+
else
|
|
34
|
+
entries = locatedEntries(await backend.call(scope, '/insight/function-entries/v1', { addresses: lookup.addresses.join(','), max_entry_distance: lookup.maximum }), lookup, String(object(observation).modification));
|
|
54
35
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
output[selector] = row;
|
|
36
|
+
nativeSelectors = entries ? [...new Set([...entries.values()].flatMap(row => row.entry ? [row.entry] : []))] : selectors;
|
|
37
|
+
const nativeParameters = { ...parameters, ...(entries ? { function: '' } : {}), functions: nativeSelectors.join(','), ...(entries && selectedFields.length ? { fields: [...new Set([...selectedFields, 'entry_point'])].join(',') } : {}) };
|
|
38
|
+
raw = nativeSelectors.length ? object(await backend.call(scope, '/get_functions', nativeParameters)) : { functions: {} };
|
|
60
39
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
fail('partial', String(single.error), single);
|
|
40
|
+
catch (error) {
|
|
41
|
+
await guard();
|
|
42
|
+
throw error;
|
|
65
43
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
for (const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
44
|
+
await guard();
|
|
45
|
+
return timed('adapter.normalize', 'functions.inspect', async () => {
|
|
46
|
+
const functions = object(raw.functions);
|
|
47
|
+
if (Object.keys(functions).length !== nativeSelectors.length || nativeSelectors.some(s => !Object.hasOwn(functions, s)))
|
|
48
|
+
fail('partial', 'Bulk function selectors changed.');
|
|
49
|
+
const output = Object.create(null), saved = [];
|
|
50
|
+
let remaining = options.total;
|
|
51
|
+
for (const [index, selector] of selectors.entries()) {
|
|
52
|
+
const located = entries?.get(entryAddress(selector)), selected = located?.entry ?? selector;
|
|
53
|
+
const provenance = located?.resolution ? { ...(located.entry ? { entry_point: located.entry } : {}), resolution: { ...located.resolution, requestedAddress: selector } } : {};
|
|
54
|
+
if (located?.reason) {
|
|
55
|
+
output[selector] = { status: located.status ?? 'not_found', selector, reason: located.reason, error: 'No function was selected by the explicit address-resolution policy.', ...provenance };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
let found;
|
|
59
|
+
try {
|
|
60
|
+
found = functionResult({ functions: { [selected]: functions[selected] } }, selected, { requireEntryPoint: Boolean(entries) || !selectedFields.length || selectedFields.includes('entry_point') });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error instanceof CliError))
|
|
64
|
+
throw error;
|
|
65
|
+
const detail = object(error.result);
|
|
66
|
+
output[selector] = { error: error.message, ...detail, ...provenance, ...(entries ? { selector } : {}), status: detail.reason === 'function_not_found' ? 'not_found' : 'failed' };
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (located?.entry && entryAddress(found.entry_point) !== located.entry)
|
|
70
|
+
fail('binding', 'Inspected function differs from the resolved entry.', { reason: 'selector_mismatch' });
|
|
71
|
+
const row = { ...found, ...provenance, status: 'success' };
|
|
72
|
+
if (typeof row.decompiled_code === 'string') {
|
|
73
|
+
const source = row.decompiled_code, window = textWindow(source, options.saveDirectory ? 0 : Math.min(options.limit, remaining), options.saveDirectory ? 0 : options.offset, Boolean(object(row.truncation ?? {}).decompiled_code));
|
|
74
|
+
const { content, ...metadata } = window;
|
|
75
|
+
remaining -= Array.from(content).length;
|
|
76
|
+
row.decompiled_code = content;
|
|
77
|
+
row.text = { ...metadata, returnedCharacters: Array.from(content).length, nextOffset: Math.min(window.totalCharacters, window.offset + Array.from(content).length) };
|
|
78
|
+
if (options.saveDirectory) {
|
|
79
|
+
if (Buffer.byteLength(source) > 4 * 1024 ** 2)
|
|
80
|
+
fail('limit', 'Native function text exceeds the four MiB saved-file bound.');
|
|
81
|
+
const name = String(index).padStart(2, '0') + '-' + window.textSha256 + '.c';
|
|
82
|
+
saved.push({ name, content: source });
|
|
83
|
+
delete row.decompiled_code;
|
|
84
|
+
row.savedText = { file: join(resolve(options.saveDirectory), name), textSha256: window.textSha256, totalCharacters: window.totalCharacters, sourceTruncated: window.sourceTruncated, coverage: 'entire returned native text' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (!selectedFields.length || selectedFields.includes('decompiled_code')) {
|
|
88
|
+
row.status = 'partial';
|
|
89
|
+
row.text = { reason: 'missing_decompiler_text' };
|
|
90
|
+
}
|
|
91
|
+
output[selector] = row;
|
|
92
|
+
}
|
|
93
|
+
if (!parameters.functions) {
|
|
94
|
+
const single = object(output[selectors[0]]);
|
|
95
|
+
if (single.status === 'failed' || single.status === 'not_found' || boundaryLookup && single.reason)
|
|
96
|
+
fail('partial', String(single.error), { ...single, ...(boundaryLookup ? { boundaryLookup } : {}), view });
|
|
97
|
+
}
|
|
98
|
+
const result = { ...(parameters.functions ? { ...(entries ? (typeof raw.program === 'string' ? { program: raw.program } : {}) : raw), functions: output } : object(output[selectors[0]])), ...(boundaryLookup ? { boundaryLookup } : {}), view, textBudget: { maximumCharacters: options.total, returnedCharacters: options.total - remaining }, ...(options.saveDirectory ? { save: { host: 'owner', directory: resolve(options.saveDirectory), manifest: join(resolve(options.saveDirectory), 'manifest.json') } } : {}) };
|
|
99
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 1048576)
|
|
100
|
+
fail('limit', 'Function response exceeds one MiB; narrow fields or selectors.');
|
|
101
|
+
if (options.saveDirectory) {
|
|
102
|
+
// A fresh directory prevents overwrite and makes a partial filesystem failure observable.
|
|
103
|
+
await timed('adapter.save-text', 'functions.inspect', async () => {
|
|
104
|
+
const directory = resolve(options.saveDirectory);
|
|
105
|
+
await mkdir(directory, { recursive: false });
|
|
106
|
+
for (const file of saved)
|
|
107
|
+
await writeFile(join(directory, file.name), file.content, { flag: 'wx', mode: 0o600 });
|
|
108
|
+
await writeFile(join(directory, 'manifest.json'), JSON.stringify({ ...scope, value: result }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { ...scope, value: result };
|
|
112
|
+
});
|
|
78
113
|
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function functionFailure(selector: string, reason: string, diagnostic?: unknown): never;
|
|
2
|
-
export declare function functionResult(value: unknown, selector: string
|
|
2
|
+
export declare function functionResult(value: unknown, selector: string, options?: {
|
|
3
|
+
requireEntryPoint?: boolean;
|
|
4
|
+
}): Record<string, unknown>;
|
|
@@ -2,9 +2,11 @@ import { fail } from './model.js';
|
|
|
2
2
|
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
3
3
|
export function functionFailure(selector, reason, diagnostic) {
|
|
4
4
|
const nativeDiagnostic = typeof diagnostic === 'string' ? Array.from(diagnostic.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')).slice(0, 500).join('') : undefined;
|
|
5
|
-
|
|
5
|
+
const recovery = reason === 'function_not_found' ? ' For a sparse interior address, use ghidra functions inspect --functions <address> --resolve-address containing-or-preceding --max-entry-distance <bytes> with the same session/target; a preceding entry is only a candidate. For known executable bytes without a function definition, use ghidra functions decode --address <hex> --max-bytes 64 --max-instructions 16 with the same session/target. Decode is read-only and does not create functions or xrefs.' :
|
|
6
|
+
reason === 'native_function_failure' || reason === 'missing_decompiler_text' ? ' For an existing function entry, try ghidra functions force-decompile --function <entry> with the same session/target. This refreshes decompiler output, not Listing instructions or stored xrefs; use functions decode for bounded local bytes.' : '';
|
|
7
|
+
fail('partial', 'Function inspection failed: ' + reason + '.' + recovery, { selector, reason, ...(nativeDiagnostic ? { nativeDiagnostic } : {}) });
|
|
6
8
|
}
|
|
7
|
-
export function functionResult(value, selector) {
|
|
9
|
+
export function functionResult(value, selector, options = {}) {
|
|
8
10
|
if (!record(value) || !record(value.functions))
|
|
9
11
|
functionFailure(selector, 'invalid_native_shape');
|
|
10
12
|
const functions = value.functions;
|
|
@@ -15,7 +17,11 @@ export function functionResult(value, selector) {
|
|
|
15
17
|
functionFailure(selector, 'invalid_native_shape');
|
|
16
18
|
if (found.error !== undefined)
|
|
17
19
|
functionFailure(selector, typeof found.error === 'string' && /^Function not found\b/i.test(found.error) ? 'function_not_found' : 'native_function_failure', found.error);
|
|
18
|
-
if (typeof found.name !== 'string' || !found.name
|
|
20
|
+
if (typeof found.name !== 'string' || !found.name)
|
|
19
21
|
functionFailure(selector, 'invalid_native_shape');
|
|
22
|
+
if (options.requireEntryPoint !== false || found.entry_point !== undefined) {
|
|
23
|
+
if (typeof found.entry_point !== 'string' || found.entry_point.length > 128 || !/^(?:[A-Za-z0-9_]+:)?(?:0x)?[a-fA-F0-9]+$/.test(found.entry_point))
|
|
24
|
+
functionFailure(selector, 'invalid_native_shape');
|
|
25
|
+
}
|
|
20
26
|
return found;
|
|
21
27
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HttpBridge } from './transport.js';
|
|
2
|
+
export declare const inlineSourceHash: (code: string) => string;
|
|
3
|
+
/** Native facts come from the fixed helper's receipt, never from caller console output. */
|
|
4
|
+
export declare function inlineReceipt(raw: unknown, expected: {
|
|
5
|
+
attemptId: string;
|
|
6
|
+
requestId: string;
|
|
7
|
+
sourceSha256: string;
|
|
8
|
+
}): {
|
|
9
|
+
response: Record<string, unknown>;
|
|
10
|
+
safe: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function runInline(http: HttpBridge, program: string, root: string, parameters: Record<string, unknown>, requestId: string): Promise<{
|
|
13
|
+
response: Record<string, unknown>;
|
|
14
|
+
safe: boolean;
|
|
15
|
+
}>;
|