@eyeauras/insight-cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cli-package-build.json +1 -0
- package/LICENSE +21 -0
- package/README.md +60 -2
- package/SKILL.md +58 -0
- package/dist/src/administration-results.d.ts +1 -0
- package/dist/src/administration-results.js +84 -0
- package/dist/src/administration.d.ts +14 -0
- package/dist/src/administration.js +134 -0
- package/dist/src/archive-contract.d.ts +6 -0
- package/dist/src/archive-contract.js +108 -0
- package/dist/src/archive.d.ts +12 -0
- package/dist/src/archive.js +116 -0
- package/dist/src/arguments.d.ts +14 -0
- package/dist/src/arguments.js +62 -0
- package/dist/src/artifacts.d.ts +17 -0
- package/dist/src/artifacts.js +115 -0
- package/dist/src/bin.d.ts +2 -0
- package/dist/src/bin.js +4 -0
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +419 -0
- package/dist/src/comment-effects.d.ts +3 -0
- package/dist/src/comment-effects.js +55 -0
- package/dist/src/contexts.d.ts +115 -0
- package/dist/src/contexts.js +1078 -0
- package/dist/src/coordinator.d.ts +41 -0
- package/dist/src/coordinator.js +160 -0
- package/dist/src/cross-results.d.ts +14 -0
- package/dist/src/cross-results.js +71 -0
- package/dist/src/cross.d.ts +3 -0
- package/dist/src/cross.js +43 -0
- package/dist/src/data-effects.d.ts +3 -0
- package/dist/src/data-effects.js +103 -0
- package/dist/src/debugger-results.d.ts +4 -0
- package/dist/src/debugger-results.js +151 -0
- package/dist/src/debugger-supervisor.d.ts +1 -0
- package/dist/src/debugger-supervisor.js +136 -0
- package/dist/src/debugger.d.ts +21 -0
- package/dist/src/debugger.js +267 -0
- package/dist/src/documentation-effects.d.ts +5 -0
- package/dist/src/documentation-effects.js +56 -0
- package/dist/src/extensions.d.ts +64 -0
- package/dist/src/extensions.js +289 -0
- package/dist/src/function-effects.d.ts +4 -0
- package/dist/src/function-effects.js +112 -0
- package/dist/src/ghidra.d.ts +50 -0
- package/dist/src/ghidra.js +468 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/initial-model.d.ts +6 -0
- package/dist/src/initial-model.js +85 -0
- package/dist/src/json.d.ts +2 -0
- package/dist/src/json.js +8 -0
- package/dist/src/lifecycle.d.ts +49 -0
- package/dist/src/lifecycle.js +154 -0
- package/dist/src/memory-inputs.d.ts +8 -0
- package/dist/src/memory-inputs.js +56 -0
- package/dist/src/merge.d.ts +29 -0
- package/dist/src/merge.js +66 -0
- package/dist/src/model.d.ts +74 -0
- package/dist/src/model.js +67 -0
- package/dist/src/mutation-effects.d.ts +4 -0
- package/dist/src/mutation-effects.js +321 -0
- package/dist/src/mutation-inputs.d.ts +2 -0
- package/dist/src/mutation-inputs.js +71 -0
- package/dist/src/mutations.d.ts +4 -0
- package/dist/src/mutations.js +53 -0
- package/dist/src/native-inputs.d.ts +28 -0
- package/dist/src/native-inputs.js +182 -0
- package/dist/src/native-queries.d.ts +2 -0
- package/dist/src/native-queries.js +29 -0
- package/dist/src/projects.d.ts +31 -0
- package/dist/src/projects.js +301 -0
- package/dist/src/prototype-cli.d.ts +8 -0
- package/dist/src/prototype-cli.js +155 -0
- package/dist/src/queries.d.ts +5 -0
- package/dist/src/queries.js +84 -0
- package/dist/src/query-policy.d.ts +6 -0
- package/dist/src/query-policy.js +72 -0
- package/dist/src/query-results.d.ts +2 -0
- package/dist/src/query-results.js +224 -0
- package/dist/src/research.d.ts +184 -0
- package/dist/src/research.js +808 -0
- package/dist/src/runtime-archives.d.ts +5 -0
- package/dist/src/runtime-archives.js +127 -0
- package/dist/src/runtime-catalog.d.ts +18 -0
- package/dist/src/runtime-catalog.js +27 -0
- package/dist/src/runtime-manager.d.ts +101 -0
- package/dist/src/runtime-manager.js +444 -0
- package/dist/src/runtime-selection.d.ts +13 -0
- package/dist/src/runtime-selection.js +25 -0
- package/dist/src/saved-materials.d.ts +12 -0
- package/dist/src/saved-materials.js +196 -0
- package/dist/src/scripts.d.ts +19 -0
- package/dist/src/scripts.js +79 -0
- package/dist/src/sdk-contracts.d.ts +13 -0
- package/dist/src/sdk-contracts.js +37 -0
- package/dist/src/sdk-files.d.ts +29 -0
- package/dist/src/sdk-files.js +249 -0
- package/dist/src/sdk.d.ts +4 -0
- package/dist/src/sdk.js +125 -0
- package/dist/src/server.d.ts +46 -0
- package/dist/src/server.js +460 -0
- package/dist/src/store.d.ts +33 -0
- package/dist/src/store.js +178 -0
- package/dist/src/sync-policy.d.ts +22 -0
- package/dist/src/sync-policy.js +123 -0
- package/dist/src/transport.d.ts +23 -0
- package/dist/src/transport.js +135 -0
- package/dist/src/ui-results.d.ts +2 -0
- package/dist/src/ui-results.js +80 -0
- package/dist/src/ui.d.ts +17 -0
- package/dist/src/ui.js +88 -0
- package/dist/src/utility.d.ts +14 -0
- package/dist/src/utility.js +43 -0
- package/dist/src/version-tracking.d.ts +57 -0
- package/dist/src/version-tracking.js +200 -0
- package/dist/src/workspace.d.ts +199 -0
- package/dist/src/workspace.js +564 -0
- package/package.json +47 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +34 -0
- package/references/archive.md +32 -0
- package/references/artifacts.md +47 -0
- package/references/debugger.md +40 -0
- package/references/extensions-and-sync.md +124 -0
- package/references/managed-runtime.md +116 -0
- package/references/mutations.json +1424 -0
- package/references/native/InsightApplyDocumentation.java +48 -0
- package/references/native/InsightArchivePayloads.java +36 -0
- package/references/native/InsightBlockTransfer.java +57 -0
- package/references/native/InsightEmulation.java +93 -0
- package/references/native/InsightExtensions.java +36 -0
- package/references/native/InsightFunctionDatabases.java +98 -0
- package/references/native/InsightGuiMain.java +32 -0
- package/references/native/InsightHeadlessMain.java +10 -0
- package/references/native/InsightInitialImport.java +42 -0
- package/references/native/InsightInitialJournal.java +48 -0
- package/references/native/InsightInitialPublication.java +142 -0
- package/references/native/InsightKeytool.java +16 -0
- package/references/native/InsightNativeReceipt.java +31 -0
- package/references/native/InsightPlugins.java +34 -0
- package/references/native/InsightProjectFiles.java +92 -0
- package/references/native/InsightPublicationRecovery.java +63 -0
- package/references/native/InsightPythonWheels.java +31 -0
- package/references/native/InsightResearchQueries.java +188 -0
- package/references/native/InsightRuntimeDoctor.java +33 -0
- package/references/native/InsightSdkFiles.java +142 -0
- package/references/native/InsightSdkScript.java +31 -0
- package/references/native/InsightSdkTools.java +338 -0
- package/references/native/InsightSdkTypes.java +132 -0
- package/references/native/InsightServerAdministration.java +82 -0
- package/references/native/InsightServerInit.java +62 -0
- package/references/native/InsightServerMain.java +43 -0
- package/references/native/InsightServerProbe.java +29 -0
- package/references/native/InsightSnapshotFields.java +68 -0
- package/references/native/InsightSnapshotInspect.java +189 -0
- package/references/native/InsightSnapshotPublisher.java +123 -0
- package/references/native/InsightStartDebugger.java +28 -0
- package/references/native/InsightStopDebugger.java +20 -0
- package/references/native/InsightTraceInspect.java +24 -0
- package/references/native/InsightTraceState.java +130 -0
- package/references/native/InsightTraceTools.java +114 -0
- package/references/native/InsightVersionTracking.java +91 -0
- package/references/native-workflows.md +112 -0
- package/references/queries.json +1761 -0
- package/references/query-semantics.md +96 -0
- package/references/recovery.md +53 -0
- package/references/research.md +101 -0
- package/references/setup.md +49 -0
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-LICENSE +202 -0
- package/runtime/GhidraMCP-NOTICE +10 -0
- package/runtime/provenance.json +11 -0
- package/skills/insight-re/SKILL.md +108 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Historical publication fixture only. The shipped executable uses cli.ts.
|
|
2
|
+
import { command, createCli, Permission } from '@eyeauras/cli-factory';
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { Store } from './store.js';
|
|
7
|
+
import { Coordinator } from './coordinator.js';
|
|
8
|
+
import { GhidraRuntime } from './ghidra.js';
|
|
9
|
+
import { id, text, fail } from './model.js';
|
|
10
|
+
class Owner {
|
|
11
|
+
options;
|
|
12
|
+
profiles = new Map();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
selected(c) {
|
|
17
|
+
const path = c.appArguments.AppDataDirectory;
|
|
18
|
+
let profile = this.profiles.get(path);
|
|
19
|
+
if (!profile) {
|
|
20
|
+
const root = join(path, 'ghidra');
|
|
21
|
+
profile = { store: new Store(root, c.profile.name, text(c.profile.values.server)), runtime: new GhidraRuntime(root, c), tasks: new Map() };
|
|
22
|
+
this.profiles.set(path, profile);
|
|
23
|
+
}
|
|
24
|
+
if (profile.store.server !== c.profile.values.server)
|
|
25
|
+
fail('binding', 'Profile server changed while durable work remains; restore its original binding before recovery.');
|
|
26
|
+
const runtime = profile.runtime.fork(c);
|
|
27
|
+
return { ...profile, ghidra: runtime, coordinator: new Coordinator(profile.store, this.options.publisher?.(c) ?? runtime, undefined, profile.tasks) };
|
|
28
|
+
}
|
|
29
|
+
async invalidateProfile(app) {
|
|
30
|
+
const profile = this.profiles.get(app.AppDataDirectory);
|
|
31
|
+
if (Store.pending(join(app.AppDataDirectory, 'ghidra')))
|
|
32
|
+
fail('blocked', 'Profile contains unverified findings; its journal must be preserved.');
|
|
33
|
+
if (!profile)
|
|
34
|
+
return;
|
|
35
|
+
if (profile.tasks.size || profile.store.batches().some(b => !['published_verified', 'conflict_pending'].includes(b.state)))
|
|
36
|
+
fail('blocked', 'Profile has unresolved publication work; its AppData must be preserved.');
|
|
37
|
+
await profile.runtime.dispose();
|
|
38
|
+
profile.store.close();
|
|
39
|
+
this.profiles.delete(app.AppDataDirectory);
|
|
40
|
+
}
|
|
41
|
+
async dispose() {
|
|
42
|
+
for (const profile of this.profiles.values()) {
|
|
43
|
+
await Promise.allSettled([...profile.tasks.values()]);
|
|
44
|
+
await profile.runtime.dispose();
|
|
45
|
+
profile.store.close();
|
|
46
|
+
}
|
|
47
|
+
this.profiles.clear();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function jsonFile(c, path) {
|
|
51
|
+
const absolute = resolve(c.cwd, text(path, 4096));
|
|
52
|
+
const info = await stat(absolute);
|
|
53
|
+
if (!info.isFile() || info.size > 65536)
|
|
54
|
+
fail('limit', 'Proposal must be a regular file of at most 64 KiB.');
|
|
55
|
+
try {
|
|
56
|
+
const content = await readFile(absolute);
|
|
57
|
+
if (content.length > 65536)
|
|
58
|
+
fail('limit', 'Proposal file exceeds 64 KiB.');
|
|
59
|
+
return JSON.parse(content.toString('utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
fail('invalid', 'File must contain valid JSON.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const fileOption = [{ flags: '--file <path>', description: 'Read the JSON proposal file', required: true }];
|
|
66
|
+
const fields = [
|
|
67
|
+
['server', '--server <origin>', 'Select the Ghidra Server origin'],
|
|
68
|
+
['readerUser', '--reader-user <user>', 'Select the read-only server account'],
|
|
69
|
+
['publisherUser', '--publisher-user <user>', 'Select the publisher server account'],
|
|
70
|
+
['java', '--java <path>', 'Locate Java 21'],
|
|
71
|
+
['ghidraHome', '--ghidra-home <path>', 'Locate the matching Ghidra installation'],
|
|
72
|
+
['mcpJar', '--mcp-jar <path>', 'Locate the GhidraMCP jar'],
|
|
73
|
+
['mcpJarSha256', '--mcp-jar-sha256 <sha>', 'Verify the GhidraMCP artifact hash'],
|
|
74
|
+
['python', '--python <path>', 'Locate the bridge Python executable'],
|
|
75
|
+
['bridgeDirectory', '--bridge-directory <path>', 'Locate the pinned Python bridge module'],
|
|
76
|
+
];
|
|
77
|
+
export function createDefinition(options = {}) {
|
|
78
|
+
const owner = new Owner(options);
|
|
79
|
+
return {
|
|
80
|
+
name: 'insight-ghidra', applicationId: 'insight-ghidra', version: '0.1.0',
|
|
81
|
+
description: 'Coordinate Ghidra readers and scoped comment publication',
|
|
82
|
+
permissions: {}, resources: [owner], ...(options.runtime ? { runtime: options.runtime } : {}),
|
|
83
|
+
profile: { fields: fields.map(([name, flags, description]) => ({ name: name, flags: flags, description: description, ...(name === 'server' ? { required: true, normalize: (value) => { const server = text(value); if (!/^ghidra:\/\/[^/?#@]+$/.test(server))
|
|
84
|
+
fail('invalid', 'Server must be a ghidra://host:port origin.'); return server; } } : {}) })) },
|
|
85
|
+
auth: {
|
|
86
|
+
loginOptions: [{ flags: '--role <role>', description: 'Select reader or publisher credentials', defaultValue: 'reader' }, { flags: '--password-stdin', description: 'Read the password from stdin' }],
|
|
87
|
+
async login(c, o) {
|
|
88
|
+
const role = text(o.role);
|
|
89
|
+
if (!['reader', 'publisher'].includes(role))
|
|
90
|
+
fail('invalid', 'Role must be reader or publisher.');
|
|
91
|
+
if (!o.passwordStdin || !c.stdinAvailable)
|
|
92
|
+
fail('credentials', 'Supply --password-stdin for the selected role.');
|
|
93
|
+
let candidate = '';
|
|
94
|
+
for await (const chunk of c.io.input) {
|
|
95
|
+
candidate += chunk.toString();
|
|
96
|
+
if (candidate.length > 4096)
|
|
97
|
+
fail('limit', 'Password input exceeds 4096 characters.');
|
|
98
|
+
}
|
|
99
|
+
candidate = candidate.replace(/\r?\n$/, '');
|
|
100
|
+
if (!candidate || /[\r\n\0]/.test(candidate))
|
|
101
|
+
fail('credentials', 'Password must be one non-empty line.');
|
|
102
|
+
await c.secrets.set(role + 'Password', candidate);
|
|
103
|
+
return { authenticated: true, identity: { role, user: c.profile.values[role + 'User'], validation: 'stored; server checked at worker startup' } };
|
|
104
|
+
},
|
|
105
|
+
async status(c) { const reader = Boolean(await c.secrets.get('readerPassword')), publisher = Boolean(await c.secrets.get('publisherPassword')); return { authenticated: reader, identity: { readerStored: reader, publisherStored: publisher } }; },
|
|
106
|
+
async logout(c) { await c.secrets.delete('readerPassword'); await c.secrets.delete('publisherPassword'); },
|
|
107
|
+
},
|
|
108
|
+
builtins: [command('skill', 'Locate the installed workflow skill', () => ({ version: '0.1.0', path: fileURLToPath(new URL('../../SKILL.md', import.meta.url)) }))],
|
|
109
|
+
commands: [
|
|
110
|
+
command('targets', 'Select versioned programs', [
|
|
111
|
+
command('register', 'Register a complete program identity', async ({ options: o }, c) => owner.selected(c).store.register(await jsonFile(c, o.file)), { permission: Permission.Update, options: fileOption }),
|
|
112
|
+
command('list', 'List registered program identities', (_i, c) => owner.selected(c).store.targets(), { permission: Permission.ReadOnly }),
|
|
113
|
+
command('show <target>', 'Show a registered program identity', ({ args }, c) => owner.selected(c).store.target(id(args.target)), { permission: Permission.ReadOnly }),
|
|
114
|
+
]),
|
|
115
|
+
command('workers', 'Manage independent reader processes', [
|
|
116
|
+
command('start <target> <worker>', 'Start a reader and dedicated bridge', async ({ args }, c) => {
|
|
117
|
+
const selected = owner.selected(c), target = selected.store.target(id(args.target)), worker = id(args.worker);
|
|
118
|
+
if (selected.store.workers().some(w => w.id === worker))
|
|
119
|
+
fail('workerExists', 'Worker ID is already registered.');
|
|
120
|
+
const binding = await selected.ghidra.startReader(target, worker);
|
|
121
|
+
try {
|
|
122
|
+
selected.store.putWorker(worker, target.id, binding);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
await selected.ghidra.stop(target, binding);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
return { id: worker, targetId: target.id, ...binding };
|
|
129
|
+
}, { permission: Permission.Update }),
|
|
130
|
+
command('list', 'List registered reader bindings', (_i, c) => owner.selected(c).store.workers(), { permission: Permission.ReadOnly }),
|
|
131
|
+
command('stop <worker>', 'Stop an owned reader process', async ({ args }, c) => {
|
|
132
|
+
const selected = owner.selected(c), worker = id(args.worker), binding = selected.store.worker(worker);
|
|
133
|
+
await selected.ghidra.stop(binding.identity, binding);
|
|
134
|
+
selected.store.removeWorker(worker);
|
|
135
|
+
return { id: worker, state: 'stopped' };
|
|
136
|
+
}, { permission: Permission.Update }),
|
|
137
|
+
]),
|
|
138
|
+
command('inspect <worker>', 'Read selected functions from a worker', async ({ args, options: o }, c) => {
|
|
139
|
+
const selected = owner.selected(c), binding = selected.store.worker(id(args.worker));
|
|
140
|
+
const result = await selected.ghidra.inspect(binding.identity, binding, text(o.functions));
|
|
141
|
+
selected.store.updateReader(id(args.worker), result.binding);
|
|
142
|
+
return result;
|
|
143
|
+
}, { permission: Permission.ReadOnly, options: [{ flags: '--functions <selectors>', description: 'Select up to eight function references', required: true }] }),
|
|
144
|
+
command('findings', 'Journal scoped comment proposals', [
|
|
145
|
+
command('submit', 'Persist an idempotent finding proposal', async ({ options: o }, c) => owner.selected(c).store.submit(await jsonFile(c, o.file)), { permission: Permission.Update, options: fileOption }),
|
|
146
|
+
command('list', 'List durable findings', ({ options: o }, c) => owner.selected(c).store.findings(o.target ? id(o.target) : undefined), { permission: Permission.ReadOnly, options: [{ flags: '--target <id>', description: 'Filter by registered target' }] }),
|
|
147
|
+
command('show <finding>', 'Show a finding and its outcome', ({ args }, c) => owner.selected(c).store.getFinding(id(args.finding)), { permission: Permission.ReadOnly }),
|
|
148
|
+
]),
|
|
149
|
+
command('integrate <target>', 'Publish queued comments for one program', ({ args }, c) => owner.selected(c).coordinator.integrate(id(args.target)), { permission: Permission.Update }),
|
|
150
|
+
command('status', 'Show durable publication state', (_i, c) => { const selected = owner.selected(c); return { ownerPid: process.pid, profile: c.profile.name, targets: selected.store.targets(), workers: selected.store.workers(), batches: selected.store.batches() }; }, { permission: Permission.ReadOnly }),
|
|
151
|
+
command('recover <batch>', 'Reconcile an interrupted publication batch', ({ args, options: o }, c) => owner.selected(c).coordinator.recover(id(args.batch), Boolean(o.abandonStaged)), { permission: Permission.Update, options: [{ flags: '--abandon-staged', description: 'Abandon a stopped pre-publication stage' }] }),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export const createInsightCli = (options = {}) => createCli(createDefinition(options));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Research } from './research.js';
|
|
3
|
+
/** Static pinned domain leaves. Callers choose typed flags, never endpoint/payload routing. */
|
|
4
|
+
export declare function queryCommands(research: (context: CommandContext) => Research): CommandDefinition[];
|
|
5
|
+
export declare function extendQueries(existing: readonly CommandDefinition[], queries: readonly CommandDefinition[]): CommandDefinition[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
3
|
+
import { scope } from './research.js';
|
|
4
|
+
import { fail } from './model.js';
|
|
5
|
+
import { queryResult } from './query-results.js';
|
|
6
|
+
import { key, flag, parsed, option, selectedOptions } from './arguments.js';
|
|
7
|
+
import { nativeQueries, qualifyQuery, queryDescription } from './query-policy.js';
|
|
8
|
+
const catalog = JSON.parse(readFileSync(new URL('../../references/queries.json', import.meta.url), 'utf8'));
|
|
9
|
+
if (catalog.source !== '57e37a8bf388b1279cff46378f85be1554548f61' || catalog.tools.length !== 74)
|
|
10
|
+
throw Error('Installed query contracts differ from the pinned facade build.');
|
|
11
|
+
function value(result) {
|
|
12
|
+
if (result === undefined || result === null)
|
|
13
|
+
fail('partial', 'Query did not return its result.');
|
|
14
|
+
let encoded;
|
|
15
|
+
try {
|
|
16
|
+
encoded = JSON.stringify(result);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
fail('partial', 'Query returned malformed JSON.');
|
|
20
|
+
}
|
|
21
|
+
if (encoded.length > 1048576)
|
|
22
|
+
fail('limit', 'Query result exceeds one MiB; narrow its scope or request a smaller page.');
|
|
23
|
+
if (typeof result === 'object' && !Array.isArray(result)) {
|
|
24
|
+
const o = result;
|
|
25
|
+
if (o.error || o.success === false || o.status === 'error')
|
|
26
|
+
fail('remote', 'Query reported a failure.');
|
|
27
|
+
for (const name of ['total', 'count', 'offset', 'limit'])
|
|
28
|
+
if (o[name] !== undefined && (!Number.isSafeInteger(o[name]) || Number(o[name]) < 0))
|
|
29
|
+
fail('partial', 'Query returned inconsistent pagination metadata.');
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/** Static pinned domain leaves. Callers choose typed flags, never endpoint/payload routing. */
|
|
34
|
+
export function queryCommands(research) {
|
|
35
|
+
const groups = new Map();
|
|
36
|
+
for (const q of catalog.tools) {
|
|
37
|
+
const [group, leaf, ...rest] = q.command.split(' ');
|
|
38
|
+
if (!group || !leaf || rest.length || q.params.some(p => ['session', 'target', 'program'].includes(p.name)))
|
|
39
|
+
throw Error('Invalid installed query contract.');
|
|
40
|
+
const children = groups.get(group) ?? [];
|
|
41
|
+
groups.set(group, children);
|
|
42
|
+
children.push(command(leaf, queryDescription(q.path, leaf.replaceAll('-', ' ')), async ({ options: o }, c) => {
|
|
43
|
+
const parameters = {};
|
|
44
|
+
for (const p of q.params) {
|
|
45
|
+
const input = o[key(p.name)];
|
|
46
|
+
if (input === undefined) {
|
|
47
|
+
if (p.required)
|
|
48
|
+
fail('invalid', `Missing --${flag(p.name)}.`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
parameters[p.name] = parsed(p, input);
|
|
52
|
+
}
|
|
53
|
+
if (nativeQueries.has(q.path)) {
|
|
54
|
+
if (Number(parameters.timeout_seconds ?? 5) > 30)
|
|
55
|
+
fail('invalid', 'Native query timeout must be at most 30 seconds.');
|
|
56
|
+
if (Number(parameters.max_steps ?? 10000) > 100000)
|
|
57
|
+
fail('invalid', 'Native emulation supports at most 100000 steps per candidate.');
|
|
58
|
+
if (parameters.abi !== undefined && !['auto', 'x86-cdecl', 'windows-x64', 'sysv-x64'].includes(String(parameters.abi)))
|
|
59
|
+
fail('invalid', 'ABI must be auto, x86-cdecl, windows-x64 or sysv-x64.');
|
|
60
|
+
}
|
|
61
|
+
const who = scope(o), result = q.path === '/list_scripts' ?
|
|
62
|
+
await research(c).scripts(who, String(parameters.filter ?? ''), Number(o.limit), Number(o.offset)) :
|
|
63
|
+
await research(c).read(who, q.path, parameters);
|
|
64
|
+
return { ...who, value: value(qualifyQuery(q.path, queryResult(q.path, parameters, value(result)))) };
|
|
65
|
+
}, { permission: Permission.ReadOnly, options: [...selectedOptions, ...q.params.map(option), ...(q.path === '/list_scripts' ?
|
|
66
|
+
[option({ name: 'limit', type: 'integer', required: false, default: '50' }), option({ name: 'offset', type: 'integer', required: false, default: '0' })] : [])] }));
|
|
67
|
+
}
|
|
68
|
+
return [...groups].map(([group, children]) => command(group, `${group} research`, children));
|
|
69
|
+
}
|
|
70
|
+
export function extendQueries(existing, queries) {
|
|
71
|
+
const result = [...existing];
|
|
72
|
+
for (const group of queries) {
|
|
73
|
+
const index = result.findIndex(entry => entry.name === group.name);
|
|
74
|
+
if (index < 0)
|
|
75
|
+
result.push(group);
|
|
76
|
+
else {
|
|
77
|
+
const before = result[index];
|
|
78
|
+
if (!before.children || group.children?.some(leaf => before.children.some(entry => entry.name === leaf.name)))
|
|
79
|
+
throw Error('Duplicate query command.');
|
|
80
|
+
result[index] = { ...before, children: [...before.children, ...group.children] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** These operations have facade-owned, bounded native implementations. */
|
|
2
|
+
export declare const nativeQueries: Set<string>;
|
|
3
|
+
export declare function queryDescription(path: string, fallback: string): string;
|
|
4
|
+
/** Reject placeholder/error payloads anywhere in the returned domain value. */
|
|
5
|
+
export declare function rejectPlaceholders(value: unknown, depth?: number): void;
|
|
6
|
+
export declare function qualifyQuery(path: string, value: unknown): unknown;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { fail, object } from './model.js';
|
|
2
|
+
/** These operations have facade-owned, bounded native implementations. */
|
|
3
|
+
export const nativeQueries = new Set(['/find_dead_code', '/detect_crypto_constants', '/get_valid_data_types',
|
|
4
|
+
'/validate_data_type', '/validate_function_prototype', '/get_entry_points', '/list_globals',
|
|
5
|
+
'/list_strings', '/search_strings', '/find_data_types', '/search_byte_patterns', '/search_instructions',
|
|
6
|
+
'/emulate_function', '/emulate_hash_batch']);
|
|
7
|
+
const heuristics = Object.fromEntries([
|
|
8
|
+
['/find_similar_functions_fuzzy', 'similarity', 'mcp-fuzzy-instruction-match'],
|
|
9
|
+
['/bulk_fuzzy_match', 'similarity', 'mcp-fuzzy-instruction-match'],
|
|
10
|
+
['/analyze_function_completeness', 'score', 'mcp-documentation-completeness'],
|
|
11
|
+
['/analyze_for_documentation', 'suggestion', 'mcp-documentation-suggestions'],
|
|
12
|
+
['/find_similar_functions', 'similarity', 'mcp-function-metrics'],
|
|
13
|
+
['/analyze_global_completeness', 'score', 'mcp-global-documentation-completeness'],
|
|
14
|
+
['/audit_global', 'score', 'mcp-global-documentation-audit'],
|
|
15
|
+
['/audit_globals_in_function', 'score', 'mcp-global-documentation-audit'],
|
|
16
|
+
['/suggest_field_names', 'suggestion', 'mcp-type-name-patterns'],
|
|
17
|
+
['/detect_array_bounds', 'estimate', 'mcp-reference-stride-estimator'],
|
|
18
|
+
['/analyze_api_call_chains', 'pattern-match', 'mcp-api-name-patterns'],
|
|
19
|
+
['/detect_malware_behaviors', 'pattern-match', 'mcp-api-instruction-patterns'],
|
|
20
|
+
['/find_anti_analysis_techniques', 'pattern-match', 'mcp-symbol-instruction-patterns'],
|
|
21
|
+
['/extract_iocs_with_context', 'pattern-match', 'mcp-string-patterns'],
|
|
22
|
+
['/find_undocumented_by_string', 'suggestion', 'mcp-string-reference-documentation'],
|
|
23
|
+
].map(([path, kind, algorithm]) => [path, { kind: kind, algorithm: algorithm, limitations: [
|
|
24
|
+
'A static observation, heuristic or suggestion; not a proof of runtime behavior or completeness.',
|
|
25
|
+
'Scores and confidence labels are not calibrated probabilities; the agent decides what to accept.',
|
|
26
|
+
] }]));
|
|
27
|
+
export function queryDescription(path, fallback) {
|
|
28
|
+
if (path === '/find_dead_code')
|
|
29
|
+
return 'Find blocks unreachable in the bounded static CFG; unresolved flow is reported';
|
|
30
|
+
if (path === '/detect_crypto_constants')
|
|
31
|
+
return 'Match versioned constant-table signatures within an explicit byte budget';
|
|
32
|
+
if (path === '/get_valid_data_types')
|
|
33
|
+
return 'Enumerate actually resolvable program/builtin types, with provenance and scan limits';
|
|
34
|
+
if (path === '/validate_function_prototype')
|
|
35
|
+
return 'Parse a prototype and resolve its convention without persisting changes';
|
|
36
|
+
if (path === '/validate_data_type')
|
|
37
|
+
return 'Resolve a type and report layout/alignment diagnostics without changing the program';
|
|
38
|
+
if (path === '/get_entry_points')
|
|
39
|
+
return 'Read external-entry metadata; report name/image-base candidates separately';
|
|
40
|
+
if (path === '/list_globals')
|
|
41
|
+
return 'Read structured global records with bounded symbol/reference scanning';
|
|
42
|
+
if (path.startsWith('/emulate_'))
|
|
43
|
+
return 'Bounded isolated x86/x64 emulation with explicit ABI and termination reason';
|
|
44
|
+
return heuristics[path] ? fallback + ' (qualified ' + heuristics[path].kind + '; not ground truth)' : fallback;
|
|
45
|
+
}
|
|
46
|
+
/** Reject placeholder/error payloads anywhere in the returned domain value. */
|
|
47
|
+
export function rejectPlaceholders(value, depth = 0) {
|
|
48
|
+
if (depth > 64)
|
|
49
|
+
fail('partial', 'Query result nesting exceeds 64 levels.');
|
|
50
|
+
if (!value || typeof value !== 'object')
|
|
51
|
+
return;
|
|
52
|
+
const row = value;
|
|
53
|
+
if (typeof row.status === 'string' && /not[ _-]*(?:yet[ _-]*)?implemented|placeholder|unsupported|unavailable/i.test(row.status))
|
|
54
|
+
fail('unsupported', 'The backend returned an unavailable analysis instead of a completed result.');
|
|
55
|
+
for (const child of Object.values(row))
|
|
56
|
+
rejectPlaceholders(child, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
export function qualifyQuery(path, value) {
|
|
59
|
+
rejectPlaceholders(value);
|
|
60
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
61
|
+
return value;
|
|
62
|
+
const result = object(value), semantics = heuristics[path];
|
|
63
|
+
if (path === '/suggest_field_names' && Array.isArray(result.suggestions))
|
|
64
|
+
result.suggestions = result.suggestions.map(raw => ({
|
|
65
|
+
...object(raw), confidence: null, confidence_kind: 'uncalibrated',
|
|
66
|
+
}));
|
|
67
|
+
return { ...result, ...(semantics ? { semantics } : {}), ...(!nativeQueries.has(path) ? { scanPolicy: {
|
|
68
|
+
execution: 'pinned-upstream', pagination: 'may-follow-full-scan', workBound: 'not-guaranteed',
|
|
69
|
+
maximumAcceptedResultBytes: 1048576,
|
|
70
|
+
note: 'A page/output limit does not bound backend scan cost. Use a narrow function/address/filter; heavy upstream work may continue after transport cancellation.',
|
|
71
|
+
} } : {}) };
|
|
72
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { fail } from './model.js';
|
|
2
|
+
import { rejectPlaceholders } from './query-policy.js';
|
|
3
|
+
const shapes = {
|
|
4
|
+
'/analyze_data_region': { start_address: 'string', end_address: 'string', byte_span: 'number', xref_count: 'number', classification_hint: 'string' },
|
|
5
|
+
'/detect_array_bounds': { address: 'string', estimated_size: 'number', stride: 'number', element_count: 'number', confidence: 'string' },
|
|
6
|
+
'/analyze_control_flow': { function_name: 'string', entry_point: 'string', metrics: 'object', basic_block_details: 'array' },
|
|
7
|
+
'/analyze_dataflow': { function: 'object', anchor: 'object', direction: 'string', chain: 'array', truncated: 'boolean' },
|
|
8
|
+
'/analyze_for_documentation': { name: 'string', address: 'string', parameters: 'array', locals: 'array' },
|
|
9
|
+
'/analyze_function_complete': { name: 'string', address: 'string' },
|
|
10
|
+
'/analyze_function_completeness': { function_name: 'string', completeness_score: 'number' },
|
|
11
|
+
'/find_code_gaps': { gaps: 'array', total: 'number', offset: 'number', limit: 'number' },
|
|
12
|
+
'/find_next_undefined_function': { found: 'boolean' },
|
|
13
|
+
'/find_similar_functions': { target_function: 'string', similar_functions: 'array', matches_found: 'number' },
|
|
14
|
+
'/get_field_access_context': { struct_address: 'string', field_offset: 'number', examples: 'array' },
|
|
15
|
+
'/get_function_pcode': { name: 'string', address: 'string', basic_blocks: 'array' },
|
|
16
|
+
'/inspect_memory_content': { address: 'string', bytes_read: 'number', hex_dump: 'string' },
|
|
17
|
+
'/list_analyzers': { analyzers: 'array', count: 'number' },
|
|
18
|
+
'/search_instructions': { matches: 'array' },
|
|
19
|
+
'/analyze_global_completeness': { address: 'string', score: 'number', issues: 'array' },
|
|
20
|
+
'/analyze_struct_field_usage': { struct_name: 'string', struct_size: 'number', field_usage: 'object' },
|
|
21
|
+
'/audit_global': { address: 'string', issues: 'array', fully_documented: 'boolean' },
|
|
22
|
+
'/audit_globals_in_function': { function: 'object', globals: 'array', summary: 'object' },
|
|
23
|
+
'/get_enum_values': { name: 'string', size: 'number', values: 'array' },
|
|
24
|
+
'/get_struct_layout': { name: 'string', size: 'number', alignment: 'number', fields: 'array' },
|
|
25
|
+
'/get_type_size': { type_name: 'string', size: 'number', alignment: 'number' },
|
|
26
|
+
'/get_valid_data_types': { builtin_types: 'array', windows_types: 'array' },
|
|
27
|
+
'/suggest_field_names': { struct_address: 'string', suggestions: 'array' },
|
|
28
|
+
'/validate_data_type': { type_name: 'string', valid: 'boolean', warnings: 'array' },
|
|
29
|
+
'/validate_function_prototype': { valid: 'boolean' },
|
|
30
|
+
'/batch_string_anchor_report': { anchors: 'array', total_anchors: 'number' },
|
|
31
|
+
'/find_undocumented_by_string': { undocumented_functions: 'array', undocumented_count: 'number' },
|
|
32
|
+
'/get_function_documentation': { hash: 'string', function_name: 'string', parameters: 'array', comments: 'array', labels: 'array' },
|
|
33
|
+
'/emulate_function': { success: 'boolean', steps_executed: 'number', stop_reason: 'string', registers: 'object' },
|
|
34
|
+
'/emulate_hash_batch': { tested: 'number', matches: 'array', resolved: 'boolean' },
|
|
35
|
+
'/disassemble_function': { instructions: 'array', count: 'number' },
|
|
36
|
+
'/force_decompile': { address: 'string', name: 'string', decompiled: 'string' },
|
|
37
|
+
'/get_functions': { functions: 'object', count: 'number', requested: 'number' },
|
|
38
|
+
'/list_class_members': { class_name: 'string', members: 'array', total_members: 'number' },
|
|
39
|
+
'/list_function_tags': { tags: 'array', total: 'number' },
|
|
40
|
+
'/find_functions': { functions: 'array', total: 'number', count: 'number' },
|
|
41
|
+
'/get_entry_points': { entry_points: 'array', count: 'number' },
|
|
42
|
+
'/get_function_count': { function_count: 'number' },
|
|
43
|
+
'/list_calling_conventions': { calling_conventions: 'array', count: 'number' },
|
|
44
|
+
'/list_globals': { globals: 'array', count: 'number', total: 'number' },
|
|
45
|
+
'/list_program_items': { kind: 'string', items: 'array', count: 'number', total: 'number' },
|
|
46
|
+
'/list_shadowed_globals': { shadowed: 'array', count: 'number', total: 'number' },
|
|
47
|
+
'/list_strings': { strings: 'array', count: 'number', total: 'number' },
|
|
48
|
+
'/search_strings': { matches: 'array', total: 'number', offset: 'number', limit: 'number' },
|
|
49
|
+
'/analyze_api_call_chains': { detected_patterns: 'array', total_patterns_detected: 'number' },
|
|
50
|
+
'/detect_malware_behaviors': { behaviors: 'array', total_behaviors_detected: 'number' },
|
|
51
|
+
'/extract_iocs_with_context': { iocs: 'array', total_iocs: 'number' },
|
|
52
|
+
'/find_anti_analysis_techniques': { findings: 'array', total_findings: 'number' },
|
|
53
|
+
'/analysis_status': { analyzing: 'boolean', analyzed: 'boolean', function_count: 'number' },
|
|
54
|
+
'/get_address_spaces': { address_spaces: 'array', count: 'number' },
|
|
55
|
+
'/get_language_metadata': { language_id: 'string', processor: 'string', endian: 'string', address_spaces: 'array' },
|
|
56
|
+
'/get_metadata': { architecture: 'string', compiler: 'string', language: 'string', base_address: 'string', function_count: 'number' },
|
|
57
|
+
'/get_property': { address: 'string', has_value: 'boolean' },
|
|
58
|
+
'/list_bookmarks': { bookmarks: 'array', count: 'number' },
|
|
59
|
+
'/list_properties': { map: 'string', entries: 'array', count: 'number', total: 'number' },
|
|
60
|
+
'/read_memory': { address: 'string', length: 'number', data: 'array', hex: 'string' },
|
|
61
|
+
'/can_rename_at_address': { can_rename: 'boolean', type: 'string' },
|
|
62
|
+
'/get_function_call_graph': { edges: 'array', count: 'number' },
|
|
63
|
+
'/get_xrefs_from': { references: 'array', count: 'number', total: 'number' },
|
|
64
|
+
'/get_xrefs_to': { references: 'array', count: 'number', total: 'number' },
|
|
65
|
+
};
|
|
66
|
+
function checkShape(result, shape) {
|
|
67
|
+
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
68
|
+
fail('partial', 'Query expected a result object.');
|
|
69
|
+
const value = result;
|
|
70
|
+
for (const [key, type] of Object.entries(shape)) {
|
|
71
|
+
const item = value[key], valid = type === 'array' ? Array.isArray(item) : type === 'object' ? Boolean(item) && typeof item === 'object' && !Array.isArray(item) :
|
|
72
|
+
type === 'number' ? typeof item === 'number' && Number.isFinite(item) : typeof item === type;
|
|
73
|
+
if (!valid)
|
|
74
|
+
fail('partial', `Query omitted or malformed ${key}.`);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
/** Validate the pinned operation's result variant, independently of its HTTP acknowledgment. */
|
|
79
|
+
export function queryResult(path, parameters, result) {
|
|
80
|
+
rejectPlaceholders(result);
|
|
81
|
+
let shape = shapes[path];
|
|
82
|
+
if (path === '/find_dead_code')
|
|
83
|
+
shape = { findings: 'array', complete: 'boolean', unresolved_flow: 'boolean', algorithm: 'string', scan: 'object', truncated: 'boolean' };
|
|
84
|
+
if (path === '/detect_crypto_constants' || path === '/search_byte_patterns')
|
|
85
|
+
shape = { matches: 'array', complete: 'boolean', scanned_bytes: 'number', maximum_bytes: 'number', algorithm: 'string', scan: 'object', truncated: 'boolean' };
|
|
86
|
+
if (path === '/get_valid_data_types')
|
|
87
|
+
shape = { types: 'array', scope: 'string', scan: 'object', truncated: 'boolean' };
|
|
88
|
+
if (path === '/validate_function_prototype')
|
|
89
|
+
shape = { valid: 'boolean', diagnostics: 'array', parser: 'string', persisted_changes: 'boolean' };
|
|
90
|
+
const bulkComments = path === '/get_comment' && Boolean(String(parameters.addresses ?? '').trim());
|
|
91
|
+
if (path === '/get_comment')
|
|
92
|
+
shape = bulkComments ? { results: 'array', requested: 'number', returned: 'number', with_comments: 'number' } : { address: 'string', has_comment: 'boolean' };
|
|
93
|
+
if (path === '/find_data_types')
|
|
94
|
+
shape = { ...(parameters.categories ? { categories: 'array' } : { data_types: 'array' }), count: 'number' };
|
|
95
|
+
if (path === '/get_function_hash')
|
|
96
|
+
shape = parameters.function ? { hash: 'string', function_name: 'string', instruction_count: 'number' } : { functions: 'array' };
|
|
97
|
+
// The pinned service returns a bare bundle for function=, a map/counts for functions=.
|
|
98
|
+
// Bulk selection takes precedence even when both selectors are supplied.
|
|
99
|
+
if (path === '/get_functions' && !String(parameters.functions ?? '').trim())
|
|
100
|
+
shape = { name: 'string', size: 'number' };
|
|
101
|
+
if (path === '/get_program_options')
|
|
102
|
+
shape = parameters.group ? { options: 'array' } : { groups: 'array' };
|
|
103
|
+
// The pinned implementation retains format as a deprecated no-op and always returns JSON.
|
|
104
|
+
if (path === '/list_data_items_by_xrefs')
|
|
105
|
+
shape = { data_items: 'array', count: 'number', total: 'number' };
|
|
106
|
+
if (path === '/get_external_location')
|
|
107
|
+
shape = { address: 'string' };
|
|
108
|
+
if (path === '/get_full_call_graph')
|
|
109
|
+
shape = parameters.format === 'json_edges' ? { edges: 'array', edge_count: 'number', caller_count: 'number' } :
|
|
110
|
+
parameters.format === 'dot' || parameters.format === 'mermaid' ? { format: 'string', diagram: 'string', count: 'number' } :
|
|
111
|
+
parameters.format === 'adjacency' ? { format: 'string', adjacency: 'object', count: 'number' } : { edges: 'array', count: 'number' };
|
|
112
|
+
if (path === '/analyze_call_graph')
|
|
113
|
+
shape = parameters.analysis_type === undefined || parameters.analysis_type === 'summary' ? { total_functions: 'number', total_call_edges: 'number' } : { analysis_type: 'string' };
|
|
114
|
+
if (path === '/get_assembly_context') {
|
|
115
|
+
const value = checkShape(result, {}), input = parameters.xref_sources;
|
|
116
|
+
const addresses = Array.isArray(input) ? input : typeof input === 'string' ? input.split(',').map(v => v.trim()).filter(Boolean) : [];
|
|
117
|
+
for (const at of addresses)
|
|
118
|
+
if (!Object.hasOwn(value, String(at)))
|
|
119
|
+
fail('partial', 'Assembly context omitted a requested address.');
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
if (path === '/list_scripts')
|
|
123
|
+
shape = { items: 'array', count: 'number', total: 'number', offset: 'number', limit: 'number', truncated: 'boolean' };
|
|
124
|
+
if (!shape)
|
|
125
|
+
fail('schema', 'Pinned query has no result contract.');
|
|
126
|
+
const value = checkShape(result, shape);
|
|
127
|
+
const rowShapes = {
|
|
128
|
+
'/list_globals': { key: 'globals', shape: { address: 'string', name: 'string', type: 'string', size: 'number', xrefs: 'number', named: 'boolean', typed: 'boolean' } },
|
|
129
|
+
'/list_strings': { key: 'strings', shape: { address: 'string', value: 'string', encoding: 'string' } },
|
|
130
|
+
'/search_strings': { key: 'matches', shape: { address: 'string', value: 'string', encoding: 'string' } },
|
|
131
|
+
'/search_instructions': { key: 'matches', shape: { address: 'string', mnemonic: 'string', instruction: 'string', length: 'number' } },
|
|
132
|
+
'/get_valid_data_types': { key: 'types', shape: { name: 'string', path: 'string', size: 'number', alignment: 'number', source: 'string' } },
|
|
133
|
+
'/find_dead_code': { key: 'findings', shape: { address: 'string', end: 'string', size: 'number', kind: 'string' } },
|
|
134
|
+
'/detect_crypto_constants': { key: 'matches', shape: { address: 'string', signature: 'string', length: 'number', kind: 'string' } },
|
|
135
|
+
'/search_byte_patterns': { key: 'matches', shape: { address: 'string', signature: 'string', length: 'number', kind: 'string' } },
|
|
136
|
+
'/get_function_call_graph': { key: 'edges', shape: { caller: 'string', callee: 'string' } },
|
|
137
|
+
};
|
|
138
|
+
const checkedRows = rowShapes[path];
|
|
139
|
+
if (checkedRows)
|
|
140
|
+
for (const row of value[checkedRows.key])
|
|
141
|
+
checkShape(row, checkedRows.shape);
|
|
142
|
+
if (value.scan !== undefined) {
|
|
143
|
+
const scan = checkShape(value.scan, { visited: 'number', maximum: 'number', complete: 'boolean', reason: 'string' });
|
|
144
|
+
if (!Number.isSafeInteger(scan.visited) || !Number.isSafeInteger(scan.maximum) || Number(scan.visited) < 0 || Number(scan.visited) > Number(scan.maximum))
|
|
145
|
+
fail('partial', 'Query scan counters are inconsistent.');
|
|
146
|
+
if (scan.complete === false && value.truncated !== true)
|
|
147
|
+
fail('partial', 'Incomplete query scan was reported as untruncated.');
|
|
148
|
+
}
|
|
149
|
+
for (const entry of [['globals', 'count'], ['strings', 'count'], ['matches', 'count'], ['types', 'count']])
|
|
150
|
+
if (Array.isArray(value[entry[0]]) && value[entry[1]] !== undefined && value[entry[1]] !== value[entry[0]].length)
|
|
151
|
+
fail('partial', 'Query row count is inconsistent.');
|
|
152
|
+
if (path === '/find_dead_code' && (value.complete === false || value.unresolved_flow === true) && value.findings.length)
|
|
153
|
+
fail('partial', 'Incomplete CFG analysis cannot classify unreachable blocks.');
|
|
154
|
+
if (path === '/detect_crypto_constants' || path === '/search_byte_patterns') {
|
|
155
|
+
if (Number(value.scanned_bytes) > Number(value.maximum_bytes) || Number(value.scanned_bytes) < 0)
|
|
156
|
+
fail('partial', 'Byte scan exceeded its declared budget.');
|
|
157
|
+
if (value.complete === false && typeof value.next_address !== 'string')
|
|
158
|
+
fail('partial', 'Partial byte scan omitted its continuation.');
|
|
159
|
+
if (path === '/detect_crypto_constants') {
|
|
160
|
+
const database = checkShape(value.database, { version: 'string', signatures: 'array' });
|
|
161
|
+
for (const row of database.signatures)
|
|
162
|
+
checkShape(row, { name: 'string', length: 'number' });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (path === '/get_entry_points') {
|
|
166
|
+
for (const raw of value.entry_points) {
|
|
167
|
+
const row = checkShape(raw, { address: 'string', name: 'string', kind: 'string', confirmed: 'boolean', provenance: 'string' });
|
|
168
|
+
if (row.confirmed !== true || row.kind !== 'external_entry' || row.provenance !== 'ghidra.symbol.external-entry-point')
|
|
169
|
+
fail('partial', 'Entry-point metadata was replaced by an unconfirmed candidate.');
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(value.candidates))
|
|
172
|
+
fail('partial', 'Entry-point result omitted its candidates.');
|
|
173
|
+
for (const raw of value.candidates) {
|
|
174
|
+
const row = checkShape(raw, { address: 'string', name: 'string', kind: 'string', confirmed: 'boolean', provenance: 'string' });
|
|
175
|
+
if (row.confirmed !== false)
|
|
176
|
+
fail('partial', 'Entry candidate was labeled confirmed.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (path === '/validate_function_prototype' || path === '/validate_data_type') {
|
|
180
|
+
if (!Array.isArray(value.diagnostics))
|
|
181
|
+
fail('partial', 'Validation omitted parser/resolution diagnostics.');
|
|
182
|
+
for (const raw of value.diagnostics)
|
|
183
|
+
checkShape(raw, { severity: 'string', code: 'string', message: 'string' });
|
|
184
|
+
if (value.valid === true) {
|
|
185
|
+
if (path === '/validate_data_type')
|
|
186
|
+
checkShape(value.resolved, { name: 'string', path: 'string', size: 'number', alignment: 'number' });
|
|
187
|
+
else
|
|
188
|
+
checkShape(value.signature, { name: 'string', return_type: 'object', parameters: 'array', calling_convention: 'string' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (path === '/emulate_function' && value.completed !== undefined) {
|
|
192
|
+
checkShape(value, { completed: 'boolean', hit_return: 'boolean', abi: 'string', pointer_bytes: 'number' });
|
|
193
|
+
if (value.completed !== value.hit_return || value.completed === true && value.stop_reason !== 'return')
|
|
194
|
+
fail('partial', 'Emulation termination was reported inconsistently.');
|
|
195
|
+
}
|
|
196
|
+
if (path === '/get_comment') {
|
|
197
|
+
const rows = bulkComments ? value.results : [value];
|
|
198
|
+
if (bulkComments && (!['requested', 'returned', 'with_comments'].every(key => Number.isSafeInteger(value[key]) && Number(value[key]) >= 0) || value.returned !== rows.length || Number(value.returned) > Number(value.requested) || Number(value.with_comments) > Number(value.requested)))
|
|
199
|
+
fail('partial', 'Bulk comment counts are inconsistent.');
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
const comment = checkShape(row, { address: 'string', has_comment: 'boolean' });
|
|
202
|
+
for (const kind of ['plate', 'pre', 'eol', 'post', 'repeatable'])
|
|
203
|
+
if (comment[kind] !== null && typeof comment[kind] !== 'string')
|
|
204
|
+
fail('partial', 'Query omitted or malformed comment kind.');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (path === '/get_full_call_graph' && parameters.format === 'json_edges') {
|
|
208
|
+
const rows = value.edges;
|
|
209
|
+
if (value.edge_count !== rows.length || !Number.isSafeInteger(value.caller_count) || Number(value.caller_count) < 0)
|
|
210
|
+
fail('partial', 'Call graph counts are inconsistent.');
|
|
211
|
+
for (const row of rows)
|
|
212
|
+
checkShape(row, { caller_addr: 'string', callee_addr: 'string', caller_name: 'string', callee_name: 'string' });
|
|
213
|
+
}
|
|
214
|
+
if (path === '/get_struct_layout')
|
|
215
|
+
for (const field of value.fields)
|
|
216
|
+
checkShape(field, { offset: 'number', size: 'number', type: 'string', name: 'string' });
|
|
217
|
+
if (path === '/get_enum_values')
|
|
218
|
+
for (const member of value.values) {
|
|
219
|
+
const observed = checkShape(member, { name: 'string' }), number = observed.value;
|
|
220
|
+
if (!(typeof number === 'number' && Number.isSafeInteger(number) || typeof number === 'string' && /^-?\d+$/.test(number)))
|
|
221
|
+
fail('partial', 'Enum result omitted its exact integer value.');
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|