feat(ghidra): add atomic function annotation edits (#709)
* feat(ghidra): edit function annotations atomically Return annotation readback and refreshed function analysis through CLI and MCP. Own each database transaction, invalidate caches and immutable snapshots, preserve executable bytes, and retain the Windows mutation boundary. Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering) * fix(ghidra): preserve qualified function annotation identity Preserve namespaces during local name writes and match qualified readback to the refreshed dossier. Add a producer-shaped regression and verify a real namespaced C++ function through the CLI. Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering) * test(ghidra): align verification with annotation authority Keep strict capability and profile checks while admitting the explicit session metadata operation. Verify packaged transport authority and atomic transaction commitments and preserve a read-only Windows expectation. Assisted-by: Nitori (gensokyo-skills:nitori-reverse-engineering)
This commit is contained in:
parent
c3bc38bf6c
commit
9bab55337a
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Product Direction
|
||||
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
|
||||
Prioritize:
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -149,7 +149,7 @@ Update either installation with `rea update`.
|
|||
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+
|
||||
- npm; REA does not require or install a particular npm version
|
||||
|
||||
Native binary analysis requires [Hopper](https://www.hopperapp.com/) or [Ghidra](#ghidra-read-only-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. REA can use Ghidra that you have already installed.
|
||||
Native binary analysis requires [Hopper](https://www.hopperapp.com/) or [Ghidra](#ghidra-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. REA can use Ghidra that you have already installed.
|
||||
|
||||
Windows Ghidra support is experimental for native x86-64 PE applications on local NTFS. The package bundles native Job Object, private-DACL, and path-admission controls. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ Install the missing packages and rerun `rea setup`. The Linux demo needs Xvfb, P
|
|||
|
||||
REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
|
||||
|
||||
### Ghidra read-only analysis provider
|
||||
### Ghidra analysis provider
|
||||
|
||||
Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It requires **Ghidra 12.1.4** and a **64-bit JDK 21**. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ rea providers --json
|
|||
|
||||
Setup checks the installations and saves their paths in your selected agents' configuration after approval. Ghidra and Java must already be installed; REA does not download or change them.
|
||||
|
||||
The adapter exposes **25 read-only operations** for functions, strings, symbols, assembly, decompilation, calls, references, instructions, and data types. These also support REA's overview, search, call-graph, and function-analysis workflows. Ghidra does not provide GUI controls or annotation changes through REA.
|
||||
The adapter exposes inventory, function, memory and load-image inspection, plus atomic function annotation edits on Linux and macOS. `annotate_native_function` edits names and entry comments in the session database and returns a refreshed function dossier; executable bytes stay unchanged. Ghidra does not provide GUI controls through REA.
|
||||
|
||||
REA analyzes a temporary copy of the target and removes the temporary project when the session closes. Results identify what Ghidra observed and what it could not resolve. Decompilation produces pseudocode rather than the original source.
|
||||
|
||||
|
|
@ -307,7 +307,7 @@ See [native investigation](docs/native-investigation.md) for keyed archives, ins
|
|||
|
||||
| Tool family | Count | Examples |
|
||||
| ------------------------- | ----: | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native inspection | 40 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
|
||||
| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
|
||||
| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery |
|
||||
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper |
|
||||
| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction |
|
||||
|
|
@ -453,7 +453,7 @@ flowchart LR
|
|||
REA --> Session["Target-bound session router"]
|
||||
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
|
||||
Registry --> Hopper["Hopper provider"]
|
||||
Registry --> Ghidra["Ghidra provider<br/>read-only inventory + function analysis"]
|
||||
Registry --> Ghidra["Ghidra provider<br/>inventory + function analysis + annotations"]
|
||||
Hopper --> Runtime["Owned provider runtime<br/>deadline + bounded diagnostics + cleanup"]
|
||||
Ghidra --> Runtime
|
||||
Session --> Native["Native macOS provider"]
|
||||
|
|
@ -522,7 +522,7 @@ Use `--provider`, or `provider_id` in MCP, to choose Hopper or Ghidra for a targ
|
|||
|
||||
With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool.
|
||||
|
||||
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra exposes 25 read-only operations on supported Linux and macOS hosts. GUI controls and annotation changes require Hopper. Windows Ghidra P0 supports native x86-64 PE applications on local NTFS with bundled native controls.
|
||||
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls.
|
||||
|
||||
The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details.
|
||||
|
||||
|
|
@ -628,7 +628,7 @@ No. Setup can install Hopper for you, but Hopper remains separate software with
|
|||
<details>
|
||||
<summary><strong>Does REA install or include Ghidra or Java?</strong></summary>
|
||||
|
||||
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-read-only-analysis-provider).
|
||||
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-analysis-provider).
|
||||
|
||||
</details>
|
||||
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ rea setup
|
|||
|
||||
| عائلة الأدوات | العدد | الاستخدام |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 40 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
|
||||
| فحص الملفات التنفيذية | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
|
||||
| التحليل المركب | 14 | النظرة العامة وتحليل الدوال وفك الترجمة الدفعي ومخطط الاستدعاء وفحص Swift وObjC |
|
||||
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift |
|
||||
| الملفات والحزم | 5 | فحص الدلائل والحزم وInterface Builder وموارد Apple والاستخراج |
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
|||
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | --------------------------------------------------------------------------- |
|
||||
| バイナリ検査 | 40 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
|
||||
| バイナリ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
|
||||
| 組み合わせた解析 | 14 | 概要、関数解析、一括逆コンパイル、呼び出しグラフ、Swift と ObjC |
|
||||
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
|
||||
| ファイルとパッケージ | 5 | ディレクトリとパッケージ、Interface Builder、Apple リソース、抽出 |
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
|||
|
||||
| 도구 분류 | 수 | 용도 |
|
||||
| ----------------- | --: | -------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 40 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
|
||||
| 바이너리 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
|
||||
| 결합 분석 | 14 | 개요, 함수 분석, 일괄 디컴파일, 호출 그래프, Swift와 ObjC 검사 |
|
||||
| macOS 네이티브 | 7 | Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
|
||||
| 파일과 패키지 | 5 | 디렉터리와 패키지 검사, Interface Builder, Apple 리소스, 추출 |
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
|||
|
||||
| 工具类别 | 数量 | 用途 |
|
||||
| ----------------- | ---: | --------------------------------------------------------- |
|
||||
| 二进制检查 | 40 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
|
||||
| 二进制检查 | 41 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
|
||||
| 组合分析 | 14 | 概览、函数分析、批量反编译、调用图、Swift 与 ObjC 检查 |
|
||||
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原 |
|
||||
| 制品检查 | 5 | 目录与软件包检查、Interface Builder、Apple 资源目录与提取 |
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
"params"
|
||||
);
|
||||
private static final String[] CAPABILITIES = {
|
||||
"annotate_native_function",
|
||||
"inspect_native_load_image",
|
||||
"read_bytes",
|
||||
"address_to_file_offset",
|
||||
|
|
@ -191,6 +192,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
"Ghidra imported-byte digest does not match the admitted target"
|
||||
);
|
||||
}
|
||||
// GhidraScript wraps run() in a transaction. End it before serving so
|
||||
// each edit owns a real outer transaction and can roll back immediately.
|
||||
end(true);
|
||||
try {
|
||||
initializeDecompiler();
|
||||
serve(descriptor);
|
||||
|
|
@ -363,6 +367,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
case "resolve_native_call_targets" -> resolveNativeCallTargets(request.params);
|
||||
case "xrefs" -> xrefs(request.params);
|
||||
case "analyze_function" -> analyzeFunction(request.params);
|
||||
case "annotate_native_function" -> {
|
||||
if (!descriptor.transport.equals("unix-socket"))
|
||||
throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation");
|
||||
yield annotateNativeFunction(request.params);
|
||||
}
|
||||
default -> throw new RequestFailure(
|
||||
"method_unavailable",
|
||||
"Bridge method is unavailable"
|
||||
|
|
@ -395,10 +404,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
result.addProperty("run_id", descriptor.runId);
|
||||
result.addProperty("profile_digest", descriptor.profileDigest);
|
||||
result.add("provider", provider);
|
||||
result.addProperty("read_only", true);
|
||||
boolean readOnly = !descriptor.transport.equals("unix-socket");
|
||||
result.addProperty("read_only", readOnly);
|
||||
result.addProperty("analysis_complete", !timedOut);
|
||||
result.addProperty("analysis_timed_out", timedOut);
|
||||
result.add("capabilities", GSON.toJsonTree(CAPABILITIES));
|
||||
result.add("capabilities", GSON.toJsonTree(
|
||||
java.util.Arrays.stream(CAPABILITIES)
|
||||
.filter(value -> !readOnly || !value.equals("annotate_native_function"))
|
||||
.toArray(String[]::new)
|
||||
));
|
||||
result.add("target", target);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -954,6 +968,90 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
|||
return result;
|
||||
}
|
||||
|
||||
private void invalidateAnalysisCaches() {
|
||||
nameInventory = null;
|
||||
procedureInventory = null;
|
||||
stringInventory = null;
|
||||
if (decompiler != null) decompiler.flushCache();
|
||||
}
|
||||
|
||||
private JsonObject annotationReadback(Function function) {
|
||||
Address entry = function.getEntryPoint();
|
||||
JsonObject value = new JsonObject();
|
||||
value.addProperty("address", canonicalAddress(entry));
|
||||
value.addProperty("name", procedureName(function));
|
||||
value.addProperty("comment", currentProgram.getListing().getComment(CommentType.PRE, entry));
|
||||
value.addProperty("inline_comment", currentProgram.getListing().getComment(CommentType.EOL, entry));
|
||||
return value;
|
||||
}
|
||||
|
||||
private JsonObject annotateNativeFunction(JsonObject params) throws Exception {
|
||||
if (!Set.of("procedure", "name", "comment", "inline_comment").containsAll(params.keySet()))
|
||||
throw new RequestFailure("invalid_request", "Unknown function annotation field");
|
||||
if (!params.has("name") && !params.has("comment") && !params.has("inline_comment"))
|
||||
throw new RequestFailure("invalid_request", "Supply at least one annotation change");
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
Address entry = function.getEntryPoint();
|
||||
if (function.isExternal() || !currentProgram.getMemory().contains(entry))
|
||||
throw new RequestFailure("invalid_request", "Annotations require a local function entry: " + canonicalAddress(entry));
|
||||
int transaction = currentProgram.startTransaction("REA function annotations");
|
||||
boolean commit = false;
|
||||
try {
|
||||
// Validate edits through Ghidra's own setters, including name rules.
|
||||
// A later invalid name must roll back earlier comment writes.
|
||||
for (String field : List.of("comment", "inline_comment")) {
|
||||
if (params.has(field)) {
|
||||
String text = requireText(params, field);
|
||||
CommentType type = field.equals("comment") ? CommentType.PRE : CommentType.EOL;
|
||||
currentProgram.getListing().setComment(entry, type, text.isEmpty() ? null : text);
|
||||
}
|
||||
}
|
||||
if (params.has("name"))
|
||||
function.setName(requireString(params, "name"), SourceType.USER_DEFINED);
|
||||
monitor.checkCancelled();
|
||||
invalidateAnalysisCaches();
|
||||
JsonObject readback = annotationReadback(function);
|
||||
for (String field : List.of("name", "comment", "inline_comment")) {
|
||||
if (!params.has(field)) continue;
|
||||
String requested = requireText(params, field);
|
||||
JsonElement measured = readback.get(field);
|
||||
String expected = !field.equals("name") && requested.isEmpty() ? null : requested;
|
||||
String observed = field.equals("name") ? function.getName() :
|
||||
(measured.isJsonNull() ? null : measured.getAsString());
|
||||
if (!java.util.Objects.equals(expected, observed))
|
||||
throw new RequestFailure("annotation_readback_mismatch", "Annotation readback differs for " + field + " at " + canonicalAddress(entry));
|
||||
}
|
||||
JsonObject query = new JsonObject();
|
||||
query.addProperty("procedure", canonicalAddress(entry));
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("annotations", readback);
|
||||
result.add("dossier", analyzeFunction(query));
|
||||
JsonObject effects = new JsonObject();
|
||||
effects.addProperty("scope", "session-analysis-database");
|
||||
effects.addProperty("source_bytes_modified", false);
|
||||
effects.addProperty("persists_after_close", false);
|
||||
result.add("effects", effects);
|
||||
monitor.checkCancelled();
|
||||
commit = true;
|
||||
return result;
|
||||
}
|
||||
catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) {
|
||||
throw new RequestFailure("invalid_function_name", "Invalid function name at " + canonicalAddress(entry) + ": " + safeMessage(exception));
|
||||
}
|
||||
finally {
|
||||
currentProgram.endTransaction(transaction, commit);
|
||||
// Rollback also invalidates inventory and decompiler views.
|
||||
invalidateAnalysisCaches();
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireText(JsonObject params, String field) {
|
||||
JsonElement value = params.get(field);
|
||||
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
|
||||
throw new RequestFailure("invalid_request", "Annotation " + field + " must be text");
|
||||
return value.getAsString();
|
||||
}
|
||||
|
||||
private JsonObject analyzeFunction(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of("procedure"));
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
|
|
|
|||
|
|
@ -569,5 +569,7 @@ The first six implementation stages are shipped:
|
|||
|
||||
Future stages may deepen format and indirect-flow coverage, but must continue
|
||||
to compare normalized semantics rather than provider-specific pseudocode or
|
||||
assembly text. Ghidra continues to omit GUI and mutation operations, so
|
||||
unsupported requests cannot route to an unverified implementation.
|
||||
assembly text. Linux and macOS now admit atomic function-name and entry-comment
|
||||
edits in the ephemeral database, with readback, rollback, and cache invalidation.
|
||||
Executable bytes remain unchanged. Windows P0 retains its read-only boundary;
|
||||
GUI and other unimplemented mutations remain unavailable.
|
||||
|
|
|
|||
|
|
@ -167,3 +167,22 @@ executable fixtures, an emulator, or an existing analysis database.
|
|||
Provider p-code representations can contain process-specific address-space selector
|
||||
tokens. The lane preserves those reported values and compares stable dossier
|
||||
observations rather than asserting cross-process identity of raw p-code.
|
||||
|
||||
## Function annotations
|
||||
|
||||
`annotate_native_function` changes the name and/or comments at one exact local function entry, verifies readback, and returns a refreshed dossier. This is useful when recording recovered DOS function roles; it does not claim original-source names. Name writes preserve the existing namespace; readback returns the fully qualified name used by function inventories. Regular comments map to Ghidra PRE comments and inline comments to EOL comments. Empty text clears a comment; omitted fields preserve it. The whole edit rolls back if a setter, readback or analysis fails.
|
||||
|
||||
```json
|
||||
{
|
||||
"procedure": "0x10100",
|
||||
"name": "entry",
|
||||
"comment": "Recovered startup role",
|
||||
"inline_comment": "Inspect segment setup"
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
rea annotate-native-function /local/program.com 0x10100 --target-format dos-com --provider ghidra --name entry --comment 'Recovered startup role' --json
|
||||
```
|
||||
|
||||
The CLI returns the updated analysis before closing its isolated session. MCP edits remain visible to later reads until `close_binary`. Original bytes remain unchanged, database edits are discarded on close, and immutable analysis snapshots are invalidated after edits. This operation does not save a Ghidra project, change ABI/types/body ranges, patch instructions, or emulate a DOS runtime. Windows P0 does not admit it.
|
||||
|
|
|
|||
|
|
@ -224,9 +224,11 @@ operations and twelve function-analysis operations. These cover metadata,
|
|||
decompilation, assembly, resolved calls, typed references, xrefs, function
|
||||
dossiers, instructions, recovered data types, measured load mappings, loaded
|
||||
memory bytes, and observed file offsets. Independent load-image attestation
|
||||
currently supports DOS MZ only; PE returns its measurements with that limitation.
|
||||
GUI controls and annotation
|
||||
changes require Hopper.
|
||||
supports DOS MZ and explicitly selected COM; PE returns its measurements with that limitation.
|
||||
On Linux and macOS, `annotate_native_function` also edits a function name and/or
|
||||
entry comments atomically and returns refreshed analysis. These session metadata
|
||||
edits leave executable bytes unchanged and are discarded on close. GUI controls
|
||||
require Hopper; Windows P0 remains read-only.
|
||||
|
||||
Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes.
|
||||
The npm package bundles native Job Object ownership, protected private runtime
|
||||
|
|
|
|||
|
|
@ -150,8 +150,10 @@ the matching native decompiler. Experimental Windows x64 P0 admits native
|
|||
x86-64 PE applications on local NTFS using bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
Ghidra has no GUI or mutation authority, and REA never falls back automatically
|
||||
to Hopper.
|
||||
On Linux and macOS, `annotate_native_function` atomically edits a function name
|
||||
and entry comments in the ephemeral database, returning refreshed analysis
|
||||
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
|
||||
GUI authority, and REA never falls back automatically to Hopper.
|
||||
|
||||
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
|
|
|
|||
|
|
@ -10,15 +10,16 @@
|
|||
"skill_version": "24"
|
||||
},
|
||||
"tools": {
|
||||
"total": 117,
|
||||
"total": 118,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
"surface": "official-proxy",
|
||||
"count": 40,
|
||||
"count": 41,
|
||||
"tools": [
|
||||
"address_name",
|
||||
"address_to_file_offset",
|
||||
"annotate_native_function",
|
||||
"comment",
|
||||
"current_address",
|
||||
"current_document",
|
||||
|
|
@ -210,6 +211,7 @@
|
|||
"address_name",
|
||||
"address_to_file_offset",
|
||||
"analyze_function",
|
||||
"annotate_native_function",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"inspect_native_load_image",
|
||||
|
|
@ -469,12 +471,13 @@
|
|||
}
|
||||
],
|
||||
"cli": {
|
||||
"primary_count": 72,
|
||||
"primary_count": 73,
|
||||
"commands": [
|
||||
"address-to-file-offset",
|
||||
"analyze",
|
||||
"analyze-javascript-application",
|
||||
"analyze-web-bundle",
|
||||
"annotate-native-function",
|
||||
"build-reconstruction-obligation-ledger",
|
||||
"capabilities",
|
||||
"capture-browser-scenario",
|
||||
|
|
@ -553,15 +556,15 @@
|
|||
},
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 72,
|
||||
"mcp_tools": 117,
|
||||
"cli_commands": 73,
|
||||
"mcp_tools": 118,
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "be74884c49199aa22146a94dfbf8c11dd0ae0b87c0896e32a10b9ff691fdd342",
|
||||
"tools_sha256": "bbd9aacf7bb0bc54a8f8743491583482e1176252513d99780a309d0fddeb3df0",
|
||||
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
|
||||
"combined_sha256": "f63ae9b3480f206700cf334c54b3267aff8d0c3a7f4e782d8b5f7952cfdd5271",
|
||||
"providers_sha256": "c1a6849b2524347e6cf9e004a17ba1805f86114b2da72eb689a075afbbb1aada"
|
||||
"combined_sha256": "b4f7e5860d90b029876b58fe7cd4a8041fef1f2add23440e37e90827d808e503",
|
||||
"providers_sha256": "e094c361b60ec930a6062a1a90ee05536ce411541dba5b51ace7e2da2ba25af0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ operations after analysis completes. They cover inventories, search,
|
|||
decompilation, assembly, function metadata, resolved calls, references, control
|
||||
flow, instructions, and recovered types. Results identify the provider version,
|
||||
analysis settings, target digest, and any unavailable or incomplete facts.
|
||||
Ghidra does not expose Hopper's GUI or annotation operations.
|
||||
On Linux and macOS, `annotate_native_function` additionally edits names and entry
|
||||
comments atomically, verifies readback, and returns refreshed function analysis.
|
||||
Changes live only in the session database and leave executable bytes unchanged.
|
||||
Ghidra does not expose GUI controls; Windows P0 remains read-only.
|
||||
|
||||
Windows x64 P0 supports native x86-64 PE applications on local NTFS with
|
||||
bundled Job Object ownership, protected runtime DACLs, and handle-based path
|
||||
|
|
@ -152,12 +155,12 @@ only the generic process mechanisms.
|
|||
|
||||
## Shortlist
|
||||
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; GUI and mutation operations are unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
|
||||
## Recommended order
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ GitHub Copilot CLI, Command Code, and VS Code using each client's configuration
|
|||
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.4 and a
|
||||
64-bit full JDK 21. macOS also requires the matching native decompiler. The
|
||||
adapter exposes thirteen inventory operations and twelve function-analysis
|
||||
operations, for 25 read-only operations total. Approved setup saves verified
|
||||
operations, for 25 read-only operations total. Linux and macOS additionally support
|
||||
atomic function-name and entry-comment edits with refreshed analysis; metadata
|
||||
is discarded on close and executable bytes stay unchanged. Approved setup saves verified
|
||||
installation paths in agent configurations without installing or changing
|
||||
Ghidra or Java.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_8d5469adca2b281028f4b94f0e82c8a104c988d97542a91b51d903e764d6c779",
|
||||
"manifest_id": "ecm_125743de92b0cbb08a3cb11ee0d9305f60e5c4fb81152b8b748d348f91f99ee8",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "73d1f6ba53bb3d3a115a5a5895dd21975494147b05f3360cc19d35437d060cd0"
|
||||
"sha256": "26338313efd5c941e1f3e1f1d35e2605d47749f674c359458e76d5a470b0cbb1"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -257,7 +257,10 @@ try {
|
|||
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
|
||||
)
|
||||
).GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(name) => name !== "ping" && name !== "shutdown",
|
||||
(name) =>
|
||||
name !== "ping" &&
|
||||
name !== "shutdown" &&
|
||||
name !== "annotate_native_function",
|
||||
);
|
||||
assert.ok(
|
||||
expected.every((name) => report.mcpOperations.includes(name)),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ export async function verifyPackedBridge({
|
|||
"extends HeadlessScript",
|
||||
'request.method.equals("ping")',
|
||||
'request.method.equals("shutdown")',
|
||||
'result.addProperty("read_only", true)',
|
||||
'boolean readOnly = !descriptor.transport.equals("unix-socket")',
|
||||
'result.addProperty("read_only", readOnly)',
|
||||
'case "annotate_native_function"',
|
||||
'currentProgram.startTransaction("REA function annotations")',
|
||||
"currentProgram.endTransaction(transaction, commit)",
|
||||
"analysisTimeoutOccurred()",
|
||||
])
|
||||
if (!ghidraSource.includes(commitment))
|
||||
|
|
|
|||
|
|
@ -173,6 +173,88 @@ try {
|
|||
(await cli("address-to-file-offset", fixture.near)).normalized_result,
|
||||
mapping,
|
||||
);
|
||||
const changes = {
|
||||
procedure: fixture.entry,
|
||||
name: "rea_entry",
|
||||
comment: "Regular\nUnicode 註記",
|
||||
inline_comment: "Inline finding",
|
||||
};
|
||||
// Prime provider inventories and the session snapshot before edits.
|
||||
await call("list_names");
|
||||
const updated = await call("annotate_native_function", changes);
|
||||
assert.equal(updated.annotations.name, changes.name);
|
||||
assert.equal(updated.annotations.comment, changes.comment);
|
||||
assert.equal(updated.annotations.inline_comment, changes.inline_comment);
|
||||
assert.equal(updated.dossier.procedure.name, changes.name);
|
||||
assert.ok(updated.dossier.pseudocode.includes(changes.name));
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: changes.name }),
|
||||
fixture.entry,
|
||||
);
|
||||
assert.equal(
|
||||
await call("address_name", { address: fixture.entry }),
|
||||
changes.name,
|
||||
);
|
||||
assert.ok(
|
||||
(await call("list_procedures")).some((p) => p.value === changes.name),
|
||||
);
|
||||
assert.ok((await call("list_names")).some((p) => p.value === changes.name));
|
||||
assert.equal(
|
||||
(await call("analyze_function", { procedure: fixture.entry })).procedure
|
||||
.name,
|
||||
changes.name,
|
||||
);
|
||||
const rejected = await client.callTool({
|
||||
name: "annotate_native_function",
|
||||
arguments: {
|
||||
procedure: fixture.entry,
|
||||
comment: "MUST ROLL BACK",
|
||||
name: "invalid name\n",
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
rejected.isError,
|
||||
true,
|
||||
"Ghidra accepted an invalid function name",
|
||||
);
|
||||
const retained = await call("annotate_native_function", {
|
||||
procedure: fixture.entry,
|
||||
name: changes.name,
|
||||
});
|
||||
assert.equal(
|
||||
retained.annotations.comment,
|
||||
changes.comment,
|
||||
"Failed edit leaked its earlier comment write",
|
||||
);
|
||||
assert.equal(retained.annotations.inline_comment, changes.inline_comment);
|
||||
assert.equal(retained.dossier.procedure.name, changes.name);
|
||||
const cleared = await call("annotate_native_function", {
|
||||
procedure: fixture.entry,
|
||||
comment: "",
|
||||
inline_comment: "",
|
||||
});
|
||||
assert.equal(cleared.annotations.comment, null);
|
||||
assert.equal(cleared.annotations.inline_comment, null);
|
||||
assert.ok(!cleared.dossier.comments.some((c) => c.address === fixture.entry));
|
||||
assert.equal(
|
||||
(
|
||||
await call("read_bytes", {
|
||||
address: fixture.entry,
|
||||
length: fixture.bytes.length,
|
||||
})
|
||||
).bytes_hex,
|
||||
fixture.bytes.toString("hex"),
|
||||
);
|
||||
assert.equal((await call("inspect_native_load_image")).status, "verified");
|
||||
const cliAnnotated = await cli("annotate-native-function", fixture.entry, [
|
||||
"--name",
|
||||
changes.name,
|
||||
"--comment",
|
||||
changes.comment,
|
||||
"--inline-comment",
|
||||
changes.inline_comment,
|
||||
]);
|
||||
assert.deepEqual(cliAnnotated.normalized_result, updated);
|
||||
if (captureDirectory !== undefined)
|
||||
await writeFile(
|
||||
join(captureDirectory, "image.json"),
|
||||
|
|
@ -204,6 +286,13 @@ try {
|
|||
partial_read_bytes: 2,
|
||||
},
|
||||
functions: { entry: true, near: true, pseudocode_nonempty: true },
|
||||
annotations: {
|
||||
atomic_rollback: true,
|
||||
fresh_inventories: true,
|
||||
fresh_decompiler: true,
|
||||
clear_comments: true,
|
||||
cli_mcp_parity: true,
|
||||
},
|
||||
original_unchanged: true,
|
||||
limitations: image.limitations,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -498,7 +498,11 @@ function assertProfile(profile) {
|
|||
);
|
||||
assert.equal(profile.provider.id, "ghidra");
|
||||
assert.equal(profile.parameters.target_format, "dos-mz");
|
||||
assert.equal(profile.parameters.import_mode, "ephemeral-read-only");
|
||||
assert.equal(profile.parameters.import_mode, "ephemeral-source-immutable");
|
||||
assert.equal(
|
||||
profile.parameters.annotation_policy,
|
||||
"atomic-function-entry-metadata-v1",
|
||||
);
|
||||
assert.equal(profile.parameters.loader, "MzLoader");
|
||||
assert.equal(profile.parameters.language_id, "x86:LE:16:Real Mode");
|
||||
assert.equal(profile.parameters.compiler_spec_id, "default");
|
||||
|
|
|
|||
|
|
@ -22,16 +22,26 @@ import {
|
|||
} from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES as SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(name) => name !== "annotate_native_function",
|
||||
);
|
||||
|
||||
if (process.platform !== "win32" || process.arch !== "x64")
|
||||
throw new Error(
|
||||
"The real Windows Ghidra verifier requires a Windows x64 host",
|
||||
);
|
||||
if (
|
||||
windowsP0Capabilities().some(
|
||||
({ available, effects }) => available && effects.mutatesArtifact,
|
||||
)
|
||||
)
|
||||
throw new Error("Windows Ghidra must not advertise database mutation");
|
||||
const installDir = process.env.GHIDRA_INSTALL_DIR;
|
||||
if (installDir === undefined || !isAbsolute(installDir))
|
||||
throw new Error(
|
||||
|
|
@ -185,7 +195,9 @@ try {
|
|||
|
||||
const expected = [
|
||||
...GHIDRA_INVENTORY_OPERATIONS,
|
||||
...GHIDRA_FUNCTION_OPERATIONS,
|
||||
...GHIDRA_FUNCTION_OPERATIONS.filter(
|
||||
(name) => name !== "annotate_native_function",
|
||||
),
|
||||
];
|
||||
if (
|
||||
observed.size !== expected.length ||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ name: reverse-engineer-anything
|
|||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "24"
|
||||
tool_count: 117
|
||||
catalog_digest: "f63ae9b3480f206700cf334c54b3267aff8d0c3a7f4e782d8b5f7952cfdd5271"
|
||||
tool_count: 118
|
||||
catalog_digest: "b4f7e5860d90b029876b58fe7cd4a8041fef1f2add23440e37e90827d808e503"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -60,3 +60,7 @@ For headerless DOS COM, explicitly open with `format: "dos-com"` (CLI
|
|||
`--target-format dos-com`). Inspect the load image before trusting function
|
||||
analysis: the entry is `0x10100`, file offset zero, with imposed real-mode segment
|
||||
context. PSP/stack/device state remains unmodeled. See [DOS guide](../../../docs/ghidra-dos.md).
|
||||
|
||||
### Function annotations in Ghidra
|
||||
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export {
|
|||
} from "./DirectAnalysisStatus.js";
|
||||
|
||||
type DirectAnalysisTool =
|
||||
| "annotate_native_function"
|
||||
| "inspect_native_load_image"
|
||||
| "read_bytes"
|
||||
| "address_to_file_offset"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const registerCoreAnalysisCommands = (
|
|||
logger: Logger,
|
||||
): void => {
|
||||
registerCoreCommands(cli, logger);
|
||||
registerAnnotationCommand(cli, logger);
|
||||
cli.command(CLI_COMMANDS.inspectNativeLoadImage, {
|
||||
description:
|
||||
"Verify loaded native bytes, source mappings, relocations and entry",
|
||||
|
|
@ -722,3 +723,51 @@ const registerSearchCommand = (cli: CliInstance, logger: Logger): void => {
|
|||
),
|
||||
});
|
||||
};
|
||||
|
||||
const registerAnnotationCommand = (cli: CliInstance, logger: Logger): void => {
|
||||
cli.command(CLI_COMMANDS.annotateNativeFunction, {
|
||||
description: "Edit function annotations and return refreshed analysis",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local executable path"),
|
||||
procedure: z
|
||||
.string()
|
||||
.describe("Exact function entry address or unique name"),
|
||||
}),
|
||||
options: z.object({
|
||||
name: z.string().min(1).optional().describe("Analyst function name"),
|
||||
comment: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Regular entry comment; empty text clears it"),
|
||||
"inline-comment": z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Inline entry comment; empty text clears it"),
|
||||
"target-format": formatSelectionOption,
|
||||
provider: providerSelectionOption,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.annotateNativeFunction, () =>
|
||||
runDirectAnalysis(
|
||||
args.path,
|
||||
"annotate_native_function",
|
||||
{
|
||||
procedure: args.procedure,
|
||||
...(options.name === undefined ? {} : { name: options.name }),
|
||||
...(options.comment === undefined
|
||||
? {}
|
||||
: { comment: options.comment }),
|
||||
...(options["inline-comment"] === undefined
|
||||
? {}
|
||||
: { inline_comment: options["inline-comment"] }),
|
||||
},
|
||||
directAnalysisOptions(
|
||||
logger,
|
||||
undefined,
|
||||
options.provider,
|
||||
options["target-format"],
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const CLI_COMMANDS = Object.freeze({
|
|||
capabilities: "capabilities",
|
||||
providers: "providers",
|
||||
function: "function",
|
||||
annotateNativeFunction: "annotate-native-function",
|
||||
inspectNativeApi: "inspect-native-api",
|
||||
instructions: "instructions",
|
||||
search: "search",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { nativeFunctionAnnotationsInputSchema } from "../domain/nativeFunctionAnnotations.js";
|
||||
|
||||
import {
|
||||
officialOutputSchemas,
|
||||
|
|
@ -36,6 +37,11 @@ const official = <Name extends string, Schema extends z.ZodObject>(
|
|||
|
||||
/** Bridge operations exposed without additional application composition. */
|
||||
export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
official(
|
||||
"annotate_native_function",
|
||||
"Atomically update one function name and/or entry comments, read them back, and return a refreshed complete function dossier. Ghidra changes its ephemeral session analysis database only; original executable bytes remain unchanged. Empty comment text clears that comment. Edits remain available to subsequent MCP calls until close; CLI returns the updated dossier before discarding its session. Invalid edits roll back the entire operation. Windows P0 and providers without this capability are unsupported.",
|
||||
nativeFunctionAnnotationsInputSchema,
|
||||
),
|
||||
official(
|
||||
"inspect_native_load_image",
|
||||
"Verify the provider's loaded DOS MZ or explicitly selected COM image against its immutable target snapshot: complete original/modified source-byte digests, header/module file mappings and memory digests, relocations and external entry. Returns measured observations and independent checks with mismatch coordinates. COM verification includes its imposed entry register context and does not claim PSP/runtime coverage. Overlay and uninitialized coverage remain explicit; other formats return unsupported. Does not execute or modify the target.",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
|
|||
get_call_graph: { address: "0x1000" },
|
||||
find_xrefs_to_name: { name: "malloc" },
|
||||
analyze_function: { procedure: "main" },
|
||||
annotate_native_function: {
|
||||
procedure: "0x10100",
|
||||
name: "entry",
|
||||
comment: "Analyst observation",
|
||||
},
|
||||
analyze_swift_types: { category: "classes", pattern: "Account" },
|
||||
inspect_native_api: { procedure: "main" },
|
||||
trace_feature: { query: "license" },
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ const sessionEvidence = effects({ mutatesSession: true });
|
|||
|
||||
/** Explicit effect audit for every public tool. */
|
||||
export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
annotate_native_function: effects({
|
||||
mutatesTarget: true,
|
||||
mutatesSession: true,
|
||||
launchesProcess: true,
|
||||
writesFilesystem: true,
|
||||
}),
|
||||
inspect_native_load_image: evidence,
|
||||
inspect_native_dispatch_metadata: evidence,
|
||||
trace_native_ui_action: evidence,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { nativeFunctionAnnotationsSchema } from "../domain/nativeFunctionAnnotations.js";
|
||||
import { nativeLoadImageSchema } from "../domain/nativeLoadImage.js";
|
||||
import { nativeUiResultSchema } from "../domain/nativeUiObservation.js";
|
||||
import { nativeValueTraceSchema } from "../domain/nativeValueTrace.js";
|
||||
|
|
@ -101,6 +102,7 @@ const addressedString = z.object({
|
|||
|
||||
/** Exact structured-content schemas shared by direct analysis providers. */
|
||||
export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
annotate_native_function: resultOf(nativeFunctionAnnotationsSchema),
|
||||
inspect_native_load_image: resultOf(nativeLoadImageSchema),
|
||||
inspect_native_data_type: resultOf(nativeDataTypeSchema),
|
||||
inspect_native_instruction: resultOf(nativeInstructionSchema),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { z } from "zod";
|
||||
import { functionDossierSchema } from "./hopperValues.js";
|
||||
|
||||
/** One explicit function's analyst-authored metadata changes. */
|
||||
export const nativeFunctionAnnotationsInputSchema = z
|
||||
.strictObject({
|
||||
procedure: z.string().min(1),
|
||||
name: z.string().min(1).optional(),
|
||||
comment: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Regular entry comment; empty text clears it"),
|
||||
inline_comment: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Inline entry comment; empty text clears it"),
|
||||
})
|
||||
.refine(
|
||||
(value) =>
|
||||
value.name !== undefined ||
|
||||
value.comment !== undefined ||
|
||||
value.inline_comment !== undefined,
|
||||
"Supply at least one annotation change",
|
||||
);
|
||||
|
||||
/** Readback and refreshed analysis after one atomic metadata edit. */
|
||||
export const nativeFunctionAnnotationsSchema = z
|
||||
.strictObject({
|
||||
annotations: z.strictObject({
|
||||
address: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
comment: z.string().nullable(),
|
||||
inline_comment: z.string().nullable(),
|
||||
}),
|
||||
dossier: functionDossierSchema,
|
||||
effects: z.strictObject({
|
||||
scope: z.literal("session-analysis-database"),
|
||||
source_bytes_modified: z.literal(false),
|
||||
persists_after_close: z.literal(false),
|
||||
}),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (
|
||||
value.annotations.address !== value.dossier.procedure.address ||
|
||||
value.annotations.name !== value.dossier.procedure.name
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"Annotation readback disagrees with refreshed function identity",
|
||||
});
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -38,7 +38,8 @@ export const resolveGhidraAnalysisProfile = (
|
|||
available_architectures: [
|
||||
...(target.availableArchitectures ?? []),
|
||||
].sort(),
|
||||
import_mode: "ephemeral-read-only",
|
||||
import_mode: "ephemeral-source-immutable",
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
load_image_observations: "source-mappings-entry-context-v2",
|
||||
function_body_evidence: "complete-inclusive-ranges-v1",
|
||||
...(dos
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import { functionDossierSchema } from "../domain/hopperValues.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { nativeFunctionAnnotationsInputSchema } from "../domain/nativeFunctionAnnotations.js";
|
||||
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
|
||||
import {
|
||||
parseGhidraFunctionInput,
|
||||
parseGhidraFunctionResult,
|
||||
} from "./GhidraFunctionValues.js";
|
||||
import {
|
||||
CAPABILITIES,
|
||||
windowsP0Capabilities,
|
||||
} from "./GhidraProviderCapabilities.js";
|
||||
import { HOPPER_PROVIDER_TOOL_CONTRACTS } from "../hopper/HopperProvider.js";
|
||||
|
||||
const result = () => {
|
||||
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
||||
return {
|
||||
annotations: {
|
||||
address: dossier.procedure.address,
|
||||
name: dossier.procedure.name,
|
||||
comment: null,
|
||||
inline_comment: "Finding",
|
||||
},
|
||||
dossier,
|
||||
effects: {
|
||||
scope: "session-analysis-database",
|
||||
source_bytes_modified: false,
|
||||
persists_after_close: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe("Ghidra function annotations", () => {
|
||||
it("requires an explicit function and at least one change without approval flags", () => {
|
||||
for (const input of [
|
||||
{},
|
||||
{ procedure: "main" },
|
||||
{ procedure: "", name: "entry" },
|
||||
{ procedure: "main", name: "" },
|
||||
{ procedure: "main", comment: null },
|
||||
{ procedure: "main", name: "entry", approve: true },
|
||||
])
|
||||
expect(
|
||||
nativeFunctionAnnotationsInputSchema.safeParse(input).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
parseGhidraFunctionInput("annotate_native_function", {
|
||||
procedure: "0x10100",
|
||||
comment: "",
|
||||
}),
|
||||
).toEqual({ ok: true, value: { procedure: "0x10100", comment: "" } });
|
||||
});
|
||||
|
||||
it("preserves multiline Unicode text and omitted fields", () => {
|
||||
const input = { procedure: "entry", inline_comment: "Line one\n註記" };
|
||||
expect(parseGhidraFunctionInput("annotate_native_function", input)).toEqual(
|
||||
{ ok: true, value: input },
|
||||
);
|
||||
});
|
||||
|
||||
it("validates readback identity, exact effects and the complete Ghidra dossier", () => {
|
||||
const value = result();
|
||||
expect(
|
||||
parseGhidraFunctionResult("annotate_native_function", value).ok,
|
||||
).toBe(true);
|
||||
for (const malformed of [
|
||||
{ ...value, annotations: { ...value.annotations, name: "different" } },
|
||||
{ ...value, annotations: { ...value.annotations, address: "0x9999" } },
|
||||
{ ...value, effects: { ...value.effects, source_bytes_modified: true } },
|
||||
{ ...value, effects: { ...value.effects, persists_after_close: true } },
|
||||
{ ...value, dossier: { ...value.dossier, native_value_flow: null } },
|
||||
])
|
||||
expect(
|
||||
parseGhidraFunctionResult("annotate_native_function", malformed).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves qualified names in readback and the refreshed dossier", () => {
|
||||
const value = result();
|
||||
const name = "namespace::recovered_routine";
|
||||
expect(
|
||||
parseGhidraFunctionResult("annotate_native_function", {
|
||||
...value,
|
||||
annotations: { ...value.annotations, name },
|
||||
dossier: {
|
||||
...value.dossier,
|
||||
procedure: { ...value.dossier.procedure, name },
|
||||
},
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("declares mutation only for the admitted operation and keeps Windows and Hopper unavailable", () => {
|
||||
expect(
|
||||
CAPABILITIES.find((c) => c.operation === "annotate_native_function"),
|
||||
).toMatchObject({
|
||||
available: true,
|
||||
effects: { mutatesArtifact: true, mayShowUi: false },
|
||||
});
|
||||
expect(
|
||||
CAPABILITIES.find((c) => c.operation === "analyze_function"),
|
||||
).toMatchObject({ effects: { mutatesArtifact: false } });
|
||||
expect(
|
||||
windowsP0Capabilities().find(
|
||||
(c) => c.operation === "annotate_native_function",
|
||||
),
|
||||
).toMatchObject({ available: false });
|
||||
expect(
|
||||
HOPPER_PROVIDER_TOOL_CONTRACTS.some(
|
||||
(c) => c.name === "annotate_native_function",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -59,7 +59,7 @@ export type GhidraOperation =
|
|||
| GhidraInventoryOperation
|
||||
| GhidraFunctionOperation;
|
||||
|
||||
/** Owns one authenticated, private, read-only Ghidra headless session. */
|
||||
/** Owns an authenticated private Ghidra session with transport-bound mutation authority. */
|
||||
export class GhidraClient {
|
||||
readonly #options: Required<
|
||||
Pick<GhidraClientOptions, "startupTimeoutMs" | "transport" | "platform">
|
||||
|
|
@ -406,6 +406,8 @@ export class GhidraClient {
|
|||
return err(this.#failure("protocol", "Ghidra run identity is missing"));
|
||||
const parsed = parseGhidraSessionInfo(value, {
|
||||
runId,
|
||||
expectedReadOnly:
|
||||
this.#options.transport === "authenticated-loopback-tcp",
|
||||
providerVersion: this.#options.providerVersion,
|
||||
profileDigest: this.#options.profileDigest,
|
||||
targetSha256: this.#options.targetSha256,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { z } from "zod";
|
||||
import {
|
||||
nativeFunctionAnnotationsInputSchema,
|
||||
nativeFunctionAnnotationsSchema,
|
||||
} from "../domain/nativeFunctionAnnotations.js";
|
||||
import { nativeDataTypeSchema } from "../domain/nativeDataType.js";
|
||||
import {
|
||||
nativeInstructionSchema,
|
||||
|
|
@ -27,8 +31,9 @@ import {
|
|||
ghidraProcedureIdentitySchema as procedureIdentity,
|
||||
} from "./GhidraInventoryValues.js";
|
||||
|
||||
/** Read-only function operations admitted by the Ghidra adapter. */
|
||||
/** Function analysis and annotation operations admitted by the Ghidra adapter. */
|
||||
export const GHIDRA_FUNCTION_OPERATIONS = [
|
||||
"annotate_native_function",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"resolve_native_call_targets",
|
||||
|
|
@ -59,6 +64,7 @@ const procedure = ghidraIdentifierSchema;
|
|||
const directProcedure = { document, procedure };
|
||||
|
||||
const inputSchemas = {
|
||||
annotate_native_function: nativeFunctionAnnotationsInputSchema,
|
||||
inspect_native_data_type: z
|
||||
.object({
|
||||
document,
|
||||
|
|
@ -356,6 +362,9 @@ const ghidraFunctionInstructionWindow = functionInstructionWindowSchema
|
|||
.strict();
|
||||
|
||||
const resultSchemas = {
|
||||
annotate_native_function: nativeFunctionAnnotationsSchema.safeExtend({
|
||||
dossier: ghidraFunctionDossier,
|
||||
}),
|
||||
analyze_function: ghidraFunctionDossier,
|
||||
procedure_assembly: z.string(),
|
||||
procedure_callees: z.array(ghidraCanonicalAddressSchema),
|
||||
|
|
|
|||
|
|
@ -209,6 +209,19 @@ describe("Ghidra provider", () => {
|
|||
});
|
||||
|
||||
describe("Ghidra platform support", () => {
|
||||
it("keeps Windows annotation mutation unavailable independently of native controls", () => {
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
expect(
|
||||
ghidra
|
||||
.capabilities()
|
||||
.find(({ operation }) => operation === "annotate_native_function"),
|
||||
).toMatchObject({
|
||||
available: false,
|
||||
reason: "Windows Ghidra P0 does not admit database mutation.",
|
||||
availabilityCode: "unsupported_host",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Windows P0 unavailable until native isolation authority exists", () => {
|
||||
const ghidra = provider({ ...installationHost(), platform: "win32" });
|
||||
const nativeApplication = peTarget("x86_64");
|
||||
|
|
@ -343,7 +356,8 @@ describe("Ghidra client projection", () => {
|
|||
expect(resolved.value.profile).toMatchObject({
|
||||
provider: { id: "ghidra", name: "Ghidra", version: "12.1.4" },
|
||||
parameters: {
|
||||
import_mode: "ephemeral-read-only",
|
||||
import_mode: "ephemeral-source-immutable",
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
analyzer_preset: "ghidra-default",
|
||||
},
|
||||
});
|
||||
|
|
@ -452,6 +466,46 @@ describe("Ghidra result projection", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("preserves the rejected name constraint and function address", async () => {
|
||||
const message =
|
||||
"Invalid function name at 0x10100: Symbol name contains invalid characters";
|
||||
const ghidra = provider(installationHost(), () => ({
|
||||
start: () => Promise.resolve(ok(sessionInfo())),
|
||||
callTool: () =>
|
||||
Promise.resolve(
|
||||
err(
|
||||
new GhidraSessionError(
|
||||
"remote",
|
||||
message,
|
||||
{},
|
||||
{ remoteCode: "invalid_function_name" },
|
||||
),
|
||||
),
|
||||
),
|
||||
close: () => Promise.resolve(),
|
||||
}));
|
||||
const resolved = await ghidra.resolveAnalysisProfile(
|
||||
executableTarget("elf", "x86_64"),
|
||||
);
|
||||
if (!resolved.ok) throw resolved.error;
|
||||
if (resolved.value.profile === null)
|
||||
throw new Error("Expected a bound profile");
|
||||
await expect(
|
||||
ghidra
|
||||
.createClient(executableTarget("elf", "x86_64"), resolved.value.profile)
|
||||
.execute("annotate_native_function", {
|
||||
procedure: "0x10100",
|
||||
name: "bad name",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [{ path: ["name"], reason: "invalid_value", message }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("projects remote decompile cancellation as a provider-neutral interruption", async () => {
|
||||
const code = "decompile_cancelled";
|
||||
const tag = "AnalysisCancelledError";
|
||||
|
|
@ -566,7 +620,7 @@ const sessionInfo = () => ({
|
|||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: "a".repeat(64),
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: true as const,
|
||||
read_only: false as const,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
|
|||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
|
||||
describe("Ghidra provider capabilities", () => {
|
||||
it("publishes only admitted read-only operations and resists caller mutation", () => {
|
||||
it("publishes admitted operation effects and resists caller mutation", () => {
|
||||
const config = parseConfig({});
|
||||
expect(config.ok).toBe(true);
|
||||
if (!config.ok) return;
|
||||
const provider = new GhidraProvider(config.value, silentLogger, {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => undefined,
|
||||
executable: () => false,
|
||||
probeJava: () => undefined,
|
||||
|
|
@ -26,7 +28,7 @@ describe("Ghidra provider capabilities", () => {
|
|||
available: true,
|
||||
reason: null,
|
||||
effects: {
|
||||
mutatesArtifact: false,
|
||||
mutatesArtifact: descriptor.operation === "annotate_native_function",
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
changesPermissions: false,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export type GhidraProviderClientFactory = (
|
|||
) => Pick<GhidraClient, "start" | "callTool" | "close"> &
|
||||
Partial<Pick<GhidraClient, "runtimeLineage" | "readTargetSnapshot">>;
|
||||
|
||||
/** Ghidra candidate backed by an isolated read-only headless import. */
|
||||
/** Ghidra candidate backed by an isolated ephemeral headless import. */
|
||||
export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
#installation: GhidraInstallationInspection | undefined;
|
||||
|
||||
|
|
@ -475,6 +475,14 @@ const projectSessionError = (
|
|||
operation: AnalysisOperation,
|
||||
failure: GhidraSessionError,
|
||||
): AnalysisError => {
|
||||
if (
|
||||
operation === "annotate_native_function" &&
|
||||
failure.kind === "remote" &&
|
||||
failure.remoteCode === "invalid_function_name"
|
||||
)
|
||||
return new AnalysisInputError(operation, { cause: failure }, [
|
||||
{ path: ["name"], reason: "invalid_value", message: failure.message },
|
||||
]);
|
||||
if (failure.kind === "cancelled")
|
||||
return new AnalysisCancelledError(operation);
|
||||
if (failure.kind === "timeout" || failure.kind === "analysis_timeout")
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const providerContractByName = new Map(
|
|||
).map((contract) => [contract.name, contract]),
|
||||
);
|
||||
|
||||
/** Provider-neutral read-only contracts implemented by the Ghidra adapter. */
|
||||
/** Provider-neutral contracts implemented by the Ghidra adapter. */
|
||||
export const GHIDRA_PROVIDER_TOOL_CONTRACTS = Object.freeze(
|
||||
[...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].map(
|
||||
(operation) => {
|
||||
|
|
@ -40,7 +40,7 @@ export const GHIDRA_PROVIDER_TOOL_CONTRACTS = Object.freeze(
|
|||
/** Health limitations shared by every Ghidra-backed capability. */
|
||||
export const healthLimitations = Object.freeze([
|
||||
"The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.",
|
||||
"The imported Program and temporary project are ephemeral, read-only to REA, and deleted on close.",
|
||||
"The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.",
|
||||
]);
|
||||
|
||||
/** Additional limitations applied to the experimental Windows x64 P0 boundary. */
|
||||
|
|
@ -57,6 +57,12 @@ export const limitationsFor = (operation: string): readonly string[] => {
|
|||
"Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.",
|
||||
];
|
||||
switch (operation) {
|
||||
case "annotate_native_function":
|
||||
return [
|
||||
...common,
|
||||
"Names use Ghidra USER_DEFINED source. Name writes preserve the existing namespace; readback uses the fully qualified name. Regular comments map to PRE and inline comments to EOL at the exact function entry. Changes commit together after readback and refreshed analysis; failure rolls them all back.",
|
||||
"Metadata edits invalidate immutable analysis snapshots and are discarded on close. CLI returns the updated dossier before session cleanup; this is not a saved Ghidra project.",
|
||||
];
|
||||
case "inspect_native_load_image":
|
||||
return [
|
||||
...common,
|
||||
|
|
@ -161,7 +167,7 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
|
|||
available: true,
|
||||
reason: null,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
mutatesArtifact: operation === "annotate_native_function",
|
||||
launchesProcess: true,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
|
|
@ -183,6 +189,14 @@ export const windowsP0Capabilities = (): readonly CapabilityDescriptor[] => {
|
|||
...capability.limitations,
|
||||
...windowsP0Limitations,
|
||||
]);
|
||||
if (capability.effects.mutatesArtifact)
|
||||
return Object.freeze({
|
||||
...capability,
|
||||
available: false,
|
||||
reason: "Windows Ghidra P0 does not admit database mutation.",
|
||||
availabilityCode: "unsupported_host",
|
||||
limitations,
|
||||
});
|
||||
return available
|
||||
? Object.freeze({ ...capability, limitations })
|
||||
: Object.freeze({
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const session = (
|
|||
run_id: expected.runId,
|
||||
profile_digest: expected.profileDigest,
|
||||
provider: { id: "ghidra", version: expected.providerVersion },
|
||||
read_only: true,
|
||||
read_only: false,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
|
||||
|
|
@ -50,3 +50,46 @@ describe("Ghidra DOS import commitment", () => {
|
|||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra mutation handshake", () => {
|
||||
it("requires the exact mutation authority and rejects duplicate capabilities", () => {
|
||||
const value = session();
|
||||
expect(
|
||||
parseGhidraSessionInfo({ ...value, read_only: true }, expected).ok,
|
||||
).toBe(false);
|
||||
expect(
|
||||
parseGhidraSessionInfo(
|
||||
{ ...value, capabilities: [...value.capabilities, "ping"] },
|
||||
expected,
|
||||
).ok,
|
||||
).toBe(false);
|
||||
expect(
|
||||
parseGhidraSessionInfo(
|
||||
{
|
||||
...value,
|
||||
capabilities: value.capabilities.filter(
|
||||
(c) => c !== "annotate_native_function",
|
||||
),
|
||||
},
|
||||
expected,
|
||||
).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
it("excludes database mutation from the Windows transport handshake", () => {
|
||||
const value = session();
|
||||
const windows = {
|
||||
...value,
|
||||
read_only: true,
|
||||
capabilities: value.capabilities.filter(
|
||||
(c) => c !== "annotate_native_function",
|
||||
),
|
||||
};
|
||||
expect(
|
||||
parseGhidraSessionInfo(windows, { ...expected, expectedReadOnly: true })
|
||||
.ok,
|
||||
).toBe(true);
|
||||
expect(
|
||||
parseGhidraSessionInfo(value, { ...expected, expectedReadOnly: true }).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,12 +26,10 @@ const sessionInfoSchema = z
|
|||
version: z.string().min(1),
|
||||
})
|
||||
.strict(),
|
||||
read_only: z.literal(true),
|
||||
read_only: z.boolean(),
|
||||
analysis_complete: z.boolean(),
|
||||
analysis_timed_out: z.boolean(),
|
||||
capabilities: z
|
||||
.array(capabilitySchema)
|
||||
.length(GHIDRA_SESSION_CAPABILITIES.length),
|
||||
capabilities: z.array(capabilitySchema),
|
||||
target: z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
|
|
@ -56,14 +54,21 @@ export const parseGhidraSessionInfo = (
|
|||
readonly providerVersion: string;
|
||||
readonly profileDigest: string;
|
||||
readonly targetSha256: string;
|
||||
readonly expectedReadOnly?: boolean;
|
||||
readonly expectedLanguageId?: string;
|
||||
readonly expectedCompilerSpecId?: string;
|
||||
},
|
||||
): Result<GhidraSessionInfo, Error> => {
|
||||
const parsed = sessionInfoSchema.safeParse(value);
|
||||
const readOnly = expected.expectedReadOnly ?? false;
|
||||
const expectedCapabilities = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(capability) => !readOnly || capability !== "annotate_native_function",
|
||||
);
|
||||
if (
|
||||
!parsed.success ||
|
||||
parsed.data.run_id !== expected.runId ||
|
||||
parsed.data.read_only !== readOnly ||
|
||||
parsed.data.capabilities.length !== expectedCapabilities.length ||
|
||||
parsed.data.provider.version !== expected.providerVersion ||
|
||||
parsed.data.profile_digest !== expected.profileDigest ||
|
||||
parsed.data.target.sha256 !== expected.targetSha256 ||
|
||||
|
|
@ -72,9 +77,8 @@ export const parseGhidraSessionInfo = (
|
|||
(expected.expectedCompilerSpecId !== undefined &&
|
||||
parsed.data.target.compiler_spec_id !==
|
||||
expected.expectedCompilerSpecId) ||
|
||||
new Set(parsed.data.capabilities).size !==
|
||||
GHIDRA_SESSION_CAPABILITIES.length ||
|
||||
GHIDRA_SESSION_CAPABILITIES.some(
|
||||
new Set(parsed.data.capabilities).size !== expectedCapabilities.length ||
|
||||
expectedCapabilities.some(
|
||||
(capability) => !parsed.data.capabilities.includes(capability),
|
||||
) ||
|
||||
parsed.data.analysis_complete === parsed.data.analysis_timed_out
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
|
|||
...GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ kind, name }) =>
|
||||
kind === "official-proxy" &&
|
||||
name !== "annotate_native_function" &&
|
||||
name !== "inspect_native_load_image" &&
|
||||
name !== "inspect_native_data_type" &&
|
||||
name !== "inspect_native_instruction" &&
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ const projectOfficialArguments = (
|
|||
contract: ToolContract,
|
||||
input: unknown,
|
||||
): Readonly<Record<string, JsonValue>> => {
|
||||
// Annotation omissions preserve existing values; they are not Python defaults.
|
||||
if (contract.name === "annotate_native_function")
|
||||
return jsonObjectSchema.parse(contract.inputSchema.parse(input));
|
||||
const parsed = jsonObjectSchema.parse(input);
|
||||
if (!(contract.inputSchema instanceof z.ZodObject))
|
||||
throw new TypeError(
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ describe("canonical CLI catalog", () => {
|
|||
"xrefs",
|
||||
"trace",
|
||||
"function",
|
||||
"annotate-native-function",
|
||||
"inspect-native-api",
|
||||
"search",
|
||||
]) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ describe("Ghidra MCP evidence parity", () => {
|
|||
analysis_profile: {
|
||||
provider: { id: "ghidra", version: "12.1.4" },
|
||||
parameters: {
|
||||
import_mode: "ephemeral-read-only",
|
||||
import_mode: "ephemeral-source-immutable",
|
||||
annotation_policy: "atomic-function-entry-metadata-v1",
|
||||
analyzer_preset: "ghidra-default",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const sessionInfo = (profileDigest: string, targetSha256: string) => ({
|
|||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: profileDigest,
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: true as const,
|
||||
read_only: false as const,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
|
||||
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
|
|
@ -12,6 +14,7 @@ import { createServer } from "../../../src/server/createServer.js";
|
|||
const VALID_INPUTS: Readonly<
|
||||
Record<string, Readonly<Record<string, JsonValue>>>
|
||||
> = {
|
||||
annotate_native_function: { procedure: "0x1000", name: "entry" },
|
||||
inspect_native_load_image: {},
|
||||
address_name: {},
|
||||
inspect_native_instruction: { address: "0x1000" },
|
||||
|
|
@ -128,6 +131,23 @@ const connect = async (analysis: AnalysisOperationPort) => {
|
|||
const inventory: JsonValue = [];
|
||||
|
||||
const outputFor = (name: string): JsonValue => {
|
||||
if (name === "annotate_native_function") {
|
||||
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
||||
return {
|
||||
annotations: {
|
||||
address: dossier.procedure.address,
|
||||
name: dossier.procedure.name,
|
||||
comment: null,
|
||||
inline_comment: null,
|
||||
},
|
||||
dossier,
|
||||
effects: {
|
||||
scope: "session-analysis-database",
|
||||
source_bytes_modified: false,
|
||||
persists_after_close: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (name === "inspect_native_load_image")
|
||||
return {
|
||||
status: "unsupported",
|
||||
|
|
@ -275,6 +295,10 @@ describe("official Hopper proxy tools", () => {
|
|||
expect(result.isError).not.toBe(true);
|
||||
}
|
||||
|
||||
expect(
|
||||
invocations.find(({ name }) => name === "annotate_native_function")
|
||||
?.arguments_,
|
||||
).toEqual({ procedure: "0x1000", name: "entry" });
|
||||
expect(invocations.map(({ name }) => name)).toEqual(
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -151,10 +151,14 @@ describe("GhidraClient", () => {
|
|||
value: {
|
||||
provider: { id: "ghidra", version: PROVIDER_VERSION },
|
||||
profile_digest: PROFILE_DIGEST,
|
||||
read_only: true,
|
||||
read_only: HOST_TRANSPORT !== "unix-socket",
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
capabilities: GHIDRA_SESSION_CAPABILITIES,
|
||||
capabilities: GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(value) =>
|
||||
HOST_TRANSPORT === "unix-socket" ||
|
||||
value !== "annotate_native_function",
|
||||
),
|
||||
target: {
|
||||
image_base: "0x1000",
|
||||
default_address_space: "ram",
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ const sessionInfo = ({
|
|||
run_id: sessionRunId,
|
||||
profile_digest: digest,
|
||||
provider: { id: "ghidra", version },
|
||||
read_only: true,
|
||||
read_only: transport !== "unix-socket",
|
||||
analysis_complete: !timedOut,
|
||||
analysis_timed_out: timedOut,
|
||||
capabilities: [
|
||||
|
|
@ -209,6 +209,7 @@ const sessionInfo = ({
|
|||
"resolve_containing_procedure",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"annotate_native_function",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"resolve_native_call_targets",
|
||||
|
|
@ -221,7 +222,10 @@ const sessionInfo = ({
|
|||
"read_function_instructions",
|
||||
"procedure_references",
|
||||
"xrefs",
|
||||
],
|
||||
].filter(
|
||||
(value) =>
|
||||
transport === "unix-socket" || value !== "annotate_native_function",
|
||||
),
|
||||
target: {
|
||||
name: "fixture",
|
||||
language_id: "x86:LE:64:default",
|
||||
|
|
|
|||
Loading…
Reference in New Issue