@eyeauras/insight-cli 0.0.0-stage → 0.1.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 +54 -2
- package/SKILL.md +55 -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 +120 -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 +387 -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 +1068 -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 +148 -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 +171 -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/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 +44 -0
- package/dist/src/server.js +455 -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 +45 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +33 -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/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/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 +47 -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,12 @@
|
|
|
1
|
+
import { type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
export interface SavedMaterials {
|
|
3
|
+
state: 'saved';
|
|
4
|
+
path: string;
|
|
5
|
+
kind: 'file' | 'directory';
|
|
6
|
+
files: number;
|
|
7
|
+
bytes: number;
|
|
8
|
+
sha256: string;
|
|
9
|
+
}
|
|
10
|
+
/** Copy user materials without initializing Ghidra, targets, sessions or Workspace. */
|
|
11
|
+
export declare function saveMaterials(appDataDirectory: string, source: string, signal?: AbortSignal): Promise<SavedMaterials>;
|
|
12
|
+
export declare function artifactsCommands(): CommandDefinition[];
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { command, CliError, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, open, opendir, realpath, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
const maximumEntries = 100_000, maximumDepth = 128;
|
|
7
|
+
function fail(code, message) { throw new CliError(message, { code: 'artifacts.' + code }); }
|
|
8
|
+
function check(signal) { if (signal?.aborted)
|
|
9
|
+
fail('cancelled', 'Saving materials was cancelled; no snapshot was published.'); }
|
|
10
|
+
function inside(parent, child) {
|
|
11
|
+
const value = relative(parent, child);
|
|
12
|
+
return value === '' || (!isAbsolute(value) && value !== '..' && !value.startsWith('..' + sep));
|
|
13
|
+
}
|
|
14
|
+
function stamp(info) { return [info.dev, info.ino, info.mode, info.size, info.mtimeNs, info.ctimeNs].join(':'); }
|
|
15
|
+
async function syncDirectory(path) {
|
|
16
|
+
if (process.platform === 'win32')
|
|
17
|
+
return;
|
|
18
|
+
const handle = await open(path, constants.O_RDONLY);
|
|
19
|
+
try {
|
|
20
|
+
await handle.sync();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await handle.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function inventory(source, signal) {
|
|
27
|
+
const entries = [];
|
|
28
|
+
async function visit(path, name, depth) {
|
|
29
|
+
check(signal);
|
|
30
|
+
if (depth > maximumDepth || entries.length >= maximumEntries)
|
|
31
|
+
fail('limit', 'Materials exceed the traversal bound of 100000 entries or 128 directory levels; save a smaller selection.');
|
|
32
|
+
const info = await lstat(path, { bigint: true });
|
|
33
|
+
if (!info.isFile() && !info.isDirectory())
|
|
34
|
+
fail('unsupported', 'Materials must contain only ordinary files and directories; symbolic links and special files are unsupported.');
|
|
35
|
+
if (info.size > BigInt(Number.MAX_SAFE_INTEGER))
|
|
36
|
+
fail('limit', 'A materials entry exceeds the supported size counter.');
|
|
37
|
+
entries.push({ path: name, kind: info.isFile() ? 'file' : 'directory', stamp: stamp(info), size: Number(info.size) });
|
|
38
|
+
if (info.isDirectory()) {
|
|
39
|
+
// Sorting by code unit keeps manifest digests independent of locale and enumeration order.
|
|
40
|
+
const names = [];
|
|
41
|
+
for await (const child of await opendir(path)) {
|
|
42
|
+
check(signal);
|
|
43
|
+
if (names.length + entries.length >= maximumEntries)
|
|
44
|
+
fail('limit', 'Materials exceed the traversal bound of 100000 entries; save a smaller selection.');
|
|
45
|
+
names.push(child.name);
|
|
46
|
+
}
|
|
47
|
+
names.sort();
|
|
48
|
+
for (const child of names)
|
|
49
|
+
await visit(join(path, child), name ? name + '/' + child : child, depth + 1);
|
|
50
|
+
if (stamp(await lstat(path, { bigint: true })) !== stamp(info))
|
|
51
|
+
fail('changed', 'Source changed while saving; save a stable file or directory.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await visit(source, '', 0);
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
async function verifyParents(source, entry, entries) {
|
|
58
|
+
let parent = entry.path ? entry.path.split('/').slice(0, -1).join('/') : undefined;
|
|
59
|
+
while (parent !== undefined) {
|
|
60
|
+
const expected = entries.get(parent);
|
|
61
|
+
if (stamp(await lstat(parent ? join(source, parent) : source, { bigint: true })) !== expected.stamp)
|
|
62
|
+
fail('changed', 'Source directory changed while saving; no snapshot was published.');
|
|
63
|
+
parent = parent ? parent.split('/').slice(0, -1).join('/') : undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Copy user materials without initializing Ghidra, targets, sessions or Workspace. */
|
|
67
|
+
export async function saveMaterials(appDataDirectory, source, signal) {
|
|
68
|
+
let stage, published;
|
|
69
|
+
try {
|
|
70
|
+
check(signal);
|
|
71
|
+
const sourcePath = resolve(source);
|
|
72
|
+
// Reject a symbolic source itself before resolving its parents (e.g. macOS /tmp).
|
|
73
|
+
const info = await lstat(sourcePath, { bigint: true });
|
|
74
|
+
if (!info.isFile() && !info.isDirectory())
|
|
75
|
+
fail('unsupported', 'Save an ordinary file or directory; symbolic links and special files are unsupported.');
|
|
76
|
+
const canonicalSource = await realpath(sourcePath);
|
|
77
|
+
await mkdir(appDataDirectory, { recursive: true, mode: 0o700 });
|
|
78
|
+
const appData = await realpath(appDataDirectory), storage = join(appData, 'materials');
|
|
79
|
+
if (info.isDirectory() && inside(canonicalSource, storage))
|
|
80
|
+
fail('recursive', 'Source contains the materials storage; choose a source outside its storage ancestry.');
|
|
81
|
+
await mkdir(storage, { recursive: true, mode: 0o700 });
|
|
82
|
+
if ((await lstat(storage)).isSymbolicLink() || await realpath(storage) !== storage)
|
|
83
|
+
fail('storage', 'Materials storage must be an ordinary profile-owned directory.');
|
|
84
|
+
const entries = await inventory(canonicalSource, signal), byPath = new Map(entries.map(entry => [entry.path, entry]));
|
|
85
|
+
if (entries[0].stamp !== stamp(info))
|
|
86
|
+
fail('changed', 'Source changed while preparing its snapshot; save a stable file or directory.');
|
|
87
|
+
const identity = randomUUID(), candidate = join(storage, '.pending-' + identity);
|
|
88
|
+
const final = join(storage, identity), content = join(candidate, 'content');
|
|
89
|
+
await mkdir(candidate, { mode: 0o700 });
|
|
90
|
+
stage = candidate;
|
|
91
|
+
await mkdir(content, { mode: 0o700 });
|
|
92
|
+
const name = basename(canonicalSource) || 'root', output = join(content, name);
|
|
93
|
+
const manifest = [], directories = [stage, content];
|
|
94
|
+
let files = 0, bytes = 0, fileDigest;
|
|
95
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
check(signal);
|
|
98
|
+
await verifyParents(canonicalSource, entry, byPath);
|
|
99
|
+
const inputPath = entry.path ? join(canonicalSource, entry.path) : canonicalSource;
|
|
100
|
+
const outputPath = entry.path ? join(output, entry.path) : output;
|
|
101
|
+
if (entry.kind === 'directory') {
|
|
102
|
+
await mkdir(outputPath, { mode: 0o700 });
|
|
103
|
+
directories.push(outputPath);
|
|
104
|
+
manifest.push({ path: entry.path, kind: 'directory' });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// NONBLOCK avoids a hang if a regular source is exchanged for a FIFO before open.
|
|
108
|
+
const input = await open(inputPath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
109
|
+
try {
|
|
110
|
+
const before = await input.stat({ bigint: true });
|
|
111
|
+
if (!before.isFile() || stamp(before) !== entry.stamp)
|
|
112
|
+
fail('changed', 'Source file changed while saving; no snapshot was published.');
|
|
113
|
+
const destination = await open(outputPath, 'wx', 0o600), hash = createHash('sha256');
|
|
114
|
+
let copied = 0;
|
|
115
|
+
try {
|
|
116
|
+
while (true) {
|
|
117
|
+
check(signal);
|
|
118
|
+
const { bytesRead } = await input.read(buffer, 0, buffer.length, copied);
|
|
119
|
+
if (!bytesRead)
|
|
120
|
+
break;
|
|
121
|
+
copied += bytesRead;
|
|
122
|
+
if (copied > entry.size)
|
|
123
|
+
fail('changed', 'Source file grew while saving; no snapshot was published.');
|
|
124
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
125
|
+
hash.update(chunk);
|
|
126
|
+
await destination.writeFile(chunk);
|
|
127
|
+
}
|
|
128
|
+
if (copied !== entry.size || stamp(await input.stat({ bigint: true })) !== entry.stamp ||
|
|
129
|
+
stamp(await lstat(inputPath, { bigint: true })) !== entry.stamp)
|
|
130
|
+
fail('changed', 'Source file changed while saving; no snapshot was published.');
|
|
131
|
+
await destination.sync();
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await destination.close();
|
|
135
|
+
}
|
|
136
|
+
const sha256 = hash.digest('hex');
|
|
137
|
+
manifest.push({ path: entry.path, kind: 'file', bytes: copied, sha256 });
|
|
138
|
+
fileDigest = sha256;
|
|
139
|
+
files++;
|
|
140
|
+
bytes += copied;
|
|
141
|
+
if (!Number.isSafeInteger(bytes))
|
|
142
|
+
fail('limit', 'Materials exceed the supported size counter.');
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await input.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Detect edits to earlier files and directory membership changes during the entire capture.
|
|
149
|
+
const after = await inventory(canonicalSource, signal);
|
|
150
|
+
if (JSON.stringify(after) !== JSON.stringify(entries) || await realpath(sourcePath) !== canonicalSource ||
|
|
151
|
+
stamp(await lstat(sourcePath, { bigint: true })) !== stamp(info))
|
|
152
|
+
fail('changed', 'Source changed while saving; no snapshot was published.');
|
|
153
|
+
const manifestBytes = JSON.stringify({ version: 1, entries: manifest }) + '\n';
|
|
154
|
+
const metadata = await open(join(stage, 'manifest.json'), 'wx', 0o600);
|
|
155
|
+
try {
|
|
156
|
+
await metadata.writeFile(manifestBytes);
|
|
157
|
+
await metadata.sync();
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await metadata.close();
|
|
161
|
+
}
|
|
162
|
+
for (const directory of directories.reverse())
|
|
163
|
+
await syncDirectory(directory);
|
|
164
|
+
check(signal);
|
|
165
|
+
await rename(stage, final);
|
|
166
|
+
published = final;
|
|
167
|
+
stage = undefined;
|
|
168
|
+
await syncDirectory(storage);
|
|
169
|
+
return { state: 'saved', path: join(final, 'content', name), kind: entries[0].kind, files, bytes,
|
|
170
|
+
sha256: entries[0].kind === 'file' ? fileDigest : createHash('sha256').update(manifestBytes).digest('hex') };
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
let cleanupFailed = false;
|
|
174
|
+
if (stage)
|
|
175
|
+
await rm(stage, { recursive: true, force: true }).catch(() => { cleanupFailed = true; });
|
|
176
|
+
// A directory-sync failure is not acknowledged as a durable save.
|
|
177
|
+
if (published)
|
|
178
|
+
await rm(published, { recursive: true, force: true }).catch(() => { cleanupFailed = true; });
|
|
179
|
+
if (error instanceof CliError) {
|
|
180
|
+
if (cleanupFailed)
|
|
181
|
+
throw new CliError(error.message + ' Some owned temporary files could not be removed; preserve the profile for cleanup.', { code: error.code });
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const code = error.code;
|
|
185
|
+
fail('save', `Could not save materials${code && /^[A-Z0-9_]+$/.test(code) ? ' (' + code + ')' : ''}; no complete snapshot was acknowledged.${cleanupFailed ? ' Some owned temporary files could not be removed; preserve the profile for cleanup.' : ''}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function artifactsCommands() {
|
|
189
|
+
return [command('artifacts', 'Retain ordinary research files independently of analysis tools', [
|
|
190
|
+
command('save <path>', 'Save a complete independent copy of a file or directory in this profile', ({ args }, context) => {
|
|
191
|
+
if (typeof args.path !== 'string' || !args.path || args.path.includes('\0'))
|
|
192
|
+
fail('invalid', 'Provide one file or directory path.');
|
|
193
|
+
return saveMaterials(context.appArguments.AppDataDirectory, resolve(context.cwd, args.path), context.signal);
|
|
194
|
+
}, { permission: Permission.Update }),
|
|
195
|
+
])];
|
|
196
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ScriptItem {
|
|
2
|
+
script: string;
|
|
3
|
+
name: string;
|
|
4
|
+
language: 'Java' | 'Python';
|
|
5
|
+
origin: 'installation' | 'context' | 'extension';
|
|
6
|
+
category?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ScriptPage {
|
|
9
|
+
items: ScriptItem[];
|
|
10
|
+
count: number;
|
|
11
|
+
total: number;
|
|
12
|
+
offset: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve only an explicit inventory ID; native search paths never choose another script. */
|
|
17
|
+
export declare function scriptPath(home: string, contextRoot: string, script: string, extensions?: string[]): Promise<string>;
|
|
18
|
+
/** Enumerate configured installation and owned context scripts without exposing native paths. */
|
|
19
|
+
export declare function scriptInventory(home: string, contextRoot: string, filter: string, limit: number, offset: number, extensions?: string[]): Promise<ScriptPage>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readdir, open } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { fail } from './model.js';
|
|
5
|
+
/** Resolve only an explicit inventory ID; native search paths never choose another script. */
|
|
6
|
+
export async function scriptPath(home, contextRoot, script, extensions = []) {
|
|
7
|
+
if (!/^(installation|context|extension):[^\0]+\.(java|py)$/.test(script) || script.includes('..') || script.includes('\\'))
|
|
8
|
+
fail('invalid', 'Choose an explicit script ID returned by ghidra scripts list.');
|
|
9
|
+
const page = await scriptInventory(home, contextRoot, script, 20000, 0, extensions), item = page.items.find(item => item.script === script);
|
|
10
|
+
if (!item)
|
|
11
|
+
fail('notFound', 'Script ID is absent from this context and installation.');
|
|
12
|
+
const part = script.slice(script.indexOf(':') + 1);
|
|
13
|
+
if (item.origin === 'extension') {
|
|
14
|
+
const [module, ...rest] = part.split('/'), root = extensions.find(p => p.split(/[\\/]/).pop() === module);
|
|
15
|
+
if (!root)
|
|
16
|
+
fail('binding', 'Script module is no longer enabled.');
|
|
17
|
+
return join(root, 'ghidra_scripts', ...rest);
|
|
18
|
+
}
|
|
19
|
+
if (item.origin === 'context')
|
|
20
|
+
return join(contextRoot, 'ghidra_scripts', part);
|
|
21
|
+
const segments = part.split('/'), filename = segments.pop();
|
|
22
|
+
return join(home, 'Ghidra', ...segments, 'ghidra_scripts', filename);
|
|
23
|
+
}
|
|
24
|
+
/** Enumerate configured installation and owned context scripts without exposing native paths. */
|
|
25
|
+
export async function scriptInventory(home, contextRoot, filter, limit, offset, extensions = []) {
|
|
26
|
+
const items = [];
|
|
27
|
+
async function scripts(folder, origin, module) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = await readdir(folder, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT')
|
|
34
|
+
return;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38
|
+
if (!entry.isFile() || !entry.name.endsWith('.java') && !entry.name.endsWith('.py'))
|
|
39
|
+
continue;
|
|
40
|
+
if (items.length >= 20000)
|
|
41
|
+
fail('limit', 'Script inventory exceeds 20000 entries.');
|
|
42
|
+
const name = entry.name, script = origin + ':' + (module ? module + '/' : '') + name;
|
|
43
|
+
const handle = await open(join(folder, name), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
44
|
+
let category;
|
|
45
|
+
try {
|
|
46
|
+
const data = Buffer.alloc(4096), read = await handle.read(data, 0, data.length, 0);
|
|
47
|
+
if (!(await handle.stat()).isFile())
|
|
48
|
+
fail('binding', 'Script inventory file changed during inspection.');
|
|
49
|
+
category = /^\s*(?:\/\/|#)\s*@category\s+([^\r\n]+)/m.exec(data.subarray(0, read.bytesRead).toString('utf8'))?.[1]?.trim().slice(0, 256);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
items.push({ script, name, language: name.endsWith('.java') ? 'Java' : 'Python', origin, ...(category ? { category } : {}) });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const installed = join(home, 'Ghidra');
|
|
58
|
+
async function modules(folder, depth) {
|
|
59
|
+
if (depth > 16)
|
|
60
|
+
fail('limit', 'Script module nesting exceeds 16 levels.');
|
|
61
|
+
for (const entry of (await readdir(folder, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
62
|
+
if (!entry.isDirectory())
|
|
63
|
+
continue;
|
|
64
|
+
const path = join(folder, entry.name);
|
|
65
|
+
if (entry.name === 'ghidra_scripts')
|
|
66
|
+
await scripts(path, 'installation', relative(installed, folder).split(sep).join('/'));
|
|
67
|
+
else if (!['data', 'lib', 'os', 'src', 'docs'].includes(entry.name))
|
|
68
|
+
await modules(path, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await modules(installed, 0);
|
|
72
|
+
for (const root of extensions)
|
|
73
|
+
await scripts(join(root, 'ghidra_scripts'), 'extension', root.split(/[\\/]/).pop());
|
|
74
|
+
await scripts(join(contextRoot, 'ghidra_scripts'), 'context', '');
|
|
75
|
+
const matched = items.filter(item => !filter || [item.name, item.script, item.category ?? ''].some(value => value.toLowerCase().includes(filter.toLowerCase())))
|
|
76
|
+
.sort((a, b) => a.script.localeCompare(b.script));
|
|
77
|
+
const page = matched.slice(offset, offset + limit);
|
|
78
|
+
return { items: page, count: page.length, total: matched.length, offset, limit, truncated: offset + page.length < matched.length };
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Parameter } from './arguments.js';
|
|
2
|
+
interface SdkOperation {
|
|
3
|
+
operation: string;
|
|
4
|
+
group: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
write?: boolean;
|
|
8
|
+
pair?: boolean;
|
|
9
|
+
params: Parameter[];
|
|
10
|
+
}
|
|
11
|
+
export declare const sdkOperations: SdkOperation[];
|
|
12
|
+
export declare const sdkWrites: Set<string>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const str = (name, required = true) => ({ name, type: 'string', required });
|
|
2
|
+
const at = (name = 'address') => ({ ...str(name), semantic: 'address' });
|
|
3
|
+
const json = (name, type = 'object', required = true) => ({ name, type, required });
|
|
4
|
+
const count = (name, value) => ({ name, type: 'integer', required: false, default: value });
|
|
5
|
+
const empty = (name) => ({ ...str(name, false), default: '', allowEmpty: true });
|
|
6
|
+
const bool = (name) => ({ name, type: 'boolean', required: false, default: 'false' });
|
|
7
|
+
const original = [str('name'), at('expect_start'), { ...count('expect_size', '1'), required: true }, str('expect_sha256')];
|
|
8
|
+
export const sdkOperations = [
|
|
9
|
+
{ operation: 'type-layout', group: 'types', name: 'layout', description: 'Read native packing, alignment, bitfields, settings and source archive identity', params: [str('path')] },
|
|
10
|
+
{ operation: 'type-structure', group: 'types', name: 'define-layout', description: 'Create a native structure with packing/alignment/bitfields; replace only an explicitly selected empty placeholder', write: true, params: [str('path'), json('fields', 'array'), { name: 'packing', type: 'integer', required: false, default: '0' }, { name: 'alignment', type: 'integer', required: false, default: '0' }, bool('replace_placeholder')] },
|
|
11
|
+
{ operation: 'type-settings', group: 'types', name: 'set-settings', description: 'Edit default settings by actual native definition names; null clears an explicit setting', write: true, params: [str('path'), json('settings')] },
|
|
12
|
+
{ operation: 'type-archive-import', group: 'types', name: 'archive-import', description: 'Import explicit GDT type paths with archive identity and saved layout verification', write: true, params: [str('input'), json('types', 'array')] },
|
|
13
|
+
{ operation: 'changes', group: 'changes', name: 'compare', description: 'Categorized native ProgramDiff of two saved private contexts; does not merge', pair: true, params: [{ ...json('categories', 'array', false), default: '["bytes","comments","symbols","functions"]' }, count('max_ranges', '100'), count('timeout_seconds', '30')] },
|
|
14
|
+
{ operation: 'block-state', group: 'memory', name: 'block-state', description: 'Read exact block layout and bounded full-content digest', params: [str('name')] },
|
|
15
|
+
{ operation: 'assembly-preview', group: 'assembly', name: 'preview', description: 'Assemble in this program language and processor context without writing', params: [at(), json('instructions', 'array')] },
|
|
16
|
+
{ operation: 'patch-bytes', group: 'memory', name: 'patch-bytes', description: 'Replace equal-length bytes only when the expected bytes match; clears overlapping listing', write: true, params: [at(), str('expect_hex'), str('bytes_hex')] },
|
|
17
|
+
{ operation: 'assemble', group: 'assembly', name: 'apply', description: 'Apply context-aware assembly with exact byte preconditions; clears overlapping listing', write: true, params: [at(), str('expect_hex'), json('instructions', 'array')] },
|
|
18
|
+
{ operation: 'split-block', group: 'memory', name: 'split-block', description: 'Split one initialized block while verifying all original bytes and layout', write: true, params: [...original, at('split_address'), str('new_name')] },
|
|
19
|
+
{ operation: 'move-block', group: 'memory', name: 'move-block', description: 'Move a block only when its original layout and bytes match', write: true, params: [...original, at()] },
|
|
20
|
+
{ operation: 'join-blocks', group: 'memory', name: 'join-blocks', description: 'Join adjacent compatible blocks with independent content and size preconditions', write: true, params: [str('name'), str('second_name'), str('expect_sha256'), str('second_sha256'), { ...count('expect_size', '1'), required: true }, { ...count('second_size', '1'), required: true }] },
|
|
21
|
+
{ operation: 'context-read', group: 'processor-context', name: 'read', description: 'Read bounded register-value ranges in the saved program context', params: [str('register'), at(), { ...at('end_address'), required: false }, count('limit', '100')] },
|
|
22
|
+
{ operation: 'context-set', group: 'processor-context', name: 'set', description: 'Set a processor register value over at most 65536 bytes; requires unchanged-base publication', write: true, params: [str('register'), at(), at('end_address'), str('value')] },
|
|
23
|
+
{ operation: 'context-clear', group: 'processor-context', name: 'clear', description: 'Remove an explicit processor register value over at most 65536 bytes', write: true, params: [str('register'), at(), at('end_address')] },
|
|
24
|
+
{ operation: 'namespace-list', group: 'namespaces', name: 'list', description: 'List direct namespace/class children with explicit visited-symbol budget', params: [empty('parent'), count('limit', '100'), count('max_scan', '10000')] },
|
|
25
|
+
{ operation: 'namespace-create', group: 'namespaces', name: 'create', description: 'Create an explicit namespace or class below an existing parent', write: true, params: [empty('parent'), str('name'), bool('class')] },
|
|
26
|
+
{ operation: 'namespace-update', group: 'namespaces', name: 'update', description: 'Rename or reparent an ordinary namespace/class without recursive deletion', write: true, params: [str('path'), str('new_name'), empty('parent')] },
|
|
27
|
+
{ operation: 'namespace-delete', group: 'namespaces', name: 'delete', description: 'Delete one empty ordinary namespace/class', write: true, params: [str('path')] },
|
|
28
|
+
{ operation: 'external-list', group: 'externals', name: 'list', description: 'Read library identities or structured locations in one explicit library', params: [empty('library'), count('limit', '100')] },
|
|
29
|
+
{ operation: 'library-create', group: 'externals', name: 'create-library', description: 'Create a named external library in the private program', write: true, params: [str('name')] },
|
|
30
|
+
{ operation: 'library-update', group: 'externals', name: 'update-library', description: 'Rename an external library or change its logical project path', write: true, params: [str('name'), str('new_name', false), str('path', false)] },
|
|
31
|
+
{ operation: 'library-delete', group: 'externals', name: 'delete-library', description: 'Delete an empty external library', write: true, params: [str('name')] },
|
|
32
|
+
{ operation: 'external-create', group: 'externals', name: 'create-location', description: 'Create an external label/function with an optional memory address', write: true, params: [str('library'), str('label'), { ...at(), required: false }, bool('function')] },
|
|
33
|
+
{ operation: 'external-update', group: 'externals', name: 'update-location', description: 'Rename/update one unambiguous external location', write: true, params: [str('library'), str('label'), str('new_label'), { ...at(), required: false }] },
|
|
34
|
+
{ operation: 'external-delete', group: 'externals', name: 'delete-location', description: 'Delete one unambiguous external location, refusing retained dependencies', write: true, params: [str('library'), str('label')] },
|
|
35
|
+
{ operation: 'decompiler-ir', group: 'decompiler', name: 'ir', description: 'Read bounded typed SSA P-code with explicit simplification/options and completion diagnostics', params: [at(), { ...str('simplification', false), default: 'decompile' }, count('max_ops', '1000'), count('timeout_seconds', '30'), bool('c_code'), { ...json('options', 'object', false), default: '{}' }] },
|
|
36
|
+
];
|
|
37
|
+
export const sdkWrites = new Set(sdkOperations.filter(v => v.write).map(v => '/sdk/' + v.operation));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Workspace, type Scope } from './workspace.js';
|
|
3
|
+
import { Research, type ResearchBackend } from './research.js';
|
|
4
|
+
/** Native SDK file effects have retained input/output bytes and never repeat an uncertain call. */
|
|
5
|
+
export declare class SdkFiles {
|
|
6
|
+
private readonly workspace;
|
|
7
|
+
private readonly research;
|
|
8
|
+
private readonly backend;
|
|
9
|
+
constructor(workspace: Workspace, research: Research, backend: () => Promise<ResearchBackend>);
|
|
10
|
+
loaders(input: string): Promise<{
|
|
11
|
+
inputSha256: string;
|
|
12
|
+
}>;
|
|
13
|
+
exporters(selected: Scope): Promise<{
|
|
14
|
+
value: Record<string, unknown>;
|
|
15
|
+
session: string;
|
|
16
|
+
target: string;
|
|
17
|
+
}>;
|
|
18
|
+
private copy;
|
|
19
|
+
private completedOutput;
|
|
20
|
+
databaseQuery(selected: Scope, operation: 'fid-query' | 'bsim-query', database: string, p: Record<string, unknown>): Promise<{
|
|
21
|
+
databaseSha256: string;
|
|
22
|
+
sourceSnapshotSha256: string;
|
|
23
|
+
value: Record<string, unknown>;
|
|
24
|
+
session: string;
|
|
25
|
+
target: string;
|
|
26
|
+
}>;
|
|
27
|
+
export(selected: Scope, operation: 'export' | 'archive-export' | 'fid-build' | 'bsim-build', parameters: Record<string, unknown>, directory: string, callerId?: string): Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
export declare function sdkFileCommands(service: (c: CommandContext) => SdkFiles): CommandDefinition[];
|