Compare commits
163
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06444ad67 | ||
|
|
2609352eef | ||
|
|
90f532feb1 | ||
|
|
e9ebfb94ef | ||
|
|
13935077ba | ||
|
|
ae9aaee16b | ||
|
|
cdddfddf49 | ||
|
|
441813c070 | ||
|
|
c12bad7f13 | ||
|
|
70e5c24718 | ||
|
|
283c82f68c | ||
|
|
c29d85a7db | ||
|
|
b7d803dd10 | ||
|
|
5ac3dc96f0 | ||
|
|
96af2fa744 | ||
|
|
67756dacc0 | ||
|
|
80c3d36dfa | ||
|
|
c715af8058 | ||
|
|
55c9ce26d1 | ||
|
|
a69b8337ab | ||
|
|
1d9b96a9c4 | ||
|
|
3e598b2d38 | ||
|
|
1f79002497 | ||
|
|
d84bc8f11a | ||
|
|
65a809a0da | ||
|
|
4034883e98 | ||
|
|
1d7115b7b4 | ||
|
|
c1d0002470 | ||
|
|
e8939ca220 | ||
|
|
329cf206d1 | ||
|
|
2b27515476 | ||
|
|
44fd18305f | ||
|
|
6c13fc8e0a | ||
|
|
70e8a8f860 | ||
|
|
7a7460f04f | ||
|
|
b3f1b4e822 | ||
|
|
480b16b9c1 | ||
|
|
e762e32afb | ||
|
|
f7366a517f | ||
|
|
faf209e50d | ||
|
|
0a0174b9ac | ||
|
|
e1df5e8555 | ||
|
|
d575a7340b | ||
|
|
6495171812 | ||
|
|
14b19a0b8a | ||
|
|
9fd65ce071 | ||
|
|
567a82f8d4 | ||
|
|
82626c4499 | ||
|
|
ae06dcf83f | ||
|
|
08561e0eb7 | ||
|
|
0639a541a3 | ||
|
|
826583dd43 | ||
|
|
c3136064c7 | ||
|
|
2a4979b7cd | ||
|
|
a61491ad00 | ||
|
|
2847311a57 | ||
|
|
4458128a84 | ||
|
|
34f7c4fd66 | ||
|
|
67252b23b9 | ||
|
|
02d0702761 | ||
|
|
e714fb027a | ||
|
|
f983c718c3 | ||
|
|
3d450a29fc | ||
|
|
73b09f4c5c | ||
|
|
dbcf8c3391 | ||
|
|
f2e9c491ea | ||
|
|
4a45c14742 | ||
|
|
2b86ea9c7c | ||
|
|
f201b04fbb | ||
|
|
246170238e | ||
|
|
0a49d5bfb9 | ||
|
|
458771cb58 | ||
|
|
7d7b3f961a | ||
|
|
35f1ddd6bc | ||
|
|
b2d5bc1109 | ||
|
|
704fa7699c | ||
|
|
2f59287f1a | ||
|
|
d83f0983d5 | ||
|
|
1136ea8810 | ||
|
|
e44cb45908 | ||
|
|
8d39eae6b9 | ||
|
|
09d7b9bab5 | ||
|
|
9a70fb482c | ||
|
|
f591e37a1e | ||
|
|
a0784c0d2c | ||
|
|
96d3cafb07 | ||
|
|
ee043e2cbf | ||
|
|
3eb7dbdc16 | ||
|
|
90b06db6c0 | ||
|
|
6b92002597 | ||
|
|
bc4aa46d7c | ||
|
|
549effa056 | ||
|
|
dcae3c6c14 | ||
|
|
84a17d5519 | ||
|
|
017297c40c | ||
|
|
ffd9611308 | ||
|
|
81ae838567 | ||
|
|
d1c904bac7 | ||
|
|
8f9da25164 | ||
|
|
fe48e779c2 | ||
|
|
9a5b077899 | ||
|
|
9588482bfe | ||
|
|
0e0b386dc3 | ||
|
|
a1b7d40bb0 | ||
|
|
5945915f91 | ||
|
|
bef234d6ce | ||
|
|
64717ad7da | ||
|
|
d830cb79b3 | ||
|
|
f2e22e13a6 | ||
|
|
b743bd20a0 | ||
|
|
ec6db0f063 | ||
|
|
1f5be750bc | ||
|
|
e2fc2493b6 | ||
|
|
be783efaba | ||
|
|
7005166f58 | ||
|
|
2f71375efc | ||
|
|
34cc9259c1 | ||
|
|
8b08ec0af9 | ||
|
|
82560ebf59 | ||
|
|
8a4c4b657f | ||
|
|
1d0baa352f | ||
|
|
7c323870c2 | ||
|
|
d6cc9ca3a6 | ||
|
|
fe297e56e8 | ||
|
|
1e63144390 | ||
|
|
cd756b07d5 | ||
|
|
7914e36ab9 | ||
|
|
3b2ec69c54 | ||
|
|
4bb056bbb5 | ||
|
|
c789af04b7 | ||
|
|
acb3e0f2e5 | ||
|
|
3db46813f8 | ||
|
|
cc0ee2edc6 | ||
|
|
f8e143a3bc | ||
|
|
ae0630a619 | ||
|
|
81b5104276 | ||
|
|
4a1837d9d5 | ||
|
|
770d2f60aa | ||
|
|
eb89a16d9b | ||
|
|
bfb7776006 | ||
|
|
4b1066501b | ||
|
|
8b01e0cc89 | ||
|
|
915855e75c | ||
|
|
488f448ae2 | ||
|
|
15f92f2988 | ||
|
|
16f4609d68 | ||
|
|
eaf5734083 | ||
|
|
135d102f9d | ||
|
|
d4c2c3c18f | ||
|
|
17809caacd | ||
|
|
0136ee8c22 | ||
|
|
65a6288257 | ||
|
|
99d31ee24a | ||
|
|
3c157b4ec6 | ||
|
|
c1b370ce0d | ||
|
|
5a67c7fc54 | ||
|
|
b5c2954ad6 | ||
|
|
6218d289e2 | ||
|
|
23862af1b5 | ||
|
|
e94be95c66 | ||
|
|
83defc054b | ||
|
|
eb69763d27 | ||
|
|
289aef6838 |
@@ -61,7 +61,8 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
|
||||
- Breaking changes or migration steps:
|
||||
<!-- For ! / BREAKING CHANGE: show a previously valid call/configuration,
|
||||
its new behavior and migration, and why compatibility/deprecation cannot
|
||||
preserve it. Compatible fixes/additions should use patch/minor semantics.
|
||||
preserve it. Every release increments minor, including breaking changes;
|
||||
keep breaking markers and migration notes so callers can assess compatibility.
|
||||
See docs/releasing.md. -->
|
||||
- Real Hopper/Ghidra, browser, or OS coverage:
|
||||
- Package or release metadata impact:
|
||||
@@ -69,6 +70,7 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
|
||||
|
||||
## Review checklist
|
||||
|
||||
- [ ] This PR addresses a concrete problem or an agreed enhancement.
|
||||
- [ ] The PR has one focused outcome and the title follows `type(scope): outcome`.
|
||||
- [ ] Related issue is linked, or the reason for not linking one is stated above.
|
||||
- [ ] Tests cover changed observable behavior and meaningful failure paths.
|
||||
|
||||
@@ -329,10 +329,12 @@ jobs:
|
||||
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||
- name: Native Swift demangling CLI and MCP regressions
|
||||
run: npm run test:focused -- tests/boundary/cli/nativeDemangleCli.test.ts tests/boundary/mcp/nativeDemangleMcp.test.ts
|
||||
- name: Native signature target binding and CLI/MCP recovery regressions
|
||||
run: npm run test:focused -- tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts tests/boundary/cli/nativeSignatureCli.test.ts tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts
|
||||
- name: Process filesystem snapshot descriptor regressions
|
||||
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
|
||||
- name: Native Darwin ownership and process cleanup regressions
|
||||
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/cli/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
|
||||
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
|
||||
|
||||
reconstruction-readiness:
|
||||
name: Reconstruction readiness
|
||||
@@ -450,6 +452,7 @@ jobs:
|
||||
tests/boundary/filesystem/windowsNpxRegistration.test.ts
|
||||
src/application/ClientRegistrationIdentity.test.ts
|
||||
tests/boundary/filesystem/webScriptManifestPaths.test.ts
|
||||
tests/boundary/process/ProcessEvidence.host.test.ts
|
||||
tests/acceptance/webModuleCli.test.ts
|
||||
tests/acceptance/webSourceLocationCli.test.ts
|
||||
tests/boundary/cli/referenceSourceImportStatus.test.ts
|
||||
@@ -473,6 +476,21 @@ jobs:
|
||||
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
|
||||
- run: npm run verify:windows-native
|
||||
- run: npm run verify:package:windows
|
||||
- name: Verify native directory identity on Node 24
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
|
||||
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
|
||||
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.19.0
|
||||
- name: Verify native directory identity on Node 22
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
|
||||
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
|
||||
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [closed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_branch:
|
||||
@@ -18,12 +23,89 @@ env:
|
||||
HUSKY: "0"
|
||||
|
||||
concurrency:
|
||||
group: release-${{ inputs.release_branch }}
|
||||
group: release-${{ inputs.release_branch || github.event.pull_request.merge_commit_sha || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-proposal:
|
||||
name: Update release proposal
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
target-branch: main
|
||||
skip-github-release: true
|
||||
|
||||
merged-release:
|
||||
name: Release reviewed main merge
|
||||
if: >-
|
||||
github.event_name == 'pull_request' && github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
startsWith(github.event.pull_request.head.ref, 'release-please--branches--main--')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
sha: ${{ steps.release.outputs.sha }}
|
||||
steps:
|
||||
- name: Check out reviewed release merge
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
package-manager-cache: false
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Validate reviewed merge before release creation
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
run: |
|
||||
if [[ "${SOURCE_SHA}" != "${GITHUB_SHA}" ]]; then
|
||||
echo "The reviewed merge differs from the workflow source; refusing mismatched publication provenance." >&2
|
||||
exit 1
|
||||
fi
|
||||
node scripts/verify-release-checkpoint.mjs \
|
||||
--phase publish --release-branch main --source-sha "$SOURCE_SHA"
|
||||
|
||||
- uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
target-branch: main
|
||||
skip-github-release: false
|
||||
skip-github-pull-request: true
|
||||
|
||||
- name: Bind publication to the reviewed merge
|
||||
env:
|
||||
RELEASE_CREATED: ${{ steps.release.outputs.release_created }}
|
||||
RELEASE_SHA: ${{ steps.release.outputs.sha }}
|
||||
SOURCE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
run: |
|
||||
if [[ "${RELEASE_CREATED}" != true ]]; then
|
||||
echo "Release Please did not create a release for the reviewed merge. Check its pending label and release metadata; for a partial publication, re-run failed jobs in the original run." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${RELEASE_SHA}" != "${SOURCE_SHA}" ]]; then
|
||||
echo "The tagged release differs from the reviewed merge; refusing mismatched publication provenance. The GitHub release already exists; do not create another release as a retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release-please:
|
||||
name: Release Please
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -176,8 +258,11 @@ jobs:
|
||||
|
||||
publish:
|
||||
name: Publish to npm
|
||||
needs: release-please
|
||||
if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'
|
||||
needs: [merged-release, release-please]
|
||||
if: >-
|
||||
always() && !cancelled() &&
|
||||
((needs.merged-release.result == 'success' && needs.merged-release.outputs.release_created == 'true') ||
|
||||
(needs.release-please.result == 'success' && inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -185,7 +270,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.release-please.outputs.sha }}
|
||||
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
@@ -213,7 +298,8 @@ jobs:
|
||||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||||
echo "rea-agents@${version} is already published; verifying the existing package."
|
||||
else
|
||||
npm publish --access public
|
||||
tag="$(node scripts/release-npm-tag.mjs "${version}")"
|
||||
npm publish --access public --tag "${tag}"
|
||||
fi
|
||||
|
||||
- name: Verify published CLI from npm
|
||||
@@ -247,8 +333,11 @@ jobs:
|
||||
|
||||
publish-mcp:
|
||||
name: Publish to MCP Registry
|
||||
needs: [release-please, publish]
|
||||
if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
|
||||
needs: [merged-release, release-please, publish]
|
||||
if: >-
|
||||
always() && !cancelled() && needs.publish.result == 'success' &&
|
||||
((needs.merged-release.result == 'success' && needs.merged-release.outputs.release_created == 'true') ||
|
||||
(needs.release-please.result == 'success' && inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -256,7 +345,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.release-please.outputs.sha }}
|
||||
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify package and registry metadata versions
|
||||
|
||||
@@ -50,6 +50,13 @@ Thumbs.db
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
|
||||
# Local Cloudflare development state
|
||||
/website/.wrangler/
|
||||
/website/.dev.vars*
|
||||
/website/.env*
|
||||
!/website/.dev.vars.example
|
||||
!/website/.env*.example
|
||||
|
||||
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
|
||||
src/generatedMcpToolCatalog.ts
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "6.0.0"
|
||||
".": "6.1.0"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Product Direction
|
||||
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, the bring-your-own Ghidra adapter, and the bring-your-own IDA MCP adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64/arm64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86 and x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. IDA adapts upstream legacy attached GUI and modern headless database-supervisor profiles for live read-only analysis; initial real verification covers Windows, and an attached GUI database is never saved or closed. Keep provider-specific code out of the domain and application layers.
|
||||
|
||||
Prioritize:
|
||||
|
||||
@@ -60,6 +60,8 @@ Keep portable evidence and scenario validation distinct from host-native executi
|
||||
|
||||
Preserve meaningful failure reasons through application and adapter layers. Malformed input is distinct from an unsupported target, unavailable provider, or host operating-system permission denial. Diagnostics should identify the failed constraint and the target or lifecycle request it applies to. Recovery advice must address that reason and point to an available workflow; generic catches must not erase actionable validation details. Preserve collected observations on execution failure independently of whether cleanup succeeds.
|
||||
|
||||
Preserve partial native facts. A shared schema must not require one provider's full metadata before another provider can report the facts it observes. Keep observed classifications and endpoints alongside explicit unknowns for unsupported flags; never invent missing flags or discard known facts to satisfy an all-or-nothing availability shape. Before claiming a native API lacks a capability, inspect the installed API and probe representative producer objects. Exercise the resulting partial representation through CLI, MCP, and composed results such as function dossiers.
|
||||
|
||||
Leave meaningful target, action, capture, and output choices to the agent. A selected operation already expresses intent; do not require approval booleans or repeated permission declarations. Trace each setting to its consumer: remove ignored options and single-value confirmations, derive built-in lifecycle behavior, and supply defaults for omitted optional metadata. Report actual effects and limitations where they help interpret results rather than asking callers to restate them.
|
||||
|
||||
Verify the representation consumed at the next boundary. Internal validator success does not establish JSON Schema validity or client compatibility: validate advertised input and output schemas against their declared dialect after SDK conversion, and keep generated contracts aligned. Use representative producer data and regressions that exercise the failed behavior. Platform and provider support claims require the corresponding real workflow; package startup, capability probes, and mock transport tests establish only their narrower claims. Report unverified coverage explicitly.
|
||||
@@ -90,6 +92,8 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Prioritize real end-to-end workflows, boundary integration, and golden producer data. Keep a focused module test when it covers a distinct failure or semantic case absent from those workflows; test paths do not establish depth. See [docs/testing.md](docs/testing.md) for pruning and classification rules.
|
||||
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, host-permission, and target-identity boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
|
||||
|
||||
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
|
||||
@@ -102,7 +106,7 @@ Development requires Node.js 24.18.0 and npm 11.16.0 (`.nvmrc` and `packageManag
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
|
||||
Use Conventional Commit subjects because Release Please derives changelogs from them. Release Please uses `always-bump-minor`: every release increments minor, including breaking changes. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
|
||||
|
||||
<!-- BEGIN:turborepo-agent-rules -->
|
||||
|
||||
|
||||
+223
@@ -1,5 +1,228 @@
|
||||
# Changelog
|
||||
|
||||
## [6.1.0](https://github.com/morluto/rea/compare/rea-agents-6.0.0...rea-agents-6.1.0) (2026-10-09)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **hopper:** Hopper regex mode uses ECMAScript Unicode syntax instead of Python re. Matching runs in a cancellable worker with a five-second deadline; literal mode keeps Unicode casefold matching.
|
||||
* **hopper:** remove set_current_document. Use open_binary to switch targets; document arguments are scoped to the active target.
|
||||
|
||||
### Features
|
||||
|
||||
* **clients:** register Grok Build and Grok Bot during setup ([#1134](https://github.com/morluto/rea/issues/1134)) ([08561e0](https://github.com/morluto/rea/commit/08561e0eb7a4a01a256f3693a97061fbea7fb8fa))
|
||||
* **ghidra:** make the startup deadline configurable ([#974](https://github.com/morluto/rea/issues/974)) ([d84bc8f](https://github.com/morluto/rea/commit/d84bc8f11abd6cbf5612daf5c8f7e1805d23c16e))
|
||||
* **website:** add GitHub link with live star count ([2f59287](https://github.com/morluto/rea/commit/2f59287f1a1794a68102a45c61cc657659fa5de6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analysis:** bind overview document identity to the active target ([#1094](https://github.com/morluto/rea/issues/1094)) ([81b5104](https://github.com/morluto/rea/commit/81b51042767ad75310bad1a7a9faf954339841c0))
|
||||
* **analysis:** resolve procedure identities and preserve literal queries ([d830cb7](https://github.com/morluto/rea/commit/d830cb79b305e6d4d255f8080de69ca668cbe0af))
|
||||
* **android:** report actual provider readiness ([c715af8](https://github.com/morluto/rea/commit/c715af80587f808de816d3add6a6f989437ab66a))
|
||||
* **android:** report startup and selector failure causes ([6c13fc8](https://github.com/morluto/rea/commit/6c13fc8e0a318ebe7be6b038089b350d99f8e286))
|
||||
* **apple:** account for repeated archive projections ([e1df5e8](https://github.com/morluto/rea/commit/e1df5e85551125b27ef0debb7573de5507b149e1))
|
||||
* **apple:** budget Interface Builder decoding and projection ([dbcf8c3](https://github.com/morluto/rea/commit/dbcf8c33915d924fdbc881bf2cf9676587f3b96f))
|
||||
* **apple:** classify empty dyld settings by consumer semantics ([#1110](https://github.com/morluto/rea/issues/1110)) ([fe297e5](https://github.com/morluto/rea/commit/fe297e56e804130dd49cc0a95fcf2d115c183ae3))
|
||||
* **apple:** decode bundle plist bytes before executable lookup ([d575a73](https://github.com/morluto/rea/commit/d575a7340bb19b84733c79b14de9f4df7181c6b3))
|
||||
* **apple:** decode Interface Builder XML byte encodings ([#1095](https://github.com/morluto/rea/issues/1095)) ([f8e143a](https://github.com/morluto/rea/commit/f8e143a3bcd20ddcc0d76d90e73e238d456fe5a3))
|
||||
* **apple:** derive dispatch coverage from located decode facts ([488f448](https://github.com/morluto/rea/commit/488f448ae27fd62904967ec1e0ab072e834a9822))
|
||||
* **apple:** keep skipped catalog entries from reading as absence ([a079af2](https://github.com/morluto/rea/commit/a079af297bb454417dfd3d89b0037397554ad83e))
|
||||
* **apple:** report apps without asset catalogs as empty inventories ([e2f0378](https://github.com/morluto/rea/commit/e2f03786f496ed742572acd87f47270c958171ec))
|
||||
* **apple:** report apps without asset catalogs as empty inventories ([79e5a39](https://github.com/morluto/rea/commit/79e5a3923db896323c0491e31004214856529b74))
|
||||
* **apple:** report missing keyed archive hierarchy references ([#1104](https://github.com/morluto/rea/issues/1104)) ([3b2ec69](https://github.com/morluto/rea/commit/3b2ec69c54fc9cf65bf8441edbb96e9619c3a37e))
|
||||
* **apple:** validate dylib candidates and preserve search uncertainty ([#1100](https://github.com/morluto/rea/issues/1100)) ([7914e36](https://github.com/morluto/rea/commit/7914e36ab9adbbf591a6e1f3fafcb2e17f55d667))
|
||||
* **apple:** validate physical slices and dylib platform compatibility ([15f92f2](https://github.com/morluto/rea/commit/15f92f298891c8b16d7d13c78cb6ca66cb90cd85))
|
||||
* **application:** budget bridge candidate projection by bytes ([3d450a2](https://github.com/morluto/rea/commit/3d450a29fc91cef3e06ffce9a925ac70dcb24272))
|
||||
* **artifacts:** preserve keyed archive integer precision ([4034883](https://github.com/morluto/rea/commit/4034883e9860a97edee03260fab59f9229debfa6))
|
||||
* **artifacts:** report keyed-archive selection mistakes as invalid input ([5a55d5b](https://github.com/morluto/rea/commit/5a55d5b1c6aefb4a4e5df63ec43b7e2fc0a7d385))
|
||||
* **artifacts:** report keyed-archive selection mistakes as invalid input ([25fb6d3](https://github.com/morluto/rea/commit/25fb6d32a325d88bb0f4e05e16f5f82e4f91c5ca))
|
||||
* **artifacts:** retain failed DMG cleanup state and diagnostics ([#1103](https://github.com/morluto/rea/issues/1103)) ([4bb056b](https://github.com/morluto/rea/commit/4bb056bbb5b735e214ed8d725aa3de1c06fb0dc9))
|
||||
* **browser:** advertise zero-based units for runtime source coordinates ([#1085](https://github.com/morluto/rea/issues/1085)) ([6218d28](https://github.com/morluto/rea/commit/6218d289e241715d0868004caafe15b5c4758450))
|
||||
* **browser:** bind correlated replies to their selected session ([96d3caf](https://github.com/morluto/rea/commit/96d3cafb0725881b6cdab0e2185104957dc69f71))
|
||||
* **browser:** bound aggregate source-map decoding before expansion ([f983c71](https://github.com/morluto/rea/commit/f983c718c33789ee40aec43f84315f6b53e95bde))
|
||||
* **browser:** bound script metadata and preserve omissions ([faf209e](https://github.com/morluto/rea/commit/faf209e50d9faab03a185c1f044de3582552d11e))
|
||||
* **browser:** bound source-map fetches and retain predecessor requests ([eaf5734](https://github.com/morluto/rea/commit/eaf57340831a753faec58246290f9502d159b6b1))
|
||||
* **browser:** compose nested source-map column offsets ([#1109](https://github.com/morluto/rea/issues/1109)) ([8b08ec0](https://github.com/morluto/rea/commit/8b08ec0af936405fd20db25f941e2ae6318c5145))
|
||||
* **browser:** keep initiator callsites out of script coordinate guidance ([#1096](https://github.com/morluto/rea/issues/1096)) ([3c157b4](https://github.com/morluto/rea/commit/3c157b4ec6297e6737bd3c2ec3dc520c7403402f))
|
||||
* **browser:** normalize selected script manifest roots on Windows ([#1078](https://github.com/morluto/rea/issues/1078)) ([b49beb4](https://github.com/morluto/rea/commit/b49beb42d7a97f73d052a27fb99673b1bd0d3d39))
|
||||
* **browser:** preserve captures when response bodies are unavailable ([96af2fa](https://github.com/morluto/rea/commit/96af2fa7444a3777d51ff46202ef563146929dbd))
|
||||
* **browser:** preserve command failures and reject malformed replies ([6b92002](https://github.com/morluto/rea/commit/6b92002597d34555c32ebf24b9db6e4c17c0a08d))
|
||||
* **browser:** treat module-import initiator positions as script positions ([ae0630a](https://github.com/morluto/rea/commit/ae0630a619ee2c98a68e65ca44bf0b93424e2f8e))
|
||||
* **browser:** treat module-import initiator positions as script positions ([99d31ee](https://github.com/morluto/rea/commit/99d31ee24ad060babace9eca3325858418e0f55a))
|
||||
* **browser:** validate and bound source-map evidence ([0a0174b](https://github.com/morluto/rea/commit/0a0174b9ac8a73b61094e857a2950f4e5a06f544))
|
||||
* **build:** exclude the test-only generated catalog from packages ([#1116](https://github.com/morluto/rea/issues/1116)) ([d1c904b](https://github.com/morluto/rea/commit/d1c904bac7977d44b06bf2f823587baedcc88b9e))
|
||||
* **build:** make catalogs and packaged skills generated outputs ([#1086](https://github.com/morluto/rea/issues/1086)) ([e613405](https://github.com/morluto/rea/commit/e6134056cf519410160a196363f8a04f0729324e))
|
||||
* **capture:** retain Electron observations and HAR resource failures ([44fd183](https://github.com/morluto/rea/commit/44fd18305fa5a8893ebb5b952099fdf45dbd6e55))
|
||||
* **ci:** align diagnostic regressions and format merged changes ([67756da](https://github.com/morluto/rea/commit/67756dacc0d435a61d2d0c403412151769da028f))
|
||||
* **ci:** restore generated shard inputs and cleanup assertions ([5f93b4b](https://github.com/morluto/rea/commit/5f93b4b125e78f088a2349a178e60f8c0af3e1a2))
|
||||
* **cli:** accept exact native names through explicit selectors ([c313606](https://github.com/morluto/rea/commit/c3136064c7f8f72b2f6c6799ec41ea6424111bb7))
|
||||
* **cli:** drive mcp doctor help from a single option schema ([b7d803d](https://github.com/morluto/rea/commit/b7d803dd10c7ef8bb73f81f5e893168af8ea86e6))
|
||||
* **cli:** preserve blank JSON paths for input validation ([#1068](https://github.com/morluto/rea/issues/1068)) ([3dcb732](https://github.com/morluto/rea/commit/3dcb732da33f6ceef597506b14a4536f1c9aff96))
|
||||
* **cli:** preserve web cancellation exit status ([1d9b96a](https://github.com/morluto/rea/commit/1d9b96a9c497694592137cf95bea2af0a190abe7))
|
||||
* **cli:** support MCP doctor help ([a69b833](https://github.com/morluto/rea/commit/a69b8337ab38707aaad2ac2fedb652f05afeb71c))
|
||||
* **contracts:** preserve local inputs and comparison uncertainty ([70e8a8f](https://github.com/morluto/rea/commit/70e8a8f86060496452ccbc1a49b39822efef4113))
|
||||
* **docs:** validate translated links to canonical product facts ([34f7c4f](https://github.com/morluto/rea/commit/34f7c4fd661e07db66c560a4d54272b0163a52b6))
|
||||
* **domain:** order canonical paths by code point, not UTF-16 unit ([#1034](https://github.com/morluto/rea/issues/1034)) ([b5c2954](https://github.com/morluto/rea/commit/b5c2954ad67f0280732df043d5d9702f3d202819))
|
||||
* **domain:** preserve prototype-named JSON members through Evidence boundaries ([#1049](https://github.com/morluto/rea/issues/1049)) ([40e18b2](https://github.com/morluto/rea/commit/40e18b2e37f2be11f452606c2d875981b03ad03e))
|
||||
* **domain:** reject excessive JSON depth with actionable diagnostics ([#1029](https://github.com/morluto/rea/issues/1029)) ([bfbfc73](https://github.com/morluto/rea/commit/bfbfc7330aa7175018de04f1d099b8d4aeec3bd1))
|
||||
* **electron:** bound hook retention and report dropped coverage ([02d0702](https://github.com/morluto/rea/commit/02d07027617a5df014f92a351aa9418aade6f06f))
|
||||
* **evidence:** name the failed constraint when a bundle is rejected ([82a1a52](https://github.com/morluto/rea/commit/82a1a525d7aa725d939926ba953434fbf5db0ab0))
|
||||
* **evidence:** name the failed constraint when a bundle is rejected ([cf96ceb](https://github.com/morluto/rea/commit/cf96ceb2119f52f61a621d2a790b7c8804c65595))
|
||||
* **evidence:** report missing evidence files instead of permission advice ([e8b92a4](https://github.com/morluto/rea/commit/e8b92a40c003a5e6ce3cefe4ad18392d39759653))
|
||||
* **evidence:** report missing evidence files instead of permission advice ([89252f1](https://github.com/morluto/rea/commit/89252f1f35de91494bd49460b1645d8c09cc6d33))
|
||||
* **evidence:** require a complete record before the single-record hint ([38bbb65](https://github.com/morluto/rea/commit/38bbb656da2659088e30a1ca3e4434b2f65bab32))
|
||||
* **firmware:** reject malformed UTF-8 producer reports ([4a45c14](https://github.com/morluto/rea/commit/4a45c14742e6868f687cead2ddf736c3f7c49acd))
|
||||
* **ghidra:** admit Linux arm64 with matching native tools ([3e598b2](https://github.com/morluto/rea/commit/3e598b2d38a728cd9fd4a359741500a021ba9d3a))
|
||||
* **ghidra:** harden location resolution and provider lifecycle ([1f5be75](https://github.com/morluto/rea/commit/1f5be750bced759339183945488cea352b18f28b))
|
||||
* **ghidra:** make qualified function renames idempotent ([4458128](https://github.com/morluto/rea/commit/4458128a845c1448140b44d6e62d6ccfe611d37c))
|
||||
* **ghidra:** parse long procedure selectors without recursion ([480b16b](https://github.com/morluto/rea/commit/480b16b9c151e1d4bfbca8b49a0668d74ceaa400))
|
||||
* **ghidra:** preserve function references and snapshot boundaries ([84a17d5](https://github.com/morluto/rea/commit/84a17d55199b1b46493aea6bf4e41029a3b200fc))
|
||||
* **ghidra:** preserve POSIX runtime paths with spaces ([80c3d36](https://github.com/morluto/rea/commit/80c3d36dfa2597f34fdeb69e0ba75277ee50c85c))
|
||||
* **ghidra:** preserve source failures and snapshot ownership ([704fa76](https://github.com/morluto/rea/commit/704fa7699ca183d5d5e0518a6fcdbb953ec35a10))
|
||||
* **ghidra:** preserve Unicode boundaries without breaking sessions ([bef234d](https://github.com/morluto/rea/commit/bef234d6ce63cd296a654400443fcdbbcd44d7de))
|
||||
* **ghidra:** recover regex stack exhaustion without losing annotations ([bc4aa46](https://github.com/morluto/rea/commit/bc4aa46d7c3960a1f325523e48aed2089b176e84))
|
||||
* **ghidra:** reject address truncation before reads and edits ([dcae3c6](https://github.com/morluto/rea/commit/dcae3c6c148582d235f5650aace6f9744eeb153c))
|
||||
* **ghidra:** resolve exact external function entries ([549effa](https://github.com/morluto/rea/commit/549effa056729e5f9239d9ed7864537a9c49e421))
|
||||
* **hopper:** bind analysis to the active target and correct boundary contracts ([2f71375](https://github.com/morluto/rea/commit/2f71375efc517bb9e6f9800f8e8a6849766bc893))
|
||||
* **hopper:** bind file mappings to verified source bytes ([82626c4](https://github.com/morluto/rea/commit/82626c4499a53c227187575d40a845fadd5318e2))
|
||||
* **hopper:** diagnose MCP provider failures with stage, state, and request context ([#1044](https://github.com/morluto/rea/issues/1044)) ([5a67c7f](https://github.com/morluto/rea/commit/5a67c7fc540c494371de76dcdf353923656993d3))
|
||||
* **hopper:** enforce caller-supplied request deadlines ([#1119](https://github.com/morluto/rea/issues/1119)) ([e8939ca](https://github.com/morluto/rea/commit/e8939ca2208bc41454b73b90a32418714e6a2969))
|
||||
* **hopper:** isolate regex matching from the native API thread ([f201b04](https://github.com/morluto/rea/commit/f201b04fbbb29591b0d2bb3f3b24636526e4ec4e))
|
||||
* **hopper:** load verified FAT64 slices with native Mach-O semantics ([0a49d5b](https://github.com/morluto/rea/commit/0a49d5bfb931124d3d398dfbd64e9f6ae8a01405))
|
||||
* **hopper:** normalize native block endpoints and retain terminal evidence ([2847311](https://github.com/morluto/rea/commit/2847311a579ce2da4c8ded4d89fcdd01545e6da5))
|
||||
* **hopper:** preserve launcher evidence on readiness timeout ([458771c](https://github.com/morluto/rea/commit/458771cb583e38b848b3d8c58595d163143ff50a))
|
||||
* **hopper:** preserve native navigation and annotation semantics ([f2e22e1](https://github.com/morluto/rea/commit/f2e22e13a6956985bd65a2d75e4db7e3128cce81))
|
||||
* **hopper:** preserve owned basic-block endpoint instructions ([#1118](https://github.com/morluto/rea/issues/1118)) ([329cf20](https://github.com/morluto/rea/commit/329cf206d1111c051733c7115cabff4b46722deb))
|
||||
* **hopper:** preserve partial native call and string evidence ([2461702](https://github.com/morluto/rea/commit/246170238ebb8669a20b57b559b03cfc26ee1b8c))
|
||||
* **hopper:** preserve typed string values and verify real searches ([7d7b3f9](https://github.com/morluto/rea/commit/7d7b3f961af2dc9b7572e91aaec29e6c4eccf9d5))
|
||||
* **hopper:** report launcher failures before bridge timeout ([#1105](https://github.com/morluto/rea/issues/1105)) ([cd756b0](https://github.com/morluto/rea/commit/cd756b07d5b9c06e085d778e9f1c4fc9bd403219))
|
||||
* **hopper:** resolve FAT loaders and original file coordinates ([35f1ddd](https://github.com/morluto/rea/commit/35f1ddd6bc77588b9971961cd70968a815aae266))
|
||||
* **hopper:** validate annotation destinations and symbol extents ([b2d5bc1](https://github.com/morluto/rea/commit/b2d5bc110914f2ad42d17bc739d5f37c1b0f0a4d))
|
||||
* **hopper:** verify lease cleanup and repair the macOS test lane ([#1111](https://github.com/morluto/rea/issues/1111)) ([b743bd2](https://github.com/morluto/rea/commit/b743bd20a0cbc39728e2e822fa75c013d87e1730))
|
||||
* **inspector:** bound retained metadata and preserve cancellation ([e714fb0](https://github.com/morluto/rea/commit/e714fb027afdf11bfbb0838722523d3fa9ebf244))
|
||||
* **install:** guard empty prefix_args expansion on macOS Bash 3.2 under set -u ([#1061](https://github.com/morluto/rea/issues/1061)) ([68b9fa4](https://github.com/morluto/rea/commit/68b9fa489b0c07f580633785ec61c17fa20b5083))
|
||||
* **javascript:** bound primitive string growth and repeated evaluation ([4a1837d](https://github.com/morluto/rea/commit/4a1837d9d52b1a6454469e2dc63eeb8d716fb296))
|
||||
* **javascript:** bound semantic expansion and resolve lexical receivers ([16f4609](https://github.com/morluto/rea/commit/16f4609d689445c05a0befc744fce7e72f7047ec))
|
||||
* **javascript:** classify direct defaults and argv elements ([6495171](https://github.com/morluto/rea/commit/649517181268a0b729970b7cbbda15e04b199e32))
|
||||
* **javascript:** honor cancellation before evidence publication ([#1098](https://github.com/morluto/rea/issues/1098)) ([cc0ee2e](https://github.com/morluto/rea/commit/cc0ee2edc60dda42d7b7cfb2c08e39b0b736b5e9))
|
||||
* **javascript:** omit self-referential import edges with disclosure ([#1130](https://github.com/morluto/rea/issues/1130)) ([1d7115b](https://github.com/morluto/rea/commit/1d7115b7b470719a32e477ca3cbb681f0ad408b8))
|
||||
* **javascript:** parse plain TypeScript artifact dialects ([#1102](https://github.com/morluto/rea/issues/1102)) ([3db4681](https://github.com/morluto/rea/commit/3db46813f80f84504c5b87c73a4bece82a8fdc5e))
|
||||
* **javascript:** preserve HTML carriage return source ranges ([#1092](https://github.com/morluto/rea/issues/1092)) ([23862af](https://github.com/morluto/rea/commit/23862af1b57413e09abf394f6e4f8369f6355d5a))
|
||||
* **javascript:** preserve lexical open and exact template semantics ([d6cc9ca](https://github.com/morluto/rea/commit/d6cc9ca3a6e92604c837f61c193baf5abedaabc8))
|
||||
* **javascript:** preserve original BOM source coordinates ([#1093](https://github.com/morluto/rea/issues/1093)) ([83defc0](https://github.com/morluto/rea/commit/83defc054b69e94da888d70bb3b05d835e1bf74d))
|
||||
* **javascript:** retain NodeNext artifact source facts ([#1087](https://github.com/morluto/rea/issues/1087)) ([c1b370c](https://github.com/morluto/rea/commit/c1b370ce0dfcbd5725cf6aebe23b022dfc75ac84))
|
||||
* **javascript:** use Node legacy package entry fields ([1706005](https://github.com/morluto/rea/commit/170600541bf44f2a63731e8b3d42aae3cbb8d6b3))
|
||||
* **managed:** bind inspection evidence to artifact identity ([73b09f4](https://github.com/morluto/rea/commit/73b09f4c5c511ce80baa0c89717181bbe0ecba8f))
|
||||
* **managed:** match exact method bytes without decoded signatures ([17809ca](https://github.com/morluto/rea/commit/17809caacd0f44fcc7c1af42932ea655489d64ae))
|
||||
* **mcp:** flatten root union input schemas ([#1062](https://github.com/morluto/rea/issues/1062)) ([6a7650e](https://github.com/morluto/rea/commit/6a7650e93cba9169baad95d2c52b11351ae39d0e))
|
||||
* **mcp:** preserve retained evidence on oversized delivery ([0639a54](https://github.com/morluto/rea/commit/0639a541a3b712940119f59e8189bc962b267726))
|
||||
* **mcp:** retain complete application results across response limits ([a1ed423](https://github.com/morluto/rea/commit/a1ed4232de635d4acc0dd9eb293e309a29d03e80))
|
||||
* **mcp:** retain oversized errors before transport delivery ([b3f1b4e](https://github.com/morluto/rea/commit/b3f1b4e822aec5626d1952a88f50c7520dc265d3))
|
||||
* **metadata:** stop schema changes churning generated skill evidence ([#1075](https://github.com/morluto/rea/issues/1075)) ([d52f66d](https://github.com/morluto/rea/commit/d52f66d62a93ec09cccdf53e64e3ccd24d16e520))
|
||||
* **native:** bind signature observations to registered target versions ([#1099](https://github.com/morluto/rea/issues/1099)) ([c789af0](https://github.com/morluto/rea/commit/c789af04b76c0e76a522a5dece34f461288f5ecd))
|
||||
* **native:** bound retained observations and preserve failure evidence ([7c32387](https://github.com/morluto/rea/commit/7c323870c2dcab78654ee7a80128b06affdf2bbe))
|
||||
* **native:** preserve bridge failure types during cleanup ([1d0baa3](https://github.com/morluto/rea/commit/1d0baa352fc76d1760abbe29a3cc80fa17296916))
|
||||
* **native:** resample tool identity for each invocation ([14b19a0](https://github.com/morluto/rea/commit/14b19a0b8a5af401212c552173e761eac53bfe46))
|
||||
* **native:** share command supervision and retain failure output ([f2e9c49](https://github.com/morluto/rea/commit/f2e9c491ea00bf79d5665102d95a3339b63cfe4f))
|
||||
* **native:** supervise LLDB and preserve bounded capture uncertainty ([915855e](https://github.com/morluto/rea/commit/915855e75ca3fe3b1b54978ace6bbe10d1973f64))
|
||||
* **process:** avoid cold ownership helper deadline exhaustion ([#1139](https://github.com/morluto/rea/issues/1139)) ([65a809a](https://github.com/morluto/rea/commit/65a809a0da0555aa8eeafbc2ec2559517471be02))
|
||||
* **process:** derive executable names from the capture host ([#1108](https://github.com/morluto/rea/issues/1108)) ([34cc925](https://github.com/morluto/rea/commit/34cc9259c1076a0d9575a87679befca692ec86c2))
|
||||
* **process:** identify unknown live processes in cleanup diagnostics ([eb89a16](https://github.com/morluto/rea/commit/eb89a16d9bc51b65dcc75e05f8492de894320753))
|
||||
* **process:** name the reason two captures cannot be compared ([#1080](https://github.com/morluto/rea/issues/1080)) ([eb69763](https://github.com/morluto/rea/commit/eb69763d27e29fe3247c008925016e4c73cb6abe))
|
||||
* **process:** preserve observations after successful cleanup ([2af12a1](https://github.com/morluto/rea/commit/2af12a18a36bb80fb7d91ef4c8009212750daa0a))
|
||||
* **process:** retain ownership after incomplete cleanup ([e2fc249](https://github.com/morluto/rea/commit/e2fc2493b6a59f0663495d277c0cfec1aeb25a7a))
|
||||
* **process:** stabilize native capture and ownership prerequisites ([2b27515](https://github.com/morluto/rea/commit/2b2751547662f8cb142f96d91eff50388a1ecb31))
|
||||
* **process:** stop failing captures on tokens macOS cannot expose ([#1057](https://github.com/morluto/rea/issues/1057)) ([5547c1e](https://github.com/morluto/rea/commit/5547c1e697af8c2e1c771abe5344d10b7c1cd340))
|
||||
* **providers:** validate ELF versions and preserve probe cancellation ([8b01e0c](https://github.com/morluto/rea/commit/8b01e0cc8904b512b517b4471ef2150f40c98f0d))
|
||||
* **release:** always bump minor for releases ([be783ef](https://github.com/morluto/rea/commit/be783efaba62dc76c5d8d42229869a32afdb302a))
|
||||
* **release:** publish prereleases under the next npm tag ([bfb7776](https://github.com/morluto/rea/commit/bfb7776006303c6164dba2f22b8e81fdb7c20beb))
|
||||
* **release:** publish reviewed release PR merges ([cdddfdd](https://github.com/morluto/rea/commit/cdddfddf495ffd172f62a6a801f7697fe502847e))
|
||||
* **release:** recognize conventional titles in GitHub merge commits ([f5009ed](https://github.com/morluto/rea/commit/f5009ed55f081939409808870458162991f836ca))
|
||||
* **release:** restore automatic release proposals on main ([acb3e0f](https://github.com/morluto/rea/commit/acb3e0f2e5b38a9c97df7ed78c8e6e5f38bfa08f))
|
||||
* **release:** validate frozen checkpoint versions and breaking notes ([8a31508](https://github.com/morluto/rea/commit/8a31508c8958800364e255540ded1586298d7008))
|
||||
* **release:** validate frozen checkpoint versions and breaking notes ([60825fb](https://github.com/morluto/rea/commit/60825fb44751c9482bdf4cafdac3599da7c82d45)), closes [#1063](https://github.com/morluto/rea/issues/1063)
|
||||
* select Node legacy package entry fields for import and require ([#1035](https://github.com/morluto/rea/issues/1035)) ([1706005](https://github.com/morluto/rea/commit/170600541bf44f2a63731e8b3d42aae3cbb8d6b3))
|
||||
* **session:** serialize snapshot lifecycle with admitted requests ([e44cb45](https://github.com/morluto/rea/commit/e44cb45908af8d3902e0db044e0cf7b86cdd7ed0))
|
||||
* **setup:** launch Windows npx registrations through cmd.exe ([#1072](https://github.com/morluto/rea/issues/1072)) ([ef30343](https://github.com/morluto/rea/commit/ef30343154d64d78b620d41c37457eee5d99fc71))
|
||||
* **setup:** treat empty client config files as new documents ([#1117](https://github.com/morluto/rea/issues/1117)) ([9fd65ce](https://github.com/morluto/rea/commit/9fd65ce0711ce9257aee15e62a5ecf9832960035))
|
||||
* **snapshot:** retain MCP workflow replay bindings ([5e1afe4](https://github.com/morluto/rea/commit/5e1afe454f2f283eeeea4532d07a9bdbea8bd51e))
|
||||
* **target:** preserve app bundle filesystem failure reasons ([d4c2c3c](https://github.com/morluto/rea/commit/d4c2c3c18f691d04526fe38867347faa2530861d))
|
||||
* **update:** accept npm 12 release metadata ([#1138](https://github.com/morluto/rea/issues/1138)) ([567a82f](https://github.com/morluto/rea/commit/567a82f8d4feb629a7c10d2cc9180377eb1f6f66))
|
||||
* **verify:** close both sides of the browser proxy ([f7366a5](https://github.com/morluto/rea/commit/f7366a517ff0545f01df6202b1ed3231d9dd3b4a))
|
||||
* **verify:** isolate package archives and published canary caches ([3eb7dbd](https://github.com/morluto/rea/commit/3eb7dbdc1689737b44cc4ff044bcb1a311511969))
|
||||
* **verify:** remove stale mocked bridge package probe ([826583d](https://github.com/morluto/rea/commit/826583dd43fc918c70a87dfe148c2fdbc8612f3b))
|
||||
* **verify:** report final success only after resource cleanup ([ee043e2](https://github.com/morluto/rea/commit/ee043e2cbf30152c60b403fff050a0fc848a3286))
|
||||
* **workflows:** keep caller-input details in workflow input errors ([#1089](https://github.com/morluto/rea/issues/1089)) ([289aef6](https://github.com/morluto/rea/commit/289aef6838e424c14f331a9905020079e6b7539b))
|
||||
* **workflows:** retain residual unknowns through CLI and MCP ([0136ee8](https://github.com/morluto/rea/commit/0136ee8c22300ed351af5eab6329ab4d271b5bb6))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **mcp:** advertise each tool JSON Schema once per target ([#1059](https://github.com/morluto/rea/issues/1059)) ([5ccb924](https://github.com/morluto/rea/commit/5ccb9241be15bb6a19b99fbbfb27ff3356ba157b))
|
||||
* **build:** avoid duplicate documentation generation ([c701376](https://github.com/morluto/rea/commit/c701376777212ec9acfd10c97f45b1eaf0c885c6))
|
||||
* **javascript:** index configuration and module lookups ([67252b2](https://github.com/morluto/rea/commit/67252b23b9e35e9526fed7842dab47a2c7c9fe5f))
|
||||
* **javascript:** reuse validated immutable graphs ([#1112](https://github.com/morluto/rea/issues/1112)) ([ec6db0f](https://github.com/morluto/rea/commit/ec6db0f0630c4b0b35f0db84c0030784c1aa58a6))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **artifacts:** centralize stable inventory identities ([135d102](https://github.com/morluto/rea/commit/135d102f9df844d70d352870afce80f67c0ae96f))
|
||||
* **browser:** charge source-map records before retention ([e762e32](https://github.com/morluto/rea/commit/e762e32afbba3fd32532fd42a3f4fdcd05252ad3))
|
||||
* **config:** prune unused format wrappers and launcher export ([81ae838](https://github.com/morluto/rea/commit/81ae8385679ef10b95d34d0f0b5ba11732ac86b5))
|
||||
* **input:** share schema issue error construction ([65a6288](https://github.com/morluto/rea/commit/65a6288257e4c3cab9adb53b0f9123ef567b61bd))
|
||||
* **setup:** validate formats through canonical configuration ([4b10665](https://github.com/morluto/rea/commit/4b1066501b379107b2375e270f386fc9855a85c1))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add Persian README translation with full language navigation ([7a7460f](https://github.com/morluto/rea/commit/7a7460f04f3cfd7e365d70ff891bdd0c474901b6))
|
||||
* add ten README translations and expand language navigation ([a61491a](https://github.com/morluto/rea/commit/a61491ad00b21e912e16f68bff829927aed6e16f))
|
||||
* align translated READMEs and website links with English ([d83f098](https://github.com/morluto/rea/commit/d83f0983d5aa18d29d95d24c4ccb3928be6f29f5))
|
||||
* celebrate 20,000 GitHub stars 🎉 ([#1079](https://github.com/morluto/rea/issues/1079)) ([9455e7c](https://github.com/morluto/rea/commit/9455e7c8bc9d5c4a9a1fc9077aa15007ad80997e))
|
||||
* check surrounding cases when changing boundaries ([46d195e](https://github.com/morluto/rea/commit/46d195e8a48b710fc355e84cb6b79461b9c39005))
|
||||
* clarify contribution scope and cosmetic suggestions ([1f79002](https://github.com/morluto/rea/commit/1f7900249783956070af49c10f86002a0dfdc42b))
|
||||
* clarify evidence ownership and verification guidance ([a9e00b8](https://github.com/morluto/rea/commit/a9e00b83f37651a8dd241375615b248062e04227))
|
||||
* move Portuguese toward the end of language navigation ([2a4979b](https://github.com/morluto/rea/commit/2a4979b7cdc1a517865573d6c36654c022347d0a))
|
||||
* move star milestone into history sections across readmes ([6e6ff7b](https://github.com/morluto/rea/commit/6e6ff7bd2eabb668602258ad5e31eb758b31f823))
|
||||
* require partial native facts at provider boundaries ([2b86ea9](https://github.com/morluto/rea/commit/2b86ea9c7cd3cfc2141a021f8b531dbc84395c81))
|
||||
* require surrounding boundary checks for review fixes ([9833c94](https://github.com/morluto/rea/commit/9833c94656e1d54e7711dc483bf8091ced14234f))
|
||||
* **testing:** prioritize real workflows and distinct regressions ([9588482](https://github.com/morluto/rea/commit/9588482bfeeea869d7274d27fb96bac70278dd42))
|
||||
* **website:** add a CTF showcase and FAQ ([#1077](https://github.com/morluto/rea/issues/1077)) ([a4d77f0](https://github.com/morluto/rea/commit/a4d77f0ff0242b7ab891030d2ffa09a4b4bc1c3c))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **acceptance:** add application workflow MCP parity harness and scenarios ([5ac3dc9](https://github.com/morluto/rea/commit/5ac3dc96f064e691b357aeddc11fa76b7b4d66d1))
|
||||
* **artifacts:** retire simulated mounts after detach failures ([f591e37](https://github.com/morluto/rea/commit/f591e37a1e45d9b7179ed788c4e7ee77a708c6b0))
|
||||
* **artifacts:** verify native directory identity on supported Windows runtimes ([#1069](https://github.com/morluto/rea/issues/1069)) ([e94be95](https://github.com/morluto/rea/commit/e94be95c66515fa6fba2a46e4b29850d34141fef))
|
||||
* **browser:** pin DOM and source-map URL policy classifications ([#1120](https://github.com/morluto/rea/issues/1120)) ([c1d0002](https://github.com/morluto/rea/commit/c1d0002470dc2b6bde73e4ffe438e734dbb24d8d))
|
||||
* **browser:** reject unmodeled CDP commands in protocol fixtures ([017297c](https://github.com/morluto/rea/commit/017297c40c0c38b9d9c17f5194f0f4c51748f0c3))
|
||||
* classify application scenarios by their actual boundaries ([0e0b386](https://github.com/morluto/rea/commit/0e0b386dc35bdee1174df9a2d718e0f3e5793666))
|
||||
* **cli:** verify self-import parity and cleanup ([55c9ce2](https://github.com/morluto/rea/commit/55c9ce26d1b52f359a21512103088048f9aa7492))
|
||||
* consolidate failure guards and trim unused fixture APIs ([ffd9611](https://github.com/morluto/rea/commit/ffd9611308c91b894a7b0d27f331238f5117dacf))
|
||||
* **hopper:** cover real boundary failures and CLI parity ([64717ad](https://github.com/morluto/rea/commit/64717ad7daa3152eeb718b4eb5fdcc77f1d76542))
|
||||
* **hopper:** verify real boundaries and prune redundant happy paths ([7005166](https://github.com/morluto/rea/commit/7005166f5878737fb7b3c7ec4e3d2ee5f4dcce30))
|
||||
* **mcp:** serialize real process capture and report unavailable authority ([8f9da25](https://github.com/morluto/rea/commit/8f9da251647beb530f6851c9dad5040b19a50a35))
|
||||
* **process:** cover argv rewrites in native ownership inspection ([09d7b9b](https://github.com/morluto/rea/commit/09d7b9bab53ae968502fd6e19df694cca8cab817))
|
||||
* **process:** distinguish native startup from producer verification ([9a70fb4](https://github.com/morluto/rea/commit/9a70fb482c974658088167c9d279bd70159e5dc8))
|
||||
* **process:** expose unavailable capture and unexpected teardown failures ([90b06db](https://github.com/morluto/rea/commit/90b06db6c0b2c5863f292f63a226a5c5678a86ac))
|
||||
* **process:** report the cause of terminal capture failures ([8a4c4b6](https://github.com/morluto/rea/commit/8a4c4b657fe3b0c249dd2e2147f6960361684c59))
|
||||
* **process:** retain cancellation checks under unverified host cleanup ([9a5b077](https://github.com/morluto/rea/commit/9a5b0778993102460e333de6cad3ba85383c10be))
|
||||
* **process:** retire native caches at worker teardown ([a0784c0](https://github.com/morluto/rea/commit/a0784c0d2cb70c48ecfeb0be23c6d68de5ee5be1))
|
||||
* **process:** serialize real CLI capture boundaries ([82560eb](https://github.com/morluto/rea/commit/82560ebf593a7dfc704698ee312e4d8f1629be8f))
|
||||
* prune redundant cases and promote configuration lifecycle coverage ([5945915](https://github.com/morluto/rea/commit/5945915f91adf85de91e16b69534bc3ed1d1b69a))
|
||||
* prune resource-recovery wording assertions ([ae06dcf](https://github.com/morluto/rea/commit/ae06dcf83f65b06abb390d2e0b0ea33543278d6e))
|
||||
* remove duplicate result assertions before throwing guards ([a1b7d40](https://github.com/morluto/rea/commit/a1b7d40bb0212cdfdddd770a1bccbed097601e0a))
|
||||
* **session:** preserve live binding after cancelled switches ([1136ea8](https://github.com/morluto/rea/commit/1136ea881029eab59ffb0834149e86a6aff69e3b))
|
||||
* **setup:** scope removal regression to its fixture client ([fe48e77](https://github.com/morluto/rea/commit/fe48e779c264745d28b5a819e51c06e6b5bd2776))
|
||||
* **workflows:** verify residual questions in CLI snapshots ([770d2f6](https://github.com/morluto/rea/commit/770d2f60aa948cfdb61c71304be2f3e35f673840))
|
||||
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
* run signature target binding regressions on native macOS ([#1107](https://github.com/morluto/rea/issues/1107)) ([1e63144](https://github.com/morluto/rea/commit/1e63144390070f569998271fafd1459788ef3265))
|
||||
|
||||
## [6.0.0](https://github.com/morluto/rea/compare/rea-agents-5.0.0...rea-agents-6.0.0) (2026-10-08)
|
||||
|
||||
|
||||
|
||||
+39
-12
@@ -2,6 +2,29 @@
|
||||
|
||||
REA welcomes focused bug fixes, documentation improvements, tests, and reverse-engineering workflow enhancements. Open an issue before a large contract or architecture change so its scope can be agreed before implementation.
|
||||
|
||||
## Choosing a contribution
|
||||
|
||||
Small contributions are welcome when they solve a concrete problem, including
|
||||
broken commands, misleading documentation, and missing regression coverage.
|
||||
Explain the problem, keep the diff focused, and share how you checked the change.
|
||||
|
||||
Please avoid standalone cosmetic changes without a clear benefit, such as fixing
|
||||
a typo in an internal comment, changing capitalization or punctuation in
|
||||
already-clear prose, or reformatting unchanged code. Include related polish in a
|
||||
useful fix when practical. For cosmetic-only suggestions, please open an issue
|
||||
instead of a standalone PR so they can be collected and addressed together.
|
||||
Corrections to confusing explanations or incorrect instructions are welcome.
|
||||
|
||||
Avoid speculative cleanup without a concrete problem to solve. Keep related
|
||||
corrections in one PR.
|
||||
|
||||
Before requesting review, read the final diff and run the relevant checks. Be clear
|
||||
about anything you couldn't test. You should be able to explain the changes you
|
||||
submit.
|
||||
|
||||
We may decline changes when the benefit doesn't justify the review or maintenance
|
||||
work.
|
||||
|
||||
For capability organization and provider composition, follow the incremental
|
||||
[migration guide](docs/capability-migration.md). Run `npm run verify:test-discovery`
|
||||
after adding or moving tests.
|
||||
@@ -39,10 +62,14 @@ use `npm run docs:generate`; `docs:build` generates them before building the web
|
||||
|
||||
Pull requests run `npm run docs:check`. `.github/workflows/pages.yml` is a
|
||||
manual VitePress build and does not publish. The public site at
|
||||
<https://morluto.github.io/rea/> is published only by the manual website
|
||||
workflow on `main`. The repository's **Settings → Pages → Build and
|
||||
deployment → Source** must be set to **GitHub Actions** before the first
|
||||
deployment.
|
||||
<https://rea.tools/> serves `website/public/` through Cloudflare Workers. See
|
||||
the [website README](website/README.md#cloudflare-workers) for previews and
|
||||
manual deployments.
|
||||
|
||||
The separate GitHub Pages host uses only the manual
|
||||
`.github/workflows/website-pages.yml` workflow on `main`. The repository's
|
||||
**Settings → Pages → Build and deployment → Source** must be set to
|
||||
**GitHub Actions** before deploying there.
|
||||
|
||||
## Development feedback and PR verification
|
||||
|
||||
@@ -138,14 +165,14 @@ Describe the behavior change and verification performed in the pull request. Nev
|
||||
|
||||
## Maintainer release checklist
|
||||
|
||||
Use the [checkpoint release guide](docs/releasing.md). Releases start from an
|
||||
explicit `release/VERSION` branch cut at a recorded commit. Manually prepare
|
||||
the bot PR, wait for its exact-head CI and review, merge into that frozen
|
||||
branch, and manually publish through the official Release workflow. Main
|
||||
pushes do not update release PRs or publish packages. Both publishers build
|
||||
the exact SHA tagged by Release Please.
|
||||
Use the [release guide](docs/releasing.md). Main pushes automatically refresh a
|
||||
Release Please PR with the next version and changelog. Review its migration
|
||||
notes, wait for the current CI checks to pass, and merge it into main to publish.
|
||||
The Release workflow validates and tags that merge; both publishers build its
|
||||
exact SHA. Ordinary main pushes cannot publish packages. An optional manual
|
||||
`release/VERSION` checkpoint retains explicit preparation and publication.
|
||||
|
||||
Keep new implementation commits on main for the next release. The workflow
|
||||
owns packaged-artifact verification, npm publication, the published CLI/MCP
|
||||
canary, and MCP Registry publication. Sync release metadata back to main after
|
||||
publication; see the guide for partial-publication recovery and verification.
|
||||
canary, and MCP Registry publication. Sync manual checkpoint metadata back to
|
||||
main after publication; see the guide for recovery and verification.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
**English** · [简体中文](README_zh.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [العربية](README_ar.md)
|
||||
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [Showcases](https://morluto.github.io/rea/showcase/)**
|
||||
**[Website](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Showcases](https://rea.tools/showcase/)**
|
||||
|
||||
[Quick start](#quick-start) · [How REA works](#how-rea-works) · [What you can analyze](#what-you-can-analyze) · [Showcases](#showcases) · [FAQ](#faq) · [Documentation](#documentation)
|
||||
|
||||
@@ -54,7 +54,7 @@ REA connects your agent to tools for inspecting native binaries, JavaScript and
|
||||
|
||||
Setup registers REA with your agent and installs matching workflow instructions. Native analysis can use an existing Hopper or Ghidra installation; setup can optionally install Hopper with approval. Static JavaScript analysis needs neither engine.
|
||||
|
||||
> **[Visit the REA website](https://morluto.github.io/rea/)** for setup instructions, illustrated guides, and real case studies.
|
||||
> **[Visit the REA website](https://rea.tools/)** for setup instructions, illustrated guides, and real case studies.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -70,7 +70,7 @@ Choose your agents, review the proposed changes, and approve them. Setup adds
|
||||
REA's MCP server and matching workflow instructions, with backups of existing
|
||||
configuration. Restart your agent afterward.
|
||||
|
||||
Setup supports Claude Code, Codex, Cursor, Gemini CLI and
|
||||
Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build and
|
||||
[other agents](docs/installation.md#supported-agents). See
|
||||
[installation and setup](docs/installation.md) for provider configuration and
|
||||
manual MCP registration.
|
||||
@@ -149,12 +149,12 @@ 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://morluto.github.io/rea/guides/native/) |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
|
||||
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools; 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://morluto.github.io/rea/guides/javascript/) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://morluto.github.io/rea/guides/browser/) |
|
||||
| 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/) |
|
||||
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | Static inspection; [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; [Android guide](docs/android-analysis.md) |
|
||||
@@ -173,6 +173,8 @@ Native formats and host support vary by provider. See
|
||||
[IDA guide](docs/ida-provider.md), and
|
||||
[experimental Windows Ghidra support](docs/windows-ghidra-p0.md).
|
||||
Ghidra also supports [16-bit DOS analysis](docs/ghidra-dos.md).
|
||||
For large binaries, raise its startup deadline with
|
||||
`REA_GHIDRA_STARTUP_TIMEOUT_MS`.
|
||||
For provider selection, see the [CLI guide](docs/cli.md#choose-a-provider).
|
||||
Check [release availability](docs/installation.md#released-package-and-main)
|
||||
for features added since the latest npm release.
|
||||
@@ -185,7 +187,7 @@ Follow a sound call into its position-to-pan helper, inspect the instructions,
|
||||
and turn incomplete pseudocode into C. The reconstruction passes 3,205
|
||||
original-x86 cases and reproduces all 63 compiled function bytes.
|
||||
|
||||
[Read the case study](https://morluto.github.io/rea/showcase/dx-ball/) ·
|
||||
[Read the case study](https://rea.tools/showcase/dx-ball/) ·
|
||||
[Reconstruction repository](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: trace the Electron clipboard bridge
|
||||
@@ -193,7 +195,7 @@ original-x86 cases and reproduces all 63 compiled function bytes.
|
||||
Find the renderer's clipboard API, follow it through preload and IPC into the
|
||||
main process, and inspect the rich clipboard format.
|
||||
|
||||
[Read the case study](https://morluto.github.io/rea/showcase/notion/)
|
||||
[Read the case study](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: recover a DOS bullet-ring calculation
|
||||
|
||||
@@ -201,7 +203,7 @@ Inspect the original PC-98 game's 16-bit instructions, recover the fixed and
|
||||
aimed angle calculations, and compare the reconstructed C++ with the
|
||||
historical compiler output.
|
||||
|
||||
[Read the case study](https://morluto.github.io/rea/showcase/th04/) ·
|
||||
[Read the case study](https://rea.tools/showcase/th04/) ·
|
||||
[Reconstruction repository](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
If you've used REA on something interesting, we'd love to see it. Share your
|
||||
@@ -289,7 +291,7 @@ with your REA version, target type, steps to reproduce and error output.
|
||||
|
||||
## Documentation
|
||||
|
||||
Start with the website's [worked guides](https://morluto.github.io/rea/guides/).
|
||||
Start with the website's [worked guides](https://rea.tools/guides/).
|
||||
For exact options, prerequisites and result contracts:
|
||||
|
||||
- [Installation and setup](docs/installation.md): agent registration, provider configuration, updates and uninstall.
|
||||
@@ -313,7 +315,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks,
|
||||
|
||||
## Project links
|
||||
|
||||
[Website](https://morluto.github.io/rea/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md)
|
||||
[Website](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Security](SECURITY.md)
|
||||
|
||||
## Star history
|
||||
|
||||
|
||||
+173
-241
@@ -1,355 +1,287 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **العربية**
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md)
|
||||
|
||||
# REA: اعكس هندسة أي شيء
|
||||
# REA: هندسة عكسية لأي شيء
|
||||
|
||||
### وسيط MCP واحد للهندسة العكسية عبر الملفات التنفيذية والتطبيقات وسلوك وقت التشغيل.
|
||||
### خادم MCP واحد للهندسة العكسية للملفات الثنائية والتطبيقات والسلوك أثناء التشغيل.
|
||||
|
||||
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
|
||||
**وجدت ميزة تعجبك؟ افهم طريقة عملها حتى مستوى الشيفرة الثنائية.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#منصة-أدوات-التحقيق)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
**[الموقع](https://rea.tools/) · [الأدلة](https://rea.tools/guides/) · [دراسات الحالة](https://rea.tools/showcase/)**
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
[البدء السريع](#البدء-السريع) · [كيف يعمل REA](#كيف-يعمل-rea) · [ما الذي يمكنك تحليله](#ما-الذي-يمكنك-تحليله) · [دراسات الحالة](#دراسات-الحالة) · [الأسئلة الشائعة](#الأسئلة-الشائعة) · [التوثيق](#التوثيق)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف ثنائي للشيفرة الأصلية" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="مجتمع REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>انضم إلى مجتمع الهندسة العكسية</strong>
|
||||
</a><br />
|
||||
<sub>Discord · أسئلة وأجوبة · شارك أعمالك</sub>
|
||||
<sub>Discord · أسئلة وأجوبة · مشاركة النتائج</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف تنفيذي أصلي" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
هل وجدت في تطبيق ميزة تريدها في منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
|
||||
هل وجدت في تطبيق ميزة تريد إضافتها إلى منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
|
||||
|
||||
يوفر REA أدوات لتحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. يمكنك استخدامها من وكيلك أو من الطرفية. يعمل التحليل محليًا، وتتضمن النتائج الأدلة والقيود.
|
||||
يربط REA وكيلك بأدوات لفحص الملفات الثنائية للشيفرة الأصلية، وتطبيقات JavaScript وElectron، وتجميعات .NET، والمواقع. يمكنك أيضًا استخدام الأدوات نفسها من الطرفية. يجري التحليل محليًا، وتتضمن النتائج الأدلة والقيود التي تستند إليها كل خلاصة.
|
||||
|
||||
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra، ويثبّت سير عمل REA الموجّه. ويمكنه تثبيت Hopper بعد موافقتك. ولا يحتاج التحليل الثابت لتطبيقات JavaScript إلى أي محرك.
|
||||
يسجّل الإعداد REA لدى وكيلك ويثبّت تعليمات سير العمل المطابقة. يمكن لتحليل الشيفرة الأصلية استخدام تثبيت موجود من Hopper أو Ghidra؛ ويمكن للإعداد أيضًا تثبيت Hopper اختياريًا بعد موافقتك. لا يحتاج التحليل الثابت لـ JavaScript إلى أي من المحركين.
|
||||
|
||||
## اطلب من وكيلك مباشرة
|
||||
|
||||
بعد [الإعداد](#البدء-السريع)، أعد تشغيل الوكيل ثم اطلب:
|
||||
|
||||
```text
|
||||
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
|
||||
```
|
||||
|
||||
استبدل Notes بالتطبيق الذي تريد فهمه، أو اطلب نظرة عامة أولًا.
|
||||
|
||||
## من الملف التنفيذي إلى السلوك
|
||||
|
||||
| فك الترجمة | الفهم | إعادة الإنشاء |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| استخرج الشيفرة شبه المصدرية، والتعليمات، والسلاسل النصية، والرموز | تتبع تدفق التحكم والمراجع المتبادلة والاستدعاءات والأنواع | حوّل ما تعلمه الوكيل إلى ميزة تناسب تقنياتك وواجهتك ومتطلباتك |
|
||||
|
||||
يوضح REA كيف وصل إلى نتائجه. وهو لا يدّعي استعادة الشيفرة المصدرية الأصلية أو استنساخ التطبيق تلقائيًا.
|
||||
|
||||
## لماذا REA؟
|
||||
|
||||
| | |
|
||||
| ------------------------ | ---------------------------------------------------------------------------- |
|
||||
| **مصمم للوكلاء** | اسأل عما يفعله التطبيق ودع الوكيل يفحصه بدلًا من التخمين. |
|
||||
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو الوكيل. |
|
||||
| **إعداد موجّه** | اضبط وكيلك، واربط أداة تحليل موجودة، أو ثبّت Hopper بموافقتك. |
|
||||
| **من الفهم إلى الشيفرة** | افهم الميزة ثم ابنِ نسختك الخاصة في جلسة البرمجة نفسها. |
|
||||
| **محلي حسب التصميم** | يجري التحليل على مضيفك المحلي المدعوم، ولا يرفع REA التطبيق إلى خدمة مستضافة. |
|
||||
| **يحافظ على السياق** | استقصِ عدة تطبيقات دون بدء التحليل من جديد عند كل سؤال. |
|
||||
> **[زر موقع REA](https://rea.tools/)** للاطلاع على تعليمات الإعداد والأدلة المصوّرة ودراسات الحالة الفعلية.
|
||||
|
||||
## البدء السريع
|
||||
|
||||
### تشغيل الإعداد (موصى به)
|
||||
### إعداد وكيلك
|
||||
|
||||
اربط REA بوكيلك:
|
||||
بعد تثبيت Node.js وnpm، نفّذ:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
يعرض Setup أولاً قائمة متعددة الاختيار للوكلاء الذين تريد ربطهم. تكون تسجيلات REA الموجودة محددة افتراضياً، ولا يؤدي اكتشاف عميل جديد وحده إلى تحديده؛ ويمكنك اختيار عميل غير مهيأ. راجع المسارات والتغييرات المحددة قبل الموافقة. يثبت Setup سير عمل REA للوكلاء المحددين افتراضياً. Hopper خيار منفصل يتطلب موافقة مستقلة، ويمكن أيضاً حفظ مسارات تثبيت Ghidra الموجودة.
|
||||
اختر الوكلاء، وراجع التغييرات المقترحة، ثم وافق عليها. يضيف الإعداد خادم MCP الخاص بـ REA وتعليمات سير العمل المطابقة، مع الاحتفاظ بنسخ احتياطية من الإعدادات الموجودة. أعد تشغيل وكيلك بعد ذلك.
|
||||
|
||||
يعرض التغييرات قبل تطبيقها وينشئ نسخة احتياطية من الإعدادات الموجودة. راجع [دليل التثبيت والإعداد](docs/installation.md) للمتطلبات والخيارات الإضافية.
|
||||
يدعم الإعداد Claude Code وCodex وCursor وGemini CLI وGrok Build و[وكلاء آخرين](docs/installation.md#supported-agents). راجع [التثبيت والإعداد](docs/installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا.
|
||||
|
||||
### الاستخدام مع وكيل
|
||||
### اطلب من وكيلك
|
||||
|
||||
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
|
||||
|
||||
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
|
||||
|
||||
### مهارة مساعد البرمجة (اختياري)
|
||||
|
||||
أضف المهارة إلى مساعد البرمجة للحصول على سياق أغنى:
|
||||
|
||||
```bash
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```text
|
||||
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
|
||||
```
|
||||
|
||||
توفر المهارة سير عمل التحقيق في REA. نفّذ الإعداد أعلاه لربط REA بالوكيل وضبط أدوات التحليل. يثبّت الإعداد افتراضيًا مهارة مطابقة للإصدار؛ وهذا الأمر يثبّت نسخة المستودع.
|
||||
استبدل Notes بالتطبيق المستهدف وحدّد الميزة التي تريد فهمها.
|
||||
|
||||
لشجرة JavaScript/Electron المستخرجة أو ملف ASAR، لا يلزم إعداد MCP ولا محرك أصلي:
|
||||
### استخدام الطرفية
|
||||
|
||||
افحص مجلد تطبيق JavaScript/Electron بعد فك حزمه، أو ملف ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
استبدل المسار بهدفك (مثل `"D:/apps/example"` على Windows). يدعم سير العمل الثابت هذه المسارات، بينما يتطلب التحليل الأصلي محركه الخاص.
|
||||
تتضمن النتيجة الوحدات وعمليات الاستيراد وحدود Electron والأدلة المتعلقة بها. استبدل المسار بمسار هدفك، مثل `"D:/apps/example"` على Windows.
|
||||
|
||||
### تثبيت أمر rea
|
||||
|
||||
ثبت واجهة سطر الأوامر:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
يجب تثبيت Node.js وnpm مسبقًا. عند تشغيله من طرفية، يثبت البرنامج `rea` ويبدأ الإعداد.
|
||||
|
||||
يمكنك بدلًا من ذلك التثبيت باستخدام npm ثم تشغيل الإعداد:
|
||||
لتثبيت أمر `rea` للاستخدام المعتاد:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
rea --help
|
||||
```
|
||||
|
||||
### المتطلبات
|
||||
لتحليل الشيفرة الأصلية، هيّئ موفّرًا أولًا. راجع [دليل CLI وEvidence](docs/cli.md) لأوامر تحليل الشيفرة الأصلية، واختيار الموفّر، واللقطات، والاستخدام في السكربتات.
|
||||
|
||||
- macOS 12 أو أحدث
|
||||
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت أو CachyOS
|
||||
- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm
|
||||
### تحديث REA
|
||||
|
||||
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
|
||||
يتطور REA بسرعة، وتتضمن الإصدارات الجديدة إصلاحات متكررة للأخطاء. حافظ على تحديث نسختك.
|
||||
|
||||
يدعم Ghidra نظامي Linux x64 وmacOS x64/arm64. ثبت Ghidra 12.1.x وJDK الكامل بنواة 64 بت الذي يعلنه ذلك التثبيت (`application.java.min` حتى `application.java.max`) بصورة منفصلة، ثم اضبط REA لاستخدامهما. تتطلب إصدارات 12.1 الحالية JDK 21 أو أحدث ولا تحدد حدًا أعلى. جرى التحقق من الجسر مع Ghidra 12.1.4 وJDK 21. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
|
||||
|
||||
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
|
||||
|
||||
يتضمن فرع main والحزمة npm 4.1.0 دعمًا تجريبيًا لـ Windows x64 Ghidra P0 لتطبيقات PE الأصلية x86-64 (غير المُدارة وغير DLL) على NTFS محلي، مع ضوابط Job Object وقوائم DACL الخاصة وقبول المسارات المجمّعة. تحقق من [حد الإصدار](docs/installation.md#released-package-and-main) قبل توقع ذلك من حزمة npm أقدم. راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للمتطلبات والنطاق الموثق.
|
||||
|
||||
### تشخيص المشكلات
|
||||
|
||||
يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم.
|
||||
|
||||
على Linux، يفضّل REA الملف التنفيذي `/opt/hopper/bin/Hopper`، وإذا لم يكن متاحًا يتحقق تلقائيًا من `~/.local/share/rea/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /absolute/path/to/Hopper | grep 'not found'` على مسار Hopper الفعلي للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
|
||||
|
||||
### التحديث والإزالة
|
||||
|
||||
- يحدّث `rea update` تثبيت REA الحالي.
|
||||
- يزيل `rea uninstall` تسجيلات الوكلاء وملفات سير العمل التي يديرها REA، ويترك Hopper مثبتًا.
|
||||
- يزيل `rea uninstall --purge-data` أيضًا ذاكرة REA المؤقتة وحالته. استخدمه فقط إذا أردت حذف هذه البيانات.
|
||||
|
||||
## حالة الدعم
|
||||
|
||||
تُوصف قدرات المستودع الحالية ومتطلبات الأنظمة في [دليل الدعم الإنجليزي](README.md#current-status). قد يسبق فرع main [إصدار npm](docs/installation.md#released-package-and-main).
|
||||
|
||||
- يوفر Ghidra 25 عملية للقراءة فقط على Linux x64 وmacOS x64/arm64 وحدود Windows x64 P0 التجريبية. يدعم Linux/macOS أيضًا تعليقات الدوال الذرية على مستوى الجلسة. نظام Windows P0 للقراءة فقط، ولا يوفر Ghidra أي تحكم في الواجهة.
|
||||
- يتطلب الفحص الثابت لنظام Android تجهيز JADX/Java بشكل منفصل؛ ويغطي التحقق بالموفر الحقيقي نظام Linux. راجع [تحليل Android](docs/android-analysis.md).
|
||||
- تحدد طلبات المتصفح وElectron والعمليات هدفها وإجراءاتها ودورة حياتها مباشرة، وتعتمد على وصول المضيف الفعلي دون منح أذونات منفصلة من REA. ما زالت كتابة إعدادات Setup وتثبيت Hopper تتطلب الموافقة على خطتها الدقيقة.
|
||||
- يصف `rea capabilities` عمليات جلسة الملف التنفيذي لا كل أدوات التطبيقات ووقت التشغيل. راجع قائمة أدوات MCP الكاملة والدليل ذا الصلة.
|
||||
|
||||
## استقصاء كامل بطلب واحد
|
||||
|
||||
بعد الإعداد، يمكنك أن تطلب من وكيلك:
|
||||
|
||||
> افتح التطبيق، واكتشف آلية عمل البحث من دون اتصال، واشرح تدفق التحكم، ثم ابنِ نسخة لمشروعي باستخدام TypeScript وSQLite.
|
||||
|
||||
يمكن للوكيل تحويل ذلك إلى سير عمل قابل للتحقق:
|
||||
|
||||
```text
|
||||
فتح الهدف
|
||||
→ انتظار اكتمال تحليل Hopper
|
||||
→ جرد المعماريات والرموز والسلاسل النصية
|
||||
→ تحديد نقاط الدخول والدوال المهمة
|
||||
→ تتبع الاستدعاءات والمراجع المتبادلة وتدفق التحكم
|
||||
→ فك ترجمة التنفيذ ذي الصلة
|
||||
→ تلخيص السلوك والقيود والحالات الطرفية
|
||||
→ بناء الميزة بما يلائم تقنيات المشروع ومتطلباته
|
||||
```
|
||||
|
||||
## ما الذي يمكنك فعله؟
|
||||
|
||||
### فهم تطبيق لا تملك مصدره
|
||||
|
||||
اكشف نقاط الدخول، وأسماء الفئات، والسلاسل النصية، والأطر المستخدمة، ومسارات الميزات من الملف التنفيذي وحده.
|
||||
|
||||
### بناء ميزة تعجبك بطريقتك
|
||||
|
||||
استقصِ الميزة، وافهم سلوكها، ثم ابنِ نسخة ملائمة لمنتجك وتقنياتك ومتطلباتك.
|
||||
|
||||
### توثيق بروتوكول أو تنسيق غير موثق
|
||||
|
||||
تتبع الموزعات، والثوابت، والمقارنات، والقراءة والكتابة لفهم الرسائل وتنسيقات الملفات وآلات الحالات.
|
||||
|
||||
### إعادة إنشاء سلوك متوافق
|
||||
|
||||
حوّل منطقًا مفكوك الترجمة إلى مواصفات واضحة، ثم نفذ بديلًا نظيفًا تحكمه اختبارات السلوك الملحوظ.
|
||||
|
||||
### تدقيق منطق أمني حساس
|
||||
|
||||
افحص التحقق من الإدخال، والتشفير، والصلاحيات، والتخزين، ومسارات الأخطاء من دون رفع الملف التنفيذي إلى خدمة بعيدة.
|
||||
|
||||
## منصة أدوات التحقيق
|
||||
|
||||
| عائلة الأدوات | العدد | الاستخدام |
|
||||
| ---------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| الفحص الأصلي | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والاستدعاءات والمراجع والتعليقات وقراءة البايتات ومواضع الملفات |
|
||||
| سير عمل التحقيق | 14 | نظرات عامة على التطبيقات وملفات الدوال وواجهات API الأصلية والتوزيع وفك الترجمة الدفعي وتتبع الميزات ومسارات الاستدعاء ومخططاته واكتشاف Swift وObjective-C |
|
||||
| أدوات الملفات الثنائية | 10 | بيانات Mach-O والتوقيعات وplist والمعماريات وأسماء Swift ومراقبة الاستدعاءات عبر LLDB على macOS؛ تخطيط ELF وأدلة الحماية الثابتة وسجلات الأعطال على Linux |
|
||||
| مخطط المخرجات | 7 | جرد الدلائل والحزم وملفات Interface Builder المترجمة وكتالوجات موارد Apple وتحليل تحميل Mach-O dylib والاستخراج واستدلال واجهات EVM دون اتصال |
|
||||
| PE/CLI المُدار | 7 | هوية تجميعات .NET وبياناتها الوصفية وتعليمات CIL والاعتماديات الأصلية واستيراد إعادة البناء ومقارنة الإصدارات |
|
||||
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة على Linux واستخراجها الصريح |
|
||||
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف وقوائم الأعضاء وفك ترجمة الأساليب والمراجع الثابتة |
|
||||
| مراقبة المتصفح | 12 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
|
||||
| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ |
|
||||
| سير عمل التطبيقات | 13 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
|
||||
| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة |
|
||||
|
||||
## خطة العمل
|
||||
|
||||
يشمل العمل التالي توسيع التحقق من الأهداف الأصلية، ومقارنة إصدارات JavaScript و.NET، ومراقبة وقت التشغيل والتحقق من إعادة الإنشاء. يتيح Setup بالفعل اختيار تكامل الوكيل وتثبيت Hopper. تثبيت أدوات تحليل أخرى ما زال عملًا لاحقًا. راجع [خطة التثبيت](docs/roadmap.md) و[تقييم أدوات التحليل](docs/provider-evaluation.md).
|
||||
|
||||
## استخدام REA مع وكلاء برمجة آخرين
|
||||
|
||||
يدعم Setup Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.0.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
## كيف يعمل؟
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Human[المستخدم] --> Agent[وكيل البرمجة]
|
||||
Agent --> REA[REA]
|
||||
Human -->|CLI| REA
|
||||
REA --> Hopper[عمليات تحليل Hopper]
|
||||
Hopper --> App[تطبيقك]
|
||||
REA --> Ghidra[تحليل Ghidra للقراءة فقط<br/>جرد + تحليل الدوال]
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
|
||||
|
||||
## أوامر CLI
|
||||
|
||||
سير عمل الوكيل أعلاه هو أسهل طريقة لاستخدام REA. للحصول على نظرة سريعة على تطبيق من الطرفية:
|
||||
لواجهة CLI المثبّتة عبر npm:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
|
||||
rea update
|
||||
```
|
||||
|
||||
شغّل `npx -y rea-agents@latest --help` لمعرفة خيارات فك الترجمة المباشر والخيارات الأخرى.
|
||||
لتحديث تسجيلات الوكلاء والمهارة، نفّذ أمر الإعداد الذي يعرضه التحديث.
|
||||
|
||||
يمكن لـ REA فتح مجلد `.app` على Mac مباشرة. إذا لم يعثر الوكيل على التطبيق، فأخبره بمكان تثبيته.
|
||||
إذا كنت تستخدم `npx`، فحدّث إعداد وكيلك بالأمر التالي:
|
||||
|
||||
## سلوك Hopper
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
يشغّل REA برنامج Hopper عند الحاجة. على macOS قد تظهر نافذته في المقدمة أو يحتاج أحد الحوارات إلى استجابة منك. لا يمكن ضمان بقاء النافذة في الخلفية.
|
||||
راجع تغييرات الإعداد وأعد تشغيل وكيلك. لأوامر CLI التي تنفّذها مرة واحدة، استخدم `npx rea-agents@latest` متبوعًا بالأمر المطلوب.
|
||||
|
||||
يحدد REA تنسيق الهدف ومعماريته لتجنب حوارات الاختيار المعتادة. عند إغلاق الجلسة، يوقف الجسر وينظف موارده الخاصة، لكنه لا يغلق تطبيق Hopper الذي تستخدمه.
|
||||
## كيف يعمل REA
|
||||
|
||||
## الأمان والخصوصية
|
||||
يستدعي وكيلك REA عبر MCP لفحص الهدف وتتبع الشيفرة ذات الصلة. يعيد REA النتائج مع أدلتها. يستخدمها الوكيل لطرح أسئلة متابعة، أو شرح السلوك، أو كتابة تنفيذ واختباره. تستخدم أوامر CLI سير العمل نفسه.
|
||||
|
||||
- يبقى التحليل محليًا بين REA وموفر التحليل المختار عبر مقابس محلية خاصة، ومع IDA عبر تسجيل MCP المحلي المضبوط.
|
||||
- لا يرفع REA الأهداف إلى خدمة مستضافة.
|
||||
- تعمل أدوات التحليل والأهداف المطلقة بصلاحيات المستخدم نفسه؛ وما زال التقاط واجهة النظام على macOS يعتمد على الوصول إلى تسهيلات الاستخدام وتسجيل الشاشة. ولا ينفّذ التحليل الثابت للـ JavaScript الوحدات المستخرجة.
|
||||
- تستخدم جلسات Ghidra مشروعًا مؤقتًا معزولًا ولا تفتح مشاريع Ghidra التي يملكها المستخدم أو تعدّلها.
|
||||
- راجع الملفات التنفيذية غير الموثوقة واعزلها كما تفعل مع أي مدخل أصلي قد يكون ضارًا.
|
||||
- أبلغ عن الثغرات وفق [سياسة الأمان](SECURITY.md)، وليس عبر قضية عامة.
|
||||

|
||||
|
||||
[افتح الشكل بحجمه الكامل](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## ما الذي يمكنك تحليله
|
||||
|
||||
يتطلب REA إصدار Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+، إضافة إلى npm. تعتمد الأدوات الإضافية والأنظمة المضيفة المدعومة على الهدف:
|
||||
|
||||
| الهدف | ما يعيده REA | المتطلبات والدليل |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/guides/native/) |
|
||||
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](docs/binary-diagnostics.md) |
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/guides/javascript/) |
|
||||
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/guides/browser/) |
|
||||
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](docs/web-network-captures.md) |
|
||||
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](docs/managed-code-analysis.md) |
|
||||
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS؛ [دليل Android](docs/android-analysis.md) |
|
||||
| البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](docs/firmware-analysis.md) |
|
||||
| الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](docs/javascript-artifact-reconstruction.md)، [تطبيقات Apple](docs/apple-application-analysis.md) |
|
||||
| سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](docs/process-capture.md) |
|
||||
|
||||
يقرأ الفحص الثابت لـ JavaScript و.NET الملفات المقدّمة دون تشغيل التطبيق. يشغّل التسجيل أثناء التنفيذ الهدف المحدد أو يتفاعل معه بصلاحيات مستخدمك؛ ويشرح كل دليل للتسجيل أثناء التنفيذ آثاره.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
تختلف صيغ ملفات الشيفرة الأصلية والأنظمة المضيفة المدعومة باختلاف الموفّر. راجع [إعداد Hopper وGhidra](docs/installation.md#hopper)، و[دليل IDA](docs/ida-provider.md)، و[دعم Ghidra التجريبي على Windows](docs/windows-ghidra-p0.md). يدعم Ghidra أيضًا [تحليل DOS ذي 16 بت](docs/ghidra-dos.md). لاختيار الموفّر، راجع [دليل CLI](docs/cli.md#choose-a-provider). تحقّق من [توفر الميزات في الإصدارات](docs/installation.md#released-package-and-main) للميزات المضافة بعد أحدث إصدار على npm.
|
||||
|
||||
## دراسات الحالة
|
||||
|
||||
### DX-Ball: إعادة بناء حساب توزيع الصوت بين القناتين
|
||||
|
||||
تتبّع استدعاء صوت إلى الدالة المساعدة التي تحوّل الموضع إلى توزيع بين القناتين، وافحص التعليمات، وحوّل الشيفرة شبه البرمجية غير المكتملة إلى C. تجتاز إعادة البناء 3,205 حالات اختبار على x86 الأصلي، وتعيد إنتاج جميع بايتات الدالة المترجمة البالغ عددها 63 بايتًا.
|
||||
|
||||
[اقرأ دراسة الحالة](https://rea.tools/showcase/dx-ball/) ·
|
||||
[مستودع إعادة البناء](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: تتبّع جسر الحافظة في Electron
|
||||
|
||||
اعثر على API الحافظة في عملية العرض، وتتبّعه عبر preload وIPC إلى العملية الرئيسية، وافحص صيغة الحافظة ذات التنسيق الغني.
|
||||
|
||||
[اقرأ دراسة الحالة](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: استعادة حساب حلقة المقذوفات في DOS
|
||||
|
||||
افحص تعليمات اللعبة الأصلية على PC-98 ذات 16 بت، واستعد حساب الزوايا الثابتة والموجّهة نحو الهدف، وقارن شيفرة C++ المعاد بناؤها بمخرجات المترجم المستخدم آنذاك.
|
||||
|
||||
[اقرأ دراسة الحالة](https://rea.tools/showcase/th04/) ·
|
||||
[مستودع إعادة البناء](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
إذا استخدمت REA لفحص هدف مثير للاهتمام، فنود الاطلاع عليه. شارك حالتك في [issue](https://github.com/morluto/rea/issues) أو [pull request](https://github.com/morluto/rea/pulls)، مع ذكر الهدف، وسؤالك، وكيف ساعدك REA، وما توصلت إليه.
|
||||
|
||||
## الأسئلة الشائعة
|
||||
|
||||
<details>
|
||||
<summary><strong>هل يتضمن REA محرك فك ترجمة خاصًا به؟</strong></summary>
|
||||
<summary><strong>أي وكلاء يمكنهم استخدام REA؟</strong></summary>
|
||||
|
||||
لا. يستطيع Setup تثبيت Hopper، لكنه يظل برنامجًا منفصلًا بترخيص خاص به. يوفر REA واجهة CLI وخادم MCP وسير العمل المخصص للوكلاء.
|
||||
أي وكيل يدعم خوادم MCP المحلية. يهيّئ الإعداد [الوكلاء المدعومين](docs/installation.md#supported-agents)؛ ويمكن للعملاء الآخرين استخدام [تسجيل MCP يدويًا](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>هل يجب أن يكون Hopper مفتوحًا مسبقًا؟</strong></summary>
|
||||
<summary><strong>هل أحتاج إلى Hopper أو Ghidra أو IDA؟</strong></summary>
|
||||
|
||||
لا. يستطيع REA تشغيله. لكن Hopper قد يصبح مرئيًا أو يطلب إدخالًا عند الحاجة إلى قرار تفاعلي.
|
||||
يستخدم تحليل الشيفرة الأصلية المتعمق أحدها. يعمل الفحص الثابت لـ JavaScript و.NET دون محرك لتحليل الشيفرة الأصلية. يستطيع الإعداد تثبيت Hopper بعد الموافقة؛ أما Ghidra وIDA فيستخدمان تثبيتاتك الموجودة. راجع [إعداد الموفّرين](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
|
||||
<summary><strong>هل أحتاج إلى تشغيل Hopper أولًا؟</strong></summary>
|
||||
|
||||
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت وCachyOS. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
|
||||
يشغّل REA برنامج Hopper عندما تحتاج العملية إليه. على macOS، قد تظهر عند التشغيل الأول نافذة تطلب اختيار وضع العرض التجريبي أو تفعيل الترخيص. راجع [تشغيل Hopper واستكشاف الأخطاء](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>هل يمكنني تحليل قواعد Hopper المحفوظة؟</strong></summary>
|
||||
<summary><strong>ماذا يفعل تثبيت المهارة من skills.sh؟</strong></summary>
|
||||
|
||||
نعم. يدعم وقت التشغيل الملفات التنفيذية وقواعد `.hop`.
|
||||
توفّر المهارة تعليمات الاستقصاء لوكيلك. استخدم `rea setup` لتسجيل خادم MCP الخاص بـ REA وتثبيت التعليمات المطابقة، ثم أعد تشغيل وكيلك. راجع [تثبيت المهارة وحدها](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
## التطوير
|
||||
<details>
|
||||
<summary><strong>ما الشيفرة التي يعيدها REA؟</strong></summary>
|
||||
|
||||
راجع [CONTRIBUTING.md](CONTRIBUTING.md) لإعداد بيئة التطوير وبنية المشروع والاختبارات وإرشادات الإصدار.
|
||||
يعيد تحليل الشيفرة الأصلية شيفرة شبه برمجية وتعليمات تجميع. يستعيد تحليل JavaScript/Electron الوحدات والعلاقات بينها. يستخدم وكيلك هذه النتائج لكتابة تنفيذ واختباره؛ وتقدّم [دراسات الحالة](#دراسات-الحالة) أمثلة عملية.
|
||||
|
||||
## المشروع
|
||||
</details>
|
||||
|
||||
- [حزمة npm](https://www.npmjs.com/package/rea-agents)
|
||||
- [GitHub](https://github.com/morluto/rea)
|
||||
- [القضايا والطلبات](https://github.com/morluto/rea/issues)
|
||||
- [Hopper Disassembler](https://www.hopperapp.com/)
|
||||
- [سياسة الأمان](SECURITY.md)
|
||||
<details>
|
||||
<summary><strong>هل يرفع REA تطبيقي؟</strong></summary>
|
||||
|
||||
## تاريخ النجوم
|
||||
يحلّل REA الأهداف محليًا. يتلقى وكيلك نتائج الأدوات، ولموفّر النموذج الذي يستخدمه سياسة بيانات خاصة به.
|
||||
|
||||
🎉 **وصلنا إلى 20,000 نجمة على GitHub، شكرًا لكم!**
|
||||
</details>
|
||||
|
||||
شكرًا لكل من يستخدم REA ويبلّغ عن الأخطاء ويختبر الإصدارات ويساهم في إصلاحها.
|
||||
<details>
|
||||
<summary><strong>ماذا أفعل إذا واجهت خطأ؟</strong></summary>
|
||||
|
||||
حدّث أولًا؛ فقد يكون إصدار حديث قد أصلح المشكلة بالفعل.
|
||||
|
||||
لواجهة CLI المثبّتة عبر npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
لإعداد الوكيل عبر `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
إذا كنت تستخدم وكيلًا، فأكمل [تحديث الإعداد](#تحديث-rea) وأعد تشغيله. أعد محاولة المهمة نفسها. إذا استمرت المشكلة، [افتح issue](https://github.com/morluto/rea/issues) يتضمن إصدار REA، ونوع الهدف، وخطوات إعادة الإنتاج، ومخرجات الخطأ.
|
||||
|
||||
</details>
|
||||
|
||||
## التوثيق
|
||||
|
||||
ابدأ بـ [الأدلة العملية](https://rea.tools/guides/) على الموقع. للخيارات الدقيقة والمتطلبات وعقود النتائج:
|
||||
|
||||
- [التثبيت والإعداد](docs/installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت.
|
||||
- [الجاهزية واستكشاف الأخطاء](docs/installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد.
|
||||
- [CLI وEvidence](docs/cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج.
|
||||
- [عقود MCP](docs/mcp-contracts.md) و[تعليمات الوكلاء](docs/mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
|
||||
- [كتالوج الأدوات](docs/mcp-contracts.md#generated-catalog): قائمة الأدوات والموفّرين وأوامر CLI المولّدة أثناء البناء.
|
||||
- [خطة العمل](docs/roadmap.md): الأعمال المخطط لها ومتابعة القدرات.
|
||||
|
||||
أبلغ عن الثغرات وفق [SECURITY.md](SECURITY.md).
|
||||
|
||||
## المساهمة
|
||||
|
||||
نرحّب بمساعدتك في تطوير REA! [افتح issue](https://github.com/morluto/rea/issues) للإبلاغ عن خطأ أو اقتراح ميزة، أو [أرسل pull request](https://github.com/morluto/rea/pulls) لتحسين الشيفرة أو التوثيق.
|
||||
|
||||
راجع [CONTRIBUTING.md](CONTRIBUTING.md) لإعداد بيئة التطوير والفحوص، و[دليل الاختبار](docs/testing.md) لمسارات التحقق، و[خريطة البنية](docs/architecture.mermaid) لهيكل المشروع.
|
||||
|
||||
## روابط المشروع
|
||||
|
||||
[الموقع](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [الأمان](SECURITY.md)
|
||||
|
||||
## سجل النجوم
|
||||
|
||||
🎉 **20,000 نجمة على GitHub — شكرًا لكم!**
|
||||
|
||||
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="تاريخ نجوم REA على GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="سجل نجوم REA على GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## إخلاء المسؤولية
|
||||
|
||||
يوفّر REA أدوات لأبحاث الهندسة العكسية والتحليل وإعادة البناء المشروعة. أنت مسؤول عن الحصول على أي تفويض مطلوب والالتزام بالقوانين المعمول بها. لا يؤيد المشروع الاستخدام غير القانوني أو غير المصرّح به.
|
||||
|
||||
## الترخيص
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Alles reverse-engineeren
|
||||
|
||||
### Ein MCP für das Reverse Engineering von Binärdateien, Anwendungen und Laufzeitverhalten.
|
||||
|
||||
**Eine Funktion gefällt dir? Verstehe, wie sie funktioniert – bis hinunter auf die Binärebene.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Website](https://rea.tools/) · [Anleitungen](https://rea.tools/guides/) · [Fallstudien](https://rea.tools/showcase/)**
|
||||
|
||||
[Schnellstart](#schnellstart) · [So funktioniert REA](#so-funktioniert-rea) · [Was du analysieren kannst](#was-du-analysieren-kannst) · [Fallstudien](#fallstudien) · [Häufige Fragen](#häufige-fragen) · [Dokumentation](#dokumentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA startet seine Analysebrücke in Hopper und untersucht eine native Binärdatei" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA-Community">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Der Reverse-Engineering-Community beitreten</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Fragen und Antworten · Ergebnisse teilen</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Du hast in einer App eine Funktion entdeckt, die du in dein eigenes Produkt übernehmen möchtest? Lass deinen Agenten sie mit REA untersuchen. Er kann die App ohne ihren Quellcode analysieren, die Funktionsweise erklären, Belege zeigen und eine passende Umsetzung für dein Projekt entwickeln.
|
||||
|
||||
REA verbindet deinen Agenten mit Werkzeugen zum Untersuchen nativer Binärdateien, von JavaScript- und Electron-Apps, .NET-Assemblies und Websites. Dieselben Werkzeuge kannst du auch im Terminal nutzen. Die Analyse läuft lokal; die Ergebnisse enthalten die Belege und Einschränkungen hinter jeder Schlussfolgerung.
|
||||
|
||||
Die Einrichtung registriert REA bei deinem Agenten und installiert passende Workflow-Anweisungen. Für native Analysen kannst du eine vorhandene Hopper- oder Ghidra-Installation verwenden. Mit deiner Zustimmung kann die Einrichtung auch Hopper installieren. Statische JavaScript-Analysen benötigen keines der beiden Programme.
|
||||
|
||||
> **[Besuche die REA-Website](https://rea.tools/)** für Einrichtungshinweise, bebilderte Anleitungen und echte Fallstudien.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
### Deinen Agenten einrichten
|
||||
|
||||
Wenn Node.js und npm installiert sind, führe diesen Befehl aus:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Wähle deine Agenten aus, prüfe die geplanten Änderungen und bestätige sie. Die Einrichtung fügt den MCP-Server von REA und passende Workflow-Anweisungen hinzu und sichert vorhandene Konfigurationen. Starte deinen Agenten anschließend neu.
|
||||
|
||||
Die Einrichtung unterstützt Claude Code, Codex, Cursor, Gemini CLI, Grok Build und [weitere Agenten](docs/installation.md#supported-agents). Informationen zur Provider-Konfiguration und manuellen MCP-Registrierung findest du unter [Installation und Einrichtung](docs/installation.md).
|
||||
|
||||
### Deinen Agenten fragen
|
||||
|
||||
```text
|
||||
Untersuche, wie die Suche in der Notes-App funktioniert, zeige mir die Belege und entwickle eine ähnliche Funktion für mein Projekt.
|
||||
```
|
||||
|
||||
Ersetze Notes durch deine Ziel-App und benenne die Funktion, die du verstehen möchtest.
|
||||
|
||||
### Das Terminal verwenden
|
||||
|
||||
Untersuche ein entpacktes JavaScript-/Electron-App-Verzeichnis oder eine ASAR-Datei:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Das Ergebnis enthält Module, Imports, Electron-Grenzen und die zugehörigen Belege. Ersetze den Pfad durch dein Ziel, beispielsweise `"D:/apps/example"` unter Windows.
|
||||
|
||||
Für die regelmäßige Nutzung kannst du den Befehl `rea` installieren:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Konfiguriere für native Analysen zuerst einen Provider. Native Befehle, Provider-Auswahl, Snapshots und Skriptnutzung beschreibt der [CLI- und Evidence-Leitfaden](docs/cli.md).
|
||||
|
||||
### REA aktualisieren
|
||||
|
||||
REA entwickelt sich schnell weiter; neue Versionen enthalten häufig Fehlerbehebungen. Halte deine Installation aktuell.
|
||||
|
||||
Für eine über npm installierte CLI:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Um die Agentenregistrierungen und den Skill zu aktualisieren, führe den Einrichtungsbefehl aus, den das Update ausgibt.
|
||||
|
||||
Wenn du `npx` verwendest, aktualisiere die Agenteneinrichtung mit:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Prüfe die Änderungen und starte deinen Agenten neu. Für einzelne CLI-Aufrufe verwende `npx rea-agents@latest`, gefolgt vom gewünschten Befehl.
|
||||
|
||||
## So funktioniert REA
|
||||
|
||||
Dein Agent ruft REA über MCP auf, um das Ziel zu untersuchen und relevanten Code zu verfolgen. REA liefert Ergebnisse mit ihren Belegen zurück. Der Agent nutzt sie für weitere Fragen, Erklärungen oder zum Schreiben und Testen einer Implementierung. CLI-Befehle verwenden dieselben Workflows.
|
||||
|
||||

|
||||
|
||||
[Abbildung in voller Größe öffnen](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Was du analysieren kannst
|
||||
|
||||
REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätzliche Werkzeuge und unterstützte Hostsysteme hängen vom Ziel ab:
|
||||
|
||||
| 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/guides/native/) |
|
||||
| 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](docs/binary-diagnostics.md) |
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung; [Offline-EVM-Leitfaden](docs/evm-bytecode.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools; optional GDB/pwndbg; [aufgezeichnete Abstürze](docs/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/guides/javascript/) |
|
||||
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/guides/browser/) |
|
||||
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](docs/web-network-captures.md) |
|
||||
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](docs/managed-code-analysis.md) |
|
||||
| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS; [Android-Leitfaden](docs/android-analysis.md) |
|
||||
| Firmware | Bereiche, Extraktionsergebnisse und Übergaben an die native Analyse | Binwalk / Unblob unter Linux; [Firmware-Leitfaden](docs/firmware-analysis.md) |
|
||||
| Pakete und Ressourcen | Dateiinventare, Hashwerte, plists, Aufbau von Apple-Bundles und extrahierte Ressourcen | [Artefakt- und JavaScript-Leitfaden](docs/javascript-artifact-reconstruction.md), [Apple-Anwendungen](docs/apple-application-analysis.md) |
|
||||
| Prozessverhalten | Terminalausgabe, Interaktionen, Beobachtungen zu Prozessende und Dateisystem sowie Laufvergleiche | Linux/macOS mit nativer PTY; [Prozessaufzeichnung](docs/process-capture.md) |
|
||||
|
||||
Statische JavaScript- und .NET-Untersuchungen lesen die bereitgestellten Dateien, ohne die Anwendung auszuführen. Laufzeitaufzeichnungen führen das gewählte Ziel mit deinen Benutzerrechten aus oder interagieren damit; die jeweiligen Anleitungen beschreiben die Auswirkungen.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Native Formate und unterstützte Hostsysteme unterscheiden sich je nach Provider. Siehe [Hopper- und Ghidra-Einrichtung](docs/installation.md#hopper), den [IDA-Leitfaden](docs/ida-provider.md) und die [experimentelle Ghidra-Unterstützung unter Windows](docs/windows-ghidra-p0.md). Ghidra unterstützt auch [16-Bit-DOS-Analyse](docs/ghidra-dos.md). Die Provider-Auswahl beschreibt der [CLI-Leitfaden](docs/cli.md#choose-a-provider). Prüfe die [Verfügbarkeit in Releases](docs/installation.md#released-package-and-main) für Funktionen, die seit der neuesten npm-Version hinzugekommen sind.
|
||||
|
||||
## Fallstudien
|
||||
|
||||
### DX-Ball: Eine Stereo-Panning-Berechnung rekonstruieren
|
||||
|
||||
Verfolge einen Sound-Aufruf zur Hilfsfunktion, die Positionen in Panning-Werte umrechnet, untersuche die Anweisungen und überführe unvollständigen Pseudocode in C. Die Rekonstruktion besteht 3.205 Tests mit dem ursprünglichen x86-Code und reproduziert alle 63 Bytes der kompilierten Funktion.
|
||||
|
||||
[Fallstudie lesen](https://rea.tools/showcase/dx-ball/) · [Rekonstruktionsrepository](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: Die Electron-Zwischenablagebrücke verfolgen
|
||||
|
||||
Finde die Zwischenablage-API des Renderers, verfolge sie über preload und IPC bis zum Hauptprozess und untersuche das Zwischenablageformat samt Formatierungsdaten.
|
||||
|
||||
[Fallstudie lesen](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: Eine DOS-Geschossring-Berechnung wiederherstellen
|
||||
|
||||
Untersuche die 16-Bit-Anweisungen des ursprünglichen PC-98-Spiels, rekonstruiere die festen und zielgerichteten Winkelberechnungen und vergleiche das rekonstruierte C++ mit der Ausgabe des damaligen Compilers.
|
||||
|
||||
[Fallstudie lesen](https://rea.tools/showcase/th04/) · [Rekonstruktionsrepository](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Wenn du mit REA etwas Interessantes untersucht hast, würden wir es gern sehen. Teile deine Fallstudie in einem [Issue](https://github.com/morluto/rea/issues) oder [Pull Request](https://github.com/morluto/rea/pulls) und beschreibe das Ziel, deine Frage, wie REA geholfen hat und was du herausgefunden hast.
|
||||
|
||||
## Häufige Fragen
|
||||
|
||||
<details>
|
||||
<summary><strong>Welche Agenten können REA verwenden?</strong></summary>
|
||||
|
||||
Jeder Agent, der lokale MCP-Server unterstützt. Die Einrichtung konfiguriert die [unterstützten Agenten](docs/installation.md#supported-agents); andere Clients können die [manuelle MCP-Registrierung](docs/installation.md#mcp-registry) nutzen.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Brauche ich Hopper, Ghidra oder IDA?</strong></summary>
|
||||
|
||||
Für tiefgehende native Analysen wird eines dieser Programme verwendet. Statische JavaScript- und .NET-Untersuchungen funktionieren ohne native Analyse-Engine. Die Einrichtung kann Hopper nach Zustimmung installieren; Ghidra und IDA nutzen deine vorhandenen Installationen. Siehe [Provider-Einrichtung](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Muss ich Hopper vorher starten?</strong></summary>
|
||||
|
||||
REA startet Hopper, wenn eine Operation es benötigt. Unter macOS kann beim ersten Start ein Dialog erscheinen, in dem du den Demomodus auswählst oder deine Lizenz aktivierst. Siehe [Hopper-Start und Fehlerbehebung](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Was bewirkt die Installation des Skills über skills.sh?</strong></summary>
|
||||
|
||||
Der Skill stellt Untersuchungsanweisungen für deinen Agenten bereit. Verwende `rea setup`, um den MCP-Server von REA zu registrieren und passende Anweisungen zu installieren. Starte deinen Agenten anschließend neu. Siehe [Installation nur des Skills](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Welchen Code liefert REA zurück?</strong></summary>
|
||||
|
||||
Native Analysen liefern Pseudocode und Assemblercode. JavaScript-/Electron-Analysen rekonstruieren Module und ihre Beziehungen. Dein Agent nutzt diese Ergebnisse zum Schreiben und Testen einer Implementierung; die [Fallstudien](#fallstudien) zeigen konkrete Beispiele.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Lädt REA meine App hoch?</strong></summary>
|
||||
|
||||
REA analysiert Ziele lokal. Dein Agent erhält die Werkzeugergebnisse; sein Modellanbieter hat eine eigene Datenrichtlinie.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Was soll ich tun, wenn ich einen Fehler finde?</strong></summary>
|
||||
|
||||
Aktualisiere zuerst; eine neuere Version könnte den Fehler bereits behoben haben.
|
||||
|
||||
Für eine über npm installierte CLI:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Für die Agenteneinrichtung über `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Wenn du einen Agenten verwendest, schließe die [Aktualisierung der Einrichtung](#rea-aktualisieren) ab und starte ihn neu. Wiederhole dieselbe Aufgabe. Besteht das Problem weiterhin, [öffne ein Issue](https://github.com/morluto/rea/issues) mit deiner REA-Version, dem Zieltyp, den Reproduktionsschritten und der Fehlerausgabe.
|
||||
|
||||
</details>
|
||||
|
||||
## Dokumentation
|
||||
|
||||
Beginne mit den [praxisnahen Anleitungen](https://rea.tools/guides/) auf der Website. Genaue Optionen, Voraussetzungen und Ergebniskontrakte findest du hier:
|
||||
|
||||
- [Installation und Einrichtung](docs/installation.md): Agentenregistrierung, Provider-Konfiguration, Updates und Deinstallation.
|
||||
- [Bereitschaft und Fehlerbehebung](docs/installation.md#check-readiness-for-your-task): Diagnose eines bestimmten Agenten oder einer Analyse-Engine.
|
||||
- [CLI und Evidence](docs/cli.md): Befehle, Provider-Auswahl, Snapshots, Import/Export und Exit-Status.
|
||||
- [MCP-Kontrakte](docs/mcp-contracts.md) und [Agenten-Prompts](docs/mcp-prompts.md): Werkzeugergebnisse, Sitzungen und geführte Untersuchungen.
|
||||
- [Werkzeugkatalog](docs/mcp-contracts.md#generated-catalog): Beim Build erzeugtes Inventar von Werkzeugen, Providern und CLI-Befehlen.
|
||||
- [Roadmap](docs/roadmap.md): Geplante Arbeiten und Fortschrittsübersichten zu Fähigkeiten.
|
||||
|
||||
Melde Sicherheitslücken gemäß [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Mitwirken
|
||||
|
||||
Wir freuen uns über deine Hilfe bei REA! [Öffne ein Issue](https://github.com/morluto/rea/issues), um einen Fehler zu melden oder eine Funktion vorzuschlagen, oder [sende einen Pull Request](https://github.com/morluto/rea/pulls), um Code oder Dokumentation zu verbessern.
|
||||
|
||||
Die Entwicklungsumgebung und Prüfungen beschreibt [CONTRIBUTING.md](CONTRIBUTING.md), die Verifikationspfade der [Testleitfaden](docs/testing.md) und die Projektstruktur die [Architekturübersicht](docs/architecture.mermaid).
|
||||
|
||||
## Projektlinks
|
||||
|
||||
[Website](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Sicherheit](SECURITY.md)
|
||||
|
||||
## Sternverlauf
|
||||
|
||||
🎉 **20.000 GitHub-Sterne – vielen Dank!**
|
||||
|
||||
Danke an alle, die REA nutzen, Fehler melden, Builds testen und Fehlerbehebungen beitragen.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="GitHub-Sternverlauf von REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Haftungsausschluss
|
||||
|
||||
REA stellt Werkzeuge für rechtmäßige Reverse-Engineering-Forschung, Analyse und Rekonstruktion bereit. Du bist dafür verantwortlich, erforderliche Genehmigungen einzuholen und geltendes Recht einzuhalten. Das Projekt unterstützt keine rechtswidrige oder unbefugte Nutzung.
|
||||
|
||||
## Lizenz
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Ingeniería inversa de cualquier cosa
|
||||
|
||||
### Un solo MCP para la ingeniería inversa de binarios, aplicaciones y comportamiento en ejecución.
|
||||
|
||||
**¿Ves una función que te gusta? Entiende cómo funciona, hasta el nivel binario.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Sitio web](https://rea.tools/) · [Guías](https://rea.tools/guides/) · [Casos prácticos](https://rea.tools/showcase/)**
|
||||
|
||||
[Inicio rápido](#inicio-rápido) · [Cómo funciona REA](#cómo-funciona-rea) · [Qué puedes analizar](#qué-puedes-analizar) · [Casos prácticos](#casos-prácticos) · [Preguntas frecuentes](#preguntas-frecuentes) · [Documentación](#documentación)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA inicia su puente de análisis dentro de Hopper mientras inspecciona un binario nativo" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="Comunidad REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Únete a la comunidad de ingeniería inversa</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Preguntas y respuestas · Comparte tus resultados</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
¿Has visto una función en una aplicación que quieres incorporar a tu producto? Pide a tu agente que la investigue con REA. Puede inspeccionar la aplicación sin su código fuente, explicar cómo funciona la función, mostrar las pruebas y crear una versión para tu proyecto.
|
||||
|
||||
REA conecta tu agente con herramientas para inspeccionar binarios nativos, aplicaciones JavaScript y Electron, ensamblados .NET y sitios web. También puedes usar las mismas herramientas desde la terminal. El análisis se ejecuta localmente y los resultados incluyen las pruebas y limitaciones que sustentan cada conclusión.
|
||||
|
||||
La configuración registra REA en tu agente e instala las instrucciones de trabajo correspondientes. El análisis nativo puede usar una instalación existente de Hopper o Ghidra; la configuración también puede instalar Hopper, si lo autorizas. El análisis estático de JavaScript no necesita ninguno de los dos motores.
|
||||
|
||||
> **[Visita el sitio web de REA](https://rea.tools/)** para encontrar instrucciones de configuración, guías ilustradas y casos reales.
|
||||
|
||||
## Inicio rápido
|
||||
|
||||
### Configura tu agente
|
||||
|
||||
Con Node.js y npm instalados, ejecuta:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Elige tus agentes, revisa los cambios propuestos y apruébalos. La configuración añade el servidor MCP de REA y las instrucciones de trabajo correspondientes, con copias de seguridad de la configuración existente. Reinicia tu agente al terminar.
|
||||
|
||||
La configuración admite Claude Code, Codex, Cursor, Gemini CLI, Grok Build y [otros agentes](docs/installation.md#supported-agents). Consulta [instalación y configuración](docs/installation.md) para configurar proveedores y registrar MCP manualmente.
|
||||
|
||||
### Pregunta a tu agente
|
||||
|
||||
```text
|
||||
Investiga cómo funciona la búsqueda en la aplicación Notes, muéstrame las pruebas y crea una función similar para mi proyecto.
|
||||
```
|
||||
|
||||
Sustituye Notes por la aplicación objetivo y especifica la función que quieres entender.
|
||||
|
||||
### Usa la terminal
|
||||
|
||||
Inspecciona un directorio extraído de una aplicación JavaScript/Electron o un archivo ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
El resultado incluye módulos, importaciones, límites de Electron y sus pruebas. Sustituye la ruta por la de tu objetivo, por ejemplo `"D:/apps/example"` en Windows.
|
||||
|
||||
Para instalar el comando `rea` y usarlo habitualmente:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Para el análisis nativo, configura primero un proveedor. Consulta la [guía de CLI y Evidence](docs/cli.md) para los comandos nativos, la selección de proveedores, las instantáneas y el uso en scripts.
|
||||
|
||||
### Actualiza REA
|
||||
|
||||
REA cambia rápidamente y las nuevas versiones incluyen correcciones frecuentes. Mantén tu instalación actualizada.
|
||||
|
||||
Para una CLI instalada mediante npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Para actualizar los registros de tus agentes y la skill, ejecuta el comando de configuración que muestra la actualización.
|
||||
|
||||
Si usas `npx`, actualiza la configuración de tu agente con:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Revisa los cambios de configuración y reinicia tu agente. Para ejecutar comandos de CLI puntuales, usa `npx rea-agents@latest` seguido del comando.
|
||||
|
||||
## Cómo funciona REA
|
||||
|
||||
Tu agente llama a REA mediante MCP para inspeccionar el objetivo y seguir el código relevante. REA devuelve los hallazgos junto con sus pruebas. El agente los utiliza para hacer preguntas de seguimiento, explicar el comportamiento o escribir y probar una implementación. Los comandos de CLI usan los mismos flujos de trabajo.
|
||||
|
||||

|
||||
|
||||
[Abre la figura a tamaño completo](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Qué puedes analizar
|
||||
|
||||
REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las herramientas adicionales y los sistemas anfitriones compatibles dependen del objetivo:
|
||||
|
||||
| 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/guides/native/) |
|
||||
| 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](docs/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; [guía de EVM sin ejecutar](docs/evm-bytecode.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; GDB/pwndbg opcionales; [fallos registrados](docs/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/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/guides/browser/) |
|
||||
| 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](docs/web-network-captures.md) |
|
||||
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](docs/managed-code-analysis.md) |
|
||||
| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS; [guía de Android](docs/android-analysis.md) |
|
||||
| Firmware | Regiones, resultados de extracción y derivaciones al análisis nativo | Binwalk / Unblob en Linux; [guía de firmware](docs/firmware-analysis.md) |
|
||||
| Paquetes y recursos | Inventarios de archivos, resúmenes criptográficos, plists, estructura de paquetes de Apple y recursos extraídos | [Guía de artefactos y JavaScript](docs/javascript-artifact-reconstruction.md), [aplicaciones de Apple](docs/apple-application-analysis.md) |
|
||||
| Comportamiento de procesos | Salida de la terminal, interacciones, observaciones de salida y del sistema de archivos, y comparaciones entre ejecuciones | Linux/macOS con una PTY nativa; [captura de procesos](docs/process-capture.md) |
|
||||
|
||||
La inspección estática de JavaScript y .NET lee los archivos suministrados sin ejecutar la aplicación. La captura en ejecución inicia el objetivo seleccionado o interactúa con él con los permisos de tu usuario; cada guía describe sus efectos.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Los formatos nativos y los sistemas anfitriones compatibles varían según el proveedor. Consulta [configuración de Hopper y Ghidra](docs/installation.md#hopper), la [guía de IDA](docs/ida-provider.md) y la [compatibilidad experimental de Ghidra en Windows](docs/windows-ghidra-p0.md). Ghidra también admite [análisis de DOS de 16 bits](docs/ghidra-dos.md). Para elegir un proveedor, consulta la [guía de CLI](docs/cli.md#choose-a-provider). Comprueba la [disponibilidad por versión](docs/installation.md#released-package-and-main) de las funciones añadidas después de la última versión publicada en npm.
|
||||
|
||||
## Casos prácticos
|
||||
|
||||
### DX-Ball: reconstruir un cálculo de panoramización de sonido
|
||||
|
||||
Sigue una llamada de sonido hasta la función auxiliar que convierte la posición en panoramización, inspecciona las instrucciones y transforma el pseudocódigo incompleto en C. La reconstrucción supera 3.205 casos con el x86 original y reproduce los 63 bytes de la función compilada.
|
||||
|
||||
[Lee el caso práctico](https://rea.tools/showcase/dx-ball/) · [Repositorio de reconstrucción](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: seguir el puente del portapapeles de Electron
|
||||
|
||||
Encuentra la API del portapapeles del proceso de renderizado, síguela a través de preload e IPC hasta el proceso principal e inspecciona el formato enriquecido del portapapeles.
|
||||
|
||||
[Lee el caso práctico](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: recuperar el cálculo de un anillo de proyectiles en DOS
|
||||
|
||||
Inspecciona las instrucciones de 16 bits del juego original para PC-98, recupera los cálculos de ángulos fijos y dirigidos, y compara el C++ reconstruido con la salida del compilador de la época.
|
||||
|
||||
[Lee el caso práctico](https://rea.tools/showcase/th04/) · [Repositorio de reconstrucción](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Si has usado REA para investigar algo interesante, nos gustaría verlo. Comparte tu caso en una [issue](https://github.com/morluto/rea/issues) o una [pull request](https://github.com/morluto/rea/pulls), incluyendo el objetivo, tu pregunta, cómo te ayudó REA y qué encontraste.
|
||||
|
||||
## Preguntas frecuentes
|
||||
|
||||
<details>
|
||||
<summary><strong>¿Qué agentes pueden usar REA?</strong></summary>
|
||||
|
||||
Cualquier agente que admita servidores MCP locales. La configuración prepara los [agentes compatibles](docs/installation.md#supported-agents); otros clientes pueden usar el [registro manual de MCP](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>¿Necesito Hopper, Ghidra o IDA?</strong></summary>
|
||||
|
||||
El análisis nativo en profundidad utiliza uno de ellos. La inspección estática de JavaScript y .NET funciona sin un motor de análisis nativo. La configuración puede instalar Hopper después de tu aprobación; Ghidra e IDA usan las instalaciones que ya tienes. Consulta [configuración de proveedores](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>¿Tengo que iniciar Hopper primero?</strong></summary>
|
||||
|
||||
REA inicia Hopper cuando una operación lo necesita. En macOS, puede aparecer un diálogo al iniciarlo por primera vez para elegir el modo de demostración o activar tu licencia. Consulta [inicio de Hopper y resolución de problemas](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>¿Qué hace la instalación de la skill desde skills.sh?</strong></summary>
|
||||
|
||||
La skill proporciona instrucciones de investigación a tu agente. Usa `rea setup` para registrar el servidor MCP de REA e instalar las instrucciones correspondientes; después reinicia tu agente. Consulta [instalación solo de la skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>¿Qué código devuelve REA?</strong></summary>
|
||||
|
||||
El análisis nativo devuelve pseudocódigo y ensamblador. El análisis de JavaScript/Electron recupera módulos y sus relaciones. Tu agente utiliza estos hallazgos para escribir y probar una implementación; los [casos prácticos](#casos-prácticos) ofrecen ejemplos completos.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>¿REA sube mi aplicación?</strong></summary>
|
||||
|
||||
REA analiza los objetivos localmente. Tu agente recibe los resultados de las herramientas, y su proveedor de modelos tiene su propia política de datos.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>¿Qué debo hacer si encuentro un error?</strong></summary>
|
||||
|
||||
Actualiza primero; una versión reciente puede haber solucionado el problema.
|
||||
|
||||
Para una CLI instalada mediante npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Para configurar el agente mediante `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Si usas un agente, completa la [actualización de la configuración](#actualiza-rea) y reinícialo. Repite la misma tarea. Si el problema continúa, [abre una issue](https://github.com/morluto/rea/issues) con tu versión de REA, el tipo de objetivo, los pasos para reproducirlo y la salida del error.
|
||||
|
||||
</details>
|
||||
|
||||
## Documentación
|
||||
|
||||
Empieza con las [guías prácticas](https://rea.tools/guides/) del sitio web. Para opciones exactas, requisitos y contratos de resultados:
|
||||
|
||||
- [Instalación y configuración](docs/installation.md): registro de agentes, configuración de proveedores, actualizaciones y desinstalación.
|
||||
- [Preparación y resolución de problemas](docs/installation.md#check-readiness-for-your-task): diagnóstico de un agente o motor de análisis concreto.
|
||||
- [CLI y Evidence](docs/cli.md): comandos, selección de proveedores, instantáneas, importación/exportación y estados de salida.
|
||||
- [Contratos de MCP](docs/mcp-contracts.md) y [prompts para agentes](docs/mcp-prompts.md): resultados de herramientas, sesiones e investigaciones guiadas.
|
||||
- [Catálogo de herramientas](docs/mcp-contracts.md#generated-catalog): inventario de herramientas, proveedores y comandos de CLI generado durante la compilación.
|
||||
- [Hoja de ruta](docs/roadmap.md): trabajo previsto y seguimiento de capacidades.
|
||||
|
||||
Informa de vulnerabilidades siguiendo [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Contribuir
|
||||
|
||||
¡Nos encantaría que ayudaras con REA! [Abre una issue](https://github.com/morluto/rea/issues) para comunicar un error o proponer una función, o [envía una pull request](https://github.com/morluto/rea/pulls) para mejorar el código o la documentación.
|
||||
|
||||
Consulta [CONTRIBUTING.md](CONTRIBUTING.md) para preparar el entorno de desarrollo y las comprobaciones, la [guía de pruebas](docs/testing.md) para los procesos de verificación y el [mapa de arquitectura](docs/architecture.mermaid) para la estructura del proyecto.
|
||||
|
||||
## Enlaces del proyecto
|
||||
|
||||
[Sitio web](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Seguridad](SECURITY.md)
|
||||
|
||||
## Historial de estrellas
|
||||
|
||||
🎉 **20.000 estrellas en GitHub: ¡gracias!**
|
||||
|
||||
Gracias a quienes usan REA, informan de errores, prueban compilaciones y contribuyen con correcciones.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Historial de estrellas de REA en GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Aviso legal
|
||||
|
||||
REA proporciona herramientas para la investigación, el análisis y la reconstrucción mediante ingeniería inversa conforme a la ley. Eres responsable de obtener las autorizaciones necesarias y cumplir las leyes aplicables. El proyecto no respalda usos ilegales ni no autorizados.
|
||||
|
||||
## Licencia
|
||||
|
||||
[MIT](LICENSE)
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
<div align="center" dir="rtl">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی**
|
||||
|
||||
# REA: مهندسی معکوس هر چیزی
|
||||
|
||||
### یک MCP برای مهندسی معکوس فایلهای باینری، برنامهها و رفتار زمان اجرا.
|
||||
|
||||
**قابلیتی دیدهاید که دوستش دارید؟ سازوکارش را تا سطح باینری درک کنید.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
**[وبسایت](https://rea.tools/) · [راهنماها](https://rea.tools/guides/) · [نمونههای عملی](https://rea.tools/showcase/)**
|
||||
|
||||
[شروع سریع](#شروع-سریع) · [REA چگونه کار میکند؟](#rea-چگونه-کار-میکند) · [چه چیزهایی را میتوان تحلیل کرد؟](#چه-چیزهایی-را-میتوان-تحلیل-کرد) · [نمونههای عملی](#نمونههای-عملی) · [پرسشهای متداول](#پرسشهای-متداول) · [مستندات](#مستندات)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="اجرای پل تحلیل REA در Hopper هنگام بررسی یک فایل باینری بومی" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="انجمن REA"><tr><td align="center" width="360"><a href="https://discord.gg/GkcryMnJDM"><img src="docs/assets/discord.svg" height="42" alt="Discord" /><br /><strong>به انجمن مهندسی معکوس بپیوندید</strong></a><br /><sub>دیسکورد · پرسش و پاسخ · نمایش دستاوردها</sub></td></tr></table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
<div dir="rtl">
|
||||
|
||||
قابلیتی در یک برنامه دیدهاید که میخواهید در محصول خودتان داشته باشید؟ از ایجنت خود بخواهید با REA آن را بررسی کند. ایجنت میتواند بدون دسترسی به کد منبع برنامه، نحوهٔ کار قابلیت را بررسی و توضیح دهد، شواهد را نشان دهد و نسخهای مشابه برای پروژهٔ شما بسازد.
|
||||
|
||||
REA ایجنت شما را به ابزارهایی برای بررسی باینریهای بومی، برنامههای JavaScript و Electron، اسمبلیهای .NET و وبسایتها متصل میکند. همین ابزارها از ترمینال نیز قابل استفادهاند. تحلیل بهصورت محلی انجام میشود و نتایج شامل شواهد و محدودیتهای پشت هر نتیجهگیری هستند.
|
||||
|
||||
فرایند راهاندازی، REA را در ایجنت شما ثبت میکند و دستورالعملهای گردشکار متناظر را نصب میکند. تحلیل بومی میتواند از نصب موجود Hopper یا Ghidra استفاده کند؛ همچنین راهاندازی میتواند با تأیید شما Hopper را بهصورت اختیاری نصب کند. تحلیل ایستای JavaScript به هیچیک از این موتورهای تحلیل نیاز ندارد.
|
||||
|
||||
> برای دستورالعملهای راهاندازی، راهنماهای تصویری و مطالعات موردی واقعی، **[به وبسایت REA مراجعه کنید](https://rea.tools/)**.
|
||||
|
||||
## شروع سریع
|
||||
|
||||
### ایجنت خود را راهاندازی کنید
|
||||
|
||||
پس از نصب Node.js و npm، دستور زیر را اجرا کنید:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
ایجنتهای موردنظر را انتخاب کنید، تغییرات پیشنهادی را بررسی و تأیید کنید. فرایند راهاندازی، سرور MCP مربوط به REA و دستورالعملهای گردشکار متناظر را اضافه میکند و از پیکربندی موجود نسخهٔ پشتیبان میگیرد. پس از آن ایجنت خود را مجدداً راهاندازی کنید.
|
||||
|
||||
راهاندازی از Claude Code، Codex، Cursor، Gemini CLI، Grok Build و [ایجنتهای دیگر](docs/installation.md#supported-agents) پشتیبانی میکند. برای پیکربندی ارائهدهنده و ثبت دستی MCP، [نصب و راهاندازی](docs/installation.md) را ببینید.
|
||||
|
||||
### از ایجنت خود بپرسید
|
||||
|
||||
```text
|
||||
بررسی کن جستوجو در برنامهٔ Notes چگونه کار میکند، شواهدش را نشان بده و قابلیتی مشابه برای پروژهٔ من بساز.
|
||||
```
|
||||
|
||||
بهجای Notes، نام برنامهٔ هدف و قابلیتی را قرار دهید که میخواهید درک کنید.
|
||||
|
||||
### استفاده از ترمینال
|
||||
|
||||
برای بررسی پوشهٔ استخراجشدهٔ یک برنامهٔ JavaScript/Electron یا فایل ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
نتیجه شامل ماژولها، importها، مرزهای Electron و شواهد مربوط به آنهاست. مسیر را با مسیر برنامهٔ هدف جایگزین کنید؛ مثلاً در ویندوز `"D:/apps/example"`.
|
||||
|
||||
برای نصب دستور `rea` جهت استفادهٔ مداوم:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
برای تحلیل بومی ابتدا یک ارائهدهنده پیکربندی کنید. برای دستورات بومی، انتخاب ارائهدهنده، snapshotها و اسکریپتنویسی، [راهنمای CLI و شواهد](docs/cli.md) را ببینید.
|
||||
|
||||
### بهروزرسانی REA
|
||||
|
||||
REA بهسرعت تغییر میکند و نسخههای جدید معمولاً شامل رفع اشکالهای متعددی هستند. نصب خود را بهروز نگه دارید.
|
||||
|
||||
برای CLI نصبشده از طریق npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
برای بهروزرسانی ثبت ایجنتها و skill، دستور setup نمایشدادهشده توسط بهروزرسانی را اجرا کنید.
|
||||
|
||||
اگر از `npx` استفاده میکنید، راهاندازی ایجنت را با دستور زیر بهروز کنید:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
تغییرات راهاندازی را بررسی کنید و ایجنت را دوباره اجرا کنید. برای اجرای یکبارهٔ دستورات CLI، از `npx rea-agents@latest` و سپس دستور موردنظر استفاده کنید.
|
||||
|
||||
## REA چگونه کار میکند؟
|
||||
|
||||
ایجنت شما از طریق MCP، REA را فراخوانی میکند تا هدف را بررسی و کد مرتبط را ردیابی کند. REA یافتهها را همراه با شواهد برمیگرداند. ایجنت از این نتایج برای طرح پرسشهای تکمیلی، توضیح رفتار یا نوشتن و آزمایش یک پیادهسازی استفاده میکند. دستورات CLI نیز از همین گردشکارها بهره میبرند.
|
||||
|
||||

|
||||
|
||||
[نمایش تصویر در اندازهٔ کامل](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## چه چیزهایی را میتوان تحلیل کرد؟
|
||||
|
||||
REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه npm نیاز دارد. ابزارهای اضافی و پشتیبانی سیستم میزبان به نوع هدف بستگی دارند:
|
||||
|
||||
| هدف | خروجی REA | پیشنیازها و راهنما |
|
||||
| --------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/guides/native/) |
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](docs/binary-diagnostics.md) |
|
||||
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی؛ [راهنمای EVM آفلاین](docs/evm-bytecode.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/guides/javascript/) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/guides/browser/) |
|
||||
| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](docs/web-network-captures.md) |
|
||||
| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](docs/managed-code-analysis.md) |
|
||||
| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS؛ [راهنمای اندروید](docs/android-analysis.md) |
|
||||
| Firmware | ناحیهها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](docs/firmware-analysis.md) |
|
||||
| بستهها و منابع | فهرست فایلها، digestها، فایلهای plist، ساختار bundle اپل و منابع استخراجشده | [راهنمای آرتیفکت و JavaScript](docs/javascript-artifact-reconstruction.md)، [برنامههای اپل](docs/apple-application-analysis.md) |
|
||||
| رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](docs/process-capture.md) |
|
||||
|
||||
بررسی ایستای JavaScript و .NET فایلهای ارائهشده را بدون اجرای برنامه میخواند. ضبط زمان اجرا، هدف انتخابشده را با مجوزهای کاربری شما اجرا میکند یا با آن تعامل دارد؛ آثار این عملیات در راهنمای مربوط به هر نوع ضبط توضیح داده شدهاند.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
فرمتهای بومی و پشتیبانی سیستم میزبان بر اساس ارائهدهنده متفاوتاند. [راهاندازی Hopper و Ghidra](docs/installation.md#hopper)، [راهنمای IDA](docs/ida-provider.md) و [پشتیبانی آزمایشی Ghidra در ویندوز](docs/windows-ghidra-p0.md) را ببینید. Ghidra همچنین از [تحلیل DOS شانزدهبیتی](docs/ghidra-dos.md) پشتیبانی میکند. برای انتخاب ارائهدهنده، به [راهنمای CLI](docs/cli.md#choose-a-provider) مراجعه کنید. برای قابلیتهایی که پس از آخرین انتشار npm اضافه شدهاند، [وضعیت انتشار](docs/installation.md#released-package-and-main) را بررسی کنید.
|
||||
|
||||
## نمونههای عملی
|
||||
|
||||
### DX-Ball: بازسازی محاسبهٔ موقعیت استریوی صدا
|
||||
|
||||
یک فراخوانی صوتی را تا تابع کمکی تبدیل موقعیت به pan دنبال کنید، دستورالعملها را بررسی کنید و شبهکد ناقص را به C تبدیل کنید. بازسازی حاصل، هر ۳٬۲۰۵ مورد آزمایش x86 اصلی را با موفقیت پشت سر میگذارد و هر ۶۳ بایت تابع کامپایلشده را دقیقاً بازتولید میکند.
|
||||
|
||||
[مطالعهٔ موردی](https://rea.tools/showcase/dx-ball/) · [مخزن بازسازی](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: ردیابی پل کلیپبورد Electron
|
||||
|
||||
API کلیپبورد در renderer را پیدا کنید، مسیر آن را از preload و IPC تا فرایند اصلی دنبال کنید و قالب غنی کلیپبورد را بررسی کنید.
|
||||
|
||||
[مطالعهٔ موردی](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: بازیابی محاسبهٔ حلقهٔ گلوله در DOS
|
||||
|
||||
دستورالعملهای ۱۶بیتی بازی اصلی PC-98 را بررسی کنید، محاسبات زاویهٔ ثابت و هدفگیریشده را بازیابی کنید و کد C++ بازسازیشده را با خروجی کامپایلر تاریخی مقایسه کنید.
|
||||
|
||||
[مطالعهٔ موردی](https://rea.tools/showcase/th04/) · [مخزن بازسازی](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
اگر از REA برای بررسی موضوع جالبی استفاده کردهاید، خوشحال میشویم آن را به اشتراک بگذارید. یک [issue](https://github.com/morluto/rea/issues) یا [pull request](https://github.com/morluto/rea/pulls) ثبت کنید و هدف، پرسش خود، نقش REA و یافتههایتان را توضیح دهید.
|
||||
|
||||
## پرسشهای متداول
|
||||
|
||||
<details><summary><strong>کدام ایجنتها میتوانند از REA استفاده کنند؟</strong></summary>
|
||||
|
||||
هر ایجنتی که از سرورهای MCP محلی پشتیبانی کند. فرایند setup، [ایجنتهای پشتیبانیشده](docs/installation.md#supported-agents) را پیکربندی میکند؛ سایر کلاینتها میتوانند از [ثبت دستی MCP](docs/installation.md#mcp-registry) استفاده کنند.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>آیا به Hopper، Ghidra یا IDA نیاز دارم؟</strong></summary>
|
||||
|
||||
تحلیل عمیق بومی به یکی از آنها نیاز دارد. بررسی ایستای JavaScript و .NET بدون موتور تحلیل بومی کار میکند. setup میتواند با تأیید شما Hopper را نصب کند؛ Ghidra و IDA از نصبهای موجود شما استفاده میکنند. [راهاندازی ارائهدهنده](docs/installation.md#hopper) را ببینید.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>آیا باید ابتدا Hopper را اجرا کنم؟</strong></summary>
|
||||
|
||||
REA زمانی که عملیاتی به Hopper نیاز داشته باشد آن را اجرا میکند. در macOS، ممکن است هنگام نخستین اجرا پنجرهای برای انتخاب حالت آزمایشی یا فعالسازی مجوز نمایش داده شود. [راهاندازی Hopper و رفع اشکال](docs/installation.md#launcher-paths-and-troubleshooting) را ببینید.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>نصب skill از skills.sh چه کاری انجام میدهد؟</strong></summary>
|
||||
|
||||
این skill دستورالعملهای بررسی را برای ایجنت فراهم میکند. برای ثبت سرور MCP مربوط به REA و نصب دستورالعملهای متناظر، `rea setup` را اجرا کنید و سپس ایجنت را مجدداً راهاندازی کنید. [نصب مستقل skill](docs/installation.md#skill-only-installation) را ببینید.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA چه نوع کدی برمیگرداند؟</strong></summary>
|
||||
|
||||
تحلیل بومی، شبهکد و اسمبلی برمیگرداند. تحلیل JavaScript/Electron ماژولها و روابط میان آنها را بازیابی میکند. ایجنت شما از این یافتهها برای نوشتن و آزمایش یک پیادهسازی استفاده میکند؛ [نمونههای عملی](#نمونههای-عملی) مثالهای گامبهگام ارائه میدهند.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>آیا REA برنامهٔ من را آپلود میکند؟</strong></summary>
|
||||
|
||||
REA اهداف را بهصورت محلی تحلیل میکند. ایجنت شما نتایج ابزارها را دریافت میکند و ارائهدهندهٔ مدل آن سیاست مستقل خود را دربارهٔ دادهها دارد.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>اگر با خطا مواجه شدم چه کنم؟</strong></summary>
|
||||
|
||||
ابتدا بهروزرسانی کنید؛ ممکن است مشکل در نسخهای جدید رفع شده باشد.
|
||||
|
||||
برای CLI نصبشده با npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
برای راهاندازی ایجنت با `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
اگر از ایجنت استفاده میکنید، [بهروزرسانی راهاندازی](#بهروزرسانی-rea) را تکمیل و ایجنت را مجدداً اجرا کنید. همان کار را دوباره امتحان کنید. اگر مشکل ادامه داشت، یک [issue ثبت کنید](https://github.com/morluto/rea/issues) و نسخهٔ REA، نوع هدف، مراحل بازتولید و خروجی خطا را ذکر کنید.
|
||||
|
||||
</details>
|
||||
|
||||
## مستندات
|
||||
|
||||
از [راهنماهای عملی وبسایت](https://rea.tools/guides/) شروع کنید. برای گزینههای دقیق، پیشنیازها و قراردادهای خروجی:
|
||||
|
||||
- [نصب و راهاندازی](docs/installation.md): ثبت ایجنت، پیکربندی ارائهدهنده، بهروزرسانی و حذف نصب.
|
||||
- [آمادگی و رفع اشکال](docs/installation.md#check-readiness-for-your-task): عیبیابی یک ایجنت یا موتور تحلیل.
|
||||
- [CLI و شواهد](docs/cli.md): دستورات، انتخاب ارائهدهنده، snapshotها، ورود/خروج داده و وضعیتهای خروج.
|
||||
- [قراردادهای MCP](docs/mcp-contracts.md) و [پرامپتهای ایجنت](docs/mcp-prompts.md): نتایج ابزارها، نشستها و بررسیهای هدایتشده.
|
||||
- [فهرست ابزارها](docs/mcp-contracts.md#generated-catalog): فهرست تولیدشده هنگام build از ابزارها، ارائهدهندگان و دستورات CLI.
|
||||
- [نقشهٔ راه](docs/roadmap.md): برنامههای آینده و پیگیری قابلیتها.
|
||||
|
||||
آسیبپذیریها را از طریق [SECURITY.md](SECURITY.md) گزارش کنید.
|
||||
|
||||
## مشارکت
|
||||
|
||||
از کمک شما به REA استقبال میکنیم! برای گزارش خطا یا پیشنهاد قابلیت، یک [issue باز کنید](https://github.com/morluto/rea/issues) یا برای بهبود کد و مستندات [pull request بفرستید](https://github.com/morluto/rea/pulls).
|
||||
|
||||
برای راهاندازی محیط توسعه و بررسیها، [CONTRIBUTING.md](CONTRIBUTING.md)، برای مراحل اعتبارسنجی [آزمایشها](docs/testing.md) و برای ساختار پروژه [نقشهٔ معماری](docs/architecture.mermaid) را ببینید.
|
||||
|
||||
## لینکهای پروژه
|
||||
|
||||
[وبسایت](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [امنیت](SECURITY.md)
|
||||
|
||||
## تاریخچهٔ ستارهها
|
||||
|
||||
🎉 **۲۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
|
||||
|
||||
از همهٔ کسانی که از REA استفاده میکنند، خطاها را گزارش میدهند، buildها را آزمایش میکنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date"><picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" /><source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" /><img alt="تاریخچهٔ ستارههای GitHub پروژهٔ REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" /></picture></a>
|
||||
|
||||
## سلب مسئولیت
|
||||
|
||||
REA ابزارهایی برای پژوهش، تحلیل و بازسازی قانونی در حوزهٔ مهندسی معکوس فراهم میکند. مسئولیت دریافت مجوزهای لازم و رعایت قوانین مربوط بر عهدهٔ شماست. این پروژه استفادهٔ غیرقانونی یا بدون مجوز را تأیید نمیکند.
|
||||
|
||||
## مجوز
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
</div>
|
||||
+176
-274
@@ -1,27 +1,36 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · **日本語** · [한국어](README_ko.md) · [العربية](README_ar.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA:あらゆるものをリバースエンジニアリング
|
||||
|
||||
### バイナリ、アプリ、実行時の動作をひとつの MCP でリバースエンジニアリング。
|
||||
### バイナリ、アプリケーション、実行時の動作を、ひとつの MCP でリバースエンジニアリング。
|
||||
|
||||
**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#調査ツールカタログ)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
**[ウェブサイト](https://rea.tools/) · [ガイド](https://rea.tools/guides/) · [事例](https://rea.tools/showcase/)**
|
||||
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
[クイックスタート](#クイックスタート) · [REA の仕組み](#rea-の仕組み) · [分析できる対象](#分析できる対象) · [事例](#事例) · [よくある質問](#よくある質問) · [ドキュメント](#ドキュメント)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA が Hopper 内で分析ブリッジを起動し、ネイティブバイナリを調べる様子" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA コミュニティ">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
@@ -35,342 +44,231 @@
|
||||
|
||||
<br />
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper 内で解析ブリッジを起動し、ネイティブバイナリを調べる REA" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
自分のプロダクトにも欲しい機能を他のアプリで見つけたら、REA を使ってエージェントに調査を依頼できます。エージェントはソースコードがなくてもアプリを調べ、機能の仕組みを説明して根拠を示し、あなたのプロジェクト向けの実装まで進められます。
|
||||
アプリに、自分の製品にも取り入れたい機能を見つけましたか?エージェントに REA で調査を依頼してください。ソースコードがなくてもアプリを調べ、機能の仕組みを説明し、根拠を示したうえで、あなたのプロジェクト向けに実装できます。
|
||||
|
||||
REA は、ネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトを調べるためのツールをエージェントに提供します。同じツールはターミナルからも使えます。解析はローカルで実行され、結果には各結論の根拠と制限が含まれます。
|
||||
REA はエージェントを分析ツールにつなぎ、ネイティブバイナリ、JavaScript や Electron のアプリ、.NET アセンブリ、ウェブサイトを調べられるようにします。同じツールをターミナルからも使えます。分析はローカルで実行され、結果には各結論を裏付ける根拠と制限事項が含まれます。
|
||||
|
||||
セットアップは REA をエージェントに登録し、対応するワークフロー手順もインストールします。ネイティブ解析には既存の Hopper または Ghidra を使えます。承認すれば、セットアップで Hopper をインストールすることもできます。静的な JavaScript 解析にはどちらのエンジンも不要です。
|
||||
セットアップはエージェントに REA を登録し、対応するワークフローの手順をインストールします。ネイティブ分析には既存の Hopper または Ghidra を使えます。承認すれば、セットアップで Hopper をインストールすることもできます。静的な JavaScript 分析には、どちらのエンジンも必要ありません。
|
||||
|
||||
## エージェントに頼むだけ
|
||||
|
||||
[セットアップ](#クイックスタート)後にエージェントを再起動し、次のように依頼します:
|
||||
|
||||
```text
|
||||
メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。
|
||||
```
|
||||
|
||||
「メモアプリ」を調べたいアプリに置き換えてください。まずは概要の調査を依頼することもできます。
|
||||
|
||||
## バイナリから動作へ
|
||||
|
||||
| 逆コンパイル | 理解 | 再現 |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| ネイティブアプリや実行ファイルから、プロシージャ、疑似コード、アセンブリ、文字列、シンボル、セグメント、メタデータを復元します。 | 呼び出し元、呼び出し先、クロスリファレンス、コールグラフをたどり、機能やアルゴリズムの実際の動作を説明できる状態にします。 | エージェントが得た知見を、あなたの技術スタック、画面、要件に合うプロダクト機能へ変えます。 |
|
||||
|
||||
REA は結論に至った根拠を示します。元のソースコードを復元できる、あるいはアプリ全体を自動的に複製できるとは主張しません。
|
||||
|
||||
## REA を選ぶ理由
|
||||
|
||||
| | |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| **エージェント向け** | アプリの動作を質問すると、エージェントが推測ではなく実際に調べて答えます。 |
|
||||
| **CLI と MCP** | 同じリバースエンジニアリング機能を、ターミナルからもエージェントからも使えます。 |
|
||||
| **ガイド付きセットアップ** | エージェントの設定、既存の解析ツールへの接続、承認したうえでの Hopper のインストールに対応します。 |
|
||||
| **洞察からコードへ** | 機能を理解したら、同じコーディングセッションで自分のプロダクト向けに実装できます。 |
|
||||
| **ローカルで解析** | 解析は対応するローカルホストで実行されます。REA がアプリをホスト型の解析サービスへ送ることはありません。 |
|
||||
| **コンテキストを維持** | 質問のたびに解析をやり直すことなく、複数のアプリを続けて調査できます。 |
|
||||
> **[REA のウェブサイト](https://rea.tools/)**で、セットアップ手順、図解ガイド、実際の事例を確認できます。
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### セットアップを実行(推奨)
|
||||
### エージェントを設定する
|
||||
|
||||
REA をエージェントで使えるように設定します:
|
||||
Node.js と npm がインストールされている環境で実行します。
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
セットアップでは、まず REA を使うエージェントを選び(複数選択可)、具体的なパスと変更内容を確認してから承認します。既存の REA 登録は最初から選択されています。新しく検出されたエージェントは選択肢に表示されますが、検出されただけでは選択されません。選択したエージェントには、MCP 接続と REA のガイド付きワークフローが追加されます。Hopper のインストールは別の任意項目で、個別に承認が必要です。既存の Ghidra のインストール先を登録することもできます。
|
||||
使うエージェントを選び、予定されている変更を確認して承認してください。セットアップは REA の MCP サーバーと対応するワークフローの手順を追加し、既存の設定をバックアップします。完了後にエージェントを再起動してください。
|
||||
|
||||
セットアップは変更を適用する前に内容を表示し、既存の設定をバックアップします。要件とセットアップのオプションは[インストールガイド](docs/installation.md)を参照してください。
|
||||
セットアップは Claude Code、Codex、Cursor、Gemini CLI、Grok Build、および[その他のエージェント](docs/installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](docs/installation.md)を参照してください。
|
||||
|
||||
### エージェントで使う(推奨)
|
||||
### エージェントに依頼する
|
||||
|
||||
セットアップ後にエージェントを再起動し、理解したいアプリや機能を[説明してください](#エージェントに頼むだけ)。Hopper はデモモードでも動作します。初回起動の画面が表示されたら、デモを選ぶか既存のライセンスを入力してください。
|
||||
|
||||
REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応しています。セットアップでは既存の REA 登録が最初から選択され、それ以外の検出済みエージェントは選択するまで対象になりません。その他のエージェントは[手動の MCP 設定](#他のコーディングエージェントで使う)を使えます。
|
||||
|
||||
### AI コーディングアシスタント向けスキル(任意)
|
||||
|
||||
AI コーディングアシスタントにスキルを追加すると、より豊富なコンテキストを利用できます:
|
||||
|
||||
```bash
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```text
|
||||
Notes アプリの検索機能の仕組みを調べ、根拠を示して、私のプロジェクトにも同様の機能を実装してください。
|
||||
```
|
||||
|
||||
このスキルは REA の調査ワークフローを提供します。REA をエージェントに接続して解析ツールを設定するには、上記のセットアップを実行してください。セットアップは既定でバージョンの一致したスキルをインストールします。このコマンドはリポジトリ版のスキルをインストールします。
|
||||
Notes を対象のアプリに置き換え、理解したい機能を指定してください。
|
||||
|
||||
### ターミナルで最初の結果を得る
|
||||
### ターミナルを使う
|
||||
|
||||
展開済みの JavaScript/Electron アプリのディレクトリまたは ASAR に対して、次を実行します:
|
||||
展開済みの JavaScript/Electron アプリのディレクトリ、または ASAR を調べます。
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
パスは調査対象に置き換えてください(Windows では `"D:/apps/example"` など)。MCP の設定、Hopper、Ghidra を必要とせず、アプリを実行することもなく、Evidence、復元したグラフ、制限、未解決事項をその場で返します。ネイティブアプリの場合は、先にエンジンを設定してから、そのアプリのパスを指定して `analyze` を使います。`doctor` は問題を診断したいときに使うもので、解析のたびに実行する必要はありません。
|
||||
結果にはモジュール、インポート、Electron の境界と、それらの根拠が含まれます。パスは対象に合わせて変更してください。Windows では、たとえば `"D:/apps/example"` を指定します。
|
||||
|
||||
### 作業に合わせて準備状況を確認する
|
||||
|
||||
オプションなしの `rea doctor` は連携全体の監査です。検出したすべてのエージェント登録、インストール済みのスキル、すべての任意の解析エンジンを確認するため、今の作業には支障がなくても `healthy: false` を報告することがあります。作業に合わせて確認範囲を指定してください:
|
||||
|
||||
| 作業 | 準備状況の確認 |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------------------- |
|
||||
| 静的な JavaScript/Electron 解析 | 不要です。`analyze-javascript-application` を直接実行します。 |
|
||||
| 特定の解析エンジンのトラブルシューティング | `rea doctor --provider ghidra --json`(または `hopper`、`ida`) |
|
||||
| 特定のエージェントの MCP 登録を確認 | `rea doctor --client codex --json`([クライアント ID](docs/installation.md#supported-agents)) |
|
||||
| インストール済みのスキルを確認 | `rea doctor --skill --json` |
|
||||
|
||||
範囲を指定したレポートでは、`healthy` と終了ステータスを決めるのは `scope_checks` だけです。それ以外の項目は `informational_checks` に表示され、今の作業のために修正する必要はありません。たとえば、使っていないエンジンが見つからなくても対処は不要です。
|
||||
|
||||
ネイティブ対象に対応するエンジンが複数インストールされている場合、REA は自動ではどれも選びません。対象を開くと `code: "capability_unavailable"` と `details.selection_reason: "ambiguous"` で失敗し、`details.candidate_ids` に候補が示されます。CLI の `--provider` か `open_binary` の `provider_id` で一度選ぶか、常用する設定として `REA_ANALYSIS_PROVIDER` を指定してください。詳しくは[英語版の解説](README.md#choosing-a-deep-analysis-provider)を参照してください。
|
||||
|
||||
### rea コマンドをインストール
|
||||
|
||||
コマンドラインツールをインストールします:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
Node.js と npm を先にインストールしておいてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
|
||||
|
||||
npm でインストールしてから、セットアップを実行することもできます:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### 要件
|
||||
|
||||
静的な JavaScript 解析に必要なのは Node.js と npm だけです。ホストと外部ツールの前提条件は使うワークフローによって異なり、ネイティブ解析プロバイダーの対応プラットフォームは各ガイドで説明しています。
|
||||
|
||||
- macOS 12 以降
|
||||
- Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux、または CachyOS
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+
|
||||
- npm(特定のバージョンは不要で、REA が npm をインストールすることもありません)
|
||||
|
||||
ネイティブバイナリの詳細な解析には Hopper、Ghidra、IDA Pro のいずれかが必要です。Hopper は独自のライセンスを持つ別製品で、デモ版でもベンダーが定める制限の範囲で解析できます。Ghidra と IDA は、利用者が用意したものを使います。
|
||||
|
||||
Ghidra は Linux x64 と macOS x64/arm64 に対応しています。Ghidra 12.1.x と、そのインストールが宣言する 64 ビットの完全な JDK(`application.java.min` から `application.java.max` まで)を別途インストールし、REA で使うように設定してください。現行の 12.1 リリースは JDK 21 以降を必要とし、上限はありません。ブリッジは Ghidra 12.1.4 と JDK 21 で検証しています。macOS では、ホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
|
||||
|
||||
```bash
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
|
||||
export JAVA_HOME=/absolute/path/to/jdk-21 # java と javac が PATH から解決できる場合は省略可
|
||||
rea doctor --provider ghidra --json
|
||||
rea setup
|
||||
```
|
||||
|
||||
セットアップはこれらのインストールを確認し、承認後にパスを保存します。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
|
||||
|
||||
IDA Pro は、すでに動作している [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) の MCP 登録を再利用します。開いている GUI の対象を読み取り専用で解析するか(`attached`、既定)、データベースのスーパーバイザーで指定したバイナリをヘッドレスで開いて解析します(`headless`)。REA が attached の GUI データベースを保存したり閉じたりすることはありません。最初の実環境での検証は、Windows の GUI と Windows x64 のヘッドレス IDA 9.3 が対象です。設定方法と対応範囲は [IDA プロバイダーガイド](docs/ida-provider.md)を参照してください。
|
||||
|
||||
```bash
|
||||
export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json
|
||||
rea function /absolute/path/to/program main --provider ida --json
|
||||
```
|
||||
|
||||
リポジトリの main と npm 4.1.0 には、ローカル NTFS 上のネイティブ x86-64 PE アプリケーション(マネージドコードと DLL を除く)を対象とする、実験的な Windows x64 Ghidra P0 対応が含まれています。Job Object、プライベート DACL、パスの受け入れ検査の各制御も同梱されています。古い npm パッケージでこの機能を使えるか確認するには[リリース境界](docs/installation.md#released-package-and-main)を、前提条件と検証済みの範囲は [Windows Ghidra P0](docs/windows-ghidra-p0.md) を参照してください。
|
||||
|
||||
### トラブルシューティング
|
||||
|
||||
`npx -y rea-agents@latest doctor` は、ホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断結果が必要な場合は `--json` を追加してください。
|
||||
|
||||
Linux では実行可能な `/opt/hopper/bin/Hopper` を優先し、利用できない場合は `~/.local/share/rea/hopper/bin/Hopper` を自動的に確認します。それ以外の場所にインストールした場合は `HOPPER_LAUNCHER_PATH` を設定してください。ファイルがあるのに解析エンジンが見つからないと報告される場合は、実際の Hopper のパスに対して `ldd /absolute/path/to/Hopper | grep 'not found'` を実行し、不足しているライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
|
||||
|
||||
### 更新とアンインストール
|
||||
|
||||
- `rea update` は現在の REA インストールを更新します。
|
||||
- `rea uninstall` は REA が管理するエージェント登録とスキルだけを削除します。Hopper、Node.js、Evidence ファイル、キャプチャ、無関係なスキル、他の MCP サーバーは残ります。
|
||||
- `rea uninstall --purge-data` は `~/.rea/cache` と `~/.rea/state` も削除します。それらを消したい場合にだけ使ってください。
|
||||
|
||||
## 現在の対応状況
|
||||
|
||||
リポジトリの現在の機能とプラットフォーム要件は[英語の対応状況ガイド](README.md#current-status)で説明しています。main は [npm リリース](docs/installation.md#released-package-and-main)より先行する場合があります。
|
||||
|
||||
- Ghidra は Linux x64、macOS x64/arm64、実験的な Windows x64 P0 境界で 25 種類の読み取り専用操作を提供します。Linux と macOS では、セッション内での関数名とエントリーコメントのアトミックな編集にも対応します。Windows P0 には変更の権限がなく、Ghidra には REA から操作できる GUI 機能はありません。
|
||||
- IDA アダプターは、ドキュメントに記載された読み取り専用の関数・文字列操作を提供します。
|
||||
- 静的な Android APK の検査は Linux と macOS に対応し、別途用意したヘッドレス JADX と完全な JDK が必要です。現在のメタデータブリッジは macOS arm64 で検証しています。詳しくは [Android 解析](docs/android-analysis.md)を参照してください。
|
||||
- ブラウザー、Electron、プロセスの各リクエストは対象、操作、ライフサイクルを直接指定し、ホストで実際に許可されたアクセスの範囲で動作します。REA 側で別途権限を付与する必要はありません。セットアップによる設定の書き込みと Hopper のインストールには、引き続き具体的な計画の承認が必要です。
|
||||
- `rea capabilities` と `rea providers` はバイナリセッションのプロバイダーと補助操作を説明するもので、ブラウザー、Android、アプリのすべてのワークフローの一覧ではありません。MCP の機能全体は、接続中の MCP ツール一覧と `binary_session` が示すツールの利用可否で確認してください。
|
||||
|
||||
## ひとつのプロンプトで調査を完結
|
||||
|
||||
```text
|
||||
メモアプリをリバースエンジニアリングし、オフライン検索機能の仕組みを説明して、
|
||||
TypeScript と SQLite を使って私のプロジェクト向けに実装してください。
|
||||
```
|
||||
|
||||
| 手順 | エージェントの処理 | REA ツール |
|
||||
| ---: | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| 1 | バイナリを開いて識別 | `open_binary`, `binary_overview` |
|
||||
| 2 | オフライン検索の手掛かりを探す | `search_strings`, `search_procedures`, `list_names` |
|
||||
| 3 | 手掛かりを実行コードに結び付け | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
|
||||
| 4 | 関連する制御フローを復元 | `get_call_graph`, `procedure_callees`, `procedure_info` |
|
||||
| 5 | 関連する処理を逆コンパイル | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
|
||||
| 6 | プロジェクトに機能を実装 | 技術スタック、プロダクト、要件に合わせたコード |
|
||||
|
||||
手順 1〜5 のアプリ解析は REA が担当します。手順 6 は、アプリについて得た知見をもとに、エージェントが通常の編集ツールとテストツールで行います。
|
||||
|
||||
## エージェントにできること
|
||||
|
||||
- 気に入った機能を調査し、自分のプロダクトに合わせた形で実装する。
|
||||
- ソースコードが手に入らない機能の仕組みを説明する。
|
||||
- アプリの認証、保存、更新、ネットワーク処理の流れを復元する。
|
||||
- 文書化されていない形式やインターフェースについて、文書化に必要な構造を復元する。
|
||||
- 文字列やシンボルから、疑わしい動作を実装しているコードまでたどる。
|
||||
- 復元した動作を、プロダクト機能、テスト、移行メモ、移植版、相互運用できる代替実装に変える。
|
||||
- マングルされたシンボルをひとつずつ手作業で読み解かなくても、Swift と Objective-C のメタデータを解析する。
|
||||
- Hopper に名前、コメント、ブックマークを残し、人とエージェントの解析を互いに活かす。
|
||||
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照、注釈、バイト読み取り、ファイルオフセット |
|
||||
| 調査ワークフロー | 14 | アプリ概要、関数ドシエ、ネイティブ API とディスパッチ、一括逆コンパイル、機能トレース、コールパス、コールグラフ、Swift と Objective-C の検出 |
|
||||
| ネイティブバイナリ | 10 | macOS の Mach-O メタデータ、署名、plist、アーキテクチャ、Swift シンボル、LLDB による呼び出し観測;Linux の ELF レイアウト、静的な保護機構の証拠と記録済みクラッシュ |
|
||||
| 成果物グラフ | 7 | ディレクトリとパッケージの目録、コンパイル済み Interface Builder ファイル、Apple アセットカタログ、Mach-O dylib のロード解決、抽出、オフライン EVM インターフェース推定 |
|
||||
| マネージド PE/CLI | 7 | .NET の識別情報、メタデータ、CIL 命令、ネイティブ依存関係、再構築結果のインポート、ビルド比較 |
|
||||
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
|
||||
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッドの逆コンパイル、静的な被参照 |
|
||||
| ブラウザー観察 | 12 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP の検出、スクリーンショット、キャプチャ比較 |
|
||||
| Electron 解析 | 5 | レンダラーの観察、静的なアプリのマッピング、静的結果と実行時結果の照合 |
|
||||
| JavaScript 実行時 | 2 | Node/Electron Inspector の対象検出、スクリプトの位置、実行コンテキストのイベント |
|
||||
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、Android/Apple の目録の投影、層をまたぐ機能トレース、ビルド比較、過去のソースとの対応付け、静的な戻り値形状の比較、再構築の検証 |
|
||||
| ワークスペースと観察 | 21 | セッション、Evidence バンドル、ナビゲーションの文脈、プロセス・成果物・関数の比較、未解決事項の記録 |
|
||||
|
||||
## ロードマップ
|
||||
|
||||
[現在の対応状況](#現在の対応状況)は提供済みの機能です。今後の作業は次のとおりです。
|
||||
|
||||
- **現在:** ツール、プロバイダー、セットアップの選択肢、バージョンの変更に合わせてドキュメントを正確に保ち、Hopper と Ghidra で検証するネイティブバイナリのアーキテクチャと間接呼び出しの範囲を広げます。
|
||||
- **次:** 機能トレースに静的抽出と実行時の観察を追加し、難読化された .NET アセンブリの比較を改善し、プロセス、プロトコル、ファイルシステム、再接続、バージョン比較の範囲を広げます。
|
||||
- **その後:** ブラウザーと Electron のシナリオ操作を増やし、LLDB や Frida などによるネイティブアプリの実行時観察と、他の解析ツールや対象の評価を進めます。
|
||||
|
||||
セットアップでは、すでにエージェント連携と Hopper のインストールを選べます。その他の解析ツールのインストールへの対応は今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
||||
セットアップは Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応しています。既存の REA 登録は最初から選択され、それ以外の検出済みエージェントは選択するまで対象になりません。ローカル MCP サーバーに対応するエージェントであれば、次の設定でも接続できます。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.0.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
常用する登録では、パッケージのバージョンを 1 つに固定してください。`rea setup` はその固定バージョンを維持し、同梱のスキルも同時に更新します。
|
||||
|
||||
## 仕組み
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Agent["コーディングエージェント"] --> REA["REA<br/>CLI + MCP"]
|
||||
Terminal["ターミナル"] --> REA
|
||||
REA --> Hopper["Hopper プロバイダー"]
|
||||
REA --> Ghidra["Ghidra プロバイダー<br/>インベントリ + 関数解析 + 注釈"]
|
||||
REA --> Ida["IDA MCP プロバイダー<br/>attached GUI または headless データベース"]
|
||||
Hopper --> App["あなたのアプリ"]
|
||||
Ghidra --> App
|
||||
Ida --> App
|
||||
```
|
||||
|
||||
CLI と MCP サーバーは、同じアプリケーションワークフローと Evidence の契約を使います。ターミナルのコマンドは完了すると自身のブリッジセッションを解放し、エージェントのセッションは調査中の対象と Evidence を保持できます。REA のセッションを閉じても、利用中の Hopper アプリは終了しません。
|
||||
|
||||
## CLI
|
||||
|
||||
上のエージェントワークフローが、REA を使う最も簡単な方法です。ターミナルからアプリの概要を一度だけ調べる場合は、次を実行します。
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
直接の逆コンパイルやその他のオプションは、`npx -y rea-agents@latest --help` で確認できます。
|
||||
|
||||
グローバルな `rea` コマンドとしてもインストールできます。
|
||||
日常的に使う場合は、`rea` コマンドをインストールできます。
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
rea mcp
|
||||
```
|
||||
|
||||
REA は Mac の `.app` フォルダーを直接開けます。エージェントがアプリを名前で見つけられない場合は、インストール場所を伝えてください。
|
||||
ネイティブ分析には、先にプロバイダーの設定が必要です。ネイティブ分析のコマンド、プロバイダーの選択、スナップショット、スクリプトでの利用は、[CLI と Evidence のガイド](docs/cli.md)を参照してください。
|
||||
|
||||
## Hopper アプリの動作
|
||||
### REA を更新する
|
||||
|
||||
REA は操作に必要になった時点で Hopper を起動します。Hopper のランチャーは内部でアプリをアクティブにするため、REA がバックグラウンドでの起動を要求しても、macOS では Hopper のウィンドウやダイアログが前面に出る場合があります。デモやライセンスの画面では操作が必要になることがあります。
|
||||
REA は更新が速く、新しいリリースには頻繁にバグ修正が含まれます。インストールしたバージョンを最新に保ってください。
|
||||
|
||||
Hopper は解析リクエストを一度に 1 つずつ処理します。待機をキャンセルしても、Hopper 内ですでに実行中の処理は止まりません。処理中かどうかはセッションが報告します。セッションを閉じると REA のブリッジと一時ソケットディレクトリを削除しますが、利用中の Hopper アプリは終了しません。後片付けを確認できなかった場合、`close_binary` は `cleanup_incomplete` と対象のリソースを報告します。
|
||||
npm でインストールした CLI の場合:
|
||||
|
||||
## セキュリティモデル
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
解析はローカルで実行されます。REA は Hopper や Ghidra とは認証付きのプライベートなローカルソケットで、IDA とは設定済みのローカル MCP 登録を通じて通信します。Ghidra セッションは隔離された一時プロジェクトを使い、ユーザーの Ghidra プロジェクトを開いたり変更したりしません。
|
||||
エージェントの登録とスキルを更新するには、更新処理が表示するセットアップコマンドを実行してください。
|
||||
|
||||
これはサンドボックスではありません。解析ツールと起動した対象は現在のユーザー権限で動作し、同じ OS ユーザー権限で動く悪意のあるプロセスは防げません。Windows Ghidra P0 では、ネイティブの Job Object、保護されたプライベート DACL、ハンドルベースの受け入れ検査を自動的に使います。脆弱性は [SECURITY.md](SECURITY.md) に記載された非公開の手順で報告してください。
|
||||
`npx` を使っている場合は、次のコマンドでエージェントの設定を更新します。
|
||||
|
||||
## FAQ
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
<details><summary><strong>Hopper を先に起動しておく必要はありますか?</strong></summary>
|
||||
設定の変更を確認し、エージェントを再起動してください。単発の CLI 操作では、`npx rea-agents@latest` に続けてコマンドを指定します。
|
||||
|
||||
いいえ。REA は必要になった時点で Hopper を起動します。すでに起動している Hopper でも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
|
||||
## REA の仕組み
|
||||
|
||||
エージェントは MCP 経由で REA を呼び出し、対象を調べて関連するコードを追跡します。REA は調査結果と根拠を返します。エージェントはそれを使って追加の質問をしたり、動作を説明したり、実装を書いてテストしたりします。CLI コマンドも同じワークフローを使います。
|
||||
|
||||

|
||||
|
||||
[原寸大の図を開く](website/public/assets/figures/rea-investigation-flow.svg)。
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## 分析できる対象
|
||||
|
||||
REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm が必要です。追加のツールや対応ホストは、対象によって異なります。
|
||||
|
||||
| 対象 | REA が返すもの | 要件とガイド |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/guides/native/) |
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](docs/binary-diagnostics.md) |
|
||||
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | ローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | 利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/guides/javascript/) |
|
||||
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/guides/browser/) |
|
||||
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](docs/web-network-captures.md) |
|
||||
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](docs/managed-code-analysis.md) |
|
||||
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
|
||||
| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) |
|
||||
| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) |
|
||||
| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) |
|
||||
|
||||
静的な JavaScript と .NET の検査は、指定されたファイルを読み取り、アプリケーションを実行しません。実行時キャプチャは、あなたのユーザー権限で対象を実行したり、対象とやり取りしたりします。それぞれの実行時ガイドに、どのような操作が行われるかを記載しています。
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
対応するネイティブ形式とホストは、プロバイダーによって異なります。[Hopper と Ghidra のセットアップ](docs/installation.md#hopper)、[IDA ガイド](docs/ida-provider.md)、[Windows 向け Ghidra の実験的対応](docs/windows-ghidra-p0.md)を参照してください。Ghidra は [16 ビット DOS 分析](docs/ghidra-dos.md)にも対応しています。プロバイダーの選択は [CLI ガイド](docs/cli.md#choose-a-provider)を参照してください。最新の npm リリース以降に追加された機能については、[リリースでの利用可否](docs/installation.md#released-package-and-main)を確認してください。
|
||||
|
||||
## 事例
|
||||
|
||||
### DX-Ball:音声のパン計算を再構築する
|
||||
|
||||
音声の呼び出しから位置をパンに変換する補助関数をたどり、命令を調べ、不完全な疑似コードを C に書き直します。再構築した実装は、元の x86 に対する 3,205 ケースの検証を通過し、コンパイル後の関数の全 63 バイトを再現しています。
|
||||
|
||||
[事例を読む](https://rea.tools/showcase/dx-ball/) ·
|
||||
[再構築リポジトリ](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion:Electron のクリップボードブリッジを追跡する
|
||||
|
||||
レンダラーのクリップボード API を見つけ、preload と IPC を経由してメインプロセスまでたどり、リッチなクリップボード形式を調べます。
|
||||
|
||||
[事例を読む](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04:DOS の弾幕リングの計算を復元する
|
||||
|
||||
元の PC-98 ゲームの 16 ビット命令を調べ、固定角度と自機を狙う角度の計算を復元し、再構築した C++ を当時のコンパイラーの出力と比較します。
|
||||
|
||||
[事例を読む](https://rea.tools/showcase/th04/) ·
|
||||
[再構築リポジトリ](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
REA で興味深い対象を調べたら、ぜひ共有してください。[issue](https://github.com/morluto/rea/issues) または [pull request](https://github.com/morluto/rea/pulls) に、対象、調べたかったこと、REA がどう役立ったか、何が分かったかを記載してください。
|
||||
|
||||
## よくある質問
|
||||
|
||||
<details>
|
||||
<summary><strong>どのエージェントで REA を使えますか?</strong></summary>
|
||||
|
||||
ローカル MCP サーバーに対応するエージェントなら使えます。セットアップは[対応エージェント](docs/installation.md#supported-agents)を設定します。その他のクライアントでは、[MCP の手動登録](docs/installation.md#mcp-registry)を利用できます。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA に Hopper は含まれていますか?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>Hopper、Ghidra、IDA は必要ですか?</strong></summary>
|
||||
|
||||
含まれていません。セットアップで Hopper をインストールできますが、Hopper は独自のライセンスを持つ別製品です。REA は、エージェントが Hopper を使えるようにする CLI、MCP サーバー、ワークフローを提供します。
|
||||
深いネイティブ分析には、そのいずれかを使います。静的な JavaScript と .NET の検査には、ネイティブ分析エンジンは必要ありません。セットアップは承認後に Hopper をインストールできます。Ghidra と IDA は既存のインストールを使います。[プロバイダーのセットアップ](docs/installation.md#hopper)を参照してください。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA は Ghidra や Java をインストールしたり同梱したりしますか?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>先に Hopper を起動する必要はありますか?</strong></summary>
|
||||
|
||||
いいえ。REA は既存の Ghidra のインストールに接続します。[要件](#要件)にある Ghidra と Java のパスを設定してから、セットアップを実行してください。
|
||||
REA は必要な操作の際に Hopper を起動します。macOS では、初回起動時にデモモードを選ぶか、ライセンスを有効化するよう求められることがあります。[Hopper の起動とトラブルシューティング](docs/installation.md#launcher-paths-and-troubleshooting)を参照してください。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>アプリはアップロードされますか?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>skills.sh からスキルをインストールすると何ができますか?</strong></summary>
|
||||
|
||||
REA にホスト型の解析サービスはありません。現在のプロバイダーは、成果物の解析と動作のキャプチャをローカルで行います。エージェントやモデルの提供元には独自のデータポリシーがあるため、別途確認してください。
|
||||
スキルはエージェントに調査の手順を提供します。`rea setup` で REA の MCP サーバーを登録し、対応する手順をインストールしてから、エージェントを再起動してください。[スキルのみのインストール](docs/installation.md#skill-only-installation)を参照してください。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>元のソースコードを復元できますか?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>REA はどのようなコードを返しますか?</strong></summary>
|
||||
|
||||
元のソースコードを確実に復元できるデコンパイラーはありません。REA はエージェントに疑似コード、アセンブリ、シンボル、文字列、メタデータ、それらの関係を提供し、観察された動作の説明や互換性のある再現に役立てられるようにします。
|
||||
ネイティブ分析は疑似コードとアセンブリを返します。JavaScript/Electron 分析はモジュールとその関係を復元します。エージェントはその結果を使って実装を書き、テストします。[事例](#事例)に具体的な手順があります。
|
||||
|
||||
</details>
|
||||
|
||||
## 開発
|
||||
<details>
|
||||
<summary><strong>REA はアプリをアップロードしますか?</strong></summary>
|
||||
|
||||
開発環境、アーキテクチャ、テスト、リリース手順は [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。
|
||||
REA は対象をローカルで分析します。エージェントはツールの結果を受け取り、そのモデルプロバイダーには独自のデータポリシーがあります。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>バグに遭遇したらどうすればよいですか?</strong></summary>
|
||||
|
||||
まず更新してください。最近のリリースですでに修正されている可能性があります。
|
||||
|
||||
npm でインストールした CLI の場合:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
`npx` でエージェントを設定している場合:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
エージェントを使っている場合は、[設定の更新](#rea-を更新する)を完了し、再起動してください。同じタスクをもう一度試し、問題が続く場合は [issue を作成](https://github.com/morluto/rea/issues)して、REA のバージョン、対象の種類、再現手順、エラー出力を記載してください。
|
||||
|
||||
</details>
|
||||
|
||||
## ドキュメント
|
||||
|
||||
まずはウェブサイトの[実践ガイド](https://rea.tools/guides/)を参照してください。正確なオプション、前提条件、結果の契約については、以下を参照してください。
|
||||
|
||||
- [インストールとセットアップ](docs/installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。
|
||||
- [準備状況の確認とトラブルシューティング](docs/installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。
|
||||
- [CLI と Evidence](docs/cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。
|
||||
- [MCP 契約](docs/mcp-contracts.md)と[エージェント向けプロンプト](docs/mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。
|
||||
- [ツールカタログ](docs/mcp-contracts.md#generated-catalog):ビルド時に生成されるツール、プロバイダー、CLI コマンドの一覧。
|
||||
- [ロードマップ](docs/roadmap.md):計画中の作業と機能の進捗。
|
||||
|
||||
脆弱性の報告は [SECURITY.md](SECURITY.md) に従ってください。
|
||||
|
||||
## 貢献する
|
||||
|
||||
REA への貢献を歓迎しますしてバグや機能の提案を報告したり、[pull request を送信](https://github.com/morluto/rea/pulls)してコードやドキュメントを改善したりできます。
|
||||
|
||||
開発環境とチェックは [CONTRIBUTING.md](CONTRIBUTING.md)、検証レーンは[テストガイド](docs/testing.md)、プロジェクト構造は[アーキテクチャ図](docs/architecture.mermaid)を参照してください。
|
||||
|
||||
## プロジェクトリンク
|
||||
|
||||
[ウェブサイト](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [セキュリティ](SECURITY.md)
|
||||
|
||||
## スター履歴
|
||||
|
||||
🎉 **GitHub スターが 20,000 を突破しました。ありがとうございます!**
|
||||
🎉 **GitHub スター 20,000 件、ありがとうございます!**
|
||||
|
||||
REA を使い、問題を報告し、ビルドをテストし、修正を届けてくださる皆さんに感謝します。
|
||||
REA の利用、バグ報告、ビルドのテスト、修正への貢献に感謝します。
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
@@ -380,6 +278,10 @@ REA を使い、問題を報告し、ビルドをテストし、修正を届け
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 免責事項
|
||||
|
||||
REA は、合法的なリバースエンジニアリングの研究、分析、再構築のためのツールを提供します。必要な許可を得て、適用される法律に従う責任は利用者にあります。このプロジェクトは違法または無許可の利用を支持しません。
|
||||
|
||||
## ライセンス
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
+183
-230
@@ -1,325 +1,274 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [日本語](README_ja.md) · **한국어** · [العربية](README_ar.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: 무엇이든 리버스 엔지니어링
|
||||
|
||||
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
|
||||
### 하나의 MCP로 바이너리, 애플리케이션, 런타임 동작을 리버스 엔지니어링합니다.
|
||||
|
||||
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
|
||||
**마음에 드는 기능을 발견했다면, 바이너리 수준까지 작동 원리를 이해하세요.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#조사-도구-카탈로그)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
**[웹사이트](https://rea.tools/) · [가이드](https://rea.tools/guides/) · [사례](https://rea.tools/showcase/)**
|
||||
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [문서](#문서)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA가 Hopper 안에서 분석 브리지를 실행해 네이티브 바이너리를 검사하는 모습" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA 커뮤니티">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>리버스 엔지니어링 커뮤니티에 참여하세요</strong>
|
||||
</a><br />
|
||||
<sub>Discord · 질문과 답변 · 작업 공유</sub>
|
||||
<sub>Discord · 질문과 답변 · 결과 공유</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper에서 분석 브리지를 시작하고 네이티브 바이너리를 조사하는 REA" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
앱에서 마음에 드는 기능을 찾았다면 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 분석하고, 기능의 동작과 근거를 설명한 뒤, 프로젝트에 맞는 기능을 구현할 수 있습니다.
|
||||
앱에서 내 제품에도 넣고 싶은 기능을 발견했나요? 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 검사하고, 기능의 작동 원리를 설명하고, 근거를 보여 주고, 프로젝트에 맞는 기능을 구현할 수 있습니다.
|
||||
|
||||
REA는 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트 분석 도구를 제공합니다. 에이전트와 터미널에서 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며 결과에는 근거와 제한 사항이 포함됩니다.
|
||||
REA는 에이전트를 분석 도구에 연결해 네이티브 바이너리, JavaScript와 Electron 앱, .NET 어셈블리, 웹사이트를 검사할 수 있게 합니다. 터미널에서도 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며, 결과에는 각 결론의 근거와 한계가 포함됩니다.
|
||||
|
||||
Setup은 에이전트를 설정하고 기존 Hopper 또는 Ghidra 설치에 연결합니다. 승인 후 Hopper를 설치할 수도 있습니다.
|
||||
설정 과정은 에이전트에 REA를 등록하고 해당 버전에 맞는 워크플로 지침을 설치합니다. 네이티브 분석에는 기존 Hopper 또는 Ghidra 설치를 사용할 수 있습니다. 승인하면 설정 과정에서 Hopper를 설치할 수도 있습니다. 정적 JavaScript 분석에는 두 엔진 모두 필요하지 않습니다.
|
||||
|
||||
## 에이전트에게 바로 요청하기
|
||||
|
||||
[설정](#빠른-시작)을 마친 뒤 에이전트를 다시 시작하고 요청하세요.
|
||||
|
||||
```text
|
||||
메모 앱의 검색 기능이 어떻게 동작하는지 조사하고 근거를 보여 주세요.
|
||||
그다음 제 프로젝트에 비슷한 기능을 구현해 주세요.
|
||||
```
|
||||
|
||||
메모 앱 대신 조사할 앱을 지정하거나 먼저 개요를 요청할 수 있습니다.
|
||||
|
||||
## 바이너리에서 동작까지
|
||||
|
||||
| 디컴파일 | 이해 | 재현 |
|
||||
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| 네이티브 앱이나 실행 파일에서 프로시저, 의사 코드, 어셈블리, 문자열, 심볼, 세그먼트, 메타데이터를 복구합니다. | 호출자, 피호출자, 상호 참조, 호출 그래프를 따라 기능이나 알고리즘의 실제 동작을 설명합니다. | 에이전트가 배운 내용을 사용자의 기술 스택, 화면, 요구 사항에 맞는 제품 기능으로 만듭니다. |
|
||||
|
||||
REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스 코드를 복원하거나 앱 전체를 자동으로 복제한다고 주장하지 않습니다.
|
||||
|
||||
## REA를 사용하는 이유
|
||||
|
||||
| | |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **에이전트용** | 앱의 동작에 대해 질문하고 추측 대신 에이전트가 조사하게 합니다. |
|
||||
| **CLI와 MCP** | 터미널과 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
|
||||
| **안내식 설정** | 에이전트 설정, 기존 분석 도구 연결, 승인 후 Hopper 설치를 지원합니다. |
|
||||
| **통찰에서 코드로** | 기능을 이해한 뒤 같은 코딩 세션에서 자신의 제품에 맞는 버전을 구현합니다. |
|
||||
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
|
||||
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 앱을 연속으로 조사합니다. |
|
||||
> **[REA 웹사이트](https://rea.tools/)**에서 설정 방법, 그림으로 설명한 가이드, 실제 사례를 확인하세요.
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 설정 실행하기(권장)
|
||||
### 에이전트 설정
|
||||
|
||||
REA를 에이전트에 연결하세요.
|
||||
Node.js와 npm이 설치된 환경에서 다음 명령을 실행하세요.
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합니다. 기존 REA 등록은 기본 선택되며, 감지만 된 클라이언트는 자동 선택되지 않습니다. 아직 설정되지 않은 클라이언트도 직접 선택할 수 있습니다. 정확한 경로와 변경 사항을 검토한 뒤 승인하세요. 선택한 에이전트에는 기본적으로 REA 워크플로가 설치됩니다. Hopper는 별도의 선택 사항이며 따로 승인해야 합니다. 기존 Ghidra 경로도 등록할 수 있습니다.
|
||||
에이전트를 선택하고, 예정된 변경 사항을 검토한 뒤 승인하세요. 설정 과정은 기존 설정을 백업하고 REA의 MCP 서버와 버전에 맞는 워크플로 지침을 추가합니다. 완료 후 에이전트를 다시 시작하세요.
|
||||
|
||||
변경 전에 계획을 표시하고 기존 설정을 백업합니다. 요구 사항과 추가 옵션은 [설치 안내](docs/installation.md)를 참고하세요.
|
||||
설정은 Claude Code, Codex, Cursor, Gemini CLI, Grok Build 및 [다른 에이전트](docs/installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](docs/installation.md)을 참고하세요.
|
||||
|
||||
### 에이전트에서 사용하기
|
||||
### 에이전트에게 요청
|
||||
|
||||
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
|
||||
|
||||
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
|
||||
|
||||
### AI 코딩 어시스턴트용 스킬(선택)
|
||||
|
||||
AI 코딩 어시스턴트에 스킬을 추가하면 더 풍부한 컨텍스트를 얻을 수 있습니다.
|
||||
|
||||
```bash
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```text
|
||||
Notes 앱의 검색 기능이 어떻게 작동하는지 조사하고, 근거를 보여 준 다음, 내 프로젝트에 비슷한 기능을 구현해 줘.
|
||||
```
|
||||
|
||||
이 스킬은 REA의 조사 워크플로를 제공합니다. 위 Setup을 실행해 REA를 에이전트에 연결하고 분석 도구를 설정하세요. Setup은 기본적으로 버전에 맞는 스킬을 설치합니다. 이 명령은 저장소 버전의 스킬을 설치합니다.
|
||||
Notes를 대상 앱으로 바꾸고 이해하고 싶은 기능을 지정하세요.
|
||||
|
||||
압축을 해제한 JavaScript/Electron 앱 트리나 ASAR라면 MCP 설정이나 네이티브 엔진 없이 바로 분석할 수 있습니다.
|
||||
### 터미널 사용
|
||||
|
||||
압축을 푼 JavaScript/Electron 앱 디렉터리 또는 ASAR를 검사합니다.
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
경로를 조사 대상의 경로로 바꾸세요(Windows에서는 `"D:/apps/example"` 같은 경로). 이 정적 워크플로에서는 이런 경로도 지원됩니다. 네이티브 분석에는 전용 엔진 설정이 필요합니다.
|
||||
결과에는 모듈, 임포트, Electron 경계와 관련 근거가 포함됩니다. 경로를 분석할 대상으로 바꾸세요. Windows에서는 `"D:/apps/example"` 같은 경로를 사용할 수 있습니다.
|
||||
|
||||
### rea 명령 설치하기
|
||||
|
||||
명령줄 도구를 설치하세요.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
Node.js와 npm이 먼저 설치되어 있어야 합니다. 터미널에서 실행하면 설치 프로그램이 `rea`를 설치하고 설정을 시작합니다.
|
||||
|
||||
npm으로 설치한 뒤 설정을 실행할 수도 있습니다.
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### 요구 사항
|
||||
|
||||
- macOS 12 이상
|
||||
- Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux 또는 CachyOS
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
|
||||
|
||||
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
|
||||
|
||||
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.x와 그 설치본이 선언한 완전한 64비트 JDK(`application.java.min`부터 `application.java.max`까지)를 별도로 설치한 뒤 REA에서 사용하도록 설정하세요. 현재 12.1 릴리스는 JDK 21 이상을 요구하고 상한은 없습니다. 브리지는 Ghidra 12.1.4와 JDK 21에서 검증됩니다. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
|
||||
|
||||
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
|
||||
|
||||
저장소 main과 npm 4.1.0에는 로컬 NTFS상의 네이티브 x86-64 PE 애플리케이션(비관리, 비 DLL 대상)을 위한 실험적인 Windows x64 Ghidra P0 지원이 포함되어 있습니다. Job Object, 비공개 DACL, 경로 승인 제어가 함께 제공됩니다. 이전 npm 패키지에 이 기능이 있을 것으로 기대하기 전에 [릴리스 경계](docs/installation.md#released-package-and-main)를 확인하세요. 전제 조건과 검증 범위는 [Windows Ghidra P0](docs/windows-ghidra-p0.md)를 참고하세요.
|
||||
|
||||
### 문제 해결
|
||||
|
||||
`npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요.
|
||||
|
||||
Linux에서는 실행 가능한 `/opt/hopper/bin/Hopper`를 우선 사용하고, 사용할 수 없으면 `~/.local/share/rea/hopper/bin/Hopper`를 자동으로 확인합니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 실제 Hopper 경로에 대해 `ldd /absolute/path/to/Hopper | grep 'not found'`를 실행해 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
|
||||
|
||||
### 업데이트와 제거
|
||||
|
||||
- `rea update`는 현재 REA 설치를 업데이트합니다.
|
||||
- `rea uninstall`은 REA가 관리하는 에이전트 등록과 워크플로 파일을 제거합니다. Hopper는 유지합니다.
|
||||
- `rea uninstall --purge-data`는 REA 캐시와 상태도 삭제합니다. 해당 데이터를 지우려는 경우에만 사용하세요.
|
||||
|
||||
## 현재 지원 범위
|
||||
|
||||
저장소의 현재 기능과 플랫폼 요구 사항은 [영문 지원 가이드](README.md#current-status)에 설명되어 있습니다. main은 [npm 릴리스](docs/installation.md#released-package-and-main)보다 앞선 내용이 포함될 수 있습니다.
|
||||
|
||||
- Ghidra는 Linux x64, macOS x64/arm64, 실험적인 Windows x64 P0 경계에서 25개의 읽기 전용 작업을 제공합니다. Linux/macOS에서는 원자적 세션 함수 주석도 추가로 지원합니다. Windows P0는 읽기 전용이며 Ghidra에는 GUI 제어가 없습니다.
|
||||
- 정적 Android 검사는 별도로 준비한 JADX/Java가 필요합니다. 실제 제공자 검증은 Linux를 대상으로 합니다. [Android 분석](docs/android-analysis.md)을 참고하세요.
|
||||
- 브라우저, Electron, 프로세스 요청은 대상·동작·수명 주기를 직접 지정하며 호스트의 실제 접근에 의존합니다. 별도의 REA 권한 승인은 필요 없습니다. Setup의 설정 쓰기와 Hopper 설치는 여전히 정확한 계획에 대한 승인이 필요합니다.
|
||||
- `rea capabilities`는 모든 앱/런타임 도구의 목록이 아니라 바이너리 세션 작업을 설명합니다. 연결된 MCP 도구 목록과 관련 가이드를 참고하세요.
|
||||
|
||||
## 하나의 프롬프트로 전체 조사
|
||||
|
||||
```text
|
||||
메모 앱을 리버스 엔지니어링해 오프라인 검색 기능의 작동 방식을 설명한 다음,
|
||||
TypeScript와 SQLite를 사용해 제 프로젝트에 맞는 버전을 구현해 주세요.
|
||||
```
|
||||
|
||||
| 단계 | 에이전트 작업 | REA 도구 |
|
||||
| ---: | ----------------------- | ---------------------------------------------------------------- |
|
||||
| 1 | 바이너리 열기 및 식별 | `open_binary`, `binary_overview` |
|
||||
| 2 | 오프라인 검색 단서 찾기 | `search_strings`, `search_procedures`, `list_names` |
|
||||
| 3 | 단서를 실행 코드에 연결 | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
|
||||
| 4 | 제어 흐름 재구성 | `get_call_graph`, `procedure_callees`, `procedure_info` |
|
||||
| 5 | 관련 루틴 디컴파일 | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
|
||||
| 6 | 프로젝트에 기능 구현 | 기술 스택, 제품, 요구 사항에 맞는 코드 |
|
||||
|
||||
REA는 1~5단계의 바이너리 분석을 처리합니다. 6단계는 에이전트가 일반 파일 편집 및 테스트 도구로 수행합니다.
|
||||
|
||||
## 에이전트가 할 수 있는 일
|
||||
|
||||
- 소스 코드가 없는 기능의 동작을 설명합니다.
|
||||
- 앱의 인증, 저장소, 업데이트 또는 네트워크 흐름을 재구성합니다.
|
||||
- 문서화되지 않은 형식이나 인터페이스의 구조를 복구합니다.
|
||||
- 문자열이나 심볼에서 의심스러운 동작을 구현한 코드까지 추적합니다.
|
||||
- 한 세션에서 두 앱 버전을 전환하며 구현 경로를 비교합니다.
|
||||
- 마음에 드는 기능을 조사하고 자신의 제품에 맞는 버전으로 구현합니다.
|
||||
- 복구한 동작을 제품 기능, 테스트, 마이그레이션 문서, 포팅 또는 상호 운용 가능한 대체품으로 변환합니다.
|
||||
- Swift 및 Objective-C 메타데이터를 분석합니다.
|
||||
- Hopper에 이름, 주석, 북마크를 남겨 사람과 에이전트의 분석을 연결합니다.
|
||||
|
||||
## 조사 도구 카탈로그
|
||||
|
||||
| 도구 분류 | 수 | 용도 |
|
||||
| ----------------- | --: | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 네이티브 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조, 주석, 바이트 읽기, 파일 오프셋 |
|
||||
| 조사 워크플로 | 14 | 앱 개요, 함수 기록서, 네이티브 API와 디스패치, 일괄 디컴파일, 기능 추적, 호출 경로, 호출 그래프, Swift와 Objective-C 탐색 |
|
||||
| 네이티브 바이너리 | 10 | macOS의 Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 심볼과 LLDB 호출 관찰; Linux의 ELF 레이아웃, 정적 보호 증거와 기록된 크래시 |
|
||||
| 아티팩트 그래프 | 7 | 디렉터리와 패키지 목록, 컴파일된 Interface Builder 파일, Apple 애셋 카탈로그, Mach-O dylib 로드 해석, 추출, 오프라인 EVM 인터페이스 추론 |
|
||||
| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 |
|
||||
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
|
||||
| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
|
||||
| 브라우저 관찰 | 12 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
|
||||
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
|
||||
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
|
||||
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
|
||||
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
|
||||
|
||||
## 로드맵
|
||||
|
||||
다음 작업은 네이티브 대상 검증 확대, JavaScript와 .NET의 버전 비교, 런타임 관찰과 재구현 검증입니다. Setup은 에이전트 연결과 Hopper 설치 선택을 이미 지원합니다. 다른 분석 도구 설치는 향후 작업입니다. [설치 로드맵](docs/roadmap.md)과 [분석 도구 평가](docs/provider-evaluation.md)를 참고하세요.
|
||||
|
||||
## 다른 코딩 에이전트에서 사용하기
|
||||
|
||||
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.0.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
## 작동 방식
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Agent["코딩 에이전트"] --> REA["REA<br/>CLI + MCP"]
|
||||
Terminal["터미널"] --> REA
|
||||
REA --> Hopper["Hopper 분석 작업"]
|
||||
Hopper --> App["사용자의 앱"]
|
||||
REA --> Ghidra["Ghidra 읽기 전용 분석<br/>인벤토리 + 함수 분석"]
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
|
||||
|
||||
## CLI
|
||||
|
||||
위의 에이전트 워크플로가 REA를 사용하는 가장 쉬운 방법입니다. 터미널에서 앱을 한 번 빠르게 살펴보려면 다음을 실행하세요.
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
직접 디컴파일하는 방법과 기타 옵션은 `npx -y rea-agents@latest --help`에서 확인할 수 있습니다.
|
||||
|
||||
전역 `rea` 명령으로 설치할 수도 있습니다.
|
||||
자주 사용할 경우 `rea` 명령을 설치하세요.
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
rea mcp
|
||||
```
|
||||
|
||||
REA는 macOS의 `.app` 폴더를 직접 열 수 있습니다. 에이전트가 앱을 찾지 못하면 설치 위치를 알려 주세요.
|
||||
네이티브 분석에는 먼저 제공자를 설정해야 합니다. 네이티브 명령, 제공자 선택, 스냅샷, 스크립트 사용은 [CLI 및 Evidence 가이드](docs/cli.md)를 참고하세요.
|
||||
|
||||
## Hopper 앱 동작
|
||||
### REA 업데이트
|
||||
|
||||
REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로 앱을 활성화하므로 대상을 열 때 Hopper가 다른 창 앞으로 나타날 수 있습니다. REA는 백그라운드 시작을 요청하지만 항상 뒤에 머무르는 것을 보장할 수 없습니다.
|
||||
REA는 빠르게 바뀌며 새 릴리스에는 버그 수정이 자주 포함됩니다. 설치된 버전을 최신으로 유지하세요.
|
||||
|
||||
명시적인 형식과 아키텍처 인자로 일반적인 FAT/ARM 선택 대화상자를 피하지만, 다른 Hopper 또는 macOS 대화상자에는 사람이 응답해야 할 수 있습니다. 세션 종료 시 브리지와 소켓 디렉터리를 제거하지만 사용자가 작업 중인 Hopper 앱은 종료하지 않습니다.
|
||||
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
|
||||
## 보안 모델
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
각 세션은 무작위 capability token과 현재 사용자 전용 Unix 소켓을 사용합니다. Ghidra 세션은 격리된 임시 프로젝트도 사용하며 사용자 소유 Ghidra 프로젝트를 열거나 수정하지 않습니다. 이는 샌드박스가 아니며 같은 운영 체제 사용자로 실행되는 악성 프로세스를 방어하지 않습니다. 신뢰할 수 없는 바이너리를 열면 선택한 로컬 공급자가 현재 사용자 권한으로 분석합니다. 취약점은 [SECURITY.md](SECURITY.md)의 비공개 절차로 신고하세요.
|
||||
에이전트 등록과 스킬을 갱신하려면 업데이트 과정에서 출력된 설정 명령을 실행하세요.
|
||||
|
||||
## FAQ
|
||||
`npx`를 사용한다면 다음 명령으로 에이전트 설정을 갱신하세요.
|
||||
|
||||
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
|
||||
설정 변경 사항을 검토하고 에이전트를 다시 시작하세요. 일회성 CLI 작업에는 `npx rea-agents@latest` 뒤에 필요한 명령을 붙여 실행하세요.
|
||||
|
||||
## REA의 작동 방식
|
||||
|
||||
에이전트는 MCP를 통해 REA를 호출하여 대상을 검사하고 관련 코드를 추적합니다. REA는 조사 결과와 근거를 반환합니다. 에이전트는 이를 바탕으로 추가 질문을 하고, 동작을 설명하거나, 구현을 작성하고 테스트합니다. CLI 명령도 같은 워크플로를 사용합니다.
|
||||
|
||||

|
||||
|
||||
[원본 크기의 그림 열기](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## 분석할 수 있는 대상
|
||||
|
||||
REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니다. 추가 도구와 호스트 지원은 대상에 따라 다릅니다.
|
||||
|
||||
| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
|
||||
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/guides/native/) |
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](docs/binary-diagnostics.md) |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
|
||||
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/guides/javascript/) |
|
||||
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/guides/browser/) |
|
||||
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](docs/web-network-captures.md) |
|
||||
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](docs/managed-code-analysis.md) |
|
||||
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
|
||||
| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](docs/firmware-analysis.md) |
|
||||
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](docs/javascript-artifact-reconstruction.md), [Apple 애플리케이션](docs/apple-application-analysis.md) |
|
||||
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](docs/process-capture.md) |
|
||||
|
||||
정적 JavaScript 및 .NET 검사는 제공된 파일을 읽으며 애플리케이션을 실행하지 않습니다. 런타임 캡처는 사용자의 권한으로 선택한 대상을 실행하거나 대상과 상호작용합니다. 각 런타임 가이드에서 그 영향을 설명합니다.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
지원하는 네이티브 형식과 호스트는 제공자마다 다릅니다. [Hopper 및 Ghidra 설정](docs/installation.md#hopper), [IDA 가이드](docs/ida-provider.md), [실험적 Windows Ghidra 지원](docs/windows-ghidra-p0.md)을 참고하세요. Ghidra는 [16비트 DOS 분석](docs/ghidra-dos.md)도 지원합니다. 제공자 선택은 [CLI 가이드](docs/cli.md#choose-a-provider)를 참고하세요. 최신 npm 릴리스 이후 추가된 기능은 [릴리스에서의 사용 가능 여부](docs/installation.md#released-package-and-main)를 확인하세요.
|
||||
|
||||
## 사례
|
||||
|
||||
### DX-Ball: 사운드 패닝 계산 재구현
|
||||
|
||||
사운드 호출에서 위치를 패닝 값으로 변환하는 보조 함수까지 추적하고, 명령을 검사하여 불완전한 의사 코드를 C로 옮깁니다. 재구현한 코드는 원본 x86에 대한 3,205개 테스트 케이스를 통과했으며, 컴파일된 함수의 63바이트 전체를 재현합니다.
|
||||
|
||||
[사례 읽기](https://rea.tools/showcase/dx-ball/) ·
|
||||
[재구현 저장소](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: Electron 클립보드 브리지 추적
|
||||
|
||||
렌더러의 클립보드 API를 찾고, preload와 IPC를 거쳐 메인 프로세스까지 추적한 뒤, 서식 있는 클립보드 형식을 검사합니다.
|
||||
|
||||
[사례 읽기](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: DOS 원형 탄막 계산 복원
|
||||
|
||||
원본 PC-98 게임의 16비트 명령을 검사하고 고정 각도와 조준 각도의 계산을 복원합니다. 재구현한 C++를 당시 컴파일러의 출력과 비교합니다.
|
||||
|
||||
[사례 읽기](https://rea.tools/showcase/th04/) ·
|
||||
[재구현 저장소](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
REA로 흥미로운 대상을 분석했다면 알려 주세요. [issue](https://github.com/morluto/rea/issues) 또는 [pull request](https://github.com/morluto/rea/pulls)에 대상, 조사하려던 질문, REA가 어떻게 도움이 되었는지, 무엇을 알아냈는지를 담아 공유하세요.
|
||||
|
||||
## 자주 묻는 질문
|
||||
|
||||
<details>
|
||||
<summary><strong>어떤 에이전트에서 REA를 사용할 수 있나요?</strong></summary>
|
||||
|
||||
로컬 MCP 서버를 지원하는 에이전트라면 사용할 수 있습니다. 설정 과정은 [지원하는 에이전트](docs/installation.md#supported-agents)를 구성합니다. 다른 클라이언트는 [수동 MCP 등록](docs/installation.md#mcp-registry)을 이용할 수 있습니다.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA에 Hopper가 포함되나요?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>Hopper, Ghidra 또는 IDA가 필요한가요?</strong></summary>
|
||||
|
||||
아니요. Setup이 Hopper를 설치할 수 있지만 Hopper는 자체 라이선스가 필요한 별도 소프트웨어입니다. REA는 CLI, MCP 서버, 에이전트용 워크플로를 제공합니다.
|
||||
심층 네이티브 분석에는 이 중 하나를 사용합니다. 정적 JavaScript 및 .NET 검사에는 네이티브 분석 엔진이 필요하지 않습니다. 설정 과정에서 승인 후 Hopper를 설치할 수 있으며, Ghidra와 IDA는 기존 설치를 사용합니다. [제공자 설정](docs/installation.md#hopper)을 참고하세요.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>바이너리가 업로드되나요?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>Hopper를 먼저 실행해야 하나요?</strong></summary>
|
||||
|
||||
REA에는 호스팅 분석 서비스가 없습니다. 로컬 Unix 소켓을 통해 Hopper를 조작합니다. 에이전트나 모델 공급자의 데이터 정책은 별도로 확인하세요.
|
||||
REA는 작업에 필요할 때 Hopper를 실행합니다. macOS에서는 처음 실행할 때 데모 모드를 선택하거나 라이선스를 활성화하라는 대화 상자가 나타날 수 있습니다. [Hopper 실행 및 문제 해결](docs/installation.md#launcher-paths-and-troubleshooting)을 참고하세요.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>원본 소스 코드를 복구할 수 있나요?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>skills.sh에서 설치하는 스킬은 어떤 역할을 하나요?</strong></summary>
|
||||
|
||||
보장할 수 없습니다. REA는 의사 코드, 어셈블리, 심볼, 문자열, 메타데이터 및 관계를 제공하여 에이전트가 관찰된 동작을 설명하거나 호환되게 재현하도록 돕습니다.
|
||||
스킬은 에이전트에게 조사 지침을 제공합니다. `rea setup`으로 REA의 MCP 서버를 등록하고 버전에 맞는 지침을 설치한 뒤 에이전트를 다시 시작하세요. [스킬만 설치하기](docs/installation.md#skill-only-installation)를 참고하세요.
|
||||
|
||||
</details>
|
||||
|
||||
## 개발
|
||||
<details>
|
||||
<summary><strong>REA는 어떤 코드를 반환하나요?</strong></summary>
|
||||
|
||||
개발 환경, 아키텍처, 테스트, 릴리스 지침은 [CONTRIBUTING.md](CONTRIBUTING.md)를 참고하세요.
|
||||
네이티브 분석은 의사 코드와 어셈블리를 반환합니다. JavaScript/Electron 분석은 모듈과 그 관계를 복원합니다. 에이전트는 이 결과를 사용해 구현을 작성하고 테스트합니다. [사례](#사례)에서 구체적인 예를 볼 수 있습니다.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA가 내 앱을 업로드하나요?</strong></summary>
|
||||
|
||||
REA는 대상을 로컬에서 분석합니다. 에이전트는 도구 결과를 받으며, 에이전트의 모델 제공자에는 별도의 데이터 정책이 있습니다.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>버그가 발생하면 어떻게 해야 하나요?</strong></summary>
|
||||
|
||||
먼저 업데이트하세요. 최근 릴리스에서 이미 수정되었을 수 있습니다.
|
||||
|
||||
npm으로 설치한 CLI에서는 다음 명령을 사용합니다.
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
`npx`로 에이전트를 설정했다면 다음 명령을 사용합니다.
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
에이전트를 사용 중이라면 [설정 갱신](#rea-업데이트)을 완료하고 다시 시작하세요. 같은 작업을 다시 시도하세요. 문제가 계속되면 REA 버전, 대상 유형, 재현 단계, 오류 출력을 포함해 [issue를 등록](https://github.com/morluto/rea/issues)하세요.
|
||||
|
||||
</details>
|
||||
|
||||
## 문서
|
||||
|
||||
웹사이트의 [실습 가이드](https://rea.tools/guides/)부터 읽어 보세요. 정확한 옵션, 사전 요구 사항, 결과 계약은 다음 문서를 참고하세요.
|
||||
|
||||
- [설치 및 설정](docs/installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거.
|
||||
- [준비 상태 확인 및 문제 해결](docs/installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단.
|
||||
- [CLI 및 Evidence](docs/cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태.
|
||||
- [MCP 계약](docs/mcp-contracts.md) 및 [에이전트 프롬프트](docs/mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사.
|
||||
- [도구 카탈로그](docs/mcp-contracts.md#generated-catalog): 빌드 시 생성되는 도구, 제공자, CLI 명령 목록.
|
||||
- [로드맵](docs/roadmap.md): 계획된 작업과 기능 진행 상황.
|
||||
|
||||
취약점은 [SECURITY.md](SECURITY.md)에 따라 보고하세요.
|
||||
|
||||
## 기여
|
||||
|
||||
REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/rea/issues)해 버그나 기능을 제안하거나, [pull request를 보내](https://github.com/morluto/rea/pulls) 코드와 문서를 개선할 수 있습니다.
|
||||
|
||||
개발 환경과 검사 항목은 [CONTRIBUTING.md](CONTRIBUTING.md), 검증 절차는 [테스트 가이드](docs/testing.md), 프로젝트 구조는 [아키텍처 지도](docs/architecture.mermaid)를 참고하세요.
|
||||
|
||||
## 프로젝트 링크
|
||||
|
||||
[웹사이트](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [보안](SECURITY.md)
|
||||
|
||||
## 스타 기록
|
||||
|
||||
🎉 **GitHub 스타 20,000개, 감사합니다!**
|
||||
|
||||
REA를 사용하고, 버그를 제보하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
|
||||
REA를 사용하고, 버그를 보고하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
@@ -329,6 +278,10 @@ REA를 사용하고, 버그를 제보하고, 빌드를 테스트하고, 수정
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 면책 조항
|
||||
|
||||
REA는 합법적인 리버스 엔지니어링 연구, 분석, 재구성을 위한 도구를 제공합니다. 필요한 권한을 얻고 관련 법률을 준수할 책임은 사용자에게 있습니다. 이 프로젝트는 불법적이거나 허가받지 않은 사용을 지지하지 않습니다.
|
||||
|
||||
## 라이선스
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
### Jeden serwer MCP do inżynierii wstecznej plików binarnych, aplikacji i zachowania w czasie wykonywania.
|
||||
|
||||
**Widzisz funkcję, która Ci się podoba? Dowiedz się, jak działa, aż do poziomu kodu binarnego.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Strona internetowa](https://rea.tools/) · [Przewodniki](https://rea.tools/guides/) · [Przykłady zastosowań](https://rea.tools/showcase/)**
|
||||
|
||||
[Szybki start](#szybki-start) · [Jak działa REA](#jak-działa-rea) · [Co możesz analizować](#co-możesz-analizować) · [Przykłady zastosowań](#przykłady-zastosowań) · [FAQ](#faq) · [Dokumentacja](#dokumentacja)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA uruchamia most analityczny w Hopperze podczas analizy natywnego pliku binarnego" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="Społeczność REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Dołącz do społeczności inżynierii wstecznej</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Pytania i odpowiedzi · Pokaż swój projekt</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Widzisz w aplikacji funkcję, którą chcesz mieć we własnym produkcie? Poproś swojego agenta, aby zbadał ją za pomocą REA. Może przeanalizować aplikację bez jej kodu źródłowego, wyjaśnić działanie funkcji, pokazać dowody i stworzyć jej wersję dla Twojego projektu.
|
||||
|
||||
REA łączy Twojego agenta z narzędziami do analizy natywnych plików binarnych, aplikacji JavaScript i Electron, zestawów .NET oraz stron internetowych. Z tych samych narzędzi możesz korzystać w terminalu. Analiza odbywa się lokalnie, a wyniki zawierają dowody i ograniczenia stojące za każdym wnioskiem.
|
||||
|
||||
Konfigurator rejestruje REA w Twoim agencie i instaluje pasujące instrukcje pracy. Analiza kodu natywnego może korzystać z istniejącej instalacji Hoppera lub Ghidry; konfigurator może też zainstalować Hoppera po uzyskaniu zgody. Statyczna analiza JavaScript nie wymaga żadnego z tych silników.
|
||||
|
||||
> **[Odwiedź stronę REA](https://rea.tools/)**, aby znaleźć instrukcje konfiguracji, ilustrowane przewodniki i rzeczywiste studia przypadków.
|
||||
|
||||
## Szybki start
|
||||
|
||||
### Skonfiguruj agenta
|
||||
|
||||
Po zainstalowaniu Node.js i npm uruchom:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Wybierz swoich agentów, sprawdź proponowane zmiany i zatwierdź je. Konfigurator dodaje serwer MCP REA i pasujące instrukcje pracy, tworząc kopie zapasowe istniejącej konfiguracji. Następnie uruchom agenta ponownie.
|
||||
|
||||
Konfigurator obsługuje Claude Code, Codex, Cursor, Gemini CLI, Grok Build i [innych agentów](docs/installation.md#supported-agents). Konfigurację dostawców analizy i ręczną rejestrację MCP opisuje przewodnik [instalacji i konfiguracji](docs/installation.md).
|
||||
|
||||
### Zapytaj agenta
|
||||
|
||||
```text
|
||||
Sprawdź, jak działa wyszukiwanie w aplikacji Notes, pokaż mi dowody i stwórz podobną funkcję dla mojego projektu.
|
||||
```
|
||||
|
||||
Zastąp Notes wybraną aplikacją i wskaż funkcję, którą chcesz zrozumieć.
|
||||
|
||||
### Korzystaj z terminala
|
||||
|
||||
Przeanalizuj rozpakowany katalog aplikacji JavaScript/Electron lub archiwum ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Wynik obejmuje moduły, importy, granice komponentów Electron oraz dowody. Zastąp ścieżkę ścieżką do swojego celu, na przykład `"D:/apps/example"` w systemie Windows.
|
||||
|
||||
Aby zainstalować polecenie `rea` do regularnego użytku:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Przed analizą kodu natywnego skonfiguruj dostawcę analizy. Polecenia analizy natywnej, wybór dostawcy, migawki i użycie w skryptach opisuje [przewodnik CLI i Evidence](docs/cli.md).
|
||||
|
||||
### Zaktualizuj REA
|
||||
|
||||
REA szybko się rozwija, a nowe wydania często zawierają poprawki błędów. Dbaj o aktualność swojej instalacji.
|
||||
|
||||
Dla CLI zainstalowanego przez npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Aby odświeżyć rejestracje agentów i umiejętność, uruchom polecenie konfiguracji wyświetlone po aktualizacji.
|
||||
|
||||
Jeśli używasz `npx`, zaktualizuj konfigurację agenta poleceniem:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Sprawdź zmiany konfiguracji i uruchom agenta ponownie. Do jednorazowych poleceń CLI używaj `npx rea-agents@latest`, a następnie nazwy polecenia.
|
||||
|
||||
## Jak działa REA
|
||||
|
||||
Twój agent wywołuje REA przez MCP, aby analizować cel i śledzić istotny kod. REA zwraca ustalenia wraz z dowodami. Agent wykorzystuje je do zadawania dalszych pytań, wyjaśniania zachowania lub pisania i testowania implementacji. Polecenia CLI korzystają z tych samych procedur.
|
||||
|
||||

|
||||
|
||||
[Otwórz ilustrację w pełnym rozmiarze](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Co możesz analizować
|
||||
|
||||
REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe narzędzia i obsługiwane platformy zależą od celu analizy:
|
||||
|
||||
| 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/guides/native/) |
|
||||
| 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](docs/binary-diagnostics.md) |
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym; [przewodnik EVM offline](docs/evm-bytecode.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; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](docs/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/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/guides/browser/) |
|
||||
| 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](docs/web-network-captures.md) |
|
||||
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](docs/managed-code-analysis.md) |
|
||||
| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemie Linux/macOS; [przewodnik Androida](docs/android-analysis.md) |
|
||||
| Oprogramowanie układowe | Regiony, wyniki wyodrębniania i przekazanie do analizy kodu natywnego | Binwalk / Unblob w systemie Linux; [przewodnik oprogramowania układowego](docs/firmware-analysis.md) |
|
||||
| Pakiety i zasoby | Spisy plików, skróty, pliki plist, struktura pakietów Apple i wyodrębnione zasoby | [Przewodnik artefaktów i JavaScript](docs/javascript-artifact-reconstruction.md), [aplikacje Apple](docs/apple-application-analysis.md) |
|
||||
| Zachowanie procesów | Wyjście terminala, interakcje, obserwacje zakończenia i systemu plików oraz porównania uruchomień | Linux/macOS z natywnym PTY; [przechwytywanie procesów](docs/process-capture.md) |
|
||||
|
||||
Statyczna analiza JavaScript i .NET odczytuje dostarczone pliki bez uruchamiania aplikacji. Przechwytywanie w czasie wykonywania uruchamia wybrany cel lub wchodzi z nim w interakcje z uprawnieniami Twojego użytkownika; skutki opisano w odpowiednich przewodnikach.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Obsługiwane formaty natywne i platformy zależą od dostawcy analizy. Zobacz [konfigurację Hoppera i Ghidry](docs/installation.md#hopper), [przewodnik IDA](docs/ida-provider.md) oraz [eksperymentalną obsługę Ghidry w systemie Windows](docs/windows-ghidra-p0.md). Ghidra obsługuje też [analizę 16-bitowego DOS-a](docs/ghidra-dos.md). Wybór dostawcy opisuje [przewodnik CLI](docs/cli.md#choose-a-provider). Sprawdź [dostępność w wydaniach](docs/installation.md#released-package-and-main) dla funkcji dodanych po najnowszym wydaniu npm.
|
||||
|
||||
## Przykłady zastosowań
|
||||
|
||||
### DX-Ball: odtworzenie obliczania panoramy dźwięku
|
||||
|
||||
Prześledź wywołanie dźwięku do funkcji pomocniczej przeliczającej pozycję na panoramę, sprawdź instrukcje i przekształć niepełny pseudokod w C. Odtworzona implementacja przechodzi 3 205 przypadków dla oryginalnego x86 i odtwarza wszystkie 63 bajty skompilowanej funkcji.
|
||||
|
||||
[Przeczytaj studium przypadku](https://rea.tools/showcase/dx-ball/) · [Repozytorium odtworzonej implementacji](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: śledzenie mostu schowka w Electron
|
||||
|
||||
Znajdź API schowka w procesie renderującym, prześledź je przez preload i IPC do procesu głównego, a następnie sprawdź format schowka wraz z danymi formatowania.
|
||||
|
||||
[Przeczytaj studium przypadku](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: odtworzenie obliczania pierścienia pocisków w DOS-ie
|
||||
|
||||
Sprawdź 16-bitowe instrukcje oryginalnej gry na PC-98, odtwórz obliczenia kątów stałych i skierowanych na cel oraz porównaj odtworzony C++ z wynikiem historycznego kompilatora.
|
||||
|
||||
[Przeczytaj studium przypadku](https://rea.tools/showcase/th04/) · [Repozytorium odtworzonej implementacji](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Jeśli użyłeś REA do czegoś ciekawego, chętnie to zobaczymy. Opisz swój przypadek w [zgłoszeniu](https://github.com/morluto/rea/issues) lub [pull requeście](https://github.com/morluto/rea/pulls), podając cel analizy, pytanie, sposób, w jaki REA pomogło, i swoje ustalenia.
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary><strong>Którzy agenci mogą korzystać z REA?</strong></summary>
|
||||
|
||||
Każdy agent obsługujący lokalne serwery MCP. Konfigurator konfiguruje [obsługiwanych agentów](docs/installation.md#supported-agents); inne klienty mogą użyć [ręcznej rejestracji MCP](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Czy potrzebuję Hoppera, Ghidry lub IDA?</strong></summary>
|
||||
|
||||
Dogłębna analiza kodu natywnego używa jednego z nich. Statyczna analiza JavaScript i .NET działa bez silnika analizy natywnej. Konfigurator może zainstalować Hoppera po uzyskaniu zgody; Ghidra i IDA korzystają z istniejących instalacji. Zobacz [konfigurację dostawców analizy](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Czy muszę najpierw uruchomić Hoppera?</strong></summary>
|
||||
|
||||
REA uruchamia Hoppera, gdy wymaga tego operacja. W systemie macOS przy pierwszym uruchomieniu okno dialogowe może poprosić o wybór trybu demonstracyjnego lub aktywację licencji. Zobacz [uruchamianie Hoppera i rozwiązywanie problemów](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Co daje zainstalowanie umiejętności z skills.sh?</strong></summary>
|
||||
|
||||
Umiejętność dostarcza agentowi instrukcje prowadzenia analizy. Użyj `rea setup`, aby zarejestrować serwer MCP REA i zainstalować pasujące instrukcje, a następnie uruchom agenta ponownie. Zobacz [instalację samej umiejętności](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Jaki kod zwraca REA?</strong></summary>
|
||||
|
||||
Analiza kodu natywnego zwraca pseudokod i kod asemblera. Analiza JavaScript/Electron odzyskuje moduły i ich powiązania. Agent wykorzystuje te ustalenia do napisania i przetestowania implementacji; [przykłady zastosowań](#przykłady-zastosowań) pokazują ten proces w praktyce.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Czy REA przesyła moją aplikację?</strong></summary>
|
||||
|
||||
REA analizuje cele lokalnie. Twój agent otrzymuje wyniki narzędzi, a dostawca jego modelu ma własną politykę dotyczącą danych.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Co zrobić, gdy napotkam błąd?</strong></summary>
|
||||
|
||||
Najpierw zaktualizuj REA; najnowsze wydanie mogło już naprawić ten problem.
|
||||
|
||||
Dla CLI zainstalowanego przez npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Dla konfiguracji agenta przez `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Jeśli używasz agenta, wykonaj [odświeżenie konfiguracji](#zaktualizuj-rea) i uruchom go ponownie. Ponów tę samą operację. Jeśli problem pozostaje, [otwórz zgłoszenie](https://github.com/morluto/rea/issues), podając wersję REA, typ celu analizy, kroki odtworzenia problemu i komunikaty błędu.
|
||||
|
||||
</details>
|
||||
|
||||
## Dokumentacja
|
||||
|
||||
Zacznij od [przewodników z przykładami](https://rea.tools/guides/) na stronie internetowej. Dokładne opcje, wymagania wstępne i kontrakty wyników opisano tutaj:
|
||||
|
||||
- [Instalacja i konfiguracja](docs/installation.md): rejestracja agentów, konfiguracja dostawców analizy, aktualizacje i odinstalowanie.
|
||||
- [Gotowość i rozwiązywanie problemów](docs/installation.md#check-readiness-for-your-task): diagnostyka jednego agenta lub silnika analizy.
|
||||
- [CLI i Evidence](docs/cli.md): polecenia, wybór dostawcy, migawki, import/eksport i kody zakończenia.
|
||||
- [Kontrakty MCP](docs/mcp-contracts.md) i [prompty dla agentów](docs/mcp-prompts.md): wyniki narzędzi, sesje i prowadzone analizy.
|
||||
- [Katalog narzędzi](docs/mcp-contracts.md#generated-catalog): generowany podczas kompilacji wykaz narzędzi, dostawców i poleceń CLI.
|
||||
- [Plan rozwoju](docs/roadmap.md): planowane prace i śledzenie funkcjonalności.
|
||||
|
||||
Zgłaszaj podatności zgodnie z [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Współtworzenie
|
||||
|
||||
Chętnie przyjmiemy Twoją pomoc w rozwoju REA! [Otwórz zgłoszenie](https://github.com/morluto/rea/issues), aby poinformować o błędzie lub zaproponować funkcję, albo [wyślij pull request](https://github.com/morluto/rea/pulls) z ulepszeniami kodu lub dokumentacji.
|
||||
|
||||
Zobacz [CONTRIBUTING.md](CONTRIBUTING.md), aby poznać konfigurację środowiska programistycznego i kontrole, [testowanie](docs/testing.md), aby poznać ścieżki weryfikacji, oraz [mapę architektury](docs/architecture.mermaid), aby poznać strukturę projektu.
|
||||
|
||||
## Linki projektu
|
||||
|
||||
[Strona internetowa](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Zgłoszenia](https://github.com/morluto/rea/issues) · [Bezpieczeństwo](SECURITY.md)
|
||||
|
||||
## Historia gwiazdek
|
||||
|
||||
🎉 **20 000 gwiazdek na GitHubie — dziękujemy!**
|
||||
|
||||
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, testują kompilacje i przesyłają poprawki.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Historia gwiazdek REA na GitHubie" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Zastrzeżenie
|
||||
|
||||
REA dostarcza narzędzia do zgodnych z prawem badań z zakresu inżynierii wstecznej, analizy i odtwarzania. Odpowiadasz za uzyskanie wymaganych zezwoleń i przestrzeganie obowiązującego prawa. Projekt nie popiera działań nielegalnych ani nieautoryzowanych.
|
||||
|
||||
## Licencja
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Faça engenharia reversa de qualquer coisa
|
||||
|
||||
### Um único MCP para engenharia reversa de binários, aplicativos e comportamento em execução.
|
||||
|
||||
**Viu uma funcionalidade de que gostou? Entenda como ela funciona, até o nível binário.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Site](https://rea.tools/) · [Guias](https://rea.tools/guides/) · [Casos de uso](https://rea.tools/showcase/)**
|
||||
|
||||
[Início rápido](#início-rápido) · [Como o REA funciona](#como-o-rea-funciona) · [O que você pode analisar](#o-que-você-pode-analisar) · [Casos de uso](#casos-de-uso) · [Perguntas frequentes](#perguntas-frequentes) · [Documentação](#documentação)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA inicia sua ponte de análise dentro do Hopper enquanto inspeciona um binário nativo" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="Comunidade REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Participe da comunidade de engenharia reversa</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Perguntas e respostas · Compartilhe seus resultados</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Viu uma funcionalidade em um aplicativo que gostaria de ter no seu próprio produto? Peça ao seu agente para investigá-la com o REA. Ele pode inspecionar o aplicativo sem o código-fonte, explicar como a funcionalidade funciona, apresentar as evidências e criar uma versão para o seu projeto.
|
||||
|
||||
O REA conecta seu agente a ferramentas para inspecionar binários nativos, aplicativos JavaScript e Electron, assemblies .NET e sites. Você também pode usar as mesmas ferramentas pelo terminal. A análise acontece localmente, e os resultados incluem as evidências e limitações por trás de cada conclusão.
|
||||
|
||||
A configuração registra o REA no seu agente e instala as instruções de fluxo de trabalho correspondentes. A análise nativa pode usar uma instalação existente do Hopper ou do Ghidra; a configuração também pode instalar o Hopper opcionalmente, mediante aprovação. A análise estática de JavaScript não precisa de nenhum dos dois motores.
|
||||
|
||||
> **[Visite o site do REA](https://rea.tools/)** para instruções de configuração, guias ilustrados e estudos de caso reais.
|
||||
|
||||
## Início rápido
|
||||
|
||||
### Configure seu agente
|
||||
|
||||
Com Node.js e npm instalados, execute:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Escolha seus agentes, revise as alterações propostas e aprove-as. A configuração adiciona o servidor MCP do REA e as instruções de fluxo de trabalho correspondentes, com backups das configurações existentes. Reinicie seu agente depois.
|
||||
|
||||
A configuração oferece suporte ao Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [outros agentes](docs/installation.md#supported-agents). Consulte [instalação e configuração](docs/installation.md) para configurar provedores e registrar o MCP manualmente.
|
||||
|
||||
### Peça ao seu agente
|
||||
|
||||
```text
|
||||
Investigue como a busca funciona no aplicativo Notes, mostre as evidências e crie uma funcionalidade semelhante para o meu projeto.
|
||||
```
|
||||
|
||||
Substitua Notes pelo aplicativo alvo e indique a funcionalidade que deseja entender.
|
||||
|
||||
### Use o terminal
|
||||
|
||||
Inspecione um diretório extraído de um aplicativo JavaScript/Electron ou um arquivo ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
O resultado inclui módulos, importações, limites do Electron e suas evidências. Substitua o caminho pelo do seu alvo, como `"D:/apps/example"` no Windows.
|
||||
|
||||
Para instalar o comando `rea` e usá-lo regularmente:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Para análise nativa, configure primeiro um provedor. Consulte o [guia de CLI e Evidence](docs/cli.md) para comandos nativos, seleção de provedores, snapshots e uso em scripts.
|
||||
|
||||
### Atualize o REA
|
||||
|
||||
O REA muda rapidamente, e novas versões incluem correções frequentes de bugs. Mantenha sua instalação atualizada.
|
||||
|
||||
Para uma CLI instalada pelo npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Para atualizar os registros dos agentes e a skill, execute o comando de configuração exibido pela atualização.
|
||||
|
||||
Se você usa `npx`, atualize a configuração do agente com:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Revise as alterações de configuração e reinicie seu agente. Para comandos avulsos da CLI, use `npx rea-agents@latest` seguido do comando.
|
||||
|
||||
## Como o REA funciona
|
||||
|
||||
Seu agente chama o REA por MCP para inspecionar o alvo e rastrear o código relevante. O REA retorna os resultados com suas evidências. O agente usa esses resultados para fazer novas perguntas, explicar o comportamento ou escrever e testar uma implementação. Os comandos da CLI usam os mesmos fluxos de trabalho.
|
||||
|
||||

|
||||
|
||||
[Abra a figura em tamanho completo](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## O que você pode analisar
|
||||
|
||||
O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As ferramentas adicionais e os sistemas anfitriões compatíveis dependem do alvo:
|
||||
|
||||
| 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/guides/native/) |
|
||||
| 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](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal; [guia de EVM sem execução](docs/evm-bytecode.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador; GDB/pwndbg opcionais; [falhas registradas](docs/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/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/guides/browser/) |
|
||||
| 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](docs/web-network-captures.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](docs/managed-code-analysis.md) |
|
||||
| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS; [guia de Android](docs/android-analysis.md) |
|
||||
| Firmware | Regiões, resultados de extração e encaminhamentos à análise nativa | Binwalk / Unblob no Linux; [guia de firmware](docs/firmware-analysis.md) |
|
||||
| Pacotes e recursos | Inventários de arquivos, digests, plists, estrutura de bundles Apple e recursos extraídos | [Guia de artefatos e JavaScript](docs/javascript-artifact-reconstruction.md), [aplicativos Apple](docs/apple-application-analysis.md) |
|
||||
| Comportamento de processos | Saída do terminal, interações, observações de encerramento e do sistema de arquivos, e comparações entre execuções | Linux/macOS com PTY nativo; [captura de processos](docs/process-capture.md) |
|
||||
|
||||
A inspeção estática de JavaScript e .NET lê os arquivos fornecidos sem executar o aplicativo. A captura em execução inicia o alvo selecionado ou interage com ele usando as permissões do seu usuário; cada guia descreve seus efeitos.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Os formatos nativos e os sistemas anfitriões compatíveis variam por provedor. Consulte [configuração do Hopper e Ghidra](docs/installation.md#hopper), o [guia do IDA](docs/ida-provider.md) e o [suporte experimental ao Ghidra no Windows](docs/windows-ghidra-p0.md). O Ghidra também oferece [análise de DOS de 16 bits](docs/ghidra-dos.md). Para selecionar um provedor, consulte o [guia da CLI](docs/cli.md#choose-a-provider). Verifique a [disponibilidade nas versões](docs/installation.md#released-package-and-main) das funcionalidades adicionadas após a versão mais recente no npm.
|
||||
|
||||
## Casos de uso
|
||||
|
||||
### DX-Ball: reconstruir um cálculo de panorâmica de áudio
|
||||
|
||||
Siga uma chamada de áudio até a função auxiliar que converte posição em panorâmica, inspecione as instruções e transforme pseudocódigo incompleto em C. A reconstrução passa em 3.205 casos com o x86 original e reproduz todos os 63 bytes da função compilada.
|
||||
|
||||
[Leia o estudo de caso](https://rea.tools/showcase/dx-ball/) · [Repositório da reconstrução](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: rastrear a ponte da área de transferência do Electron
|
||||
|
||||
Encontre a API da área de transferência do renderizador, siga-a por preload e IPC até o processo principal e inspecione o formato da área de transferência com seus dados de formatação.
|
||||
|
||||
[Leia o estudo de caso](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: recuperar o cálculo de um anel de projéteis no DOS
|
||||
|
||||
Inspecione as instruções de 16 bits do jogo original para PC-98, recupere os cálculos de ângulos fixos e direcionados e compare o C++ reconstruído com a saída do compilador da época.
|
||||
|
||||
[Leia o estudo de caso](https://rea.tools/showcase/th04/) · [Repositório da reconstrução](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Se você usou o REA em algo interessante, gostaríamos de conhecer. Compartilhe seu caso em uma [issue](https://github.com/morluto/rea/issues) ou um [pull request](https://github.com/morluto/rea/pulls), incluindo o alvo, sua pergunta, como o REA ajudou e o que descobriu.
|
||||
|
||||
## Perguntas frequentes
|
||||
|
||||
<details>
|
||||
<summary><strong>Quais agentes podem usar o REA?</strong></summary>
|
||||
|
||||
Qualquer agente que ofereça suporte a servidores MCP locais. A configuração prepara os [agentes compatíveis](docs/installation.md#supported-agents); outros clientes podem usar o [registro manual do MCP](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Preciso do Hopper, Ghidra ou IDA?</strong></summary>
|
||||
|
||||
A análise nativa aprofundada usa um desses motores. A inspeção estática de JavaScript e .NET funciona sem um motor de análise nativa. A configuração pode instalar o Hopper após aprovação; Ghidra e IDA usam as instalações existentes. Consulte [configuração dos provedores](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Preciso iniciar o Hopper antes?</strong></summary>
|
||||
|
||||
O REA inicia o Hopper quando uma operação precisa dele. No macOS, um diálogo na primeira execução pode pedir que você escolha o modo de demonstração ou ative a licença. Consulte [inicialização do Hopper e solução de problemas](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>O que faz a instalação da skill pelo skills.sh?</strong></summary>
|
||||
|
||||
A skill fornece instruções de investigação ao seu agente. Use `rea setup` para registrar o servidor MCP do REA e instalar as instruções correspondentes, depois reinicie seu agente. Consulte [instalação apenas da skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Que código o REA retorna?</strong></summary>
|
||||
|
||||
A análise nativa retorna pseudocódigo e assembly. A análise de JavaScript/Electron recupera módulos e suas relações. Seu agente usa esses resultados para escrever e testar uma implementação; os [casos de uso](#casos-de-uso) apresentam exemplos completos.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>O REA envia meu aplicativo para algum servidor?</strong></summary>
|
||||
|
||||
O REA analisa os alvos localmente. Seu agente recebe os resultados das ferramentas, e o provedor do modelo tem sua própria política de dados.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>O que devo fazer se encontrar um bug?</strong></summary>
|
||||
|
||||
Atualize primeiro; uma versão recente pode já ter corrigido o problema.
|
||||
|
||||
Para uma CLI instalada pelo npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Para configurar o agente por `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Se você usa um agente, conclua a [atualização da configuração](#atualize-o-rea) e reinicie-o. Tente executar a mesma tarefa novamente. Se o problema persistir, [abra uma issue](https://github.com/morluto/rea/issues) com sua versão do REA, o tipo de alvo, os passos para reproduzir e a saída do erro.
|
||||
|
||||
</details>
|
||||
|
||||
## Documentação
|
||||
|
||||
Comece pelos [guias práticos](https://rea.tools/guides/) do site. Para opções exatas, pré-requisitos e contratos dos resultados:
|
||||
|
||||
- [Instalação e configuração](docs/installation.md): registro de agentes, configuração de provedores, atualizações e desinstalação.
|
||||
- [Prontidão e solução de problemas](docs/installation.md#check-readiness-for-your-task): diagnóstico de um agente ou motor de análise específico.
|
||||
- [CLI e Evidence](docs/cli.md): comandos, seleção de provedores, snapshots, importação/exportação e status de saída.
|
||||
- [Contratos MCP](docs/mcp-contracts.md) e [prompts para agentes](docs/mcp-prompts.md): resultados das ferramentas, sessões e investigações guiadas.
|
||||
- [Catálogo de ferramentas](docs/mcp-contracts.md#generated-catalog): inventário de ferramentas, provedores e comandos da CLI gerado durante o build.
|
||||
- [Roadmap](docs/roadmap.md): trabalho planejado e acompanhamento das capacidades.
|
||||
|
||||
Relate vulnerabilidades conforme [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Contribuir
|
||||
|
||||
Sua ajuda com o REA é bem-vinda! [Abra uma issue](https://github.com/morluto/rea/issues) para relatar um bug ou sugerir uma funcionalidade, ou [envie um pull request](https://github.com/morluto/rea/pulls) para melhorar o código ou a documentação.
|
||||
|
||||
Consulte [CONTRIBUTING.md](CONTRIBUTING.md) para configurar o ambiente de desenvolvimento e as verificações, o [guia de testes](docs/testing.md) para os fluxos de verificação e o [mapa da arquitetura](docs/architecture.mermaid) para a estrutura do projeto.
|
||||
|
||||
## Links do projeto
|
||||
|
||||
[Site](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Segurança](SECURITY.md)
|
||||
|
||||
## Histórico de estrelas
|
||||
|
||||
🎉 **20.000 estrelas no GitHub — muito obrigado!**
|
||||
|
||||
Obrigado a todos que usam o REA, relatam bugs, testam builds e contribuem com correções.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Histórico de estrelas do REA no GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Aviso legal
|
||||
|
||||
O REA fornece ferramentas para pesquisa, análise e reconstrução por engenharia reversa dentro da lei. Você é responsável por obter as autorizações necessárias e cumprir as leis aplicáveis. O projeto não apoia usos ilegais ou não autorizados.
|
||||
|
||||
## Licença
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Реверс-инжиниринг чего угодно
|
||||
|
||||
### Один MCP для реверс-инжиниринга бинарных файлов, приложений и поведения во время выполнения.
|
||||
|
||||
**Нашли интересную функцию? Разберитесь, как она работает, вплоть до двоичного кода.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Сайт](https://rea.tools/) · [Руководства](https://rea.tools/guides/) · [Примеры](https://rea.tools/showcase/)**
|
||||
|
||||
[Быстрый старт](#быстрый-старт) · [Как работает REA](#как-работает-rea) · [Что можно анализировать](#что-можно-анализировать) · [Примеры](#примеры) · [Частые вопросы](#частые-вопросы) · [Документация](#документация)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA запускает мост анализа внутри Hopper при исследовании нативного бинарного файла" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="Сообщество REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Присоединяйтесь к сообществу реверс-инжиниринга</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Вопросы и ответы · Обмен результатами</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Увидели в приложении функцию, которую хотите добавить в свой продукт? Попросите агента исследовать её с помощью REA. Он может изучить приложение без исходного кода, объяснить работу функции, показать подтверждающие данные и создать подходящую реализацию для вашего проекта.
|
||||
|
||||
REA подключает вашего агента к инструментам для исследования нативных бинарных файлов, приложений JavaScript и Electron, сборок .NET и сайтов. Те же инструменты доступны в терминале. Анализ выполняется локально, а результаты содержат данные и ограничения, на которых основан каждый вывод.
|
||||
|
||||
Настройка регистрирует REA в вашем агенте и устанавливает соответствующие инструкции по работе. Для нативного анализа можно использовать уже установленный Hopper или Ghidra; настройка также может установить Hopper с вашего согласия. Для статического анализа JavaScript не нужен ни один из этих движков.
|
||||
|
||||
> **[Посетите сайт REA](https://rea.tools/)**: там есть инструкции по настройке, иллюстрированные руководства и реальные исследования.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Настройте агента
|
||||
|
||||
Если Node.js и npm установлены, выполните:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Выберите агентов, проверьте предлагаемые изменения и подтвердите их. Настройка добавляет MCP-сервер REA и соответствующие рабочие инструкции, сохраняя резервные копии существующих настроек. После этого перезапустите агента.
|
||||
|
||||
Настройка поддерживает Claude Code, Codex, Cursor, Gemini CLI, Grok Build и [других агентов](docs/installation.md#supported-agents). Настройка провайдеров и ручная регистрация MCP описаны в разделе [установка и настройка](docs/installation.md).
|
||||
|
||||
### Задайте вопрос агенту
|
||||
|
||||
```text
|
||||
Исследуй, как работает поиск в приложении Notes, покажи подтверждающие данные и создай похожую функцию для моего проекта.
|
||||
```
|
||||
|
||||
Замените Notes нужным приложением и укажите функцию, которую хотите понять.
|
||||
|
||||
### Используйте терминал
|
||||
|
||||
Исследуйте распакованный каталог приложения JavaScript/Electron или файл ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Результат включает модули, импорты, границы Electron и подтверждающие данные. Замените путь своим, например `"D:/apps/example"` в Windows.
|
||||
|
||||
Для регулярного использования установите команду `rea`:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Для нативного анализа сначала настройте провайдер. Нативные команды, выбор провайдера, снимки и работу со скриптами описывает [руководство по CLI и Evidence](docs/cli.md).
|
||||
|
||||
### Обновите REA
|
||||
|
||||
REA быстро развивается, и новые версии часто содержат исправления ошибок. Регулярно обновляйте установленную версию.
|
||||
|
||||
Для CLI, установленного через npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Чтобы обновить регистрации агентов и skill, выполните команду настройки, которую выведет обновление.
|
||||
|
||||
Если используете `npx`, обновите настройку агента командой:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Проверьте изменения настроек и перезапустите агента. Для разовых команд CLI используйте `npx rea-agents@latest`, а затем нужную команду.
|
||||
|
||||
## Как работает REA
|
||||
|
||||
Агент вызывает REA через MCP, чтобы исследовать цель и проследить связанный код. REA возвращает результаты вместе с подтверждающими данными. Агент использует их для уточняющих вопросов, объяснения поведения или написания и тестирования реализации. Команды CLI используют те же рабочие процессы.
|
||||
|
||||

|
||||
|
||||
[Открыть рисунок в полном размере](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Что можно анализировать
|
||||
|
||||
REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а также npm. Дополнительные инструменты и поддерживаемые системы зависят от цели:
|
||||
|
||||
| Цель | Что возвращает REA | Требования и руководство |
|
||||
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/guides/native/) |
|
||||
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](docs/binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление; [руководство по EVM без запуска](docs/evm-bytecode.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной; необязательные GDB/pwndbg; [записанные сбои](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/guides/javascript/) |
|
||||
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/guides/browser/) |
|
||||
| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](docs/web-network-captures.md) |
|
||||
| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](docs/managed-code-analysis.md) |
|
||||
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS; [руководство по Android](docs/android-analysis.md) |
|
||||
| Прошивки | Области, результаты извлечения и передача в нативный анализ | Binwalk / Unblob в Linux; [руководство по прошивкам](docs/firmware-analysis.md) |
|
||||
| Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](docs/javascript-artifact-reconstruction.md), [приложения Apple](docs/apple-application-analysis.md) |
|
||||
| Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](docs/process-capture.md) |
|
||||
|
||||
Статическое исследование JavaScript и .NET читает предоставленные файлы, не запуская приложение. Захват во время выполнения запускает выбранную цель или взаимодействует с ней с правами вашего пользователя; соответствующие руководства описывают последствия.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Нативные форматы и поддерживаемые системы различаются у разных провайдеров. См. [настройку Hopper и Ghidra](docs/installation.md#hopper), [руководство по IDA](docs/ida-provider.md) и [экспериментальную поддержку Ghidra в Windows](docs/windows-ghidra-p0.md). Ghidra также поддерживает [анализ 16-битного DOS](docs/ghidra-dos.md). Выбор провайдера описан в [руководстве по CLI](docs/cli.md#choose-a-provider). Для функций, добавленных после последней версии в npm, проверьте [доступность в релизах](docs/installation.md#released-package-and-main).
|
||||
|
||||
## Примеры
|
||||
|
||||
### DX-Ball: восстановление расчёта звуковой панорамы
|
||||
|
||||
Проследите звуковой вызов до функции, преобразующей координату в значение панорамы, изучите инструкции и превратите неполный псевдокод в C. Восстановленная реализация проходит 3 205 тестов с исходным x86-кодом и воспроизводит все 63 байта скомпилированной функции.
|
||||
|
||||
[Прочитать исследование](https://rea.tools/showcase/dx-ball/) · [Репозиторий реконструкции](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: исследование моста буфера обмена Electron
|
||||
|
||||
Найдите API буфера обмена в процессе рендеринга, проследите его через preload и IPC до основного процесса и изучите формат буфера обмена с форматированием.
|
||||
|
||||
[Прочитать исследование](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: восстановление расчёта кольца пуль в DOS
|
||||
|
||||
Изучите 16-битные инструкции оригинальной игры для PC-98, восстановите расчёты фиксированных и прицельных углов и сравните восстановленный C++ с выводом компилятора того времени.
|
||||
|
||||
[Прочитать исследование](https://rea.tools/showcase/th04/) · [Репозиторий реконструкции](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Если вы исследовали с REA что-то интересное, поделитесь результатом. Создайте [issue](https://github.com/morluto/rea/issues) или [pull request](https://github.com/morluto/rea/pulls) и укажите цель, ваш вопрос, как помог REA и что удалось узнать.
|
||||
|
||||
## Частые вопросы
|
||||
|
||||
<details>
|
||||
<summary><strong>Какие агенты могут использовать REA?</strong></summary>
|
||||
|
||||
Любой агент с поддержкой локальных MCP-серверов. Настройка конфигурирует [поддерживаемых агентов](docs/installation.md#supported-agents); другие клиенты могут использовать [ручную регистрацию MCP](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Нужны ли мне Hopper, Ghidra или IDA?</strong></summary>
|
||||
|
||||
Углублённый нативный анализ использует один из этих инструментов. Статическое исследование JavaScript и .NET работает без движка нативного анализа. Настройка может установить Hopper после подтверждения; Ghidra и IDA используют существующие установки. См. [настройку провайдеров](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Нужно ли сначала запускать Hopper?</strong></summary>
|
||||
|
||||
REA запускает Hopper, когда он нужен для операции. В macOS при первом запуске может появиться диалог выбора деморежима или активации лицензии. См. [запуск Hopper и устранение неполадок](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Что даёт установка skill с skills.sh?</strong></summary>
|
||||
|
||||
Skill предоставляет агенту инструкции по исследованию. Используйте `rea setup`, чтобы зарегистрировать MCP-сервер REA и установить соответствующие инструкции, затем перезапустите агента. См. [установку только skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Какой код возвращает REA?</strong></summary>
|
||||
|
||||
Нативный анализ возвращает псевдокод и ассемблер. Анализ JavaScript/Electron восстанавливает модули и их связи. Агент использует эти результаты, чтобы написать и протестировать реализацию; в [примерах](#примеры) показаны практические исследования.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA загружает моё приложение на сервер?</strong></summary>
|
||||
|
||||
REA анализирует цели локально. Ваш агент получает результаты инструментов, а у его поставщика моделей есть собственная политика обработки данных.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Что делать, если я столкнулся с ошибкой?</strong></summary>
|
||||
|
||||
Сначала обновитесь: недавний релиз мог уже исправить ошибку.
|
||||
|
||||
Для CLI, установленного через npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Для настройки агента через `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Если используете агента, завершите [обновление настройки](#обновите-rea) и перезапустите его. Повторите ту же задачу. Если проблема сохраняется, [откройте issue](https://github.com/morluto/rea/issues), указав версию REA, тип цели, шаги воспроизведения и вывод ошибки.
|
||||
|
||||
</details>
|
||||
|
||||
## Документация
|
||||
|
||||
Начните с [практических руководств](https://rea.tools/guides/) на сайте. Точные параметры, требования и контракты результатов описаны здесь:
|
||||
|
||||
- [Установка и настройка](docs/installation.md): регистрация агентов, настройка провайдеров, обновление и удаление.
|
||||
- [Готовность и устранение неполадок](docs/installation.md#check-readiness-for-your-task): диагностика конкретного агента или движка анализа.
|
||||
- [CLI и Evidence](docs/cli.md): команды, выбор провайдера, снимки, импорт/экспорт и коды завершения.
|
||||
- [Контракты MCP](docs/mcp-contracts.md) и [промпты агентов](docs/mcp-prompts.md): результаты инструментов, сеансы и исследования с подсказками.
|
||||
- [Каталог инструментов](docs/mcp-contracts.md#generated-catalog): создаваемый при сборке перечень инструментов, провайдеров и команд CLI.
|
||||
- [План развития](docs/roadmap.md): запланированные работы и отслеживание возможностей.
|
||||
|
||||
Сообщайте об уязвимостях согласно [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Участие в проекте
|
||||
|
||||
Будем рады вашей помощи с REA! [Откройте issue](https://github.com/morluto/rea/issues), чтобы сообщить об ошибке или предложить функцию, либо [отправьте pull request](https://github.com/morluto/rea/pulls) с улучшениями кода или документации.
|
||||
|
||||
Настройка среды разработки и проверки описаны в [CONTRIBUTING.md](CONTRIBUTING.md), направления проверки — в [руководстве по тестированию](docs/testing.md), а структура проекта — на [схеме архитектуры](docs/architecture.mermaid).
|
||||
|
||||
## Ссылки проекта
|
||||
|
||||
[Сайт](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Безопасность](SECURITY.md)
|
||||
|
||||
## История звёзд
|
||||
|
||||
🎉 **20 000 звёзд на GitHub — спасибо!**
|
||||
|
||||
Спасибо всем, кто использует REA, сообщает об ошибках, тестирует сборки и помогает с исправлениями.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="История звёзд REA на GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Отказ от ответственности
|
||||
|
||||
REA предоставляет инструменты для законных исследований, анализа и реконструкции с помощью реверс-инжиниринга. Вы отвечаете за получение необходимых разрешений и соблюдение применимых законов. Проект не поддерживает незаконное или несанкционированное использование.
|
||||
|
||||
## Лицензия
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
### MCP เดียวสำหรับวิศวกรรมย้อนกลับ ทั้งไบนารี แอปพลิเคชัน และพฤติกรรมขณะทำงาน
|
||||
|
||||
**เห็นฟีเจอร์ที่ชอบแล้วอยากรู้ว่าทำงานอย่างไร? เข้าใจได้ลึกถึงระดับไบนารี**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[เว็บไซต์](https://rea.tools/) · [คู่มือ](https://rea.tools/guides/) · [ตัวอย่างการใช้งาน](https://rea.tools/showcase/)**
|
||||
|
||||
[เริ่มต้นอย่างรวดเร็ว](#เริ่มต้นอย่างรวดเร็ว) · [REA ทำงานอย่างไร](#rea-ทำงานอย่างไร) · [สิ่งที่วิเคราะห์ได้](#สิ่งที่วิเคราะห์ได้) · [ตัวอย่างการใช้งาน](#ตัวอย่างการใช้งาน) · [คำถามที่พบบ่อย](#คำถามที่พบบ่อย) · [เอกสาร](#เอกสาร)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA เริ่มบริดจ์การวิเคราะห์ภายใน Hopper ขณะตรวจสอบไบนารีเนทีฟ" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="ชุมชน REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>เข้าร่วมชุมชนวิศวกรรมย้อนกลับ</strong>
|
||||
</a><br />
|
||||
<sub>Discord · ถามตอบ · แบ่งปันผลงาน</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
เห็นฟีเจอร์ในแอปที่อยากนำมาใช้ในผลิตภัณฑ์ของคุณเองหรือไม่? ขอให้เอเจนต์ของคุณตรวจสอบด้วย REA เอเจนต์สามารถวิเคราะห์แอปโดยไม่ต้องมีซอร์สโค้ด อธิบายการทำงานของฟีเจอร์ แสดงหลักฐาน และสร้างเวอร์ชันสำหรับโปรเจกต์ของคุณได้
|
||||
|
||||
REA เชื่อมเอเจนต์ของคุณกับเครื่องมือสำหรับตรวจสอบไบนารีเนทีฟ แอป JavaScript และ Electron แอสเซมบลี .NET และเว็บไซต์ คุณใช้เครื่องมือเดียวกันจากเทอร์มินัลได้ การวิเคราะห์ทำงานบนเครื่องของคุณ และผลลัพธ์มีทั้งหลักฐานและข้อจำกัดที่ประกอบแต่ละข้อสรุป
|
||||
|
||||
ขั้นตอนตั้งค่าจะลงทะเบียน REA กับเอเจนต์และติดตั้งคำแนะนำการทำงานที่ตรงกัน การวิเคราะห์เนทีฟใช้ Hopper หรือ Ghidra ที่ติดตั้งอยู่แล้วได้ และขั้นตอนตั้งค่าสามารถติดตั้ง Hopper เพิ่มเติมเมื่อได้รับอนุมัติ การวิเคราะห์ JavaScript แบบสถิตไม่ต้องใช้เอนจินทั้งสองนี้
|
||||
|
||||
> **[เยี่ยมชมเว็บไซต์ REA](https://rea.tools/)** เพื่อดูคำแนะนำการตั้งค่า คู่มือพร้อมภาพประกอบ และกรณีศึกษาจริง
|
||||
|
||||
## เริ่มต้นอย่างรวดเร็ว
|
||||
|
||||
### ตั้งค่าเอเจนต์
|
||||
|
||||
เมื่อติดตั้ง Node.js และ npm แล้ว ให้เรียกใช้:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
เลือกเอเจนต์ ตรวจสอบการเปลี่ยนแปลงที่เสนอ แล้วอนุมัติ ขั้นตอนตั้งค่าจะเพิ่มเซิร์ฟเวอร์ MCP ของ REA และคำแนะนำการทำงานที่ตรงกัน พร้อมสำรองการตั้งค่าที่มีอยู่ จากนั้นเริ่มเอเจนต์ใหม่
|
||||
|
||||
ขั้นตอนตั้งค่ารองรับ Claude Code, Codex, Cursor, Gemini CLI, Grok Build และ[เอเจนต์อื่น ๆ](docs/installation.md#supported-agents) ดู[การติดตั้งและตั้งค่า](docs/installation.md) สำหรับการตั้งค่าผู้ให้บริการวิเคราะห์และการลงทะเบียน MCP ด้วยตนเอง
|
||||
|
||||
### ถามเอเจนต์
|
||||
|
||||
```text
|
||||
ตรวจสอบว่าการค้นหาในแอป Notes ทำงานอย่างไร แสดงหลักฐานให้ฉันดู และสร้างฟีเจอร์ที่คล้ายกันสำหรับโปรเจกต์ของฉัน
|
||||
```
|
||||
|
||||
แทนที่ Notes ด้วยแอปเป้าหมายและฟีเจอร์ที่คุณต้องการเข้าใจ
|
||||
|
||||
### ใช้เทอร์มินัล
|
||||
|
||||
ตรวจสอบไดเรกทอรีแอป JavaScript/Electron ที่แตกไฟล์แล้ว หรือไฟล์ ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
ผลลัพธ์ประกอบด้วยโมดูล การนำเข้า ขอบเขตของ Electron และหลักฐาน แทนที่พาธด้วยเป้าหมายของคุณ เช่น `"D:/apps/example"` บน Windows
|
||||
|
||||
หากต้องการติดตั้งคำสั่ง `rea` สำหรับใช้งานเป็นประจำ:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
สำหรับการวิเคราะห์เนทีฟ ให้ตั้งค่าผู้ให้บริการวิเคราะห์ก่อน ดู[คู่มือ CLI และ Evidence](docs/cli.md) สำหรับคำสั่งเนทีฟ การเลือกผู้ให้บริการ สแนปช็อต และการใช้งานในสคริปต์
|
||||
|
||||
### อัปเดต REA
|
||||
|
||||
REA เปลี่ยนแปลงอย่างรวดเร็ว และรุ่นใหม่มีการแก้ไขบั๊กอยู่บ่อยครั้ง โปรดอัปเดตการติดตั้งของคุณให้เป็นปัจจุบัน
|
||||
|
||||
สำหรับ CLI ที่ติดตั้งผ่าน npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
หากต้องการปรับปรุงการลงทะเบียนเอเจนต์และสกิลให้เป็นปัจจุบัน ให้เรียกใช้คำสั่งตั้งค่าที่แสดงหลังการอัปเดต
|
||||
|
||||
หากใช้ `npx` ให้อัปเดตการตั้งค่าเอเจนต์ด้วย:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
ตรวจสอบการเปลี่ยนแปลงการตั้งค่าและเริ่มเอเจนต์ใหม่ สำหรับคำสั่ง CLI ที่ใช้ครั้งเดียว ให้ใช้ `npx rea-agents@latest` ตามด้วยคำสั่งที่ต้องการ
|
||||
|
||||
## REA ทำงานอย่างไร
|
||||
|
||||
เอเจนต์เรียก REA ผ่าน MCP เพื่อตรวจสอบเป้าหมายและติดตามโค้ดที่เกี่ยวข้อง REA ส่งข้อค้นพบกลับมาพร้อมหลักฐาน เอเจนต์ใช้ข้อมูลนี้เพื่อถามคำถามต่อ อธิบายพฤติกรรม หรือเขียนและทดสอบการนำไปใช้งาน คำสั่ง CLI ใช้กระบวนการเดียวกัน
|
||||
|
||||

|
||||
|
||||
[เปิดภาพขนาดเต็ม](website/public/assets/figures/rea-investigation-flow.svg)
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## สิ่งที่วิเคราะห์ได้
|
||||
|
||||
REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26+ และ npm เครื่องมือเพิ่มเติมและแพลตฟอร์มที่รองรับขึ้นอยู่กับเป้าหมาย:
|
||||
|
||||
| เป้าหมาย | สิ่งที่ REA ส่งกลับ | ข้อกำหนดและคู่มือ |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/guides/native/) |
|
||||
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](docs/binary-diagnostics.md) |
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหก; [คู่มือ EVM แบบออฟไลน์](docs/evm-bytecode.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียม; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/guides/javascript/) |
|
||||
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/guides/browser/) |
|
||||
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](docs/web-network-captures.md) |
|
||||
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](docs/managed-code-analysis.md) |
|
||||
| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS; [คู่มือ Android](docs/android-analysis.md) |
|
||||
| เฟิร์มแวร์ | พื้นที่ข้อมูล ผลการแตกไฟล์ และการส่งต่อไปวิเคราะห์เนทีฟ | Binwalk / Unblob บน Linux; [คู่มือเฟิร์มแวร์](docs/firmware-analysis.md) |
|
||||
| แพ็กเกจและทรัพยากร | รายการไฟล์ ค่าแฮช plist โครงสร้างบันเดิลของ Apple และทรัพยากรที่แตกออกมา | [คู่มืออาร์ติแฟกต์และ JavaScript](docs/javascript-artifact-reconstruction.md), [แอปพลิเคชัน Apple](docs/apple-application-analysis.md) |
|
||||
| พฤติกรรมของโปรเซส | เอาต์พุตเทอร์มินัล การโต้ตอบ ข้อมูลการสิ้นสุดและระบบไฟล์ที่สังเกตได้ และการเปรียบเทียบการรัน | Linux/macOS ที่มี PTY เนทีฟ; [การบันทึกโปรเซส](docs/process-capture.md) |
|
||||
|
||||
การตรวจสอบ JavaScript และ .NET แบบสถิตอ่านไฟล์ที่ให้มาโดยไม่รันแอปพลิเคชัน การบันทึกขณะทำงานจะรันหรือโต้ตอบกับเป้าหมายที่เลือกโดยใช้สิทธิ์ของผู้ใช้คุณ คู่มือการทำงานแต่ละฉบับอธิบายผลที่เกิดขึ้นไว้
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
รูปแบบไบนารีเนทีฟและแพลตฟอร์มที่รองรับแตกต่างกันตามผู้ให้บริการวิเคราะห์ ดู[การตั้งค่า Hopper และ Ghidra](docs/installation.md#hopper), [คู่มือ IDA](docs/ida-provider.md) และ[การรองรับ Ghidra บน Windows แบบทดลอง](docs/windows-ghidra-p0.md) Ghidra ยังรองรับ[การวิเคราะห์ DOS แบบ 16 บิต](docs/ghidra-dos.md) สำหรับการเลือกผู้ให้บริการ ดู[คู่มือ CLI](docs/cli.md#choose-a-provider) ตรวจสอบ[ความพร้อมใช้งานในแต่ละรุ่น](docs/installation.md#released-package-and-main) สำหรับฟีเจอร์ที่เพิ่มหลังรุ่นล่าสุดบน npm
|
||||
|
||||
## ตัวอย่างการใช้งาน
|
||||
|
||||
### DX-Ball: สร้างการคำนวณแพนเสียงขึ้นใหม่
|
||||
|
||||
ติดตามการเรียกเสียงไปยังฟังก์ชันช่วยแปลงตำแหน่งเป็นค่าแพน ตรวจสอบคำสั่ง และแปลงโค้ดเทียมที่ยังไม่สมบูรณ์เป็น C การสร้างขึ้นใหม่นี้ผ่านการทดสอบ 3,205 กรณีเทียบกับ x86 ต้นฉบับ และสร้างไบต์ทั้ง 63 ไบต์ของฟังก์ชันที่คอมไพล์แล้วได้ตรงกัน
|
||||
|
||||
[อ่านกรณีศึกษา](https://rea.tools/showcase/dx-ball/) · [รีโพซิทอรีการสร้างขึ้นใหม่](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: ติดตามบริดจ์คลิปบอร์ดของ Electron
|
||||
|
||||
ค้นหา API คลิปบอร์ดของ renderer ติดตามผ่าน preload และ IPC ไปยังโปรเซสหลัก แล้วตรวจสอบรูปแบบคลิปบอร์ดที่มีข้อมูลการจัดรูปแบบ
|
||||
|
||||
[อ่านกรณีศึกษา](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: กู้คืนการคำนวณวงแหวนกระสุนของ DOS
|
||||
|
||||
ตรวจสอบคำสั่ง 16 บิตของเกมต้นฉบับบน PC-98 กู้คืนการคำนวณมุมคงที่และมุมเล็งเป้าหมาย แล้วเปรียบเทียบ C++ ที่สร้างขึ้นใหม่กับผลลัพธ์จากคอมไพเลอร์ในยุคนั้น
|
||||
|
||||
[อ่านกรณีศึกษา](https://rea.tools/showcase/th04/) · [รีโพซิทอรีการสร้างขึ้นใหม่](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
หากคุณใช้ REA กับสิ่งที่น่าสนใจ เราอยากเห็นผลงานของคุณ แบ่งปันกรณีของคุณผ่าน [issue](https://github.com/morluto/rea/issues) หรือ [pull request](https://github.com/morluto/rea/pulls) พร้อมระบุเป้าหมาย คำถามของคุณ REA ช่วยอย่างไร และสิ่งที่คุณค้นพบ
|
||||
|
||||
## คำถามที่พบบ่อย
|
||||
|
||||
<details>
|
||||
<summary><strong>เอเจนต์ใดใช้ REA ได้บ้าง?</strong></summary>
|
||||
|
||||
เอเจนต์ใดก็ตามที่รองรับเซิร์ฟเวอร์ MCP บนเครื่อง ขั้นตอนตั้งค่าจะกำหนดค่าให้กับ[เอเจนต์ที่รองรับ](docs/installation.md#supported-agents) ส่วนไคลเอนต์อื่นใช้[การลงทะเบียน MCP ด้วยตนเอง](docs/installation.md#mcp-registry) ได้
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>จำเป็นต้องมี Hopper, Ghidra หรือ IDA หรือไม่?</strong></summary>
|
||||
|
||||
การวิเคราะห์เนทีฟเชิงลึกต้องใช้หนึ่งในเครื่องมือเหล่านี้ การตรวจสอบ JavaScript และ .NET แบบสถิตทำงานได้โดยไม่มีเอนจินวิเคราะห์เนทีฟ ขั้นตอนตั้งค่าสามารถติดตั้ง Hopper หลังได้รับอนุมัติ ส่วน Ghidra และ IDA ใช้การติดตั้งที่คุณมีอยู่แล้ว ดู[การตั้งค่าผู้ให้บริการวิเคราะห์](docs/installation.md#hopper)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>ต้องเริ่ม Hopper ก่อนหรือไม่?</strong></summary>
|
||||
|
||||
REA เริ่ม Hopper เมื่อการดำเนินการต้องใช้ บน macOS อาจมีหน้าต่างเมื่อรันครั้งแรกให้เลือกโหมดทดลองหรือเปิดใช้ใบอนุญาต ดู[การเริ่ม Hopper และการแก้ไขปัญหา](docs/installation.md#launcher-paths-and-troubleshooting)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>การติดตั้งสกิลจาก skills.sh ทำอะไร?</strong></summary>
|
||||
|
||||
สกิลให้คำแนะนำการตรวจสอบแก่เอเจนต์ ใช้ `rea setup` เพื่อลงทะเบียนเซิร์ฟเวอร์ MCP ของ REA และติดตั้งคำแนะนำที่ตรงกัน แล้วเริ่มเอเจนต์ใหม่ ดู[การติดตั้งเฉพาะสกิล](docs/installation.md#skill-only-installation)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA ส่งกลับโค้ดแบบใด?</strong></summary>
|
||||
|
||||
การวิเคราะห์เนทีฟส่งกลับโค้ดเทียมและแอสเซมบลี การวิเคราะห์ JavaScript/Electron กู้คืนโมดูลและความสัมพันธ์ระหว่างโมดูล เอเจนต์ใช้ข้อค้นพบเหล่านี้เพื่อเขียนและทดสอบการนำไปใช้งาน ดูตัวอย่างที่ทำจริงใน[ตัวอย่างการใช้งาน](#ตัวอย่างการใช้งาน)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA อัปโหลดแอปของฉันหรือไม่?</strong></summary>
|
||||
|
||||
REA วิเคราะห์เป้าหมายบนเครื่องของคุณ เอเจนต์ได้รับผลลัพธ์ของเครื่องมือ และผู้ให้บริการโมเดลของเอเจนต์มีนโยบายข้อมูลของตนเอง
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>ควรทำอย่างไรเมื่อพบข้อผิดพลาด?</strong></summary>
|
||||
|
||||
อัปเดตก่อน รุ่นล่าสุดอาจแก้ไขปัญหานี้แล้ว
|
||||
|
||||
สำหรับ CLI ที่ติดตั้งผ่าน npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
สำหรับการตั้งค่าเอเจนต์ผ่าน `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
หากใช้เอเจนต์ ให้ทำ[การปรับปรุงการตั้งค่า](#อัปเดต-rea) ให้เสร็จแล้วเริ่มเอเจนต์ใหม่ ลองงานเดิมอีกครั้ง หากยังพบปัญหา ให้[เปิด issue](https://github.com/morluto/rea/issues) พร้อมระบุรุ่น REA ประเภทเป้าหมาย ขั้นตอนทำให้เกิดปัญหา และข้อความข้อผิดพลาด
|
||||
|
||||
</details>
|
||||
|
||||
## เอกสาร
|
||||
|
||||
เริ่มจาก[คู่มือพร้อมตัวอย่าง](https://rea.tools/guides/) บนเว็บไซต์ สำหรับตัวเลือก ข้อกำหนดเบื้องต้น และสัญญาผลลัพธ์ที่แน่นอน:
|
||||
|
||||
- [การติดตั้งและตั้งค่า](docs/installation.md): การลงทะเบียนเอเจนต์ การตั้งค่าผู้ให้บริการ การอัปเดต และการถอนการติดตั้ง
|
||||
- [ความพร้อมใช้งานและการแก้ไขปัญหา](docs/installation.md#check-readiness-for-your-task): ตรวจวินิจฉัยเอเจนต์หรือเอนจินวิเคราะห์หนึ่งตัว
|
||||
- [CLI และ Evidence](docs/cli.md): คำสั่ง การเลือกผู้ให้บริการ สแนปช็อต การนำเข้า/ส่งออก และรหัสการสิ้นสุด
|
||||
- [สัญญา MCP](docs/mcp-contracts.md) และ[พรอมป์ต์สำหรับเอเจนต์](docs/mcp-prompts.md): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ
|
||||
- [แค็ตตาล็อกเครื่องมือ](docs/mcp-contracts.md#generated-catalog): รายการเครื่องมือ ผู้ให้บริการ และคำสั่ง CLI ที่สร้างระหว่างการบิลด์
|
||||
- [แผนพัฒนา](docs/roadmap.md): งานที่วางแผนไว้และรายการติดตามความสามารถ
|
||||
|
||||
รายงานช่องโหว่ผ่าน [SECURITY.md](SECURITY.md)
|
||||
|
||||
## การมีส่วนร่วม
|
||||
|
||||
เรายินดีรับความช่วยเหลือในการพัฒนา REA! [เปิด issue](https://github.com/morluto/rea/issues) เพื่อรายงานบั๊กหรือเสนอฟีเจอร์ หรือ[ส่ง pull request](https://github.com/morluto/rea/pulls) เพื่อปรับปรุงโค้ดหรือเอกสาร
|
||||
|
||||
ดู [CONTRIBUTING.md](CONTRIBUTING.md) สำหรับการตั้งค่าสภาพแวดล้อมพัฒนาและการตรวจสอบ ดู[การทดสอบ](docs/testing.md) สำหรับแนวทางการตรวจยืนยัน และ[แผนผังสถาปัตยกรรม](docs/architecture.mermaid) สำหรับโครงสร้างโปรเจกต์
|
||||
|
||||
## ลิงก์ของโปรเจกต์
|
||||
|
||||
[เว็บไซต์](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [ความปลอดภัย](SECURITY.md)
|
||||
|
||||
## ประวัติดาว
|
||||
|
||||
🎉 **20,000 ดาวบน GitHub — ขอบคุณทุกคน!**
|
||||
|
||||
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก ทดสอบบิลด์ และช่วยแก้ไข
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="ประวัติดาว GitHub ของ REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## ข้อสงวนสิทธิ์
|
||||
|
||||
REA มีเครื่องมือสำหรับการวิจัยวิศวกรรมย้อนกลับ การวิเคราะห์ และการสร้างขึ้นใหม่อย่างถูกกฎหมาย คุณมีหน้าที่ขออนุญาตตามที่จำเป็นและปฏิบัติตามกฎหมายที่เกี่ยวข้อง โปรเจกต์นี้ไม่สนับสนุนการใช้งานที่ผิดกฎหมายหรือไม่ได้รับอนุญาต
|
||||
|
||||
## สัญญาอนุญาต
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Her şeyi tersine mühendislikle inceleyin
|
||||
|
||||
### İkili dosyaların, uygulamaların ve çalışma zamanı davranışının tersine mühendisliği için tek bir MCP.
|
||||
|
||||
**Beğendiğiniz bir özellik mi gördünüz? İkili kod düzeyine kadar nasıl çalıştığını anlayın.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Web sitesi](https://rea.tools/) · [Rehberler](https://rea.tools/guides/) · [Örnek çalışmalar](https://rea.tools/showcase/)**
|
||||
|
||||
[Hızlı başlangıç](#hızlı-başlangıç) · [REA nasıl çalışır](#rea-nasıl-çalışır) · [Neleri analiz edebilirsiniz](#neleri-analiz-edebilirsiniz) · [Örnek çalışmalar](#örnek-çalışmalar) · [Sık sorulan sorular](#sık-sorulan-sorular) · [Belgeler](#belgeler)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/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" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA topluluğu">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Tersine mühendislik topluluğuna katılın</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Soru-cevap · Sonuç paylaşımı</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Bir uygulamada kendi ürününüze eklemek istediğiniz bir özellik mi gördünüz? Ajanınızdan REA ile araştırmasını isteyin. Kaynak kodu olmadan uygulamayı inceleyebilir, özelliğin nasıl çalıştığını açıklayabilir, kanıtları gösterebilir ve projenize uygun bir sürümünü geliştirebilir.
|
||||
|
||||
REA, ajanınızı yerel makine kodu içeren ikili dosyaları, JavaScript ve Electron uygulamalarını, .NET derlemelerini ve web sitelerini inceleyen araçlara bağlar. Aynı araçları terminalden de kullanabilirsiniz. Analiz yerel olarak çalışır; sonuçlar her çıkarımın dayandığı kanıtları ve sınırlamaları içerir.
|
||||
|
||||
Kurulum, REA'yı ajanınıza kaydeder ve uyumlu iş akışı talimatlarını yükler. Yerel makine kodu analizi mevcut bir Hopper veya Ghidra kurulumunu kullanabilir; kurulum işlemi, onayınızla isteğe bağlı olarak Hopper da yükleyebilir. Statik JavaScript analizi bu iki motora da ihtiyaç duymaz.
|
||||
|
||||
> Kurulum talimatları, görselli rehberler ve gerçek örnek çalışmalar için **[REA web sitesini ziyaret edin](https://rea.tools/)**.
|
||||
|
||||
## Hızlı başlangıç
|
||||
|
||||
### Ajanınızı yapılandırın
|
||||
|
||||
Node.js ve npm yüklüyken şu komutu çalıştırın:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Ajanlarınızı seçin, önerilen değişiklikleri inceleyin ve onaylayın. Kurulum, mevcut yapılandırmaları yedekleyerek REA'nın MCP sunucusunu ve uyumlu iş akışı talimatlarını ekler. Ardından ajanınızı yeniden başlatın.
|
||||
|
||||
Kurulum Claude Code, Codex, Cursor, Gemini CLI, Grok Build ve [diğer ajanları](docs/installation.md#supported-agents) destekler. Sağlayıcı yapılandırması ve elle MCP kaydı için [yükleme ve kurulum](docs/installation.md) belgesine bakın.
|
||||
|
||||
### Ajanınıza sorun
|
||||
|
||||
```text
|
||||
Notes uygulamasında aramanın nasıl çalıştığını araştır, kanıtları göster ve projem için benzer bir özellik geliştir.
|
||||
```
|
||||
|
||||
Notes yerine hedef uygulamanızı ve anlamak istediğiniz özelliği belirtin.
|
||||
|
||||
### Terminali kullanın
|
||||
|
||||
Çıkarılmış bir JavaScript/Electron uygulama dizinini veya ASAR dosyasını inceleyin:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Sonuç; modülleri, içe aktarmaları, Electron sınırlarını ve bunlara ilişkin kanıtları içerir. Yolu hedefinizle değiştirin; Windows'ta örneğin `"D:/apps/example"` kullanabilirsiniz.
|
||||
|
||||
Düzenli kullanım için `rea` komutunu yüklemek üzere:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Yerel makine kodu analizi için önce bir sağlayıcı yapılandırın. Yerel analiz komutları, sağlayıcı seçimi, anlık görüntüler ve betik kullanımı için [CLI ve Evidence rehberine](docs/cli.md) bakın.
|
||||
|
||||
### REA'yı güncelleyin
|
||||
|
||||
REA hızla değişir ve yeni sürümler sık sık hata düzeltmeleri içerir. Kurulumunuzu güncel tutun.
|
||||
|
||||
npm ile yüklenen CLI için:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Ajan kayıtlarını ve skill'i güncellemek için güncellemenin çıktısında gösterilen kurulum komutunu çalıştırın.
|
||||
|
||||
`npx` kullanıyorsanız ajan kurulumunu şu komutla güncelleyin:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Kurulum değişikliklerini inceleyin ve ajanınızı yeniden başlatın. Tek seferlik CLI işlemleri için `npx rea-agents@latest` komutunun ardından istediğiniz komutu yazın.
|
||||
|
||||
## REA nasıl çalışır
|
||||
|
||||
Ajanınız, hedefi incelemek ve ilgili kodu izlemek için MCP üzerinden REA'yı çağırır. REA bulguları kanıtlarıyla birlikte döndürür. Ajan bunları takip soruları sormak, davranışı açıklamak veya bir uygulama yazıp test etmek için kullanır. CLI komutları aynı iş akışlarını kullanır.
|
||||
|
||||

|
||||
|
||||
[Şekli tam boyutuyla açın](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Neleri analiz edebilirsiniz
|
||||
|
||||
REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araçlar ve desteklenen ana bilgisayar sistemleri hedefe bağlıdır:
|
||||
|
||||
| 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/guides/native/) |
|
||||
| Ç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](docs/binary-diagnostics.md) |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](docs/evm-bytecode.md) |
|
||||
| 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ı | Çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](docs/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/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/guides/browser/) |
|
||||
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR; mitmproxy'nin kendi biçimindeki yakalamalar için Linux üzerinde mitmdump; [ağ yakalama rehberi](docs/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](docs/managed-code-analysis.md) |
|
||||
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](docs/android-analysis.md) |
|
||||
| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](docs/firmware-analysis.md) |
|
||||
| Paketler ve kaynaklar | Dosya envanterleri, özet değerleri, plist'ler, Apple paket yapısı ve çıkarılmış kaynaklar | [Artefakt ve JavaScript rehberi](docs/javascript-artifact-reconstruction.md), [Apple uygulamaları](docs/apple-application-analysis.md) |
|
||||
| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](docs/process-capture.md) |
|
||||
|
||||
Statik JavaScript ve .NET incelemesi, uygulamayı çalıştırmadan sağlanan dosyaları okur. Çalışma zamanı yakalaması, seçilen hedefi kullanıcı izinlerinizle çalıştırır veya onunla etkileşime girer; her çalışma zamanı rehberi etkilerini açıklar.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Yerel makine kodu içeren ikili dosya biçimleri ve ana bilgisayar desteği sağlayıcıya göre değişir. [Hopper ve Ghidra kurulumu](docs/installation.md#hopper), [IDA rehberi](docs/ida-provider.md) ve [Windows için deneysel Ghidra desteği](docs/windows-ghidra-p0.md) belgelerine bakın. Ghidra ayrıca [16 bit DOS analizini](docs/ghidra-dos.md) destekler. Sağlayıcı seçimi için [CLI rehberine](docs/cli.md#choose-a-provider) bakın. En son npm sürümünden sonra eklenen özellikler için [sürümlerde kullanılabilirliği](docs/installation.md#released-package-and-main) kontrol edin.
|
||||
|
||||
## Örnek çalışmalar
|
||||
|
||||
### DX-Ball: Sesin stereo konumlandırma hesabını yeniden oluşturma
|
||||
|
||||
Bir ses çağrısından konumu stereo konumlandırmaya dönüştüren yardımcı fonksiyona ilerleyin, talimatları inceleyin ve eksik sözde kodu C'ye dönüştürün. Yeniden oluşturulan kod, özgün x86 üzerinde 3.205 test durumunu geçer ve derlenmiş fonksiyonun 63 baytının tamamını yeniden üretir.
|
||||
|
||||
[Örnek çalışmayı okuyun](https://rea.tools/showcase/dx-ball/) · [Yeniden oluşturma deposu](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: Electron pano köprüsünü izleme
|
||||
|
||||
İşleyici sürecin pano API'sini bulun, preload ve IPC üzerinden ana sürece kadar izleyin ve zengin pano biçimini inceleyin.
|
||||
|
||||
[Örnek çalışmayı okuyun](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: DOS mermi halkası hesabını geri çıkarma
|
||||
|
||||
Özgün PC-98 oyununun 16 bit talimatlarını inceleyin, sabit ve hedefe yönelik açı hesaplarını geri çıkarın ve yeniden oluşturulan C++ kodunu dönemin derleyici çıktısıyla karşılaştırın.
|
||||
|
||||
[Örnek çalışmayı okuyun](https://rea.tools/showcase/th04/) · [Yeniden oluşturma deposu](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
REA'yı ilginç bir konuda kullandıysanız görmek isteriz. Hedefi, sorunuzu, REA'nın nasıl yardımcı olduğunu ve bulgularınızı bir [issue](https://github.com/morluto/rea/issues) veya [pull request](https://github.com/morluto/rea/pulls) ile paylaşın.
|
||||
|
||||
## Sık sorulan sorular
|
||||
|
||||
<details>
|
||||
<summary><strong>Hangi ajanlar REA'yı kullanabilir?</strong></summary>
|
||||
|
||||
Yerel MCP sunucularını destekleyen her ajan REA'yı kullanabilir. Kurulum, [desteklenen ajanları](docs/installation.md#supported-agents) yapılandırır; diğer istemciler [elle MCP kaydını](docs/installation.md#mcp-registry) kullanabilir.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Hopper, Ghidra veya IDA gerekiyor mu?</strong></summary>
|
||||
|
||||
Derin yerel kod analizi bunlardan birini kullanır. Statik JavaScript ve .NET incelemesi yerel analiz motoru olmadan çalışır. Kurulum onaydan sonra Hopper yükleyebilir; Ghidra ve IDA mevcut kurulumlarınızı kullanır. [Sağlayıcı kurulumuna](docs/installation.md#hopper) bakın.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Önce Hopper'ı başlatmam gerekiyor mu?</strong></summary>
|
||||
|
||||
REA, bir işlem gerektirdiğinde Hopper'ı başlatır. macOS'ta ilk çalıştırmada demo modunu seçmenizi veya lisansınızı etkinleştirmenizi isteyen bir iletişim kutusu açılabilir. [Hopper başlatma ve sorun giderme](docs/installation.md#launcher-paths-and-troubleshooting) belgesine bakın.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>skills.sh üzerinden skill yüklemek ne yapar?</strong></summary>
|
||||
|
||||
Skill, ajanınıza araştırma talimatları sağlar. REA'nın MCP sunucusunu kaydetmek ve uyumlu talimatları yüklemek için `rea setup` kullanın, ardından ajanınızı yeniden başlatın. [Yalnızca skill yükleme](docs/installation.md#skill-only-installation) belgesine bakın.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA hangi kodu döndürür?</strong></summary>
|
||||
|
||||
Yerel kod analizi sözde kod ve assembly kodu döndürür. JavaScript/Electron analizi modülleri ve aralarındaki ilişkileri geri çıkarır. Ajanınız bu bulgularla bir uygulama yazar ve test eder; [örnek çalışmalar](#örnek-çalışmalar) uygulamalı örnekler sunar.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA uygulamamı yükler mi?</strong></summary>
|
||||
|
||||
REA hedefleri yerel olarak analiz eder. Ajanınız araç sonuçlarını alır ve model sağlayıcısının kendi veri politikası vardır.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Bir hatayla karşılaşırsam ne yapmalıyım?</strong></summary>
|
||||
|
||||
Önce güncelleyin; yakın tarihli bir sürüm hatayı düzeltmiş olabilir.
|
||||
|
||||
npm ile yüklenen CLI için:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
`npx` üzerinden ajan kurulumu için:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Ajan kullanıyorsanız [kurulum yenilemesini](#reayı-güncelleyin) tamamlayın ve ajanı yeniden başlatın. Aynı görevi tekrar deneyin. Sorun sürerse REA sürümünüzü, hedef türünü, yeniden üretme adımlarını ve hata çıktısını içeren bir [issue açın](https://github.com/morluto/rea/issues).
|
||||
|
||||
</details>
|
||||
|
||||
## Belgeler
|
||||
|
||||
Web sitesindeki [uygulamalı rehberlerden](https://rea.tools/guides/) başlayın. Kesin seçenekler, ön koşullar ve sonuç sözleşmeleri için:
|
||||
|
||||
- [Yükleme ve kurulum](docs/installation.md): ajan kaydı, sağlayıcı yapılandırması, güncellemeler ve kaldırma.
|
||||
- [Hazırlık ve sorun giderme](docs/installation.md#check-readiness-for-your-task): belirli bir ajanı veya analiz motorunu tanılama.
|
||||
- [CLI ve Evidence](docs/cli.md): komutlar, sağlayıcı seçimi, anlık görüntüler, içe/dışa aktarma ve çıkış durumları.
|
||||
- [MCP sözleşmeleri](docs/mcp-contracts.md) ve [ajan istemleri](docs/mcp-prompts.md): araç sonuçları, oturumlar ve yönlendirmeli araştırmalar.
|
||||
- [Araç kataloğu](docs/mcp-contracts.md#generated-catalog): derleme sırasında oluşturulan araç, sağlayıcı ve CLI komutu envanteri.
|
||||
- [Yol haritası](docs/roadmap.md): planlanan çalışmalar ve yetenek takibi.
|
||||
|
||||
Güvenlik açıklarını [SECURITY.md](SECURITY.md) üzerinden bildirin.
|
||||
|
||||
## Katkıda bulunma
|
||||
|
||||
REA'ya katkınızı bekliyoruz! Hata bildirmek veya özellik önermek için bir [issue açın](https://github.com/morluto/rea/issues); kodu veya belgeleri geliştirmek için bir [pull request gönderin](https://github.com/morluto/rea/pulls).
|
||||
|
||||
Geliştirme ortamı ve kontroller için [CONTRIBUTING.md](CONTRIBUTING.md), doğrulama süreçleri için [test rehberi](docs/testing.md) ve proje yapısı için [mimari harita](docs/architecture.mermaid) belgelerine bakın.
|
||||
|
||||
## Proje bağlantıları
|
||||
|
||||
[Web sitesi](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Güvenlik](SECURITY.md)
|
||||
|
||||
## Yıldız geçmişi
|
||||
|
||||
🎉 **GitHub'da 20.000 yıldız — teşekkürler!**
|
||||
|
||||
REA'yı kullanan, hata bildiren, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="REA GitHub yıldız geçmişi" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Sorumluluk reddi
|
||||
|
||||
REA, yasal tersine mühendislik araştırması, analizi ve yeniden oluşturma için araçlar sağlar. Gerekli izinleri almak ve geçerli yasalara uymak sizin sorumluluğunuzdadır. Proje yasa dışı veya yetkisiz kullanımı desteklemez.
|
||||
|
||||
## Lisans
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Реверс-інжиніринг будь-чого
|
||||
|
||||
### Один MCP для реверс-інжинірингу бінарних файлів, застосунків і поведінки під час виконання.
|
||||
|
||||
**Побачили цікаву функцію? Зрозумійте, як вона працює, аж до рівня двійкового коду.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Сайт](https://rea.tools/) · [Посібники](https://rea.tools/guides/) · [Приклади](https://rea.tools/showcase/)**
|
||||
|
||||
[Швидкий старт](#швидкий-старт) · [Як працює REA](#як-працює-rea) · [Що можна аналізувати](#що-можна-аналізувати) · [Приклади](#приклади) · [Поширені запитання](#поширені-запитання) · [Документація](#документація)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA запускає міст аналізу всередині Hopper під час дослідження нативного бінарного файла" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="Спільнота REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Приєднуйтеся до спільноти реверс-інжинірингу</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Запитання й відповіді · Обмін результатами</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Побачили в застосунку функцію, яку хочете додати до свого продукту? Попросіть агента дослідити її за допомогою REA. Він може перевірити застосунок без вихідного коду, пояснити роботу функції, показати підтвердження та створити реалізацію для вашого проєкту.
|
||||
|
||||
REA підключає вашого агента до інструментів дослідження нативних бінарних файлів, застосунків JavaScript і Electron, збірок .NET та сайтів. Ті самі інструменти можна використовувати з термінала. Аналіз виконується локально, а результати містять підтвердження й обмеження, на яких ґрунтується кожен висновок.
|
||||
|
||||
Налаштування реєструє REA у вашому агенті та встановлює відповідні інструкції робочих процесів. Для нативного аналізу можна використати вже встановлений Hopper або Ghidra; налаштування також може встановити Hopper за вашою згодою. Статичному аналізу JavaScript не потрібен жоден із цих рушіїв.
|
||||
|
||||
> **[Відвідайте сайт REA](https://rea.tools/)**, щоб знайти інструкції налаштування, ілюстровані посібники та реальні дослідження.
|
||||
|
||||
## Швидкий старт
|
||||
|
||||
### Налаштуйте агента
|
||||
|
||||
Якщо Node.js і npm встановлено, виконайте:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Виберіть агентів, перевірте запропоновані зміни та схваліть їх. Налаштування додає MCP-сервер REA та відповідні інструкції робочих процесів, створюючи резервні копії наявної конфігурації. Після цього перезапустіть агента.
|
||||
|
||||
Налаштування підтримує Claude Code, Codex, Cursor, Gemini CLI, Grok Build та [інших агентів](docs/installation.md#supported-agents). Конфігурацію провайдерів і ручну реєстрацію MCP описано в розділі [встановлення й налаштування](docs/installation.md).
|
||||
|
||||
### Запитайте агента
|
||||
|
||||
```text
|
||||
Досліди, як працює пошук у застосунку Notes, покажи підтвердження й створи подібну функцію для мого проєкту.
|
||||
```
|
||||
|
||||
Замініть Notes цільовим застосунком і вкажіть функцію, яку хочете зрозуміти.
|
||||
|
||||
### Використовуйте термінал
|
||||
|
||||
Перевірте розпакований каталог застосунку JavaScript/Electron або файл ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Результат містить модулі, імпорти, межі Electron і підтвердження для них. Замініть шлях своїм, наприклад `"D:/apps/example"` у Windows.
|
||||
|
||||
Для регулярного використання встановіть команду `rea`:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Для нативного аналізу спочатку налаштуйте провайдера. Нативні команди, вибір провайдера, знімки та використання у скриптах описує [посібник CLI та Evidence](docs/cli.md).
|
||||
|
||||
### Оновіть REA
|
||||
|
||||
REA швидко розвивається, а нові випуски часто містять виправлення помилок. Підтримуйте встановлену версію актуальною.
|
||||
|
||||
Для CLI, встановленого через npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Щоб оновити реєстрації агентів і skill, виконайте команду налаштування, яку виведе оновлення.
|
||||
|
||||
Якщо використовуєте `npx`, оновіть налаштування агента командою:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Перевірте зміни налаштувань і перезапустіть агента. Для разових команд CLI використовуйте `npx rea-agents@latest`, а потім потрібну команду.
|
||||
|
||||
## Як працює REA
|
||||
|
||||
Агент викликає REA через MCP, щоб перевірити ціль і простежити пов'язаний код. REA повертає знахідки разом із підтвердженнями. Агент використовує їх для уточнювальних запитань, пояснення поведінки або написання й тестування реалізації. Команди CLI використовують ті самі робочі процеси.
|
||||
|
||||

|
||||
|
||||
[Відкрити зображення в повному розмірі](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Що можна аналізувати
|
||||
|
||||
REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а також npm. Додаткові інструменти й підтримувані системи залежать від цілі:
|
||||
|
||||
| Ціль | Що повертає REA | Вимоги й посібник |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/guides/native/) |
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](docs/binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення; [посібник EVM без запуску](docs/evm-bytecode.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент; необов'язкові GDB/pwndbg; [записані аварійні завершення](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/guides/javascript/) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/guides/browser/) |
|
||||
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](docs/web-network-captures.md) |
|
||||
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](docs/managed-code-analysis.md) |
|
||||
| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS; [посібник Android](docs/android-analysis.md) |
|
||||
| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) |
|
||||
| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) |
|
||||
| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) |
|
||||
|
||||
Статична перевірка JavaScript і .NET читає надані файли, не запускаючи застосунок. Захоплення під час виконання запускає вибрану ціль або взаємодіє з нею з правами вашого користувача; відповідні посібники описують його вплив.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Нативні формати й підтримка систем відрізняються залежно від провайдера. Дивіться [налаштування Hopper і Ghidra](docs/installation.md#hopper), [посібник IDA](docs/ida-provider.md) та [експериментальну підтримку Ghidra у Windows](docs/windows-ghidra-p0.md). Ghidra також підтримує [аналіз 16-бітного DOS](docs/ghidra-dos.md). Для вибору провайдера дивіться [посібник CLI](docs/cli.md#choose-a-provider). Для функцій, доданих після найновішої версії в npm, перевірте [доступність у випусках](docs/installation.md#released-package-and-main).
|
||||
|
||||
## Приклади
|
||||
|
||||
### DX-Ball: відновлення розрахунку звукової панорами
|
||||
|
||||
Простежте звуковий виклик до допоміжної функції, що перетворює координату на панорамування, перевірте інструкції та перетворіть неповний псевдокод на C. Відновлена реалізація проходить 3 205 тестів з оригінальним x86-кодом і відтворює всі 63 байти скомпільованої функції.
|
||||
|
||||
[Прочитати дослідження](https://rea.tools/showcase/dx-ball/) · [Репозиторій реконструкції](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: відстеження мосту буфера обміну Electron
|
||||
|
||||
Знайдіть API буфера обміну в процесі рендерингу, простежте його через preload та IPC до головного процесу й перевірте формат буфера обміну з даними форматування.
|
||||
|
||||
[Прочитати дослідження](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: відновлення розрахунку кільця куль у DOS
|
||||
|
||||
Перевірте 16-бітні інструкції оригінальної гри для PC-98, відновіть розрахунки фіксованих і прицільних кутів та порівняйте відновлений C++ із виводом тогочасного компілятора.
|
||||
|
||||
[Прочитати дослідження](https://rea.tools/showcase/th04/) · [Репозиторій реконструкції](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Якщо ви дослідили з REA щось цікаве, поділіться результатом. Створіть [issue](https://github.com/morluto/rea/issues) або [pull request](https://github.com/morluto/rea/pulls) і вкажіть ціль, ваше запитання, як допоміг REA та що ви з'ясували.
|
||||
|
||||
## Поширені запитання
|
||||
|
||||
<details>
|
||||
<summary><strong>Які агенти можуть використовувати REA?</strong></summary>
|
||||
|
||||
Будь-який агент, що підтримує локальні MCP-сервери. Налаштування конфігурує [підтримуваних агентів](docs/installation.md#supported-agents); інші клієнти можуть використати [ручну реєстрацію MCP](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Чи потрібні мені Hopper, Ghidra або IDA?</strong></summary>
|
||||
|
||||
Поглиблений нативний аналіз використовує один із цих інструментів. Статична перевірка JavaScript і .NET працює без рушія нативного аналізу. Налаштування може встановити Hopper після схвалення; Ghidra й IDA використовують наявні встановлення. Дивіться [налаштування провайдерів](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Чи потрібно спочатку запускати Hopper?</strong></summary>
|
||||
|
||||
REA запускає Hopper, коли він потрібен для операції. У macOS під час першого запуску може з'явитися діалог вибору деморежиму або активації ліцензії. Дивіться [запуск Hopper та усунення неполадок](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Що дає встановлення skill із skills.sh?</strong></summary>
|
||||
|
||||
Skill надає агенту інструкції дослідження. Використовуйте `rea setup`, щоб зареєструвати MCP-сервер REA та встановити відповідні інструкції, а потім перезапустіть агента. Дивіться [встановлення лише skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Який код повертає REA?</strong></summary>
|
||||
|
||||
Нативний аналіз повертає псевдокод і асемблерний код. Аналіз JavaScript/Electron відновлює модулі та їхні зв'язки. Агент використовує ці результати, щоб написати й протестувати реалізацію; [приклади](#приклади) містять практичні дослідження.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Чи завантажує REA мій застосунок на сервер?</strong></summary>
|
||||
|
||||
REA аналізує цілі локально. Ваш агент отримує результати інструментів, а постачальник його моделі має власну політику даних.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Що робити, якщо я зіткнувся з помилкою?</strong></summary>
|
||||
|
||||
Спочатку оновіться: нещодавній випуск уже міг виправити помилку.
|
||||
|
||||
Для CLI, встановленого через npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Для налаштування агента через `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Якщо використовуєте агента, завершіть [оновлення налаштувань](#оновіть-rea) і перезапустіть його. Повторіть те саме завдання. Якщо проблема залишається, [відкрийте issue](https://github.com/morluto/rea/issues) із версією REA, типом цілі, кроками відтворення та виводом помилки.
|
||||
|
||||
</details>
|
||||
|
||||
## Документація
|
||||
|
||||
Почніть із [практичних посібників](https://rea.tools/guides/) на сайті. Точні параметри, передумови й контракти результатів описано тут:
|
||||
|
||||
- [Встановлення й налаштування](docs/installation.md): реєстрація агентів, конфігурація провайдерів, оновлення та видалення.
|
||||
- [Готовність та усунення неполадок](docs/installation.md#check-readiness-for-your-task): діагностика конкретного агента або рушія аналізу.
|
||||
- [CLI та Evidence](docs/cli.md): команди, вибір провайдера, знімки, імпорт/експорт і статуси завершення.
|
||||
- [Контракти MCP](docs/mcp-contracts.md) і [промпти агентів](docs/mcp-prompts.md): результати інструментів, сеанси й керовані дослідження.
|
||||
- [Каталог інструментів](docs/mcp-contracts.md#generated-catalog): створюваний під час збірки перелік інструментів, провайдерів і команд CLI.
|
||||
- [План розвитку](docs/roadmap.md): заплановані роботи й відстеження можливостей.
|
||||
|
||||
Повідомляйте про вразливості згідно з [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Участь у проєкті
|
||||
|
||||
Будемо раді вашій допомозі з REA! [Відкрийте issue](https://github.com/morluto/rea/issues), щоб повідомити про помилку чи запропонувати функцію, або [надішліть pull request](https://github.com/morluto/rea/pulls) з поліпшеннями коду чи документації.
|
||||
|
||||
Налаштування середовища розробки й перевірки описано в [CONTRIBUTING.md](CONTRIBUTING.md), шляхи верифікації — у [посібнику тестування](docs/testing.md), а структуру проєкту — на [схемі архітектури](docs/architecture.mermaid).
|
||||
|
||||
## Посилання проєкту
|
||||
|
||||
[Сайт](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Безпека](SECURITY.md)
|
||||
|
||||
## Історія зірок
|
||||
|
||||
🎉 **20 000 зірок на GitHub — дякуємо!**
|
||||
|
||||
Дякуємо всім, хто використовує REA, повідомляє про помилки, тестує збірки й допомагає з виправленнями.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Історія зірок REA на GitHub" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Відмова від відповідальності
|
||||
|
||||
REA надає інструменти для законних досліджень, аналізу й реконструкції за допомогою реверс-інжинірингу. Ви відповідаєте за отримання необхідних дозволів і дотримання застосовних законів. Проєкт не підтримує незаконне або несанкціоноване використання.
|
||||
|
||||
## Ліцензія
|
||||
|
||||
[MIT](LICENSE)
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Kỹ thuật dịch ngược cho mọi thứ
|
||||
|
||||
### Một MCP để dịch ngược tệp nhị phân, ứng dụng và hành vi khi chạy.
|
||||
|
||||
**Thấy một tính năng bạn thích? Hiểu cách nó hoạt động, đến tận mức mã nhị phân.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Trang web](https://rea.tools/) · [Hướng dẫn](https://rea.tools/guides/) · [Các nghiên cứu điển hình](https://rea.tools/showcase/)**
|
||||
|
||||
[Bắt đầu nhanh](#bắt-đầu-nhanh) · [Cách REA hoạt động](#cách-rea-hoạt-động) · [Những gì bạn có thể phân tích](#những-gì-bạn-có-thể-phân-tích) · [Các nghiên cứu điển hình](#các-nghiên-cứu-điển-hình) · [Câu hỏi thường gặp](#câu-hỏi-thường-gặp) · [Tài liệu](#tài-liệu)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/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" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="Cộng đồng REA">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Tham gia cộng đồng kỹ thuật dịch ngược</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Hỏi đáp · Chia sẻ kết quả</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Bạn thấy một tính năng trong ứng dụng và muốn đưa nó vào sản phẩm của mình? Hãy nhờ tác nhân của bạn điều tra bằng REA. Tác nhân có thể kiểm tra ứng dụng mà không cần mã nguồn, giải thích cách tính năng hoạt động, đưa ra bằng chứng và xây dựng một phiên bản cho dự án của bạn.
|
||||
|
||||
REA kết nối tác nhân của bạn với các công cụ kiểm tra tệp nhị phân mã máy, ứng dụng JavaScript và Electron, assembly .NET và trang web. Bạn cũng có thể dùng các công cụ đó từ terminal. Việc phân tích chạy cục bộ; kết quả bao gồm bằng chứng và giới hạn làm cơ sở cho từng kết luận.
|
||||
|
||||
Quá trình thiết lập đăng ký REA với tác nhân và cài đặt các hướng dẫn quy trình tương ứng. Phân tích mã máy có thể sử dụng Hopper hoặc Ghidra đã cài sẵn; quá trình thiết lập cũng có thể cài Hopper nếu bạn chấp thuận. Phân tích tĩnh JavaScript không cần công cụ nào trong hai công cụ này.
|
||||
|
||||
> **[Truy cập trang web REA](https://rea.tools/)** để xem cách thiết lập, hướng dẫn có hình minh họa và các nghiên cứu điển hình thực tế.
|
||||
|
||||
## Bắt đầu nhanh
|
||||
|
||||
### Thiết lập tác nhân
|
||||
|
||||
Sau khi cài Node.js và npm, chạy:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Chọn các tác nhân, xem lại những thay đổi được đề xuất và chấp thuận. Quá trình thiết lập thêm máy chủ MCP của REA cùng hướng dẫn quy trình tương ứng, đồng thời sao lưu cấu hình hiện có. Sau đó khởi động lại tác nhân.
|
||||
|
||||
Quá trình thiết lập hỗ trợ Claude Code, Codex, Cursor, Gemini CLI, Grok Build và [các tác nhân khác](docs/installation.md#supported-agents). Xem [cài đặt và thiết lập](docs/installation.md) để cấu hình trình phân tích và đăng ký MCP thủ công.
|
||||
|
||||
### Hỏi tác nhân
|
||||
|
||||
```text
|
||||
Tìm hiểu cách tính năng tìm kiếm trong ứng dụng Notes hoạt động, cho tôi xem bằng chứng và xây dựng tính năng tương tự cho dự án của tôi.
|
||||
```
|
||||
|
||||
Thay Notes bằng ứng dụng mục tiêu và chỉ rõ tính năng bạn muốn hiểu.
|
||||
|
||||
### Sử dụng terminal
|
||||
|
||||
Kiểm tra thư mục ứng dụng JavaScript/Electron đã giải nén hoặc tệp ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Kết quả bao gồm mô-đun, các khai báo import, ranh giới Electron và bằng chứng tương ứng. Thay đường dẫn bằng mục tiêu của bạn, chẳng hạn `"D:/apps/example"` trên Windows.
|
||||
|
||||
Để cài lệnh `rea` cho việc sử dụng thường xuyên:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Để phân tích mã máy, trước tiên hãy cấu hình trình phân tích. Xem [hướng dẫn CLI và Evidence](docs/cli.md) về các lệnh phân tích mã máy, lựa chọn trình phân tích, ảnh chụp trạng thái và sử dụng trong script.
|
||||
|
||||
### Cập nhật REA
|
||||
|
||||
REA thay đổi nhanh và các bản phát hành mới thường xuyên có bản sửa lỗi. Hãy giữ bản cài đặt của bạn được cập nhật.
|
||||
|
||||
Với CLI được cài bằng npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Để cập nhật đăng ký tác nhân và skill, chạy lệnh thiết lập được in ra sau khi cập nhật.
|
||||
|
||||
Nếu dùng `npx`, cập nhật thiết lập tác nhân bằng:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Xem lại thay đổi cấu hình và khởi động lại tác nhân. Với lệnh CLI dùng một lần, dùng `npx rea-agents@latest` rồi thêm lệnh cần chạy.
|
||||
|
||||
## Cách REA hoạt động
|
||||
|
||||
Tác nhân gọi REA qua MCP để kiểm tra mục tiêu và lần theo mã liên quan. REA trả về các phát hiện kèm bằng chứng. Tác nhân dùng chúng để đặt câu hỏi tiếp theo, giải thích hành vi hoặc viết và kiểm thử một cách triển khai. Các lệnh CLI sử dụng cùng quy trình.
|
||||
|
||||

|
||||
|
||||
[Mở hình ở kích thước đầy đủ](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Những gì bạn có thể phân tích
|
||||
|
||||
REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với npm. Công cụ bổ sung và nền tảng chạy REA được hỗ trợ tùy thuộc vào mục tiêu:
|
||||
|
||||
| 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/guides/native/) |
|
||||
| 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](docs/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; [hướng dẫn EVM ngoại tuyến](docs/evm-bytecode.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; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](docs/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/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/guides/browser/) |
|
||||
| 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](docs/web-network-captures.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ý](docs/managed-code-analysis.md) |
|
||||
| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS; [hướng dẫn Android](docs/android-analysis.md) |
|
||||
| Phần sụn | Các vùng, kết quả trích xuất và dữ liệu chuyển sang phân tích mã máy | Binwalk / Unblob trên Linux; [hướng dẫn phần sụn](docs/firmware-analysis.md) |
|
||||
| Gói và tài nguyên | Danh sách tệp, giá trị băm, plist, cấu trúc bundle Apple và tài nguyên được trích xuất | [Hướng dẫn hiện vật phần mềm và JavaScript](docs/javascript-artifact-reconstruction.md), [ứng dụng Apple](docs/apple-application-analysis.md) |
|
||||
| Hành vi tiến trình | Đầu ra terminal, tương tác, quan sát khi thoát và hệ thống tệp, cùng so sánh các lần chạy | Linux/macOS có PTY gốc; [ghi lại tiến trình](docs/process-capture.md) |
|
||||
|
||||
Kiểm tra tĩnh JavaScript và .NET đọc các tệp được cung cấp mà không chạy ứng dụng. Ghi lại khi chạy sẽ khởi chạy hoặc tương tác với mục tiêu đã chọn bằng quyền người dùng của bạn; từng hướng dẫn mô tả tác động của nó.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Định dạng mã máy và nền tảng chạy REA được hỗ trợ khác nhau tùy trình phân tích. Xem [thiết lập Hopper và Ghidra](docs/installation.md#hopper), [hướng dẫn IDA](docs/ida-provider.md) và [hỗ trợ Ghidra thử nghiệm trên Windows](docs/windows-ghidra-p0.md). Ghidra cũng hỗ trợ [phân tích DOS 16 bit](docs/ghidra-dos.md). Để chọn trình phân tích, xem [hướng dẫn CLI](docs/cli.md#choose-a-provider). Kiểm tra [khả năng sử dụng theo bản phát hành](docs/installation.md#released-package-and-main) đối với tính năng được bổ sung sau phiên bản npm mới nhất.
|
||||
|
||||
## Các nghiên cứu điển hình
|
||||
|
||||
### DX-Ball: tái tạo phép tính phân bố âm thanh trái phải
|
||||
|
||||
Lần theo lời gọi âm thanh đến hàm hỗ trợ chuyển vị trí thành giá trị phân bố âm thanh, kiểm tra các lệnh và chuyển mã giả chưa đầy đủ sang C. Bản tái tạo vượt qua 3.205 trường hợp kiểm thử với x86 gốc và tái tạo đủ 63 byte của hàm đã biên dịch.
|
||||
|
||||
[Đọc nghiên cứu điển hình](https://rea.tools/showcase/dx-ball/) · [Kho mã tái tạo](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion: lần theo cầu nối clipboard của Electron
|
||||
|
||||
Tìm API clipboard trong tiến trình kết xuất, lần theo qua preload và IPC đến tiến trình chính, rồi kiểm tra định dạng clipboard có dữ liệu định dạng phong phú.
|
||||
|
||||
[Đọc nghiên cứu điển hình](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04: khôi phục phép tính vòng đạn trong DOS
|
||||
|
||||
Kiểm tra các lệnh 16 bit của trò chơi PC-98 gốc, khôi phục phép tính góc cố định và góc nhắm mục tiêu, rồi so sánh C++ được tái tạo với đầu ra của trình biên dịch thời đó.
|
||||
|
||||
[Đọc nghiên cứu điển hình](https://rea.tools/showcase/th04/) · [Kho mã tái tạo](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Nếu bạn dùng REA cho một mục tiêu thú vị, chúng tôi muốn biết. Chia sẻ trường hợp của bạn qua [issue](https://github.com/morluto/rea/issues) hoặc [pull request](https://github.com/morluto/rea/pulls), nêu mục tiêu, câu hỏi, cách REA giúp bạn và những gì tìm được.
|
||||
|
||||
## Câu hỏi thường gặp
|
||||
|
||||
<details>
|
||||
<summary><strong>Tác nhân nào có thể dùng REA?</strong></summary>
|
||||
|
||||
Bất kỳ tác nhân nào hỗ trợ máy chủ MCP cục bộ. Quá trình thiết lập cấu hình các [tác nhân được hỗ trợ](docs/installation.md#supported-agents); ứng dụng khách khác có thể dùng [đăng ký MCP thủ công](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Tôi có cần Hopper, Ghidra hoặc IDA không?</strong></summary>
|
||||
|
||||
Phân tích mã máy chuyên sâu sử dụng một trong những công cụ đó. Kiểm tra tĩnh JavaScript và .NET hoạt động mà không cần bộ máy phân tích mã máy. Quá trình thiết lập có thể cài Hopper sau khi được chấp thuận; Ghidra và IDA dùng bản cài đặt sẵn có. Xem [thiết lập trình phân tích](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Tôi có cần khởi chạy Hopper trước không?</strong></summary>
|
||||
|
||||
REA khởi chạy Hopper khi một thao tác cần đến nó. Trên macOS, hộp thoại lần chạy đầu có thể yêu cầu chọn chế độ dùng thử hoặc kích hoạt giấy phép. Xem [khởi động Hopper và khắc phục sự cố](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Cài skill từ skills.sh có tác dụng gì?</strong></summary>
|
||||
|
||||
Skill cung cấp hướng dẫn điều tra cho tác nhân. Dùng `rea setup` để đăng ký máy chủ MCP của REA và cài hướng dẫn tương ứng, sau đó khởi động lại tác nhân. Xem [chỉ cài skill](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA trả về mã gì?</strong></summary>
|
||||
|
||||
Phân tích mã máy trả về mã giả và hợp ngữ. Phân tích JavaScript/Electron khôi phục mô-đun cùng quan hệ giữa chúng. Tác nhân dùng các phát hiện này để viết và kiểm thử cách triển khai; các [nghiên cứu điển hình](#các-nghiên-cứu-điển-hình) cung cấp ví dụ thực hành.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA có tải ứng dụng của tôi lên không?</strong></summary>
|
||||
|
||||
REA phân tích mục tiêu cục bộ. Tác nhân nhận kết quả công cụ, và nhà cung cấp mô hình của tác nhân có chính sách dữ liệu riêng.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Tôi nên làm gì khi gặp lỗi?</strong></summary>
|
||||
|
||||
Hãy cập nhật trước; một bản phát hành gần đây có thể đã sửa lỗi.
|
||||
|
||||
Với CLI được cài bằng npm:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Với thiết lập tác nhân qua `npx`:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Nếu dùng tác nhân, hãy hoàn tất [làm mới thiết lập](#cập-nhật-rea) và khởi động lại nó. Thử lại cùng tác vụ. Nếu vấn đề vẫn còn, [mở issue](https://github.com/morluto/rea/issues) với phiên bản REA, loại mục tiêu, các bước tái hiện và đầu ra lỗi.
|
||||
|
||||
</details>
|
||||
|
||||
## Tài liệu
|
||||
|
||||
Bắt đầu với [hướng dẫn thực hành](https://rea.tools/guides/) trên trang web. Để biết tùy chọn cụ thể, điều kiện tiên quyết và hợp đồng kết quả:
|
||||
|
||||
- [Cài đặt và thiết lập](docs/installation.md): đăng ký tác nhân, cấu hình trình phân tích, cập nhật và gỡ cài đặt.
|
||||
- [Kiểm tra mức sẵn sàng và khắc phục sự cố](docs/installation.md#check-readiness-for-your-task): chẩn đoán một tác nhân hoặc bộ máy phân tích cụ thể.
|
||||
- [CLI và Evidence](docs/cli.md): lệnh, lựa chọn trình phân tích, ảnh chụp trạng thái, nhập/xuất và trạng thái thoát.
|
||||
- [Hợp đồng MCP](docs/mcp-contracts.md) và [prompt cho tác nhân](docs/mcp-prompts.md): kết quả công cụ, phiên làm việc và điều tra có hướng dẫn.
|
||||
- [Danh mục công cụ](docs/mcp-contracts.md#generated-catalog): danh sách công cụ, trình phân tích và lệnh CLI được tạo khi dựng dự án.
|
||||
- [Lộ trình](docs/roadmap.md): công việc dự kiến và theo dõi các khả năng.
|
||||
|
||||
Báo cáo lỗ hổng theo [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Đóng góp
|
||||
|
||||
Chúng tôi hoan nghênh bạn giúp phát triển REA! [Mở issue](https://github.com/morluto/rea/issues) để báo lỗi hoặc đề xuất tính năng, hoặc [gửi pull request](https://github.com/morluto/rea/pulls) để cải thiện mã hay tài liệu.
|
||||
|
||||
Xem [CONTRIBUTING.md](CONTRIBUTING.md) về thiết lập môi trường phát triển và các bước kiểm tra, [hướng dẫn kiểm thử](docs/testing.md) về các luồng xác minh và [sơ đồ kiến trúc](docs/architecture.mermaid) về cấu trúc dự án.
|
||||
|
||||
## Liên kết dự án
|
||||
|
||||
[Trang web](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Bảo mật](SECURITY.md)
|
||||
|
||||
## Lịch sử sao
|
||||
|
||||
🎉 **20.000 sao trên GitHub — xin cảm ơn!**
|
||||
|
||||
Cảm ơn mọi người đã sử dụng REA, báo lỗi, kiểm thử bản dựng và đóng góp bản sửa lỗi.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Lịch sử sao GitHub của REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Miễn trừ trách nhiệm
|
||||
|
||||
REA cung cấp công cụ cho nghiên cứu dịch ngược, phân tích và tái tạo hợp pháp. Bạn có trách nhiệm có được các quyền cho phép cần thiết và tuân thủ luật áp dụng. Dự án không ủng hộ việc sử dụng trái pháp luật hoặc không được cho phép.
|
||||
|
||||
## Giấy phép
|
||||
|
||||
[MIT](LICENSE)
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA:逆向分析一切
|
||||
|
||||
### 通過一個 MCP 服務,逆向分析二進位檔案、應用程式和執行階段行為。
|
||||
|
||||
**看到喜歡的功能,弄清它的工作原理,深入到二進位層面。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[網站](https://rea.tools/) · [指南](https://rea.tools/guides/) · [案例](https://rea.tools/showcase/)**
|
||||
|
||||
[快速開始](#快速開始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什麼](#可以分析什麼) · [案例](#案例) · [常見問題](#常見問題) · [文件](#文件)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中啟動分析橋,檢查原生二進位檔案" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA 社群">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>加入逆向工程社群</strong>
|
||||
</a><br />
|
||||
<sub>Discord · 問答 · 成果分享</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
在應用中看到一個想加入自己產品的功能?讓 AI 代理用 REA 調查它。即使沒有原始碼,AI 代理也可以檢查應用、解釋功能的工作方式、展示證據,並為你的專案實現類似功能。
|
||||
|
||||
REA 將 AI 代理連接到分析工具,用於檢查原生二進位檔案、JavaScript 和 Electron 應用、.NET 程式集以及網站。你也可以在終端中使用同樣的工具。分析在本機執行,結果包含支撐各項結論的證據和相關限制。
|
||||
|
||||
設定流程會向 AI 代理註冊 REA,並安裝對應的工作流程指引。原生分析可以使用已有的 Hopper 或 Ghidra;設定流程也可以在你核准後安裝 Hopper。靜態 JavaScript 分析不需要這兩個引擎。
|
||||
|
||||
> **[訪問 REA 網站](https://rea.tools/)**,查看設定說明、圖解指南和真實案例。
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 設定 AI 代理
|
||||
|
||||
安裝 Node.js 和 npm 後,執行:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
選擇 AI 代理,檢查計劃中的變更並核准。設定流程會添加 REA 的 MCP 服務和對應的工作流程指引,並備份已有組態。完成後重新啟動 AI 代理。
|
||||
|
||||
設定流程支援 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他 AI 代理](docs/installation.md#supported-agents)。提供者組態和手動註冊 MCP 的方法見[安裝與設定](docs/installation.md)。
|
||||
|
||||
### 詢問 AI 代理
|
||||
|
||||
```text
|
||||
弄清 Notes 應用的搜尋功能如何工作,展示證據,並為我的專案實現類似功能。
|
||||
```
|
||||
|
||||
把 Notes 替換為目標應用,並描述你想瞭解的功能。
|
||||
|
||||
### 使用終端
|
||||
|
||||
檢查已解包的 JavaScript/Electron 應用目錄或 ASAR 檔案:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
結果包含模組、匯入、Electron 邊界及相關證據。將路徑替換為你的目標路徑,例如 Windows 上的 `"D:/apps/example"`。
|
||||
|
||||
如果需要經常使用,可以安裝 `rea` 命令:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
原生分析需要先設定分析提供者。原生命令、提供者選擇、快照和腳本用法見 [CLI 與 Evidence 指南](docs/cli.md)。
|
||||
|
||||
### 更新 REA
|
||||
|
||||
REA 更新較快,新版本經常包含錯誤修復。請將安裝的版本保持最新。
|
||||
|
||||
通過 npm 安裝的 CLI 使用:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
要更新 AI 代理註冊和 skill,請執行更新命令輸出的設定命令。
|
||||
|
||||
如果使用 `npx`,通過以下命令更新 AI 代理設定:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
檢查設定變更並重新啟動 AI 代理。單次 CLI 操作可使用 `npx rea-agents@latest`,後接所需命令。
|
||||
|
||||
## REA 的工作原理
|
||||
|
||||
AI 代理通過 MCP 呼叫 REA,檢查目標並追蹤相關程式碼。REA 返回發現及對應證據。AI 代理據此提出後續問題、解釋行為,或編寫並測試實現。CLI 命令使用同樣的工作流程。
|
||||
|
||||

|
||||
|
||||
[打開完整尺寸的圖示](website/public/assets/figures/rea-investigation-flow.svg)。
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## 可以分析什麼
|
||||
|
||||
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。額外工具和主機支援取決於分析目標:
|
||||
|
||||
| 目標 | REA 返回的內容 | 要求與指南 |
|
||||
| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
|
||||
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](docs/binary-diagnostics.md) |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | 本地原始位元組/十六進位輸入載體;[離線 EVM 指南](docs/evm-bytecode.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | 呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/guides/javascript/) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/guides/browser/) |
|
||||
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](docs/web-network-captures.md) |
|
||||
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](docs/managed-code-analysis.md) |
|
||||
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS 上的無界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
|
||||
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](docs/firmware-analysis.md) |
|
||||
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 應用](docs/apple-application-analysis.md) |
|
||||
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](docs/process-capture.md) |
|
||||
|
||||
靜態 JavaScript 和 .NET 檢查讀取提供的檔案,不執行應用。執行階段擷取會以你的使用者權限執行目標或與之互動;各執行階段指南說明瞭具體影響。
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
不同提供者支援的原生格式和主機系統有所不同。請參閱 [Hopper 與 Ghidra 設定](docs/installation.md#hopper)、[IDA 指南](docs/ida-provider.md)和[實驗性 Windows Ghidra 支援](docs/windows-ghidra-p0.md)。Ghidra 也支援 [16 位 DOS 分析](docs/ghidra-dos.md)。提供者選擇見 [CLI 指南](docs/cli.md#choose-a-provider)。對於最新 npm 版本發佈後新增的功能,請查看[發佈可用性](docs/installation.md#released-package-and-main)。
|
||||
|
||||
## 案例
|
||||
|
||||
### DX-Ball:重建聲像計算
|
||||
|
||||
沿聲音呼叫追蹤到根據位置計算聲像的輔助函數,檢查指令,將不完整的偽程式碼轉為 C。重建結果通過了 3,205 個原始 x86 測試用例,並復現了編譯後函數的全部 63 個位元組。
|
||||
|
||||
[閱讀案例](https://rea.tools/showcase/dx-ball/) ·
|
||||
[重建專案儲存庫](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion:追蹤 Electron 剪貼簿橋
|
||||
|
||||
找到渲染處理程序的剪貼簿 API,沿 preload 和 IPC 追蹤到主處理程序,並檢查富格式剪貼簿資料。
|
||||
|
||||
[閱讀案例](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04:恢復 DOS 環形子彈的計算
|
||||
|
||||
檢查原始 PC-98 遊戲的 16 位指令,恢復固定角度和瞄準角度的計算,並將重建的 C++ 與當年的編譯器輸出進行對比。
|
||||
|
||||
[閱讀案例](https://rea.tools/showcase/th04/) ·
|
||||
[重建專案儲存庫](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
如果你用 REA 分析了有趣的目標,我們很想瞭解。歡迎通過 [issue](https://github.com/morluto/rea/issues) 或 [pull request](https://github.com/morluto/rea/pulls) 分享案例,說明目標、你的問題、REA 如何提供幫助,以及你的發現。
|
||||
|
||||
## 常見問題
|
||||
|
||||
<details>
|
||||
<summary><strong>哪些 AI 代理可以使用 REA?</strong></summary>
|
||||
|
||||
任何支援本地 MCP 服務的 AI 代理都可以使用。設定流程會設定[受支援的 AI 代理](docs/installation.md#supported-agents);其他客戶端可通過[手動註冊 MCP](docs/installation.md#mcp-registry) 使用。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>需要 Hopper、Ghidra 或 IDA 嗎?</strong></summary>
|
||||
|
||||
深入的原生分析需要其中一個引擎。靜態 JavaScript 和 .NET 檢查不需要原生分析引擎。設定流程可以在你核准後安裝 Hopper;Ghidra 和 IDA 則使用已有安裝。見[提供者設定](docs/installation.md#hopper)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>需要先啟動 Hopper 嗎?</strong></summary>
|
||||
|
||||
REA 會在操作需要時啟動 Hopper。在 macOS 上,首次執行時可能會出現對話框,讓你選擇演示模式或啟用授權。見 [Hopper 啟動與故障排查](docs/installation.md#launcher-paths-and-troubleshooting)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>從 skills.sh 安裝 skill 有什麼作用?</strong></summary>
|
||||
|
||||
skill 為 AI 代理提供調查指引。使用 `rea setup` 註冊 REA 的 MCP 服務並安裝對應的指引,然後重新啟動 AI 代理。見[僅安裝 skill](docs/installation.md#skill-only-installation)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA 返回什麼程式碼?</strong></summary>
|
||||
|
||||
原生分析返回偽程式碼和組合語言。JavaScript/Electron 分析恢復模組及其關係。AI 代理利用這些發現編寫並測試實現;[案例](#案例)提供了完整示例。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA 會上傳我的應用嗎?</strong></summary>
|
||||
|
||||
REA 在本機分析目標。AI 代理會接收工具結果,其模型提供商有自己的資料政策。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>遇到錯誤時該怎麼辦?</strong></summary>
|
||||
|
||||
先更新;最近的版本可能已經修復了問題。
|
||||
|
||||
通過 npm 安裝的 CLI 使用:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
通過 `npx` 設定 AI 代理時使用:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
如果正在使用 AI 代理,請完成[設定更新](#更新-rea)並重新啟動 AI 代理。重試同一任務。如果問題仍然存在,請[提交 issue](https://github.com/morluto/rea/issues),附上 REA 版本、目標類型、復現步驟和錯誤輸出。
|
||||
|
||||
</details>
|
||||
|
||||
## 文件
|
||||
|
||||
可以先閱讀網站上的[實操指南](https://rea.tools/guides/)。具體選項、前提條件和結果契約見:
|
||||
|
||||
- [安裝與設定](docs/installation.md):AI 代理註冊、提供者組態、更新和卸載。
|
||||
- [就緒檢查與故障排查](docs/installation.md#check-readiness-for-your-task):診斷特定 AI 代理或分析引擎。
|
||||
- [CLI 與 Evidence](docs/cli.md):命令、提供者選擇、快照、匯入/匯出和退出狀態。
|
||||
- [MCP 契約](docs/mcp-contracts.md)和[AI 代理提示詞](docs/mcp-prompts.md):工具結果、會話和引導式調查。
|
||||
- [工具目錄](docs/mcp-contracts.md#generated-catalog):建置時生成的工具、提供者和 CLI 命令清單。
|
||||
- [路線圖](docs/roadmap.md):計劃中的工作和能力跟蹤。
|
||||
|
||||
請按照 [SECURITY.md](SECURITY.md) 報告漏洞。
|
||||
|
||||
## 參與貢獻
|
||||
|
||||
歡迎幫助改進 REA!你可以[提交 issue](https://github.com/morluto/rea/issues) 報告錯誤或建議功能,也可以[提交 pull request](https://github.com/morluto/rea/pulls) 改進程式碼或文件。
|
||||
|
||||
開發設定和檢查見 [CONTRIBUTING.md](CONTRIBUTING.md),驗證流程見[測試指南](docs/testing.md),專案結構見[架構圖](docs/architecture.mermaid)。
|
||||
|
||||
## 專案連結
|
||||
|
||||
[網站](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [安全](SECURITY.md)
|
||||
|
||||
## Star 歷史
|
||||
|
||||
🎉 **GitHub Star 達到 20,000 個,感謝大家!**
|
||||
|
||||
感謝每一位使用 REA、報告錯誤、測試建置和貢獻修復的朋友。
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="REA 的 GitHub Star 歷史" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 免責聲明
|
||||
|
||||
REA 為合法的逆向工程研究、分析和重建提供工具。你有責任取得所需授權並遵守適用法律。本專案不認同非法或未經授權的使用。
|
||||
|
||||
## 授權條款
|
||||
|
||||
[MIT](LICENSE)
|
||||
+183
-231
@@ -1,27 +1,36 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · **简体中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [العربية](README_ar.md)
|
||||
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA:逆向工程一切
|
||||
# REA:逆向分析一切
|
||||
|
||||
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
|
||||
### 通过一个 MCP 服务,逆向分析二进制文件、应用程序和运行时行为。
|
||||
|
||||
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
|
||||
**看到喜欢的功能,弄清它的工作原理,深入到二进制层面。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](#调查工具目录)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
**[网站](https://rea.tools/) · [指南](https://rea.tools/guides/) · [案例](https://rea.tools/showcase/)**
|
||||
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
[快速开始](#快速开始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什么](#可以分析什么) · [案例](#案例) · [常见问题](#常见问题) · [文档](#文档)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥,检查原生二进制文件" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA 社区">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
@@ -35,301 +44,244 @@
|
||||
|
||||
<br />
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥并检查原生二进制文件" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
看到某个应用中想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
|
||||
在应用中看到一个想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
|
||||
|
||||
REA 提供原生二进制、JavaScript 与 Electron 应用、.NET 程序集和网站的分析工具。你可以从智能体或终端使用它们。分析在本机运行,结果会说明证据和限制。
|
||||
REA 将智能体连接到分析工具,用于检查原生二进制文件、JavaScript 和 Electron 应用、.NET 程序集以及网站。你也可以在终端中使用同样的工具。分析在本机运行,结果包含支撑各项结论的证据和相关限制。
|
||||
|
||||
Setup 可以配置智能体、连接已安装的 Hopper 或 Ghidra,也可以在你批准后安装 Hopper。
|
||||
设置流程会向智能体注册 REA,并安装匹配的工作流指引。原生分析可以使用已有的 Hopper 或 Ghidra;设置流程也可以在你批准后安装 Hopper。静态 JavaScript 分析不需要这两个引擎。
|
||||
|
||||
## 直接询问智能体
|
||||
|
||||
完成[设置](#快速开始)后,重启智能体并提出问题:
|
||||
|
||||
```text
|
||||
解释“备忘录”应用的搜索功能,展示证据,然后为我的项目实现类似功能。
|
||||
```
|
||||
|
||||
把“备忘录”替换成你想了解的应用,也可以先请求概览。
|
||||
|
||||
## 从二进制到行为
|
||||
|
||||
| 反编译 | 理解 | 重建 |
|
||||
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| 打开原生应用或可执行文件,恢复过程、伪代码、汇编、字符串、符号、段和元数据。 | 沿调用者、被调用者、交叉引用和调用图追踪,直到智能体能够解释功能或算法的实际工作方式。 | 将智能体学到的内容变成适合你的技术栈、界面和需求的产品功能。 |
|
||||
|
||||
REA 让调查始终以二进制证据为依据。它不会声称能恢复原始源代码,也不会自动克隆整个应用。
|
||||
|
||||
## 为什么选择 REA
|
||||
|
||||
| | |
|
||||
| ---------------- | -------------------------------------------------------------------- |
|
||||
| **为智能体设计** | 直接询问应用的行为,让智能体去检查,而不是猜测。 |
|
||||
| **CLI 与 MCP** | 在终端或智能体中使用同一套逆向工程能力。 |
|
||||
| **引导式设置** | 配置智能体、连接已有分析工具,或在你批准后安装 Hopper。 |
|
||||
| **从洞察到代码** | 理解功能后,在同一次编程会话中构建属于你自己的版本。 |
|
||||
| **本地运行** | 分析在受支持的本机系统上运行;REA 不会把二进制上传到托管式分析服务。 |
|
||||
| **保留上下文** | 连续调查多个应用,无需为每个问题重新开始整个分析过程。 |
|
||||
> **[访问 REA 网站](https://rea.tools/)**,查看设置说明、图解指南和真实案例。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 运行设置(推荐)
|
||||
### 设置智能体
|
||||
|
||||
配置 REA 与智能体的连接:
|
||||
安装 Node.js 和 npm 后,运行:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Setup 首先让你多选要连接的智能体。已有 REA 注册默认选中;仅被检测到的客户端不会自动选中,也可以手动选择尚未配置的客户端。查看具体路径和变更后再批准。所选智能体默认安装 REA 工作流;Hopper 是单独的可选操作,需要单独批准。Setup 也可以记录现有 Ghidra 的路径。
|
||||
选择智能体,检查计划中的变更并批准。设置流程会添加 REA 的 MCP 服务和匹配的工作流指引,并备份已有配置。完成后重启智能体。
|
||||
|
||||
Setup 会先展示变更并备份已有配置。要求与更多选项见[安装与设置](docs/installation.md)。
|
||||
设置流程支持 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他智能体](docs/installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](docs/installation.md)。
|
||||
|
||||
### 使用智能体
|
||||
### 询问智能体
|
||||
|
||||
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
|
||||
|
||||
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
|
||||
|
||||
### AI 编程助手技能(可选)
|
||||
|
||||
为你的 AI 编程助手添加 skill,以获得更丰富的上下文:
|
||||
|
||||
```bash
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```text
|
||||
弄清 Notes 应用的搜索功能如何工作,展示证据,并为我的项目实现类似功能。
|
||||
```
|
||||
|
||||
该 skill 提供 REA 的调查工作流。先运行上文的 setup,把 REA 连接到你的智能体并配置分析工具。Setup 默认会安装版本匹配的 skill;这条命令安装的是仓库版本。
|
||||
把 Notes 替换为目标应用,并描述你想了解的功能。
|
||||
|
||||
对于已解包的 JavaScript/Electron 应用目录或 ASAR,无需 MCP 配置,也无需原生引擎:
|
||||
### 使用终端
|
||||
|
||||
检查已解包的 JavaScript/Electron 应用目录或 ASAR 文件:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
把路径换成你的目标(例如 Windows 上的 `"D:/apps/example"`)。这条静态流程支持此类路径。原生分析仍需要先配置对应的分析引擎。
|
||||
结果包含模块、导入、Electron 边界及相关证据。将路径替换为你的目标路径,例如 Windows 上的 `"D:/apps/example"`。
|
||||
|
||||
### 安装 rea 命令
|
||||
|
||||
安装命令行工具:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
需要预先安装 Node.js 和 npm。在终端中运行时,安装程序会安装 `rea` 并启动设置。
|
||||
|
||||
也可以用 npm 安装,再运行设置:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### 系统要求
|
||||
|
||||
- macOS 12 或更高版本
|
||||
- Ubuntu 24.04+、Fedora 41+、64 位 Arch Linux 或 CachyOS
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
|
||||
|
||||
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
|
||||
|
||||
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.x,以及该安装在 `application.java.min` 到 `application.java.max` 中声明的完整 64 位 JDK,然后配置 REA 使用它们。当前 12.1 发行版要求 JDK 21 或更新版本,并且不设置上限。桥接在 Ghidra 12.1.4 和 JDK 21 上完成验证。macOS 还需要对应架构的原生反编译器。
|
||||
|
||||
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
|
||||
|
||||
仓库 main 分支与 npm 4.1.0 已包含实验性的 Windows x64 Ghidra P0 支持,适用于本地 NTFS 上的原生 x86-64 PE 应用(非托管、非 DLL),并附带 Job Object、私有 DACL 与路径准入控制。请先确认[发布边界](docs/installation.md#released-package-and-main),再假定旧版 npm 包具备该能力。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md) 的前置条件与已验证范围。
|
||||
|
||||
### 故障排除
|
||||
|
||||
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
|
||||
|
||||
Linux 上 REA 会优先使用可执行的 `/opt/hopper/bin/Hopper`;如果不可用,则自动检查 `~/.local/share/rea/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,请对实际 Hopper 路径运行 `ldd /absolute/path/to/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
|
||||
|
||||
### 更新与卸载
|
||||
|
||||
- `rea update` 更新当前 REA 安装。
|
||||
- `rea uninstall` 移除 REA 管理的智能体配置项和工作流文件,保留 Hopper。
|
||||
- `rea uninstall --purge-data` 还会删除 REA 的缓存和状态。仅在需要移除这些数据时使用。
|
||||
|
||||
## 当前状态
|
||||
|
||||
仓库当前能力与平台要求见[英文支持指南](README.md#current-status)。main 分支可能领先于 [npm 发布版](docs/installation.md#released-package-and-main)。
|
||||
|
||||
- Ghidra 在 Linux x64、macOS x64/arm64 以及实验性的 Windows x64 P0 边界上提供 25 个只读操作。Linux/macOS 额外支持原子化的会话级函数注释。Windows P0 为只读;Ghidra 不提供 GUI 控制。
|
||||
- 静态 Android 检查需要自备 JADX/Java,前置条件独立;真机验证覆盖 Linux。详见 [Android 分析](docs/android-analysis.md)。
|
||||
- 浏览器、Electron 与进程请求需直接声明目标、动作与生命周期,并依赖宿主的实际访问权限,无需 REA 另行授权。Setup 的配置写入与 Hopper 安装仍需逐项批准确切计划。
|
||||
- `rea capabilities` 描述的是二进制会话操作,而非全部应用/运行时工具。请以已连接 MCP 工具列表与相关指南为准。
|
||||
|
||||
## 一个提示词,完成一次完整调查
|
||||
|
||||
```text
|
||||
逆向工程“备忘录”应用,找到离线搜索功能的工作方式,解释其控制流,
|
||||
并使用 TypeScript 和 SQLite 为我的项目构建一个版本。
|
||||
```
|
||||
|
||||
| 步骤 | 智能体的操作 | REA 工具 |
|
||||
| ---: | ---------------------- | ---------------------------------------------------------------- |
|
||||
| 1 | 打开并识别二进制文件 | `open_binary`, `binary_overview` |
|
||||
| 2 | 搜索可能的离线搜索线索 | `search_strings`, `search_procedures`, `list_names` |
|
||||
| 3 | 将线索连接到可执行代码 | `find_xrefs_to_name`, `xrefs`, `procedure_callers` |
|
||||
| 4 | 重建相关控制流 | `get_call_graph`, `procedure_callees`, `procedure_info` |
|
||||
| 5 | 反编译相关程序 | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
|
||||
| 6 | 在你的项目中构建该功能 | 适合你的技术栈、产品和需求的代码 |
|
||||
|
||||
REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常规文件编辑与测试工具完成。
|
||||
|
||||
## 智能体可以完成什么
|
||||
|
||||
- 在没有源代码时解释某项功能的实现方式。
|
||||
- 重建应用的身份验证、存储、更新或网络流程。
|
||||
- 恢复足够的结构,以记录未公开的格式或接口。
|
||||
- 从字符串或符号追踪到实现可疑行为的代码。
|
||||
- 在一个会话中切换两个应用版本并比较实现路径。
|
||||
- 调查你喜欢的功能,并为自己的产品构建量身定制的版本。
|
||||
- 将恢复的行为转换为产品功能、测试、迁移说明、移植代码或互操作替代品。
|
||||
- 分析 Swift 和 Objective-C 元数据。
|
||||
- 在 Hopper 中留下名称、注释与书签,使人与智能体的分析互相增强。
|
||||
|
||||
## 调查工具目录
|
||||
|
||||
| 工具类别 | 数量 | 用途 |
|
||||
| ----------------- | ---: | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| 原生检查 | 41 | 函数、伪代码、汇编、字符串、符号、调用、引用、注释、字节读取与文件偏移 |
|
||||
| 调查工作流 | 14 | 应用概览、函数档案、原生 API 与分发、批量反编译、功能追踪、调用路径与调用图、Swift 与 Objective-C 发现 |
|
||||
| 原生二进制工具 | 10 | macOS 的 Mach-O 元数据、签名、plist、架构、Swift 符号与 LLDB 调用观察;Linux 的 ELF 布局、静态保护证据与历史崩溃记录 |
|
||||
| 产物图 | 7 | 目录与软件包清单、编译后的 Interface Builder 文件、Apple 资源目录、Mach-O dylib 加载解析、提取与离线 EVM 接口推断 |
|
||||
| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 |
|
||||
| 固件 | 2 | Linux 固件区域检查与显式提取 |
|
||||
| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 |
|
||||
| 浏览器观察 | 12 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
|
||||
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
|
||||
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
|
||||
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
|
||||
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
|
||||
|
||||
## 路线图
|
||||
|
||||
接下来的工作包括更多原生目标验证、JavaScript 与 .NET 跨版本比较,以及运行时观察和重建验证。当前设置已支持选择智能体集成和 Hopper 安装;安装其他分析工具仍是后续工作。详见[安装路线图](docs/roadmap.md)和[分析工具评估](docs/provider-evaluation.md)。
|
||||
|
||||
## 与其他编程智能体一起使用
|
||||
|
||||
Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.0.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
## 工作原理
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Agent["编程智能体"] --> REA["REA<br/>CLI + MCP"]
|
||||
Terminal["终端"] --> REA
|
||||
REA --> Hopper["Hopper 分析操作"]
|
||||
Hopper --> App["你的应用"]
|
||||
REA --> Ghidra["Ghidra 只读分析<br/>清单 + 函数分析"]
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
|
||||
|
||||
## CLI
|
||||
|
||||
上面的智能体工作流是使用 REA 最简单的方式。如果只想在终端中快速了解一个应用:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
运行 `npx -y rea-agents@latest --help` 查看直接反编译和其他选项。
|
||||
|
||||
也可以全局安装 `rea` 命令:
|
||||
如果需要经常使用,可以安装 `rea` 命令:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
rea mcp
|
||||
```
|
||||
|
||||
REA 可以直接打开 Mac 的 `.app` 文件夹。如果智能体找不到应用,请告诉它应用安装在哪里。
|
||||
原生分析需要先配置分析提供方。原生命令、提供方选择、快照和脚本用法见 [CLI 与 Evidence 指南](docs/cli.md)。
|
||||
|
||||
## Hopper 应用行为
|
||||
### 更新 REA
|
||||
|
||||
REA 会在需要时启动 Hopper,无需预先运行。Hopper 启动器内部会激活应用,因此打开目标时 Hopper 可能出现在其他窗口前。REA 会请求 macOS 在后台启动 Hopper,但无法保证窗口始终位于后台。
|
||||
REA 更新较快,新版本经常包含错误修复。请保持安装的版本为最新。
|
||||
|
||||
REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选择对话框。其他 Hopper 或 macOS 对话框仍可能需要人工响应。关闭 REA 会话会终止桥并删除私有套接字目录,但不会退出用户正在使用的 Hopper 应用。
|
||||
通过 npm 安装的 CLI 使用:
|
||||
|
||||
## 安全模型
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
|
||||
要更新智能体注册和 skill,请运行更新命令输出的设置命令。
|
||||
|
||||
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
|
||||
如果使用 `npx`,通过以下命令更新智能体设置:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
检查设置变更并重启智能体。单次 CLI 操作可使用 `npx rea-agents@latest`,后接所需命令。
|
||||
|
||||
## REA 的工作原理
|
||||
|
||||
智能体通过 MCP 调用 REA,检查目标并追踪相关代码。REA 返回发现及对应证据。智能体据此提出后续问题、解释行为,或编写并测试实现。CLI 命令使用同样的工作流。
|
||||
|
||||

|
||||
|
||||
[打开完整尺寸的图示](website/public/assets/figures/rea-investigation-flow.svg)。
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## 可以分析什么
|
||||
|
||||
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
|
||||
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](docs/binary-diagnostics.md) |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | 本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | 调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/guides/javascript/) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/guides/browser/) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](docs/web-network-captures.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](docs/managed-code-analysis.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
|
||||
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](docs/firmware-analysis.md) |
|
||||
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 应用](docs/apple-application-analysis.md) |
|
||||
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](docs/process-capture.md) |
|
||||
|
||||
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
不同提供方支持的原生格式和主机系统有所不同。请参阅 [Hopper 与 Ghidra 设置](docs/installation.md#hopper)、[IDA 指南](docs/ida-provider.md)和[实验性 Windows Ghidra 支持](docs/windows-ghidra-p0.md)。Ghidra 也支持 [16 位 DOS 分析](docs/ghidra-dos.md)。提供方选择见 [CLI 指南](docs/cli.md#choose-a-provider)。对于最新 npm 版本发布后新增的功能,请查看[发布可用性](docs/installation.md#released-package-and-main)。
|
||||
|
||||
## 案例
|
||||
|
||||
### DX-Ball:重建声像计算
|
||||
|
||||
沿声音调用追踪到根据位置计算声像的辅助函数,检查指令,将不完整的伪代码转为 C。重建结果通过了 3,205 个原始 x86 测试用例,并复现了编译后函数的全部 63 个字节。
|
||||
|
||||
[阅读案例](https://rea.tools/showcase/dx-ball/) ·
|
||||
[重建项目仓库](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion:追踪 Electron 剪贴板桥
|
||||
|
||||
找到渲染进程的剪贴板 API,沿 preload 和 IPC 追踪到主进程,并检查富格式剪贴板数据。
|
||||
|
||||
[阅读案例](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04:恢复 DOS 环形子弹的计算
|
||||
|
||||
检查原始 PC-98 游戏的 16 位指令,恢复固定角度和瞄准角度的计算,并将重建的 C++ 与当年的编译器输出进行对比。
|
||||
|
||||
[阅读案例](https://rea.tools/showcase/th04/) ·
|
||||
[重建项目仓库](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
如果你用 REA 分析了有趣的目标,我们很想了解。欢迎通过 [issue](https://github.com/morluto/rea/issues) 或 [pull request](https://github.com/morluto/rea/pulls) 分享案例,说明目标、你的问题、REA 如何提供帮助,以及你的发现。
|
||||
|
||||
## 常见问题
|
||||
|
||||
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>哪些智能体可以使用 REA?</strong></summary>
|
||||
|
||||
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
|
||||
任何支持本地 MCP 服务的智能体都可以使用。设置流程会配置[受支持的智能体](docs/installation.md#supported-agents);其他客户端可通过[手动注册 MCP](docs/installation.md#mcp-registry) 使用。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA 是否包含 Hopper?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>需要 Hopper、Ghidra 或 IDA 吗?</strong></summary>
|
||||
|
||||
不包含。Setup 可以为你安装 Hopper,但 Hopper 仍是需要单独授权的软件。REA 提供 CLI、MCP 服务器和面向智能体的工作流。
|
||||
深入的原生分析需要其中一个引擎。静态 JavaScript 和 .NET 检查不需要原生分析引擎。设置流程可以在你批准后安装 Hopper;Ghidra 和 IDA 则使用已有安装。见[提供方设置](docs/installation.md#hopper)。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA 会上传我的二进制文件吗?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>需要先启动 Hopper 吗?</strong></summary>
|
||||
|
||||
REA 不提供托管分析服务,而是通过本地 Unix 套接字把操作交给 Hopper。你的智能体或模型服务商可能有自己的数据政策,请单独核查。
|
||||
REA 会在操作需要时启动 Hopper。在 macOS 上,首次运行时可能会出现对话框,让你选择演示模式或激活许可证。见 [Hopper 启动与故障排查](docs/installation.md#launcher-paths-and-troubleshooting)。
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary><strong>REA 能恢复原始源代码吗?</strong></summary>
|
||||
<details>
|
||||
<summary><strong>从 skills.sh 安装 skill 有什么作用?</strong></summary>
|
||||
|
||||
不能保证。REA 提供伪代码、汇编、符号、字符串、元数据和关系,智能体可据此解释或兼容地重建观察到的行为。
|
||||
skill 为智能体提供调查指引。使用 `rea setup` 注册 REA 的 MCP 服务并安装匹配的指引,然后重启智能体。见[仅安装 skill](docs/installation.md#skill-only-installation)。
|
||||
|
||||
</details>
|
||||
|
||||
## 开发
|
||||
<details>
|
||||
<summary><strong>REA 返回什么代码?</strong></summary>
|
||||
|
||||
开发环境、架构、测试和发布说明请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
||||
原生分析返回伪代码和汇编。JavaScript/Electron 分析恢复模块及其关系。智能体利用这些发现编写并测试实现;[案例](#案例)提供了完整示例。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA 会上传我的应用吗?</strong></summary>
|
||||
|
||||
REA 在本机分析目标。智能体会接收工具结果,其模型提供商有自己的数据政策。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>遇到错误时该怎么办?</strong></summary>
|
||||
|
||||
先更新;最近的版本可能已经修复了问题。
|
||||
|
||||
通过 npm 安装的 CLI 使用:
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
通过 `npx` 设置智能体时使用:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
如果正在使用智能体,请完成[设置更新](#更新-rea)并重启智能体。重试同一任务。如果问题仍然存在,请[提交 issue](https://github.com/morluto/rea/issues),附上 REA 版本、目标类型、复现步骤和错误输出。
|
||||
|
||||
</details>
|
||||
|
||||
## 文档
|
||||
|
||||
可以先阅读网站上的[实操指南](https://rea.tools/guides/)。具体选项、前提条件和结果契约见:
|
||||
|
||||
- [安装与设置](docs/installation.md):智能体注册、提供方配置、更新和卸载。
|
||||
- [就绪检查与故障排查](docs/installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。
|
||||
- [CLI 与 Evidence](docs/cli.md):命令、提供方选择、快照、导入/导出和退出状态。
|
||||
- [MCP 契约](docs/mcp-contracts.md)和[智能体提示词](docs/mcp-prompts.md):工具结果、会话和引导式调查。
|
||||
- [工具目录](docs/mcp-contracts.md#generated-catalog):构建时生成的工具、提供方和 CLI 命令清单。
|
||||
- [路线图](docs/roadmap.md):计划中的工作和能力跟踪。
|
||||
|
||||
请按照 [SECURITY.md](SECURITY.md) 报告漏洞。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎帮助改进 REA!你可以[提交 issue](https://github.com/morluto/rea/issues) 报告错误或建议功能,也可以[提交 pull request](https://github.com/morluto/rea/pulls) 改进代码或文档。
|
||||
|
||||
开发设置和检查见 [CONTRIBUTING.md](CONTRIBUTING.md),验证流程见[测试指南](docs/testing.md),项目结构见[架构图](docs/architecture.mermaid)。
|
||||
|
||||
## 项目链接
|
||||
|
||||
[网站](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [安全](SECURITY.md)
|
||||
|
||||
## Star 历史
|
||||
|
||||
🎉 **GitHub Star 突破 20,000,感谢大家!**
|
||||
🎉 **GitHub Star 达到 20,000 个,感谢大家!**
|
||||
|
||||
感谢每一位使用 REA、反馈问题、测试构建和贡献修复的朋友。
|
||||
感谢每一位使用 REA、报告错误、测试构建和贡献修复的朋友。
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="REA GitHub Star 历史" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="REA 的 GitHub Star 历史" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 免责声明
|
||||
|
||||
REA 为合法的逆向工程研究、分析和重建提供工具。你有责任取得所需授权并遵守适用法律。项目不支持非法或未经授权的使用。
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
@@ -25,6 +25,11 @@ class ReaJadxBridge {
|
||||
private record MethodSnapshot(String name, String shortId, Map<String, Object> summary) {}
|
||||
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary) {}
|
||||
|
||||
private static final class InputFailure extends IllegalArgumentException {
|
||||
final String field;
|
||||
InputFailure(String field, String message) { super(message); this.field = field; }
|
||||
}
|
||||
|
||||
private ReaJadxBridge(SessionHolder holder) { this.holder = holder; }
|
||||
|
||||
private static CallToolResult text(String value, boolean failed) {
|
||||
@@ -100,8 +105,9 @@ class ReaJadxBridge {
|
||||
}
|
||||
String name = string(args, "class_name");
|
||||
List<ClassSnapshot> candidates = classes.get(name);
|
||||
if (candidates == null) throw new IllegalArgumentException("Exact class not found: " + name);
|
||||
if (candidates.size() != 1) throw new IllegalArgumentException("Ambiguous metadata class identity: " + name
|
||||
if (candidates == null) throw new InputFailure("class_name", "Exact class not found: " + name
|
||||
+ "; use search_android_classes to discover fully qualified names.");
|
||||
if (candidates.size() != 1) throw new InputFailure("class_name", "Ambiguous metadata class identity: " + name
|
||||
+ "; raw identities: " + candidates.stream().map(candidate -> candidate.node().getRawName()).toList());
|
||||
return candidates.get(0);
|
||||
}
|
||||
@@ -109,7 +115,8 @@ class ReaJadxBridge {
|
||||
private List<MethodSnapshot> methods(ClassSnapshot cls, JsonObject args) {
|
||||
String name = string(args, "method_name");
|
||||
List<MethodSnapshot> result = cls.methods().stream().filter(method -> method.name().equals(name)).toList();
|
||||
if (result.isEmpty()) throw new IllegalArgumentException("Method not found: " + name);
|
||||
if (result.isEmpty()) throw new InputFailure("method_name", "Method not found: " + name
|
||||
+ "; use inspect_android_class to discover available methods.");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -141,7 +148,7 @@ class ReaJadxBridge {
|
||||
if (name.equals("get_method_body")) {
|
||||
List<MethodSnapshot> overloads = methods(cls, args);
|
||||
int index = integer(args, "overload_index", 0);
|
||||
if (index >= overloads.size()) throw new IllegalArgumentException("overload_index out of range: " + index);
|
||||
if (index >= overloads.size()) throw new InputFailure("overload_index", "overload_index out of range: " + index);
|
||||
JavaMethod method = method(cls, overloads.get(index));
|
||||
JadxSession.SmartCode code = current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
|
||||
return json(Map.of("class_name", cls.summary().get("full_name"),
|
||||
@@ -155,7 +162,7 @@ class ReaJadxBridge {
|
||||
if (name.equals("get_xrefs_to_class")) uses = cls.node().getUseIn();
|
||||
else {
|
||||
List<MethodSnapshot> overloads = methods(cls, args);
|
||||
if (overloads.size() != 1) throw new IllegalArgumentException("Method references require a unique overload");
|
||||
if (overloads.size() != 1) throw new InputFailure("method_name", "Method references require a unique overload");
|
||||
target += "." + overloads.get(0).name();
|
||||
uses = method(cls, overloads.get(0)).getUseIn();
|
||||
}
|
||||
@@ -211,6 +218,8 @@ class ReaJadxBridge {
|
||||
tool.getInputSchema().getRequired(), null), description, null, null, null, null, null, null);
|
||||
server.addTool(narrowed, (connection, request, continuation) -> {
|
||||
try { return bridge.handle(name, request.getArguments()); }
|
||||
catch (InputFailure error) { return text(JSON.toJson(Map.of(
|
||||
"kind", "invalid-input", "field", error.field, "message", error.getMessage())), true); }
|
||||
catch (IllegalArgumentException error) { return text(error.getMessage(), true); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ import ghidra.app.decompiler.ClangCaseToken;
|
||||
import ghidra.app.decompiler.ClangNode;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressIterator;
|
||||
import ghidra.program.model.address.AddressSet;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.program.model.address.AddressRange;
|
||||
import ghidra.program.model.address.AddressRangeIterator;
|
||||
@@ -723,8 +725,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!direction.equals("incoming") && !direction.equals("outgoing")) {
|
||||
throw new RequestFailure("invalid_request", "direction must be incoming or outgoing");
|
||||
}
|
||||
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
|
||||
List<Reference> references = collectReferences(scan.instructions, direction);
|
||||
List<Reference> references = collectReferences(function, direction);
|
||||
JsonArray edges = new JsonArray();
|
||||
for (Reference reference : references) {
|
||||
edges.add(referenceEdge(reference));
|
||||
@@ -735,7 +736,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.add("references", edges);
|
||||
result.addProperty("reference_kinds_available", true);
|
||||
JsonArray unresolvedCalls = new JsonArray();
|
||||
if (direction.equals("outgoing")) for (Instruction instruction : scan.instructions) {
|
||||
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;
|
||||
@@ -1007,6 +1008,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Address entry = function.getEntryPoint();
|
||||
if (function.isExternal() || !currentProgram.getMemory().contains(entry))
|
||||
throw new RequestFailure("invalid_request", "Annotations require a local function entry: " + canonicalAddress(entry));
|
||||
for (String field : List.of("name", "comment", "inline_comment")) {
|
||||
if (params.has(field)) validateAnnotationText(requireText(params, field), field);
|
||||
}
|
||||
String leafName = params.has("name")
|
||||
? annotationLeafName(function, requireString(params, "name")) : null;
|
||||
int transaction = currentProgram.startTransaction("REA function annotations");
|
||||
boolean commit = false;
|
||||
try {
|
||||
@@ -1020,7 +1026,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
}
|
||||
if (params.has("name"))
|
||||
function.setName(requireString(params, "name"), SourceType.USER_DEFINED);
|
||||
function.setName(leafName, SourceType.USER_DEFINED);
|
||||
monitor.checkCancelled();
|
||||
invalidateAnalysisCaches();
|
||||
JsonObject readback = annotationReadback(function);
|
||||
@@ -1028,7 +1034,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!params.has(field)) continue;
|
||||
String requested = requireText(params, field);
|
||||
JsonElement measured = readback.get(field);
|
||||
String expected = !field.equals("name") && requested.isEmpty() ? null : requested;
|
||||
String expected = field.equals("name") ? leafName : requested.isEmpty() ? null : requested;
|
||||
String observed = field.equals("name") ? function.getName() :
|
||||
(measured.isJsonNull() ? null : measured.getAsString());
|
||||
if (!java.util.Objects.equals(expected, observed))
|
||||
@@ -1058,6 +1064,22 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
}
|
||||
|
||||
private String annotationLeafName(Function function, String requested) {
|
||||
// Fully qualified readback must be usable as an idempotent rename input.
|
||||
if (requested.equals(procedureName(function))) return function.getName();
|
||||
String namespace = function.getParentNamespace().isGlobal()
|
||||
? "" : function.getParentNamespace().getName(true);
|
||||
String prefix = namespace + "::";
|
||||
if (!namespace.isEmpty() && requested.startsWith(prefix)) {
|
||||
String leaf = requested.substring(prefix.length());
|
||||
if (!leaf.isEmpty()) return leaf;
|
||||
throw new RequestFailure("invalid_function_name",
|
||||
"Function name has an empty leaf within namespace " + namespace + " at " +
|
||||
canonicalAddress(function.getEntryPoint()) + ": " + requested);
|
||||
}
|
||||
return requested;
|
||||
}
|
||||
|
||||
private static String requireText(JsonObject params, String field) {
|
||||
JsonElement value = params.get(field);
|
||||
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
|
||||
@@ -1065,6 +1087,21 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return value.getAsString();
|
||||
}
|
||||
|
||||
private static void validateAnnotationText(String text, String field) {
|
||||
for (int index = 0; index < text.length(); index++) {
|
||||
char character = text.charAt(index);
|
||||
if (character == 0)
|
||||
throw new RequestFailure("invalid_request", "Annotation " + field + " contains NUL at UTF-16 index " + index + "; Ghidra's decompiler cannot marshal NUL text");
|
||||
if (Character.isHighSurrogate(character) && index + 1 < text.length() &&
|
||||
Character.isLowSurrogate(text.charAt(index + 1))) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (Character.isSurrogate(character))
|
||||
throw new RequestFailure("invalid_request", "Annotation " + field + " contains an unpaired Unicode surrogate at UTF-16 index " + index + "; Ghidra cannot preserve it as annotation text");
|
||||
}
|
||||
}
|
||||
|
||||
private JsonObject analyzeFunction(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of("procedure"));
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
@@ -1074,11 +1111,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (pseudocode == null) {
|
||||
pseudocode = "";
|
||||
}
|
||||
List<Reference> incomingReferences = collectReferences(scan.instructions, "incoming")
|
||||
List<Reference> incomingReferences = collectReferences(function, "incoming")
|
||||
.stream()
|
||||
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
|
||||
.toList();
|
||||
List<Reference> outgoingReferences = collectReferences(scan.instructions, "outgoing");
|
||||
List<Reference> outgoingReferences = collectReferences(function, "outgoing");
|
||||
JsonArray incoming = referenceEdges(incomingReferences);
|
||||
JsonArray outgoing = referenceEdges(outgoingReferences);
|
||||
JsonArray comments = comments(scan.instructions);
|
||||
@@ -1471,6 +1508,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
: regexMatcher(expression, caseSensitive);
|
||||
JsonArray items = new JsonArray();
|
||||
for (InventoryItem item : inventory) {
|
||||
monitor.checkCancelled();
|
||||
if (!matcher.matches(item.value)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1581,33 +1619,36 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
|
||||
private Function resolveProcedure(String value) throws Exception {
|
||||
Address address = tryParseAddress(value);
|
||||
if (address != null) {
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
if (function == null && address.isMemoryAddress()) {
|
||||
function = currentProgram.getFunctionManager().getFunctionContaining(address);
|
||||
// Explicit address syntax wins; an unprefixed identifier is first an
|
||||
// exact symbol name, even when every character is a hexadecimal digit.
|
||||
boolean explicitAddress = isExplicitReaAddress(value);
|
||||
if (!explicitAddress) {
|
||||
List<Function> matches = new ArrayList<>();
|
||||
for (FunctionEntry entry : procedures()) {
|
||||
monitor.checkCancelled();
|
||||
Function function = entry.function;
|
||||
Symbol symbol = function.getSymbol();
|
||||
String qualified = symbol == null ? function.getName() : symbol.getName(true);
|
||||
if (function.getName().equals(value) || qualified.equals(value)) {
|
||||
matches.add(function);
|
||||
}
|
||||
}
|
||||
if (function == null) {
|
||||
throw new RequestFailure("not_found", "No procedure exists at the requested address");
|
||||
}
|
||||
return function;
|
||||
if (matches.size() > 1)
|
||||
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
|
||||
"; select an exact entry address from " + matches.stream()
|
||||
.map(function -> canonicalAddress(function.getEntryPoint())).toList());
|
||||
if (matches.size() == 1) return matches.get(0);
|
||||
}
|
||||
List<Function> matches = new ArrayList<>();
|
||||
for (FunctionEntry entry : procedures()) {
|
||||
Function function = entry.function;
|
||||
Symbol symbol = function.getSymbol();
|
||||
String qualified = symbol == null ? function.getName() : symbol.getName(true);
|
||||
if (function.getName().equals(value) || qualified.equals(value)) {
|
||||
matches.add(function);
|
||||
}
|
||||
}
|
||||
if (matches.isEmpty()) {
|
||||
throw new RequestFailure("not_found", "Unknown Ghidra procedure name");
|
||||
}
|
||||
if (matches.size() != 1) {
|
||||
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous");
|
||||
}
|
||||
return matches.get(0);
|
||||
// Retain legacy bare-hex address input only when no symbol matches.
|
||||
Address address = explicitAddress ? parseReaAddress(value) : tryParseAddress(value);
|
||||
if (address == null)
|
||||
throw new RequestFailure("not_found", "Unknown Ghidra procedure name or address: " + value);
|
||||
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
|
||||
if (function == null && address.isMemoryAddress())
|
||||
function = currentProgram.getFunctionManager().getFunctionContaining(address);
|
||||
if (function == null)
|
||||
throw new RequestFailure("not_found", "No procedure exists at the requested address: " + value);
|
||||
return function;
|
||||
}
|
||||
|
||||
private JsonObject nativeApiBoundary(
|
||||
@@ -2530,21 +2571,32 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
|
||||
private List<Reference> collectReferences(
|
||||
List<Instruction> instructions,
|
||||
Function function,
|
||||
String direction) 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());
|
||||
boolean outgoing = direction.equals("outgoing");
|
||||
AddressIterator addresses = outgoing
|
||||
? currentProgram.getReferenceManager().getReferenceSourceIterator(scope, true)
|
||||
: currentProgram.getReferenceManager().getReferenceDestinationIterator(scope, true);
|
||||
TreeMap<String, Reference> observed = new TreeMap<>();
|
||||
for (Instruction instruction : instructions) {
|
||||
while (addresses.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
if (direction.equals("outgoing")) {
|
||||
Address address = addresses.next();
|
||||
if (outgoing) {
|
||||
for (Reference reference : currentProgram.getReferenceManager()
|
||||
.getReferencesFrom(instruction.getAddress())) {
|
||||
.getReferencesFrom(address)) {
|
||||
monitor.checkCancelled();
|
||||
addReference(observed, reference);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ReferenceIterator iterator = currentProgram.getReferenceManager()
|
||||
.getReferencesTo(instruction.getAddress());
|
||||
.getReferencesTo(address);
|
||||
while (iterator.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
addReference(observed, iterator.next());
|
||||
}
|
||||
}
|
||||
@@ -2944,10 +2996,46 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return parseReaAddress(requireString(params, name));
|
||||
}
|
||||
|
||||
private static boolean isExplicitReaAddress(String value) {
|
||||
if (value.regionMatches(true, 0, "0x", 0, 2) &&
|
||||
isHexadecimal(value, 2, value.length()))
|
||||
return true;
|
||||
int separator = value.toLowerCase(Locale.ROOT).lastIndexOf(":0x");
|
||||
if (separator <= 0 || !isHexadecimal(value, separator + 3, value.length()))
|
||||
return false;
|
||||
// Classify the encoded space iteratively. A repeated regex alternation
|
||||
// can exhaust Java's stack on a long ordinary symbol name.
|
||||
for (int index = 0; index < separator; index++) {
|
||||
char item = value.charAt(index);
|
||||
if ((item >= 'A' && item <= 'Z') ||
|
||||
(item >= 'a' && item <= 'z') ||
|
||||
(item >= '0' && item <= '9') ||
|
||||
item == '.' || item == '_' || item == '~' || item == '-')
|
||||
continue;
|
||||
if (item != '%' || index + 2 >= separator ||
|
||||
!isHexadecimal(value, index + 1, index + 3))
|
||||
return false;
|
||||
index += 2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isHexadecimal(String value, int start, int end) {
|
||||
if (start >= end) return false;
|
||||
for (int index = start; index < end; index++) {
|
||||
char item = value.charAt(index);
|
||||
if (!((item >= '0' && item <= '9') ||
|
||||
(item >= 'a' && item <= 'f') ||
|
||||
(item >= 'A' && item <= 'F')))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Address parseReaAddress(String value) {
|
||||
Address address = tryParseAddress(value);
|
||||
if (address == null) {
|
||||
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address");
|
||||
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address: " + value);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
@@ -2956,22 +3044,31 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
try {
|
||||
String spaceName = null;
|
||||
String offset = value;
|
||||
int separator = value.lastIndexOf(":0x");
|
||||
int separator = value.toLowerCase(Locale.ROOT).lastIndexOf(":0x");
|
||||
if (separator >= 0) {
|
||||
spaceName = decodeAddressSpace(value.substring(0, separator));
|
||||
offset = value.substring(separator + 3);
|
||||
}
|
||||
else if (value.startsWith("0x")) {
|
||||
else if (value.startsWith("0x") || value.startsWith("0X")) {
|
||||
offset = value.substring(2);
|
||||
}
|
||||
if (!offset.matches("[0-9A-Fa-f]+")) {
|
||||
if (!isHexadecimal(offset, 0, offset.length())) {
|
||||
return null;
|
||||
}
|
||||
AddressSpace space = spaceName == null
|
||||
? currentProgram.getAddressFactory().getDefaultAddressSpace()
|
||||
: currentProgram.getAddressFactory().getAddressSpace(spaceName);
|
||||
return space == null ? null : space.getAddress(offset);
|
||||
if (space == null) return null;
|
||||
Address address = space.getAddress(offset);
|
||||
BigInteger requestedOffset = new BigInteger(offset, 16);
|
||||
BigInteger parsedOffset = new BigInteger(Long.toUnsignedString(address.getOffset()));
|
||||
if (!requestedOffset.equals(parsedOffset))
|
||||
throw new RequestFailure("invalid_request",
|
||||
"Ghidra address offset cannot be represented without truncation in space " +
|
||||
space.getName() + ": requested " + value + ", parsed " + canonicalAddress(address));
|
||||
return address;
|
||||
}
|
||||
catch (RequestFailure failure) { throw failure; }
|
||||
catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
@@ -3055,9 +3152,29 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
);
|
||||
}
|
||||
catch (PatternSyntaxException exception) {
|
||||
throw new RequestFailure("invalid_request", "Invalid regex pattern");
|
||||
// Java wraps compiler stack exhaustion in PatternSyntaxException
|
||||
// without retaining its cause. This producer reason is not bad syntax.
|
||||
if (exception.getDescription().equals("Stack overflow during pattern compilation"))
|
||||
throw regexStackExhausted(expression, "compiling");
|
||||
throw new RequestFailure("invalid_request", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
|
||||
}
|
||||
return value -> pattern.matcher(value).find();
|
||||
catch (StackOverflowError exhausted) {
|
||||
throw regexStackExhausted(expression, "compiling");
|
||||
}
|
||||
return value -> {
|
||||
try {
|
||||
return pattern.matcher(value).find();
|
||||
}
|
||||
catch (StackOverflowError exhausted) {
|
||||
throw regexStackExhausted(expression, "matching");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static RequestFailure regexStackExhausted(String expression, String stage) {
|
||||
return new RequestFailure("regex_stack_exhausted",
|
||||
"Ghidra's Java regex engine exhausted its stack while " + stage +
|
||||
" pattern " + expression + ". Use literal mode or simplify the regex; the session remains usable.");
|
||||
}
|
||||
|
||||
private static SessionDescriptor readDescriptor(Path path) throws IOException {
|
||||
@@ -3114,7 +3231,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private void requireDocument(JsonObject params) {
|
||||
String document = optionalString(params, "document");
|
||||
if (document != null && !document.equals(currentProgram.getName())) {
|
||||
throw new RequestFailure("not_found", "Unknown Ghidra Program identity");
|
||||
throw new RequestFailure("not_found", "Unknown Ghidra Program identity: " + document + "; active program: " + currentProgram.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3226,7 +3343,16 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
|
||||
private static void writeResponse(BufferedWriter writer, JsonObject response) throws IOException {
|
||||
String encoded = GSON.toJson(response);
|
||||
writer.write(encoded);
|
||||
// Gson leaves UTF-16 surrogates literal. Escape them in JSON so even
|
||||
// diagnostics quoting ill-formed caller text remain lossless UTF-8.
|
||||
int start = 0;
|
||||
for (int index = 0; index < encoded.length(); index++) {
|
||||
if (!Character.isSurrogate(encoded.charAt(index))) continue;
|
||||
writer.write(encoded, start, index - start);
|
||||
writer.write("\\u" + Integer.toHexString(encoded.charAt(index)));
|
||||
start = index + 1;
|
||||
}
|
||||
writer.write(encoded, start, encoded.length() - start);
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
+311
-129
@@ -8,12 +8,12 @@ API access on this thread: moving dispatch to a worker can deadlock Hopper.
|
||||
import json
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from typing import Any, Optional, Protocol, Sequence
|
||||
|
||||
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
|
||||
_selected_document = None
|
||||
# The native label setter truncates NSString names at this UTF-16 extent.
|
||||
HOPPER_SYMBOL_NAME_UTF16_UNITS = 1024
|
||||
_rea_session_document = None
|
||||
_search_inventory_cache = {}
|
||||
_pseudocode_cache = {}
|
||||
@@ -89,9 +89,11 @@ def _session_document():
|
||||
global _rea_session_document
|
||||
documents = _api().documents()
|
||||
if _rea_session_document is not None:
|
||||
# Hopper recreates Python wrappers; equality compares native handles.
|
||||
# Retain our bound wrapper so cache identity stays stable across calls.
|
||||
return (
|
||||
_rea_session_document
|
||||
if any(document is _rea_session_document for document in documents)
|
||||
if any(document == _rea_session_document for document in documents)
|
||||
else None
|
||||
)
|
||||
return _bind_session_document(documents)
|
||||
@@ -174,46 +176,89 @@ def _json_safe(value):
|
||||
|
||||
|
||||
def _document(name=None):
|
||||
"""Resolve an explicit or session-selected document without changing Hopper UI."""
|
||||
global _selected_document
|
||||
api = _api()
|
||||
documents = api.documents()
|
||||
if name is not None:
|
||||
matches = [candidate for candidate in documents if candidate.getDocumentName() == name]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if len(matches) > 1:
|
||||
current = api.current_document()
|
||||
if current is not None and current.getDocumentName() == name:
|
||||
return current
|
||||
raise InvalidRequestError("Hopper document name is ambiguous")
|
||||
raise InvalidRequestError("Unknown Hopper document")
|
||||
if _selected_document is not None:
|
||||
for candidate in documents:
|
||||
if candidate.getDocumentName() == _selected_document:
|
||||
return candidate
|
||||
"""Keep observations and mutations bound to the active REA target."""
|
||||
documents = _api().documents()
|
||||
session_document = _session_document()
|
||||
if session_document is not None:
|
||||
return session_document
|
||||
current = api.current_document()
|
||||
if current is None:
|
||||
raise CapabilityUnavailableError("No Hopper document is loaded")
|
||||
return current
|
||||
if session_document is None:
|
||||
raise CapabilityUnavailableError("The active REA target's Hopper document is no longer open; reopen the target with open_binary")
|
||||
if name is not None:
|
||||
# A document name is display metadata; scope it to the bound native
|
||||
# handle even when another open target happens to have the same name.
|
||||
if session_document.getDocumentName() == name:
|
||||
return session_document
|
||||
if any(candidate.getDocumentName() == name for candidate in documents):
|
||||
raise InvalidRequestError("Hopper document belongs to another target; use open_binary to select that target")
|
||||
raise InvalidRequestError("Unknown Hopper document")
|
||||
return session_document
|
||||
|
||||
|
||||
def _api_text(value, field):
|
||||
"""Validate text before passing it to Hopper's native UTF-8 string API."""
|
||||
if not isinstance(value, str):
|
||||
raise InvalidRequestError("%s must be a string" % field)
|
||||
if "\0" in value:
|
||||
raise InvalidRequestError("%s contains a NUL character, which Hopper's Python API cannot represent" % field)
|
||||
try:
|
||||
value.encode("utf-8")
|
||||
except UnicodeEncodeError as error:
|
||||
raise InvalidRequestError("%s contains an unpaired Unicode surrogate, which Hopper's Python API cannot represent" % field) from error
|
||||
return value
|
||||
|
||||
|
||||
def _validate_name_targets(document, names):
|
||||
"""Keep Hopper's unique-label moves within the explicitly selected addresses."""
|
||||
targets = {address for _, address, _ in names}
|
||||
if len(targets) != len(names):
|
||||
raise InvalidRequestError("Batch rename selects the same address more than once; use one canonical address per target")
|
||||
labels = set()
|
||||
for _, address, name in names:
|
||||
_segment(document, address)
|
||||
if not name:
|
||||
continue
|
||||
if name in labels:
|
||||
raise InvalidRequestError("Batch rename assigns the same name more than once: %s" % name)
|
||||
labels.add(name)
|
||||
owner = document.getAddressForName(name)
|
||||
if owner not in BAD_ADDRESSES and owner != address and owner not in targets:
|
||||
raise InvalidRequestError("Name %s is already assigned to %s; include that address in the batch with a replacement name" % (name, _hex(owner)))
|
||||
|
||||
|
||||
def _symbol_name(value, field):
|
||||
"""Reject native label truncation without altering the caller's name."""
|
||||
value = _api_text(value, field)
|
||||
if len(value.encode("utf-16-le")) // 2 > HOPPER_SYMBOL_NAME_UTF16_UNITS:
|
||||
raise InvalidRequestError("%s exceeds Hopper's %s UTF-16 code-unit symbol-name limit" % (field, HOPPER_SYMBOL_NAME_UTF16_UNITS))
|
||||
return value
|
||||
|
||||
|
||||
def _name_matches(document, address, name):
|
||||
observed = _segment(document, address).getNameAtAddress(address)
|
||||
return observed == name or (not name and observed is None)
|
||||
|
||||
|
||||
def _address(document, value=None):
|
||||
"""Resolve hexadecimal addresses first, then fall back to Hopper symbol names."""
|
||||
"""Keep explicit hexadecimal coordinates distinct from symbol names."""
|
||||
if value is None:
|
||||
return document.getCurrentAddress()
|
||||
if not isinstance(value, str):
|
||||
raise InvalidRequestError("Address must be a string")
|
||||
try:
|
||||
return int(value, 16)
|
||||
except ValueError:
|
||||
result = document.getAddressForName(value)
|
||||
if result in BAD_ADDRESSES:
|
||||
raise InvalidRequestError("Unknown Hopper address or name")
|
||||
return result
|
||||
_api_text(value, "Address")
|
||||
if value.lower().startswith("0x"):
|
||||
try:
|
||||
address = int(value, 16)
|
||||
except ValueError as error:
|
||||
raise InvalidRequestError("Invalid hexadecimal Hopper address: %s" % value) from error
|
||||
else:
|
||||
# Names such as `add` and `face` are valid hexadecimal strings too.
|
||||
# Prefer an observed symbol, retaining bare-hex compatibility only
|
||||
# when Hopper has no symbol with the supplied name.
|
||||
address = document.getAddressForName(value)
|
||||
if address in BAD_ADDRESSES:
|
||||
try:
|
||||
address = int(value, 16)
|
||||
except ValueError as error:
|
||||
raise InvalidRequestError("Unknown Hopper address or name: %s" % value) from error
|
||||
if not 0 <= address < 0xFFFFFFFFFFFFFFFF:
|
||||
raise InvalidRequestError("Address is outside Hopper's usable unsigned 64-bit range: %s" % value)
|
||||
return address
|
||||
|
||||
|
||||
def _segment(document, address):
|
||||
@@ -249,14 +294,18 @@ def _procedure_identity(procedure):
|
||||
|
||||
|
||||
def _procedure_locals(procedure):
|
||||
"""Project opaque Hopper local-variable objects into an exact public shape."""
|
||||
return [
|
||||
{
|
||||
"description": str(local),
|
||||
"""Preserve public local-variable names and observed stack displacements."""
|
||||
result = []
|
||||
for local in procedure.getLocalVariableList():
|
||||
name = local.name()
|
||||
displacement = local.displacement()
|
||||
result.append({
|
||||
"description": "%s (stack displacement %s)" % (name if name is not None else "Unnamed local", displacement),
|
||||
"name": name,
|
||||
"stack_displacement": str(displacement) if displacement is not None else None,
|
||||
"provenance": "hopper-public-python-api",
|
||||
}
|
||||
for local in procedure.getLocalVariableList()
|
||||
]
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def _containing_procedure(document, address):
|
||||
@@ -267,26 +316,75 @@ def _containing_procedure(document, address):
|
||||
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
|
||||
|
||||
|
||||
def _basic_block_end(procedure, block):
|
||||
"""Normalize native block endpoints to an exclusive byte address."""
|
||||
owner_at = getattr(procedure, "getBasicBlockAtAddress", None)
|
||||
if not callable(owner_at):
|
||||
raise CapabilityUnavailableError(
|
||||
"Instruction enumeration requires Hopper basic-block ownership"
|
||||
)
|
||||
end = block.getEndingAddress()
|
||||
# Hopper 6.1 returns the final instruction address, despite the Python
|
||||
# documentation describing an exclusive end. Membership distinguishes
|
||||
# that representation from an actual exclusive endpoint in other builds.
|
||||
if owner_at(end) == block:
|
||||
instruction = procedure.getSegment().getInstructionAtAddress(end)
|
||||
if instruction is not None and instruction.getInstructionLength() > 0:
|
||||
return end + instruction.getInstructionLength()
|
||||
return end
|
||||
|
||||
|
||||
def _block_instruction_addresses(procedure, block):
|
||||
"""Walk either endpoint convention using Hopper's block ownership API.
|
||||
|
||||
Some Hopper builds return the final instruction start, despite the public
|
||||
API documenting an exclusive end. Equality compares fresh native wrappers;
|
||||
probing ownership also keeps an exclusive endpoint in the next block out.
|
||||
"""
|
||||
segment = procedure.getSegment()
|
||||
address = block.getStartingAddress()
|
||||
end = _basic_block_end(procedure, block)
|
||||
while address < end:
|
||||
if procedure.getBasicBlockAtAddress(address) != block:
|
||||
break
|
||||
instruction = segment.getInstructionAtAddress(address)
|
||||
if instruction is None:
|
||||
break
|
||||
length = instruction.getInstructionLength()
|
||||
if length <= 0 or address + length > end:
|
||||
break
|
||||
yield address
|
||||
address += length
|
||||
|
||||
|
||||
def _instruction_addresses(procedure):
|
||||
result = []
|
||||
seen = set()
|
||||
segment = procedure.getSegment()
|
||||
for block in procedure.basicBlockIterator():
|
||||
address = block.getStartingAddress()
|
||||
end = block.getEndingAddress()
|
||||
while address < end and address not in seen:
|
||||
for address in _block_instruction_addresses(procedure, block):
|
||||
if address in seen:
|
||||
continue
|
||||
seen.add(address)
|
||||
instruction = segment.getInstructionAtAddress(address)
|
||||
if instruction is None:
|
||||
break
|
||||
result.append(address)
|
||||
length = instruction.getInstructionLength()
|
||||
if length <= 0:
|
||||
break
|
||||
address += length
|
||||
return result
|
||||
|
||||
|
||||
def _native_calls(procedure, direction):
|
||||
references = procedure.getAllCallees() if direction == "outgoing" else procedure.getAllCallers()
|
||||
kinds = {0: "none", 1: "unknown", 2: "direct", 3: "objective_c"}
|
||||
calls = {}
|
||||
unresolved = []
|
||||
for reference in references:
|
||||
source, target, kind = reference.fromAddress(), reference.toAddress(), reference.type()
|
||||
if source in BAD_ADDRESSES:
|
||||
continue
|
||||
if target in BAD_ADDRESSES:
|
||||
unresolved.append({"address": _hex(source), "reason": "Native CallReference has no resolved target (type %s)" % kind})
|
||||
continue
|
||||
calls[(source, target)] = {"classification": kinds.get(kind, "unknown"), "provider_type": kind, "provenance": "hopper-public-python-api:CallReference"}
|
||||
return calls, sorted(unresolved, key=lambda item: int(item["address"], 16))
|
||||
|
||||
|
||||
def _procedure_references(document, params):
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
direction = params.get("direction", "outgoing")
|
||||
@@ -299,6 +397,8 @@ def _procedure_references(document, params):
|
||||
references = segment.getReferencesFromAddress(address) if direction == "outgoing" else segment.getReferencesOfAddress(address)
|
||||
for reference in references:
|
||||
edges.add((address, reference) if direction == "outgoing" else (reference, address))
|
||||
calls, unresolved = _native_calls(procedure, direction)
|
||||
edges.update(calls)
|
||||
ordered = sorted(edges)
|
||||
items = []
|
||||
for source, target in ordered:
|
||||
@@ -309,16 +409,15 @@ def _procedure_references(document, params):
|
||||
"target_address": _hex(target),
|
||||
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
|
||||
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
||||
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
|
||||
"kind": _unavailable("Hopper exposes partial CallReference classification, but not detailed reference flags"),
|
||||
**({"call": calls[(source, target)]} if (source, target) in calls else {}),
|
||||
})
|
||||
return {
|
||||
"procedure": _procedure_identity(procedure), "direction": direction,
|
||||
"reference_kinds_available": False,
|
||||
# The public Hopper API returns observed references but does not expose
|
||||
# enough flow metadata to enumerate calls with no resolved target.
|
||||
# Keep the required list field explicit; the provider records this as
|
||||
# unknown coverage rather than claiming the list is exhaustive.
|
||||
"unresolved_calls": [],
|
||||
# These are native CallReference objects, not proof that every unresolved
|
||||
# dispatch site has been enumerated by Hopper.
|
||||
"unresolved_calls": unresolved,
|
||||
"references": items,
|
||||
}
|
||||
|
||||
@@ -333,11 +432,61 @@ def _procedure_map(document):
|
||||
|
||||
|
||||
def _strings(document):
|
||||
result = {}
|
||||
for segment in document.getSegmentsList():
|
||||
for value, address in segment.getStringsList():
|
||||
result[_hex(address)] = value
|
||||
return result
|
||||
return {address: record["value"] for address, record in _string_inventory(document).items()}
|
||||
|
||||
|
||||
def _string_record(segment, address, display):
|
||||
"""Read one native typed object, retaining its shortened display separately."""
|
||||
length = segment.getObjectLength(address)
|
||||
end = segment.getStartingAddress() + segment.getLength()
|
||||
if isinstance(length, bool) or not isinstance(length, int) or length <= 0 or length > end - address:
|
||||
raise CapabilityUnavailableError("Hopper reported an invalid string object extent at %s" % _hex(address))
|
||||
raw = segment.readBytes(address, length)
|
||||
if not isinstance(raw, (bytes, bytearray)) or len(raw) != length:
|
||||
raise CapabilityUnavailableError("Hopper could not read the complete string object at %s" % _hex(address))
|
||||
kind = segment.getTypeAtAddress(address)
|
||||
encodings = ("utf-8", "latin-1") if kind == segment.TYPE_ASCII else ("utf-16-le", "utf-16-be") if kind == segment.TYPE_UNICODE else ()
|
||||
# Native display escaping is not invertible (a literal backslash-n and a
|
||||
# newline can render alike). Decode byte strings directly; for UTF-16,
|
||||
# compare rendered candidates to establish an unambiguous byte order.
|
||||
candidates = []
|
||||
for encoding in encodings:
|
||||
terminator = b"\x00\x00" if encoding.startswith("utf-16") else b"\x00"
|
||||
terminated = raw.endswith(terminator)
|
||||
payload = raw[:-len(terminator)] if terminated else raw
|
||||
try:
|
||||
value = payload.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
record = {"value": value, "provider_value": display, "string": {"encoding": encoding, "encoding_status": "inferred", "termination": "present_or_not_required" if terminated else "missing", "byte_length": length}}
|
||||
if kind == segment.TYPE_ASCII:
|
||||
return record
|
||||
rendered = value.replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
|
||||
if rendered == display or (display.endswith("\u2026") and display[:-1] and rendered.startswith(display[:-1])):
|
||||
candidates.append(record)
|
||||
if candidates and len({candidate["value"] for candidate in candidates}) == 1:
|
||||
return candidates[0]
|
||||
raise CapabilityUnavailableError("Hopper's string display cannot be reconciled with its typed bytes at %s; inspect the bytes and encoding explicitly" % _hex(address))
|
||||
|
||||
|
||||
def _string_inventory(document, addresses=None):
|
||||
key = (id(document), "string_objects")
|
||||
objects = _search_inventory_cache.get(key)
|
||||
if objects is None:
|
||||
objects = {address: (segment, display) for segment in document.getSegmentsList() for display, address in segment.getStringsList()}
|
||||
_search_inventory_cache[key] = objects
|
||||
key = (id(document), "string_records")
|
||||
records = _search_inventory_cache.setdefault(key, {})
|
||||
selected = sorted(objects if addresses is None else objects.keys() & addresses)
|
||||
for address in selected:
|
||||
name = _hex(address)
|
||||
if name not in records:
|
||||
segment, display = objects[address]
|
||||
try:
|
||||
records[name] = _string_record(segment, address, display)
|
||||
except CapabilityUnavailableError as error:
|
||||
records[name] = {"value": display, "provider_value": display, "decoding": _unavailable(str(error))}
|
||||
return {_hex(address): records[_hex(address)] for address in selected}
|
||||
|
||||
|
||||
def _invalidate_search_inventory(document):
|
||||
@@ -397,18 +546,14 @@ def _search_results(document, kind, params):
|
||||
needle = pattern if case_sensitive else pattern.casefold()
|
||||
matches = lambda value: needle in (value if case_sensitive else value.casefold())
|
||||
else:
|
||||
try:
|
||||
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
|
||||
except (re.error, OverflowError) as error:
|
||||
raise InvalidRequestError("Invalid regex pattern") from error
|
||||
matches = lambda value: expression.search(value) is not None
|
||||
raise CapabilityUnavailableError("Regex matching requires REA's supervised worker; the native bridge only executes literal searches")
|
||||
|
||||
selected = []
|
||||
for item in _search_inventory(document, kind):
|
||||
if not matches(item[1]):
|
||||
continue
|
||||
selected.append(item)
|
||||
return [
|
||||
return [{"address": address, **_string_inventory(document)[address]} for address, _ in selected] if kind == "string" else [
|
||||
{
|
||||
"address": address,
|
||||
"value": value,
|
||||
@@ -450,25 +595,11 @@ def _render_instruction(segment, address):
|
||||
|
||||
def _assembly(procedure):
|
||||
"""Render assembly while guarding against malformed instruction cycles."""
|
||||
lines = []
|
||||
segment = procedure.getSegment()
|
||||
seen = set()
|
||||
for block in procedure.basicBlockIterator():
|
||||
address = block.getStartingAddress()
|
||||
end = block.getEndingAddress()
|
||||
while address < end and address not in seen:
|
||||
seen.add(address)
|
||||
instruction = segment.getInstructionAtAddress(address)
|
||||
if instruction is None:
|
||||
break
|
||||
line = _render_instruction(segment, address)
|
||||
if line is not None:
|
||||
lines.append(line)
|
||||
length = instruction.getInstructionLength()
|
||||
if length <= 0:
|
||||
break
|
||||
address += length
|
||||
return "\n".join(lines)
|
||||
return "\n".join(
|
||||
line for address in _instruction_addresses(procedure)
|
||||
if (line := _render_instruction(segment, address)) is not None
|
||||
)
|
||||
|
||||
|
||||
def _read_function_instructions(document, params):
|
||||
@@ -505,7 +636,7 @@ def _analyze_function(document, params):
|
||||
successors.append(_hex(successor))
|
||||
blocks.append({
|
||||
"start": _hex(block.getStartingAddress()),
|
||||
"end": _hex(block.getEndingAddress()),
|
||||
"end": _hex(_basic_block_end(procedure, block)),
|
||||
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
|
||||
})
|
||||
pseudo = _pseudocode(document, procedure) or ""
|
||||
@@ -529,6 +660,10 @@ def _analyze_function(document, params):
|
||||
incoming = []
|
||||
outgoing = []
|
||||
procedure_addresses = set(addresses)
|
||||
outgoing_calls, unresolved_calls = _native_calls(procedure, "outgoing")
|
||||
incoming_calls, _ = _native_calls(procedure, "incoming")
|
||||
calls = {**incoming_calls, **outgoing_calls}
|
||||
edges.update(calls)
|
||||
for source, target in sorted(edges):
|
||||
source_procedure, _ = _containing_procedure(document, source)
|
||||
target_procedure, _ = _containing_procedure(document, target)
|
||||
@@ -537,16 +672,14 @@ def _analyze_function(document, params):
|
||||
"target_address": _hex(target),
|
||||
"source_procedure": _procedure_identity(source_procedure) if source_procedure is not None else None,
|
||||
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
|
||||
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
|
||||
"kind": _unavailable("Hopper exposes partial CallReference classification, but not detailed reference flags"),
|
||||
**({"call": calls[(source, target)]} if (source, target) in calls else {}),
|
||||
}
|
||||
if target in procedure_addresses and source not in procedure_addresses:
|
||||
incoming.append(item)
|
||||
if source in procedure_addresses:
|
||||
outgoing.append(item)
|
||||
string_map = {
|
||||
int(address, 16): value
|
||||
for address, value in _search_inventory(document, "string")
|
||||
}
|
||||
string_map = {int(address, 16): record for address, record in _string_inventory(document, {int(edge["target_address"], 16) for edge in outgoing}).items()}
|
||||
name_map = {
|
||||
int(address, 16): value
|
||||
for address, value in _search_inventory(document, "name")
|
||||
@@ -556,7 +689,7 @@ def _analyze_function(document, params):
|
||||
for edge in outgoing:
|
||||
target = int(edge["target_address"], 16)
|
||||
if target in string_map:
|
||||
referenced_strings.append({"address": edge["target_address"], "value": string_map[target], "source_address": edge["source_address"]})
|
||||
referenced_strings.append({"address": edge["target_address"], **string_map[target], "source_address": edge["source_address"]})
|
||||
if target in name_map:
|
||||
referenced_names.append({"address": edge["target_address"], "value": name_map[target], "source_address": edge["source_address"]})
|
||||
comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
|
||||
@@ -574,13 +707,14 @@ def _analyze_function(document, params):
|
||||
"callers": callers, "callees": callees,
|
||||
"incoming_references": incoming,
|
||||
"outgoing_references": outgoing,
|
||||
"unresolved_calls": unresolved_calls,
|
||||
"referenced_strings": referenced_strings,
|
||||
"referenced_names": referenced_names,
|
||||
"basic_blocks": blocks,
|
||||
"limitations": [
|
||||
"Hopper's public Python API does not classify reference kinds.",
|
||||
"Native CallReference classifications are observed where available; detailed reference flags remain unknown.",
|
||||
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
|
||||
"Unresolved indirect calls without reported target addresses are not represented as call edges.",
|
||||
"Native calls without resolved target addresses are not represented as edges; enumeration beyond reported CallReference objects is unknown.",
|
||||
"Pseudocode and assembly are provider-specific representations, not original source.",
|
||||
],
|
||||
}
|
||||
@@ -588,7 +722,6 @@ def _analyze_function(document, params):
|
||||
|
||||
def _dispatch(method, params):
|
||||
"""Dispatch only the closed operation set implemented by REA's public tools."""
|
||||
global _selected_document
|
||||
if method == "health":
|
||||
return {"name": "REA Hopper bridge", "version": "1.0.0", "run_id": REA_RUN_ID}
|
||||
if method in ("shutdown", "shutdown_document"):
|
||||
@@ -622,14 +755,9 @@ def _dispatch(method, params):
|
||||
"document_closed": document_closed,
|
||||
}
|
||||
if method == "list_documents":
|
||||
return [document.getDocumentName() for document in Document.getAllDocuments()]
|
||||
return [document.getDocumentName() for document in _api().documents()]
|
||||
if method == "current_document":
|
||||
return _document().getDocumentName()
|
||||
if method == "set_current_document":
|
||||
document = _document(params.get("document"))
|
||||
_selected_document = document.getDocumentName()
|
||||
return _selected_document
|
||||
|
||||
document = _document(params.get("document"))
|
||||
if method in EXHAUSTIVE_ANALYSIS_METHODS:
|
||||
_api().require_analysis_complete(document, method)
|
||||
@@ -650,10 +778,17 @@ def _dispatch(method, params):
|
||||
"Hopper's public Python API does not expose readBytes"
|
||||
)
|
||||
address = _address(document, params.get("address"))
|
||||
value = reader(address, length)
|
||||
segment = _segment(document, address)
|
||||
# Document.readBytes delegates to one Segment and returns False when
|
||||
# the requested range crosses its end. Preserve the readable prefix.
|
||||
available = segment.getStartingAddress() + segment.getLength() - address
|
||||
value = reader(address, min(length, available))
|
||||
if value is False or value is None:
|
||||
raise InvalidRequestError("Hopper could not read the requested byte range at %s" % _hex(address))
|
||||
if not isinstance(value, (bytes, bytearray)):
|
||||
raise CapabilityUnavailableError(
|
||||
"Hopper's public Python API returned an unsupported byte representation"
|
||||
"Hopper readBytes returned %s for the requested %s-byte range at %s"
|
||||
% (type(value).__name__, length, _hex(address))
|
||||
)
|
||||
data = bytes(value)
|
||||
return {
|
||||
@@ -678,7 +813,26 @@ def _dispatch(method, params):
|
||||
or offset < 0
|
||||
):
|
||||
raise InvalidRequestError("Address has no authoritative file-offset mapping")
|
||||
return {"address": _hex(address), "file_offset": offset}
|
||||
inverse = getattr(document, "getAddressFromFileOffset", None)
|
||||
if not callable(inverse):
|
||||
raise CapabilityUnavailableError(
|
||||
"Hopper's public Python API cannot verify the file-offset mapping by reverse lookup"
|
||||
)
|
||||
# Synthetic memory can report offset zero, which belongs to the actual
|
||||
# file header. An integer alone does not prove source-file provenance.
|
||||
if inverse(offset) != address:
|
||||
raise InvalidRequestError("Address has no authoritative file-offset mapping: reverse lookup disagrees at %s" % _hex(address))
|
||||
if offset > 9007199254740991:
|
||||
raise CapabilityUnavailableError("Provider file offset exceeds the exact JSON integer range")
|
||||
header_address = inverse(0)
|
||||
reader = getattr(document, "readBytes", None)
|
||||
header = reader(header_address, 12) if header_address not in BAD_ADDRESSES and document.getSegmentAtAddress(header_address) is not None and callable(reader) else None
|
||||
return {
|
||||
"address": _hex(address),
|
||||
"provider_file_offset": offset,
|
||||
"provider_source_path": document.getExecutableFilePath(),
|
||||
"provider_image_header_hex": bytes(header).hex() if isinstance(header, (bytes, bytearray)) and len(header) == 12 else None,
|
||||
}
|
||||
if method == "resolve_containing_procedure":
|
||||
address = _address(document, params.get("address"))
|
||||
procedure, reason = _containing_procedure(document, address)
|
||||
@@ -694,8 +848,13 @@ def _dispatch(method, params):
|
||||
return _procedure_name(_procedure(document))
|
||||
if method == "goto_address":
|
||||
address = _address(document, params.get("address"))
|
||||
segment = _segment(document, address)
|
||||
expected = segment.getInstructionStart(address)
|
||||
document.moveCursorAtAddress(address)
|
||||
return _hex(address)
|
||||
observed = document.getCurrentAddress()
|
||||
if observed != (address if expected in BAD_ADDRESSES else expected):
|
||||
raise InvalidRequestError("Hopper cursor did not reach the requested object at %s; observed %s" % (_hex(address), _hex(observed)))
|
||||
return _hex(observed)
|
||||
if method in ("address_name", "comment", "inline_comment", "xrefs"):
|
||||
target = _address(document, params.get("address"))
|
||||
segment = _segment(document, target)
|
||||
@@ -708,12 +867,22 @@ def _dispatch(method, params):
|
||||
return [_hex(value) for value in segment.getReferencesOfAddress(target)]
|
||||
if method in ("next_address", "prev_address"):
|
||||
target = _address(document, params.get("address"))
|
||||
segment = _segment(document, target)
|
||||
if method == "next_address":
|
||||
result = target + max(1, document.getObjectLength(target))
|
||||
start = segment.getInstructionStart(target)
|
||||
if start in BAD_ADDRESSES:
|
||||
raise InvalidRequestError("No analyzed object exists at the requested address")
|
||||
length = segment.getObjectLength(start)
|
||||
if length in BAD_ADDRESSES or length <= 0:
|
||||
raise InvalidRequestError("No next analyzed object exists at the requested address")
|
||||
result = start + length
|
||||
else:
|
||||
result = document.getInstructionStart(max(0, target - 1))
|
||||
if result in BAD_ADDRESSES:
|
||||
raise InvalidRequestError("No adjacent address")
|
||||
previous_segment = document.getSegmentAtAddress(target - 1) if target > 0 else None
|
||||
if previous_segment is None:
|
||||
raise InvalidRequestError("No previous address exists in mapped memory")
|
||||
result = previous_segment.getInstructionStart(target - 1)
|
||||
if result in BAD_ADDRESSES or document.getSegmentAtAddress(result) is None:
|
||||
raise InvalidRequestError("No adjacent analyzed object exists in mapped memory")
|
||||
return _hex(result)
|
||||
if method == "list_segments":
|
||||
result = []
|
||||
@@ -747,12 +916,9 @@ def _dispatch(method, params):
|
||||
if method == "list_procedures":
|
||||
return [{"address": address, "value": value} for address, value in _search_inventory(document, "procedure")]
|
||||
if method == "list_strings":
|
||||
values = dict(_search_inventory(document, "string"))
|
||||
requested = params.get("address")
|
||||
if requested is not None:
|
||||
key = _hex(_address(document, requested))
|
||||
values = {key: values[key]} if key in values else {}
|
||||
return [{"address": address, "value": value} for address, value in values.items()]
|
||||
values = _string_inventory(document, None if requested is None else {_address(document, requested)})
|
||||
return [{"address": address, **record} for address, record in values.items()]
|
||||
if method == "list_names":
|
||||
result = dict(_search_inventory(document, "name"))
|
||||
requested = params.get("address")
|
||||
@@ -777,7 +943,7 @@ def _dispatch(method, params):
|
||||
return sorted((_hex(item.getEntryPoint()) for item in procedure.getAllCalleeProcedures()), key=lambda value: int(value, 16))
|
||||
if method == "procedure_info":
|
||||
blocks = list(procedure.basicBlockIterator())
|
||||
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
|
||||
length = sum(max(0, _basic_block_end(procedure, block) - block.getStartingAddress()) for block in blocks)
|
||||
return {
|
||||
"name": _procedure_name(procedure),
|
||||
"entrypoint": _hex(procedure.getEntryPoint()),
|
||||
@@ -790,35 +956,51 @@ def _dispatch(method, params):
|
||||
}
|
||||
if method == "set_address_name":
|
||||
address = _address(document, params.get("address"))
|
||||
name = _symbol_name(params["name"], "Name")
|
||||
_validate_name_targets(document, [(params.get("address"), address, name)])
|
||||
try:
|
||||
result = document.setNameAtAddress(address, params["name"])
|
||||
result = document.setNameAtAddress(address, name)
|
||||
finally:
|
||||
_invalidate_search_inventory(document)
|
||||
_invalidate_pseudocode(document)
|
||||
return result
|
||||
return bool(result) and _name_matches(document, address, name)
|
||||
if method == "set_addresses_names":
|
||||
# Validate all destinations before applying any annotation. A malformed
|
||||
# later address must not discard the result of an earlier mutation.
|
||||
names = [(key, _address(document, key), _symbol_name(value, "Name for %s" % key)) for key, value in params["names"].items()]
|
||||
_validate_name_targets(document, names)
|
||||
try:
|
||||
result = {key: document.setNameAtAddress(_address(document, key), value) for key, value in params["names"].items()}
|
||||
result = {key: document.setNameAtAddress(address, value) for key, address, value in names}
|
||||
finally:
|
||||
_invalidate_search_inventory(document)
|
||||
_invalidate_pseudocode(document)
|
||||
return result
|
||||
return {key: bool(result[key]) and _name_matches(document, address, value) for key, address, value in names}
|
||||
if method in ("set_comment", "set_inline_comment"):
|
||||
address = _address(document, params.get("address"))
|
||||
comment = _api_text(params["comment"], "Comment")
|
||||
segment = _segment(document, address)
|
||||
setter = segment.setCommentAtAddress if method == "set_comment" else segment.setInlineCommentAtAddress
|
||||
getter = segment.getCommentAtAddress if method == "set_comment" else segment.getInlineCommentAtAddress
|
||||
setter(address, params["comment"])
|
||||
setter(address, comment)
|
||||
_invalidate_pseudocode(document)
|
||||
return getter(address) == params["comment"]
|
||||
observed = getter(address)
|
||||
return observed == comment or (comment == "" and observed is None)
|
||||
if method == "list_bookmarks":
|
||||
return [{"address": _hex(item), "name": document.getBookmarkName(item)} for item in document.getBookmarks()]
|
||||
if method == "set_bookmark":
|
||||
address = _address(document, params.get("address"))
|
||||
document.setBookmarkAtAddress(address, params.get("name"))
|
||||
_segment(document, address)
|
||||
name = params.get("name")
|
||||
if name is not None:
|
||||
_api_text(name, "Bookmark name")
|
||||
document.setBookmarkAtAddress(address, name)
|
||||
return document.hasBookmarkAtAddress(address)
|
||||
if method == "unset_bookmark":
|
||||
address = _address(document, params.get("address"))
|
||||
# Permit removing a legacy orphan bookmark, but do not report success
|
||||
# for an unmapped address with no bookmark to remove.
|
||||
if not document.hasBookmarkAtAddress(address):
|
||||
_segment(document, address)
|
||||
document.removeBookmarkAtAddress(address)
|
||||
return not document.hasBookmarkAtAddress(address)
|
||||
raise InvalidRequestError("Unknown bridge method")
|
||||
@@ -913,14 +1095,14 @@ def _diagnostic_type(error):
|
||||
|
||||
|
||||
def _safe_diagnostic(error):
|
||||
"""Project one exception without retaining provider or credential text."""
|
||||
"""Preserve bridge validation reasons while excluding transport credentials."""
|
||||
diagnostic_type = _diagnostic_type(error)
|
||||
if diagnostic_type == "capability_unavailable":
|
||||
message = str(error)
|
||||
elif diagnostic_type == "authorization":
|
||||
message = "Invalid bridge capability"
|
||||
elif diagnostic_type == "invalid_request":
|
||||
message = "Invalid Hopper bridge request"
|
||||
message = str(error)
|
||||
else:
|
||||
message = "%s: Hopper bridge operation failed" % type(error).__name__
|
||||
return {"code": -32000, "message": message, "type": diagnostic_type}
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
REA imports this module into `lldb --batch` and runs `rea_trace <config.json>`.
|
||||
It launches one owned process, stops at the requested functions and
|
||||
Objective-C methods, records each entry from registers and memory reads only,
|
||||
continues, and kills the process once it exits, the event limit is reached or
|
||||
the duration elapses. No expression is evaluated and no target code runs on
|
||||
the tracer's behalf. The result is written as JSON to the configured path.
|
||||
continues, and kills the process once it exits, the event or aggregate resource
|
||||
limit is reached, or the duration elapses. Target output is drained through
|
||||
bounded FIFOs. No expression is evaluated and no target code runs on the
|
||||
tracer's behalf. The result is written as JSON to the configured path.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
|
||||
import lldb
|
||||
@@ -22,9 +28,218 @@ ARGUMENT_REGISTERS = {
|
||||
MAX_SELECTOR_BYTES = 1024
|
||||
MAX_REPORTED_LOCATIONS = 64
|
||||
MAX_OTHER_STOPS = 256
|
||||
MAX_TRACE_BYTES = 8 * 1024 * 1024
|
||||
MAX_TRACE_FRAMES = 65_536
|
||||
MAX_OBSERVATION_JOURNAL_BYTES = MAX_TRACE_BYTES
|
||||
MAX_BREAKPOINT_LOCATION_BYTES = 8 * 1024 * 1024
|
||||
MAX_TARGET_OUTPUT_BYTES = 1024 * 1024
|
||||
OUTPUT_DRAIN_BYTES_PER_PASS = 1024 * 1024
|
||||
OUTPUT_EOF_WAIT_SECONDS = 1.0
|
||||
OUTPUT_READER_JOIN_SECONDS = 0.5
|
||||
LISTENER_NAME = "rea-native-call-observation"
|
||||
|
||||
|
||||
def _can_retain_trace(current_bytes, current_frames, event_bytes, event_frames):
|
||||
"""Whether a complete event fits both aggregate trace budgets."""
|
||||
return (
|
||||
current_bytes + event_bytes <= MAX_TRACE_BYTES
|
||||
and current_frames + event_frames <= MAX_TRACE_FRAMES
|
||||
)
|
||||
|
||||
|
||||
def _estimated_json_size(value):
|
||||
"""Estimate JSON size without encoding potentially oversized values."""
|
||||
if isinstance(value, str):
|
||||
# json.dumps defaults to ensure_ascii=True; supplementary characters
|
||||
# can become two six-byte Unicode escapes.
|
||||
return len(value) * 12 + 2
|
||||
if value is None or isinstance(value, (bool, int, float)):
|
||||
return 32
|
||||
if isinstance(value, dict):
|
||||
return 2 + sum(
|
||||
len(key) * 6 + 4 + _estimated_json_size(item)
|
||||
for key, item in value.items()
|
||||
)
|
||||
if isinstance(value, (list, tuple)):
|
||||
return 2 + sum(_estimated_json_size(item) + 1 for item in value)
|
||||
return 64
|
||||
|
||||
|
||||
def _fits_trace_budget(value, remaining_bytes):
|
||||
return _estimated_json_size(value) <= remaining_bytes
|
||||
|
||||
|
||||
class _ObservationJournal:
|
||||
"""Flush a bounded JSONL prefix so observations survive tracer failure."""
|
||||
|
||||
def __init__(self, handle, max_bytes=MAX_OBSERVATION_JOURNAL_BYTES):
|
||||
self.handle = handle
|
||||
self.max_bytes = max_bytes
|
||||
self.bytes = 0
|
||||
|
||||
def append(self, record):
|
||||
encoded = (json.dumps(record) + "\n").encode("utf-8")
|
||||
if self.bytes + len(encoded) > self.max_bytes:
|
||||
return False
|
||||
self.handle.write(encoded)
|
||||
self.handle.flush()
|
||||
self.bytes += len(encoded)
|
||||
return True
|
||||
|
||||
|
||||
def _set_launch_flags(launch):
|
||||
launch.SetLaunchFlags(
|
||||
launch.GetLaunchFlags()
|
||||
| lldb.eLaunchFlagDebug
|
||||
| lldb.eLaunchFlagStopAtEntry
|
||||
)
|
||||
|
||||
|
||||
def _hash_file(path):
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as target_file:
|
||||
while True:
|
||||
chunk = target_file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _file_identity(path):
|
||||
info = os.stat(path)
|
||||
return str(info.st_dev), str(info.st_ino)
|
||||
|
||||
|
||||
def _module_identity(target):
|
||||
module = target.FindModule(target.GetExecutable())
|
||||
if not module.IsValid():
|
||||
return None, None, None
|
||||
spec = module.GetFileSpec()
|
||||
path = str(spec) if spec.IsValid() else None
|
||||
identity = None
|
||||
if path:
|
||||
try:
|
||||
identity = _file_identity(path)
|
||||
except OSError:
|
||||
pass
|
||||
return path, identity, module.GetUUIDString() or None
|
||||
|
||||
|
||||
def _wait_for_identity_ack(config):
|
||||
"""Hold the target at entry while the host adapter captures its start identity."""
|
||||
path = config.get("identity_ack_path")
|
||||
if not path:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
with open(path, "r") as handle:
|
||||
return handle.read(32).strip()
|
||||
except FileNotFoundError:
|
||||
time.sleep(0.025)
|
||||
|
||||
|
||||
class _OutputPipe:
|
||||
"""Drain a target FIFO while retaining only its bounded output prefix."""
|
||||
|
||||
def __init__(self, fifo_path, capture_path, max_bytes):
|
||||
os.mkfifo(fifo_path, 0o600)
|
||||
self.path = fifo_path
|
||||
try:
|
||||
self.fd = os.open(fifo_path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
except Exception:
|
||||
os.unlink(fifo_path)
|
||||
raise
|
||||
try:
|
||||
self.capture = open(capture_path, "wb")
|
||||
except Exception:
|
||||
os.close(self.fd)
|
||||
os.unlink(fifo_path)
|
||||
raise
|
||||
self.max_bytes = max_bytes
|
||||
self.bytes = 0
|
||||
self.truncated = False
|
||||
self.closed = False
|
||||
self.stop_reader = threading.Event()
|
||||
self.reader_thread = None
|
||||
self.reader_error = None
|
||||
self.launched = False
|
||||
self.eof = threading.Event()
|
||||
self.complete = False
|
||||
|
||||
def start(self):
|
||||
if self.reader_thread is None:
|
||||
self.reader_thread = threading.Thread(target=self._reader_loop, daemon=True)
|
||||
self.reader_thread.start()
|
||||
|
||||
def _reader_loop(self):
|
||||
try:
|
||||
while not self.stop_reader.is_set():
|
||||
readable, _, _ = select.select([self.fd], [], [], 0.05)
|
||||
if not readable:
|
||||
continue
|
||||
if self.drain(OUTPUT_DRAIN_BYTES_PER_PASS) == 0:
|
||||
# A FIFO read end is readable at EOF before the target opens it.
|
||||
time.sleep(0.01)
|
||||
except Exception as error:
|
||||
self.reader_error = error
|
||||
self.stop_reader.set()
|
||||
|
||||
def mark_launched(self):
|
||||
self.launched = True
|
||||
|
||||
def drain(self, max_bytes=OUTPUT_DRAIN_BYTES_PER_PASS):
|
||||
consumed = 0
|
||||
while consumed < max_bytes:
|
||||
try:
|
||||
chunk = os.read(self.fd, min(65_536, max_bytes - consumed))
|
||||
except OSError as error:
|
||||
if error.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
|
||||
return consumed
|
||||
raise
|
||||
if not chunk:
|
||||
if self.launched:
|
||||
self.eof.set()
|
||||
return consumed
|
||||
consumed += len(chunk)
|
||||
self.bytes += len(chunk)
|
||||
remaining = max(0, self.max_bytes - self.capture.tell())
|
||||
if remaining:
|
||||
self.capture.write(chunk[:remaining])
|
||||
if len(chunk) > remaining:
|
||||
self.truncated = True
|
||||
|
||||
def close(self, expected_exit=False):
|
||||
if self.closed:
|
||||
return
|
||||
if expected_exit and self.launched:
|
||||
self.eof.wait(OUTPUT_EOF_WAIT_SECONDS)
|
||||
self.stop_reader.set()
|
||||
if self.reader_thread is not None:
|
||||
self.reader_thread.join(OUTPUT_READER_JOIN_SECONDS)
|
||||
stopped = self.reader_thread is None or not self.reader_thread.is_alive()
|
||||
if stopped:
|
||||
try:
|
||||
self.drain(OUTPUT_DRAIN_BYTES_PER_PASS)
|
||||
finally:
|
||||
try:
|
||||
self.capture.close()
|
||||
finally:
|
||||
os.close(self.fd)
|
||||
self.closed = True
|
||||
else:
|
||||
# A daemon reader may still be unwinding a blocked filesystem write.
|
||||
# Leave its handles alone rather than racing that thread at shutdown.
|
||||
self.closed = True
|
||||
self.complete = bool(
|
||||
stopped
|
||||
and (not self.launched or (expected_exit and self.eof.is_set()))
|
||||
and self.reader_error is None
|
||||
)
|
||||
if not self.complete:
|
||||
self.truncated = True
|
||||
|
||||
|
||||
def _hex(value):
|
||||
return "0x%x" % value
|
||||
|
||||
@@ -34,7 +249,7 @@ def _location(address, target):
|
||||
module = address.GetModule()
|
||||
symbol = address.GetSymbol()
|
||||
file_address = address.GetFileAddress()
|
||||
return {
|
||||
location = {
|
||||
"load_address": _hex(address.GetLoadAddress(target)),
|
||||
"file_address": None
|
||||
if file_address == lldb.LLDB_INVALID_ADDRESS
|
||||
@@ -43,6 +258,7 @@ def _location(address, target):
|
||||
"module_path": str(module.GetFileSpec()) if module.IsValid() else None,
|
||||
"symbol": symbol.GetName() if symbol.IsValid() else None,
|
||||
}
|
||||
return location
|
||||
|
||||
|
||||
def _no_files():
|
||||
@@ -107,33 +323,54 @@ def _wanted(spec, symbol):
|
||||
return symbol.startswith("-[" if method_type == "instance" else "+[")
|
||||
|
||||
|
||||
def _record(context, thread, index, sequence):
|
||||
def _read_register(frame, name):
|
||||
error = lldb.SBError()
|
||||
value = frame.FindRegister(name)
|
||||
if not value.IsValid():
|
||||
raise RuntimeError("LLDB breakpoint register %s is unavailable" % name)
|
||||
number = value.GetValueAsUnsigned(error)
|
||||
if error.Fail():
|
||||
raise RuntimeError(
|
||||
"LLDB breakpoint register %s could not be read: %s" % (name, error)
|
||||
)
|
||||
return number
|
||||
|
||||
|
||||
def _record(context, thread, frame, index, sequence, location, remaining_bytes):
|
||||
target, process, config = context["target"], context["process"], context["config"]
|
||||
frame = thread.GetFrameAtIndex(0)
|
||||
location = _location(frame.GetPCAddress(), target)
|
||||
symbol = location["symbol"] or ""
|
||||
if not _wanted(config["breakpoints"][index], symbol):
|
||||
return None
|
||||
names = ARGUMENT_REGISTERS.get(config["architecture"], [])
|
||||
objc_method = symbol.startswith("-[") or symbol.startswith("+[")
|
||||
values = {
|
||||
name: _read_register(frame, name)
|
||||
for name in names[: max(config["argument_registers"], 2 if objc_method else 0)]
|
||||
}
|
||||
registers = [
|
||||
{"name": name, "value": _hex(frame.FindRegister(name).GetValueAsUnsigned())}
|
||||
{"name": name, "value": _hex(values[name])}
|
||||
for name in names[: config["argument_registers"]]
|
||||
]
|
||||
receiver_class = None
|
||||
selector = None
|
||||
if symbol.startswith("-[") or symbol.startswith("+["):
|
||||
if objc_method:
|
||||
error = lldb.SBError()
|
||||
text = process.ReadCStringFromMemory(
|
||||
frame.FindRegister(names[1]).GetValueAsUnsigned(),
|
||||
values[names[1]],
|
||||
MAX_SELECTOR_BYTES,
|
||||
error,
|
||||
)
|
||||
selector = text if error.Success() and text else None
|
||||
if symbol.startswith("-["):
|
||||
receiver_class = _dynamic_class(
|
||||
target, frame.FindRegister(names[0]).GetValueAsUnsigned()
|
||||
)
|
||||
frames = min(thread.GetNumFrames(), 1 + config["backtrace_frames"])
|
||||
receiver_class = _dynamic_class(target, values[names[0]])
|
||||
estimated_base = (
|
||||
_estimated_json_size(location)
|
||||
+ _estimated_json_size(receiver_class)
|
||||
+ _estimated_json_size(selector)
|
||||
+ 2048
|
||||
)
|
||||
if estimated_base > remaining_bytes:
|
||||
raise ValueError("trace event exceeds aggregate byte budget")
|
||||
record = {
|
||||
"sequence": sequence,
|
||||
"elapsed_ms": round((time.monotonic() - context["started"]) * 1000, 3),
|
||||
@@ -142,55 +379,154 @@ def _record(context, thread, index, sequence):
|
||||
"receiver_class": receiver_class,
|
||||
"selector": selector,
|
||||
"registers": registers,
|
||||
"backtrace": [
|
||||
_location(thread.GetFrameAtIndex(depth).GetPCAddress(), target)
|
||||
for depth in range(1, frames)
|
||||
],
|
||||
"backtrace": [],
|
||||
}
|
||||
record.update(location)
|
||||
return record
|
||||
|
||||
|
||||
def _handle_stop(context, events, other_stops):
|
||||
"""Record breakpoint hits of one stop; return an outcome when tracing must end."""
|
||||
process = context["process"]
|
||||
for thread in process:
|
||||
reason = thread.GetStopReason()
|
||||
if reason == lldb.eStopReasonBreakpoint:
|
||||
index = context["breakpoint_index"].get(thread.GetStopReasonDataAtIndex(0))
|
||||
if index is None:
|
||||
continue
|
||||
record = _record(context, thread, index, len(events))
|
||||
if record is not None:
|
||||
events.append(record)
|
||||
if len(events) >= context["config"]["max_events"]:
|
||||
return "event-limit"
|
||||
elif reason in (lldb.eStopReasonSignal, lldb.eStopReasonException):
|
||||
other_stops.append(thread.GetStopDescription(256))
|
||||
# One trace runs in each owned LLDB subprocess. Its callbacks execute at the
|
||||
# breakpoint before LLDB resumes, unlike snapshots of later public stop events.
|
||||
_TRACE_CONTEXT = None
|
||||
|
||||
|
||||
def _handle_breakpoint(context, frame, index):
|
||||
"""Read one hit while LLDB's breakpoint callback owns the stopped frame."""
|
||||
if not frame.IsValid() or frame.GetPC() == lldb.LLDB_INVALID_ADDRESS:
|
||||
raise RuntimeError("LLDB breakpoint callback did not supply a readable frame")
|
||||
thread = frame.GetThread()
|
||||
events = context["events"]
|
||||
location = _location(frame.GetPCAddress(), context["target"])
|
||||
symbol = location["symbol"] or ""
|
||||
if not _wanted(context["config"]["breakpoints"][index], symbol):
|
||||
return None
|
||||
frame_count = max(
|
||||
0,
|
||||
min(thread.GetNumFrames() - 1, context["config"]["backtrace_frames"]),
|
||||
)
|
||||
event_frame_count = 1 + frame_count
|
||||
if not _can_retain_trace(
|
||||
context["retained_trace_bytes"],
|
||||
context["retained_frames"],
|
||||
0,
|
||||
event_frame_count,
|
||||
):
|
||||
return "resource-limit"
|
||||
try:
|
||||
remaining_bytes = MAX_TRACE_BYTES - context["retained_trace_bytes"]
|
||||
record = _record(
|
||||
context, thread, frame, index, len(events), location, remaining_bytes
|
||||
)
|
||||
except ValueError:
|
||||
return "resource-limit"
|
||||
if record is not None:
|
||||
if not _fits_trace_budget(record, remaining_bytes):
|
||||
return "resource-limit"
|
||||
record_bytes = len(json.dumps(record).encode("utf-8"))
|
||||
if context["retained_trace_bytes"] + record_bytes > MAX_TRACE_BYTES:
|
||||
return "resource-limit"
|
||||
for depth in range(1, frame_count + 1):
|
||||
try:
|
||||
location = _location(
|
||||
thread.GetFrameAtIndex(depth).GetPCAddress(), context["target"]
|
||||
)
|
||||
except ValueError:
|
||||
return "resource-limit"
|
||||
if (
|
||||
_estimated_json_size(location) + record_bytes + 1
|
||||
> MAX_TRACE_BYTES - context["retained_trace_bytes"]
|
||||
):
|
||||
return "resource-limit"
|
||||
location_bytes = len(json.dumps(location).encode("utf-8"))
|
||||
addition = location_bytes + (1 if record["backtrace"] else 0)
|
||||
if (
|
||||
context["retained_trace_bytes"]
|
||||
+ record_bytes
|
||||
+ addition
|
||||
> MAX_TRACE_BYTES
|
||||
):
|
||||
return "resource-limit"
|
||||
record["backtrace"].append(location)
|
||||
record_bytes += addition
|
||||
journal = context.get("observation_journal")
|
||||
if journal is not None and not journal.append(
|
||||
{"kind": "event", "event": record}
|
||||
):
|
||||
return "resource-limit"
|
||||
events.append(record)
|
||||
context["retained_frames"] += event_frame_count
|
||||
context["retained_trace_bytes"] += record_bytes
|
||||
if len(events) >= context["config"]["max_events"]:
|
||||
return "event-limit"
|
||||
return None
|
||||
|
||||
|
||||
def breakpoint_hit(frame, location, internal_dict):
|
||||
"""LLDB callback: capture before resuming, and stop on a limit or failure."""
|
||||
context = _TRACE_CONTEXT
|
||||
if context is None or context["outcome"] is not None or context["failure"] is not None:
|
||||
return True
|
||||
try:
|
||||
index = context["breakpoint_index"][location.GetBreakpoint().GetID()]
|
||||
context["outcome"] = _handle_breakpoint(context, frame, index)
|
||||
except Exception as error:
|
||||
context["failure"] = "%s: %s" % (type(error).__name__, error)
|
||||
return True
|
||||
return context["outcome"] is not None
|
||||
|
||||
|
||||
def _handle_stop(context, other_stops):
|
||||
"""Retain non-breakpoint stop observations; callbacks already capture hits."""
|
||||
for thread in context["process"]:
|
||||
if thread.GetStopReason() in (lldb.eStopReasonSignal, lldb.eStopReasonException):
|
||||
stop_description = thread.GetStopDescription(256)
|
||||
journal = context.get("observation_journal")
|
||||
if journal is not None and not journal.append(
|
||||
{"kind": "other-stop", "reason": stop_description}
|
||||
):
|
||||
return "resource-limit"
|
||||
other_stops.append(stop_description)
|
||||
if len(other_stops) >= MAX_OTHER_STOPS:
|
||||
return "stop-limit"
|
||||
return None
|
||||
|
||||
|
||||
def _breakpoint_report(target, created):
|
||||
def _breakpoint_report(target, created, config):
|
||||
report = []
|
||||
retained_location_bytes = 0
|
||||
locations_truncated = False
|
||||
for index, breakpoints in enumerate(created):
|
||||
locations = []
|
||||
count = 0
|
||||
for breakpoint in breakpoints:
|
||||
count += breakpoint.GetNumLocations()
|
||||
for position in range(breakpoint.GetNumLocations()):
|
||||
address = breakpoint.GetLocationAtIndex(position).GetAddress()
|
||||
location = _location(address, target)
|
||||
if not _wanted(config["breakpoints"][index], location["symbol"] or ""):
|
||||
continue
|
||||
count += 1
|
||||
if len(locations) < MAX_REPORTED_LOCATIONS:
|
||||
locations.append(
|
||||
_location(breakpoint.GetLocationAtIndex(position).GetAddress(), target)
|
||||
remaining_bytes = (
|
||||
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
|
||||
)
|
||||
if not _fits_trace_budget(location, remaining_bytes):
|
||||
locations_truncated = True
|
||||
continue
|
||||
location_bytes = len(json.dumps(location).encode("utf-8"))
|
||||
addition = location_bytes + (1 if locations else 0)
|
||||
if addition > remaining_bytes:
|
||||
locations_truncated = True
|
||||
continue
|
||||
locations.append(location)
|
||||
retained_location_bytes += addition
|
||||
report.append({"index": index, "location_count": count, "locations": locations})
|
||||
return report
|
||||
return report, locations_truncated
|
||||
|
||||
|
||||
def _launch_info(target, config, listener):
|
||||
launch = lldb.SBLaunchInfo(config["arguments"])
|
||||
launch.SetListener(listener)
|
||||
_set_launch_flags(launch)
|
||||
environment = target.GetEnvironment()
|
||||
for key, value in config["environment"].items():
|
||||
environment.Set(key, value, True)
|
||||
@@ -210,81 +546,281 @@ def _wait_for_exit(listener, process, seconds):
|
||||
listener.WaitForEvent(1, event)
|
||||
|
||||
|
||||
def _failed_launch_description(process):
|
||||
if process.GetState() != lldb.eStateExited or process.GetExitStatus() != -1:
|
||||
return None
|
||||
description = process.GetExitDescription()
|
||||
if isinstance(description, str) and description.lower().startswith(("attach failed", "launch failed")):
|
||||
return description
|
||||
return None
|
||||
|
||||
|
||||
def trace(debugger, config):
|
||||
global _TRACE_CONTEXT
|
||||
debugger.SetAsync(True)
|
||||
# Stop at the symbol itself, where argument registers still hold the ABI
|
||||
# arguments, even when line tables would let LLDB skip the prologue.
|
||||
lldb.SBDebugger.SetInternalVariable(
|
||||
"target.skip-prologue", "false", debugger.GetInstanceName()
|
||||
)
|
||||
try:
|
||||
before_digest = _hash_file(config["executable"])
|
||||
before_identity = _file_identity(config["executable"])
|
||||
except OSError as error:
|
||||
return {"status": "target-integrity-error", "error": str(error)}
|
||||
if before_digest != config["expected_sha256"]:
|
||||
return {
|
||||
"status": "target-integrity-error",
|
||||
"error": "selected executable digest changed before LLDB target creation",
|
||||
}
|
||||
error = lldb.SBError()
|
||||
target = debugger.CreateTarget(
|
||||
config["executable"], config["architecture"], None, False, error
|
||||
)
|
||||
if not target.IsValid():
|
||||
return {"status": "target-error", "error": error.GetCString()}
|
||||
module_path_before, module_file_identity_before, module_uuid_before = (
|
||||
_module_identity(target)
|
||||
)
|
||||
if (
|
||||
module_file_identity_before is not None
|
||||
and module_file_identity_before != before_identity
|
||||
):
|
||||
return {
|
||||
"status": "target-integrity-error",
|
||||
"error": "LLDB executable module resolved to a different pre-launch file identity",
|
||||
}
|
||||
created = [_create_breakpoints(target, spec) for spec in config["breakpoints"]]
|
||||
breakpoint_index = {}
|
||||
for index, breakpoints in enumerate(created):
|
||||
for breakpoint in breakpoints:
|
||||
breakpoint_index[breakpoint.GetID()] = index
|
||||
listener = lldb.SBListener(LISTENER_NAME)
|
||||
output_budget = config.get("max_output_bytes", MAX_TARGET_OUTPUT_BYTES)
|
||||
stdout = _OutputPipe(
|
||||
config["stdout_path"], config["stdout_capture_path"], output_budget
|
||||
)
|
||||
try:
|
||||
stderr = _OutputPipe(
|
||||
config["stderr_path"], config["stderr_capture_path"], output_budget
|
||||
)
|
||||
except Exception:
|
||||
stdout.close()
|
||||
raise
|
||||
stdout.start()
|
||||
stderr.start()
|
||||
observation_journal = None
|
||||
started = time.monotonic()
|
||||
process = target.Launch(_launch_info(target, config, listener), error)
|
||||
if not error.Success() or not process.IsValid():
|
||||
return {"status": "launch-error", "error": error.GetCString()}
|
||||
with open(config["pid_path"], "w") as handle:
|
||||
handle.write(str(process.GetProcessID()))
|
||||
context = {
|
||||
"target": target,
|
||||
"process": process,
|
||||
"config": config,
|
||||
"started": started,
|
||||
"breakpoint_index": breakpoint_index,
|
||||
}
|
||||
events = []
|
||||
other_stops = []
|
||||
outcome = None
|
||||
deadline = started + config["duration_ms"] / 1000
|
||||
event = lldb.SBEvent()
|
||||
while outcome is None:
|
||||
if time.monotonic() >= deadline:
|
||||
outcome = "duration-elapsed"
|
||||
break
|
||||
if not listener.WaitForEvent(1, event):
|
||||
continue
|
||||
if not lldb.SBProcess.EventIsProcessEvent(event):
|
||||
continue
|
||||
state = lldb.SBProcess.GetStateFromEvent(event)
|
||||
if state == lldb.eStateExited:
|
||||
outcome = "exited"
|
||||
elif state == lldb.eStateStopped and not lldb.SBProcess.GetRestartedFromEvent(event):
|
||||
outcome = _handle_stop(context, events, other_stops)
|
||||
if outcome is None:
|
||||
process.Continue()
|
||||
pid = process.GetProcessID()
|
||||
exit_status = None
|
||||
exit_description = None
|
||||
if outcome == "exited":
|
||||
exit_status = process.GetExitStatus()
|
||||
exit_description = process.GetExitDescription()
|
||||
else:
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
return {
|
||||
"status": "traced",
|
||||
"version": lldb.SBDebugger.GetVersionString().splitlines()[0],
|
||||
"pid": pid,
|
||||
"outcome": outcome,
|
||||
"exit_status": exit_status,
|
||||
"exit_description": exit_description,
|
||||
"killed": outcome != "exited",
|
||||
"terminated": process.GetState() == lldb.eStateExited,
|
||||
"elapsed_ms": round((time.monotonic() - started) * 1000, 3),
|
||||
"breakpoints": _breakpoint_report(target, created),
|
||||
"events": events,
|
||||
"other_stops": other_stops,
|
||||
}
|
||||
process = None
|
||||
try:
|
||||
observation_path = config.get("observation_path")
|
||||
if observation_path:
|
||||
observation_journal = _ObservationJournal(
|
||||
open(observation_path, "wb")
|
||||
)
|
||||
process = target.Launch(_launch_info(target, config, listener), error)
|
||||
if not error.Success() or not process.IsValid():
|
||||
stdout.close()
|
||||
stderr.close()
|
||||
return {"status": "launch-error", "error": error.GetCString()}
|
||||
launch_failure = _failed_launch_description(process)
|
||||
if launch_failure is not None:
|
||||
stdout.close(expected_exit=True)
|
||||
stderr.close(expected_exit=True)
|
||||
return {"status": "launch-error", "error": launch_failure}
|
||||
stdout.mark_launched()
|
||||
stderr.mark_launched()
|
||||
# Publish the PID before any post-launch file or module inspection. The
|
||||
# process is stopped at entry and remains there until identity_ack.
|
||||
with open(config["pid_path"], "w") as handle:
|
||||
handle.write(str(process.GetProcessID()))
|
||||
identity_ack = _wait_for_identity_ack(config)
|
||||
if identity_ack != "readable":
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
expected_exit = process.GetState() == lldb.eStateExited
|
||||
stdout.close(expected_exit=expected_exit)
|
||||
stderr.close(expected_exit=expected_exit)
|
||||
return {
|
||||
"status": "target-identity-unavailable",
|
||||
"error": "host could not establish the launched process identity; target was stopped",
|
||||
}
|
||||
try:
|
||||
after_digest = _hash_file(config["executable"])
|
||||
after_identity = _file_identity(config["executable"])
|
||||
except OSError as identity_error:
|
||||
process.Kill()
|
||||
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
return {"status": "target-integrity-error", "error": str(identity_error)}
|
||||
if after_digest != config["expected_sha256"] or after_identity != before_identity:
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
return {
|
||||
"status": "target-integrity-error",
|
||||
"error": "selected executable digest or file identity changed during LLDB launch",
|
||||
}
|
||||
module_path_after, module_file_identity_after, module_uuid_after = (
|
||||
_module_identity(target)
|
||||
)
|
||||
if (
|
||||
module_file_identity_after is not None
|
||||
and module_file_identity_after != before_identity
|
||||
):
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
return {
|
||||
"status": "target-integrity-error",
|
||||
"error": "LLDB executable module resolved to a different file identity",
|
||||
}
|
||||
if (
|
||||
module_uuid_before is not None
|
||||
and module_uuid_after is not None
|
||||
and module_uuid_before != module_uuid_after
|
||||
):
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
|
||||
return {
|
||||
"status": "target-integrity-error",
|
||||
"error": "LLDB executable module identity changed during launch",
|
||||
}
|
||||
context = {
|
||||
"target": target,
|
||||
"process": process,
|
||||
"config": config,
|
||||
"started": started,
|
||||
"breakpoint_index": breakpoint_index,
|
||||
"retained_frames": 0,
|
||||
"retained_trace_bytes": 0,
|
||||
"observation_journal": observation_journal,
|
||||
}
|
||||
events = []
|
||||
context.update({"events": events, "outcome": None, "failure": None})
|
||||
_TRACE_CONTEXT = context
|
||||
for breakpoints in created:
|
||||
for breakpoint in breakpoints:
|
||||
breakpoint.SetScriptCallbackFunction("rea_lldb_tracer.breakpoint_hit")
|
||||
other_stops = []
|
||||
outcome = None
|
||||
deadline = started + config["duration_ms"] / 1000
|
||||
event = lldb.SBEvent()
|
||||
while outcome is None:
|
||||
if stdout.reader_error is not None:
|
||||
raise RuntimeError("stdout capture failed: %s" % stdout.reader_error)
|
||||
if stderr.reader_error is not None:
|
||||
raise RuntimeError("stderr capture failed: %s" % stderr.reader_error)
|
||||
if time.monotonic() >= deadline:
|
||||
outcome = "duration-elapsed"
|
||||
break
|
||||
if not listener.WaitForEvent(1, event):
|
||||
continue
|
||||
if not lldb.SBProcess.EventIsProcessEvent(event):
|
||||
continue
|
||||
state = lldb.SBProcess.GetStateFromEvent(event)
|
||||
if state == lldb.eStateExited:
|
||||
outcome = "exited"
|
||||
elif state == lldb.eStateStopped and not lldb.SBProcess.GetRestartedFromEvent(event):
|
||||
if context["failure"] is not None:
|
||||
raise RuntimeError(context["failure"])
|
||||
outcome = context["outcome"] or _handle_stop(context, other_stops)
|
||||
if outcome is None:
|
||||
process.Continue()
|
||||
pid = process.GetProcessID()
|
||||
exit_status = None
|
||||
exit_description = None
|
||||
if outcome == "exited":
|
||||
exit_status = process.GetExitStatus()
|
||||
exit_description = process.GetExitDescription()
|
||||
else:
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
process_exited = process.GetState() == lldb.eStateExited
|
||||
stdout.close(expected_exit=process_exited)
|
||||
stderr.close(expected_exit=process_exited)
|
||||
# SBTarget.Launch may succeed before debugserver reports an asynchronous
|
||||
# attach failure as an exited SBProcess. Its -1 status is not a target
|
||||
# exit code; preserve the debugger's concrete launch denial.
|
||||
launch_failure = _failed_launch_description(process)
|
||||
if launch_failure is not None:
|
||||
return {"status": "launch-error", "error": launch_failure}
|
||||
launch_identity_stable = (
|
||||
module_path_before is not None
|
||||
and module_path_after == module_path_before
|
||||
and module_file_identity_before == before_identity
|
||||
and module_file_identity_after == module_file_identity_before
|
||||
and module_uuid_before is not None
|
||||
and module_uuid_after == module_uuid_before
|
||||
)
|
||||
breakpoint_report, breakpoint_locations_truncated = _breakpoint_report(
|
||||
target, created, config
|
||||
)
|
||||
return {
|
||||
"status": "traced",
|
||||
"version": lldb.SBDebugger.GetVersionString().splitlines()[0],
|
||||
"pid": pid,
|
||||
"outcome": outcome,
|
||||
"exit_status": exit_status,
|
||||
"exit_description": exit_description,
|
||||
"killed": outcome != "exited",
|
||||
"terminated": process.GetState() == lldb.eStateExited,
|
||||
"elapsed_ms": round((time.monotonic() - started) * 1000, 3),
|
||||
"breakpoints": breakpoint_report,
|
||||
"breakpoint_locations_truncated": breakpoint_locations_truncated,
|
||||
"events": events,
|
||||
"resource_limit_reached": outcome == "resource-limit",
|
||||
"target_output": {
|
||||
"stdout_bytes": stdout.bytes,
|
||||
"stderr_bytes": stderr.bytes,
|
||||
"stdout_truncated": stdout.truncated,
|
||||
"stderr_truncated": stderr.truncated,
|
||||
"stdout_complete": stdout.complete,
|
||||
"stderr_complete": stderr.complete,
|
||||
},
|
||||
"target_identity": {
|
||||
"selected_file_sha256": after_digest,
|
||||
"loaded_image_sha256": None,
|
||||
"file_device": after_identity[0],
|
||||
"file_inode": after_identity[1],
|
||||
"module_path": module_path_after,
|
||||
"module_uuid": module_uuid_after,
|
||||
# This records matching path/module identity observations. LLDB
|
||||
# does not expose the exact mapped bytes, so it never proves a digest.
|
||||
"stable": launch_identity_stable,
|
||||
},
|
||||
"other_stops": other_stops,
|
||||
}
|
||||
|
||||
finally:
|
||||
_TRACE_CONTEXT = None
|
||||
if observation_journal is not None:
|
||||
try:
|
||||
observation_journal.handle.close()
|
||||
except Exception:
|
||||
pass
|
||||
if process is not None:
|
||||
try:
|
||||
if process.IsValid() and process.GetState() != lldb.eStateExited:
|
||||
process.Kill()
|
||||
_wait_for_exit(listener, process, 5)
|
||||
except Exception:
|
||||
pass
|
||||
for output_pipe in (stdout, stderr):
|
||||
try:
|
||||
output_pipe.close(
|
||||
expected_exit=(
|
||||
process is not None
|
||||
and process.IsValid()
|
||||
and process.GetState() == lldb.eStateExited
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run(debugger, command, result, internal_dict):
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Source-owned tests for the LLDB bridge's bounded retention helpers."""
|
||||
|
||||
from io import BytesIO
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
|
||||
|
||||
fake_lldb = sys.modules.setdefault("lldb", types.ModuleType("lldb"))
|
||||
fake_lldb.LLDB_INVALID_ADDRESS = -1
|
||||
fake_lldb.eLaunchFlagDebug = 2
|
||||
fake_lldb.eLaunchFlagStopAtEntry = 4
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"rea_lldb_tracer", os.path.join(os.path.dirname(__file__), "rea_lldb_tracer.py")
|
||||
)
|
||||
tracer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(tracer)
|
||||
|
||||
|
||||
class NativeTraceBudgetTests(unittest.TestCase):
|
||||
def test_journal_checks_exact_byte_budget_before_writing(self):
|
||||
record = {"kind": "other-stop", "reason": "signal"}
|
||||
encoded_size = len((tracer.json.dumps(record) + "\n").encode("utf-8"))
|
||||
stream = BytesIO()
|
||||
journal = tracer._ObservationJournal(stream, max_bytes=encoded_size - 1)
|
||||
self.assertFalse(journal.append(record))
|
||||
self.assertEqual(stream.getvalue(), b"")
|
||||
self.assertEqual(journal.bytes, 0)
|
||||
|
||||
def test_journal_appends_each_row_once_and_flushes_it(self):
|
||||
class CountingStream(BytesIO):
|
||||
writes = 0
|
||||
flushes = 0
|
||||
|
||||
def write(self, value):
|
||||
self.writes += 1
|
||||
return super().write(value)
|
||||
|
||||
def flush(self):
|
||||
self.flushes += 1
|
||||
return super().flush()
|
||||
|
||||
stream = CountingStream()
|
||||
journal = tracer._ObservationJournal(stream)
|
||||
journal.append({"kind": "event", "event": {"sequence": 0}})
|
||||
journal.append({"kind": "other-stop", "reason": "signal"})
|
||||
self.assertEqual((stream.writes, stream.flushes), (2, 2))
|
||||
self.assertEqual(journal.bytes, len(stream.getvalue()))
|
||||
|
||||
def test_aggregate_trace_budget_admits_exact_boundary_and_rejects_next_record(self):
|
||||
self.assertTrue(
|
||||
tracer._can_retain_trace(
|
||||
tracer.MAX_TRACE_BYTES - 10,
|
||||
tracer.MAX_TRACE_FRAMES - 2,
|
||||
10,
|
||||
2,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
tracer._can_retain_trace(
|
||||
tracer.MAX_TRACE_BYTES - 10,
|
||||
tracer.MAX_TRACE_FRAMES - 2,
|
||||
11,
|
||||
2,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
tracer._can_retain_trace(
|
||||
tracer.MAX_TRACE_BYTES - 10,
|
||||
tracer.MAX_TRACE_FRAMES - 2,
|
||||
10,
|
||||
3,
|
||||
)
|
||||
)
|
||||
|
||||
def test_oversized_producer_string_is_rejected_before_serialization(self):
|
||||
self.assertTrue(tracer._fits_trace_budget("method", 1024))
|
||||
self.assertFalse(tracer._fits_trace_budget("m" * 2048, 1024))
|
||||
self.assertGreater(
|
||||
tracer._estimated_json_size("🙂"), len('"🙂"'.encode("utf-8"))
|
||||
)
|
||||
|
||||
def test_launch_flags_preserve_default_debug_and_stop_at_entry(self):
|
||||
class Launch:
|
||||
flags = 10
|
||||
|
||||
def GetLaunchFlags(self):
|
||||
return self.flags
|
||||
|
||||
def SetLaunchFlags(self, flags):
|
||||
self.flags = flags
|
||||
|
||||
launch = Launch()
|
||||
tracer._set_launch_flags(launch)
|
||||
self.assertEqual(launch.flags, 14)
|
||||
|
||||
def test_output_pipe_counts_all_bytes_and_keeps_only_the_prefix(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fifo = os.path.join(directory, "stdout.fifo")
|
||||
capture = os.path.join(directory, "stdout.capture")
|
||||
sink = tracer._OutputPipe(fifo, capture, 5)
|
||||
sink.start()
|
||||
sink.mark_launched()
|
||||
writer = os.open(fifo, os.O_WRONLY)
|
||||
try:
|
||||
for index in range(100):
|
||||
chunk = (b"hello world" if index == 0 else b"x" * 4096)
|
||||
os.write(writer, chunk)
|
||||
finally:
|
||||
os.close(writer)
|
||||
sink.drain()
|
||||
sink.close(expected_exit=True)
|
||||
with open(capture, "rb") as retained:
|
||||
self.assertEqual(retained.read(), b"hello")
|
||||
self.assertEqual(sink.bytes, 11 + 99 * 4096)
|
||||
self.assertTrue(sink.truncated)
|
||||
self.assertTrue(sink.complete)
|
||||
|
||||
def test_output_pipe_reports_incomplete_capture_after_unexpected_close(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fifo = os.path.join(directory, "stdout.fifo")
|
||||
capture = os.path.join(directory, "stdout.capture")
|
||||
sink = tracer._OutputPipe(fifo, capture, 5)
|
||||
sink.start()
|
||||
sink.mark_launched()
|
||||
sink.close(expected_exit=False)
|
||||
self.assertFalse(sink.complete)
|
||||
self.assertTrue(sink.truncated)
|
||||
|
||||
def test_selector_location_report_uses_the_requested_method_type(self):
|
||||
class Symbol:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def GetName(self):
|
||||
return "+[Greeter shared]"
|
||||
|
||||
class FileSpec:
|
||||
def GetFilename(self):
|
||||
return "Fixture"
|
||||
|
||||
def __str__(self):
|
||||
return "/tmp/Fixture"
|
||||
|
||||
class Module:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def GetFileSpec(self):
|
||||
return FileSpec()
|
||||
|
||||
class Address:
|
||||
def GetModule(self):
|
||||
return Module()
|
||||
|
||||
def GetSymbol(self):
|
||||
return Symbol()
|
||||
|
||||
def GetFileAddress(self):
|
||||
return 1
|
||||
|
||||
def GetLoadAddress(self, target):
|
||||
return 2
|
||||
|
||||
class Location:
|
||||
def GetAddress(self):
|
||||
return Address()
|
||||
|
||||
class Breakpoint:
|
||||
def GetNumLocations(self):
|
||||
return 1
|
||||
|
||||
def GetLocationAtIndex(self, index):
|
||||
return Location()
|
||||
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
object(),
|
||||
[[Breakpoint()]],
|
||||
{
|
||||
"breakpoints": [
|
||||
{
|
||||
"kind": "objc-method",
|
||||
"selector": "shared",
|
||||
"method_type": "instance",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
self.assertEqual(report[0]["location_count"], 0)
|
||||
self.assertEqual(report[0]["locations"], [])
|
||||
self.assertFalse(locations_truncated)
|
||||
|
||||
def test_breakpoint_report_bounds_aggregate_location_json(self):
|
||||
tracer.lldb.LLDB_INVALID_ADDRESS = -1
|
||||
symbol_name = (
|
||||
"-[VeryLongButPlausibleObjectiveCController "
|
||||
"performTransactionWithRequestIdentifier:accountContext:"
|
||||
"securityPolicy:completionHandler:]"
|
||||
)
|
||||
module_path = (
|
||||
"/Applications/Enterprise Product.app/Contents/Frameworks/"
|
||||
"EnterpriseSubsystem.framework/Versions/A/EnterpriseSubsystem"
|
||||
)
|
||||
|
||||
class Symbol:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def GetName(self):
|
||||
return symbol_name
|
||||
|
||||
class FileSpec:
|
||||
def GetFilename(self):
|
||||
return "EnterpriseSubsystem"
|
||||
|
||||
def __str__(self):
|
||||
return module_path
|
||||
|
||||
class Module:
|
||||
def IsValid(self):
|
||||
return True
|
||||
|
||||
def GetFileSpec(self):
|
||||
return FileSpec()
|
||||
|
||||
class Address:
|
||||
def GetModule(self):
|
||||
return Module()
|
||||
|
||||
def GetSymbol(self):
|
||||
return Symbol()
|
||||
|
||||
def GetFileAddress(self):
|
||||
return 0x123456789
|
||||
|
||||
def GetLoadAddress(self, target):
|
||||
return 0x7123456789
|
||||
|
||||
class Location:
|
||||
def GetAddress(self):
|
||||
return Address()
|
||||
|
||||
class Breakpoint:
|
||||
def GetNumLocations(self):
|
||||
return 64
|
||||
|
||||
def GetLocationAtIndex(self, index):
|
||||
return Location()
|
||||
|
||||
breakpoint_count = 1000
|
||||
created = [[Breakpoint()] for _ in range(breakpoint_count)]
|
||||
config = {"breakpoints": [{"kind": "objc-method"}] * breakpoint_count}
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
object(), created, config
|
||||
)
|
||||
retained_bytes = sum(
|
||||
len(json.dumps(location).encode("utf-8"))
|
||||
+ (1 if index > 0 else 0)
|
||||
for row in report
|
||||
for index, location in enumerate(row["locations"])
|
||||
)
|
||||
self.assertTrue(locations_truncated)
|
||||
self.assertEqual(sum(row["location_count"] for row in report), 64_000)
|
||||
self.assertLess(sum(len(row["locations"]) for row in report), 64_000)
|
||||
self.assertLessEqual(retained_bytes, tracer.MAX_BREAKPOINT_LOCATION_BYTES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -152,7 +152,14 @@ private func readRunToken(pid: Int32, pointerSize: Int) -> RunTokenObservation {
|
||||
}
|
||||
bytes = Array(bytes.prefix(size))
|
||||
do {
|
||||
return RunTokenObservation(pid: pid, state: "readable", run_id: try token(in: bytes, pointerSize: pointerSize), reason: nil)
|
||||
let runId = try token(in: bytes, pointerSize: pointerSize)
|
||||
// A complete empty environment is readable for an ordinary image. Apple
|
||||
// platform images can expose the same layout after the kernel withholds
|
||||
// their environment, so they remain explicitly unverified without a token.
|
||||
if runId == nil && isPlatformBinary(pid: pid) {
|
||||
return unavailable(pid: pid, reason: "platform_binary_environment_withheld")
|
||||
}
|
||||
return RunTokenObservation(pid: pid, state: "readable", run_id: runId, reason: nil)
|
||||
} catch RunTokenReadError.duplicateToken {
|
||||
return unavailable(pid: pid, reason: "duplicate_run_token")
|
||||
} catch RunTokenReadError.invalidTokenEncoding {
|
||||
@@ -259,9 +266,6 @@ func token(in bytes: [UInt8], pointerSize: Int = MemoryLayout<UnsafeRawPointer>.
|
||||
throw RunTokenReadError.ambiguousEnvironmentBoundary
|
||||
}
|
||||
let environmentEntries = trailingEntries[..<appleVectorStart]
|
||||
guard !environmentEntries.isEmpty else {
|
||||
throw RunTokenReadError.environmentUnavailable
|
||||
}
|
||||
var found: String?
|
||||
for entry in environmentEntries {
|
||||
if entry.starts(with: Array("REA_PROCESS_RUN_ID=".utf8)) {
|
||||
|
||||
@@ -43,6 +43,10 @@ def inspect_core(path, cache):
|
||||
raise CoreFailure("unsupported-target", "Initial recorded-core profile requires ELF64 little-endian.")
|
||||
if len(content) < 64:
|
||||
raise CoreFailure("format", "Selected ELF64 core header is truncated.")
|
||||
if content[6] != 1:
|
||||
raise CoreFailure("format", "ELF identification version is not EV_CURRENT.")
|
||||
if int.from_bytes(content[20:24], "little") != 1:
|
||||
raise CoreFailure("format", "ELF header version is not EV_CURRENT.")
|
||||
# Linux commonly emits ELFOSABI_NONE (0), also accepted as GNU/Linux (3).
|
||||
# Other declared OSABIs must never select Linux note ctypes.
|
||||
if content[7] not in (0, 3) or content[8] != 0:
|
||||
|
||||
@@ -23,6 +23,14 @@ export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
|
||||
export JAVA_HOME=/absolute/path/existing-jdk
|
||||
```
|
||||
|
||||
Readiness checks the selected JAR's ZIP directory for the classes consumed by
|
||||
REA's metadata bridge, as well as Java's compiler and version modules. It does
|
||||
not load an APK or execute engine code. Invalid archives and missing bridge
|
||||
classes report JAR-specific recovery; classpath inventory alone does not prove
|
||||
that every class or engine operation will load successfully. Discovery limits
|
||||
each ZIP metadata read to 8 MiB before allocation; a larger directory reports
|
||||
the exhausted read budget and the selected JAR path.
|
||||
|
||||
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
|
||||
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
|
||||
bridge has not yet undergone real verification on Linux/JDK 25.
|
||||
@@ -111,7 +119,14 @@ rea project-android-application-graph '{"inventory_evidence":[<inventory_artifac
|
||||
|
||||
The projection reports exact component paths and hashes, runtime-family hints,
|
||||
and path-based bridge hypotheses. It does not decode DEX or claim observed
|
||||
runtime calls.
|
||||
runtime calls. Bridge hypotheses form a DEX/JVM-class by native-library
|
||||
Cartesian product. The projection computes the product and exact serialized
|
||||
candidate-array size before creating candidate objects, then retains a
|
||||
deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted,
|
||||
`bridge_candidate_coverage` reports total, emitted, and omitted counts; overall
|
||||
coverage is `partial` while component arrays still include every component
|
||||
from the supplied inventory pages. A path pair does not establish that a
|
||||
managed declaration names, loads, or calls the native library.
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
@@ -123,6 +138,13 @@ target/configuration changes, failure, timeout, cancellation, MCP disconnect or
|
||||
server shutdown. One-shot CLI commands always join cleanup before returning.
|
||||
REA never writes to the original APK or launches target code.
|
||||
|
||||
If Java cannot start the bridge, REA reports the selected executable and retained
|
||||
startup diagnostics. Select a full JDK through `JAVA_HOME` or `PATH`, and use
|
||||
`java --list-modules` to confirm `jdk.compiler` is present. A runnable JRE alone
|
||||
cannot compile the bridge. Missing or ambiguous class and method selections are
|
||||
input failures; discover names with `search_android_classes` and
|
||||
`inspect_android_class` before correcting the selected argument.
|
||||
|
||||
If process cleanup cannot be confirmed, REA retains the workspace and reports its
|
||||
location. That provider instance blocks subsequent engine launches until the
|
||||
caller resolves the reported resources and starts a fresh instance.
|
||||
@@ -170,6 +192,11 @@ export REA_ANDROID_TEST_APK="$PWD/_reference/apk-integration/ApiDemos-debug.apk"
|
||||
npm run verify:android
|
||||
```
|
||||
|
||||
The lane also uses the selected full JDK's `jlink` to create a disposable JRE
|
||||
without the compiler module. It checks real Java startup failures, missing
|
||||
selectors, recovery after failures, and CLI cancellation with owned cleanup.
|
||||
The original APK and host Java configuration remain unchanged.
|
||||
|
||||
The download script verifies fixed SHA-256 values, reuses matching files and
|
||||
refuses to overwrite a different existing file. APK/JAR files live under ignored
|
||||
`_reference/` and are excluded from package contents. The lane exercises actual
|
||||
|
||||
@@ -20,16 +20,27 @@ rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact
|
||||
}
|
||||
```
|
||||
|
||||
The result includes every component in each inventory category and every
|
||||
JavaScript-to-native bridge candidate pair. It has no caller-selected component
|
||||
budget, prefix truncation, or omitted-count fields. Duplicate inventory pages
|
||||
are merged by artifact identity and occurrence path; their Evidence IDs remain
|
||||
attached as source Evidence.
|
||||
The result includes every component in each inventory category. Bridge
|
||||
hypotheses are the path-based JavaScript/native pairs within each application
|
||||
root. To keep their Cartesian expansion bounded, the projection limits the
|
||||
serialized candidate array to 2 MiB. It computes the pair count and
|
||||
exact UTF-8 JSON size before creating candidate objects, then retains a
|
||||
deterministic prefix when the byte budget is exceeded. The result's
|
||||
`bridge_candidate_coverage` reports total, emitted, and omitted pair counts;
|
||||
overall coverage becomes `partial` and a limitation explains the omission.
|
||||
Duplicate inventory pages are merged by artifact identity and occurrence path;
|
||||
their Evidence IDs remain attached as source Evidence.
|
||||
|
||||
For IPA inventories, archive components outside every application root remain
|
||||
in the component lists. Their JavaScript and native candidates are grouped with
|
||||
other unrooted components, preserving the inventory projection's path pairing
|
||||
semantics without attributing them to an application bundle.
|
||||
|
||||
Coverage is `complete-within-inventory` when the supplied pages reconstruct the
|
||||
complete authenticated inventory, and `partial` otherwise. A partial result
|
||||
states that absence is unknown. Component and bridge-candidate arrays are still
|
||||
the complete projection of the inventory that was supplied.
|
||||
states that absence is unknown. Component arrays remain complete when candidate
|
||||
hypotheses are omitted; component arrays still include every component from
|
||||
the supplied inventory pages, and overall coverage is `partial` in that case.
|
||||
|
||||
## Application roots
|
||||
|
||||
@@ -177,10 +188,22 @@ cache rather than on disk, so REA does not check them against the host.
|
||||
- `required-load-unresolved`, `weak-load-unresolved`, and `lazy-load-unresolved`.
|
||||
`LC_LAZY_LOAD_DYLIB` dependencies are resolved but not traversed, because dyld
|
||||
loads them on first use; an unresolved one is not a launch failure.
|
||||
- `earlier-rpath-candidate-absent`: a Mach-O placed at an earlier search path
|
||||
would load first. Whether code-signing library validation would reject it is
|
||||
not evaluated; use `inspect_signature`.
|
||||
- `dyld-environment-present`: `LC_DYLD_ENVIRONMENT` search paths are not modeled.
|
||||
- `earlier-rpath-candidate-absent`: a compatible Mach-O placed at an earlier
|
||||
modeled search path could take precedence, subject to unmodeled search inputs
|
||||
and code-signing library validation. These findings do not establish which
|
||||
image dyld will load; use `inspect_signature` for signing metadata.
|
||||
- `dyld-environment-present`: preserves observed `LC_DYLD_ENVIRONMENT` entries.
|
||||
Only an executable root supplies process settings; entries on library and
|
||||
other non-executable roots do not alter resolution or coverage. Applicable
|
||||
executable image-selection overrides are not modeled.
|
||||
|
||||
Empty dyld settings have different consumers. Modern dyld appends a slash and
|
||||
image name to ordinary and fallback directory entries, so an empty entry can
|
||||
search `/child.dylib` or `/Foo.framework/Foo`. REA retains uncertainty for those
|
||||
unchecked paths; an empty fallback does not preempt an ordinary-search hit.
|
||||
Empty versioned-directory lists do not scan a directory, and empty suffixes
|
||||
only repeat the original candidate, so they do not add search uncertainty.
|
||||
Reported command values are retained in all cases.
|
||||
|
||||
`verify:macos-bundle` checks the parser against `otool -l` for every traced
|
||||
image. It also compares the predicted load order of two process roots with the
|
||||
|
||||
@@ -138,7 +138,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Script metadata admission bounds retained identity and source-map strings to 8 MiB and 50,000 records. When a source-map URL cannot fit, the script identity remains and source-map coverage reports `resource_budget_exhausted`; rejected script observations are counted without retaining their IDs. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
|
||||
|
||||
Frames, resources, scripts, events, and workers outside the selected origin scope are excluded. An explicit `allowed_origins` list can include additional origins; excluded target details are counted without being exposed.
|
||||
Retained workers include validated opener-target and parent-frame IDs when CDP
|
||||
@@ -148,18 +148,21 @@ prove static module ownership.
|
||||
## Completeness and limits
|
||||
|
||||
The default inspection window is 500 ms; callers choose its duration. The
|
||||
capture returns all in-scope frames, DOM and accessibility nodes, scripts,
|
||||
capture returns in-scope frames, DOM and accessibility nodes, admitted scripts,
|
||||
resources, workers, storage names, and observed network, console, and WebSocket
|
||||
events. Full selected text, script sources, and JSON shapes are returned inline.
|
||||
The result distinguishes
|
||||
`complete_within_window`, `policy_filtered`, `attach_limited`, and `truncated`
|
||||
coverage. It reports excluded items and any values omitted or truncated by
|
||||
validation; it does not drop records to meet an internal collection quota.
|
||||
validation and resource bounds. Omitted script metadata makes its affected
|
||||
section explicitly truncated.
|
||||
Disallowed-origin entries are filtered before retention.
|
||||
|
||||
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited protocol events poison the connection so later commands fail closed.
|
||||
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited events and malformed replies to pending commands poison the connection so later commands fail closed. When a correlated reply reports a session ID, it must match the command's selected session. An optional method is reported unavailable only when the browser rejects it as unknown; other command rejections retain their reported failure.
|
||||
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete.
|
||||
|
||||
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation has a 30-second deadline and a 64 MiB aggregate streamed-response budget across redirects. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
|
||||
|
||||
## Non-goals and threat model
|
||||
|
||||
|
||||
@@ -86,16 +86,33 @@ rea capture-electron-scenario scenario.json --json
|
||||
|
||||
The result records action status and targets, correlated app/window/WebContents,
|
||||
preload, session, navigation, shell, permission, popup, download, protocol,
|
||||
native-addon, process, and IPC timeline events. IPC channels are capped at
|
||||
1,024 characters and argument-shape metadata at 32 entries; values are never
|
||||
retained. Renderer crash/restart and deep-link actions are synthetic
|
||||
native-addon, process, and IPC timeline events. IPC channels and argument-shape
|
||||
metadata retain their observed lengths; payload values are never retained.
|
||||
Renderer crash/restart and deep-link actions are synthetic
|
||||
scenario controls; their attempted and observed outcomes remain in the
|
||||
timeline. The active hook blocks and records external shell/navigation,
|
||||
permission, download, popup, updater, and OS-integration effects. The timeline
|
||||
is explicitly partial when attachment starts after application activity. The
|
||||
owned experiment has an internal 60-second deadline and a 5-second per-action
|
||||
timeout so hung work reaches process cleanup; these are lifecycle timeouts, not
|
||||
caller-selected capture limits.
|
||||
startup has a 60-second deadline. Actions wait until completion or request
|
||||
cancellation; a click whose target never appears remains pending until cancelled.
|
||||
CLI SIGINT/SIGTERM requests cancellation and awaits owned-process cleanup.
|
||||
Window IDs, URLs, and cached titles come from one main-process Electron snapshot,
|
||||
so a crashed renderer does not require a successful renderer-side title query.
|
||||
Window instrumentation uses Electron's window-created event, including runtimes
|
||||
with a read-only `BrowserWindow` export. Preload configuration comes from the
|
||||
window's runtime metadata, including Electron's optional internal preload
|
||||
descriptor when web preferences omit it. This descriptor is verified on
|
||||
Electron 44.7.0; runtimes that expose neither representation retain an unavailable
|
||||
path. A configured path or working IPC handler does not prove preload execution.
|
||||
If cleanup cannot be verified, the error retains collected observations in
|
||||
`details.partial_observation` and marks their application cleanup `unverified`.
|
||||
Cleanup uncertainty does not authorize signaling unrelated processes.
|
||||
|
||||
The real verifier exercises compiled CLI and stdio MCP discovery, page/script
|
||||
inspection, static/runtime reconciliation, active observation, and CLI SIGTERM
|
||||
cleanup before checking the IPC, renderer crash/reload, utility/child-process,
|
||||
deep-link, and blocked-effect fixture. A cleanup error keeps the full lane failed
|
||||
even when the retained capture contains useful observations.
|
||||
|
||||
Active Electron Evidence can also be supplied to
|
||||
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
|
||||
|
||||
@@ -129,8 +129,9 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture.
|
||||
Unblob one worker. No automatic parallel extraction engines.
|
||||
- **120-second** deadline; inherited **1 GiB per-process address-space** limit,
|
||||
120 CPU seconds and zero core-dump allowance through `prlimit`.
|
||||
- JSON reports: at most **8 MiB**; oversized reports fail without a truncated
|
||||
success. Diagnostic output is bounded with exact captured byte counts.
|
||||
- JSON reports: at most **8 MiB** and valid UTF-8; malformed encoding and
|
||||
oversized reports fail without repairing or truncating producer data.
|
||||
Diagnostic output is bounded with exact captured byte counts.
|
||||
- Default staging budget: **256 MiB** and **10,000 entries**, configurable using
|
||||
`max_output_bytes` / `max_output_files` (CLI `--max-output-bytes` /
|
||||
`--max-output-files`). Depth defaults to 3, allowed range 1–10.
|
||||
|
||||
+6
-2
@@ -1,7 +1,7 @@
|
||||
# DOS MZ and COM analysis with Ghidra
|
||||
|
||||
REA can analyze DOS MZ executables and explicitly selected COM images through the bring-your-own Ghidra adapter.
|
||||
The Linux x64 verification lane exercises actual 16-bit disassembly and
|
||||
The Linux x64 and macOS arm64 verification lanes exercise actual 16-bit disassembly and
|
||||
decompilation through the CLI and stdio MCP. Provider admission accepts Ghidra
|
||||
12.1.x and that installation's declared JDK range. The verification lane uses
|
||||
Ghidra 12.1.4 and JDK 21; no DOS emulator or cross-compiler is required.
|
||||
@@ -87,6 +87,10 @@ address for subsequent lookups and leaves instruction decoding context with
|
||||
Ghidra. These coordinates are analysis load addresses, not file offsets or
|
||||
observations of a running DOS system.
|
||||
|
||||
Offsets that Ghidra would truncate to a different address are rejected before
|
||||
reading or annotating a function. This also applies to explicit or encoded
|
||||
address-space names; leading zeros preserve the numeric offset.
|
||||
|
||||
`procedure_info` and function dossiers include `body` evidence:
|
||||
|
||||
```json
|
||||
@@ -150,7 +154,7 @@ are separate from the initialized MZ module. Raw COM files and DOS extenders
|
||||
without an admitted MZ real-mode entry are outside this import boundary.
|
||||
|
||||
DOS MZ is available through Ghidra, with no Hopper support claim. It is outside
|
||||
the Windows native PE P0 boundary. macOS DOS verification remains unverified.
|
||||
the Windows native PE P0 boundary. macOS x64 DOS verification remains unverified.
|
||||
|
||||
## Real verification
|
||||
|
||||
|
||||
+105
-13
@@ -119,7 +119,8 @@ readiness for one task instead of auditing every integration, see
|
||||
|
||||
## Supported agents
|
||||
|
||||
Setup can configure these clients for REA's local MCP server:
|
||||
Setup can configure these clients for REA's local MCP server. Grok Bot is
|
||||
listed after the table because its connector is not one of these files:
|
||||
|
||||
| Client | `--client` value |
|
||||
| ------------------ | ---------------- |
|
||||
@@ -135,12 +136,37 @@ Setup can configure these clients for REA's local MCP server:
|
||||
| GitHub Copilot CLI | `copilot_cli` |
|
||||
| Command Code | `commandcode` |
|
||||
| VS Code | `vscode` |
|
||||
| Grok Build | `grok_build` |
|
||||
|
||||
For OpenCode, setup writes the V1 `mcp.rea` entry, which OpenCode V1 and V2
|
||||
both load. If the configuration already uses OpenCode V2's native
|
||||
`mcp.servers` table, setup registers REA there instead and replaces any earlier
|
||||
`mcp.rea` entry from REA.
|
||||
|
||||
Grok Build loads `[mcp_servers.rea]` from `$GROK_HOME/config.toml`, or from
|
||||
`~/.grok/config.toml` when `GROK_HOME` is unset. Setup edits that server
|
||||
table, `[mcp_servers.rea.env]`, and a root `disabled_mcp_servers` entry that
|
||||
names `rea`. It sets `startup_timeout_sec = 30` and leaves every other name
|
||||
in that list. The shared skill installed under `~/.agents/skills` is already
|
||||
on Grok Build's skill path.
|
||||
|
||||
Grok Bot (`grok_bot`) is detected from `~/.grokbot`, or from `SAND_DATA_ROOT`
|
||||
when that value is an absolute path. A relative or empty `SAND_DATA_ROOT`
|
||||
stays on `~/.grokbot`. That directory is not the connector store. Grok Bot
|
||||
keeps connectors in the signed-in account and runs them on its hosted
|
||||
computer. It does not import `mcp.json` from the data directory, and it does
|
||||
not attach a stdio server running on this machine. Setup does not call the
|
||||
account connector API, does not write a registration file, and does not report
|
||||
the data directory as aligned. Ask the Grok Bot chat to add a custom MCP
|
||||
server named `rea` that runs on the Bot's computer:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@<version> mcp
|
||||
```
|
||||
|
||||
Do not put credentials in that command or its arguments. `rea doctor --client grok_bot`
|
||||
reports this manual step. `rea uninstall` does not remove the account connector.
|
||||
|
||||
## Review setup changes
|
||||
|
||||
`rea setup` first offers the supported agents in a multi-select. Existing REA
|
||||
@@ -209,7 +235,7 @@ rea setup --yes --all-detected --install-hopper --json
|
||||
|
||||
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. `rea update` installs the exact resolved
|
||||
`startup_timeout_sec = 30` for Codex and Grok Build. `rea update` installs the exact resolved
|
||||
release into the npm prefix that owns the running package, then checks the new
|
||||
executable's version before reporting success. It does not reopen onboarding.
|
||||
Release lookup and installation both use npm's configured registry.
|
||||
@@ -314,8 +340,57 @@ Hopper serializes analysis requests. Cancelling a wait can leave provider work
|
||||
running, which the session reports. Successful decompilation is cached until
|
||||
a relevant rename or comment changes it.
|
||||
|
||||
Closing a session shuts down REA's bridge and removes its temporary socket
|
||||
directory while preserving the Hopper application. A `cleanup_incomplete`
|
||||
Analysis and annotation calls stay bound to the active target's native Hopper
|
||||
document, even when GUI focus changes or other documents have the same display
|
||||
name. Use `open_binary` to change targets. Byte reads stop at a segment boundary
|
||||
and return the readable prefix with `complete: false`. File-offset mapping checks
|
||||
the reverse lookup and original executable bounds; synthetic external-symbol
|
||||
memory has no original file offset. For FAT Mach-O, offsets refer to the original
|
||||
container file. Results retain Hopper's image-relative offset, the observed slice
|
||||
base, and the source executable path.
|
||||
Loader selection reads the Mach-O container header, including FAT files with a
|
||||
single architecture. For FAT64, REA validates the architecture table and selected
|
||||
Mach-O header, prepares a private thin image, and loads it with Hopper's native
|
||||
Mach-O loader. It checks the entire source's SHA-256 while copying the slice;
|
||||
source identity and reported offsets still refer to the original container.
|
||||
An ambiguous architecture subtype requires an explicitly extracted thin image;
|
||||
REA does not guess. Configured loader arguments remain explicit overrides.
|
||||
FAT64 is distinct from the CPU architecture: FAT32 containers can contain 64-bit
|
||||
executable images. This preparation avoids the Raw Binary loader dialog observed
|
||||
with native FAT64 loading on Hopper 6.1.0-demo.
|
||||
The prepared image and its owned document close together, including on MCP exit;
|
||||
if document closure is unconfirmed, REA retains the backing image and reports
|
||||
`cleanup_incomplete` with its path. Ordinary documents retain their existing
|
||||
MCP-exit behavior.
|
||||
Startup deadlines report missing bridge readiness and preserve the launcher
|
||||
outcome; a successful helper exit does not prove that a loader dialog completed.
|
||||
Cursor navigation returns the observed object start when Hopper snaps an interior
|
||||
address; adjacent-object navigation rejects unmapped inputs and document ends.
|
||||
Native API text rejects NUL characters and unpaired Unicode surrogates before
|
||||
annotation changes. Renames preserve unselected label owners; use a batch with
|
||||
all affected addresses to move or swap existing labels explicitly. Every rename
|
||||
destination must be mapped, and native symbol names must fit Hopper's 1024 UTF-16
|
||||
code-unit limit. Oversized names fail before any batch edits; bookmarks and
|
||||
literal string results are not subject to that symbol-name limit. Rename success
|
||||
requires exact final readback. New bookmarks must point into mapped memory;
|
||||
existing legacy bookmarks outside it can still be removed.
|
||||
String results read each native typed object's complete bytes, retain the original
|
||||
provider display in `provider_value`, and report its encoding, byte length, and
|
||||
termination. `encoding_status: inferred` distinguishes REA's decoding from an
|
||||
observed source encoding. Hopper can split long literals into adjacent
|
||||
unterminated objects; search matches each object's decoded bytes independently.
|
||||
Undecodable objects retain native display text with `decoding.available: false`
|
||||
and a reason, so one uncertain object does not block unrelated inspections.
|
||||
Function dossiers retain this same string evidence. Native call edges retain
|
||||
Hopper's partial `CallReference` classification and exact endpoints; detailed
|
||||
reference flags remain unavailable rather than being invented.
|
||||
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
|
||||
five-second matching deadline. Deadline or cancellation stops matching while
|
||||
leaving the Hopper API available. Literal mode retains Unicode casefold matching.
|
||||
|
||||
Closing or switching a target closes its bound Hopper document, shuts down REA's
|
||||
bridge and removes its temporary socket directory while preserving the Hopper
|
||||
application and unrelated documents. A `cleanup_incomplete`
|
||||
result identifies resources whose cleanup could not be verified.
|
||||
|
||||
### Hopper in CI
|
||||
@@ -340,14 +415,17 @@ copying license secrets into logs, or killing unrelated Hopper processes.
|
||||
|
||||
## Ghidra
|
||||
|
||||
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
|
||||
REA connects to an existing Ghidra installation on Linux x64/arm64, macOS x64/arm64,
|
||||
or experimental Windows x64 P0.
|
||||
It accepts Ghidra 12.1.x and the 64-bit full JDK declared by that installation's
|
||||
`application.java.min` and `application.java.max`. Current 12.1 releases require
|
||||
JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4
|
||||
and JDK 21. On macOS, the installation
|
||||
must include the native decompiler for the host architecture; REA does not
|
||||
build it or change Gatekeeper quarantine settings.
|
||||
and JDK 21. Each installation must include the native decompiler for the host
|
||||
architecture in `Ghidra/Features/Decompiler/os/<platform>/` or the corresponding
|
||||
`build/os/<platform>/` directory. Linux ARM64 uses `linux_arm_64`; official
|
||||
release archives may require you to build that native component separately.
|
||||
REA checks the executable prerequisite and does not build or install native
|
||||
tools, or change Gatekeeper quarantine settings.
|
||||
|
||||
The adapter exposes 25 read-only operations: thirteen inventory/name/search
|
||||
operations and twelve function-analysis operations. These cover metadata,
|
||||
@@ -404,14 +482,28 @@ home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
|
||||
Ghidra's default analysis and resource settings, and loads its packaged Java
|
||||
bridge via `-scriptPath`; it never opens an existing user project. Linux and
|
||||
macOS use a current-user-only local bridge socket and descriptor. The
|
||||
experimental Windows transport uses authenticated IPv4 loopback with a
|
||||
project remains under the selected temporary directory. If its Unix socket
|
||||
pathname would exceed the host's byte limit, REA allocates a separate mode-0700
|
||||
socket directory under `/tmp` and removes it on close, cancellation, or failure.
|
||||
Diagnostics retain the actual endpoint and both owned directories.
|
||||
On macOS, REA starts the inspected JVM directly using Ghidra's own LaunchSupport
|
||||
configuration. Apple platform shell wrappers hide their environments from
|
||||
ownership inspection, so retaining those wrappers would prevent verified
|
||||
process-group cancellation during startup.
|
||||
If ownership remains unverifiable, it reports the reason and retains the process
|
||||
supervisor and private runtime instead of removing files beneath a live provider.
|
||||
The experimental Windows transport uses authenticated IPv4 loopback with a
|
||||
private native-owned bearer descriptor and Job Object process ownership.
|
||||
|
||||
Operations begin only after default auto-analysis completes. `open_binary`
|
||||
selects and validates the target/provider binding; it does not wait for Ghidra
|
||||
import and auto-analysis. The first Ghidra-backed query starts that work lazily.
|
||||
The provider startup deadline is 330,000 ms for import, analysis, bridge, and
|
||||
health readiness; a startup failure is returned by the query that triggered it,
|
||||
The provider startup deadline is 330,000 ms by default for import, analysis,
|
||||
bridge, and health readiness. Large binaries can need more: set
|
||||
`REA_GHIDRA_STARTUP_TIMEOUT_MS` to an integer between 1 and 2,147,483,647
|
||||
milliseconds in the server environment. An absent, empty, invalid or out-of-range
|
||||
value keeps the default. REA parses the supplied configuration environment and
|
||||
passes the deadline to each provider client; a startup failure is returned by the query that triggered it,
|
||||
without exposing partial analysis. This deadline is separate from MCP transport
|
||||
initialization and the client's deadline for that individual tool call. A client
|
||||
can time out earlier even when Ghidra would complete within its startup deadline.
|
||||
@@ -436,7 +528,7 @@ continues. These messages redact known bridge authentication tokens and
|
||||
preserve local paths and other analysis context.
|
||||
|
||||
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
|
||||
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64
|
||||
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64/arm64
|
||||
or Mach-O on macOS), plus a native DWARF 4 type-layout object. This lane needs a
|
||||
host C compiler in addition to Ghidra and its JDK.
|
||||
|
||||
@@ -490,7 +582,7 @@ For a client that requires manual configuration, use:
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.0.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@6.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,13 @@ SHA-256 digests, inventory IDs, ASAR container identity, and `.asar.unpacked`
|
||||
status. Direct ASAR inputs and filesystem-backed ASAR files nested beneath a
|
||||
directory are supported.
|
||||
|
||||
JavaScript sources (`.js`, `.jsx`, `.mjs`, and `.cjs`) and TypeScript sources
|
||||
(`.ts`, `.tsx`, `.mts`, and `.cts`) are parsed as inert text; analysis does not
|
||||
execute them.
|
||||
|
||||
JavaScript and HTML source ranges retain an initial UTF-8 BOM as one UTF-16
|
||||
code unit, matching the original bytes identified by the artifact digest.
|
||||
|
||||
If an ASAR declares an unpacked companion entry but the corresponding
|
||||
`<archive>.unpacked` file is absent from the operator-supplied artifact set, REA
|
||||
keeps the ASAR occurrence with `hash_status: unavailable`, records an explicit
|
||||
@@ -133,6 +140,9 @@ missing bytes. This allows static JavaScript/Electron reconstruction to proceed
|
||||
for the embedded files while preserving the missing native/resource bytes as an
|
||||
unknown instead of silently treating them as absent or verified.
|
||||
|
||||
Plain `.ts` artifact sources use TypeScript syntax without JSX, including
|
||||
angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing.
|
||||
|
||||
Selected bounded text is then parsed as inert data to recover:
|
||||
|
||||
- `package.json` metadata and declared main or renderer entrypoints;
|
||||
@@ -156,6 +166,12 @@ Selected bounded text is then parsed as inert data to recover:
|
||||
- utility-process entrypoints and native `.node` binding requests without
|
||||
parsing or executing the add-on.
|
||||
|
||||
Overloaded `.open` calls use lexical receiver facts: ambient browser window and
|
||||
document receivers are treated as browsing-context or document operations,
|
||||
while locally shadowed receivers can still contribute network endpoint
|
||||
candidates. A template recovered with a parser error and no cooked value stays
|
||||
dynamic; its raw spelling is not treated as a valid JavaScript string.
|
||||
|
||||
Each recovered bundle module retains the exact factory-source digest. A complete
|
||||
bounded AST also receives a `babel-ast-v1` structural fingerprint that ignores
|
||||
ordinary identifier names while retaining syntax, literals, operators, object
|
||||
@@ -217,6 +233,22 @@ These safeguards are not caller-selectable output budgets. Source-map parsing
|
||||
reports truncation when its format or parser safety boundary is reached. The
|
||||
application graph has no aggregate node, edge, root, or observation prefix cap.
|
||||
|
||||
Semantic evaluation retains at most 256 distinct primitive candidates per
|
||||
expression and at most 1 MiB of worst-case JSON string bytes across a normalized
|
||||
primitive value's string candidates. The byte budget estimates six JSON bytes
|
||||
per UTF-16 code unit, the expansion bound for escaped strings, and bounds both
|
||||
retained candidate strings and temporary canonical-key serialization. This is
|
||||
an in-memory semantic allocation bound independent of CLI or MCP transport
|
||||
budgets. Source literal bytes remain in the parsed source and artifact evidence;
|
||||
when a normalized semantic value exceeds the bound, its value is unknown and
|
||||
coverage is partial. Expression
|
||||
evaluation and provenance walks stop after 256 nested levels. Union products
|
||||
and string lengths are checked before allocating combinations or concatenated
|
||||
strings. When a boundary is reached, the value stays unknown, semantic coverage
|
||||
becomes partial, and graph evidence retains the expression location and typed
|
||||
limiting reason. The number of unresolved alternatives is unknown; it is not
|
||||
reported as an exact omission count.
|
||||
|
||||
Byte, entry, path, and graph-shape bounds are hard limits. The parse deadline is
|
||||
checked before and between bounded parsing and traversal phases; the synchronous
|
||||
Babel and JSON parser calls cannot be preempted mid-call, so their input byte
|
||||
@@ -265,3 +297,5 @@ document base and query/fragment rules. CommonJS module lookups retain extension
|
||||
and directory resolution.
|
||||
Unresolved HTML references retain their declaration, source range, and resolution
|
||||
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.
|
||||
|
||||
@@ -119,12 +119,18 @@ targets belong to the same Electron application.
|
||||
|
||||
## Capture and determinism
|
||||
|
||||
Every observation records its time window and returns all valid script and
|
||||
execution-context events received during it. Per-location protocol validation
|
||||
remains in force. Scripts are validated after capture, deduplicated by stable
|
||||
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
|
||||
are excluded from the durable result, so identical inputs and captured metadata
|
||||
produce the same Evidence ID.
|
||||
Capture retains valid script and execution-context metadata within an 8 MiB
|
||||
aggregate budget, including conservative estimates for UTF-16 strings, record
|
||||
and collection overhead, and projected URL fields. Each CDP message also has a
|
||||
16 MiB transport limit. A message over that limit fails with `payload_limit`;
|
||||
when the aggregate budget is reached, later metadata events are dropped and
|
||||
the partial result reports `events_dropped`, `truncated: true`, and
|
||||
`retained_metadata_budget_exceeded`. Repeated context transitions update the
|
||||
current context record instead of accumulating duplicate objects. Scripts are
|
||||
validated after capture, deduplicated by stable metadata, and canonically
|
||||
sorted. Wall-clock timestamps and protocol script IDs are excluded from the
|
||||
durable result, so identical inputs and captured metadata produce the same
|
||||
Evidence ID.
|
||||
|
||||
Inspector attach is inherently incomplete. Enabling `Debugger` reports known
|
||||
and uncollected scripts, but scripts collected before attachment may be
|
||||
|
||||
@@ -137,6 +137,12 @@ function implements a specific managed method.
|
||||
|
||||
## Evidence record shape
|
||||
|
||||
Member comparison, application graph projection, and managed/native verification
|
||||
authenticate supplied inspection Evidence and check that any subject SHA-256
|
||||
matches the normalized artifact SHA-256. A valid Evidence ID alone does not
|
||||
establish that those two identities agree. Evidence without a subject remains
|
||||
usable, with no subject digest available to cross-check.
|
||||
|
||||
Every planned operation returns a provider result and Evidence with four
|
||||
commitment groups:
|
||||
|
||||
|
||||
+27
-7
@@ -3,8 +3,8 @@
|
||||
## Generated catalog
|
||||
|
||||
Run `npm run build:cached` in a source checkout to generate the machine-readable
|
||||
catalog at `docs/public/product-catalog.json`. The [catalog](product-catalog.json)
|
||||
is also included in documentation builds. PR CI retains it with the packaged
|
||||
catalog at `docs/public/product-catalog.json`. Documentation deployments serve
|
||||
the same file at [/rea/product-catalog.json](/rea/product-catalog.json). PR CI retains it with the packaged
|
||||
skill and portable conformance projections in the `generated-docs` artifact.
|
||||
These outputs describe the exact source revision being built; they are not
|
||||
checked-in snapshots. For a running server, `binary_session` remains the
|
||||
@@ -31,6 +31,10 @@ check the complete catalog after SDK conversion and its generated counterpart.
|
||||
This is REA's local compatibility profile; individual model APIs can impose
|
||||
additional limits.
|
||||
|
||||
Output schemas share repeated definitions through local references while
|
||||
preserving their complete fields and validation rules. Canonical Zod validation
|
||||
and the input compatibility profile continue to apply.
|
||||
|
||||
For passive `compare_web_captures` inputs, pass each complete
|
||||
`inspect_web_page` result in `before.inspection` or `after.inspection`, with an
|
||||
optional complete `discover_webmcp_tools` result in the matching `webmcp` field.
|
||||
@@ -78,9 +82,25 @@ Provider calls receive the request cancellation signal. Artifact traversal,
|
||||
hashing, version comparisons, Hopper requests, and process capture
|
||||
check the same signal. Cancellation is distinct from timeout. A cleanup failure
|
||||
uses `cleanup_incomplete` and lists only the owned resource kinds that remain.
|
||||
Native call tracing and process capture retain available observations in
|
||||
`details.partial_observation` on failure, including when cleanup succeeds.
|
||||
The observation reports its partial coverage; cleanup details describe host
|
||||
state separately from the execution failure.
|
||||
Derived comparisons and reconstruction verification yield before computation
|
||||
and before publication, so cancellation cannot race with successful Evidence.
|
||||
|
||||
`analyze_javascript_application` also yields between reconstruction phases and
|
||||
during graph/result sealing, cross-graph binding checks, Evidence JSON validation,
|
||||
and canonical hashing. Its final result validation reuses exact graphs whose
|
||||
owned constructors validated and completely sealed them; imported graphs still
|
||||
receive full schema and commitment checks.
|
||||
Cancellation observed before completion returns `cancelled` and prevents the
|
||||
provisional result from entering the session ledger; prior Evidence stays usable.
|
||||
Single-file parsing, graph construction, and validation of imported graphs
|
||||
still run synchronously, so control messages can wait for those
|
||||
phases to release the event loop. A rejected client promise alone does not
|
||||
establish that the server has stopped its work.
|
||||
|
||||
CLI calls work without a progress token and translate SIGINT into the same
|
||||
AbortSignal used by providers. Existing controlled-process cleanup and provider
|
||||
shutdown rules still apply; REA never kills a process it cannot prove it owns.
|
||||
@@ -95,11 +115,11 @@ auto-analysis, bridge connection, and health readiness before returning analysis
|
||||
|
||||
These deadlines have different owners:
|
||||
|
||||
| Deadline | Owner and effect |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
|
||||
| Ghidra startup | REA allows 330,000 ms for engine readiness from the first provider query; startup failure is reported by that query. |
|
||||
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
|
||||
| Deadline | Owner and effect |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
|
||||
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts 1–2,147,483,647 ms. Invalid values keep the default. Startup failure is reported by the first provider query. |
|
||||
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
|
||||
|
||||
For an already connected client using the pinned SDK, request options are the
|
||||
**second** argument of `callTool`:
|
||||
|
||||
@@ -12,6 +12,27 @@ input file and include the observed lipo slice offset for universal binaries.
|
||||
If that offset is unavailable, segment evidence locations are omitted with an
|
||||
explicit limitation; architecture inventory locations remain available.
|
||||
|
||||
Hopper's `address_to_file_offset` combines the native image-relative mapping with
|
||||
original-file coordinates only after checking the source against the session's
|
||||
SHA-256. FAT tables and embedded Mach-O headers come from that same hashing pass.
|
||||
A changed or nonregular source reports `artifact_changed`; an unreadable or
|
||||
missing source reports its access or filesystem failure. Those errors retain the native offset,
|
||||
loaded header and provider source path in `details.partial_observation`, with the
|
||||
original-file coordinate explicitly unavailable. Restore the original bytes or
|
||||
reopen the changed executable to obtain verified coordinates. Snapshot replay
|
||||
remains historical evidence for its recorded artifact identity.
|
||||
|
||||
A selected Hopper database's digest identifies the database rather than its
|
||||
original executable. Original-file mapping is unavailable for that selection;
|
||||
native mapping facts remain in partial evidence, and `read_bytes` still reads
|
||||
the loaded database. Open the original executable to verify source coordinates.
|
||||
|
||||
Native Xcode command execution uses shared process supervision with a 60-second
|
||||
deadline and a 64 MiB aggregate stdout/stderr budget. Timeout, cancellation,
|
||||
stream failure, and output exhaustion retain the captured output, exit details,
|
||||
and cleanup outcome in the error result. Truncated output is never parsed as a
|
||||
complete observation.
|
||||
|
||||
- `trace_dylib_resolution` / `rea trace-dylib-resolution <app-or-mach-o>`
|
||||
parses Mach-O load commands in TypeScript and follows dyld's path expansion
|
||||
for every executable in an app bundle, or for one Mach-O within its directory.
|
||||
@@ -37,6 +58,28 @@ explicit limitation; architecture inventory locations remain available.
|
||||
connections and controller/class names. `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.
|
||||
App bundle `Info.plist` executable resolution uses the same decoder as keyed
|
||||
archives and Interface Builder. The decoder consumes an initial
|
||||
BOM and rejects malformed byte sequences, unsupported encoding declarations,
|
||||
and declarations that disagree with the detected encoding. It never substitutes
|
||||
UTF-8 for a declared encoding it cannot process. Archive and evidence digests
|
||||
still identify the original serialized bytes. Bundle resolution reports
|
||||
malformed bytes and unsupported encodings against the original plist path
|
||||
before looking up the executable. Interface Builder decoding
|
||||
limits aggregate input archives to 32 MiB.
|
||||
Before decoding, it also bounds the serialized structure: expanded binary
|
||||
plist references, NIB table records, XML elements and text, plus their
|
||||
projected representations must fit the 256 MiB aggregate decode budget.
|
||||
NIB string values, data, property names and class names are charged at each
|
||||
projected JSON occurrence, including when the archive reuses one source
|
||||
value. Binary plist data projection also accounts for the temporary byte
|
||||
copy made by the pinned decoder projection. These are format-derived
|
||||
representation reservations, not exact heap measurements or a process-wide
|
||||
heap ceiling.
|
||||
Archives beyond either aggregate budget are omitted with their paths and an
|
||||
explicitly partial `archive_decode` facet; previously decoded documents and
|
||||
their identities remain available.
|
||||
- `inspect_native_dispatch_metadata` /
|
||||
`rea inspect-native-dispatch-metadata <app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
|
||||
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
|
||||
@@ -62,10 +105,70 @@ explicit limitation; architecture inventory locations remain available.
|
||||
partial coverage. Other providers retain the
|
||||
existing symbol-based inventory with its narrower coverage.
|
||||
|
||||
## Exact CLI selectors
|
||||
|
||||
`function`, `instructions` and `decompile` accept `--procedure=<name-or-address>`;
|
||||
`xrefs` accepts `--address=<name-or-address>`. `search` accepts `--pattern=<text>`
|
||||
and `trace` accepts `--query=<text>`. These alternatives preserve names and text
|
||||
that begin with a dash, including Objective-C names such as
|
||||
`rea function ./app '--procedure=-[REAWidget delegate]' --provider hopper --format json`.
|
||||
Use the equals form when the value resembles a CLI flag, for example
|
||||
`rea trace ./app --query=--help --provider hopper --format json`.
|
||||
The existing positional forms remain available. Missing selectors and conflicting
|
||||
positional/named values fail before analysis starts; identical selections agree.
|
||||
|
||||
## Native instruction, call and type primitives
|
||||
|
||||
Ghidra supplies three exact-object operations:
|
||||
|
||||
Ghidra accepts hexadecimal addresses with or without a `0x` prefix, in either
|
||||
letter case, and explicit address-space coordinates such as `EXTERNAL:0x1`.
|
||||
Results use canonical lowercase hexadecimal offsets. For a procedure identifier,
|
||||
an explicit `0x` or address-space prefix selects an address; otherwise an exact
|
||||
database symbol name takes precedence over a bare hexadecimal address. Thus a
|
||||
function renamed to `dead` remains selectable by name. Use the names returned
|
||||
by the inventory, including their namespaces and any platform symbol prefix.
|
||||
Overloads can share a fully qualified name; ambiguity errors return every
|
||||
matching entry address so the caller can select the intended function directly.
|
||||
|
||||
`resolve_containing_procedure` also resolves an exact external entry. Its empty
|
||||
body remains explicit; nearby external addresses do not inherit that identity.
|
||||
|
||||
Ghidra function references cover the complete function-body AddressSet and its
|
||||
exact entry, including references into instruction interiors and references
|
||||
from embedded data. Addresses inside an enclosing span but outside the owned
|
||||
body are excluded. `procedure_references` returns internal and external edges;
|
||||
`analyze_function` omits incoming edges whose sources belong to the same function.
|
||||
The reference collector's semantics participate in the analysis profile, so
|
||||
older snapshots cannot replay the former incomplete results under this profile.
|
||||
|
||||
Ghidra regex searches use Java Pattern semantics. If compiling or matching a
|
||||
pattern exhausts the engine's stack, REA returns a resource constraint and
|
||||
preserves the active database and annotations. Retry in literal mode or simplify
|
||||
the regex; successful literal searches retain complete matching strings.
|
||||
Cancelling an active request terminates the ephemeral database and discards its
|
||||
annotations. REA retains the selected target and imports the original artifact
|
||||
again on the next Ghidra query.
|
||||
|
||||
Opening the same target and profile again retains the live database, including
|
||||
annotations. It does not make those edits eligible for an immutable snapshot.
|
||||
Snapshot closes drain earlier provider requests before saving and keep later
|
||||
requests waiting until the save and close finish. A concurrent successful edit
|
||||
therefore rejects the snapshot save and leaves the edited session open.
|
||||
After metadata edits, snapshot saves and imports remain unavailable in that
|
||||
session. Use `export_evidence_bundle` to retain the observations, close without
|
||||
`snapshot_path`, and reopen the target before importing or saving a snapshot.
|
||||
|
||||
Ghidra checks its private import copy against the artifact digest selected by
|
||||
`open_binary`. If the source changes before that copy is acquired, the query
|
||||
reports `artifact_changed` with the selected path and both digests. Reopen the
|
||||
stable target to acquire its current identity. An already imported database
|
||||
continues to describe its captured bytes even if the original path is changed
|
||||
or removed; this identity failure does not mark the Ghidra installation unavailable.
|
||||
On Linux and macOS, an unreadable source reports `access_denied`; a removed or
|
||||
nonregular replacement reports `artifact_changed`. Source copying is cancellable
|
||||
and never replaces an existing private snapshot.
|
||||
|
||||
```bash
|
||||
rea inspect-native-instruction <binary> <address> --provider ghidra
|
||||
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
|
||||
@@ -188,16 +291,42 @@ arguments. The receiver class comes from LLDB's Objective-C runtime reader
|
||||
without running target code; no expression is evaluated, and REA never
|
||||
attaches to a process it did not launch.
|
||||
|
||||
The run ends when the process exits, `max_events` entries are recorded, or
|
||||
`duration_ms` elapses. In the last two cases the process is killed, and REA
|
||||
confirms that it is gone. The result reports:
|
||||
The run ends when the process exits, `max_events` entries are recorded,
|
||||
`duration_ms` elapses, or the aggregate trace resource budget is exhausted.
|
||||
Bounded termination reports partial coverage. REA checks process termination
|
||||
using a launch-time OS start identity; if ownership or cleanup cannot be
|
||||
verified, the result or error preserves that uncertainty. The result reports:
|
||||
|
||||
- the outcome, exit status and captured stdout/stderr (1 MiB kept per stream);
|
||||
- the outcome, exit status and captured stdout/stderr (1 MiB kept per stream
|
||||
while excess bytes are drained and counted without growing capture files).
|
||||
Each stream reports `complete`; when draining could not finish, its byte
|
||||
count is an observed lower bound and truncation is explicit;
|
||||
- caller-selected arguments, working directory, and environment overrides;
|
||||
- `selected_file_sha256` and LLDB module identity observations. These checks do
|
||||
not pin the executable against concurrent mutation; `loaded_image_sha256`
|
||||
remains unknown;
|
||||
- signal or exception stops;
|
||||
- every breakpoint's resolved locations; a breakpoint that matched no loaded
|
||||
code makes coverage partial;
|
||||
- limitations.
|
||||
|
||||
The bridge limits retained event JSON to 8 MiB and caller frames to 65,536
|
||||
across the run. It checks admission before retaining another complete event;
|
||||
`resource-limit` reports which observations could not be completed without
|
||||
silently changing the requested event or frame settings.
|
||||
Resolved breakpoint locations have a separate aggregate 8 MiB metadata budget.
|
||||
If their details exceed it, `location_count` retains the full observed match
|
||||
count, coverage is partial, and `breakpoint_locations_truncated` is true. This
|
||||
post-run metadata limit does not imply that the target was killed.
|
||||
|
||||
Accepted entries and signal stops are also flushed to a bounded 8 MiB journal.
|
||||
If cancellation, timeout, capture failure, or a later bridge error prevents a
|
||||
full result, the error retains this evidence in `details.partial_observation`
|
||||
with partial coverage. Available output prefixes remain inline; their byte
|
||||
counts are observed lower bounds when final drain counters are unavailable.
|
||||
The original failure classification and cleanup uncertainty remain separate.
|
||||
An interrupted journal row is ignored with an explicit limitation.
|
||||
|
||||
Only entries are observed: return values, floating-point and stack arguments,
|
||||
and inlined or `objc_direct` calls are not. The target runs with the current
|
||||
user's permissions and may change files, show UI or use the network.
|
||||
@@ -210,15 +339,24 @@ the tool fails with a `debugger-attach-denied` reason.
|
||||
## Provider and verification boundaries
|
||||
|
||||
Install a Ghidra 12.1.x release and the 64-bit full JDK it declares, then configure REA to
|
||||
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
|
||||
use them. Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64; each installation requires
|
||||
the matching native decompiler. Experimental Windows x64 P0 admits native
|
||||
x86-64 PE applications on local NTFS using bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
On Linux and macOS, `annotate_native_function` atomically edits a function name
|
||||
and entry comments in the ephemeral database, returning refreshed analysis
|
||||
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
|
||||
without changing executable bytes. Annotation text must contain no NUL or
|
||||
unpaired Unicode surrogate; a rejection identifies the field and UTF-16 index
|
||||
and leaves every annotation unchanged. CRLF, supplementary Unicode characters,
|
||||
and combining characters are preserved. Windows P0 remains read-only. Ghidra has no
|
||||
GUI authority, and REA never falls back automatically to Hopper.
|
||||
Ghidra name edits preserve the existing namespace. Supply either a leaf name
|
||||
such as `renamed` or a fully qualified name in that namespace, such as
|
||||
`alpha::renamed`. The returned qualified name can be reused as an idempotent
|
||||
rename input. Edits retain the existing namespace; other namespace-like text
|
||||
remains literal leaf-name text. A qualified name with an empty leaf is rejected
|
||||
before any comment or name is changed.
|
||||
|
||||
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
@@ -238,3 +376,32 @@ GUI authority, and REA never falls back automatically to Hopper.
|
||||
macOS ARM64 is the real host verified during this implementation. Admission of
|
||||
macOS Intel does not claim an Intel verification run. Unsupported metadata and
|
||||
unresolved runtime/value semantics remain visible in results.
|
||||
|
||||
## Interface Builder hierarchy coverage
|
||||
|
||||
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
|
||||
digests identify the original serialized bytes.
|
||||
|
||||
## Keyed archive integer precision
|
||||
|
||||
The archive reader observes XML number element types and binary number markers.
|
||||
Integers beyond the exact range of a JSON number are reported with the original
|
||||
decimal text as `{ "$plist_type": "integer", "decimal": "<exact digits>" }` when
|
||||
that source value can be associated unambiguously. Integral real values remain
|
||||
numbers. Colliding integer/real values, unclassified numbers, and malformed UID
|
||||
markers remain decoded with an explicit precision limitation. The limitation
|
||||
also counts observed unsafe integer literals, including observations outside the
|
||||
selected object page. Incomplete supplemental metadata produces an explicit
|
||||
precision note without rejecting an archive accepted by the byte decoder.
|
||||
Mixed XML number content (comments, CDATA, processing instructions or child
|
||||
elements) and integer text the supplemental reader cannot classify disable exact
|
||||
association and retain decoded numbers with that precision note. Nonnumeric
|
||||
`UID` and `CF$UID` dictionary values remain ordinary data and are projected
|
||||
recursively; numeric reference markers retain their original representation.
|
||||
|
||||
Value projection preserves serialized node kinds, IDs, references and pagination.
|
||||
Exact and ambiguous value counts describe emitted root/object value instances;
|
||||
a root and an object may contain the same value. Archive digests identify the
|
||||
original bytes.
|
||||
|
||||
+15
-2
@@ -108,6 +108,14 @@ and process residual unknowns. Successful cleanup verifies the owned group;
|
||||
it does not attribute those unrelated processes. Related or otherwise unexplained
|
||||
unreadable processes still prevent successful cleanup.
|
||||
|
||||
On macOS, changing a Node process's `process.title` can make its run token
|
||||
unreadable. Node documents that [setting the title overwrites argv memory](https://nodejs.org/download/release/v24.18.0/docs/api/process.html#processtitle).
|
||||
With the pinned toolchain, a live child retained a readable start identity
|
||||
while its token became unavailable after this change; npm changes its title
|
||||
as well. A newly started unreadable process can prevent verified cleanup even
|
||||
after the selected command exits. REA preserves this uncertainty and leaves
|
||||
that process untouched.
|
||||
|
||||
## Compare two captures
|
||||
|
||||
Compare saved capture Evidence with:
|
||||
@@ -130,7 +138,12 @@ causal evidence. Trace comparison returns `unknown` when a capture lacks the
|
||||
required complete event journal or contains relevant residual unknowns.
|
||||
|
||||
Each capture carries commitments for the selected scenario, executable,
|
||||
comparison contract, and normalization rules. Comparison rejects captures
|
||||
whose comparison contracts differ. Captures are local Evidence files; keep
|
||||
comparison contract, and normalization rules. The comparison contract covers
|
||||
the working directory, explicit environment, filesystem observation paths,
|
||||
terminal size, scripted events, timeouts, limits, and normalization; the
|
||||
executable and its arguments may differ. Comparison rejects captures whose
|
||||
comparison contracts differ and names the differing fields, so run both
|
||||
scenarios from the same absolute working directory and observation paths.
|
||||
Captures are local Evidence files; keep
|
||||
their source artifacts and invocation context available when interpreting a
|
||||
difference.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
This guide describes repository main. Check the [released package boundary](installation.md#released-package-and-main) when using npm.
|
||||
|
||||
Ghidra read-only analysis is available on Linux x64 and macOS x64/arm64.
|
||||
Ghidra read-only analysis is available on Linux x64/arm64 and macOS x64/arm64.
|
||||
Install a Ghidra 12.1.x release and the 64-bit full JDK that release declares,
|
||||
then configure REA to use them. Current 12.1 releases require JDK 21 or newer
|
||||
and set no maximum. The bridge is verified with Ghidra 12.1.4 and JDK 21.
|
||||
macOS installations need the matching native decompiler.
|
||||
Each installation needs the matching native decompiler.
|
||||
|
||||
REA imports one target into a temporary project and exposes 25 read-only
|
||||
operations after analysis completes. They cover inventories, search,
|
||||
@@ -121,7 +121,7 @@ capability from a successful import.
|
||||
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
|
||||
|
||||
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
|
||||
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
|
||||
host-native targets (x86-64/AArch64 ELF on Linux or Mach-O on macOS), plus a native
|
||||
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
|
||||
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
|
||||
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,
|
||||
|
||||
+46
-21
@@ -1,15 +1,32 @@
|
||||
# Releasing from a checkpoint
|
||||
# Releasing REA
|
||||
|
||||
REA releases use an explicit source checkpoint. Main can continue accepting
|
||||
changes while a release is tested and published. The Release workflow runs
|
||||
only when a maintainer dispatches it; main pushes do not refresh release PRs.
|
||||
Pushes to main automatically open or refresh a Release Please PR with the next
|
||||
version and changelog. Review its migration notes and wait for its current CI
|
||||
checks to pass. Merging that PR into main starts publication automatically.
|
||||
|
||||
## 1. Select the source
|
||||
The Release workflow validates the merged release metadata and ancestry before
|
||||
creating a tag. npm and MCP Registry publication use the exact reviewed merge
|
||||
SHA returned by Release Please. Later main commits cannot change that source.
|
||||
Ordinary main pushes and closed, unmerged PRs cannot publish packages. Check
|
||||
both publication jobs and the published-package canary; a merged PR or GitHub
|
||||
tag alone does not establish that publication finished. Retry failed jobs in
|
||||
the original run after a partial publication.
|
||||
|
||||
Choose the next version from the unreleased Conventional Commits, including
|
||||
For a release that needs an independently frozen application checkpoint, use
|
||||
the optional manual path below. It retains explicit prepare and publish phases.
|
||||
|
||||
## 1. Select a source for a manual checkpoint
|
||||
|
||||
Use the next minor version proposed by Release Please, including releases with
|
||||
breaking changes. Record the full source SHA and create `release/VERSION` at
|
||||
that commit. The version is a maintainer choice; Release Please's proposed
|
||||
version must agree before publication.
|
||||
that commit. The selected version and Release Please's proposed version must
|
||||
agree before publication.
|
||||
|
||||
When choosing the manual path, leave the automatic main release PR unmerged.
|
||||
Select the application commit from main, create the frozen branch, and prepare
|
||||
its own release PR using the steps below. Merging the main release PR starts
|
||||
automatic publication instead. Close the superseded main PR after the manual
|
||||
release has published and its metadata has been synchronized back to main.
|
||||
|
||||
`release/VERSION` also sets the expected version for the workflow. Use an exact
|
||||
SemVer such as `release/6.1.0` or `release/6.1.0-rc.1`, without build metadata.
|
||||
@@ -18,19 +35,21 @@ registry metadata or changelog disagrees with that version before creating a tag
|
||||
|
||||
### Compatibility and version selection
|
||||
|
||||
REA uses the default SemVer strategy: compatible fixes increment patch;
|
||||
compatible capabilities increment minor; incompatible public behavior increments
|
||||
major. Adding tools, internal refactoring and correcting behavior outside the
|
||||
supported contract do not by themselves require a major release.
|
||||
REA uses Release Please's `always-bump-minor` strategy: every release increments
|
||||
minor and resets patch, including releases with breaking changes. For example,
|
||||
the next release after 6.0.0 is 6.1.0. Version numbers use the SemVer format,
|
||||
but a minor increment does not promise backward compatibility. Review the
|
||||
changelog's breaking-change section and migration notes before upgrading.
|
||||
|
||||
The supported public surface includes documented CLI commands/options, MCP tool
|
||||
names and input/result contracts, saved evidence formats and supported runtime
|
||||
requirements. Before adding `!` or a `BREAKING CHANGE` footer, a PR must identify
|
||||
a previously valid call or configuration that will fail or change meaning,
|
||||
explain why compatibility cannot be preserved, and give its migration. Prefer
|
||||
optional additions, compatibility adapters and a documented deprecation period;
|
||||
group deliberate removals into a planned major release. Maintainers review this
|
||||
impact before merging; the bot parses markers and cannot infer compatibility.
|
||||
optional additions, compatibility adapters and a documented deprecation period.
|
||||
Keep breaking markers so the bot includes migration notes without incrementing
|
||||
major. Maintainers review this impact before merging; the bot parses markers
|
||||
and cannot infer compatibility.
|
||||
|
||||
For example, adding an optional inspection tool is a minor change. Rejecting a
|
||||
relative MCP path previously accepted by the public contract is breaking;
|
||||
@@ -80,9 +99,11 @@ and the PR titles in GitHub's default merge messages. The report preserves the
|
||||
original merge subject alongside the extracted Conventional Commit title.
|
||||
This matters after merging a side-branch release back into a newer main:
|
||||
Release Please's chronological history cutoff can omit unreleased mainline
|
||||
commits. Unreleased breaking markers require a major increment and references
|
||||
in the new release's breaking-change migration section. Other omitted entries
|
||||
are reported for review. Historical notes from an older release cannot satisfy
|
||||
commits. Under `always-bump-minor`, unreleased breaking markers allow a minor
|
||||
increment and still require references in the new release's breaking-change
|
||||
migration section. Checkpoints using the default strategy retain the major
|
||||
increment requirement. Other omitted entries are reported for review.
|
||||
Historical notes from an older release cannot satisfy
|
||||
the new release's migration check. This audits declared markers and references;
|
||||
it does not prove API compatibility or the quality of a migration explanation.
|
||||
|
||||
@@ -115,6 +136,8 @@ gh workflow run release.yml --ref release/5.0.0 \
|
||||
Publication creates the release from the merged bot PR without preparing or
|
||||
updating another PR. Both npm and MCP Registry jobs check out Release Please's
|
||||
exact release SHA. They do not build the current main tip or a mutable branch.
|
||||
Stable versions publish to npm's `latest` tag; prerelease versions publish to
|
||||
the `next` tag so they cannot replace the stable install by default.
|
||||
The publish dispatch runs from the frozen release branch so npm's provenance
|
||||
records the actual release commit. Before Release Please creates a tag, the
|
||||
workflow accepts only `prepare` or `publish` and requires the selected branch
|
||||
@@ -139,7 +162,7 @@ A GitHub tag alone does not establish npm or MCP Registry publication.
|
||||
|
||||
## 4. Sync metadata back to main
|
||||
|
||||
Post-release synchronization is part of completing the release. Finish it
|
||||
Manual checkpoint synchronization is part of completing that release. Finish it
|
||||
before cutting the next checkpoint; otherwise main retains the previous
|
||||
release baseline and can propose an already-published version again.
|
||||
|
||||
@@ -161,8 +184,10 @@ git fetch origin main --tags
|
||||
git merge-base --is-ancestor rea-agents-5.0.0 origin/main
|
||||
```
|
||||
|
||||
Close any superseded rolling release PR. Future releases repeat the checkpoint
|
||||
procedure from main; never resume automatic release-PR refreshes on main pushes.
|
||||
Close the superseded main proposal before merging the synchronization PR.
|
||||
That merge's main push opens a proposal for the following release using the
|
||||
updated baseline. Future releases can use the automatic main PR or repeat the
|
||||
manual checkpoint procedure; automatic proposals do not move frozen branches.
|
||||
Preparation generates and validates the catalog, portable conformance
|
||||
projections, and packaged skill from the candidate checkout without committing
|
||||
them. The checkpoint validator binds tracked version authority to the source
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ installation changes and requires approval before writing or installing.
|
||||
|
||||
## Planned work
|
||||
|
||||
The [website guides](https://morluto.github.io/rea/guides/) and
|
||||
The [website guides](https://rea.tools/guides/) and
|
||||
[generated catalog](product-catalog.json) describe available workflows.
|
||||
Current development priorities are:
|
||||
|
||||
@@ -67,12 +67,12 @@ REA setup lets you select agent integration and optional Hopper installation.
|
||||
It installs the bundled workflow, configures detected agents, and can save
|
||||
verified paths for an existing Ghidra installation. It configures Claude Code,
|
||||
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
|
||||
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
|
||||
GitHub Copilot CLI, Command Code, VS Code, and Grok Build using each client's configuration format. Grok Bot is detected locally, but its connector is added from the Grok Bot chat because the signed-in account store is not a data-directory file.
|
||||
|
||||
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.x and the
|
||||
Ghidra analysis supports Linux x64/arm64 and macOS x64/arm64 with Ghidra 12.1.x and the
|
||||
64-bit full JDK that installation declares. Current 12.1 releases require JDK 21
|
||||
or newer and set no maximum; the bridge is verified with Ghidra 12.1.4 and JDK 21.
|
||||
macOS also requires the matching native decompiler. The
|
||||
Each installation requires the matching native decompiler. The
|
||||
adapter exposes thirteen inventory operations and twelve function-analysis
|
||||
operations, for 25 read-only operations total. Linux and macOS additionally support
|
||||
atomic function-name and entry-comment edits with refreshed analysis; metadata
|
||||
|
||||
+209
-15
@@ -7,8 +7,10 @@ semantic cases that these workflows cannot reliably reproduce. A test's path
|
||||
or suite name does not establish its behavioral depth.
|
||||
|
||||
Avoid tests that mirror getters, enum mappings, fixture helpers, or prescribed
|
||||
call sequences. Prefer one representative workflow over a Cartesian matrix
|
||||
when every row crosses the same boundary. Keep distinct command handlers,
|
||||
call sequences, or assert cache object identity without a caller-visible contract.
|
||||
Prefer representative workflows over a Cartesian matrix when rows cross the
|
||||
same boundary. Retain combinations that exercise a distinct interaction, such as
|
||||
filtering within a full-output envelope. Keep distinct command handlers,
|
||||
producer representations, failure reasons, and lifecycle states covered.
|
||||
Consolidate their shared setup without hiding the inputs or expected evidence.
|
||||
|
||||
@@ -21,6 +23,34 @@ tests consume a production module, trace CLI/MCP and verifier imports,
|
||||
including imports of compiled files, and remove abandoned scaffolding with its
|
||||
tests when it has no runtime consumer.
|
||||
|
||||
When a test expects a `Result` to succeed, throw its error before asserting the
|
||||
returned value. An `.ok` assertion immediately before the same throwing guard
|
||||
adds no coverage and hides the typed failure. A removed parser matrix can leave
|
||||
one golden malformed-input case and a real filesystem lifecycle that proves
|
||||
configure, update, readback, and removal; an add-only journey does not cover the
|
||||
whole lifecycle.
|
||||
|
||||
Protocol fixtures must model the commands they support and their producer reply
|
||||
shapes. Reject unmodeled commands instead of returning a generic success; an
|
||||
empty result is appropriate only for a supported command with no result fields.
|
||||
|
||||
Keep transport-only in-memory MCP sessions in the MCP boundary lane and direct
|
||||
filesystem workflows in the filesystem boundary lane. Real process-tree capture
|
||||
belongs in the serial process boundary lane, including captures invoked through
|
||||
MCP. Report unavailable capture authority as a named skip rather than returning
|
||||
from the test without assertions. Reserve acceptance paths for journeys
|
||||
through the compiled public CLI or MCP runtime. Calling a compiled private
|
||||
workflow or injecting a provider still establishes its narrower integration
|
||||
claim, even if a directory name calls it acceptance.
|
||||
|
||||
Emit a verifier's final success report only after awaited cleanup. Observe final
|
||||
process lineage after closing its owned resources.
|
||||
|
||||
The Vitest runner releases cached native process inspectors during awaited
|
||||
worker teardown. Fork termination does not run Node's normal exit hooks; a
|
||||
per-file teardown would instead retire shared inspectors before later files
|
||||
can use them.
|
||||
|
||||
Measure slow files before removing capacity regressions. Optimize repeated
|
||||
process startup, fixture construction, and production algorithms when they
|
||||
dominate runtime. Keep at least one input beyond each formerly failing size or
|
||||
@@ -113,6 +143,12 @@ covers `trace-dylib-resolution`:
|
||||
- for the main executable and an XPC service, a predicted load order equal to
|
||||
the images dyld actually loads under `DYLD_PRINT_LIBRARIES`.
|
||||
|
||||
The lane also compiles executable/library pairs with empty embedded directory,
|
||||
versioned-path, and suffix settings. It checks present and removed dependencies
|
||||
through CLI and MCP, and compares actual `DYLD_PRINT_SEARCHING` diagnostics for
|
||||
root-level candidates. These cases distinguish an empty search directory from
|
||||
an empty versioned scan or a suffix that only repeats the original path.
|
||||
|
||||
It runs in macOS CI.
|
||||
|
||||
Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle
|
||||
@@ -138,13 +174,80 @@ case. Real capture tests preserve actual descendant and cleanup checks.
|
||||
The existing Apple job also exercises the relocated filesystem snapshot identity,
|
||||
cancellation and descriptor cleanup regressions on macOS.
|
||||
|
||||
Prepare the native inspector before cases that measure producer output or exit
|
||||
behavior; keep startup deadlines and cancellation in distinct cases. Run real
|
||||
process-capture verification separately from package or build checks. On macOS,
|
||||
new npm processes can become token-unreadable after changing their display
|
||||
title and prevent verified cleanup during a capture's ownership sweep.
|
||||
|
||||
MCP SDK transport tests with recording providers remain integration tests.
|
||||
They are useful for schema drift and failure projection but do not prove that
|
||||
Hopper, Ghidra or another substituted engine works. `verify:package` proves
|
||||
packaging/install behavior and fake-provider integration; use the corresponding
|
||||
real-provider lanes for engine claims. Real Apple dispatch and Interface
|
||||
real-provider lanes for engine claims. Packed-bridge checks verify shipped files
|
||||
and Python syntax without deleted mock fixtures or source-spelling assertions.
|
||||
Real Apple dispatch and Interface
|
||||
Builder verifiers currently prove format integration through production readers.
|
||||
|
||||
`verify:hopper` exercises an installed Hopper through the production stdio MCP
|
||||
server and CLI. It checks source-owned call chains, CFG edges, references and
|
||||
complete large inventories, then probes unnamed bookmarks, annotation clearing,
|
||||
batch validation before mutation, malformed addresses and regexes, segment-end
|
||||
partial reads, and synthetic file-offset rejection. Advertised schemas are
|
||||
validated in their JSON Schema dialect and successful replies are checked against
|
||||
their advertised output schemas. Navigation checks cover interior-object cursor
|
||||
readback and mapped-memory boundaries. Annotation probes verify invalid native
|
||||
text and duplicate destinations/names before mutation, preserve unselected label
|
||||
owners, and exercise explicit batch label swaps. Function locals retain observed
|
||||
names and stack displacements. Graph probes check symbol/interior-address parity
|
||||
and a source-owned recursive cycle; literal tracing preserves complete queries
|
||||
and whitespace. Disposable binary copies prove that switching and closing
|
||||
actually removes the native document, and that CLI byte, function-dossier,
|
||||
literal-trace results and invalid-address diagnostics agree with MCP.
|
||||
No provider is mocked in this lane.
|
||||
Real search probes cover Unicode names, literal metacharacters, case and regex
|
||||
modes, annotation cache invalidation, complete native fragments of long literals
|
||||
checked against byte reads, escaped UTF-8/control text, byte-preserving Latin-1
|
||||
decoding, and Hopper's UTF-16 symbol-name truncation boundary. It verifies native
|
||||
CallReference classifications across reference and dossier results, retains long
|
||||
string fragment metadata in dossiers, and exercises pathological regex deadline
|
||||
and cancellation followed by successful requests in the same native session. When the macOS
|
||||
Objective-C fixture is present, the lane also verifies native UTF-16 string objects
|
||||
and their inferred encodings against their actual bytes. Native terminal calls
|
||||
are checked across reference, instruction, assembly, block-range and procedure-length
|
||||
projections; block endpoints are normalized using actual native membership.
|
||||
Exact Objective-C names also exercise named CLI selectors for function, instruction,
|
||||
decompilation, reference and search operations. A literal `--help` trace query proves
|
||||
that selector data is preserved independently of global CLI flags.
|
||||
Unmapped annotation destinations and
|
||||
oversized later batch names fail before any earlier edit is applied.
|
||||
|
||||
`verify:hopper:deadlines` checks the native client's optional request deadlines
|
||||
on macOS with an owned source-built fixture. Zero and elapsed deadlines must
|
||||
leave native comments unchanged, including when a synchronous progress observer
|
||||
delays timer dispatch. It also observes a short analysis deadline and verifies
|
||||
subsequent wire recovery and clean shutdown. Caller timeout does not interrupt
|
||||
Hopper's synchronous native operation. Socket boundary tests deterministically
|
||||
cover active and queued expiry, late replies and timer cleanup.
|
||||
|
||||
The Linux demo lane remains a separate `verify:hopper:linux` command.
|
||||
|
||||
`verify:hopper:fat` is a separate macOS lane requiring installed Hopper and the
|
||||
existing Xcode clang/lipo toolchain. It compiles arm64/x86-64 thin executables and
|
||||
one- and two-slice FAT32 containers, verifies exact/interior address mappings
|
||||
against bytes in the original files, checks CLI/MCP parity, and checks owned
|
||||
runtime cleanup. Source byte changes, removal, permission denial (for non-root
|
||||
callers), and nonregular replacement must retain native partial mapping facts,
|
||||
reject unverified original-file coordinates,
|
||||
and recover after restoration. Single-slice FAT cases also relocate the slice
|
||||
without changing its loaded bytes. `verify:hopper:fat64` additionally checks
|
||||
FAT64 preparation through Hopper's native Mach-O loader, source-container
|
||||
mappings, profile identity,
|
||||
malformed and ambiguous slice rejection, and temporary-image lifetime. Both
|
||||
lanes have been verified on Hopper 6.1.0-demo; this establishes REA's prepared
|
||||
FAT64 workflow, not native FAT64-loader support.
|
||||
Cross-architecture fixture compilation is not required by `verify:hopper`.
|
||||
|
||||
Golden tests use immutable captured text inputs with producer/source provenance
|
||||
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
|
||||
claim; capture commands do not automatically approve new expected outputs.
|
||||
@@ -152,6 +255,8 @@ Do not call handcrafted utility output or synthetic binary builders real-data
|
||||
goldens. Keep unsupported binary layouts and malformed boundaries as targeted
|
||||
regressions until a real fixture establishes equivalent coverage.
|
||||
|
||||
`verify:browser` exercises source-map failure isolation and expanded-output limits through the compiled CLI and stdio MCP with real Chrome. It also submits five 2 MiB source-map annotations, checks retained script identities and explicit map omissions, and closes its owned fixture target. These public workflows replace redundant metadata-admission assertions; replacement and document-reset boundaries remain focused producer regressions.
|
||||
|
||||
`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;
|
||||
@@ -202,12 +307,18 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
|
||||
|
||||
| Ghidra lane | Supported runner/target | Additional local tools |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
|
||||
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
|
||||
|
||||
The host lane also verifies namespaced C++ ABI symbols and annotation identity.
|
||||
That facet requires Ghidra's matching `demangler_gnu_v2_41` native component in
|
||||
`GPL/DemanglerGnu/os/<platform>/` or `build/os/<platform>/`, in addition to the
|
||||
native decompiler. The namespace verifier reports missing or non-executable
|
||||
components before testing the fixture. REA does not build or install them.
|
||||
|
||||
Windows native conformance runs with `npm run verify:windows-native` and does
|
||||
not require Ghidra or Java. An optional independently compiled Windows fixture
|
||||
adds in-place reparse, breakaway, environment, and token observations.
|
||||
@@ -219,6 +330,70 @@ The host-native Ghidra lane also verifies native value tracing through the
|
||||
production CLI and a separate stdio MCP process. It compares complete dependency
|
||||
graphs, validates Evidence and upstream/workflow profiles, checks capability
|
||||
discovery, and closes the MCP session. No provider or transport is mocked.
|
||||
It also validates every advertised input/output JSON Schema and the exercised
|
||||
MCP outputs, probes address spelling and name/address ambiguity, and checks
|
||||
direct versus targetless calls, byte-read completeness, invalid input diagnostics,
|
||||
CLI/MCP parity, atomic annotation rollback, refreshed inventories, unchanged
|
||||
executable bytes, and discarded edits after reopen. A deliberately long temporary
|
||||
path exercises private Unix socket allocation and cleanup, including cancellation
|
||||
after a real headless process launches. Native annotation probes reject NUL and
|
||||
unpaired Unicode surrogates without partial edits or a broken bridge, preserve
|
||||
supported Unicode and control text, and check lossless malformed-text diagnostics.
|
||||
Memory-to-file mapping is checked against original artifact bytes.
|
||||
The fixture also stores a pointer one byte past a function entry; exact
|
||||
xrefs, raw procedure references, and CLI/MCP dossiers must retain that data edge.
|
||||
A valid legacy snapshot reconstructs the former omitted edge under its older
|
||||
profile; CLI and MCP must reject that binding with a mismatch reason and
|
||||
recovery advice. The rejected open must preserve the active live session.
|
||||
Exact external entries must resolve while retaining an empty body; unknown
|
||||
external addresses remain unresolved and external annotations are rejected.
|
||||
An adversarial regex over a full 12 KB literal must report stack exhaustion as
|
||||
a resource constraint, preserve live annotations, and allow complete literal
|
||||
searches afterward; CLI and MCP must agree on both results and recovery advice.
|
||||
Real snapshot lifecycle checks retain edited API results as Evidence while
|
||||
rejecting immutable snapshot saves and imports before and after a repeated
|
||||
open of the same target. They verify unchanged live annotations and run identity,
|
||||
absent rejected output files, an unchanged source snapshot, and successful
|
||||
snapshot import/save after closing and recreating the database. The pristine
|
||||
snapshot is written by an independent real CLI session.
|
||||
They also start a real annotation and snapshot close concurrently: the edit
|
||||
must succeed, the snapshot must be rejected without creating a file, and the
|
||||
edited session must remain usable until explicitly closed.
|
||||
Source-admission probes change a caller-owned fixture after `open_binary` but
|
||||
before the first Ghidra query. They require an actionable `artifact_changed`
|
||||
error preserving both digests and the selected path, unchanged provider
|
||||
availability, failed-copy cleanup, and successful recovery after reopening.
|
||||
After import, deleting that source must preserve the captured database identity.
|
||||
Instruction inspection and containing-function lookup also agree across
|
||||
hexadecimal case, leading zeros, and encoded default address-space spellings.
|
||||
The same source-acquisition workflow exercises missing and directory-replaced
|
||||
inputs, plus real read-permission denial on a non-root host. Root runs report
|
||||
that permission-denial check as unverified. A focused filesystem integration
|
||||
check retains selected-platform routing and exclusive creation; two copy/digest
|
||||
checks duplicated by the real workflow have been removed.
|
||||
Namespace annotation probes compile a separate host C fixture with C++ ABI
|
||||
symbols, avoiding a C++ runtime prerequisite. Real Ghidra demangling supplies
|
||||
duplicate leaf names in two top-level namespaces and a nested namespace. The
|
||||
workflow verifies leaf and qualified renames, repeated reuse of fully qualified
|
||||
readback, lookup by the returned name, literal namespace-like leaf names,
|
||||
rejection of empty qualified leaf names without changing comments, CLI
|
||||
behavior, and independent CLI/MCP database ownership.
|
||||
Large-result probes compile initialized host-native data sized from the pinned
|
||||
MCP SDK receive budget. Real byte reads, annotation edits and function dossiers
|
||||
exceed that budget while preserving the connection and active analysis run.
|
||||
Successful-result and oversized-error delivery constraints must identify their
|
||||
successfully retained Evidence records;
|
||||
export must recover every source byte, complete annotation and original error
|
||||
diagnostic, with CLI parity
|
||||
and an unchanged executable. The focused formatter check retains only the
|
||||
separate case where recording was not acknowledged.
|
||||
Long ordinary procedure names and encoded address-space selectors must produce
|
||||
normal validation errors without exhausting Java's regex stack or losing the
|
||||
private bridge connection. Each rejection is followed by a real provider lookup;
|
||||
address-like literal names still resolve exactly after annotation.
|
||||
Malformed annotation readback, memory completeness, and inventory data remain separate
|
||||
SDK/provider integration cases; success from a real
|
||||
provider cannot establish rejection of a contradictory provider response.
|
||||
|
||||
The Linux switch lane checks dense, sparse-with-holes, shared-body, nonzero,
|
||||
negative, and nonexact JSON integer labels plus a comparison-only control.
|
||||
@@ -284,7 +459,7 @@ are separate proof levels.
|
||||
Synthetic producer regressions run independently:
|
||||
|
||||
```sh
|
||||
npm run test:focused -- tests/boundary/android/jadxIntegration.test.ts tests/boundary/mcp/androidAnalysisMcp.test.ts
|
||||
npm run test:focused -- tests/boundary/process/jadxIntegration.test.ts tests/boundary/process/androidAnalysisMcp.test.ts
|
||||
```
|
||||
|
||||
## Optional NativeAOT Ghidra analysis
|
||||
@@ -356,22 +531,27 @@ a DOS emulator or compiler, then checks real 16-bit decoding, segment
|
||||
relocation, near/far calls, decompilation, disjoint function body ranges,
|
||||
stable CLI/MCP observations, unchanged source bytes, and owned process/project
|
||||
cleanup. Raw p-code address-space selector tokens are reported separately from
|
||||
the stable observation comparison. Linux x64 is verified; macOS DOS remains
|
||||
unverified. This lane is separate from host-native and optional cross-format
|
||||
the stable observation comparison. Linux x64 and macOS arm64 are verified;
|
||||
macOS x64 remains unverified. This lane is separate from host-native and optional cross-format
|
||||
verification. See [DOS analysis](ghidra-dos.md).
|
||||
|
||||
`npm run verify:ghidra:com` uses a generated headerless fixture with no compiler,
|
||||
DOS emulator or game data. It exercises explicit admission, BinaryLoader entry
|
||||
preparation, measured register context, whole-file byte readback, source offsets,
|
||||
unmapped PSP/partial reads, actual decompilation, CLI/MCP parity and owned cleanup.
|
||||
Both segmented-address lanes reject oversized default, explicit-space and encoded-space
|
||||
coordinates through real reads, function queries and annotation attempts. Rejected
|
||||
annotations must preserve the live function dossier; CLI and MCP must report the
|
||||
truncation constraint, while leading-zero coordinates still resolve correctly.
|
||||
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
|
||||
runtime or PC-98 device execution.
|
||||
|
||||
## Apple Interface Builder archives
|
||||
|
||||
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
|
||||
real `.nib` with Xcode `ibtool`, wraps it in a temporary app bundle, and checks
|
||||
the decoded view hierarchy, outlet, action, evidence coverage, and truncation
|
||||
real `.nib` with Xcode `ibtool`, wraps it in a temporary app bundle, and runs
|
||||
the compiled CLI and stdio MCP server with production providers. It compares
|
||||
their decoded results and checks the view hierarchy, outlet, action, evidence coverage, and truncation
|
||||
status. Storyboard compilation additionally requires an installed iOS platform.
|
||||
|
||||
Keep the provider-specific acceptance path independent from optional
|
||||
@@ -463,7 +643,9 @@ provider evidence.
|
||||
Each Vitest project uses up to two workers, bounded by available host parallelism.
|
||||
Default-group projects can run together. Only `process-boundary` runs in the
|
||||
later sequence group and serializes its files because process-tree observations
|
||||
share host sampling resources. Acceptance and process-global files retain isolated
|
||||
share host sampling resources. Tests that launch the real process harness belong
|
||||
in `tests/boundary/process/`, including CLI command and environment tests.
|
||||
Acceptance and process-global files retain isolated
|
||||
forks but are not serialized; isolation does not imply serial scheduling. CI uses
|
||||
the same per-project worker bound.
|
||||
The pure domain/contracts and recording-port service projects share one worker
|
||||
@@ -532,24 +714,36 @@ conformance/vtable fixture.
|
||||
Both commands reject a changed executable digest and clean up the fixture
|
||||
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
|
||||
|
||||
`npm run verify:native-calls` needs only macOS with Command Line Tools (`clang`,
|
||||
`lldb`, `codesign`, `nm`). It compiles `tests/conformance/native/calls.m` and
|
||||
`npm run verify:native-calls` needs macOS with Command Line Tools (`clang`,
|
||||
`lldb`, `codesign`, `nm`) and actual debugger access to the owned fixtures.
|
||||
The lane reports Developer Mode status without treating it as proof of access
|
||||
or denial, and does not change host settings. LLDB and target entitlements
|
||||
establish the tested permission boundary.
|
||||
It compiles `tests/conformance/native/calls.m` and
|
||||
runs `observe-native-calls` through the CLI and stdio MCP. It checks:
|
||||
|
||||
- the receiver class, selector and argument registers of every entry, and that
|
||||
breakpoint addresses equal `nm`'s symbol addresses;
|
||||
- captured stdout and an environment override;
|
||||
- overlapping breakpoint selections retain each selection's hits and share
|
||||
the aggregate event limit; callback capture reads the stopped frame before
|
||||
LLDB resumes, avoiding stale frame metadata from asynchronous stop events;
|
||||
- captured stdout and an environment override, plus a 2 MiB flood on each
|
||||
output stream with bounded retained prefixes and exact drained-byte counts;
|
||||
- the event-limit and duration outcomes, with the process confirmed gone;
|
||||
- that a hardened-runtime copy is refused with `debugger-attach-denied`, and
|
||||
that the same copy signed with `get-task-allow` is traced.
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
|
||||
### Firmware adapters
|
||||
|
||||
Linux-owned provider launches require a procps-compatible `ps` on REA's `PATH`
|
||||
so ownership can be inspected before launching a child. A missing or incompatible
|
||||
command fails with an actionable capability error before the launch.
|
||||
|
||||
`npm run fixtures:firmware` uses existing Python 3 and a host C compiler to make
|
||||
an ignored gzip/USTAR firmware fixture and independent offset/hash oracle.
|
||||
`npm run verify:firmware` requires caller-supplied Binwalk 3.1.0, Unblob 26.6.4
|
||||
and util-linux prlimit on Linux. The provider also accepts other 3.1.x and
|
||||
and util-linux prlimit on Linux, with `7z` on `PATH` for the gzip/USTAR
|
||||
fixture's Unblob extractor. The provider also accepts other 3.1.x and
|
||||
26.6.x builds and reports them as unverified; this lane proves the audited
|
||||
releases. It verifies CLI/MCP parity, selected ranges,
|
||||
unknown chunks, depth limits and extracted child digests. The optional
|
||||
|
||||
@@ -126,6 +126,15 @@ process cleanup. Owned decoder diagnostics retain at most 1 MiB across stdout
|
||||
and stderr; the collector drops overflow as it arrives, reports the limit
|
||||
failure and verifies cleanup. Heap limits alone do not establish aggregate RSS limits.
|
||||
|
||||
Observed decoder memory exhaustion returns `resource_constraint`, with the
|
||||
selected capture, applicable limits and retained process diagnostics. HAR's
|
||||
old-generation heap is fixed; inherited `NODE_OPTIONS` cannot raise it.
|
||||
Record selection happens after complete decoding and does not reduce that
|
||||
workload. Use a smaller capture exported by its producer or another decoder
|
||||
with sufficient capacity, retaining the original for provenance. Subsets do
|
||||
not establish complete-capture coverage. Repeating the same workload or
|
||||
running provider health diagnostics does not resolve the heap constraint.
|
||||
|
||||
Stable regular-file reads reject symlinks, replacement and concurrent changes.
|
||||
Private snapshots, declarations and tool configuration are removed before the
|
||||
result returns. Cleanup uncertainty retains the selected capture, resource and
|
||||
|
||||
+3
-1
@@ -20,7 +20,9 @@ Results retain:
|
||||
- The selected target, frame and document loader identity; local armed/end timestamps and separate backend monotonic coverage timestamps.
|
||||
- Every admitted producer function/block range, exclusive end offset and execution count in UTF-16 code units, with verified bounds when exact text is available. Nested/overlapping counts are preserved and must not be summed.
|
||||
- Main-document script inventory, exact session script IDs, resource start coordinates, sourceURL/source-map declarations, producer hash and independently computed UTF-8 text digest.
|
||||
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality.
|
||||
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality. Callsite and listener `line_number`/`column_number` values are CDP's zero-based lines and UTF-16 columns in the resource named by `url`. With a `script_id`, that is the script's enclosing resource: the script file for an external script, or the HTML document for an inline script. `resource_start` is where the script text starts in that resource. A callsite with an empty `script_id` comes from the request's reported initiator rather than a script call frame, such as markup that the parser read in a document or a module import in a script file. It still locates a position in `url`, but has no session-script association, so it cannot be joined to captured source or `resource_start`. `trace_web_source_location` takes one-based, script-relative lines and zero-based UTF-16 columns.
|
||||
|
||||
For an external script, where `url` is the script file, use `line = line_number + 1` and keep `column = column_number`; this also applies to an empty-`script_id` initiator position in a script file. For an inline script, which requires a `script_id` and its `resource_start`, use `line = line_number - resource_start.line_number + 1`. Subtract `resource_start.column_number` from `column_number` only when `line_number === resource_start.line_number`; keep the reported column on later lines. Columns remain zero-based. This conversion requires known source-start and column coordinates; preserve unavailable coordinates as unknown.
|
||||
|
||||
The coverage counter interval starts at backend acceptance before the armed receipt and ends at the resetting sample after the local timer. Counts may include execution during these command intervals; request events are restricted to the locally armed window. The result retains both backend timestamps and the distinct local clock.
|
||||
|
||||
|
||||
@@ -84,6 +84,33 @@ are made.
|
||||
|
||||
## Verification
|
||||
|
||||
Browser bundle collection uses absolute section offsets for nested inline maps.
|
||||
An enclosing column offset contributes only when the nested section starts on
|
||||
that enclosing section's first generated line. Parent boundaries still clip
|
||||
preceding browser mappings when a nested section starts with a gap or has no
|
||||
child leaves. Raw map text, original source identities and the dedicated decoder
|
||||
profile are preserved.
|
||||
|
||||
Browser collection accepts up to 64 MiB of map responses per inspection and
|
||||
preflights indexed sections, sources, names, generated rows and mapping segments
|
||||
against a 262144-record decode budget before upstream flattening. Indexed maps
|
||||
are limited to 64 nested levels. These structural limits bound decoded object
|
||||
growth while preserving large embedded `sourcesContent` strings within the
|
||||
response-byte budget. It also bounds the expanded inline evidence to 32 MiB
|
||||
across the inspection, checking resolved source identities and mapping rows
|
||||
before publishing them, and bounds raw resolved-source identities to 64 MiB
|
||||
before upstream materializes their arrays. Exceeding either decoder budget
|
||||
returns a `fetch_failed` item while retaining other maps. Decoded coordinates
|
||||
and source/name indexes are validated per map; a malformed map is reported as
|
||||
`invalid` without discarding earlier or later maps.
|
||||
Collection reserves each requested map's failure context before fetching it.
|
||||
When the aggregate inline-result budget cannot reserve another item, remaining
|
||||
requests are left unprocessed and reported as unknown through the top-level
|
||||
`limitation`; `requested` retains the full count and `processed` counts returned
|
||||
items.
|
||||
Malformed UTF-8 map responses are reported as invalid rather than repaired with
|
||||
replacement characters.
|
||||
|
||||
Focused tests cover duplicate mappings, indexed offsets/overlap/local indexes,
|
||||
32-bit VLQ overflow, nullable content, UTF-16/CRLF positions, immutable artifact
|
||||
identity, cancellation, late output closure, cleanup failures, SDK schemas and
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"entry": [
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"tests/processOwnershipRunner.ts"
|
||||
],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"vitepress": {
|
||||
"entry": ["docs/.vitepress/config.ts"]
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "6.0.0",
|
||||
"version": "6.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "6.0.0",
|
||||
"version": "6.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "6.0.0",
|
||||
"version": "6.1.0",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -138,6 +138,9 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
|
||||
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
|
||||
"verify:hopper:fat64": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs --fat64",
|
||||
"verify:apple-dispatch": "npm run build:cached && node scripts/verify/apple/dispatch.mjs",
|
||||
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
||||
"release-type": "node",
|
||||
"versioning": "always-bump-minor",
|
||||
"include-v-in-tag": false,
|
||||
"changelog-sections": [
|
||||
{ "type": "feat", "section": "Features", "hidden": false },
|
||||
@@ -16,10 +17,6 @@
|
||||
".": {
|
||||
"package-name": "rea-agents",
|
||||
"extra-files": [
|
||||
{ "type": "generic", "path": "README_zh.md" },
|
||||
{ "type": "generic", "path": "README_ja.md" },
|
||||
{ "type": "generic", "path": "README_ko.md" },
|
||||
{ "type": "generic", "path": "README_ar.md" },
|
||||
{ "type": "generic", "path": "docs/installation.md" },
|
||||
{
|
||||
"type": "generic",
|
||||
|
||||
@@ -61,7 +61,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
const target = typeof args[0] === "string" ? args[0] : null;
|
||||
recordRuntime("preload", "context-bridge-exposure", "attempted", {
|
||||
target,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
process_type: processType(),
|
||||
});
|
||||
try {
|
||||
@@ -110,7 +110,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
event === "will-download" ? "blocked" : "observed",
|
||||
{
|
||||
target: identity(args[0], "webContents"),
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
error: event === "will-download",
|
||||
},
|
||||
);
|
||||
@@ -124,7 +124,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
(webContents, permission, callback) => {
|
||||
recordRuntime("permission", "permission-request", "blocked", {
|
||||
target: identity(webContents, "webContents"),
|
||||
argument_shapes: [shape(permission)],
|
||||
argument_shape_values: [permission],
|
||||
error: true,
|
||||
});
|
||||
if (typeof callback === "function") callback(false);
|
||||
@@ -137,7 +137,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
return originalPermissionCheck.call(this, (webContents, permission) => {
|
||||
recordRuntime("permission", "permission-check", "blocked", {
|
||||
target: identity(webContents, "webContents"),
|
||||
argument_shapes: [shape(permission)],
|
||||
argument_shape_values: [permission],
|
||||
error: true,
|
||||
});
|
||||
return false;
|
||||
@@ -148,7 +148,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
session.setCertificateVerifyProc = function blockedCertificate() {
|
||||
return originalCertificate.call(this, (request, callback) => {
|
||||
recordRuntime("permission", "certificate-error", "blocked", {
|
||||
argument_shapes: [shape(request)],
|
||||
argument_shape_values: [request],
|
||||
error: true,
|
||||
});
|
||||
if (typeof callback === "function") callback(-2);
|
||||
@@ -168,14 +168,14 @@ const createElectronActiveBoundaryPatches = ({
|
||||
} catch {
|
||||
recordRuntime("popup-attempt", "window-open-handler", "failed", {
|
||||
target: contentsId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
error: true,
|
||||
});
|
||||
return { action: "deny" };
|
||||
}
|
||||
recordRuntime("popup-attempt", "window-open-handler", "blocked", {
|
||||
target: contentsId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
error: true,
|
||||
});
|
||||
return { action: "deny" };
|
||||
@@ -195,9 +195,11 @@ const createElectronActiveBoundaryPatches = ({
|
||||
};
|
||||
};
|
||||
|
||||
const patchSessionManager = (sessionModule) => {
|
||||
const patchSessionManager = (sessionModule, app) => {
|
||||
if (sessionModule === null || sessionModule === undefined) return;
|
||||
patchSession(sessionModule.defaultSession);
|
||||
if (app === undefined || app.isReady())
|
||||
patchSession(sessionModule.defaultSession);
|
||||
else app.once("ready", () => patchSession(sessionModule.defaultSession));
|
||||
for (const method of ["fromPartition", "fromPath"]) {
|
||||
const original = sessionModule[method];
|
||||
if (typeof original !== "function") continue;
|
||||
@@ -223,7 +225,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
method === "relaunch" ? "updater" : "protocol",
|
||||
method,
|
||||
"blocked",
|
||||
{ argument_shapes: args.map((value) => shape(value)), error: true },
|
||||
{ argument_shape_values: args, error: true },
|
||||
);
|
||||
return method === "setAsDefaultProtocolClient" ? false : undefined;
|
||||
};
|
||||
@@ -250,11 +252,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
recordRuntime("native-addon", "dlopen", "attempted", {
|
||||
artifact_path: typeof filename === "string" ? filename : null,
|
||||
artifact_sha256: digest,
|
||||
argument_shapes: [
|
||||
shape(module),
|
||||
shape(filename),
|
||||
...args.map((value) => shape(value)),
|
||||
],
|
||||
argument_shape_value_groups: [[module, filename], args],
|
||||
});
|
||||
try {
|
||||
const result = original.call(this, module, filename, ...args);
|
||||
@@ -280,7 +278,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
if (typeof original !== "function") return;
|
||||
utilityProcess.fork = function patchedFork(...args) {
|
||||
recordRuntime("process-lifecycle", "utility-process-fork", "attempted", {
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
process_type: "main",
|
||||
});
|
||||
const child = original.apply(this, args);
|
||||
@@ -304,7 +302,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
recordRuntime("process-lifecycle", `utility.${event}`, "observed", {
|
||||
target: childId,
|
||||
process_type: "utility",
|
||||
argument_shapes: eventArgs.map((value) => shape(value)),
|
||||
argument_shape_values: eventArgs,
|
||||
});
|
||||
});
|
||||
if (child && typeof child.postMessage === "function") {
|
||||
@@ -344,7 +342,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
if (typeof original !== "function") continue;
|
||||
shell[method] = function blockedShellCall(...args) {
|
||||
recordRuntime("shell-attempt", method, "blocked", {
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
error: true,
|
||||
});
|
||||
return Promise.reject(new Error("External shell effects are blocked"));
|
||||
@@ -359,7 +357,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
if (typeof original !== "function") continue;
|
||||
childProcess[method] = function patchedChildProcess(...args) {
|
||||
recordRuntime("process-lifecycle", `child.${method}`, "attempted", {
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
});
|
||||
const child = original.apply(this, args);
|
||||
const childId = identity(child, "child");
|
||||
@@ -369,7 +367,7 @@ const createElectronActiveBoundaryPatches = ({
|
||||
patchEmitter(child, (event, eventArgs) =>
|
||||
recordRuntime("process-lifecycle", `child.${event}`, "observed", {
|
||||
target: childId,
|
||||
argument_shapes: eventArgs.map((value) => shape(value)),
|
||||
argument_shape_values: eventArgs,
|
||||
}),
|
||||
);
|
||||
return child;
|
||||
|
||||
@@ -16,9 +16,19 @@ const ipcEventKinds = new Set([
|
||||
"ipc-renderer-post-message",
|
||||
]);
|
||||
const events = [];
|
||||
// Budget serialized data and estimated retained object/array storage together.
|
||||
const retentionBudgetBytes = 16 * 1024 * 1024;
|
||||
const snapshotEnvelopeReserveBytes = 1024;
|
||||
let estimatedRetainedBytes = 0;
|
||||
let serializedBytes = 0;
|
||||
let observed = 0;
|
||||
let observedIpc = 0;
|
||||
let observedRuntime = 0;
|
||||
let dropped = 0;
|
||||
let droppedIpc = 0;
|
||||
let droppedRuntime = 0;
|
||||
const droppedFamilies = new Map();
|
||||
const droppedRoles = new Map();
|
||||
let sequence = 0;
|
||||
let correlationSequence = 0;
|
||||
|
||||
@@ -43,6 +53,73 @@ const shape = (value, depth = 0) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Measure JSON strings without allocating an encoded copy of a caller value.
|
||||
const jsonStringBytes = (value) => {
|
||||
let bytes = 2;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index);
|
||||
if (code === 0x22 || code === 0x5c) bytes += 2;
|
||||
else if (
|
||||
code === 0x08 ||
|
||||
code === 0x09 ||
|
||||
code === 0x0a ||
|
||||
code === 0x0c ||
|
||||
code === 0x0d
|
||||
)
|
||||
bytes += 2;
|
||||
else if (code <= 0x1f) bytes += 6;
|
||||
else if (code >= 0xd800 && code <= 0xdbff) {
|
||||
const next = value.charCodeAt(index + 1);
|
||||
if (next >= 0xdc00 && next <= 0xdfff) {
|
||||
bytes += 4;
|
||||
index += 1;
|
||||
} else bytes += 6;
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) bytes += 6;
|
||||
else if (code <= 0x7f) bytes += 1;
|
||||
else if (code <= 0x7ff) bytes += 2;
|
||||
else bytes += 3;
|
||||
if (bytes > retentionBudgetBytes) return bytes;
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const jsonScalarBytes = (value) => {
|
||||
if (value === null) return 4;
|
||||
if (typeof value === "string") return jsonStringBytes(value);
|
||||
if (typeof value === "boolean") return value ? 4 : 5;
|
||||
return String(value).length;
|
||||
};
|
||||
|
||||
const eventFamily = (kind, processTypeValue) => {
|
||||
if (kind === "preload" && processTypeValue === "main")
|
||||
return "preload-configuration";
|
||||
if (kind.startsWith("ipc-renderer"))
|
||||
return processTypeValue === "renderer" ? "renderer-ipc" : "ipc";
|
||||
return ipcEventKinds.has(kind) ? "ipc" : kind;
|
||||
};
|
||||
|
||||
const noteDropped = (kind, processTypeValue, isIpc) => {
|
||||
dropped += 1;
|
||||
if (isIpc) droppedIpc += 1;
|
||||
else droppedRuntime += 1;
|
||||
const family = eventFamily(kind, processTypeValue);
|
||||
droppedFamilies.set(family, (droppedFamilies.get(family) ?? 0) + 1);
|
||||
if (typeof processTypeValue === "string")
|
||||
droppedRoles.set(
|
||||
processTypeValue,
|
||||
(droppedRoles.get(processTypeValue) ?? 0) + 1,
|
||||
);
|
||||
if (kind === "window-lifecycle")
|
||||
droppedRoles.set("window", (droppedRoles.get("window") ?? 0) + 1);
|
||||
if (kind === "web-contents-lifecycle" || kind === "navigation")
|
||||
droppedRoles.set(
|
||||
"web_contents",
|
||||
(droppedRoles.get("web_contents") ?? 0) + 1,
|
||||
);
|
||||
if (kind === "preload" && processTypeValue === "preload")
|
||||
droppedRoles.set("preload", (droppedRoles.get("preload") ?? 0) + 1);
|
||||
};
|
||||
|
||||
const isAllowedNavigation = (value) => {
|
||||
if (typeof value !== "string") return true;
|
||||
try {
|
||||
@@ -83,7 +160,8 @@ const frameIdentity = (event) => {
|
||||
|
||||
const record = (event) => {
|
||||
observed += 1;
|
||||
if (ipcEventKinds.has(event.kind)) observedIpc += 1;
|
||||
const isIpc = ipcEventKinds.has(event.kind);
|
||||
if (isIpc) observedIpc += 1;
|
||||
else observedRuntime += 1;
|
||||
const raw = {
|
||||
sequence: ++sequence,
|
||||
@@ -109,10 +187,15 @@ const record = (event) => {
|
||||
error: false,
|
||||
...event,
|
||||
};
|
||||
const argumentShapes = Array.isArray(raw.argument_shapes)
|
||||
const shapeGroups = Array.isArray(raw.argument_shape_value_groups)
|
||||
? raw.argument_shape_value_groups.filter(Array.isArray)
|
||||
: Array.isArray(raw.argument_shape_values)
|
||||
? [raw.argument_shape_values]
|
||||
: [];
|
||||
const existingShapes = Array.isArray(raw.argument_shapes)
|
||||
? raw.argument_shapes
|
||||
: [];
|
||||
events.push({
|
||||
const retainedEvent = {
|
||||
...raw,
|
||||
correlation_id: raw.correlation_id,
|
||||
event: typeof raw.event === "string" ? raw.event : null,
|
||||
@@ -121,9 +204,7 @@ const record = (event) => {
|
||||
receiver: typeof raw.receiver === "string" ? raw.receiver : null,
|
||||
frame: typeof raw.frame === "string" ? raw.frame : null,
|
||||
target: typeof raw.target === "string" ? raw.target : null,
|
||||
argument_shapes: argumentShapes.map((value) =>
|
||||
typeof value === "string" ? value : "unknown",
|
||||
),
|
||||
argument_shapes: [],
|
||||
result_shape:
|
||||
typeof raw.result_shape === "string" ? raw.result_shape : null,
|
||||
process_type:
|
||||
@@ -132,8 +213,91 @@ const record = (event) => {
|
||||
typeof raw.artifact_path === "string" ? raw.artifact_path : null,
|
||||
artifact_sha256:
|
||||
typeof raw.artifact_sha256 === "string" ? raw.artifact_sha256 : null,
|
||||
});
|
||||
return events.at(-1);
|
||||
};
|
||||
delete retainedEvent.argument_shape_values;
|
||||
delete retainedEvent.argument_shape_value_groups;
|
||||
|
||||
// All event values are JSON scalars except argument_shapes. Measure them
|
||||
// incrementally so large channels, paths, or shape lists never require a
|
||||
// full JSON.stringify temporary allocation before the budget decision.
|
||||
let eventBytes = 2;
|
||||
let argumentShapeCount = 0;
|
||||
const eventKeys = Object.keys(retainedEvent);
|
||||
for (let index = 0; index < eventKeys.length; index += 1) {
|
||||
const key = eventKeys[index];
|
||||
const value = retainedEvent[key];
|
||||
if (index > 0) eventBytes += 1;
|
||||
eventBytes += jsonStringBytes(key) + 1;
|
||||
if (key === "argument_shapes") {
|
||||
eventBytes += 2;
|
||||
let shapeOverflow = false;
|
||||
const countShape = (argument) => {
|
||||
const encodedShapeBytes = jsonStringBytes(shape(argument));
|
||||
eventBytes += encodedShapeBytes + (argumentShapeCount === 0 ? 0 : 1);
|
||||
argumentShapeCount += 1;
|
||||
shapeOverflow =
|
||||
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
|
||||
retentionBudgetBytes;
|
||||
};
|
||||
for (const group of shapeGroups)
|
||||
for (const argument of group) {
|
||||
countShape(argument);
|
||||
if (shapeOverflow) break;
|
||||
}
|
||||
if (shapeOverflow) {
|
||||
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
|
||||
return null;
|
||||
}
|
||||
for (const valueShape of existingShapes)
|
||||
if (!shapeOverflow) {
|
||||
eventBytes +=
|
||||
jsonStringBytes(
|
||||
typeof valueShape === "string" ? valueShape : "unknown",
|
||||
) + (argumentShapeCount++ === 0 ? 0 : 1);
|
||||
shapeOverflow =
|
||||
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
|
||||
retentionBudgetBytes;
|
||||
}
|
||||
if (shapeOverflow) {
|
||||
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
|
||||
return null;
|
||||
}
|
||||
} else eventBytes += jsonScalarBytes(value);
|
||||
if (
|
||||
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
|
||||
retentionBudgetBytes
|
||||
) {
|
||||
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Reserve room for the only fields mutated after recording: result_shape and
|
||||
// error. The shape vocabulary's longest value is "undefined".
|
||||
const reservedMutationBytes = retainedEvent.result_shape === null ? 7 : 0;
|
||||
// Include one byte for the event-array separator and reserve space for the
|
||||
// snapshot's counters and JSON delimiters.
|
||||
// One retained event, one defensive snapshot copy, and up to two serialized
|
||||
// result views (timeline plus IPC), with per-object and per-array-slot costs.
|
||||
const eventEstimateBytes = eventBytes * 3 + 512 + argumentShapeCount * 16;
|
||||
const retainedEventBytes = eventEstimateBytes + reservedMutationBytes + 1;
|
||||
if (
|
||||
estimatedRetainedBytes + retainedEventBytes + snapshotEnvelopeReserveBytes >
|
||||
retentionBudgetBytes
|
||||
) {
|
||||
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
|
||||
return null;
|
||||
}
|
||||
for (const group of shapeGroups)
|
||||
for (const argument of group)
|
||||
retainedEvent.argument_shapes.push(shape(argument));
|
||||
for (const valueShape of existingShapes)
|
||||
retainedEvent.argument_shapes.push(
|
||||
typeof valueShape === "string" ? valueShape : "unknown",
|
||||
);
|
||||
events.push(retainedEvent);
|
||||
estimatedRetainedBytes += retainedEventBytes;
|
||||
serializedBytes += eventBytes + reservedMutationBytes + 1;
|
||||
return retainedEvent;
|
||||
};
|
||||
|
||||
const recordRuntime = (kind, event, phase, details = {}) =>
|
||||
@@ -145,7 +309,7 @@ const recordIpc = (kind, channel, args, details = {}) =>
|
||||
return record({
|
||||
kind,
|
||||
channel: typeof channel === "string" ? channel : null,
|
||||
argument_shapes: values.map((value) => shape(value)),
|
||||
argument_shape_values: values,
|
||||
...details,
|
||||
});
|
||||
})();
|
||||
@@ -222,7 +386,7 @@ const patchNavigationMethods = (webContents, contentsId) => {
|
||||
webContents[method] = function patchedNavigation(...args) {
|
||||
recordRuntime("navigation", method, "attempted", {
|
||||
target: contentsId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
});
|
||||
if (method === "loadURL" && !isAllowedNavigation(args[0])) {
|
||||
recordRuntime("navigation", method, "blocked", {
|
||||
@@ -354,7 +518,7 @@ const patchWebContents = (webContents, windowId) => {
|
||||
recordRuntime("navigation", event, "blocked", {
|
||||
target: contentsId,
|
||||
sender: windowId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
error: true,
|
||||
});
|
||||
return;
|
||||
@@ -363,7 +527,7 @@ const patchWebContents = (webContents, windowId) => {
|
||||
recordRuntime(kind, event, "observed", {
|
||||
target: contentsId,
|
||||
sender: windowId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
error: kind === "error",
|
||||
});
|
||||
if (event === "did-create-window") {
|
||||
@@ -379,31 +543,44 @@ const patchWebContents = (webContents, windowId) => {
|
||||
boundaries.patchWindowOpenHandler(webContents, contentsId);
|
||||
};
|
||||
|
||||
const patchBrowserWindow = (electron) => {
|
||||
const original = electron.BrowserWindow;
|
||||
if (typeof original !== "function") return;
|
||||
electron.BrowserWindow = new Proxy(original, {
|
||||
construct(target, args, newTarget) {
|
||||
const window = Reflect.construct(target, args, newTarget);
|
||||
const targetId = identity(window, "window");
|
||||
recordRuntime("window-lifecycle", "created", "completed", {
|
||||
target: targetId,
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
});
|
||||
const preload = args[0]?.webPreferences?.preload;
|
||||
const patchWindows = (app) => {
|
||||
if (app === null || app === undefined) return;
|
||||
app.on("browser-window-created", (_event, window) => {
|
||||
const targetId = identity(window, "window");
|
||||
recordRuntime("window-lifecycle", "created", "completed", {
|
||||
target: targetId,
|
||||
capture_method: "event-emitter",
|
||||
});
|
||||
try {
|
||||
// Electron 44 separates the preload descriptor from WebPreferences.
|
||||
// Read the engine's descriptor, never infer a path from application code.
|
||||
const preferences = window.webContents.getLastWebPreferences?.();
|
||||
const preload =
|
||||
typeof preferences?.preload === "string"
|
||||
? preferences.preload
|
||||
: window.webContents._getPreloadScript?.()?.filePath;
|
||||
if (typeof preload === "string")
|
||||
recordRuntime("preload", "configured", "completed", {
|
||||
target: targetId,
|
||||
artifact_path: preload,
|
||||
});
|
||||
patchEmitter(window, (event) =>
|
||||
recordRuntime("window-lifecycle", event, "observed", {
|
||||
} catch (cause) {
|
||||
recordRuntime(
|
||||
"error",
|
||||
cause instanceof Error ? cause.message : String(cause),
|
||||
"failed",
|
||||
{
|
||||
target: targetId,
|
||||
}),
|
||||
error: true,
|
||||
},
|
||||
);
|
||||
patchWebContents(window.webContents, targetId);
|
||||
return window;
|
||||
},
|
||||
}
|
||||
patchEmitter(window, (event) =>
|
||||
recordRuntime("window-lifecycle", event, "observed", {
|
||||
target: targetId,
|
||||
}),
|
||||
);
|
||||
patchWebContents(window.webContents, targetId);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -427,7 +604,7 @@ try {
|
||||
"observed",
|
||||
{
|
||||
target: "app",
|
||||
argument_shapes: args.map((value) => shape(value)),
|
||||
argument_shape_values: args,
|
||||
process_type: "main",
|
||||
},
|
||||
),
|
||||
@@ -435,33 +612,60 @@ try {
|
||||
patchIpcMain(electron.ipcMain);
|
||||
boundaries.patchIpcRenderer(electron.ipcRenderer);
|
||||
boundaries.patchContextBridge(electron.contextBridge);
|
||||
patchBrowserWindow(electron);
|
||||
patchWindows(electron.app);
|
||||
boundaries.patchUtilityProcess(electron.utilityProcess);
|
||||
boundaries.patchShell(electron.shell);
|
||||
boundaries.patchSessionManager(electron.session);
|
||||
boundaries.patchSessionManager(electron.session, electron.app);
|
||||
boundaries.patchApplicationEffects(electron.app);
|
||||
boundaries.patchNativeAddonLoading();
|
||||
boundaries.patchChildProcess();
|
||||
process.once("uncaughtException", (cause, origin) => {
|
||||
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
|
||||
argument_shapes: [shape(cause), shape(origin)],
|
||||
argument_shape_values: [cause, origin],
|
||||
error: true,
|
||||
});
|
||||
throw cause;
|
||||
});
|
||||
process.once("unhandledRejection", (reason, promise) => {
|
||||
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
|
||||
argument_shapes: [shape(reason), shape(promise)],
|
||||
argument_shape_values: [reason, promise],
|
||||
error: true,
|
||||
});
|
||||
throw reason;
|
||||
});
|
||||
} catch {
|
||||
} catch (cause) {
|
||||
globalThis.__reaElectronActiveHookError = true;
|
||||
recordRuntime(
|
||||
"error",
|
||||
cause instanceof Error ? cause.message : String(cause),
|
||||
"failed",
|
||||
{
|
||||
target: "active-hook-installation",
|
||||
error: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
globalThis.__reaElectronActiveSnapshot = () => ({
|
||||
events: events.slice(),
|
||||
events: events.map((event) => ({
|
||||
...event,
|
||||
argument_shapes: [...event.argument_shapes],
|
||||
})),
|
||||
retention_budget_bytes: retentionBudgetBytes,
|
||||
estimated_retained_bytes: estimatedRetainedBytes,
|
||||
event_serialized_byte_upper_bound: serializedBytes,
|
||||
retained: events.length,
|
||||
dropped,
|
||||
dropped_ipc: droppedIpc,
|
||||
dropped_runtime: droppedRuntime,
|
||||
dropped_event_families: [...droppedFamilies].map(([family, count]) => ({
|
||||
family,
|
||||
count,
|
||||
})),
|
||||
dropped_event_roles: [...droppedRoles].map(([role, count]) => ({
|
||||
role,
|
||||
count,
|
||||
})),
|
||||
observed,
|
||||
observed_ipc: observedIpc,
|
||||
observed_runtime: observedRuntime,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { access } from "node:fs/promises";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
/** Generate a real compiler map and serve only source-owned fixture code. */
|
||||
export async function startBrowserSourceMapSite() {
|
||||
export async function startBrowserSourceMapSite(options = {}) {
|
||||
const command = process.env.REA_WEB_SOURCE_MAP_COMPILER;
|
||||
if (!command || !isAbsolute(command))
|
||||
throw new Error(
|
||||
@@ -19,19 +19,40 @@ export async function startBrowserSourceMapSite() {
|
||||
const compiled = await compiler
|
||||
.transform(original, {
|
||||
loader: "ts",
|
||||
sourcefile: "../src/fixture.ts?v=7#original",
|
||||
sourcefile: options.sourcefile ?? "../src/fixture.ts?v=7#original",
|
||||
sourcemap: "external",
|
||||
minify: true,
|
||||
format: "iife",
|
||||
target: "es2022",
|
||||
})
|
||||
.finally(() => compiler.stop());
|
||||
const prefixes = options.prefixes ?? [];
|
||||
const generated =
|
||||
prefixes.join("") +
|
||||
compiled.code +
|
||||
(options.sourceMapAnnotation
|
||||
? "\n//# sourceMappingURL=/maps/app.js.map"
|
||||
: "");
|
||||
let indexed = JSON.parse(compiled.map);
|
||||
for (const prefix of prefixes.toReversed()) {
|
||||
const offset = textPosition(prefix, prefix.length);
|
||||
indexed = {
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: offset.line - 1, column: offset.column },
|
||||
map: indexed,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
const map = prefixes.length === 0 ? compiled.map : JSON.stringify(indexed);
|
||||
const token = '"source-map proof"';
|
||||
const generatedOffset = compiled.code.indexOf(token);
|
||||
const generatedOffset = generated.indexOf(token);
|
||||
const originalOffset = original.indexOf(token);
|
||||
if (generatedOffset < 0 || originalOffset < 0)
|
||||
throw new Error("Compiler fixture marker missing");
|
||||
const position = textPosition(compiled.code, generatedOffset);
|
||||
const position = textPosition(generated, generatedOffset);
|
||||
const originalPosition = textPosition(original, originalOffset);
|
||||
let mapRequests = 0;
|
||||
const server = createServer((request, response) => {
|
||||
@@ -42,12 +63,12 @@ export async function startBrowserSourceMapSite() {
|
||||
if (request.url === "/app.js?build=7")
|
||||
return response
|
||||
.writeHead(200, { "Content-Type": "text/javascript" })
|
||||
.end(compiled.code);
|
||||
.end(generated);
|
||||
if (request.url?.startsWith("/maps/app.js.map")) {
|
||||
mapRequests += 1;
|
||||
return response
|
||||
.writeHead(200, { "Content-Type": "application/json" })
|
||||
.end(compiled.map);
|
||||
.end(map);
|
||||
}
|
||||
response.writeHead(404).end("Unselected");
|
||||
});
|
||||
@@ -58,8 +79,10 @@ export async function startBrowserSourceMapSite() {
|
||||
return {
|
||||
origin: `http://127.0.0.1:${address.port}`,
|
||||
original,
|
||||
generated: compiled.code,
|
||||
map: compiled.map,
|
||||
generated,
|
||||
map,
|
||||
leafCode: compiled.code,
|
||||
leafMap: compiled.map,
|
||||
position,
|
||||
originalPosition,
|
||||
originalOffset,
|
||||
|
||||
@@ -7,6 +7,13 @@ export async function startBrowserVerifierSite() {
|
||||
let port = 0;
|
||||
let sessionGeneration = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/slow-json") {
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.write('{"pending":');
|
||||
const timer = setTimeout(() => response.end("true}"), 350);
|
||||
response.once("close", () => clearTimeout(timer));
|
||||
return;
|
||||
}
|
||||
if (request.url === "/screenshot-noise") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(`<!doctype html><html><body style="margin:0"><canvas width="2048" height="1536"></canvas><script>
|
||||
@@ -27,6 +34,50 @@ export async function startBrowserVerifierSite() {
|
||||
response.end(sourceMap());
|
||||
return;
|
||||
}
|
||||
if (request.url === "/bad.js.map") {
|
||||
response.setHeader("content-type", "application/source-map+json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
names: [],
|
||||
sources: ["../src/bad.ts"],
|
||||
mappings: "AAAAD",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/expanded.js.map") {
|
||||
response.setHeader("content-type", "application/source-map+json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
names: [],
|
||||
sources: ["../src/" + "x".repeat(300000) + ".ts"],
|
||||
mappings: "AAAA" + ",CAAA".repeat(127),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/overflow.js.map") {
|
||||
response.setHeader("content-type", "application/source-map+json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: 0, column: Number.MAX_SAFE_INTEGER },
|
||||
map: {
|
||||
version: 3,
|
||||
names: [],
|
||||
sources: ["overflow.ts"],
|
||||
mappings: "CAAA",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/session-generation") {
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end(JSON.stringify({ generation: sessionGeneration }));
|
||||
@@ -37,6 +88,27 @@ export async function startBrowserVerifierSite() {
|
||||
response.end(browserScript(port));
|
||||
return;
|
||||
}
|
||||
if (request.url?.startsWith("/bad.js") === true) {
|
||||
response.setHeader("content-type", "text/javascript");
|
||||
response.end(
|
||||
"export const badMapProbe = true;\n//# sourceMappingURL=/bad.js.map",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url?.startsWith("/expanded.js") === true) {
|
||||
response.setHeader("content-type", "text/javascript");
|
||||
response.end(
|
||||
"export const expandedMapProbe = true;\n//# sourceMappingURL=/expanded.js.map",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url?.startsWith("/overflow.js") === true) {
|
||||
response.setHeader("content-type", "text/javascript");
|
||||
response.end(
|
||||
"export const overflowMapProbe = true;\n//# sourceMappingURL=/overflow.js.map",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url?.startsWith("/api") === true) {
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.setHeader(
|
||||
@@ -109,10 +181,18 @@ const browserScript = (port) => `
|
||||
},
|
||||
body: JSON.stringify({ token: "request-body-secret-value", active: true })
|
||||
});
|
||||
fetch("/slow-json", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pending: true })
|
||||
});
|
||||
const socket = new WebSocket("ws://127.0.0.1:${String(port)}/live?token=websocket-url-secret");
|
||||
socket.addEventListener("open", () => socket.send(JSON.stringify({ token: "websocket-secret-value" })));
|
||||
socket.addEventListener("message", () => socket.close());
|
||||
};
|
||||
void import("/bad.js");
|
||||
void import("/expanded.js");
|
||||
void import("/overflow.js");
|
||||
observe();
|
||||
setInterval(observe, 150);
|
||||
let observedGeneration = 0;
|
||||
|
||||
@@ -11,6 +11,7 @@ import WebSocket, { WebSocketServer } from "ws";
|
||||
*/
|
||||
export async function startPageCdpProxy(upstreamEndpoint) {
|
||||
const clients = new Set();
|
||||
const upstreams = new Set();
|
||||
const server = createServer(async (request, response) => {
|
||||
try {
|
||||
if (request.url !== "/json/version" && request.url !== "/json/list") {
|
||||
@@ -43,13 +44,18 @@ export async function startPageCdpProxy(upstreamEndpoint) {
|
||||
const upstream = new WebSocket(
|
||||
new URL(request.url, upstreamEndpoint).href.replace(/^http/, "ws"),
|
||||
);
|
||||
upstreams.add(upstream);
|
||||
const pending = [];
|
||||
clients.add(client);
|
||||
const close = () => {
|
||||
const closeClient = () => {
|
||||
clients.delete(client);
|
||||
if (client.readyState < WebSocket.CLOSING) client.close();
|
||||
if (upstream.readyState < WebSocket.CLOSING) upstream.close();
|
||||
};
|
||||
const finishUpstream = () => {
|
||||
upstreams.delete(upstream);
|
||||
closeClient();
|
||||
};
|
||||
client.on("message", (data, binary) => {
|
||||
if (upstream.readyState === WebSocket.OPEN)
|
||||
upstream.send(data, { binary });
|
||||
@@ -63,10 +69,10 @@ export async function startPageCdpProxy(upstreamEndpoint) {
|
||||
pending.length = 0;
|
||||
webSockets.emit("connection", client, request);
|
||||
});
|
||||
client.on("close", close);
|
||||
client.on("error", close);
|
||||
upstream.on("close", close);
|
||||
upstream.on("error", close);
|
||||
client.on("close", closeClient);
|
||||
client.on("error", closeClient);
|
||||
upstream.on("close", finishUpstream);
|
||||
upstream.on("error", closeClient);
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -81,10 +87,20 @@ export async function startPageCdpProxy(upstreamEndpoint) {
|
||||
endpoint,
|
||||
disconnectClients() {
|
||||
for (const client of clients) client.terminate();
|
||||
for (const upstream of upstreams) upstream.terminate();
|
||||
},
|
||||
async close() {
|
||||
for (const client of clients) client.terminate();
|
||||
webSockets.close();
|
||||
const activeClients = [...clients];
|
||||
const activeUpstreams = [...upstreams];
|
||||
await Promise.all([
|
||||
terminateSockets(activeClients),
|
||||
terminateSockets(activeUpstreams),
|
||||
]);
|
||||
await new Promise((resolve, reject) =>
|
||||
webSockets.close((error) =>
|
||||
error === undefined ? resolve() : reject(error),
|
||||
),
|
||||
);
|
||||
await new Promise((resolve, reject) =>
|
||||
server.close((error) =>
|
||||
error === undefined ? resolve() : reject(error),
|
||||
@@ -94,6 +110,19 @@ export async function startPageCdpProxy(upstreamEndpoint) {
|
||||
};
|
||||
}
|
||||
|
||||
async function terminateSockets(sockets) {
|
||||
const closing = [...sockets]
|
||||
.filter((socket) => socket.readyState !== WebSocket.CLOSED)
|
||||
.map(
|
||||
(socket) =>
|
||||
new Promise((resolve) => {
|
||||
socket.once("close", resolve);
|
||||
socket.terminate();
|
||||
}),
|
||||
);
|
||||
await Promise.all(closing);
|
||||
}
|
||||
|
||||
async function pageScopedVersion(upstreamEndpoint, version) {
|
||||
const targets = await upstreamJson(upstreamEndpoint, "/json/list");
|
||||
const page = targets.find(
|
||||
|
||||
@@ -17,9 +17,9 @@ for (const argument of arguments_)
|
||||
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// The build-generated catalog is a portable package artifact that is not
|
||||
// committed to git. Keep its canonical provider metadata independent of the
|
||||
// machine running the generator. It is regenerated by `npm run build`.
|
||||
// This build-generated source supports schema and provider conformance tests;
|
||||
// production MCP tools are registered from their canonical contracts directly.
|
||||
// Keep the test metadata portable and regenerate it with `npm run build`.
|
||||
const CATALOG_PLATFORM = "darwin";
|
||||
const sessionToolNames = new Set([
|
||||
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
||||
|
||||
@@ -14,13 +14,23 @@ const environment = () => ({
|
||||
});
|
||||
|
||||
/** Execute the compiled CLI with production providers and no test adapters. */
|
||||
export async function artifactCli(command, target, arguments_ = []) {
|
||||
return (await artifactCliEvidence(command, target, arguments_))
|
||||
export async function artifactCli(
|
||||
command,
|
||||
target,
|
||||
arguments_ = [],
|
||||
options = {},
|
||||
) {
|
||||
return (await artifactCliEvidence(command, target, arguments_, options))
|
||||
.normalized_result;
|
||||
}
|
||||
|
||||
/** Execute the compiled CLI and return its complete Evidence record. */
|
||||
export async function artifactCliEvidence(command, target, arguments_ = []) {
|
||||
export async function artifactCliEvidence(
|
||||
command,
|
||||
target,
|
||||
arguments_ = [],
|
||||
{ timeoutMs = 60000 } = {},
|
||||
) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
@@ -30,7 +40,7 @@ export async function artifactCliEvidence(command, target, arguments_ = []) {
|
||||
...arguments_,
|
||||
"--json",
|
||||
],
|
||||
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
|
||||
{ env: environment(), timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.equal(evidence.error, undefined, stdout);
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { createServer } from "node:http";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { artifactCliEvidence, artifactMcpResult } from "./artifact-e2e.mjs";
|
||||
|
||||
const ANNOTATION_BYTES = 2 * 1024 * 1024;
|
||||
const SCRIPT_COUNT = 5;
|
||||
|
||||
/** Verify real Chrome scriptParsed metadata is bounded before public capture retention. */
|
||||
export async function verifyBrowserCaptureMetadataBudget(endpoint) {
|
||||
const marker = "rea-capture-metadata-budget";
|
||||
let page = "";
|
||||
let pageReady = false;
|
||||
let fixtureOrigin = "";
|
||||
const server = createServer(
|
||||
{ maxHeaderSize: 3 * 1024 * 1024 },
|
||||
(request, response) => {
|
||||
if (request.url?.startsWith("/maps/") === true) {
|
||||
response.writeHead(200, {
|
||||
"content-type": "application/source-map+json",
|
||||
});
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
names: [],
|
||||
sources: [`${fixtureOrigin}/source.ts`],
|
||||
sourcesContent: ["export const retainedSource = true;"],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/capture-metadata-ready") {
|
||||
pageReady = true;
|
||||
response.writeHead(204).end();
|
||||
return;
|
||||
}
|
||||
if (request.url !== "/capture-metadata-budget") {
|
||||
response.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
response.end(page);
|
||||
},
|
||||
);
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
let targetId;
|
||||
try {
|
||||
const address = server.address();
|
||||
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)}`;
|
||||
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>`,
|
||||
).join("");
|
||||
page = `<!doctype html><meta charset="utf-8">${annotations}<script>fetch("/capture-metadata-ready")</script>`;
|
||||
const fixtureUrl = `${fixtureOrigin}/capture-metadata-budget`;
|
||||
const created = await fetch(
|
||||
`${endpoint}/json/new?${encodeURIComponent(fixtureUrl)}`,
|
||||
{ method: "PUT" },
|
||||
);
|
||||
if (!created.ok)
|
||||
throw new Error(`Chrome target creation failed: HTTP ${created.status}`);
|
||||
const target = await created.json();
|
||||
if (typeof target.id !== "string" || target.id.length === 0)
|
||||
throw new Error("Chrome target creation returned no target ID");
|
||||
targetId = target.id;
|
||||
|
||||
const deadline = Date.now() + 15_000;
|
||||
let served = false;
|
||||
while (!served && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
served = pageReady;
|
||||
}
|
||||
if (!served) throw new Error("Chrome did not request the metadata fixture");
|
||||
|
||||
const evidence = await artifactCliEvidence("inspect-web-page", endpoint, [
|
||||
targetId,
|
||||
"--allowed-origins",
|
||||
fixtureOrigin,
|
||||
"--observation-ms",
|
||||
"0",
|
||||
]);
|
||||
const cliFacts = assertMetadataBudget(evidence.normalized_result);
|
||||
const bundle = await artifactCliEvidence("analyze-web-bundle", endpoint, [
|
||||
targetId,
|
||||
"--allowed-origins",
|
||||
fixtureOrigin,
|
||||
"--observation-ms",
|
||||
"0",
|
||||
"--fetch-source-maps",
|
||||
]);
|
||||
assertBundleMetadataOmissions(
|
||||
bundle.normalized_result,
|
||||
cliFacts.omittedMaps,
|
||||
);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
|
||||
env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "browser-metadata-e2e", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
assertMetadataBudget(
|
||||
await artifactMcpResult(client, "inspect_web_page", {
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [fixtureOrigin],
|
||||
target_id: targetId,
|
||||
observation_ms: 0,
|
||||
}),
|
||||
);
|
||||
assertBundleMetadataOmissions(
|
||||
await artifactMcpResult(client, "analyze_web_bundle", {
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [fixtureOrigin],
|
||||
target_id: targetId,
|
||||
observation_ms: 0,
|
||||
fetch_source_maps: true,
|
||||
}),
|
||||
cliFacts.omittedMaps,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
return { ...cliFacts, public_cli: true, public_stdio_mcp: true };
|
||||
} finally {
|
||||
try {
|
||||
if (targetId !== undefined) {
|
||||
const closed = await fetch(
|
||||
`${endpoint}/json/close/${encodeURIComponent(targetId)}`,
|
||||
);
|
||||
if (!closed.ok)
|
||||
throw new Error(
|
||||
`Chrome fixture target cleanup failed: HTTP ${closed.status}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve, reject) =>
|
||||
server.close((error) =>
|
||||
error === undefined ? resolve() : reject(error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertMetadataBudget(result) {
|
||||
const scripts = result.scripts.items.filter(
|
||||
(script) =>
|
||||
script.url.includes("capture-metadata-budget") &&
|
||||
script.length >= ANNOTATION_BYTES,
|
||||
);
|
||||
const retainedMaps = scripts.filter(
|
||||
(script) => script.source_map_url !== null,
|
||||
).length;
|
||||
const omittedMaps = SCRIPT_COUNT - retainedMaps;
|
||||
const exclusion = result.completeness.excluded.find(
|
||||
(item) =>
|
||||
item.section === "source_maps" &&
|
||||
item.reason === "resource_budget_exhausted",
|
||||
);
|
||||
if (
|
||||
scripts.length !== SCRIPT_COUNT ||
|
||||
retainedMaps <= 0 ||
|
||||
retainedMaps >= SCRIPT_COUNT ||
|
||||
exclusion?.count !== omittedMaps ||
|
||||
!result.completeness.truncated_sections.includes("source_maps")
|
||||
)
|
||||
throw new Error(
|
||||
`Real Chrome source-map metadata admission was not reported accurately: scripts=${String(scripts.length)}, retained_maps=${String(retainedMaps)}, excluded=${String(exclusion?.count)}`,
|
||||
);
|
||||
return {
|
||||
scripts: scripts.length,
|
||||
retainedMaps,
|
||||
omittedMaps,
|
||||
declaredBytes: ANNOTATION_BYTES * SCRIPT_COUNT,
|
||||
};
|
||||
}
|
||||
|
||||
function assertBundleMetadataOmissions(result, omittedMaps) {
|
||||
const unknown = result.unknowns.find(
|
||||
({ dimension, reason }) =>
|
||||
dimension === "source_maps" &&
|
||||
reason.includes(String(omittedMaps)) &&
|
||||
reason.includes("source-map declarations"),
|
||||
);
|
||||
if (
|
||||
result.observations.source_maps.status !== "included" ||
|
||||
result.observations.source_maps.items.length === 0 ||
|
||||
result.completeness.status !== "partial" ||
|
||||
unknown === undefined
|
||||
)
|
||||
throw new Error(
|
||||
"Bundle analysis lost capture omissions despite valid retained maps",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { TraceMap, eachMapping } from "@jridgewell/trace-mapping";
|
||||
import { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js";
|
||||
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
|
||||
import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.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";
|
||||
|
||||
const cases = [
|
||||
{ name: "same-line nested column control", prefixes: ["\n\n ", " "] },
|
||||
{ name: "zero outer column control", prefixes: ["\n\n", "\n "] },
|
||||
{ name: "nested line resets outer column", prefixes: ["\n\n ", "\n"] },
|
||||
{
|
||||
name: "nested line retains its own column",
|
||||
prefixes: ["\n\n ", "\n "],
|
||||
},
|
||||
];
|
||||
|
||||
/** Verify compiler-produced nested maps through an actual owned Chrome and public CLI. */
|
||||
export async function verifyBrowserNestedSourceMaps(
|
||||
executable,
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
) {
|
||||
const results = [];
|
||||
for (const scenario of cases) {
|
||||
try {
|
||||
const proof = await verifyCase(executable, entrypoint, scenario);
|
||||
results.push({ name: scenario.name, status: "pass", ...proof });
|
||||
} catch (cause) {
|
||||
results.push({
|
||||
name: scenario.name,
|
||||
status: "fail",
|
||||
phase: cause.qualificationPhase ?? "native-producer-admission",
|
||||
proof: cause.qualificationReceipt ?? null,
|
||||
error: String(cause),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Preserve every affected owner and control even when BEFORE fails.
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ nested_source_map_cases: results })}\n`,
|
||||
);
|
||||
assert.equal(results.filter(({ status }) => status === "fail").length, 0);
|
||||
return { cases: results, actual_chrome: true, public_cli: true };
|
||||
}
|
||||
|
||||
async function verifyCase(executable, entrypoint, scenario) {
|
||||
const site = await startBrowserSourceMapSite({
|
||||
prefixes: scenario.prefixes,
|
||||
sourceMapAnnotation: true,
|
||||
sourcefile: "../src/nested-fixture.ts",
|
||||
});
|
||||
let fixture;
|
||||
let connection;
|
||||
let primaryError;
|
||||
let stderr = "";
|
||||
let phase = "native-map-admission";
|
||||
let browserVersion;
|
||||
let expected;
|
||||
let observed;
|
||||
try {
|
||||
const mapUrl = `${site.origin}/maps/app.js.map`;
|
||||
// This independent existing decoder already composes valid nested offsets.
|
||||
// It is a before-product admission control, not the expected browser oracle.
|
||||
const point = traceSourceMap(site.map, mapUrl, site.position);
|
||||
assert.equal(point.matches.length, 1);
|
||||
assert.equal(
|
||||
point.matches[0].original_position.line,
|
||||
site.originalPosition.line,
|
||||
);
|
||||
assert.equal(
|
||||
point.matches[0].original_position.column,
|
||||
site.originalPosition.column,
|
||||
);
|
||||
expected = compilerMappingsAtPhysicalPositions(site, mapUrl);
|
||||
assert.ok(
|
||||
expected.length > 0,
|
||||
"Native compiler leaf must carry point mappings",
|
||||
);
|
||||
|
||||
phase = "owned-browser-startup";
|
||||
fixture = await startRuntimeFixtureBrowser(executable, site.origin);
|
||||
fixture.browser.stderr.on("data", (chunk) => {
|
||||
if (stderr.length < 65536) stderr += chunk;
|
||||
});
|
||||
const port = await waitForBrowserDevtoolsPort({
|
||||
child: fixture.browser,
|
||||
executable,
|
||||
activePortPath: join(fixture.profile, "DevToolsActivePort"),
|
||||
stderr: () => stderr,
|
||||
timeoutMs: 20_000,
|
||||
});
|
||||
const endpoint = `http://127.0.0.1:${port}`;
|
||||
const provider = new CdpBrowserProvider();
|
||||
const targets = await provider.listTargets({
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [site.origin],
|
||||
});
|
||||
if (!targets.ok) throw targets.error;
|
||||
const target = targets.value.targets[0];
|
||||
assert.ok(target, "Owned fixture target missing");
|
||||
const pages = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
const page = pages.find(({ id }) => id === target.target_id);
|
||||
assert.ok(page, "Owned CDP fixture page missing");
|
||||
connection = await CdpConnection.connect(
|
||||
page.webSocketDebuggerUrl,
|
||||
"inspect_web_page",
|
||||
);
|
||||
await ready(connection);
|
||||
browserVersion = await connection.send("Browser.getVersion");
|
||||
phase = "public-cli";
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"analyze-web-bundle",
|
||||
endpoint,
|
||||
target.target_id,
|
||||
"--allowed-origins",
|
||||
site.origin,
|
||||
"--fetch-source-maps",
|
||||
"--observation-ms",
|
||||
"0",
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
env: { ...process.env, REA_LOG_LEVEL: "silent" },
|
||||
timeout: 45_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.operation, "analyze_web_bundle");
|
||||
const maps = evidence.normalized_result.observations.source_maps;
|
||||
const item = maps.items.find(({ artifact }) => artifact?.text === site.map);
|
||||
assert.ok(
|
||||
item,
|
||||
"Public browser analysis omitted the exact compiler-derived map",
|
||||
);
|
||||
phase = "public-map-assertions";
|
||||
observed = item?.mappings;
|
||||
assert.equal(item.status, "included");
|
||||
assert.deepEqual(item.mappings, expected);
|
||||
assert.equal(item.artifact.sha256, sha(site.map));
|
||||
assert.equal(item.artifact.bytes, Buffer.byteLength(site.map));
|
||||
assert.equal(item.original_sources[0].artifact.sha256, sha(site.original));
|
||||
assert.ok(
|
||||
site.mapRequests() > 0,
|
||||
"Actual browser workflow did not fetch the declared map",
|
||||
);
|
||||
return {
|
||||
compiler: site.compiler,
|
||||
browser: browserVersion.product,
|
||||
generated_sha256: sha(site.generated),
|
||||
source_map_sha256: sha(site.map),
|
||||
original_sha256: sha(site.original),
|
||||
mappings: expected.length,
|
||||
native_decoder_control: true,
|
||||
};
|
||||
} catch (cause) {
|
||||
const failure = new Error(`Nested source-map ${phase}: ${String(cause)}`, {
|
||||
cause,
|
||||
});
|
||||
failure.qualificationPhase = phase;
|
||||
failure.qualificationReceipt = {
|
||||
compiler: site.compiler,
|
||||
browser: browserVersion?.product ?? null,
|
||||
generated_sha256: sha(site.generated),
|
||||
leaf_map_sha256: sha(site.leafMap),
|
||||
source_map_sha256: sha(site.map),
|
||||
original_sha256: sha(site.original),
|
||||
expected_first_mapping: expected?.[0] ?? null,
|
||||
observed_first_mapping: observed?.[0] ?? null,
|
||||
};
|
||||
primaryError = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
await closeRuntimeFixtureResources(
|
||||
[() => connection?.close(), () => fixture?.close(), () => site.close()],
|
||||
primaryError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function compilerMappingsAtPhysicalPositions(site, mapUrl) {
|
||||
const expected = [];
|
||||
const prefixLength = site.generated.indexOf(site.leafCode);
|
||||
assert.ok(prefixLength >= 0);
|
||||
eachMapping(new TraceMap(site.leafMap, mapUrl), (mapping) => {
|
||||
if (
|
||||
mapping.source === null ||
|
||||
mapping.originalLine === null ||
|
||||
mapping.originalColumn === null
|
||||
)
|
||||
return;
|
||||
const leafLines = site.leafCode.split("\n");
|
||||
const before = leafLines.slice(0, mapping.generatedLine - 1).join("\n");
|
||||
const leafOffset =
|
||||
before.length +
|
||||
(mapping.generatedLine > 1 ? 1 : 0) +
|
||||
mapping.generatedColumn;
|
||||
const physical = site.generated
|
||||
.slice(0, prefixLength + leafOffset)
|
||||
.split("\n");
|
||||
expected.push({
|
||||
generated_line: physical.length,
|
||||
generated_column: physical.at(-1).length,
|
||||
source: mapping.source,
|
||||
original_line: mapping.originalLine,
|
||||
original_column: mapping.originalColumn,
|
||||
name: mapping.name ?? null,
|
||||
});
|
||||
});
|
||||
return expected;
|
||||
}
|
||||
|
||||
async function ready(connection) {
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
const result = await connection.send("Runtime.evaluate", {
|
||||
expression: 'document.body.dataset.ready === "source-map proof"',
|
||||
returnByValue: true,
|
||||
});
|
||||
if (result.result.value === true) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
}
|
||||
throw new Error("Owned compiler fixture did not execute in actual Chrome");
|
||||
}
|
||||
const sha = (text) => createHash("sha256").update(text).digest("hex");
|
||||
@@ -150,7 +150,7 @@ export function assertBundleAnalysis(result) {
|
||||
)
|
||||
throw new Error("Real Chrome static bundle findings were missing");
|
||||
if (
|
||||
result.observations.source_maps.status !== "included" ||
|
||||
result.observations.source_maps.status !== "partial" ||
|
||||
!result.observations.source_maps.items.some(
|
||||
({ status, original_sources, original_module_edges, mappings }) =>
|
||||
status === "included" &&
|
||||
@@ -166,6 +166,41 @@ export function assertBundleAnalysis(result) {
|
||||
throw new Error(
|
||||
`Real Chrome approved source-map reconstruction was missing: ${JSON.stringify(sourceMapSummary(result.observations.source_maps))}`,
|
||||
);
|
||||
if (
|
||||
!result.observations.source_maps.items.some(
|
||||
({ declared_url, status, artifact, mappings }) =>
|
||||
declared_url.endsWith("/bad.js.map") &&
|
||||
status === "invalid" &&
|
||||
artifact === null &&
|
||||
mappings.length === 0,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Real Chrome malformed-map isolation was missing: ${JSON.stringify(sourceMapSummary(result.observations.source_maps))}`,
|
||||
);
|
||||
if (
|
||||
!result.observations.source_maps.items.some(
|
||||
({ declared_url, status, limitation, mappings }) =>
|
||||
declared_url.endsWith("/expanded.js.map") &&
|
||||
status === "fetch_failed" &&
|
||||
limitation.includes("Expanded source-map evidence") &&
|
||||
mappings.length === 0,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Real Chrome expanded-map omission was missing: ${JSON.stringify(sourceMapSummary(result.observations.source_maps))}`,
|
||||
);
|
||||
if (
|
||||
!result.observations.source_maps.items.some(
|
||||
({ declared_url, status, mappings }) =>
|
||||
declared_url.endsWith("/overflow.js.map") &&
|
||||
status === "invalid" &&
|
||||
mappings.length === 0,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Real Chrome overflowing-map isolation was missing: ${JSON.stringify(sourceMapSummary(result.observations.source_maps))}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Assert explicitly selected console text and JSON/WebSocket shape capture. */
|
||||
@@ -182,6 +217,19 @@ export function assertSensitiveShapes(result) {
|
||||
);
|
||||
if (request === undefined)
|
||||
throw new Error("Real Chrome JSON request/response shapes were missing");
|
||||
if (
|
||||
!result.network.requests.some(
|
||||
(item) =>
|
||||
item.url.endsWith("/slow-json") &&
|
||||
item.body_shapes.status === "partial" &&
|
||||
item.body_shapes.request !== null &&
|
||||
item.body_shapes.response === null,
|
||||
) ||
|
||||
!result.completeness.unavailable_sections.includes("json_body_shapes")
|
||||
)
|
||||
throw new Error(
|
||||
"Real Chrome unfinished JSON bodies were not reported as unavailable",
|
||||
);
|
||||
const consoleText = result.console.events.flatMap(
|
||||
(event) => event.text_capture.values,
|
||||
);
|
||||
|
||||
+29
-37
@@ -1,28 +1,15 @@
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
const README_PATHS = [
|
||||
"README.md",
|
||||
"README_zh.md",
|
||||
"README_ja.md",
|
||||
"README_ko.md",
|
||||
"README_ar.md",
|
||||
];
|
||||
const tableCounts = (content, path, expectedCounts) => {
|
||||
const lines = content.split(/\r?\n/u);
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (!/^\|\s*-/u.test(line)) continue;
|
||||
const counts = [];
|
||||
for (const row of lines.slice(index + 1)) {
|
||||
if (!row.trim().startsWith("|")) break;
|
||||
const count = Number(row.split("|")[2]?.trim());
|
||||
if (!Number.isInteger(count)) break;
|
||||
counts.push(count);
|
||||
}
|
||||
if (counts.length === expectedCounts.length) return counts;
|
||||
}
|
||||
throw new Error(`Missing tool-family inventory table in ${path}`);
|
||||
};
|
||||
const readmeLinkTargets = (content) =>
|
||||
[...content.matchAll(/\]\(([^)\s]+)\)/gu)]
|
||||
.map((match) => match[1] ?? "")
|
||||
.filter(
|
||||
(target) =>
|
||||
!target.startsWith("#") &&
|
||||
!/^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(target),
|
||||
)
|
||||
.sort();
|
||||
|
||||
const requireText = (issues, path, content, expected) => {
|
||||
if (!content.includes(expected)) issues.push(`${path}: missing ${expected}`);
|
||||
@@ -71,32 +58,37 @@ export const documentationFactIssues = async (root, catalog) => {
|
||||
const issues = await skillReferenceIssues(
|
||||
join(root, "skills/reverse-engineer-anything"),
|
||||
);
|
||||
const expectedCounts = catalog.tools.families.map(({ count }) => count);
|
||||
for (const path of README_PATHS) {
|
||||
const readmePaths = (await readdir(root))
|
||||
.filter((path) => /^README(?:_[A-Za-z0-9-]+)?\.md$/u.test(path))
|
||||
.sort();
|
||||
const readmes = new Map();
|
||||
for (const path of readmePaths) {
|
||||
const content = await readFile(join(root, path), "utf8");
|
||||
readmes.set(path, content);
|
||||
requireText(issues, path, content, "MCP-tool_catalog");
|
||||
if (path === "README.md") {
|
||||
requireText(issues, path, content, "(docs/installation.md");
|
||||
requireText(
|
||||
issues,
|
||||
path,
|
||||
content,
|
||||
"(docs/installation.md#supported-agents)",
|
||||
);
|
||||
requireText(issues, path, content, "(docs/installation.md#mcp-registry)");
|
||||
requireText(
|
||||
issues,
|
||||
path,
|
||||
content,
|
||||
"(docs/mcp-contracts.md#generated-catalog)",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const client of catalog.setup_clients)
|
||||
requireText(issues, path, content, client.display_name);
|
||||
try {
|
||||
const actualCounts = tableCounts(content, path, expectedCounts);
|
||||
if (JSON.stringify(actualCounts) !== JSON.stringify(expectedCounts))
|
||||
issues.push(
|
||||
`${path}: tool family counts ${JSON.stringify(actualCounts)} do not match ${JSON.stringify(expectedCounts)}`,
|
||||
);
|
||||
} catch (cause) {
|
||||
issues.push(cause instanceof Error ? cause.message : String(cause));
|
||||
}
|
||||
}
|
||||
const canonicalLinks = readmeLinkTargets(readmes.get("README.md") ?? "");
|
||||
for (const path of readmePaths.filter((path) => path !== "README.md"))
|
||||
if (
|
||||
JSON.stringify(readmeLinkTargets(readmes.get(path) ?? "")) !==
|
||||
JSON.stringify(canonicalLinks)
|
||||
)
|
||||
issues.push(`${path}: documentation links differ from README.md`);
|
||||
|
||||
const installationPath = "docs/installation.md";
|
||||
const installation = await readFile(join(root, installationPath), "utf8");
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { createAnalysisProfile } from "../../dist/domain/analysisProfile.js";
|
||||
import {
|
||||
createAnalysisSnapshotEntry,
|
||||
parseAnalysisSnapshot,
|
||||
serializeAnalysisSnapshot,
|
||||
snapshotBinding,
|
||||
snapshotTarget,
|
||||
} from "../../dist/domain/analysisSnapshot.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
|
||||
import { requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
|
||||
export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
client,
|
||||
{ target, procedure, omittedEdge, entrypoint, env },
|
||||
) {
|
||||
const bundle = requireMcpResult(
|
||||
await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
}),
|
||||
"get_evidence_bundle",
|
||||
);
|
||||
const observed = bundle.records.find(
|
||||
(record) =>
|
||||
record.operation === "analyze_function" &&
|
||||
record.parameters.procedure === procedure,
|
||||
);
|
||||
assert.ok(observed);
|
||||
const { function_references: coverage, ...legacyParameters } =
|
||||
observed.analysis_profile.parameters;
|
||||
assert.equal(
|
||||
typeof coverage,
|
||||
"string",
|
||||
"Reference semantics must participate in snapshot identity",
|
||||
);
|
||||
const legacyProfile = createAnalysisProfile(
|
||||
observed.provider,
|
||||
legacyParameters,
|
||||
);
|
||||
assert.notEqual(legacyProfile.digest, observed.analysis_profile.digest);
|
||||
const parsed = await parseBinaryTarget(
|
||||
target.path,
|
||||
process.cwd(),
|
||||
process.arch,
|
||||
);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
const nativeTarget = parsed.value;
|
||||
const result = structuredClone(observed.normalized_result);
|
||||
// Reconstruct the observed pre-fix omission; all other data
|
||||
// comes from the real provider and the file has a fully valid Evidence binding.
|
||||
result.incoming_references = result.incoming_references.filter(
|
||||
(edge) =>
|
||||
edge.source_address !== omittedEdge.source_address ||
|
||||
edge.target_address !== omittedEdge.target_address,
|
||||
);
|
||||
assert.equal(
|
||||
result.incoming_references.length,
|
||||
observed.normalized_result.incoming_references.length - 1,
|
||||
);
|
||||
const evidence = createEvidence(nativeTarget, observed.provider, {
|
||||
operation: observed.operation,
|
||||
parameters: observed.parameters,
|
||||
result,
|
||||
rawResult: result,
|
||||
analysisProfile: legacyProfile,
|
||||
limitations: observed.limitations,
|
||||
locations: observed.locations,
|
||||
});
|
||||
const binding = snapshotBinding(legacyProfile);
|
||||
const identity = snapshotTarget(nativeTarget);
|
||||
const snapshot = parseAnalysisSnapshot({
|
||||
target: identity,
|
||||
binding,
|
||||
entries: [
|
||||
createAnalysisSnapshotEntry({
|
||||
target: identity,
|
||||
binding,
|
||||
operation: observed.operation,
|
||||
parameters: observed.parameters,
|
||||
execution: {
|
||||
result,
|
||||
rawResult: result,
|
||||
provider: observed.provider,
|
||||
analysisProfile: legacyProfile,
|
||||
limitations: observed.limitations,
|
||||
locations: observed.locations,
|
||||
subject: nativeTarget,
|
||||
},
|
||||
}),
|
||||
],
|
||||
evidence_bundle: createEvidenceBundle([evidence]),
|
||||
});
|
||||
assert.equal(
|
||||
snapshot.entries.length,
|
||||
1,
|
||||
"The legacy query must remain valid and replayable under its own profile",
|
||||
);
|
||||
const path = join(env.TMPDIR, `legacy-reference-${randomUUID()}.json`);
|
||||
await writeFile(path, serializeAnalysisSnapshot(snapshot), { flag: "wx" });
|
||||
try {
|
||||
const session = requireMcpResult(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }),
|
||||
"binary_session",
|
||||
);
|
||||
const reply = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: {
|
||||
path: target.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: path,
|
||||
},
|
||||
});
|
||||
assert.equal(reply.isError, true);
|
||||
const mcpRejection = reply.structuredContent.error;
|
||||
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
|
||||
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
|
||||
assert.match(
|
||||
mcpRejection.remediation.action,
|
||||
/without this snapshot.*fresh snapshot/u,
|
||||
);
|
||||
assert.deepEqual(
|
||||
requireMcpResult(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }),
|
||||
"binary_session",
|
||||
),
|
||||
session,
|
||||
"A rejected snapshot must preserve the selected session",
|
||||
);
|
||||
const retained = requireMcpResult(
|
||||
await client.callTool({
|
||||
name: "analyze_function",
|
||||
arguments: { procedure },
|
||||
}),
|
||||
"analyze_function",
|
||||
);
|
||||
assert.ok(
|
||||
retained.incoming_references.some(
|
||||
(edge) =>
|
||||
edge.source_address === omittedEdge.source_address &&
|
||||
edge.target_address === omittedEdge.target_address,
|
||||
),
|
||||
"A rejected legacy snapshot must not replace complete live analysis",
|
||||
);
|
||||
await assert.rejects(
|
||||
promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
target.path,
|
||||
procedure,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--snapshot",
|
||||
path,
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
),
|
||||
(error) => {
|
||||
assert.equal(error.code, 1);
|
||||
const rejection = JSON.parse(error.stdout);
|
||||
assert.equal(rejection.code, "evidence_integrity_mismatch");
|
||||
assert.equal(rejection.details.reason, mcpRejection.details.reason);
|
||||
assert.equal(
|
||||
rejection.remediation.action,
|
||||
mcpRejection.remediation.action,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await rm(path);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -8,6 +11,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
|
||||
import { nativeValueTraceSchema } from "../../dist/domain/native/nativeValueTrace.js";
|
||||
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
|
||||
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
@@ -72,6 +76,33 @@ export async function verifyNativeValueE2e(
|
||||
procedure,
|
||||
globalAddress,
|
||||
{ entrypoint = defaultEntrypoint } = {},
|
||||
) {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-native-values-e2e-"));
|
||||
const runtime = join(
|
||||
workspace,
|
||||
`runtime${process.platform === "win32" ? "" : " with spaces"}-${"x".repeat(100)}`,
|
||||
);
|
||||
await mkdir(runtime);
|
||||
const env = { ...environment(), TMPDIR: runtime };
|
||||
try {
|
||||
return await verifyNativeValueAdapters(
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
entrypoint,
|
||||
env,
|
||||
);
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyNativeValueAdapters(
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
entrypoint,
|
||||
env,
|
||||
) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
@@ -90,7 +121,7 @@ export async function verifyNativeValueE2e(
|
||||
"5000",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment(), timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.operation, "trace_native_values");
|
||||
@@ -114,16 +145,17 @@ export async function verifyNativeValueE2e(
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: environment(),
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "native-values-real-e2e", version: "1" });
|
||||
let boundaries;
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: target.path, provider: "ghidra" },
|
||||
arguments: { path: target.path, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
@@ -168,14 +200,11 @@ export async function verifyNativeValueE2e(
|
||||
analyzed.structuredContent?.evidence,
|
||||
);
|
||||
assert.equal(functionEvidence.operation, "analyze_function");
|
||||
assert.equal(functionEvidence.provider.id, "rea-workflow");
|
||||
assert.equal(functionEvidence.provider.id, "ghidra");
|
||||
const functionProfile = analysisProfileSchema.parse(
|
||||
functionEvidence.analysis_profile,
|
||||
);
|
||||
assert.deepEqual(
|
||||
functionProfile.parameters.upstream_analysis_profile,
|
||||
upstreamProfile,
|
||||
);
|
||||
assert.deepEqual(functionProfile, upstreamProfile);
|
||||
assert.deepEqual(
|
||||
functionEvidence.normalized_result,
|
||||
analyzed.structuredContent?.result,
|
||||
@@ -219,6 +248,12 @@ export async function verifyNativeValueE2e(
|
||||
evidence,
|
||||
);
|
||||
}
|
||||
boundaries = await verifyGhidraBoundaries(client, {
|
||||
target,
|
||||
procedure,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
const closed = await client.callTool({
|
||||
name: "close_binary",
|
||||
arguments: {},
|
||||
@@ -229,6 +264,13 @@ export async function verifyNativeValueE2e(
|
||||
arguments: {},
|
||||
});
|
||||
assert.equal(status.structuredContent?.result?.open, false);
|
||||
assert.deepEqual(
|
||||
(await readdir(env.TMPDIR)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
),
|
||||
[],
|
||||
"Ghidra left runtime files after close",
|
||||
);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
@@ -239,6 +281,7 @@ export async function verifyNativeValueE2e(
|
||||
stdio_mcp: true,
|
||||
inline_evidence: true,
|
||||
direct_function_comparison: true,
|
||||
boundaries,
|
||||
nodes: cliGraph.total_nodes,
|
||||
edges: cliGraph.total_edges,
|
||||
decompilations: cliGraph.decompilations,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Exercise segmented-address truncation through real CLI/MCP reads and edits. */
|
||||
export async function verifyGhidraAddressBoundaries(client, entry, cli) {
|
||||
const query = async (name, args) => {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(reply.isError, true, JSON.stringify(reply));
|
||||
return reply.structuredContent.result;
|
||||
};
|
||||
const before = await query("analyze_function", { procedure: entry });
|
||||
// The segmented parser used to discard the high 32 bits and alias this entry.
|
||||
const overflow = (0x100000000n + BigInt(entry)).toString(16);
|
||||
const representations = [
|
||||
`0x${overflow}`,
|
||||
`0X${overflow}`,
|
||||
overflow,
|
||||
`ram:0x${overflow}`,
|
||||
`%72am:0x${overflow}`,
|
||||
];
|
||||
for (const address of representations) {
|
||||
for (const [name, args] of [
|
||||
["read_bytes", { address, length: 1 }],
|
||||
["inspect_native_instruction", { address }],
|
||||
["resolve_containing_procedure", { address }],
|
||||
["procedure_info", { procedure: address }],
|
||||
[
|
||||
"annotate_native_function",
|
||||
{
|
||||
procedure: address,
|
||||
name: "MUST_NOT_ALIAS",
|
||||
comment: "MUST NOT WRITE",
|
||||
},
|
||||
],
|
||||
]) {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(reply.isError, true, `${name} accepted ${address}`);
|
||||
const error = reply.structuredContent?.error;
|
||||
assert.equal(error?.code, "invalid_request", JSON.stringify(reply));
|
||||
assert.equal(error.details.operation, name);
|
||||
const issues = JSON.stringify(error.details.issues);
|
||||
assert.match(issues, /cannot be represented without truncation/u);
|
||||
assert.ok(issues.includes(address), issues);
|
||||
assert.equal(reply.structuredContent.evidence, undefined);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
await query("analyze_function", { procedure: entry }),
|
||||
before,
|
||||
"Rejected oversized annotations changed the aliased function",
|
||||
);
|
||||
// Leading zeros and an encoded explicit space preserve the requested offset.
|
||||
const padded = `%72am:0x0000${BigInt(entry).toString(16)}`;
|
||||
assert.deepEqual(
|
||||
await query("read_bytes", { address: padded, length: 1 }),
|
||||
await query("read_bytes", { address: entry, length: 1 }),
|
||||
);
|
||||
for (const [command, flags, operation] of [
|
||||
["read-bytes", ["--length", "1"], "read_bytes"],
|
||||
[
|
||||
"annotate-native-function",
|
||||
["--name", "MUST_NOT_ALIAS"],
|
||||
"annotate_native_function",
|
||||
],
|
||||
]) {
|
||||
let failure;
|
||||
try {
|
||||
await cli(command, representations[0], flags);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, 1);
|
||||
failure = JSON.parse(error.stdout);
|
||||
}
|
||||
assert.equal(failure?.code, "invalid_request");
|
||||
assert.equal(failure.details.operation, operation);
|
||||
assert.match(
|
||||
JSON.stringify(failure.details.issues),
|
||||
/cannot be represented without truncation/u,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { access, readFile, readdir, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
|
||||
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs";
|
||||
import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs";
|
||||
import { verifyGhidraTargetAdmission } from "./real-ghidra-target-admission.mjs";
|
||||
import { verifyGhidraLargeResults } from "./real-ghidra-large-results.mjs";
|
||||
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
|
||||
|
||||
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
|
||||
export async function verifyGhidraBoundaries(
|
||||
client,
|
||||
{ target, procedure, entrypoint, env },
|
||||
) {
|
||||
const options = { timeout: 180000 };
|
||||
const catalog = await client.listTools();
|
||||
const ajv = new Ajv({ strict: false });
|
||||
addFormats(ajv);
|
||||
const validators = new Map();
|
||||
for (const tool of catalog.tools) {
|
||||
for (const key of ["inputSchema", "outputSchema"])
|
||||
assert.ok(
|
||||
ajv.validateSchema(tool[key]),
|
||||
`${tool.name} ${key}: ${JSON.stringify(ajv.errors)}`,
|
||||
);
|
||||
validators.set(tool.name, ajv.compile(tool.outputSchema));
|
||||
}
|
||||
let successfulCalls = 0;
|
||||
let rejectedCalls = 0;
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool({ name, arguments: args }, options);
|
||||
const result = requireMcpResult(reply, name);
|
||||
const validate = validators.get(name);
|
||||
assert.ok(validate, `${name} missing from catalog`);
|
||||
assert.ok(
|
||||
validate(reply.structuredContent),
|
||||
`${name}: ${JSON.stringify(validate.errors)}`,
|
||||
);
|
||||
assert.deepEqual(reply.structuredContent, JSON.parse(mcpTextValue(reply)));
|
||||
successfulCalls++;
|
||||
return result;
|
||||
};
|
||||
const invalid = async (name, args, diagnostic) => {
|
||||
const reply = await client.callTool({ name, arguments: args }, options);
|
||||
assert.equal(
|
||||
reply.isError,
|
||||
true,
|
||||
`${name} accepted ${JSON.stringify(args)}`,
|
||||
);
|
||||
// SDK input-schema rejections are text-only; application errors use the
|
||||
// canonical structured error projection, separately from success schemas.
|
||||
if (reply.structuredContent !== undefined) {
|
||||
analysisErrorProjectionSchema.parse(reply.structuredContent.error);
|
||||
assert.deepEqual(
|
||||
reply.structuredContent,
|
||||
JSON.parse(mcpTextValue(reply)),
|
||||
);
|
||||
}
|
||||
if (diagnostic !== undefined) {
|
||||
const error = reply.structuredContent?.error;
|
||||
assert.equal(error?.code, "invalid_request");
|
||||
assert.equal(error?.details?.operation, name);
|
||||
assert.match(JSON.stringify(error.details.issues), diagnostic);
|
||||
}
|
||||
rejectedCalls++;
|
||||
return reply.structuredContent?.error;
|
||||
};
|
||||
const cli = async (command, value, flags = []) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
command,
|
||||
target.path,
|
||||
value,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
...flags,
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
return JSON.parse(stdout).normalized_result;
|
||||
};
|
||||
const original = await call("analyze_function", { procedure });
|
||||
const roots = (await readdir(env.TMPDIR)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
);
|
||||
assert.equal(roots.length, 1, "Expected one owned Ghidra runtime");
|
||||
const runtimeRoot = join(env.TMPDIR, roots[0]);
|
||||
const { endpoint_path: socketPath } = JSON.parse(
|
||||
await readFile(join(runtimeRoot, "ownership.json"), "utf8"),
|
||||
);
|
||||
const socketRoot = dirname(socketPath);
|
||||
assert.ok(
|
||||
Buffer.byteLength(join(runtimeRoot, "bridge.sock"), "utf8") > 107,
|
||||
"Fixture must exceed Unix socket path capacity",
|
||||
);
|
||||
assert.ok(Buffer.byteLength(socketPath, "utf8") <= 103);
|
||||
assert.notEqual(socketRoot, runtimeRoot);
|
||||
assert.equal((await stat(socketRoot)).mode & 0o777, 0o700);
|
||||
const address = original.procedure.address;
|
||||
const name = original.procedure.name;
|
||||
const procedures = await call("list_procedures");
|
||||
const external = procedures.find((item) => item.procedure.external);
|
||||
assert.ok(external, "Source fixture lacks an external function");
|
||||
const externalDossier = await call("analyze_function", {
|
||||
procedure: external.address,
|
||||
});
|
||||
const externalEntry = await call("resolve_containing_procedure", {
|
||||
address: external.address,
|
||||
});
|
||||
assert.equal(externalEntry.found, true);
|
||||
assert.equal(externalEntry.procedure.address, external.address);
|
||||
assert.equal(externalEntry.procedure.classification.external, true);
|
||||
assert.deepEqual(
|
||||
externalEntry.procedure.body,
|
||||
externalDossier.procedure.body,
|
||||
);
|
||||
assert.equal(externalEntry.procedure.body.contains_entry, false);
|
||||
assert.deepEqual(externalEntry.procedure.body.ranges, []);
|
||||
const externalSpace = external.address.slice(
|
||||
0,
|
||||
external.address.indexOf(":"),
|
||||
);
|
||||
const lastExternalOffset = procedures
|
||||
.filter((item) => item.procedure.external)
|
||||
.map((item) => BigInt(item.address.slice(item.address.indexOf(":") + 1)))
|
||||
.reduce((maximum, offset) => (offset > maximum ? offset : maximum), 0n);
|
||||
const unknownExternal = `${externalSpace}:0x${(lastExternalOffset + 1n).toString(16)}`;
|
||||
assert.deepEqual(
|
||||
await call("resolve_containing_procedure", { address: unknownExternal }),
|
||||
{
|
||||
query_address: unknownExternal,
|
||||
found: false,
|
||||
procedure: null,
|
||||
reason: "outside_segments",
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await cli("function", external.address)).procedure,
|
||||
externalDossier.procedure,
|
||||
"CLI and MCP must preserve external identity without inventing body bytes",
|
||||
);
|
||||
await invalid(
|
||||
"annotate_native_function",
|
||||
{ procedure: external.address, name: "rea_external_edit" },
|
||||
/Annotations require a local function entry/u,
|
||||
);
|
||||
const names = await call("list_names");
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
const pointer = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_interior_pointer"),
|
||||
);
|
||||
assert.ok(
|
||||
leaf && pointer,
|
||||
"Source fixture lacks the interior function pointer",
|
||||
);
|
||||
const interiorTarget = `0x${(BigInt(leaf.address) + 1n).toString(16)}`;
|
||||
assert.ok(
|
||||
(await call("xrefs", { address: interiorTarget })).includes(
|
||||
pointer.address,
|
||||
),
|
||||
);
|
||||
const incoming = await call("procedure_references", {
|
||||
procedure: leaf.address,
|
||||
direction: "incoming",
|
||||
});
|
||||
const interiorEdge = incoming.references.find(
|
||||
(edge) =>
|
||||
edge.source_address === pointer.address &&
|
||||
edge.target_address === interiorTarget,
|
||||
);
|
||||
assert.ok(
|
||||
interiorEdge,
|
||||
"Procedure references omitted an observed interior-body edge",
|
||||
);
|
||||
assert.equal(interiorEdge.target_procedure?.address, leaf.address);
|
||||
assert.equal(interiorEdge.source_procedure, null);
|
||||
assert.equal(interiorEdge.kind.data, true);
|
||||
const leafDossier = await call("analyze_function", {
|
||||
procedure: leaf.address,
|
||||
});
|
||||
assert.ok(
|
||||
leafDossier.incoming_references.some(
|
||||
(edge) =>
|
||||
edge.source_address === pointer.address &&
|
||||
edge.target_address === interiorTarget,
|
||||
),
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await cli("function", leaf.value)).incoming_references,
|
||||
leafDossier.incoming_references,
|
||||
"CLI and MCP must retain the same interior-body references",
|
||||
);
|
||||
await verifyLegacyGhidraReferenceSnapshot(client, {
|
||||
target,
|
||||
procedure: leaf.address,
|
||||
omittedEdge: interiorEdge,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
const longSelector = "REA_MISSING_" + "x".repeat(32768);
|
||||
for (const selected of [
|
||||
longSelector,
|
||||
`${longSelector}:0x1`,
|
||||
`${"%78".repeat(12000)}:0x1`,
|
||||
`${longSelector}%GG:0x1`,
|
||||
]) {
|
||||
await invalid(
|
||||
"procedure_address",
|
||||
{ procedure: selected },
|
||||
/Unknown|invalid/u,
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: address }),
|
||||
address,
|
||||
"Invalid selectors must preserve the live provider connection",
|
||||
);
|
||||
}
|
||||
const baseline = await call("inspect_native_instruction", { address });
|
||||
assert.equal(baseline.status, "decoded");
|
||||
const bytes = await call("read_bytes", { address, length: baseline.length });
|
||||
assert.equal(bytes.bytes_hex, baseline.bytes);
|
||||
assert.equal((await call("read_bytes", { address })).requested_bytes, 256);
|
||||
const mapped = await call("address_to_file_offset", { address });
|
||||
assert.equal(mapped.address, address);
|
||||
assert.equal(
|
||||
(await readFile(target.path))
|
||||
.subarray(mapped.file_offset, mapped.file_offset + baseline.length)
|
||||
.toString("hex"),
|
||||
bytes.bytes_hex,
|
||||
"Memory-to-file mapping must identify the bytes in the caller's artifact",
|
||||
);
|
||||
const largeRead = await call("read_bytes", {
|
||||
address,
|
||||
length: Number.MAX_SAFE_INTEGER,
|
||||
});
|
||||
assert.equal(largeRead.requested_bytes, Number.MAX_SAFE_INTEGER);
|
||||
assert.equal(largeRead.complete, false);
|
||||
assert.ok(largeRead.returned_bytes >= baseline.length);
|
||||
assert.equal(largeRead.bytes_hex.length, largeRead.returned_bytes * 2);
|
||||
for (const spelling of [
|
||||
address.toUpperCase(),
|
||||
address.slice(2).toUpperCase(),
|
||||
`ram:${address.toUpperCase()}`,
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
await call("inspect_native_instruction", { address: spelling }),
|
||||
baseline,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call("read_bytes", { address: spelling, length: baseline.length }),
|
||||
bytes,
|
||||
);
|
||||
assert.equal(await call("address_name", { address: spelling }), name);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: spelling }),
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("resolve_containing_procedure", { address: spelling }))
|
||||
.procedure.address,
|
||||
address,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await cli("inspect-native-instruction", address.toUpperCase()),
|
||||
baseline,
|
||||
);
|
||||
const interior = `0x${(BigInt(address) + 1n).toString(16)}`;
|
||||
assert.equal(
|
||||
(await call("inspect_native_instruction", { address: interior })).status,
|
||||
"not-instruction-boundary",
|
||||
);
|
||||
assert.equal(
|
||||
(await call("inspect_native_instruction", { address: "0x0" })).status,
|
||||
"outside-memory",
|
||||
);
|
||||
assert.equal(
|
||||
(await call("resolve_native_call_targets", { address })).status,
|
||||
"not-call",
|
||||
);
|
||||
const directEdge = original.outgoing_references.find(
|
||||
(edge) => edge.kind.call && !edge.kind.computed,
|
||||
);
|
||||
assert.ok(directEdge, "Source fixture lacks a direct call");
|
||||
const direct = await call("resolve_native_call_targets", {
|
||||
address: directEdge.source_address,
|
||||
});
|
||||
assert.equal(direct.status, "direct");
|
||||
assert.ok(
|
||||
direct.targets.some(
|
||||
(candidate) =>
|
||||
candidate.address === directEdge.target_address &&
|
||||
candidate.status === "direct",
|
||||
),
|
||||
);
|
||||
const indirectProcedure = (await call("list_procedures")).find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_indirect"),
|
||||
);
|
||||
assert.ok(indirectProcedure);
|
||||
const references = await call("procedure_references", {
|
||||
procedure: indirectProcedure.address,
|
||||
});
|
||||
assert.ok(
|
||||
references.unresolved_calls.length > 0,
|
||||
"Targetless calls must remain visible",
|
||||
);
|
||||
for (const site of references.unresolved_calls) {
|
||||
const unresolved = await call("resolve_native_call_targets", {
|
||||
address: site.address,
|
||||
});
|
||||
assert.equal(unresolved.status, "unresolved");
|
||||
assert.deepEqual(unresolved.targets, []);
|
||||
assert.ok(unresolved.limitations.length > 0);
|
||||
}
|
||||
const strings = await call("list_strings");
|
||||
assert.ok(strings.length > 0);
|
||||
assert.deepEqual(
|
||||
await call("list_strings", { address: strings[0].address }),
|
||||
[strings[0]],
|
||||
);
|
||||
const marker = strings.find(
|
||||
(item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY",
|
||||
);
|
||||
assert.ok(marker);
|
||||
assert.ok(
|
||||
(
|
||||
await call("search_strings", { pattern: marker.value.toLowerCase() })
|
||||
).some((item) => item.address === marker.address),
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await call("search_strings", {
|
||||
pattern: marker.value.toLowerCase(),
|
||||
case_sensitive: true,
|
||||
})
|
||||
).some((item) => item.address === marker.address),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("inspect_native_instruction", { address: strings[0].address }))
|
||||
.status,
|
||||
"data",
|
||||
);
|
||||
|
||||
await invalid(
|
||||
"inspect_native_instruction",
|
||||
{ address: "not-an-address" },
|
||||
/hexadecimal/u,
|
||||
);
|
||||
await invalid(
|
||||
"inspect_native_instruction",
|
||||
{ address: `missing:${address}` },
|
||||
/missing:0x/u,
|
||||
);
|
||||
await invalid(
|
||||
"address_name",
|
||||
{ address: `0x${(BigInt(address) + 2n ** 64n).toString(16)}` },
|
||||
/invalid Ghidra address/u,
|
||||
);
|
||||
await invalid(
|
||||
"procedure_address",
|
||||
{ procedure: "rea_missing_procedure" },
|
||||
/rea_missing_procedure/u,
|
||||
);
|
||||
await invalid(
|
||||
"search_procedures",
|
||||
{ pattern: "[", mode: "regex" },
|
||||
/Invalid regex pattern.*index/u,
|
||||
);
|
||||
await invalid(
|
||||
"list_procedures",
|
||||
{ document: "rea_missing_program" },
|
||||
/rea_missing_program.*active program/u,
|
||||
);
|
||||
// JSON permits escaped lone surrogates. Error replies must preserve them
|
||||
// without letting the Java UTF-8 writer terminate the provider session.
|
||||
await invalid(
|
||||
"procedure_address",
|
||||
{ procedure: "missing_\ud800" },
|
||||
/missing_\\ud800/u,
|
||||
);
|
||||
await invalid(
|
||||
"list_procedures",
|
||||
{ document: "missing_\udfff" },
|
||||
/missing_\\udfff/u,
|
||||
);
|
||||
for (const [operation, args] of [
|
||||
["read_bytes", { address, length: 0 }],
|
||||
["list_procedures", { limit: 100 }],
|
||||
["list_documents", { extra: true }],
|
||||
["xrefs", {}],
|
||||
["procedure_info", { procedure: address, extra: true }],
|
||||
])
|
||||
await invalid(operation, args);
|
||||
for (const changes of [
|
||||
{},
|
||||
{ name: "" },
|
||||
{ comment: null },
|
||||
{ name: "entry", approve: true },
|
||||
])
|
||||
await invalid("annotate_native_function", {
|
||||
procedure: address,
|
||||
...changes,
|
||||
});
|
||||
|
||||
let cliError;
|
||||
try {
|
||||
await cli("search", "[", ["--kind", "procedures", "--mode", "regex"]);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, 1);
|
||||
cliError = JSON.parse(error.stdout);
|
||||
}
|
||||
assert.equal(cliError?.code, "invalid_request");
|
||||
assert.equal(cliError?.details?.operation, "search_procedures");
|
||||
assert.match(
|
||||
JSON.stringify(cliError?.details?.issues),
|
||||
/Invalid regex pattern.*index/u,
|
||||
);
|
||||
|
||||
// Prime both inventories before editing so stale names cannot hide in caches.
|
||||
await call("list_names");
|
||||
await call("list_procedures");
|
||||
const changes = {
|
||||
procedure: address,
|
||||
name: "dead",
|
||||
comment: "Regular\n註記",
|
||||
inline_comment: "Inline\n註記",
|
||||
};
|
||||
const updated = await call("annotate_native_function", changes);
|
||||
assert.deepEqual(updated.annotations, {
|
||||
address,
|
||||
name: changes.name,
|
||||
comment: changes.comment,
|
||||
inline_comment: changes.inline_comment,
|
||||
});
|
||||
const regexPattern = "^(a|aa)*b$";
|
||||
const regexFailure = await client.callTool(
|
||||
{
|
||||
name: "search_strings",
|
||||
arguments: { pattern: regexPattern, mode: "regex" },
|
||||
},
|
||||
options,
|
||||
);
|
||||
assert.equal(regexFailure.isError, true);
|
||||
const regexError = regexFailure.structuredContent.error;
|
||||
assert.equal(regexError.code, "resource_constraint");
|
||||
assert.equal(regexError.details.operation, "search_strings");
|
||||
assert.equal(regexError.details.resource, "memory");
|
||||
assert.match(
|
||||
regexError.remediation.action,
|
||||
/literal mode or simplify the regex/u,
|
||||
);
|
||||
assert.match(
|
||||
regexError.details.reason,
|
||||
/exhausted its stack.*Use literal mode/u,
|
||||
);
|
||||
rejectedCalls++;
|
||||
const nestedPattern = "(".repeat(12000) + "a" + ")".repeat(12000);
|
||||
const compileFailure = await client.callTool(
|
||||
{
|
||||
name: "search_procedures",
|
||||
arguments: { pattern: nestedPattern, mode: "regex" },
|
||||
},
|
||||
options,
|
||||
);
|
||||
assert.equal(compileFailure.isError, true);
|
||||
const compileError = compileFailure.structuredContent.error;
|
||||
assert.equal(compileError.code, "resource_constraint");
|
||||
assert.equal(compileError.details.operation, "search_procedures");
|
||||
assert.match(compileError.details.reason, /stack while compiling pattern/u);
|
||||
assert.equal(compileError.remediation.action, regexError.remediation.action);
|
||||
rejectedCalls++;
|
||||
assert.deepEqual(
|
||||
await call("analyze_function", { procedure: address }),
|
||||
updated.dossier,
|
||||
"Regex stack exhaustion must preserve the live annotation database",
|
||||
);
|
||||
assert.equal(
|
||||
(await call("binary_session")).capabilities.find(
|
||||
(item) => item.operation === "search_strings",
|
||||
).available,
|
||||
true,
|
||||
);
|
||||
assert.ok(
|
||||
(await call("search_procedures", { pattern: changes.name })).some(
|
||||
(item) => item.address === address,
|
||||
),
|
||||
"Regex compiler exhaustion must preserve edited names and search availability",
|
||||
);
|
||||
const longLiteral = "a".repeat(3 * 16 ** 3) + "!";
|
||||
const longString = strings.find((item) => item.value === longLiteral);
|
||||
assert.ok(longString, "The provider must retain the full regression literal");
|
||||
const literalMatches = await call("search_strings", { pattern: longLiteral });
|
||||
assert.deepEqual(literalMatches, [
|
||||
{ address: longString.address, value: longLiteral },
|
||||
]);
|
||||
assert.deepEqual(await cli("search", longLiteral), literalMatches);
|
||||
await assert.rejects(
|
||||
cli("search", regexPattern, ["--mode", "regex"]),
|
||||
(error) => {
|
||||
assert.equal(error.code, 1);
|
||||
const rejected = JSON.parse(error.stdout);
|
||||
assert.equal(rejected.code, regexError.code);
|
||||
assert.equal(rejected.details.reason, regexError.details.reason);
|
||||
assert.equal(rejected.remediation.action, regexError.remediation.action);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
assert.deepEqual(updated.effects, {
|
||||
scope: "session-analysis-database",
|
||||
source_bytes_modified: false,
|
||||
persists_after_close: false,
|
||||
});
|
||||
assert.equal(updated.dossier.procedure.name, changes.name);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: changes.name }),
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("procedure_info", { procedure: changes.name })).entrypoint,
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("read_function_instructions", { procedure: changes.name }))
|
||||
.procedure.address,
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("analyze_function", { procedure: changes.name })).procedure
|
||||
.name,
|
||||
changes.name,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), changes.name);
|
||||
for (const inventory of ["list_names", "list_procedures"])
|
||||
assert.ok(
|
||||
(await call(inventory)).some(
|
||||
(item) => item.address === address && item.value === changes.name,
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
(
|
||||
await call("search_procedures", { pattern: "dead", case_sensitive: true })
|
||||
).some((item) => item.address === address),
|
||||
);
|
||||
|
||||
for (const field of ["name", "comment", "inline_comment"]) {
|
||||
for (const text of ["bad\0text", "bad\ud800text", "bad\udffftext"]) {
|
||||
await invalid(
|
||||
"annotate_native_function",
|
||||
{
|
||||
procedure: address,
|
||||
name: "MUST_ROLL_BACK",
|
||||
comment: "MUST ROLL BACK",
|
||||
inline_comment: "MUST ROLL BACK",
|
||||
[field]: text,
|
||||
},
|
||||
new RegExp(
|
||||
`Annotation ${field}.*(?:NUL|unpaired Unicode surrogate).*index`,
|
||||
"u",
|
||||
),
|
||||
);
|
||||
assert.deepEqual(
|
||||
(
|
||||
await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: changes.name,
|
||||
})
|
||||
).annotations,
|
||||
updated.annotations,
|
||||
"Invalid native text must leave every annotation unchanged",
|
||||
);
|
||||
}
|
||||
}
|
||||
const unicodeText =
|
||||
"CRLF\r\nastral 🧪 decomposed e\u0301 control \u0001\b\v\f\u001f noncharacter \ufffe\uffff";
|
||||
const unicode = await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
comment: unicodeText,
|
||||
inline_comment: unicodeText,
|
||||
});
|
||||
assert.equal(unicode.annotations.comment, unicodeText);
|
||||
assert.equal(unicode.annotations.inline_comment, unicodeText);
|
||||
assert.ok(unicode.dossier.comments.some((item) => item.text === unicodeText));
|
||||
await call("annotate_native_function", changes);
|
||||
|
||||
await invalid(
|
||||
"annotate_native_function",
|
||||
{
|
||||
procedure: changes.name,
|
||||
name: "invalid name\n",
|
||||
comment: "MUST ROLL BACK",
|
||||
inline_comment: "MUST ROLL BACK",
|
||||
},
|
||||
/Invalid function name/u,
|
||||
);
|
||||
const retained = await call("annotate_native_function", {
|
||||
procedure: changes.name,
|
||||
name: changes.name,
|
||||
});
|
||||
assert.deepEqual(retained.annotations, updated.annotations);
|
||||
const cleared = await call("annotate_native_function", {
|
||||
procedure: changes.name,
|
||||
comment: "",
|
||||
inline_comment: "",
|
||||
});
|
||||
assert.equal(cleared.annotations.comment, null);
|
||||
assert.equal(cleared.annotations.inline_comment, null);
|
||||
assert.equal(cleared.annotations.name, changes.name);
|
||||
assert.deepEqual(
|
||||
await call("read_bytes", { address, length: baseline.length }),
|
||||
bytes,
|
||||
);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(target.path))
|
||||
.digest("hex"),
|
||||
target.sha256,
|
||||
);
|
||||
const cliUpdated = await cli("annotate-native-function", name, [
|
||||
"--name",
|
||||
changes.name,
|
||||
"--comment",
|
||||
changes.comment,
|
||||
"--inline-comment",
|
||||
changes.inline_comment,
|
||||
]);
|
||||
assert.deepEqual(cliUpdated.annotations, updated.annotations);
|
||||
assert.deepEqual(cliUpdated.effects, updated.effects);
|
||||
for (const renamed of ["0xordinary", "probe::qualified", "🧪probe"]) {
|
||||
await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: renamed,
|
||||
});
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: renamed }),
|
||||
address,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), renamed);
|
||||
}
|
||||
for (const selected of [address, indirectProcedure.address]) {
|
||||
await call("annotate_native_function", {
|
||||
procedure: selected,
|
||||
name: "rea_ambiguous",
|
||||
});
|
||||
}
|
||||
const ambiguous = await invalid(
|
||||
"procedure_address",
|
||||
{ procedure: "rea_ambiguous" },
|
||||
/ambiguous.*select an exact entry address/u,
|
||||
);
|
||||
for (const selected of [address, indirectProcedure.address]) {
|
||||
assert.ok(
|
||||
JSON.stringify(ambiguous.details.issues).includes(selected),
|
||||
"Ambiguity diagnostics must retain every matching entry address",
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: selected }),
|
||||
selected,
|
||||
);
|
||||
}
|
||||
const snapshotLifecycle = await verifyGhidraSnapshotLifecycle(
|
||||
client,
|
||||
target,
|
||||
await call("analyze_function", { procedure: address }),
|
||||
cli,
|
||||
);
|
||||
successfulCalls += snapshotLifecycle.successfulCalls;
|
||||
rejectedCalls += snapshotLifecycle.rejectedCalls;
|
||||
const targetAdmission = await verifyGhidraTargetAdmission({
|
||||
call,
|
||||
reject: invalid,
|
||||
target,
|
||||
entry: address,
|
||||
runtimeRoot: env.TMPDIR,
|
||||
});
|
||||
await verifyGhidraNamespaceAnnotations({
|
||||
call,
|
||||
reject: invalid,
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
await verifyGhidraLargeResults({
|
||||
call,
|
||||
reject: invalid,
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
await assert.rejects(access(socketRoot), { code: "ENOENT" });
|
||||
await assert.rejects(access(runtimeRoot), { code: "ENOENT" });
|
||||
assert.equal(await call("procedure_address", { procedure: name }), address);
|
||||
assert.equal(await call("address_name", { address }), name);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: indirectProcedure.value }),
|
||||
indirectProcedure.address,
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await call("analyze_function", { procedure: address })).comments,
|
||||
original.comments,
|
||||
);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(target.path))
|
||||
.digest("hex"),
|
||||
target.sha256,
|
||||
);
|
||||
await verifyStartupCancellation(client, call, target, env, options);
|
||||
return {
|
||||
mocked: false,
|
||||
catalog_tools: catalog.tools.length,
|
||||
successful_calls: successfulCalls,
|
||||
rejected_calls: rejectedCalls,
|
||||
cli_mcp_parity: true,
|
||||
mutation_rollback: true,
|
||||
annotation_native_text_validation: true,
|
||||
lossless_unicode_transport: true,
|
||||
exact_external_entry_resolution: true,
|
||||
recoverable_regex_stack_exhaustion: true,
|
||||
complete_function_body_references: true,
|
||||
ambiguity_candidates_inline: true,
|
||||
legacy_reference_snapshot_rejected: true,
|
||||
mutation_snapshot_lifecycle: true,
|
||||
concurrent_annotation_snapshot_close: true,
|
||||
changed_source_admission_and_recovery: true,
|
||||
source_permission_denial_verified: targetAdmission.permissionDenied,
|
||||
missing_and_nonregular_source_rejected: true,
|
||||
imported_source_identity_retained: true,
|
||||
equivalent_instruction_address_spellings: true,
|
||||
qualified_annotation_name_roundtrip: true,
|
||||
oversized_result_retention_and_complete_export: true,
|
||||
long_selector_rejection_and_provider_recovery: true,
|
||||
source_immutable: true,
|
||||
reopen_discards_edits: true,
|
||||
long_tmpdir_private_socket_cleanup: true,
|
||||
real_startup_cancellation_cleanup: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyStartupCancellation(client, call, target, env, options) {
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
// The composite provider selects the target lazily. Use a fresh search input
|
||||
// so persisted immutable snapshots cannot bypass actual provider startup.
|
||||
const request = {
|
||||
name: "search_procedures",
|
||||
arguments: { pattern: `startup-cancellation-${randomUUID()}` },
|
||||
};
|
||||
const controller = new AbortController();
|
||||
let settled = false;
|
||||
let outcome;
|
||||
const pending = client
|
||||
.callTool(request, { ...options, signal: controller.signal })
|
||||
.then(
|
||||
(reply) => {
|
||||
settled = true;
|
||||
outcome = reply;
|
||||
return { reply };
|
||||
},
|
||||
(error) => {
|
||||
settled = true;
|
||||
outcome = String(error);
|
||||
return { error };
|
||||
},
|
||||
);
|
||||
const deadline = Date.now() + 30000;
|
||||
let launched;
|
||||
try {
|
||||
while (Date.now() < deadline && !settled) {
|
||||
const roots = (await readdir(env.TMPDIR)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
);
|
||||
if (roots.length === 1) {
|
||||
const root = join(env.TMPDIR, roots[0]);
|
||||
try {
|
||||
const ownership = JSON.parse(
|
||||
await readFile(join(root, "ownership.json"), "utf8"),
|
||||
);
|
||||
const endpointPath = ownership.endpoint_path;
|
||||
// Observe the JVM inside the owned process group before the bridge
|
||||
// publishes its socket. A log file can appear after analysis finishes.
|
||||
const { stdout } = await promisify(execFile)("ps", [
|
||||
"-axo",
|
||||
"pid=,pgid=,comm=",
|
||||
]);
|
||||
const javaProcess = stdout.split("\n").find((line) => {
|
||||
const fields = line.trim().split(/\s+/u);
|
||||
return (
|
||||
Number(fields[1]) === ownership.process_group_id &&
|
||||
/(?:^|\/)java$/u.test(fields.slice(2).join(" "))
|
||||
);
|
||||
});
|
||||
if (javaProcess === undefined) {
|
||||
await delay(25);
|
||||
continue;
|
||||
}
|
||||
const javaPid = Number(javaProcess.trim().split(/\s+/u)[0]);
|
||||
await assert.rejects(access(endpointPath), { code: "ENOENT" });
|
||||
process.kill(ownership.pid, 0);
|
||||
launched = {
|
||||
root,
|
||||
socketRoot: dirname(endpointPath),
|
||||
pid: ownership.pid,
|
||||
javaPid,
|
||||
};
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
await delay(25);
|
||||
}
|
||||
assert.ok(
|
||||
launched,
|
||||
`No real headless process was observed before cancellation: ${JSON.stringify(outcome)}`,
|
||||
);
|
||||
assert.equal(
|
||||
settled,
|
||||
false,
|
||||
"Cancellation must interrupt pending provider work",
|
||||
);
|
||||
} finally {
|
||||
controller.abort();
|
||||
await pending;
|
||||
}
|
||||
const cleanupDeadline = Date.now() + 30000;
|
||||
while (Date.now() < cleanupDeadline) {
|
||||
const processGone = [launched.pid, launched.javaPid].every((pid) => {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (error.code === "ESRCH") return true;
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
try {
|
||||
await access(launched.root);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT" && processGone) break;
|
||||
if (error.code === "ENOENT") {
|
||||
await delay(25);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await delay(25);
|
||||
}
|
||||
await assert.rejects(access(launched.root), { code: "ENOENT" });
|
||||
await assert.rejects(access(launched.socketRoot), { code: "ENOENT" });
|
||||
assert.throws(() => process.kill(launched.pid, 0), { code: "ESRCH" });
|
||||
assert.throws(() => process.kill(launched.javaPid, 0), { code: "ESRCH" });
|
||||
assert.equal(
|
||||
(await call("binary_session")).open,
|
||||
true,
|
||||
"Cancellation must preserve the selected target",
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call(request.name, request.arguments),
|
||||
[],
|
||||
"A cancelled startup must allow a fresh provider query",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
|
||||
|
||||
/** Exercise complete oversized results and recovery through real CLI/MCP adapters. */
|
||||
export async function verifyGhidraLargeResults({
|
||||
call,
|
||||
reject,
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
}) {
|
||||
const exec = promisify(execFile);
|
||||
const compiler = process.env.REA_CC ?? "cc";
|
||||
try {
|
||||
await exec(compiler, ["--version"], { env, timeout: 30000 });
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Ghidra large-result verification requires a working host C compiler '${compiler}'.`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
// Hex encoding, Evidence projection and the two MCP representations multiply
|
||||
// the source size. Derive the fixture from the pinned SDK's receive budget.
|
||||
const length = Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4);
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-large-results-"));
|
||||
const path = join(workspace, "large-results");
|
||||
const source = fileURLToPath(
|
||||
new URL("../../tests/conformance/ghidra/large-results.c", import.meta.url),
|
||||
);
|
||||
try {
|
||||
await exec(
|
||||
compiler,
|
||||
["-O0", `-DREA_PAYLOAD_BYTES=${length}`, source, "-o", path],
|
||||
{ env, timeout: 30000 },
|
||||
);
|
||||
const bytes = await readFile(path);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path, provider_id: "ghidra" });
|
||||
const names = await call("list_names");
|
||||
const payload = names.find(({ value }) =>
|
||||
/(?:^|_)rea_large_payload$/u.test(value),
|
||||
);
|
||||
const entry = names.find(({ value }) =>
|
||||
/(?:^|_)rea_large_entry$/u.test(value),
|
||||
);
|
||||
assert.ok(payload);
|
||||
assert.ok(entry);
|
||||
const mapping = await call("address_to_file_offset", {
|
||||
address: payload.address,
|
||||
});
|
||||
const expected = bytes.subarray(
|
||||
mapping.file_offset,
|
||||
mapping.file_offset + length,
|
||||
);
|
||||
assert.equal(expected.length, length);
|
||||
assert.equal(expected[0], 0x5a);
|
||||
assert.equal(expected.at(-1), 0xa5);
|
||||
const before = await call("binary_session");
|
||||
// This request fits the SDK receive limit, but repeating the producer's
|
||||
// diagnostic in text and structured content cannot fit the response.
|
||||
const missingSelector =
|
||||
"REA_MISSING_" +
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 2) + 64 * 1024);
|
||||
const lookupError = await reject("procedure_address", {
|
||||
procedure: missingSelector,
|
||||
});
|
||||
const lookupId = retainedReference(lookupError, "procedure_address");
|
||||
assert.equal(
|
||||
lookupError.details.reported_limits.original_error_code,
|
||||
"invalid_request",
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: entry.address }),
|
||||
entry.address,
|
||||
);
|
||||
|
||||
const readError = await reject("read_bytes", {
|
||||
address: payload.address,
|
||||
length,
|
||||
});
|
||||
const readId = retainedReference(readError, "read_bytes");
|
||||
const comment =
|
||||
"REA_BIG_COMMENT:" +
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
|
||||
const annotationError = await reject("annotate_native_function", {
|
||||
procedure: entry.address,
|
||||
name: "rea_frame_probe",
|
||||
comment,
|
||||
inline_comment: comment,
|
||||
});
|
||||
const annotationId = retainedReference(
|
||||
annotationError,
|
||||
"annotate_native_function",
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: "rea_frame_probe" }),
|
||||
entry.address,
|
||||
"A delivery constraint must preserve the successfully committed edit",
|
||||
);
|
||||
const dossierError = await reject("analyze_function", {
|
||||
procedure: "rea_frame_probe",
|
||||
});
|
||||
const dossierId = retainedReference(dossierError, "analyze_function");
|
||||
const after = await call("binary_session");
|
||||
assert.equal(after.analysis_run.run_id, before.analysis_run.run_id);
|
||||
assert.equal(after.sha256, sha256);
|
||||
assert.equal(
|
||||
(await call("read_bytes", { address: payload.address, length: 1 }))
|
||||
.bytes_hex,
|
||||
"5a",
|
||||
);
|
||||
const lastAddress = `0x${(BigInt(payload.address) + BigInt(length - 1)).toString(16)}`;
|
||||
assert.equal(
|
||||
(await call("read_bytes", { address: lastAddress, length: 1 })).bytes_hex,
|
||||
"a5",
|
||||
);
|
||||
const bundlePath = join(workspace, "bundle.json");
|
||||
await call("export_evidence_bundle", { path: bundlePath });
|
||||
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
|
||||
const retained = (id) => {
|
||||
const record = bundle.records.find(
|
||||
({ evidence_id }) => evidence_id === id,
|
||||
);
|
||||
assert.ok(record, `Missing exported retained record ${id}`);
|
||||
assert.equal(record.provider.id, "ghidra");
|
||||
assert.equal(record.subject.digest.sha256, sha256);
|
||||
return record;
|
||||
};
|
||||
const originalError = bundle.records.find(
|
||||
({ evidence_id }) => evidence_id === lookupId,
|
||||
);
|
||||
assert.ok(originalError);
|
||||
assert.equal(originalError.operation, "mcp_tool_error");
|
||||
assert.equal(originalError.provider.id, "rea-mcp");
|
||||
assert.equal(originalError.subject, null);
|
||||
assert.equal(originalError.parameters.tool_name, "procedure_address");
|
||||
assert.equal(originalError.normalized_result.error.code, "invalid_request");
|
||||
assert.equal(originalError.raw_result.isError, true);
|
||||
assert.deepEqual(
|
||||
originalError.raw_result.structuredContent,
|
||||
originalError.normalized_result,
|
||||
);
|
||||
assert.equal(
|
||||
originalError.normalized_result.error.details.operation,
|
||||
"procedure_address",
|
||||
);
|
||||
assert.equal(
|
||||
originalError.normalized_result.error.details.issues[0].message,
|
||||
`Unknown Ghidra procedure name or address: ${missingSelector}`,
|
||||
);
|
||||
const readRecord = retained(readId);
|
||||
const read = readRecord.normalized_result;
|
||||
assert.equal(read.requested_bytes, length);
|
||||
assert.equal(read.returned_bytes, length);
|
||||
assert.equal(read.complete, true);
|
||||
assert.equal(read.bytes_hex, expected.toString("hex"));
|
||||
assert.equal(readRecord.raw_result.bytes_hex, read.bytes_hex);
|
||||
const annotationRecord = retained(annotationId);
|
||||
const annotation = annotationRecord.normalized_result;
|
||||
assert.equal(annotation.annotations.name, "rea_frame_probe");
|
||||
assert.equal(annotation.annotations.comment, comment);
|
||||
assert.equal(annotation.annotations.inline_comment, comment);
|
||||
assert.equal(annotationRecord.raw_result.annotations.comment, comment);
|
||||
assert.equal(
|
||||
annotationRecord.raw_result.annotations.inline_comment,
|
||||
comment,
|
||||
);
|
||||
const dossierRecord = retained(dossierId);
|
||||
const dossier = dossierRecord.normalized_result;
|
||||
assert.equal(dossier.procedure.name, "rea_frame_probe");
|
||||
assert.ok(dossier.comments.some(({ text }) => text === comment));
|
||||
assert.ok(
|
||||
dossierRecord.raw_result.comments.some(({ text }) => text === comment),
|
||||
);
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"read-bytes",
|
||||
path,
|
||||
payload.address,
|
||||
"--length",
|
||||
String(length),
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
assert.deepEqual(JSON.parse(stdout).normalized_result, read);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function retainedReference(error, operation) {
|
||||
assert.equal(error.code, "resource_constraint");
|
||||
assert.equal(error.details.operation, operation);
|
||||
assert.equal(error.details.resource, "transport");
|
||||
const limits = error.details.reported_limits;
|
||||
assert.equal(limits.boundary, "mcp-response");
|
||||
assert.equal(limits.constraint, "receive-buffer");
|
||||
assert.ok(limits.response_bytes_at_least > limits.result_budget_bytes);
|
||||
assert.equal(limits.evidence_reference.kind, "retained-evidence");
|
||||
assert.match(limits.evidence_reference.evidence_id, /^ev_[a-f0-9]{64}$/u);
|
||||
return limits.evidence_reference.evidence_id;
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { access, mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { inspectGhidraInstallation } from "../../dist/ghidra/GhidraInstallation.js";
|
||||
|
||||
/** Exercise qualified annotation names against real demangled C++ ABI symbols. */
|
||||
export async function verifyGhidraNamespaceAnnotations({
|
||||
call,
|
||||
reject,
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
}) {
|
||||
await requireNamespaceDemangler(env);
|
||||
const exec = promisify(execFile);
|
||||
const compiler = process.env.REA_CC ?? "cc";
|
||||
try {
|
||||
await exec(compiler, ["--version"], { env, timeout: 30000 });
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Ghidra namespace annotation verification requires a working host C compiler '${compiler}'.`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-namespaces-"));
|
||||
const source = fileURLToPath(
|
||||
new URL("../../tests/conformance/ghidra/namespaces.c", import.meta.url),
|
||||
);
|
||||
const path = join(workspace, "namespaces");
|
||||
try {
|
||||
await exec(compiler, ["-O0", "-fno-inline", source, "-o", path], {
|
||||
env,
|
||||
timeout: 30000,
|
||||
});
|
||||
const bytes = await readFile(path);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path, provider_id: "ghidra" });
|
||||
const procedures = await call("list_procedures");
|
||||
const namespaces = ["alpha", "beta", "outer::inner"];
|
||||
const entries = namespaces.map((namespace) => {
|
||||
const procedure = procedures.find(
|
||||
({ value }) => value === `${namespace}::same`,
|
||||
);
|
||||
assert.ok(
|
||||
procedure,
|
||||
`Missing demangled fixture function ${namespace}::same`,
|
||||
);
|
||||
return { namespace, address: procedure.address };
|
||||
});
|
||||
const ambiguous = await reject("procedure_address", { procedure: "same" });
|
||||
assert.equal(ambiguous.code, "invalid_request");
|
||||
for (const { address } of entries)
|
||||
assert.ok(JSON.stringify(ambiguous.details.issues).includes(address));
|
||||
for (const { namespace, address } of entries) {
|
||||
const leaf = await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: "renamed",
|
||||
comment: "Preserve this comment",
|
||||
inline_comment: "Preserve this inline comment",
|
||||
});
|
||||
assert.equal(leaf.annotations.name, `${namespace}::renamed`);
|
||||
const qualified = await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: `${namespace}::qualified`,
|
||||
});
|
||||
assert.equal(qualified.annotations.name, `${namespace}::qualified`);
|
||||
for (let repeat = 0; repeat < 2; repeat++) {
|
||||
const replay = await call("annotate_native_function", {
|
||||
procedure: qualified.annotations.name,
|
||||
name: qualified.annotations.name,
|
||||
});
|
||||
assert.deepEqual(replay.annotations, qualified.annotations);
|
||||
assert.equal(replay.dossier.procedure.address, address);
|
||||
}
|
||||
assert.equal(
|
||||
await call("procedure_address", {
|
||||
procedure: qualified.annotations.name,
|
||||
}),
|
||||
address,
|
||||
);
|
||||
for (const name of [
|
||||
"other::literal",
|
||||
`${namespace}::child::literal`,
|
||||
"0xnothex",
|
||||
"address%GG:0x1",
|
||||
"address:0xnothex",
|
||||
]) {
|
||||
const literal = await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name,
|
||||
});
|
||||
const expected = name.startsWith(`${namespace}::`)
|
||||
? name
|
||||
: `${namespace}::${name}`;
|
||||
assert.equal(literal.annotations.name, expected);
|
||||
const repeated = await call("annotate_native_function", {
|
||||
procedure: expected,
|
||||
name: expected,
|
||||
});
|
||||
assert.deepEqual(repeated.annotations, literal.annotations);
|
||||
}
|
||||
await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: qualified.annotations.name,
|
||||
});
|
||||
const failure = await reject("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: `${namespace}::`,
|
||||
comment: "MUST NOT REPLACE",
|
||||
inline_comment: "MUST NOT REPLACE",
|
||||
});
|
||||
assert.equal(failure.code, "invalid_request");
|
||||
assert.deepEqual(failure.details.issues[0].path, ["name"]);
|
||||
assert.match(failure.details.issues[0].message, /empty leaf/u);
|
||||
const unchanged = await call("annotate_native_function", {
|
||||
procedure: address,
|
||||
name: qualified.annotations.name,
|
||||
});
|
||||
assert.deepEqual(unchanged.annotations, qualified.annotations);
|
||||
}
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"annotate-native-function",
|
||||
path,
|
||||
"alpha::same",
|
||||
"--name",
|
||||
"alpha::cli_renamed",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(stdout).normalized_result.annotations.name,
|
||||
"alpha::cli_renamed",
|
||||
);
|
||||
assert.equal(
|
||||
(await call("analyze_function", { procedure: entries[0].address }))
|
||||
.procedure.name,
|
||||
"alpha::qualified",
|
||||
"The independent CLI session must not alter the active MCP database",
|
||||
);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
assert.equal((await call("binary_session")).sha256, sha256);
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function requireNamespaceDemangler(env) {
|
||||
const inspection = inspectGhidraInstallation({
|
||||
installDir: env.GHIDRA_INSTALL_DIR,
|
||||
...(env.JAVA_HOME === undefined ? {} : { javaHome: env.JAVA_HOME }),
|
||||
});
|
||||
if (
|
||||
inspection.status !== "available" ||
|
||||
inspection.nativeDecompilerPath === null
|
||||
)
|
||||
throw new Error(
|
||||
`Ghidra namespace verification requires an available installation: ${JSON.stringify(inspection)}`,
|
||||
);
|
||||
const nativeDirectory = basename(dirname(inspection.nativeDecompilerPath));
|
||||
const executable =
|
||||
process.platform === "win32"
|
||||
? "demangler_gnu_v2_41.exe"
|
||||
: "demangler_gnu_v2_41";
|
||||
const failures = [];
|
||||
for (const location of ["os", "build/os"]) {
|
||||
const path = join(
|
||||
inspection.installDir,
|
||||
"GPL",
|
||||
"DemanglerGnu",
|
||||
location,
|
||||
nativeDirectory,
|
||||
executable,
|
||||
);
|
||||
try {
|
||||
await access(path, constants.X_OK);
|
||||
if ((await stat(path)).isFile()) return;
|
||||
failures.push(`${path}: not a regular file`);
|
||||
} catch (cause) {
|
||||
failures.push(
|
||||
`${path}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Ghidra namespace annotation verification requires the matching GNU demangler native component. Provide ${executable} for ${nativeDirectory} in GPL/DemanglerGnu/os or its build/os equivalent; REA does not build it. Checked: ${failures.join("; ")}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseAnalysisSnapshot } from "../../dist/domain/analysisSnapshot.js";
|
||||
import { parseEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
|
||||
import { requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Keep mutable Ghidra observations out of immutable snapshots across target opens. */
|
||||
export async function verifyGhidraSnapshotLifecycle(
|
||||
client,
|
||||
target,
|
||||
edited,
|
||||
cli,
|
||||
) {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-snapshot-proof-"));
|
||||
let successfulCalls = 0;
|
||||
let rejectedCalls = 0;
|
||||
const call = async (name, args = {}) => {
|
||||
const result = requireMcpResult(
|
||||
await client.callTool({ name, arguments: args }, { timeout: 180000 }),
|
||||
name,
|
||||
);
|
||||
successfulCalls++;
|
||||
return result;
|
||||
};
|
||||
const rejectSnapshot = async (name, args) => {
|
||||
const reply = await client.callTool({ name, arguments: args });
|
||||
assert.equal(
|
||||
reply.isError,
|
||||
true,
|
||||
`${name} accepted a mutated snapshot binding`,
|
||||
);
|
||||
const error = reply.structuredContent?.error;
|
||||
assert.equal(
|
||||
error?.code,
|
||||
"evidence_integrity_mismatch",
|
||||
JSON.stringify(reply),
|
||||
);
|
||||
assert.match(error.message, /after analysis metadata mutations/u);
|
||||
assert.match(
|
||||
error.remediation.action,
|
||||
/close.*without saving a snapshot/iu,
|
||||
);
|
||||
assert.match(error.remediation.action, /export_evidence_bundle/u);
|
||||
rejectedCalls++;
|
||||
};
|
||||
try {
|
||||
const entry = edited.procedure.address;
|
||||
const snapshotPath = join(workspace, "original.json");
|
||||
// The CLI has its own unmodified database and writes the production snapshot format.
|
||||
const original = await cli("function", entry, ["--snapshot", snapshotPath]);
|
||||
const snapshotBytes = await readFile(snapshotPath);
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(snapshotBytes.toString("utf8")),
|
||||
);
|
||||
assert.equal(snapshot.target.sha256, target.sha256);
|
||||
assert.notEqual(original.procedure.name, edited.procedure.name);
|
||||
const runId = (await call("binary_session")).analysis_run.run_id;
|
||||
const api = await call("inspect_native_api", { procedure: entry });
|
||||
assert.equal(api.procedure.name, edited.procedure.name);
|
||||
for (const repeatedOpen of [false, true]) {
|
||||
if (repeatedOpen)
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
const destination = join(
|
||||
workspace,
|
||||
repeatedOpen ? "repeated.json" : "edited.json",
|
||||
);
|
||||
await rejectSnapshot("close_binary", { snapshot_path: destination });
|
||||
await assert.rejects(access(destination), { code: "ENOENT" });
|
||||
assert.equal((await call("binary_session")).analysis_run.run_id, runId);
|
||||
assert.deepEqual(
|
||||
await call("analyze_function", { procedure: entry }),
|
||||
edited,
|
||||
);
|
||||
}
|
||||
await rejectSnapshot("open_binary", {
|
||||
path: target.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
assert.deepEqual(await readFile(snapshotPath), snapshotBytes);
|
||||
assert.equal((await call("binary_session")).analysis_run.run_id, runId);
|
||||
assert.deepEqual(
|
||||
await call("analyze_function", { procedure: entry }),
|
||||
edited,
|
||||
);
|
||||
|
||||
// Mutable results remain exportable as observations, with their mutation Evidence.
|
||||
const bundlePath = join(workspace, "observations.json");
|
||||
await call("export_evidence_bundle", { path: bundlePath });
|
||||
const bundle = parseEvidenceBundle(
|
||||
JSON.parse(await readFile(bundlePath, "utf8")),
|
||||
);
|
||||
assert.ok(
|
||||
bundle.records.some(
|
||||
(record) => record.operation === "annotate_native_function",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
bundle.records.some(
|
||||
(record) =>
|
||||
record.operation === "inspect_native_api" &&
|
||||
record.normalized_result.procedure.name === edited.procedure.name,
|
||||
),
|
||||
);
|
||||
|
||||
await call("close_binary");
|
||||
await call("open_binary", {
|
||||
path: target.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshotPath,
|
||||
});
|
||||
const restored = await call("analyze_function", { procedure: entry });
|
||||
assert.deepEqual(restored.procedure, original.procedure);
|
||||
assert.deepEqual(restored.comments, original.comments);
|
||||
assert.notEqual((await call("binary_session")).analysis_run.run_id, runId);
|
||||
const freshPath = join(workspace, "fresh.json");
|
||||
await call("close_binary", { snapshot_path: freshPath });
|
||||
const fresh = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(freshPath, "utf8")),
|
||||
);
|
||||
assert.ok(
|
||||
!fresh.evidence_bundle.records.some(
|
||||
(record) =>
|
||||
record.operation === "annotate_native_function" ||
|
||||
(record.operation === "inspect_native_api" &&
|
||||
record.normalized_result.procedure.name === edited.procedure.name),
|
||||
),
|
||||
);
|
||||
await call("open_binary", {
|
||||
path: target.path,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: freshPath,
|
||||
});
|
||||
await verifyConcurrentSnapshotClose({
|
||||
call,
|
||||
rejectSnapshot,
|
||||
workspace,
|
||||
target,
|
||||
entry,
|
||||
original,
|
||||
});
|
||||
return { successfulCalls, rejectedCalls };
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyConcurrentSnapshotClose({
|
||||
call,
|
||||
rejectSnapshot,
|
||||
workspace,
|
||||
target,
|
||||
entry,
|
||||
original,
|
||||
}) {
|
||||
const destination = join(workspace, "concurrent.json");
|
||||
const annotation = call("annotate_native_function", {
|
||||
procedure: entry,
|
||||
name: "rea_concurrent_snapshot_edit",
|
||||
comment:
|
||||
"A successful edit must invalidate a concurrently requested snapshot.",
|
||||
});
|
||||
const close = rejectSnapshot("close_binary", { snapshot_path: destination });
|
||||
const [updated] = await Promise.all([annotation, close]);
|
||||
await assert.rejects(access(destination), { code: "ENOENT" });
|
||||
assert.equal((await call("binary_session")).open, true);
|
||||
assert.deepEqual(
|
||||
await call("analyze_function", { procedure: entry }),
|
||||
updated.dossier,
|
||||
);
|
||||
// Discard the ephemeral edit and leave the caller with a pristine database.
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
assert.deepEqual(
|
||||
(await call("analyze_function", { procedure: entry })).procedure,
|
||||
original.procedure,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
realpath,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Verify changed-source admission and retained private database identity through MCP. */
|
||||
export async function verifyGhidraTargetAdmission({
|
||||
call,
|
||||
reject,
|
||||
target,
|
||||
entry,
|
||||
runtimeRoot,
|
||||
}) {
|
||||
const workspace = await mkdtemp(
|
||||
join(tmpdir(), "rea-ghidra-source-admission-"),
|
||||
);
|
||||
const source = join(workspace, "caller-selected.%PATH%");
|
||||
try {
|
||||
const original = await readFile(target.path);
|
||||
await writeFile(source, original);
|
||||
await call("close_binary");
|
||||
const opened = await call("open_binary", {
|
||||
path: source,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(opened.sha256, target.sha256);
|
||||
const selectedPath = await realpath(source);
|
||||
assert.equal(opened.path, selectedPath);
|
||||
const before = await call("binary_session");
|
||||
const changed = Buffer.concat([
|
||||
original,
|
||||
Buffer.from("REA_SOURCE_CHANGED_AFTER_OPEN"),
|
||||
]);
|
||||
const changedSha256 = createHash("sha256").update(changed).digest("hex");
|
||||
await writeFile(source, changed);
|
||||
const failure = await reject("analyze_function", { procedure: entry });
|
||||
assert.equal(failure.code, "artifact_changed");
|
||||
assert.equal(failure.category, "integrity_mismatch");
|
||||
assert.equal(failure.details.operation, "analyze_function");
|
||||
assert.equal(failure.details.path, selectedPath);
|
||||
assert.ok(failure.details.reason.includes(target.sha256));
|
||||
assert.ok(failure.details.reason.includes(changedSha256));
|
||||
assert.match(failure.remediation.action, /open_binary/u);
|
||||
assert.doesNotMatch(failure.remediation.action, /doctor/u);
|
||||
const after = await call("binary_session");
|
||||
assert.equal(after.analysis_run.run_id, before.analysis_run.run_id);
|
||||
assert.equal(after.sha256, target.sha256);
|
||||
assert.equal(
|
||||
after.capabilities.find(
|
||||
({ operation }) => operation === "analyze_function",
|
||||
)?.available,
|
||||
true,
|
||||
"An artifact identity mismatch must not mark the installed provider unavailable",
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await readdir(runtimeRoot)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
),
|
||||
[],
|
||||
"Failed admission must remove the private snapshot without launching Ghidra",
|
||||
);
|
||||
assert.deepEqual(await readFile(source), changed);
|
||||
const permissionDenied = await verifySourceReadFailures({
|
||||
call,
|
||||
reject,
|
||||
source,
|
||||
bytes: changed,
|
||||
entry,
|
||||
runtimeRoot,
|
||||
});
|
||||
|
||||
const reopened = await call("open_binary", {
|
||||
path: source,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(reopened.sha256, changedSha256);
|
||||
assert.notEqual(
|
||||
(await call("binary_session")).analysis_run.run_id,
|
||||
before.analysis_run.run_id,
|
||||
);
|
||||
const dossier = await call("analyze_function", { procedure: entry });
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: entry,
|
||||
});
|
||||
assert.equal(dossier.procedure.address, entry);
|
||||
assert.equal(instruction.status, "decoded");
|
||||
for (const address of [
|
||||
entry.toUpperCase(),
|
||||
entry.slice(2),
|
||||
`0x0000${entry.slice(2)}`,
|
||||
`ram:${entry}`,
|
||||
`%72%61%6D:${entry}`,
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
await call("inspect_native_instruction", { address }),
|
||||
instruction,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("resolve_containing_procedure", { address })).procedure
|
||||
.address,
|
||||
entry,
|
||||
);
|
||||
}
|
||||
// Imported facts belong to the captured bytes even if the caller's path disappears.
|
||||
await rm(source);
|
||||
assert.deepEqual(
|
||||
await call("analyze_function", { procedure: entry }),
|
||||
dossier,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call("inspect_native_instruction", { address: entry }),
|
||||
instruction,
|
||||
);
|
||||
assert.equal((await call("binary_session")).sha256, changedSha256);
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
return { permissionDenied };
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function verifySourceReadFailures({
|
||||
call,
|
||||
reject,
|
||||
source,
|
||||
bytes,
|
||||
entry,
|
||||
runtimeRoot,
|
||||
}) {
|
||||
const permissionDenied =
|
||||
typeof process.getuid === "function" && process.getuid() !== 0;
|
||||
const scenarios = [
|
||||
"missing",
|
||||
"directory",
|
||||
...(permissionDenied ? ["permission"] : []),
|
||||
];
|
||||
for (const scenario of scenarios) {
|
||||
await call("open_binary", { path: source, provider_id: "ghidra" });
|
||||
const before = await call("binary_session");
|
||||
if (scenario === "permission") await chmod(source, 0);
|
||||
else {
|
||||
await rm(source);
|
||||
if (scenario === "directory") await mkdir(source);
|
||||
}
|
||||
try {
|
||||
const failure = await reject("analyze_function", { procedure: entry });
|
||||
assert.equal(
|
||||
failure.code,
|
||||
scenario === "permission" ? "access_denied" : "artifact_changed",
|
||||
);
|
||||
assert.equal(failure.details.path, before.path);
|
||||
assert.equal(failure.details.operation, "analyze_function");
|
||||
if (scenario === "permission") {
|
||||
assert.ok(["EACCES", "EPERM"].includes(failure.details.system_code));
|
||||
assert.match(failure.remediation.action, /read access/u);
|
||||
} else assert.match(failure.remediation.action, /open_binary/u);
|
||||
assert.doesNotMatch(failure.remediation.action, /doctor/u);
|
||||
const after = await call("binary_session");
|
||||
assert.equal(after.analysis_run.run_id, before.analysis_run.run_id);
|
||||
assert.equal(after.sha256, before.sha256);
|
||||
assert.equal(
|
||||
after.capabilities.find(
|
||||
({ operation }) => operation === "analyze_function",
|
||||
)?.available,
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await readdir(runtimeRoot)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
),
|
||||
[],
|
||||
);
|
||||
} finally {
|
||||
if (scenario === "permission") await chmod(source, 0o600);
|
||||
await rm(source, { recursive: true, force: true });
|
||||
await writeFile(source, bytes);
|
||||
}
|
||||
}
|
||||
return permissionDenied;
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { copyFile, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
import { verifyHopperCliSelectors } from "./real-hopper-cli-selectors.mjs";
|
||||
import { verifyHopperNavigationAndText } from "./real-hopper-navigation.mjs";
|
||||
import { verifyHopperWorkflows } from "./real-hopper-workflows.mjs";
|
||||
import {
|
||||
verifyHopperSearch,
|
||||
verifyHopperRegexIsolation,
|
||||
} from "./real-hopper-search.mjs";
|
||||
|
||||
/** Exercise real Hopper annotation and malformed-input boundaries over MCP. */
|
||||
export async function verifyHopperBoundaryContracts(
|
||||
client,
|
||||
options,
|
||||
procedure,
|
||||
targetPath,
|
||||
) {
|
||||
const catalog = await client.listTools();
|
||||
const ajv = new Ajv({ strict: false });
|
||||
addFormats(ajv);
|
||||
const validators = new Map();
|
||||
for (const tool of catalog.tools) {
|
||||
assert.ok(
|
||||
ajv.validateSchema(tool.inputSchema),
|
||||
`${tool.name} input schema`,
|
||||
);
|
||||
assert.ok(tool.outputSchema, `${tool.name} omitted its output schema`);
|
||||
assert.ok(
|
||||
ajv.validateSchema(tool.outputSchema),
|
||||
`${tool.name} output schema`,
|
||||
);
|
||||
validators.set(tool.name, ajv.compile(tool.outputSchema));
|
||||
}
|
||||
let successfulCalls = 0;
|
||||
let rejectedCalls = 0;
|
||||
const rejectedOperations = new Set();
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool({ name, arguments: args }, options);
|
||||
const result = requireMcpResult(reply, name);
|
||||
const validate = validators.get(name);
|
||||
assert.ok(validate, `${name} is missing from tools/list`);
|
||||
assert.ok(
|
||||
validate(reply.structuredContent),
|
||||
`${name}: ${JSON.stringify(validate.errors)}`,
|
||||
);
|
||||
assert.deepEqual(reply.structuredContent, JSON.parse(mcpTextValue(reply)));
|
||||
successfulCalls += 1;
|
||||
return result;
|
||||
};
|
||||
const invalid = async (name, args, reason, failedOperation = name) => {
|
||||
const reply = await client.callTool({ name, arguments: args }, options);
|
||||
assert.equal(reply.isError, true, `${name} accepted invalid input`);
|
||||
const { error } = JSON.parse(mcpTextValue(reply));
|
||||
assert.equal(error.code, "invalid_request");
|
||||
assert.equal(error.category, "invalid_input");
|
||||
assert.equal(error.details.operation, failedOperation);
|
||||
assert.match(error.message, reason);
|
||||
rejectedCalls += 1;
|
||||
rejectedOperations.add(name);
|
||||
};
|
||||
const address = procedure.address;
|
||||
const originalName = await call("address_name", { address });
|
||||
const originalComment = await call("comment", { address });
|
||||
const originalInlineComment = await call("inline_comment", { address });
|
||||
const activeDocument = await call("current_document");
|
||||
const foreignDocument = (await call("list_documents")).find(
|
||||
(name) => name !== activeDocument,
|
||||
);
|
||||
const navigationAndText = await verifyHopperNavigationAndText(
|
||||
call,
|
||||
invalid,
|
||||
address,
|
||||
);
|
||||
const workflows = await verifyHopperWorkflows(call, invalid, procedure);
|
||||
const search = {
|
||||
...(await verifyHopperSearch(call, invalid, procedure)),
|
||||
...(await verifyHopperRegexIsolation(client, options, call, address)),
|
||||
};
|
||||
try {
|
||||
if (foreignDocument !== undefined) {
|
||||
await invalid(
|
||||
"read_bytes",
|
||||
{ address, length: 8, document: foreignDocument },
|
||||
/belongs to another target/u,
|
||||
);
|
||||
assert.equal(await call("current_document"), activeDocument);
|
||||
}
|
||||
for (const label of [undefined, "", "REA boundary bookmark 测试"]) {
|
||||
assert.equal(
|
||||
await call("set_bookmark", {
|
||||
address,
|
||||
...(label === undefined ? {} : { name: label }),
|
||||
}),
|
||||
true,
|
||||
);
|
||||
const bookmarks = await call("list_bookmarks");
|
||||
const bookmark = bookmarks.find((item) => item.address === address);
|
||||
assert.ok(bookmark, "set_bookmark did not create a bookmark");
|
||||
assert.equal(bookmark.name, label || null);
|
||||
const context = await call("inspect_address_context", { address });
|
||||
assert.equal(context.bookmarks.state, "available");
|
||||
assert.deepEqual(context.bookmarks.value, [bookmark]);
|
||||
assert.equal(await call("unset_bookmark", { address }), true);
|
||||
assert.ok(
|
||||
!(await call("list_bookmarks")).some(
|
||||
(item) => item.address === address,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (const [setter, getter] of [
|
||||
["set_comment", "comment"],
|
||||
["set_inline_comment", "inline_comment"],
|
||||
]) {
|
||||
const comment = "REA boundary comment 测试";
|
||||
assert.equal(await call(setter, { address, comment }), true);
|
||||
assert.equal(await call(getter, { address }), comment);
|
||||
assert.equal(await call(setter, { address, comment: "" }), true);
|
||||
assert.equal(await call(getter, { address }), null);
|
||||
}
|
||||
const inline = "REA inline\nsecond line";
|
||||
const exact = await call("set_inline_comment", {
|
||||
address,
|
||||
comment: inline,
|
||||
});
|
||||
const observed = await call("inline_comment", { address });
|
||||
assert.equal(
|
||||
exact,
|
||||
observed === inline,
|
||||
"inline normalization must remain visible",
|
||||
);
|
||||
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{
|
||||
names: {
|
||||
[address]: "rea_boundary_should_not_be_applied",
|
||||
__rea_missing_batch_address__: "missing",
|
||||
},
|
||||
},
|
||||
/Unknown Hopper address or name: __rea_missing_batch_address__/u,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), originalName);
|
||||
assert.equal(
|
||||
(await call("procedure_info", { procedure: address })).name,
|
||||
originalName,
|
||||
);
|
||||
|
||||
const renamed = "rea_boundary_renamed";
|
||||
assert.equal(
|
||||
await call("set_addresses_names", { names: { [address]: renamed } }).then(
|
||||
(value) => value[address],
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), renamed);
|
||||
const matches = await call("search_procedures", {
|
||||
pattern: renamed,
|
||||
case_sensitive: true,
|
||||
});
|
||||
assert.ok(
|
||||
matches.some(
|
||||
(item) => item.address === address && item.value === renamed,
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
(await call("analyze_function", { procedure: address })).procedure.name,
|
||||
renamed,
|
||||
);
|
||||
assert.equal(
|
||||
await call("set_address_name", { address, name: "add" }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: "add" }),
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("procedure_info", { procedure: "add" })).entrypoint,
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("read_bytes", { address: "add", length: 8 })).address,
|
||||
address,
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: address.slice(2) }),
|
||||
address,
|
||||
);
|
||||
|
||||
for (const query of ["-1", "0x10000000000000000", "0xffffffffffffffff"]) {
|
||||
for (const name of [
|
||||
"read_bytes",
|
||||
"address_to_file_offset",
|
||||
"resolve_containing_procedure",
|
||||
"procedure_info",
|
||||
]) {
|
||||
await invalid(
|
||||
name,
|
||||
name === "procedure_info"
|
||||
? { procedure: query }
|
||||
: {
|
||||
address: query,
|
||||
...(name === "read_bytes" ? { length: 8 } : {}),
|
||||
},
|
||||
/usable unsigned 64-bit range/u,
|
||||
);
|
||||
}
|
||||
}
|
||||
await invalid(
|
||||
"read_bytes",
|
||||
{ address: "0x0", length: 8 },
|
||||
/Address is outside every segment/u,
|
||||
);
|
||||
await invalid(
|
||||
"procedure_info",
|
||||
{ procedure: address, document: "__rea_missing_document__" },
|
||||
/Unknown Hopper document/u,
|
||||
);
|
||||
await invalid(
|
||||
"search_procedures",
|
||||
{ pattern: "[", mode: "regex" },
|
||||
/Invalid regex pattern/u,
|
||||
);
|
||||
// Invalid calls must leave the same authenticated bridge usable.
|
||||
assert.equal(
|
||||
(await call("read_bytes", { address, length: 8 })).returned_bytes,
|
||||
8,
|
||||
);
|
||||
const mapping = await call("address_to_file_offset", { address });
|
||||
const bytes = await call("read_bytes", { address, length: 8 });
|
||||
const file = await readFile(targetPath);
|
||||
assert.equal(
|
||||
bytes.bytes_hex,
|
||||
file
|
||||
.subarray(mapping.file_offset, mapping.file_offset + 8)
|
||||
.toString("hex"),
|
||||
);
|
||||
const segments = await call("list_segments");
|
||||
const containing = segments.find(
|
||||
(segment) =>
|
||||
BigInt(segment.start) <= BigInt(address) &&
|
||||
BigInt(address) < BigInt(segment.end),
|
||||
);
|
||||
assert.ok(containing, "fixture procedure has no containing segment");
|
||||
const lastByte = `0x${(BigInt(containing.end) - 1n).toString(16)}`;
|
||||
const prefix = await call("read_bytes", { address: lastByte, length: 8 });
|
||||
assert.equal(prefix.returned_bytes, 1);
|
||||
assert.equal(prefix.bytes_hex.length, 2);
|
||||
assert.equal(prefix.complete, false);
|
||||
const synthetic = segments.find(
|
||||
(segment) => segment.name === "External Symbols",
|
||||
);
|
||||
assert.ok(synthetic, "fixture omitted synthetic external-symbol memory");
|
||||
await invalid(
|
||||
"address_to_file_offset",
|
||||
{ address: synthetic.start },
|
||||
/no authoritative file-offset mapping/u,
|
||||
);
|
||||
const health = (await call("binary_session")).provider_operation_health;
|
||||
assert.equal(
|
||||
health.state,
|
||||
"idle",
|
||||
"a live GUI bridge must remain healthy after its launcher helper exits",
|
||||
);
|
||||
} finally {
|
||||
await call("set_address_name", { address, name: originalName ?? "" });
|
||||
await call("set_comment", { address, comment: originalComment ?? "" });
|
||||
await call("set_inline_comment", {
|
||||
address,
|
||||
comment: originalInlineComment ?? "",
|
||||
});
|
||||
await call("unset_bookmark", { address });
|
||||
}
|
||||
return {
|
||||
schemaCount: catalog.tools.length,
|
||||
successfulCalls,
|
||||
rejectedCalls,
|
||||
rejectedOperations: [...rejectedOperations],
|
||||
foreignDocumentVerified: foreignDocument !== undefined,
|
||||
navigationAndText,
|
||||
workflows,
|
||||
search,
|
||||
};
|
||||
}
|
||||
|
||||
/** Prove actual document disposal and shared CLI/MCP behavior on disposable copies. */
|
||||
export async function verifyHopperLifecycleAndCli(client, options, targets) {
|
||||
const directory = await realpath(
|
||||
await mkdtemp(join(tmpdir(), "hopper-boundary-")),
|
||||
);
|
||||
const suffix = basename(directory);
|
||||
const primary = join(directory, `primary-${suffix}`);
|
||||
const secondary = join(directory, `secondary-${suffix}`);
|
||||
const call = async (name, args = {}) =>
|
||||
requireMcpResult(
|
||||
await client.callTool({ name, arguments: args }, options),
|
||||
name,
|
||||
);
|
||||
const cli = promisify(execFile);
|
||||
const runCli = (args) => {
|
||||
const pending = cli(process.execPath, args, {
|
||||
timeout: 180_000,
|
||||
maxBuffer: 1_048_576,
|
||||
});
|
||||
if (pending.child.pid !== undefined)
|
||||
targets.ownedProcessIds.add(pending.child.pid);
|
||||
return pending;
|
||||
};
|
||||
try {
|
||||
await copyFile(targets.primary, primary);
|
||||
await copyFile(targets.secondary, secondary);
|
||||
await call("open_binary", { path: primary });
|
||||
const primaryProcedures = await call("list_procedures");
|
||||
const entry = primaryProcedures.find((item) =>
|
||||
item.value.endsWith("rea_entry"),
|
||||
);
|
||||
assert.ok(entry, "primary fixture omitted its entry procedure");
|
||||
const expectedFunction = await call("analyze_function", {
|
||||
procedure: entry.value,
|
||||
});
|
||||
const firstDocument = await call("current_document");
|
||||
await call("open_binary", { path: secondary });
|
||||
const secondDocument = await call("current_document");
|
||||
assert.notEqual(firstDocument, secondDocument);
|
||||
assert.ok(
|
||||
!(await call("list_documents")).includes(firstDocument),
|
||||
"target switching retained the old native document",
|
||||
);
|
||||
const procedures = await call("list_procedures");
|
||||
await verifyBatchCancellation(client, options, procedures);
|
||||
const address = procedures[0].address;
|
||||
const expected = await call("read_bytes", {
|
||||
address,
|
||||
length: 8,
|
||||
document: secondDocument,
|
||||
});
|
||||
await call("open_binary", { path: primary });
|
||||
const query = "REA_C_LEAF __rea_missing_phrase__";
|
||||
const expectedTrace = await call("trace_feature", { query });
|
||||
assert.deepEqual(expectedTrace.matches, []);
|
||||
await call("close_binary");
|
||||
|
||||
const dispatcher = new URL("../rea.mjs", import.meta.url).pathname;
|
||||
const common = [
|
||||
"--length",
|
||||
"8",
|
||||
"--provider",
|
||||
"hopper",
|
||||
"--format",
|
||||
"json",
|
||||
];
|
||||
const { stdout } = await runCli([
|
||||
dispatcher,
|
||||
"read-bytes",
|
||||
secondary,
|
||||
address,
|
||||
...common,
|
||||
]);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.equal(evidence.provider.id, "hopper");
|
||||
assert.equal(evidence.subject.local_path, secondary);
|
||||
assert.deepEqual(evidence.normalized_result, expected);
|
||||
const traced = await runCli([
|
||||
dispatcher,
|
||||
"trace",
|
||||
primary,
|
||||
query,
|
||||
"--provider",
|
||||
"hopper",
|
||||
"--format",
|
||||
"json",
|
||||
]);
|
||||
const traceEvidence = JSON.parse(traced.stdout);
|
||||
assert.equal(traceEvidence.subject.local_path, primary);
|
||||
assert.deepEqual(traceEvidence.normalized_result, expectedTrace);
|
||||
const analyzed = await runCli([
|
||||
dispatcher,
|
||||
"function",
|
||||
primary,
|
||||
entry.value,
|
||||
"--provider",
|
||||
"hopper",
|
||||
"--format",
|
||||
"json",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
JSON.parse(analyzed.stdout).normalized_result,
|
||||
expectedFunction,
|
||||
);
|
||||
let cliTerminalFunctionParity = null;
|
||||
let cliNamedSelectorParity = null;
|
||||
if (targets.unicode !== undefined) {
|
||||
const objc = join(directory, `objc-${suffix}`);
|
||||
await copyFile(targets.unicode, objc);
|
||||
await call("open_binary", { path: objc });
|
||||
const procedures = await call("list_procedures");
|
||||
const delegate = procedures.find(
|
||||
(item) => item.value === "-[REAWidget delegate]",
|
||||
);
|
||||
assert.ok(delegate, "Objective-C fixture omitted its tail-call method");
|
||||
const expected = await call("analyze_function", {
|
||||
procedure: delegate.address,
|
||||
});
|
||||
await call("close_binary");
|
||||
const analyzed = await runCli([
|
||||
dispatcher,
|
||||
"function",
|
||||
objc,
|
||||
delegate.address,
|
||||
"--provider",
|
||||
"hopper",
|
||||
"--format",
|
||||
"json",
|
||||
]);
|
||||
assert.deepEqual(JSON.parse(analyzed.stdout).normalized_result, expected);
|
||||
cliTerminalFunctionParity = true;
|
||||
await call("open_binary", { path: objc });
|
||||
cliNamedSelectorParity = await verifyHopperCliSelectors(
|
||||
call,
|
||||
runCli,
|
||||
dispatcher,
|
||||
objc,
|
||||
);
|
||||
}
|
||||
let failure;
|
||||
try {
|
||||
await runCli([
|
||||
dispatcher,
|
||||
"read-bytes",
|
||||
secondary,
|
||||
"0x10000000000000000",
|
||||
...common,
|
||||
]);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, 1);
|
||||
failure = JSON.parse(error.stdout);
|
||||
}
|
||||
assert.ok(failure, "CLI accepted an overflowing address");
|
||||
assert.equal(failure.code, "invalid_request");
|
||||
assert.equal(failure.category, "invalid_input");
|
||||
assert.match(failure.message, /usable unsigned 64-bit range/u);
|
||||
|
||||
await call("open_binary", { path: primary });
|
||||
assert.ok(
|
||||
!(await call("list_documents")).includes(secondDocument),
|
||||
"MCP or CLI cleanup retained the closed native document",
|
||||
);
|
||||
await call("close_binary");
|
||||
return {
|
||||
targetSwitchDisposedDocument: true,
|
||||
cliByteParity: true,
|
||||
cliInvalidAddress: true,
|
||||
cliLiteralTraceParity: true,
|
||||
cliFunctionDossierParity: true,
|
||||
cliTerminalFunctionParity,
|
||||
cliNamedSelectorParity,
|
||||
callerCancellationRecovered: true,
|
||||
closedDocumentAbsent: true,
|
||||
};
|
||||
} finally {
|
||||
await call("close_binary");
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const verifyBatchCancellation = async (client, options, procedures) => {
|
||||
const controller = new AbortController();
|
||||
let cancelled = false;
|
||||
await assert.rejects(
|
||||
client.callTool(
|
||||
{
|
||||
name: "batch_decompile",
|
||||
arguments: { addresses: procedures.map((item) => item.address) },
|
||||
},
|
||||
{
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
onprogress: (update) => {
|
||||
options.onprogress?.(update);
|
||||
// Cancel between real decompilations, after Hopper served one item.
|
||||
if (update.message?.includes("Hopper bridge completed request")) {
|
||||
cancelled = true;
|
||||
controller.abort();
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
/aborted/u,
|
||||
);
|
||||
assert.ok(cancelled, "cancellation did not follow a real Hopper response");
|
||||
const recovered = requireMcpResult(
|
||||
await client.callTool({ name: "list_procedures", arguments: {} }, options),
|
||||
"list_procedures after cancellation",
|
||||
);
|
||||
assert.deepEqual(recovered, procedures);
|
||||
const status = requireMcpResult(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }, options),
|
||||
"binary_session after cancellation",
|
||||
);
|
||||
assert.equal(status.provider_operation_health.state, "idle");
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Observe concrete Hopper runtime resources for a before/after cleanup check. */
|
||||
export function snapshotHopperRuntime(
|
||||
parent: string,
|
||||
targetLeaseDirectory: string,
|
||||
ownedProcessIds?: ReadonlySet<number>,
|
||||
): Promise<Set<string>>;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { lstat, readFile, readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Observe session paths and target leases without treating their shared parent as a session. */
|
||||
export async function snapshotHopperRuntime(
|
||||
parent,
|
||||
targetLeaseDirectory,
|
||||
ownedProcessIds,
|
||||
) {
|
||||
const resources = new Set();
|
||||
for (const entry of await readdir(parent, { withFileTypes: true })) {
|
||||
const path = join(parent, entry.name);
|
||||
if (!entry.name.startsWith("rea-") || path === targetLeaseDirectory)
|
||||
continue;
|
||||
if (
|
||||
ownedProcessIds !== undefined &&
|
||||
(!entry.isDirectory() ||
|
||||
!(await belongsToOwnedProcess(path, ownedProcessIds)))
|
||||
)
|
||||
continue;
|
||||
resources.add(path);
|
||||
}
|
||||
const leaseRoot = await lstat(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
if (leaseRoot === undefined) return resources;
|
||||
if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) {
|
||||
resources.add(targetLeaseDirectory);
|
||||
return resources;
|
||||
}
|
||||
const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
for (const name of leases ?? [])
|
||||
resources.add(join(targetLeaseDirectory, name));
|
||||
return resources;
|
||||
}
|
||||
|
||||
const missingIsAbsent = (cause) => {
|
||||
if (cause?.code === "ENOENT") return undefined;
|
||||
throw cause;
|
||||
};
|
||||
|
||||
const belongsToOwnedProcess = async (path, ownedProcessIds) => {
|
||||
const encoded = await readFile(join(path, "ownership.json"), "utf8").catch(
|
||||
missingIsAbsent,
|
||||
);
|
||||
if (encoded === undefined) return false;
|
||||
const ownership = JSON.parse(encoded);
|
||||
return (
|
||||
ownership !== null &&
|
||||
typeof ownership === "object" &&
|
||||
Number.isSafeInteger(ownership.parent_pid) &&
|
||||
ownedProcessIds.has(ownership.parent_pid)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Compare exact native symbol and literal selectors through MCP and the real CLI. */
|
||||
export async function verifyHopperCliSelectors(call, runCli, dispatcher, path) {
|
||||
const procedures = await call("list_procedures");
|
||||
const delegate = procedures.find(
|
||||
(item) => item.value === "-[REAWidget delegate]",
|
||||
);
|
||||
assert.ok(delegate, "Objective-C fixture omitted its tail-call method");
|
||||
const cases = [
|
||||
{
|
||||
command: "function",
|
||||
option: "procedure",
|
||||
operation: "analyze_function",
|
||||
parameters: { procedure: delegate.value },
|
||||
},
|
||||
{
|
||||
command: "instructions",
|
||||
option: "procedure",
|
||||
operation: "read_function_instructions",
|
||||
parameters: { procedure: delegate.value },
|
||||
},
|
||||
{
|
||||
command: "decompile",
|
||||
option: "procedure",
|
||||
operation: "procedure_pseudo_code",
|
||||
parameters: { procedure: delegate.value },
|
||||
},
|
||||
{
|
||||
command: "xrefs",
|
||||
option: "address",
|
||||
operation: "xrefs",
|
||||
parameters: { address: delegate.value },
|
||||
},
|
||||
{
|
||||
command: "search",
|
||||
option: "pattern",
|
||||
operation: "search_procedures",
|
||||
parameters: {
|
||||
pattern: delegate.value,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
},
|
||||
flags: ["--kind", "procedures", "--case-sensitive"],
|
||||
},
|
||||
{
|
||||
command: "trace",
|
||||
option: "query",
|
||||
operation: "trace_feature",
|
||||
parameters: { query: "--help" },
|
||||
},
|
||||
];
|
||||
const expected = [];
|
||||
for (const item of cases)
|
||||
expected.push(await call(item.operation, item.parameters));
|
||||
await call("close_binary");
|
||||
for (let i = 0; i < cases.length; i++) {
|
||||
const item = cases[i];
|
||||
const selector = item.parameters[item.option];
|
||||
const { stdout } = await runCli([
|
||||
dispatcher,
|
||||
item.command,
|
||||
path,
|
||||
`--${item.option}=${selector}`,
|
||||
...(item.flags ?? []),
|
||||
"--provider",
|
||||
"hopper",
|
||||
"--format",
|
||||
"json",
|
||||
]);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result,
|
||||
expected[i],
|
||||
`${item.command} named selector differs from MCP`,
|
||||
);
|
||||
assert.equal(
|
||||
evidence.parameters[item.option],
|
||||
selector,
|
||||
`${item.command} altered its selector`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
commands: cases.map(({ command }) => command),
|
||||
exactObjectiveCNames: true,
|
||||
literalGlobalFlagPreserved: true,
|
||||
};
|
||||
}
|
||||
@@ -21,12 +21,26 @@ export async function loadRealHopperFixtureTargets(manifestPath) {
|
||||
bindFixtureArtifact(root, large),
|
||||
],
|
||||
);
|
||||
requireDistinctArtifacts([primaryArtifact, secondaryArtifact, largeArtifact]);
|
||||
const objc = manifest.fixtures.find((fixture) => fixture?.name === "objc");
|
||||
const unicodeArtifact =
|
||||
objc === undefined
|
||||
? undefined
|
||||
: await bindFixtureArtifact(
|
||||
root,
|
||||
fixtureEntry(manifest.fixtures, "objc"),
|
||||
);
|
||||
requireDistinctArtifacts([
|
||||
primaryArtifact,
|
||||
secondaryArtifact,
|
||||
largeArtifact,
|
||||
...(unicodeArtifact === undefined ? [] : [unicodeArtifact]),
|
||||
]);
|
||||
return {
|
||||
manifestPath: canonicalManifest,
|
||||
primary: primaryArtifact,
|
||||
secondary: secondaryArtifact,
|
||||
large: largeArtifact,
|
||||
...(unicodeArtifact === undefined ? {} : { unicode: unicodeArtifact }),
|
||||
oracle,
|
||||
largeOracle,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
export const verifyHopperFunctionBasics = async (
|
||||
@@ -64,3 +65,117 @@ export const verifyHopperFunctionBasics = async (
|
||||
instructionWindowCount: instructions.instructions.length,
|
||||
};
|
||||
};
|
||||
|
||||
/** Check native terminal call sites across instruction, reference and dossier projections. */
|
||||
export async function verifyHopperTerminalInstructions(call) {
|
||||
const procedures = await call("list_procedures");
|
||||
const selected = procedures.filter(
|
||||
({ value }) =>
|
||||
value === "-[REAWidget delegate]" ||
|
||||
value.startsWith("imp___stubs__") ||
|
||||
value.startsWith("_objc_msgSend$"),
|
||||
);
|
||||
if (selected.length === 0)
|
||||
throw new Error("Objective-C fixture omitted terminal-call procedures");
|
||||
let terminalCalls = 0;
|
||||
for (const { address } of selected) {
|
||||
const references = await call("procedure_references", {
|
||||
procedure: address,
|
||||
direction: "outgoing",
|
||||
});
|
||||
const nativeCalls = references.references.filter(
|
||||
(edge) => edge.call !== undefined,
|
||||
);
|
||||
if (nativeCalls.length === 0)
|
||||
throw new Error(
|
||||
`Native fixture procedure ${address} omitted its terminal call`,
|
||||
);
|
||||
const dossier = await call("analyze_function", { procedure: address });
|
||||
const instructions = await call("read_function_instructions", {
|
||||
procedure: address,
|
||||
});
|
||||
const assembly = await call("procedure_assembly", { procedure: address });
|
||||
const info = await call("procedure_info", { procedure: address });
|
||||
for (const edge of nativeCalls) {
|
||||
const observed = dossier.outgoing_references.find(
|
||||
(item) =>
|
||||
item.source_address === edge.source_address &&
|
||||
item.target_address === edge.target_address,
|
||||
);
|
||||
assert.ok(
|
||||
observed,
|
||||
`Dossier discarded native call ${edge.source_address} -> ${edge.target_address}`,
|
||||
);
|
||||
assert.deepEqual(observed.call, edge.call);
|
||||
assert.ok(
|
||||
instructions.instructions.some((line) =>
|
||||
line.startsWith(`${edge.source_address}:`),
|
||||
),
|
||||
`Instruction inventory discarded native call site ${edge.source_address}`,
|
||||
);
|
||||
assert.ok(
|
||||
dossier.basic_blocks.some(
|
||||
({ start, end }) =>
|
||||
BigInt(start) <= BigInt(edge.source_address) &&
|
||||
BigInt(edge.source_address) < BigInt(end),
|
||||
),
|
||||
`Normalized block excluded native call site ${edge.source_address}`,
|
||||
);
|
||||
terminalCalls++;
|
||||
}
|
||||
assert.equal(assembly, instructions.instructions.join("\n"));
|
||||
assert.deepEqual(dossier.assembly, instructions.instructions);
|
||||
if (address === selected[0].address)
|
||||
await verifyHopperTerminalComment(
|
||||
call,
|
||||
address,
|
||||
nativeCalls.at(-1).source_address,
|
||||
);
|
||||
const expectedLength = dossier.basic_blocks.reduce(
|
||||
(total, { start, end }) => total + Number(BigInt(end) - BigInt(start)),
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
info.length,
|
||||
expectedLength,
|
||||
"Procedure length disagrees with normalized native blocks",
|
||||
);
|
||||
}
|
||||
if (terminalCalls === 0)
|
||||
throw new Error("Objective-C fixture omitted native terminal calls");
|
||||
return {
|
||||
procedures: selected.length,
|
||||
terminalCalls,
|
||||
normalizedBlockEndpoints: true,
|
||||
terminalCommentPreserved: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyHopperTerminalComment(call, address, terminal) {
|
||||
const original = await call("comment", { address: terminal });
|
||||
try {
|
||||
await call("set_comment", {
|
||||
address: terminal,
|
||||
comment: "REA terminal instruction comment",
|
||||
});
|
||||
const annotated = await call("analyze_function", {
|
||||
procedure: address,
|
||||
});
|
||||
if (
|
||||
!annotated.comments.some(
|
||||
(item) =>
|
||||
item.address === terminal &&
|
||||
item.kind === "comment" &&
|
||||
item.text === "REA terminal instruction comment",
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"Dossier discarded a comment on the native terminal instruction",
|
||||
);
|
||||
} finally {
|
||||
await call("set_comment", {
|
||||
address: terminal,
|
||||
comment: original ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify native cursor readback and reject unrepresentable text before mutations. */
|
||||
export async function verifyHopperNavigationAndText(call, invalid, address) {
|
||||
const cursor = await call("current_address");
|
||||
const procedures = await call("list_procedures");
|
||||
const other = procedures.find((item) => item.address !== address);
|
||||
assert.ok(other, "fixture omitted a second procedure for batch validation");
|
||||
const originalName = await call("address_name", { address });
|
||||
const originalComment = await call("comment", { address });
|
||||
const originalInline = await call("inline_comment", { address });
|
||||
const originalBookmark = (await call("list_bookmarks")).find(
|
||||
(item) => item.address === address,
|
||||
);
|
||||
try {
|
||||
await verifyNavigation(call, invalid, address, {
|
||||
originalName,
|
||||
originalComment,
|
||||
originalInline,
|
||||
});
|
||||
await verifyAnnotationText(call, invalid, { address, other, originalName });
|
||||
return {
|
||||
cursorReadback: true,
|
||||
interiorObjectNavigation: true,
|
||||
unmappedNavigationRejected: true,
|
||||
nativeTextValidationBeforeMutation: true,
|
||||
mappedAnnotationValidationBeforeMutation: true,
|
||||
unselectedLabelOwnersPreserved: true,
|
||||
explicitLabelSwap: true,
|
||||
observedLocalMetadata: true,
|
||||
};
|
||||
} finally {
|
||||
await call("goto_address", { address: cursor });
|
||||
await call("set_comment", { address, comment: originalComment ?? "" });
|
||||
await call("set_inline_comment", {
|
||||
address,
|
||||
comment: originalInline ?? "",
|
||||
});
|
||||
if (originalBookmark === undefined)
|
||||
await call("unset_bookmark", { address });
|
||||
else
|
||||
await call("set_bookmark", {
|
||||
address,
|
||||
...(originalBookmark.name === null
|
||||
? {}
|
||||
: { name: originalBookmark.name }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyNavigation(
|
||||
call,
|
||||
invalid,
|
||||
address,
|
||||
{ originalName, originalComment, originalInline },
|
||||
) {
|
||||
const instructions = await call("read_function_instructions", {
|
||||
procedure: address,
|
||||
});
|
||||
const info = await call("procedure_info", { procedure: address });
|
||||
assert.ok(info.locals.length > 0, "fixture omitted real stack locals");
|
||||
for (const local of info.locals) {
|
||||
assert.equal(typeof local.name, "string");
|
||||
assert.match(local.stack_displacement, /^-?\d+$/u);
|
||||
assert.ok(local.description.includes(local.name));
|
||||
assert.ok(local.description.includes(local.stack_displacement));
|
||||
assert.doesNotMatch(local.description, /object at 0x/u);
|
||||
}
|
||||
const dossier = await call("analyze_function", { procedure: address });
|
||||
assert.deepEqual(dossier.procedure.locals, info.locals);
|
||||
const coordinates = instructions.instructions.map(
|
||||
(line) => /^0x[0-9a-f]+/u.exec(line)?.[0],
|
||||
);
|
||||
assert.equal(coordinates[0], address);
|
||||
assert.ok(coordinates[1], "fixture omitted a second instruction");
|
||||
const interior = `0x${(BigInt(address) + 1n).toString(16)}`;
|
||||
assert.equal(await call("goto_address", { address: interior }), address);
|
||||
assert.equal(await call("current_address"), address);
|
||||
assert.equal(
|
||||
await call("next_address", { address: interior }),
|
||||
coordinates[1],
|
||||
);
|
||||
assert.equal(await call("prev_address", { address: interior }), address);
|
||||
for (const unmapped of ["0x0", "0xfffffffffffffffe"]) {
|
||||
for (const operation of ["goto_address", "next_address", "prev_address"])
|
||||
await invalid(
|
||||
operation,
|
||||
{ address: unmapped },
|
||||
/Address is outside every segment/u,
|
||||
);
|
||||
assert.equal(await call("current_address"), address);
|
||||
for (const operation of [
|
||||
"set_address_name",
|
||||
"set_bookmark",
|
||||
"unset_bookmark",
|
||||
"set_comment",
|
||||
"set_inline_comment",
|
||||
])
|
||||
await invalid(
|
||||
operation,
|
||||
{
|
||||
address: unmapped,
|
||||
...(operation === "unset_bookmark"
|
||||
? {}
|
||||
: operation.endsWith("comment")
|
||||
? { comment: "must_not_apply" }
|
||||
: { name: "must_not_apply" }),
|
||||
},
|
||||
/Address is outside every segment/u,
|
||||
);
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{ names: { [address]: "must_not_apply", [unmapped]: "unmapped" } },
|
||||
/Address is outside every segment/u,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), originalName);
|
||||
assert.equal(await call("comment", { address }), originalComment);
|
||||
assert.equal(await call("inline_comment", { address }), originalInline);
|
||||
assert.ok(
|
||||
!(await call("list_bookmarks")).some((item) => item.address === unmapped),
|
||||
);
|
||||
}
|
||||
const segments = await call("list_segments");
|
||||
const first = segments.reduce((left, right) =>
|
||||
BigInt(left.start) < BigInt(right.start) ? left : right,
|
||||
);
|
||||
const last = segments.reduce((left, right) =>
|
||||
BigInt(left.end) > BigInt(right.end) ? left : right,
|
||||
);
|
||||
await invalid(
|
||||
"prev_address",
|
||||
{ address: first.start },
|
||||
/No previous address exists in mapped memory/u,
|
||||
);
|
||||
await invalid(
|
||||
"next_address",
|
||||
{ address: `0x${(BigInt(last.end) - 1n).toString(16)}` },
|
||||
/No adjacent analyzed object exists in mapped memory/u,
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyAnnotationText(
|
||||
call,
|
||||
invalid,
|
||||
{ address, other, originalName },
|
||||
) {
|
||||
const comment = "REA representation boundary 测试";
|
||||
await call("set_comment", { address, comment });
|
||||
await call("set_inline_comment", { address, comment });
|
||||
await call("set_bookmark", { address, name: comment });
|
||||
for (const [text, reason] of [
|
||||
["before\0after", /contains a NUL character/u],
|
||||
["before\ud800after", /contains an unpaired Unicode surrogate/u],
|
||||
]) {
|
||||
for (const operation of ["procedure_info", "read_bytes"])
|
||||
await invalid(
|
||||
operation,
|
||||
operation === "procedure_info"
|
||||
? { procedure: text }
|
||||
: { address: text, length: 1 },
|
||||
reason,
|
||||
);
|
||||
for (const operation of ["set_address_name", "set_bookmark"])
|
||||
await invalid(operation, { address, name: text }, reason);
|
||||
for (const operation of ["set_comment", "set_inline_comment"])
|
||||
await invalid(operation, { address, comment: text }, reason);
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{ names: { [address]: "must_not_apply", [other.address]: text } },
|
||||
reason,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), originalName);
|
||||
assert.equal(
|
||||
await call("address_name", { address: other.address }),
|
||||
other.value,
|
||||
);
|
||||
assert.equal(await call("comment", { address }), comment);
|
||||
assert.equal(await call("inline_comment", { address }), comment);
|
||||
assert.equal(
|
||||
(await call("list_bookmarks")).find((item) => item.address === address)
|
||||
?.name,
|
||||
comment,
|
||||
);
|
||||
}
|
||||
await invalid(
|
||||
"set_address_name",
|
||||
{ address, name: other.value },
|
||||
/already assigned to/u,
|
||||
);
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{ names: { [address]: other.value } },
|
||||
/already assigned to/u,
|
||||
);
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{ names: { [address]: "duplicate", [other.address]: "duplicate" } },
|
||||
/assigns the same name more than once/u,
|
||||
);
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{ names: { [address]: "first", [address.slice(2)]: "second" } },
|
||||
/selects the same address more than once/u,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), originalName);
|
||||
assert.equal(
|
||||
await call("address_name", { address: other.address }),
|
||||
other.value,
|
||||
);
|
||||
try {
|
||||
const swapped = await call("set_addresses_names", {
|
||||
names: { [address]: other.value, [other.address]: originalName },
|
||||
});
|
||||
assert.equal(swapped[address], true);
|
||||
assert.equal(swapped[other.address], true);
|
||||
assert.equal(await call("address_name", { address }), other.value);
|
||||
assert.equal(
|
||||
await call("address_name", { address: other.address }),
|
||||
originalName,
|
||||
);
|
||||
} finally {
|
||||
await call("set_addresses_names", {
|
||||
names: { [address]: originalName, [other.address]: other.value },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Exercise literal/regex search, Unicode identities and cache invalidation in Hopper. */
|
||||
export async function verifyHopperSearch(call, invalid, procedure) {
|
||||
const address = procedure.address;
|
||||
const original = await call("address_name", { address });
|
||||
try {
|
||||
await verifySymbolNames(call, invalid, address);
|
||||
await verifySearchModes(call, address);
|
||||
} finally {
|
||||
await call("set_address_name", { address, name: original });
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", {
|
||||
pattern: original,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[{ address, value: original }],
|
||||
);
|
||||
return {
|
||||
unicodeIdentity: true,
|
||||
literalMetacharacters: true,
|
||||
longValuePreserved: true,
|
||||
symbolUtf16LimitValidatedBeforeMutation: true,
|
||||
regexAndCaseModes: true,
|
||||
annotationCacheInvalidation: true,
|
||||
escapedUtf8AndByteStrings: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifySymbolNames(call, invalid, address) {
|
||||
const unicode = "REA_é_e\u0301_😀_Straße_ſ_İ";
|
||||
const extentNames = ["x".repeat(1024), "é".repeat(1024), "😀".repeat(512)];
|
||||
for (const name of ["REA_a.b[0]", unicode, ...extentNames]) {
|
||||
assert.equal(await call("set_address_name", { address, name }), true);
|
||||
assert.equal(await call("address_name", { address }), name);
|
||||
assert.deepEqual(await call("list_names", { address }), [
|
||||
{ address, value: name },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", {
|
||||
pattern: name,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[{ address, value: name }],
|
||||
);
|
||||
assert.equal(await call("procedure_address", { procedure: name }), address);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", { pattern: unicode, mode: "literal" }),
|
||||
[],
|
||||
);
|
||||
const other = (await call("list_procedures")).find(
|
||||
(item) => item.address !== address,
|
||||
);
|
||||
assert.ok(other);
|
||||
for (const name of ["x".repeat(1025), "é".repeat(1025), "😀".repeat(513)]) {
|
||||
await invalid(
|
||||
"set_address_name",
|
||||
{ address, name },
|
||||
/UTF-16 code-unit symbol-name limit/u,
|
||||
);
|
||||
await invalid(
|
||||
"set_addresses_names",
|
||||
{ names: { [other.address]: "must_not_apply", [address]: name } },
|
||||
/UTF-16 code-unit symbol-name limit/u,
|
||||
);
|
||||
assert.equal(await call("address_name", { address }), extentNames.at(-1));
|
||||
assert.equal(
|
||||
await call("address_name", { address: other.address }),
|
||||
other.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifySearchModes(call, address) {
|
||||
await call("set_address_name", { address, name: "REA_Search_Upper" });
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", {
|
||||
pattern: "rea_search_upper",
|
||||
mode: "literal",
|
||||
case_sensitive: false,
|
||||
}),
|
||||
[{ address, value: "REA_Search_Upper" }],
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", {
|
||||
pattern: "rea_search_upper",
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", {
|
||||
pattern: "^REA_Search_(?:Upper|Lower)$",
|
||||
mode: "regex",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[{ address, value: "REA_Search_Upper" }],
|
||||
);
|
||||
const strings = await call("list_strings");
|
||||
const long = "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle";
|
||||
const longItem = strings.find((item) =>
|
||||
item.value.startsWith("REA_LONG_LITERAL_"),
|
||||
);
|
||||
assert.ok(longItem, "fixture omitted its real long literal");
|
||||
assert.ok(long.startsWith(longItem.value));
|
||||
assert.ok(longItem.provider_value.endsWith("…"));
|
||||
assert.equal(longItem.string.termination, "missing");
|
||||
const literalProcedure = (await call("list_procedures")).find((item) =>
|
||||
item.value.endsWith("rea_long_literal_procedure"),
|
||||
);
|
||||
assert.ok(literalProcedure);
|
||||
const dossier = await call("analyze_function", {
|
||||
procedure: literalProcedure.address,
|
||||
});
|
||||
const referenced = dossier.referenced_strings.find(
|
||||
(item) => item.address === longItem.address,
|
||||
);
|
||||
assert.ok(referenced, "function dossier omitted its long literal reference");
|
||||
const { source_address, ...stringRecord } = referenced;
|
||||
assert.ok(
|
||||
dossier.outgoing_references.some(
|
||||
(edge) =>
|
||||
edge.source_address === source_address &&
|
||||
edge.target_address === longItem.address,
|
||||
),
|
||||
);
|
||||
assert.deepEqual(
|
||||
stringRecord,
|
||||
longItem,
|
||||
"function dossier discarded typed string provenance",
|
||||
);
|
||||
|
||||
const fragments = strings.filter((item) => {
|
||||
const offset = BigInt(item.address) - BigInt(longItem.address);
|
||||
return offset >= 0n && offset < BigInt(Buffer.byteLength(long));
|
||||
});
|
||||
assert.equal(fragments.map((item) => item.value).join(""), long);
|
||||
for (const fragment of fragments) {
|
||||
assert.equal(
|
||||
Buffer.byteLength(fragment.value),
|
||||
fragment.string.byte_length -
|
||||
(fragment.string.termination === "missing" ? 0 : 1),
|
||||
);
|
||||
const bytes = await call("read_bytes", {
|
||||
address: fragment.address,
|
||||
length: fragment.string.byte_length,
|
||||
});
|
||||
const expected = Buffer.from(
|
||||
fragment.value + (fragment.string.termination === "missing" ? "" : "\0"),
|
||||
);
|
||||
assert.equal(bytes.bytes_hex, expected.toString("hex"));
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("search_strings", {
|
||||
pattern: "needle",
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[fragments.at(-1)],
|
||||
);
|
||||
const expected = strings.filter((item) =>
|
||||
/^REA_C_(?:ENTRY|LEAF)$/u.test(item.value),
|
||||
);
|
||||
assert.equal(expected.length, 2);
|
||||
assert.deepEqual(
|
||||
await call("search_strings", {
|
||||
pattern: "^REA_C_(?:ENTRY|LEAF)$",
|
||||
mode: "regex",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
expected,
|
||||
);
|
||||
await verifyHopperStringObjects(call, [
|
||||
{ value: "REA_UTF8_é_😀", encoding: "utf-8" },
|
||||
{ value: 'REA_ESCAPED_"\\line\nend\t\r', encoding: "utf-8" },
|
||||
{ value: "REA_LITERAL_BACKSLASH_\\n", encoding: "utf-8" },
|
||||
{ value: "REA_LITERAL_…", encoding: "utf-8" },
|
||||
{ value: "REA_LATIN1_ÿ", encoding: "latin-1" },
|
||||
]);
|
||||
}
|
||||
|
||||
/** Compare native string identities and values with the source fixture's bytes. */
|
||||
export async function verifyHopperStringObjects(call, expectations) {
|
||||
const strings = await call("list_strings");
|
||||
for (const { value, encoding } of expectations) {
|
||||
const item = strings.find((candidate) => candidate.value === value);
|
||||
assert.ok(
|
||||
item,
|
||||
`Hopper omitted the decoded string ${JSON.stringify(value)}`,
|
||||
);
|
||||
assert.equal(item.string.encoding, encoding);
|
||||
assert.equal(item.string.encoding_status, "inferred");
|
||||
assert.equal(item.string.termination, "present_or_not_required");
|
||||
const expectedBytes = Buffer.from(
|
||||
value + "\0",
|
||||
encoding.replaceAll("-", ""),
|
||||
);
|
||||
assert.equal(item.string.byte_length, expectedBytes.length);
|
||||
const bytes = await call("read_bytes", {
|
||||
address: item.address,
|
||||
length: expectedBytes.length,
|
||||
});
|
||||
assert.equal(bytes.bytes_hex, expectedBytes.toString("hex"));
|
||||
assert.deepEqual(await call("list_strings", { address: item.address }), [
|
||||
item,
|
||||
]);
|
||||
assert.deepEqual(
|
||||
await call("search_strings", {
|
||||
pattern: value,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[item],
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("search_strings", { pattern: "\\xC3\\xA9", mode: "literal" }),
|
||||
[],
|
||||
);
|
||||
return {
|
||||
sourceBytesVerified: true,
|
||||
inferredEncodingReported: true,
|
||||
displayEscapesExcludedFromMatching: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** A failed or cancelled pathological regex must leave the real Hopper API usable. */
|
||||
export async function verifyHopperRegexIsolation(
|
||||
client,
|
||||
options,
|
||||
call,
|
||||
address,
|
||||
) {
|
||||
const original = await call("address_name", { address });
|
||||
const pathological = "a".repeat(40) + "!";
|
||||
try {
|
||||
await call("set_address_name", { address, name: pathological });
|
||||
const reply = await client.callTool(
|
||||
{
|
||||
name: "search_procedures",
|
||||
arguments: { pattern: "(a+)+$", mode: "regex", case_sensitive: true },
|
||||
},
|
||||
options,
|
||||
);
|
||||
assert.equal(reply.isError, true);
|
||||
const { error } = JSON.parse(mcpTextValue(reply));
|
||||
assert.equal(error.code, "resource_constraint");
|
||||
assert.equal(error.details.operation, "search_procedures");
|
||||
assert.equal(error.details.resource, "cpu");
|
||||
assert.equal(await call("address_name", { address }), pathological);
|
||||
const controller = new AbortController();
|
||||
const pending = client.callTool(
|
||||
{
|
||||
name: "search_procedures",
|
||||
arguments: { pattern: "(a+)+$", mode: "regex", case_sensitive: true },
|
||||
},
|
||||
{ ...options, signal: controller.signal },
|
||||
);
|
||||
const cancelled = pending.catch((cause) => cause);
|
||||
// The same small inventory has already completed once; leave the pathological
|
||||
// matching worker running before cancelling the second caller.
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
controller.abort();
|
||||
await cancelled;
|
||||
assert.equal(await call("address_name", { address }), pathological);
|
||||
assert.deepEqual(
|
||||
await call("search_procedures", {
|
||||
pattern: pathological,
|
||||
mode: "literal",
|
||||
case_sensitive: true,
|
||||
}),
|
||||
[{ address, value: pathological }],
|
||||
);
|
||||
} finally {
|
||||
await call("set_address_name", { address, name: original });
|
||||
}
|
||||
return {
|
||||
regexDeadlinePreservesSession: true,
|
||||
regexCancellationPreservesSession: true,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { requireFunctionDossierOracle } from "../../dist/application/RealHopperAssertions.js";
|
||||
|
||||
/** Require truthful tri-state permissions from real Hopper memory regions. */
|
||||
@@ -57,7 +58,9 @@ export function requireBridgeDiagnostic(chunks) {
|
||||
(diagnostic) =>
|
||||
diagnostic.type === "bridge-diagnostic" &&
|
||||
diagnostic.category === "invalid_request" &&
|
||||
diagnostic.message === "Invalid Hopper bridge request",
|
||||
diagnostic.message.includes(
|
||||
"Unknown Hopper address or name: __rea_missing_procedure__",
|
||||
),
|
||||
)
|
||||
)
|
||||
throw new Error("The MCP runtime omitted the correlated Hopper diagnostic");
|
||||
@@ -120,6 +123,44 @@ export async function verifyRealHopperFixture({
|
||||
require_assembly: true,
|
||||
},
|
||||
);
|
||||
const calls = entryDossier.outgoing_references.filter(
|
||||
(edge) => edge.call?.classification === "direct",
|
||||
);
|
||||
assert.ok(
|
||||
calls.some((edge) => edge.target_address === branch.address),
|
||||
"Hopper native direct-call classification was discarded",
|
||||
);
|
||||
const observed = normalizedResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "procedure_references",
|
||||
arguments: { procedure: entry.address, direction: "outgoing" },
|
||||
},
|
||||
options,
|
||||
),
|
||||
"procedure_references native calls",
|
||||
);
|
||||
assert.deepEqual(entryDossier.unresolved_calls, observed.unresolved_calls);
|
||||
for (const call of calls) {
|
||||
assert.equal(call.call.provider_type, 2);
|
||||
assert.equal(
|
||||
call.call.provenance,
|
||||
"hopper-public-python-api:CallReference",
|
||||
);
|
||||
assert.equal(
|
||||
call.kind.available,
|
||||
false,
|
||||
"partial call metadata invented detailed reference flags",
|
||||
);
|
||||
assert.deepEqual(
|
||||
observed.references.find(
|
||||
(edge) =>
|
||||
edge.source_address === call.source_address &&
|
||||
edge.target_address === call.target_address,
|
||||
)?.call,
|
||||
call.call,
|
||||
);
|
||||
}
|
||||
const branchDossier = requireFunctionDossierOracle(
|
||||
await analyzeFixtureProcedure(client, options, branch, normalizedResult),
|
||||
{
|
||||
@@ -153,7 +194,13 @@ export async function verifyRealHopperFixture({
|
||||
}
|
||||
|
||||
/** Reject malformed, unsafe, or unexpected failure diagnostics from MCP. */
|
||||
export function requireSafeDiagnostics(chunks) {
|
||||
export function requireSafeDiagnostics(
|
||||
chunks,
|
||||
expectedErrorTools = ["procedure_info"],
|
||||
expectedErrorTags = ["HopperRemoteError"],
|
||||
) {
|
||||
const expected = new Set(expectedErrorTools);
|
||||
const tags = new Set(expectedErrorTags);
|
||||
const lines = chunks
|
||||
.join("")
|
||||
.split("\n")
|
||||
@@ -173,10 +220,8 @@ export function requireSafeDiagnostics(chunks) {
|
||||
typeof diagnostic.level !== "number" ||
|
||||
diagnostic.level >= 50 ||
|
||||
(diagnostic.level >= 40 &&
|
||||
diagnostic.errorTag !== "HopperRemoteError" &&
|
||||
!(
|
||||
diagnostic.tool === "procedure_info" && diagnostic.status === "error"
|
||||
))
|
||||
!tags.has(diagnostic.errorTag) &&
|
||||
!(expected.has(diagnostic.tool) && diagnostic.status === "error"))
|
||||
)
|
||||
throw new Error(`The MCP runtime emitted unsafe stderr: ${line}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Exercise source replacement, removal, and restoration against a loaded native document. */
|
||||
export async function verifyHopperSourceBinding({
|
||||
client,
|
||||
call,
|
||||
path,
|
||||
file,
|
||||
address,
|
||||
mapping,
|
||||
relocate,
|
||||
}) {
|
||||
const originalHash = createHash("sha256").update(file).digest("hex");
|
||||
const changed = relocate === undefined ? Buffer.from(file) : relocate(file);
|
||||
if (relocate === undefined) changed[changed.length - 1] ^= 1;
|
||||
const changedHash = createHash("sha256").update(changed).digest("hex");
|
||||
const at = (delta) => `0x${(BigInt(address) + BigInt(delta)).toString(16)}`;
|
||||
const observeFailure = async (delta) => {
|
||||
const reply = await client.callTool(
|
||||
{ name: "address_to_file_offset", arguments: { address: at(delta) } },
|
||||
{ timeout: 180_000 },
|
||||
);
|
||||
assert.equal(
|
||||
reply.isError,
|
||||
true,
|
||||
"mapping unexpectedly accepted an unbound source",
|
||||
);
|
||||
const error = JSON.parse(mcpTextValue(reply)).error;
|
||||
const partial = error.details.partial_observation;
|
||||
assert.equal(partial.operation, "address_to_file_offset");
|
||||
assert.equal(partial.address, at(delta));
|
||||
assert.equal(
|
||||
partial.provider_file_offset,
|
||||
mapping.provider_file_offset + delta,
|
||||
);
|
||||
assert.equal(partial.original_file_offset.available, false);
|
||||
assert.equal(typeof partial.original_file_offset.reason, "string");
|
||||
assert.match(partial.provider_image_header_hex, /^[a-f0-9]{24}$/u);
|
||||
return error;
|
||||
};
|
||||
try {
|
||||
await writeFile(path, changed);
|
||||
const error = await observeFailure(2);
|
||||
assert.equal(error.code, "artifact_changed");
|
||||
assert.ok(error.details.reason.includes(originalHash));
|
||||
assert.ok(error.details.reason.includes(changedHash));
|
||||
assert.equal((await call("binary_session")).sha256, originalHash);
|
||||
assert.equal(
|
||||
(await call("read_bytes", { address: at(2), length: 1 })).bytes_hex,
|
||||
file
|
||||
.subarray(mapping.file_offset + 2, mapping.file_offset + 3)
|
||||
.toString("hex"),
|
||||
);
|
||||
} finally {
|
||||
await writeFile(path, file);
|
||||
}
|
||||
const renamed = `${path}.temporarily-moved`;
|
||||
await rename(path, renamed);
|
||||
try {
|
||||
const error = await observeFailure(3);
|
||||
assert.equal(error.code, "target_unavailable");
|
||||
assert.match(error.details.reason, /ENOENT/u);
|
||||
} finally {
|
||||
await rename(renamed, path);
|
||||
}
|
||||
await rename(path, renamed);
|
||||
try {
|
||||
await mkdir(path);
|
||||
const error = await observeFailure(4);
|
||||
assert.equal(error.code, "artifact_changed");
|
||||
assert.match(error.details.reason, /no longer a regular file/u);
|
||||
} finally {
|
||||
await rm(path, { recursive: true, force: true });
|
||||
await rename(renamed, path);
|
||||
}
|
||||
const permissionDenied = process.getuid() !== 0;
|
||||
if (permissionDenied) {
|
||||
const mode = (await stat(path)).mode & 0o777;
|
||||
try {
|
||||
await chmod(path, 0);
|
||||
const error = await observeFailure(5);
|
||||
assert.equal(error.code, "access_denied");
|
||||
assert.ok(["EACCES", "EPERM"].includes(error.details.system_code));
|
||||
} finally {
|
||||
await chmod(path, mode);
|
||||
}
|
||||
}
|
||||
for (const delta of [2, 3, 4, ...(permissionDenied ? [5] : [])]) {
|
||||
const recovered = await call("address_to_file_offset", {
|
||||
address: at(delta),
|
||||
});
|
||||
assert.equal(recovered.file_offset, mapping.file_offset + delta);
|
||||
}
|
||||
return {
|
||||
replacement: relocate === undefined ? "same_size_bytes" : "relocated_slice",
|
||||
missingSource: "verified",
|
||||
nonregularSource: "verified",
|
||||
permissionDenied: permissionDenied ? "verified" : "not_run_root",
|
||||
nativePartialEvidence: "verified",
|
||||
recovery: "verified",
|
||||
};
|
||||
}
|
||||
|
||||
/** Relocate the sole FAT slice while retaining its exact loaded bytes and CPU identity. */
|
||||
export function relocateSingleFatSlice(file) {
|
||||
const wide = file.readUInt32BE(0) === 0xcafebabf;
|
||||
assert.equal(file.readUInt32BE(4), 1);
|
||||
const offset = wide
|
||||
? Number(file.readBigUInt64BE(16))
|
||||
: file.readUInt32BE(16);
|
||||
const size = wide ? Number(file.readBigUInt64BE(24)) : file.readUInt32BE(20);
|
||||
const relocated = Buffer.alloc(offset * 2 + size);
|
||||
file.subarray(0, offset).copy(relocated);
|
||||
file.subarray(offset, offset + size).copy(relocated, offset * 2);
|
||||
if (wide) relocated.writeBigUInt64BE(BigInt(offset * 2), 16);
|
||||
else relocated.writeUInt32BE(offset * 2, 16);
|
||||
return relocated;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Ground literal matching and graph identity in a real provider's function chain. */
|
||||
export async function verifyHopperWorkflows(call, invalid, procedure) {
|
||||
const address = procedure.address;
|
||||
const originalName = await call("address_name", { address });
|
||||
const callees = await call("procedure_callees", { procedure: address });
|
||||
assert.ok(callees.length > 0, "fixture omitted a reachable call target");
|
||||
const goal = callees[0];
|
||||
const goalName = await call("address_name", { address: goal });
|
||||
assert.equal(typeof goalName, "string");
|
||||
const canonicalPath = await call("trace_call_path", { start: address, goal });
|
||||
assert.equal(canonicalPath.goal_status, "reached");
|
||||
assert.deepEqual(
|
||||
await call("trace_call_path", { start: originalName, goal: goalName }),
|
||||
canonicalPath,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call("trace_call_path", {
|
||||
start: address.toUpperCase(),
|
||||
goal: `0x${(BigInt(goal) + 1n).toString(16)}`,
|
||||
}),
|
||||
canonicalPath,
|
||||
);
|
||||
const graph = await call("get_call_graph", { address });
|
||||
assert.deepEqual(
|
||||
await call("get_call_graph", { address: originalName }),
|
||||
graph,
|
||||
);
|
||||
assert.deepEqual(
|
||||
await call("get_call_graph", { address: address.toUpperCase() }),
|
||||
graph,
|
||||
);
|
||||
const procedures = await call("list_procedures");
|
||||
const cycle = procedures.filter((item) =>
|
||||
/rea_cycle_[ab]$/u.test(item.value),
|
||||
);
|
||||
assert.equal(cycle.length, 2, "fixture omitted its source-owned call cycle");
|
||||
const cycleGraph = await call("get_call_graph", { address: cycle[0].value });
|
||||
const cycleNodes = Object.values(cycleGraph).flat();
|
||||
assert.equal(cycleNodes.length, 2);
|
||||
assert.deepEqual(
|
||||
cycleNodes.map((node) => node.address).sort(),
|
||||
cycle.map((item) => item.address).sort(),
|
||||
);
|
||||
for (const node of cycleNodes) {
|
||||
assert.equal(node.status, "ok");
|
||||
assert.deepEqual(
|
||||
node.calls,
|
||||
cycle
|
||||
.filter((item) => item.address !== node.address)
|
||||
.map((item) => item.address),
|
||||
);
|
||||
}
|
||||
await invalid(
|
||||
"trace_call_path",
|
||||
{ start: "__rea_missing_procedure__", goal: "__rea_missing_procedure__" },
|
||||
/Unknown Hopper address or name/u,
|
||||
"procedure_address",
|
||||
);
|
||||
const missingPhrase = await call("trace_feature", {
|
||||
query: "REA_C_LEAF __rea_missing_phrase__",
|
||||
});
|
||||
assert.deepEqual(missingPhrase.matches, []);
|
||||
assert.deepEqual(missingPhrase.references, []);
|
||||
const phrase = "REA boundary phrase";
|
||||
try {
|
||||
await call("set_address_name", { address, name: phrase });
|
||||
const traced = await call("trace_feature", { query: phrase });
|
||||
assert.deepEqual(traced.matches, [
|
||||
{ type: "procedure", address, value: phrase },
|
||||
]);
|
||||
const spaced = await call("trace_feature", { query: ` ${phrase} ` });
|
||||
assert.deepEqual(spaced.matches, []);
|
||||
assert.deepEqual(spaced.references, []);
|
||||
} finally {
|
||||
await call("set_address_name", { address, name: originalName });
|
||||
}
|
||||
return {
|
||||
canonicalGraphIdentities: true,
|
||||
sourceOwnedCallCycle: true,
|
||||
symbolAndInteriorGoalParity: true,
|
||||
invalidSelfPathRejected: true,
|
||||
completeLiteralQueryPreserved: true,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ const shaSchema = z.string().regex(/^[a-f0-9]{40}$/u);
|
||||
const versionSchema = z.object({ version: z.string() });
|
||||
const manifestSchema = z.object({ ".": z.string() });
|
||||
const sectionsSchema = z.object({
|
||||
versioning: z.string().optional(),
|
||||
"changelog-sections": z.array(
|
||||
z.object({ type: z.string(), hidden: z.boolean().optional() }),
|
||||
),
|
||||
@@ -72,6 +73,14 @@ function readCommits(text) {
|
||||
breaking:
|
||||
/^[a-z]+(?:\([^)]*\))?!: /u.test(title) ||
|
||||
/^BREAKING(?: CHANGE|-CHANGE):\s*\S/mu.test(body),
|
||||
breakingNotes: Array.from(
|
||||
body
|
||||
.replace(/\r\n/gu, "\n")
|
||||
.matchAll(
|
||||
/^BREAKING(?: CHANGE|-CHANGE):[ \t]*(\S[^\n]*(?:\n(?!\n|[A-Za-z-]+: )[^\n]+)*)/gmu,
|
||||
),
|
||||
(match) => match[1].replace(/\s+/gu, " ").trim(),
|
||||
),
|
||||
});
|
||||
}
|
||||
return commits;
|
||||
@@ -206,8 +215,15 @@ async function validateCandidate(git, sha, version, commits) {
|
||||
version,
|
||||
);
|
||||
const breakingNotes = breakingSection(notes);
|
||||
const normalizedBreakingNotes = breakingNotes.replace(/\s+/gu, " ");
|
||||
const missingBreaking = commits.filter(
|
||||
(commit) => commit.breaking && !referencesCommit(breakingNotes, commit),
|
||||
(commit) =>
|
||||
commit.breaking &&
|
||||
!referencesCommit(breakingNotes, commit) &&
|
||||
(commit.breakingNotes.length === 0 ||
|
||||
commit.breakingNotes.some(
|
||||
(note) => !normalizedBreakingNotes.includes(note),
|
||||
)),
|
||||
);
|
||||
if (missingBreaking.length > 0) {
|
||||
throw new Error(
|
||||
@@ -223,14 +239,24 @@ export async function inspectReleaseCheckpoint(git, options) {
|
||||
.object({
|
||||
phase: z.enum(["prepare", "publish"]),
|
||||
stage: z.enum(["source", "candidate"]),
|
||||
releaseBranch: z.string().startsWith("release/"),
|
||||
releaseBranch: z.union([
|
||||
z.literal("main"),
|
||||
z.string().startsWith("release/"),
|
||||
]),
|
||||
sourceSha: shaSchema,
|
||||
candidateSha: shaSchema.optional(),
|
||||
})
|
||||
.parse(options);
|
||||
if (releaseBranch === "main" && (phase !== "publish" || stage !== "source")) {
|
||||
throw new Error(
|
||||
"Main publication validates its reviewed merged source SHA.",
|
||||
);
|
||||
}
|
||||
const expectedVersion = releaseVersion(
|
||||
releaseBranch.slice("release/".length),
|
||||
"release/VERSION",
|
||||
releaseBranch === "main"
|
||||
? (await readJson(git, sourceSha, "package.json", versionSchema)).version
|
||||
: releaseBranch.slice("release/".length),
|
||||
releaseBranch === "main" ? "reviewed main release" : "release/VERSION",
|
||||
);
|
||||
if (phase === "publish" && stage === "candidate") {
|
||||
throw new Error(
|
||||
@@ -297,6 +323,7 @@ export async function inspectReleaseCheckpoint(git, options) {
|
||||
).filter((commit) => commit.breaking || visibleTypes.has(commit.type));
|
||||
const breaking = commits.filter((commit) => commit.breaking);
|
||||
if (
|
||||
configuration.versioning !== "always-bump-minor" &&
|
||||
breaking.length > 0 &&
|
||||
major(baselineVersion) > 0 &&
|
||||
major(expectedVersion) <= major(baselineVersion)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import semver from "semver";
|
||||
|
||||
const version = process.argv[2];
|
||||
if (version === undefined || semver.valid(version) !== version) {
|
||||
console.error("Expected an exact SemVer version.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write(semver.prerelease(version) === null ? "latest" : "next");
|
||||
}
|
||||
@@ -13,6 +13,7 @@ if (!executable)
|
||||
const profile = await mkdtemp(join(tmpdir(), "rea-browser-dom-"));
|
||||
const site = await startBrowserVerifierSite();
|
||||
let context;
|
||||
let report;
|
||||
try {
|
||||
context = await chromium.launchPersistentContext(profile, {
|
||||
executablePath: executable,
|
||||
@@ -43,11 +44,15 @@ try {
|
||||
process.argv[2],
|
||||
page.url(),
|
||||
);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...result, browser: context.browser().version(), native_oracle: true, verified: true })}\n`,
|
||||
);
|
||||
report = {
|
||||
...result,
|
||||
browser: context.browser().version(),
|
||||
native_oracle: true,
|
||||
verified: true,
|
||||
};
|
||||
} finally {
|
||||
await context?.close();
|
||||
await site.close();
|
||||
await rm(profile, { recursive: true, force: true });
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { access } from "node:fs/promises";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { verifyBrowserSourceMap } from "./lib/browser-source-map-e2e.mjs";
|
||||
import { verifyBrowserNestedSourceMaps } from "./lib/browser-nested-source-map-e2e.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
const executable = process.env.REA_BROWSER_EXECUTABLE;
|
||||
if (!executable || !isAbsolute(executable))
|
||||
@@ -11,6 +12,7 @@ if (!executable || !isAbsolute(executable))
|
||||
await access(executable);
|
||||
const run = createVerifierRun();
|
||||
const proof = await verifyBrowserSourceMap(executable, process.argv[2]);
|
||||
const nested = await verifyBrowserNestedSourceMaps(executable, process.argv[2]);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
|
||||
`${JSON.stringify({ ...proof, nested_source_maps: nested, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
|
||||
);
|
||||
|
||||
@@ -160,6 +160,24 @@ try {
|
||||
if (mode === "mcp") assert.deepEqual(value.result, report);
|
||||
return report;
|
||||
};
|
||||
const ownershipProbe = join(root.path, "ownership-prerequisite.hex");
|
||||
await writeFile(ownershipProbe, "0x00");
|
||||
const missingInspection = await inspect(
|
||||
"cli",
|
||||
ownershipProbe,
|
||||
"hex",
|
||||
"unsupported_provider",
|
||||
{
|
||||
command: process.execPath,
|
||||
prefix: [],
|
||||
client,
|
||||
environment: { ...environment, PATH: root.path },
|
||||
},
|
||||
);
|
||||
assert.equal(missingInspection.code, "capability_unavailable");
|
||||
assert.match(missingInspection.message, /procps-compatible ps/u);
|
||||
assert.equal(missingInspection.details.operation, "prepare_owned_process");
|
||||
cases++;
|
||||
for (const [name, optimizer, viaIR] of [
|
||||
["plain", false, false],
|
||||
["optimized", true, false],
|
||||
|
||||
+127
-21
@@ -27,6 +27,7 @@ for (const tool of ["clang", "lldb", "codesign", "nm"])
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
const developerMode = await exec("/usr/sbin/DevToolsSecurity", ["-status"]);
|
||||
|
||||
const SOURCE = fileURLToPath(
|
||||
new URL("../tests/conformance/native/calls.m", import.meta.url),
|
||||
@@ -43,13 +44,17 @@ const INPUT = {
|
||||
{ kind: "function", name: "rea_call_add" },
|
||||
],
|
||||
environment: { REA_CALLS_MARKER: "observed" },
|
||||
// Entry stops and Objective-C runtime reads slow the real target on busy hosts.
|
||||
duration_ms: 30_000,
|
||||
argument_registers: 4,
|
||||
backtrace_frames: 1,
|
||||
};
|
||||
const CLI_TIMEOUT_MS = 180_000;
|
||||
|
||||
/** Pointer-free facts that two runs of the same scenario must share. */
|
||||
const stable = (result) =>
|
||||
result.events.map((event) => ({
|
||||
breakpoint_index: event.breakpoint_index,
|
||||
symbol: event.symbol,
|
||||
file_address: event.file_address,
|
||||
selector: event.selector,
|
||||
@@ -68,17 +73,23 @@ const symbolAddress = async (binary, symbol) => {
|
||||
};
|
||||
|
||||
const checkScenario = async (binary) => {
|
||||
const result = await artifactCli("observe-native-calls", binary, [
|
||||
JSON.stringify(INPUT),
|
||||
]);
|
||||
const result = await artifactCli(
|
||||
"observe-native-calls",
|
||||
binary,
|
||||
[JSON.stringify(INPUT)],
|
||||
{ timeoutMs: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(result.process.outcome, "exited");
|
||||
assert.equal(result.process.exit_status, 0);
|
||||
assert.equal(result.process.terminated, true);
|
||||
assert.match(result.process.stdout.text, /^world x0 40\n/u);
|
||||
assert.match(result.process.stdout.text, /marker observed\n$/u);
|
||||
assert.equal(result.process.stdout.complete, true);
|
||||
assert.equal(result.process.stderr.complete, true);
|
||||
assert.deepEqual(result.coverage, {
|
||||
status: "complete",
|
||||
event_limit_reached: false,
|
||||
resource_limit_reached: false,
|
||||
unresolved_breakpoints: [],
|
||||
});
|
||||
const greet = result.events.filter(({ symbol }) => symbol === GREET);
|
||||
@@ -115,33 +126,67 @@ const alive = (pid) => {
|
||||
};
|
||||
|
||||
const checkBounds = async (binary) => {
|
||||
const limited = await artifactCli("observe-native-calls", binary, [
|
||||
JSON.stringify({
|
||||
breakpoints: [{ kind: "function", name: "nanosleep" }],
|
||||
arguments: ["--wait"],
|
||||
max_events: 2,
|
||||
duration_ms: 30_000,
|
||||
}),
|
||||
]);
|
||||
const flooded = await artifactCli(
|
||||
"observe-native-calls",
|
||||
binary,
|
||||
[
|
||||
JSON.stringify({
|
||||
breakpoints: [{ kind: "function", name: "rea_never_called" }],
|
||||
arguments: ["--flood"],
|
||||
duration_ms: 30_000,
|
||||
}),
|
||||
],
|
||||
{ timeoutMs: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(flooded.process.outcome, "exited");
|
||||
assert.equal(flooded.process.stdout.bytes, 2 * 1024 * 1024);
|
||||
assert.equal(flooded.process.stderr.bytes, 2 * 1024 * 1024);
|
||||
assert.equal(flooded.process.stdout.text.length, 1024 * 1024);
|
||||
assert.equal(flooded.process.stderr.text.length, 1024 * 1024);
|
||||
assert.equal(flooded.process.stdout.text, "O".repeat(1024 * 1024));
|
||||
assert.equal(flooded.process.stderr.text, "E".repeat(1024 * 1024));
|
||||
assert.equal(flooded.process.stdout.truncated, true);
|
||||
assert.equal(flooded.process.stderr.truncated, true);
|
||||
assert.equal(flooded.process.stdout.complete, true);
|
||||
assert.equal(flooded.process.stderr.complete, true);
|
||||
|
||||
const limited = await artifactCli(
|
||||
"observe-native-calls",
|
||||
binary,
|
||||
[
|
||||
JSON.stringify({
|
||||
breakpoints: [{ kind: "function", name: "rea_call_add" }],
|
||||
max_events: 2,
|
||||
duration_ms: 30_000,
|
||||
}),
|
||||
],
|
||||
{ timeoutMs: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(limited.process.outcome, "event-limit");
|
||||
assert.equal(limited.events.length, 2);
|
||||
assert.equal(limited.process.terminated, true);
|
||||
assert.equal(alive(limited.process.pid), false);
|
||||
const elapsed = await artifactCli("observe-native-calls", binary, [
|
||||
JSON.stringify({
|
||||
breakpoints: [{ kind: "function", name: "rea_call_add" }],
|
||||
arguments: ["--wait"],
|
||||
duration_ms: 1_500,
|
||||
}),
|
||||
]);
|
||||
const elapsed = await artifactCli(
|
||||
"observe-native-calls",
|
||||
binary,
|
||||
[
|
||||
JSON.stringify({
|
||||
breakpoints: [{ kind: "function", name: "rea_call_add" }],
|
||||
arguments: ["--wait"],
|
||||
duration_ms: 1_500,
|
||||
}),
|
||||
],
|
||||
{ timeoutMs: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(elapsed.process.outcome, "duration-elapsed");
|
||||
assert.equal(elapsed.events.length, 0);
|
||||
assert.equal(alive(elapsed.process.pid), false);
|
||||
};
|
||||
|
||||
const cliFailure = async (binary) => {
|
||||
let output;
|
||||
try {
|
||||
await exec(
|
||||
const completed = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(new URL("rea.mjs", import.meta.url)),
|
||||
@@ -150,12 +195,16 @@ const cliFailure = async (binary) => {
|
||||
JSON.stringify(INPUT),
|
||||
"--json",
|
||||
],
|
||||
{ env: { ...process.env, REA_LOG_LEVEL: "silent" }, timeout: 120_000 },
|
||||
{
|
||||
env: { ...process.env, REA_LOG_LEVEL: "silent" },
|
||||
timeout: CLI_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
output = completed.stdout;
|
||||
} catch (cause) {
|
||||
return JSON.parse(cause.stdout);
|
||||
}
|
||||
throw new Error("observe-native-calls unexpectedly succeeded");
|
||||
throw new Error(`observe-native-calls unexpectedly succeeded: ${output}`);
|
||||
};
|
||||
|
||||
/** Hardened runtime blocks the debugger unless get-task-allow is granted. */
|
||||
@@ -174,6 +223,22 @@ const checkHardenedRuntime = async (directory, binary) => {
|
||||
const denied = await cliFailure(blocked);
|
||||
assert.equal(denied.code, "capability_unavailable");
|
||||
assert.match(denied.details.reason, /^debugger-attach-denied: /u);
|
||||
await withArtifactMcp(blocked, async (client) => {
|
||||
const deniedMcp = await client.callTool(
|
||||
{
|
||||
name: "observe_native_calls",
|
||||
arguments: INPUT,
|
||||
},
|
||||
{ timeout: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(deniedMcp.isError, true, JSON.stringify(deniedMcp));
|
||||
assert.equal(deniedMcp.structuredContent.error.code, denied.code);
|
||||
assert.match(
|
||||
deniedMcp.structuredContent.error.details.reason,
|
||||
/^debugger-attach-denied: /u,
|
||||
);
|
||||
assert.ok(deniedMcp.structuredContent.error.details.partial_observation);
|
||||
});
|
||||
const allowed = join(directory, "debuggable");
|
||||
const entitlements = join(directory, "get-task-allow.plist");
|
||||
await writeFile(
|
||||
@@ -215,14 +280,55 @@ try {
|
||||
"observe_native_calls",
|
||||
INPUT,
|
||||
);
|
||||
assert.equal(
|
||||
viaMcp.process.outcome,
|
||||
"exited",
|
||||
JSON.stringify(viaMcp.coverage),
|
||||
);
|
||||
assert.deepEqual(stable(viaMcp), stable(viaCli));
|
||||
assert.equal(viaMcp.process.stdout.text, viaCli.process.stdout.text);
|
||||
|
||||
// Two caller selections may resolve to one address. Each must retain its
|
||||
// own hit; consuming only the first stop-reason ID loses the second.
|
||||
const overlapping = {
|
||||
breakpoints: [
|
||||
{ kind: "function", name: "rea_call_add" },
|
||||
{ kind: "function", name: "rea_call_add", module: basename(binary) },
|
||||
],
|
||||
duration_ms: 30_000,
|
||||
argument_registers: 2,
|
||||
};
|
||||
const paired = await artifactMcpResult(
|
||||
client,
|
||||
"observe_native_calls",
|
||||
overlapping,
|
||||
);
|
||||
assert.equal(paired.process.outcome, "exited");
|
||||
assert.equal(paired.events.length, 6);
|
||||
for (const index of [0, 1]) {
|
||||
const hits = paired.events.filter(
|
||||
(event) => event.breakpoint_index === index,
|
||||
);
|
||||
assert.deepEqual(
|
||||
hits.map((event) => event.registers[0].value),
|
||||
["0x0", "0x1", "0x2"],
|
||||
);
|
||||
assert.ok(hits.every((event) => event.symbol === "rea_call_add"));
|
||||
}
|
||||
const capped = await artifactMcpResult(client, "observe_native_calls", {
|
||||
...overlapping,
|
||||
max_events: 2,
|
||||
});
|
||||
assert.equal(capped.process.outcome, "event-limit");
|
||||
assert.equal(capped.events.length, 2);
|
||||
assert.equal(capped.process.terminated, true);
|
||||
});
|
||||
await checkBounds(binary);
|
||||
await checkHardenedRuntime(directory, binary);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
developer_mode: developerMode.stdout.trim(),
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -7,6 +8,8 @@ import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { observeNativeUi } from "../dist/native/NativeUiObservation.js";
|
||||
import { NATIVE_UI_HELPER_MAX_BUFFER } from "../dist/native/NativeUiOutputBudget.js";
|
||||
import { artifactMcpResult, withArtifactMcp } from "./lib/artifact-e2e.mjs";
|
||||
import { observeProcessStartIdentity } from "../dist/process/ProcessOwnershipObservation.js";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
@@ -14,7 +17,10 @@ if (process.platform !== "darwin")
|
||||
);
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
|
||||
let child;
|
||||
let childExit;
|
||||
let fixturePid;
|
||||
let fixtureIdentity;
|
||||
let report;
|
||||
try {
|
||||
const childRetrievalTest = join(root, "child-retrieval-test");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
@@ -88,6 +94,7 @@ try {
|
||||
["-W", "-n", "-g", "--stdout", coordinates, join(root, "Fixture.app")],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
childExit = new Promise((resolve) => child.once("exit", resolve));
|
||||
let scope;
|
||||
const deadline = Date.now() + 20000;
|
||||
while (Date.now() < deadline) {
|
||||
@@ -103,6 +110,12 @@ try {
|
||||
"Source-owned UI fixture failed to expose one window within 20 seconds",
|
||||
);
|
||||
fixturePid = scope.pid;
|
||||
fixtureIdentity = await observeProcessStartIdentity(fixturePid);
|
||||
assert.equal(
|
||||
fixtureIdentity?.state,
|
||||
"readable",
|
||||
"Fixture OS start identity is unavailable",
|
||||
);
|
||||
const target = await parseBinaryTarget(executable);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await observeNativeUi(target.value, "observe_native_ui", {
|
||||
@@ -179,6 +192,43 @@ try {
|
||||
`Native UI CLI did not preserve the selected scenario: ${JSON.stringify(evidence.error ?? evidence.normalized_result?.steps?.map(({ outcome, reason }) => ({ outcome, reason })))}`,
|
||||
);
|
||||
scenarioStatus = "selected-AX-button-action-and-CLI-parity-observed";
|
||||
await withArtifactMcp(executable, async (client) => {
|
||||
const observed = await artifactMcpResult(client, "observe_native_ui", {
|
||||
...scope,
|
||||
screenshot: false,
|
||||
});
|
||||
if (
|
||||
!observed.initial.nodes.some(
|
||||
(node) =>
|
||||
node.role === "AXButton" &&
|
||||
JSON.stringify(node.path) === JSON.stringify(button.path),
|
||||
)
|
||||
)
|
||||
throw new Error("MCP omitted the selected fixture's actual AX button");
|
||||
const captured = await artifactMcpResult(
|
||||
client,
|
||||
"capture_native_ui_scenario",
|
||||
{
|
||||
...scope,
|
||||
screenshot: false,
|
||||
steps: [
|
||||
{ kind: "click", path: button.path },
|
||||
{ kind: "wait", milliseconds: 200 },
|
||||
],
|
||||
},
|
||||
);
|
||||
if (
|
||||
captured.steps.length !== 2 ||
|
||||
captured.steps.some((step) => step.outcome !== "completed") ||
|
||||
!captured.steps
|
||||
.at(-1)
|
||||
?.after?.nodes.some(
|
||||
(node) => node.title === "REA fixture incremented",
|
||||
)
|
||||
)
|
||||
throw new Error("MCP did not observe the selected fixture's AX action");
|
||||
});
|
||||
scenarioStatus = "selected-AX-button-action-and-CLI-MCP-parity-observed";
|
||||
}
|
||||
const mismatch = await observeNativeUi(
|
||||
{ ...target.value, sha256: "0".repeat(64) },
|
||||
@@ -187,21 +237,38 @@ try {
|
||||
);
|
||||
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
|
||||
throw new Error("Real helper did not reject changed target bytes");
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, verification_status: observation.ok ? "passed" : "permission-boundary-only", positive_e2e: observation.ok, observation: observation.ok ? "captured-selected-fixture-window" : "os-permission-denial-verified", scenario: scenarioStatus, permission_failure: observation.ok ? null : observation.error.message, mismatch_rejected: true, target_owned: true })}\n`,
|
||||
);
|
||||
report = {
|
||||
ok: true,
|
||||
verification_status: observation.ok ? "passed" : "permission-boundary-only",
|
||||
positive_e2e: observation.ok,
|
||||
observation: observation.ok
|
||||
? "captured-selected-fixture-window"
|
||||
: "os-permission-denial-verified",
|
||||
scenario: scenarioStatus,
|
||||
permission_failure: observation.ok ? null : observation.error.message,
|
||||
mismatch_rejected: true,
|
||||
target_owned: true,
|
||||
};
|
||||
} finally {
|
||||
if (fixturePid !== undefined) {
|
||||
try {
|
||||
process.kill(fixturePid, "SIGTERM");
|
||||
} catch (error) {
|
||||
if (error.code !== "ESRCH") throw error;
|
||||
if (fixturePid !== undefined && fixtureIdentity?.state === "readable") {
|
||||
const current = await observeProcessStartIdentity(fixturePid);
|
||||
if (current !== undefined) {
|
||||
assert.ok(
|
||||
current.state === "readable" &&
|
||||
current.identity === fixtureIdentity.identity,
|
||||
"Fixture PID identity changed; refusing to signal another process",
|
||||
);
|
||||
try {
|
||||
process.kill(fixturePid, "SIGTERM");
|
||||
} catch (cause) {
|
||||
if (cause.code !== "ESRCH") throw cause;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (child !== undefined && child.exitCode === null) {
|
||||
const exited = new Promise((resolve) => child.once("exit", resolve));
|
||||
child.kill("SIGTERM");
|
||||
await exited;
|
||||
await childExit;
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
|
||||
@@ -79,6 +79,8 @@ export async function verifyPackageEnvironment({
|
||||
CLAUDE_CONFIG_DIR: home,
|
||||
CODEX_HOME: codexDir,
|
||||
COPILOT_HOME: join(home, ".copilot"),
|
||||
GROK_HOME: join(home, ".grok"),
|
||||
SAND_DATA_ROOT: join(home, ".grokbot"),
|
||||
OPENCODE_CONFIG: undefined,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { exec, json, run } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
|
||||
export async function verifyPackageInstall({
|
||||
root,
|
||||
tarball,
|
||||
prefix,
|
||||
workspace,
|
||||
@@ -14,14 +13,7 @@ export async function verifyPackageInstall({
|
||||
}) {
|
||||
await exec(
|
||||
"npm",
|
||||
[
|
||||
"install",
|
||||
"--global",
|
||||
"--ignore-scripts",
|
||||
"--prefix",
|
||||
prefix,
|
||||
join(root, tarball),
|
||||
],
|
||||
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
|
||||
{ env: environment },
|
||||
);
|
||||
const cli = join(prefix, "bin", "rea");
|
||||
@@ -61,7 +53,7 @@ export async function verifyPackageInstall({
|
||||
"--omit=optional",
|
||||
"--prefix",
|
||||
noOptionalPrefix,
|
||||
join(root, tarball),
|
||||
tarball,
|
||||
],
|
||||
{ env: environment },
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
|
||||
|
||||
@@ -6,15 +7,23 @@ import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
|
||||
|
||||
/** Create a tarball and assert production packaging constraints. */
|
||||
export async function verifyPackagePack({ root, workspace }) {
|
||||
const tarball = (
|
||||
await exec("npm", ["pack", "--silent"], { cwd: root })
|
||||
).stdout.trim();
|
||||
const packedFiles = (
|
||||
await exec("tar", ["-tf", join(root, tarball)])
|
||||
).stdout.split("\n");
|
||||
const packDirectory = join(workspace, "pack");
|
||||
await mkdir(packDirectory);
|
||||
const tarball = join(
|
||||
packDirectory,
|
||||
(
|
||||
await exec(
|
||||
"npm",
|
||||
["pack", "--silent", "--pack-destination", packDirectory],
|
||||
{
|
||||
cwd: root,
|
||||
},
|
||||
)
|
||||
).stdout.trim(),
|
||||
);
|
||||
const packedFiles = (await exec("tar", ["-tf", tarball])).stdout.split("\n");
|
||||
const packedManifest = JSON.parse(
|
||||
(await exec("tar", ["-xOf", join(root, tarball), "package/package.json"]))
|
||||
.stdout,
|
||||
(await exec("tar", ["-xOf", tarball, "package/package.json"])).stdout,
|
||||
);
|
||||
if (
|
||||
packedManifest.scripts?.postinstall !== undefined ||
|
||||
@@ -33,10 +42,16 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
throw new Error(
|
||||
"package included authored skill sources instead of only the generated bundle",
|
||||
);
|
||||
if (
|
||||
packedFiles.some((path) =>
|
||||
/^package\/(?:dist|src)\/generatedMcpToolCatalog\./u.test(path),
|
||||
)
|
||||
)
|
||||
throw new Error("package included the test-only generated MCP catalog");
|
||||
const skill = (
|
||||
await exec("tar", [
|
||||
"-xOf",
|
||||
join(root, tarball),
|
||||
tarball,
|
||||
"package/skills/reverse-engineer-anything/SKILL.md",
|
||||
])
|
||||
).stdout;
|
||||
@@ -56,6 +71,6 @@ export async function verifyPackagePack({ root, workspace }) {
|
||||
) {
|
||||
throw new Error("package contained generated Python bytecode");
|
||||
}
|
||||
await verifyPackedBridge({ root, workspace, tarball, packedFiles });
|
||||
await verifyPackedBridge({ workspace, tarball, packedFiles });
|
||||
return { tarball };
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@ export async function verifyPackageUpdate({
|
||||
CLAUDE_CONFIG_DIR: home,
|
||||
CODEX_HOME: join(home, ".codex"),
|
||||
COPILOT_HOME: join(home, ".copilot"),
|
||||
GROK_HOME: join(home, ".grok"),
|
||||
SAND_DATA_ROOT: join(home, ".grokbot"),
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
|
||||
npm_config_prefix: prefix,
|
||||
|
||||
@@ -34,6 +34,7 @@ let tarball;
|
||||
let mcpStartup;
|
||||
let mcpModuleLoading;
|
||||
let update;
|
||||
let report;
|
||||
|
||||
try {
|
||||
({ tarball } = await verifyPackagePack({ root, workspace }));
|
||||
@@ -44,7 +45,6 @@ try {
|
||||
referenceRoot,
|
||||
});
|
||||
const { cli, packageRunnerCli } = await verifyPackageInstall({
|
||||
root,
|
||||
tarball,
|
||||
prefix: environmentData.prefix,
|
||||
workspace,
|
||||
@@ -58,7 +58,7 @@ try {
|
||||
"node_modules",
|
||||
PRODUCT_IDENTITY.packageName,
|
||||
),
|
||||
tarball: join(root, tarball),
|
||||
tarball,
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
@@ -133,10 +133,44 @@ try {
|
||||
evidenceRoot,
|
||||
artifactArchive,
|
||||
});
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 4 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading })}\n`,
|
||||
);
|
||||
report = {
|
||||
cli: true,
|
||||
analysisCli: true,
|
||||
artifactCli: true,
|
||||
managedCli: true,
|
||||
managedReconstructionCli: true,
|
||||
managedNativeVerificationCli: true,
|
||||
managedApplicationGraphCli: true,
|
||||
evidenceCli: true,
|
||||
incurMcpCommand: PRODUCT_IDENTITY.mcpCommand,
|
||||
lifecycleScriptsRequired: false,
|
||||
doctor: "platform-appropriate",
|
||||
setup: supportedSetupHost
|
||||
? "planned-then-idempotent"
|
||||
: "unsupported-host-rejected",
|
||||
setupPlanReadOnly: supportedSetupHost,
|
||||
existingHopperPreserved: supportedSetupHost,
|
||||
clients: supportedSetupHost ? 4 : 0,
|
||||
backupReadback: supportedSetupHost,
|
||||
failureRecovery: supportedSetupHost,
|
||||
configSymlinkLifecycle: supportedSetupHost,
|
||||
skill: supportedSetupHost,
|
||||
skillReferences: supportedSetupHost,
|
||||
mcpTools: TOOL_CONTRACTS.length,
|
||||
mcpPrompts: prompts.names.length,
|
||||
promptCompletion: true,
|
||||
promptCompletionLifecycle: true,
|
||||
evidenceMcp: true,
|
||||
targetFree: true,
|
||||
targetLifecycle: true,
|
||||
boundedRegexBridge: true,
|
||||
update,
|
||||
mcpStartup,
|
||||
mcpModuleLoading,
|
||||
};
|
||||
} finally {
|
||||
if (tarball) await rm(join(root, tarball), { force: true });
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
|
||||
);
|
||||
|
||||
@@ -127,7 +127,6 @@ const environment = {
|
||||
process.env.ComSpec ??
|
||||
join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe")
|
||||
).replaceAll("\\", "/"),
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_LOG_LEVEL: "error",
|
||||
GHIDRA_HEADLESS_MAXMEM: "512m",
|
||||
NODE_OPTIONS: "--max-old-space-size=512",
|
||||
@@ -291,7 +290,17 @@ try {
|
||||
expected.every((name) => report.mcpOperations.includes(name)),
|
||||
"Packaged verification omitted an admitted operation.",
|
||||
);
|
||||
await call("binary_session", {});
|
||||
const session = await call("binary_session", {});
|
||||
assert.equal(
|
||||
session.analysis_provider_binding?.selection_source,
|
||||
"auto-single-candidate",
|
||||
"Packaged MCP verification must exercise automatic provider selection.",
|
||||
);
|
||||
assert.equal(
|
||||
session.analysis_provider_binding?.provider?.id,
|
||||
"ghidra",
|
||||
"The sole available Windows analysis provider must be Ghidra.",
|
||||
);
|
||||
await call("close_binary", {});
|
||||
await assertCleanup();
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
@@ -7,67 +6,17 @@ const exec = promisify(execFile);
|
||||
const packedHopperBridge = "package/bridge/hopper_bridge.py";
|
||||
const packedGhidraBridge = "package/bridge/ghidra/ReaGhidraBridge.java";
|
||||
|
||||
/** Verify regex search through the packaged production bridge. */
|
||||
export async function verifyPackedBridge({
|
||||
root,
|
||||
workspace,
|
||||
tarball,
|
||||
packedFiles,
|
||||
}) {
|
||||
if (!packedFiles.includes(packedHopperBridge))
|
||||
throw new Error("package omitted the Hopper bridge");
|
||||
if (!packedFiles.includes(packedGhidraBridge))
|
||||
throw new Error("package omitted the Ghidra bridge");
|
||||
await exec("tar", ["-xf", join(root, tarball), "-C", workspace]);
|
||||
const ghidraSource = await readFile(
|
||||
join(workspace, packedGhidraBridge),
|
||||
"utf8",
|
||||
);
|
||||
for (const commitment of [
|
||||
"extends HeadlessScript",
|
||||
'request.method.equals("ping")',
|
||||
'request.method.equals("shutdown")',
|
||||
'boolean readOnly = !descriptor.transport.equals("unix-socket")',
|
||||
'result.addProperty("read_only", readOnly)',
|
||||
'case "annotate_native_function"',
|
||||
'currentProgram.startTransaction("REA function annotations")',
|
||||
"currentProgram.endTransaction(transaction, commit)",
|
||||
"analysisTimeoutOccurred()",
|
||||
])
|
||||
if (!ghidraSource.includes(commitment))
|
||||
throw new Error(`packaged Ghidra bridge omitted ${commitment}`);
|
||||
const probe = JSON.parse(
|
||||
(
|
||||
await exec("python3", [
|
||||
join(root, "tests/fixtures/bridgeSearchProbe.py"),
|
||||
join(workspace, packedHopperBridge),
|
||||
JSON.stringify({
|
||||
action: "search",
|
||||
items: [
|
||||
["0x1000", "REA_GHIDRA_INVENTORY_ENTRY"],
|
||||
["0x2000", "unrelated"],
|
||||
["0x3000", "REA_GHIDRA_LEAF_VALUE"],
|
||||
],
|
||||
params: {
|
||||
pattern: "^REA_GHIDRA_(?:INVENTORY_ENTRY|LEAF_VALUE)$",
|
||||
mode: "regex",
|
||||
case_sensitive: true,
|
||||
},
|
||||
}),
|
||||
])
|
||||
).stdout,
|
||||
);
|
||||
const matches = probe.result;
|
||||
if (
|
||||
probe.ok !== true ||
|
||||
!Array.isArray(matches) ||
|
||||
matches.length !== 2 ||
|
||||
matches[0]?.address !== "0x1000" ||
|
||||
matches[0]?.value !== "REA_GHIDRA_INVENTORY_ENTRY" ||
|
||||
matches[1]?.address !== "0x3000" ||
|
||||
matches[1]?.value !== "REA_GHIDRA_LEAF_VALUE"
|
||||
)
|
||||
throw new Error(
|
||||
`packaged Hopper bridge regex search drifted: ${JSON.stringify(probe)}`,
|
||||
);
|
||||
/** Check shipped bridge files; real-provider lanes verify runtime behavior. */
|
||||
export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
|
||||
for (const path of [packedHopperBridge, packedGhidraBridge]) {
|
||||
if (!packedFiles.includes(path)) throw new Error(`package omitted ${path}`);
|
||||
}
|
||||
await exec("tar", ["-xf", tarball, "-C", workspace]);
|
||||
// Parse the actual packed Python source without loading Hopper globals or
|
||||
// generating bytecode. This check has no dependency on test-only fixtures.
|
||||
await exec("python3", [
|
||||
"-c",
|
||||
"import ast, pathlib, sys; p = pathlib.Path(sys.argv[1]); ast.parse(p.read_text(encoding='utf-8'), filename=str(p))",
|
||||
join(workspace, packedHopperBridge),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,12 @@ const version = process.argv[2];
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
|
||||
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
|
||||
|
||||
const serverEnvironment = { ...process.env };
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
const canaryRoot = await mkdtemp(join(tmpdir(), "rea-published-canary-"));
|
||||
const serverEnvironment = {
|
||||
...process.env,
|
||||
npm_config_cache: join(canaryRoot, "npm-cache"),
|
||||
};
|
||||
delete serverEnvironment.HOPPER_TARGET_PATH;
|
||||
const transport = new StdioClientTransport({
|
||||
command: "npm",
|
||||
args: [
|
||||
@@ -91,16 +94,29 @@ process.stdout.write(
|
||||
);
|
||||
|
||||
async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
|
||||
const previousVersion = await findPreviousPublishedVersion(targetVersion);
|
||||
const prefix = join(canaryRoot, "update-prefix");
|
||||
const home = join(canaryRoot, "update-home");
|
||||
const environment = {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
APPDATA: join(home, "AppData", "Roaming"),
|
||||
CLAUDE_CONFIG_DIR: home,
|
||||
CODEX_HOME: join(home, ".codex"),
|
||||
COPILOT_HOME: join(home, ".copilot"),
|
||||
GROK_HOME: join(home, ".grok"),
|
||||
SAND_DATA_ROOT: join(home, ".grokbot"),
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
|
||||
npm_config_cache: join(canaryRoot, "update-cache"),
|
||||
npm_config_prefix: prefix,
|
||||
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
|
||||
};
|
||||
const previousVersion = await findPreviousPublishedVersion(
|
||||
targetVersion,
|
||||
canaryRoot,
|
||||
environment,
|
||||
);
|
||||
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
@@ -193,11 +209,11 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
};
|
||||
}
|
||||
|
||||
async function findPreviousPublishedVersion(targetVersion) {
|
||||
async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
|
||||
const { stdout } = await execFileAsync(
|
||||
"npm",
|
||||
["view", "rea-agents", "versions", "--json"],
|
||||
{ maxBuffer: 16 * 1024 * 1024 },
|
||||
{ cwd, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const versions = JSON.parse(stdout);
|
||||
const previous = versions
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user