Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3781a932e | ||
|
|
29d3dcb7ea | ||
|
|
dc6872c66f | ||
|
|
16e1cc65ed | ||
|
|
3f11bd9655 | ||
|
|
bcc884556e | ||
|
|
778114a1ab | ||
|
|
b98be5408b | ||
|
|
410537667e | ||
|
|
36c91428fc | ||
|
|
66536d0d2e | ||
|
|
9387282274 | ||
|
|
1341e8c831 | ||
|
|
07540cbcde | ||
|
|
713163e644 | ||
|
|
877d40408b | ||
|
|
e195b8ba91 | ||
|
|
4190630432 | ||
|
|
e5791229f3 | ||
|
|
7484401601 | ||
|
|
f9bc184869 | ||
|
|
7375ab6e34 | ||
|
|
e62e92526c | ||
|
|
b782064d54 | ||
|
|
ef153774f7 | ||
|
|
ae433f0983 | ||
|
|
ad9e22d05f | ||
|
|
7cfcd166e3 | ||
|
|
583166785f | ||
|
|
03ffb2c13d | ||
|
|
576b58cefe | ||
|
|
17e3cf9802 | ||
|
|
a9af4718ad | ||
|
|
bdf865252c | ||
|
|
7e1c560dd6 | ||
|
|
66dcd94d09 | ||
|
|
ee9bf58abc | ||
|
|
1883ddcb76 | ||
|
|
96e7c22a9c | ||
|
|
3f5b703379 | ||
|
|
131b2d85f3 | ||
|
|
23c70d674c | ||
|
|
6cc6d22585 | ||
|
|
02aea22f88 | ||
|
|
345c47ccf8 | ||
|
|
f79fd59e96 | ||
|
|
1a48d6896a | ||
|
|
500e32399a | ||
|
|
ec67aebed0 | ||
|
|
f4df6ab280 | ||
|
|
77be0f6666 | ||
|
|
767fe0f0cc | ||
|
|
5b5de1b527 | ||
|
|
7ad75bdc80 | ||
|
|
5830088520 | ||
|
|
f3534749e5 | ||
|
|
d901561640 | ||
|
|
b3c5672ce6 | ||
|
|
70771523c5 | ||
|
|
fd8a2e98ff | ||
|
|
278d7fb284 | ||
|
|
e8d22ec9eb | ||
|
|
e84c58dabf | ||
|
|
bffdd91053 | ||
|
|
e572c38dcf | ||
|
|
eca18e08ac | ||
|
|
443c7aff77 | ||
|
|
55778f20ba | ||
|
|
29a2d13994 | ||
|
|
88afcc738b | ||
|
|
6e344302cb | ||
|
|
896696b2e7 | ||
|
|
9fe54c4073 | ||
|
|
36db19c59c | ||
|
|
5fa7a52d1f | ||
|
|
b940ad0d65 | ||
|
|
31b99306aa | ||
|
|
c7105d9723 | ||
|
|
b8513401e1 | ||
|
|
b11fe63bf4 | ||
|
|
bcc141549a | ||
|
|
fab33a66c6 | ||
|
|
86afc040e5 | ||
|
|
127c621328 | ||
|
|
4d4631ac94 | ||
|
|
1b14870ace | ||
|
|
a688c79784 | ||
|
|
537467ab9e | ||
|
|
a769915f6e | ||
|
|
b58adafa4f | ||
|
|
41a2e84647 | ||
|
|
04672f2b91 | ||
|
|
0f4c484be5 | ||
|
|
6a7f70fd54 | ||
|
|
3ea5fdd7cf | ||
|
|
ff56f78594 | ||
|
|
679658b287 | ||
|
|
5b7fe94710 | ||
|
|
80de3145e4 | ||
|
|
f6905719c3 | ||
|
|
a6c3d3a365 | ||
|
|
ec271121e4 | ||
|
|
55dd21a5e2 | ||
|
|
d82ad1faf0 | ||
|
|
d4bd65c53c | ||
|
|
2dc89f81be | ||
|
|
ec56c1da04 | ||
|
|
30128a03cf | ||
|
|
599a8ee11d | ||
|
|
e049ef70f7 | ||
|
|
bbf0638e88 | ||
|
|
242e2b0001 |
No files matched your search
@@ -2,7 +2,7 @@
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets 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: "41"
|
||||
version: "42"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -104,6 +104,10 @@ explicit path or endpoint and do not need it.
|
||||
consult the NativeAOT workflow in `references/native-and-artifacts.md` for
|
||||
optional metadata recovery and its supported host/layout boundary.
|
||||
- User-owned browser page already open: `list_browser_targets`.
|
||||
- Browser action the agent must perform (click, type, navigate):
|
||||
`capture_browser_scenario` with declared actions. See
|
||||
[runtime observation](references/runtime-observation.md) for connect/launch
|
||||
and the focused follow-up browser tools.
|
||||
- Fiddler Everywhere traffic or a native `.saz` capture: read the
|
||||
[Fiddler workflow](references/runtime-observation.md#fiddler-everywhere-traffic-debugging).
|
||||
Inspect saved SAZ with `inspect_web_network_capture` and `format: "saz"`;
|
||||
@@ -118,7 +122,7 @@ explicit path or endpoint and do not need it.
|
||||
pages; select one trace, stream ID and direction for contribution/range pages.
|
||||
Facts retain all-recorded-events-before-selection scope. Other qlog layouts
|
||||
and PCAP correlation are unsupported.
|
||||
- User-owned Electron runtime already open: `list_electron_targets`.
|
||||
- User-owned Electron runtime already open: `list_electron_targets`. Before an electron-main Inspector attempt, check packaged fuses with `inspect_electron_fuses`; disabled `EnableNodeCliInspectArguments` means the app ignores `--inspect`.
|
||||
- Packet-field discovery: `inspect_packet_fields` searches installed protocols
|
||||
and field names/types before choosing fields for `inspect_packet_capture`.
|
||||
Query/page the relevant records; registrations do not establish observed traffic.
|
||||
@@ -323,7 +327,8 @@ as complete while required questions remain open.
|
||||
[references/javascript-applications.md](references/javascript-applications.md)
|
||||
- Android APK declarations, classes, methods, and static references:
|
||||
[references/android-applications.md](references/android-applications.md)
|
||||
- Passive browser/Electron observation and static/runtime reconciliation:
|
||||
- Browser/Electron observation (passive and scenario) and static/runtime
|
||||
reconciliation:
|
||||
[references/runtime-observation.md](references/runtime-observation.md)
|
||||
- Evidence paging, comparisons, residual unknowns, and verification:
|
||||
[references/evidence-workflows.md](references/evidence-workflows.md)
|
||||
|
||||
@@ -169,6 +169,14 @@ repeat parsing. Returned-row exhaustion
|
||||
does not prove complete metadata or absence under partial/unavailable coverage;
|
||||
preserve build-local tokens and their artifact/MVID scope.
|
||||
|
||||
`inspect_managed_members` always returns the assembly-wide `member_refs`, even
|
||||
for one selected type. For questions about a selected type, select
|
||||
`detail: "summary"` with `type_names`; it returns the selection (with unmatched
|
||||
names), coverage and per-collection counts. Page rows with
|
||||
`view: {kind: "managed-members", collection: "call_edges", offset: 0, limit: 64}`
|
||||
over `types`, `exported_types`, `fields`, `methods`, `member_refs`, `call_edges`,
|
||||
`field_accesses` or `user_strings`.
|
||||
|
||||
### .NET NativeAOT
|
||||
|
||||
NativeAOT binaries contain native machine code, so ordinary CIL decompilers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Passive browser and Electron observation
|
||||
# Browser and Electron runtime observation
|
||||
|
||||
Supply the existing browser or Electron instance's literal-port loopback CDP
|
||||
endpoint in the request. No separate permission configuration or per-call
|
||||
@@ -7,13 +7,16 @@ approval flag is needed. Browser discovery lists eligible HTTP(S) pages;
|
||||
selected page's current origin. Electron discovery lists eligible local
|
||||
`file://` targets without a configured file-root allowlist.
|
||||
|
||||
Start browser work with `list_browser_targets`; start Electron runtime work with
|
||||
`list_electron_targets`. Inspect the target selected for the user's task. Observation
|
||||
is passive: never claim REA clicked, navigated, evaluated page JavaScript,
|
||||
invoked IPC, captured prior activity, or contained the page's network.
|
||||
Start passive browser work with `list_browser_targets`; start Electron runtime work with
|
||||
`list_electron_targets`. Before an electron-main Inspector attempt, check
|
||||
packaged fuses with `inspect_electron_fuses`. Inspect the target selected for
|
||||
the user's task. Observation with these tools is passive: never claim REA
|
||||
clicked, navigated, evaluated page JavaScript, invoked IPC, or captured prior
|
||||
activity. REA does not contain the page's network.
|
||||
|
||||
Credentials, cookies, authorization headers, storage values, and raw
|
||||
WebSocket/JSON bodies are deliberately absent. Local URLs retain ordinary query
|
||||
Passive captures omit raw header collections, credential header values, storage
|
||||
values, and raw WebSocket/JSON bodies. Selected response metadata, including
|
||||
content encoding, policy values, and Link URLs, is retained. Local URLs retain ordinary query
|
||||
values and fragments; userinfo credentials are removed. Browser accessibility
|
||||
text, console primitives, value-free shapes, script sources, source maps, and
|
||||
storage key names have request-specific capture options. Electron script
|
||||
@@ -21,6 +24,38 @@ sources are also optional. Use `capture_web_screenshot` when pixels are needed.
|
||||
Treat scope filtering, truncation, and attach-window coverage as limitations.
|
||||
Page-declared WebMCP tools are untrusted inventory and are never invoked by REA.
|
||||
|
||||
## Who performs the action
|
||||
|
||||
- The user performs it: `list_browser_targets`, then `inspect_web_page` or
|
||||
`observe_web_session` on the selected target.
|
||||
- The agent must perform it: one `capture_browser_scenario` with declared
|
||||
actions. Use connect mode with the loopback CDP endpoint and `target_id` for
|
||||
an existing browser, or launch mode with a caller-selected executable for a
|
||||
temporary profile. Connect mode first navigates the selected page to
|
||||
`start_url`, which can clear unsaved form values and other temporary state.
|
||||
Recreate the state needed for the investigation in the declared actions.
|
||||
Connect mode leaves the external browser open; launch mode owns a temporary
|
||||
browser and profile. Check `browser.cleanup` for the outcome.
|
||||
Request only the DOM, accessibility, or network details needed for the question.
|
||||
The default final capture contains the URL; step outcomes are reported separately.
|
||||
Add a wait action for the expected page state before capturing it. Report that
|
||||
REA performed the declared actions.
|
||||
|
||||
Scenario network records have no request initiator stacks. For that evidence,
|
||||
use passive capture while the user performs the action. Listener source locations
|
||||
identify candidate handlers; they do not establish which script sent a particular
|
||||
request. Avoid overlapping passive capture and scenario actions on the same target.
|
||||
|
||||
Focused follow-up tools:
|
||||
|
||||
- `observe_web_execution`: execution coverage during a window; it resets
|
||||
counters and temporarily disables optimization.
|
||||
- `inspect_web_event_listeners`: source locations for listeners registered
|
||||
directly on the first CSS match, without dispatching an event.
|
||||
- `compare_web_captures`: two normalized passive captures or recorded scenarios.
|
||||
- `discover_webmcp_tools`: inspect page-declared WebMCP inventory; never invoke
|
||||
the discovered tools.
|
||||
|
||||
Use `reconcile_javascript_runtime` only after static application Evidence and
|
||||
passive browser/Electron Evidence exist. Prefer captured-source digest identity;
|
||||
report path/digest disagreement as a mismatch and preserve ambiguity. Explicit
|
||||
|
||||
@@ -634,6 +634,7 @@ jobs:
|
||||
tests/boundary/cli/nativeSignatureCli.test.ts
|
||||
tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts
|
||||
src/process/capture/FilesystemSnapshot.test.ts
|
||||
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
|
||||
tests/boundary/process/darwinProcessRunTokenReader.test.ts
|
||||
tests/boundary/process/directAnalysisSignals.test.ts
|
||||
tests/boundary/process/processCaptureLifecycle.test.ts
|
||||
@@ -714,6 +715,8 @@ jobs:
|
||||
- name: Run curated Windows capability tests
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
src/process/capture/FilesystemSnapshot.test.ts
|
||||
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
tests/boundary/filesystem/safeOutputTree.test.ts
|
||||
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
|
||||
|
||||
@@ -24,12 +24,16 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Get REA from [morluto/rea](https://github.com/morluto/rea),
|
||||
> [rea.tools](https://rea.tools/), or the [rea-agents npm package](https://www.npmjs.com/package/rea-agents).
|
||||
> Follow the installation and release links from these official sources. A similar
|
||||
> repository name or copied README does not establish that a download is official.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA launching its analysis bridge inside Hopper while inspecting a native binary" width="1200" />
|
||||
@@ -64,6 +68,11 @@ Setup registers REA with your agent and installs matching workflow instructions.
|
||||
|
||||
## Quick start
|
||||
|
||||
On Linux, Ghidra, JADX, firmware tool launches and static JavaScript/Electron
|
||||
analysis require a procps-compatible `ps` on REA's `PATH` for process ownership
|
||||
checks. Recovery steps are in `docs/installation.md` under
|
||||
"Linux owned-process prerequisite".
|
||||
|
||||
### Set up your agent
|
||||
|
||||
With Node.js and npm installed, run:
|
||||
@@ -159,7 +168,7 @@ Additional tools and host support depend on the target:
|
||||
|
||||
| Target | What REA returns | Requirements and guide |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
|
||||
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
|
||||
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
@@ -167,8 +176,8 @@ Additional tools and host support depend on the target:
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](https://rea.tools/guides/javascript/) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
|
||||
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
|
||||
@@ -335,7 +344,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Star history
|
||||
|
||||
🎉 **180,000 GitHub stars — thank you!**
|
||||
🎉 **200,000 GitHub stars — thank you!**
|
||||
|
||||
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
|
||||
|
||||
|
||||
+455
-119
@@ -5,9 +5,11 @@ import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.UnixDomainSocketAddress;
|
||||
import java.nio.channels.Channels;
|
||||
@@ -81,6 +83,7 @@ import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.FunctionIterator;
|
||||
import ghidra.program.model.listing.Instruction;
|
||||
import ghidra.program.model.listing.InstructionIterator;
|
||||
import ghidra.program.model.listing.Listing;
|
||||
import ghidra.program.model.listing.Variable;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.mem.MemoryBlockSourceInfo;
|
||||
@@ -297,7 +300,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Files.move(pendingPath, endpointPath);
|
||||
}
|
||||
}
|
||||
acceptClient(server, descriptor);
|
||||
acceptAuthenticatedLoopbackClient(server, descriptor);
|
||||
}
|
||||
finally {
|
||||
Files.deleteIfExists(pendingPath);
|
||||
@@ -309,6 +312,50 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return System.getProperty("os.name", "").startsWith("Windows");
|
||||
}
|
||||
|
||||
private static String readLoopbackHandshake(
|
||||
SocketChannel client, int maxBytes, long timeoutMillis) throws IOException {
|
||||
long deadline = System.nanoTime() + timeoutMillis * 1_000_000L;
|
||||
InputStream input = client.socket().getInputStream();
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
while (true) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
if (remaining <= 0) throw new SocketTimeoutException("Bridge authentication timed out");
|
||||
client.socket().setSoTimeout((int) Math.max(1,
|
||||
Math.min(Integer.MAX_VALUE, (remaining + 999_999L) / 1_000_000L)));
|
||||
int value = input.read();
|
||||
if (value == -1) return null;
|
||||
if (value == '\n') return bytes.toString(StandardCharsets.UTF_8);
|
||||
if (bytes.size() == maxBytes) throw new IOException("Bridge authentication frame is too large");
|
||||
bytes.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptAuthenticatedLoopbackClient(
|
||||
ServerSocketChannel server, SessionDescriptor descriptor) throws Exception {
|
||||
while (true) {
|
||||
try (SocketChannel client = server.accept()) {
|
||||
Request first;
|
||||
try {
|
||||
String line = readLoopbackHandshake(client, 64 * 1024, 1_000);
|
||||
if (line == null) continue;
|
||||
first = parseRequest(line, descriptor.token);
|
||||
}
|
||||
catch (IOException | RuntimeException rejected) {
|
||||
// A rejected unauthenticated peer cannot consume the bridge session.
|
||||
continue;
|
||||
}
|
||||
client.socket().setSoTimeout(0);
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
Channels.newReader(client, StandardCharsets.UTF_8));
|
||||
BufferedWriter writer = new BufferedWriter(
|
||||
Channels.newWriter(client, StandardCharsets.UTF_8))) {
|
||||
serveClient(descriptor, reader, writer, first);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptClient(
|
||||
ServerSocketChannel server,
|
||||
SessionDescriptor descriptor) throws Exception {
|
||||
@@ -319,26 +366,28 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
BufferedWriter writer = new BufferedWriter(
|
||||
Channels.newWriter(client, StandardCharsets.UTF_8)
|
||||
)) {
|
||||
serveClient(descriptor, reader, writer);
|
||||
serveClient(descriptor, reader, writer, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void serveClient(
|
||||
SessionDescriptor descriptor,
|
||||
BufferedReader reader,
|
||||
BufferedWriter writer) throws Exception {
|
||||
BufferedWriter writer,
|
||||
Request first) throws Exception {
|
||||
while (true) {
|
||||
String line = reader.readLine();
|
||||
if (line == null) {
|
||||
return;
|
||||
}
|
||||
Request request;
|
||||
try {
|
||||
request = parseRequest(line, descriptor.token);
|
||||
}
|
||||
catch (RuntimeException exception) {
|
||||
writeFailure(writer, 1, "invalid_request", "Bridge request is invalid");
|
||||
return;
|
||||
Request request = first;
|
||||
first = null;
|
||||
if (request == null) {
|
||||
String line = reader.readLine();
|
||||
if (line == null) return;
|
||||
try {
|
||||
request = parseRequest(line, descriptor.token);
|
||||
}
|
||||
catch (RuntimeException exception) {
|
||||
writeFailure(writer, 1, "invalid_request", "Bridge request is invalid");
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (request.method.equals("shutdown")) {
|
||||
@@ -405,7 +454,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
case "xrefs" -> xrefs(request.params);
|
||||
case "analyze_function" -> {
|
||||
FunctionAnalysisObservation observation = analyzeFunctionObservation(request.params);
|
||||
yield functionObservation(observation.dossier(), observation.limitations());
|
||||
JsonObject result = functionObservation(observation.dossier(), observation.limitations());
|
||||
result.add("relationship_coverage", observation.relationshipCoverage());
|
||||
yield result;
|
||||
}
|
||||
case "set_address_name", "set_addresses_names" -> {
|
||||
if (!descriptor.transport.equals("unix-socket"))
|
||||
@@ -884,6 +935,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject result = functionObservation(
|
||||
value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value),
|
||||
noReturnCallLimitations(
|
||||
function,
|
||||
currentProgram.getListing().getInstructions(function.getBody(), true)
|
||||
)
|
||||
);
|
||||
@@ -975,7 +1027,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
|
||||
return functionObservation(
|
||||
GSON.toJsonTree(renderAssembly(scan.instructions)),
|
||||
noReturnCallLimitations(scan.instructions)
|
||||
noReturnCallLimitations(function, scan.instructions)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -994,7 +1046,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
limitations.add(
|
||||
"The fast path does not invoke the decompiler or scan whole-program names and strings."
|
||||
);
|
||||
List<String> boundaryLimitations = noReturnCallLimitations(scan.instructions);
|
||||
List<String> boundaryLimitations = noReturnCallLimitations(function, scan.instructions);
|
||||
for (String limitation : boundaryLimitations) limitations.add(limitation);
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("procedure", procedureIdentity(function));
|
||||
@@ -1016,12 +1068,19 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
for (Function item : ordered) {
|
||||
result.add(canonicalAddress(item.getEntryPoint()));
|
||||
}
|
||||
return functionObservation(
|
||||
result,
|
||||
noReturnCallLimitations(
|
||||
if (callers) {
|
||||
JsonObject observation = functionObservation(result, noReturnCallLimitations(function,
|
||||
currentProgram.getListing().getInstructions(function.getBody(), true)
|
||||
)
|
||||
));
|
||||
observation.add("relationship_coverage", callRelationshipCoverage(null,
|
||||
unrepresentedIncomingCalls(function, observed)));
|
||||
monitor.checkCancelled();
|
||||
return observation;
|
||||
}
|
||||
CallBoundaryObservation calls = callBoundaryObservation(
|
||||
function, currentProgram.getListing().getInstructions(function.getBody(), true), observed, false, null
|
||||
);
|
||||
return callRelationshipObservation(result, calls);
|
||||
}
|
||||
|
||||
private JsonObject procedureInfo(JsonObject params) throws Exception {
|
||||
@@ -1041,6 +1100,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return functionObservation(
|
||||
result,
|
||||
noReturnCallLimitations(
|
||||
function,
|
||||
currentProgram.getListing().getInstructions(function.getBody(), true)
|
||||
)
|
||||
);
|
||||
@@ -1071,18 +1131,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("direction", direction);
|
||||
result.add("references", edges);
|
||||
result.addProperty("reference_kinds_available", true);
|
||||
JsonArray unresolvedCalls = new JsonArray();
|
||||
if (direction.equals("outgoing")) for (Instruction instruction : scanInstructions(function, Integer.MAX_VALUE).instructions) {
|
||||
if (!instruction.getFlowType().isCall()) continue;
|
||||
boolean resolved = false;
|
||||
for (Reference reference : instruction.getReferencesFrom()) if (reference.getReferenceType().isCall()) resolved = true;
|
||||
if (!resolved) {
|
||||
JsonObject site = new JsonObject();
|
||||
site.addProperty("address", canonicalAddress(instruction.getAddress()));
|
||||
site.addProperty("reason", "Instruction decoder identifies a call but ReferenceManager supplies no call destination");
|
||||
unresolvedCalls.add(site);
|
||||
}
|
||||
}
|
||||
JsonArray unresolvedCalls = direction.equals("outgoing")
|
||||
? callBoundaryObservation(
|
||||
function, currentProgram.getListing().getInstructions(function.getBody(), true), null, true, references
|
||||
).unresolvedCalls() : new JsonArray();
|
||||
result.add("unresolved_calls", unresolvedCalls);
|
||||
return result;
|
||||
}
|
||||
@@ -1490,16 +1542,28 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
|
||||
DecompileResults decompilation = decompile(function);
|
||||
String pseudocode = decompiledText(decompilation);
|
||||
List<Reference> incomingReferences = collectReferences(function, "incoming")
|
||||
.stream()
|
||||
JsonObject availability = new JsonObject();
|
||||
availability.add("pseudocode", facetAvailability(
|
||||
pseudocode != null,
|
||||
function.isExternal() ? "Ghidra does not decompile external functions."
|
||||
: function.getBody().isEmpty() ? "Ghidra has no function body to decompile."
|
||||
: "Ghidra completed decompilation without returning pseudocode."
|
||||
));
|
||||
if (pseudocode == null) {
|
||||
pseudocode = "";
|
||||
}
|
||||
List<Reference> bodyIncoming = collectReferences(function, "incoming");
|
||||
List<Reference> incomingReferences = bodyIncoming.stream()
|
||||
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
|
||||
.toList();
|
||||
List<Reference> outgoingReferences = collectReferences(function, "outgoing");
|
||||
JsonArray incoming = referenceEdges(incomingReferences);
|
||||
JsonArray outgoing = referenceEdges(outgoingReferences);
|
||||
JsonArray comments = comments(scan.instructions);
|
||||
JsonArray callers = procedureIdentities(function.getCallingFunctions(monitor));
|
||||
JsonArray callees = procedureIdentities(function.getCalledFunctions(monitor));
|
||||
JsonArray comments = comments(function);
|
||||
Set<Function> callerFunctions = function.getCallingFunctions(monitor);
|
||||
JsonArray callers = procedureIdentities(callerFunctions);
|
||||
Set<Function> calleeFunctions = function.getCalledFunctions(monitor);
|
||||
JsonArray callees = procedureIdentities(calleeFunctions);
|
||||
JsonArray referencedStrings = referencedStrings(outgoingReferences);
|
||||
JsonArray referencedNames = referencedNames(outgoingReferences);
|
||||
JsonArray blocks = basicBlockValues(function);
|
||||
@@ -1511,7 +1575,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
procedure.add("locals", functionLocals(function));
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("procedure", procedure);
|
||||
result.addProperty("pseudocode", pseudocode == null ? "" : pseudocode);
|
||||
result.addProperty("pseudocode", pseudocode);
|
||||
result.add("assembly", assembly);
|
||||
result.add("comments", comments);
|
||||
result.add("callers", callers);
|
||||
@@ -1520,20 +1584,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("outgoing_references", outgoing);
|
||||
result.add("referenced_strings", referencedStrings);
|
||||
result.add("referenced_names", referencedNames);
|
||||
JsonObject facets = new JsonObject();
|
||||
JsonObject pseudocodeAvailability = new JsonObject();
|
||||
pseudocodeAvailability.addProperty("available", pseudocode != null);
|
||||
if (pseudocode == null) {
|
||||
pseudocodeAvailability.addProperty("reason", "Ghidra did not supply pseudocode.");
|
||||
}
|
||||
facets.add("pseudocode", pseudocodeAvailability);
|
||||
for (String facet : List.of("assembly", "comments", "callers", "callees",
|
||||
"references", "strings_names", "cfg")) {
|
||||
JsonObject available = new JsonObject();
|
||||
available.addProperty("available", true);
|
||||
facets.add(facet, available);
|
||||
}
|
||||
result.add("facet_availability", facets);
|
||||
boolean swift = swiftProgram();
|
||||
boolean aarch64 = currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:");
|
||||
SwiftStringLiteralDecode swiftLiterals = swift && aarch64 ? swiftStringLiterals(function) : null;
|
||||
@@ -1547,6 +1597,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
nativeApiBoundary(function, decompilation)
|
||||
);
|
||||
result.add("native_value_flow", nativeValueFlow(decompilation));
|
||||
// Successful full queries distinguish collected empty sets from missing facts.
|
||||
// Availability does not claim that Ghidra resolved every runtime relationship.
|
||||
for (String facet : List.of("comments", "callers", "callees", "references", "strings_names", "cfg")) {
|
||||
availability.add(facet, facetAvailability(true, null));
|
||||
}
|
||||
availability.add("assembly", facetAvailability(
|
||||
!scan.truncated(), "The function instruction scan was truncated."
|
||||
));
|
||||
result.add("facet_availability", availability);
|
||||
JsonArray limitations = new JsonArray();
|
||||
limitations.add(
|
||||
"Unresolved computed or indirect flows without target addresses are not represented as reference edges."
|
||||
@@ -1562,7 +1621,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
);
|
||||
if (swift && aarch64) {
|
||||
limitations.add(
|
||||
"Swift String literals are decoded from constant register pairs at calls, returns and paired stores; literals stored one word at a time, chosen by conditional selects, passed through unresolved jumps, or built at runtime are not reported."
|
||||
"Swift String literals are decoded from constant register pairs at calls, returns and paired stores. Literals stored one word at a time, chosen by conditional selects, or built at runtime are not reported; candidate pairs at unresolved jumps are retained as unknown omissions."
|
||||
);
|
||||
if (swiftLiterals.omissions().size() > 0) {
|
||||
limitations.add(
|
||||
@@ -1574,8 +1633,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"Swift String literal decoding is implemented for AArch64 register pairs only; no Swift literals were decoded for this architecture."
|
||||
);
|
||||
}
|
||||
List<String> boundaryLimitations = noReturnCallLimitations(scan.instructions);
|
||||
for (String limitation : boundaryLimitations) {
|
||||
CallBoundaryObservation calls = callBoundaryObservation(
|
||||
function, scan.instructions, calleeFunctions, true, outgoingReferences);
|
||||
result.add("unresolved_calls", calls.unresolvedCalls());
|
||||
for (String limitation : calls.limitations()) {
|
||||
limitations.add(limitation);
|
||||
}
|
||||
if (decompilation != null && decompilation.getErrorMessage() != null &&
|
||||
@@ -1583,7 +1644,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage());
|
||||
}
|
||||
result.add("limitations", limitations);
|
||||
return new FunctionAnalysisObservation(result, boundaryLimitations);
|
||||
monitor.checkCancelled();
|
||||
int missingCallers = 0;
|
||||
for (Reference reference : bodyIncoming) {
|
||||
monitor.checkCancelled();
|
||||
if (unrepresentedCaller(reference, callerFunctions)) missingCallers++;
|
||||
}
|
||||
return new FunctionAnalysisObservation(result, calls.limitations(),
|
||||
callRelationshipCoverage(calls, missingCallers));
|
||||
}
|
||||
|
||||
private JsonObject nativeValueFlow(DecompileResults decompilation) throws Exception {
|
||||
@@ -3287,54 +3355,276 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> noReturnCallLimitations(Iterable<Instruction> instructions)
|
||||
private static JsonObject facetAvailability(boolean available, String reason) {
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("available", available);
|
||||
if (!available) result.addProperty("reason", reason);
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonObject callRelationshipObservation(JsonElement value, CallBoundaryObservation calls)
|
||||
throws Exception {
|
||||
monitor.checkCancelled();
|
||||
JsonObject result = functionObservation(value, calls.limitations());
|
||||
result.add("relationship_coverage", callRelationshipCoverage(calls, null));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JsonObject callRelationshipCoverage(
|
||||
CallBoundaryObservation calls, Integer missingCallers) {
|
||||
JsonObject coverage = completeResolvedCallCoverage();
|
||||
Integer omitted = calls == null ? null : calls.unrepresentedCalleeCalls();
|
||||
JsonArray directions = new JsonArray();
|
||||
if (missingCallers != null && missingCallers > 0) directions.add("callers");
|
||||
String callerLimitation = missingCallers != null && missingCallers > 0
|
||||
? "Ghidra's caller inventory omits sources of " + missingCallers +
|
||||
" call reference(s) into this function's body." : null;
|
||||
if ((calls != null && omitted == null) || (calls == null && missingCallers == null)) {
|
||||
coverage.addProperty("status", "unknown");
|
||||
String reason = calls == null ? "No call inventory was examined."
|
||||
: "The callee inventory was not examined alongside decoded call targets.";
|
||||
coverage.addProperty("reason", reason + (callerLimitation == null ? "" : " " + callerLimitation));
|
||||
if (calls == null && missingCallers == null) directions.add("callers");
|
||||
directions.add("callees");
|
||||
coverage.add("limited_directions", directions);
|
||||
return coverage;
|
||||
}
|
||||
JsonArray limitations = new JsonArray();
|
||||
if (callerLimitation != null) limitations.add(callerLimitation);
|
||||
if (omitted != null && omitted > 0) {
|
||||
limitations.add("Ghidra's callee inventory omits targets of " + omitted +
|
||||
" call reference(s) or decoded call(s) without destinations.");
|
||||
directions.add("callees");
|
||||
}
|
||||
if (!limitations.isEmpty()) {
|
||||
coverage.addProperty("status", "partial");
|
||||
coverage.add("limitations", limitations);
|
||||
coverage.add("limited_directions", directions);
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
|
||||
private static JsonObject completeResolvedCallCoverage() {
|
||||
JsonObject coverage = new JsonObject();
|
||||
coverage.addProperty("status", "complete");
|
||||
JsonArray diagnostics = new JsonArray();
|
||||
diagnostics.add(
|
||||
"Only resolved Ghidra call references are returned; unresolved computed or indirect calls remain unknown, while function classifications distinguish thunks and externals."
|
||||
);
|
||||
coverage.add("diagnostics", diagnostics);
|
||||
return coverage;
|
||||
}
|
||||
|
||||
private boolean unrepresentedCaller(Reference reference, Set<Function> callers) {
|
||||
if (!reference.getReferenceType().isCall()) return false;
|
||||
Function caller = containingFunction(reference.getFromAddress());
|
||||
return caller == null || !callers.contains(caller);
|
||||
}
|
||||
|
||||
private int unrepresentedIncomingCalls(Function function, Set<Function> callers)
|
||||
throws Exception {
|
||||
int[] omitted = { 0 };
|
||||
visitReferences(function, "incoming", reference -> {
|
||||
if (unrepresentedCaller(reference, callers)) omitted[0]++;
|
||||
});
|
||||
return omitted[0];
|
||||
}
|
||||
|
||||
private List<String> noReturnCallLimitations(
|
||||
Function function, Iterable<Instruction> instructions)
|
||||
throws Exception {
|
||||
List<String> limitations = new ArrayList<>();
|
||||
Instruction beforePrevious = null;
|
||||
Instruction previous = null;
|
||||
for (Instruction instruction : instructions) {
|
||||
monitor.checkCancelled();
|
||||
if (!instruction.getFlowType().isCall() || !instruction.getFlowType().isTerminal()) {
|
||||
continue;
|
||||
String limitation = terminalCallLimitation(instruction);
|
||||
if (limitation != null) limitations.add(limitation);
|
||||
if (beforePrevious != null && previous != null) {
|
||||
String seh = borlandSehHandlerLimitation(
|
||||
function, beforePrevious, previous, instruction);
|
||||
if (seh != null) limitations.add(seh);
|
||||
}
|
||||
Address[] destinations = instruction.getFlows();
|
||||
StringBuilder limitation = new StringBuilder();
|
||||
limitation.append("Ghidra Listing reports a terminal call at ")
|
||||
.append(canonicalAddress(instruction.getAddress()));
|
||||
if (destinations.length == 0) {
|
||||
limitation.append(" with no resolved callee; its no-return basis is unknown.");
|
||||
}
|
||||
else {
|
||||
limitation.append(" to ");
|
||||
for (int index = 0; index < destinations.length; index++) {
|
||||
if (index > 0) limitation.append(", ");
|
||||
Address destination = destinations[index];
|
||||
limitation.append(canonicalAddress(destination));
|
||||
Function target = currentProgram.getFunctionManager().getFunctionAt(destination);
|
||||
if (target == null) {
|
||||
limitation.append(" (callee identity unknown)");
|
||||
}
|
||||
else {
|
||||
limitation.append(" ").append(procedureName(target));
|
||||
limitation.append(" (external=").append(target.isExternal());
|
||||
limitation.append(", thunk=").append(target.isThunk());
|
||||
limitation.append(", hasNoReturn=").append(target.hasNoReturn()).append(")");
|
||||
}
|
||||
}
|
||||
limitation.append("; fallthrough is excluded by Ghidra's flow model.");
|
||||
Function target = destinations.length == 1
|
||||
? currentProgram.getFunctionManager().getFunctionAt(destinations[0])
|
||||
: null;
|
||||
if (target != null && target.hasNoReturn()) {
|
||||
limitation.append(" This records Ghidra's FunctionManager flag, not an independent verification that the callee cannot return.");
|
||||
}
|
||||
else {
|
||||
limitation.append(" The reason for the terminal flow is not established by this observation.");
|
||||
}
|
||||
}
|
||||
limitations.add(limitation.toString());
|
||||
beforePrevious = previous;
|
||||
previous = instruction;
|
||||
}
|
||||
return limitations;
|
||||
}
|
||||
|
||||
private CallBoundaryObservation callBoundaryObservation(
|
||||
Function function, Iterable<Instruction> instructions, Set<Function> returnedCallees,
|
||||
boolean includeUnresolvedSites, List<Reference> observedReferences)
|
||||
throws Exception {
|
||||
List<String> limitations = new ArrayList<>();
|
||||
CallBoundaryReferences calls = new CallBoundaryReferences(returnedCallees, includeUnresolvedSites);
|
||||
Instruction beforePrevious = null;
|
||||
Instruction previous = null;
|
||||
if (observedReferences == null) visitReferences(function, "outgoing", calls::observe);
|
||||
else for (Reference reference : observedReferences) {
|
||||
monitor.checkCancelled();
|
||||
calls.observe(reference);
|
||||
}
|
||||
for (Instruction instruction : instructions) {
|
||||
monitor.checkCancelled();
|
||||
String limitation = terminalCallLimitation(instruction);
|
||||
if (limitation != null) limitations.add(limitation);
|
||||
if (beforePrevious != null && previous != null) {
|
||||
String seh = borlandSehHandlerLimitation(
|
||||
function, beforePrevious, previous, instruction);
|
||||
if (seh != null) limitations.add(seh);
|
||||
}
|
||||
beforePrevious = previous;
|
||||
previous = instruction;
|
||||
if (!instruction.getFlowType().isCall()) continue;
|
||||
boolean destination = false;
|
||||
for (Reference reference : instruction.getReferencesFrom()) {
|
||||
if (!reference.getReferenceType().isCall()) continue;
|
||||
destination = true;
|
||||
break;
|
||||
}
|
||||
if (!destination) {
|
||||
calls.unrepresented++;
|
||||
calls.unresolved(instruction.getAddress(),
|
||||
"Instruction decoder identifies a call but ReferenceManager supplies no call destination");
|
||||
}
|
||||
}
|
||||
return new CallBoundaryObservation(List.copyOf(limitations),
|
||||
returnedCallees == null ? null : calls.unrepresented, calls.unresolvedCalls());
|
||||
}
|
||||
|
||||
private String borlandSehHandlerLimitation(
|
||||
Function function, Instruction handlerPush, Instruction framePush,
|
||||
Instruction frameLink) {
|
||||
if (!immediatelyPrecedes(handlerPush, framePush) ||
|
||||
!immediatelyPrecedes(framePush, frameLink) ||
|
||||
!handlerPush.getMnemonicString().equalsIgnoreCase("PUSH") ||
|
||||
handlerPush.getNumOperands() != 1 ||
|
||||
!framePush.getMnemonicString().equalsIgnoreCase("PUSH") ||
|
||||
framePush.getNumOperands() != 1 ||
|
||||
!frameLink.getMnemonicString().equalsIgnoreCase("MOV") ||
|
||||
frameLink.getNumOperands() != 2 ||
|
||||
!sameFsMemoryOperand(framePush, frameLink) ||
|
||||
!hasRegisterOperand(frameLink, 1, "ESP"))
|
||||
return null;
|
||||
Object[] handlerOperand = handlerPush.getOpObjects(0);
|
||||
if (handlerOperand.length != 1 || !(handlerOperand[0] instanceof Scalar scalar) ||
|
||||
scalar.bitLength() != 32)
|
||||
return null;
|
||||
Address handler = currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||
.getAddress(scalar.getUnsignedValue());
|
||||
if (function.getBody().contains(handler)) return null;
|
||||
return "Ghidra Listing shows a Borland/Delphi-style SEH frame registration at " +
|
||||
canonicalAddress(frameLink.getAddress()) + " (push " + canonicalAddress(handler) +
|
||||
" followed by fs-segment stack chaining); handler " + canonicalAddress(handler) +
|
||||
" lies outside this procedure's body, so callees, strings, and cross-references may omit the handler region.";
|
||||
}
|
||||
|
||||
private static boolean immediatelyPrecedes(Instruction first, Instruction second) {
|
||||
Address fallThrough = first.getFallThrough();
|
||||
return fallThrough != null && fallThrough.equals(second.getAddress());
|
||||
}
|
||||
|
||||
private static boolean sameFsMemoryOperand(Instruction first, Instruction second) {
|
||||
String firstOperand = normalizedFsMemoryOperand(first);
|
||||
String secondOperand = normalizedFsMemoryOperand(second);
|
||||
return firstOperand != null && firstOperand.equalsIgnoreCase(secondOperand);
|
||||
}
|
||||
|
||||
private static String normalizedFsMemoryOperand(Instruction instruction) {
|
||||
String representation = instruction.getDefaultOperandRepresentation(0);
|
||||
representation = representation.replaceAll("(?i)\\b(?:byte|word|dword|qword)\\s+ptr\\s+", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return representation.regionMatches(true, 0, "fs:", 0, 3) &&
|
||||
representation.indexOf('[') >= 0 ? representation : null;
|
||||
}
|
||||
|
||||
private static boolean hasRegisterOperand(
|
||||
Instruction instruction, int operand, String name) {
|
||||
for (Object object : instruction.getOpObjects(operand)) {
|
||||
if (object instanceof Register register && register.getName().equalsIgnoreCase(name))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private final class CallBoundaryReferences {
|
||||
private final Set<Function> returnedCallees;
|
||||
private final TreeMap<Address, String> unresolved;
|
||||
private int unrepresented;
|
||||
|
||||
CallBoundaryReferences(Set<Function> returnedCallees, boolean includeUnresolvedSites) {
|
||||
this.returnedCallees = returnedCallees;
|
||||
unresolved = includeUnresolvedSites ? new TreeMap<>() : null;
|
||||
}
|
||||
|
||||
void observe(Reference reference) {
|
||||
if (!reference.getReferenceType().isCall()) return;
|
||||
// Reference identity follows the same containing-function owner as referenceEdge.
|
||||
Function target = containingFunction(reference.getToAddress());
|
||||
if (returnedCallees != null && (target == null || !returnedCallees.contains(target)))
|
||||
unrepresented++;
|
||||
if (target == null) unresolved(reference.getFromAddress(),
|
||||
"A call destination has no known containing Ghidra procedure");
|
||||
}
|
||||
|
||||
void unresolved(Address address, String reason) {
|
||||
if (unresolved != null) unresolved.put(address, reason);
|
||||
}
|
||||
|
||||
JsonArray unresolvedCalls() {
|
||||
if (unresolved == null) return null;
|
||||
JsonArray result = new JsonArray();
|
||||
for (var item : unresolved.entrySet()) {
|
||||
JsonObject site = new JsonObject();
|
||||
site.addProperty("address", canonicalAddress(item.getKey()));
|
||||
site.addProperty("reason", item.getValue());
|
||||
result.add(site);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private String terminalCallLimitation(Instruction instruction) {
|
||||
if (!instruction.getFlowType().isCall() || !instruction.getFlowType().isTerminal())
|
||||
return null;
|
||||
Address[] destinations = instruction.getFlows();
|
||||
StringBuilder limitation = new StringBuilder();
|
||||
limitation.append("Ghidra Listing reports a terminal call at ")
|
||||
.append(canonicalAddress(instruction.getAddress()));
|
||||
if (destinations.length == 0) {
|
||||
limitation.append(" with no resolved callee; its no-return basis is unknown.");
|
||||
}
|
||||
else {
|
||||
limitation.append(" to ");
|
||||
for (int index = 0; index < destinations.length; index++) {
|
||||
if (index > 0) limitation.append(", ");
|
||||
Address destination = destinations[index];
|
||||
limitation.append(canonicalAddress(destination));
|
||||
Function target = currentProgram.getFunctionManager().getFunctionAt(destination);
|
||||
if (target == null) {
|
||||
limitation.append(" (callee identity unknown)");
|
||||
}
|
||||
else {
|
||||
limitation.append(" ").append(procedureName(target));
|
||||
limitation.append(" (external=").append(target.isExternal());
|
||||
limitation.append(", thunk=").append(target.isThunk());
|
||||
limitation.append(", hasNoReturn=").append(target.hasNoReturn()).append(")");
|
||||
}
|
||||
}
|
||||
limitation.append("; fallthrough is excluded by Ghidra's flow model.");
|
||||
Function target = destinations.length == 1
|
||||
? currentProgram.getFunctionManager().getFunctionAt(destinations[0])
|
||||
: null;
|
||||
if (target != null && target.hasNoReturn()) {
|
||||
limitation.append(" This records Ghidra's FunctionManager flag, not an independent verification that the callee cannot return.");
|
||||
}
|
||||
else {
|
||||
limitation.append(" The reason for the terminal flow is not established by this observation.");
|
||||
}
|
||||
}
|
||||
return limitation.toString();
|
||||
}
|
||||
|
||||
private static String renderAssembly(List<Instruction> instructions) {
|
||||
return String.join("\n", renderAssemblyLines(instructions));
|
||||
}
|
||||
@@ -3358,15 +3648,27 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private List<Reference> collectReferences(
|
||||
Function function,
|
||||
String direction) throws Exception {
|
||||
TreeMap<String, Reference> observed = new TreeMap<>();
|
||||
visitReferences(function, direction, reference -> addReference(observed, reference));
|
||||
List<Reference> result = new ArrayList<>(observed.values());
|
||||
result.sort(REFERENCE_ORDER);
|
||||
return result;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ReferenceVisitor {
|
||||
void accept(Reference reference) throws Exception;
|
||||
}
|
||||
|
||||
private void visitReferences(Function function, String direction, ReferenceVisitor visitor)
|
||||
throws Exception {
|
||||
// References can target instruction interiors or originate in embedded
|
||||
// data. Use actual body ownership, including an external's exact entry.
|
||||
AddressSet scope = new AddressSet(function.getBody());
|
||||
scope.add(function.getEntryPoint());
|
||||
AddressSet scope = functionObservationScope(function);
|
||||
boolean outgoing = direction.equals("outgoing");
|
||||
AddressIterator addresses = outgoing
|
||||
? currentProgram.getReferenceManager().getReferenceSourceIterator(scope, true)
|
||||
: currentProgram.getReferenceManager().getReferenceDestinationIterator(scope, true);
|
||||
TreeMap<String, Reference> observed = new TreeMap<>();
|
||||
while (addresses.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
Address address = addresses.next();
|
||||
@@ -3374,7 +3676,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
for (Reference reference : currentProgram.getReferenceManager()
|
||||
.getReferencesFrom(address)) {
|
||||
monitor.checkCancelled();
|
||||
addReference(observed, reference);
|
||||
if (!reference.isEntryPointReference()) visitor.accept(reference);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -3382,19 +3684,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
.getReferencesTo(address);
|
||||
while (iterator.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
addReference(observed, iterator.next());
|
||||
Reference reference = iterator.next();
|
||||
if (!reference.isEntryPointReference()) visitor.accept(reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Reference> result = new ArrayList<>(observed.values());
|
||||
result.sort(REFERENCE_ORDER);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void addReference(TreeMap<String, Reference> observed, Reference reference) {
|
||||
if (reference.isEntryPointReference()) {
|
||||
return;
|
||||
}
|
||||
String key = canonicalAddress(reference.getFromAddress()) + "\u0000" +
|
||||
canonicalAddress(reference.getToAddress()) + "\u0000" +
|
||||
reference.getReferenceType().getName() + "\u0000" +
|
||||
@@ -3403,6 +3700,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
observed.putIfAbsent(key, reference);
|
||||
}
|
||||
|
||||
private static AddressSet functionObservationScope(Function function) {
|
||||
AddressSet scope = new AddressSet(function.getBody());
|
||||
scope.add(function.getEntryPoint());
|
||||
return scope;
|
||||
}
|
||||
|
||||
private JsonArray referenceEdges(List<Reference> references) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (Reference reference : references) {
|
||||
@@ -3556,23 +3859,28 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JsonArray comments(List<Instruction> instructions) {
|
||||
private JsonArray comments(Function function) throws Exception {
|
||||
Listing listing = currentProgram.getListing();
|
||||
AddressSet scope = functionObservationScope(function);
|
||||
AddressIterator addresses = listing.getCommentAddressIterator(scope, true);
|
||||
TreeMap<String, JsonObject> observed = new TreeMap<>();
|
||||
for (Instruction instruction : instructions) {
|
||||
while (addresses.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
Address address = addresses.next();
|
||||
for (CommentType type : CommentType.values()) {
|
||||
String text = instruction.getComment(type);
|
||||
String text = listing.getComment(type, address);
|
||||
if (text == null || text.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String kind = type == CommentType.EOL ? "inline" : "comment";
|
||||
JsonObject item = new JsonObject();
|
||||
item.addProperty("address", canonicalAddress(instruction.getAddress()));
|
||||
item.addProperty("address", canonicalAddress(address));
|
||||
item.addProperty("kind", kind);
|
||||
item.addProperty("text", text);
|
||||
String key = canonicalAddress(instruction.getAddress()) + "\u0000" +
|
||||
String key = canonicalAddress(address) + "\u0000" +
|
||||
kind + "\u0000" + type.name() + "\u0000" + text;
|
||||
observed.put(key, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
JsonArray result = new JsonArray();
|
||||
observed.values().forEach(result::add);
|
||||
@@ -3643,13 +3951,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
* The canonical empty string is a small literal with a zero count.
|
||||
*/
|
||||
private SwiftStringLiteralDecode swiftStringLiterals(Function function) throws Exception {
|
||||
return swiftStringLiterals(function, new SwiftLiteralBudget());
|
||||
}
|
||||
|
||||
// An explicit budget makes capacity omissions deterministic in producer tests.
|
||||
private SwiftStringLiteralDecode swiftStringLiterals(
|
||||
Function function,
|
||||
SwiftLiteralBudget budget
|
||||
) throws Exception {
|
||||
Register[] registers = new Register[31];
|
||||
for (int index = 0; index < registers.length; index++) {
|
||||
registers[index] = currentProgram.getRegister("x" + index);
|
||||
}
|
||||
TreeMap<String, JsonObject> observed = new TreeMap<>();
|
||||
TreeMap<String, JsonObject> omitted = new TreeMap<>();
|
||||
SwiftLiteralBudget budget = new SwiftLiteralBudget();
|
||||
Map<String, Boolean> selectedValues = new HashMap<>();
|
||||
SymbolicPropogator propagator = new SymbolicPropogator(currentProgram, false);
|
||||
propagator.flowConstants(function.getEntryPoint(), function.getBody(), new ContextEvaluatorAdapter() {
|
||||
@@ -3657,6 +3972,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
public boolean evaluateContextBefore(VarnodeContext context, Instruction instruction) {
|
||||
// Returning true ends only the current flow, so every queued flow checks the shared stop.
|
||||
if (budget.stopped()) return true;
|
||||
FlowType flow = instruction.getFlowType();
|
||||
Address[] flowTargets = instruction.getFlows();
|
||||
boolean unresolvedJump = flow.isJump() && !flow.isConditional() && flowTargets.length == 0;
|
||||
for (Register[] pair : swiftStringPairs(function, instruction, registers)) {
|
||||
Long word = registerConstant(context, pair[0]);
|
||||
Long object = registerConstant(context, pair[1]);
|
||||
@@ -3706,12 +4024,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject item = overCapacity
|
||||
? swiftStringOmission(layout, word, object)
|
||||
: swiftStringItem(value, layout, word, object);
|
||||
if (unresolvedJump && !overCapacity)
|
||||
item = swiftStringOmission(layout, word, object, "unresolved_jump");
|
||||
item.addProperty("source_address", canonicalAddress(instruction.getAddress()));
|
||||
JsonArray pairRegisters = new JsonArray();
|
||||
pairRegisters.add(pair[0].getName());
|
||||
pairRegisters.add(pair[1].getName());
|
||||
item.add("registers", pairRegisters);
|
||||
(overCapacity ? omitted : observed).put(key, item);
|
||||
(overCapacity || unresolvedJump ? omitted : observed).put(key, item);
|
||||
if (overCapacity) {
|
||||
budget.stop();
|
||||
return true;
|
||||
@@ -3747,11 +4067,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (operation.getOpcode() == PcodeOp.RETURN) returns = true;
|
||||
}
|
||||
// A jump is a proven tail call only when it has resolved targets and all leave the
|
||||
// body. A targetless computed jump can still dispatch inside the function.
|
||||
// body. For unresolved targetless jumps, candidate literals remain unknown omissions.
|
||||
Address[] targets = instruction.getFlows();
|
||||
boolean tailCall = flow.isJump() && !flow.isConditional() && targets.length > 0 &&
|
||||
Arrays.stream(targets).allMatch(target -> !function.getBody().contains(target));
|
||||
if (!flow.isCall() && !returns && !tailCall) return pairs;
|
||||
boolean unresolvedJump = flow.isJump() && !flow.isConditional() && targets.length == 0;
|
||||
if (!flow.isCall() && !returns && !tailCall && !unresolvedJump) return pairs;
|
||||
// A String occupies two consecutive registers among x0-x7, or x0 and x1 when returned.
|
||||
int lastFirstRegister = returns ? 0 : 6;
|
||||
for (int index = 0; index <= lastFirstRegister; index++) {
|
||||
@@ -3894,6 +4215,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
remaining = Math.max(0, (runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()) / 2);
|
||||
}
|
||||
|
||||
SwiftLiteralBudget(long remaining) {
|
||||
if (remaining < 0) throw new IllegalArgumentException("Swift literal budget must be nonnegative");
|
||||
this.remaining = remaining;
|
||||
}
|
||||
|
||||
boolean takeState() {
|
||||
if (remaining < BYTES_PER_RECORD) return false;
|
||||
remaining -= BYTES_PER_RECORD;
|
||||
@@ -3932,6 +4258,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
|
||||
private static JsonObject swiftStringOmission(SwiftLiteralLayout layout, long word, long object) {
|
||||
return swiftStringOmission(layout, word, object, "capacity");
|
||||
}
|
||||
|
||||
private static JsonObject swiftStringOmission(
|
||||
SwiftLiteralLayout layout,
|
||||
long word,
|
||||
long object,
|
||||
String reason
|
||||
) {
|
||||
JsonObject item = new JsonObject();
|
||||
item.addProperty("representation", layout.representation());
|
||||
if (layout.address() == null) {
|
||||
@@ -3940,7 +4275,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
item.addProperty("address", canonicalAddress(layout.address()));
|
||||
}
|
||||
item.addProperty("byte_length", layout.byteLength());
|
||||
item.addProperty("reason", "capacity");
|
||||
item.addProperty("reason", reason);
|
||||
item.add("words", swiftStringWords(word, object));
|
||||
return item;
|
||||
}
|
||||
@@ -4508,7 +4843,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private record InventoryItem(Address address, String value, JsonObject facts) {}
|
||||
private record AddressNameRequest(String inputAddress, Address address, String name) {}
|
||||
private record FunctionEntry(Function function, InventoryItem item) {}
|
||||
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations) {}
|
||||
private record FunctionAnalysisObservation(JsonObject dossier, List<String> limitations, JsonObject relationshipCoverage) {}
|
||||
private record CallBoundaryObservation(List<String> limitations, Integer unrepresentedCalleeCalls, JsonArray unresolvedCalls) {}
|
||||
private record SwiftStringLiteralDecode(JsonArray literals, JsonArray omissions) {}
|
||||
private record SwiftLiteralLayout(
|
||||
String representation,
|
||||
|
||||
@@ -736,7 +736,12 @@ def _analyze_function(document, params):
|
||||
def _dispatch(method, params):
|
||||
"""Dispatch only the closed operation set implemented by REA's public tools."""
|
||||
if method == "health":
|
||||
return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
|
||||
return {
|
||||
"name": "REA Hopper bridge",
|
||||
"version": "1.0.0",
|
||||
"run_id": REA_RUN_ID,
|
||||
"process_id": os.getpid(),
|
||||
}
|
||||
if method in ("shutdown", "shutdown_document"):
|
||||
document = _session_document()
|
||||
if document is None:
|
||||
|
||||
+188
-12
@@ -6,11 +6,13 @@ The containing mitmdump process and its configuration directory are REA-owned.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import ipaddress
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import resource
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote_to_bytes
|
||||
|
||||
from mitmproxy import ctx, version
|
||||
from mitmproxy.io import tnetstring
|
||||
@@ -41,6 +43,167 @@ class BoundedReader:
|
||||
return self.handle.read(count)
|
||||
|
||||
|
||||
SPECIAL_SCHEMES = {"ftp", "file", "http", "https", "ws", "wss"}
|
||||
SCHEME_PREFIX = r"^[\x00-\x20]*([a-z][a-z0-9+.-]*):"
|
||||
NETWORK_PREFIX = r"^[\x00-\x20]*[/\\]{2}"
|
||||
URL_IGNORABLE_CONTROLS = "\t\n\r"
|
||||
URL_CONTROL_TRANSLATION = {ord(char): None for char in URL_IGNORABLE_CONTROLS}
|
||||
C0_CONTROL_OR_SPACE = "".join(chr(code) for code in range(0x21))
|
||||
FORBIDDEN_DOMAIN_CODE_POINTS = set(C0_CONTROL_OR_SPACE) | set("#%/:<>?@[\\]^|\x7f")
|
||||
|
||||
|
||||
def authority_start(text, base_scheme=None):
|
||||
"""Return where an authority begins and whether it is special, or None.
|
||||
|
||||
Follows WHATWG URL parsing as src/domain/recordedTransportUrl.ts does:
|
||||
special schemes (and scheme-relative references) skip every leading "/"
|
||||
or "\\" before the authority, so `https://\\user:pass@host` still has
|
||||
userinfo. File URLs never carry userinfo: an "@" in a file host makes
|
||||
WHATWG reject the whole URL, so there are no credentials to remove.
|
||||
"""
|
||||
scheme = re.match(SCHEME_PREFIX, text, flags=re.I)
|
||||
name = scheme.group(1).lower() if scheme is not None else None
|
||||
if name == "file" or (name is None and base_scheme == "file"):
|
||||
return None
|
||||
if name is not None and name in SPECIAL_SCHEMES:
|
||||
# A same-scheme Location without two slash-equivalent separators is
|
||||
# relative to the recorded request, so its @ belongs to the path.
|
||||
following = text[scheme.end():scheme.end() + 2]
|
||||
if name == base_scheme and (len(following) < 2 or any(char not in "/\\" for char in following)):
|
||||
return None
|
||||
start = scheme.end()
|
||||
else:
|
||||
if scheme is not None:
|
||||
if text[scheme.end():scheme.end() + 2] != "//":
|
||||
return None
|
||||
return scheme.end() + 2, False
|
||||
# A non-special base treats backslashes as authority data and only
|
||||
# recognizes two forward slashes as a network reference.
|
||||
if base_scheme is not None and base_scheme not in SPECIAL_SCHEMES:
|
||||
prefix = re.match(r"^[\x00-\x20]*//", text)
|
||||
return None if prefix is None else (prefix.end(), False)
|
||||
prefix = re.match(NETWORK_PREFIX, text)
|
||||
if prefix is None:
|
||||
return None
|
||||
start = prefix.end()
|
||||
while start < len(text) and text[start] in "/\\":
|
||||
start += 1
|
||||
return start, True
|
||||
|
||||
|
||||
IPV4_DIGITS = {8: "01234567", 10: "0123456789", 16: "0123456789abcdefABCDEF"}
|
||||
|
||||
|
||||
def ipv4_number(part):
|
||||
"""WHATWG IPv4 number parser: decimal, 0x hex or 0-prefixed octal, else None."""
|
||||
if part == "":
|
||||
return None
|
||||
radix, digits = 10, part
|
||||
if part[:2] in ("0x", "0X"):
|
||||
radix, digits = 16, part[2:]
|
||||
elif len(part) > 1 and part[0] == "0":
|
||||
radix, digits = 8, part[1:]
|
||||
if digits == "":
|
||||
return 0
|
||||
if any(char not in IPV4_DIGITS[radix] for char in digits):
|
||||
return None
|
||||
# Bound decimal text before int(): Python limits decimal conversions to
|
||||
# 4300 digits, and anything past 10 significant digits exceeds IPv4.
|
||||
significant = digits.lstrip("0")
|
||||
if radix == 10 and len(significant) > 10:
|
||||
return 2**32
|
||||
return int(digits, radix)
|
||||
|
||||
|
||||
def domain_parses(domain):
|
||||
"""Apply WHATWG's ends-in-a-number check and IPv4 parser to a domain."""
|
||||
parts = domain.split(".")
|
||||
if parts[-1] == "" and len(parts) > 1:
|
||||
parts.pop()
|
||||
last = parts[-1]
|
||||
if not ((last != "" and last.isascii() and last.isdigit()) or ipv4_number(last) is not None):
|
||||
return True
|
||||
numbers = [ipv4_number(part) for part in parts]
|
||||
if len(parts) > 4 or any(number is None for number in numbers):
|
||||
return False
|
||||
return all(number <= 255 for number in numbers[:-1]) and numbers[-1] < 256 ** (5 - len(numbers))
|
||||
|
||||
|
||||
def special_host_parses(host_port):
|
||||
"""Whether WHATWG could accept a special authority's host and port.
|
||||
|
||||
A rejected host fails the whole URL, so it has no credentials to remove.
|
||||
This mirrors the forbidden-code-point, empty-host, IPv4, IPv6 and port
|
||||
rules for ASCII hosts. UTS #46 mapping is not modeled: a non-ASCII host
|
||||
may map to an accepted one (fullwidth digits, ideographic full stops,
|
||||
ignored code points), so it is treated as accepted and still redacts.
|
||||
"""
|
||||
if host_port.startswith("["):
|
||||
host_end = host_port.find("]") + 1
|
||||
literal = host_port[1 : host_end - 1]
|
||||
if host_end == 0 or "%" in literal:
|
||||
return False
|
||||
try:
|
||||
ipaddress.IPv6Address(literal)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
colon = host_port.find(":")
|
||||
host_end = len(host_port) if colon < 0 else colon
|
||||
host = unquote_to_bytes(host_port[:host_end]).decode("utf-8", errors="replace")
|
||||
# Mapping never removes ASCII, so these rejections hold either way.
|
||||
if host == "" or any(char in FORBIDDEN_DOMAIN_CODE_POINTS for char in host):
|
||||
return False
|
||||
if host.isascii() and not domain_parses(host.lower()):
|
||||
return False
|
||||
port = host_port[host_end:]
|
||||
if port == "":
|
||||
return True
|
||||
digits = port[1:]
|
||||
if port[0] != ":" or not (digits == "" or (digits.isascii() and digits.isdigit())):
|
||||
return False
|
||||
# Bound the text first: int() refuses decimal strings over 4300 digits.
|
||||
significant = digits.lstrip("0")
|
||||
return len(significant) <= 5 and int(significant or "0") <= 65535
|
||||
|
||||
|
||||
def without_userinfo(value, base_scheme=None):
|
||||
"""Remove an authority's userinfo using the WHATWG authority boundary.
|
||||
|
||||
Special schemes and scheme-relative references also end the authority
|
||||
at a backslash, so later path text containing "@" is not userinfo.
|
||||
"""
|
||||
binary = isinstance(value, bytes)
|
||||
text = value.decode("latin-1") if binary else value
|
||||
# WHATWG removes these controls before interpreting scheme/separators.
|
||||
# Keep that parse view separate from the exact source spelling and bytes.
|
||||
# It also drops trailing C0 controls and spaces; offsets are unchanged.
|
||||
parsed_text = text.translate(URL_CONTROL_TRANSLATION).rstrip(C0_CONTROL_OR_SPACE)
|
||||
located = authority_start(parsed_text, base_scheme)
|
||||
if located is None:
|
||||
return value
|
||||
start, special = located
|
||||
terminators = "/?#\\" if special else "/?#"
|
||||
end = start
|
||||
while end < len(parsed_text) and parsed_text[end] not in terminators:
|
||||
end += 1
|
||||
at = parsed_text.rfind("@", start, end)
|
||||
if at < start or (special and not special_host_parses(parsed_text[at + 1 : end])):
|
||||
return value
|
||||
parsed_offset = 0
|
||||
raw_start = 0
|
||||
for raw_offset, char in enumerate(text):
|
||||
if char in URL_IGNORABLE_CONTROLS:
|
||||
continue
|
||||
if parsed_offset == start:
|
||||
raw_start = raw_offset
|
||||
if parsed_offset == at:
|
||||
safe = text[:raw_start] + text[raw_offset + 1 :]
|
||||
return safe.encode("latin-1") if binary else safe
|
||||
parsed_offset += 1
|
||||
raise CaptureFailure("decoder", "Normalized URL authority could not be bound to its source.")
|
||||
|
||||
|
||||
def project_record(state, secrets):
|
||||
binaries, numbers, redactions = [], [], []
|
||||
byte_secrets = []
|
||||
@@ -58,16 +221,23 @@ def project_record(state, secrets):
|
||||
return None
|
||||
return text
|
||||
|
||||
def transport_value(value, path, transport_url):
|
||||
def transport_value(value, path, transport_url, base_scheme):
|
||||
if not (transport_url or re.fullmatch(r"/(?:backup/)*request/(?:path|authority)", path)):
|
||||
return value, False
|
||||
binary = isinstance(value, bytes)
|
||||
pattern = rb"^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?//)[^/?#]*@" if binary else r"^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?//)[^/?#]*@"
|
||||
replacement = rb"\1" if binary else r"\1"
|
||||
separator = b"@" if binary else "@"
|
||||
safe_url = re.sub(pattern, replacement, value, flags=re.I)
|
||||
if path.endswith("/authority") and separator in safe_url:
|
||||
safe_url = safe_url.rsplit(separator, 1)[1]
|
||||
if transport_url:
|
||||
safe_url = without_userinfo(value, base_scheme)
|
||||
elif path.endswith("/authority"):
|
||||
safe_url = value.rsplit(separator, 1)[-1]
|
||||
else:
|
||||
# Native request.path is origin-form unless it explicitly carries
|
||||
# a scheme (absolute-form). Leading separators are path evidence,
|
||||
# not a header URL reference's network authority.
|
||||
text = value.decode("latin-1") if binary else value
|
||||
safe_url = without_userinfo(value) if re.match(
|
||||
SCHEME_PREFIX, text.translate(URL_CONTROL_TRANSLATION), flags=re.I
|
||||
) else value
|
||||
changed = safe_url != value
|
||||
if changed:
|
||||
redactions.append({"pointer": path, "reason": "transport-credential"})
|
||||
@@ -80,7 +250,7 @@ def project_record(state, secrets):
|
||||
del numbers[before[1]:]
|
||||
del redactions[before[2]:]
|
||||
|
||||
def visit(value, path, depth=0, credential=False, transport_url=False):
|
||||
def visit(value, path, depth=0, credential=False, transport_url=False, base_scheme=None):
|
||||
if depth > 64:
|
||||
raise CaptureFailure("input-limit", "Native capture exceeds the 64-level evidence nesting budget.", path)
|
||||
if credential:
|
||||
@@ -90,7 +260,7 @@ def project_record(state, secrets):
|
||||
binaries.append({"pointer": path, "representation": "producer-bytes", "state": "redacted", "content_base64": None, "bytes": None, "sha256": None})
|
||||
return None
|
||||
if isinstance(value, bytes):
|
||||
safe_bytes, credential_url = transport_value(value, path, transport_url)
|
||||
safe_bytes, credential_url = transport_value(value, path, transport_url, base_scheme)
|
||||
content_base64 = base64.b64encode(value).decode("ascii")
|
||||
digest = hashlib.sha256(value).hexdigest()
|
||||
hidden = any(secret in value or secret in safe_bytes for secret in byte_secrets) or any(secret in content_base64 or secret in digest or secret in str(len(value)) for secret in secrets)
|
||||
@@ -107,7 +277,7 @@ def project_record(state, secrets):
|
||||
return None
|
||||
return safe_text
|
||||
if isinstance(value, str):
|
||||
safe_text, _ = transport_value(value, path, transport_url)
|
||||
safe_text, _ = transport_value(value, path, transport_url, base_scheme)
|
||||
return redact(safe_text, path, value)
|
||||
if isinstance(value, bool) or value is None:
|
||||
return value
|
||||
@@ -125,10 +295,16 @@ def project_record(state, secrets):
|
||||
for index, field in enumerate(value):
|
||||
if not isinstance(field, (list, tuple)) or len(field) != 2 or not isinstance(field[0], bytes):
|
||||
raise CaptureFailure("format", "Native header/trailer is not an ordered byte pair.", pointer(path, index))
|
||||
result.append([visit(field[0], pointer(pointer(path, index), 0), depth + 2), visit(field[1], pointer(pointer(path, index), 1), depth + 2, field[0].lower() in CREDENTIALS, field[0].lower() in {b"location", b"referer", b"origin"})])
|
||||
result.append([visit(field[0], pointer(pointer(path, index), 0), depth + 2), visit(field[1], pointer(pointer(path, index), 1), depth + 2, field[0].lower() in CREDENTIALS, field[0].lower() in {b"location", b"referer", b"origin"}, base_scheme if field[0].lower() == b"location" else None)])
|
||||
return result
|
||||
return [visit(item, pointer(path, index), depth + 1) for index, item in enumerate(value)]
|
||||
return [visit(item, pointer(path, index), depth + 1, base_scheme=base_scheme) for index, item in enumerate(value)]
|
||||
if isinstance(value, dict):
|
||||
if path == "" or re.fullmatch(r"/backup(?:/backup)*", path):
|
||||
request = value.get("request")
|
||||
scheme = request.get("scheme") if isinstance(request, dict) else None
|
||||
if isinstance(scheme, bytes):
|
||||
scheme = scheme.decode("latin-1")
|
||||
base_scheme = scheme.lower() if isinstance(scheme, str) and re.fullmatch(r"[a-z][a-z0-9+.-]*", scheme, flags=re.I) else None
|
||||
result = {}
|
||||
for key, item in value.items():
|
||||
if not isinstance(key, str):
|
||||
@@ -139,7 +315,7 @@ def project_record(state, secrets):
|
||||
validate_omitted(item, pointer(path, key), depth + 1)
|
||||
redactions.append({"pointer": path, "reason": "explicit-sensitive-value", "scope": "property-name"})
|
||||
continue
|
||||
result[key] = visit(item, pointer(path, key), depth + 1)
|
||||
result[key] = visit(item, pointer(path, key), depth + 1, base_scheme=base_scheme)
|
||||
return result
|
||||
raise CaptureFailure("decoder", "Native parser returned an unexpected value type: " + type(value).__name__ + ".", path)
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
"""Source-owned tests for the mitmproxy adapter's record projection."""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
def load_capture():
|
||||
"""Load the adapter with fresh mitmproxy stand-ins scoped to its import."""
|
||||
fake_mitmproxy = types.ModuleType("mitmproxy")
|
||||
fake_mitmproxy.ctx = types.SimpleNamespace()
|
||||
fake_mitmproxy.version = types.SimpleNamespace(VERSION="12.2.3")
|
||||
fake_io = types.ModuleType("mitmproxy.io")
|
||||
fake_io.tnetstring = types.ModuleType("mitmproxy.io.tnetstring")
|
||||
stand_ins = {
|
||||
"mitmproxy": fake_mitmproxy,
|
||||
"mitmproxy.io": fake_io,
|
||||
"mitmproxy.io.tnetstring": fake_io.tnetstring,
|
||||
}
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"rea_mitmproxy_capture", os.path.join(os.path.dirname(__file__), "capture.py")
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with mock.patch.dict(sys.modules, stand_ins):
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
capture = load_capture()
|
||||
|
||||
|
||||
def location(value, scheme=None):
|
||||
state = {"response": {"headers": [[b"Location", value]]}}
|
||||
if scheme is not None:
|
||||
state["request"] = {"scheme": scheme}
|
||||
record = capture.project_record(state, [])
|
||||
sidecar = next(
|
||||
field for field in record["binary_fields"] if field["pointer"] == "/response/headers/0/1"
|
||||
)
|
||||
return record["reported"]["response"]["headers"][0][1], sidecar, record["redactions"]
|
||||
|
||||
|
||||
class TransportUserinfoTests(unittest.TestCase):
|
||||
def test_ignorable_controls_cannot_hide_authority_credentials(self):
|
||||
for control in [b"\t", b"\n", b"\r"]:
|
||||
for value in [b"https://" + control + b"/user:pass@host/path", b"ht" + control + b"tps://user:" + control + b"pass@host/path"]:
|
||||
with self.subTest(value=value):
|
||||
text, sidecar, redactions = location(value, b"https")
|
||||
self.assertNotIn("user", text)
|
||||
self.assertNotIn("pass", text)
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
|
||||
def test_mixed_network_separators_remove_credentials(self):
|
||||
for prefix in [b"\\\\", b"/\\", b"\\/"]:
|
||||
with self.subTest(prefix=prefix):
|
||||
text, sidecar, redactions = location(prefix + b"user:pass@host/path", b"https")
|
||||
self.assertEqual(text, prefix.decode("ascii") + "host/path")
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
|
||||
def test_non_special_base_keeps_backslashes_inside_network_authorities(self):
|
||||
text, sidecar, redactions = location(b"//user\\name:pass@host/x", b"foo")
|
||||
self.assertEqual(text, "//host/x")
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
text, sidecar, redactions = location(b"/\\invoice@customer", b"foo")
|
||||
self.assertEqual(text, "/\\invoice@customer")
|
||||
self.assertEqual(sidecar["state"], "retained")
|
||||
self.assertEqual(redactions, [])
|
||||
|
||||
def test_native_request_paths_preserve_origin_form_and_redact_absolute_form(self):
|
||||
for value in [b"/\\invoice@customer", b"//invoice@customer", b"\\\\invoice@customer"]:
|
||||
with self.subTest(value=value):
|
||||
record = capture.project_record({"request": {"path": value}}, [])
|
||||
self.assertEqual(record["reported"]["request"]["path"], value.decode("ascii"))
|
||||
self.assertEqual(record["binary_fields"][0]["state"], "retained")
|
||||
self.assertEqual(record["redactions"], [])
|
||||
record = capture.project_record({"request": {"path": b"https://user:pass@host/x"}}, [])
|
||||
self.assertEqual(record["reported"]["request"]["path"], "https://host/x")
|
||||
self.assertEqual(record["binary_fields"][0]["state"], "redacted")
|
||||
|
||||
def test_same_scheme_locations_preserve_relative_path_text_and_bytes(self):
|
||||
for value in [b"https:invoice@customer", b"https:/invoice@customer", b"https:\\invoice@customer"]:
|
||||
with self.subTest(value=value):
|
||||
text, sidecar, redactions = location(value, b"https")
|
||||
self.assertEqual(text, value.decode("ascii"))
|
||||
self.assertEqual(sidecar["state"], "retained")
|
||||
self.assertEqual(redactions, [])
|
||||
|
||||
def test_location_authorities_still_remove_credentials_with_a_base_scheme(self):
|
||||
for value, scheme, expected in [
|
||||
(b"https://user:pass@host/path", b"https", "https://host/path"),
|
||||
(b"https:\\\\user:pass@host/path", b"https", "https:\\\\host/path"),
|
||||
(b"https:user:pass@host/path", b"http", "https:host/path"),
|
||||
]:
|
||||
with self.subTest(value=value, scheme=scheme):
|
||||
text, sidecar, redactions = location(value, scheme)
|
||||
self.assertEqual(text, expected)
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
|
||||
def test_backup_location_uses_its_own_request_scheme(self):
|
||||
record = capture.project_record({
|
||||
"request": {"scheme": b"http"},
|
||||
"backup": {
|
||||
"request": {"scheme": b"https"},
|
||||
"response": {"headers": [[b"Location", b"https:invoice@customer"]]},
|
||||
},
|
||||
}, [])
|
||||
self.assertEqual(record["reported"]["backup"]["response"]["headers"][0][1], "https:invoice@customer")
|
||||
self.assertEqual(record["redactions"], [])
|
||||
|
||||
def test_backslash_ends_a_special_scheme_authority(self):
|
||||
text, sidecar, redactions = location(b"https://example.test\\part@tail")
|
||||
self.assertEqual(text, "https://example.test\\part@tail")
|
||||
self.assertEqual(sidecar["state"], "retained")
|
||||
self.assertIsNotNone(sidecar["content_base64"])
|
||||
self.assertEqual(redactions, [])
|
||||
|
||||
def test_userinfo_before_a_backslash_is_still_removed(self):
|
||||
text, sidecar, redactions = location(b"https://user:pw@example.test\\path")
|
||||
self.assertEqual(text, "https://example.test\\path")
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(
|
||||
redactions,
|
||||
[{"pointer": "/response/headers/0/1", "reason": "transport-credential"}],
|
||||
)
|
||||
|
||||
def test_ordinary_userinfo_is_removed(self):
|
||||
text, _sidecar, _redactions = location(b"https://user:pw@example.test/a@b")
|
||||
self.assertEqual(text, "https://example.test/a@b")
|
||||
|
||||
def test_empty_userinfo_marker_is_removed(self):
|
||||
self.assertEqual(
|
||||
capture.without_userinfo("https://@host"), "https://host"
|
||||
)
|
||||
|
||||
def test_special_schemes_skip_leading_slashes_before_userinfo(self):
|
||||
for value, expected in [
|
||||
(b"https://\\user:pass@host/path", b"https://\\host/path"),
|
||||
(b"https:\\\\user:pass@host/path", b"https:\\\\host/path"),
|
||||
(b"https:user:pass@host", b"https:host"),
|
||||
(b"//\\user:pass@host", b"//\\host"),
|
||||
]:
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(capture.without_userinfo(value), expected)
|
||||
text, sidecar, redactions = location(b"https://\\user:pass@host/path")
|
||||
self.assertEqual(text, "https://\\host/path")
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
|
||||
def test_file_base_keeps_paths_after_the_fixed_network_boundary(self):
|
||||
for value in [b"///u@h", b"////u@h"]:
|
||||
with self.subTest(value=value):
|
||||
text, sidecar, redactions = location(value, b"file")
|
||||
self.assertEqual(text, value.decode("ascii"))
|
||||
self.assertEqual(sidecar["state"], "retained")
|
||||
self.assertIsNotNone(sidecar["content_base64"])
|
||||
self.assertEqual(redactions, [])
|
||||
|
||||
def test_file_urls_keep_path_text(self):
|
||||
self.assertEqual(capture.without_userinfo("file:///a@b"), "file:///a@b")
|
||||
|
||||
def test_file_urls_reject_userinfo_and_keep_their_source(self):
|
||||
# WHATWG rejects an "@" in a file host, so nothing parses as credentials.
|
||||
self.assertEqual(
|
||||
capture.without_userinfo("file://user:pass@host/x"), "file://user:pass@host/x"
|
||||
)
|
||||
for value in [b"\\\\user:pass@host/x", b"/\\user:pass@host/x", b"//user:pass@host/x"]:
|
||||
with self.subTest(value=value):
|
||||
text, sidecar, redactions = location(value, b"file")
|
||||
self.assertEqual(text, value.decode("ascii"))
|
||||
self.assertEqual(sidecar["state"], "retained")
|
||||
self.assertEqual(redactions, [])
|
||||
|
||||
def test_rejected_special_hosts_keep_their_source(self):
|
||||
# A rejected host or port fails the whole URL, so it has no userinfo.
|
||||
for value in [
|
||||
"https:user@%",
|
||||
"https://user@",
|
||||
"https://user:pass@h^st/x",
|
||||
"https://user@ho st/x",
|
||||
"https://user@host:99999/x",
|
||||
"https://user@host:12a",
|
||||
"https://user@[::1",
|
||||
]:
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(capture.without_userinfo(value), value)
|
||||
text, sidecar, redactions = location(b"https://user:pass@h^st/x", b"https")
|
||||
self.assertEqual(text, "https://user:pass@h^st/x")
|
||||
self.assertEqual(sidecar["state"], "retained")
|
||||
self.assertEqual(redactions, [])
|
||||
|
||||
def test_accepted_special_hosts_still_remove_userinfo(self):
|
||||
for value, expected in [
|
||||
("https:user@%41", "https:%41"),
|
||||
("https://user@[::1]:8443/x", "https://[::1]:8443/x"),
|
||||
("https://user@host:/x", "https://host:/x"),
|
||||
("wss://user:pass@host:65535", "wss://host:65535"),
|
||||
("https://user@0x7f.1/", "https://0x7f.1/"),
|
||||
("https://user@1.2.3.4./", "https://1.2.3.4./"),
|
||||
("https://user@4294967295/", "https://4294967295/"),
|
||||
("https://user@1.2.3.08a/", "https://1.2.3.08a/"),
|
||||
("https://user@[::ffff:1.2.3.4]/", "https://[::ffff:1.2.3.4]/"),
|
||||
]:
|
||||
with self.subTest(value=value):
|
||||
self.assertEqual(capture.without_userinfo(value), expected)
|
||||
|
||||
def test_unicode_hosts_that_may_map_to_accepted_hosts_remove_userinfo(self):
|
||||
# UTS #46 maps these to 1.2.3.4; mapping is not modeled, so redact.
|
||||
for host in ["%EF%BC%91.2.3.4", "1。2.3.4", "%C2%AD1.2.3.4", "1.2.3.4"]:
|
||||
with self.subTest(host=host):
|
||||
self.assertEqual(
|
||||
capture.without_userinfo(f"https://user:pw@{host}/"), f"https://{host}/"
|
||||
)
|
||||
text, sidecar, redactions = location(b"https://user:pw@%EF%BC%91.2.3.4/", b"https")
|
||||
self.assertEqual(text, "https://%EF%BC%91.2.3.4/")
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
# ASCII rejections still hold for non-ASCII hosts.
|
||||
self.assertEqual(capture.without_userinfo("https://user@1^/"), "https://user@1^/")
|
||||
|
||||
def test_trailing_controls_and_spaces_do_not_hide_userinfo(self):
|
||||
# WHATWG strips trailing C0 controls and spaces before parsing.
|
||||
for suffix in [b" ", b"\x01", b"\x00 \x1f"]:
|
||||
with self.subTest(suffix=suffix):
|
||||
value = b"https://user:pw@example.test" + suffix
|
||||
text, sidecar, redactions = location(value, b"https")
|
||||
self.assertEqual(text, "https://example.test" + suffix.decode("latin-1"))
|
||||
self.assertEqual(sidecar["state"], "redacted")
|
||||
self.assertEqual(len(redactions), 1)
|
||||
|
||||
def test_rejected_ip_literals_keep_their_source(self):
|
||||
for value in [
|
||||
"https://user:pass@256.256.256.256/path",
|
||||
"https://user@1.2.3.4.5/",
|
||||
"https://user@1.2.3.09/",
|
||||
"https://user@4294967296/",
|
||||
"https://user@a.0x/",
|
||||
"https://user@1..2/",
|
||||
"https://user@1." + "9" * 5000 + "/",
|
||||
"https://user@[1::2::3]/",
|
||||
"https://user@[::1%25eth0]/",
|
||||
]:
|
||||
with self.subTest(value=value[:40]):
|
||||
self.assertEqual(capture.without_userinfo(value), value)
|
||||
|
||||
def test_overlong_ports_are_compared_without_numeric_overflow(self):
|
||||
# Python 3.11+ refuses int() on decimal strings over 4300 digits.
|
||||
accepted = "https://user@host:" + "0" * 5000 + "80/x"
|
||||
self.assertEqual(capture.without_userinfo(accepted), "https://host:" + "0" * 5000 + "80/x")
|
||||
rejected = "https://user@host:" + "9" * 5000 + "/x"
|
||||
self.assertEqual(capture.without_userinfo(rejected), rejected)
|
||||
|
||||
def test_non_special_schemes_keep_backslashes_in_the_authority(self):
|
||||
self.assertEqual(capture.without_userinfo("foo://a\\b@host/x"), "foo://host/x")
|
||||
self.assertEqual(capture.without_userinfo(b"//a\\b@host"), b"//a\\b@host")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -44,6 +44,7 @@ export default defineConfig({
|
||||
link: "/web-network-captures",
|
||||
},
|
||||
{ text: "Electron observation", link: "/electron-observation" },
|
||||
{ text: "Electron fuses", link: "/electron-fuses" },
|
||||
{ text: "Process capture", link: "/process-capture" },
|
||||
{ text: ".NET assemblies", link: "/managed-code-analysis" },
|
||||
{ text: "Android applications", link: "/android-analysis" },
|
||||
|
||||
+40
-28
@@ -73,32 +73,32 @@ existing analysis families directly, for example
|
||||
|
||||
## Operations
|
||||
|
||||
| Tool | Kind | What it does |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect_adb_client` | read | Resolved binary, version, install path; never contacts the server |
|
||||
| `list_adb_devices` | read | Serial, state, transport, model metadata, inferred emulator/physical kind with basis |
|
||||
| `inspect_adb_device` | read | Fixed `getprop` whitelist: build, SDK, patch, fingerprint, product, ABIs, debuggable, `ro.kernel.qemu` |
|
||||
| `inspect_adb_display` | read | `wm size` and `wm density` as declared |
|
||||
| `list_adb_features` | read | `pm list features` (hardware/software features, hex GL versions) |
|
||||
| `list_adb_services` | read | `service list` binder services and interfaces |
|
||||
| `list_adb_processes` | read | `ps -A`: user, pid, ppid, rss, state, name |
|
||||
| `inspect_adb_window` | read | `dumpsys window` focus declarations (what is on screen) |
|
||||
| `read_adb_setting` | read | One `settings get` value; device-side null is reported as such |
|
||||
| `read_adb_logcat` | read | Bounded `logcat -d -t` dump for one buffer, optional pid filter |
|
||||
| `list_adb_packages` | read | `pm list packages -f` with scope; base APK device paths |
|
||||
| `resolve_adb_packages` | read | Case-insensitive package-name substring search with exact-match flag |
|
||||
| `inspect_adb_package` | read | `dumpsys package` projection: versions, install times, installer, flags, permission count |
|
||||
| `list_adb_directory` | read | NUL-framed immediate children, including hidden entries: kinds, permissions, owner, group, sizes, symlink targets |
|
||||
| `pull_adb_package` | read | Complete split APK set from `pm path`, each file digested |
|
||||
| `pull_adb_file` | read | One absolute device path pulled and digested |
|
||||
| `capture_adb_screen` | read | One `screencap -p` frame (exec-out, no device-side file) with dimensions |
|
||||
| `collect_adb_bugreport` | read | Device-generated bugreport zip, digested |
|
||||
| `push_adb_file` | **mutates device** | One local file to a device path (e.g. pushing tools into `/data/local/tmp`); refuses existing targets without explicit overwrite; verifies via the device's own sha256sum |
|
||||
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves the device's Success/Failure verdict |
|
||||
| `uninstall_adb_package` | **mutates device** | Removes a package and its data for user 0 |
|
||||
| `start_adb_app` | **mutates device** | Resolves the package's launcher activity (`cmd package resolve-activity --brief`) and starts it (`am start -n`) |
|
||||
| `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
|
||||
| `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run |
|
||||
| Tool | Kind | What it does |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect_adb_client` | read | Resolved binary, version, install path; never contacts the server |
|
||||
| `list_adb_devices` | read | Serial, state, transport, model metadata, inferred emulator/physical kind with basis |
|
||||
| `inspect_adb_device` | read | Fixed `getprop` whitelist: build, SDK, patch, fingerprint, product, ABIs, debuggable, `ro.kernel.qemu` |
|
||||
| `inspect_adb_display` | read | `wm size` and `wm density` as declared |
|
||||
| `list_adb_features` | read | `pm list features` (hardware/software features, hex GL versions) |
|
||||
| `list_adb_services` | read | `service list` binder services and interfaces |
|
||||
| `list_adb_processes` | read | `ps -A`: user, pid, ppid, rss, state, name |
|
||||
| `inspect_adb_window` | read | `dumpsys window` focus declarations (what is on screen) |
|
||||
| `read_adb_setting` | read | One `settings get` value; device-side null is reported as such |
|
||||
| `read_adb_logcat` | read | Bounded `logcat -d -t` dump for one buffer, optional pid filter |
|
||||
| `list_adb_packages` | read | `pm list packages -f` with scope; base APK device paths |
|
||||
| `resolve_adb_packages` | read | Case-insensitive package-name substring search with exact-match flag |
|
||||
| `inspect_adb_package` | read | `dumpsys package` projection: versions, install times, installer, flags, permission count |
|
||||
| `list_adb_directory` | read | NUL-framed immediate children, including hidden entries: kinds, permissions, owner, group, sizes, symlink targets |
|
||||
| `pull_adb_package` | read | Complete split APK set from `pm path`, each file digested |
|
||||
| `pull_adb_file` | read | One absolute device path pulled and digested |
|
||||
| `capture_adb_screen` | read | One `screencap -p` frame (exec-out, no device-side file) with dimensions |
|
||||
| `collect_adb_bugreport` | read | Device-generated bugreport zip, digested |
|
||||
| `push_adb_file` | **mutates device** | One local file to a device path (e.g. pushing tools into `/data/local/tmp`); checks for existing targets and refuses them without explicit overwrite (the check is not atomic); compares digests when device `sha256sum` is available |
|
||||
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves the device's Success/Failure verdict |
|
||||
| `uninstall_adb_package` | **mutates device** | Removes a package and its data for user 0 |
|
||||
| `start_adb_app` | **mutates device** | Resolves the package's launcher activity (`cmd package resolve-activity --brief`) and starts it (`am start -n`) |
|
||||
| `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
|
||||
| `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run |
|
||||
|
||||
## Interpret the result
|
||||
|
||||
@@ -118,6 +118,16 @@ existing analysis families directly, for example
|
||||
while POSIX hosts retain valid POSIX filenames such as names with spaces or
|
||||
colons. A package pull checks reported names against the destination
|
||||
filesystem before pulling, and refuses names that would share a local path.
|
||||
- **Package path refusals are diagnosable.** Refusals from `pull_adb_package`
|
||||
return `unreadable_output` with `retryable: false`, keep the `pm path` output
|
||||
in `details.captured_output`, and name the refusal in `details.reason`.
|
||||
`unsafe_device_path` lists refused entries in
|
||||
`details.diagnostics.rejected_paths` as `{device_path, reason}`;
|
||||
`duplicate_local_destination` reports the shared `file_name` and both
|
||||
`device_paths`; `local_destination_unavailable` reports the `device_path`,
|
||||
`file_name`, and filesystem `error_code`. Diagnostics retain raw device
|
||||
strings, while the caller-facing message escapes invisible and bidirectional
|
||||
characters so they cannot hide text or reorder the path.
|
||||
- **Splits are part of the answer.** `pm path` enumerates the base APK and
|
||||
every installed split; pulls cover the whole set. System packages whose
|
||||
APKs keep other names report the `unknown` role rather than guessing.
|
||||
@@ -127,8 +137,10 @@ existing analysis families directly, for example
|
||||
partial coverage instead of dropping rows.
|
||||
- **Mutations are explicit.** `push_adb_file`, `install_adb_package`, and
|
||||
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
|
||||
pushes refuse existing device files unless `overwrite` is set and verify
|
||||
the transfer with the device's own `sha256sum`; installs preserve the
|
||||
pushes check for existing device files before transfer and refuse them
|
||||
unless `overwrite` is set. The check is not atomic, so a different process
|
||||
can create the path before `adb push` writes it. When available, the device's
|
||||
`sha256sum` verifies the transfer; installs preserve the
|
||||
device's `Failure [...]` reason verbatim.
|
||||
- **Producer identity stays attached.** Every result embeds the producing
|
||||
adb client (`binary_path`, `path_source`, `version`, `installed_path`).
|
||||
|
||||
@@ -9,6 +9,10 @@ This family is present on repository main and in npm 4.1.0. Check the
|
||||
[released package boundary](installation.md#released-package-and-main) and the
|
||||
connected server's tool list before selecting it.
|
||||
|
||||
On Linux, JADX launches an REA-owned process and requires a procps-compatible
|
||||
`ps` on REA's `PATH`; see the shared
|
||||
[Linux process prerequisite](installation.md#linux-owned-process-prerequisite).
|
||||
|
||||
## Optional package delivery
|
||||
|
||||
The MCP `inspect_android_package` request keeps complete output when `detail` is
|
||||
|
||||
@@ -18,7 +18,7 @@ Inspect native SAZ or HAR 1.2 offline with REA, or connect Everywhere's separate
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
|
||||
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. REA never exposes `WebMCP.invokeTool`; malformed protocol payloads fail explicitly instead of returning incomplete tool metadata.
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. Declared `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` annotation hints are reported per registration: `true`/`false` are declared values, `null` means the declaration did not provide the hint in that capture, and a missing `consequential` key means the hint was not recorded, either in a historical capture or because the observed annotation was invalid. Consult discovery completeness and exclusions to distinguish these cases. Malformed annotation values retain the registration with incomplete coverage and an `invalid_protocol_value` exclusion. Other malformed protocol payloads can fail discovery explicitly. REA never exposes `WebMCP.invokeTool`.
|
||||
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
|
||||
- `capture_web_screenshot` / `rea capture-web-screenshot` returns a content-addressed visible-viewport PNG.
|
||||
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
|
||||
@@ -190,6 +190,36 @@ rea discover-webmcp-tools http://127.0.0.1:9222 TARGET_ID \
|
||||
--json
|
||||
```
|
||||
|
||||
### WebMCP discovery prerequisites
|
||||
|
||||
`discover_webmcp_tools` reports only what the attached browser's experimental
|
||||
`WebMCP` CDP domain exposes. When a browser rejects the unknown domain, the
|
||||
result reports `status: "unavailable"`. Whether a page can register tools at
|
||||
all depends on several conditions that are not interchangeable:
|
||||
|
||||
- A site's own origin-trial token can enable the API for that origin
|
||||
**provided the trial is still active, the browser version and platform
|
||||
support it, and user settings permit it** — token presence alone is not a
|
||||
guarantee. The trial is announced for Chrome 149 and later.
|
||||
- The `WebMCPTesting` feature — `--enable-features=WebMCPTesting` at launch, or
|
||||
`chrome://flags/#enable-webmcp-testing` in a normal profile — enables the
|
||||
API page-side for that browser instance, independent of per-site tokens.
|
||||
In one pilot run, a site whose registration code was present but whose
|
||||
served token was scoped for third-party use registered 0 tools under a
|
||||
default launch and 4 under the flag; that the token did not enroll the
|
||||
first-party document is consistent with this but is an inference, not a
|
||||
measured Chrome behavior.
|
||||
|
||||
In Chrome 154 specifically (one tested version), `WebMCP.enable` answered
|
||||
under a default launch — the debug domain was not flag-gated in that run.
|
||||
Other versions may differ, so an unattributed zero-tool result should be
|
||||
read as registration state only after domain availability is confirmed.
|
||||
|
||||
Discovery is attach-only: the command needs a reachable loopback CDP endpoint
|
||||
and a target ID from `list_browser_targets`, and it never navigates, reloads,
|
||||
or evaluates the page. Observe the page in the state you want to measure;
|
||||
registered tools reflect the document as loaded.
|
||||
|
||||
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names are selected independently:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -15,6 +15,12 @@ there is no separate origin allowlist or request-interception layer. Environment
|
||||
settings (including service workers blocked by default), empty storage, and a
|
||||
final sanitized URL capture are supplied by default. Set
|
||||
`environment.service_workers` to `allow` when the application requires them.
|
||||
For attached browsers, `block` rejects captures when the selected page or its
|
||||
frames are controlled by a service worker or already have a registration,
|
||||
including after navigation. Existing caller registrations are never removed.
|
||||
Navigation can run destination code before a newly discovered worker is rejected;
|
||||
that rejection does not roll back browser effects. REA removes its owned
|
||||
future-document scripts, restores live frame methods, then disconnects.
|
||||
Add secret declarations, storage seeds, and additional artifact or event
|
||||
capture only when needed. Action, secret, and storage counts are not capped.
|
||||
Duration, action, and navigation
|
||||
@@ -249,8 +255,12 @@ Each observed request object receives a capture-scoped `transaction_id`.
|
||||
Requests sharing a URL remain distinct. Redirects link to the preceding
|
||||
transaction only when it was observed; a null predecessor does not establish
|
||||
that no earlier hop existed. Request events require both transaction fields;
|
||||
an absent ID is malformed capture data, while a null redirect predecessor is
|
||||
an explicit unknown.
|
||||
an absent ID is malformed capture data. `redirected_from_transaction_state`
|
||||
records `root` when Playwright observed no predecessor and `unknown` when a
|
||||
predecessor exists but was outside the capture. A linked predecessor is carried
|
||||
by `redirected_from_transaction_id` with no state field. Older captures without
|
||||
the state field retain the conservative meaning that a null predecessor is
|
||||
unknown.
|
||||
|
||||
`scenario.network_content` records the resolved content selection. Comparing
|
||||
captures with different selections reports network evidence as unknown with a
|
||||
@@ -284,8 +294,10 @@ browser cleanup, with a provider-owned five-second timeout per read and
|
||||
cancellation support. Failed or unexposed reads retain per-content states and make selected
|
||||
event coverage incomplete. Body events can arrive during later steps or final
|
||||
drain; event bounds describe receipt during the step, while the source event
|
||||
reference establishes the transaction phase. Comparison attributes content to
|
||||
its source event's step and excludes the sequence reference from normalization.
|
||||
reference establishes the transaction phase. Comparison attributes content, like
|
||||
every other event of its transaction, to the step that sent the request (the
|
||||
receipt step when that request was not observed) and excludes the sequence
|
||||
reference from normalization.
|
||||
Playwright scenario capture does not supply initiator stacks; passive CDP
|
||||
observation retains its own initiator evidence.
|
||||
|
||||
@@ -300,7 +312,22 @@ an unchanged claim; observed differences may still prove a changed result.
|
||||
The comparison records its full normalization commitment and SHA-256 digest.
|
||||
Built-in rules exclude step elapsed time and event sequence/index fields,
|
||||
compare screenshots by content digest, and compare DOM/accessibility captures
|
||||
by normalized text. Optional caller rules are bounded exact-literal
|
||||
by normalized text. Network events are grouped by transaction without their
|
||||
capture-local transaction IDs, attributed to the step that sent the request,
|
||||
and compared as an unordered collection after caller rules apply, because
|
||||
concurrent responses arrive in any order and can land in a later step; a
|
||||
redirect retains the complete predecessor ancestry, including each hop's own
|
||||
request, response outcome, and selected network content. Each transaction's
|
||||
capture-local IDs, sequence/index fields, and content source-sequence anchor
|
||||
are removed. Caller rules normalize retained strings before hashing. The
|
||||
`preserve_redirect_transaction_ancestry` built-in rule fingerprints each hop
|
||||
as canonical SHA-256 of `{ events, redirected_from }`: `events` is the sorted
|
||||
list of its normalized event SHA-256 digests, and `redirected_from` is its
|
||||
predecessor fingerprint or null for a root. Selected transaction fingerprints
|
||||
are sorted for the step's network comparison. Temporary numeric graph indices
|
||||
are lookup references only and do not enter the final digest. Missing or cyclic
|
||||
predecessor evidence yields unknown comparability instead of an unchanged claim.
|
||||
Other events keep receipt order. Optional caller rules are bounded exact-literal
|
||||
replacements over named artifact kinds. Rules and artifact-kind lists are
|
||||
sorted canonically before application, making the same policy reproducible
|
||||
regardless of input order. Rules operate only on already-redacted durable
|
||||
@@ -318,3 +345,7 @@ selection. Captures with different selections report event evidence as unknown
|
||||
with a recovery instruction, even when no selected family emitted an event.
|
||||
Captures written before this field existed omit it, so their event evidence is
|
||||
also unknown.
|
||||
|
||||
Network normalization also commits the removal of capture-local transaction IDs,
|
||||
attribution to the requesting step, and unordered transaction comparison. These
|
||||
built-in rule identities participate in the normalization digest.
|
||||
+15
-1
@@ -263,7 +263,12 @@ tree matches that commit remains unknown. It honors the source tree's `.gitignor
|
||||
and applies default exclusions for common generated and dependency paths such
|
||||
as `node_modules/`, `dist/`, and `*.log`. Pattern-based exclusions record the
|
||||
matched pattern and whether it came from project, default, caller, or sensitive
|
||||
path policy. Set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON array of ignore
|
||||
path policy. Links into excluded paths retain their internal target metadata;
|
||||
the exclusion explains why the target has no inventory entry, and the inventory
|
||||
remains partial. Exclusions record `entry_kind` when the importer observed it;
|
||||
older saved exclusions without that field have unknown kind. An excluded
|
||||
ancestor proves an internal descendant only when its kind is `directory`.
|
||||
Set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON array of ignore
|
||||
patterns when you want to mark selected paths as sensitive; those patterns are
|
||||
reported separately as configured-secret exclusions.
|
||||
|
||||
@@ -287,6 +292,7 @@ Choose the guide for the target and intended interaction:
|
||||
- [Browser observation](browser-observation.md): inspect a selected page through CDP.
|
||||
- [Browser scenarios](browser-scenario-contract.md): run declared interactions and capture their results.
|
||||
- [Electron observation](electron-observation.md): inspect an Electron renderer or capture an application scenario.
|
||||
- [Electron fuses](electron-fuses.md): read packaged fuse bytes before expecting `--inspect` to reach an Electron main process.
|
||||
- [Node/Electron Inspector](javascript-runtime-observation.md): record script locations and execution contexts.
|
||||
- [Process capture](process-capture.md): run an executable and compare terminal, exit and filesystem observations.
|
||||
|
||||
@@ -295,6 +301,14 @@ user permissions; consult the chosen guide for host requirements and effects.
|
||||
|
||||
## Output and exit status
|
||||
|
||||
`rea mcp doctor --json` checks production MCP startup and reports child logs in
|
||||
`diagnostics`. It drains stderr while retaining at most its first 64 KiB of raw
|
||||
bytes as text, omitting a final UTF-8 code point if it crosses that byte limit.
|
||||
`stderr_bytes` counts all observed bytes, including omitted output;
|
||||
`stderr_truncated` indicates that output exceeded the retention limit. The same
|
||||
diagnostics appear inside the report when `--full-output` is selected. Log
|
||||
truncation alone does not change the diagnostic health or exit status.
|
||||
|
||||
The default terminal format is TOON. Use `--json` when saving results for a
|
||||
JSON consumer. `--json` is indented on a terminal and compact when piped or
|
||||
redirected, which keeps large results smaller for agents and scripts; pipe
|
||||
|
||||
@@ -129,6 +129,12 @@ and Claude Code reports that autocompact is thrashing. To avoid this:
|
||||
The sizes above come from REA's own `tools/list`. A live Claude Code session
|
||||
against DeepSeek's API is unverified.
|
||||
|
||||
For a client whose stdio reader rejects the complete catalog frame, add
|
||||
`"REA_MCP_OUTPUT_SCHEMA_PROFILE": "off"` to REA's `env` object and restart the
|
||||
client. This omits only advertised output schemas; REA still validates complete
|
||||
structured results against its canonical contracts. See [MCP output schema
|
||||
profiles](mcp-contracts.md#output-schemas).
|
||||
|
||||
## Kimi Code
|
||||
|
||||
Setup registers `rea` in `~/.kimi-code/mcp.json`, or
|
||||
@@ -265,6 +271,21 @@ The user-level MCP paths follow the editor's VS Code layout. Trae's official
|
||||
documentation describes the raw JSON editor without specifying those paths;
|
||||
loading this registration in Trae remains unverified.
|
||||
|
||||
## WorkBuddy
|
||||
|
||||
For [WorkBuddy](https://www.workbuddy.cn/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/MCP-Guide), setup writes `mcpServers.rea` into the
|
||||
global `~/.workbuddy/mcp.json` (a standard `mcpServers` JSON file at the user
|
||||
home root on every platform). Entries carry `command`, `args`, and `env`;
|
||||
stdio is the default transport. WorkBuddy discovers personal skills from
|
||||
`~/.workbuddy/skills` (`%userprofile%\.workbuddy\skills` on Windows), so
|
||||
setup installs its skill copy there.
|
||||
|
||||
The global MCP path is a plain JSON file; setup preserves every other server
|
||||
and top-level key when it upserts `rea`. WorkBuddy also reads a project-level
|
||||
`<project>/.workbuddy/mcp.json`, which setup does not touch: it registers the
|
||||
user-level file only. Loading this registration and real REA tool calls
|
||||
through WorkBuddy are verified.
|
||||
|
||||
## ZCode
|
||||
|
||||
For [ZCode](https://zcode.z.ai/en/docs/mcp-services), setup registers `rea` in the nested `mcp.servers` table of the
|
||||
@@ -279,7 +300,11 @@ the first native server into an empty table, it copies the fallback's
|
||||
`mcpServers` entries into the native table so they keep loading, and leaves
|
||||
the fallback file untouched. Existing REA environment overrides are preserved
|
||||
when refreshing its command. A malformed or unreadable fallback file fails setup instead of
|
||||
being silently suppressed; repair or remove it and rerun. ZCode disables a
|
||||
being silently suppressed; repair or remove it and rerun. Uninstall removes an
|
||||
REA-owned `rea` entry from both the native table and the fallback, backing up
|
||||
each changed file, so an emptied native table cannot load a leftover fallback
|
||||
registration; other fallback servers, settings and a foreign `rea` entry are
|
||||
kept. ZCode disables a
|
||||
server with `enable: false` on its entry; doctor reports such a registration
|
||||
as stale, and rerunning setup removes the flag to repair it. Native configuration
|
||||
uses strict JSON; setup refuses comments, trailing commas and a BOM rather than
|
||||
|
||||
@@ -377,6 +377,11 @@ Such a stall leaves the initial page unloaded with empty or placeholder CDP URLs
|
||||
it does not establish permission to capture that page. See
|
||||
[the keyring diagnosis](https://github.com/morluto/rea/issues/1384#issuecomment-6093781686).
|
||||
|
||||
On Windows, browser verifier lanes that launch an owned fixture require a
|
||||
standard, non-elevated caller token. They inspect the verifier caller before
|
||||
allocating fixture resources and refuse elevated or unobservable tokens; they do
|
||||
not change caller authority or claim to inspect the browser process token.
|
||||
|
||||
`verify:browser` also captures a source-owned noise canvas as a real PNG above
|
||||
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
|
||||
decoding. Its SDK client explicitly permits the larger inline JSON response;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Electron fuse state
|
||||
|
||||
`inspect_electron_fuses` / `inspect-electron-fuses` answers whether a packaged
|
||||
Electron app will honor `--inspect` and `--inspect-brk`, `ELECTRON_RUN_AS_NODE`,
|
||||
embedded ASAR integrity validation, and loading application code only from
|
||||
`app.asar`. It reads the fuse wire stored in the Electron binary. It does not
|
||||
launch the app, modify fuse bytes, or use the network.
|
||||
|
||||
```bash
|
||||
rea inspect-electron-fuses /Applications/Example.app --json
|
||||
```
|
||||
|
||||
The CLI resolves a relative path against the caller's working directory. MCP
|
||||
requires an absolute filesystem path and preserves that exact selection:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_electron_fuses",
|
||||
"arguments": { "path": "/Applications/Example.app" }
|
||||
}
|
||||
```
|
||||
|
||||
On POSIX hosts, the CLI preserves path components so symlinks are followed
|
||||
before parent (`..`) steps. Bundle selections may end in `.` or parent steps
|
||||
through real directories; a terminal parent step through a directory symlink
|
||||
is rejected. Windows CLI paths retain native drive and separator resolution.
|
||||
|
||||
Check this before `observe_javascript_runtime` with `electron-main`. When
|
||||
`EnableNodeCliInspectArguments` is disabled or removed, Electron ignores
|
||||
Inspector flags, and a later target listing can simply show no main-process
|
||||
target.
|
||||
|
||||
## What is read
|
||||
|
||||
On macOS, pass the `.app` bundle or the Electron Framework binary. Bundle and
|
||||
framework selection first tries the top-level `Electron Framework` binary,
|
||||
then `Versions/Current/Electron Framework`, then
|
||||
`Versions/A/Electron Framework`. Symlinks are followed only while they stay
|
||||
inside that framework.
|
||||
On Windows and Linux, pass the main Electron executable. The wire lives in
|
||||
that executable rather than in `app.asar`.
|
||||
|
||||
The inline Evidence result contains:
|
||||
|
||||
- the selected path, the resolved regular file, its SHA-256, size, and container
|
||||
format (`mach-o`, `mach-o-universal`, `pe`, or `elf`);
|
||||
- one slice entry for a thin Mach-O, PE, or ELF file, and one entry per slice of
|
||||
a universal Mach-O, with file-absolute `sentinel_offset` values;
|
||||
- `wire_version`, `declared_length`, and ordered fuses `{ index, name, state, raw_byte }`;
|
||||
- `sentinel_count`, including zero and more than one;
|
||||
- `runtime_implications`, each marked `relationship: "derivation"` and tied to
|
||||
the fuse index it came from.
|
||||
|
||||
Version 1 names, in index order, are `RunAsNode`, `EnableCookieEncryption`,
|
||||
`EnableNodeOptionsEnvironmentVariable`, `EnableNodeCliInspectArguments`,
|
||||
`EnableEmbeddedAsarIntegrityValidation`, `OnlyLoadAppFromAsar`,
|
||||
`LoadBrowserProcessSpecificV8Snapshot`, `GrantFileProtocolExtraPrivileges`,
|
||||
and `WasmTrapHandlers`. An index past that table, or any index on another wire
|
||||
version, keeps `name: null` and the raw byte.
|
||||
|
||||
Wire bytes are `0x30` disabled, `0x31` enabled, and `0x72` removed. Any other
|
||||
byte is `unknown`. A removed fuse is not enabled: Inspector flags and
|
||||
`ELECTRON_RUN_AS_NODE` are not honored, and application code is not restricted
|
||||
to `app.asar`. On macOS and Windows, a removed or disabled
|
||||
`EnableEmbeddedAsarIntegrityValidation` byte means embedded ASAR integrity is
|
||||
not enforced, and an enabled byte means it is enforced. Electron compiles that
|
||||
validation only on macOS and Windows. On a Linux ELF executable the observed
|
||||
byte is preserved and the enforcement outcome is `unknown`.
|
||||
|
||||
## Unknown and multiple sentinels
|
||||
|
||||
No sentinel means the fuse state is unknown. It does not mean the fuses are
|
||||
disabled. A wire that continues past the slice is reported as truncated, with
|
||||
only the fuse bytes that were present. Two or more sentinels in one slice are
|
||||
all reported. REA does not pick one.
|
||||
|
||||
Fuse bytes do not prove that the application enables Inspector some other way,
|
||||
for example through `app.commandLine` or a remote-debugging switch in
|
||||
application code.
|
||||
|
||||
The selected file must be a regular file, or a bundle that resolves to one.
|
||||
Symlinks are followed only inside `Electron Framework.framework`. The reader
|
||||
admits at most 1 GiB, searches with 256 KiB windows, and keeps at most 32
|
||||
sentinels per slice. A real nine-fuse, one-slice result is about 1,200 CLI
|
||||
tokens; a two-slice result is about 1,300 tokens. Exact size varies with path
|
||||
lengths and limitations. This inspection creates no temporary files and does
|
||||
not modify the selected bytes.
|
||||
@@ -7,6 +7,10 @@ This passive runtime surface is distinct from the target-free static
|
||||
workflow. Static analysis reads a local ASAR or extracted directory without
|
||||
executing application code. Passive observation inspects an existing Electron
|
||||
page. The results keep static inferences and runtime observations distinct.
|
||||
Packaged fuse state is a separate static read:
|
||||
[`inspect_electron_fuses`](electron-fuses.md) reports whether this binary will
|
||||
honor Inspector flags, `ELECTRON_RUN_AS_NODE`, embedded ASAR integrity, and
|
||||
`app.asar`-only loading.
|
||||
Use the separate
|
||||
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
|
||||
when both Evidence sets already exist.
|
||||
|
||||
@@ -9,6 +9,10 @@ No device, root privilege, mount, emulation or execution of extracted programs
|
||||
is needed. Arbitrary firmware bytes do not pass native executable admission;
|
||||
select a returned ELF separately for supported Ghidra analysis.
|
||||
|
||||
On Linux, Binwalk and Unblob run as REA-owned processes and require a
|
||||
procps-compatible `ps` on REA's `PATH`; see the shared
|
||||
[Linux process prerequisite](installation.md#linux-owned-process-prerequisite).
|
||||
|
||||
## Caller-supplied tools
|
||||
|
||||
Provide existing tools using absolute executable paths, scoped to your shell:
|
||||
@@ -103,6 +107,23 @@ build's source revision, so that field remains unknown.
|
||||
engine finished its configured extraction without those reported conditions;
|
||||
default skip rules and format coverage still apply. It does not establish
|
||||
complete firmware reconstruction.
|
||||
- `extraction_gaps` makes those conditions recoverable: a row identifies an
|
||||
unextracted region or a failed task/stage. A chunk retains its identity and
|
||||
byte range, plus its original-file range when it belongs to the selected input.
|
||||
A multi-file stage retains its reported ID and handler with unknown byte ranges;
|
||||
a standalone task failure keeps unavailable stage identity null. Rows retain exact dependency names from
|
||||
the producer's missing-dependency reports; an accepted report with no names
|
||||
remains a failure with unknown dependencies; no executable identity or specific installation step is inferred. Each row's `continuation_id`
|
||||
references one instruction in `extraction_continuations`, so many regions
|
||||
share recovery prose without losing any region identity. Instructions use
|
||||
already configured capabilities where one exists. Every extraction rerun
|
||||
requires a fresh absent `output_directory`, preserving the prior evidence.
|
||||
Nested unknown chunks use `inspect_firmware_regions` on the actual published
|
||||
child rather than repeating the same extraction with unchanged handlers.
|
||||
Inspection requires separately configured Binwalk (`REA_BINWALK_COMMAND`);
|
||||
without it, the inspection step is unavailable and the region remains unexamined. A listed gap does not
|
||||
mean the target is corrupt or that a retained region is empty; REA installs
|
||||
nothing.
|
||||
- The destination must be absent, absolute, with an existing parent. Publication
|
||||
uses exclusive, symlink-resistant writes and durable digest readback. Existing
|
||||
destinations are never overwritten. This is not an atomic directory rename:
|
||||
@@ -167,6 +188,12 @@ records gzip offset/length and extracted file hashes. Real verification checks
|
||||
CLI/MCP, full-file and selected-range extraction, unknown chunks, depth limits,
|
||||
content parity and unchanged source bytes. No binary fixture is committed.
|
||||
|
||||
The optional missing-extractor lane asserts dependency names and the owning
|
||||
handler in `extraction_gaps`. Those assertions must be run against the pinned
|
||||
Unblob release to establish real-provider coverage; synthetic producer fixtures
|
||||
alone do not verify Unblob's report nesting or stage identities. The gap ownership
|
||||
claims remain unverified against real Unblob until that lane runs.
|
||||
|
||||
Additional lanes are explicit, so the base lane stays small:
|
||||
|
||||
```sh
|
||||
|
||||
+23
-7
@@ -52,13 +52,29 @@ section names or the record being inside the initial 4 KiB header probe.
|
||||
Program and section declarations must agree; duplicate, conflicting, truncated
|
||||
or out-of-file records are rejected. Stripped section tables and section-only
|
||||
records are supported. Extended table numbering is explicitly unsupported.
|
||||
For carved ELFs whose section table extends beyond the captured file, REA
|
||||
examines every complete captured section header and records the unavailable
|
||||
headers as a limitation. A valid program-header ABI record can still establish
|
||||
the supported declaration. Available section declarations must agree with it;
|
||||
their duplicate, conflicting or unreadable records remain errors. A missing
|
||||
program-header record does not make ABI declarations in uncaptured sections
|
||||
absent or supply a default ABI.
|
||||
GNU `SHT_GNU_ATTRIBUTES` sections are located by numeric section type; section
|
||||
names never control detection. REA reads at most 4096 payload bytes and parses
|
||||
format version `A`, the `gnu` vendor's file-scope `Tag_GNU_MIPS_ABI_FP` (4) and
|
||||
`Tag_GNU_MIPS_ABI_MSA` (8) only. Unknown vendors, scopes or tags, repeated
|
||||
declarations, malformed lengths, and out-of-file or oversized payloads remain
|
||||
refusals. A nonzero FP tag must match the separate ABI flags `fp_abi` record;
|
||||
value 0 (`ANY`) declares no requirement. The [GNU Object Attributes v1
|
||||
format](https://sourceware.org/binutils/docs/as/Object-Attributes-v1.html) and
|
||||
[GNU MIPS tag definitions](https://sourceware.org/binutils/docs/as/GNU-Object-Attributes.html)
|
||||
are the format references. The MSA tag describes the vector
|
||||
argument ABI, while the ABI flags ASE field describes instruction-set
|
||||
extensions; REA does not treat those as interchangeable. The 128-bit MSA
|
||||
argument ABI and unknown nonzero MSA values remain unsupported. Without a
|
||||
separate ABI flags record, FP attribute values are admitted only when absent,
|
||||
`ANY` (0), double precision (1), or FPXX (5), the values covered by the
|
||||
recordless profile. Parsed values are retained in the MIPS analysis profile.
|
||||
For carved ELFs whose section table extends
|
||||
beyond the captured file, REA examines every complete captured section header
|
||||
and records the unavailable headers as a limitation. The provider refuses an
|
||||
incomplete ABI declaration inspection even when a program-header ABI record is
|
||||
valid, because an unavailable section header could carry an unsupported or
|
||||
conflicting declaration. A missing program-header record does not make ABI
|
||||
declarations in uncaptured sections absent or supply a default ABI.
|
||||
The field layout follows
|
||||
[`Elf_MIPS_ABIFlags_v0`](https://github.com/bminor/glibc/blob/master/elf/elf.h).
|
||||
|
||||
|
||||
+12
-7
@@ -7,6 +7,8 @@ Executable bytes remain unchanged.
|
||||
|
||||
Use [installation](installation.md#ghidra) for supported hosts, prerequisites,
|
||||
configuration, startup deadlines, resource controls, and cleanup recovery.
|
||||
[Linux Ghidra launches](installation.md#linux-owned-process-prerequisite) also
|
||||
require a procps-compatible `ps` on REA's `PATH` for process ownership checks.
|
||||
[Windows P0](windows-ghidra-p0.md) has its own admission and lifecycle boundary;
|
||||
[NativeAOT observations](ghidra-nativeaot.md) include a built-in read-only PE
|
||||
snapshot parser and a separate optional annotation extension. Check the
|
||||
@@ -154,12 +156,15 @@ An inferred no-return call can terminate that body before subsequent bytes.
|
||||
Pseudocode, assembly, procedure info, instruction windows, callee and caller
|
||||
reads, and dossiers report observed terminal call sites in Evidence limitations,
|
||||
including resolved callee names, addresses, and FunctionManager flags when
|
||||
available. Instruction windows
|
||||
and dossiers also retain these warnings in their result limitations. An
|
||||
unresolved callee or unestablished terminal-flow basis remains explicit. These
|
||||
observations qualify the analysis; they do not prove that a no-return flag is
|
||||
incorrect or recover excluded code. Analysis profiles version this observation
|
||||
contract so older snapshots cannot replay unqualified results.
|
||||
available. On x86-32, the same views also report the exact Borland/Delphi-style
|
||||
FS exception-registration pattern when its handler address falls outside the
|
||||
function body. This is a static registration observation; it does not establish
|
||||
handler execution or change the body. Instruction windows and dossiers also
|
||||
retain these warnings in their result limitations. An unresolved callee or
|
||||
unestablished terminal-flow basis remains explicit. These observations qualify
|
||||
the analysis; they do not prove that a no-return flag is incorrect or recover
|
||||
excluded code. Analysis profiles version this observation contract so older
|
||||
snapshots cannot replay unqualified results.
|
||||
|
||||
On non-Windows hosts, the ephemeral import repair clears no-return flags on the
|
||||
known returning external ABI names `malloc`, `__tls_get_addr`, `memcpy`, `memset`,
|
||||
@@ -189,7 +194,7 @@ host/provider acceptance run with only the host compiler.
|
||||
|
||||
`npm run verify:ghidra:windows` checks source-owned native x86-64 PE applications;
|
||||
`-- --x86` selects x86 and `-- --dll` selects DLLs, and the flags can be combined.
|
||||
These lanes check all 25 read-only operations, target/snapshot/import SHA-256 linkage,
|
||||
These lanes check all 26 read-only operations, target/snapshot/import SHA-256 linkage,
|
||||
and project, endpoint, process, and runtime cleanup. The independent native
|
||||
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
|
||||
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
|
||||
|
||||
+12
-2
@@ -58,6 +58,12 @@ original stable file snapshot, including pointer-backed strings. It does not
|
||||
scan arbitrary byte strings as compiler metadata or interpret a fat Mach-O
|
||||
container as one selected architecture.
|
||||
|
||||
For ELF images without a named build-info section, discovery checks each
|
||||
nonempty writable, non-executable load segment in program-header order. This
|
||||
includes sectionless PIE images with metadata in a later data segment. Named
|
||||
sections retain priority, and alignment, file mapping and record validation
|
||||
remain required.
|
||||
|
||||
Historical moduleless Go binaries can store their empty module-string header in
|
||||
declared zero-initialized virtual memory rather than file-backed bytes. The reader
|
||||
recognizes this only when the complete optional module header lies in that memory
|
||||
@@ -78,9 +84,13 @@ host permission denial, changed artifacts and cancellation retain distinct
|
||||
errors. The selected file is never modified; this inspection creates no
|
||||
temporary files, starts no subprocesses and accesses no network endpoint.
|
||||
|
||||
Resource guards bound the selected file to 256 MiB, aggregate embedded strings
|
||||
Resource guards bound the selected file and cumulative header searches to 256 MiB
|
||||
each, aggregate embedded strings
|
||||
to 1 MiB, structural table decoding to 16 MiB and the complete normalized metadata
|
||||
report to 8 MiB. Oversized results fail without partial success. MCP also applies
|
||||
report to 8 MiB. Identical search spans with the same virtual alignment are
|
||||
searched once; partially overlapping spans consume the cumulative search budget
|
||||
and fail with a CPU resource constraint when it is exhausted. Oversized results
|
||||
fail without partial success. MCP also applies
|
||||
its [response transport budget](mcp-contracts.md), which counts both text and
|
||||
structured content; a report within the metadata limit can still require export
|
||||
from retained Evidence. These limits are not operating-system memory or CPU
|
||||
|
||||
@@ -6,7 +6,9 @@ Choose one lifecycle mode in an upstream MCP JSON registration. Set `REA_IDA_MCP
|
||||
|
||||
## Supported upstream versions
|
||||
|
||||
Each mode accepts one upstream tool profile, checked against `tools/list` before any analysis runs. Upstream's `main` branch has since moved to 2.0.0, which renamed the GUI tools and dropped `idb_close`, so it serves neither profile. Installing from that branch fails at startup with the missing tool names in the error diagnostics.
|
||||
Each mode accepts one upstream tool profile, checked against `tools/list` before any analysis runs. The attached profile requires the legacy GUI tool names; upstream 2.0.0 renamed those tools, and REA has not established an equivalent attached-mode lifecycle and ownership contract for that catalog. It therefore fails closed with the missing tool names in the error diagnostics.
|
||||
|
||||
The headless profile is defined by the database-supervisor API, not by an upstream version string. The upstream source snapshot [`c133c385`](https://github.com/mrexodia/ida-pro-mcp/tree/c133c3853faa111a9b00ee615c013b720d0c4acd) includes `idb_close` (introduced by [upstream PR #495](https://github.com/mrexodia/ida-pro-mcp/pull/495)) alongside `idb_open` and `idb_list`. Earlier builds may report the same `2.0.0` version while lacking that tool, so REA checks the live catalog rather than treating a version number or branch name as a compatibility guarantee.
|
||||
|
||||
| Mode | Upstream profile | Known working |
|
||||
| -------- | ------------------- | ------------------------------------------------------ |
|
||||
@@ -137,11 +139,11 @@ For guided registration, set `REA_IDA_MCP_CONFIG` before `rea setup --client <cl
|
||||
|
||||
All profiles support procedure/string inventory and literal or regex search, procedure resolution, pseudocode, assembly, raw function instructions, resolved callees, incoming address references, and a function dossier. Inventories and modern pseudocode/assembly follow upstream pagination. Upstream limits on modern xrefs and callees are retained as explicit truncation limitations. In the modern headless profile, targets that are not proven x86-only use processor-neutral `callgraph` edges instead of the x86 instruction checks in upstream `callees` (mrexodia/ida-pro-mcp#549). REA requests depth zero: the pinned upstream emits the root's outgoing edges without scanning child functions, then REA batches function-name lookup for those addresses. The graph covers recognized function destinations only; imports and other non-function targets remain unknown. A server without `callgraph` returns an empty relationship list with an explicit coverage limitation. Graph and path workflows retain those limitations alongside observed edges. A failed or inconsistent graph reply remains an error. A transport-level truncated preview is rejected rather than reported as complete evidence; the error preserves upstream retrieval guidance.
|
||||
|
||||
The modern headless profile also supports `resolve_containing_procedure` and `list_documents`. An address resolves through upstream `lookup_funcs` to the canonical entry of the function that contains it; the body stays unavailable like other IDA identities. When no function contains it, REA reads the database-scoped minimal `survey_binary` and reports `not_in_procedure` for an address inside one of its segments (ends are exclusive) and `outside_segments` otherwise. A missing, truncated or inconsistent survey fails the read instead of guessing a reason. `list_documents` returns the single admitted program path, so `inspect_address_context` and `trace_feature` need no explicit document. `trace_feature` marks a trace truncated, with a residual unknown for that address, when upstream caps the references it returned. The legacy and native profiles do not declare these operations.
|
||||
All profiles expose `list_documents` as the single target admitted for the session. Attached mode verifies its digest against IDA metadata before returning it; headless mode binds the digest-checked private input. `inspect_address_context`, `trace_feature`, and `binary_overview` can therefore resolve that identity without upstream document-listing tools. Segment enumeration is optional: when the selected profile cannot provide it, `binary_overview` still returns document and procedure/string counts and omits `segments` and `segment_count`; omission means unobserved, not empty. The modern headless profile also supports `resolve_containing_procedure`. An address resolves through upstream `lookup_funcs` to the canonical entry of the function that contains it; the body stays unavailable like other IDA identities. When no function contains it, REA reads the database-scoped minimal `survey_binary` and reports `not_in_procedure` for an address inside one of its segments (ends are exclusive) and `outside_segments` otherwise. A missing, truncated or inconsistent survey fails the read instead of guessing a reason. `trace_feature` marks a trace truncated, with a residual unknown for that address, when upstream caps the references it returned. Attached profiles do not declare address containment resolution.
|
||||
|
||||
The legacy profile additionally supports direct callers. Its producer may report call-site or interior addresses; REA resolves the reported function symbol to its canonical entry. The modern and native profiles do not prove direct callers from its code/data xref labels, so that capability is unavailable and the dossier marks it unknown.
|
||||
|
||||
Complete body ranges, typed reference edges, CFG, referenced data, source locals, comment kinds, and database revision are unavailable. Empty unsupported dossier facets mean **unknown**, not observed absence. `binary_overview`, whole-symbol inventory, mutation, debugging, arbitrary Python forwarding, and requested GUI navigation are outside this adapter's coverage. Check session availability before composing a workflow that needs those facets.
|
||||
Complete body ranges, typed reference edges, CFG, referenced data, source locals, comment kinds, and database revision are unavailable. Empty unsupported dossier facets mean **unknown**, not observed absence. Whole-symbol inventory, mutation, debugging, arbitrary Python forwarding, and requested GUI navigation are outside this adapter's coverage. Check session availability before composing a workflow that needs those facets.
|
||||
|
||||
`provider.version` identifies the REA adapter; the profile explicitly records `version_scope: "rea-ida-adapter"`. The MCP handshake's `serverInfo` is separate and does not establish IDA or Python distribution versions. Raw observations preserve producer metadata and output. Attached hashes identify the recorded original input; headless identity uses the digest-verified private copy and observed worker input path. Neither proves source-code recovery or an unmodified analysis database.
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
|
||||
| Inspect assemblies, packages, or firmware | [.NET](managed-code-analysis.md), [Android](android-analysis.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
|
||||
| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
|
||||
| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
|
||||
|
||||
+216
-7
@@ -64,11 +64,21 @@ preserves a verified `.rea.backup.<sha256>.recovery` copy of the current bytes.
|
||||
Both snapshots remain available; a failed rollback reports the current recovery
|
||||
copy's path and the restore failure.
|
||||
Setup also retains valid environment overrides in REA's existing server entry,
|
||||
such as `REA_MCP_INPUT_SCHEMA_PROFILE` and `REA_MCP_MAX_RESPONSE_BYTES`.
|
||||
such as `REA_MCP_INPUT_SCHEMA_PROFILE`, `REA_MCP_OUTPUT_SCHEMA_PROFILE`, and
|
||||
`REA_MCP_MAX_RESPONSE_BYTES`.
|
||||
Detected provider settings replace only matching environment keys.
|
||||
Setup and doctor report a malformed REA environment map or invalid runtime
|
||||
setting before setup writes files. Repair the named setting in the client
|
||||
configuration; setup does not silently discard the rest of the map.
|
||||
This includes malformed loader/exclusion JSON arrays and a Ghidra compiler
|
||||
specification without its required language identifier. Valid overrides keep
|
||||
their original strings, including empty Hopper arguments. DeepSeek Harness
|
||||
`!!js` environment expressions remain unevaluated and preserved; registration
|
||||
alignment does not verify the runtime values those expressions will produce.
|
||||
For layered OpenCode files, each file's values are checked before merging, while
|
||||
setting dependencies are checked against the effective merged environment.
|
||||
Setup preserves each layer's overrides instead of copying inherited settings
|
||||
into the file it updates.
|
||||
|
||||
After setup, REA reports which agents, analysis tools, and workflow files passed
|
||||
its final checks. Restart any agent named in the completion message, then begin
|
||||
@@ -129,6 +139,150 @@ Setup pins package-runner MCP registrations to the exact installed REA version,
|
||||
installs the matching skill and on-demand references in the same plan, and adds
|
||||
`startup_timeout_sec = 30` for Codex and Grok Build.
|
||||
|
||||
## Limit REA to one project or agent profile
|
||||
|
||||
Setup registers REA for the whole client. For Claude Code it writes
|
||||
`mcpServers.rea` in `~/.claude.json` and installs the bundled skill under
|
||||
`~/.claude/skills`, so every project that Claude Code opens can call REA. To
|
||||
offer REA's tools only where they're wanted, use one of the routes below.
|
||||
|
||||
Both routes control which sessions discover REA's MCP server and skill. They
|
||||
leave shell commands, filesystem access and network access available. An agent
|
||||
under the same OS account can run `rea` from `PATH` or `npx` and read other
|
||||
profiles and REA output. For clean-room separation, use an implementation
|
||||
environment with filesystem and network restrictions that prevent reading or
|
||||
fetching REA, the target and analysis output, including through shared
|
||||
installations and package registries. A separate account or container must
|
||||
enforce those restrictions.
|
||||
|
||||
For Claude Code, check existing default-profile state before choosing either
|
||||
route. Setup is additive: a previous user-level REA registration and personal
|
||||
skill remain active in other projects. Back up the default configuration and
|
||||
REA skill first, then, with `CLAUDE_CONFIG_DIR` unset, remove the REA user
|
||||
registration with `claude mcp remove --scope user rea` and remove the known
|
||||
REA-installed `~/.claude/skills/reverse-engineer-anything` directory. Preserve
|
||||
unrelated registrations and skills. `rea uninstall` applies to other clients
|
||||
as well, so it is broader than this Claude-only migration.
|
||||
|
||||
**A separate Claude Code profile.** Claude Code keeps settings, skills and session
|
||||
history under `CLAUDE_CONFIG_DIR`. Setup follows the same
|
||||
variable: it writes the registration to `$CLAUDE_CONFIG_DIR/.claude.json` and
|
||||
the skill to `$CLAUDE_CONFIG_DIR/skills`.
|
||||
|
||||
```bash
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx rea-agents setup --client claude_code
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea claude
|
||||
```
|
||||
|
||||
A new profile may need authentication. Console sign-ins without an API key are
|
||||
stored outside `CLAUDE_CONFIG_DIR`; see
|
||||
[Claude Code authentication](https://code.claude.com/docs/en/authentication#log-in-with-multiple-accounts).
|
||||
Sessions started without the variable use the default profile's registration,
|
||||
skill and history.
|
||||
|
||||
Inline assignments apply only to that command. Keep the same prefix for every
|
||||
profile check, setup rerun, update, or removal; bare commands use the current
|
||||
environment and can otherwise inspect or recreate default-profile state:
|
||||
|
||||
```bash
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea claude mcp list
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx rea-agents doctor --client claude_code --skill --json
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx -y rea-agents@latest setup --client claude_code --dry-run
|
||||
```
|
||||
|
||||
After reviewing an update plan, rerun that same prefixed setup command without
|
||||
`--dry-run` and approve its exact changes. Profile removal likewise needs the
|
||||
prefix on `claude mcp remove --scope user rea` and any `rea uninstall` command;
|
||||
keep the broader client scope of uninstall in mind. Restart the selected
|
||||
profile after changing its pinned registration.
|
||||
|
||||
`claude mcp remove` removes only the registration. To remove REA from this
|
||||
profile without running the broader uninstall flow, first back up the profile's
|
||||
REA-managed skill directory, then remove
|
||||
`$CLAUDE_CONFIG_DIR/skills/reverse-engineer-anything` as well. Keep other skills
|
||||
in the profile intact.
|
||||
|
||||
The commands above use POSIX shell assignment syntax. In native Windows
|
||||
PowerShell, select the profile for the whole setup/launch/check session and
|
||||
restore the previous selection afterward:
|
||||
|
||||
```powershell
|
||||
$previousClaudeConfigDir = $env:CLAUDE_CONFIG_DIR
|
||||
try {
|
||||
$env:CLAUDE_CONFIG_DIR = Join-Path $HOME ".claude-rea"
|
||||
npx rea-agents setup --client claude_code
|
||||
claude mcp list
|
||||
npx rea-agents doctor --client claude_code --skill --json
|
||||
npx -y rea-agents@latest setup --client claude_code --dry-run
|
||||
# After reviewing the plan, repeat setup without --dry-run and approve it.
|
||||
claude
|
||||
# Profile removal, when intended, also belongs inside this selected scope:
|
||||
# claude mcp remove --scope user rea
|
||||
} finally {
|
||||
if ($null -eq $previousClaudeConfigDir) {
|
||||
Remove-Item Env:CLAUDE_CONFIG_DIR -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
$env:CLAUDE_CONFIG_DIR = $previousClaudeConfigDir
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use PowerShell for these Windows examples rather than pasting POSIX assignments
|
||||
into cmd.exe. Keep profile updates, readiness checks and removal inside the
|
||||
same selected environment; the `finally` block restores the previous profile
|
||||
or unsets the variable when it was absent.
|
||||
|
||||
**One project.** Install without configuring Claude Code: use the curl
|
||||
installer's `--no-setup`, or run setup only for other clients. Then register
|
||||
REA from the project directory at Claude Code's local scope.
|
||||
|
||||
This route uses the default Claude profile. Keep `CLAUDE_CONFIG_DIR` unset for
|
||||
the entire local lifecycle: registration, backup, listing, version replacement
|
||||
and removal. In POSIX shells run `unset CLAUDE_CONFIG_DIR`; in PowerShell run
|
||||
`Remove-Item Env:CLAUDE_CONFIG_DIR -ErrorAction SilentlyContinue` before these
|
||||
commands. Record an existing selection first if you intend to restore it after
|
||||
finishing. The configuration path below is the default profile's
|
||||
`~/.claude.json`; a different active profile uses its own config root instead.
|
||||
|
||||
Register with the variable unset:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```bash
|
||||
claude mcp add --scope local rea -- npx -y rea-agents@6.4.0 mcp
|
||||
```
|
||||
|
||||
On native Windows, use the same project directory and wrap the package-runner
|
||||
shim so the stdio client can launch it:
|
||||
|
||||
```powershell
|
||||
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.4.0 mcp
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Local scope stores the entry under that directory's path in `~/.claude.json`,
|
||||
so other projects don't load it. This route doesn't install the bundled skill.
|
||||
Avoid `--scope project`, which writes `.mcp.json` into the repository where it
|
||||
can be committed or copied. `rea doctor` and `rea uninstall` read only the
|
||||
user-level entry, so they don't report or remove a local-scope registration;
|
||||
remove it with `claude mcp remove --scope local rea` from the same directory.
|
||||
`rea update` doesn't refresh it either, so it stays on the version it names.
|
||||
To move it to a newer release, first back up the project entry in
|
||||
`~/.claude.json` and record its environment and other metadata. In particular,
|
||||
preserve caller-selected overrides such as `REA_MCP_INPUT_SCHEMA_PROFILE` and
|
||||
`REA_MCP_MAX_RESPONSE_BYTES`. Run `claude mcp remove --scope local rea` from the
|
||||
same directory, then recreate it with the platform-appropriate command above
|
||||
and the new version. Reapply every recorded environment value with Claude
|
||||
Code's `mcp add --env NAME=value` options and restore any other retained entry
|
||||
metadata before restarting the project session. Removal and re-addition do not
|
||||
preserve these values automatically. Running setup for Claude Code instead
|
||||
would add a user-level entry for every project.
|
||||
|
||||
Check the separate profile with the prefixed `claude mcp list` above and compare
|
||||
with the default profile with `CLAUDE_CONFIG_DIR` unset. For local scope, run
|
||||
`claude mcp list` from the selected project and again from another project.
|
||||
|
||||
## Check readiness for your task
|
||||
|
||||
Run `rea doctor` when you need diagnosis. It reads host prerequisites and agent
|
||||
@@ -136,6 +290,15 @@ configuration without changing them. Without options, it audits every detected
|
||||
registration, the installed skill and optional analysis engines; its overall
|
||||
`healthy` value can be false while your chosen workflow works.
|
||||
|
||||
Doctor's `providerInspections` are host-level engine and registration checks,
|
||||
not a readiness row for every advertised MCP adapter surface. `tools/list`
|
||||
remains the complete stable catalog. Call `binary_session` with `{}` before
|
||||
choosing an operation; it works before a target is opened, and
|
||||
`result.tool_availability` reports per-operation availability, reasons and
|
||||
client requirements for the current session state. It does not guarantee that
|
||||
a particular caller-supplied input will be valid. See
|
||||
[MCP discovery](mcp-contracts.md#identity-and-discovery).
|
||||
|
||||
Select the part you want to check:
|
||||
|
||||
| Task | Readiness check |
|
||||
@@ -157,6 +320,21 @@ on `open_binary`. A `capability_unavailable` failure carries
|
||||
`details.candidate_ids`, while `provider_unavailable` asks you to repair the
|
||||
selected engine. See [provider selection](cli.md#choose-a-provider).
|
||||
|
||||
### Linux owned-process prerequisite
|
||||
|
||||
On Linux, REA's shared process supervisor requires a procps-compatible `ps`
|
||||
on REA's `PATH` for ownership checks before launching a child. Ghidra, JADX,
|
||||
firmware tools and static JavaScript/Electron analysis use these checks.
|
||||
Install your distribution's `procps` or `procps-ng` package, or put another
|
||||
procps-compatible `ps` on `PATH` before
|
||||
running those workflows. A missing or incompatible command causes a refusal
|
||||
before the supervised child starts.
|
||||
|
||||
The public error code varies by caller: `capability_unavailable`,
|
||||
`provider_unavailable` or `execution_failure`. Some provider adapters report
|
||||
only a generic startup error. Check this prerequisite before following
|
||||
provider-specific recovery advice.
|
||||
|
||||
## Diagnose, update, and remove
|
||||
|
||||
`rea doctor --json` is strictly read-only. `rea update` updates only the npm or Bun global installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest` or `bunx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
|
||||
@@ -288,11 +466,12 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
|
||||
| Qoder | `qoder` |
|
||||
| Factory Droid | `droid` |
|
||||
| Trae | `trae` |
|
||||
| WorkBuddy | `workbuddy` |
|
||||
| ZCode | `zcode` |
|
||||
| DeepSeek Harness | `dsh` |
|
||||
|
||||
`dsh` and `kimi` require a release containing them. The registry check above does
|
||||
not cover them.
|
||||
`dsh`, `kimi`, and `workbuddy` require a release containing them. The registry
|
||||
check above does not cover them.
|
||||
|
||||
See [client configuration and troubleshooting](client-configuration.md) for
|
||||
configuration paths, profiles, trust policies, and schema-limit workarounds.
|
||||
@@ -334,6 +513,20 @@ document for the same target. REA does not currently identify, focus, or reuse
|
||||
that existing GUI document, and the supported launcher exposes no attach or
|
||||
reuse action for it.
|
||||
|
||||
REA currently declines saved Hopper database (`.hop`) targets on macOS because
|
||||
the native `open database` launch route does not run REA's bridge script. To
|
||||
analyze that program with REA, select its original executable as the target and
|
||||
open it through Hopper; this starts the supported bridge route. This does not
|
||||
recover database-only annotations or analysis changes. Saved-database analysis
|
||||
on Linux continues to use its supported demo-launch route.
|
||||
|
||||
Hopper lease directories must be readable and owned by the current user.
|
||||
REA validates and secures an opened directory before reserving a lease. If
|
||||
opening that directory fails with a permission error, the diagnostic identifies
|
||||
the path and recovery steps while leaving its permissions unchanged. Check
|
||||
ownership and parent-directory access, then restore owner-only read, write, and
|
||||
search permissions (`0700`) on a directory you own before retrying.
|
||||
|
||||
On supported Linux distributions, approved setup verifies Hopper's official
|
||||
`.deb`, `.rpm`, or Arch package before invoking the native package manager.
|
||||
REA runs the supported demo build on a private Xvfb display and selects Hopper's
|
||||
@@ -346,6 +539,13 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
|
||||
`sudo`. `rea doctor --provider hopper --json` reports the selected
|
||||
strategy and both host and effective mount facts.
|
||||
|
||||
When the package-manager step fails, setup reports the failing command and its
|
||||
captured output, so a package conflict is distinguishable from an authorization
|
||||
failure. On CachyOS, an unversioned compatibility package such as
|
||||
`zlib-ng-compat` may conflict with Hopper's versioned `zlib` dependency. Resolve
|
||||
the package conflict yourself or install Hopper's official package manually,
|
||||
then rerun `rea setup`.
|
||||
|
||||
The supported Linux demo forwards additional launches into its existing
|
||||
application, even across private displays. REA therefore reserves one Linux
|
||||
Hopper application per user across cooperating REA processes. A competing
|
||||
@@ -370,8 +570,9 @@ sessions before removing that recovery directory and retrying.
|
||||
On macOS, REA uses
|
||||
`/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` by default.
|
||||
On Linux, it prefers executable `/opt/hopper/bin/Hopper`, then executable
|
||||
`~/.local/share/rea/hopper/bin/Hopper`; if neither exists, the former remains
|
||||
the diagnostic fallback. `HOPPER_LAUNCHER_PATH` overrides those choices:
|
||||
`~/.local/share/rea/hopper/bin/Hopper`, followed by `/opt/hopper-v4/bin/hopper`
|
||||
and `/usr/bin/hopper`; if none exists, the former remains the diagnostic
|
||||
fallback. `HOPPER_LAUNCHER_PATH` overrides those choices:
|
||||
|
||||
```bash
|
||||
export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper
|
||||
@@ -511,7 +712,7 @@ reported as incompatible instead of being admitted from its directory name, and
|
||||
a header that cannot be read or recognized is reported as unknown. REA does not
|
||||
build or install native tools, or change Gatekeeper quarantine settings.
|
||||
|
||||
The adapter exposes 25 read-only operations: thirteen inventory/name/search
|
||||
The adapter exposes 26 read-only operations: fourteen inventory/name/search
|
||||
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
|
||||
@@ -559,6 +760,14 @@ installation remains unavailable on Windows. Setup never installs Ghidra,
|
||||
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
|
||||
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
|
||||
|
||||
When `GHIDRA_INSTALL_DIR` is unset, doctor and the Ghidra provider also search
|
||||
`PATH` for `ghidra` or `ghidraRun`, resolve the selected launcher through its
|
||||
real path, and accept it only when that location belongs to a Ghidra tree with
|
||||
the application metadata and headless launcher. An explicitly supplied
|
||||
`GHIDRA_INSTALL_DIR` always takes precedence, including when it is invalid;
|
||||
REA reports that error instead of silently selecting another installation.
|
||||
This discovery is read-only and does not execute the Ghidra launcher.
|
||||
|
||||
Doctor validates the platform, architecture, Ghidra 12.1.x application version,
|
||||
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, the installation's
|
||||
Java major range, 64-bit JDK bitness, and the presence of `javac`/`javac.exe`.
|
||||
@@ -799,7 +1008,7 @@ On a controlled Windows x64 runner, use
|
||||
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
|
||||
PE fixture from source bytes and requires the Windows native authority before
|
||||
opening the provider. The controls are implemented on main; see the
|
||||
[release boundary](#released-package-and-main). The lane verifies all 25 admitted
|
||||
[release boundary](#released-package-and-main). The lane verifies all 26 admitted
|
||||
read-only operations, digest identity, and runtime cleanup. The separate
|
||||
`verify:ghidra:windows:package` lane checks the packed CLI/MCP boundary with an
|
||||
ordinary user. It requires the built Windows native bundle and an existing
|
||||
|
||||
@@ -282,9 +282,14 @@ suffix. Utility calls such as `useUtils().invoice.list.fetch()`, `useQueries`,
|
||||
and links whose callee was renamed by a minifier are not recognized.
|
||||
|
||||
Each recovered bundle module retains the exact factory-source digest. A complete
|
||||
bounded AST also receives a `babel-ast-v1` structural fingerprint that ignores
|
||||
bounded AST also receives a `babel-ast-v2` structural fingerprint that ignores
|
||||
ordinary identifier names while retaining syntax, literals, operators, object
|
||||
keys, and member properties. If fingerprint construction reaches its structural
|
||||
keys, and member properties. Computed member identifiers retain their access
|
||||
syntax without treating a local variable's spelling as a fixed property; template
|
||||
literal contents remain part of the fingerprint. Version comparison keeps the
|
||||
algorithm in its matching key, so retained `babel-ast-v1` fingerprints cannot match
|
||||
these new fingerprints. Re-analyze both snapshots to compare their new structure.
|
||||
If fingerprint construction reaches its structural
|
||||
bound, the fingerprint is unavailable and its status is `truncated`; a bounded
|
||||
prefix is never presented as a complete stable fingerprint.
|
||||
|
||||
@@ -619,3 +624,11 @@ Unresolved HTML references retain their declaration, source range, and resolutio
|
||||
reason in the renderer observations.
|
||||
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
|
||||
line endings, preserving UTF-16 columns.
|
||||
Inline JavaScript in local HTML, meaning classic or module `<script>` bodies
|
||||
without a `src` attribute, is not analyzed. Each HTML file that has one gets an
|
||||
unknown `parse-html-inline-scripts` scope with the exact body count and the
|
||||
script type and source range of the first 100 bodies; beyond that, coverage is
|
||||
truncated under the `html_inline_script_locations_per_file` limit, and the
|
||||
scope's evidence counts the omitted locations. The graph names the
|
||||
file and up to 10 starting lines in its limitations, and coverage is partial. Import maps, speculation rules, data blocks, and blank
|
||||
bodies are not JavaScript and are not reported.
|
||||
@@ -36,6 +36,12 @@ application starts, and REA never resumes it or sends
|
||||
`Runtime.runIfWaitingForDebugger`. Resume a paused target with a separate
|
||||
caller-owned debugger before requesting application runtime observations.
|
||||
|
||||
For a packaged Electron app, read
|
||||
[`inspect_electron_fuses`](electron-fuses.md) before starting an `electron-main`
|
||||
target. When `EnableNodeCliInspectArguments` is disabled or removed, Electron
|
||||
ignores `--inspect` and `--inspect-brk`. A missing main-process target after
|
||||
launch does not, by itself, show that fuse.
|
||||
|
||||
The caller supplies the literal-loopback HTTP endpoint directly. Selecting an endpoint exposes all targets it serves and their local script locations or renderer origins. A target must match the exact requested target ID and expose a same-port WebSocket reported by that endpoint. Targets explicitly marked attached are rejected rather than displacing another debugger. Node's `/json/list` omits attachment state, so REA preserves it as `null` (unknown), reports that limitation, and permits observation unless the endpoint explicitly reports `attached: true`. Local paths are canonicalized after symlink resolution.
|
||||
|
||||
The `attached` field in JavaScript runtime and browser target results is nullable: `true` and `false` are reported only when discovery supplied a boolean; `null` means the endpoint omitted or could not report the state. CDP `TargetInfo` worker records require an `attached` boolean; REA excludes a worker record with a missing or malformed value as an invalid protocol value.
|
||||
|
||||
@@ -10,6 +10,12 @@ Start a JEB client that serves MCP and point REA at it:
|
||||
- In the JEB GUI client, start the MCP server from the _File_ menu (or via the VIBRE assistant).
|
||||
- Headless, use JEB's documented programmatic route (`JebMcpServerInstance` from the JEB API) with the JEB client on your classpath; JEB 5.48's `-c` headless mode does not accept `--mcp` directly.
|
||||
|
||||
For setup registration, set `REA_JEB_MCP_URL` in the REA process environment
|
||||
before running `rea setup`. The approved setup plan preserves the exact selected
|
||||
URL after validating its syntax. That validation does not establish that the
|
||||
JEB service is reachable or that a project is open; use the existing
|
||||
`inspect-jeb-client` command for a read-only service query.
|
||||
|
||||
Verified against JEB 5.48.0 (Linux x64, licensed build) serving `jeb-mcp-server` 1.3.0 over streamable HTTP.
|
||||
|
||||
## Operations
|
||||
|
||||
@@ -81,6 +81,40 @@ including the existing `inspect-analysis-view` CLI. Session record lifetime and
|
||||
export/import semantics remain unchanged. A server without Evidence retention
|
||||
rejects selected summary before analysis; complete delivery remains available.
|
||||
|
||||
### Member summary and retained member rows
|
||||
|
||||
Complete `inspect_managed_members` delivery always includes the assembly-wide
|
||||
`member_refs` and `exported_types`, which `type_names` does not narrow, so even
|
||||
one selected or unmatched type can return megabytes. Select
|
||||
`{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}`
|
||||
to keep the complete source in the session and receive identity, metadata
|
||||
status, the selection with its unmatched names, coverage/issues, the separate
|
||||
user-string coverage, limitations and per-collection counts. Then page original
|
||||
rows with view kind `managed-members`:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "<parent_evidence_id>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "managed-members",
|
||||
"collection": "call_edges",
|
||||
"offset": 0,
|
||||
"limit": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Collections are `types`, `exported_types`, `fields`, `methods`, `member_refs`,
|
||||
`call_edges`, `field_accesses` and `user_strings` (the literals). Each page names
|
||||
its collection, retains original rows, order, tokens, signatures and body facts,
|
||||
and carries the source selection and coverage; `user_strings` pages carry the
|
||||
literal pool's own coverage. Older Evidence that never examined exported types
|
||||
or user strings reports a null count and refuses those pages rather than
|
||||
returning an empty one.
|
||||
|
||||
## Shipped scope
|
||||
|
||||
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
|
||||
|
||||
+47
-2
@@ -193,7 +193,8 @@ after target content was read.
|
||||
## Retained application Evidence inputs
|
||||
|
||||
`analyze_javascript_application`, `inspect_binary_layout`, `inspect_artifact`,
|
||||
`inspect_wasm_artifact`, and `inspect_macho` accept `detail`:
|
||||
`inspect_wasm_artifact`, `inspect_macho`, and `decode_interface_builder`
|
||||
accept `detail`:
|
||||
`complete` (default) returns the complete analysis Evidence; `summary` records
|
||||
that complete Evidence in the session and returns the derived
|
||||
`inspect_analysis_view` summary Evidence instead. Its
|
||||
@@ -276,6 +277,17 @@ symbols (Electron Framework: 2.5 MB of imports and exports, 3.3 KB as a
|
||||
summary), so select `detail: "summary"` on the first call. Delivery detail
|
||||
never reaches the native tools or changes the source Evidence ID.
|
||||
|
||||
Completed `decode_interface_builder` Evidence supports a summary and
|
||||
`view: {kind: "interface-builder", collection: "nodes", offset: 0, limit: 64}`.
|
||||
Collections are `documents`, `nodes`, `edges`, and `coverage`; for nodes and
|
||||
edges an optional `document` keeps rows whose evidence names that exact decoded
|
||||
`relative_path`, and an undecoded path is refused. The summary keeps every
|
||||
decoded document, the provider, node-kind, edge-relation and coverage-status
|
||||
counts, and whether the decoded graph is incomplete. Each page repeats
|
||||
`source_truncated`. A complete app graph often exceeds the default MCP response
|
||||
budget (XLD.app: 6.7 MB), so select `detail: "summary"` on the first call.
|
||||
Delivery detail never reaches the decoder or changes the source Evidence ID.
|
||||
|
||||
`trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes` accept complete inline application Evidence
|
||||
@@ -671,7 +683,13 @@ advertised input schema within a 13,312-byte budget, measured to sit below
|
||||
Moonshot's observed cap (community measurements: a 13.7 KB schema is accepted
|
||||
while a ~15 KB one is rejected). The setting is read at startup; restart the
|
||||
server to apply changes, as with `REA_MCP_MAX_RESPONSE_BYTES`. The default
|
||||
value `full` keeps the complete advertisement unchanged.
|
||||
value `full` keeps the complete advertisement unchanged. Compact presentation
|
||||
also reduces schemas whose expanded JSON tree exceeds ten levels, the limit
|
||||
reported by strict-depth MCP clients in [#2813](https://github.com/morluto/rea/issues/2813).
|
||||
WorkBuddy/CodeBuddy gateways have also rejected full REA catalogs with an
|
||||
opaque `11133` error while accepting the compact profile
|
||||
([#2999](https://github.com/morluto/rea/issues/2999)); compatibility depends on
|
||||
the selected gateway and model.
|
||||
|
||||
The compact profile renders the same tool set, output schemas, annotations,
|
||||
and canonical server-side validation. It changes only the advertised input
|
||||
@@ -696,6 +714,9 @@ JSON Schema:
|
||||
canonical schema still rejects malformed calls server-side, so callers
|
||||
following the reduced advertisement receive the canonical typed error
|
||||
instead of silent acceptance.
|
||||
- Schemas deeper than ten nodes use the same reduced projection, with
|
||||
root-property names, types, guidance, and required fields retained. The
|
||||
server continues to enforce the full canonical input contract.
|
||||
|
||||
Boundary tests pin every advertised compact schema to the budget, keep them
|
||||
valid JSON Schema that still accepts every canonical example. Client verification
|
||||
@@ -711,6 +732,30 @@ identity. Explicit schema IDs and relative external reference bases are
|
||||
preserved. This keeps every tool and validation rule in discovery and does not
|
||||
change the SDK's catalog invalidation or availability checks.
|
||||
|
||||
Some stdio hosts reject a complete `tools/list` frame before they load any
|
||||
tools. The opt-in `REA_MCP_OUTPUT_SCHEMA_PROFILE=off` setting omits `outputSchema`
|
||||
from the advertised catalog for hosts with a small frame limit (the measured
|
||||
pi-web-ui failure and catalog breakdown are in [#2512](https://github.com/morluto/rea/issues/2512)).
|
||||
The default `full` profile is unchanged. With `off`, REA validates successful
|
||||
structured results against the same named canonical tool contract before
|
||||
delivery; it still returns complete `structuredContent` and text results.
|
||||
Only client-side schema discovery and validation are omitted, so hosts using
|
||||
this profile should rely on REA's validation and retain their usual response
|
||||
size limits.
|
||||
|
||||
### Function comparison limitations
|
||||
|
||||
`compare_functions` compares the `pseudocode` and `assembly` dimensions as
|
||||
exact provider text. A `changed` status establishes a textual difference, not a
|
||||
behavior change. Addresses and generated names are not normalized, so relocated
|
||||
labels can change these dimensions while `cfg` remains unchanged. Both text
|
||||
dimensions carry this limitation inline. Numeric constants remain part of the
|
||||
exact comparison. Reconstruction verification and static/runtime correlation
|
||||
retain limitations from the selected dimension; an overall function claim
|
||||
includes all dimension limitations. Equal CFGs establish only equal observed
|
||||
block sizes and successor relationships. Text from different provider
|
||||
identities remains unknown.
|
||||
|
||||
### Comparison input shapes
|
||||
|
||||
`compare_web_captures` accepts exactly one of two input shapes:
|
||||
|
||||
@@ -227,6 +227,10 @@ nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
Within one value trace, each exact procedure selector is attempted at most once,
|
||||
including failed analyses. Failures remain unknown observations and do not
|
||||
establish canonical function identity. A new trace can retry those selectors.
|
||||
|
||||
The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
|
||||
`block_membership` reports actual syntax-tree membership separately.
|
||||
|
||||
@@ -319,7 +323,16 @@ complete observation.
|
||||
digest are preserved; key interpretation follows the pinned plist decoder.
|
||||
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
|
||||
compiled storyboard and nib resources into objects, outlets, actions,
|
||||
connections and controller/class names. `verify:interface-builder` compiles
|
||||
connections and controller/class names. A full app graph is often several
|
||||
megabytes (XLD.app: 6.7 MB, about 2M tokens, which also exceeds the default
|
||||
MCP response budget), so MCP callers should pass `detail: "summary"`. The
|
||||
summary keeps every decoded document and node/edge/coverage counts and
|
||||
retains the complete Evidence; page rows with `inspect_analysis_view`
|
||||
`view: {kind: "interface-builder", collection: "nodes", document:
|
||||
"Contents/Resources/Base.lproj/MainMenu.nib", offset: 0, limit: 64}`.
|
||||
`document` selects rows whose evidence names that exact
|
||||
`documents[].relative_path` (nodes and edges only); `source_truncated` keeps
|
||||
graph incompleteness visible on every page. `verify:interface-builder` compiles
|
||||
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
|
||||
iOS platform; unsupported archive forms remain explicit.
|
||||
XML plist archives accept UTF-8 and BOM-marked UTF-16 in either byte order.
|
||||
@@ -523,6 +536,12 @@ unresolved runtime/value semantics remain visible in results.
|
||||
|
||||
## Interface Builder hierarchy coverage
|
||||
|
||||
A document's `hierarchy_complete` flag follows its own hierarchy coverage.
|
||||
If graph limits omit a hierarchy source or destination, or prevent a hierarchy
|
||||
edge or traversal from being retained, that document reports partial hierarchy
|
||||
coverage. Retained unresolved edges remain in the graph and do not add to the
|
||||
omitted-edge count. Coverage is evaluated separately for each document.
|
||||
|
||||
A keyed-archive hierarchy UID without an object-table entry marks the hierarchy
|
||||
as partial while preserving decoded objects and known links. UID 0 remains
|
||||
archived nil and does not count as a missing reference. Archive and evidence
|
||||
|
||||
+27
-8
@@ -159,14 +159,30 @@ line. Stdout stays the final Evidence document:
|
||||
`cleanup`. `completed` counts collected terminal frames, process samples, and
|
||||
interaction events. `total` stays null; REA does not invent a unit total or a
|
||||
percentage. `message` reports elapsed time and those counts. The final line uses
|
||||
`phase` `cleanup`, sets `terminal` to true, and adds `disposition` (`exited`,
|
||||
`timeout`, `idle_timeout`, `cancelled`, or `failed`) plus the owned-process,
|
||||
renderer, and temporary-root cleanup states. Updates use the shared progress
|
||||
boundary and are limited to one intermediate line per 100 ms; the terminal line
|
||||
is always emitted. If a receiver is slow, intermediate observations are merged
|
||||
into the latest status, and stale live observations are discarded before final
|
||||
cleanup status. Progress never copies child output. Child output remains in the
|
||||
capture Evidence on stdout.
|
||||
`phase` `cleanup` and sets `terminal` to true. Its `message` also contains
|
||||
`disposition=<value>` (`exited`, `timeout`, `idle_timeout`, `cancelled`, or `failed`)
|
||||
and the `owned_process_group`, `terminal_renderer`, and `temporary_root` cleanup
|
||||
states as `key=value` text. These values are not separate JSON fields. For
|
||||
example, a completed capture can end with:
|
||||
|
||||
```json
|
||||
{
|
||||
"rea_progress": {
|
||||
"phase": "cleanup",
|
||||
"completed": 0,
|
||||
"total": null,
|
||||
"message": "elapsed_ms=2180 frames=0 samples=0 interactions=0 disposition=exited owned_process_group=cleaned terminal_renderer=cleaned temporary_root=cleaned",
|
||||
"terminal": true,
|
||||
"sequence": 13
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Updates use the shared progress boundary and are limited to one intermediate line
|
||||
per 100 ms; the terminal line is always emitted. If a receiver is slow,
|
||||
intermediate observations are merged into the latest status, and stale live
|
||||
observations are discarded before final cleanup status. Progress never copies
|
||||
child output. Child output remains in the capture Evidence on stdout.
|
||||
|
||||
### Scenario with interactions
|
||||
|
||||
@@ -380,6 +396,9 @@ Every capture requires `truncation_details`, with separate accounting for:
|
||||
When a directory exceeds that capacity, the retained subset follows filesystem
|
||||
enumeration order and is then sorted; it is explicitly incomplete and cannot
|
||||
establish that an omitted path is absent.
|
||||
An unreadable directory is recorded in `enumeration_omissions` with its
|
||||
root-aliased path and operating-system error code. Entries observed before
|
||||
that directory remain available, while descendants beneath it stay unknown.
|
||||
Each retained regular file without a digest has an aliased path, size,
|
||||
remaining budget and reason: `file_bytes_budget`,
|
||||
`file_changed_or_short_read`, or `file_unavailable`. Its `system_code` is
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> المصادر الرسمية لـ REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> استخدم روابط التثبيت والإصدارات المنشورة في هذه المصادر الرسمية. تشابه اسم المستودع أو نسخ ملف README لا يثبت أن التنزيل رسمي.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف ثنائي للشيفرة الأصلية" width="1200" />
|
||||
@@ -137,13 +141,13 @@ npx rea-agents@latest setup
|
||||
|
||||
| الهدف | ما يعيده REA | المتطلبات والدليل |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](../native-investigation.md) |
|
||||
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
|
||||
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-application-workflows.md) |
|
||||
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](../browser-observation.md) |
|
||||
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](../web-network-captures.md) |
|
||||
| التقاط الحزم | نقاط النهاية وDNS وTLS SNI وتوزيع البروتوكولات وطلبات HTTP | tshark وcapinfos يوفّرهما المُستدعي؛ [دليل pcap](../pcap-analysis.md) |
|
||||
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](../managed-code-analysis.md) |
|
||||
@@ -270,7 +274,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## سجل النجوم
|
||||
|
||||
🎉 **170,000 نجمة على GitHub — شكرًا لكم!**
|
||||
🎉 **200,000 نجمة على GitHub — شكرًا لكم!**
|
||||
|
||||
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويقترح ميزات، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Offizielle Quellen für REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Folge den Installations- und Release-Links aus diesen offiziellen Quellen. Ein ähnlicher Repository-Name oder eine kopierte README belegt nicht, dass ein Download offiziell ist.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA startet seine Analysebrücke in Hopper und untersucht eine native Binärdatei" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
|
||||
|
||||
| Ziel | Was REA zurückgibt | Voraussetzungen und Anleitung |
|
||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
|
||||
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](../native-investigation.md) |
|
||||
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
|
||||
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
|
||||
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-application-workflows.md) |
|
||||
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](../browser-observation.md) |
|
||||
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](../web-network-captures.md) |
|
||||
| Paketerfassungen | Endpunkte, DNS, TLS SNI, Protokollverteilung und HTTP-Anfragen | Vom Aufrufer bereitgestellte tshark- und capinfos-Binaries; [pcap-Leitfaden](../pcap-analysis.md) |
|
||||
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ Melde Sicherheitslücken gemäß [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Sternverlauf
|
||||
|
||||
🎉 **170.000 GitHub-Sterne – vielen Dank!**
|
||||
🎉 **200.000 GitHub-Sterne – vielen Dank!**
|
||||
|
||||
Danke an alle, die REA nutzen, Fehler melden, Funktionswünsche äußern, Builds testen und Fehlerbehebungen beitragen.
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Κατέβασε το REA από το [morluto/rea](https://github.com/morluto/rea),
|
||||
> το [rea.tools](https://rea.tools/) ή το [πακέτο rea-agents στο npm](https://www.npmjs.com/package/rea-agents).
|
||||
@@ -30,6 +32,8 @@
|
||||
> Ένα παρόμοιο όνομα αποθετηρίου ή ένα αντιγραμμένο README δεν αποδεικνύει ότι
|
||||
> ένα αρχείο προς λήψη προέρχεται από επίσημη πηγή.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="Το REA ξεκινά τη γέφυρα ανάλυσής του μέσα στο Hopper κατά την εξέταση ενός δυαδικού αρχείου με κώδικα μηχανής" width="1200" />
|
||||
@@ -141,7 +145,7 @@ npx rea-agents@latest setup
|
||||
|
||||
| Στόχος | Τι επιστρέφει το REA | Απαιτήσεις και οδηγός |
|
||||
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Δυαδικά αρχεία με κώδικα μηχανής | Ψευδοκώδικα, κώδικα assembly, συμβολοσειρές, σύμβολα, κλήσεις και αναφορές | Hopper, Ghidra ή IDA· [ανάλυση κώδικα μηχανής](https://rea.tools/guides/native/) |
|
||||
| Δυαδικά αρχεία με κώδικα μηχανής | Ψευδοκώδικα, κώδικα assembly, συμβολοσειρές, σύμβολα, κλήσεις και αναφορές | Hopper, Ghidra ή IDA· [ανάλυση κώδικα μηχανής](../native-investigation.md) |
|
||||
| Δομή ELF χωρίς εκτέλεση | Ενότητες, τμήματα, αρχικά σύμβολα/εγγραφές μετεγκατάστασης και ενδείξεις πιθανών μέτρων προστασίας από στατική ανάλυση | pwntools που παρέχει ο χρήστης σε Linux x64· [διαγνωστικά δυαδικών αρχείων](../binary-diagnostics.md) |
|
||||
| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
|
||||
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
|
||||
@@ -149,8 +153,8 @@ npx rea-agents@latest setup
|
||||
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
|
||||
| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
|
||||
| Καταγεγραμμένες καταρρεύσεις στο Linux | Ακατέργαστες εγγραφές notes, καταχωρητές/σήματα κάθε καταγεγραμμένου νήματος και προαιρετικές πιθανές αντιστοιχίσεις μνήμης | pwntools που παρέχει ο χρήστης σε Linux x64· προαιρετικά GDB/pwndbg· [καταγεγραμμένες καταρρεύσεις](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, εισαγωγές, source maps, διαδρομές, IPC και σχέσεις με πρόσθετα κώδικα μηχανής | Node.js και npm· [ανάλυση εφαρμογών](https://rea.tools/guides/javascript/) |
|
||||
| Ιστότοποι | Δομή σελίδων, scripts, παρατηρήσεις δικτύου και ζητούμενα στιγμιότυπα οθόνης | Πρόγραμμα περιήγησης της οικογένειας Chrome· [ανάλυση μέσω προγράμματος περιήγησης](https://rea.tools/guides/browser/) |
|
||||
| JavaScript / Electron | Modules, εισαγωγές, source maps, διαδρομές, IPC και σχέσεις με πρόσθετα κώδικα μηχανής | Node.js και npm· [ανάλυση εφαρμογών](../javascript-application-workflows.md) |
|
||||
| Ιστότοποι | Δομή σελίδων, scripts, παρατηρήσεις δικτύου και ζητούμενα στιγμιότυπα οθόνης | Πρόγραμμα περιήγησης της οικογένειας Chrome· [ανάλυση μέσω προγράμματος περιήγησης](../browser-observation.md) |
|
||||
| Αποθηκευμένες καταγραφές δικτύου | Αιτήματα, αποκρίσεις, διαθέσιμα δεδομένα φορτίου και θέσεις στην πηγή | HAR ή SAZ του Fiddler Everywhere· mitmdump σε Linux για την εγγενή μορφή mitmproxy· [οδηγός καταγραφών](../web-network-captures.md) |
|
||||
| Assemblies .NET | Μεταδεδομένα, εντολές CIL, δηλωμένες εξαρτήσεις από κώδικα μηχανής και συγκρίσεις builds | Καμία πρόσθετη απαίτηση (μόνο στατική ανάλυση)· [οδηγός διαχειριζόμενου κώδικα](../managed-code-analysis.md) |
|
||||
| APK Android | Δηλώσεις manifest, κλάσεις, απομεταγλωττισμένες μέθοδοι και αναφορές | JADX χωρίς γραφικό περιβάλλον και πλήρες JDK σε Linux/macOS/Windows x64· [οδηγός Android](../android-analysis.md) |
|
||||
@@ -281,7 +285,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## Ιστορικό αστεριών
|
||||
|
||||
🎉 **180.000 αστέρια στο GitHub — σας ευχαριστούμε!**
|
||||
🎉 **200.000 αστέρια στο GitHub — σας ευχαριστούμε!**
|
||||
|
||||
Ευχαριστούμε όλους όσοι χρησιμοποιούν το REA, αναφέρουν σφάλματα, ζητούν νέες λειτουργίες, δοκιμάζουν builds και συνεισφέρουν διορθώσεις.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Fuentes oficiales de REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Usa los enlaces de instalación y de versiones publicados en estas fuentes oficiales. Un nombre de repositorio parecido o un README copiado no demuestra que una descarga sea oficial.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA inicia su puente de análisis dentro de Hopper mientras inspecciona un binario nativo" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
|
||||
|
||||
| Objetivo | Qué devuelve REA | Requisitos y guía |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
|
||||
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](../native-investigation.md) |
|
||||
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
|
||||
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
|
||||
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](../javascript-application-workflows.md) |
|
||||
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](../browser-observation.md) |
|
||||
| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](../web-network-captures.md) |
|
||||
| Capturas de paquetes | Endpoints, DNS, TLS SNI, distribución de protocolos y solicitudes HTTP | tshark y capinfos suministrados por el autor de la llamada; [guía pcap](../pcap-analysis.md) |
|
||||
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ Informa de vulnerabilidades siguiendo [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Historial de estrellas
|
||||
|
||||
🎉 **170.000 estrellas en GitHub: ¡gracias!**
|
||||
🎉 **200.000 estrellas en GitHub: ¡gracias!**
|
||||
|
||||
Gracias a quienes usan REA, informan de errores, solicitan funciones, prueban compilaciones y contribuyen con correcciones.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> منابع رسمی REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> از پیوندهای نصب و انتشار در این منابع رسمی استفاده کنید. نام مشابه مخزن یا README کپیشده ثابت نمیکند که فایل دانلودی رسمی است.
|
||||
|
||||
<div align="center" dir="rtl">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="اجرای پل تحلیل REA در Hopper هنگام بررسی یک فایل باینری بومی" width="1200" />
|
||||
@@ -129,13 +133,13 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه np
|
||||
|
||||
| هدف | خروجی REA | پیشنیازها و راهنما |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](../native-investigation.md) |
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
|
||||
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](../javascript-application-workflows.md) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](../browser-observation.md) |
|
||||
| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](../web-network-captures.md) |
|
||||
| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](../managed-code-analysis.md) |
|
||||
| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](../android-analysis.md) |
|
||||
@@ -253,7 +257,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## تاریخچهٔ ستارهها
|
||||
|
||||
🎉 **۱۷۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
|
||||
🎉 **۲۰۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
|
||||
|
||||
از همهٔ کسانی که از REA استفاده میکنند، خطاها را گزارش میدهند، قابلیتهای جدید پیشنهاد میکنند، buildها را آزمایش میکنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Sources officielles de REA : [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Utilisez les liens d’installation et de publication de ces sources officielles. Un nom de dépôt similaire ou un README copié ne prouve pas qu’un téléchargement est officiel.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA lance son pont d'analyse dans Hopper pendant l'inspection d'un binaire natif" width="1200" />
|
||||
@@ -140,14 +144,14 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
|
||||
|
||||
| Cible | Ce que REA renvoie | Prérequis et guide |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/fr/guides/native/) |
|
||||
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](../native-investigation.md) |
|
||||
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) |
|
||||
| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
|
||||
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](../javascript-application-workflows.md) |
|
||||
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](../browser-observation.md) |
|
||||
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](../web-network-captures.md) |
|
||||
| Captures de paquets | Points de terminaison, DNS, TLS SNI, distribution des protocoles et requêtes HTTP | tshark et capinfos fournis par l'appelant ; [guide pcap](../pcap-analysis.md) |
|
||||
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](../managed-code-analysis.md) |
|
||||
@@ -272,7 +276,7 @@ Signalez les vulnérabilités via [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Historique des étoiles
|
||||
|
||||
🎉 **170 000 étoiles GitHub — merci !**
|
||||
🎉 **200 000 étoiles GitHub — merci !**
|
||||
|
||||
Merci à toutes les personnes qui utilisent REA, signalent des bugs, proposent des fonctionnalités, testent les builds et contribuent des correctifs.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Sumber resmi REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Gunakan tautan instalasi dan rilis dari sumber resmi ini. Nama repositori yang mirip atau README yang disalin tidak membuktikan bahwa unduhan tersebut resmi.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA menjalankan jembatan analisisnya di dalam Hopper saat memeriksa biner native" width="1200" />
|
||||
@@ -143,14 +147,14 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
|
||||
|
||||
| Target | Hasil yang diberikan REA | Persyaratan dan panduan |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](https://rea.tools/id/guides/native/) |
|
||||
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](../native-investigation.md) |
|
||||
| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) |
|
||||
| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
|
||||
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
|
||||
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](../javascript-application-workflows.md) |
|
||||
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](../browser-observation.md) |
|
||||
| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](../web-network-captures.md) |
|
||||
| Capturan paket | Endpoint, DNS, TLS SNI, distribusi protokol, dan permintaan HTTP | tshark dan capinfos buatan pemanggil; [panduan pcap](../pcap-analysis.md) |
|
||||
| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](../managed-code-analysis.md) |
|
||||
@@ -293,7 +297,7 @@ Laporkan kerentanan melalui [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Riwayat Bintang
|
||||
|
||||
🎉 **170.000 bintang GitHub, terima kasih!**
|
||||
🎉 **200.000 bintang GitHub, terima kasih!**
|
||||
|
||||
Terima kasih kepada semua orang yang menggunakan REA, melaporkan bug, meminta fitur, menguji build, dan berkontribusi dalam perbaikan.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Fonti ufficiali di REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Segui i link di installazione e delle versioni pubblicate da queste fonti ufficiali. Un nome di repository simile o un README copiato non dimostra che un download sia ufficiale.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA che avvia il suo bridge di analisi dentro Hopper mentre ispeziona un binario nativo" width="1200" />
|
||||
@@ -140,14 +144,14 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
|
||||
|
||||
| Obiettivo | Cosa restituisce REA | Requisiti e guida |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/it/guides/native/) |
|
||||
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](../native-investigation.md) |
|
||||
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) |
|
||||
| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
|
||||
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
|
||||
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](../javascript-application-workflows.md) |
|
||||
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](../browser-observation.md) |
|
||||
| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](../web-network-captures.md) |
|
||||
| Captured di pacchetti | Endpoint, DNS, TLS SNI, distribuzione dei protocolli e richieste HTTP | tshark e capinfos forniti dal chiamante; [guida pcap](../pcap-analysis.md) |
|
||||
| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](../managed-code-analysis.md) |
|
||||
@@ -272,7 +276,7 @@ Segnala vulnerabilità tramite [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Cronologia delle stelle
|
||||
|
||||
🎉 **170.000 stelle su GitHub — grazie!**
|
||||
🎉 **200.000 stelle su GitHub — grazie!**
|
||||
|
||||
Grazie a tutti coloro che usano REA, segnalano bug, propongono funzionalità, testano le build e contribuiscono con correzioni.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> REA の公式配布元: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> インストールとリリースのリンクは、これらの公式配布元からたどってください。似たリポジトリ名やコピーされた README だけでは、公式の配布物とは確認できません。
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA が Hopper 内で分析ブリッジを起動し、ネイティブバイナリを調べる様子" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
|
||||
|
||||
| 対象 | REA が返すもの | 要件とガイド |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](../native-investigation.md) |
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
|
||||
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
|
||||
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](../javascript-application-workflows.md) |
|
||||
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](../browser-observation.md) |
|
||||
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](../web-network-captures.md) |
|
||||
| パケットキャプチャ | エンドポイント、DNS、TLS SNI、プロトコル分布、HTTPリクエスト | 呼び出し側が用意した tshark と capinfos;[pcapガイド](../pcap-analysis.md) |
|
||||
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](../managed-code-analysis.md) |
|
||||
@@ -270,7 +274,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## スター履歴
|
||||
|
||||
🎉 **GitHub スター 170,000 件、ありがとうございます!**
|
||||
🎉 **GitHub スター 200,000 件、ありがとうございます!**
|
||||
|
||||
REA の利用、バグ報告、機能の要望、ビルドのテスト、修正への貢献に感謝します。
|
||||
|
||||
|
||||
+30
-29
@@ -20,14 +20,18 @@
|
||||
|
||||
**[웹사이트](https://rea.tools/ko/) · [가이드](https://rea.tools/ko/guides/) · [사례](https://rea.tools/ko/showcase/)**
|
||||
|
||||
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [GNU objdump 및 Rizin 도구](../reverse-engineering-tools.md) · [문서](#문서)
|
||||
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [문서](#문서)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> REA 공식 배포처: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> 이 공식 배포처에서 제공하는 설치 및 릴리스 링크를 이용하세요. 비슷한 저장소 이름이나 복사된 README만으로는 공식 배포 파일인지 확인할 수 없습니다.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA가 Hopper 안에서 분석 브리지를 실행해 네이티브 바이너리를 검사하는 모습" width="1200" />
|
||||
@@ -56,7 +60,7 @@
|
||||
|
||||
REA는 에이전트를 분석 도구에 연결해 네이티브 바이너리, JavaScript와 Electron 앱, .NET 어셈블리, 웹사이트를 검사할 수 있게 합니다. 터미널에서도 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며, 결과에는 각 결론의 근거와 한계가 포함됩니다.
|
||||
|
||||
설정 과정은 에이전트에 REA를 등록하고 해당 버전에 맞는 워크플로 지침을 설치합니다. 네이티브 분석에는 기존 Hopper 또는 Ghidra 설치를 사용할 수 있습니다. 승인하면 설정 과정에서 Hopper를 설치할 수도 있습니다. 정적 JavaScript 분석에는 두 엔진 모두 필요하지 않습니다.
|
||||
설정 과정은 에이전트에 REA를 등록하고 해당 버전에 맞는 워크플로 지침을 설치합니다. 네이티브 분석에는 기존 Hopper, Ghidra 또는 IDA 설치를 사용할 수 있습니다. 승인하면 설정 과정에서 Hopper를 설치할 수도 있습니다. 정적 JavaScript 분석에는 네이티브 분석 엔진이 필요하지 않습니다.
|
||||
|
||||
> **[REA 웹사이트](https://rea.tools/ko/)**에서 설정 방법, 그림으로 설명한 가이드, 실제 사례를 확인하세요.
|
||||
|
||||
@@ -105,12 +109,14 @@ rea --help
|
||||
|
||||
REA는 빠르게 바뀌며 새 릴리스에는 버그 수정이 자주 포함됩니다. 설치된 버전을 최신으로 유지하세요.
|
||||
|
||||
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
`npm install --global` 또는 `bun add --global`로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Bun 전역 업데이트에는 Bun 1.2.17 이상이 필요합니다. 이전 버전의 Bun을 사용한다면 `rea update`를 실행하기 전에 Bun을 직접 업그레이드하세요.
|
||||
|
||||
에이전트 등록과 스킬을 갱신하려면 업데이트 과정에서 출력된 설정 명령을 실행하세요.
|
||||
|
||||
`npx`를 사용한다면 다음 명령으로 에이전트 설정을 갱신하세요.
|
||||
@@ -119,7 +125,7 @@ rea update
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
설정 변경 사항을 검토하고 에이전트를 다시 시작하세요. 일회성 CLI 작업에는 `npx rea-agents@latest` 뒤에 필요한 명령을 붙여 실행하세요.
|
||||
설정 변경 사항을 검토하고 에이전트를 다시 시작하세요. 일회성 CLI 작업에는 `npx rea-agents@latest` 뒤에 필요한 명령을 붙여 실행하세요. Bun을 사용한다면 위 명령의 `npx rea-agents@latest`를 `bunx rea-agents@latest`로 바꿔 실행하세요.
|
||||
|
||||
## REA의 작동 방식
|
||||
|
||||
@@ -137,16 +143,18 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
|
||||
| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
|
||||
| --------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](../native-investigation.md) |
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
|
||||
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
|
||||
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
|
||||
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
|
||||
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
|
||||
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
|
||||
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
|
||||
| 패킷 캡처 | 엔드포인트, DNS, TLS SNI, 프로토콜 분포, HTTP 요청 | 호출자 제공 tshark 및 capinfos; [pcap 가이드](../pcap-analysis.md) |
|
||||
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](../managed-code-analysis.md) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](../javascript-application-workflows.md) |
|
||||
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](../browser-observation.md) |
|
||||
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR 또는 Fiddler Everywhere SAZ; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
|
||||
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 별도 도구 불필요(정적 검사만); [관리 코드 가이드](../managed-code-analysis.md) |
|
||||
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](../android-analysis.md) |
|
||||
| Android 기기 | 기기, 패키지, 프로세스, 로그, 덤프, 화면 캡처, APK/파일 전송 | 호출자 제공 adb(에뮬레이터 또는 실제 기기); [ADB 가이드](../adb-device-analysis.md) |
|
||||
| JEB 프로젝트 | 엔진 기반 프로젝트 유닛, 아티팩트 해시, 타입/메서드 의사코드 | 호출자가 시작하고 MCP를 제공하는 JEB 클라이언트(REA_JEB_MCP_URL로 선택); [JEB 가이드](../jeb-analysis.md) |
|
||||
@@ -154,15 +162,18 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
| Flutter 앱 | Dart 스냅샷 해시, ABI별 라이브러리 해시, 엔진 빌드 출처 | 없음 (정적 APK 파싱); [Flutter 가이드](../flutter-analysis.md) |
|
||||
| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](../firmware-analysis.md) |
|
||||
| 기록된 패킷 | 선택 필드, 반복 값, 필터 및 디코딩 규칙 | Linux: TShark 4.x + prlimit; [캡처 가이드](../packet-captures.md) |
|
||||
| 패킷 요약 | 엔드포인트, DNS, TLS SNI, 프로토콜 분포, HTTP 요청 | 호출자 제공 tshark 및 capinfos(Linux에서 검증됨); [pcap 가이드](../pcap-analysis.md) |
|
||||
| 기록된 논리 신호 | 디코더 주석, 샘플 범위, 진단 정보 | Linux: sigrok-cli 0.7.2 + prlimit; [신호 기록 가이드](../signal-recordings.md) |
|
||||
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](../javascript-artifact-reconstruction.md), [Apple 애플리케이션](../apple-application-analysis.md) |
|
||||
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](../process-capture.md) |
|
||||
|
||||
정적 JavaScript 및 .NET 검사는 제공된 파일을 읽으며 애플리케이션을 실행하지 않습니다. 런타임 캡처는 사용자의 권한으로 선택한 대상을 실행하거나 대상과 상호작용합니다. 각 런타임 가이드에서 그 영향을 설명합니다.
|
||||
|
||||
오프라인 네이티브 바이너리 검사에는 호출자가 제공하는 GNU objdump와 Rizin도 사용할 수 있습니다. 제공자 설정, CLI 명령, Evidence의 한계는 [리버스 엔지니어링 도구 가이드](../reverse-engineering-tools.md)를 참고하세요. REA는 objdump와 Rizin을 설치하거나 업데이트하지 않습니다.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](../installation.md#hopper), [IDA 가이드](../ida-provider.md), [실험적 Windows Ghidra 지원](../windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](../ghidra-dos.md)도 지원합니다. 제공자 선택은 [CLI 가이드](../cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](../installation.md#released-package-and-main)를 확인하세요.
|
||||
지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](../installation.md#hopper), [IDA 가이드](../ida-provider.md)(선택 사항인 네이티브 MCP 브리지 포함), [실험적 Windows Ghidra 지원](../windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](../ghidra-dos.md)도 지원합니다. 큰 바이너리를 분석할 때는 `REA_GHIDRA_STARTUP_TIMEOUT_MS` 또는 `REA_HOPPER_STARTUP_TIMEOUT_MS`로 제공자의 시작 제한 시간을 늘리세요. 제공자 선택은 [CLI 가이드](../cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](../installation.md#released-package-and-main)를 확인하세요.
|
||||
|
||||
## 사례
|
||||
|
||||
@@ -209,7 +220,7 @@ REA로 흥미로운 대상을 분석했다면 알려 주세요. [issue](https://
|
||||
<details>
|
||||
<summary><strong>Hopper를 먼저 실행해야 하나요?</strong></summary>
|
||||
|
||||
REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. [Hopper 실행 및 문제 해결](../installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
|
||||
REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. 첫 CLI/MCP 작업이나 무인 작업을 실행하기 전에 같은 사용자 세션에서 이 대화 상자를 완료하세요. 필요하면 Hopper를 직접 여세요. [Hopper 실행 및 문제 해결](../installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -239,7 +250,7 @@ REA는 대상을 로컬에서 분석합니다. 에이전트는 도구 결과를
|
||||
|
||||
먼저 업데이트하세요. 최근 릴리스에서 이미 수정되었을 수 있습니다.
|
||||
|
||||
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
`npm install --global` 또는 `bun add --global`로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
|
||||
```bash
|
||||
rea update
|
||||
@@ -270,7 +281,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## 스타 기록
|
||||
|
||||
🎉 **GitHub 스타 170,000개, 감사합니다!**
|
||||
🎉 **GitHub 스타 200,000개, 감사합니다!**
|
||||
|
||||
REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
|
||||
|
||||
@@ -286,11 +297,9 @@ REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를
|
||||
|
||||
책임 있는 사용에 관한 안내는 [DISCLAIMER.md](../../DISCLAIMER.md)를 참고하세요.
|
||||
|
||||
REA는 법을 준수하는 연구, 교육, 상호운용성 확보, 소프트웨어 보존, 그리고 본인이 소유하거나 조사 권한을 가진 소프트웨어의 분석을 위한 도구입니다.
|
||||
불법적이거나 허가되지 않은 사용에 대한 프로젝트의 입장은 모든 REA 버전, 배포 채널, 플랫폼에 적용되며, 각 사본에는 해당 라이선스가 그대로 유지됩니다.
|
||||
|
||||
The project's position on illegal or unauthorized use covers all REA
|
||||
versions, distribution channels, and platforms; each copy retains its
|
||||
applicable licenses.
|
||||
REA는 법을 준수하는 연구, 교육, 상호운용성 확보, 소프트웨어 보존, 그리고 본인이 소유하거나 조사 권한을 가진 소프트웨어의 분석을 위한 도구입니다.
|
||||
|
||||
## 기여
|
||||
|
||||
@@ -302,14 +311,6 @@ REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/
|
||||
|
||||
[MIT](../../LICENSE)
|
||||
|
||||
REA-authored code is licensed under MIT. Third-party material retains its own
|
||||
licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
|
||||
for the dependency inventory, preserved license texts, and external-tool terms.
|
||||
REA가 작성한 코드는 MIT 라이선스를 따릅니다. 제3자 자료에는 각자의 라이선스와 저작권 고지가 그대로 유지됩니다. 의존성 목록, 보존된 라이선스 원문, 외부 도구 약관은 [제3자 고지](../../THIRD_PARTY_NOTICES.md)를 참고하세요.
|
||||
|
||||
**You must legally obtain and be authorized to use commercial tools**, including
|
||||
IDA Pro / Hex-Rays decompilers, Hopper, and JEB, under their applicable licenses.
|
||||
REA grants no tool license, key, or activation entitlement. Trials and demos
|
||||
remain subject to vendor terms, including user, feature, automation, and output
|
||||
sharing restrictions. Cracked copies, stolen keys, unauthorized key sharing,
|
||||
and bypassing license restrictions are not authorized. This guidance does not
|
||||
amend MIT or third-party licenses.
|
||||
IDA Pro / Hex-Rays 디컴파일러, Hopper, JEB 등 상용 도구는 해당 라이선스에 따라 **합법적으로 취득하고 사용 권한을 갖춰야 합니다.** REA는 어떤 도구의 라이선스, 키, 활성화 권한도 부여하지 않습니다. 체험판과 데모는 사용자, 기능, 자동화, 출력 공유 제한을 포함한 공급업체 약관을 따릅니다. 크랙 버전, 도난 키, 무단 키 공유, 라이선스 제한 우회는 허용되지 않습니다. 이 안내는 MIT 라이선스나 제3자 라이선스를 변경하지 않습니다.
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Oficjalne źródła REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Korzystaj z odnośników do instalacji i wydań w tych oficjalnych źródłach. Podobna nazwa repozytorium lub skopiowany README nie potwierdzają, że pobierany plik jest oficjalny.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA uruchamia most analityczny w Hopperze podczas analizy natywnego pliku binarnego" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
|
||||
|
||||
| Cel | Co zwraca REA | Wymagania i przewodnik |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
|
||||
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](../native-investigation.md) |
|
||||
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
|
||||
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
|
||||
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](../javascript-application-workflows.md) |
|
||||
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](../browser-observation.md) |
|
||||
| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](../web-network-captures.md) |
|
||||
| Przechwytywanie pakietów | Punkty końcowe, DNS, TLS SNI, dystrybucja protokołów i żądania HTTP | Dostarczone przez wywołującego tshark i capinfos; [przewodnik pcap](../pcap-analysis.md) |
|
||||
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ Zgłaszaj podatności zgodnie z [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Historia gwiazdek
|
||||
|
||||
🎉 **170 000 gwiazdek na GitHubie — dziękujemy!**
|
||||
🎉 **200 000 gwiazdek na GitHubie — dziękujemy!**
|
||||
|
||||
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, proponują nowe funkcje, testują kompilacje i przesyłają poprawki.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Fontes oficiais do REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Use os links de instalação e de versões dessas fontes oficiais. Um nome de repositório parecido ou um README copiado não comprova que um download seja oficial.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA inicia sua ponte de análise dentro do Hopper enquanto inspeciona um binário nativo" width="1200" />
|
||||
@@ -137,13 +141,13 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
|
||||
|
||||
| Alvo | O que o REA retorna | Requisitos e guia |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
|
||||
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](../native-investigation.md) |
|
||||
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
|
||||
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
|
||||
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](../javascript-application-workflows.md) |
|
||||
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](../browser-observation.md) |
|
||||
| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](../web-network-captures.md) |
|
||||
| Capturas de pacotes | Endpoints, DNS, TLS SNI, distribuição de protocolos e requisições HTTP | tshark e capinfos fornecidos pelo chamador; [guia pcap](../pcap-analysis.md) |
|
||||
| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ Relate vulnerabilidades conforme [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Histórico de estrelas
|
||||
|
||||
🎉 **170.000 estrelas no GitHub — muito obrigado!**
|
||||
🎉 **200.000 estrelas no GitHub — muito obrigado!**
|
||||
|
||||
Obrigado a todos que usam o REA, relatam bugs, sugerem funcionalidades, testam builds e contribuem com correções.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Официальные источники REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Используйте ссылки на установку и выпуски из этих официальных источников. Похожее название репозитория или скопированный README не подтверждают, что скачиваемый файл является официальным.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA запускает мост анализа внутри Hopper при исследовании нативного бинарного файла" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
|
||||
|
||||
| Цель | Что возвращает REA | Требования и руководство |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
|
||||
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](../native-investigation.md) |
|
||||
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
|
||||
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
|
||||
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](../browser-observation.md) |
|
||||
| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](../web-network-captures.md) |
|
||||
| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](../managed-code-analysis.md) |
|
||||
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](../android-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## История звёзд
|
||||
|
||||
🎉 **170 000 звёзд на GitHub — спасибо!**
|
||||
🎉 **200 000 звёзд на GitHub — спасибо!**
|
||||
|
||||
Спасибо всем, кто использует REA, сообщает об ошибках, предлагает новые функции, тестирует сборки и помогает с исправлениями.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> แหล่งทางการของ REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> โปรดใช้ลิงก์ติดตั้งและรุ่นเผยแพร่จากแหล่งทางการเหล่านี้ ชื่อที่เก็บโค้ดที่คล้ายกันหรือ README ที่คัดลอกมาไม่ได้ยืนยันว่าไฟล์ดาวน์โหลดเป็นของทางการ
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA เริ่มบริดจ์การวิเคราะห์ภายใน Hopper ขณะตรวจสอบไบนารีเนทีฟ" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
|
||||
|
||||
| เป้าหมาย | สิ่งที่ REA ส่งกลับ | ข้อกำหนดและคู่มือ |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](../native-investigation.md) |
|
||||
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
|
||||
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-application-workflows.md) |
|
||||
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](../browser-observation.md) |
|
||||
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](../web-network-captures.md) |
|
||||
| การจับแพ็กเก็ต | จุดเชื่อมต่อ, DNS, TLS SNI, การกระจายโปรโตคอล และคำขอ HTTP | tshark และ capinfos ที่ผู้เรียกจัดเตรียม; [คู่มือ pcap](../pcap-analysis.md) |
|
||||
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## ประวัติดาว
|
||||
|
||||
🎉 **170,000 ดาวบน GitHub — ขอบคุณทุกคน!**
|
||||
🎉 **200,000 ดาวบน GitHub — ขอบคุณทุกคน!**
|
||||
|
||||
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก เสนอฟีเจอร์ ทดสอบบิลด์ และช่วยแก้ไข
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> REA’nın resmî kaynakları: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Kurulum ve sürüm bağlantılarını bu resmî kaynaklardan takip edin. Benzer bir depo adı veya kopyalanmış bir README, indirilen dosyanın resmî olduğunu kanıtlamaz.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA, yerel bir ikili dosyayı incelerken Hopper içinde analiz köprüsünü başlatıyor" width="1200" />
|
||||
@@ -139,7 +143,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
|
||||
|
||||
| Hedef | REA'nın döndürdükleri | Gereksinimler ve rehber |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
|
||||
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](../native-investigation.md) |
|
||||
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
|
||||
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
|
||||
| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) |
|
||||
@@ -147,8 +151,8 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
|
||||
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
|
||||
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/tr/guides/javascript/) |
|
||||
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/tr/guides/browser/) |
|
||||
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](../javascript-application-workflows.md) |
|
||||
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](../browser-observation.md) |
|
||||
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR veya Fiddler Everywhere SAZ; Linux'ta native mitmproxy için mitmdump; [ağ yakalama rehberi](../web-network-captures.md) |
|
||||
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](../managed-code-analysis.md) |
|
||||
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](../android-analysis.md) |
|
||||
@@ -273,7 +277,7 @@ Güvenlik açıklarını [SECURITY.md](../../SECURITY.md) üzerinden bildirin.
|
||||
|
||||
## Yıldız geçmişi
|
||||
|
||||
🎉 **GitHub'da 170.000 yıldız — teşekkürler!**
|
||||
🎉 **GitHub'da 200.000 yıldız — teşekkürler!**
|
||||
|
||||
REA'yı kullanan, hata bildiren, özellik talep eden, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Офіційні джерела REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Користуйтеся посиланнями на встановлення та випуски з цих офіційних джерел. Схожа назва репозиторію або скопійований README не підтверджують, що завантажуваний файл є офіційним.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA запускає міст аналізу всередині Hopper під час дослідження нативного бінарного файла" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
|
||||
|
||||
| Ціль | Що повертає REA | Вимоги й посібник |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](../native-investigation.md) |
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-application-workflows.md) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](../browser-observation.md) |
|
||||
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](../web-network-captures.md) |
|
||||
| Захоплення пакетів | Кінцеві точки, DNS, TLS SNI, розподіл протоколів та HTTP-запити | Надані викликаючою стороною tshark та capinfos; [підручник pcap](../pcap-analysis.md) |
|
||||
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## Історія зірок
|
||||
|
||||
🎉 **170 000 зірок на GitHub — дякуємо!**
|
||||
🎉 **200 000 зірок на GitHub — дякуємо!**
|
||||
|
||||
Дякуємо всім, хто використовує REA, повідомляє про помилки, пропонує нові функції, тестує збірки й допомагає з виправленнями.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Các nguồn chính thức của REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> Hãy dùng các liên kết cài đặt và bản phát hành từ những nguồn chính thức này. Tên kho mã tương tự hoặc README được sao chép không chứng minh rằng tệp tải xuống là bản chính thức.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA khởi chạy cầu nối phân tích bên trong Hopper khi kiểm tra tệp nhị phân mã máy" width="1200" />
|
||||
@@ -137,13 +141,13 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
|
||||
|
||||
| Mục tiêu | REA trả về gì | Yêu cầu và hướng dẫn |
|
||||
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
|
||||
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](../native-investigation.md) |
|
||||
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
|
||||
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](../javascript-application-workflows.md) |
|
||||
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](../browser-observation.md) |
|
||||
| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](../web-network-captures.md) |
|
||||
| Bắt gói tin | Điểm cuối, DNS, TLS SNI, phân bố giao thức và yêu cầu HTTP | tshark và capinfos do nơi gọi cung cấp; [hướng dẫn pcap](../pcap-analysis.md) |
|
||||
| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](../managed-code-analysis.md) |
|
||||
@@ -268,7 +272,7 @@ Báo cáo lỗ hổng theo [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Lịch sử sao
|
||||
|
||||
🎉 **170.000 sao trên GitHub — xin cảm ơn!**
|
||||
🎉 **200.000 sao trên GitHub — xin cảm ơn!**
|
||||
|
||||
Cảm ơn mọi người đã sử dụng REA, báo lỗi, đề xuất tính năng, kiểm thử bản dựng và đóng góp bản sửa lỗi.
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> REA 官方來源: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> 請透過這些官方來源提供的安裝與發布連結取得 REA。相似的儲存庫名稱或複製的 README 不能證明下載內容來自官方。
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中啟動分析橋,檢查原生二進位檔案" width="1200" />
|
||||
@@ -139,15 +143,15 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
|
||||
| 目標 | REA 返回的內容 | 要求與指南 |
|
||||
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-TW/guides/native/) |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
|
||||
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
|
||||
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](../browser-observation.md) |
|
||||
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
|
||||
| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
|
||||
@@ -274,7 +278,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## Star 歷史
|
||||
|
||||
🎉 **GitHub Star 達到 170,000 個,感謝大家!**
|
||||
🎉 **GitHub Star 達到 200,000 個,感謝大家!**
|
||||
|
||||
感謝每一位使用 REA、報告錯誤、提出功能需求、測試建置和貢獻修復的朋友。
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
</div>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> REA 官方来源: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
|
||||
> 请通过这些官方来源提供的安装和发布链接获取 REA。相似的仓库名称或复制的 README 不能证明下载内容来自官方。
|
||||
|
||||
<div align="center">
|
||||
|
||||
<br />
|
||||
|
||||
<img src="../assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥,检查原生二进制文件" width="1200" />
|
||||
@@ -139,15 +143,15 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-CN/guides/native/) |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
|
||||
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
|
||||
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
|
||||
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
|
||||
@@ -274,7 +278,7 @@ npx rea-agents@latest setup
|
||||
|
||||
## Star 历史
|
||||
|
||||
🎉 **GitHub Star 达到 170,000 个,感谢大家!**
|
||||
🎉 **GitHub Star 达到 200,000 个,感谢大家!**
|
||||
|
||||
感谢每一位使用 REA、报告错误、提出功能需求、测试构建和贡献修复的朋友。
|
||||
|
||||
|
||||
+4
-2
@@ -84,8 +84,10 @@ registry metadata or changelog disagrees with that version before creating a tag
|
||||
|
||||
### Compatibility and version selection
|
||||
|
||||
For the 6.2 candidate, review the [6.1 to 6.2 migration guide](migration-6.2.md)
|
||||
alongside the changelog before publication.
|
||||
Before publishing a candidate, review its changelog's breaking changes and the
|
||||
migration notes for the release being prepared. The
|
||||
[6.1 to 6.2 migration guide](migration-6.2.md) records that release's changes;
|
||||
it is a historical reference, not the compatibility checklist for later releases.
|
||||
|
||||
REA uses Release Please's `always-bump-minor` strategy: every release increments
|
||||
minor and resets patch, including releases with breaking changes. For example,
|
||||
|
||||
@@ -55,6 +55,11 @@ dependency sections are typed failures. A cycle is reported and is not walked.
|
||||
|
||||
A missing section returns success with `auditable_metadata: null` and a
|
||||
limitation. Absence of `.dep-v0` does not mean the binary has no dependencies.
|
||||
Unknown embedded format revisions are retained in `format_version` and
|
||||
interpreted using the highest known preceding revision, following the
|
||||
cargo-auditable schema. The output includes a limitation that identifies this
|
||||
fallback and notes that the particular revision has not been independently
|
||||
verified. An omitted revision remains `null` and follows revision 0 semantics.
|
||||
Embedded names and versions are file content. They do not authenticate the
|
||||
packages or the compiler.
|
||||
|
||||
|
||||
+4
-3
@@ -19,7 +19,7 @@ behavior, and complete CI evidence before merging.
|
||||
| `npm run test:boundary` | Boundary, CLI boundary, MCP boundary, process boundary, and process-global projects |
|
||||
| `npm run test:mcp` | MCP boundary project |
|
||||
| `npm run test:acceptance` | Complete compiled CLI and MCP acceptance workflows |
|
||||
| `npm run test:python` | LLDB producer retention and launch contracts with a fake LLDB API; no native debugger required |
|
||||
| `npm run test:python` | LLDB and mitmproxy producer contracts plus analysis-efficiency checks; no real debugger or proxy required |
|
||||
| `npm run test:watch` | Dirty source tests in watch mode, without build |
|
||||
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
|
||||
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
|
||||
@@ -449,8 +449,9 @@ adds in-place reparse, breakaway, environment, and token observations.
|
||||
The separate `scripts/verify-windows-native-inspection.mjs` lane uses the
|
||||
`.cache/windows-native-inspection-fixture` artifact built on Linux with
|
||||
`node scripts/build-windows-native.mjs --inspection-fixture`. Its controlled
|
||||
first deletion failure checks retained NTFS ownership, later-query recovery,
|
||||
successful-admission caching, and environment-teardown cleanup. It neither
|
||||
first deletion failure checks retained NTFS ownership across a same-volume
|
||||
rename, later-query handle-based recovery, successful-admission caching, and
|
||||
environment-teardown cleanup. It neither
|
||||
establishes the cause of the intermittent #1701 host failure nor replaces
|
||||
verification with the shipped production artifact.
|
||||
`npm run verify:ghidra:windows:package` additionally packs and installs REA into
|
||||
|
||||
@@ -19,7 +19,7 @@ additional permission flag or degraded mode is required.
|
||||
- a 64-bit full JDK inside that installation's declared Java range (JDK 21 or newer, with no maximum, for current 12.1 releases);
|
||||
- an explicit native, non-managed x86 or x86-64 PE application/DLL, or a
|
||||
[raw NMOS 6502 image](raw-binary.md#nmos-6502) with a caller-supplied entry; and
|
||||
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
|
||||
- the 26 read-only Ghidra inventory, memory, and function-analysis operations.
|
||||
|
||||
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
|
||||
PE load-image inspection returns an explicit unsupported attestation status.
|
||||
@@ -78,6 +78,19 @@ OS failures. It checks the Ghidra release, headless launcher, full JDK, x64
|
||||
host, and native controls. A missing or mismatched addon requires the matching
|
||||
Windows x64 package; changing MCP registration cannot supply that artifact.
|
||||
A filesystem or access failure identifies the failed native constraint.
|
||||
An incompatible host uses `unsupported_host`. Missing, mismatched or unloadable
|
||||
native artifacts use `runtime_missing` and doctor classification
|
||||
`missing_dependency`. A failed native invocation or invalid inspection uses
|
||||
`runtime_unavailable` and doctor classification `runtime_failure`; it does not
|
||||
mean Windows x64 itself is unsupported. Provider diagnostics retain the failure
|
||||
stage under `windows_native_failure`, together with the corresponding remediation.
|
||||
Self-test failures preserve the native operation and OS error text. Check the
|
||||
process's writable local NTFS temporary directory and the reported filesystem,
|
||||
DACL, Job Object or cleanup constraint, then rerun doctor after resolving it.
|
||||
Ghidra or Java reinstallation cannot resolve a native self-test failure. The
|
||||
provider remains unavailable until all native controls are proven; a later query
|
||||
may retry a failed native invocation, while artifact or malformed-inspection
|
||||
failures remain stable for the process lifetime.
|
||||
Other doctor checks, including unconfigured agent integrations, can remain
|
||||
unready while Ghidra itself is available.
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ Follow-up: [SQLite existing-control measurements](sqlite-existing-controls.md)
|
||||
tests QGIS's shipped GeoPackage through the existing column selection, preserving
|
||||
full selected-table schema and typed cells before considering another API.
|
||||
|
||||
Follow-up: [small real PCAP recordings](pcap-small-captures.md) compares existing
|
||||
complete delivery with summary/pages. Preserve the negative SDK/model input
|
||||
results: compact captures do not justify another production mechanism.
|
||||
|
||||
Follow-up: [native PE/DOS entry views and real model verification](native-entry-views.md)
|
||||
records the post-merge native continuation, including a small-function case where
|
||||
four selected replies cost more bytes than the complete dossier.
|
||||
|
||||
@@ -180,7 +180,7 @@ def main():
|
||||
'inspect_android_package', 'search_android_classes',
|
||||
'inspect_android_class', 'inspect_android_method',
|
||||
'inspect_firmware_regions', 'inspect_managed_artifact',
|
||||
'inspect_sqlite_database',
|
||||
'inspect_sqlite_database', 'inspect_pcap_capture',
|
||||
'inspect_managed_members', 'inspect_managed_native_boundaries',
|
||||
'inspect_artifact', 'inspect_wasm_artifact',
|
||||
'inspect_evm_interface', 'get_evidence_bundle', 'export_evidence_bundle',
|
||||
|
||||
@@ -70,6 +70,44 @@ for (const [side, answer] of [
|
||||
value: answer.evidence_ids,
|
||||
});
|
||||
}
|
||||
// PCAP delivery observations preserve each actual receipt's identity, limitations
|
||||
// and view coverage. Summary/page scopes differ from complete source scope;
|
||||
// retain them separately rather than equating page exhaustion with capture
|
||||
// coverage. Repeated equivalent deliveries must also match every observation
|
||||
// field except the receipt and parent IDs, which identify their own invocations.
|
||||
if (
|
||||
["wireshark-http.cap", "wireshark-http_redirects.pcapng"].includes(
|
||||
left.artifact,
|
||||
)
|
||||
) {
|
||||
for (const [side, answer] of [
|
||||
["left", a],
|
||||
["right", b],
|
||||
]) {
|
||||
if (
|
||||
!Array.isArray(answer.delivery_observations) ||
|
||||
!answer.delivery_observations.length
|
||||
)
|
||||
throw new Error("Missing original PCAP delivery observations");
|
||||
runSpecific.push({
|
||||
side,
|
||||
path: ["delivery_observations"],
|
||||
value: answer.delivery_observations,
|
||||
});
|
||||
if (left.delivery === right.delivery) {
|
||||
answer.delivery_observations = answer.delivery_observations.map(
|
||||
(observation) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(observation).filter(
|
||||
([key]) => key !== "evidence_id" && key !== "parent_evidence_id",
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
delete answer.delivery_observations;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Binwalk assigns a new opaque region UUID in each invocation. Preserve both
|
||||
// original lookup values in this report; compare the observed region facts
|
||||
// separately. The product and original replies retain those IDs unchanged.
|
||||
|
||||
@@ -263,6 +263,34 @@
|
||||
"url": "https://raw.githubusercontent.com/qgis/QGIS/e1223b8e9e71fc50ee995ed556f711c54ccc101b/resources/data/world_map.gpkg",
|
||||
"derivation": "unmodified official resource file; streaming Git blob and SHA-256 verification"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wireshark-http.cap",
|
||||
"file": "http.cap",
|
||||
"bytes": 25803,
|
||||
"sha256": "25a72bdf10339f2c29916920c8b9501d294923108de8f29b19aba7cc001ab60d",
|
||||
"provenance": {
|
||||
"project": "Wireshark public sample captures",
|
||||
"source_page": "https://wiki.wireshark.org/SampleCaptures",
|
||||
"url": "https://wiki.wireshark.org/uploads/27707187aeb30df68e70c8fb9d614981/http.cap",
|
||||
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/27707187aeb30df68e70c8fb9d614981/http.cap",
|
||||
"upstream_expected_digest": null,
|
||||
"derivation": "Unmodified capture published by official Wireshark wiki. Local digest pins downloaded bytes; no independently published upstream digest is claimed."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wireshark-http_redirects.pcapng",
|
||||
"file": "http_redirects.pcapng",
|
||||
"bytes": 47804,
|
||||
"sha256": "ff432a6b71d06cd20720f7763e584014edd0a43c3b55981c682d1b7640207abc",
|
||||
"provenance": {
|
||||
"project": "Wireshark public sample captures",
|
||||
"source_page": "https://wiki.wireshark.org/SampleCaptures",
|
||||
"url": "https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/http_redirects.pcapng",
|
||||
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/__moin_import__/attachments/SampleCaptures/http_redirects.pcapng",
|
||||
"upstream_expected_digest": null,
|
||||
"derivation": "Unmodified capture published by official Wireshark wiki. Local digest pins downloaded bytes; no independently published upstream digest is claimed."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -523,7 +523,207 @@ const inspectEvmTarget = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const PCAP_EVIDENCE_ID = /^ev_[a-f0-9]{64}$/u;
|
||||
const pcapObservation = (evidence, requestedView, parseEvidence) => {
|
||||
// Authenticate with the selected runtime's owner, preserving the original
|
||||
// envelope for measurement and provenance rather than reconstructing it.
|
||||
parseEvidence(evidence);
|
||||
const evidenceId = evidence.evidence_id;
|
||||
const value = normalized(evidence);
|
||||
for (const key of ["limitations", "unknowns"])
|
||||
if (
|
||||
Object.hasOwn(value, key) &&
|
||||
(!Array.isArray(value[key]) ||
|
||||
!value[key].every((item) => typeof item === "string"))
|
||||
)
|
||||
throw new Error("PCAP normalized provenance lacks a valid string list");
|
||||
const target =
|
||||
value.kind === "summary"
|
||||
? value.summary?.target
|
||||
: value.kind === "pcap"
|
||||
? value.artifact
|
||||
: value.target;
|
||||
if (
|
||||
evidence.subject?.digest?.sha256 !== inputSha ||
|
||||
evidence.subject?.local_path !== values.path ||
|
||||
target?.sha256 !== inputSha ||
|
||||
target?.path !== values.path ||
|
||||
(value.kind === "summary" &&
|
||||
(value.artifact?.sha256 !== inputSha ||
|
||||
value.artifact?.path !== values.path))
|
||||
)
|
||||
throw new Error("PCAP Evidence does not identify the selected capture");
|
||||
// Summary delivery delegates to a retained view, whose Evidence operation is
|
||||
// inspect_analysis_view even when inspect_pcap_capture was requested.
|
||||
const operation = requestedView
|
||||
? "inspect_analysis_view"
|
||||
: "inspect_pcap_capture";
|
||||
if (evidence.operation !== operation)
|
||||
throw new Error(
|
||||
"PCAP Evidence operation differs from the requested delivery",
|
||||
);
|
||||
if (
|
||||
requestedView &&
|
||||
(typeof value.parent_evidence_id !== "string" ||
|
||||
!PCAP_EVIDENCE_ID.test(value.parent_evidence_id) ||
|
||||
value.parent_evidence_id === evidenceId ||
|
||||
!Array.isArray(evidence.evidence_links) ||
|
||||
!evidence.evidence_links.includes(value.parent_evidence_id))
|
||||
)
|
||||
throw new Error("PCAP retained parent lacks a distinct Evidence link");
|
||||
if (
|
||||
requestedView &&
|
||||
value.parent_digest !== value.parent_evidence_id.slice(3)
|
||||
)
|
||||
throw new Error("PCAP retained parent digest differs from its identity");
|
||||
const parameters = requestedView
|
||||
? { parent_evidence_id: value.parent_evidence_id, view: requestedView }
|
||||
: { path: values.path };
|
||||
if (
|
||||
!isDeepStrictEqual(evidence.parameters, parameters) ||
|
||||
(requestedView && !isDeepStrictEqual(value.view, requestedView))
|
||||
)
|
||||
throw new Error("PCAP Evidence parameters or view differ from the request");
|
||||
return {
|
||||
evidence_id: evidenceId,
|
||||
limitations: evidence.limitations,
|
||||
...Object.fromEntries(
|
||||
[
|
||||
"kind",
|
||||
"view",
|
||||
"parent_evidence_id",
|
||||
"coverage",
|
||||
"limitations",
|
||||
"unknowns",
|
||||
]
|
||||
.filter((key) => Object.hasOwn(value, key) && key !== "limitations")
|
||||
.map((key) => [key, value[key]]),
|
||||
),
|
||||
...(Object.hasOwn(value, "limitations")
|
||||
? { normalized_limitations: value.limitations }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
const inspectPcapTarget = async () => {
|
||||
const { parseEvidence } = await import(
|
||||
pathToFileURL(resolve(values["runtime-root"], "dist/domain/evidence.js"))
|
||||
.href
|
||||
);
|
||||
const input = {
|
||||
path: values.path,
|
||||
...(values.delivery === "summary" ? { detail: "summary" } : {}),
|
||||
};
|
||||
const definition = catalogDefinitions.get("inspect_pcap_capture");
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
if (
|
||||
!definition ||
|
||||
!ajv.validateSchema(definition.inputSchema) ||
|
||||
!ajv.validate(definition.inputSchema, input)
|
||||
)
|
||||
throw new Error("Selected PCAP request is not advertised by this runtime");
|
||||
const inspected = await call("inspect_pcap_capture", input);
|
||||
const n = normalized(inspected);
|
||||
const observations = [
|
||||
pcapObservation(
|
||||
inspected,
|
||||
values.delivery === "summary" ? { kind: "summary" } : undefined,
|
||||
parseEvidence,
|
||||
),
|
||||
];
|
||||
let capture, uris;
|
||||
if (values.delivery === "summary") {
|
||||
if (n.kind !== "summary" || n.parent_operation !== "inspect_pcap_capture")
|
||||
throw new Error("Runtime returned no requested PCAP summary");
|
||||
if (
|
||||
!isDeepStrictEqual(n.coverage, {
|
||||
status: "complete-within-view",
|
||||
examined: 1,
|
||||
total: 1,
|
||||
next_offset: null,
|
||||
exhausted: true,
|
||||
})
|
||||
)
|
||||
throw new Error("PCAP summary coverage differs from its complete view");
|
||||
capture = n.summary;
|
||||
const parentEvidenceId = observations[0].parent_evidence_id;
|
||||
const view = { kind: "pcap", facet: "http-uris", offset: 0, limit: 2 };
|
||||
const selected = await call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: parentEvidenceId },
|
||||
view,
|
||||
});
|
||||
const page = normalized(selected);
|
||||
const observation = pcapObservation(selected, view, parseEvidence);
|
||||
const total = capture.retained_counts.http_request_uris;
|
||||
const examined = Math.min(view.limit, total);
|
||||
const expectedCoverage = {
|
||||
status: "page",
|
||||
examined,
|
||||
total,
|
||||
next_offset: examined < total ? examined : null,
|
||||
exhausted: examined >= total,
|
||||
};
|
||||
if (
|
||||
page.parent_evidence_id !== parentEvidenceId ||
|
||||
page.parent_operation !== "inspect_pcap_capture" ||
|
||||
page.kind !== "pcap" ||
|
||||
page.page.facet !== "http-uris" ||
|
||||
page.source_coverage !== capture.coverage ||
|
||||
!isDeepStrictEqual(page.coverage, expectedCoverage)
|
||||
)
|
||||
throw new Error(
|
||||
"PCAP page identity, scope or coverage differs from its parent",
|
||||
);
|
||||
uris = page.page.items;
|
||||
if (
|
||||
observations.some((item) => item.evidence_id === observation.evidence_id)
|
||||
)
|
||||
throw new Error("PCAP retained read reused an Evidence identity");
|
||||
observations.push(observation);
|
||||
} else {
|
||||
const {
|
||||
endpoints,
|
||||
dns_queries,
|
||||
tls_sni_values,
|
||||
protocol_distribution,
|
||||
http_request_uris,
|
||||
unattributed_dns_addresses,
|
||||
diagnostics,
|
||||
...metadata
|
||||
} = n;
|
||||
capture = {
|
||||
...metadata,
|
||||
retained_counts: {
|
||||
endpoints: endpoints.length,
|
||||
dns_queries: dns_queries.length,
|
||||
tls_sni_values: tls_sni_values.length,
|
||||
protocol_distribution: protocol_distribution.length,
|
||||
http_request_uris: http_request_uris.length,
|
||||
unattributed_dns_addresses: unattributed_dns_addresses.length,
|
||||
},
|
||||
diagnostics_code_units:
|
||||
diagnostics === undefined
|
||||
? null
|
||||
: {
|
||||
tshark: diagnostics.tshark_stderr.length,
|
||||
capinfos: diagnostics.capinfos_stderr.length,
|
||||
},
|
||||
};
|
||||
uris = http_request_uris.slice(0, 2);
|
||||
}
|
||||
if (uris.length !== Math.min(2, capture.retained_counts.http_request_uris))
|
||||
throw new Error("Runtime did not return the requested retained URI prefix");
|
||||
return {
|
||||
artifact_sha256: targetSha(inspected),
|
||||
capture,
|
||||
http_request_uris_first2: uris,
|
||||
delivery_observations: observations,
|
||||
evidence_ids: observations.map((item) => item.evidence_id),
|
||||
};
|
||||
};
|
||||
|
||||
const workflows = {
|
||||
"wireshark-http.cap": inspectPcapTarget,
|
||||
"wireshark-http_redirects.pcapng": inspectPcapTarget,
|
||||
"qgis-world-map.gpkg": async () => {
|
||||
const columns = ["fid", "NAME", "ISO_A3"];
|
||||
const input = {
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# Small real PCAP recordings: keep complete delivery
|
||||
|
||||
Continues [#2016](https://github.com/morluto/rea/issues/2016) after the QGIS
|
||||
SQLite round. Measured main is pinned to
|
||||
`3ae826ceba6f25b08bce50bcb51001209bc16b57`, with 23 intervening commits.
|
||||
These include bounded PCAP summaries/pages, authenticated artifact subjects,
|
||||
unknown-snapshot reuse and shared transcript grading. This round measures existing
|
||||
PCAP delivery choices; it adds no production, API, default, cache or skill change.
|
||||
|
||||
## Corpus and question
|
||||
|
||||
Use two unmodified recordings published on the official
|
||||
[Wireshark sample-capture page](https://wiki.wireshark.org/SampleCaptures):
|
||||
|
||||
| Recording | Bytes | SHA-256 |
|
||||
| --------------------------------------- | -----: | ------------------------------------------------------------------ |
|
||||
| HTTP request/response, `http.cap` | 25,803 | `25a72bdf10339f2c29916920c8b9501d294923108de8f29b19aba7cc001ab60d` |
|
||||
| HTTP redirects, `http_redirects.pcapng` | 47,804 | `ff432a6b71d06cd20720f7763e584014edd0a43c3b55981c682d1b7640207abc` |
|
||||
|
||||
`corpus.json` preserves original/resolved URLs. Local digests pin downloaded
|
||||
bytes; no independently published upstream digest or immutable source commit is
|
||||
claimed. These are upstream saved application-protocol recordings, not generated
|
||||
REA fixtures. Recorded clients and current deployments are unknown. No new
|
||||
traffic is captured and no recorded target/URL is executed or fetched.
|
||||
|
||||
The question asks for complete returned identity/client/scalar metadata, every
|
||||
observed count and source coverage, six retained inventory counts, explicit nulls,
|
||||
diagnostic UTF-16 lengths, and the first two retained HTTP URIs in original order.
|
||||
Preserve each used receipt's limitations and identity. Summary/page scope and
|
||||
unknowns remain distinct from source coverage; exhaustion does not establish
|
||||
network completeness. Other inventory lists and diagnostic text are unrequested.
|
||||
|
||||
Compare default `{path}` with existing `{path, detail: "summary"}` plus a retained
|
||||
`http-uris` page, offset 0, limit 2. Ordinary selected-field
|
||||
`inspect_packet_capture` remains a separate boundary, not a substitute claim
|
||||
about complete inventories. `questions/pcap-http.txt` contains the fixed question.
|
||||
|
||||
## SDK measurements
|
||||
|
||||
Frozen dist/bridge file-table SHA-256:
|
||||
`db050545e94dc305a48042e06df96ac6c64b21ee6c5c4707c787d5e46df79e47`.
|
||||
Linux x64, Node 24.18.0, SDK client/server 2.3.1, installed Wireshark 4.0.17.
|
||||
Explicit commands are `/usr/bin/tshark`, `/usr/bin/capinfos`, `/usr/bin/prlimit`;
|
||||
private receipts retain executable hashes. Each workflow starts a fresh server
|
||||
and uses actual named advertised definitions/schema validation. Host page caches
|
||||
are not flushed. Both tools read the owned snapshot.
|
||||
|
||||
Eight trials run complete/summary/summary/complete per recording. Both pairs
|
||||
match selected capture facts, URI prefixes, settings and original source bytes.
|
||||
Each delivery's exact limitations, identities and coverage remain separately
|
||||
recorded, rather than equating source coverage with page exhaustion.
|
||||
|
||||
| Trial | Reply bytes | SDK seconds | Payload CPU seconds | Kernel peak bytes |
|
||||
| -------------------- | ----------: | ----------: | ------------------: | ----------------: |
|
||||
| HTTP complete-1 | 7,601 | 4.349 | 5.336 | 611,778,560 |
|
||||
| HTTP summary-1 | 17,540 | 4.972 | 5.973 | 601,661,440 |
|
||||
| HTTP summary-2 | 17,540 | 5.101 | 6.092 | 611,438,592 |
|
||||
| HTTP complete-2 | 7,601 | 4.463 | 5.503 | 617,148,416 |
|
||||
| Redirects complete-1 | 7,535 | 4.670 | 5.641 | 610,627,584 |
|
||||
| Redirects summary-1 | 17,240 | 5.099 | 6.104 | 615,522,304 |
|
||||
| Redirects summary-2 | 17,240 | 5.060 | 6.056 | 610,385,920 |
|
||||
| Redirects complete-2 | 7,535 | 4.466 | 5.458 | 608,141,312 |
|
||||
|
||||
Bytes are compact client-result JSON summed over one complete or two summary/page
|
||||
calls, not model tokens or raw wire bytes. Summary/page expands replies to more
|
||||
than twice the complete result on both recordings. All four SDK pairs are slower
|
||||
and use more payload CPU; memory differences are small/inconsistent. CPU includes
|
||||
driver/catalog work outside the SDK workflow timer.
|
||||
|
||||
Reproduce with `measure.mjs --artifact wireshark-http.cap` or
|
||||
`wireshark-http_redirects.pcapng`, pinned path/runtime/revision, explicit provider
|
||||
JSON, `--schemas selected`, and `--delivery complete|summary`. Use `compare.mjs`
|
||||
on completed output directories. Original replies, catalogs, comparison receipts
|
||||
and exact measured driver source stay private. Repeated deliveries in the same
|
||||
mode also compare the ordered receipt observations, including limitations, view
|
||||
parameters, coverage and unknowns; only receipt/parent IDs are invocation-specific.
|
||||
Comparator regressions run with `npm run test:local -- tests/evaluation/analysisEfficiencyComparison.test.ts`
|
||||
using synthetic measurement directories; they do not establish provider behavior.
|
||||
|
||||
## Actual model tasks
|
||||
|
||||
Four actual Codex CLI 0.161.0 tasks use requested `gpt-5.6-terra` high, the same
|
||||
redirects question/runtime/artifact/provider settings, direct mode, two exact
|
||||
enabled tools and equal context/discovery capture flags. The requested alias is
|
||||
not verified server-resolved model identity. Equal cohort instructions select
|
||||
complete or summary plus one URI page. The experiment driver applies its existing
|
||||
private per-tool approval policy to `inspect_pcap_capture`; global configuration
|
||||
and REA authority are unchanged.
|
||||
|
||||
| Trial | Input | Cached | Uncached | Output | CLI wall seconds | MCP calls | Facts |
|
||||
| ---------- | -----: | -----: | -------: | -----: | ---------------: | --------: | ----- |
|
||||
| complete-1 | 38,947 | 9,984 | 28,963 | 2,119 | 69.469 | 1 | pass |
|
||||
| summary-1 | 58,779 | 38,144 | 20,635 | 3,241 | 141.304 | 2 | pass |
|
||||
| summary-2 | 58,584 | 38,144 | 20,440 | 4,261 | 50.916 | 2 | pass |
|
||||
| complete-2 | 38,917 | 23,040 | 15,877 | 2,813 | 106.896 | 1 | pass |
|
||||
|
||||
Summary/page total input **increases 50.92% / 50.54%** and output increases
|
||||
52.95% / 51.48%. Uncached input is mixed: 28.75% lower / 28.74% higher. Time is
|
||||
mixed: 103.41% slower / 52.37% faster. No cause is inferred for caching or timing
|
||||
variation. Cached input is included in total; each CLI reports one final aggregate
|
||||
usage. Output includes 1,149 / 1,162 / 2,196 / 1,837 reasoning tokens.
|
||||
|
||||
All calls succeed without extras/retries. Events contain only the selected MCP
|
||||
tools and agent messages. All source-digest and private-profile cleanup checks
|
||||
pass. Model payload CPU seconds/kernel peaks: 7.818/791,060,480;
|
||||
9.793/752,705,536; 7.685/757,891,072; 7.999/750,993,408. These do not establish
|
||||
a general CPU/memory gain.
|
||||
|
||||
The first summary initially fails the grader because it uses explicit equivalent
|
||||
labels and a URI object retaining page/source coverage. Its facts are correct.
|
||||
The grading boundary accepts these known forms while checking exact types,
|
||||
null presence, original limitations and extra scopes against actual receipts.
|
||||
Conflicting duplicates and unsupported claims remain failures. Each cited
|
||||
receipt's own ID must be reported; its parent lookup ID is checked separately.
|
||||
Original answers/reviews/grader source remain intact. No model is rerun. A grader
|
||||
review pause separates the pairs; it is not task time or a cold-cache claim.
|
||||
|
||||
## Verification and decision
|
||||
|
||||
An independent stdlib raw PCAP/PCAPNG/Ethernet/IP/TCP/complete-HTTP-header parser
|
||||
agrees on 43/271 packets, per-packet endpoint counts, first URI prefixes and 2/23
|
||||
distinct HTTP URIs. It invokes no Wireshark. DNS associations, protocol-stack
|
||||
dissection, nullable timestamps and live-network semantics are not independently
|
||||
validated.
|
||||
|
||||
Both real CLI complete results match full MCP normalized results, subjects,
|
||||
parameters, IDs and limitations. Portable CLI summary/URI views match MCP retained
|
||||
normalized facts and subjects; source forms/caller parameters remain distinct.
|
||||
Source bytes stay unchanged. Thirty-four context/grading regressions pass, including
|
||||
cited complete metadata/counts/UTF-16 diagnostics and exact ordered response ID
|
||||
lists that reject parent lookups, duplicates and nested representations. URI
|
||||
prefix coverage must come from its cited page, even if an uncited page has the
|
||||
same items. The SDK driver also checks the returned view against the exact
|
||||
requested kind, facet, offset and limit. Five real-answer
|
||||
mutations—omitted null, false page exhaustion, contradictory source scope,
|
||||
unsupported absence claim and non-string receipt identity—fail correctly.
|
||||
Malformed identities produce failed facts rather than crashing the grader.
|
||||
JS syntax, Python compilation and
|
||||
relevant standard lint pass.
|
||||
|
||||
A fresh real SDK summary/URI smoke matches the original selected facts. Three
|
||||
private consumer fault injections alter the returned view's offset, limit or
|
||||
presence after a real SDK reply; the driver rejects each. These establish the
|
||||
experiment consumer's guard, not provider malformed-reply behavior. The initial
|
||||
injection setup loaded the separate CommonJS client and did not inject a fault;
|
||||
that discarded setup attempt remains private alongside the corrected ESM runs.
|
||||
Original measured driver sources, token totals and timing data remain unchanged.
|
||||
|
||||
The driver also requires a distinct Evidence receipt for the retained URI page.
|
||||
A private SDK consumer injection reuses the summary's response ID after a real
|
||||
MCP reply: the preceding driver accepts it, the corrected driver rejects it
|
||||
without writing an answer, and an unmodified real reply still passes. This
|
||||
checks receipt uniqueness before recording observations; it does not establish
|
||||
that a provider actually returned duplicate IDs.
|
||||
|
||||
The page guard checks the complete expected coverage object, including status,
|
||||
continuation and exhaustion. Private actual-reply injections demonstrate that
|
||||
the preceding driver accepts inconsistent coverage, while the corrected driver
|
||||
rejects wrong status, next offset or exhaustion. Unmodified exhausted and
|
||||
continuable URI pages still pass. The grader also requires each cited receipt's
|
||||
subject digest and local path to identify the reference capture, and uses the
|
||||
reference page's verified coverage. Thirty-four grading/context regressions pass,
|
||||
including missing or malformed subjects and faithful but inconsistent coverage;
|
||||
all four original model answers still pass factual regrading. Strict resource
|
||||
grades remain false where terminal whole-scope counters are unknown.
|
||||
|
||||
PCAP observation production now validates both the Evidence subject and
|
||||
normalized target against the selected path/digest for complete, summary and
|
||||
page replies. Retained parents must be declared in each view receipt's Evidence
|
||||
links and differ from that response's own ID. The grader enforces the same link
|
||||
relationship. Private actual-reply injections and normal complete/summary
|
||||
controls exercise this boundary; these do not establish a provider defect.
|
||||
|
||||
The grader preserves each receipt's producing tool while indexing it; conflicting
|
||||
tool/envelope ownership for one ID remains ambiguous, while identical repeated
|
||||
receipts are allowed. Complete delivery requires the capture tool/operation.
|
||||
Summary delivery uses the capture tool but correctly delegates its Evidence
|
||||
operation to `inspect_analysis_view`; the URI page uses that view tool/operation.
|
||||
Both the grader and SDK observation producer validate this actual contract.
|
||||
Wrong-tool/operation regressions fail before the correction and pass afterward;
|
||||
private SDK operation faults are rejected while real complete/summary controls
|
||||
and all four stored model factual grades still pass. No operation is rewritten
|
||||
to make the tool name and delegated Evidence operation identical.
|
||||
|
||||
The receipt index parses the required envelope/nested-view containers before
|
||||
allowing lookup, so missing or malformed normalized results/limitations become
|
||||
failed facts with a written report. Empty observation lists also remain failed
|
||||
facts and never enter citation checks that require a first receipt. Coverage uses type-sensitive JSON comparison
|
||||
to distinguish booleans, integers and floats. Receipt-identity conflict checks
|
||||
use the same type-sensitive rule, so a scalar type change cannot hide in a
|
||||
repeated ID. Existing PCAP scope-alias checks already preserve JSON types.
|
||||
Requested and cited URI views use the same type-sensitive JSON rule, rejecting
|
||||
boolean or floating-point offsets/limits even when an answer faithfully copies
|
||||
them. The observation producer rejects missing, null or malformed response IDs
|
||||
before recording either capture or page receipts. Private real-reply injections
|
||||
exercise these ID failures alongside normal complete/summary controls; they do
|
||||
not establish malformed provider behavior.
|
||||
Completed-call arguments stay with their producing tool and receipt in the
|
||||
grader's identity index. Cited envelope parameters and actual call arguments
|
||||
must match the selected path or retained parent/view, including delegated
|
||||
summary parameters. The observation producer validates both summary and URI
|
||||
views and envelope parameters at their common boundary. Private real-reply
|
||||
faults reproduce missing-parameter/summary-view acceptance before the fix and
|
||||
are rejected afterward; normal complete/summary controls still pass.
|
||||
Negative regressions reproduce
|
||||
prior crashes and false passes; all original real-model factual grades still
|
||||
pass without model reruns. The SDK workflow is unchanged by these grader fixes.
|
||||
|
||||
**Decision: keep existing complete defaults; add no production optimization.**
|
||||
For these compact recordings and this question, summary/page costs more total
|
||||
input in both real-model pairs. No stable speed or CPU/memory benefit justifies
|
||||
another API, automatic delivery switch, cache or plugin layer. Existing pages
|
||||
remain available; their benefit needs evidence for the actual question/inventory.
|
||||
Large captures, other protocols, Windows/macOS/Bun Wireshark and other model
|
||||
workloads remain unverified here.
|
||||
|
||||
Heavy work stays serial with one CPU, 3 GiB early stop/4 GiB hard memory and no
|
||||
swap. Build kernel peak: 2,575,732,736 bytes. Payload cleanup and zero phase limit
|
||||
events are verified; missing terminal whole-scope counters remain unknown, so
|
||||
strict whole-resource grades stay false. No raw capture, trace, credential,
|
||||
runtime or result JSON is committed. No unrelated REA product bug is identified
|
||||
or fixed, and no release is performed.
|
||||
|
||||
Review follow-up binds the cited URI page to its exact requested facet/range,
|
||||
reported values and the summary retained parent. Small synthetic trial receipts
|
||||
exercise the entire grading entrypoint, including faithful but wrong-page/range/
|
||||
parent citations, missing nulls, false scope and malformed identities. These are
|
||||
grader regressions, not real-provider evidence. All four stored actual model
|
||||
answers still pass; no model reruns are required. Transcript readers close their
|
||||
owned file handles after grading.
|
||||
|
||||
The SDK driver and factual grader require summary coverage to describe its one
|
||||
complete view and require each present normalized limitations/unknowns field to
|
||||
be a string array. Missing fields and valid empty arrays remain distinct.
|
||||
Controlled consumer mutations over actual SDK replies reproduce the previously
|
||||
accepted inconsistent summary coverage and verify rejection after the guard;
|
||||
complete and summary controls still pass. These mutations do not establish a
|
||||
malformed-response defect in the provider. The four stored model answers still
|
||||
pass factual regrading without another model run.
|
||||
|
||||
Summary views must preserve both the outer artifact path/digest and the nested
|
||||
summary target, as well as the Evidence subject. The full grader rejects
|
||||
faithful receipts with missing or incorrect outer artifact identities. Private
|
||||
mutations over actual SDK replies reproduce the previous false success and
|
||||
verify rejection after the shared observation guard; normal deliveries and all
|
||||
four original model answers retain their recorded facts.
|
||||
|
||||
Declared retained parent digests must match the digest encoded in the linked
|
||||
parent Evidence ID for both summary and page views. SDK acceptance and factual
|
||||
grading reject mismatches and missing digests while preserving complete
|
||||
deliveries. Before/after private actual-reply mutations verify both view roles;
|
||||
unmodified deliveries and four stored original-model factual regrades pass.
|
||||
|
||||
The SDK observation boundary additionally authenticates each complete, summary
|
||||
and page envelope with the selected runtime’s existing `parseEvidence` owner.
|
||||
It preserves the original envelope, without duplicating semantic-ID hashing.
|
||||
Format-valid swapped-ID mutations over actual SDK replies pass before this
|
||||
guard and fail afterward; unmodified complete/summary controls pass. The model
|
||||
grader tests selected facts against cited transcript receipts; its synthetic
|
||||
fixtures do not establish Evidence authentication. The original real-model
|
||||
receipts are independently authenticated against the pinned runtime.
|
||||
|
||||
The grader separates truthful cited facts from equivalence of the measured
|
||||
workflow. PCAP workflow checks require the planned ordered capture call (complete)
|
||||
or summary capture followed by the URI-page call, with exact arguments and
|
||||
completed status. Extra, reversed, failed or undecodable attempts fail workflow
|
||||
acceptance even when the final cited facts are correct; their actual cost and
|
||||
errors remain recorded. All four original model trials pass both factual and
|
||||
workflow checks without model reruns. Strict resource grades remain unchanged.
|
||||
|
||||
Each planned call must also decode as an Evidence object with its own cited
|
||||
response ID. An unreadable planned reply cannot independently pass workflow
|
||||
checks merely because tool names, arguments and terminal status match. The
|
||||
regression reproduces the previous workflow-only false success; original model
|
||||
factual/workflow grades remain unchanged.
|
||||
|
||||
Workflow citation cardinality must equal the planned call count before binding
|
||||
per-call IDs. A shorter or longer answer list cannot silently shorten Python
|
||||
zip’s expected plan; missing URI calls and extra citations are rejected.
|
||||
Original complete/summary model workflows continue to pass unchanged.
|
||||
|
||||
The six comparator CLI regressions now live in the normally discovered Vitest
|
||||
evaluation suite, so routine local/PR/CI gates execute them. The original six
|
||||
scenarios and CLI assertions are retained; the former manual Node suite is
|
||||
removed. They remain synthetic consumer checks, not provider evidence.
|
||||
|
||||
Failed calls with scalar/list replies or malformed nested resource-error
|
||||
containers are parsed defensively. The grader writes its report, preserves the
|
||||
original failure and any supported cited facts, and rejects workflow acceptance
|
||||
instead of crashing. Genuine structured resource Evidence references remain
|
||||
observable; no malformed producer behavior is inferred from synthetic cases.
|
||||
@@ -0,0 +1 @@
|
||||
Statically inspect the supplied saved Wireshark capture at {{artifact_path}} using only registered REA inspect_pcap_capture and inspect_analysis_view. Report its original client/target identity and every returned scalar metadata/count/coverage field exactly, preserving explicit nulls. Include retained_counts for endpoints, dns_queries, tls_sni_values, protocol_distribution, http_request_uris and unattributed_dns_addresses; derive array lengths only when reading complete Evidence. Include diagnostics_code_units as exact UTF-16 lengths of retained capinfos/tshark stderr, or null if diagnostics are unrecorded. In capture, omit inventory arrays and diagnostic text; do not omit their observed counts or change source coverage. Report the first two retained HTTP URIs in original order, or all if fewer; do not request other inventory pages. Report delivery_observations in dependency order for the Evidence used: evidence_id, outer limitations, and each present normalized kind, view, parent_evidence_id, coverage, unknowns and normalized_limitations. Preserve presence, explicit nulls, scalar types and original limitation order. Distinguish source coverage from URI-page exhaustion. Final JSON fields: artifact_sha256, capture, http_request_uris_first2, delivery_observations, evidence_ids. Include only actual response IDs. Saved packet/URI observations do not establish current deployment, process identity, missing traffic, decrypted payloads or current DNS resolution. Do not use shell, host file tools, network, other agents, traffic acquisition or execute recorded targets. Stop once these selected facts are supported.
|
||||
@@ -67,9 +67,47 @@ def scope_with_verified_counts(value, expected_scope, returned_counts):
|
||||
return scope, checks
|
||||
|
||||
|
||||
def pcap_observation_fields(value):
|
||||
"""Equivalent explicit labels are accepted; conflicting duplicates remain failures."""
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
result = dict(value)
|
||||
for label, canonical in {
|
||||
'outer_limitations': 'limitations', 'normalized_kind': 'kind',
|
||||
'normalized_view': 'view', 'normalized_parent_evidence_id': 'parent_evidence_id',
|
||||
'normalized_coverage': 'coverage', 'normalized_unknowns': 'unknowns',
|
||||
'normalized_source_coverage': 'source_coverage',
|
||||
}.items():
|
||||
if label not in result:
|
||||
continue
|
||||
if canonical in result and json.dumps(result[label], sort_keys=True) != json.dumps(result[canonical], sort_keys=True):
|
||||
return {**result, 'conflicting_fields': [label, canonical]}
|
||||
result[canonical] = result.pop(label)
|
||||
return result
|
||||
|
||||
|
||||
def pcap_complete_capture(value):
|
||||
"""Project the selected metadata/counts from the cited complete receipt."""
|
||||
inventories = ['endpoints', 'dns_queries', 'tls_sni_values', 'protocol_distribution',
|
||||
'http_request_uris', 'unattributed_dns_addresses']
|
||||
if not isinstance(value, dict) or not all(isinstance(value.get(key), list) for key in inventories):
|
||||
return None
|
||||
capture = {key: item for key, item in value.items() if key not in inventories + ['diagnostics']}
|
||||
capture['retained_counts'] = {key: len(value[key]) for key in inventories}
|
||||
diagnostics = value.get('diagnostics')
|
||||
if 'diagnostics' not in value:
|
||||
capture['diagnostics_code_units'] = None
|
||||
elif isinstance(diagnostics, dict) and all(isinstance(diagnostics.get(key + '_stderr'), str) for key in ['tshark', 'capinfos']):
|
||||
capture['diagnostics_code_units'] = {key: len(diagnostics[key + '_stderr'].encode('utf-16-le', errors='surrogatepass')) // 2
|
||||
for key in ['tshark', 'capinfos']}
|
||||
else:
|
||||
return None
|
||||
return capture
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'flutter-aot', 'sqlite', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
|
||||
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'flutter-aot', 'sqlite', 'pcap', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
|
||||
parser.add_argument('--trial', type=Path, required=True)
|
||||
parser.add_argument('--reference', type=Path)
|
||||
parser.add_argument('--resource-log', type=Path, required=True)
|
||||
@@ -83,25 +121,43 @@ def main():
|
||||
reference = read(args.reference / 'answer.json') if args.reference else None
|
||||
observed_ids = set()
|
||||
producer_results = {}
|
||||
pcap_calls = []
|
||||
pcap_attempts = []
|
||||
workflow_checks = {'pcap_exact_call_sequence': False} if args.domain == 'pcap' else {}
|
||||
tool_errors = []
|
||||
for line in (args.trial / 'events.jsonl').open():
|
||||
event = json.loads(line)
|
||||
item = event.get('item', {})
|
||||
if event.get('type') != 'item.completed' or item.get('type') != 'mcp_tool_call':
|
||||
continue
|
||||
result = item.get('result') or {}
|
||||
parsed = decode(result)
|
||||
if item['status'] == 'completed':
|
||||
# Only response text contributes observed IDs, never model arguments.
|
||||
for text in strings(result):
|
||||
observed_ids.update(re.findall(r'ev_[a-f0-9]{64}', text))
|
||||
if parsed is not None:
|
||||
producer_results.setdefault(item['tool'], []).append(parsed)
|
||||
else:
|
||||
tool_errors.append({'tool': item['tool'], 'arguments': item['arguments'], 'response': parsed if parsed is not None else result})
|
||||
if parsed is not None and parsed.get('error', {}).get('code') == 'resource_constraint':
|
||||
reference_value = parsed['error'].get('details', {}).get('reported_limits', {}).get('evidence_reference')
|
||||
observed_ids.update(evidence_ids(reference_value))
|
||||
with (args.trial / 'events.jsonl').open() as event_lines:
|
||||
for line in event_lines:
|
||||
event = json.loads(line)
|
||||
item = event.get('item', {})
|
||||
if event.get('type') != 'item.completed' or item.get('type') != 'mcp_tool_call':
|
||||
continue
|
||||
result = item.get('result') or {}
|
||||
parsed = decode(result)
|
||||
if args.domain == 'pcap':
|
||||
pcap_attempts.append({'tool': item.get('tool'), 'arguments': item.get('arguments'),
|
||||
'status': item.get('status'),
|
||||
'evidence_id': parsed.get('evidence_id') if isinstance(parsed, dict) else None,
|
||||
'readable_evidence': isinstance(parsed, dict)
|
||||
and isinstance(parsed.get('normalized_result'), dict)
|
||||
and isinstance(parsed.get('evidence_id'), str)
|
||||
and re.fullmatch(r'ev_[a-f0-9]{64}', parsed['evidence_id']) is not None})
|
||||
if item['status'] == 'completed':
|
||||
# Only response text contributes observed IDs, never model arguments.
|
||||
for text in strings(result):
|
||||
observed_ids.update(re.findall(r'ev_[a-f0-9]{64}', text))
|
||||
if parsed is not None:
|
||||
if args.domain == 'pcap':
|
||||
pcap_calls.append((item['tool'], item.get('arguments'), parsed))
|
||||
else:
|
||||
producer_results.setdefault(item['tool'], []).append(parsed)
|
||||
else:
|
||||
tool_errors.append({'tool': item['tool'], 'arguments': item['arguments'], 'response': parsed if parsed is not None else result})
|
||||
error = parsed.get('error') if isinstance(parsed, dict) else None
|
||||
if isinstance(error, dict) and error.get('code') == 'resource_constraint':
|
||||
details = error.get('details')
|
||||
limits = details.get('reported_limits') if isinstance(details, dict) else None
|
||||
if isinstance(limits, dict):
|
||||
observed_ids.update(evidence_ids(limits.get('evidence_reference')))
|
||||
reported_ids = evidence_ids(answer.get('evidence_ids'))
|
||||
checks = {
|
||||
'reported_evidence_ids_observed_in_responses': bool(reported_ids) and reported_ids <= observed_ids,
|
||||
@@ -138,6 +194,187 @@ def main():
|
||||
# Missing is not SQL/JSON null; booleans are not numeric counters.
|
||||
check(key, (key in answer, json.dumps(answer.get(key), sort_keys=True)),
|
||||
(True, json.dumps(reference[key], sort_keys=True)))
|
||||
elif domain == 'pcap':
|
||||
check('capture', ('capture' in answer, json.dumps(answer.get('capture'), sort_keys=True)),
|
||||
(True, json.dumps(reference['capture'], sort_keys=True)))
|
||||
prefix = answer.get('http_request_uris_first2')
|
||||
records = {}
|
||||
for tool, arguments, value in pcap_calls:
|
||||
if not isinstance(value, dict) or not isinstance(value.get('evidence_id'), str):
|
||||
continue
|
||||
identity = value['evidence_id']
|
||||
normalized = value.get('normalized_result')
|
||||
supported = (isinstance(normalized, dict) and isinstance(value.get('limitations'), list)
|
||||
and all(isinstance(item, str) for item in value['limitations']))
|
||||
if supported:
|
||||
supported = all(key not in normalized or (isinstance(normalized[key], list)
|
||||
and all(isinstance(item, str) for item in normalized[key]))
|
||||
for key in ['limitations', 'unknowns'])
|
||||
if supported and ('summary' in normalized or normalized.get('kind') == 'summary'):
|
||||
supported = isinstance(normalized.get('summary'), dict)
|
||||
if supported and ('page' in normalized or normalized.get('kind') == 'pcap'):
|
||||
supported = isinstance(normalized.get('page'), dict)
|
||||
entry = (tool, arguments, value) if supported else None
|
||||
if identity not in records:
|
||||
records[identity] = entry
|
||||
elif json.dumps(records[identity], sort_keys=True) != json.dumps(entry, sort_keys=True):
|
||||
# An ID with conflicting producers or envelopes is ambiguous.
|
||||
records[identity] = None
|
||||
observations = answer.get('delivery_observations')
|
||||
canonical_observations = [pcap_observation_fields(value) for value in observations] if isinstance(observations, list) else observations
|
||||
if isinstance(prefix, dict):
|
||||
checks['pcap_prefix_fields_supported'] = set(prefix) <= {'items', 'coverage', 'source_coverage'} and 'items' in prefix
|
||||
if 'source_coverage' in prefix:
|
||||
check('pcap_prefix_source_coverage', prefix['source_coverage'], reference['capture']['coverage'])
|
||||
if 'coverage' in prefix:
|
||||
last = canonical_observations[-1] if isinstance(canonical_observations, list) and canonical_observations else None
|
||||
identity = last.get('evidence_id') if isinstance(last, dict) else None
|
||||
entry = records.get(identity) if isinstance(identity, str) else None
|
||||
record = entry[2] if entry is not None else None
|
||||
page = record['normalized_result'] if record is not None else {}
|
||||
checks['pcap_prefix_coverage_observed'] = (
|
||||
page.get('kind') == 'pcap'
|
||||
and page.get('page', {}).get('facet') == 'http-uris'
|
||||
and json.dumps(page['page'].get('items'), sort_keys=True) == json.dumps(prefix.get('items'), sort_keys=True)
|
||||
and 'coverage' in page
|
||||
and json.dumps(prefix['coverage'], sort_keys=True) == json.dumps(page['coverage'], sort_keys=True)
|
||||
)
|
||||
prefix = prefix.get('items')
|
||||
check('http_request_uris_first2', ('http_request_uris_first2' in answer, json.dumps(prefix, sort_keys=True)),
|
||||
(True, json.dumps(reference['http_request_uris_first2'], sort_keys=True)))
|
||||
expected_observations = []
|
||||
if isinstance(canonical_observations, list):
|
||||
for observation in canonical_observations:
|
||||
identity = observation.get('evidence_id') if isinstance(observation, dict) else None
|
||||
entry = records.get(identity) if isinstance(identity, str) else None
|
||||
record = entry[2] if entry is not None else None
|
||||
if record is None:
|
||||
expected_observations.append(None)
|
||||
continue
|
||||
value = record['normalized_result']
|
||||
expected = {'evidence_id': record['evidence_id'], 'limitations': record['limitations']}
|
||||
for key in ['kind', 'view', 'parent_evidence_id', 'coverage', 'unknowns']:
|
||||
if key in value:
|
||||
expected[key] = value[key]
|
||||
if 'limitations' in value:
|
||||
expected['normalized_limitations'] = value['limitations']
|
||||
if 'source_coverage' in observation and 'source_coverage' in value:
|
||||
expected['source_coverage'] = value['source_coverage']
|
||||
expected_observations.append(expected)
|
||||
checks['pcap_delivery_observations_nonempty'] = isinstance(observations, list) and bool(observations)
|
||||
check('pcap_original_delivery_observations', json.dumps(canonical_observations, sort_keys=True),
|
||||
json.dumps(expected_observations, sort_keys=True))
|
||||
if isinstance(canonical_observations, list) and canonical_observations and all(isinstance(x, dict) for x in canonical_observations):
|
||||
check('pcap_requested_delivery_chain', [x.get('kind', 'complete') for x in canonical_observations],
|
||||
[x.get('kind', 'complete') for x in reference['delivery_observations']])
|
||||
check('pcap_requested_views', json.dumps([x.get('view') for x in canonical_observations], sort_keys=True),
|
||||
json.dumps([x.get('view') for x in reference['delivery_observations']], sort_keys=True))
|
||||
if len(reference['delivery_observations']) == 2:
|
||||
check('pcap_summary_coverage', json.dumps(canonical_observations[0].get('coverage'), sort_keys=True),
|
||||
json.dumps({'status': 'complete-within-view', 'examined': 1, 'total': 1,
|
||||
'next_offset': None, 'exhausted': True}, sort_keys=True))
|
||||
identities = [x.get('evidence_id') for x in canonical_observations]
|
||||
checks['pcap_observations_have_reported_ids'] = (
|
||||
isinstance(answer.get('evidence_ids'), list)
|
||||
and answer['evidence_ids'] == identities
|
||||
and all(isinstance(value, str) and re.fullmatch(r'ev_[a-f0-9]{64}', value) for value in identities)
|
||||
and len(set(identities)) == len(identities)
|
||||
)
|
||||
entries = [records.get(identity) if isinstance(identity, str) else None for identity in identities]
|
||||
cited = [entry[2] if entry is not None else None for entry in entries]
|
||||
expected_tools = ['inspect_pcap_capture'] + (['inspect_analysis_view'] if len(reference['delivery_observations']) == 2 else [])
|
||||
check('pcap_cited_producer_tools', [entry[0] if entry is not None else None for entry in entries], expected_tools)
|
||||
# Summary delivery delegates to a retained view; its envelope operation
|
||||
# is inspect_analysis_view even though the requested tool is inspect_pcap_capture.
|
||||
expected_operations = ['inspect_pcap_capture'] if len(expected_tools) == 1 else ['inspect_analysis_view'] * 2
|
||||
check('pcap_cited_operations', [record.get('operation') if isinstance(record, dict) else None for record in cited], expected_operations)
|
||||
path = reference['capture']['target']['path']
|
||||
expected_arguments = [{'path': path}]
|
||||
expected_parameters = [{'path': path}]
|
||||
if len(expected_tools) == 2:
|
||||
parent = cited[0]['normalized_result'].get('parent_evidence_id') if isinstance(cited[0], dict) else None
|
||||
views = [observation.get('view') for observation in reference['delivery_observations']]
|
||||
expected_arguments = [{'path': path, 'detail': 'summary'},
|
||||
{'source': {'kind': 'retained-evidence', 'evidence_id': parent}, 'view': views[-1]}]
|
||||
expected_parameters = [{'parent_evidence_id': parent, 'view': view} for view in views]
|
||||
check('pcap_cited_call_arguments', json.dumps([entry[1] if entry is not None else None for entry in entries], sort_keys=True),
|
||||
json.dumps(expected_arguments, sort_keys=True))
|
||||
workflow_checks['pcap_exact_call_sequence'] = (
|
||||
len(identities) == len(expected_tools)
|
||||
and json.dumps(pcap_attempts, sort_keys=True) == json.dumps([
|
||||
{'tool': tool, 'arguments': arguments, 'status': 'completed',
|
||||
'evidence_id': identity, 'readable_evidence': True}
|
||||
for tool, arguments, identity in zip(expected_tools, expected_arguments, identities)
|
||||
], sort_keys=True)
|
||||
)
|
||||
check('pcap_cited_parameters', json.dumps([record.get('parameters') if isinstance(record, dict) else None for record in cited], sort_keys=True),
|
||||
json.dumps(expected_parameters, sort_keys=True))
|
||||
checks['pcap_cited_subjects_match_artifact'] = bool(cited) and all(
|
||||
isinstance(record, dict)
|
||||
and isinstance(record.get('subject'), dict)
|
||||
and isinstance(record['subject'].get('digest'), dict)
|
||||
and record['subject']['digest'].get('sha256') == reference['artifact_sha256']
|
||||
and record['subject'].get('local_path') == reference['capture']['target']['path']
|
||||
for record in cited
|
||||
)
|
||||
checks['pcap_cited_parents_linked'] = bool(cited) and all(isinstance(record, dict) for record in cited) and (
|
||||
len(cited) == 1 or (len(cited) == 2 and all(
|
||||
isinstance(record['normalized_result'].get('parent_evidence_id'), str)
|
||||
and re.fullmatch(r'ev_[a-f0-9]{64}', record['normalized_result']['parent_evidence_id'])
|
||||
and record['normalized_result'].get('parent_digest') == record['normalized_result']['parent_evidence_id'][3:]
|
||||
and record['normalized_result']['parent_evidence_id'] != record['evidence_id']
|
||||
and isinstance(record.get('evidence_links'), list)
|
||||
and record['normalized_result']['parent_evidence_id'] in record['evidence_links']
|
||||
for record in cited
|
||||
))
|
||||
)
|
||||
checks['pcap_cited_source_supports_uri_prefix'] = False
|
||||
checks['pcap_cited_capture'] = False
|
||||
if all(isinstance(record, dict) for record in cited):
|
||||
if len(cited) == 1:
|
||||
source = cited[0]['normalized_result']
|
||||
check('pcap_cited_capture', json.dumps(answer.get('capture'), sort_keys=True),
|
||||
json.dumps(pcap_complete_capture(source), sort_keys=True))
|
||||
checks['pcap_cited_source_supports_uri_prefix'] = (
|
||||
source.get('target') == reference['capture']['target']
|
||||
and isinstance(source.get('http_request_uris'), list)
|
||||
and json.dumps(source['http_request_uris'][:2], sort_keys=True) == json.dumps(prefix, sort_keys=True)
|
||||
)
|
||||
elif len(cited) == 2:
|
||||
source, page = [record['normalized_result'] for record in cited]
|
||||
capture = source.get('summary', {})
|
||||
check('pcap_cited_capture', json.dumps(answer.get('capture'), sort_keys=True),
|
||||
json.dumps(capture, sort_keys=True))
|
||||
selected_page = page.get('page', {})
|
||||
checks['pcap_cited_source_supports_uri_prefix'] = (
|
||||
source.get('kind') == 'summary'
|
||||
and source.get('parent_operation') == 'inspect_pcap_capture'
|
||||
and capture == reference['capture']
|
||||
and source.get('artifact') == {key: capture['target'][key] for key in ['path', 'sha256']}
|
||||
and isinstance(source.get('parent_evidence_id'), str)
|
||||
and page.get('parent_evidence_id') == source['parent_evidence_id']
|
||||
and page.get('parent_operation') == 'inspect_pcap_capture'
|
||||
and json.dumps(page.get('view'), sort_keys=True) == json.dumps({'kind': 'pcap', 'facet': 'http-uris', 'offset': 0, 'limit': 2}, sort_keys=True)
|
||||
and selected_page.get('facet') == 'http-uris'
|
||||
and json.dumps(selected_page.get('items'), sort_keys=True) == json.dumps(prefix, sort_keys=True)
|
||||
and page.get('source_coverage') == capture.get('coverage')
|
||||
and json.dumps(page.get('coverage'), sort_keys=True) == json.dumps(reference['delivery_observations'][-1].get('coverage'), sort_keys=True)
|
||||
and page.get('artifact') == {key: capture['target'][key] for key in ['path', 'sha256']}
|
||||
)
|
||||
else:
|
||||
checks['pcap_requested_delivery_chain'] = False
|
||||
if len(reference['delivery_observations']) == 2:
|
||||
checks['pcap_summary_coverage'] = False
|
||||
checks['pcap_observations_have_reported_ids'] = False
|
||||
checks['pcap_requested_views'] = False
|
||||
checks['pcap_cited_source_supports_uri_prefix'] = False
|
||||
checks['pcap_cited_capture'] = False
|
||||
checks['pcap_cited_subjects_match_artifact'] = False
|
||||
checks['pcap_cited_parents_linked'] = False
|
||||
checks['pcap_cited_producer_tools'] = False
|
||||
checks['pcap_cited_operations'] = False
|
||||
checks['pcap_cited_call_arguments'] = False
|
||||
checks['pcap_cited_parameters'] = False
|
||||
elif domain == 'flutter-aot':
|
||||
for key in ['target', 'abi', 'libapp', 'string_pool', 'coverage', 'limitations']:
|
||||
check(key, answer.get(key), reference[key])
|
||||
@@ -323,6 +560,7 @@ def main():
|
||||
statuses[status] = statuses.get(status, 0) + 1
|
||||
check('network_response_statuses', network['observed_response_statuses'], statuses)
|
||||
factual_checks = dict(checks)
|
||||
checks.update(workflow_checks)
|
||||
scopes = [json.loads(line) for line in args.resource_log.read_text().splitlines() if line.startswith('{')]
|
||||
scope = next(value for value in reversed(scopes) if 'resource_scope' in value)
|
||||
checks['no_resource_abort'] = scope['abort'] is None
|
||||
@@ -342,6 +580,8 @@ def main():
|
||||
'review_source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
'domain': domain, 'checks': checks, 'all_selected_checks_pass': all(checks.values()),
|
||||
'factual_checks': factual_checks, 'all_factual_checks_pass': all(factual_checks.values()),
|
||||
**({'workflow_checks': workflow_checks, 'all_workflow_checks_pass': all(workflow_checks.values())}
|
||||
if domain == 'pcap' else {}),
|
||||
'differences': differences, 'tool_errors': tool_errors, 'summary': summary,
|
||||
'fingerprints': {key: value for key, value in settings.items() if 'sha256' in key or key in ['runtime_revision', 'enabled_tools', 'authority']},
|
||||
'answer_file_sha256': hashlib.sha256((args.trial / 'answer.txt').read_bytes()).hexdigest(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Distinct capture/cleanup failures; simulated CLI is not real-model evidence."""
|
||||
|
||||
import importlib.util
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -38,6 +39,546 @@ class ManagedCoverageTests(unittest.TestCase):
|
||||
self.assertNotEqual(scope, expected)
|
||||
|
||||
|
||||
class PcapCoverageTests(unittest.TestCase):
|
||||
def test_explicit_labels_preserve_original_nulls_and_source_scope(self):
|
||||
canonical = runpy.run_path(str(Path(__file__).with_name('review-model.py')))['pcap_observation_fields']
|
||||
original = {'outer_limitations': [], 'normalized_kind': 'pcap',
|
||||
'normalized_coverage': {'examined': 2, 'total': 23, 'next_offset': 2, 'exhausted': False},
|
||||
'normalized_source_coverage': 'partial', 'normalized_parent_evidence_id': None}
|
||||
result = canonical(original)
|
||||
self.assertEqual(result['limitations'], [])
|
||||
self.assertEqual(result['source_coverage'], 'partial')
|
||||
self.assertEqual(result['coverage']['total'], 23)
|
||||
self.assertIsNone(result['parent_evidence_id'])
|
||||
self.assertIn('outer_limitations', original)
|
||||
self.assertNotIn('parent_evidence_id', canonical({}))
|
||||
|
||||
def test_conflicting_duplicates_and_json_type_substitutions_stay_failures(self):
|
||||
canonical = runpy.run_path(str(Path(__file__).with_name('review-model.py')))['pcap_observation_fields']
|
||||
for wrong in ['partial', None]:
|
||||
result = canonical({'normalized_source_coverage': 'complete', 'source_coverage': wrong})
|
||||
self.assertIn('conflicting_fields', result)
|
||||
self.assertEqual(result['normalized_source_coverage'], 'complete')
|
||||
self.assertEqual(result['source_coverage'], wrong)
|
||||
result = canonical({'normalized_coverage': {'examined': 1}, 'coverage': {'examined': True}})
|
||||
self.assertIn('conflicting_fields', result)
|
||||
self.assertEqual(canonical({'outer_limitations': [], 'limitations': []}), {'limitations': []})
|
||||
|
||||
def test_unsupported_claims_are_retained_for_exact_producer_checking(self):
|
||||
canonical = runpy.run_path(str(Path(__file__).with_name('review-model.py')))['pcap_observation_fields']
|
||||
original = {'normalized_kind': 'pcap', 'unrecorded_traffic_absent': True}
|
||||
self.assertIn('unrecorded_traffic_absent', canonical(original))
|
||||
self.assertIsNone(canonical(None))
|
||||
|
||||
|
||||
class PcapGraderWorkflowTests(unittest.TestCase):
|
||||
"""Synthetic receipts test the grader boundary, not Wireshark or real models."""
|
||||
|
||||
def fixture(self):
|
||||
parent, summary_id, page_id = ['ev_' + value * 64 for value in ['a', 'b', 'c']]
|
||||
capture = {'target': {'path': '/fixture/http.pcapng', 'sha256': 'd' * 64},
|
||||
'capture_start': None, 'coverage': 'complete',
|
||||
'retained_counts': {'http_request_uris': 3}}
|
||||
prefix = ['http://example.test/one', 'http://example.test/two']
|
||||
coverage = {'status': 'page', 'examined': 2, 'total': 3, 'next_offset': 2, 'exhausted': False}
|
||||
subject = {'digest': {'sha256': capture['target']['sha256']}, 'local_path': capture['target']['path']}
|
||||
summary = {'evidence_id': summary_id, 'operation': 'inspect_analysis_view',
|
||||
'subject': copy.deepcopy(subject), 'evidence_links': [parent],
|
||||
'limitations': ['source', 'view'],
|
||||
'normalized_result': {'kind': 'summary', 'view': {'kind': 'summary'},
|
||||
'parent_operation': 'inspect_pcap_capture', 'parent_evidence_id': parent,
|
||||
'summary': capture, 'artifact': copy.deepcopy(capture['target']),
|
||||
'coverage': {'status': 'complete-within-view', 'examined': 1, 'total': 1, 'next_offset': None, 'exhausted': True},
|
||||
'limitations': ['source', 'view'], 'unknowns': ['unrecorded traffic']}}
|
||||
page = {'evidence_id': page_id, 'operation': 'inspect_analysis_view',
|
||||
'subject': copy.deepcopy(subject), 'evidence_links': [parent],
|
||||
'limitations': ['source', 'view'],
|
||||
'normalized_result': {'kind': 'pcap',
|
||||
'view': {'kind': 'pcap', 'facet': 'http-uris', 'offset': 0, 'limit': 2},
|
||||
'parent_operation': 'inspect_pcap_capture', 'parent_evidence_id': parent,
|
||||
'page': {'facet': 'http-uris', 'items': prefix}, 'coverage': coverage,
|
||||
'source_coverage': 'complete', 'artifact': capture['target'],
|
||||
'limitations': ['source', 'view'], 'unknowns': ['unrecorded traffic']}}
|
||||
for record in [summary, page]:
|
||||
record['normalized_result']['parent_digest'] = parent[3:]
|
||||
record['parameters'] = {'parent_evidence_id': parent, 'view': copy.deepcopy(record['normalized_result']['view'])}
|
||||
observations = [self.observation(value) for value in [summary, page]]
|
||||
reference = {'artifact_sha256': 'd' * 64, 'capture': capture,
|
||||
'http_request_uris_first2': prefix, 'delivery_observations': observations}
|
||||
answer = copy.deepcopy({**reference, 'http_request_uris_first2': {
|
||||
'items': prefix, 'coverage': coverage, 'source_coverage': 'complete'},
|
||||
'evidence_ids': [summary_id, page_id]})
|
||||
return answer, [summary, page], copy.deepcopy(reference)
|
||||
|
||||
def observation(self, record):
|
||||
value = record['normalized_result']
|
||||
return {'evidence_id': record['evidence_id'], 'limitations': record['limitations'],
|
||||
'kind': value['kind'], 'view': value['view'],
|
||||
'parent_evidence_id': value['parent_evidence_id'], 'coverage': value['coverage'],
|
||||
'unknowns': value['unknowns'], 'normalized_limitations': value['limitations']}
|
||||
|
||||
def grade(self, answer, records, reference, producer_tools=None, producer_arguments=None, extra_events=None, producer_results=None):
|
||||
review = runpy.run_path(str(Path(__file__).with_name('review-model.py')))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory);trial = root / 'trial';trial.mkdir()
|
||||
for name, value in [('summary.json', {'completed_mcp_calls': []}),
|
||||
('trial-settings.json', {}), ('cleanup.json', {'profile_removed': True})]:
|
||||
(trial / name).write_text(json.dumps(value))
|
||||
(trial / 'answer.txt').write_text(json.dumps(answer))
|
||||
events = []
|
||||
for index, value in enumerate(records):
|
||||
tool = producer_tools[index] if producer_tools is not None else \
|
||||
('inspect_analysis_view' if value['normalized_result'].get('kind') == 'pcap' else 'inspect_pcap_capture')
|
||||
arguments = {'path': reference['capture']['target']['path']}
|
||||
if tool == 'inspect_analysis_view':
|
||||
normalized = value.get('normalized_result')
|
||||
parent = normalized.get('parent_evidence_id') if isinstance(normalized, dict) else None
|
||||
arguments = {'source': {'kind': 'retained-evidence', 'evidence_id': parent},
|
||||
'view': reference['delivery_observations'][-1].get('view')}
|
||||
elif len(reference['delivery_observations']) == 2:
|
||||
arguments['detail'] = 'summary'
|
||||
if producer_arguments is not None:
|
||||
arguments = producer_arguments[index]
|
||||
events.append({'type': 'item.completed', 'item': {'type': 'mcp_tool_call',
|
||||
'tool': tool, 'arguments': arguments, 'status': 'completed',
|
||||
'result': {'structuredContent': value} if producer_results is None else producer_results[index]}})
|
||||
events.extend(extra_events or [])
|
||||
(trial / 'events.jsonl').write_text(''.join(json.dumps(value) + '\n' for value in events))
|
||||
source = root / 'reference';source.mkdir();(source / 'answer.json').write_text(json.dumps(reference))
|
||||
resource = root / 'resource.log'
|
||||
resource.write_text(json.dumps({'resource_scope': 'simulated-grader-only', 'abort': None,
|
||||
'resource_counters_verified': False, 'resource_events_final': None}) + '\n')
|
||||
output = root / 'review.json'
|
||||
argv = ['review-model.py', '--domain', 'pcap', '--trial', str(trial), '--reference', str(source),
|
||||
'--resource-log', str(resource), '--output', str(output)]
|
||||
with patch.object(sys, 'argv', argv), patch('sys.stdout', io.StringIO()), self.assertRaises(SystemExit) as stopped:
|
||||
review['main']()
|
||||
self.assertEqual(stopped.exception.code, 1) # Unknown resource counters stay false.
|
||||
return json.loads(output.read_text())
|
||||
|
||||
def test_complete_grader_accepts_supported_receipts_and_equivalent_labels(self):
|
||||
answer, records, reference = self.fixture()
|
||||
self.assertTrue(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
for observation in answer['delivery_observations']:
|
||||
observation['outer_limitations'] = observation.pop('limitations')
|
||||
observation['normalized_coverage'] = observation.pop('coverage')
|
||||
self.assertTrue(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_complete_grader_rejects_missing_nulls_bad_scope_and_malformed_identity(self):
|
||||
mutations = [lambda x: x['capture'].pop('capture_start'),
|
||||
lambda x: x['http_request_uris_first2']['coverage'].__setitem__('exhausted', True),
|
||||
lambda x: x['delivery_observations'][-1].__setitem__('evidence_id', {'unexpected': 'value'}),
|
||||
lambda x: x['delivery_observations'][-1].__setitem__('unrecorded_traffic_absent', True)]
|
||||
for mutate in mutations:
|
||||
with self.subTest(mutation=mutate):
|
||||
answer, records, reference = self.fixture();mutate(answer)
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_faithful_wrong_receipts_do_not_support_the_reported_uri_prefix(self):
|
||||
for wrong in ['facet', 'range', 'parent']:
|
||||
with self.subTest(wrong=wrong):
|
||||
answer, records, reference = self.fixture()
|
||||
page = copy.deepcopy(records[-1]);page['evidence_id'] = 'ev_' + 'e' * 64
|
||||
value = page['normalized_result']
|
||||
if wrong == 'facet':
|
||||
value['view']['facet'] = 'endpoints'
|
||||
value['page'] = {'facet': 'endpoints', 'items': [{'address': '127.0.0.1'}]}
|
||||
elif wrong == 'range':
|
||||
value['view']['offset'] = 1
|
||||
value['page']['items'] = ['http://example.test/two', 'http://example.test/three']
|
||||
else:
|
||||
value['parent_evidence_id'] = 'ev_' + 'f' * 64
|
||||
records.append(page)
|
||||
answer['delivery_observations'][-1] = self.observation(page)
|
||||
answer['evidence_ids'][-1] = page['evidence_id']
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_complete_receipt_metadata_counts_and_diagnostics_support_reported_capture(self):
|
||||
answer, records, reference = self.fixture()
|
||||
capture = reference['capture']
|
||||
capture.update({'packet_count': 3, 'client': {'name': 'fixture', 'version': '1'},
|
||||
'retained_counts': {key: 0 for key in ['endpoints', 'dns_queries', 'tls_sni_values',
|
||||
'protocol_distribution', 'http_request_uris', 'unattributed_dns_addresses']},
|
||||
'diagnostics_code_units': {'tshark': 0, 'capinfos': 2}})
|
||||
capture['retained_counts']['http_request_uris'] = 3
|
||||
source = {key: value for key, value in capture.items() if key not in ['retained_counts', 'diagnostics_code_units']}
|
||||
source.update({key: [] for key in capture['retained_counts']})
|
||||
source['http_request_uris'] = reference['http_request_uris_first2'] + ['http://example.test/three']
|
||||
source['diagnostics'] = {'tshark_stderr': '', 'capinfos_stderr': '\U0001f4a5'}
|
||||
receipt = {'evidence_id': 'ev_' + 'a' * 64, 'operation': 'inspect_pcap_capture',
|
||||
'parameters': {'path': source['target']['path']},
|
||||
'subject': copy.deepcopy(records[0]['subject']), 'evidence_links': [],
|
||||
'limitations': ['source'], 'normalized_result': source}
|
||||
reference['delivery_observations'] = [{'evidence_id': receipt['evidence_id'],
|
||||
'limitations': receipt['limitations'], 'coverage': source['coverage']}]
|
||||
answer = copy.deepcopy({**reference, 'evidence_ids': [receipt['evidence_id']]})
|
||||
self.assertTrue(self.grade(answer, [receipt], reference)['all_factual_checks_pass'])
|
||||
for parameters in [None, {}, {'path': '/fixture/other.pcapng'}]:
|
||||
changed = copy.deepcopy(receipt);changed['parameters'] = parameters
|
||||
report = self.grade(answer, [changed], reference)
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_parameters'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
self.assertFalse(self.grade(answer, [receipt], reference,
|
||||
producer_arguments=[{'path': '/fixture/other.pcapng'}])['all_factual_checks_pass'])
|
||||
self.assertFalse(self.grade(answer, [receipt], reference, ['inspect_analysis_view'])['all_factual_checks_pass'])
|
||||
wrong_operation = copy.deepcopy(receipt);wrong_operation['operation'] = 'inspect_analysis_view'
|
||||
self.assertFalse(self.grade(answer, [wrong_operation], reference)['all_factual_checks_pass'])
|
||||
for subject in [None, {'digest': {'sha256': 'e' * 64}, 'local_path': source['target']['path']}]:
|
||||
changed = copy.deepcopy(receipt);changed['subject'] = subject
|
||||
report = self.grade(answer, [changed], reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_cited_capture'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_subjects_match_artifact'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
mutations = [lambda x: x.__setitem__('packet_count', 4),
|
||||
lambda x: x['client'].__setitem__('version', '2'),
|
||||
lambda x: x.__setitem__('capture_start', 'changed'),
|
||||
lambda x: x.__setitem__('coverage', 'partial'),
|
||||
lambda x: x['endpoints'].append({'address': '127.0.0.1'}),
|
||||
lambda x: x['diagnostics'].__setitem__('capinfos_stderr', '')]
|
||||
for mutate in mutations:
|
||||
changed = copy.deepcopy(receipt);mutate(changed['normalized_result'])
|
||||
report = self.grade(answer, [changed], reference)
|
||||
self.assertTrue(report['factual_checks']['capture'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_capture'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
receipt['normalized_result'].pop('diagnostics')
|
||||
reference['capture']['diagnostics_code_units'] = None
|
||||
answer['capture']['diagnostics_code_units'] = None
|
||||
self.assertTrue(self.grade(answer, [receipt], reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_cited_receipt_subjects_must_identify_the_reference_capture(self):
|
||||
for index in [0, 1]:
|
||||
for subject in [None, [], 'artifact', {}, {'digest': None},
|
||||
{'digest': {'sha256': 'e' * 64}, 'local_path': '/fixture/http.pcapng'},
|
||||
{'digest': {'sha256': 'd' * 64}, 'local_path': '/fixture/other.pcapng'}]:
|
||||
with self.subTest(index=index, subject=subject):
|
||||
answer, records, reference = self.fixture()
|
||||
records[index]['subject'] = subject
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_subjects_match_artifact'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
answer, records, reference = self.fixture();records[index].pop('subject')
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_faithful_inconsistent_page_coverage_does_not_support_the_prefix(self):
|
||||
for field, wrong in [('status', 'complete-within-view'), ('next_offset', None), ('exhausted', True)]:
|
||||
with self.subTest(field=field):
|
||||
answer, records, reference = self.fixture()
|
||||
records[-1]['normalized_result']['coverage'][field] = wrong
|
||||
answer['delivery_observations'][-1] = self.observation(records[-1])
|
||||
answer['http_request_uris_first2']['coverage'][field] = wrong
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertTrue(report['factual_checks']['pcap_prefix_coverage_observed'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_cited_coverage_preserves_exact_json_scalar_types(self):
|
||||
for field, wrong in [('exhausted', 0), ('examined', 2.0), ('total', 3.0), ('next_offset', 2.0)]:
|
||||
with self.subTest(field=field):
|
||||
answer, records, reference = self.fixture()
|
||||
records[-1]['normalized_result']['coverage'][field] = wrong
|
||||
answer['delivery_observations'][-1] = self.observation(records[-1])
|
||||
answer['http_request_uris_first2']['coverage'][field] = wrong
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertTrue(report['factual_checks']['pcap_prefix_coverage_observed'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_cited_views_preserve_exact_json_scalar_types(self):
|
||||
for field, wrong in [('offset', False), ('offset', 0.0), ('limit', 2.0), ('limit', True)]:
|
||||
with self.subTest(field=field, wrong=wrong):
|
||||
answer, records, reference = self.fixture()
|
||||
records[-1]['normalized_result']['view'][field] = wrong
|
||||
answer['delivery_observations'][-1] = self.observation(records[-1])
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['factual_checks']['pcap_requested_views'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_empty_delivery_observations_write_failed_facts(self):
|
||||
answer, records, reference = self.fixture()
|
||||
answer['delivery_observations'] = []
|
||||
answer['evidence_ids'] = []
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertFalse(report['factual_checks']['pcap_delivery_observations_nonempty'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_call_arguments'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_parameters'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_cited_view_parameters_and_call_arguments_bind_actual_requests(self):
|
||||
for index in [0, 1]:
|
||||
for parameters in [None, {}, {'parent_evidence_id': 'ev_' + 'f' * 64,
|
||||
'view': {'kind': 'summary'}}]:
|
||||
with self.subTest(index=index, parameters=parameters):
|
||||
answer, records, reference = self.fixture()
|
||||
records[index]['parameters'] = parameters
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_parameters'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
for wrong in [None, {}, {'source': {'kind': 'retained-evidence', 'evidence_id': 'ev_' + 'f' * 64},
|
||||
'view': {'kind': 'pcap', 'facet': 'http-uris', 'offset': 0, 'limit': 2}},
|
||||
{'source': {'kind': 'retained-evidence', 'evidence_id': 'ev_' + 'a' * 64},
|
||||
'view': {'kind': 'pcap', 'facet': 'http-uris', 'offset': False, 'limit': 2}}]:
|
||||
with self.subTest(arguments=wrong):
|
||||
answer, records, reference = self.fixture()
|
||||
report = self.grade(answer, records, reference,
|
||||
producer_arguments=[{'path': reference['capture']['target']['path'], 'detail': 'summary'}, wrong])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_call_arguments'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_malformed_receipt_envelopes_write_failed_facts_instead_of_crashing(self):
|
||||
tools = ['inspect_pcap_capture', 'inspect_analysis_view']
|
||||
for index in [0, 1]:
|
||||
for field, wrong in [('normalized_result', None), ('normalized_result', []),
|
||||
('normalized_result', 'capture'), ('limitations', None),
|
||||
('limitations', {}), ('limitations', [None]),
|
||||
('limitations', ['source', 1])]:
|
||||
with self.subTest(index=index, field=field, wrong=wrong):
|
||||
answer, records, reference = self.fixture();records[index][field] = wrong
|
||||
if field == 'limitations':
|
||||
answer['delivery_observations'][index]['limitations'] = copy.deepcopy(wrong)
|
||||
report = self.grade(answer, records, reference, tools)
|
||||
self.assertTrue(report['factual_checks']['capture'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
for field in ['normalized_result', 'limitations']:
|
||||
with self.subTest(index=index, missing=field):
|
||||
answer, records, reference = self.fixture();records[index].pop(field)
|
||||
self.assertFalse(self.grade(answer, records, reference, tools)['all_factual_checks_pass'])
|
||||
for index, field in [(0, 'summary'), (1, 'page')]:
|
||||
for wrong in [None, [], 'view']:
|
||||
with self.subTest(index=index, nested=field, wrong=wrong):
|
||||
answer, records, reference = self.fixture();records[index]['normalized_result'][field] = wrong
|
||||
self.assertFalse(self.grade(answer, records, reference, tools)['all_factual_checks_pass'])
|
||||
|
||||
def test_retained_parent_digest_must_match_its_declared_identity(self):
|
||||
for index in [0, 1]:
|
||||
for digest in [None, {}, [], True, 'digest', 'e' * 64]:
|
||||
with self.subTest(index=index, digest=digest):
|
||||
answer, records, reference = self.fixture()
|
||||
records[index]['normalized_result']['parent_digest'] = digest
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
answer, records, reference = self.fixture()
|
||||
records[index]['normalized_result'].pop('parent_digest')
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_summary_outer_artifact_must_identify_the_selected_capture(self):
|
||||
for wrong in [None, {}, [], 'artifact',
|
||||
{'path': '/fixture/other.pcapng', 'sha256': 'd' * 64},
|
||||
{'path': '/fixture/http.pcapng', 'sha256': 'e' * 64}]:
|
||||
with self.subTest(artifact=wrong):
|
||||
answer, records, reference = self.fixture()
|
||||
records[0]['normalized_result']['artifact'] = wrong
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
answer, records, reference = self.fixture()
|
||||
records[0]['normalized_result'].pop('artifact')
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_faithful_summary_coverage_must_describe_the_single_complete_view(self):
|
||||
for field, wrong in [('status', 'page'), ('examined', 0), ('total', 2),
|
||||
('next_offset', 0), ('exhausted', False), ('examined', 1.0)]:
|
||||
with self.subTest(field=field, wrong=wrong):
|
||||
answer, records, reference = self.fixture()
|
||||
records[0]['normalized_result']['coverage'][field] = wrong
|
||||
answer['delivery_observations'][0] = self.observation(records[0])
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_present_normalized_provenance_requires_string_arrays(self):
|
||||
for index in [0, 1]:
|
||||
for field in ['limitations', 'unknowns']:
|
||||
for wrong in [None, {}, 'scope', [None], [1]]:
|
||||
with self.subTest(index=index, field=field, wrong=wrong):
|
||||
answer, records, reference = self.fixture()
|
||||
records[index]['normalized_result'][field] = wrong
|
||||
answer['delivery_observations'][index] = self.observation(records[index])
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
for empty in [True, False]:
|
||||
answer, records, reference = self.fixture()
|
||||
key = 'normalized_limitations' if field == 'limitations' else field
|
||||
if empty:
|
||||
records[index]['normalized_result'][field] = []
|
||||
answer['delivery_observations'][index][key] = []
|
||||
else:
|
||||
records[index]['normalized_result'].pop(field)
|
||||
answer['delivery_observations'][index].pop(key)
|
||||
self.assertTrue(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_missing_or_wrong_kind_does_not_hide_malformed_view_containers(self):
|
||||
for index, container in [(0, 'summary'), (1, 'page')]:
|
||||
for kind in [None, 'unexpected']:
|
||||
for wrong in [None, [], 'view']:
|
||||
with self.subTest(index=index, kind=kind, container=container):
|
||||
answer, records, reference = self.fixture()
|
||||
if kind is None:
|
||||
records[index]['normalized_result'].pop('kind')
|
||||
else:
|
||||
records[index]['normalized_result']['kind'] = kind
|
||||
records[index]['normalized_result'][container] = wrong
|
||||
report = self.grade(answer, records, reference,
|
||||
producer_tools=['inspect_pcap_capture', 'inspect_analysis_view'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_retained_parents_require_declared_distinct_evidence_links(self):
|
||||
for index in [0, 1]:
|
||||
for links in [None, [], ['ev_' + 'f' * 64], {'parent': 'ev_' + 'a' * 64}]:
|
||||
with self.subTest(index=index, links=links):
|
||||
answer, records, reference = self.fixture();records[index]['evidence_links'] = links
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_parents_linked'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
answer, records, reference = self.fixture();records[index].pop('evidence_links')
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
answer, records, reference = self.fixture()
|
||||
parent = records[index]['evidence_id']
|
||||
for record in records:
|
||||
record['normalized_result']['parent_evidence_id'] = parent;record['evidence_links'] = [parent]
|
||||
answer['delivery_observations'] = [self.observation(record) for record in records]
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertFalse(report['factual_checks']['pcap_cited_parents_linked'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_cited_receipts_preserve_tool_and_delegated_operation_provenance(self):
|
||||
for tools in [['inspect_analysis_view', 'inspect_analysis_view'],
|
||||
['inspect_pcap_capture', 'inspect_pcap_capture'],
|
||||
['unrelated_tool', 'inspect_analysis_view']]:
|
||||
with self.subTest(tools=tools):
|
||||
answer, records, reference = self.fixture()
|
||||
report = self.grade(answer, records, reference, tools)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
for index in [0, 1]:
|
||||
answer, records, reference = self.fixture();records[index]['operation'] = 'inspect_pcap_capture'
|
||||
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_failed_reply_shapes_preserve_a_written_report_and_partial_facts(self):
|
||||
payloads = ['malformed', [1], None, False, 7, {'error': 'malformed'},
|
||||
{'error': []}, {'error': None}]
|
||||
payloads += [{'error': {'code': 'resource_constraint', 'details': value}}
|
||||
for value in [None, [], 'details']]
|
||||
payloads += [{'error': {'code': 'resource_constraint', 'details': {'reported_limits': value}}}
|
||||
for value in [None, [], 'limits']]
|
||||
for payload in payloads:
|
||||
with self.subTest(payload=payload):
|
||||
answer, records, reference = self.fixture()
|
||||
result = {'structuredContent': payload}
|
||||
event = {'type': 'item.completed', 'item': {
|
||||
'type': 'mcp_tool_call', 'tool': 'inspect_analysis_view', 'status': 'failed',
|
||||
'arguments': {'view': {'kind': 'pcap', 'facet': 'endpoints'}}, 'result': result}}
|
||||
report = self.grade(answer, records, reference, extra_events=[event])
|
||||
self.assertTrue(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report['all_workflow_checks_pass'])
|
||||
self.assertEqual(report['tool_errors'][-1]['response'], payload if payload is not None else result)
|
||||
answer, records, reference = self.fixture()
|
||||
partial_id = 'ev_' + 'e' * 64
|
||||
payload = {'error': {'code': 'resource_constraint', 'details': {
|
||||
'reported_limits': {'evidence_reference': {'evidence_id': partial_id}}}}}
|
||||
event = {'type': 'item.completed', 'item': {
|
||||
'type': 'mcp_tool_call', 'tool': 'inspect_analysis_view', 'status': 'failed',
|
||||
'arguments': {}, 'result': {'structuredContent': payload}}}
|
||||
answer['evidence_ids'].append(partial_id)
|
||||
report = self.grade(answer, records, reference, extra_events=[event])
|
||||
self.assertTrue(report['factual_checks']['reported_evidence_ids_observed_in_responses'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report['all_workflow_checks_pass'])
|
||||
|
||||
def test_workflow_citation_count_must_match_the_planned_calls(self):
|
||||
for count, made_page_call in [(1, False), (1, True), (3, True)]:
|
||||
with self.subTest(count=count, made_page_call=made_page_call):
|
||||
answer, records, reference = self.fixture()
|
||||
if count == 1:
|
||||
answer['delivery_observations'] = answer['delivery_observations'][:1]
|
||||
answer['evidence_ids'] = answer['evidence_ids'][:1]
|
||||
else:
|
||||
extra = copy.deepcopy(answer['delivery_observations'][-1])
|
||||
extra['evidence_id'] = 'ev_' + 'e' * 64
|
||||
answer['delivery_observations'].append(extra)
|
||||
answer['evidence_ids'].append(extra['evidence_id'])
|
||||
report = self.grade(answer, records if made_page_call else records[:1], reference)
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report['all_workflow_checks_pass'])
|
||||
|
||||
def test_unreadable_planned_reply_does_not_pass_workflow_acceptance(self):
|
||||
answer, records, reference = self.fixture()
|
||||
report = self.grade(answer, records, reference,
|
||||
producer_results=[{'structuredContent': records[0]}, {}])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report['all_workflow_checks_pass'])
|
||||
|
||||
def test_extra_calls_are_not_an_equivalent_workflow_even_with_correct_facts(self):
|
||||
for status in ['completed', 'failed']:
|
||||
for with_receipt in [True, False]:
|
||||
with self.subTest(status=status, with_receipt=with_receipt):
|
||||
answer, records, reference = self.fixture()
|
||||
extra = copy.deepcopy(records[-1]);extra['evidence_id'] = 'ev_' + 'e' * 64
|
||||
extra['normalized_result']['view']['facet'] = 'endpoints'
|
||||
event = {'type': 'item.completed', 'item': {
|
||||
'type': 'mcp_tool_call', 'tool': 'inspect_analysis_view', 'status': status,
|
||||
'arguments': {'source': {'kind': 'retained-evidence', 'evidence_id': 'ev_' + 'a' * 64},
|
||||
'view': extra['normalized_result']['view']},
|
||||
'result': {'structuredContent': extra} if with_receipt else {}}}
|
||||
report = self.grade(answer, records, reference, extra_events=[event])
|
||||
self.assertTrue(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report.get('all_workflow_checks_pass', True))
|
||||
answer, records, reference = self.fixture()
|
||||
report = self.grade(answer, records[::-1], reference)
|
||||
self.assertTrue(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report.get('all_workflow_checks_pass', True))
|
||||
|
||||
def test_repeated_identical_receipts_pass_but_conflicting_id_ownership_fails(self):
|
||||
answer, records, reference = self.fixture();duplicate = copy.deepcopy(records[0])
|
||||
report = self.grade(answer, [duplicate, *records], reference)
|
||||
self.assertTrue(report['all_factual_checks_pass'])
|
||||
self.assertFalse(report['all_workflow_checks_pass'])
|
||||
for duplicate_first in [True, False]:
|
||||
with self.subTest(duplicate_first=duplicate_first):
|
||||
items = [duplicate, *records] if duplicate_first else [*records, duplicate]
|
||||
tools = ['unrelated_tool', 'inspect_pcap_capture', 'inspect_analysis_view'] if duplicate_first else \
|
||||
['inspect_pcap_capture', 'inspect_analysis_view', 'unrelated_tool']
|
||||
self.assertFalse(self.grade(answer, items, reference, tools)['all_factual_checks_pass'])
|
||||
duplicate = copy.deepcopy(records[-1]);duplicate['normalized_result']['coverage']['exhausted'] = 0
|
||||
for items in [[*records, duplicate], [records[0], duplicate, records[-1]]]:
|
||||
self.assertFalse(self.grade(answer, items, reference)['all_factual_checks_pass'])
|
||||
|
||||
def test_reported_ids_are_only_the_ordered_unique_response_receipts(self):
|
||||
answer, records, reference = self.fixture()
|
||||
original = answer['evidence_ids']
|
||||
parent = records[0]['normalized_result']['parent_evidence_id']
|
||||
for ids in [original + [parent], original + [original[-1]], original[::-1],
|
||||
[parent, *original], [original], {'receipts': original},
|
||||
[*original, {'extra': parent}], original + ['not-an-id']]:
|
||||
with self.subTest(ids=ids):
|
||||
changed = copy.deepcopy(answer);changed['evidence_ids'] = ids
|
||||
report = self.grade(changed, records, reference)
|
||||
self.assertFalse(report['factual_checks']['pcap_observations_have_reported_ids'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
def test_prefix_coverage_must_come_from_the_cited_uri_receipt(self):
|
||||
answer, records, reference = self.fixture()
|
||||
extra = copy.deepcopy(records[-1]);extra['evidence_id'] = 'ev_' + 'e' * 64
|
||||
extra['normalized_result']['coverage']['next_offset'] = None
|
||||
extra['normalized_result']['coverage']['exhausted'] = True
|
||||
records.append(extra)
|
||||
answer['http_request_uris_first2']['coverage'] = extra['normalized_result']['coverage']
|
||||
report = self.grade(answer, records, reference)
|
||||
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
|
||||
self.assertTrue(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
|
||||
self.assertFalse(report['factual_checks']['pcap_prefix_coverage_observed'])
|
||||
self.assertFalse(report['all_factual_checks_pass'])
|
||||
|
||||
|
||||
class ContextCaptureTests(unittest.TestCase):
|
||||
def test_filters_ambient_account_and_nested_usage_state(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
+4
-1
@@ -16,6 +16,7 @@ prerelease_identifier="(${numeric_identifier}|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]
|
||||
build_identifier='[0-9A-Za-z-]+'
|
||||
version_pattern="${numeric_identifier}\\.${numeric_identifier}\\.${numeric_identifier}(-${prerelease_identifier}(\\.${prerelease_identifier})*)?(\\+${build_identifier}(\\.${build_identifier})*)?"
|
||||
version="${REA_VERSION:-}"
|
||||
version_requested=false
|
||||
dry_run=false
|
||||
start_setup=true
|
||||
verbose=false
|
||||
@@ -81,6 +82,7 @@ main() {
|
||||
--version)
|
||||
(($# >= 2)) || fail "--version requires a value."
|
||||
version="$2"
|
||||
version_requested=true
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
@@ -121,7 +123,7 @@ main() {
|
||||
fail_node_version "$node_version"
|
||||
fi
|
||||
|
||||
if [ -n "$version" ]; then
|
||||
if [ "$version_requested" = true ] || [ -n "$version" ]; then
|
||||
matches "$version" "^${version_pattern}$" || fail "version must be an exact semantic version."
|
||||
else
|
||||
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
|
||||
@@ -164,6 +166,7 @@ main() {
|
||||
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
|
||||
[ -x "$install_bin" ] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
|
||||
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
||||
[ -n "$installed_version" ] || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
||||
[ "$installed_version" = "$version" ] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
|
||||
|
||||
printf 'REA %s is installed.\n' "$version"
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"src/application/RealHopperAssertions.ts",
|
||||
"scripts/verify/fridaBindingsCompatibility.ts",
|
||||
"scripts/verify/termux/browser.mjs",
|
||||
"scripts/verify-real-cutter.mjs",
|
||||
"scripts/verify-real-ghidra-noreturn.mjs",
|
||||
"scripts/run-exact-tests.mjs",
|
||||
"tests/processOwnershipRunner.ts"
|
||||
],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
|
||||
@@ -2,7 +2,7 @@ REA npm dependency inventory
|
||||
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
|
||||
License identifiers are upstream lockfile declarations, not a legal assessment.
|
||||
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
|
||||
Locked package entries: 719; runtime: 175.
|
||||
Locked package entries: 721; runtime: 177.
|
||||
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
|
||||
|
||||
@algolia/abtesting@1.25.0 | development | MIT
|
||||
@@ -3000,6 +3000,11 @@ safer-buffer@2.1.2 | development | MIT
|
||||
Package: https://www.npmjs.com/package/safer-buffer/v/2.1.2
|
||||
Archive: https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz
|
||||
Integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
saxes@6.0.0 | runtime | ISC
|
||||
Lockfile path: node_modules/saxes
|
||||
Package: https://www.npmjs.com/package/saxes/v/6.0.0
|
||||
Archive: https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz
|
||||
Integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
|
||||
search-insights@2.17.3 | development | MIT
|
||||
Lockfile path: node_modules/search-insights
|
||||
Package: https://www.npmjs.com/package/search-insights/v/2.17.3
|
||||
@@ -3580,6 +3585,11 @@ xmlbuilder@15.1.1 | runtime | MIT
|
||||
Package: https://www.npmjs.com/package/xmlbuilder/v/15.1.1
|
||||
Archive: https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz
|
||||
Integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
|
||||
xmlchars@2.2.0 | runtime | MIT
|
||||
Lockfile path: node_modules/xmlchars
|
||||
Package: https://www.npmjs.com/package/xmlchars/v/2.2.0
|
||||
Archive: https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz
|
||||
Integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
yaml@2.9.0 | runtime | ISC
|
||||
Lockfile path: node_modules/yaml
|
||||
Package: https://www.npmjs.com/package/yaml/v/2.9.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
REA third-party npm runtime copyright, license and NOTICE texts
|
||||
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
|
||||
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
|
||||
Runtime dependency fingerprint (SHA-256): b0d0151958456c6e72c5720af80d783893be88a7b4c9a409db8505296fe10bd9
|
||||
Runtime dependency fingerprint (SHA-256): 49b181923ab240c68c64d35ecdd8f93cb5cd516c765f2282d0e0eca0bcadd13e
|
||||
|
||||
===== @babel/helper-string-parser@8.0.6 =====
|
||||
Declared license: MIT
|
||||
@@ -8441,3 +8441,108 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
===== saxes@6.0.0 =====
|
||||
Declared license: ISC
|
||||
Archive: https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz
|
||||
Integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
|
||||
Scope: npm runtime dependency, including optional platform packages.
|
||||
|
||||
The locked npm archive omits LICENSE; preserved from the matching v6.0.0 upstream tag.
|
||||
Source: https://raw.githubusercontent.com/lddubeau/saxes/v6.0.0/LICENSE
|
||||
SHA-256: 0fac2374380621b22e6b50451057721a9c52935b02d16d106a9f04897f061d0e
|
||||
|
||||
--- upstream LICENSE ---
|
||||
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
The following license is the one that governed sax, from which saxes
|
||||
was forked. Isaac Schlueter is not *directly* involved with saxes so
|
||||
don't go bugging him for saxes issues.
|
||||
|
||||
The ISC License
|
||||
|
||||
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
`String.fromCodePoint` by Mathias Bynens is no longer used, but it can
|
||||
still be found in old commits. It was once used according to terms of
|
||||
MIT License, as follows:
|
||||
|
||||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
===== xmlchars@2.2.0 =====
|
||||
Declared license: MIT
|
||||
Archive: https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz
|
||||
Integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
Scope: npm runtime dependency, including optional platform packages.
|
||||
|
||||
--- package/LICENSE ---
|
||||
|
||||
Copyright Louis-Dominique Dubeau and contributors to xmlchars
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -214,18 +214,32 @@ std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::ws
|
||||
}
|
||||
|
||||
static napi_value readOutput(napi_env env, Handle& read, bool& ended) {
|
||||
std::array<unsigned char, 65536> bytes;
|
||||
DWORD available = 0, count = 0;
|
||||
constexpr size_t kMaximumReadPerPoll = 1024 * 1024;
|
||||
std::array<unsigned char, 65536> chunk;
|
||||
std::vector<unsigned char> bytes;
|
||||
if (!read.valid()) ended = true;
|
||||
else if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
|
||||
require(GetLastError() == ERROR_BROKEN_PIPE, "Observe owned process output failed");
|
||||
read.reset(); ended = true;
|
||||
} else if (available > 0) {
|
||||
require(ReadFile(read.get(), bytes.data(), std::min<DWORD>(available, bytes.size()), &count, nullptr),
|
||||
"Read owned process output failed");
|
||||
else {
|
||||
while (bytes.size() < kMaximumReadPerPoll) {
|
||||
DWORD available = 0;
|
||||
if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
|
||||
require(GetLastError() == ERROR_BROKEN_PIPE,
|
||||
"Observe owned process output failed");
|
||||
read.reset();
|
||||
ended = true;
|
||||
break;
|
||||
}
|
||||
if (available == 0) break;
|
||||
const auto requested = static_cast<DWORD>(std::min<size_t>(
|
||||
{available, chunk.size(), kMaximumReadPerPoll - bytes.size()}));
|
||||
DWORD count = 0;
|
||||
require(ReadFile(read.get(), chunk.data(), requested, &count, nullptr) &&
|
||||
count > 0,
|
||||
"Read owned process output failed");
|
||||
bytes.insert(bytes.end(), chunk.begin(), chunk.begin() + count);
|
||||
}
|
||||
}
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, bytes.data(), nullptr, &result));
|
||||
check(napi_create_buffer_copy(env, bytes.size(), bytes.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Generated
+19
@@ -38,6 +38,7 @@
|
||||
"pino": "10.4.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"plist": "5.0.0",
|
||||
"saxes": "6.0.0",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "1.9.0",
|
||||
"stream-chain": "4.2.6",
|
||||
@@ -8672,6 +8673,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/search-insights": {
|
||||
"version": "2.17.3",
|
||||
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
|
||||
@@ -10585,6 +10598,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
|
||||
+2
-1
@@ -138,7 +138,7 @@
|
||||
"test": "npm run test:with-build",
|
||||
"test:with-build": "npm run test:prepare && vitest run",
|
||||
"test:run": "vitest run",
|
||||
"test:python": "python3 -m unittest bridge/native/test_rea_lldb_tracer.py -v && python3 -m unittest discover -s experiments/analysis-efficiency -p 'test_*.py' -v",
|
||||
"test:python": "python3 -m unittest bridge/native/test_rea_lldb_tracer.py bridge/mitmproxy/test_capture.py -v && python3 -m unittest discover -s experiments/analysis-efficiency -p 'test_*.py' -v",
|
||||
"test:local": "node scripts/run-development-tests.mjs local",
|
||||
"test:fast": "node scripts/ci/test-group.mjs source",
|
||||
"test:built": "node scripts/ci/test-group.mjs built",
|
||||
@@ -341,6 +341,7 @@
|
||||
"pino": "10.4.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"plist": "5.0.0",
|
||||
"saxes": "6.0.0",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "1.9.0",
|
||||
"stream-chain": "4.2.6",
|
||||
|
||||
@@ -175,6 +175,11 @@ if (tier === "reused") {
|
||||
file.startsWith(".github/ISSUE_TEMPLATE/")
|
||||
) {
|
||||
selected.docs = true;
|
||||
// Authored code and its Node package scopes belong to static checkers.
|
||||
selected.code ||=
|
||||
file.startsWith("docs/") &&
|
||||
(/\.(?:[cm]?[jt]s|[jt]sx)$/u.test(file) ||
|
||||
file.endsWith("/package.json"));
|
||||
selected.website ||= matchesAny(file, SCOPES.website);
|
||||
continue;
|
||||
}
|
||||
@@ -202,6 +207,9 @@ if (tier === "reused") {
|
||||
if (GUARDED_WORKFLOWS.has(file)) selected.code = true;
|
||||
if (file !== ".github/workflows/ci.yml") continue;
|
||||
}
|
||||
// The Notes example is also the source-owned agent evaluation fixture.
|
||||
if (file.startsWith("website/public/examples/notes-electron/"))
|
||||
selected.code = true;
|
||||
if (owners.length === 1 && owners[0] === "website") continue;
|
||||
if (/^src\/(contracts|domain)\//.test(file)) selected.docs = true;
|
||||
if (file === "package.json") {
|
||||
|
||||
@@ -79,6 +79,7 @@ export const SCOPES = {
|
||||
"tests/boundary/cli/nativeSignatureCli.test.ts",
|
||||
"tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts",
|
||||
"src/process/capture/FilesystemSnapshot.test.ts",
|
||||
"tests/process-global/filesystemSnapshotFallbackCancellation.test.ts",
|
||||
"tests/boundary/process/darwinProcessRunTokenReader.test.ts",
|
||||
"tests/boundary/process/directAnalysisSignals.test.ts",
|
||||
"tests/boundary/process/processCaptureLifecycle.test.ts",
|
||||
@@ -89,6 +90,9 @@ export const SCOPES = {
|
||||
package: [
|
||||
"src/windows/**",
|
||||
"native/windows/**",
|
||||
"src/process/capture/FilesystemSnapshot.ts",
|
||||
"src/process/capture/FilesystemSnapshot.test.ts",
|
||||
"tests/process-global/filesystemSnapshotFallbackCancellation.test.ts",
|
||||
"src/inspector/**",
|
||||
"scripts/verify/inspector/**",
|
||||
"scripts/verify-package.mjs",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writeWindowsGhidraByteSearchFixture } from "./lib/ghidra-windows-byte-search.mjs";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// Source-owned static fixtures: no compiler, target loading or execution.
|
||||
@@ -61,7 +62,7 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
}
|
||||
|
||||
bytes.write(".text", sectionTable, "ascii");
|
||||
bytes.writeUInt32LE(dll ? 0x180 : 0x50, sectionTable + 8);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 8);
|
||||
bytes.writeUInt32LE(0x1000, sectionTable + 12);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 16);
|
||||
bytes.writeUInt32LE(0x200, sectionTable + 20);
|
||||
@@ -136,6 +137,8 @@ for (const architecture of ["x86_64", "x86"]) {
|
||||
bytes.write("fixture_return42\0", 0x340, "binary");
|
||||
}
|
||||
|
||||
writeWindowsGhidraByteSearchFixture(bytes);
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await writeFile(output, bytes);
|
||||
if (!x86 && !dll) process.stdout.write(`${output}\n`);
|
||||
|
||||
@@ -91,7 +91,10 @@ const jsonScalarBytes = (value) => {
|
||||
};
|
||||
|
||||
const eventFamily = (kind, processTypeValue) => {
|
||||
if (kind === "preload" && processTypeValue === "main")
|
||||
if (
|
||||
kind === "preload" &&
|
||||
(processTypeValue === "main" || processTypeValue === "browser")
|
||||
)
|
||||
return "preload-configuration";
|
||||
if (kind.startsWith("ipc-renderer"))
|
||||
return processTypeValue === "renderer" ? "renderer-ipc" : "ipc";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface BrowserVerifierSite {
|
||||
readonly origin: string;
|
||||
triggerSessionNavigation(): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export function startBrowserVerifierSite(): Promise<BrowserVerifierSite>;
|
||||
@@ -184,11 +184,13 @@ export async function startBrowserVerifierSite() {
|
||||
async close() {
|
||||
for (const client of webSockets.clients) client.terminate();
|
||||
await new Promise((resolve) => webSockets.close(resolve));
|
||||
await new Promise((resolve, reject) =>
|
||||
const serverClosed = new Promise((resolve, reject) =>
|
||||
server.close((error) =>
|
||||
error === undefined ? resolve() : reject(error),
|
||||
),
|
||||
);
|
||||
server.closeAllConnections();
|
||||
await serverClosed;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface ManagedPeFixtureMethod {
|
||||
readonly flags?: number;
|
||||
readonly implFlags?: number;
|
||||
readonly name: string;
|
||||
/** An explicit metadata RVA; zero represents a method without an IL body. */
|
||||
readonly rva?: number;
|
||||
}
|
||||
|
||||
export interface ManagedPeFixtureOptions {
|
||||
@@ -19,6 +21,7 @@ export interface ManagedPeFixtureOptions {
|
||||
readonly pinvoke?: {
|
||||
readonly importName?: string;
|
||||
readonly mappingFlags?: number;
|
||||
readonly memberRow?: number;
|
||||
readonly moduleName?: string;
|
||||
};
|
||||
readonly readyToRun?: boolean;
|
||||
|
||||
@@ -280,7 +280,7 @@ const buildManagedMetadata = (options, methods) => {
|
||||
methodDefRow({
|
||||
name: stringsForMethods[index],
|
||||
signature: methodSignature,
|
||||
rva: method.rva ?? 0x2800 + index * 0x20,
|
||||
rva: method.rva,
|
||||
flags: method.flags ?? 0x0016,
|
||||
implFlags: method.implFlags ?? 0,
|
||||
}),
|
||||
@@ -325,13 +325,64 @@ const buildManagedMetadata = (options, methods) => {
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const align = (value, boundary) => Math.ceil(value / boundary) * boundary;
|
||||
|
||||
const managedFixtureLayout = (options, methods, metadataLength) => {
|
||||
const resourceLength =
|
||||
4 + (options.resourceData ?? Buffer.from("resource-data")).length;
|
||||
const metadataOffset = 0x0300;
|
||||
const resourceOffset = Math.max(
|
||||
0x0800,
|
||||
align(metadataOffset + metadataLength, 0x200),
|
||||
);
|
||||
const readyToRunOffset = Math.max(
|
||||
0x0900,
|
||||
align(resourceOffset + resourceLength, 0x200),
|
||||
);
|
||||
const methodOffsets = [];
|
||||
let nextMethodOffset = Math.max(
|
||||
0x0a00,
|
||||
align(readyToRunOffset + (options.readyToRun === true ? 4 : 0), 0x200),
|
||||
);
|
||||
for (const method of methods) {
|
||||
// RVA zero denotes a method without a mapped IL body, such as P/Invoke.
|
||||
if (method.rva === 0) {
|
||||
methodOffsets.push(null);
|
||||
continue;
|
||||
}
|
||||
const offset =
|
||||
method.rva === undefined
|
||||
? nextMethodOffset
|
||||
: 0x0200 + (method.rva - 0x2000);
|
||||
methodOffsets.push(offset);
|
||||
nextMethodOffset =
|
||||
Math.max(offset, nextMethodOffset) +
|
||||
align(Math.max(0x20, (method.body ?? retOnlyIlBody).length), 0x20);
|
||||
}
|
||||
return { metadataOffset, resourceOffset, readyToRunOffset, methodOffsets };
|
||||
};
|
||||
|
||||
const buildManagedImage = (options, methods, metadata) => {
|
||||
const resourceData = options.resourceData ?? Buffer.from("resource-data");
|
||||
const resourceDirectory = Buffer.concat([
|
||||
u32(resourceData.length),
|
||||
resourceData,
|
||||
]);
|
||||
const image = Buffer.alloc(0x1000);
|
||||
const { metadataOffset, resourceOffset, readyToRunOffset, methodOffsets } =
|
||||
managedFixtureLayout(options, methods, metadata.length);
|
||||
const contentEnd = Math.max(
|
||||
metadataOffset + metadata.length,
|
||||
resourceOffset + resourceDirectory.length,
|
||||
readyToRunOffset + (options.readyToRun === true ? 4 : 0),
|
||||
...methodOffsets.flatMap((offset, index) =>
|
||||
offset === null
|
||||
? []
|
||||
: [offset + (methods[index].body ?? retOnlyIlBody).length],
|
||||
),
|
||||
0x1000,
|
||||
);
|
||||
const imageSize = align(contentEnd, 0x200);
|
||||
const image = Buffer.alloc(imageSize);
|
||||
image.write("MZ", 0, "ascii");
|
||||
image.writeUInt32LE(0x80, 0x3c);
|
||||
image.writeUInt32LE(0x0000_4550, 0x80);
|
||||
@@ -351,16 +402,16 @@ const buildManagedImage = (options, methods, metadata) => {
|
||||
else image.writeUInt32LE(0x0040_0000, optional + 28);
|
||||
image.writeUInt32LE(0x1000, optional + 32);
|
||||
image.writeUInt32LE(0x200, optional + 36);
|
||||
image.writeUInt32LE(0x3000, optional + 56);
|
||||
image.writeUInt32LE(align(0x2000 + imageSize - 0x200, 0x1000), optional + 56);
|
||||
image.writeUInt32LE(0x200, optional + 60);
|
||||
image.writeUInt32LE(16, optional + directoryOffset - 4);
|
||||
image.writeUInt32LE(0x2000, optional + directoryOffset + 14 * 8);
|
||||
image.writeUInt32LE(72, optional + directoryOffset + 14 * 8 + 4);
|
||||
const section = optional + optionalSize;
|
||||
image.write(".text\0\0\0", section, "ascii");
|
||||
image.writeUInt32LE(0x1000, section + 8);
|
||||
image.writeUInt32LE(imageSize - 0x200, section + 8);
|
||||
image.writeUInt32LE(0x2000, section + 12);
|
||||
image.writeUInt32LE(0x0e00, section + 16);
|
||||
image.writeUInt32LE(imageSize - 0x200, section + 16);
|
||||
image.writeUInt32LE(0x0200, section + 20);
|
||||
image.writeUInt32LE(0x6000_0020, section + 36);
|
||||
const cli = 0x0200;
|
||||
@@ -371,27 +422,41 @@ const buildManagedImage = (options, methods, metadata) => {
|
||||
image.writeUInt32LE(metadata.length, cli + 12);
|
||||
image.writeUInt32LE(options.cliFlags ?? 1, cli + 16);
|
||||
image.writeUInt32LE(0x0600_0001, cli + 20);
|
||||
image.writeUInt32LE(0x2600, cli + 24);
|
||||
image.writeUInt32LE(0x2000 + (resourceOffset - 0x0200), cli + 24);
|
||||
image.writeUInt32LE(resourceDirectory.length, cli + 28);
|
||||
if (options.readyToRun === true) {
|
||||
image.writeUInt32LE(0x2700, cli + 64);
|
||||
image.writeUInt32LE(0x2000 + (readyToRunOffset - 0x0200), cli + 64);
|
||||
image.writeUInt32LE(4, cli + 68);
|
||||
image.write("RTR\0", 0x0900, "ascii");
|
||||
image.write("RTR\0", readyToRunOffset, "ascii");
|
||||
}
|
||||
methods.forEach((method, index) => {
|
||||
const offset = 0x0a00 + index * 0x20;
|
||||
const offset = methodOffsets[index];
|
||||
if (offset === null) return;
|
||||
(method.body ?? retOnlyIlBody).copy(image, offset);
|
||||
});
|
||||
metadata.copy(image, 0x0300);
|
||||
resourceDirectory.copy(image, 0x0800);
|
||||
metadata.copy(image, metadataOffset);
|
||||
resourceDirectory.copy(image, resourceOffset);
|
||||
return image;
|
||||
};
|
||||
|
||||
/** Build a source-owned PE/CLI fixture without committing compiled binaries. */
|
||||
export const buildManagedPeFixture = (options = {}) => {
|
||||
const methods = managedFixtureMethods(options);
|
||||
const metadata = buildManagedMetadata(options, methods);
|
||||
return buildManagedImage(options, methods, metadata);
|
||||
const provisionalMetadata = buildManagedMetadata(
|
||||
options,
|
||||
methods.map((method) => ({ ...method, rva: method.rva ?? 0x2800 })),
|
||||
);
|
||||
const layout = managedFixtureLayout(
|
||||
options,
|
||||
methods,
|
||||
provisionalMetadata.length,
|
||||
);
|
||||
const methodsWithRvas = methods.map((method, index) => ({
|
||||
...method,
|
||||
rva: method.rva ?? 0x2000 + (layout.methodOffsets[index] - 0x0200),
|
||||
}));
|
||||
const metadata = buildManagedMetadata(options, methodsWithRvas);
|
||||
return buildManagedImage(options, methodsWithRvas, metadata);
|
||||
};
|
||||
export const buildNativePeFixture = () => {
|
||||
const image = buildManagedPeFixture();
|
||||
|
||||
@@ -54,10 +54,12 @@ export async function verifyBrowserCaptureMetadataBudget(endpoint) {
|
||||
if (address === null || typeof address === "string")
|
||||
throw new Error("Browser metadata fixture did not bind a TCP port");
|
||||
fixtureOrigin = `http://127.0.0.1:${String(address.port)}`;
|
||||
// Each independent capture opens a new Debugger session. Keep each script
|
||||
// reachable so V8 cannot collect it before the later sessions attach.
|
||||
const annotations = Array.from(
|
||||
{ length: SCRIPT_COUNT },
|
||||
(_, index) =>
|
||||
`<script>globalThis["${marker}${index}"]=true;\n//# sourceMappingURL=${fixtureOrigin}/maps/${String(index)}-${"x".repeat(ANNOTATION_BYTES)}.map\n</script>`,
|
||||
`<script>globalThis["${marker}${index}"]=()=>true;\n//# sourceMappingURL=${fixtureOrigin}/maps/${String(index)}-${"x".repeat(ANNOTATION_BYTES)}.map\n</script>`,
|
||||
).join("");
|
||||
page = `<!doctype html><meta charset="utf-8">${annotations}<script>fetch("/capture-metadata-ready")</script>`;
|
||||
const fixtureUrl = `${fixtureOrigin}/capture-metadata-budget`;
|
||||
|
||||
@@ -14,36 +14,42 @@ import { webModuleTraceResultSchema } from "../../dist/domain/webModuleTrace.js"
|
||||
import { NativeModuleResolver } from "../../dist/browser/modules/NativeModuleResolver.js";
|
||||
import { startBrowserModuleSite } from "../fixtures/browser-module-site.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
/** Check native URL resolution against an independently loaded module graph through CLI/MCP. */
|
||||
export async function verifyBrowserModules(
|
||||
executable,
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
) {
|
||||
const site = await startBrowserModuleSite();
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_BROWSER_EXECUTABLE: executable,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "module-real-e2e", version: "1" });
|
||||
let site;
|
||||
let root;
|
||||
let transport;
|
||||
let client;
|
||||
let primaryError;
|
||||
try {
|
||||
site = await startBrowserModuleSite();
|
||||
root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_BROWSER_EXECUTABLE: executable,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
client = new Client({ name: "module-real-e2e", version: "1" });
|
||||
const oracle = await loadingOracle(executable, site);
|
||||
const scenario = {
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
@@ -298,11 +304,17 @@ export async function verifyBrowserModules(
|
||||
map_identity_verified: true,
|
||||
implicit_refetches: 0,
|
||||
};
|
||||
} catch (cause) {
|
||||
primaryError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await site.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
const cleanup = [];
|
||||
if (client !== undefined) cleanup.push(() => client.close());
|
||||
if (transport !== undefined) cleanup.push(() => transport.close());
|
||||
if (site !== undefined) cleanup.push(() => site.close());
|
||||
if (root !== undefined)
|
||||
cleanup.push(() => rm(root, { recursive: true, force: true }));
|
||||
await closeVerifierResources(cleanup, primaryError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,8 @@ import { CdpConnection } from "../../dist/browser/CdpConnection.js";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { traceSourceMap } from "../../dist/javascript/sourceMaps/TraceSourceMap.js";
|
||||
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
|
||||
import {
|
||||
startRuntimeFixtureBrowser,
|
||||
closeRuntimeFixtureResources,
|
||||
} from "./browser-runtime-fixture-lifecycle.mjs";
|
||||
import { startRuntimeFixtureBrowser } from "./browser-runtime-fixture-lifecycle.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
const cases = [
|
||||
{ name: "same-line nested column control", prefixes: ["\n\n ", " "] },
|
||||
@@ -184,7 +182,7 @@ async function verifyCase(executable, entrypoint, scenario) {
|
||||
primaryError = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
await closeRuntimeFixtureResources(
|
||||
await closeVerifierResources(
|
||||
[() => connection?.close(), () => fixture?.close(), () => site.close()],
|
||||
primaryError,
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../fixtures/browser-network-site.mjs";
|
||||
import { assertBrowserNetworkEvidence } from "./browser-network-assertions.mjs";
|
||||
import { scenarioProfiles } from "./browser-scenario-verifier.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
/** Exercise selected network content through real CLI and stdio MCP, one browser at a time. */
|
||||
export async function verifyBrowserNetworkEvidence(
|
||||
@@ -21,9 +22,14 @@ export async function verifyBrowserNetworkEvidence(
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
attachment,
|
||||
) {
|
||||
const site = await startBrowserNetworkSite();
|
||||
const profilesBefore = await scenarioProfiles();
|
||||
let site;
|
||||
let profilesBefore;
|
||||
let transport;
|
||||
let client;
|
||||
let primaryError;
|
||||
try {
|
||||
site = await startBrowserNetworkSite();
|
||||
profilesBefore = await scenarioProfiles();
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url: site.origin },
|
||||
@@ -97,58 +103,50 @@ export async function verifyBrowserNetworkEvidence(
|
||||
() => assertBrowserNetworkEvidence(swapped, site.origin),
|
||||
/associated with a different same-URL request/,
|
||||
);
|
||||
const transport = new StdioClientTransport({
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "network-real-e2e", version: "1" });
|
||||
client = new Client({ name: "network-real-e2e", version: "1" });
|
||||
let mcpProof;
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const result = await client.callTool(
|
||||
{ name: "capture_browser_scenario", arguments: scenario },
|
||||
await client.connect(transport);
|
||||
const result = await client.callTool(
|
||||
{ name: "capture_browser_scenario", arguments: scenario },
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const evidence = parseEvidence(result.structuredContent);
|
||||
mcpProof = assertBrowserNetworkEvidence(
|
||||
browserScenarioCaptureSchema.parse(evidence.normalized_result),
|
||||
site.origin,
|
||||
);
|
||||
if (attachment !== undefined) {
|
||||
const connectedScenario = browserScenarioSchema.parse({
|
||||
...scenario,
|
||||
browser: { mode: "connect", ...attachment },
|
||||
});
|
||||
const connected = await client.callTool(
|
||||
{ name: "capture_browser_scenario", arguments: connectedScenario },
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const evidence = parseEvidence(result.structuredContent);
|
||||
mcpProof = assertBrowserNetworkEvidence(
|
||||
browserScenarioCaptureSchema.parse(evidence.normalized_result),
|
||||
site.origin,
|
||||
);
|
||||
if (attachment !== undefined) {
|
||||
const connectedScenario = browserScenarioSchema.parse({
|
||||
...scenario,
|
||||
browser: { mode: "connect", ...attachment },
|
||||
});
|
||||
const connected = await client.callTool(
|
||||
{ name: "capture_browser_scenario", arguments: connectedScenario },
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(connected.isError, true, JSON.stringify(connected));
|
||||
const connectedEvidence = parseEvidence(connected.structuredContent);
|
||||
assert.deepEqual(
|
||||
assertBrowserNetworkEvidence(
|
||||
browserScenarioCaptureSchema.parse(
|
||||
connectedEvidence.normalized_result,
|
||||
),
|
||||
site.origin,
|
||||
"disconnected-external",
|
||||
assert.notEqual(connected.isError, true, JSON.stringify(connected));
|
||||
const connectedEvidence = parseEvidence(connected.structuredContent);
|
||||
assert.deepEqual(
|
||||
assertBrowserNetworkEvidence(
|
||||
browserScenarioCaptureSchema.parse(
|
||||
connectedEvidence.normalized_result,
|
||||
),
|
||||
cliProof,
|
||||
);
|
||||
assert.ok(
|
||||
(await fetch(`${attachment.cdp_endpoint}/json/version`)).ok,
|
||||
"Network capture closed the external browser",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
site.origin,
|
||||
"disconnected-external",
|
||||
),
|
||||
cliProof,
|
||||
);
|
||||
assert.ok(
|
||||
(await fetch(`${attachment.cdp_endpoint}/json/version`)).ok,
|
||||
"Network capture closed the external browser",
|
||||
);
|
||||
}
|
||||
assert.deepEqual(mcpProof, cliProof);
|
||||
for (const path of [
|
||||
@@ -180,7 +178,14 @@ export async function verifyBrowserNetworkEvidence(
|
||||
profile_cleanup: true,
|
||||
connected_network_verified: attachment !== undefined,
|
||||
};
|
||||
} catch (cause) {
|
||||
primaryError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await site.close();
|
||||
const cleanup = [];
|
||||
if (client !== undefined) cleanup.push(() => client.close());
|
||||
if (transport !== undefined) cleanup.push(() => transport.close());
|
||||
if (site !== undefined) cleanup.push(() => site.close());
|
||||
await closeVerifierResources(cleanup, primaryError);
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,10 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webExecutionSchema } from "../../dist/domain/webExecution.js";
|
||||
import { webEventListenersSchema } from "../../dist/domain/webEventListeners.js";
|
||||
import { startBrowserRuntimeSite } from "../fixtures/browser-runtime-site.mjs";
|
||||
import { requireOrdinaryWindowsVerifierCaller } from "./windows-ordinary-user-verifier.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import {
|
||||
startRuntimeFixtureBrowser,
|
||||
closeRuntimeFixtureResources,
|
||||
} from "./browser-runtime-fixture-lifecycle.mjs";
|
||||
import { startRuntimeFixtureBrowser } from "./browser-runtime-fixture-lifecycle.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
import { verifyRuntimeTargetTermination } from "./browser-runtime-termination-e2e.mjs";
|
||||
import { verifyRuntimeInvalidSelectors } from "./browser-runtime-selector-e2e.mjs";
|
||||
import {
|
||||
@@ -37,6 +36,7 @@ export async function verifyBrowserRuntime(
|
||||
executable,
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
) {
|
||||
await requireOrdinaryWindowsVerifierCaller();
|
||||
await access(executable);
|
||||
const site = await startBrowserRuntimeSite();
|
||||
let fixture;
|
||||
@@ -180,7 +180,7 @@ export async function verifyBrowserRuntime(
|
||||
primaryError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await closeRuntimeFixtureResources(
|
||||
await closeVerifierResources(
|
||||
[
|
||||
() => client?.close(),
|
||||
() => action?.close(),
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
|
||||
waitForBrowserDevtoolsPort,
|
||||
} from "../../dist/browser/BrowserProcessStartup.js";
|
||||
import { requireOrdinaryWindowsVerifierCaller } from "./windows-ordinary-user-verifier.mjs";
|
||||
|
||||
/** Launch the synthetic fixture browser and await CDP in its own verified process group. */
|
||||
export async function startRuntimeFixtureBrowser(executable, origin) {
|
||||
await requireOrdinaryWindowsVerifierCaller();
|
||||
const profile = await mkdtemp(join(tmpdir(), "rea-real-web-runtime-"));
|
||||
let owned;
|
||||
try {
|
||||
@@ -151,20 +153,3 @@ async function stopOwnedBrowser(owned, profile) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempt every owned cleanup and retain the original verification failure if cleanup also fails. */
|
||||
export async function closeRuntimeFixtureResources(tasks, primaryError) {
|
||||
const failures = [];
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
await task();
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
if (failures.length > 0)
|
||||
throw new AggregateError(
|
||||
primaryError === undefined ? failures : [primaryError, ...failures],
|
||||
"Runtime fixture cleanup failed",
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import { closeRuntimeFixtureResources } from "./browser-runtime-fixture-lifecycle.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
/** Close a separately owned fixture page after actual MCP arming and inspect native cleanup diagnostics. */
|
||||
export async function verifyRuntimeTargetTermination(client, input, origin) {
|
||||
@@ -76,7 +76,7 @@ export async function verifyRuntimeTargetTermination(client, input, origin) {
|
||||
primaryError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await closeRuntimeFixtureResources(
|
||||
await closeVerifierResources(
|
||||
[
|
||||
() =>
|
||||
targetId === undefined || closed
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { once } from "node:events";
|
||||
import { createServer, request } from "node:http";
|
||||
import { connect } from "node:net";
|
||||
|
||||
import { createBrowserScenarioProvider } from "../../dist/composition/browserScenario.js";
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { PlaywrightScenarioServiceWorkers } from "../../dist/browser/PlaywrightScenarioServiceWorkers.js";
|
||||
|
||||
const beforeDeadline = async (promise, label, timeoutMs = 2_000) => {
|
||||
let timer;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error(`${label} did not settle`)),
|
||||
timeoutMs,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
|
||||
// Real loopback proxy: Playwright's first transport is forwarded to Chrome,
|
||||
// then only the policy owner's discovery/handshake is deliberately withheld.
|
||||
const startProxy = async (endpoint, mode, ownerOnly = false) => {
|
||||
const upstream = new URL(endpoint);
|
||||
const sockets = new Set();
|
||||
let versions = 0;
|
||||
let upgrades = 0;
|
||||
let enabled = true;
|
||||
let entered;
|
||||
const stalled = new Promise((resolve) => {
|
||||
entered = resolve;
|
||||
});
|
||||
let closed;
|
||||
let stalledClose;
|
||||
const stall = (socket) => {
|
||||
socket.resume();
|
||||
socket.once("end", () => socket.end());
|
||||
stalledClose = new Promise((resolve) => {
|
||||
closed = resolve;
|
||||
});
|
||||
socket.once("close", () => closed());
|
||||
entered();
|
||||
};
|
||||
const server = createServer((incoming, response) => {
|
||||
const versionRequest = incoming.url.replace(/\/$/, "") === "/json/version";
|
||||
if (versionRequest) versions += 1;
|
||||
if (
|
||||
enabled &&
|
||||
mode === "http-discovery" &&
|
||||
versions === 2 &&
|
||||
versionRequest
|
||||
) {
|
||||
stall(incoming.socket);
|
||||
return;
|
||||
}
|
||||
const outgoing = request(new URL(incoming.url, upstream), (received) => {
|
||||
const chunks = [];
|
||||
received.on("data", (chunk) => chunks.push(chunk));
|
||||
received.on("end", () => {
|
||||
const content = Buffer.concat(chunks).toString("utf8");
|
||||
response.writeHead(received.statusCode, {
|
||||
"content-type": "application/json",
|
||||
});
|
||||
response.end(
|
||||
content.replaceAll(
|
||||
`127.0.0.1:${upstream.port}`,
|
||||
`127.0.0.1:${server.address().port}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
outgoing.on("error", () => response.destroy());
|
||||
response.on("close", () => outgoing.destroy());
|
||||
outgoing.end();
|
||||
});
|
||||
server.on("connection", (socket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
});
|
||||
server.on("upgrade", (incoming, socket, head) => {
|
||||
upgrades += 1;
|
||||
if (
|
||||
enabled &&
|
||||
mode !== "http-discovery" &&
|
||||
upgrades === (ownerOnly ? 1 : 2)
|
||||
) {
|
||||
stall(socket);
|
||||
return;
|
||||
}
|
||||
const outgoing = connect(Number(upstream.port), "127.0.0.1", () => {
|
||||
const headers = incoming.rawHeaders.flatMap((value, index, all) =>
|
||||
index % 2 === 0 ? [`${value}: ${all[index + 1]}`] : [],
|
||||
);
|
||||
outgoing.write(
|
||||
`${incoming.method} ${incoming.url} HTTP/${incoming.httpVersion}\r\n${headers.join("\r\n")}\r\n\r\n`,
|
||||
);
|
||||
if (head.length > 0) outgoing.write(head);
|
||||
outgoing.pipe(socket);
|
||||
socket.pipe(outgoing);
|
||||
});
|
||||
outgoing.on("error", () => socket.destroy());
|
||||
socket.on("error", () => outgoing.destroy());
|
||||
socket.on("close", () => outgoing.destroy());
|
||||
});
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||
let proxyEndpoint = origin;
|
||||
if (mode === "browser-websocket") {
|
||||
const version = await (await fetch(`${endpoint}/json/version`)).json();
|
||||
proxyEndpoint = version.webSocketDebuggerUrl.replace(
|
||||
`127.0.0.1:${upstream.port}`,
|
||||
`127.0.0.1:${server.address().port}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
endpoint: proxyEndpoint,
|
||||
stalled,
|
||||
waitClosed: () => stalledClose,
|
||||
resume: () => {
|
||||
enabled = false;
|
||||
},
|
||||
close: async () => {
|
||||
const done = once(server, "close");
|
||||
server.close();
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await done;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const verifyScenarioPolicySetup = async (
|
||||
endpoint,
|
||||
targetId,
|
||||
origin,
|
||||
page,
|
||||
) => {
|
||||
const result = {};
|
||||
for (const mode of ["http-websocket", "http-discovery", "deadline"]) {
|
||||
const proxy = await startProxy(endpoint, mode);
|
||||
const provider = createBrowserScenarioProvider(process.env);
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: {
|
||||
mode: "connect",
|
||||
cdp_endpoint: proxy.endpoint,
|
||||
target_id: targetId,
|
||||
},
|
||||
start_url: { url: origin },
|
||||
environment: { service_workers: "block" },
|
||||
actions: [
|
||||
{ step_id: "settle", action: "wait_for_timeout", duration_ms: 1 },
|
||||
],
|
||||
capture: { events: [], after_each_step: ["dom"], at_end: ["dom"] },
|
||||
});
|
||||
const cancellation = new AbortController();
|
||||
let failure;
|
||||
try {
|
||||
const pending = provider.captureScenario(scenario, {
|
||||
signal: cancellation.signal,
|
||||
});
|
||||
await beforeDeadline(proxy.stalled, `${mode} setup`);
|
||||
if (mode !== "deadline") cancellation.abort();
|
||||
const cancelled = await beforeDeadline(
|
||||
pending,
|
||||
`${mode} termination`,
|
||||
mode === "deadline" ? 12_000 : 2_000,
|
||||
);
|
||||
assert.equal(cancelled.ok, false);
|
||||
assert.equal(cancelled.error.cleanupIncomplete, false);
|
||||
if (mode === "deadline") assert.equal(cancelled.error.reason, "timeout");
|
||||
await beforeDeadline(proxy.waitClosed(), `${mode} socket release`);
|
||||
assert.equal(
|
||||
await page.evaluate(() =>
|
||||
navigator.serviceWorker.register.toString().includes("[native code]"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
proxy.resume();
|
||||
const next = await beforeDeadline(
|
||||
provider.captureScenario(scenario),
|
||||
`${mode} target reacquisition`,
|
||||
);
|
||||
if (!next.ok) throw next.error;
|
||||
assert.equal(next.value.browser.cleanup, "disconnected-external");
|
||||
result[mode] = {
|
||||
...(mode === "deadline" ? { timed_out: true } : { cancelled: true }),
|
||||
socket_closed: true,
|
||||
target_reused: true,
|
||||
};
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
try {
|
||||
await proxy.close();
|
||||
} finally {
|
||||
try {
|
||||
await provider.close();
|
||||
} catch (cleanupFailure) {
|
||||
if (failure !== undefined)
|
||||
throw new AggregateError([failure, cleanupFailure]);
|
||||
throw cleanupFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const proxy = await startProxy(endpoint, "browser-websocket", true);
|
||||
const policy = new PlaywrightScenarioServiceWorkers(
|
||||
page,
|
||||
proxy.endpoint,
|
||||
targetId,
|
||||
);
|
||||
try {
|
||||
const rejected = assert.rejects(
|
||||
policy.initialize(),
|
||||
(error) => error._tag === "AnalysisCancelledError",
|
||||
);
|
||||
await beforeDeadline(proxy.stalled, "browser websocket approval handshake");
|
||||
await beforeDeadline(policy.close(), "browser websocket cleanup");
|
||||
await rejected;
|
||||
await beforeDeadline(
|
||||
proxy.waitClosed(),
|
||||
"browser websocket socket release",
|
||||
);
|
||||
proxy.resume();
|
||||
const next = new PlaywrightScenarioServiceWorkers(
|
||||
page,
|
||||
proxy.endpoint,
|
||||
targetId,
|
||||
);
|
||||
try {
|
||||
await beforeDeadline(
|
||||
next.initialize(),
|
||||
"browser websocket reacquisition",
|
||||
);
|
||||
} finally {
|
||||
await next.close();
|
||||
}
|
||||
result["browser-websocket"] = {
|
||||
cancelled: true,
|
||||
socket_closed: true,
|
||||
target_reused: true,
|
||||
direct_owner: true,
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
await policy.close();
|
||||
} finally {
|
||||
await proxy.close();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,605 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { once } from "node:events";
|
||||
import { createServer } from "node:http";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
import { chromium } from "playwright-core";
|
||||
|
||||
import { PlaywrightScenarioSession } from "../../dist/browser/PlaywrightScenarioSession.js";
|
||||
import { PlaywrightScenarioServiceWorkers } from "../../dist/browser/PlaywrightScenarioServiceWorkers.js";
|
||||
import { createBrowserScenarioProvider } from "../../dist/composition/browserScenario.js";
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { runScenarioCli } from "./browser-scenario-verifier.mjs";
|
||||
import { verifyScenarioPolicySetup } from "./browser-scenario-service-worker-setup-e2e.mjs";
|
||||
|
||||
const startSite = async () => {
|
||||
let otherOrigin;
|
||||
const server = createServer((request, response) => {
|
||||
const path = new URL(request.url, "http://localhost").pathname;
|
||||
if (path === "/sw.js") {
|
||||
response.setHeader("content-type", "application/javascript");
|
||||
response.end(`self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", event => event.waitUntil(clients.claim()));
|
||||
self.addEventListener("fetch", event => {
|
||||
if (new URL(event.request.url).pathname === "/probe")
|
||||
event.respondWith(new Response("worker"));
|
||||
});`);
|
||||
} else if (path === "/probe") {
|
||||
response.end("network");
|
||||
} else if (path === "/failed-navigation") {
|
||||
response.destroy();
|
||||
} else if (path === "/stalled-image.png") {
|
||||
response.writeHead(200, { "content-type": "image/png" });
|
||||
response.flushHeaders();
|
||||
} else if (path === "/pending-image" || path === "/pending-child") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(
|
||||
'<!doctype html><img id="pending-image" src="/stalled-image.png">',
|
||||
);
|
||||
} else if (path === "/pending-frame") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(
|
||||
'<!doctype html><iframe id="pending-frame" src="/pending-child"></iframe>',
|
||||
);
|
||||
} else {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(`<!doctype html><p id="state">ready</p>
|
||||
<a id="navigate" href="${otherOrigin}/">Navigate</a>
|
||||
${path === "/frame" ? `<iframe src="${otherOrigin}/"></iframe>` : ""}
|
||||
${
|
||||
path === "/register"
|
||||
? `<script>
|
||||
navigator.serviceWorker.register('/sw.js').then(
|
||||
() => document.querySelector('#state').textContent = 'registered',
|
||||
() => {
|
||||
document.querySelector('#state').textContent = 'blocked';
|
||||
document.querySelector('#state').dataset.blocked = 'true';
|
||||
});
|
||||
</script>`
|
||||
: ""
|
||||
}`);
|
||||
}
|
||||
});
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
return {
|
||||
origin: `http://127.0.0.1:${server.address().port}`,
|
||||
setOtherOrigin: (origin) => {
|
||||
otherOrigin = origin;
|
||||
},
|
||||
close: async () => {
|
||||
const closed = once(server, "close");
|
||||
server.close();
|
||||
server.closeAllConnections();
|
||||
await closed;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Real attached-page policy checks, with caller-owned workers on a second origin. */
|
||||
export async function verifyScenarioServiceWorkers(endpoint, targetId) {
|
||||
const first = await startSite();
|
||||
const second = await startSite();
|
||||
first.setOtherOrigin(second.origin);
|
||||
second.setOtherOrigin(first.origin);
|
||||
const observer = await chromium.connectOverCDP(endpoint);
|
||||
let provider = createBrowserScenarioProvider(process.env);
|
||||
let session;
|
||||
let failure;
|
||||
try {
|
||||
let page;
|
||||
for (const context of observer.contexts()) {
|
||||
for (const candidate of context.pages()) {
|
||||
const cdp = await context.newCDPSession(candidate);
|
||||
const { targetInfo } = await cdp.send("Target.getTargetInfo");
|
||||
await cdp.detach();
|
||||
if (targetInfo.targetId === targetId) page = candidate;
|
||||
}
|
||||
}
|
||||
assert.ok(page, "External fixture page disappeared");
|
||||
const scenarioFor = (url, actions = [], policy = "block") =>
|
||||
browserScenarioSchema.parse({
|
||||
browser: {
|
||||
mode: "connect",
|
||||
cdp_endpoint: endpoint,
|
||||
target_id: targetId,
|
||||
},
|
||||
start_url: { url },
|
||||
environment: { service_workers: policy },
|
||||
actions:
|
||||
actions.length === 0
|
||||
? [
|
||||
{
|
||||
step_id: "settle",
|
||||
action: "wait_for_timeout",
|
||||
duration_ms: 1,
|
||||
},
|
||||
]
|
||||
: actions,
|
||||
capture: { events: [], after_each_step: ["dom"], at_end: ["dom"] },
|
||||
});
|
||||
const assertRestored = async () => {
|
||||
assert.equal(page.isClosed(), false);
|
||||
assert.equal(
|
||||
await page.evaluate(() =>
|
||||
ServiceWorkerContainer.prototype.register
|
||||
.toString()
|
||||
.includes("[native code]"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
};
|
||||
const assertRejected = async (scenario) => {
|
||||
const result = await provider.captureScenario(scenario);
|
||||
assert.equal(
|
||||
result.ok,
|
||||
false,
|
||||
"Worker-influenced capture unexpectedly succeeded",
|
||||
);
|
||||
assert.equal(result.error.reason, "target_not_allowed");
|
||||
assert.equal(
|
||||
result.error.partialObservation.capture.browser.cleanup,
|
||||
"disconnected-external",
|
||||
);
|
||||
await assertRestored();
|
||||
};
|
||||
|
||||
// A fresh attached origin blocks registration, then restores both the
|
||||
// current document and future documents when its transport disconnects.
|
||||
const clean = scenarioFor(`${first.origin}/register`, [
|
||||
{
|
||||
step_id: "registration-rejected",
|
||||
action: "wait_for",
|
||||
locator: { kind: "css", selector: "#state[data-blocked='true']" },
|
||||
state: "visible",
|
||||
},
|
||||
]);
|
||||
const cleanCapture = (await runScenarioCli(clean)).normalized_result;
|
||||
assert.ok(cleanCapture.steps.every(({ status }) => status === "completed"));
|
||||
assert.equal(cleanCapture.browser.cleanup, "disconnected-external");
|
||||
await assertRestored();
|
||||
await page.reload();
|
||||
await page.getByText("registered", { exact: true }).waitFor();
|
||||
await page.evaluate(async () => {
|
||||
for (const registration of await navigator.serviceWorker.getRegistrations())
|
||||
await registration.unregister();
|
||||
});
|
||||
await page.goto("about:blank");
|
||||
|
||||
// The caller installs a real worker at a different origin, then moves the
|
||||
// selected target away before REA attaches. Its registration must survive.
|
||||
await page.goto(second.origin);
|
||||
await page.evaluate(async () => {
|
||||
await navigator.serviceWorker.register("/sw.js");
|
||||
await navigator.serviceWorker.ready;
|
||||
});
|
||||
await page.reload();
|
||||
assert.equal(
|
||||
await page.evaluate(async () => (await fetch("/probe")).text()),
|
||||
"worker",
|
||||
);
|
||||
await page.goto("about:blank");
|
||||
await assertRejected(scenarioFor(second.origin));
|
||||
await page.goto("about:blank");
|
||||
await assertRejected(
|
||||
scenarioFor(first.origin, [
|
||||
{
|
||||
step_id: "cross-origin",
|
||||
action: "goto",
|
||||
destination: { url: second.origin },
|
||||
wait_until: "load",
|
||||
},
|
||||
]),
|
||||
);
|
||||
await page.goto("about:blank");
|
||||
await assertRejected(
|
||||
scenarioFor(first.origin, [
|
||||
{
|
||||
step_id: "link-navigation",
|
||||
action: "click",
|
||||
locator: { kind: "css", selector: "#navigate" },
|
||||
},
|
||||
{ step_id: "loaded", action: "wait_for_timeout", duration_ms: 100 },
|
||||
]),
|
||||
);
|
||||
await page.goto("about:blank");
|
||||
await assertRejected(scenarioFor(`${first.origin}/frame`));
|
||||
|
||||
const allowed = await provider.captureScenario(
|
||||
scenarioFor(second.origin, [], "allow"),
|
||||
);
|
||||
if (!allowed.ok) throw allowed.error;
|
||||
assert.ok(
|
||||
allowed.value.steps.every(({ status }) => status === "completed"),
|
||||
);
|
||||
assert.equal(
|
||||
await page.evaluate(async () => (await fetch("/probe")).text()),
|
||||
"worker",
|
||||
);
|
||||
assert.equal(
|
||||
await page.evaluate(
|
||||
async () => (await navigator.serviceWorker.getRegistrations()).length,
|
||||
),
|
||||
1,
|
||||
);
|
||||
|
||||
// Restoration also runs after navigation failure and active cancellation.
|
||||
await page.goto("about:blank");
|
||||
const failed = await provider.captureScenario(
|
||||
scenarioFor(`${first.origin}/failed-navigation`),
|
||||
);
|
||||
assert.equal(failed.ok, false);
|
||||
// Chromium completes its error-document transition after goto rejects.
|
||||
await page.waitForSelector("#main-frame-error", { state: "attached" });
|
||||
// A context replacement can defer restoration. Retained cleanup must
|
||||
// restore the replacement document before releasing the external page.
|
||||
await provider.close();
|
||||
provider = createBrowserScenarioProvider(process.env);
|
||||
await page.goto(first.origin);
|
||||
await assertRestored();
|
||||
const cancellation = new AbortController();
|
||||
const pending = provider.captureScenario(
|
||||
scenarioFor(`${first.origin}/register`, [
|
||||
{
|
||||
step_id: "wait",
|
||||
action: "wait_for",
|
||||
locator: { kind: "css", selector: "#absent" },
|
||||
state: "visible",
|
||||
timeout_ms: 30_000,
|
||||
},
|
||||
]),
|
||||
{ signal: cancellation.signal },
|
||||
);
|
||||
try {
|
||||
const deadline = Date.now() + 10_000;
|
||||
while ((await page.locator("#state").textContent()) !== "blocked") {
|
||||
assert.ok(
|
||||
Date.now() < deadline,
|
||||
"Scenario never applied registration policy",
|
||||
);
|
||||
await delay(25);
|
||||
}
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
}
|
||||
const cancelled = await pending;
|
||||
assert.ok(
|
||||
!cancelled.ok ||
|
||||
cancelled.value.steps.some(({ status }) => status === "cancelled"),
|
||||
);
|
||||
await assertRestored();
|
||||
|
||||
// Caller changes made after installation belong to the caller, too.
|
||||
session = await PlaywrightScenarioSession.open(
|
||||
scenarioFor(first.origin),
|
||||
process.env,
|
||||
{},
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
ServiceWorkerContainer.prototype.register =
|
||||
function callerReplacement() {};
|
||||
});
|
||||
await session.close();
|
||||
session = undefined;
|
||||
assert.equal(
|
||||
await page.evaluate(() => ServiceWorkerContainer.prototype.register.name),
|
||||
"callerReplacement",
|
||||
);
|
||||
await page.reload();
|
||||
await assertRestored();
|
||||
await page.evaluate(() => {
|
||||
delete ServiceWorkerContainer.prototype.register;
|
||||
});
|
||||
const policyBrowser = await chromium.connectOverCDP(endpoint);
|
||||
try {
|
||||
let policyPage;
|
||||
for (const context of policyBrowser.contexts()) {
|
||||
for (const candidate of context.pages()) {
|
||||
const cdp = await context.newCDPSession(candidate);
|
||||
const { targetInfo } = await cdp.send("Target.getTargetInfo");
|
||||
await cdp.detach();
|
||||
if (targetInfo.targetId === targetId) policyPage = candidate;
|
||||
}
|
||||
}
|
||||
assert.ok(policyPage);
|
||||
const policy = new PlaywrightScenarioServiceWorkers(
|
||||
policyPage,
|
||||
endpoint,
|
||||
targetId,
|
||||
);
|
||||
await policy.initialize();
|
||||
await policy.close();
|
||||
} finally {
|
||||
await policyBrowser.close();
|
||||
}
|
||||
assert.equal(
|
||||
await page.evaluate(() =>
|
||||
Object.hasOwn(ServiceWorkerContainer.prototype, "register"),
|
||||
),
|
||||
false,
|
||||
);
|
||||
await page.reload();
|
||||
await assertRestored();
|
||||
const policyCleanup = await verifyPolicyCleanup({
|
||||
page,
|
||||
endpoint,
|
||||
targetId,
|
||||
provider,
|
||||
scenarioFor,
|
||||
origin: first.origin,
|
||||
});
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
production_provider: true,
|
||||
initial_navigation: true,
|
||||
action_navigation: true,
|
||||
cross_origin_frame: true,
|
||||
allow_preserves_existing_worker: true,
|
||||
registration_blocked: true,
|
||||
live_document_and_reload_restored: true,
|
||||
failure_and_cancellation_cleanup: true,
|
||||
caller_replacement_preserved: true,
|
||||
caller_original_absence_preserved: true,
|
||||
policyCleanup,
|
||||
external_target_preserved: true,
|
||||
};
|
||||
} catch (cause) {
|
||||
failure = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await session?.close();
|
||||
try {
|
||||
await provider.close();
|
||||
} catch (cleanupFailure) {
|
||||
if (failure !== undefined)
|
||||
throw new AggregateError([failure, cleanupFailure]);
|
||||
throw cleanupFailure;
|
||||
} finally {
|
||||
await observer.close();
|
||||
await first.close();
|
||||
await second.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const verifyPolicyCleanup = async ({
|
||||
page,
|
||||
endpoint,
|
||||
targetId,
|
||||
provider,
|
||||
scenarioFor,
|
||||
origin,
|
||||
}) => {
|
||||
const nativeRegister = async () =>
|
||||
page.evaluate(() =>
|
||||
navigator.serviceWorker.register.toString().includes("[native code]"),
|
||||
);
|
||||
for (const path of ["/pending-image", "/pending-frame"]) {
|
||||
const cancellation = new AbortController();
|
||||
const pending = provider.captureScenario(scenarioFor(`${origin}${path}`), {
|
||||
signal: cancellation.signal,
|
||||
});
|
||||
try {
|
||||
await page
|
||||
.locator(
|
||||
path === "/pending-image" ? "#pending-image" : "#pending-frame",
|
||||
)
|
||||
.waitFor({ state: "attached" });
|
||||
if (path === "/pending-frame") {
|
||||
const child = page
|
||||
.frames()
|
||||
.find((frame) => frame.parentFrame() !== null);
|
||||
assert.ok(child);
|
||||
await child.waitForURL(`${origin}/pending-child`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
cancellation.abort();
|
||||
}
|
||||
const result = await pending;
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(
|
||||
result.error.partialObservation.capture.browser.cleanup,
|
||||
"disconnected-external",
|
||||
);
|
||||
assert.equal(await nativeRegister(), true);
|
||||
const next = await provider.captureScenario(scenarioFor(origin));
|
||||
if (!next.ok) throw next.error;
|
||||
assert.equal(next.value.browser.cleanup, "disconnected-external");
|
||||
}
|
||||
|
||||
await page.addInitScript("globalThis.reaCallerInitScript = 'preserved'");
|
||||
const policy = new PlaywrightScenarioServiceWorkers(page, endpoint, targetId);
|
||||
await policy.initialize();
|
||||
const exposure = await page.evaluate(async () => {
|
||||
const register = navigator.serviceWorker.register;
|
||||
const originals = Object.getOwnPropertySymbols(register).map(
|
||||
(symbol) => register[symbol],
|
||||
);
|
||||
const restore = Object.getOwnPropertyNames(globalThis).find((name) =>
|
||||
name.startsWith("rea-restore-"),
|
||||
);
|
||||
const denied = globalThis[restore]("not-the-session-secret");
|
||||
let blocked = false;
|
||||
try {
|
||||
await navigator.serviceWorker.register("/sw.js");
|
||||
} catch {
|
||||
blocked = true;
|
||||
}
|
||||
return {
|
||||
exposesOriginal: originals.some(
|
||||
(value) => typeof value?.value === "function",
|
||||
),
|
||||
denied,
|
||||
blocked,
|
||||
registrations: (await navigator.serviceWorker.getRegistrations()).length,
|
||||
};
|
||||
});
|
||||
assert.deepEqual(exposure, {
|
||||
exposesOriginal: false,
|
||||
denied: false,
|
||||
blocked: true,
|
||||
registrations: 0,
|
||||
});
|
||||
|
||||
// Crossing the renderer boundary must not layer a wrapper over an earlier
|
||||
// wrapper in a surviving realm, or leave an OOPIF unprotected.
|
||||
const crossSite = `${origin.replace("127.0.0.1", "localhost")}/register`;
|
||||
await page.evaluate((url) => {
|
||||
const child = document.createElement("iframe");
|
||||
child.id = "cross-site";
|
||||
child.src = url;
|
||||
document.body.append(child);
|
||||
}, crossSite);
|
||||
const child = page.frames().find((frame) => frame.parentFrame() !== null);
|
||||
assert.ok(child);
|
||||
await child.waitForURL(crossSite, { waitUntil: "load" });
|
||||
assert.equal(await child.locator("#state").textContent(), "blocked");
|
||||
await page.locator("#cross-site").evaluate((frame, url) => {
|
||||
frame.src = url;
|
||||
}, `${origin}/register`);
|
||||
await child.waitForURL(`${origin}/register`, { waitUntil: "load" });
|
||||
assert.equal(await child.locator("#state").textContent(), "blocked");
|
||||
|
||||
// Gate one real frame evaluation to force a new main document between live
|
||||
// frame restorations. Browser replies and navigation remain real.
|
||||
const originalEvaluate = child.evaluate.bind(child);
|
||||
let release;
|
||||
const gate = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let entered;
|
||||
const started = new Promise((resolve) => {
|
||||
entered = resolve;
|
||||
});
|
||||
child.evaluate = async (...args) => {
|
||||
entered();
|
||||
await gate;
|
||||
return originalEvaluate(...args);
|
||||
};
|
||||
const closing = policy.close();
|
||||
void closing.catch(() => undefined);
|
||||
try {
|
||||
await started;
|
||||
await page.goto(origin);
|
||||
} finally {
|
||||
release();
|
||||
child.evaluate = originalEvaluate;
|
||||
}
|
||||
await closing;
|
||||
assert.equal(await nativeRegister(), true);
|
||||
await page.reload();
|
||||
assert.equal(await nativeRegister(), true);
|
||||
assert.equal(
|
||||
await page.evaluate(() => globalThis.reaCallerInitScript),
|
||||
"preserved",
|
||||
);
|
||||
assert.equal(
|
||||
await page.evaluate(() =>
|
||||
Object.getOwnPropertyNames(globalThis).some((name) =>
|
||||
name.startsWith("rea-restore-"),
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
await verifyRejectedPolicyInstallation(page.context(), endpoint, origin);
|
||||
const setup = await verifyScenarioPolicySetup(
|
||||
endpoint,
|
||||
targetId,
|
||||
origin,
|
||||
page,
|
||||
);
|
||||
return {
|
||||
stalled_image_cancelled: true,
|
||||
stalled_frame_cancelled: true,
|
||||
target_reused_after_cancel: true,
|
||||
private_original_descriptor: true,
|
||||
wrong_restore_token_rejected: true,
|
||||
renderer_transition: true,
|
||||
navigation_during_restoration: true,
|
||||
caller_init_script_preserved: true,
|
||||
failed_main_installation_rejected: true,
|
||||
failed_accessor_installation_rejected: true,
|
||||
failed_iframe_installation_rejected: true,
|
||||
replaced_iframe_policy_rejected: true,
|
||||
setup,
|
||||
};
|
||||
};
|
||||
|
||||
const verifyRejectedPolicyInstallation = async (context, endpoint, origin) => {
|
||||
for (const realm of ["main", "accessor", "existing-iframe", "new-iframe"]) {
|
||||
const page = await context.newPage();
|
||||
let policy;
|
||||
try {
|
||||
await page.goto(origin);
|
||||
const cdp = await context.newCDPSession(page);
|
||||
const { targetInfo } = await cdp.send("Target.getTargetInfo");
|
||||
await cdp.detach();
|
||||
policy = new PlaywrightScenarioServiceWorkers(
|
||||
page,
|
||||
endpoint,
|
||||
targetInfo.targetId,
|
||||
);
|
||||
if (realm === "new-iframe") await policy.initialize();
|
||||
let frame = page.mainFrame();
|
||||
if (realm === "existing-iframe" || realm === "new-iframe") {
|
||||
const crossSite = origin.replace("127.0.0.1", "localhost");
|
||||
await page.evaluate((url) => {
|
||||
const child = document.createElement("iframe");
|
||||
child.src = url;
|
||||
document.body.append(child);
|
||||
}, crossSite);
|
||||
frame = page
|
||||
.frames()
|
||||
.find((candidate) => candidate.parentFrame() !== null);
|
||||
assert.ok(frame);
|
||||
await frame.waitForURL(crossSite, { waitUntil: "load" });
|
||||
}
|
||||
await frame.evaluate((kind) => {
|
||||
const prototype = ServiceWorkerContainer.prototype;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(
|
||||
prototype,
|
||||
"register",
|
||||
);
|
||||
if (kind === "accessor") {
|
||||
const original = descriptor.value;
|
||||
Object.defineProperty(prototype, "register", {
|
||||
configurable: true,
|
||||
enumerable: descriptor.enumerable,
|
||||
get: () => original,
|
||||
});
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(prototype, "register", {
|
||||
...descriptor,
|
||||
configurable: false,
|
||||
...(kind === "new-iframe"
|
||||
? { value: function callerReplacement() {} }
|
||||
: {}),
|
||||
});
|
||||
}, realm);
|
||||
await assert.rejects(
|
||||
realm === "new-iframe" ? policy.assertBlocked() : policy.initialize(),
|
||||
(error) => error.reason === "target_not_allowed",
|
||||
);
|
||||
await policy.close();
|
||||
assert.equal(
|
||||
await frame.evaluate((replaced) => {
|
||||
const register = navigator.serviceWorker.register;
|
||||
return replaced
|
||||
? register.name === "callerReplacement"
|
||||
: register.toString().includes("[native code]");
|
||||
}, realm === "new-iframe"),
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await policy?.close();
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
assertExportedScriptAnalysis,
|
||||
} from "./browser-script-export-assertions.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
/** Verify capture → export → existing analysis through real CLI and stdio MCP. */
|
||||
export async function verifyBrowserScriptExport(
|
||||
@@ -22,24 +23,29 @@ export async function verifyBrowserScriptExport(
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
attachment,
|
||||
) {
|
||||
const site = await startBrowserScriptSite();
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-browser-script-export-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = exportCli(entrypoint, env);
|
||||
const scenario = scriptCaptureScenario(executable, site.origin);
|
||||
const save = (name, capture) => saveCapture(root, name, capture);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "web-script-real-e2e", version: "1" });
|
||||
let site;
|
||||
let root;
|
||||
let transport;
|
||||
let client;
|
||||
let primaryError;
|
||||
try {
|
||||
site = await startBrowserScriptSite();
|
||||
root = await mkdtemp(join(tmpdir(), "rea-browser-script-export-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = exportCli(entrypoint, env);
|
||||
const scenario = scriptCaptureScenario(executable, site.origin);
|
||||
const save = (name, capture) => saveCapture(root, name, capture);
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
client = new Client({ name: "web-script-real-e2e", version: "1" });
|
||||
const cliCapture = await cli(
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify(scenario),
|
||||
@@ -103,11 +109,17 @@ export async function verifyBrowserScriptExport(
|
||||
query_variants_isolated: true,
|
||||
implicit_refetches: 0,
|
||||
};
|
||||
} catch (cause) {
|
||||
primaryError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await site.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
const cleanup = [];
|
||||
if (client !== undefined) cleanup.push(() => client.close());
|
||||
if (transport !== undefined) cleanup.push(() => transport.close());
|
||||
if (site !== undefined) cleanup.push(() => site.close());
|
||||
if (root !== undefined)
|
||||
cleanup.push(() => rm(root, { recursive: true, force: true }));
|
||||
await closeVerifierResources(cleanup, primaryError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,35 +12,41 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js";
|
||||
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
||||
|
||||
/** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */
|
||||
export async function verifyBrowserSourceMap(
|
||||
executable,
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
) {
|
||||
const site = await startBrowserSourceMapSite();
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "source-map-real-e2e", version: "1" });
|
||||
let site;
|
||||
let root;
|
||||
let transport;
|
||||
let client;
|
||||
let primaryError;
|
||||
try {
|
||||
site = await startBrowserSourceMapSite();
|
||||
root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
client = new Client({ name: "source-map-real-e2e", version: "1" });
|
||||
const capture = await cli(
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify({
|
||||
@@ -126,11 +132,17 @@ export async function verifyBrowserSourceMap(
|
||||
selected_inert_userinfo_context_preserved: true,
|
||||
public_point_cases: 4,
|
||||
};
|
||||
} catch (cause) {
|
||||
primaryError = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await site.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
const cleanup = [];
|
||||
if (client !== undefined) cleanup.push(() => client.close());
|
||||
if (transport !== undefined) cleanup.push(() => transport.close());
|
||||
if (site !== undefined) cleanup.push(() => site.close());
|
||||
if (root !== undefined)
|
||||
cleanup.push(() => rm(root, { recursive: true, force: true }));
|
||||
await closeVerifierResources(cleanup, primaryError);
|
||||
}
|
||||
}
|
||||
function assertPoint(evidence, { site, input, capture, identity }) {
|
||||
|
||||
@@ -167,6 +167,7 @@ export const providerCatalog = (sources) => {
|
||||
reconciliationContracts,
|
||||
observationContracts,
|
||||
activeContracts,
|
||||
fuseContracts,
|
||||
} = electronContractSlices(sources);
|
||||
return [
|
||||
{
|
||||
@@ -261,6 +262,10 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
|
||||
contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.electronFuseProvider.ELECTRON_FUSE_PROVIDER_IDENTITY,
|
||||
contracts: fuseContracts,
|
||||
},
|
||||
{
|
||||
identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY,
|
||||
contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
|
||||
@@ -425,17 +430,23 @@ const electronContractSlices = (sources) => {
|
||||
({ name }) =>
|
||||
name !== "analyze_javascript_application" &&
|
||||
name !== "reconcile_javascript_runtime" &&
|
||||
name !== "capture_electron_scenario",
|
||||
name !== "capture_electron_scenario" &&
|
||||
name !== "inspect_electron_fuses",
|
||||
);
|
||||
const activeContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "capture_electron_scenario",
|
||||
);
|
||||
const fuseContracts =
|
||||
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "inspect_electron_fuses",
|
||||
);
|
||||
if (
|
||||
applicationContracts.length !== 1 ||
|
||||
reconciliationContracts.length !== 1 ||
|
||||
observationContracts.length !== 2 ||
|
||||
activeContracts.length !== 1
|
||||
activeContracts.length !== 1 ||
|
||||
fuseContracts.length !== 1
|
||||
)
|
||||
throw new Error("Electron provider capability ownership drifted");
|
||||
return {
|
||||
@@ -443,5 +454,6 @@ const electronContractSlices = (sources) => {
|
||||
reconciliationContracts,
|
||||
observationContracts,
|
||||
activeContracts,
|
||||
fuseContracts,
|
||||
};
|
||||
};
|
||||
@@ -96,6 +96,7 @@ const SOURCE_PATHS = {
|
||||
webNetworkCaptureProvider: "dist/browser/history/CaptureRelease.js",
|
||||
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
|
||||
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
|
||||
electronFuseProvider: "dist/electron/ElectronFuseProvider.js",
|
||||
javascriptRuntimeObservationContracts:
|
||||
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
|
||||
applicationContracts: "dist/contracts/applicationToolContracts.js",
|
||||
|
||||
@@ -1,8 +1,36 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { readdir, stat } from "node:fs/promises";
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
// CPython's interface option ends interpreter parsing; following values are
|
||||
// script arguments. -W/-X consume either an attached value or the next operand.
|
||||
// https://docs.python.org/3.12/using/cmdline.html
|
||||
const pythonScriptOperand = (arguments_) => {
|
||||
for (let index = 1; index < arguments_.length; index++) {
|
||||
const option = arguments_[index];
|
||||
if (option === "--") return arguments_[index + 1];
|
||||
if (option === "-" || option === "") return undefined;
|
||||
if (!option.startsWith("-")) return option;
|
||||
if (option === "--check-hash-based-pycs") {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (/^--(?:help(?:-env|-xoptions|-all)?|version)$/u.test(option))
|
||||
return undefined;
|
||||
if (option.startsWith("--"))
|
||||
throw new Error(`Unknown Python helper invocation option: ${option}`);
|
||||
for (let character = 1; character < option.length; character++) {
|
||||
const flag = option[character];
|
||||
if ("cmh?V".includes(flag)) return undefined;
|
||||
if (flag === "W" || flag === "X") {
|
||||
if (character === option.length - 1) index++;
|
||||
break;
|
||||
}
|
||||
if (!"bBdEiIOPqRsSuvx".includes(flag))
|
||||
throw new Error(`Unknown Python helper invocation option: ${option}`);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe leftover demo processes on a dedicated clean Linux runner, including
|
||||
@@ -18,9 +46,6 @@ export const assertCleanLinuxHopperRuntime = async (
|
||||
],
|
||||
procRoot = "/proc",
|
||||
) => {
|
||||
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "command="]);
|
||||
if (/\/opt\/hopper\/bin\/Hopper|\bXvfb\b|hopper-demo-x11\.py/u.test(stdout))
|
||||
throw new Error("Clean Linux runner retained a Hopper demo process");
|
||||
const launchers = [];
|
||||
for (const path of launcherPaths) {
|
||||
try {
|
||||
@@ -29,20 +54,55 @@ export const assertCleanLinuxHopperRuntime = async (
|
||||
if (error?.code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
if (launchers.length === 0) return;
|
||||
// /proc exposes the executed file even when argv[0] names a removed symlink.
|
||||
// Compare file identity, not a display path or a process name.
|
||||
let inspectionError;
|
||||
for (const entry of await readdir(procRoot, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) continue;
|
||||
let executable;
|
||||
let retainedRuntime;
|
||||
try {
|
||||
executable = await stat(`${procRoot}/${entry.name}/exe`, {
|
||||
bigint: true,
|
||||
});
|
||||
// procfs preserves argument boundaries. A caller merely mentioning a
|
||||
// runtime in its script text or other arguments is not that runtime.
|
||||
const commandLine = await readFile(
|
||||
`${procRoot}/${entry.name}/cmdline`,
|
||||
"utf8",
|
||||
);
|
||||
const arguments_ = commandLine.split("\0");
|
||||
// The kernel command field identifies renamed runtime processes such
|
||||
// as Xvfb. Keep it separate from arguments so ordinary script text does
|
||||
// not count as a retained runtime.
|
||||
const kernelCommand = (
|
||||
await readFile(`${procRoot}/${entry.name}/comm`, "utf8")
|
||||
).replace(/\n$/u, "");
|
||||
if (
|
||||
commandLine.length === 0 &&
|
||||
/^python3(?:\.\d+)?$/u.test(kernelCommand)
|
||||
)
|
||||
throw new Error(
|
||||
"Cannot identify a command-less Python process on the clean Linux runner",
|
||||
);
|
||||
retainedRuntime =
|
||||
kernelCommand === "Hopper" ||
|
||||
kernelCommand === "Xvfb" ||
|
||||
arguments_[0] === "/opt/hopper/bin/Hopper" ||
|
||||
basename(arguments_[0] ?? "") === "Xvfb" ||
|
||||
(/^python3(?:\.\d+)?$/u.test(basename(arguments_[0] ?? "")) &&
|
||||
arguments_.some(
|
||||
(value) => basename(value) === "hopper-demo-x11.py",
|
||||
) &&
|
||||
basename(pythonScriptOperand(arguments_) ?? "") ===
|
||||
"hopper-demo-x11.py");
|
||||
if (!retainedRuntime && launchers.length > 0) {
|
||||
const executable = await stat(`${procRoot}/${entry.name}/exe`, {
|
||||
bigint: true,
|
||||
});
|
||||
retainedRuntime = launchers.some(
|
||||
({ dev, ino }) => dev === executable.dev && ino === executable.ino,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT" || error?.code === "ESRCH") continue;
|
||||
// Other users' executables may be unreadable. The setup lane launches
|
||||
// Other users' process facts may be unreadable. The setup lane launches
|
||||
// providers as the caller; unreadable caller-owned processes fail closed.
|
||||
if (error?.code === "EACCES") {
|
||||
const owner = await stat(`${procRoot}/${entry.name}`).catch((cause) => {
|
||||
@@ -56,11 +116,7 @@ export const assertCleanLinuxHopperRuntime = async (
|
||||
inspectionError ??= error;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
launchers.some(
|
||||
({ dev, ino }) => dev === executable.dev && ino === executable.ino,
|
||||
)
|
||||
)
|
||||
if (retainedRuntime)
|
||||
throw new Error("Clean Linux runner retained a Hopper demo process");
|
||||
}
|
||||
if (inspectionError !== undefined) throw inspectionError;
|
||||
|
||||
@@ -43,6 +43,7 @@ const artifactConsumers = {
|
||||
"tests/boundary/mcp/toolSchemaValidity.test.ts",
|
||||
],
|
||||
"artifacts:product-catalog": [
|
||||
"tests/boundary/filesystem/documentationFacts.test.ts",
|
||||
"tests/boundary/filesystem/productCatalog.test.ts",
|
||||
],
|
||||
"artifacts:managed-evidence": [
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
export const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({
|
||||
pattern: "a0 ?? c0",
|
||||
mask: "f0 ff f0",
|
||||
startFileOffset: 0x380,
|
||||
endFileOffset: 0x392,
|
||||
markerOffsets: Object.freeze([0x380, 0x390]),
|
||||
});
|
||||
|
||||
/** Put two masked, wildcarded signatures in loaded, source-owned PE bytes. */
|
||||
export function writeWindowsGhidraByteSearchFixture(bytes) {
|
||||
bytes.set([0xa1, 0xb2, 0xc3], 0x380);
|
||||
bytes.set([0xa4, 0xb5, 0xc6], 0x390);
|
||||
}
|
||||
|
||||
/** Verify real Ghidra search pages against the immutable source fixture. */
|
||||
export async function verifyWindowsGhidraByteSearch({
|
||||
invoke,
|
||||
sourceBytes,
|
||||
architecture,
|
||||
sourceOwnedFixture = true,
|
||||
fallbackAddress,
|
||||
fallbackFileOffset,
|
||||
runCliPage,
|
||||
}) {
|
||||
const imageBase = architecture === "x86" ? 0x400000n : 0x140000000n;
|
||||
const query = sourceOwnedFixture
|
||||
? {
|
||||
pattern: WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.pattern,
|
||||
mask: WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.mask,
|
||||
start: addressAtFileOffset(
|
||||
imageBase,
|
||||
WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.startFileOffset,
|
||||
),
|
||||
end: addressAtFileOffset(
|
||||
imageBase,
|
||||
WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.endFileOffset,
|
||||
),
|
||||
limit: 1,
|
||||
}
|
||||
: {
|
||||
pattern: sourceBytes
|
||||
.subarray(fallbackFileOffset, fallbackFileOffset + 1)
|
||||
.toString("hex"),
|
||||
start: fallbackAddress,
|
||||
end: fallbackAddress,
|
||||
limit: 1,
|
||||
};
|
||||
|
||||
if (!sourceOwnedFixture) {
|
||||
const result = await invoke("search_bytes", query);
|
||||
assert.equal(result.complete, true);
|
||||
assert.equal(result.next_cursor, null);
|
||||
assert.deepEqual(
|
||||
result.matches.map(({ address, file_offset }) => ({
|
||||
address,
|
||||
file_offset,
|
||||
})),
|
||||
[{ address: fallbackAddress, file_offset: fallbackFileOffset }],
|
||||
);
|
||||
assert.ok(result.searched.bytes > 0);
|
||||
return { bounded_source_match: true };
|
||||
}
|
||||
|
||||
const { startFileOffset, endFileOffset } = WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE;
|
||||
const expected = [];
|
||||
for (
|
||||
let fileOffset = startFileOffset;
|
||||
fileOffset + 2 <= endFileOffset;
|
||||
fileOffset += 1
|
||||
) {
|
||||
const first = sourceBytes[fileOffset];
|
||||
const last = sourceBytes[fileOffset + 2];
|
||||
if (first === undefined || last === undefined) break;
|
||||
if ((first & 0xf0) === 0xa0 && (last & 0xf0) === 0xc0)
|
||||
expected.push({
|
||||
address: addressAtFileOffset(imageBase, fileOffset),
|
||||
file_offset: fileOffset,
|
||||
});
|
||||
}
|
||||
assert.deepEqual(
|
||||
expected.map(({ file_offset }) => file_offset),
|
||||
[...WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE.markerOffsets],
|
||||
"The source-owned byte oracle must retain both distinct signatures",
|
||||
);
|
||||
|
||||
const first = await invoke("search_bytes", query);
|
||||
assert.deepEqual(
|
||||
first.matches.map(({ address, file_offset }) => ({ address, file_offset })),
|
||||
[expected[0]],
|
||||
);
|
||||
assert.equal(first.complete, false);
|
||||
assert.equal(typeof first.next_cursor, "string");
|
||||
assert.ok(first.searched.bytes > 0);
|
||||
if (runCliPage !== undefined) {
|
||||
const cli = await runCliPage(query);
|
||||
assert.deepEqual(cli.matches, first.matches);
|
||||
assert.deepEqual(cli.searched, first.searched);
|
||||
assert.equal(cli.complete, first.complete);
|
||||
}
|
||||
|
||||
const second = await invoke("search_bytes", {
|
||||
...query,
|
||||
cursor: first.next_cursor,
|
||||
});
|
||||
assert.deepEqual(
|
||||
second.matches.map(({ address, file_offset }) => ({
|
||||
address,
|
||||
file_offset,
|
||||
})),
|
||||
[expected[1]],
|
||||
);
|
||||
assert.equal(second.complete, true);
|
||||
assert.equal(second.next_cursor, null);
|
||||
assert.ok(second.searched.bytes > 0);
|
||||
|
||||
const covered = new Set();
|
||||
for (const page of [first, second]) {
|
||||
assert.equal(
|
||||
page.searched.bytes,
|
||||
page.searched.blocks.reduce((sum, block) => sum + block.bytes, 0),
|
||||
);
|
||||
for (const block of page.searched.blocks) {
|
||||
const start = BigInt(block.start);
|
||||
const end = BigInt(block.end);
|
||||
assert.ok(start >= BigInt(query.start) && end <= BigInt(query.end));
|
||||
for (let offset = start; offset <= end; offset += 1n)
|
||||
covered.add(offset.toString());
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
covered.size,
|
||||
endFileOffset - startFileOffset + 1,
|
||||
"Paged byte-search coverage must account for the complete bounded source range",
|
||||
);
|
||||
return {
|
||||
bounded: true,
|
||||
wildcardAndMask: true,
|
||||
matches: expected,
|
||||
continuation: true,
|
||||
examinedBytes: covered.size,
|
||||
cliMcpParity: runCliPage !== undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function addressAtFileOffset(imageBase, fileOffset) {
|
||||
return `0x${(imageBase + 0x1000n + BigInt(fileOffset - 0x200)).toString(16)}`;
|
||||
}
|
||||
@@ -215,6 +215,27 @@ async function verifyNativeValueAdapters(
|
||||
const functionEvidence = parseEvidence(analyzed.structuredContent);
|
||||
assert.equal(functionEvidence.operation, "analyze_function");
|
||||
assert.equal(functionEvidence.provider.id, "ghidra");
|
||||
assert.deepEqual(
|
||||
functionEvidence.normalized_result.facet_availability,
|
||||
Object.fromEntries(
|
||||
[
|
||||
"pseudocode",
|
||||
"assembly",
|
||||
"comments",
|
||||
"callers",
|
||||
"callees",
|
||||
"references",
|
||||
"strings_names",
|
||||
"cfg",
|
||||
].map((facet) => [facet, { available: true }]),
|
||||
),
|
||||
);
|
||||
assert.equal(functionEvidence.relationship_coverage?.status, "complete");
|
||||
assert.ok(
|
||||
functionEvidence.relationship_coverage.diagnostics.some((diagnostic) =>
|
||||
diagnostic.includes("indirect calls remain unknown"),
|
||||
),
|
||||
);
|
||||
const functionProfile = analysisProfileSchema.parse(
|
||||
functionEvidence.analysis_profile,
|
||||
);
|
||||
@@ -247,6 +268,7 @@ async function verifyNativeValueAdapters(
|
||||
assert.ok(
|
||||
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
|
||||
);
|
||||
await verifyUnresolvedCallEvidence(client);
|
||||
const bundle = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
@@ -307,3 +329,63 @@ async function verifyNativeValueAdapters(
|
||||
unknowns: cliGraph.unknowns.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyUnresolvedCallEvidence(client) {
|
||||
const inventory = await client.callTool({
|
||||
name: "list_procedures",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
|
||||
const procedures = parseEvidence(
|
||||
inventory.structuredContent,
|
||||
).normalized_result;
|
||||
const indirect = procedures.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_indirect"),
|
||||
);
|
||||
const leaf = procedures.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
assert.ok(
|
||||
indirect && leaf,
|
||||
"Source fixture lacks the unresolved callback and its possible target",
|
||||
);
|
||||
const analyzed = await client.callTool(
|
||||
{ name: "analyze_function", arguments: { procedure: indirect.address } },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
|
||||
const evidence = parseEvidence(analyzed.structuredContent);
|
||||
assert.equal(evidence.relationship_coverage?.status, "partial");
|
||||
assert.ok(evidence.normalized_result.unresolved_calls.length > 0);
|
||||
const traced = await client.callTool(
|
||||
{
|
||||
name: "trace_call_path",
|
||||
arguments: {
|
||||
start: indirect.address,
|
||||
goal: leaf.address,
|
||||
direction: "forward",
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(traced.isError, true, JSON.stringify(traced));
|
||||
const trace = parseEvidence(traced.structuredContent).normalized_result;
|
||||
assert.equal(trace.goal_status, "unknown");
|
||||
assert.equal(trace.truncated, true);
|
||||
assert.ok(trace.residual_unknowns.length > 0);
|
||||
const compared = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent,
|
||||
right: analyzed.structuredContent,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(compared.isError, true, JSON.stringify(compared));
|
||||
assert.equal(
|
||||
parseEvidence(compared.structuredContent).normalized_result.status,
|
||||
"unknown",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Client } from "@modelcontextprotocol/client";
|
||||
|
||||
/** Verify segmented aliases and restore the entry's observed public name. */
|
||||
export function verifyGhidraAddressBoundaries(
|
||||
client: Pick<Client, "callTool">,
|
||||
entry: string,
|
||||
cli: (command: string, address: string, flags: string[]) => Promise<unknown>,
|
||||
): Promise<void>;
|
||||
@@ -62,36 +62,68 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
|
||||
await query("read_bytes", { address: padded, length: 1 }),
|
||||
await query("read_bytes", { address: entry, length: 1 }),
|
||||
);
|
||||
const aliasKeys = [entry, padded];
|
||||
assert.deepEqual(
|
||||
await query("set_addresses_names", {
|
||||
names: {
|
||||
[entry]: "rea_same_address_alias",
|
||||
[padded]: "rea_same_address_alias",
|
||||
},
|
||||
}),
|
||||
Object.fromEntries(aliasKeys.map((address) => [address, true])),
|
||||
"Equivalent address spellings with one requested name share one edit",
|
||||
);
|
||||
const nameBeforeAliasedBatch = await query("address_name", {
|
||||
address: entry,
|
||||
});
|
||||
assert.equal(nameBeforeAliasedBatch, "rea_same_address_alias");
|
||||
assert.deepEqual(
|
||||
await query("set_addresses_names", {
|
||||
names: {
|
||||
[entry]: "rea_first_address_alias",
|
||||
[padded]: "rea_second_address_alias",
|
||||
},
|
||||
}),
|
||||
Object.fromEntries(aliasKeys.map((address) => [address, false])),
|
||||
"Conflicting spellings of one Ghidra address must be rejected together",
|
||||
);
|
||||
const originalName = await query("address_name", { address: entry });
|
||||
assert.equal(
|
||||
await query("address_name", { address: entry }),
|
||||
nameBeforeAliasedBatch,
|
||||
"A rejected alias collision must leave the preexisting target name unchanged",
|
||||
typeof originalName,
|
||||
"string",
|
||||
"The function entry needs an observed name that can be restored",
|
||||
);
|
||||
const aliasKeys = [entry, padded];
|
||||
let primaryFailure;
|
||||
try {
|
||||
assert.deepEqual(
|
||||
await query("set_addresses_names", {
|
||||
names: {
|
||||
[entry]: "rea_same_address_alias",
|
||||
[padded]: "rea_same_address_alias",
|
||||
},
|
||||
}),
|
||||
Object.fromEntries(aliasKeys.map((address) => [address, true])),
|
||||
"Equivalent address spellings with one requested name share one edit",
|
||||
);
|
||||
const nameBeforeAliasedBatch = await query("address_name", {
|
||||
address: entry,
|
||||
});
|
||||
assert.equal(nameBeforeAliasedBatch, "rea_same_address_alias");
|
||||
assert.deepEqual(
|
||||
await query("set_addresses_names", {
|
||||
names: {
|
||||
[entry]: "rea_first_address_alias",
|
||||
[padded]: "rea_second_address_alias",
|
||||
},
|
||||
}),
|
||||
Object.fromEntries(aliasKeys.map((address) => [address, false])),
|
||||
"Conflicting spellings of one Ghidra address must be rejected together",
|
||||
);
|
||||
assert.equal(
|
||||
await query("address_name", { address: entry }),
|
||||
nameBeforeAliasedBatch,
|
||||
"A rejected alias collision must leave the preexisting target name unchanged",
|
||||
);
|
||||
} catch (cause) {
|
||||
primaryFailure = cause;
|
||||
throw cause;
|
||||
} finally {
|
||||
try {
|
||||
assert.equal(
|
||||
await query("set_address_name", { address: entry, name: originalName }),
|
||||
true,
|
||||
"The temporary alias edit must be restored",
|
||||
);
|
||||
assert.equal(
|
||||
await query("address_name", { address: entry }),
|
||||
originalName,
|
||||
"The original function-entry name must survive restoration",
|
||||
);
|
||||
} catch (cleanupCause) {
|
||||
if (primaryFailure !== undefined)
|
||||
throw new AggregateError(
|
||||
[primaryFailure, cleanupCause],
|
||||
"Ghidra address-boundary verification and alias restoration failed",
|
||||
);
|
||||
throw cleanupCause;
|
||||
}
|
||||
}
|
||||
for (const [command, flags, operation] of [
|
||||
["read-bytes", ["--length", "1"], "read_bytes"],
|
||||
[
|
||||
|
||||
@@ -136,7 +136,7 @@ export async function verifyGhidraBoundaries(
|
||||
assert.deepEqual(externalDossier.callees, []);
|
||||
assert.deepEqual(externalDossier.facet_availability.pseudocode, {
|
||||
available: false,
|
||||
reason: "Ghidra did not supply pseudocode.",
|
||||
reason: "Ghidra does not decompile external functions.",
|
||||
});
|
||||
for (const facet of [
|
||||
"assembly",
|
||||
|
||||
Loaded 100 of 814 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user