Compare commits
114
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9622b4b4cf | ||
|
|
8238232485 | ||
|
|
c8e66c704a | ||
|
|
91daaa68ae | ||
|
|
9a4e6da5a9 | ||
|
|
fb679b63c2 | ||
|
|
6d1f39b250 | ||
|
|
99263a720d | ||
|
|
5af1699103 | ||
|
|
d3f498ceec | ||
|
|
1d15bcb9ec | ||
|
|
739039092f | ||
|
|
3f6d6dc756 | ||
|
|
5a7c769692 | ||
|
|
8d7071da4c | ||
|
|
72548712fe | ||
|
|
904d03e987 | ||
|
|
d0f3480a3c | ||
|
|
00eb03d0f2 | ||
|
|
75a75cbb20 | ||
|
|
09c249deb8 | ||
|
|
a5b970c6bf | ||
|
|
65c879757e | ||
|
|
18c18290d0 | ||
|
|
b1b687fb75 | ||
|
|
863ed47de2 | ||
|
|
1dccd0dee0 | ||
|
|
6582b2433a | ||
|
|
66f3cfa594 | ||
|
|
e2c23f3640 | ||
|
|
8d44b3edd3 | ||
|
|
431e8ebfcf | ||
|
|
9dd6cef90a | ||
|
|
9a2f395028 | ||
|
|
fd008cd8e0 | ||
|
|
fe843ec0b1 | ||
|
|
38ebd54776 | ||
|
|
3201f0538d | ||
|
|
af48fe8791 | ||
|
|
417737748a | ||
|
|
98de3380e3 | ||
|
|
9bab55337a | ||
|
|
c3bc38bf6c | ||
|
|
7a96391001 | ||
|
|
ab5a1c61b1 | ||
|
|
f04445c57c | ||
|
|
44b3627ab0 | ||
|
|
26881da76a | ||
|
|
d311940f24 | ||
|
|
cf061159ab | ||
|
|
5bcb6645b5 | ||
|
|
fd3282a87e | ||
|
|
a4e57366dd | ||
|
|
d565ecdc25 | ||
|
|
8189081511 | ||
|
|
7f1fa64019 | ||
|
|
c8aed4740d | ||
|
|
fa02df9ad3 | ||
|
|
ddea06360a | ||
|
|
94cc8475e0 | ||
|
|
1a24be574d | ||
|
|
217e8cf59f | ||
|
|
a55b5bb8ee | ||
|
|
ba35b5509d | ||
|
|
11d0ef2e09 | ||
|
|
805c338f36 | ||
|
|
a6a8f67649 | ||
|
|
7864d665e1 | ||
|
|
df6e476d67 | ||
|
|
ed3a7f070c | ||
|
|
132a3a46be | ||
|
|
e828acce0e | ||
|
|
53fa55937f | ||
|
|
e60aa11108 | ||
|
|
8b228ae701 | ||
|
|
03bc6d36db | ||
|
|
b6c21b1db5 | ||
|
|
433ebda8b4 | ||
|
|
2c1bf92010 | ||
|
|
7d4ed59816 | ||
|
|
7f1ae42f71 | ||
|
|
6ad62cec6a | ||
|
|
39db9fe424 | ||
|
|
c3d013b2fd | ||
|
|
a5d2986b1e | ||
|
|
6a37346acb | ||
|
|
f94fc7967b | ||
|
|
88a3c3f8a4 | ||
|
|
8dd1d84471 | ||
|
|
5802ef8746 | ||
|
|
3aaf7e2c54 | ||
|
|
fc80ea88df | ||
|
|
b89de8feab | ||
|
|
ae72abd019 | ||
|
|
ea0b733687 | ||
|
|
19f8cddfd6 | ||
|
|
9f42495d11 | ||
|
|
9b392bb2f8 | ||
|
|
5f27e02fae | ||
|
|
735c10059c | ||
|
|
77799e6c90 | ||
|
|
965d0b35f9 | ||
|
|
5295d56b10 | ||
|
|
dee8853fde | ||
|
|
494d4db6d9 | ||
|
|
e7f54b3310 | ||
|
|
3c84080aab | ||
|
|
55e5eb090b | ||
|
|
5e7f35c7c3 | ||
|
|
9c10a64f87 | ||
|
|
ba3869c597 | ||
|
|
38b293329c | ||
|
|
feeb19b8b3 | ||
|
|
2ef02ced16 |
No files matched your search
+45
-18
@@ -105,7 +105,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: vitest-report-${{ strategy.job-index }}
|
||||
path: .vitest-reports/*
|
||||
path: .vitest/blob/*
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
with:
|
||||
path: .vitest-reports
|
||||
path: .vitest/blob
|
||||
pattern: vitest-report-*
|
||||
merge-multiple: true
|
||||
- name: Merge coverage, JUnit, and timing reports
|
||||
@@ -215,9 +215,30 @@ jobs:
|
||||
- name: Verify fixtures from a clean checkout
|
||||
run: npm run verify:fixtures
|
||||
|
||||
windows-native-build:
|
||||
name: Build bundled Windows x64 native controls
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Prepare cross compiler
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||||
- run: npm run build:windows-native
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
if-no-files-found: error
|
||||
|
||||
package-e2e:
|
||||
name: Package/install integration (${{ matrix.os }})
|
||||
needs: changes
|
||||
needs: [changes, windows-native-build]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -231,6 +252,11 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run verify:package
|
||||
|
||||
apple-artifacts:
|
||||
@@ -302,7 +328,7 @@ jobs:
|
||||
|
||||
windows-curated:
|
||||
name: Windows capability and package lane
|
||||
needs: changes
|
||||
needs: [changes, windows-native-build]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
@@ -315,6 +341,11 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build:cached
|
||||
- run: npm run typecheck
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- name: Record named Windows host capabilities
|
||||
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
|
||||
- name: Validate Windows host capability report
|
||||
@@ -327,18 +358,16 @@ jobs:
|
||||
# ignores unmatched paths, so a stale entry would otherwise report green
|
||||
# while skipping coverage.
|
||||
#
|
||||
# Every path here must pass on windows-latest WITHOUT verified Windows
|
||||
# native authority. src/ghidra/GhidraProvider.test.ts,
|
||||
# src/ghidra/GhidraProvider.behavior.test.ts successors, and
|
||||
# tests/boundary/process/providerProcess.test.ts are deliberately absent:
|
||||
# they require Job Object process ownership and private-runtime DACL
|
||||
# enforcement, which this runner does not provide by design. Those claims
|
||||
# belong behind the same approval boundary as the real Ghidra Windows
|
||||
# workflow, not in the default lane.
|
||||
# The matching native artifact establishes real Job Object and private
|
||||
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
|
||||
# real engine claims remain in the separate controlled-provider workflow.
|
||||
- name: Run curated Windows capability tests
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
tests/boundary/filesystem/safeOutputTree.test.ts
|
||||
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
|
||||
tests/boundary/filesystem/setupUnavailableProvider.test.ts
|
||||
tests/boundary/filesystem/asarInventoryFreshness.test.ts
|
||||
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
|
||||
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
|
||||
@@ -347,6 +376,9 @@ jobs:
|
||||
tests/boundary/providers/ghidra/ghidraTransport.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
|
||||
src/windows/WindowsNativeLoader.test.ts
|
||||
src/windows/WindowsOwnedProcess.test.ts
|
||||
src/ghidra/GhidraLauncherEnvironment.test.ts
|
||||
src/process/ProcessOwnership.test.ts
|
||||
src/process/ProcessOwnership.part2.test.ts
|
||||
src/process/ProcessOwnership.part3.test.ts
|
||||
@@ -354,6 +386,7 @@ jobs:
|
||||
src/application/Update.test.ts
|
||||
tests/process-global/updateMaintenance.test.ts
|
||||
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
|
||||
- run: npm run verify:windows-native
|
||||
- run: npm run verify:package:windows
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
@@ -374,9 +407,3 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:check
|
||||
- run: npm run docs:api:cached
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: build/api-docs
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Source-built NativeAOT fixtures
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "bridge/ghidra/extensions/nativeaot/**"
|
||||
- "scripts/generate-nativeaot-fixtures.mjs"
|
||||
- "tests/fixtures/nativeaot/**"
|
||||
- ".github/workflows/nativeaot-fixtures.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows-fixture:
|
||||
name: Build Windows x64 NativeAOT fixture
|
||||
# .NET 8.0.22's bundled linker discovery supports VS 2019/2022.
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_PROCESSOR_COUNT: "1"
|
||||
COMPlus_GCHeapHardLimit: "30000000"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.416"
|
||||
- name: Build benign fixture and independent byte oracle
|
||||
run: node scripts/generate-nativeaot-fixtures.mjs
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nativeaot-win-x64-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: |
|
||||
_reference/nativeaot-integration/generated/win-x64/symbols/NativeAotFixture.exe
|
||||
_reference/nativeaot-integration/generated/win-x64/NativeAotFixture.map
|
||||
_reference/nativeaot-integration/generated/win-x64/symbols.txt
|
||||
_reference/nativeaot-integration/generated/win-x64/oracle.json
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
@@ -11,9 +11,29 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
native-artifact:
|
||||
name: Build matching Windows native artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Prepare cross compiler
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||||
- run: npm run build:windows-native
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: real-ghidra-windows-native
|
||||
path: native/windows/build/
|
||||
if-no-files-found: error
|
||||
|
||||
verify:
|
||||
name: Real Ghidra 12.1.4 Windows x64 PE proof
|
||||
if: github.ref == 'refs/heads/main'
|
||||
needs: native-artifact
|
||||
environment: real-ghidra-windows
|
||||
runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
|
||||
timeout-minutes: 20
|
||||
@@ -23,13 +43,25 @@ jobs:
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: real-ghidra-windows-native
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run verify:windows-native
|
||||
- name: Verify all admitted Windows Ghidra operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log
|
||||
- name: Verify ordinary-user CLI and MCP operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: windows-ghidra-proof
|
||||
path: windows-ghidra-proof.log
|
||||
path: |
|
||||
windows-ghidra-proof.log
|
||||
windows-ghidra-package-proof.log
|
||||
retention-days: 30
|
||||
@@ -108,6 +108,12 @@ jobs:
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Prepare Windows native build toolchain
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||||
|
||||
- name: Build and verify bundled Windows native controls
|
||||
run: npm run build:windows-native && npm run verify:windows-native:artifact
|
||||
|
||||
- name: Verify packaged release before publishing
|
||||
run: npm run verify:package
|
||||
|
||||
@@ -129,23 +135,22 @@ jobs:
|
||||
verification_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${verification_root}"' EXIT
|
||||
cd "${verification_root}"
|
||||
for attempt in $(seq 1 20); do
|
||||
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
for attempt in $(seq 1 80); do
|
||||
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||||
if npm exec --yes --package="rea-agents@${version}" -- rea --help \
|
||||
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
|
||||
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Published rea-agents@${version} is visible, but CLI verification failed on attempt ${attempt}" >&2
|
||||
echo "rea-agents@${version} is visible on npm after attempt ${attempt}; verifying the published CLI."
|
||||
npm exec --yes --package="rea-agents@${version}" -- rea --help
|
||||
npm exec --yes --package="rea-agents@${version}" -- rea --llms
|
||||
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
|
||||
exit 0
|
||||
else
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/20)" >&2
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/80; up to 20 minutes)" >&2
|
||||
fi
|
||||
if [ "${attempt}" -lt 20 ]; then
|
||||
if [ "${attempt}" -lt 80 ]; then
|
||||
sleep 15
|
||||
fi
|
||||
done
|
||||
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
|
||||
echo "rea-agents@${version} did not become visible on npm within 20 minutes" >&2
|
||||
exit 1
|
||||
|
||||
publish-mcp:
|
||||
|
||||
@@ -6,7 +6,6 @@ __pycache__/
|
||||
# Distribution
|
||||
dist/
|
||||
build/
|
||||
docs/api/
|
||||
node_modules
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[submodule "third_party/jadx-headless-mcp"]
|
||||
path = third_party/jadx-headless-mcp
|
||||
url = https://github.com/1013503897/jadx-headless-mcp.git
|
||||
[submodule "third_party/binwalk"]
|
||||
path = third_party/binwalk
|
||||
url = https://github.com/ReFirmLabs/binwalk.git
|
||||
[submodule "third_party/unblob"]
|
||||
path = third_party/unblob
|
||||
url = https://github.com/onekey-sec/unblob.git
|
||||
[submodule "third_party/ghidra-nativeaot"]
|
||||
path = third_party/ghidra-nativeaot
|
||||
url = https://github.com/Washi1337/ghidra-nativeaot.git
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.7,
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.4% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.5,
|
||||
"reporters": ["consoleFull"],
|
||||
"ignore": [
|
||||
"node_modules/**",
|
||||
@@ -9,6 +9,7 @@
|
||||
".codex/**",
|
||||
".devin/**",
|
||||
"tests/fixtures/**",
|
||||
"third_party/**",
|
||||
"tests/conformance/**/*.c",
|
||||
"tests/conformance/**/*.m",
|
||||
"tests/conformance/**/*.swift",
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@
|
||||
"node_modules/**",
|
||||
"CHANGELOG.md",
|
||||
"package-lock.json",
|
||||
"_reference/**"
|
||||
"_reference/**",
|
||||
"third_party/**"
|
||||
]
|
||||
}
|
||||
+24
-2
@@ -17,6 +17,15 @@
|
||||
"no-var-requires": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"scripts/electron-active-hook*.cjs",
|
||||
"tests/conformance/readiness/electron/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"no-var-requires": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"src/{contracts,domain,server}/**/*.ts",
|
||||
@@ -29,7 +38,7 @@
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["**/ghidra/**", "**/hopper/**"],
|
||||
"group": ["**/ghidra/**", "**/hopper/**", "**/ida/**"],
|
||||
"message": "Deep-provider imports belong behind adapters or in the application composition root."
|
||||
}
|
||||
]
|
||||
@@ -62,6 +71,7 @@
|
||||
"**/browser/**",
|
||||
"**/ghidra/**",
|
||||
"**/hopper/**",
|
||||
"**/ida/**",
|
||||
"**/replay/**",
|
||||
"**/cli.*",
|
||||
"**/main.*"
|
||||
@@ -120,5 +130,17 @@
|
||||
"node": true,
|
||||
"es2023": true
|
||||
},
|
||||
"ignorePatterns": ["dist", "node_modules", "docs/api", ".codex", ".devin"]
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules",
|
||||
"docs/api",
|
||||
"coverage",
|
||||
".turbo",
|
||||
".cache",
|
||||
".codex",
|
||||
".devin",
|
||||
".factory",
|
||||
"third_party"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "4.0.0"
|
||||
".": "4.1.0"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Product Direction
|
||||
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper, 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-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:
|
||||
|
||||
@@ -20,7 +20,7 @@ REA is a local-only tool. Preserve caller-selected inputs, captured output, URLs
|
||||
REA is a layered ESM TypeScript application. Dependencies flow inward from pure domain logic through contracts, providers, application workflows, and CLI/MCP adapters. See [docs/architecture.mermaid](docs/architecture.mermaid) for the component map.
|
||||
|
||||
- `src/domain/` owns pure provider-neutral semantics; `src/contracts/` owns caller-visible schemas and the canonical tool inventory.
|
||||
- `src/hopper/`, `src/ghidra/`, `src/browser/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
|
||||
- `src/hopper/`, `src/ghidra/`, `src/ida/`, `src/browser/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
|
||||
- `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
|
||||
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters.
|
||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` contains real-toolchain checks.
|
||||
@@ -35,7 +35,7 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
|
||||
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
|
||||
- `npm run check:fast`: run cached typecheck and lint checks.
|
||||
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
|
||||
- `npm run docs:check`: check committed generated documents; `npm run docs:api:cached` separately renders API HTML when needed.
|
||||
- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them.
|
||||
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
|
||||
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
|
||||
|
||||
@@ -94,3 +94,14 @@ Keep tool catalogs complete and self-describing; prefer capability- and session-
|
||||
## 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/`).
|
||||
|
||||
<!-- BEGIN:turborepo-agent-rules -->
|
||||
|
||||
# This is NOT the Turborepo you know
|
||||
|
||||
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`.
|
||||
|
||||
Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
|
||||
|
||||
This block is written and re-added by `turbo` before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in `crates/turborepo-cli/src/cli/agent_guidance.rs`. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set `"agentGuidance": false` in the root `turbo.json` or `turbo.jsonc` to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation.
|
||||
<!-- END:turborepo-agent-rules -->
|
||||
+136
@@ -1,5 +1,141 @@
|
||||
# Changelog
|
||||
|
||||
## [4.1.0](https://github.com/morluto/rea/compare/rea-agents-4.0.1...rea-agents-4.1.0) (2026-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **android:** integrate static APK analysis with headless JADX ([#738](https://github.com/morluto/rea/issues/738)) ([904d03e](https://github.com/morluto/rea/commit/904d03e987a6c498561b314a266202dee84b13ab))
|
||||
* **evidence:** accept exact retained application references ([#734](https://github.com/morluto/rea/issues/734)) ([d0f3480](https://github.com/morluto/rea/commit/d0f3480a3cb9887499c2b188c783dd1c4db09787))
|
||||
* **firmware:** integrate Binwalk and Unblob analysis ([5a7c769](https://github.com/morluto/rea/commit/5a7c769692c21efc4cc0e5ebf06a9f2534dc75ed))
|
||||
* **ghidra:** add atomic function annotation edits ([#709](https://github.com/morluto/rea/issues/709)) ([9bab553](https://github.com/morluto/rea/commit/9bab55337ab10616be6929afddb08fb19fbb8ab7))
|
||||
* **ghidra:** admit explicit DOS COM analysis ([#702](https://github.com/morluto/rea/issues/702)) ([5bcb664](https://github.com/morluto/rea/commit/5bcb6645b561e666b7e07dd8f38d2296fa7cbba8))
|
||||
* **ghidra:** enable Windows x64 read-only analysis ([#699](https://github.com/morluto/rea/issues/699)) ([26881da](https://github.com/morluto/rea/commit/26881da76aa320c8529fcc741df0fc9898d97bdc))
|
||||
* **ghidra:** integrate optional NativeAOT metadata recovery ([#748](https://github.com/morluto/rea/issues/748)) ([fb679b6](https://github.com/morluto/rea/commit/fb679b63c27e6c5f137532953cfa24a2ffb1aaf2))
|
||||
* **ghidra:** verify DOS load images and expose memory evidence ([#697](https://github.com/morluto/rea/issues/697)) ([a6a8f67](https://github.com/morluto/rea/commit/a6a8f67649cab558b0de11601e65425f280c7170))
|
||||
* **ida:** add read-only GUI and headless MCP providers ([#754](https://github.com/morluto/rea/issues/754)) ([8238232](https://github.com/morluto/rea/commit/82382324855a3a4ed51d2cdc3b2b455c9e8419e2))
|
||||
* **setup:** support Command Code MCP registration ([#645](https://github.com/morluto/rea/issues/645)) ([7f1ae42](https://github.com/morluto/rea/commit/7f1ae42f71ce85547d317eed5e62fa330d9876bb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** validate explicit Java runtime ([#742](https://github.com/morluto/rea/issues/742)) ([7390390](https://github.com/morluto/rea/commit/739039092f98c67e8e6ccfb1f8445e8f13ac8f15))
|
||||
* **artifacts:** make extraction output tree work on Windows ([#753](https://github.com/morluto/rea/issues/753)) ([c8e66c7](https://github.com/morluto/rea/commit/c8e66c704a70f5133922aa1bd1a3ba42959093ba))
|
||||
* **browser:** classify CSP host sources by their own origin ([#707](https://github.com/morluto/rea/issues/707)) ([1d15bcb](https://github.com/morluto/rea/commit/1d15bcb9ecd7efa487a8fd3f0d9755bd60051722))
|
||||
* **browser:** keep bare module locations unresolved ([#676](https://github.com/morluto/rea/issues/676)) ([8189081](https://github.com/morluto/rea/commit/8189081511441f0e3909b21b80c21c658ec9a453))
|
||||
* **ci:** upload Vitest blob reports from current path ([77799e6](https://github.com/morluto/rea/commit/77799e6c90ce36cd52e45b62c9cba2d46641c4cf))
|
||||
* **cli:** disable unmanaged skill synchronization ([433ebda](https://github.com/morluto/rea/commit/433ebda8b492f388faf6e6cac63ccc6f9c2d8d7e))
|
||||
* **cli:** disable unmanaged skill synchronization ([2c1bf92](https://github.com/morluto/rea/commit/2c1bf9201040ed3d60f475af9129b7bf85a508fa))
|
||||
* **cli:** fail when JSON input files cannot be parsed ([18c1829](https://github.com/morluto/rea/commit/18c18290d048b78a435f6864dfc778a9fea0af2e))
|
||||
* **cli:** fail when JSON input files cannot be parsed ([3201f05](https://github.com/morluto/rea/commit/3201f0538dc88e54bce00decf689bf9b5603bfd3))
|
||||
* **cli:** include build step in missing-runtime recovery ([6a37346](https://github.com/morluto/rea/commit/6a37346acb9829acaab0e2042624e6ea0d03e2a9))
|
||||
* **cli:** include build step in missing-runtime recovery ([ae72abd](https://github.com/morluto/rea/commit/ae72abd019b25347bfc7593b5dcadc5a2cbc3786))
|
||||
* **cli:** report reference-source import failures ([00eb03d](https://github.com/morluto/rea/commit/00eb03d0f2247da95dfa7bda0f41a0f366a25dd9))
|
||||
* **cli:** report reference-source import failures ([431e8eb](https://github.com/morluto/rea/commit/431e8ebfcfba2925904c0deb4888bbd3755b17bf))
|
||||
* **comparison:** break function collection collation ties ([c3d013b](https://github.com/morluto/rea/commit/c3d013b2fd5c3a32dfa73ef40dafdaeaf935f37e))
|
||||
* **comparison:** break function collection collation ties ([3aaf7e2](https://github.com/morluto/rea/commit/3aaf7e2c54029a456145d6e3287fd9122f3abbe3))
|
||||
* **comparison:** break JSON shape collation ties ([217e8cf](https://github.com/morluto/rea/commit/217e8cf59f49e2f68b394fbcbc87f914dbee97ee))
|
||||
* **comparison:** break JSON shape collation ties ([805c338](https://github.com/morluto/rea/commit/805c338f360184d73ae335a723fca656830d6328))
|
||||
* **comparison:** retain container-only export shape paths ([a55b5bb](https://github.com/morluto/rea/commit/a55b5bb8ee400ef5565954d57916e8aae7dac760))
|
||||
* **comparison:** retain container-only export shape paths ([7864d66](https://github.com/morluto/rea/commit/7864d665e15edec29ade0f3fb7ac374b8579c2c0))
|
||||
* **dotnet:** preserve leading Unicode content in metadata strings ([#667](https://github.com/morluto/rea/issues/667)) ([ddea063](https://github.com/morluto/rea/commit/ddea06360a09508a0ff695ab6e141eff19534619))
|
||||
* **electron:** respect computed option keys and overrides ([af48fe8](https://github.com/morluto/rea/commit/af48fe879161ee749692751b3e29ba06b63b7409))
|
||||
* **electron:** respect computed option keys and overrides ([98de338](https://github.com/morluto/rea/commit/98de3380e3062cb25778263bfea07b215a09b39c))
|
||||
* **evidence:** admit current process provider identity ([ab5a1c6](https://github.com/morluto/rea/commit/ab5a1c61b12475c554a5e4d60b48b642b2464bbb))
|
||||
* **evidence:** admit current process provider identity ([d311940](https://github.com/morluto/rea/commit/d311940f2414fe5cceb750de4464b681bb44ae1e))
|
||||
* **firmware:** validate configured executables ([#751](https://github.com/morluto/rea/issues/751)) ([9a4e6da](https://github.com/morluto/rea/commit/9a4e6da5a96ed2708a0f628b436dcd7415999327))
|
||||
* **ghidra:** preserve request and shutdown failure diagnostics ([#647](https://github.com/morluto/rea/issues/647)) ([19f8cdd](https://github.com/morluto/rea/commit/19f8cddfd6f283188567b8ff1e49b810d79952ee))
|
||||
* **ghidra:** refuse ambiguous universal Mach-O imports ([b1b687f](https://github.com/morluto/rea/commit/b1b687fb75325c0275a0cf161843fc5969ba1d47))
|
||||
* **ghidra:** refuse ambiguous universal Mach-O imports ([6582b24](https://github.com/morluto/rea/commit/6582b2433a955167e33246ad07de1a40bcb20144))
|
||||
* **hopper:** retain shutdown request diagnostics ([8d7071d](https://github.com/morluto/rea/commit/8d7071da4c61c5cfb4894be6536405affd90f276))
|
||||
* **hopper:** retain shutdown request diagnostics ([7254871](https://github.com/morluto/rea/commit/72548712fea972b920834663a5e2ceea4738ce1e))
|
||||
* integrate 14 approved fix PRs (668,671,673,675,678-681,685-690) ([#705](https://github.com/morluto/rea/issues/705)) ([cf06115](https://github.com/morluto/rea/commit/cf061159ab69a746675e48896fa953d6990c05f9))
|
||||
* **javascript:** count all source line terminators ([e60aa11](https://github.com/morluto/rea/commit/e60aa1110800e1309b482e7a44ecd1762bade992))
|
||||
* **javascript:** count all source line terminators ([03bc6d3](https://github.com/morluto/rea/commit/03bc6d36db94e70c709557cb7302d40af9e99e6b))
|
||||
* **javascript:** expose result schema failures ([6d1f39b](https://github.com/morluto/rea/commit/6d1f39b250f8a1c6f924b1a6b9d26d56dff4beee))
|
||||
* **javascript:** expose result schema failures ([99263a7](https://github.com/morluto/rea/commit/99263a720d7106aff059ad7fa43849151a94c963))
|
||||
* **javascript:** hash graph and evidence JSON incrementally ([#646](https://github.com/morluto/rea/issues/646)) ([9f42495](https://github.com/morluto/rea/commit/9f42495d11100adc8646de5c8b62762ab9715ae6))
|
||||
* **javascript:** preserve path.resolve reset semantics ([863ed47](https://github.com/morluto/rea/commit/863ed47de2e35ef9b734e45e3bb44ea9e9dc8ff6))
|
||||
* **javascript:** preserve path.resolve reset semantics ([4177377](https://github.com/morluto/rea/commit/417737748aa96ba38ced7b0b22dbb972187f2ee0))
|
||||
* **javascript:** preserve primitive addition coercion ([#666](https://github.com/morluto/rea/issues/666)) ([94cc847](https://github.com/morluto/rea/commit/94cc8475e0e9e24cd2e8345bd36a743a979e3620))
|
||||
* **javascript:** preserve unknown branches in primitive unions ([75a75cb](https://github.com/morluto/rea/commit/75a75cbb20aa9b2148f9fd0547f613eadda68edd))
|
||||
* **javascript:** preserve unknown branches in primitive unions ([9a2f395](https://github.com/morluto/rea/commit/9a2f3950286a01922be0b07b29c4aefee466ec1e))
|
||||
* **javascript:** read source-map directives only from comments ([#672](https://github.com/morluto/rea/issues/672)) ([7f1fa64](https://github.com/morluto/rea/commit/7f1fa64019e01a654ad0eb80b47d268a58f04e57))
|
||||
* **javascript:** scope candidate ambiguity to query traversal ([6ad62ce](https://github.com/morluto/rea/commit/6ad62cec6abf182a52cf9844145e147fb6399c8e))
|
||||
* **javascript:** scope candidate ambiguity to query traversal ([f94fc79](https://github.com/morluto/rea/commit/f94fc7967b2e5604c144d3638515caad6273a0c6))
|
||||
* **mcp:** retain shutdown failure diagnostics ([09c249d](https://github.com/morluto/rea/commit/09c249deb86068503611fc474e76018d5c2a4d0b))
|
||||
* **mcp:** retain shutdown failure diagnostics ([fd008cd](https://github.com/morluto/rea/commit/fd008cd8e0b8277e691efd8e69dc96ac8e188d9e))
|
||||
* **native:** inspect mixed-signature Mach-O slices ([1dccd0d](https://github.com/morluto/rea/commit/1dccd0dee04abfdc80e8b48e3f05fa39cccf0feb))
|
||||
* **native:** inspect mixed-signature Mach-O slices ([66f3cfa](https://github.com/morluto/rea/commit/66f3cfa594a86cbf1f21fc6d2eca1a82e236c896))
|
||||
* **native:** retain spaced symbols in dyld inventory ([#669](https://github.com/morluto/rea/issues/669)) ([fa02df9](https://github.com/morluto/rea/commit/fa02df9ad3bc3139ef10422dedffac88c4d51d36))
|
||||
* **native:** select concrete arm64e Mach-O slices ([f04445c](https://github.com/morluto/rea/commit/f04445c57c597a8312211139b75da706cb0c2a96))
|
||||
* **native:** select concrete arm64e Mach-O slices ([fd3282a](https://github.com/morluto/rea/commit/fd3282a87e40417cf3d7d6211743809f9bad2dc8))
|
||||
* **process:** honor disabled PID normalization in samples ([44b3627](https://github.com/morluto/rea/commit/44b3627ab0b32c42e545dd2fd095006d249a77f6))
|
||||
* **process:** honor disabled PID normalization in samples ([a4e5736](https://github.com/morluto/rea/commit/a4e57366dd75077bde07f25c7966204cedad929e))
|
||||
* **process:** scope comparison unknowns to their evidence ([#732](https://github.com/morluto/rea/issues/732)) ([9dd6cef](https://github.com/morluto/rea/commit/9dd6cef90abf89f9191bce942e31900870231968))
|
||||
* **reconstruction:** accept function comparison source parameters ([c3bc38b](https://github.com/morluto/rea/commit/c3bc38bf6c74d2175a4680530dcadd6cab0e7791))
|
||||
* **reconstruction:** accept function comparison source parameters ([7a96391](https://github.com/morluto/rea/commit/7a96391001780012f704f4ef5cb6597baa590b7d))
|
||||
* **reference:** bound Dockerfile language detection ([132a3a4](https://github.com/morluto/rea/commit/132a3a46be648b036ed36372a50d6e32a9e7f2e5))
|
||||
* **reference:** bound Dockerfile language detection ([53fa559](https://github.com/morluto/rea/commit/53fa55937f241fc9ef3091eb22314cc370c749b9))
|
||||
* **reference:** classify test suffixes before extensions ([e828acc](https://github.com/morluto/rea/commit/e828acce0e0bf3808773590cf2dbb0bc8227df7b))
|
||||
* **reference:** classify test suffixes before extensions ([8b228ae](https://github.com/morluto/rea/commit/8b228ae7010144b69a243c9682aeb0a1dc4249a3))
|
||||
* **reference:** distinguish computed require member names ([ba35b55](https://github.com/morluto/rea/commit/ba35b5509db60c18dd43e350e0dd4a0b5f229c69))
|
||||
* **reference:** distinguish computed require member names ([df6e476](https://github.com/morluto/rea/commit/df6e476d67b2719c4b998daa3e9ad8a4b157ace2))
|
||||
* **reference:** preserve unresolved absolute module specifiers ([#670](https://github.com/morluto/rea/issues/670)) ([c8aed47](https://github.com/morluto/rea/commit/c8aed4740d52b1eecaa93b3a527e8b3814dc88a4))
|
||||
* **reference:** read linked worktree commit provenance ([#677](https://github.com/morluto/rea/issues/677)) ([d565ecd](https://github.com/morluto/rea/commit/d565ecdc257514f5d0c5a412081e322fc66d4215))
|
||||
* **reference:** recognize CMake build manifests ([#665](https://github.com/morluto/rea/issues/665)) ([1a24be5](https://github.com/morluto/rea/commit/1a24be574d1860931ff8d4c8ec07f3968fff9fd5))
|
||||
* **reference:** retain dynamic import relationships ([a5d2986](https://github.com/morluto/rea/commit/a5d2986b1e517b71a1ed51be433c95e975eafe32))
|
||||
* **reference:** retain dynamic import relationships ([b89de8f](https://github.com/morluto/rea/commit/b89de8feab389477a468f0b89b8dfa35d7ddc102))
|
||||
* **reference:** retain recovered parser diagnostics ([11d0ef2](https://github.com/morluto/rea/commit/11d0ef2e09308ac06889865b3fcf356b8ffa647f))
|
||||
* **reference:** retain recovered parser diagnostics ([ed3a7f0](https://github.com/morluto/rea/commit/ed3a7f070cf0793e22b9a0bbfd1f2ebe9dabcfd0))
|
||||
* **setup:** accept leading BOM in JSON client configs ([a5b970c](https://github.com/morluto/rea/commit/a5b970c6bf6848145a82de51c001410aa4d50f6f))
|
||||
* **setup:** accept leading BOM in JSON client configs ([fe843ec](https://github.com/morluto/rea/commit/fe843ec0b101544e22d7599d93b36377d3e3bc34))
|
||||
* **setup:** align Node runtime checks with package engines ([5802ef8](https://github.com/morluto/rea/commit/5802ef8746412649bb641332f2fa7e69bb1a86a3))
|
||||
* **setup:** align Node runtime checks with package engines ([ea0b733](https://github.com/morluto/rea/commit/ea0b733687582d6cc28b42beaf2fc7a48be07b6a))
|
||||
* **target:** explain the JavaScript directory analysis route ([#733](https://github.com/morluto/rea/issues/733)) ([8d44b3e](https://github.com/morluto/rea/commit/8d44b3edd36b528c308e32328552ca386dc43e91))
|
||||
* **trace:** match typed seeds against member arrays ([65c8797](https://github.com/morluto/rea/commit/65c879757ea09fabc5fc036d6e3395fd2939995c))
|
||||
* **trace:** match typed seeds against member arrays ([38ebd54](https://github.com/morluto/rea/commit/38ebd54776d661ced2e6cc2c5e4b260653a086d9))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **app:** split Setup and ProcessOwnership modules ([#628](https://github.com/morluto/rea/issues/628)) ([3c84080](https://github.com/morluto/rea/commit/3c84080aab5e5aa6c9fc14bafa07c635b537b64d))
|
||||
* **cli:** collapse duplicate CLI layer into src/cli ([#630](https://github.com/morluto/rea/issues/630)) ([494d4db](https://github.com/morluto/rea/commit/494d4db6d9d519f9258a25504cf29b07d61fbb9f))
|
||||
* **contracts:** split tool contracts by family ([#634](https://github.com/morluto/rea/issues/634)) ([5295d56](https://github.com/morluto/rea/commit/5295d56b105074b290e0d881df43cd13e34a2f8b))
|
||||
* **domain:** consolidate canonical ordering and digest helpers ([#624](https://github.com/morluto/rea/issues/624)) ([ba3869c](https://github.com/morluto/rea/commit/ba3869c597133b63fdcc0077c558923ae2e17a3f))
|
||||
* **domain:** split error taxonomy into thematic modules ([#636](https://github.com/morluto/rea/issues/636)) ([735c100](https://github.com/morluto/rea/commit/735c10059ca161967d71ccf8fa3f62011d2a22ca))
|
||||
* **native:** extract FAT header format switch in Mach-O slice selection ([#632](https://github.com/morluto/rea/issues/632)) ([38b2933](https://github.com/morluto/rea/commit/38b293329ca06c424a58bb59a28728f19015ce22))
|
||||
* **runtime:** centralize safe JSON parsing and preserve error causes ([#629](https://github.com/morluto/rea/issues/629)) ([e7f54b3](https://github.com/morluto/rea/commit/e7f54b3310b7f45af11c1754e468d4d3804191db))
|
||||
* **runtime:** name all silent catch causes without changing diagnostics ([#637](https://github.com/morluto/rea/issues/637)) ([5f27e02](https://github.com/morluto/rea/commit/5f27e02fae670774667de3618c88ba2ab0e7b661))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add the official Trendshift badge to README headers ([b6c21b1](https://github.com/morluto/rea/commit/b6c21b1db5d4ff4be0968aa26693916537622a06))
|
||||
* add the official Trendshift badge to README headers ([7d4ed59](https://github.com/morluto/rea/commit/7d4ed59816388409885310f40c12dfdcdd2d9246))
|
||||
* align MCP discovery and skill guidance with the stable catalog ([#664](https://github.com/morluto/rea/issues/664)) ([39db9fe](https://github.com/morluto/rea/commit/39db9fe4245cc8142f57f4feeadaff64cb894e31))
|
||||
* **browser:** add an interaction example that captures the visible result ([#750](https://github.com/morluto/rea/issues/750)) ([91daaa6](https://github.com/morluto/rea/commit/91daaa68aea3bbee02b837ac64d3ad13127d601e))
|
||||
* clarify onboarding and align provider guidance ([#745](https://github.com/morluto/rea/issues/745)) ([5af1699](https://github.com/morluto/rea/commit/5af169910391f6b95492865247fabf3c4735a9d6))
|
||||
* correct CLI examples and documentation checks ([#655](https://github.com/morluto/rea/issues/655)) ([fc80ea8](https://github.com/morluto/rea/commit/fc80ea88dfb7bde43e89db0f341c99d7b53c1a7c))
|
||||
* fix architecture diagram and pretty-print tool catalog ([#626](https://github.com/morluto/rea/issues/626)) ([5e7f35c](https://github.com/morluto/rea/commit/5e7f35c7c3ea8d504421a2623eafd1d73fea08a0))
|
||||
* **ghidra:** explain first-query deadlines and recovery ([#736](https://github.com/morluto/rea/issues/736)) ([e2c23f3](https://github.com/morluto/rea/commit/e2c23f3640f83c13587603c763dbd7b5ef1bbbb0))
|
||||
* **hopper:** document HopperTargetLease realpath fallback ([#711](https://github.com/morluto/rea/issues/711)) ([d3f498c](https://github.com/morluto/rea/commit/d3f498ceec65e4f61f39036ab943ace7ef1b9de5))
|
||||
* **installation:** pin manual MCP registration to the release version ([#658](https://github.com/morluto/rea/issues/658)) ([8dd1d84](https://github.com/morluto/rea/commit/8dd1d8447146b9962ec7ab2f245b0fe47d3ddf44))
|
||||
* update Trendshift repository badge ([3f6d6dc](https://github.com/morluto/rea/commit/3f6d6dc756d99ca6e7cdc1737522842fcbaee839))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* extract shared fixtures, drop knip carve-outs ([#627](https://github.com/morluto/rea/issues/627)) ([55e5eb0](https://github.com/morluto/rea/commit/55e5eb090bb3d6cf347d7cc2f8f028b35b7461ba))
|
||||
* **hopper:** synchronize startup cancellation with launcher readiness ([#662](https://github.com/morluto/rea/issues/662)) ([88a3c3f](https://github.com/morluto/rea/commit/88a3c3f8a4bf5cb31f60bca4696431c4e4512b30))
|
||||
|
||||
## [4.0.1](https://github.com/morluto/rea/compare/rea-agents-4.0.0...rea-agents-4.0.1) (2026-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **release:** allow npm propagation time ([2ef02ce](https://github.com/morluto/rea/commit/2ef02ced1603809f0fc6984612de31dce1801dd8))
|
||||
|
||||
## [4.0.0](https://github.com/morluto/rea/compare/rea-agents-3.2.1...rea-agents-4.0.0) (2026-10-05)
|
||||
|
||||
|
||||
|
||||
+4
-5
@@ -20,7 +20,7 @@ Turbo. Turbo caches deterministic builds and static checks across Git
|
||||
worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
`npm run metadata:generate` before building.
|
||||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
|
||||
|
||||
## Development feedback and PR verification
|
||||
|
||||
@@ -52,9 +52,8 @@ require the matching real-provider `verify:*` lane.
|
||||
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
|
||||
the same configuration. Pre-commit formats and lints staged files; pre-push runs
|
||||
`check:fast`.
|
||||
`docs:check` checks committed generated metadata without rendering API HTML.
|
||||
`docs:generate` regenerates those files; render API HTML separately with
|
||||
`npm run docs:api:cached`. PR CI renders and uploads the `api-docs` artifact.
|
||||
`docs:check` checks committed generated metadata. `docs:generate` regenerates
|
||||
those files, and the docs CI lane checks them.
|
||||
Real-provider execution remains uncached; deterministic builds use Turbo.
|
||||
|
||||
Local `npm test` runs every deterministic Vitest project without coverage or
|
||||
@@ -101,7 +100,7 @@ Describe the behavior change and verification performed in the pull request. Nev
|
||||
|
||||
## Maintainer release checklist
|
||||
|
||||
Run `npm run check:pr`, `npm run docs:api:cached`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
|
||||
Run `npm run check:pr`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
|
||||
|
||||
```bash
|
||||
npm pack
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
### Reverse engineer anything with agents, from app behavior down to native binaries.
|
||||
### One MCP for reverse engineering across binaries, applications, and runtime behavior.
|
||||
|
||||
**See a feature you like. Understand how it works, down to the binary level.**
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
[](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>
|
||||
|
||||
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
@@ -47,7 +49,7 @@ See a feature in an app that you want in your own product? Ask your agent to inv
|
||||
|
||||
REA connects your agent to tools for inspecting native binaries, JavaScript and Electron apps, .NET assemblies, and websites. You can also use the same tools from your terminal. Analysis runs locally, and results include the evidence and limitations behind each conclusion.
|
||||
|
||||
Setup configures your agent and connects it to Hopper or Ghidra. If you need an analysis tool, setup can install Hopper for you.
|
||||
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.
|
||||
|
||||
## Just ask your agent
|
||||
|
||||
@@ -110,17 +112,36 @@ Setup shows its changes before applying them and backs up existing configuration
|
||||
|
||||
After setup, restart your agent and [describe the app or feature](#just-ask-your-agent) you want to understand. Hopper can run in demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
|
||||
|
||||
REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration).
|
||||
REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration).
|
||||
|
||||
### From the terminal with npx
|
||||
|
||||
After setup, run:
|
||||
### Install the skill separately
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
This installs agent instructions, not MCP registration or analysis engines.
|
||||
Follow the skill's [conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed):
|
||||
prepare a scoped `npx rea-agents setup` plan if registration is missing, review
|
||||
and approve its actual changes, then restart/reconnect and verify the tools.
|
||||
Guided setup above installs a version-matched skill by default. The skills.sh
|
||||
route uses repository instructions, which can be ahead of the released server;
|
||||
see [released package and main](docs/installation.md#released-package-and-main).
|
||||
|
||||
### First result from the terminal
|
||||
|
||||
For your extracted JavaScript/Electron application tree or ASAR, run:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Replace the path with your target (for example, `"D:/apps/example"` on Windows).
|
||||
This returns inline Evidence, recovered graph, limitations, and unknowns without
|
||||
MCP setup, Hopper, Ghidra, or executing the application. For a native app, configure
|
||||
its engine first, then use `analyze` with that app's path. Run `doctor` when you
|
||||
need diagnosis; it is not a prerequisite for each analysis.
|
||||
|
||||
### Install the rea command
|
||||
|
||||
Install the command-line interface:
|
||||
@@ -142,14 +163,22 @@ Update either installation with `rea update`.
|
||||
|
||||
### Requirements
|
||||
|
||||
Static JavaScript inspection requires the Node/npm runtime only. Host and
|
||||
external-tool prerequisites depend on the selected workflow; the native
|
||||
provider guides describe their supported platforms.
|
||||
|
||||
- macOS 12 or newer
|
||||
- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
|
||||
- Node.js 22.19+ or 24.11+ (including newer releases)
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+
|
||||
- npm; REA does not require or install a particular npm version
|
||||
|
||||
Native binary analysis requires [Hopper](https://www.hopperapp.com/) or [Ghidra](#ghidra-read-only-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. REA can use Ghidra that you have already installed.
|
||||
Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghidra](#ghidra-analysis-provider), or [IDA Pro](#ida-pro-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. Ghidra and IDA are bring-your-own providers.
|
||||
|
||||
Windows Ghidra support is experimental and currently unavailable. The required Windows process ownership, private-directory permissions, and safe-path checks are not implemented. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for the remaining requirements.
|
||||
Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff.
|
||||
|
||||
Static APK analysis uses a separately supplied headless JADX JAR and Java, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures.
|
||||
|
||||
Repository main includes experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
|
||||
|
||||
If something is not working, run:
|
||||
|
||||
@@ -182,7 +211,7 @@ Install the missing packages and rerun `rea setup`. The Linux demo needs Xvfb, P
|
||||
|
||||
REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
|
||||
|
||||
### Ghidra read-only analysis provider
|
||||
### Ghidra analysis provider
|
||||
|
||||
Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It requires **Ghidra 12.1.4** and a **64-bit JDK 21**. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
|
||||
|
||||
@@ -198,13 +227,17 @@ rea providers --json
|
||||
|
||||
Setup checks the installations and saves their paths in your selected agents' configuration after approval. Ghidra and Java must already be installed; REA does not download or change them.
|
||||
|
||||
The adapter exposes **22 read-only operations** for functions, strings, symbols, assembly, decompilation, calls, references, instructions, and data types. These also support REA's overview, search, call-graph, and function-analysis workflows. Ghidra does not provide GUI controls or annotation changes through REA.
|
||||
The adapter exposes inventory, function, memory and load-image inspection, plus atomic function annotation edits on Linux and macOS. `annotate_native_function` edits names and entry comments in the session database and returns a refreshed function dossier; executable bytes stay unchanged. Ghidra does not provide GUI controls through REA.
|
||||
|
||||
Opening a Ghidra target selects its provider; the first analysis query starts import and auto-analysis. That query can take longer than a client's default request deadline. See [first-query deadlines and recovery](docs/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery) for an SDK example and cancellation recovery.
|
||||
|
||||
REA analyzes a temporary copy of the target and removes the temporary project when the session closes. Results identify what Ghidra observed and what it could not resolve. Decompilation produces pseudocode rather than the original source.
|
||||
|
||||
Ghidra also imports DOS MZ executables with an explicit 16-bit x86 real-mode profile. Function results include complete observed body ranges, distinguishing owned bytes from the enclosing span. See the [DOS analysis guide](docs/ghidra-dos.md) for addresses, packing, and verification boundaries.
|
||||
See [optional NativeAOT metadata recovery](docs/ghidra-nativeaot.md) for the pinned
|
||||
headless adapter, supported layout and existing native-tool workflow.
|
||||
|
||||
Windows operations remain unavailable pending the controls described in the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification.
|
||||
Windows Ghidra P0 uses bundled native controls for its read-only native x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification.
|
||||
|
||||
To remove only REA-owned MCP registrations and the managed skill:
|
||||
|
||||
@@ -215,6 +248,19 @@ rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
|
||||
|
||||
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
|
||||
|
||||
### IDA Pro analysis provider
|
||||
|
||||
Already have [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) working? REA can reuse its MCP registration for read-only analysis of the current GUI target, or use its database supervisor to open and analyze a supplied binary headlessly.
|
||||
|
||||
```bash
|
||||
export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json
|
||||
rea function /absolute/path/to/program main --provider ida --json
|
||||
```
|
||||
|
||||
The registration selects `attached` (default, legacy 1.4 tools) or `headless` (modern database supervisor). Setup can preserve this file reference in your selected agent's REA registration. REA adapts existing analysis contracts and manages its own headless database; it leaves an attached GUI database open and never saves it. Results stay live because external IDA database changes are not immutable snapshots.
|
||||
|
||||
See the [IDA provider guide](docs/ida-provider.md) for upstream installation links, exact configuration examples, Windows-native headless operation, lifecycle cleanup, and coverage. The initial real workflows cover a Windows GUI and Windows x64 headless IDA 9.3; other engine/platform combinations remain unverified. Use a package version that includes this adapter; repository main can lead the npm release.
|
||||
|
||||
### CLI or agent?
|
||||
|
||||
| If you want to… | Use |
|
||||
@@ -305,11 +351,13 @@ See [native investigation](docs/native-investigation.md) for keyed archives, ins
|
||||
|
||||
| Tool family | Count | Examples |
|
||||
| ------------------------- | ----: | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native inspection | 39 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
|
||||
| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
|
||||
| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery |
|
||||
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper |
|
||||
| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction |
|
||||
| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons |
|
||||
| Firmware | 2 | Linux firmware region inspection and explicit extraction |
|
||||
| Android APK | 5 | package and manifest declarations, class search, member inventories, method decompilation, and incoming static references |
|
||||
| Browser observation | 9 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
|
||||
| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation |
|
||||
| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events |
|
||||
@@ -320,9 +368,11 @@ The public interface describes what the agent is trying to learn. Providers deci
|
||||
|
||||
## Current status
|
||||
|
||||
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites; use `rea capabilities` to check what is available on your host.
|
||||
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites. `rea capabilities` and `rea providers` describe the binary-session providers and auxiliary operations; they are not an inventory of every browser, Android, or application workflow. Use the connected MCP tool list and `binary_session` tool availability for the full MCP surface, and the relevant guide for each tool's prerequisites. Repository main can be ahead of [the npm release](docs/installation.md#released-package-and-main).
|
||||
|
||||
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through Hopper or Ghidra. Inspect functions, strings, assembly, decompilation, calls, and references. Hopper also accepts `.hop` databases and supports annotations.
|
||||
Static Android APK inspection is verified on Linux with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
|
||||
|
||||
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through a selected deep provider. Hopper and Ghidra cover broad inventory and function analysis; the IDA adapter supplies its documented read-only function/string operations. Hopper also accepts `.hop` databases and supports annotations.
|
||||
- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support.
|
||||
- **JavaScript and Electron:** Map modules, imports, source maps, routes, IPC channels, storage, and native add-ons without running the app. Compare builds and trace a feature across the recovered graph. Dynamic and ambiguous relationships remain unresolved. See [JavaScript application workflows](docs/javascript-application-workflows.md).
|
||||
- **Websites:** Inspect a selected page in an existing Chrome-family browser. Capture page structure, network metadata, script evidence, and screenshots requested by the call. Passive observation does not navigate or execute page JavaScript. See [browser observation](docs/browser-observation.md).
|
||||
@@ -333,7 +383,7 @@ REA supports native application, JavaScript, Electron, .NET, and browser investi
|
||||
- **Open questions:** Track unresolved findings, contradictions, and follow-up probes. Reconstruction checks report pass, fail, or unknown rather than treating missing evidence as a pass.
|
||||
- **Guided workflows:** Start six [MCP investigation workflows](docs/mcp-prompts.md) with suggestions based on your current session.
|
||||
|
||||
Windows Ghidra operations are currently unavailable. Hopper-only features, such as GUI controls and annotations, are not available through Ghidra.
|
||||
Windows x64 Ghidra P0 supports native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS with 25 read-only operations. Linux/macOS Ghidra additionally supports atomic session-scoped function names and entry comments. Ghidra has no GUI controls; Windows P0 has no mutation authority.
|
||||
|
||||
### Website observation with CDP
|
||||
|
||||
@@ -363,8 +413,12 @@ the launched browser. Connect mode accepts one exact loopback CDP target and
|
||||
disconnects without closing the external browser. The request supplies the
|
||||
selected executable or endpoint, actions, and any origin or environment
|
||||
selections needed by the scenario. Scenario JSON contains secret references and
|
||||
environment-variable names, never secret values. See the
|
||||
[browser scenario contract](docs/browser-scenario-contract.md).
|
||||
environment-variable names, never secret values. The default capture retains
|
||||
only the final URL; request `dom`, `accessibility`, or `screenshot` when an
|
||||
interaction changes the page without navigating. For asynchronous updates,
|
||||
wait for a page-specific result-ready condition before capture. See the
|
||||
[browser scenario contract](docs/browser-scenario-contract.md), including its
|
||||
interaction example.
|
||||
|
||||
### Node and Electron V8 Inspector observation
|
||||
|
||||
@@ -382,7 +436,7 @@ relationships, event activity, IPC, process identity, or Electron roles. See
|
||||
[Node and Electron runtime observation](docs/javascript-runtime-observation.md)
|
||||
for the exact coverage.
|
||||
|
||||
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
|
||||
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, and generated schemas.
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -422,7 +476,7 @@ Setup offers supported agent integrations for selection. Existing REA registrati
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@4.0.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,12 +505,19 @@ flowchart LR
|
||||
REA --> Session["Target-bound session router"]
|
||||
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
|
||||
Registry --> Hopper["Hopper provider"]
|
||||
Registry --> Ghidra["Ghidra provider<br/>read-only inventory + function analysis"]
|
||||
Registry --> Ghidra["Ghidra provider<br/>inventory + function analysis + annotations"]
|
||||
Registry --> Ida["IDA MCP provider<br/>attached GUI or owned headless database"]
|
||||
Hopper --> Runtime["Owned provider runtime<br/>deadline + bounded diagnostics + cleanup"]
|
||||
Ghidra --> Runtime
|
||||
Ida --> IdaRuntime["Upstream MCP lifecycle<br/>live observations + owned database cleanup"]
|
||||
IdaRuntime --> Target
|
||||
Session --> Native["Native macOS provider"]
|
||||
Session --> Artifact["Artifact graph provider"]
|
||||
REA --> Browser["Browser CDP provider"]
|
||||
REA --> Android["Android static provider<br/>headless JADX adapter"]
|
||||
Android --> Runtime
|
||||
REA --> Firmware["Firmware providers<br/>Binwalk / Unblob adapters"]
|
||||
Firmware --> Runtime
|
||||
REA --> Process["Process capture provider"]
|
||||
Runtime --> Target["Target software"]
|
||||
Process --> Target
|
||||
@@ -516,11 +577,11 @@ For MCP, pass the optional selector on `open_binary`:
|
||||
}
|
||||
```
|
||||
|
||||
Use `--provider`, or `provider_id` in MCP, to choose Hopper or Ghidra for a target. This choice overrides `REA_ANALYSIS_PROVIDER`.
|
||||
Use `--provider`, or `provider_id` in MCP, to choose Hopper, Ghidra, or IDA for a target. This choice overrides `REA_ANALYSIS_PROVIDER`.
|
||||
|
||||
With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool.
|
||||
|
||||
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra exposes 22 read-only operations on supported Linux and macOS hosts. GUI controls and annotation changes require Hopper. Windows Ghidra operations remain unavailable.
|
||||
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls.
|
||||
|
||||
The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details.
|
||||
|
||||
@@ -532,7 +593,9 @@ The session also reports active work and cleanup status. If a caller times out,
|
||||
| `1` | The operation could not complete, for example because of invalid input, host permission denial, cancellation, or timeout. Structured output reports the reason when available. |
|
||||
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
|
||||
|
||||
`setup` returns `1` for `planned`, `needs_confirmation`, or `needs_human`
|
||||
`setup --dry-run` returns status `planned` and exits `0`. A setup result with
|
||||
status `cancelled` also exits `0`.
|
||||
`setup` returns `1` for `needs_confirmation` or `needs_human`
|
||||
because configuration is not ready; rerun it after approval or remediation.
|
||||
`doctor` returns `1` when required checks for its readiness scope are unhealthy.
|
||||
Unavailable optional provider prerequisites remain visible as informational
|
||||
@@ -544,7 +607,7 @@ cannot hide its failure:
|
||||
|
||||
```bash
|
||||
set -o pipefail
|
||||
rea inventory-artifact ./app.asar --json | jq . > inventory.json
|
||||
rea inspect-artifact ./app.asar --json | jq . > inspection.json
|
||||
```
|
||||
|
||||
## Current Hopper provider
|
||||
@@ -592,11 +655,11 @@ verified or absent.
|
||||
|
||||
## Security model
|
||||
|
||||
Analysis runs locally. REA communicates with Hopper and Ghidra through authenticated private local sockets. Your agent or model provider has its own data policy.
|
||||
Analysis runs locally. REA communicates with Hopper and Ghidra through authenticated private local sockets and with IDA through its configured local MCP registration. Your agent or model provider has its own data policy.
|
||||
|
||||
Runtime requests act on the declared target and lifecycle. Analysis tools and launched targets run with your user permissions, and native UI capture still depends on macOS Accessibility and Screen Recording access. Static JavaScript analysis does not execute extracted modules; use direct browser, Electron, or process capture when runtime behavior is needed.
|
||||
|
||||
Windows Ghidra operations are blocked until REA implements the required process ownership, private-directory permissions, and safe-path checks. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -624,7 +687,7 @@ No. Setup can install Hopper for you, but Hopper remains separate software with
|
||||
<details>
|
||||
<summary><strong>Does REA install or include Ghidra or Java?</strong></summary>
|
||||
|
||||
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-read-only-analysis-provider).
|
||||
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-analysis-provider).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -651,7 +714,7 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution checks, and [docs/testing.md](docs/testing.md) for test scopes and real-tool verification. PR CI publishes generated API documentation as the `api-docs` artifact.
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution checks, and [docs/testing.md](docs/testing.md) for test scopes and real-tool verification. `npm run docs:check` checks committed generated documents; `npm run docs:generate` regenerates them.
|
||||
|
||||
`npm run verify:agent` evaluates native, JavaScript, managed, and browser investigation tasks through a real local Codex CLI. Its report covers tool selection, repeated calls, token use, completion quality, and handling of permissions and unknowns.
|
||||
|
||||
|
||||
+43
-16
@@ -15,6 +15,8 @@
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -92,19 +94,33 @@ npx rea-agents setup
|
||||
|
||||
### الاستخدام مع وكيل
|
||||
|
||||
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
|
||||
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
|
||||
|
||||
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
|
||||
|
||||
### الاستخدام من الطرفية
|
||||
|
||||
بعد الإعداد، شغّل:
|
||||
### Skill instructions and a first CLI result
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
This installs instructions, not MCP registration or analysis engines. Follow the
|
||||
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
`npx rea-agents setup` installs a matching skill and registers selected clients
|
||||
only after showing the plan and receiving approval. Restart/reconnect after
|
||||
registration, then verify REA tools in the active session. Repository skills can
|
||||
be ahead of the [released package](docs/installation.md#released-package-and-main).
|
||||
|
||||
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
|
||||
engine is required:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Replace the path with your target; Windows paths such as `"D:/apps/example"`
|
||||
are supported by this static workflow. Native analysis requires its own engine.
|
||||
|
||||
### تثبيت أمر rea
|
||||
|
||||
ثبت واجهة سطر الأوامر:
|
||||
@@ -126,7 +142,7 @@ rea setup
|
||||
|
||||
- macOS 12 أو أحدث
|
||||
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت
|
||||
- Node.js 22.19+ أو 24.11+ وnpm
|
||||
- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm
|
||||
|
||||
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
|
||||
|
||||
@@ -134,7 +150,7 @@ rea setup
|
||||
|
||||
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
|
||||
|
||||
تحليل Ghidra على Windows غير متاح. لم تُنفذ بعد ضوابط ملكية العمليات وأذونات المجلدات الخاصة والتحقق الآمن من المسارات. حتى التثبيت الصحيح لـ Ghidra وJava لا يفعّل التحليل. راجع [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
|
||||
### تشخيص المشكلات
|
||||
|
||||
@@ -150,12 +166,21 @@ rea setup
|
||||
|
||||
## حالة الدعم
|
||||
|
||||
يوفر REA عبر CLI وMCP تحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. تعتمد العمليات المتاحة على الجهاز والهدف وأداة التحليل المختارة.
|
||||
The current repository capabilities and platform requirements are described in
|
||||
[the English support guide](README.md#current-status). Main can be ahead of the
|
||||
[npm release](docs/installation.md#released-package-and-main).
|
||||
|
||||
- يدعم Hopper التحليل الأصلي والتعليقات؛ ويختلف سلوك واجهته الرسومية حسب المنصة.
|
||||
- يقدم Ghidra على Linux x64 وmacOS x64/arm64 عددًا قدره 22 عملية للقراءة فقط: الجرد والبحث وفك الترجمة والتعليمات والاستدعاءات والمراجع والأنواع. لا يقدم عمليات واجهة رسومية أو تعديل.
|
||||
- لسير عمل المتصفح وElectron وتشغيل العمليات متطلبات مستقلة للإعداد والموافقة وإدارة دورة الحياة. راجع [English README](README.md#current-status) للتفاصيل الكاملة.
|
||||
- تحليل Ghidra على Windows غير مفعّل. تابع التقدم في [#527](https://github.com/morluto/rea/issues/527).
|
||||
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
|
||||
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
|
||||
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
|
||||
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
|
||||
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
|
||||
- Browser, Electron, and process requests specify their target, actions, and
|
||||
lifecycle directly. They require the host's actual access, without separate
|
||||
REA permission grants. Setup configuration writes and Hopper installation
|
||||
still require approval of their exact plan.
|
||||
- `rea capabilities` describes binary-session operations rather than every
|
||||
application/runtime tool. Consult the full MCP tool list and relevant guide.
|
||||
|
||||
## استقصاء كامل بطلب واحد
|
||||
|
||||
@@ -202,11 +227,13 @@ rea setup
|
||||
|
||||
| عائلة الأدوات | العدد | الاستخدام |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 39 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
|
||||
| فحص الملفات التنفيذية | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
|
||||
| التحليل المركب | 14 | النظرة العامة وتحليل الدوال وفك الترجمة الدفعي ومخطط الاستدعاء وفحص Swift وObjC |
|
||||
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift |
|
||||
| الملفات والحزم | 5 | فحص الدلائل والحزم وInterface Builder وموارد Apple والاستخراج |
|
||||
| .NET PE/CLI | 7 | هوية التجميع والبيانات الوصفية وCIL والاستدعاءات الأصلية والمقارنة واستيراد إعادة البناء |
|
||||
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة واستخراجها الصريح على Linux |
|
||||
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف والأعضاء وفك ترجمة الأساليب والمراجع الثابتة |
|
||||
| مراقبة المتصفح | 9 | الصفحات والبرامج النصية وخرائط المصدر وWebMCP والصور ومقارنة الالتقاط |
|
||||
| تحليل Electron | 5 | مراقبة الصفحات وبنية التطبيق وربط النتائج الثابتة بنتائج التشغيل |
|
||||
| وقت تشغيل JavaScript | 2 | الاتصال بهدف Node/Electron Inspector موجود لمراقبة البرامج وسياقات التنفيذ |
|
||||
@@ -219,7 +246,7 @@ rea setup
|
||||
|
||||
## استخدام REA مع وكلاء برمجة آخرين
|
||||
|
||||
يدعم Setup Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
|
||||
يدعم 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 -->
|
||||
|
||||
@@ -228,7 +255,7 @@ rea setup
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@4.0.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-27
@@ -15,6 +15,8 @@
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -92,19 +94,33 @@ Setup は最初に連携するエージェントを複数選択できるよう
|
||||
|
||||
### エージェントで使う
|
||||
|
||||
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
|
||||
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
|
||||
|
||||
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
|
||||
|
||||
### ターミナルで使う
|
||||
|
||||
設定後に実行します:
|
||||
### Skill instructions and a first CLI result
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
This installs instructions, not MCP registration or analysis engines. Follow the
|
||||
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
`npx rea-agents setup` installs a matching skill and registers selected clients
|
||||
only after showing the plan and receiving approval. Restart/reconnect after
|
||||
registration, then verify REA tools in the active session. Repository skills can
|
||||
be ahead of the [released package](docs/installation.md#released-package-and-main).
|
||||
|
||||
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
|
||||
engine is required:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Replace the path with your target; Windows paths such as `"D:/apps/example"`
|
||||
are supported by this static workflow. Native analysis requires its own engine.
|
||||
|
||||
### rea コマンドをインストール
|
||||
|
||||
コマンドラインツールをインストールします:
|
||||
@@ -126,7 +142,7 @@ rea setup
|
||||
|
||||
- macOS 12 以降
|
||||
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
|
||||
- Node.js 22.19+ または 24.11+ と npm
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
|
||||
|
||||
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
|
||||
|
||||
@@ -134,7 +150,7 @@ Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.4 と
|
||||
|
||||
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
|
||||
|
||||
Windows の Ghidra 解析は利用できません。プロセスの所有権、プライベートディレクトリの権限、安全なパス検証が未実装のため、Ghidra と Java を正しくインストールしても有効になりません。[Windows Ghidra P0](docs/windows-ghidra-p0.md)を参照してください。
|
||||
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
|
||||
### トラブルシューティング
|
||||
|
||||
@@ -150,12 +166,21 @@ Linux の既定ランチャーは `/opt/hopper/bin/Hopper` です。別の場所
|
||||
|
||||
## 現在の対応状況
|
||||
|
||||
REA は CLI と MCP からネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトを解析できます。使える操作はホスト、対象、選択した解析ツールによって異なります。
|
||||
The current repository capabilities and platform requirements are described in
|
||||
[the English support guide](README.md#current-status). Main can be ahead of the
|
||||
[npm release](docs/installation.md#released-package-and-main).
|
||||
|
||||
- Hopper はネイティブ解析と注釈操作に対応します。GUI の動作はプラットフォームによって異なります。
|
||||
- Ghidra は Linux x64 と macOS x64/arm64 で、一覧、検索、逆コンパイル、アセンブリ、呼び出し、参照、命令、型の検査など 22 の読み取り専用操作を提供します。GUI 操作や変更操作はありません。
|
||||
- ブラウザー、Electron、プロセスの実行時ワークフローには、それぞれ設定、承認、終了処理の要件があります。詳細は [English README](README.md#current-status) を参照してください。
|
||||
- Windows の Ghidra 解析は未対応です。進捗は [#527](https://github.com/morluto/rea/issues/527) で確認できます。
|
||||
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
|
||||
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
|
||||
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
|
||||
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
|
||||
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
|
||||
- Browser, Electron, and process requests specify their target, actions, and
|
||||
lifecycle directly. They require the host's actual access, without separate
|
||||
REA permission grants. Setup configuration writes and Hopper installation
|
||||
still require approval of their exact plan.
|
||||
- `rea capabilities` describes binary-session operations rather than every
|
||||
application/runtime tool. Consult the full MCP tool list and relevant guide.
|
||||
|
||||
## ひとつのプロンプトで調査を完結
|
||||
|
||||
@@ -189,18 +214,20 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | --------------------------------------------------------------------------- |
|
||||
| バイナリ検査 | 39 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
|
||||
| 組み合わせた解析 | 14 | 概要、関数解析、一括逆コンパイル、呼び出しグラフ、Swift と ObjC |
|
||||
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
|
||||
| ファイルとパッケージ | 5 | ディレクトリとパッケージ、Interface Builder、Apple リソース、抽出 |
|
||||
| .NET PE/CLI | 7 | アセンブリ識別、メタデータ、CIL、ネイティブ呼び出し、比較、再構築の取り込み |
|
||||
| ブラウザー観察 | 9 | ページ、スクリプト、ソースマップ、WebMCP、スクリーンショット、比較 |
|
||||
| Electron 解析 | 5 | ページ観察、アプリ構造、静的・実行時結果の対応付け |
|
||||
| JavaScript 実行時 | 2 | 既存の Node/Electron Inspector に接続し、スクリプトと実行コンテキストを観察 |
|
||||
| アプリのワークフロー | 7 | 機能追跡、バージョン比較、戻り値構造の比較、再実装の検証 |
|
||||
| バイナリセッション | 21 | 対象切り替え、根拠の保存、プロセス・関数比較、未解決事項の記録 |
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | ------------------------------------------------------------------------------- |
|
||||
| バイナリ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
|
||||
| 組み合わせた解析 | 14 | 概要、関数解析、一括逆コンパイル、呼び出しグラフ、Swift と ObjC |
|
||||
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
|
||||
| ファイルとパッケージ | 5 | ディレクトリとパッケージ、Interface Builder、Apple リソース、抽出 |
|
||||
| .NET PE/CLI | 7 | アセンブリ識別、メタデータ、CIL、ネイティブ呼び出し、比較、再構築の取り込み |
|
||||
| ファームウェア | 2 | Linux の領域検査と明示的な展開 |
|
||||
| Android APK | 5 | パッケージと manifest、クラス検索、メンバー一覧、メソッド逆コンパイル、静的参照 |
|
||||
| ブラウザー観察 | 9 | ページ、スクリプト、ソースマップ、WebMCP、スクリーンショット、比較 |
|
||||
| Electron 解析 | 5 | ページ観察、アプリ構造、静的・実行時結果の対応付け |
|
||||
| JavaScript 実行時 | 2 | 既存の Node/Electron Inspector に接続し、スクリプトと実行コンテキストを観察 |
|
||||
| アプリのワークフロー | 7 | 機能追跡、バージョン比較、戻り値構造の比較、再実装の検証 |
|
||||
| バイナリセッション | 21 | 対象切り替え、根拠の保存、プロセス・関数比較、未解決事項の記録 |
|
||||
|
||||
## ロードマップ
|
||||
|
||||
@@ -208,7 +235,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
||||
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
|
||||
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 -->
|
||||
|
||||
@@ -217,7 +244,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@4.0.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-27
@@ -15,6 +15,8 @@
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -93,19 +95,33 @@ Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합
|
||||
|
||||
### 에이전트에서 사용하기
|
||||
|
||||
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
|
||||
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
|
||||
|
||||
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
|
||||
|
||||
### 터미널에서 실행하기
|
||||
|
||||
설정 후 실행하세요.
|
||||
### Skill instructions and a first CLI result
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
This installs instructions, not MCP registration or analysis engines. Follow the
|
||||
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
`npx rea-agents setup` installs a matching skill and registers selected clients
|
||||
only after showing the plan and receiving approval. Restart/reconnect after
|
||||
registration, then verify REA tools in the active session. Repository skills can
|
||||
be ahead of the [released package](docs/installation.md#released-package-and-main).
|
||||
|
||||
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
|
||||
engine is required:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Replace the path with your target; Windows paths such as `"D:/apps/example"`
|
||||
are supported by this static workflow. Native analysis requires its own engine.
|
||||
|
||||
### rea 명령 설치하기
|
||||
|
||||
명령줄 도구를 설치하세요.
|
||||
@@ -127,7 +143,7 @@ rea setup
|
||||
|
||||
- macOS 12 이상
|
||||
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
|
||||
- Node.js 22.19+ 또는 24.11+와 npm
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
|
||||
|
||||
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
|
||||
|
||||
@@ -135,7 +151,7 @@ Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.4와 완
|
||||
|
||||
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
|
||||
|
||||
Windows Ghidra 분석은 사용할 수 없습니다. 프로세스 소유권, 비공개 디렉터리 권한, 안전한 경로 검사가 구현되지 않았으므로 Ghidra와 Java를 올바르게 설치해도 분석을 활성화할 수 없습니다. [Windows Ghidra P0](docs/windows-ghidra-p0.md)를 참고하세요.
|
||||
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
|
||||
### 문제 해결
|
||||
|
||||
@@ -151,12 +167,21 @@ Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다
|
||||
|
||||
## 현재 지원 범위
|
||||
|
||||
REA는 CLI와 MCP로 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트를 분석합니다. 사용할 수 있는 작업은 호스트, 대상, 선택한 분석 도구에 따라 다릅니다.
|
||||
The current repository capabilities and platform requirements are described in
|
||||
[the English support guide](README.md#current-status). Main can be ahead of the
|
||||
[npm release](docs/installation.md#released-package-and-main).
|
||||
|
||||
- Hopper는 네이티브 분석과 주석 작업을 지원합니다. GUI 동작은 플랫폼에 따라 다릅니다.
|
||||
- Ghidra는 Linux x64와 macOS x64/arm64에서 목록, 검색, 디컴파일, 어셈블리, 호출, 참조, 명령어, 타입 검사 등 읽기 전용 작업 22개를 제공합니다. GUI나 변경 작업은 제공하지 않습니다.
|
||||
- 브라우저, Electron, 프로세스 런타임 워크플로에는 각각 설정, 승인, 종료 처리 요구 사항이 있습니다. 자세한 내용은 [English README](README.md#current-status)를 참고하세요.
|
||||
- Windows Ghidra 분석은 아직 사용할 수 없습니다. 진행 상황은 [#527](https://github.com/morluto/rea/issues/527)에서 확인하세요.
|
||||
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
|
||||
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
|
||||
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
|
||||
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
|
||||
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
|
||||
- Browser, Electron, and process requests specify their target, actions, and
|
||||
lifecycle directly. They require the host's actual access, without separate
|
||||
REA permission grants. Setup configuration writes and Hopper installation
|
||||
still require approval of their exact plan.
|
||||
- `rea capabilities` describes binary-session operations rather than every
|
||||
application/runtime tool. Consult the full MCP tool list and relevant guide.
|
||||
|
||||
## 하나의 프롬프트로 전체 조사
|
||||
|
||||
@@ -190,18 +215,20 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
|
||||
## 조사 도구 카탈로그
|
||||
|
||||
| 도구 분류 | 수 | 용도 |
|
||||
| ----------------- | --: | -------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 39 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
|
||||
| 결합 분석 | 14 | 개요, 함수 분석, 일괄 디컴파일, 호출 그래프, Swift와 ObjC 검사 |
|
||||
| macOS 네이티브 | 7 | Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
|
||||
| 파일과 패키지 | 5 | 디렉터리와 패키지 검사, Interface Builder, Apple 리소스, 추출 |
|
||||
| .NET PE/CLI | 7 | 어셈블리 식별, 메타데이터, CIL, 네이티브 호출, 비교, 재구성 가져오기 |
|
||||
| 브라우저 관찰 | 9 | 페이지, 스크립트, 소스 맵, WebMCP, 스크린샷, 캡처 비교 |
|
||||
| Electron 분석 | 5 | 페이지 관찰, 앱 구조, 정적·런타임 결과 연결 |
|
||||
| JavaScript 런타임 | 2 | 기존 Node/Electron Inspector에 연결해 스크립트와 실행 컨텍스트 관찰 |
|
||||
| 앱 워크플로 | 7 | 기능 추적, 버전 비교, 반환 구조 비교, 재구현 검증 |
|
||||
| 바이너리 세션 | 21 | 대상 전환, 근거 저장, 프로세스·함수 비교, 미해결 항목 기록 |
|
||||
| 도구 분류 | 수 | 용도 |
|
||||
| ----------------- | --: | --------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
|
||||
| 결합 분석 | 14 | 개요, 함수 분석, 일괄 디컴파일, 호출 그래프, Swift와 ObjC 검사 |
|
||||
| macOS 네이티브 | 7 | Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
|
||||
| 파일과 패키지 | 5 | 디렉터리와 패키지 검사, Interface Builder, Apple 리소스, 추출 |
|
||||
| .NET PE/CLI | 7 | 어셈블리 식별, 메타데이터, CIL, 네이티브 호출, 비교, 재구성 가져오기 |
|
||||
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
|
||||
| Android APK | 5 | 패키지와 manifest, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
|
||||
| 브라우저 관찰 | 9 | 페이지, 스크립트, 소스 맵, WebMCP, 스크린샷, 캡처 비교 |
|
||||
| Electron 분석 | 5 | 페이지 관찰, 앱 구조, 정적·런타임 결과 연결 |
|
||||
| JavaScript 런타임 | 2 | 기존 Node/Electron Inspector에 연결해 스크립트와 실행 컨텍스트 관찰 |
|
||||
| 앱 워크플로 | 7 | 기능 추적, 버전 비교, 반환 구조 비교, 재구현 검증 |
|
||||
| 바이너리 세션 | 21 | 대상 전환, 근거 저장, 프로세스·함수 비교, 미해결 항목 기록 |
|
||||
|
||||
## 로드맵
|
||||
|
||||
@@ -209,7 +236,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
|
||||
## 다른 코딩 에이전트에서 사용하기
|
||||
|
||||
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
|
||||
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 -->
|
||||
|
||||
@@ -218,7 +245,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@4.0.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-27
@@ -15,6 +15,8 @@
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -92,19 +94,33 @@ Setup 会先展示变更并备份已有配置。要求与更多选项见[安装
|
||||
|
||||
### 使用智能体
|
||||
|
||||
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
|
||||
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
|
||||
|
||||
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
|
||||
|
||||
### 从终端运行
|
||||
|
||||
设置完成后:
|
||||
### Skill instructions and a first CLI result
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
This installs instructions, not MCP registration or analysis engines. Follow the
|
||||
[conditional connection guide](skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
`npx rea-agents setup` installs a matching skill and registers selected clients
|
||||
only after showing the plan and receiving approval. Restart/reconnect after
|
||||
registration, then verify REA tools in the active session. Repository skills can
|
||||
be ahead of the [released package](docs/installation.md#released-package-and-main).
|
||||
|
||||
For your extracted JavaScript/Electron tree or ASAR, no MCP setup or native
|
||||
engine is required:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Replace the path with your target; Windows paths such as `"D:/apps/example"`
|
||||
are supported by this static workflow. Native analysis requires its own engine.
|
||||
|
||||
### 安装 rea 命令
|
||||
|
||||
安装命令行工具:
|
||||
@@ -126,7 +142,7 @@ rea setup
|
||||
|
||||
- macOS 12 或更高版本
|
||||
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
|
||||
- Node.js 22.19+ 或 24.11+,以及 npm
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
|
||||
|
||||
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
|
||||
|
||||
@@ -134,7 +150,7 @@ Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.4 和完
|
||||
|
||||
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
|
||||
|
||||
Windows Ghidra 分析目前不可用。进程归属、私有目录权限和安全路径检查尚未实现,正确安装 Ghidra 和 Java 也不能启用分析。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md)。
|
||||
Repository main includes experimental Windows x64 Ghidra P0 for native, non-managed, non-DLL x86-64 PE applications on fixed local NTFS. The native controls are implemented; npm 4.0.1 does not contain them. See the [release boundary](docs/installation.md#released-package-and-main) and [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
|
||||
### 故障排查
|
||||
|
||||
@@ -150,12 +166,21 @@ Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径
|
||||
|
||||
## 当前状态
|
||||
|
||||
REA 可以通过 CLI 和 MCP 分析原生二进制、JavaScript/Electron 应用、.NET 程序集和网站。具体操作取决于主机、目标和所选分析工具。
|
||||
The current repository capabilities and platform requirements are described in
|
||||
[the English support guide](README.md#current-status). Main can be ahead of the
|
||||
[npm release](docs/installation.md#released-package-and-main).
|
||||
|
||||
- Hopper 支持原生分析和注释操作;GUI 行为取决于平台。
|
||||
- Ghidra 在 Linux x64 和 macOS x64/arm64 上提供 22 项只读操作,包括清单、搜索、反编译、汇编、调用关系、引用、指令与类型检查。它不提供 GUI 或修改操作。
|
||||
- 浏览器、Electron 和进程运行时工作流有独立的配置、批准和生命周期要求。详见 [English README](README.md#current-status) 中的完整状态说明。
|
||||
- Windows Ghidra 分析尚未启用,跟踪进展见 [#527](https://github.com/morluto/rea/issues/527)。
|
||||
- Ghidra provides 25 read-only operations on Linux x64, macOS x64/arm64, and the
|
||||
experimental Windows x64 P0 boundary. Linux/macOS additionally supports atomic
|
||||
session function annotations. Windows P0 is read-only; Ghidra has no GUI controls.
|
||||
- Static Android inspection has separate bring-your-own JADX/Java prerequisites;
|
||||
real-provider verification covers Linux. See [Android analysis](docs/android-analysis.md).
|
||||
- Browser, Electron, and process requests specify their target, actions, and
|
||||
lifecycle directly. They require the host's actual access, without separate
|
||||
REA permission grants. Setup configuration writes and Hopper installation
|
||||
still require approval of their exact plan.
|
||||
- `rea capabilities` describes binary-session operations rather than every
|
||||
application/runtime tool. Consult the full MCP tool list and relevant guide.
|
||||
|
||||
## 一个提示词,完成一次完整调查
|
||||
|
||||
@@ -189,18 +214,20 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
|
||||
## 调查工具目录
|
||||
|
||||
| 工具类别 | 数量 | 用途 |
|
||||
| ----------------- | ---: | --------------------------------------------------------- |
|
||||
| 二进制检查 | 39 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
|
||||
| 组合分析 | 14 | 概览、函数分析、批量反编译、调用图、Swift 与 ObjC 检查 |
|
||||
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原 |
|
||||
| 制品检查 | 5 | 目录与软件包检查、Interface Builder、Apple 资源目录与提取 |
|
||||
| .NET PE/CLI | 7 | 程序集身份、元数据、CIL、原生调用声明、成员比较与重建导入 |
|
||||
| 浏览器观察 | 9 | 页面、脚本、来源映射、WebMCP 工具、截图与捕获比较 |
|
||||
| Electron 分析 | 5 | 页面观察、应用结构与静态/运行时结果关联 |
|
||||
| JavaScript 运行时 | 2 | 连接现有 Node/Electron Inspector,观察脚本与执行上下文 |
|
||||
| 应用工作流 | 7 | 功能追踪、版本比较、返回结构比较与重建验证 |
|
||||
| 二进制会话 | 21 | 目标切换、证据包、进程与函数比较、待解决问题记录 |
|
||||
| 工具类别 | 数量 | 用途 |
|
||||
| ----------------- | ---: | -------------------------------------------------------------- |
|
||||
| 二进制检查 | 41 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
|
||||
| 组合分析 | 14 | 概览、函数分析、批量反编译、调用图、Swift 与 ObjC 检查 |
|
||||
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原 |
|
||||
| 制品检查 | 5 | 目录与软件包检查、Interface Builder、Apple 资源目录与提取 |
|
||||
| .NET PE/CLI | 7 | 程序集身份、元数据、CIL、原生调用声明、成员比较与重建导入 |
|
||||
| 韧体 | 2 | Linux 韧体区域识别与显式解包 |
|
||||
| Android APK | 5 | 包与 manifest 声明、class 搜索、成员清单、方法反编译与静态引用 |
|
||||
| 浏览器观察 | 9 | 页面、脚本、来源映射、WebMCP 工具、截图与捕获比较 |
|
||||
| Electron 分析 | 5 | 页面观察、应用结构与静态/运行时结果关联 |
|
||||
| JavaScript 运行时 | 2 | 连接现有 Node/Electron Inspector,观察脚本与执行上下文 |
|
||||
| 应用工作流 | 7 | 功能追踪、版本比较、返回结构比较与重建验证 |
|
||||
| 二进制会话 | 21 | 目标切换、证据包、进程与函数比较、待解决问题记录 |
|
||||
|
||||
## 路线图
|
||||
|
||||
@@ -208,7 +235,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
|
||||
## 与其他编程智能体一起使用
|
||||
|
||||
Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
|
||||
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 -->
|
||||
|
||||
@@ -217,7 +244,7 @@ Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsu
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@4.0.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ import ghidra.program.model.listing.Instruction;
|
||||
import ghidra.program.model.listing.InstructionIterator;
|
||||
import ghidra.program.model.listing.Variable;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.mem.MemoryBlockSourceInfo;
|
||||
import ghidra.program.database.mem.FileBytes;
|
||||
import ghidra.program.model.address.SegmentedAddress;
|
||||
import ghidra.program.model.reloc.Relocation;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import ghidra.program.model.symbol.RefType;
|
||||
import ghidra.program.model.symbol.Reference;
|
||||
import ghidra.program.model.symbol.ReferenceIterator;
|
||||
@@ -112,6 +117,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"params"
|
||||
);
|
||||
private static final String[] CAPABILITIES = {
|
||||
"annotate_native_function",
|
||||
"inspect_native_load_image",
|
||||
"read_bytes",
|
||||
"address_to_file_offset",
|
||||
"ping",
|
||||
"shutdown",
|
||||
"address_name",
|
||||
@@ -142,6 +151,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private List<FunctionEntry> procedureInventory;
|
||||
private List<InventoryItem> stringInventory;
|
||||
private DecompInterface decompiler;
|
||||
private final ReaGhidraExtensions analysisExtensions = new ReaGhidraExtensions();
|
||||
private static AddressSpace sessionDefaultAddressSpace;
|
||||
|
||||
@Override
|
||||
@@ -152,12 +162,24 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
Path descriptorPath = Path.of(arguments[0]);
|
||||
SessionDescriptor descriptor = readDescriptor(descriptorPath);
|
||||
Files.deleteIfExists(descriptorPath);
|
||||
// Windows retains a native immutable bearer descriptor lease until
|
||||
// runtime_close; POSIX removes it after this read. Normal cleanup removes
|
||||
// the Windows file, while abrupt owner death can leave private residue.
|
||||
// Its protected runtime DACL also protects the bearer token while the
|
||||
// bridge is running. POSIX keeps its existing consume-and-delete flow.
|
||||
if (!isWindowsHost()) {
|
||||
Files.deleteIfExists(descriptorPath);
|
||||
}
|
||||
if (currentProgram == null) {
|
||||
throw new IllegalStateException("REA bridge requires an imported program");
|
||||
}
|
||||
sessionDefaultAddressSpace =
|
||||
currentProgram.getAddressFactory().getDefaultAddressSpace();
|
||||
if (currentProgram.getExecutableFormat().equals("Raw Binary") &&
|
||||
!currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getBoolean("REA DOS COM prepared", false)) {
|
||||
throw new IllegalStateException("REA DOS COM entry/context preparation failed");
|
||||
}
|
||||
if (!Application.getApplicationVersion().equals(descriptor.providerVersion)) {
|
||||
throw new IllegalStateException("Ghidra provider version does not match the session");
|
||||
}
|
||||
@@ -171,7 +193,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"Ghidra imported-byte digest does not match the admitted target"
|
||||
);
|
||||
}
|
||||
// GhidraScript wraps run() in a transaction. End it before serving so
|
||||
// each edit owns a real outer transaction and can roll back immediately.
|
||||
end(true);
|
||||
try {
|
||||
if (!descriptor.transport.equals("unix-socket") && descriptor.analysisExtensions.size() > 0)
|
||||
throw new IOException("Windows Ghidra P0 does not admit metadata recovery or database mutation.");
|
||||
analysisExtensions.analyze(descriptor.analysisExtensions, currentProgram, monitor);
|
||||
initializeDecompiler();
|
||||
serve(descriptor);
|
||||
}
|
||||
@@ -180,6 +208,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
decompiler.dispose();
|
||||
decompiler = null;
|
||||
}
|
||||
analysisExtensions.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,18 +248,23 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject endpoint = new JsonObject();
|
||||
endpoint.addProperty("host", "127.0.0.1");
|
||||
endpoint.addProperty("port", address.getPort());
|
||||
// The Windows native reader refuses a file while a writer still
|
||||
// owns it. Closing CREATE_NEW publishes the complete record without
|
||||
// requesting rename rights on the retained private directory guard.
|
||||
Files.writeString(
|
||||
pendingPath,
|
||||
isWindowsHost() ? endpointPath : pendingPath,
|
||||
GSON.toJson(endpoint) + "\n",
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE_NEW,
|
||||
StandardOpenOption.WRITE
|
||||
);
|
||||
try {
|
||||
Files.move(pendingPath, endpointPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(pendingPath, endpointPath);
|
||||
if (!isWindowsHost()) {
|
||||
try {
|
||||
Files.move(pendingPath, endpointPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(pendingPath, endpointPath);
|
||||
}
|
||||
}
|
||||
acceptClient(server, descriptor);
|
||||
}
|
||||
@@ -240,6 +274,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isWindowsHost() {
|
||||
return System.getProperty("os.name", "").startsWith("Windows");
|
||||
}
|
||||
|
||||
private void acceptClient(
|
||||
ServerSocketChannel server,
|
||||
SessionDescriptor descriptor) throws Exception {
|
||||
@@ -309,6 +347,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
);
|
||||
}
|
||||
return switch (request.method) {
|
||||
case "inspect_native_load_image" -> inspectNativeLoadImage(request.params);
|
||||
case "read_bytes" -> readMemoryBytes(request.params);
|
||||
case "address_to_file_offset" -> addressToFileOffset(request.params);
|
||||
case "address_name" -> addressName(request.params);
|
||||
case "list_documents" -> listDocuments(request.params);
|
||||
case "list_names" -> listNames(request.params);
|
||||
@@ -331,6 +372,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
case "resolve_native_call_targets" -> resolveNativeCallTargets(request.params);
|
||||
case "xrefs" -> xrefs(request.params);
|
||||
case "analyze_function" -> analyzeFunction(request.params);
|
||||
case "annotate_native_function" -> {
|
||||
if (!descriptor.transport.equals("unix-socket"))
|
||||
throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation");
|
||||
yield annotateNativeFunction(request.params);
|
||||
}
|
||||
default -> throw new RequestFailure(
|
||||
"method_unavailable",
|
||||
"Bridge method is unavailable"
|
||||
@@ -363,11 +409,217 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("run_id", descriptor.runId);
|
||||
result.addProperty("profile_digest", descriptor.profileDigest);
|
||||
result.add("provider", provider);
|
||||
result.addProperty("read_only", true);
|
||||
boolean readOnly = !descriptor.transport.equals("unix-socket");
|
||||
result.addProperty("read_only", readOnly);
|
||||
result.addProperty("analysis_complete", !timedOut);
|
||||
result.addProperty("analysis_timed_out", timedOut);
|
||||
result.add("capabilities", GSON.toJsonTree(CAPABILITIES));
|
||||
result.add("capabilities", GSON.toJsonTree(
|
||||
java.util.Arrays.stream(CAPABILITIES)
|
||||
.filter(value -> !readOnly || !value.equals("annotate_native_function"))
|
||||
.toArray(String[]::new)
|
||||
));
|
||||
result.add("target", target);
|
||||
JsonArray extensions = analysisExtensions.reports();
|
||||
if (extensions.size() > 0) result.add("analysis_extensions", extensions);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String bytesHex(byte[] bytes) {
|
||||
return java.util.HexFormat.of().formatHex(bytes);
|
||||
}
|
||||
|
||||
private String hashFileBytes(FileBytes source, boolean modified) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] chunk = new byte[65536];
|
||||
for (long offset = 0; offset < source.getSize();) {
|
||||
monitor.checkCancelled();
|
||||
int length = (int) Math.min(chunk.length, source.getSize() - offset);
|
||||
int read = modified
|
||||
? source.getModifiedBytes(offset, chunk, 0, length)
|
||||
: source.getOriginalBytes(offset, chunk, 0, length);
|
||||
if (read != length) throw new IOException("Incomplete FileBytes read at " + offset);
|
||||
digest.update(chunk, 0, read);
|
||||
offset += read;
|
||||
}
|
||||
return bytesHex(digest.digest());
|
||||
}
|
||||
|
||||
private String hashMemory(Address start, long length) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] chunk = new byte[65536];
|
||||
for (long offset = 0; offset < length;) {
|
||||
monitor.checkCancelled();
|
||||
int count = (int) Math.min(chunk.length, length - offset);
|
||||
int read = currentProgram.getMemory().getBytes(start.add(offset), chunk, 0, count);
|
||||
if (read != count) throw new IOException("Incomplete memory read at " + start.add(offset));
|
||||
digest.update(chunk, 0, read);
|
||||
offset += read;
|
||||
}
|
||||
return bytesHex(digest.digest());
|
||||
}
|
||||
|
||||
private JsonObject inspectNativeLoadImage(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of());
|
||||
JsonObject result = new JsonObject();
|
||||
JsonArray recovery = analysisExtensions.inspectImage();
|
||||
if (recovery.size() > 0) result.add("metadata_recovery", recovery);
|
||||
result.addProperty("executable_format", currentProgram.getExecutableFormat());
|
||||
result.addProperty("language_id", currentProgram.getLanguageID().getIdAsString());
|
||||
result.addProperty("compiler_spec_id", currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString());
|
||||
result.addProperty("image_base", canonicalAddress(currentProgram.getImageBase()));
|
||||
result.addProperty("default_address_space", sessionDefaultAddressSpace.getName());
|
||||
List<FileBytes> files = currentProgram.getMemory().getAllFileBytes();
|
||||
JsonArray sources = new JsonArray();
|
||||
for (FileBytes file : files) {
|
||||
JsonObject source = new JsonObject();
|
||||
source.addProperty("name", file.getFilename());
|
||||
source.addProperty("size", file.getSize());
|
||||
source.addProperty("original_sha256", hashFileBytes(file, false));
|
||||
source.addProperty("modified_sha256", hashFileBytes(file, true));
|
||||
sources.add(source);
|
||||
}
|
||||
result.add("source_files", sources);
|
||||
JsonArray mappings = new JsonArray();
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
|
||||
monitor.checkCancelled();
|
||||
JsonObject mapping = new JsonObject();
|
||||
mapping.addProperty("block", block.getName());
|
||||
mapping.addProperty("start", canonicalAddress(source.getMinAddress()));
|
||||
mapping.addProperty("end", canonicalAddress(source.getMaxAddress()));
|
||||
mapping.addProperty("address_space", source.getMinAddress().getAddressSpace().getName());
|
||||
mapping.addProperty("initialized", block.isInitialized());
|
||||
mapping.addProperty("loaded", block.isLoaded());
|
||||
mapping.addProperty("overlay", block.isOverlay());
|
||||
mapping.addProperty("length", source.getLength());
|
||||
FileBytes backing = source.getFileBytes().orElse(null);
|
||||
int sourceIndex = backing == null ? -1 : files.indexOf(backing);
|
||||
if (sourceIndex < 0) mapping.add("source_file_index", JsonNull.INSTANCE);
|
||||
else mapping.addProperty("source_file_index", sourceIndex);
|
||||
long offset = source.getFileBytesOffset();
|
||||
if (offset < 0) mapping.add("file_offset", JsonNull.INSTANCE);
|
||||
else mapping.addProperty("file_offset", offset);
|
||||
if (block.isInitialized()) mapping.addProperty("sha256", hashMemory(source.getMinAddress(), source.getLength()));
|
||||
else mapping.add("sha256", JsonNull.INSTANCE);
|
||||
mappings.add(mapping);
|
||||
}
|
||||
}
|
||||
result.add("mappings", mappings);
|
||||
JsonArray relocations = new JsonArray();
|
||||
Iterator<Relocation> iterator = currentProgram.getRelocationTable().getRelocations();
|
||||
while (iterator.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
Relocation relocation = iterator.next();
|
||||
Address location = relocation.getAddress();
|
||||
JsonObject row = new JsonObject();
|
||||
row.addProperty("address", canonicalAddress(location));
|
||||
if (location instanceof SegmentedAddress segmented) {
|
||||
row.addProperty("segment", segmented.getSegment());
|
||||
row.addProperty("offset", segmented.getSegmentOffset());
|
||||
} else {
|
||||
row.add("segment", JsonNull.INSTANCE);
|
||||
row.add("offset", JsonNull.INSTANCE);
|
||||
}
|
||||
row.addProperty("status", relocation.getStatus().toString());
|
||||
row.addProperty("type", relocation.getType());
|
||||
row.add("values", GSON.toJsonTree(relocation.getValues() == null ? new long[0] : relocation.getValues()));
|
||||
byte[] original = relocation.getBytes();
|
||||
row.add("original_bytes_hex", original == null ? JsonNull.INSTANCE : GSON.toJsonTree(bytesHex(original)));
|
||||
if (original != null && original.length > 0) {
|
||||
byte[] memory = new byte[original.length];
|
||||
int read = currentProgram.getMemory().getBytes(location, memory);
|
||||
row.add("memory_bytes_hex", read == memory.length ? GSON.toJsonTree(bytesHex(memory)) : JsonNull.INSTANCE);
|
||||
} else row.add("memory_bytes_hex", JsonNull.INSTANCE);
|
||||
relocations.add(row);
|
||||
}
|
||||
result.add("relocations", relocations);
|
||||
List<Address> entries = new ArrayList<>();
|
||||
var entryIterator = currentProgram.getSymbolTable().getExternalEntryPointIterator();
|
||||
while (entryIterator.hasNext()) entries.add(entryIterator.next());
|
||||
entries.sort(Address::compareTo);
|
||||
JsonArray entryPoints = new JsonArray();
|
||||
for (Address entry : entries) entryPoints.add(canonicalAddress(entry));
|
||||
result.add("entry_points", entryPoints);
|
||||
JsonArray contexts = new JsonArray();
|
||||
for (Address entry : entries) {
|
||||
JsonObject context = new JsonObject();
|
||||
context.addProperty("address", canonicalAddress(entry));
|
||||
JsonArray registers = new JsonArray();
|
||||
for (String name : new String[] { "CS", "DS", "ES", "SS" }) {
|
||||
Register register = currentProgram.getRegister(name);
|
||||
if (register == null) continue;
|
||||
JsonObject row = new JsonObject();
|
||||
row.addProperty("name", name);
|
||||
BigInteger value = currentProgram.getProgramContext().getValue(register, entry, false);
|
||||
row.add("value_hex", value == null ? JsonNull.INSTANCE : GSON.toJsonTree("0x" + value.toString(16)));
|
||||
registers.add(row);
|
||||
}
|
||||
context.add("registers", registers);
|
||||
contexts.add(context);
|
||||
}
|
||||
result.add("entry_context", contexts);
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonObject readMemoryBytes(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of("document", "address", "length"));
|
||||
requireDocument(params);
|
||||
Address start = requireAddress(params, "address");
|
||||
JsonElement requested = params.get("length");
|
||||
long length;
|
||||
try {
|
||||
if (requested == null || !requested.isJsonPrimitive() || !requested.getAsJsonPrimitive().isNumber())
|
||||
throw new IllegalArgumentException();
|
||||
length = requested.getAsBigDecimal().longValueExact();
|
||||
if (length < 1 || length > 9007199254740991L) throw new IllegalArgumentException();
|
||||
} catch (RuntimeException failure) {
|
||||
throw new RequestFailure("invalid_request", "Byte length must be a positive safe integer");
|
||||
}
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
byte[] chunk = new byte[65536];
|
||||
for (long offset = 0; offset < length;) {
|
||||
monitor.checkCancelled();
|
||||
Address at;
|
||||
try { at = start.addNoWrap(offset); }
|
||||
catch (Exception endOfSpace) { break; }
|
||||
MemoryBlock block = currentProgram.getMemory().getBlock(at);
|
||||
if (block == null || !block.isInitialized()) break;
|
||||
int count = (int) Math.min(chunk.length, Math.min(length - offset, block.getEnd().subtract(at) + 1));
|
||||
int read = currentProgram.getMemory().getBytes(at, chunk, 0, count);
|
||||
bytes.write(chunk, 0, read);
|
||||
if (read != count) break;
|
||||
offset += read;
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", canonicalAddress(start));
|
||||
result.addProperty("requested_bytes", length);
|
||||
result.addProperty("returned_bytes", bytes.size());
|
||||
result.addProperty("bytes_hex", bytesHex(bytes.toByteArray()));
|
||||
result.addProperty("complete", bytes.size() == length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonObject addressToFileOffset(JsonObject params) {
|
||||
requireKeys(params, Set.of("document", "address"));
|
||||
requireDocument(params);
|
||||
Address address = requireAddress(params, "address");
|
||||
MemoryBlock block = currentProgram.getMemory().getBlock(address);
|
||||
if (block == null || !block.isInitialized())
|
||||
throw new RequestFailure("invalid_request", "Address has no initialized file-backed memory: " + canonicalAddress(address));
|
||||
Long fileOffset = null;
|
||||
int mappings = 0;
|
||||
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
|
||||
if (!source.contains(address) || source.getFileBytes().isEmpty()) continue;
|
||||
long offset = source.getFileBytesOffset(address);
|
||||
if (offset < 0) continue;
|
||||
fileOffset = offset;
|
||||
mappings++;
|
||||
}
|
||||
if (mappings != 1)
|
||||
throw new RequestFailure("invalid_request", "Address has " + mappings + " authoritative source mappings: " + canonicalAddress(address));
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", canonicalAddress(address));
|
||||
result.addProperty("file_offset", fileOffset);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -579,6 +831,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("total_fields", totalFields);
|
||||
result.addProperty("truncated", totalFields > 20_000);
|
||||
result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Nested and recursive types retain category-path identities for separate inspection.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation.")));
|
||||
if (type != null) {
|
||||
JsonObject metadata = analysisExtensions.inspectDataType(currentProgram, type, address, monitor);
|
||||
if (metadata != null) result.add("metadata_recovery", metadata);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -725,6 +981,90 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void invalidateAnalysisCaches() {
|
||||
nameInventory = null;
|
||||
procedureInventory = null;
|
||||
stringInventory = null;
|
||||
if (decompiler != null) decompiler.flushCache();
|
||||
}
|
||||
|
||||
private JsonObject annotationReadback(Function function) {
|
||||
Address entry = function.getEntryPoint();
|
||||
JsonObject value = new JsonObject();
|
||||
value.addProperty("address", canonicalAddress(entry));
|
||||
value.addProperty("name", procedureName(function));
|
||||
value.addProperty("comment", currentProgram.getListing().getComment(CommentType.PRE, entry));
|
||||
value.addProperty("inline_comment", currentProgram.getListing().getComment(CommentType.EOL, entry));
|
||||
return value;
|
||||
}
|
||||
|
||||
private JsonObject annotateNativeFunction(JsonObject params) throws Exception {
|
||||
if (!Set.of("procedure", "name", "comment", "inline_comment").containsAll(params.keySet()))
|
||||
throw new RequestFailure("invalid_request", "Unknown function annotation field");
|
||||
if (!params.has("name") && !params.has("comment") && !params.has("inline_comment"))
|
||||
throw new RequestFailure("invalid_request", "Supply at least one annotation change");
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
Address entry = function.getEntryPoint();
|
||||
if (function.isExternal() || !currentProgram.getMemory().contains(entry))
|
||||
throw new RequestFailure("invalid_request", "Annotations require a local function entry: " + canonicalAddress(entry));
|
||||
int transaction = currentProgram.startTransaction("REA function annotations");
|
||||
boolean commit = false;
|
||||
try {
|
||||
// Validate edits through Ghidra's own setters, including name rules.
|
||||
// A later invalid name must roll back earlier comment writes.
|
||||
for (String field : List.of("comment", "inline_comment")) {
|
||||
if (params.has(field)) {
|
||||
String text = requireText(params, field);
|
||||
CommentType type = field.equals("comment") ? CommentType.PRE : CommentType.EOL;
|
||||
currentProgram.getListing().setComment(entry, type, text.isEmpty() ? null : text);
|
||||
}
|
||||
}
|
||||
if (params.has("name"))
|
||||
function.setName(requireString(params, "name"), SourceType.USER_DEFINED);
|
||||
monitor.checkCancelled();
|
||||
invalidateAnalysisCaches();
|
||||
JsonObject readback = annotationReadback(function);
|
||||
for (String field : List.of("name", "comment", "inline_comment")) {
|
||||
if (!params.has(field)) continue;
|
||||
String requested = requireText(params, field);
|
||||
JsonElement measured = readback.get(field);
|
||||
String expected = !field.equals("name") && requested.isEmpty() ? null : requested;
|
||||
String observed = field.equals("name") ? function.getName() :
|
||||
(measured.isJsonNull() ? null : measured.getAsString());
|
||||
if (!java.util.Objects.equals(expected, observed))
|
||||
throw new RequestFailure("annotation_readback_mismatch", "Annotation readback differs for " + field + " at " + canonicalAddress(entry));
|
||||
}
|
||||
JsonObject query = new JsonObject();
|
||||
query.addProperty("procedure", canonicalAddress(entry));
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("annotations", readback);
|
||||
result.add("dossier", analyzeFunction(query));
|
||||
JsonObject effects = new JsonObject();
|
||||
effects.addProperty("scope", "session-analysis-database");
|
||||
effects.addProperty("source_bytes_modified", false);
|
||||
effects.addProperty("persists_after_close", false);
|
||||
result.add("effects", effects);
|
||||
monitor.checkCancelled();
|
||||
commit = true;
|
||||
return result;
|
||||
}
|
||||
catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) {
|
||||
throw new RequestFailure("invalid_function_name", "Invalid function name at " + canonicalAddress(entry) + ": " + safeMessage(exception));
|
||||
}
|
||||
finally {
|
||||
currentProgram.endTransaction(transaction, commit);
|
||||
// Rollback also invalidates inventory and decompiler views.
|
||||
invalidateAnalysisCaches();
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireText(JsonObject params, String field) {
|
||||
JsonElement value = params.get(field);
|
||||
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
|
||||
throw new RequestFailure("invalid_request", "Annotation " + field + " must be text");
|
||||
return value.getAsString();
|
||||
}
|
||||
|
||||
private JsonObject analyzeFunction(JsonObject params) throws Exception {
|
||||
requireKeys(params, Set.of("procedure"));
|
||||
Function function = resolveProcedure(requireString(params, "procedure"));
|
||||
@@ -2724,10 +3064,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IllegalArgumentException("REA session descriptor is invalid");
|
||||
}
|
||||
JsonObject object = requireObject(
|
||||
JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8)),
|
||||
DESCRIPTOR_KEYS
|
||||
);
|
||||
JsonElement parsed = JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8));
|
||||
Set<String> keys = new HashSet<>(DESCRIPTOR_KEYS);
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("analysis_extensions"))
|
||||
keys.add("analysis_extensions");
|
||||
JsonObject object = requireObject(parsed, keys);
|
||||
return new SessionDescriptor(
|
||||
requireString(object, "transport"),
|
||||
requireString(object, "endpoint_path"),
|
||||
@@ -2735,7 +3076,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
requireString(object, "run_id"),
|
||||
requireSha256(object, "target_sha256"),
|
||||
requireString(object, "provider_version"),
|
||||
requireString(object, "profile_digest")
|
||||
requireString(object, "profile_digest"),
|
||||
object.has("analysis_extensions") ? object.getAsJsonArray("analysis_extensions") : new JsonArray()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2919,7 +3261,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
String runId,
|
||||
String targetSha256,
|
||||
String providerVersion,
|
||||
String profileDigest
|
||||
String profileDigest,
|
||||
JsonArray analysisExtensions
|
||||
) {}
|
||||
private record Request(int id, String method, JsonObject params) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Session-owned extension loading; analyzer algorithms stay behind each adapter ABI. */
|
||||
final class ReaGhidraExtensions implements AutoCloseable {
|
||||
private record Loaded(URLClassLoader loader, Object instance, String sha256) {}
|
||||
private final List<Loaded> loaded = new ArrayList<>();
|
||||
private final JsonArray reports = new JsonArray();
|
||||
|
||||
void analyze(JsonArray descriptors, Program program, TaskMonitor monitor) throws Exception {
|
||||
for (var item : descriptors) {
|
||||
monitor.checkCancelled();
|
||||
JsonObject descriptor = item.getAsJsonObject();
|
||||
String id = descriptor.get("id").getAsString();
|
||||
String entry = descriptor.get("entry_class").getAsString();
|
||||
String digest = descriptor.get("sha256").getAsString();
|
||||
JsonObject report = new JsonObject();
|
||||
report.addProperty("id", id);
|
||||
report.addProperty("sha256", digest);
|
||||
URLClassLoader loader = null;
|
||||
try {
|
||||
if (!id.equals("nativeaot") || !entry.equals("rea.extensions.nativeaot.NativeAotExtension") ||
|
||||
descriptor.get("integration_api").getAsInt() != 1)
|
||||
throw new IllegalArgumentException("Unregistered Ghidra extension ABI: " + id);
|
||||
Path path = Path.of(descriptor.get("path").getAsString());
|
||||
if (!path.isAbsolute() || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > 8 * 1024 * 1024)
|
||||
throw new IllegalArgumentException("Invalid private extension artifact: " + path);
|
||||
String measured = java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)));
|
||||
if (!measured.equals(digest)) throw new IllegalArgumentException("Private extension digest mismatch: " + id);
|
||||
loader = new URLClassLoader(new java.net.URL[] { path.toUri().toURL() }, getClass().getClassLoader());
|
||||
Class<?> type = Class.forName(entry, true, loader);
|
||||
Object instance = type.getConstructor().newInstance();
|
||||
Object response = type.getMethod("analyze", Program.class, TaskMonitor.class).invoke(instance, program, monitor);
|
||||
if (!(response instanceof JsonObject result)) throw new IllegalArgumentException("Invalid Ghidra extension result: " + id);
|
||||
report.addProperty("status", result.get("status").getAsString());
|
||||
report.add("reason", result.get("reason").deepCopy());
|
||||
report.add("result", result);
|
||||
loaded.add(new Loaded(loader, instance, digest));
|
||||
loader = null;
|
||||
} catch (InvocationTargetException ex) {
|
||||
if (ex.getCause() instanceof CancelledException cancelled) throw cancelled;
|
||||
failure(report, ex.getCause());
|
||||
} catch (Exception | LinkageError ex) {
|
||||
failure(report, ex);
|
||||
} finally {
|
||||
if (loader != null) loader.close();
|
||||
}
|
||||
reports.add(report);
|
||||
}
|
||||
}
|
||||
|
||||
JsonArray reports() { return reports.deepCopy(); }
|
||||
|
||||
JsonArray inspectImage() throws Exception {
|
||||
JsonArray summaries = new JsonArray();
|
||||
for (Loaded extension : loaded) {
|
||||
Object response = extension.instance.getClass().getMethod("inspectImage").invoke(extension.instance);
|
||||
if (!(response instanceof JsonObject summary)) throw new IllegalArgumentException("Invalid extension image summary");
|
||||
summary.addProperty("analysis_artifact_sha256", extension.sha256);
|
||||
summaries.add(summary);
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
JsonObject inspectDataType(Program program, DataType type, Address address, TaskMonitor monitor) throws Exception {
|
||||
for (Loaded extension : loaded) {
|
||||
try {
|
||||
Object response = extension.instance.getClass().getMethod("inspectDataType", Program.class, DataType.class, Address.class, TaskMonitor.class)
|
||||
.invoke(extension.instance, program, type, address, monitor);
|
||||
if (response instanceof JsonObject result) return result;
|
||||
if (response != null) throw new IllegalArgumentException("Invalid extension type metadata result");
|
||||
} catch (InvocationTargetException ex) {
|
||||
if (ex.getCause() instanceof CancelledException cancelled) throw cancelled;
|
||||
throw new IllegalStateException("Ghidra extension type inspection failed: " + ex.getCause(), ex.getCause());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void failure(JsonObject report, Throwable ex) {
|
||||
String reason = "Extension loading/analysis failed: " + ex;
|
||||
if (ex.getCause() != null) reason += "; caused by " + ex.getCause();
|
||||
report.addProperty("status", "failed");
|
||||
report.addProperty("reason", reason);
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("loader_failure", reason);
|
||||
report.add("result", result);
|
||||
}
|
||||
|
||||
@Override public void close() throws Exception {
|
||||
Exception failure = null;
|
||||
for (Loaded extension : loaded) {
|
||||
try { extension.loader.close(); }
|
||||
catch (Exception ex) { if (failure == null) failure = ex; else failure.addSuppressed(ex); }
|
||||
}
|
||||
loaded.clear();
|
||||
if (failure != null) throw failure;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/** Seed the declared DOS COM analysis context before default auto-analysis. */
|
||||
public final class ReaGhidraPrepareCom extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
if (!currentProgram.getLanguageID().getIdAsString().equals("x86:LE:16:Real Mode") ||
|
||||
!currentProgram.getExecutableFormat().equals("Raw Binary")) {
|
||||
throw new IllegalArgumentException("DOS COM preparation requires raw x86 real-mode import");
|
||||
}
|
||||
Address entry = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress("1000:0100");
|
||||
Address start = currentProgram.getMinAddress();
|
||||
Address end = currentProgram.getMaxAddress();
|
||||
if (!entry.equals(start) || end.subtract(start) + 1 > 0xff00) {
|
||||
throw new IllegalArgumentException("DOS COM import must map 1..65280 bytes at 1000:0100");
|
||||
}
|
||||
for (String name : new String[] { "CS", "DS", "ES", "SS" }) {
|
||||
monitor.checkCancelled();
|
||||
currentProgram.getProgramContext().setValue(
|
||||
currentProgram.getRegister(name), start, end, BigInteger.valueOf(0x1000)
|
||||
);
|
||||
}
|
||||
addEntryPoint(entry);
|
||||
if (!disassemble(entry) || createFunction(entry, "entry") == null) {
|
||||
throw new IllegalStateException("DOS COM entry cannot be decoded as a function");
|
||||
}
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA DOS COM prepared", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package rea.extensions.nativeaot;
|
||||
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import java.util.LinkedHashSet;
|
||||
import nativeaot.rtr.ReadyToRunDirectory;
|
||||
import nativeaot.rtr.ReadyToRunSection;
|
||||
import nativeaot.rtr.SignatureReadyToRunLocator;
|
||||
import nativeaot.rtr.SymbolReadyToRunLocator;
|
||||
|
||||
/** Validate the producer's NativeAOT row layout before database mutation. */
|
||||
final class NativeAotDirectory {
|
||||
record Located(Address address, ReadyToRunDirectory directory, String discovery) {}
|
||||
|
||||
static Located locate(Program program, TaskMonitor monitor, MessageLog log) throws Exception {
|
||||
Address[] symbols = SymbolReadyToRunLocator.instance.locateModules(program, monitor, log);
|
||||
String discovery = symbols.length == 0 ? "signature-heuristic" : "symbol";
|
||||
Address[] found = symbols.length == 0
|
||||
? SignatureReadyToRunLocator.instance.locateModules(program, monitor, log) : symbols;
|
||||
var candidates = new LinkedHashSet<Address>();
|
||||
for (Address address : found) if (address.getOffset() != 0) candidates.add(address);
|
||||
if (candidates.isEmpty()) return null;
|
||||
if (candidates.size() != 1)
|
||||
throw new UnsupportedOperationException("Ambiguous NativeAOT directory candidates: " + candidates);
|
||||
Address address = candidates.iterator().next();
|
||||
var memory = program.getMemory();
|
||||
byte[] header = new byte[16];
|
||||
if (memory.getBytes(address, header) != header.length)
|
||||
throw new IllegalArgumentException("Incomplete NativeAOT header at " + address);
|
||||
int count = Short.toUnsignedInt(memory.getShort(address.add(12)));
|
||||
if (count < 1 || count > 80 || Byte.toUnsignedInt(header[14]) != 24 || header[15] != 1)
|
||||
throw new UnsupportedOperationException("Unsupported NativeAOT row layout at " + address +
|
||||
": sections=" + count + ", entry_size=" + Byte.toUnsignedInt(header[14]) + ", entry_type=" + Byte.toUnsignedInt(header[15]));
|
||||
byte[] rows = new byte[count * 24];
|
||||
if (memory.getBytes(address.add(16), rows) != rows.length)
|
||||
throw new IllegalArgumentException("Incomplete NativeAOT directory rows at " + address);
|
||||
ReadyToRunDirectory directory = ReadyToRunDirectory.readAtAddress(program, address);
|
||||
if (directory.getMajorVersion() != 9 || directory.getMinorVersion() != 1)
|
||||
throw new UnsupportedOperationException("Unsupported NativeAOT RTR format " +
|
||||
directory.getMajorVersion() + "." + directory.getMinorVersion() + " at " + address + "; verified format is 9.1 (.NET 8.0.22 Linux x64 fixture).");
|
||||
var types = new LinkedHashSet<Integer>();
|
||||
for (ReadyToRunSection section : directory.getSections()) {
|
||||
monitor.checkCancelled();
|
||||
if (!types.add(section.getType())) throw new IllegalArgumentException("Duplicate NativeAOT section " + section.getType() + " at " + address);
|
||||
// ModuleInfoRow flags bit 0 declares an end pointer. ThreadStaticIndex
|
||||
// (204) is a single-address entry with flags=0 and an absent end.
|
||||
if (section.getFlags() != 0 && section.getFlags() != 1)
|
||||
throw new UnsupportedOperationException("Unsupported NativeAOT section flags " + section.getFlags() + " for section " + section.getType());
|
||||
boolean hasEnd = section.getFlags() == 1;
|
||||
if (!hasEnd && section.getEnd() != 0)
|
||||
throw new IllegalArgumentException("NativeAOT single-address section " + section.getType() + " reports an unexpected end");
|
||||
if (hasEnd && Long.compareUnsigned(section.getStart(), section.getEnd()) > 0)
|
||||
throw new IllegalArgumentException("Reversed NativeAOT section " + section.getType() + " at " + address);
|
||||
if (section.getStart() != 0 && (!hasEnd || section.getStart() != section.getEnd()) &&
|
||||
(!memory.contains(address.getNewAddress(section.getStart())) ||
|
||||
(hasEnd && !memory.contains(address.getNewAddress(section.getEnd() - 1)))))
|
||||
throw new IllegalArgumentException("Unmapped NativeAOT section " + section.getType() + " at " + address);
|
||||
}
|
||||
if (directory.getSectionByType(ReadyToRunSection.DEHYDRATED_DATA) == null)
|
||||
throw new UnsupportedOperationException("NativeAOT directory at " + address + " has no DEHYDRATED_DATA; non-dehydrated layouts are not verified.");
|
||||
for (int type : new int[] { ReadyToRunSection.DEHYDRATED_DATA, ReadyToRunSection.FROZEN_OBJECT_REGION }) {
|
||||
ReadyToRunSection section = directory.getSectionByType(type);
|
||||
if (section != null && section.getFlags() != 1)
|
||||
throw new IllegalArgumentException("NativeAOT section " + type + " requires an end pointer");
|
||||
}
|
||||
return new Located(address, directory, discovery);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package rea.extensions.nativeaot;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.app.util.importer.MessageLog;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressSet;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import nativeaot.objectmodel.FrozenObjectAnnotator;
|
||||
import nativeaot.objectmodel.MethodTableCrawler;
|
||||
import nativeaot.objectmodel.MethodTableManager;
|
||||
import nativeaot.rehydration.MetadataRehydratorNet80;
|
||||
import nativeaot.rtr.ReadyToRunSection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
/** Optional headless facade over the unchanged upstream analysis primitives. */
|
||||
public final class NativeAotExtension {
|
||||
private MethodTableManager manager;
|
||||
private NativeAotDirectory.Located located;
|
||||
private JsonObject report;
|
||||
private final List<String> diagnostics = new ArrayList<>();
|
||||
private JsonObject derivedRange;
|
||||
|
||||
public JsonObject analyze(Program program, TaskMonitor monitor) throws Exception {
|
||||
report = new JsonObject();
|
||||
report.addProperty("id", "nativeaot");
|
||||
report.addProperty("integration_api", 1);
|
||||
report.addProperty("source_revision", "effeb734fc570c32650f88b159608979dc7b423e");
|
||||
report.addProperty("source_revision_authority", "build-reported-unattested");
|
||||
report.addProperty("status", "not_applicable");
|
||||
report.addProperty("reason", "No NativeAOT directory with the supported row layout was found.");
|
||||
report.addProperty("method_tables", 0);
|
||||
if (!program.getLanguageID().getIdAsString().startsWith("x86:LE:64"))
|
||||
return failure("unsupported", "NativeAOT adapter supports x86-64 only.");
|
||||
MessageLog log = new MessageLog();
|
||||
int transaction = program.startTransaction("REA NativeAOT metadata recovery");
|
||||
boolean commit = false;
|
||||
String stage = "directory-discovery";
|
||||
try {
|
||||
located = NativeAotDirectory.locate(program, monitor, log);
|
||||
if (located == null) return finish(log);
|
||||
report.addProperty("header_address", address(located.address()));
|
||||
report.addProperty("discovery", located.discovery());
|
||||
report.addProperty("format_major", located.directory().getMajorVersion());
|
||||
report.addProperty("format_minor", located.directory().getMinorVersion());
|
||||
stage = "metadata-rehydration";
|
||||
var section = located.directory().getSectionByType(ReadyToRunSection.DEHYDRATED_DATA);
|
||||
Address start = located.address().getNewAddress(section.getStart());
|
||||
Address end = located.address().getNewAddress(section.getEnd());
|
||||
if (start.compareTo(end) >= 0) throw new IllegalArgumentException("Empty dehydrated section at " + start);
|
||||
var scan = new MetadataRehydratorNet80().rehydrate(program, new AddressSet(start, end).getFirstRange(), monitor, log);
|
||||
// Upstream reports an inclusive end at base + written length.
|
||||
long writtenLength = scan.getScanningRange().getLength() - 1;
|
||||
derivedRange = new JsonObject();
|
||||
derivedRange.addProperty("address", address(scan.getScanningRange().getMinAddress()));
|
||||
derivedRange.addProperty("size_bytes", writtenLength);
|
||||
derivedRange.addProperty("sha256", hashMemory(program, scan.getScanningRange().getMinAddress(), writtenLength, monitor));
|
||||
derivedRange.add("file_offset", JsonNull.INSTANCE);
|
||||
stage = "method-table-recovery";
|
||||
manager = MethodTableManager.createForDirectory(program, located.directory());
|
||||
var conventions = NativeAotMethodConventions.capture(program, monitor);
|
||||
new MethodTableCrawler(manager, program, scan).analyze(monitor, log);
|
||||
stage = "calling-convention-preservation";
|
||||
NativeAotMethodConventions.restore(program, manager, conventions, monitor);
|
||||
log.appendMsg("REA", "Preserved pre-recovery calling conventions; new functions use the loaded compiler specification default. Original method signatures remain unknown.");
|
||||
if (manager.getObjectMT() == null || manager.getMethodTableCount() == 0)
|
||||
throw new UnsupportedOperationException("No unambiguous System.Object method table was recovered at header " + located.address());
|
||||
stage = "frozen-object-annotation";
|
||||
if (manager.getStringMT() == null || scan.getPointerLocations().length == 0) {
|
||||
diagnostics.add("Frozen-object annotation unavailable: System.String table or pointer locations were not recovered.");
|
||||
} else {
|
||||
new FrozenObjectAnnotator(program, manager).analyze(located.directory(), scan.getPointerLocations(), monitor, log);
|
||||
}
|
||||
monitor.checkCancelled();
|
||||
report.addProperty("method_tables", manager.getMethodTableCount());
|
||||
JsonObject coverage = auditFrozenObjects(program, scan.getPointerLocations(), monitor);
|
||||
report.add("coverage", coverage);
|
||||
if (coverage.get("frozen_objects_annotated").getAsInt() != coverage.get("frozen_object_candidates").getAsInt())
|
||||
diagnostics.add("Some frozen-object candidates lack committed instance annotations; inspect coverage counts and Ghidra diagnostics.");
|
||||
report.addProperty("status", diagnostics.isEmpty() ? "complete" : "partial");
|
||||
report.add("reason", JsonNull.INSTANCE);
|
||||
report.add("derived_memory", derivedRange.deepCopy());
|
||||
JsonArray tables = new JsonArray();
|
||||
for (var mt : manager.getMethodTables()) {
|
||||
monitor.checkCancelled();
|
||||
JsonObject identity = new JsonObject();
|
||||
identity.addProperty("address", address(mt.getAddress()));
|
||||
identity.addProperty("type", mt.getMTType().getPathName());
|
||||
tables.add(identity);
|
||||
}
|
||||
report.add("types", tables);
|
||||
commit = true;
|
||||
return finish(log);
|
||||
} catch (CancelledException ex) {
|
||||
manager = null;
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
manager = null;
|
||||
diagnostics.add(stage + ": " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
|
||||
report.addProperty("status", ex instanceof UnsupportedOperationException ? "unsupported" : "failed");
|
||||
report.addProperty("reason", stage + ": " + ex.getMessage());
|
||||
return finish(log);
|
||||
} finally {
|
||||
program.endTransaction(transaction, commit);
|
||||
}
|
||||
}
|
||||
|
||||
/** Inline discovery for the existing loaded-image inspection. */
|
||||
public JsonObject inspectImage() {
|
||||
JsonObject summary = new JsonObject();
|
||||
summary.addProperty("format", "dotnet-nativeaot");
|
||||
for (String key : new String[] { "status", "reason", "header_address", "discovery", "format_major", "format_minor", "method_tables", "derived_memory", "coverage", "diagnostics" })
|
||||
summary.add(key, report.has(key) ? report.get(key).deepCopy() : JsonNull.INSTANCE);
|
||||
summary.add("types", report.has("types") ? report.get("types").deepCopy() : new JsonArray());
|
||||
JsonArray limitations = new JsonArray();
|
||||
limitations.add("Recovery covers the reported metadata candidates; it does not reconstruct original C# source, custom field layouts, every runtime object, or execution behavior.");
|
||||
limitations.add("Method prototypes and parameter names remain Ghidra inferences; verify them against instructions and call sites. Pre-recovery conventions are retained rather than universally forcing __thiscall.");
|
||||
limitations.add("NativeAOT type identities and relationships are inferred; generated names are not original names. Derived memory has no original file offsets.");
|
||||
summary.add("limitations", limitations);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/** Read recovered relationships without restoring or mutating database state. */
|
||||
public JsonObject inspectDataType(Program program, DataType type, Address selected, TaskMonitor monitor) throws Exception {
|
||||
if (manager == null) return null;
|
||||
return NativeAotTypeMetadata.describe(program, manager, located, type, report, derivedRange, monitor);
|
||||
}
|
||||
|
||||
private JsonObject auditFrozenObjects(Program program, Address[] pointerLocations, TaskMonitor monitor) throws Exception {
|
||||
JsonObject coverage = new JsonObject();
|
||||
int candidates = 0, annotated = 0;
|
||||
var section = located.directory().getSectionByType(ReadyToRunSection.FROZEN_OBJECT_REGION);
|
||||
if (section != null) for (Address location : section.getPointersInSection(pointerLocations)) {
|
||||
monitor.checkCancelled();
|
||||
var table = manager.getMethodTable(program.getMemory().getLong(location));
|
||||
if (table == null) continue;
|
||||
candidates++;
|
||||
var expected = table.getInstanceType();
|
||||
var actual = program.getListing().getDataAt(location);
|
||||
if (expected != null && actual != null && actual.getDataType().getPathName().equals(expected.getPathName())) annotated++;
|
||||
}
|
||||
coverage.addProperty("frozen_object_candidates", candidates);
|
||||
coverage.addProperty("frozen_objects_annotated", annotated);
|
||||
coverage.addProperty("basis", "rehydrated-pointer-candidates-and-committed-instance-types");
|
||||
return coverage;
|
||||
}
|
||||
|
||||
private JsonObject finish(MessageLog log) {
|
||||
if (!log.toString().isBlank()) diagnostics.add(log.toString());
|
||||
JsonArray messages = new JsonArray();
|
||||
for (String message : diagnostics) messages.add(message);
|
||||
report.add("diagnostics", messages);
|
||||
return report;
|
||||
}
|
||||
|
||||
private JsonObject failure(String status, String reason) {
|
||||
report.addProperty("status", status);
|
||||
report.addProperty("reason", reason);
|
||||
return finish(new MessageLog());
|
||||
}
|
||||
|
||||
static String address(Address value) { return "0x" + Long.toUnsignedString(value.getOffset(), 16); }
|
||||
|
||||
private static String hashMemory(Program program, Address start, long length, TaskMonitor monitor) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] buffer = new byte[65536];
|
||||
for (long offset = 0; offset < length;) {
|
||||
monitor.checkCancelled();
|
||||
int count = (int)Math.min(buffer.length, length - offset);
|
||||
if (program.getMemory().getBytes(start.add(offset), buffer, 0, count) != count)
|
||||
throw new IllegalStateException("Incomplete derived-memory read at " + start.add(offset));
|
||||
digest.update(buffer, 0, count);
|
||||
offset += count;
|
||||
}
|
||||
return java.util.HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package rea.extensions.nativeaot;
|
||||
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import nativeaot.objectmodel.MethodTableManager;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
/** Keep upstream's unconditional __thiscall assignment out of x64 ABI evidence. */
|
||||
final class NativeAotMethodConventions {
|
||||
static Map<Address, String> capture(Program program, TaskMonitor monitor) throws Exception {
|
||||
Map<Address, String> result = new HashMap<>();
|
||||
var functions = program.getFunctionManager().getFunctions(true);
|
||||
while (functions.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
var function = functions.next();
|
||||
result.put(function.getEntryPoint(), function.getCallingConventionName());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void restore(Program program, MethodTableManager manager, Map<Address, String> previous, TaskMonitor monitor) throws Exception {
|
||||
var visited = new HashSet<Address>();
|
||||
String defaultConvention = program.getCompilerSpec().getDefaultCallingConvention().getName();
|
||||
for (var table : manager.getMethodTables()) for (long target : table.getVTable()) {
|
||||
monitor.checkCancelled();
|
||||
Address address = table.getAddress().getNewAddress(target);
|
||||
if (!visited.add(address)) continue;
|
||||
var function = program.getFunctionManager().getFunctionAt(address);
|
||||
if (function == null) continue;
|
||||
String convention = previous.containsKey(address) ? previous.get(address) : defaultConvention;
|
||||
if (!java.util.Objects.equals(function.getCallingConventionName(), convention))
|
||||
function.setCallingConvention(convention);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package rea.extensions.nativeaot;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import nativeaot.objectmodel.MethodTable;
|
||||
import nativeaot.objectmodel.MethodTableManager;
|
||||
|
||||
/** Translate a selected recovered type into provider-neutral metadata evidence. */
|
||||
final class NativeAotTypeMetadata {
|
||||
static JsonObject describe(Program program, MethodTableManager manager, NativeAotDirectory.Located located,
|
||||
DataType type, JsonObject report, JsonObject derivedRange, TaskMonitor monitor) throws Exception {
|
||||
for (MethodTable table : manager.getMethodTables()) {
|
||||
monitor.checkCancelled();
|
||||
boolean sameType = type.getPathName().equals(table.getMTType().getPathName()) ||
|
||||
(table.getInstanceType() != null && type.getPathName().equals(table.getInstanceType().getPathName()));
|
||||
if (!sameType) continue;
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("format", "dotnet-nativeaot");
|
||||
result.addProperty("status", report.get("status").getAsString());
|
||||
result.addProperty("header_address", NativeAotExtension.address(located.address()));
|
||||
result.addProperty("format_major", located.directory().getMajorVersion());
|
||||
result.addProperty("format_minor", located.directory().getMinorVersion());
|
||||
result.addProperty("method_table_address", NativeAotExtension.address(table.getAddress()));
|
||||
result.addProperty("name_origin", table == manager.getObjectMT() || table == manager.getStringMT() ? "inferred" : "generated");
|
||||
result.add("original_name", JsonNull.INSTANCE);
|
||||
result.addProperty("base_size_bytes", table.getBaseSize());
|
||||
result.add("related_type", identity(manager, table.getRelatedTypeAddress()));
|
||||
JsonArray interfaces = new JsonArray();
|
||||
for (long value : table.getInterfaceAddresses()) if (value != 0) interfaces.add(identity(manager, value));
|
||||
result.add("interfaces", interfaces);
|
||||
JsonArray slots = new JsonArray();
|
||||
for (int slot = 0; slot < table.getVTableSlotCount(); slot++) {
|
||||
monitor.checkCancelled();
|
||||
long value = table.getVTableSlot(slot);
|
||||
JsonObject target = new JsonObject();
|
||||
target.addProperty("slot", slot);
|
||||
target.addProperty("slot_address", NativeAotExtension.address(table.getAddress().add(24 + slot * 8L)));
|
||||
target.add("target_address", value == 0 ? JsonNull.INSTANCE : new com.google.gson.JsonPrimitive("0x" + Long.toUnsignedString(value, 16)));
|
||||
var function = value == 0 ? null : program.getFunctionManager().getFunctionAt(table.getAddress().getNewAddress(value));
|
||||
target.add("procedure_name", function == null ? JsonNull.INSTANCE : new com.google.gson.JsonPrimitive(function.getName(true)));
|
||||
target.addProperty("basis", "method-table-pointer");
|
||||
slots.add(target);
|
||||
}
|
||||
result.add("virtual_slots", slots);
|
||||
result.add("derived_memory", derivedRange.deepCopy());
|
||||
result.add("diagnostics", report.get("diagnostics").deepCopy());
|
||||
JsonArray limitations = new JsonArray();
|
||||
limitations.add("Type identities and relationships are recovered by upstream heuristics. Original source names and custom field layouts are not recovered.");
|
||||
limitations.add("Method prototypes are inferred, not original signatures. REA preserves pre-recovery calling conventions and the loaded compiler default for new functions.");
|
||||
limitations.add("Rehydrated metadata bytes are derived analysis-memory content; their original file offsets and runtime behavior remain unknown.");
|
||||
limitations.add("Verified workflow: .NET 8.0.22 NativeAOT RTR 9.1, Linux x64 ELF / Windows x64 PE on Linux; other runtime layouts and hosts are unverified.");
|
||||
result.add("limitations", limitations);
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static com.google.gson.JsonElement identity(MethodTableManager manager, long value) throws Exception {
|
||||
if (value == 0) return JsonNull.INSTANCE;
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("address", "0x" + Long.toUnsignedString(value, 16));
|
||||
MethodTable table = manager.getMethodTable(value);
|
||||
result.add("type", table == null ? JsonNull.INSTANCE : new com.google.gson.JsonPrimitive(table.getMTType().getPathName()));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@
|
||||
- Implementation status: The provider registry, deterministic selection,
|
||||
target binding, analysis-profile commitment, and snapshot/Evidence migration
|
||||
are implemented. Ghidra discovery, target/profile resolution, doctor checks,
|
||||
the private headless-session foundation, ten read-only inventory capabilities,
|
||||
and twelve function-analysis capabilities are implemented with real
|
||||
cross-format conformance.
|
||||
the private headless-session foundation, thirteen read-only inventory/load-image
|
||||
capabilities, and twelve function-analysis capabilities are implemented with
|
||||
real cross-format conformance. Linux/macOS also supports atomic session
|
||||
function annotations; experimental Windows x64 P0 uses bundled native controls
|
||||
and admits only the 25 read-only operations. See the
|
||||
[release boundary](../installation.md#released-package-and-main).
|
||||
|
||||
## Context
|
||||
|
||||
@@ -25,7 +28,7 @@ engine, engine version, or complete analysis configuration. Two analyses of the
|
||||
same bytes could therefore disagree because of language, compiler, loader,
|
||||
analyzer, or provider-version differences while appearing target-compatible.
|
||||
|
||||
These constraints are embodied in the current
|
||||
The decision addressed the earlier representations of
|
||||
[`CompositeProvider`](../../src/application/CompositeProvider.ts),
|
||||
[runtime composition](../../src/application/runtime.ts),
|
||||
[`BinarySession`](../../src/application/BinarySession.ts),
|
||||
@@ -569,5 +572,7 @@ The first six implementation stages are shipped:
|
||||
|
||||
Future stages may deepen format and indirect-flow coverage, but must continue
|
||||
to compare normalized semantics rather than provider-specific pseudocode or
|
||||
assembly text. Ghidra continues to omit GUI and mutation operations, so
|
||||
unsupported requests cannot route to an unverified implementation.
|
||||
assembly text. Linux and macOS now admit atomic function-name and entry-comment
|
||||
edits in the ephemeral database, with readback, rollback, and cache invalidation.
|
||||
Executable bytes remain unchanged. Windows P0 retains its read-only boundary;
|
||||
GUI and other unimplemented mutations remain unavailable.
|
||||
+5
-5
@@ -4,8 +4,8 @@ Accepted decisions describe the architecture that implementation PRs must
|
||||
follow. Acceptance does not by itself mean the behavior is shipped; each record
|
||||
states its implementation status separately.
|
||||
|
||||
| ADR | Status | Implementation |
|
||||
| ------------------------------------------------------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Provider selection and 22 Ghidra operations shipped on Linux/macOS; Windows analysis blocked |
|
||||
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Superseded | Historical design; the controlled JavaScript replay tool was removed |
|
||||
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Partially superseded | Seven static managed tools remain; runtime planning removed |
|
||||
| ADR | Status | Implementation |
|
||||
| ------------------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Provider selection and 25 read-only Ghidra operations on Linux/macOS and experimental Windows x64 P0; Linux/macOS session annotations |
|
||||
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Superseded | Historical design; the controlled JavaScript replay tool was removed |
|
||||
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Partially superseded | Seven static managed tools remain; runtime planning removed |
|
||||
@@ -0,0 +1,130 @@
|
||||
# Static Android APK analysis
|
||||
|
||||
REA wraps an existing headless JADX engine through `src/android/`. Its source is
|
||||
kept as an unmodified, commit-pinned Git submodule; see
|
||||
[upstream provenance](../third_party/README.md). CLI and MCP use the same
|
||||
application workflow and return artifact-bound Evidence inline.
|
||||
|
||||
This family is present on repository main and absent from npm 4.0.1. Check the
|
||||
[released package boundary](installation.md#released-package-and-main) and the
|
||||
connected server's tool list before selecting it.
|
||||
|
||||
## Supply tools
|
||||
|
||||
Use **Java 17 or newer** and **jadx-headless-mcp 0.7.1**. REA does not install Java,
|
||||
an Android SDK, JADX, a device service or an emulator. Set the path in the shell
|
||||
or MCP server environment:
|
||||
|
||||
```sh
|
||||
export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
|
||||
# Optional: select an existing JDK rather than java on PATH.
|
||||
export JAVA_HOME=/absolute/path/existing-jdk
|
||||
```
|
||||
|
||||
Linux is verified with the public Appium ApiDemos fixture. The POSIX adapter can
|
||||
run on macOS, but that host has not undergone real Android verification.
|
||||
Windows is unsupported for this provider's owned stdio process boundary.
|
||||
|
||||
## Answer an analyst question
|
||||
|
||||
```sh
|
||||
rea inspect-android-package /path/Example.apk
|
||||
rea search-android-classes /path/Example.apk MainActivity
|
||||
rea inspect-android-class /path/Example.apk example.MainActivity
|
||||
rea inspect-android-method /path/Example.apk example.MainActivity onCreate
|
||||
rea inspect-android-method /path/Example.apk example.MainActivity select --overload-index 1
|
||||
rea trace-android-references /path/Example.apk example.MainActivity
|
||||
rea trace-android-references /path/Example.apk example.MainActivity --method-name onCreate
|
||||
```
|
||||
|
||||
Equivalent MCP methods are `inspect_android_package`, `search_android_classes`,
|
||||
`inspect_android_class`, `inspect_android_method` and `trace_android_references`.
|
||||
Every request includes `path`. Class/method operations use `class_name`,
|
||||
`method_name` and optional `overload_index` as applicable. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_android_method",
|
||||
"arguments": {
|
||||
"path": "/path/Example.apk",
|
||||
"class_name": "example.MainActivity",
|
||||
"method_name": "select",
|
||||
"overload_index": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `inspect_android_class` to choose an overload. REA rejects an ambiguous
|
||||
method request rather than silently selecting the first match. The engine's
|
||||
method reference operation cannot select overloads, so REA reports that boundary
|
||||
as unsupported when multiple same-name methods exist.
|
||||
|
||||
The engine's smali fallback also joins same-name overloads; REA rejects that
|
||||
fallback for an overloaded method instead of attributing all bodies to one
|
||||
selected overload. Native/abstract methods with no decompiled body report
|
||||
`body_status: "not_available"` without inferring which implementation flag caused it.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- Package inspection retains decoded manifest XML, summary declarations,
|
||||
permissions and class/resource counts. Summary fields are extracted by the
|
||||
upstream engine and should be checked against the retained XML when needed.
|
||||
Signature verification is explicitly `not_performed`.
|
||||
- Class search consumes all pages and checks their counts. An empty `query`
|
||||
inventories all class names; class names are decompiler representations.
|
||||
- Class inventories return display signatures, fields and inner class names.
|
||||
Exact DEX descriptors are unknown. Overload indices are tied to this artifact
|
||||
and engine, not stable identities across different builds.
|
||||
- Method inspection reports Java or smali, fallback markers and complete/partial
|
||||
returned text. Decompiled source is a derived representation. A complete text
|
||||
result does not establish complete semantics or a byte-identical reconstruction.
|
||||
- Reference results are incoming static relationships between provider nodes.
|
||||
Source lines and DEX instruction offsets remain unknown. They are not observed
|
||||
runtime calls.
|
||||
- Original requests, APK SHA-256, actual engine JAR SHA-256 and raw producer
|
||||
responses remain in Evidence. Source revision is populated only for bytes
|
||||
matching the audited release JAR.
|
||||
|
||||
These operations do not execute the APK or provide split APK/AAB handling,
|
||||
signature validation, full resource-table semantics, native-library analysis or
|
||||
Android runtime capture. Existing artifact inventory/extraction tools can supply
|
||||
archive evidence; `project_android_application_graph` remains a separate,
|
||||
execution-free inventory projection.
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
Each request creates private, immutable APK and JAR copies. Their workspace and
|
||||
owned process group are cleaned on success, failure, timeout and cancellation.
|
||||
REA never writes to the original APK or launches target code.
|
||||
|
||||
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.
|
||||
|
||||
The provider serializes requests within each REA instance, uses one worker and
|
||||
sets JVM maximum heap to **512 MiB**. Heap is not a total RSS cap: the JVM also
|
||||
uses native memory. Independent REA instances have independent queues. Whole
|
||||
operations have a **120-second** execution deadline after reaching the queue's
|
||||
front; upstream decompilation is limited to **90 seconds**.
|
||||
|
||||
Decoded manifest and method text have a **1 MiB** upstream byte budget and report
|
||||
truncation. A protocol frame above **8 MiB**, or cumulative stdout/stderr above
|
||||
**32 MiB**, fails with a diagnostic instead of returning an incomplete inventory
|
||||
as complete. These are memory-safety boundaries, not undocumented result caps.
|
||||
|
||||
## Real verification
|
||||
|
||||
Download the explicit public fixtures once; this installs no tools:
|
||||
|
||||
```sh
|
||||
npm run fixtures:android
|
||||
export REA_JADX_MCP_JAR="$PWD/_reference/apk-integration/jadx-headless-mcp-0.7.1-all.jar"
|
||||
export REA_ANDROID_TEST_APK="$PWD/_reference/apk-integration/ApiDemos-debug.apk"
|
||||
npm run verify:android
|
||||
```
|
||||
|
||||
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
|
||||
REA CLI and MCP results with the real engine, separately from synthetic protocol
|
||||
regressions. It needs no Gradle build, emulator, Android SDK or Ghidra.
|
||||
@@ -22,16 +22,25 @@ graph TD
|
||||
subgraph "Provider Layers"
|
||||
HOPPER["src/hopper/<br/>Hopper launch & Unix-socket protocol<br/>BridgeLauncher, HopperClient, protocol"]
|
||||
GHIDRA["src/ghidra/<br/>Ghidra discovery, profile & function analysis<br/>snapshot, Unix/Windows transport, bounded lifecycle"]
|
||||
IDA["src/ida/<br/>Upstream MCP contract adaptation<br/>live GUI binding or owned headless database"]
|
||||
IDA_MCP["External ida-pro-mcp<br/>GUI proxy or database supervisor<br/>upstream installation and engine lifecycle"]
|
||||
AUX["src/artifacts/ + src/native/<br/>disjoint artifact and native capabilities"]
|
||||
BROWSER["src/browser/<br/>Loopback CDP transport<br/>exact-origin passive observation"]
|
||||
DOTNET["src/dotnet/<br/>Managed static metadata provider<br/>PE/CLI metadata inventory, members, IL"]
|
||||
FIRMWARE["src/firmware/<br/>Binwalk / Unblob adapters<br/>owned processes, report normalization, private extraction"]
|
||||
ANDROID["src/android/<br/>Static APK adapter<br/>owned SDK stdio, pinned headless JADX, normalization"]
|
||||
REFERENCE["src/reference/<br/>Filesystem reference-source reader<br/>bounded traversal, validation, errors"]
|
||||
end
|
||||
|
||||
subgraph "Provider Process Foundation"
|
||||
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
|
||||
WINDOWS["src/windows/<br/>Windows host boundary adapters<br/>package loader, opaque handles, owned jobs and private runtimes"]
|
||||
WINDOWS_NATIVE["native/windows/src/<br/>Node-API implementation<br/>NTFS admission, protected DACLs and atomic Job Object assignment"]
|
||||
end
|
||||
|
||||
subgraph "Contracts Layer"
|
||||
CONTRACTS["src/contracts/<br/>Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"]
|
||||
ERRORS["src/contracts/errorSchemas.ts<br/>Shared analysis error schemas<br/>CLI diagnostics, generated error catalog"]
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
@@ -46,41 +55,64 @@ graph TD
|
||||
TESTS["tests/<br/>Vitest suite<br/>fixtures/ holds fake launcher, bridge, and CDP seams"]
|
||||
end
|
||||
|
||||
subgraph "Evaluation"
|
||||
EVALUATION["src/evaluation/<br/>Agent release evaluation<br/>session transcript scoring"]
|
||||
end
|
||||
|
||||
DISPATCH --> CLI
|
||||
DISPATCH --> MAIN
|
||||
MAIN --> CONFIG
|
||||
MAIN --> SERVER
|
||||
MAIN --> APP
|
||||
CLI --> APP
|
||||
APP --> FIRMWARE
|
||||
FIRMWARE --> PROCESS
|
||||
FIRMWARE --> CONTRACTS
|
||||
APP --> ANDROID
|
||||
ANDROID --> PROCESS
|
||||
ANDROID --> CONTRACTS
|
||||
ANDROID --> DOMAIN
|
||||
CLI --> ERRORS
|
||||
APP --> SESSION
|
||||
SESSION --> REGISTRY
|
||||
REGISTRY --> HOPPER
|
||||
REGISTRY --> GHIDRA
|
||||
REGISTRY --> IDA
|
||||
IDA --> IDA_MCP
|
||||
IDA --> CONTRACTS
|
||||
IDA --> DOMAIN
|
||||
SESSION --> AUX
|
||||
APP --> JS_RECON
|
||||
APP --> INSTALL
|
||||
SERVER --> CONTRACTS
|
||||
SERVER --> APP
|
||||
APP --> BROWSER
|
||||
APP --> REPLAY
|
||||
APP --> DOTNET
|
||||
DOTNET --> DOMAIN
|
||||
HOPPER --> HOPPER_BRIDGE
|
||||
GHIDRA --> GHIDRA_BRIDGE
|
||||
HOPPER --> PROCESS
|
||||
GHIDRA --> PROCESS
|
||||
GHIDRA --> WINDOWS
|
||||
PROCESS --> WINDOWS
|
||||
WINDOWS --> WINDOWS_NATIVE
|
||||
APP --> PROCESS
|
||||
APP --> DOMAIN
|
||||
JS_RECON --> AUX
|
||||
JS_RECON --> DOMAIN
|
||||
APP --> REFERENCE
|
||||
REFERENCE --> DOMAIN
|
||||
CONTRACTS --> DOMAIN
|
||||
HOPPER --> DOMAIN
|
||||
GHIDRA --> DOMAIN
|
||||
AUX --> DOMAIN
|
||||
BROWSER --> DOMAIN
|
||||
REPLAY --> DOMAIN
|
||||
PROCESS --> DOMAIN
|
||||
TESTS -.-> MAIN
|
||||
TESTS -.-> HOPPER
|
||||
TESTS -.-> GHIDRA
|
||||
TESTS -.-> IDA
|
||||
TESTS -.-> PROCESS
|
||||
TESTS -.-> BROWSER
|
||||
TESTS -.-> REPLAY
|
||||
TESTS -.-> EVALUATION
|
||||
TESTS -.-> ERRORS
|
||||
@@ -4,7 +4,7 @@ REA can attach to a user-owned Chrome-family browser through the Chrome DevTools
|
||||
|
||||
## Shipped surfaces
|
||||
|
||||
- `list_browser_targets` / `rea list-browser-targets` discovers page targets at the selected target's current origin by default, with an optional exact-origin filter.
|
||||
- `list_browser_targets` / `rea list-browser-targets` discovers all eligible page targets at the endpoint by default, with an optional exact-origin filter.
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
|
||||
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
|
||||
@@ -37,9 +37,9 @@ REA does not launch, own, or terminate this browser. Use a dedicated profile and
|
||||
|
||||
## Request boundary
|
||||
|
||||
Each request supplies a literal loopback CDP endpoint and target. Target
|
||||
discovery uses the selected target's current origin by default; an optional
|
||||
exact HTTP(S) origin filter can narrow the result. `localhost`, private-LAN
|
||||
Discovery supplies a literal loopback CDP endpoint and returns all eligible
|
||||
page targets unless an exact HTTP(S) origin filter narrows the result. Follow-up
|
||||
inspection requests select one target; their default scope is its current origin. `localhost`, private-LAN
|
||||
addresses, HTTPS CDP endpoints, credentials, paths, queries, fragments, and
|
||||
implicit ports are not valid CDP endpoints. Origin filters contain only scheme,
|
||||
host, and port; paths, credentials, queries, fragments, and wildcards are not
|
||||
|
||||
@@ -21,6 +21,115 @@ Duration, action, and navigation
|
||||
timeouts remain fixed provider-owned liveness limits; the request does not
|
||||
accept a caller-controlled `limits` object.
|
||||
|
||||
## Minimal launch example
|
||||
|
||||
Save this as `browser-scenario.json`, replacing the executable path with an
|
||||
already-installed Chrome-family browser and the URL with your running fixture:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": {
|
||||
"mode": "launch",
|
||||
"executable_path": "/absolute/path/to/chromium"
|
||||
},
|
||||
"start_url": { "url": "http://127.0.0.1:3000" },
|
||||
"actions": [
|
||||
{ "step_id": "settle", "action": "wait_for_timeout", "duration_ms": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
rea capture-browser-scenario ./browser-scenario.json --json > browser-capture.json
|
||||
```
|
||||
|
||||
The default capture includes the final URL. Select DOM, screenshots, or events
|
||||
only when they help answer the question. REA launches and cleans up the browser
|
||||
profile for this request; it does not start your fixture server or install a
|
||||
browser. A schema-valid scenario does not prove that the executable or target
|
||||
is available on the host.
|
||||
|
||||
## Interaction example: capture the visible result
|
||||
|
||||
The default final URL answers "where did the page end up?", not "what did the
|
||||
page show?". Many interactions, such as a search button that renders results in
|
||||
place, change the page without changing the URL. A capture with only the
|
||||
default `url` artifact then reports that the click succeeded and the URL is
|
||||
unchanged, which cannot tell you whether results appeared.
|
||||
|
||||
Request the artifacts that answer your question and wait for the state you want
|
||||
to inspect. This scenario clicks a Search button, waits for the current search
|
||||
results, and captures the final DOM, accessibility tree, and URL. It assumes the
|
||||
local page clears `#results`'s `data-state` while loading and sets it to `ready`
|
||||
after rendering the current response. Adapt the selectors and readiness marker
|
||||
to your page; an already-visible container or previous results are insufficient.
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": {
|
||||
"mode": "launch",
|
||||
"executable_path": "/absolute/path/to/chromium"
|
||||
},
|
||||
"start_url": { "url": "http://127.0.0.1:3000" },
|
||||
"actions": [
|
||||
{
|
||||
"step_id": "search",
|
||||
"action": "click",
|
||||
"locator": { "kind": "css", "selector": "#search" },
|
||||
"timeout_ms": 10000
|
||||
},
|
||||
{
|
||||
"step_id": "results-ready",
|
||||
"action": "wait_for",
|
||||
"locator": { "kind": "css", "selector": "#results[data-state='ready']" },
|
||||
"state": "visible",
|
||||
"timeout_ms": 10000
|
||||
}
|
||||
],
|
||||
"capture": { "at_end": ["dom", "accessibility", "url"] }
|
||||
}
|
||||
```
|
||||
|
||||
Save it as `browser-search.json` and run it the same way as the minimal
|
||||
example:
|
||||
|
||||
```bash
|
||||
rea capture-browser-scenario ./browser-search.json --json > browser-search-capture.json
|
||||
```
|
||||
|
||||
Through MCP, pass the same object as the `capture_browser_scenario` arguments.
|
||||
The final step's DOM and accessibility artifacts retain the page state at that
|
||||
capture time inline. REA still launches and cleans up the browser profile.
|
||||
|
||||
The click's `timeout_ms` covers the click action. A non-navigating `fetch` or
|
||||
delayed render can finish later, so use `wait_for` to observe a page-specific
|
||||
ready condition before final capture. A page that updates synchronously can
|
||||
omit that wait. REA does not infer application readiness from capture selection.
|
||||
|
||||
Choose captures by the question being asked:
|
||||
|
||||
- `dom` and `accessibility`: text or structure the page rendered, such as
|
||||
search results, validation messages, or updated lists. The accessibility tree
|
||||
is usually the more compact way to read visible text.
|
||||
- `screenshot`: visual layout or styling, or content not exposed as text, such
|
||||
as a canvas.
|
||||
- `history` or `storage`: navigation history, or cookies and web storage the
|
||||
interaction wrote. Request them only when that state is the question; they
|
||||
are never captured implicitly.
|
||||
- `after_each_step` instead of `at_end`: intermediate state between actions in
|
||||
a multi-step scenario.
|
||||
- `events` (`console`, `page-errors`, `network`, and so on): how the page
|
||||
reached its result, such as the request a search issued or an error it
|
||||
logged. Select only the event families you need.
|
||||
|
||||
Capture states are reported per artifact. An artifact you did not request is
|
||||
`not_requested`, which means REA did not look, not that the page had nothing
|
||||
to show. A step's `completeness` of `complete` covers only the sections that
|
||||
were requested. A URL-only capture can be `complete` and still be unable to
|
||||
answer a question about rendered content, so add the relevant artifact and run
|
||||
the scenario again rather than inferring from its absence. Capture completeness
|
||||
also does not assert that the page's background work has finished.
|
||||
|
||||
The browser boundary is part of the contract. Launch mode requires a
|
||||
caller-selected executable and always uses a provider-owned temporary profile
|
||||
that is closed and deleted during cleanup. Connect mode accepts only an
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# Firmware analysis
|
||||
|
||||
REA integrates **Binwalk 3.1.0** for region inspection and **Unblob 26.6.4** for
|
||||
explicit extraction. This first firmware milestone is verified on Linux x64.
|
||||
No device, root privilege, mount, emulation or execution of extracted programs
|
||||
is needed. Arbitrary firmware bytes do not pass native executable admission;
|
||||
select a returned ELF separately for supported Ghidra analysis.
|
||||
|
||||
## Caller-supplied tools
|
||||
|
||||
Provide existing tools using absolute executable paths, scoped to your shell:
|
||||
|
||||
```sh
|
||||
export REA_BINWALK_COMMAND=/absolute/path/to/binwalk
|
||||
export REA_UNBLOB_COMMAND=/absolute/path/to/unblob
|
||||
```
|
||||
|
||||
REA does not install these tools, their dependencies or system packages. It
|
||||
checks the actual tool version when executing a request. Inspection and
|
||||
extraction have independent prerequisites; one does not require the other.
|
||||
Both require util-linux `prlimit`, normally `/usr/bin/prlimit`; override its
|
||||
absolute path with `REA_FIRMWARE_PRLIMIT_COMMAND` if needed.
|
||||
|
||||
Unblob needs the external extractor for the selected format on its inherited
|
||||
`PATH`: for example, `debugfs` for ext2/ext3/ext4. Its default Landlock sandbox
|
||||
and skip rules stay enabled. Sandbox startup failures remain actionable errors;
|
||||
REA does not retry with `--no-sandbox`. Missing extractors reported in a valid
|
||||
analysis report retain partial results and dependency names inline.
|
||||
|
||||
Upstream sources are preserved unmodified as optional pinned submodules:
|
||||
|
||||
```sh
|
||||
git submodule update --init third_party/binwalk third_party/unblob
|
||||
```
|
||||
|
||||
Normal REA builds and npm packages do not include or build these sources or
|
||||
engine binaries. See [upstream provenance](../third_party/README.md).
|
||||
|
||||
## CLI and MCP
|
||||
|
||||
```sh
|
||||
rea inspect-firmware-regions /path/to/firmware.bin
|
||||
rea extract-firmware /path/to/firmware.bin /path/to/new-output
|
||||
rea extract-firmware /path/to/firmware.bin /path/to/selected-output \
|
||||
--offset 64 --length 4096 --max-depth 3
|
||||
```
|
||||
|
||||
The corresponding MCP tools accept the same intent:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_firmware_regions",
|
||||
"arguments": { "path": "/path/to/firmware.bin" }
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "extract_firmware",
|
||||
"arguments": {
|
||||
"path": "/path/to/firmware.bin",
|
||||
"output_directory": "/path/to/new-output",
|
||||
"range": { "offset": 64, "length": 4096 },
|
||||
"max_depth": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both return Evidence with the original path and SHA-256, raw producer reports,
|
||||
actual executable path/digest, version and limitations. Binwalk signature UUIDs
|
||||
and Unblob blob IDs are producer-local identities; compare content hashes and
|
||||
source ranges across runs. A matching version does not attest an installed
|
||||
build's source revision, so that field remains unknown.
|
||||
|
||||
### Interpret the results
|
||||
|
||||
- Region offsets refer to bytes in the original firmware. `reported_size`
|
||||
preserves Binwalk's output; `size_basis=provider_reported_validation_unknown`
|
||||
reflects that Binwalk can replace unknown lengths with the next signature or
|
||||
EOF before serialization. Neither a signature nor its confidence byte proves
|
||||
a valid partition, encryption or runtime behavior.
|
||||
- Extraction returns regular file paths, independently verified SHA-256 values,
|
||||
producer hashes when available, chunks, diagnostics and task derivations.
|
||||
Unblob's task report is unordered; REA resolves relationships by paths and
|
||||
blob IDs. Logical references use `$input`, `$output`, or `$output/<relative-path>`
|
||||
so a real file named `$input` stays distinct from the original input.
|
||||
Chunk offsets belong to the task's input file. Only chunks in the
|
||||
selected input have a known `root_file_range`, adjusted by the selected offset.
|
||||
- Decompressed children have unknown original byte ranges and runtime addresses.
|
||||
Task lineage does not establish a byte-for-byte mapping through decompression.
|
||||
- `partial` marks unknown/encrypted chunks, reported extraction problems,
|
||||
unpublished links/special files or reached depth limits. `complete` means the
|
||||
engine finished its configured extraction without those reported conditions;
|
||||
default skip rules and format coverage still apply. It does not establish
|
||||
complete firmware reconstruction.
|
||||
- The destination must be absent, absolute, with an existing parent. Publication
|
||||
uses exclusive, symlink-resistant writes and durable digest readback. Existing
|
||||
destinations are never overwritten. This is not an atomic directory rename:
|
||||
a new tree can be visible while files are written. Failed uncommitted output
|
||||
is rolled back. Symlinks and special files are recorded without publication;
|
||||
empty directories, ownership and executable permission bits are not reproduced.
|
||||
|
||||
Select a verified native child for the existing tools:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "open_binary",
|
||||
"arguments": {
|
||||
"path": "/path/from/extraction/usr/bin/program",
|
||||
"provider_id": "ghidra"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then use `analyze_function`, `inspect_native_api` or the other native primitives.
|
||||
Native architecture and executable validation still apply. This milestone does
|
||||
not add MIPS/PPC/RISC-V decompilation, raw flash load profiles, bootloader/device
|
||||
tree semantics, vulnerability scanning, emulation or hardware runtime capture.
|
||||
|
||||
## Resource and lifecycle policy
|
||||
|
||||
- Nonempty regular firmware inputs: at most **128 MiB**, copied and hashed into
|
||||
a private workspace before analysis. No in-place source modification.
|
||||
- One queued heavy operation per provider instance; Binwalk one thread and
|
||||
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.
|
||||
- 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.
|
||||
|
||||
Staging budgets are polled and rechecked after exit; short-lived overshoot is
|
||||
possible. The inherited file-size limit bounds each file, not total storage;
|
||||
address-space limits are per process, not an aggregate memory quota. Worker
|
||||
count is not CPU affinity for every external extractor. These limits do not
|
||||
claim cgroup/container containment. Cancellation cleans the owned process group
|
||||
before removing its workspace; uncertain cleanup retains resources and blocks
|
||||
further operations in that provider instance.
|
||||
|
||||
## Verification
|
||||
|
||||
Generate small public, source-derived fixtures into ignored `_reference/`:
|
||||
|
||||
```sh
|
||||
npm run fixtures:firmware
|
||||
npm run verify:firmware
|
||||
```
|
||||
|
||||
Fixture generation requires existing Python 3 and a host C compiler (`cc`);
|
||||
override with `REA_FIRMWARE_PYTHON` / `REA_FIRMWARE_CC`. The independent oracle
|
||||
records gzip offset/length and extracted file hashes. Real verification checks
|
||||
CLI/MCP, full-file and selected-range extraction, unknown chunks, depth limits,
|
||||
content parity and unchanged source bytes. No binary fixture is committed.
|
||||
|
||||
Additional lanes are explicit, so the base lane stays small:
|
||||
|
||||
```sh
|
||||
# Existing mke2fs and debugfs required; put debugfs on the scoped PATH.
|
||||
REA_FIRMWARE_VERIFY_EXT4=1 npm run fixtures:firmware
|
||||
REA_FIRMWARE_VERIFY_EXT4=1 npm run verify:firmware
|
||||
|
||||
# Existing Ghidra 12.1.4 and Java required.
|
||||
REA_FIRMWARE_VERIFY_GHIDRA=1 npm run verify:firmware
|
||||
```
|
||||
|
||||
`REA_FIRMWARE_MKE2FS_COMMAND` selects the optional ext4 fixture producer.
|
||||
`REA_FIRMWARE_FIXTURE_ROOT` selects generated data storage.
|
||||
`REA_FIRMWARE_TEST_ENTRYPOINT` can select an isolated installed npm package's
|
||||
`scripts/rea.mjs` to verify the packaged CLI/MCP workflow. Real coverage here is
|
||||
generated gzip/USTAR/ext4 with a Linux x64 ELF child; SquashFS, UBIFS, proprietary
|
||||
firmware, other hosts and architectures remain unverified.
|
||||
+85
-2
@@ -1,6 +1,6 @@
|
||||
# DOS MZ analysis with Ghidra
|
||||
# DOS MZ and COM analysis with Ghidra
|
||||
|
||||
REA can analyze DOS MZ executables through the bring-your-own Ghidra adapter.
|
||||
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
|
||||
decompilation through the CLI and stdio MCP. It requires the same Ghidra
|
||||
12.1.4 and 64-bit JDK 21 installation as other Ghidra sessions; no DOS emulator
|
||||
@@ -13,6 +13,9 @@ rea inspect /absolute/path/to/legacy.exe --provider ghidra --json
|
||||
rea search /absolute/path/to/legacy.exe entry --kind procedures --provider ghidra --json
|
||||
rea instructions /absolute/path/to/legacy.exe entry --provider ghidra --json
|
||||
rea function /absolute/path/to/legacy.exe entry --provider ghidra --json
|
||||
rea inspect-native-load-image /absolute/path/to/legacy.exe --provider ghidra --json
|
||||
rea read-bytes /absolute/path/to/legacy.exe 0x10000 --length 16 --provider ghidra --json
|
||||
rea address-to-file-offset /absolute/path/to/legacy.exe 0x10000 --provider ghidra --json
|
||||
```
|
||||
|
||||
Use an observed procedure name or address from discovery when the target does
|
||||
@@ -38,6 +41,43 @@ The profile also commits `function_body_evidence: complete-inclusive-ranges-v1`,
|
||||
so snapshots from older length-only Ghidra results do not satisfy the current
|
||||
profile.
|
||||
|
||||
## Headerless COM images
|
||||
|
||||
COM has no identifying header. Select its interpretation explicitly; neither the
|
||||
`.com` suffix nor arbitrary unrecognized bytes are enough to admit it.
|
||||
|
||||
```bash
|
||||
rea inspect /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
|
||||
rea function /absolute/path/to/legacy.com 0x10100 --target-format dos-com --provider ghidra --json
|
||||
rea inspect-native-load-image /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "open_binary",
|
||||
"arguments": {
|
||||
"path": "/absolute/path/to/legacy.com",
|
||||
"format": "dos-com",
|
||||
"provider_id": "ghidra"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The caller's interpretation applies to the complete file, even if its filename
|
||||
suggests another format. Files must contain 1..65280 bytes: the 64 KiB COM segment
|
||||
minus the 256-byte PSP prefix. Ghidra uses `BinaryLoader` with real-mode language
|
||||
and maps file offset zero at `1000:0100` (linear `0x10100`). A packaged pre-analysis
|
||||
script seeds the entry function and CS/DS/ES/SS context `0x1000`. These settings
|
||||
and the preparation policy are committed in the analysis profile; a failed
|
||||
preparation prevents the resident bridge from serving results.
|
||||
|
||||
Load-image verification checks all original and modified file bytes, complete
|
||||
source mapping/memory digests, absence of relocations, entry and measured register
|
||||
context. The context is imposed for analysis, not observed from execution. PSP
|
||||
memory, initial stack contents/SP, DOS interrupts and PC-98 devices are not
|
||||
modeled. COM has no MZ header, relocation table or appended-overlay model; all
|
||||
selected bytes belong to its analysis module. The original file stays unchanged.
|
||||
|
||||
## Addresses and function extent
|
||||
|
||||
Returned default-space addresses are linear byte coordinates. A Ghidra
|
||||
@@ -71,6 +111,30 @@ Ghidra's database observation, not a proof of original source ownership or
|
||||
exhaustive code discovery. Providers that do not report complete ranges return
|
||||
`available: false` and an explicit reason.
|
||||
|
||||
## Loaded-image evidence
|
||||
|
||||
`inspect_native_load_image` independently checks the immutable session snapshot
|
||||
against the loaded Ghidra Program. It measures complete original and modified
|
||||
FileBytes digests, every source mapping and its memory digest, relocation records
|
||||
(including multiplicity), and external entry points. It verifies MZ header and
|
||||
module partitions, relocation fixups and linear coordinates rather than relying
|
||||
on the imported executable SHA alone.
|
||||
|
||||
The result is `verified`, `mismatch`, or `unsupported`. Every check retains its
|
||||
expected and observed values. A memory-digest mismatch identifies the source
|
||||
range's file offset and analysis address; it does not locate the first differing
|
||||
byte inside that range. Mapping ends are **inclusive**, unlike the exclusive ends
|
||||
from `list_segments`. Appended overlays remain in source-file identity and are
|
||||
excluded from initialized module coverage. Uninitialized allocations are reported;
|
||||
their contents and DOS runtime semantics are not verified. Formats other than MZ and explicit COM return
|
||||
`unsupported` with measured observations, not a successful MZ verification.
|
||||
|
||||
`read_bytes` returns initialized provider memory, including loader fixups; an
|
||||
unmapped or uninitialized byte ends the read with an explicit returned length and
|
||||
`complete: false`. `address_to_file_offset` uses the provider's source mapping;
|
||||
unmapped, uninitialized and ambiguous mappings fail explicitly. A relocated word
|
||||
can differ from the original file word even when its source offset is known.
|
||||
|
||||
## Coverage boundaries
|
||||
|
||||
Static decompilation does not execute the target or emulate BIOS, DOS
|
||||
@@ -103,3 +167,22 @@ executable fixtures, an emulator, or an existing analysis database.
|
||||
Provider p-code representations can contain process-specific address-space selector
|
||||
tokens. The lane preserves those reported values and compares stable dossier
|
||||
observations rather than asserting cross-process identity of raw p-code.
|
||||
|
||||
## Function annotations
|
||||
|
||||
`annotate_native_function` changes the name and/or comments at one exact local function entry, verifies readback, and returns a refreshed dossier. This is useful when recording recovered DOS function roles; it does not claim original-source names. Name writes preserve the existing namespace; readback returns the fully qualified name used by function inventories. Regular comments map to Ghidra PRE comments and inline comments to EOL comments. Empty text clears a comment; omitted fields preserve it. The whole edit rolls back if a setter, readback or analysis fails.
|
||||
|
||||
```json
|
||||
{
|
||||
"procedure": "0x10100",
|
||||
"name": "entry",
|
||||
"comment": "Recovered startup role",
|
||||
"inline_comment": "Inspect segment setup"
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
rea annotate-native-function /local/program.com 0x10100 --target-format dos-com --provider ghidra --name entry --comment 'Recovered startup role' --json
|
||||
```
|
||||
|
||||
The CLI returns the updated analysis before closing its isolated session. MCP edits remain visible to later reads until `close_binary`. Original bytes remain unchanged, database edits are discarded on close, and immutable analysis snapshots are invalidated after edits. This operation does not save a Ghidra project, change ABI/types/body ranges, patch instructions, or emulate a DOS runtime. Windows P0 does not admit it.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Optional NativeAOT metadata recovery
|
||||
|
||||
REA integrates [Washi1337/ghidra-nativeaot](https://github.com/Washi1337/ghidra-nativeaot)
|
||||
through an optional headless adapter. The original source is an opt-in submodule,
|
||||
pinned to `effeb734fc570c32650f88b159608979dc7b423e`; its MIT license and existing
|
||||
source notices are retained. No upstream source or algorithms are rewritten.
|
||||
|
||||
The initial tested layout is .NET runtime **8.0.22**, NativeAOT RTR **9.1**,
|
||||
x86-64 ELF and native Windows PE **targets analyzed on Linux x64**, using
|
||||
Ghidra **12.1.4** and JDK **21**. Other layouts/architectures/hosts are unsupported.
|
||||
Windows Ghidra P0 has no database mutation authority. A PE/CLI or ReadyToRun
|
||||
assembly should use `inspect_managed_artifact`; it is not NativeAOT.
|
||||
|
||||
## Build and configure
|
||||
|
||||
Bring your own supported Ghidra/JDK installation. This developer build needs
|
||||
existing `git`, `javac` and `jar`; it does not invoke Gradle, download toolchains,
|
||||
or change host configuration:
|
||||
|
||||
```sh
|
||||
git submodule update --init third_party/ghidra-nativeaot
|
||||
npm run build:ghidra:nativeaot
|
||||
export REA_GHIDRA_NATIVEAOT_JAR="$PWD/_reference/nativeaot-integration/extension/rea-ghidra-nativeaot.jar"
|
||||
```
|
||||
|
||||
The build requires a clean pinned upstream checkout, compiles unchanged upstream
|
||||
Java plus the REA facade, uses a fresh classes directory, and writes the JAR and
|
||||
build manifest under ignored `_reference/`. `REA_NATIVEAOT_BUILD_ROOT` can select
|
||||
another local output directory; `REA_NATIVEAOT_SOURCE` can select an equivalent
|
||||
clean pinned source checkout. Neither output nor compiled fixture belongs in Git
|
||||
or the npm package. An installed REA accepts an existing explicitly supplied JAR.
|
||||
|
||||
REA commits the actual JAR SHA-256 into the analysis profile, copies identical
|
||||
bytes into the owned runtime, rechecks the digest before loading, and closes the
|
||||
classloader/project on session close. Profiles cannot enable code absent from
|
||||
explicit local configuration. JARs execute within Ghidra; supply a build you trust.
|
||||
The producer's reported source revision is build metadata, not an attestation of
|
||||
arbitrary caller-supplied bytes. The bounded loader accepts regular JARs up to
|
||||
8 MiB to limit startup artifact memory; this does not truncate analysis results.
|
||||
|
||||
## Use existing tools
|
||||
|
||||
Open the executable with provider `ghidra`. `inspect_native_load_image` returns
|
||||
optional `observations.metadata_recovery` inline, including the detected header,
|
||||
layout, recovery status, type category paths/addresses, diagnostics, annotation
|
||||
coverage and derived-memory SHA. The enclosing `unsupported` load-image status
|
||||
for PE/ELF concerns independent DOS loader verification; metadata recovery has
|
||||
its own status. It is not a failed import.
|
||||
|
||||
Pass one returned type path or its typed address to `inspect_native_data_type`.
|
||||
Its optional `metadata_recovery` includes related/base type, implemented
|
||||
interfaces and virtual slots with targets and known procedure names. Follow
|
||||
these with `analyze_function`, string searches and ordinary cross-references.
|
||||
For CLI use:
|
||||
|
||||
```sh
|
||||
rea inspect-native-load-image ./NativeAotFixture --provider ghidra --json
|
||||
rea inspect-native-data-type ./NativeAotFixture --type /NativeAOT/MethodTables/Class_ADDRESS_MT --provider ghidra --json
|
||||
rea function ./NativeAotFixture 0xADDRESS --provider ghidra --json
|
||||
```
|
||||
|
||||
No new MCP workflow or approval flag is required. Configuration opts into the
|
||||
adapter; without it ordinary Ghidra profiles/results remain unchanged.
|
||||
|
||||
## Evidence boundaries
|
||||
|
||||
Discovery prefers an imported RTR symbol, otherwise the upstream initialized,
|
||||
non-executable-memory signature heuristic. REA rejects ambiguous candidates,
|
||||
unknown layout versions, malformed directory rows, unsupported flags and
|
||||
unmapped ranges before recovery. It requires the verified dehydrated layout.
|
||||
|
||||
The adapter performs rehydration, method-table recovery and frozen-object
|
||||
annotation in one ephemeral Program transaction. Fatal failure/cancellation
|
||||
rolls it back. Recovery may change analysis-memory bytes, never the original
|
||||
executable file. Derived ranges have a digest and **no original file offset**;
|
||||
they are not captured runtime state. Candidate/committed instance annotation
|
||||
counts distinguish partial frozen-object coverage. Coverage concerns the
|
||||
recovered candidate set, not every possible object or every runtime type.
|
||||
|
||||
REA preserves pre-recovery calling conventions and uses the loaded compiler
|
||||
specification default for newly created methods. The upstream universal
|
||||
`__thiscall` assignment is not treated as x64 ABI authority. Parameters and
|
||||
method prototypes remain inferences; verify them against instructions/call sites.
|
||||
|
||||
Type relationships and System.Object/System.String identification use upstream
|
||||
heuristics. Generated names are explicitly marked; original class/member names
|
||||
and custom field layouts remain unknown. Pseudocode is recovered native logic,
|
||||
not original C# source. Unsupported recovery reports its stage and reason; it
|
||||
does not imply that ordinary native disassembly/decompilation is impossible.
|
||||
|
||||
## Verification
|
||||
|
||||
See [the dedicated lane](testing.md#optional-nativeaot-ghidra-analysis).
|
||||
Fixtures are source-built benign .NET programs with inheritance, an interface,
|
||||
virtual dispatch, representative arithmetic and frozen strings. Independent
|
||||
compiler/linker symbols and raw directory bytes provide the oracle. Stripped
|
||||
fixtures exercise heuristic detection. Tests do not execute the resulting
|
||||
NativeAOT binaries. Fixture generation and adapter compilation are optional and
|
||||
never prerequisites of ordinary native analysis or setup.
|
||||
@@ -0,0 +1,111 @@
|
||||
# IDA Pro provider
|
||||
|
||||
REA adapts an existing [mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp) server into its native analysis tools. Install and configure IDA, Hex-Rays, Python, and upstream MCP using that repository's instructions. REA does not install, upgrade, activate, or license them, and does not bundle another IDA server.
|
||||
|
||||
Choose one lifecycle mode in an upstream MCP JSON registration. Set `REA_IDA_MCP_CONFIG` to that file, then select `ida` once through `REA_ANALYSIS_PROVIDER`, the CLI's `--provider ida`, or MCP's `open_binary.provider_id`. No provider startup occurs during discovery.
|
||||
|
||||
## Analyze an already-open GUI target
|
||||
|
||||
The attached profile supports upstream **1.4.0 legacy tools**, including `get_metadata`, `list_functions`, and `decompile_function`. Copy the upstream registration's `command`, `args`, and optional `env` into a JSON file. An existing `mcpServers` object with an `ida-pro-mcp` entry is also accepted. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "python",
|
||||
"args": [
|
||||
"/absolute/path/to/ida_pro_mcp/server.py",
|
||||
"--ida-rpc",
|
||||
"http://127.0.0.1:13337"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use the executable and arguments from your working upstream registration. The legacy GUI's `/mcp` endpoint is its internal RPC endpoint; connect REA through the upstream stdio proxy rather than treating that URL as Streamable HTTP.
|
||||
|
||||
```bash
|
||||
export REA_IDA_MCP_CONFIG=/absolute/path/to/ida-mcp.json
|
||||
export REA_ANALYSIS_PROVIDER=ida
|
||||
rea function /absolute/path/to/program main --json
|
||||
rea decompile /absolute/path/to/program main --json
|
||||
```
|
||||
|
||||
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows.
|
||||
|
||||
REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched.
|
||||
|
||||
## Give REA a target for headless analysis
|
||||
|
||||
The headless profile supports upstream's **database supervisor** API: `idb_open`, `idb_list`, `idb_close`, explicit `database` arguments, and modern analysis tools. Legacy 1.4.0 `idalib-mcp`, which accepts one initial filename and serves SSE, does not implement this profile. Follow the [upstream headless instructions](https://github.com/mrexodia/ida-pro-mcp#headless-idalib-session-model) for an installation with the supervisor API.
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "idalib-mcp",
|
||||
"args": ["--stdio", "--max-workers", "1"],
|
||||
"mode": "headless"
|
||||
}
|
||||
```
|
||||
|
||||
Use an already-installed executable. When required by your existing idalib installation, include `"env": { "IDADIR": "/absolute/path/to/ida" }`. REA neither edits the global activation configuration nor installs dependencies. A configured local Streamable HTTP supervisor can instead use `"url": "http://127.0.0.1:8745/mcp"` and `"mode": "headless"`.
|
||||
|
||||
The same CLI commands work in both modes. For a Windows-native REA process:
|
||||
|
||||
```powershell
|
||||
$env:REA_IDA_MCP_CONFIG = 'D:\analysis\ida-mcp.json'
|
||||
$env:REA_ANALYSIS_PROVIDER = 'ida'
|
||||
rea function 'D:\samples\program.exe' main --json
|
||||
```
|
||||
|
||||
Headless REA and the configured supervisor must share the host filesystem and native path syntax. Run REA on Windows when the supervisor runs on Windows. The attached profile can span WSL and a Windows GUI because it binds recorded input hashes instead of asking the GUI to open a WSL path.
|
||||
|
||||
REA copies the admitted binary into a private temporary directory, checks its digest, and requests an owned worker with `force_headless`. POSIX workspaces use mode `0700`; Windows uses the matching package's native private-DACL boundary on a local NTFS temporary directory. It injects the returned database ID into analysis requests. It verifies the active owned worker and input path rather than adopting a user database. On close or failed startup, it releases its session with `save: false`, verifies that it is absent from upstream session discovery, and removes its private input and sidecars. An unverified release retains the workspace and reports incomplete cleanup.
|
||||
|
||||
One REA session owns one database. Stdio registrations default `IDA_MCP_MAX_WORKERS` to `1`; the explicit upstream `--max-workers 1` also bounds a supervisor. Database size and engine memory depend on the target and upstream; REA does not impose an engine memory limit. The original binary is never executed or overwritten. Optional `workspaceRoot` selects an existing absolute temporary-directory parent on the REA host. Ensure it has space for the input copy and IDA working files.
|
||||
|
||||
Headless cleanup uses the upstream database lifecycle. Abruptly terminating REA, its proxy, or the supervisor does not establish worker release. Prefer `close_binary` for MCP sessions; after an interrupted process, inspect upstream session inventory and retained workspace diagnostics before removing working files. This adapter does not claim Windows Job Object containment for the upstream detached workers.
|
||||
|
||||
## Connect an agent once
|
||||
|
||||
Add the registration path to the **REA** MCP entry's environment, keeping the upstream registration in its separate JSON file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@<version-with-ida>", "mcp"],
|
||||
"env": {
|
||||
"REA_ANALYSIS_PROVIDER": "ida",
|
||||
"REA_IDA_MCP_CONFIG": "/absolute/path/to/ida-mcp.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use a package version that actually includes this adapter; repository main can be ahead of the npm release. After reconnecting, call:
|
||||
|
||||
```json
|
||||
{"name":"open_binary","arguments":{"path":"/absolute/path/to/program","provider_id":"ida"}}
|
||||
{"name":"analyze_function","arguments":{"procedure":"main"}}
|
||||
{"name":"search_strings","arguments":{"pattern":"license"}}
|
||||
{"name":"close_binary","arguments":{}}
|
||||
```
|
||||
|
||||
Opening an MCP target starts its selected provider session; headless opening waits for auto-analysis. Set the agent's MCP request deadline long enough for the target. The registration's optional `timeoutMs` defaults to 300000 for upstream requests. Cancellation drains the current upstream request before cleanup; it does not force-stop GUI analysis. An open request that times out without a completed response retains the private workspace because worker inventory alone cannot prove the request has stopped. `binary_session` reports actual tool availability. Repeated doctor/setup calls are unnecessary when the registration already works.
|
||||
|
||||
For guided registration, set `REA_IDA_MCP_CONFIG` before `rea setup --client <client>`. Setup includes that file reference in its reviewed plan and preserves it after approval; it does not copy upstream credentials into the agent configuration. `rea doctor --provider ida --json` validates the registration without starting IDA and explicitly leaves runtime compatibility and target binding for analysis.
|
||||
|
||||
## Coverage and evidence
|
||||
|
||||
Both profiles support procedure/string inventory and literal or regex search, procedure resolution, pseudocode, assembly, raw function instructions, resolved callees, incoming address references, and a function dossier. Inventories and modern pseudocode/assembly follow upstream pagination. Upstream limits on modern xrefs and callees are retained as explicit truncation limitations. A transport-level truncated preview is rejected rather than reported as complete evidence; the error preserves upstream retrieval guidance.
|
||||
|
||||
The legacy profile additionally supports direct callers. Its producer may report call-site or interior addresses; REA resolves the reported function symbol to its canonical entry. The modern profile does not prove direct callers from its code/data xref labels, so that capability is unavailable and the dossier marks it unknown.
|
||||
|
||||
Complete body ranges, typed reference edges, CFG, referenced data, source locals, comment kinds, and database revision are unavailable. Empty unsupported dossier facets mean **unknown**, not observed absence. `binary_overview`, whole-symbol inventory, mutation, debugging, arbitrary Python forwarding, and GUI navigation are outside this adapter's coverage. Check session availability before composing a workflow that needs those facets.
|
||||
|
||||
`provider.version` identifies the REA adapter; the profile explicitly records `version_scope: "rea-ida-adapter"`. The MCP handshake's `serverInfo` is separate and does not establish IDA or Python distribution versions. Raw observations preserve producer metadata and output. Attached hashes identify the recorded original input; headless identity uses the digest-verified private copy and observed worker input path. Neither proves source-code recovery or an unmodified analysis database.
|
||||
|
||||
## Verification
|
||||
|
||||
Run `npm run verify:ida -- --target /absolute/path/to/program --procedure main` with a configured upstream server. It exercises production CLI and MCP tools, validates their output contracts and Evidence, checks original-input preservation and session cleanup, and keeps local target data out of its console summary. Supply `--package-root` to verify an extracted or installed package. See [testing](testing.md) for the distinction between adapter tests and real-provider evidence.
|
||||
|
||||
The initial real workflows use stdio registrations for the legacy Windows GUI profile and a Windows x64 headless supervisor with IDA 9.3. Streamable HTTP is tested against a loopback MCP fixture with the pinned client SDK; a real IDA HTTP-supervisor workflow remains unverified. Linux/macOS headless engine operation, other IDA releases, architectures, and modern attached GUI tools require their corresponding real workflows before support is claimed.
|
||||
+107
-25
@@ -36,7 +36,7 @@ Setup continues to pin persistent MCP registrations to the exact version that
|
||||
performed setup. Running current setup later migrates unversioned or older
|
||||
managed registrations through the normal reviewed setup transaction.
|
||||
|
||||
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
|
||||
REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
|
||||
|
||||
Running `npm install rea-agents` without `--global` installs the executable only
|
||||
in the current project's `node_modules/.bin`; it does not make `rea` available
|
||||
@@ -61,6 +61,56 @@ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh |
|
||||
|
||||
Supported options are `--version <semver>`, `--dry-run`, `--no-setup`, `--no-prompt`, and `--verbose`. Neither `--no-prompt` nor a non-interactive shell grants permission to install external dependencies.
|
||||
|
||||
## Released package and main
|
||||
|
||||
Repository main documents its current code and generated catalog. `@latest`
|
||||
selects the npm release, and persistent MCP registrations are pinned to the
|
||||
version that performed setup. Installing newer instructions does not update a
|
||||
running server or its registration.
|
||||
|
||||
The release checked on **2026-10-06** was **4.0.1** (116 MCP tools). That artifact
|
||||
has static JavaScript inspection and full inline Evidence workflows, but does
|
||||
not include main's Windows native control bundle, Android/JADX or firmware
|
||||
tools, Ghidra function annotation workflow, or retained application-Evidence
|
||||
references.
|
||||
Main's catalog describes those additions; a source build or a subsequent
|
||||
release containing them is required. Package startup alone does not verify a
|
||||
provider's real platform workflow.
|
||||
|
||||
To check the published version, run `npm view rea-agents dist-tags.latest`.
|
||||
Use the connected server's actual tool list and advertised input schemas for
|
||||
feature selection. The same package version string in a development checkout
|
||||
does not establish that its bytes match the npm tarball. Update a registration
|
||||
through a reviewed, scoped setup plan, then restart/reconnect the agent.
|
||||
|
||||
## Skill-only installation
|
||||
|
||||
```bash
|
||||
npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
```
|
||||
|
||||
This installs agent instructions and bundled references, not REA MCP
|
||||
registration or analysis engines. Follow the skill's
|
||||
[conditional connection guide](../skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
Working tools can be used immediately. If tools are missing, inspect the current
|
||||
client's registration with `doctor --client codex --json` (substitute its client
|
||||
ID), then plan repairs with `setup --client codex --dry-run --json`. Show and
|
||||
approve the exact changes before applying that scope. An aligned registration
|
||||
with tools absent from the active session needs a restart/reconnection;
|
||||
`doctor` checks files and prerequisites, not the live agent connection.
|
||||
|
||||
Guided setup installs the package's matching skill by default. The skills.sh
|
||||
route can select newer repository instructions, so follow actual server schemas
|
||||
and the release boundary above. Static JavaScript CLI inspection can proceed
|
||||
while MCP is unavailable:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
It returns the complete Evidence record directly and requires no native engine.
|
||||
Provider failures in doctor do not prevent unrelated target-free tools.
|
||||
|
||||
## Supported agents
|
||||
|
||||
Setup can configure these clients for REA's local MCP server:
|
||||
@@ -77,6 +127,7 @@ Setup can configure these clients for REA's local MCP server:
|
||||
| OpenCode | `opencode` |
|
||||
| Antigravity | `antigravity` |
|
||||
| GitHub Copilot CLI | `copilot_cli` |
|
||||
| Command Code | `commandcode` |
|
||||
| VS Code | `vscode` |
|
||||
|
||||
## Review setup changes
|
||||
@@ -212,22 +263,28 @@ copying license secrets into logs, or killing unrelated Hopper processes.
|
||||
|
||||
## Ghidra
|
||||
|
||||
REA connects to an existing Ghidra installation on Linux x64 or macOS x64/arm64.
|
||||
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
|
||||
or experimental Windows x64 P0.
|
||||
It requires Ghidra 12.1.4 and a 64-bit full 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.
|
||||
|
||||
The adapter exposes 22 read-only operations: ten inventory/name/search
|
||||
The adapter exposes 25 read-only operations: thirteen inventory/name/search
|
||||
operations and twelve function-analysis operations. These cover metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, function
|
||||
dossiers, instructions, and recovered data types. GUI controls and annotation
|
||||
changes require Hopper.
|
||||
dossiers, instructions, recovered data types, measured load mappings, loaded
|
||||
memory bytes, and observed file offsets. Independent load-image attestation
|
||||
supports DOS MZ and explicitly selected COM; PE returns its measurements with that limitation.
|
||||
On Linux and macOS, `annotate_native_function` also edits a function name and/or
|
||||
entry comments atomically and returns refreshed analysis. These session metadata
|
||||
edits leave executable bytes unchanged and are discarded on close. GUI controls
|
||||
require Hopper; Windows P0 remains read-only.
|
||||
|
||||
Windows Ghidra operations are currently unavailable. The adapter reports
|
||||
`unsupported_host` until verified Job Object process ownership, private runtime
|
||||
DACLs, and reparse-safe path admission are implemented. The
|
||||
[Windows Ghidra P0 guide](windows-ghidra-p0.md) describes the intended boundary
|
||||
and remaining controls.
|
||||
Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes.
|
||||
The npm package bundles native Job Object ownership, protected private runtime
|
||||
DACLs, and handle-based path admission; no separate addon installation is needed.
|
||||
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
|
||||
limitations.
|
||||
|
||||
Extract Ghidra and install the JDK outside REA, then export absolute paths:
|
||||
|
||||
@@ -238,7 +295,7 @@ rea doctor --json
|
||||
rea setup
|
||||
```
|
||||
|
||||
For Windows diagnostics, use the same installation paths in PowerShell. These settings do not enable the blocked analysis operations:
|
||||
On Windows, configure the existing installation in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
@@ -251,8 +308,8 @@ rea providers --json
|
||||
REA skill after approval. Direct registrations use Node to launch REA's entry
|
||||
script; package-runner registrations use the pinned `npx` command. Hopper
|
||||
installation remains unavailable on Windows. Setup never installs Ghidra,
|
||||
Java, or Python, and agent registration does not enable blocked Ghidra
|
||||
operations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
|
||||
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
|
||||
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
|
||||
|
||||
Doctor validates the platform, architecture, application version,
|
||||
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, Java
|
||||
@@ -267,12 +324,21 @@ 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, but missing
|
||||
native ownership and path controls keep Windows operations unavailable.
|
||||
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. The provider
|
||||
startup deadline fails the open rather than exposing partial analysis. One
|
||||
session contains exactly one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
|
||||
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,
|
||||
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.
|
||||
See [Ghidra first-query deadlines and recovery](mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
|
||||
for client options and the close/reopen recovery flow.
|
||||
|
||||
One session contains exactly one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
|
||||
`REA_ANALYSIS_PROVIDER=ghidra` when both Hopper and Ghidra support the target.
|
||||
One persistent decompiler is owned by the Program, and a serial queue keeps
|
||||
Ghidra API calls on the owning Program thread without a fixed queue length.
|
||||
@@ -281,6 +347,14 @@ is no fixed per-operation or response-size ceiling. Unresolved computed calls
|
||||
remain unknown, reference-kind provenance is preserved, and provider-specific
|
||||
pseudocode is never treated as original source or Hopper-equivalent text.
|
||||
|
||||
Unexpected request failures retain their original internal cause. CLI and MCP
|
||||
errors expose Error names, messages, and available codes under
|
||||
`details.diagnostics.failure_cause`; other rejection values retain their
|
||||
primitive value or an explicit type. Shutdown warnings include the failure
|
||||
kind, message, and diagnostics, while process and temporary-project cleanup
|
||||
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
|
||||
or Mach-O on macOS), plus a native DWARF 4 type-layout object. This lane needs a
|
||||
@@ -295,9 +369,12 @@ host-native Ghidra acceptance lane.
|
||||
On a controlled Windows x64 runner, use
|
||||
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
|
||||
PE fixture from source bytes and requires the Windows native authority before
|
||||
opening the provider. This lane remains blocked until those controls are
|
||||
implemented; its intended checks include operation coverage, digest identity,
|
||||
and complete runtime cleanup.
|
||||
opening the provider. The controls are implemented on main; see the
|
||||
[release boundary](#released-package-and-main). The lane verifies all 25 admitted
|
||||
read-only operations, digest identity, and runtime cleanup. The separate
|
||||
`verify:ghidra:windows:package` lane checks the packed CLI/MCP boundary with an
|
||||
ordinary user. It requires the built Windows native bundle and an existing
|
||||
Ghidra/JDK installation; neither is inferred from startup alone.
|
||||
|
||||
## Diagnose, update, and remove
|
||||
|
||||
@@ -315,17 +392,22 @@ server through `npx` with the `mcp` command.
|
||||
|
||||
For a client that requires manual configuration, use:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@latest", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.1.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
|
||||
configuration is required. `rea setup` writes the same package-runner shape and
|
||||
pins it to the exact version that performed setup.
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Persistent registrations should use one exact package version. `rea setup`
|
||||
writes the same package-runner shape and pins it to the exact version that
|
||||
performed setup. Run current setup to refresh an older registration, then
|
||||
restart the client.
|
||||
@@ -139,9 +139,34 @@ available browser, Electron, or process workflows.
|
||||
|
||||
## CLI and verification
|
||||
|
||||
All five CLI commands accept inline JSON or a path to a JSON file. Put the full
|
||||
Evidence records in the workflow input, as in the corresponding MCP tool; the
|
||||
CLI does not resolve Evidence IDs from a separate bundle.
|
||||
All five CLI commands accept inline JSON or a path to a JSON file. The CLI
|
||||
returns an Evidence record directly. Put the full records in a later CLI input;
|
||||
a separate CLI process has no retained MCP connection state.
|
||||
|
||||
For a literal string trace, analyze your supplied tree once, then build the
|
||||
input from the saved Evidence (replace the target and seed):
|
||||
|
||||
```bash
|
||||
rea analyze-javascript-application /absolute/path/to/app --json > application-evidence.json
|
||||
node --input-type=module -e '
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
const application = JSON.parse(readFileSync("application-evidence.json", "utf8"));
|
||||
writeFileSync("trace-input.json", JSON.stringify({
|
||||
application, seed: { kind: "string", value: "search-result" }, direction: "both"
|
||||
}));'
|
||||
rea trace-application-feature ./trace-input.json --json
|
||||
```
|
||||
|
||||
MCP analysis returns an envelope containing `result`, `evidence_id`, and full
|
||||
`evidence`. If the connected server advertises retained references, reuse its
|
||||
exact returned ID as `{"kind":"retained-evidence","evidence_id":"RETURNED_ID"}`
|
||||
in `application`, or `left`/`right` for comparisons. `RETURNED_ID` is a template,
|
||||
not a literal valid ID. Native Evidence arrays still use complete records.
|
||||
References are scoped to one connection; `close_binary` clears them. Export a
|
||||
bundle before closing and import it on another connection, or supply the full
|
||||
inline Evidence there. npm 4.0.1 accepts full inline Evidence only; installing
|
||||
newer skill instructions does not change that schema. See
|
||||
[MCP Evidence inputs](mcp-contracts.md#retained-application-evidence-inputs).
|
||||
|
||||
For two operator-provided directories or ASARs, run:
|
||||
|
||||
|
||||
@@ -52,6 +52,23 @@ manifest and graph commitments, JavaScript Application Graph, static
|
||||
Electron summary, reconstruction statistics, and explicit limitations. It does
|
||||
not require a live Hopper, Ghidra, browser, or Electron process.
|
||||
|
||||
## Large results
|
||||
|
||||
Graph and Evidence identifiers hash canonical JSON incrementally, without
|
||||
assembling a single string for the whole graph. The canonical bytes and existing
|
||||
identifiers remain unchanged. The opt-in regression check is
|
||||
`npm run verify:javascript:digests`;
|
||||
it hashes a value larger than the running Node engine's single-string limit.
|
||||
|
||||
Output formatters and MCP transport serialization still assemble whole strings.
|
||||
For a large CLI result, select the needed fields
|
||||
before formatting, for example `--format json --filter-output
|
||||
evidence_id,normalized_result.statistics`. Complete serialization of a result
|
||||
beyond the engine's string limit is not established by this digest check.
|
||||
Client framing limits also apply: the pinned Node MCP SDK's stdio transport
|
||||
defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate
|
||||
the complete response, including text and structured Evidence projections.
|
||||
|
||||
## What is reconstructed
|
||||
|
||||
The projector reuses the content-addressed artifact inventory and safe artifact
|
||||
|
||||
@@ -18,6 +18,24 @@ rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
|
||||
--runtime-kind node --json
|
||||
```
|
||||
|
||||
## Start a caller-owned Node target
|
||||
|
||||
Run your selected application with Inspector bound to loopback:
|
||||
|
||||
```bash
|
||||
node --inspect=127.0.0.1:9229 ./app.mjs
|
||||
```
|
||||
|
||||
Keep it running while listing targets and observing the selected ID. Use a free
|
||||
port and the same literal endpoint in both REA calls. Stop the application
|
||||
through its normal lifecycle when finished; REA disconnects but does not own or
|
||||
terminate it. Node.js must already be installed.
|
||||
|
||||
Use `--inspect` for this passive workflow. `--inspect-brk` pauses before the
|
||||
application starts, and REA never resumes it or sends
|
||||
`Runtime.runIfWaitingForDebugger`. Resume a paused target with a separate
|
||||
caller-owned debugger before requesting application runtime observations.
|
||||
|
||||
The caller supplies the literal-loopback HTTP endpoint directly. Selecting an endpoint exposes all targets it serves and their local script locations or renderer origins. A target must match the exact requested target ID and expose a same-port WebSocket reported by that endpoint. Targets already marked attached are rejected rather than displacing another debugger. Local paths are canonicalized after symlink resolution.
|
||||
|
||||
`list_javascript_runtime_targets` returns every available target in one inline
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Managed-code analysis plan
|
||||
# Managed-code analysis and planned extensions
|
||||
|
||||
REA inspects .NET PE/CLI artifacts without loading or executing their code.
|
||||
It can identify an assembly, inspect metadata and CIL, compare members across
|
||||
@@ -27,6 +27,20 @@ This guide describes the implementation and verification of
|
||||
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md). The canonical
|
||||
tool inventory is [`product-catalog.json`](product-catalog.json).
|
||||
|
||||
## Shipped scope
|
||||
|
||||
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
|
||||
an assembly. It reports observed implementation markers and unavailable facts;
|
||||
it does not unpack .NET single-file hosts, decode IL2CPP metadata, or infer a
|
||||
NativeAOT identity from an ordinary native PE. Inputs without admitted CLI
|
||||
metadata do not become managed assemblies through naming or routing guesses.
|
||||
Inspect separately obtained components explicitly and preserve their identities.
|
||||
|
||||
The broader deployment classification and native-body mapping below are design
|
||||
goals from ADR-0003, not claims that every row has an implemented parser. A
|
||||
valid marker can establish a candidate native boundary, not recovered native
|
||||
semantics or runtime behavior.
|
||||
|
||||
## Analysis objective
|
||||
|
||||
REA's managed-code track is intended to answer five different questions without
|
||||
@@ -43,9 +57,9 @@ collapsing them:
|
||||
The ordinary workflow ends at question four. Static analysis never loads or
|
||||
executes the target.
|
||||
|
||||
## Classification workflow
|
||||
## Planned deployment classification
|
||||
|
||||
Classification proceeds from the outermost authenticated bytes inward:
|
||||
The intended extended classifier proceeds from outermost authenticated bytes inward:
|
||||
|
||||
```text
|
||||
source path
|
||||
@@ -58,7 +72,7 @@ source path
|
||||
-> managed-only, native-only, composed, degraded, or unsupported route
|
||||
```
|
||||
|
||||
The result is a vector rather than one label. For example, a single-file modern
|
||||
The planned result is a vector rather than one label. For example, a single-file modern
|
||||
.NET deployment can contain a native host, ordinary CIL assemblies, and
|
||||
ReadyToRun components. Each component receives its own digest, classification,
|
||||
coverage, and route while retaining the outer bundle commitment.
|
||||
@@ -257,17 +271,20 @@ or server-side behavior.
|
||||
## Tool and packaging boundary
|
||||
|
||||
The production parser is REA-owned TypeScript and ships with the existing Node
|
||||
application. It is verified against the ECMA-335 format and independent pinned
|
||||
oracles but has no runtime dependency on them.
|
||||
application. Deterministic conformance uses source-owned byte-built PE/CLI
|
||||
fixtures and expected semantic facts. An optional real ILSpy lane runs only when
|
||||
its executable is explicitly supplied; the default lane does not establish
|
||||
independent pinned-oracle parity. Production inspection needs none of those tools.
|
||||
|
||||
- `System.Reflection.Metadata` is the primary independent metadata/CIL oracle.
|
||||
- `ICSharpCode.Decompiler` and `ilspycmd` are reconstruction and differential
|
||||
oracles. An admitted BYO reconstruction operation records its exact version
|
||||
- A pinned `System.Reflection.Metadata` differential oracle is planned; it is
|
||||
not part of the current verifier.
|
||||
- `ICSharpCode.Decompiler`/`ilspycmd` can supply reconstruction inference.
|
||||
A BYO reconstruction import records the supplied version
|
||||
and remains non-canonical. When `REA_ILSPY_CMD_PATH` points to an absolute
|
||||
runnable `ilspycmd`, `verify:managed` runs a source-owned real ILSpy oracle
|
||||
and imports its C# output as reconstruction inference against exact static
|
||||
member Evidence.
|
||||
- dnlib and Mono.Cecil may increase differential coverage. Their mutation APIs
|
||||
- dnlib and Mono.Cecil are potential future differential oracles. Their mutation APIs
|
||||
are not exposed or included in the production parsing boundary.
|
||||
- The package contains no .NET runtime, SDK, ILSpy installation, proprietary
|
||||
assembly, or compiled conformance fixture.
|
||||
@@ -288,6 +305,11 @@ by the selected Hopper/Ghidra host and format matrix.
|
||||
|
||||
## Source-built conformance corpus
|
||||
|
||||
The current lane uses source-owned byte-built fixtures and malformed-input
|
||||
regressions, with optional operator-supplied applications and ILSpy verification.
|
||||
The table below describes the desired expanded corpus; it is not a report that
|
||||
all compiler-generated forms have been independently verified.
|
||||
|
||||
Fixture sources are intentionally small and behavior-focused. Build outputs
|
||||
are generated outside tracked fixture directories and must not remain after
|
||||
verification or packaging.
|
||||
@@ -437,3 +459,11 @@ The managed-code track advances as reviewable pull requests:
|
||||
|
||||
Each implementation PR updates generated product facts only for behavior it
|
||||
actually ships and states which real-tool checks were performed.
|
||||
|
||||
### Method metadata during build comparison
|
||||
|
||||
Managed member comparisons report a `metadata` dimension when an observed
|
||||
MethodDef flags or implementation flags value differs, including accessibility
|
||||
or synchronization changes. This is separate from the CIL/signature matching
|
||||
tiers: identical instructions do not establish unchanged method metadata. The
|
||||
reported difference is static metadata evidence, not proof of runtime behavior.
|
||||
+120
-7
@@ -8,15 +8,19 @@ registrations, reports their command vectors as aligned, stale, missing, or
|
||||
invalid, and keeps live-server state `unknown` unless the active connection
|
||||
supplies identity.
|
||||
|
||||
Canonical tool names remain stable, while `tools/list` advertises only the
|
||||
operations callable for the current target, provider, host, and
|
||||
negotiated client capabilities. `binary_session.tool_availability` remains the
|
||||
complete inventory: it explains advertised and hidden operations with stable
|
||||
availability reasons and remediation. Each entry also reports required and
|
||||
optional negotiated client features plus the currently missing features.
|
||||
Opening or closing a target or observing a provider health transition emits
|
||||
`tools/list` returns the complete canonical tool inventory, including tools that
|
||||
are currently unavailable. Opening or closing a target or observing a provider
|
||||
health transition leaves that catalog unchanged and does not emit
|
||||
`notifications/tools/list_changed`.
|
||||
|
||||
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
|
||||
callable operation for the current target, provider, host, and negotiated client
|
||||
capabilities. The default result includes the complete inventory with each
|
||||
tool's availability, reason, and remediation. Each entry also reports required
|
||||
and optional negotiated client features plus the currently missing features.
|
||||
The optional inputs `expected_package_version`, `expected_catalog_digest`, and
|
||||
`expected_server_path` compare the live session with the caller's expectations.
|
||||
|
||||
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
|
||||
discovery. Target-free discovery is sorted by provider ID, reports host
|
||||
availability and `unknown` target support, and does not create an analysis
|
||||
@@ -53,6 +57,74 @@ 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.
|
||||
|
||||
## Ghidra first-query deadlines and recovery
|
||||
|
||||
A successful MCP initialize handshake establishes the REA connection.
|
||||
`open_binary` then selects a target and provider binding; it does not establish
|
||||
that Ghidra has finished importing the target. The first Ghidra-backed query,
|
||||
such as `binary_overview`, starts the engine and waits for import, default
|
||||
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. |
|
||||
|
||||
For an already connected client using the pinned SDK, request options are the
|
||||
**second** argument of `callTool`:
|
||||
|
||||
```js
|
||||
const overview = await client.callTool(
|
||||
{ name: "binary_overview", arguments: {} },
|
||||
{
|
||||
timeout: 240000,
|
||||
onprogress: ({ progress, total, message }) => {
|
||||
console.error({ progress, total, message });
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
The SDK's `onprogress` option supplies a progress token and handles matching
|
||||
notifications. A raw MCP client can instead send `_meta.progressToken` in the
|
||||
request and handle `notifications/progress`. Progress reports do not establish
|
||||
engine readiness or a percentage of Ghidra auto-analysis. A longer server
|
||||
startup allowance does not extend the client's deadline; progress also does not
|
||||
extend it unless the client explicitly implements that policy.
|
||||
|
||||
The 240-second setting is a measured example, not a universal timeout: a
|
||||
controlled Linux x64/WSL2 fixture took about 64 seconds for its first overview
|
||||
with Ghidra 12.1.4 on a mounted NTFS installation, native JDK 21, two CPUs, and a
|
||||
512 MiB Java heap. Target size, storage, and analysis work can change that time.
|
||||
Configure the deadline in the caller's existing request settings; it is not a
|
||||
tool argument, setup grant, or server-side automatic extension. CLI analysis has
|
||||
no MCP client request deadline, but keeps the provider startup deadline and
|
||||
SIGINT cancellation.
|
||||
|
||||
If the client timed out or cancelled during startup, the pending query may have
|
||||
been interrupted. Reissuing `open_binary` for the same active target can reuse
|
||||
the current client, so it is not a fresh-start recovery. On the same connection:
|
||||
|
||||
1. Keep any useful inline Evidence, or export a bundle before closing if retained
|
||||
records are needed. `binary_session` can show the selected binding and its
|
||||
recorded state; target selection alone does not prove engine readiness.
|
||||
2. Call `close_binary` and check its result. It drains owned work, closes the
|
||||
provider, and clears retained session records. A `cleanup_incomplete` result
|
||||
must be addressed according to its reported owned resources before retrying.
|
||||
3. Call `open_binary` with the same caller-selected path and
|
||||
`provider_id: "ghidra"`, then retry the first query with an appropriate client
|
||||
deadline. The connection and selected provider do not need to change.
|
||||
|
||||
This close/reopen flow was exercised on one real Linux stdio connection after a
|
||||
controlled startup timeout, followed by a successful overview and function
|
||||
analysis. It is not a Windows/macOS coverage claim. REA cleans only resources it
|
||||
owns and never switches to another provider automatically. A provider timeout,
|
||||
installation failure, or host permission denial needs its own reported recovery;
|
||||
increasing a client deadline alone does not fix those failures.
|
||||
|
||||
## Tool results
|
||||
|
||||
Evidence-producing tools return `{ result, evidence_id, evidence }` in both
|
||||
@@ -64,6 +136,47 @@ directly to `compare_functions`, and `inspect_artifact` Evidence to
|
||||
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
|
||||
retained session history or an explicit bundle for transfer.
|
||||
|
||||
## Retained application Evidence inputs
|
||||
|
||||
`trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes` accept complete inline application Evidence
|
||||
or an exact reference to a record already retained by the current connection:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "trace_application_feature",
|
||||
"arguments": {
|
||||
"application": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "ev_<64 lowercase hex characters>"
|
||||
},
|
||||
"seed": { "kind": "module", "value": "search.js", "match": "exact" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use the `evidence_id` returned by `analyze_javascript_application` (or another
|
||||
compatible application-graph producer). Comparisons accept this form in `left`
|
||||
and `right`; each side can independently be inline or retained. Native
|
||||
observation arrays continue to take complete inline Evidence. Results and their
|
||||
Evidence remain complete inline, and both input forms pass the same semantic,
|
||||
identity, authority, and provenance checks without running the producer again.
|
||||
|
||||
References belong to the current connection's Evidence ledger. Opening another
|
||||
target preserves retained records; `close_binary` clears them, even when no
|
||||
binary is active. A fresh connection has its own ledger. A missing reference
|
||||
reports its exact ID and `details.reason: "missing"`; the server cannot infer
|
||||
whether it was never recorded, cleared, or retained by another connection.
|
||||
Supply complete inline Evidence, repeat its producer, or import an exported
|
||||
Evidence bundle before referencing that imported record. Export a bundle before
|
||||
closing if the investigation needs it later.
|
||||
|
||||
CLI application workflows continue to read portable inline Evidence from files
|
||||
and run the same analysis workflows; a standalone CLI invocation cannot resolve
|
||||
another MCP connection's retained records. No additional lookup call, provider
|
||||
selection, or approval step is required for a same-session follow-up.
|
||||
|
||||
## Aggregate native context
|
||||
|
||||
`get_navigation_context` composes the selected document, current address, and
|
||||
|
||||
@@ -25,6 +25,13 @@ Tool results include their Evidence inline. Prompts do not require a bundle or
|
||||
resource fetch to read a result; an Evidence ID is only a reference when a
|
||||
prompt argument or tool explicitly accepts one.
|
||||
|
||||
For a JavaScript/Electron application directory, `investigate_feature` points
|
||||
directly to `analyze_javascript_application` with `input_path`; it does not
|
||||
require `open_binary`. The CLI equivalent is `rea analyze <directory>`.
|
||||
`open_binary` and the doctor target check admit files and macOS app bundles.
|
||||
A directory outside that opening route is reported with an available analysis
|
||||
action; this does not establish support for every directory format.
|
||||
|
||||
For target paths in `compare_application_versions`, the prompt suggests
|
||||
opening each target before calling `inspect_artifact`; inspection operates on
|
||||
the active target and returns the graph and findings together inline.
|
||||
|
||||
@@ -146,12 +146,14 @@ actions may change application data or trigger network activity.
|
||||
|
||||
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
|
||||
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
|
||||
the matching native decompiler. Windows Ghidra analysis is unavailable until
|
||||
Job Object process ownership, private runtime DACLs, and reparse-safe path checks
|
||||
are implemented and verified. See [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
the matching native decompiler. Experimental Windows x64 P0 admits native
|
||||
x86-64 PE applications on local NTFS using bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
Ghidra has no GUI or mutation authority, and REA never falls back automatically
|
||||
to Hopper.
|
||||
On Linux and macOS, `annotate_native_function` atomically edits a function name
|
||||
and entry comments in the ephemeral database, returning refreshed analysis
|
||||
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
|
||||
GUI authority, and REA never falls back automatically to Hopper.
|
||||
|
||||
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
|
||||
@@ -17,9 +17,12 @@ recorded, so its influence may remain unknown.
|
||||
Write a JSON scenario and pass its path to the CLI:
|
||||
|
||||
```sh
|
||||
rea capture-process ./scenario.json > capture.json
|
||||
rea capture-process ./scenario.json --json > capture.json
|
||||
```
|
||||
|
||||
`--json` is required when saving input for JSON consumers; the default terminal
|
||||
format is TOON. The file contains the complete capture Evidence record.
|
||||
|
||||
For example:
|
||||
|
||||
```json
|
||||
|
||||
+98
-14
@@ -1,24 +1,25 @@
|
||||
{
|
||||
"package": {
|
||||
"name": "rea-agents",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"sdk": {
|
||||
"server": "2.0.0",
|
||||
"client": "2.0.0",
|
||||
"core": "2.0.0"
|
||||
"server": "2.3.1",
|
||||
"client": "2.3.1",
|
||||
"core": "2.3.1"
|
||||
},
|
||||
"skill_version": "24"
|
||||
"skill_version": "26"
|
||||
},
|
||||
"tools": {
|
||||
"total": 116,
|
||||
"total": 125,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
"surface": "official-proxy",
|
||||
"count": 39,
|
||||
"count": 41,
|
||||
"tools": [
|
||||
"address_name",
|
||||
"address_to_file_offset",
|
||||
"annotate_native_function",
|
||||
"comment",
|
||||
"current_address",
|
||||
"current_document",
|
||||
@@ -27,6 +28,7 @@
|
||||
"inline_comment",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"inspect_native_load_image",
|
||||
"list_bookmarks",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
@@ -119,6 +121,24 @@
|
||||
"verify_managed_native_boundaries"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "firmware",
|
||||
"surface": "firmware-provider",
|
||||
"count": 2,
|
||||
"tools": ["extract_firmware", "inspect_firmware_regions"]
|
||||
},
|
||||
{
|
||||
"id": "android",
|
||||
"surface": "android-provider",
|
||||
"count": 5,
|
||||
"tools": [
|
||||
"inspect_android_class",
|
||||
"inspect_android_method",
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"trace_android_references"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "browser",
|
||||
"surface": "browser-provider",
|
||||
@@ -201,15 +221,24 @@
|
||||
]
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"id": "binwalk",
|
||||
"name": "Binwalk",
|
||||
"version": "3.1.0",
|
||||
"capabilities": ["inspect_firmware_regions"]
|
||||
},
|
||||
{
|
||||
"id": "ghidra",
|
||||
"name": "Ghidra",
|
||||
"version": null,
|
||||
"capabilities": [
|
||||
"address_name",
|
||||
"address_to_file_offset",
|
||||
"analyze_function",
|
||||
"annotate_native_function",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"inspect_native_load_image",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
"list_procedures",
|
||||
@@ -222,6 +251,7 @@
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"procedure_references",
|
||||
"read_bytes",
|
||||
"read_function_instructions",
|
||||
"resolve_containing_procedure",
|
||||
"resolve_native_call_targets",
|
||||
@@ -274,6 +304,37 @@
|
||||
"xrefs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ida",
|
||||
"name": "IDA Pro MCP adapter",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"analyze_function",
|
||||
"list_procedures",
|
||||
"list_strings",
|
||||
"procedure_address",
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"procedure_pseudo_code",
|
||||
"read_function_instructions",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"xrefs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "jadx",
|
||||
"name": "JADX headless",
|
||||
"version": "0.7.1",
|
||||
"capabilities": [
|
||||
"inspect_android_class",
|
||||
"inspect_android_method",
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"trace_android_references"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "native-macos",
|
||||
"name": "macOS native inspection utilities",
|
||||
@@ -388,6 +449,12 @@
|
||||
"list_javascript_runtime_targets",
|
||||
"observe_javascript_runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "unblob",
|
||||
"name": "Unblob",
|
||||
"version": "26.6.4",
|
||||
"capabilities": ["extract_firmware"]
|
||||
}
|
||||
],
|
||||
"setup_clients": [
|
||||
@@ -451,6 +518,12 @@
|
||||
"format": "copilot_cli",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "commandcode",
|
||||
"display_name": "Command Code",
|
||||
"format": "commandcode",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "vscode",
|
||||
"display_name": "VS Code",
|
||||
@@ -459,11 +532,13 @@
|
||||
}
|
||||
],
|
||||
"cli": {
|
||||
"primary_count": 69,
|
||||
"primary_count": 80,
|
||||
"commands": [
|
||||
"address-to-file-offset",
|
||||
"analyze",
|
||||
"analyze-javascript-application",
|
||||
"analyze-web-bundle",
|
||||
"annotate-native-function",
|
||||
"build-reconstruction-obligation-ledger",
|
||||
"capabilities",
|
||||
"capture-browser-scenario",
|
||||
@@ -488,13 +563,18 @@
|
||||
"evidence-export",
|
||||
"evidence-import",
|
||||
"extract-artifact",
|
||||
"extract-firmware",
|
||||
"function",
|
||||
"import-managed-reconstruction",
|
||||
"import-reference-source",
|
||||
"inspect",
|
||||
"inspect-android-class",
|
||||
"inspect-android-method",
|
||||
"inspect-android-package",
|
||||
"inspect-artifact",
|
||||
"inspect-asset-catalog",
|
||||
"inspect-electron-page",
|
||||
"inspect-firmware-regions",
|
||||
"inspect-keyed-archive",
|
||||
"inspect-macho",
|
||||
"inspect-managed-artifact",
|
||||
@@ -504,6 +584,7 @@
|
||||
"inspect-native-data-type",
|
||||
"inspect-native-dispatch-metadata",
|
||||
"inspect-native-instruction",
|
||||
"inspect-native-load-image",
|
||||
"inspect-plist",
|
||||
"inspect-signature",
|
||||
"inspect-web-page",
|
||||
@@ -517,11 +598,14 @@
|
||||
"observe-web-session",
|
||||
"project-managed-application-graph",
|
||||
"providers",
|
||||
"read-bytes",
|
||||
"reconcile-javascript-runtime",
|
||||
"resolve-native-call-targets",
|
||||
"search",
|
||||
"search-android-classes",
|
||||
"setup",
|
||||
"trace",
|
||||
"trace-android-references",
|
||||
"trace-application-feature",
|
||||
"trace-javascript-semantics",
|
||||
"trace-native-ui-action",
|
||||
@@ -540,15 +624,15 @@
|
||||
},
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 69,
|
||||
"mcp_tools": 116,
|
||||
"cli_commands": 80,
|
||||
"mcp_tools": 125,
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "7849b6f39bed200cfbf5f4d09ba3f9e0661824efb5753ae2edde8ef2fb66535e",
|
||||
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
|
||||
"combined_sha256": "5bb3f994a3c4d0c87a34ff5aab4e28141b5fa6f3a98b5ae7e95686d85d9d983e",
|
||||
"providers_sha256": "2ae8b85b20be338739e500549d275e78e875aaa10235d2ca283f0ade7787da90"
|
||||
"tools_sha256": "eec3abb8fa5b25c6e5d6e361b7ebd157e3599f69b68defda85680dbfe061b3d3",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "ed9c36900e9223561d38ba30a44921f4ff101d5eca989920f9edc9dc91df470b",
|
||||
"providers_sha256": "044634314b36b966bd918f99f116f4684108556ac1dabc912e76ccd351d97e3e"
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-30
@@ -1,21 +1,27 @@
|
||||
# Static-analysis provider evaluation
|
||||
|
||||
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.
|
||||
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
|
||||
use them. macOS installations need the matching native decompiler.
|
||||
|
||||
REA imports one target into a temporary project and exposes 22 read-only
|
||||
REA imports one target into a temporary project and exposes 25 read-only
|
||||
operations after analysis completes. They cover inventories, search,
|
||||
decompilation, assembly, function metadata, resolved calls, references, control
|
||||
flow, instructions, and recovered types. Results identify the provider version,
|
||||
analysis settings, target digest, and any unavailable or incomplete facts.
|
||||
Ghidra does not expose Hopper's GUI or annotation operations.
|
||||
On Linux and macOS, `annotate_native_function` additionally edits names and entry
|
||||
comments atomically, verifies readback, and returns refreshed function analysis.
|
||||
Changes live only in the session database and leave executable bytes unchanged.
|
||||
Ghidra does not expose GUI controls; Windows P0 remains read-only.
|
||||
|
||||
Windows Ghidra analysis is unavailable, including on a correctly configured
|
||||
Ghidra/JDK host. Job Object process ownership, private runtime DACLs, and
|
||||
reparse-safe path checks are not implemented. Package and adapter tests do not
|
||||
establish real Windows analysis support. See [Windows Ghidra P0](windows-ghidra-p0.md)
|
||||
and [issue #527](https://github.com/morluto/rea/issues/527).
|
||||
Windows x64 P0 supports native x86-64 PE applications on local NTFS with
|
||||
bundled Job Object ownership, protected runtime DACLs, and handle-based path
|
||||
admission. Real ordinary-user verification covers all 25 read-only operations
|
||||
through the packaged CLI and MCP, target integrity, and cleanup. This remains
|
||||
an experimental boundary; see [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
|
||||
[ADR-0001](adr/0001-provider-selection-and-analysis-profiles.md) fixes the
|
||||
provider registry, deterministic selection, target binding, analysis profile,
|
||||
@@ -51,8 +57,9 @@ follow.
|
||||
optional `JAVA_HOME` must identify a 64-bit full JDK 21, otherwise doctor probes
|
||||
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
|
||||
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
|
||||
and target compatibility are separate checks. The intended Windows P0 accepts
|
||||
only native x86-64 PE applications; it remains blocked before launch.
|
||||
and target compatibility are separate checks. Windows x64 P0 admits only native,
|
||||
non-managed, non-DLL x86-64 PE applications on fixed local NTFS, with the packaged
|
||||
native authority available before launch.
|
||||
|
||||
The launcher creates one ephemeral runtime root with project,
|
||||
home/cache/config/data/temp, logs, descriptor, endpoint, target snapshot, and
|
||||
@@ -63,18 +70,21 @@ On Linux, the mode-0600 descriptor carries the random token without
|
||||
exposing it in argv or environment and the Java bridge binds a mode-restricted
|
||||
Unix socket. macOS uses the same local transport. The bridge reports actual
|
||||
Ghidra/language/compiler/analysis/import-digest metadata and accepts only
|
||||
authenticated `ping`, `shutdown`, ten inventory methods, and twelve
|
||||
function-analysis methods. Close, cancellation, timeout, malformed protocol,
|
||||
authenticated `ping`, `shutdown`, thirteen read-only inventory/load-image methods,
|
||||
and twelve function-analysis methods. Linux/macOS also admits atomic function
|
||||
annotation changes in the ephemeral database. Close, cancellation, timeout, malformed protocol,
|
||||
or process exit stops the owned process resources, closes the socket, and
|
||||
removes the runtime root.
|
||||
|
||||
The experimental Windows transport uses authenticated IPv4 loopback with a
|
||||
token-free endpoint record, but cannot start while the native authority is
|
||||
unavailable. Its `taskkill` cleanup code does not establish Job Object ownership,
|
||||
private DACLs, or reparse-safe paths.
|
||||
token-free endpoint record. The bundled native authority establishes Job Object
|
||||
ownership, protected runtime DACLs, and handle-based path admission before
|
||||
launch. If the bundle or an admission constraint is unavailable, the provider
|
||||
remains unavailable; a `taskkill` fallback does not establish that authority.
|
||||
|
||||
The provider catalog lists the 22 Ghidra operations. GUI cursor,
|
||||
navigation, and mutation operations remain absent; the router therefore
|
||||
The provider catalog lists 25 read-only Ghidra operations and the Linux/macOS
|
||||
session annotation operation. GUI cursor/navigation and persistent mutations
|
||||
remain unavailable; Windows P0 admits no database mutation. The router therefore
|
||||
reports them unavailable instead of borrowing Hopper semantics or inferring
|
||||
capability from a successful import.
|
||||
|
||||
@@ -122,12 +132,14 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
|
||||
these tools before compilation. Keeping this matrix separate lets Linux
|
||||
host/provider acceptance run with only the host compiler.
|
||||
|
||||
`npm run verify:ghidra:windows` remains blocked by the missing Windows native
|
||||
authority. After those controls are implemented, the lane must check the
|
||||
source-owned native x86-64 PE fixture, all read-only operations,
|
||||
target/snapshot/import SHA-256 linkage, and project, endpoint, process, and
|
||||
runtime cleanup. The existence of this verifier is not evidence that Windows
|
||||
Ghidra analysis works.
|
||||
`npm run verify:ghidra:windows` checks the source-owned native x86-64 PE
|
||||
fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
|
||||
and project, endpoint, process, and runtime cleanup. The independent native
|
||||
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
|
||||
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
|
||||
prefix, then checks ordinary-user CLI/MCP operations against canonical contracts.
|
||||
All lanes require matching native controls; package startup alone establishes
|
||||
only package compatibility.
|
||||
|
||||
## Shared provider-process foundation
|
||||
|
||||
@@ -138,8 +150,8 @@ request cancellation and lifecycle-deadline cleanup, bounded stdout and stderr r
|
||||
exact byte counts, process-exit diagnostics, and bounded TERM-to-KILL shutdown.
|
||||
Reusable fixtures exercise exit, timeout, cancellation, graceful termination,
|
||||
forced termination, double-close, spawn failure, and resource release. Windows
|
||||
process-tree termination code exists, but the missing Job Object and private
|
||||
runtime controls prevent it from establishing an available Ghidra session.
|
||||
Ghidra uses its native Job Object, private runtime, and path-admission boundary;
|
||||
generic process-tree termination alone cannot establish provider availability.
|
||||
|
||||
The foundation does not define a bridge schema, socket framing, health payload,
|
||||
analysis model, or shutdown acknowledgement. Hopper keeps its authenticated
|
||||
@@ -149,12 +161,12 @@ only the generic process mechanisms.
|
||||
|
||||
## Shortlist
|
||||
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 22 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Windows analysis is blocked; GUI and mutation operations are unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
|
||||
## Recommended order
|
||||
|
||||
|
||||
+14
-13
@@ -43,12 +43,14 @@ 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, and VS Code using each client's configuration format.
|
||||
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
|
||||
|
||||
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.4 and a
|
||||
64-bit full JDK 21. macOS also requires the matching native decompiler. The
|
||||
adapter exposes ten inventory operations and twelve function-analysis
|
||||
operations, for 22 read-only operations total. Approved setup saves verified
|
||||
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
|
||||
is discarded on close and executable bytes stay unchanged. Approved setup saves verified
|
||||
installation paths in agent configurations without installing or changing
|
||||
Ghidra or Java.
|
||||
|
||||
@@ -57,11 +59,11 @@ a native type-layout object. A separate lane covers AArch64 ELF, PE, and
|
||||
Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites
|
||||
and exact commands.
|
||||
|
||||
Windows Ghidra operations remain unavailable until REA implements verified
|
||||
Job Object process ownership, private runtime DACLs, and reparse-safe path
|
||||
admission. Windows package and adapter tests do not establish a usable real
|
||||
Ghidra session. The [Windows P0 guide](windows-ghidra-p0.md) describes the
|
||||
intended boundary and remaining requirements.
|
||||
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based admission for native x86-64 PE
|
||||
applications on local NTFS. Real ordinary-user CLI/MCP verification covers
|
||||
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
|
||||
describes this boundary and unverified broader coverage.
|
||||
|
||||
## Ghidra maintenance boundary
|
||||
|
||||
@@ -71,11 +73,10 @@ not expected to match; unresolved targetless flow remains unknown. Automatic
|
||||
Ghidra acquisition, if ever added, remains a separately planned and approved
|
||||
related-tool change; setup must never install Java.
|
||||
|
||||
Before enabling Windows Ghidra operations, add current-user-only DACL creation and
|
||||
readback, handle-based reparse-point-safe path authority, a DACL-protected IPC
|
||||
backend, and Job Object assignment before provider execution. Process capture,
|
||||
Hopper, and broad filesystem-sensitive workflows remain separate Windows
|
||||
projects rather than implied parity.
|
||||
Maintain Windows P0 with independent DACL readback, handle-based admission,
|
||||
private authenticated IPC, and Job Object assignment before provider execution.
|
||||
Process capture, Hopper, and broad filesystem-sensitive workflows remain
|
||||
separate Windows projects; P0 does not imply their parity.
|
||||
|
||||
## Capability-selective setup
|
||||
|
||||
|
||||
+108
-6
@@ -47,7 +47,7 @@ The process-global Vitest configuration contract rejects new direct temporary-ro
|
||||
creation outside the workspace seam and its narrowly documented boundary/package
|
||||
exceptions.
|
||||
|
||||
Real Hopper, Ghidra, browser, package, and managed-code claims belong to their
|
||||
Real Hopper, Ghidra, IDA, browser, package, and managed-code claims belong to their
|
||||
explicit `npm run verify:*` lanes. The reconstruction-readiness lane also
|
||||
checks deterministic rerun, tamper, and stale-input handling; those checks do
|
||||
not execute extracted JavaScript modules. When application runtime behavior is
|
||||
@@ -103,8 +103,15 @@ that a host or target is covered when it was skipped.
|
||||
| `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 | Ghidra 12.1.4, full JDK 21, and the Windows P0 fixture toolchain |
|
||||
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
|
||||
|
||||
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.
|
||||
`npm run verify:ghidra:windows:package` additionally packs and installs REA into
|
||||
an isolated prefix and runs ordinary-user CLI/MCP operations against their
|
||||
canonical schemas. Run the controlled fixture generator before that lane, or
|
||||
supply an installed package root and an explicit fixture as arguments.
|
||||
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
|
||||
@@ -141,6 +148,84 @@ the CLI and stdio MCP, including special filenames, unresolved discovery
|
||||
locations, and independently resolved loaded scripts. Double-quote filenames
|
||||
are tested on POSIX only because Windows does not support them.
|
||||
|
||||
## Android APK analysis
|
||||
|
||||
`npm run verify:android` requires an existing Java 17+ and an explicit
|
||||
`REA_JADX_MCP_JAR` for jadx-headless-mcp 0.7.1. Set `REA_ANDROID_TEST_APK` to the
|
||||
fixed public ApiDemos v6.0.18 fixture. Obtain both with the explicit
|
||||
`npm run fixtures:android` command; files are SHA-256 verified and
|
||||
kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or
|
||||
application execution is required. The lane compares real CLI/MCP package,
|
||||
class search, class inventory, method decompilation and incoming references.
|
||||
See [Android analysis](android-analysis.md) for boundaries and resource budgets.
|
||||
|
||||
Synthetic producer regressions run independently:
|
||||
|
||||
```sh
|
||||
npm run test:focused -- tests/boundary/android/jadxIntegration.test.ts tests/boundary/mcp/androidAnalysisMcp.test.ts
|
||||
```
|
||||
|
||||
## Optional NativeAOT Ghidra analysis
|
||||
|
||||
This lane is separate from the default native lane. `build:fixtures:nativeaot`
|
||||
requires an existing .NET SDK 8.0.416, the platform NativeAOT compiler/linker and
|
||||
runtime pack 8.0.22. It builds benign sources into ignored `_reference/` and
|
||||
records independent symbol/directory/SHA oracles; it never executes the target.
|
||||
Linux also builds stripped, ordinary-native and small malformed/ambiguous/layout
|
||||
negative inputs. The optional Windows fixture workflow builds a PE on a Windows
|
||||
runner; analyzing that PE on Linux does not verify a Windows Ghidra host.
|
||||
|
||||
Build the clean pinned upstream adapter with `build:ghidra:nativeaot`, then set
|
||||
`REA_GHIDRA_NATIVEAOT_JAR`. Run `verify:ghidra:nativeaot -- symbols`, `-- stripped`,
|
||||
`-- ordinary`, `-- unsupported`, `-- malformed`, `-- ambiguous`, and
|
||||
`-- loader-failure`, and `-- default-native` separately. The loader-failure mode source-builds
|
||||
a deliberately failing JDK 21 initializer and checks the actual loader cause and
|
||||
cleanup. The default-native mode verifies ordinary analysis with the optional
|
||||
extension disabled.
|
||||
The real MCP lane checks source identity, inline format discovery, metadata
|
||||
relationships/slots against independent compiler symbols, frozen strings,
|
||||
pseudocode and owned cleanup. Set `REA_NATIVEAOT_PROOF_CLI=1` for one equivalent
|
||||
CLI type inspection; this costs an additional full import. Select an unpacked
|
||||
installed package with `REA_NATIVEAOT_PROOF_PACKAGE_ROOT`, a fixture directory
|
||||
with `REA_NATIVEAOT_PROOF_FIXTURE_ROOT`, and optional evidence capture directory
|
||||
with `REA_NATIVEAOT_PROOF_CAPTURE_DIR` (absolute paths).
|
||||
|
||||
Keep builds/imports sequential on small hosts; scope `GHIDRA_HEADLESS_MAXMEM`
|
||||
(e.g. `768M`) to this command and use CPU affinity if needed. REA does not install
|
||||
or upgrade Java, Ghidra, .NET or native toolchains. See
|
||||
[the supported layout and provenance](ghidra-nativeaot.md).
|
||||
|
||||
## IDA MCP adapter
|
||||
|
||||
`npm run verify:ida -- --target /absolute/path/to/program --procedure main`
|
||||
uses the existing `REA_IDA_MCP_CONFIG` registration. It installs no engine,
|
||||
Python package, or compiler. The target must already be open in the GUI for
|
||||
the legacy attached profile; the database-supervisor headless profile opens
|
||||
a digest-verified private copy. A caller-supplied fixture keeps prerequisites
|
||||
limited to the selected engine and host. `tests/conformance/ida/inventory.c`
|
||||
provides an optional small native fixture source with an exported
|
||||
`rea_fixture_add` function.
|
||||
|
||||
The lane invokes the production CLI dispatcher and connects the pinned MCP
|
||||
client SDK to the production REA server. It verifies function Evidence and
|
||||
CLI/MCP parity, inventory/search, pseudocode, instructions, xrefs, malformed
|
||||
input, original-input preservation, and lifecycle cleanup. For headless
|
||||
analysis it confirms that the owned database IDs disappear from upstream
|
||||
discovery and private workspaces are removed. For attached analysis it confirms
|
||||
the existing GUI target remains reachable with the same input identity.
|
||||
`--package-root` selects an installed/extracted REA artifact. `--report` writes
|
||||
private local observations with mode `0600`; the console summary contains no
|
||||
target paths or upstream output.
|
||||
|
||||
Adapter and composition tests cover producer parsing, pagination, canonical
|
||||
entries, external callees, target switches, cancellation draining, snapshot
|
||||
replay exclusion, ownership failures, and incomplete cleanup. They do not
|
||||
establish real IDA operation. The initial real workflows cover legacy upstream
|
||||
1.4.0 on a Windows GUI and the modern supervisor at upstream commit
|
||||
`c133c3853faa111a9b00ee615c013b720d0c4acd` with Windows x64 IDA 9.3.
|
||||
Linux/macOS headless, modern attached GUI tools, other engine versions and
|
||||
architectures remain unverified; see the [provider guide](ida-provider.md).
|
||||
|
||||
## DOS Ghidra analysis
|
||||
|
||||
`npm run verify:ghidra:dos` requires the supported Ghidra and JDK installation
|
||||
@@ -153,6 +238,13 @@ the stable observation comparison. Linux x64 is verified; macOS DOS 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.
|
||||
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
|
||||
@@ -182,8 +274,7 @@ behavior, and complete CI evidence before merging.
|
||||
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
|
||||
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
|
||||
| `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks |
|
||||
| `npm run docs:check` | Committed generated-document freshness, without API HTML rendering |
|
||||
| `npm run docs:api:cached` | Explicit cached API HTML rendering |
|
||||
| `npm run docs:check` | Committed generated-document freshness |
|
||||
|
||||
For example:
|
||||
|
||||
@@ -230,8 +321,7 @@ process-global, and other boundary projects retain per-file isolation.
|
||||
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
|
||||
repository-local locks and fail fast when the same class of command is already
|
||||
running. The `npm test` build is inside that lock. `check:pr` runs its test task
|
||||
before starting generated-document validation. TypeDoc rendering is a separate
|
||||
command and CI step.
|
||||
before starting generated-document validation.
|
||||
|
||||
Vitest and Node persistent compile caches are deliberately not enabled by
|
||||
default. To evaluate repeated local runs, opt in for both cold and warm
|
||||
@@ -282,3 +372,15 @@ allows a host-permission-boundary-only result and explicitly reports
|
||||
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)
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
|
||||
### Firmware adapters
|
||||
|
||||
`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. It verifies CLI/MCP parity, selected ranges,
|
||||
unknown chunks, depth limits and extracted child digests. The optional
|
||||
`REA_FIRMWARE_VERIFY_EXT4=1` lane requires existing mke2fs/debugfs; the separate
|
||||
`REA_FIRMWARE_VERIFY_GHIDRA=1` lane checks a selected host ELF through real Ghidra.
|
||||
Neither optional toolchain is a base-lane prerequisite. See
|
||||
[firmware analysis](firmware-analysis.md) for limits and unverified formats.
|
||||
@@ -7,7 +7,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
6,src/domain/errors.ts:AnalysisProtocolError,Application protocol fails,Raw constructor message,Can expose protocol internals,"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",implemented,focused assertions passed; full `npm run check` passed
|
||||
7,src/domain/errors.ts:HopperProcessError,Analysis subprocess stops,"Hopper bridge stopped unexpectedly with code {exitCode}",Exposes provider bridge and process code,"Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
8,src/domain/errors.ts:HopperStartError,Analysis subprocess cannot start,Hopper application bridge could not be started,Exposes provider and bridge implementation,"Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
9,src/domain/errors.ts:BinaryTargetError,Target cannot open,"Cannot open artifact: {reason}",May expose raw reason and lacks checks,"REA could not open that app or binary. Check that the path exists, is readable, and points to a supported file.",implemented,focused assertions passed; full `npm run check` passed
|
||||
9,src/domain/errors.ts:BinaryTargetError,Target cannot open,"Cannot open artifact: {reason}",May expose raw reason and lacks checks,"Preserve the target admission reason; directories name analyze_javascript_application and rea analyze as available alternatives.",implemented,focused assertions passed; full `npm run check` passed
|
||||
10,src/domain/errors.ts:ConfigurationError,Environment configuration invalid,Raw constructor message,May expose provider-specific variables and lacks recovery,"REA configuration is invalid. Run `rea doctor` and fix the reported setting.",implemented,focused assertions passed; full `npm run check` passed
|
||||
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
@@ -55,7 +55,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
|
||||
46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed
|
||||
47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test
|
||||
48,src/application/Doctor.ts:node,Runtime version is outside supported ranges,Install Node.js 22.19+ or 24.11+.,Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
|
||||
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
|
||||
49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
|
||||
50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests
|
||||
51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test
|
||||
@@ -65,7 +65,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test
|
||||
56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy,Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test
|
||||
57,src/application/Setup.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test
|
||||
58,src/application/Setup.ts:unsupported Node,Runtime version cannot run setup,Install Node.js 22.19+ or 24.11+ and rerun setup.,Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
|
||||
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
|
||||
59,src/application/Setup.ts:unsupported macOS,macOS version is missing or older than 12,Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup host tests
|
||||
60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
|
||||
61,src/application/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
|
||||
@@ -83,11 +83,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection
|
||||
75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection
|
||||
76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test
|
||||
77,install.sh:node missing,node is absent,Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry.,Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
|
||||
80,install.sh:Node 22 floor,Node 22 minor version is below 19,Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+.,Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
|
||||
81,install.sh:Node 24 floor,Node 24 minor version is below 11,Node.js VERSION is unsupported; use Node.js 24.11 or newer.,Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
|
||||
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
|
||||
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
|
||||
82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection
|
||||
83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test
|
||||
84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_7da021af56cf2eb25489ed7ce494bfe92244e791fa3b5ad5a238a5eb14a23d9a",
|
||||
"manifest_id": "ecm_fbd47cbd3815d7e252ebcb60c4d086dcc1fc6b63f020e83513e28bf4d94c732c",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "411d5b61e237e3b9b2cb9b3a987d83a36134a2ea1810ddd8d9f695dc05709f5b"
|
||||
"sha256": "b317babebb82157b46b4dff698a170045490bcb21aaf9af582e20908ba2bb998"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
+112
-108
@@ -1,165 +1,169 @@
|
||||
# Windows Ghidra P0
|
||||
|
||||
Status: experimental and currently unavailable. The production adapter reports
|
||||
`unsupported_host` because verified Job Object process ownership, private
|
||||
runtime DACLs, and reparse-safe path admission are not implemented. Installation
|
||||
paths or manual MCP registration do not enable operations.
|
||||
This guide describes repository main. npm 4.0.1 does not include the Windows
|
||||
native bundle; check the [release boundary](installation.md#released-package-and-main)
|
||||
before applying these instructions to a published package.
|
||||
|
||||
This guide describes the intended P0 boundary and adapter mechanisms. It does
|
||||
not establish general Windows REA support. Even after the required controls
|
||||
are implemented, hostile, sensitive, security-decision, and concurrently
|
||||
mutable targets require separate acceptance.
|
||||
Status: experimental Windows x64 support for the read-only P0 boundary. Windows
|
||||
package builds bundle native process, filesystem, and DACL controls. REA
|
||||
uses them automatically when the analyst selects a Ghidra operation; no
|
||||
additional permission flag or degraded mode is required.
|
||||
|
||||
## Supported boundary
|
||||
|
||||
Once the required native controls are implemented, the intended P0 is limited to:
|
||||
|
||||
- a Windows x64 host;
|
||||
- Node.js 22.19+ or 24.11+;
|
||||
- Windows 10+ x64 with fixed local NTFS target and temporary volumes;
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
|
||||
- an operator-installed official Ghidra 12.1.4 distribution;
|
||||
- a 64-bit full JDK 21;
|
||||
- an explicit native, non-managed, non-DLL x86-64 PE application; and
|
||||
- the 22 read-only Ghidra inventory and function-analysis operations.
|
||||
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
|
||||
|
||||
Hopper, Ghidra GUI state, mutation, process capture, browser/Electron
|
||||
observation, artifact extraction, and general Windows feature parity are not
|
||||
implied by this boundary. Managed PE/CLI
|
||||
inspection remains a separate execution-free provider and is not routed
|
||||
through Windows Ghidra P0.
|
||||
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
|
||||
PE load-image inspection returns measurements with an explicit unsupported
|
||||
attestation status; independent format-specific verification currently supports
|
||||
DOS MZ only.
|
||||
|
||||
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or
|
||||
another package on Windows. The production adapter uses the packaged Java
|
||||
`HeadlessScript`; Python and PyGhidra are not prerequisites.
|
||||
This boundary does not establish general Windows feature parity. Ghidra GUI
|
||||
state, annotations, other target architectures and formats, and Hopper-only
|
||||
operations remain unavailable through this provider. Managed PE/CLI inspection
|
||||
uses its separate execution-free provider.
|
||||
|
||||
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a
|
||||
compiler. The adapter uses the packaged Java `HeadlessScript`; Python and
|
||||
PyGhidra are not prerequisites. Users do not build the native addon.
|
||||
|
||||
## Configuration
|
||||
|
||||
For installation diagnostics, configure REA, Ghidra, and JDK 21 in PowerShell. Analysis remains blocked:
|
||||
|
||||
```powershell
|
||||
npm install --global rea-agents
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
$env:JAVA_HOME = "C:\tools\jdk-21"
|
||||
$env:REA_ANALYSIS_PROVIDER = "ghidra"
|
||||
|
||||
rea doctor --json
|
||||
rea providers --json
|
||||
rea inspect "C:\approved-fixtures\sample.exe" --provider ghidra --format json
|
||||
rea inspect "C:\fixtures\sample.exe" --provider ghidra --format json
|
||||
```
|
||||
|
||||
`rea doctor` checks the x64 host, exact Ghidra release,
|
||||
`support\analyzeHeadless.bat`, `java.exe`, `javac.exe`, JDK bitness, and JDK
|
||||
major version. `rea setup` can configure supported agent integrations and the
|
||||
bundled REA skill on Windows after approval. It does not install Hopper, Ghidra,
|
||||
or Java, and configuring an agent does not enable the blocked Ghidra operations.
|
||||
`rea doctor` distinguishes installation prerequisites from native artifact or
|
||||
OS failures. It checks the Ghidra release, headless launcher, full JDK, x64
|
||||
host, and native controls. A missing or mismatched addon requires the matching
|
||||
Windows x64 package; changing MCP registration cannot supply that artifact.
|
||||
A filesystem or access failure identifies the failed native constraint.
|
||||
Other doctor checks, including unconfigured agent integrations, can remain
|
||||
unready while Ghidra itself is available.
|
||||
|
||||
For an MCP client, register the resolved Node entry point and preserve the
|
||||
three environment variables above. A representative configuration is:
|
||||
`rea setup` can register supported agent integrations and the bundled skill.
|
||||
Its existing preview and approval apply to configuration writes. Setup
|
||||
preserves valid Ghidra and Java settings even when a separate runtime
|
||||
prerequisite is unavailable.
|
||||
|
||||
For MCP, register the resolved Node entry point and the same provider settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea-windows-ghidra-p0": {
|
||||
"command": "C:\\Program Files\\nodejs\\node.exe",
|
||||
"args": [
|
||||
"C:\\Users\\analyst\\AppData\\Roaming\\npm\\node_modules\\rea-agents\\scripts\\rea.mjs",
|
||||
"mcp"
|
||||
],
|
||||
"rea": {
|
||||
"command": "C:\\tools\\node\\node.exe",
|
||||
"args": ["C:\\tools\\rea\\scripts\\rea.mjs", "mcp"],
|
||||
"env": {
|
||||
"REA_ANALYSIS_PROVIDER": "ghidra",
|
||||
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
|
||||
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-21"
|
||||
"JAVA_HOME": "C:\\tools\\jdk-21"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Resolve the actual global package path on the intended machine; do not copy the
|
||||
example path blindly. Restart the MCP client after changing its registration.
|
||||
These paths are examples. Use the actual package location and restart the MCP
|
||||
client after registration changes. Cold Ghidra import can exceed a client's
|
||||
short default request timeout; the real verification lane allows the existing
|
||||
330-second provider startup deadline to complete.
|
||||
|
||||
## Target and import identity
|
||||
## Native authority and target identity
|
||||
|
||||
PE parsing records architecture, COFF executable role, and whether the CLI
|
||||
header data-directory entry is non-empty. Header classification and SHA-256
|
||||
are read from the same open file handle. Windows admission then rejects:
|
||||
The verified boundary excludes SUBST aliases and changing drive-letter/DOS-device
|
||||
namespaces. REA does not claim a separate SUBST detector. Mounted-folder entries
|
||||
that expose reparse tags fail component admission; broader namespace variants
|
||||
have not been established by this lane. Requested paths, final handle paths,
|
||||
volume serials, and file IDs remain separate observations.
|
||||
|
||||
- non-PE formats;
|
||||
- x86, ARM, ARM64, or unknown architectures;
|
||||
- DLL/shared-library and non-executable image roles;
|
||||
- managed or incompletely classified PE targets; and
|
||||
- malformed optional headers or data-directory declarations.
|
||||
The loader validates package version, ABI, Node-API compatibility, PE machine,
|
||||
and artifact SHA-256 before loading the package-owned addon. Native failure
|
||||
reasons remain distinct from a valid installation and an unsupported target.
|
||||
|
||||
After admission, REA copies the target into the ephemeral Ghidra runtime and
|
||||
requires the snapshot digest to match the admitted SHA-256. Ghidra imports only
|
||||
that snapshot. Before serving a request, the Java bridge requires Ghidra's
|
||||
`Program.getExecutableSHA256()` observation to match the same digest. A path
|
||||
replacement that changes bytes therefore fails before Evidence is created.
|
||||
PE header classification and SHA-256 come from the same open file. The Windows
|
||||
provider admits native x86-64 PE applications and rejects unsupported roles,
|
||||
architectures, managed images, and malformed headers. The original selected
|
||||
source coordinate is preserved for native admission.
|
||||
|
||||
This linkage does not establish Windows reparse-point or hostile-path safety.
|
||||
The current implementation has no handle-based `CreateFileW` authority backend
|
||||
and no verified no-follow equivalent for every path component.
|
||||
Admission opens and checks every path component without following a reparse
|
||||
point. It retains handles, verifies local NTFS semantics, and observes volume
|
||||
serial, 128-bit file ID, final handle path, requested path, and size. Reparse
|
||||
paths, alternate streams, ambiguous coordinates, UNC paths, and other
|
||||
filesystems are rejected with the actual constraint.
|
||||
|
||||
## Local transport and lifecycle
|
||||
The snapshot streams from the admitted source handle on a background worker.
|
||||
Its digest must match the parsed target digest. Ghidra imports that private,
|
||||
immutable snapshot, and the bridge requires the imported program SHA-256 to
|
||||
match before serving operations. Source and snapshot identities remain
|
||||
separate diagnostic observations. Snapshot cancellation is checked between
|
||||
64 KiB reads and cleanup waits for worker settlement.
|
||||
|
||||
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
|
||||
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
|
||||
publishes a bounded endpoint record containing only the literal loopback host
|
||||
and port. The random 256-bit request token remains only in the
|
||||
ephemeral session descriptor and every request is authenticated.
|
||||
## Private runtime, transport, and lifecycle
|
||||
|
||||
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
|
||||
argument sequence, command extensions enabled for the Ghidra script, delayed
|
||||
expansion disabled, and Node shell execution disabled. Every batch token is
|
||||
quoted. Paths containing command-interpreter metacharacters are rejected
|
||||
instead of escaped heuristically.
|
||||
Runtime creation installs a protected DACL permitting exactly the current user
|
||||
and SYSTEM. Readback checks the actual owner and ACEs by handle. Descriptors
|
||||
and snapshots remain locked until owner cleanup. Child files inherit the
|
||||
private boundary. Analysis runs with the caller's authority; these controls
|
||||
are not a sandbox for the Ghidra engine or other processes using the same user.
|
||||
|
||||
Close, cancellation, timeout, protocol failure, or provider exit removes the
|
||||
project and runtime root. Windows P0 uses bounded `taskkill /T /F` process-tree
|
||||
termination. It does not assign Ghidra to a Job Object and cannot make the same
|
||||
PID-reuse and descendant-ownership claim as the POSIX process-group backend.
|
||||
Windows uses an authenticated listener bound to `127.0.0.1` on an ephemeral
|
||||
port. The endpoint record contains only the literal loopback host and port.
|
||||
The bridge writes a new record and closes its writer before the native reader
|
||||
can admit it. The reader refuses concurrent writers and verifies its opened
|
||||
object's private DACL. The random 256-bit bearer token stays in the private,
|
||||
immutable session descriptor and is redacted from diagnostic output.
|
||||
|
||||
## Known security gates
|
||||
The batch launcher uses an absolute `cmd.exe`, fixed switches, delayed
|
||||
expansion disabled, and quoted tokens. Command-interpreter metacharacters in
|
||||
launch paths are unsupported. JVM home and temporary properties use quoted
|
||||
`JDK_JAVA_OPTIONS`, preserving spaces. `APPDATA` and `LOCALAPPDATA` point into
|
||||
the private runtime, isolating Ghidra preferences, logs, and caches.
|
||||
|
||||
The following remain explicitly unavailable:
|
||||
|
||||
- verified current-user-only DACLs for runtime directories, descriptors, logs,
|
||||
snapshots, and endpoint records;
|
||||
- a DACL-protected Windows named pipe or equivalent OS-identity-restricted IPC;
|
||||
- reparse-point-safe, handle-based input and output authority;
|
||||
- Job Object assignment before child execution and kill-on-close proof;
|
||||
- general extraction containment and filesystem-sensitive workflows; and
|
||||
- independent security acceptance for hostile or mutable targets.
|
||||
|
||||
Successful symlink creation or ConPTY availability does not satisfy any of
|
||||
these gates. Inspect the machine-readable report with:
|
||||
|
||||
```powershell
|
||||
npm run probe:windows-capabilities
|
||||
```
|
||||
Process creation assigns a suspended child atomically to an owned Job Object.
|
||||
Membership and kill-on-owner-close policy are verified before resume.
|
||||
Breakaway is disabled. Only the child's default file owner is normalized;
|
||||
the caller token stays unchanged. Caller CPU affinity and priority are
|
||||
preserved. Close, cancellation, timeout, startup failure, and provider failure
|
||||
terminate the job and verify all members have settled before runtime cleanup.
|
||||
Cleanup removes admitted objects by handle and unlinks reparse entries without
|
||||
following their targets. Abrupt owner death kills the job but can leave
|
||||
private runtime files; crash-time transactional deletion is not claimed.
|
||||
|
||||
## Verification
|
||||
|
||||
Hosted CI builds, type-checks, runs the curated Windows tests, records the named
|
||||
host capability report, packs the real npm artifact, installs it in isolation,
|
||||
and compares the packaged MCP catalog with `TOOL_CONTRACTS`.
|
||||
|
||||
The controlled real-engine lane requires the missing native authority and currently remains blocked:
|
||||
See [native controls](../native/windows/README.md) for the separate artifact
|
||||
build lane and native boundary checks. Development tarballs without the built
|
||||
artifact report native controls unavailable; publishing requires the verified
|
||||
artifact and manifest.
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
npm ci
|
||||
npm run verify:windows-native
|
||||
npm run verify:ghidra:windows
|
||||
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
|
||||
```
|
||||
|
||||
The verifier requires the native authority before opening Ghidra. Its intended
|
||||
checks include a deterministic x86-64 PE fixture, the read-only operation
|
||||
contract, target/snapshot/import digest linkage, and endpoint, project,
|
||||
process, and runtime cleanup. The GitHub workflow
|
||||
accepts only the fixed `real-ghidra-windows` repository-dispatch event against
|
||||
default-branch `main`, a protected `real-ghidra-windows` environment, and a
|
||||
self-hosted runner labelled `Windows`, `x64`, and `ghidra-12-1-2`.
|
||||
The package lane must run with an ordinary, non-elevated user token. It exercises
|
||||
CLI inspection and every admitted MCP operation, validates named canonical
|
||||
input/output contracts, checks the source digest stays unchanged, and verifies
|
||||
runtime cleanup. Use a source-owned fixture, one analysis process at a time,
|
||||
and a bounded heap for real-provider verification.
|
||||
|
||||
Build, package, or adapter-test success does not prove the P0 operation claim.
|
||||
Real-engine acceptance requires the native controls and the separate
|
||||
verification lane; broader target and IPC security requirements remain distinct.
|
||||
The controlled GitHub workflow runs only default-branch code on its protected
|
||||
self-hosted Windows environment. A separate Linux job builds the matching
|
||||
native artifact. Hosted Windows CI verifies native controls and packaged
|
||||
startup separately from real Ghidra acceptance. Broader hostile-target,
|
||||
concurrently mutable-path, extraction, and IPC-security claims require their
|
||||
own acceptance and are not implied by this P0 result.
|
||||
@@ -1,28 +0,0 @@
|
||||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist/**", "node_modules/**"] },
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-confusing-void-expression": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-unsafe-type-assertion": "error",
|
||||
"@typescript-eslint/only-throw-error": "error",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
},
|
||||
},
|
||||
);
|
||||
+4
-6
@@ -55,17 +55,15 @@ fi
|
||||
platform="$(uname -s)"
|
||||
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
||||
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
|
||||
command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
|
||||
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
|
||||
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
||||
|
||||
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
||||
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
||||
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
||||
if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
|
||||
fi
|
||||
if ((node_major == 24 && node_minor < 11)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer."
|
||||
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
|
||||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
|
||||
fi
|
||||
|
||||
if [[ -n "$version" ]]; then
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": [
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"scripts/run-development-tests.mjs"
|
||||
],
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"ignoreIssues": {
|
||||
"src/domain/mobileApplicationInvestigation.ts": ["types"],
|
||||
"src/domain/objcSwiftMetadata.ts": ["types"],
|
||||
"src/domain/interfaceBuilderGraph.ts": ["types"],
|
||||
"src/domain/nativeInvestigationGraph.ts": ["types"]
|
||||
},
|
||||
"rules": {
|
||||
"files": "error",
|
||||
"dependencies": "error",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Windows native controls
|
||||
|
||||
REA ships one Windows x64 Node-API 8 addon. Users do not compile it or install
|
||||
another runtime. Ghidra and a full JDK remain bring-your-own prerequisites.
|
||||
The addon uses Windows 10+ APIs and admits fixed local NTFS volumes only.
|
||||
|
||||
The package owns the artifact path. The loader checks the package version,
|
||||
ABI, Node-API compatibility, PE architecture, and SHA-256 before loading it.
|
||||
There is no environment override for loading an arbitrary native module.
|
||||
|
||||
## Build
|
||||
|
||||
The artifact lane requires Linux, MinGW-w64 `x86_64-w64-mingw32-g++`,
|
||||
`x86_64-w64-mingw32-dlltool`, and `tar`. It never installs these commands.
|
||||
Node-API headers are reused from the build cache or downloaded from the
|
||||
pinned official Node release with its published SHA-256 checked.
|
||||
|
||||
```sh
|
||||
npm run build:windows-native
|
||||
npm run verify:windows-native:artifact
|
||||
npm pack
|
||||
```
|
||||
|
||||
`build/` is ignored by Git. The npm file inventory includes only the addon and
|
||||
its manifest. Release CI builds both, and `prepublishOnly` rejects a missing,
|
||||
stale, mismatched, or wrong-architecture artifact. A development tarball built
|
||||
without this lane reports Windows native controls as unavailable.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Opaque Node objects carry a native type tag, resource kind, and closed state.
|
||||
A pathname, serialized object, or forged handle cannot grant authority.
|
||||
- Input admission opens every component with `OPEN_REPARSE_POINT`, verifies
|
||||
the actual NTFS object, and retains handles that deny replacement and source
|
||||
writing. An admitted child keeps each ancestor nonempty; ancestor handles
|
||||
permit unrelated writes while NTFS rejects in-place reparse conversion. Volume serial, 128-bit file ID, final handle path,
|
||||
requested path, size, and digest observations are preserved separately.
|
||||
- New runtime roots and private child objects are created relative to admitted
|
||||
parent handles with `NtCreateFile`. A protected DACL and current-user owner
|
||||
are installed at creation. Readback permits exactly the current user and SYSTEM. Child
|
||||
files inherit this boundary; private files are also checked by handle.
|
||||
- Snapshots stream through a 64 KiB buffer on a Node async worker. Cancellation
|
||||
is checked between reads, cleanup waits for settlement, and the digest
|
||||
commits to the bytes written. Descriptor and snapshot handles remain held
|
||||
until runtime cleanup.
|
||||
Snapshot ownership is single-flight per runtime: an overlapping request fails
|
||||
with `ERROR_BUSY` before acquiring cancellation ownership. The TypeScript owner
|
||||
records only the accepted promise and abort listener; aborting a rejected copy
|
||||
must not cancel the active copy.
|
||||
- `PROC_THREAD_ATTRIBUTE_JOB_LIST` assigns a suspended child atomically to an
|
||||
unnamed, non-inherited Job Object. Membership and kill-on-close policy are
|
||||
checked before resume. Breakaway is disabled, and cleanup waits for all
|
||||
job members and captured output, including detached descendants.
|
||||
- Only the suspended child's default file owner is normalized. Caller token
|
||||
privileges and ownership remain unchanged. Caller CPU affinity and process
|
||||
priority are preserved in the child. Environment names are sorted and
|
||||
deduplicated using Windows ordinal comparison, preserving their native Unicode
|
||||
semantics and caller-selected values.
|
||||
- Cleanup walks locked directory objects, removes files by handle, and unlinks
|
||||
reparse entries without traversing their targets. Normal close, cancellation,
|
||||
startup failure, and timeout release provider resources. Abrupt owner death
|
||||
kills the owned job; it can leave private runtime files for later operator
|
||||
cleanup and does not claim transactional deletion after a crash.
|
||||
|
||||
Windows failures retain their constraint, requested coordinate, Win32 code,
|
||||
and system message. Unsupported filesystems and reparse paths are distinct
|
||||
from OS access denial and missing packaged controls.
|
||||
|
||||
The bearer-token descriptor remains under its immutable private lease until
|
||||
`runtime_close` on Windows; POSIX removes the descriptor after the bridge reads
|
||||
it. Job ownership guarantees process termination on owner death, not deletion
|
||||
of private files after a crash. SUBST drive aliases and changing DOS-device
|
||||
namespaces are outside the verified P0 scope and are not detected as a separate
|
||||
alias policy. Mounted-folder paths that report a reparse tag are rejected by
|
||||
component admission; broader namespace variants remain unverified. Preserve
|
||||
requested and final handle coordinates rather than treating them as one identity.
|
||||
|
||||
## Real verification
|
||||
|
||||
```powershell
|
||||
npm run verify:windows-native
|
||||
npm run verify:ghidra:windows
|
||||
```
|
||||
|
||||
The native lane checks DACL readback independently, source write/replacement
|
||||
rejection, unrelated sibling renames, reparse rejection, immutable snapshots,
|
||||
asynchronous cancellation, cleanup containment,
|
||||
descendant termination, unrelated-process preservation, normal exit, owner
|
||||
close, and forced owner exit. An optional independently built
|
||||
`tests/fixtures/windows/processBoundary.cc` executable adds breakaway and
|
||||
caller/child token observations and an in-place ancestor reparse attempt:
|
||||
|
||||
```powershell
|
||||
node scripts/verify-windows-native.mjs . C:\fixtures\process-boundary.exe
|
||||
```
|
||||
|
||||
`nonAdminRunner.cc` is a test-only launcher for an elevated development shell.
|
||||
It duplicates an existing non-elevated Explorer token belonging to the same
|
||||
user, creates private temporary window-station objects, and launches the test
|
||||
inside a kill-on-close fixture job. It does not create accounts, modify host
|
||||
policy, or change the caller's token. A real-provider claim must additionally
|
||||
verify the packaged CLI and MCP on that ordinary-user token.
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "authority.hpp"
|
||||
|
||||
namespace rea {
|
||||
|
||||
static const napi_type_tag RESOURCE_TAG = {0x45fcbf0b49e84fe3ULL, 0x9e3e5074130de843ULL};
|
||||
|
||||
void check(napi_status status) {
|
||||
if (status != napi_ok) throw Failure("Invalid native boundary value", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
napi_value object(napi_env env) { napi_value value; check(napi_create_object(env, &value)); return value; }
|
||||
napi_value null(napi_env env) { napi_value value; check(napi_get_null(env, &value)); return value; }
|
||||
napi_value string(napi_env env, const std::string& text) {
|
||||
napi_value value; check(napi_create_string_utf8(env, text.data(), text.size(), &value)); return value;
|
||||
}
|
||||
napi_value string(napi_env env, const std::wstring& text) {
|
||||
static_assert(sizeof(wchar_t) == sizeof(char16_t));
|
||||
napi_value value;
|
||||
check(napi_create_string_utf16(env, reinterpret_cast<const char16_t*>(text.data()), text.size(), &value));
|
||||
return value;
|
||||
}
|
||||
napi_value number(napi_env env, double numeric) {
|
||||
napi_value value; check(napi_create_double(env, numeric, &value)); return value;
|
||||
}
|
||||
napi_value boolean(napi_env env, bool flag) {
|
||||
napi_value value; check(napi_get_boolean(env, flag, &value)); return value;
|
||||
}
|
||||
void set(napi_env env, napi_value target, const char* key, napi_value value) {
|
||||
check(napi_set_named_property(env, target, key, value));
|
||||
}
|
||||
std::wstring wide(napi_env env, napi_value value) {
|
||||
size_t size = 0;
|
||||
check(napi_get_value_string_utf16(env, value, nullptr, 0, &size));
|
||||
require(size <= 32767, "Windows value exceeds the native string limit", L"", ERROR_INVALID_PARAMETER);
|
||||
std::vector<char16_t> buffer(size + 1);
|
||||
check(napi_get_value_string_utf16(env, value, buffer.data(), buffer.size(), &size));
|
||||
std::wstring result(reinterpret_cast<const wchar_t*>(buffer.data()), size);
|
||||
require(result.find(L'\0') == std::wstring::npos, "Windows value contains NUL", L"", ERROR_INVALID_PARAMETER);
|
||||
return result;
|
||||
}
|
||||
double numeric(napi_env env, napi_value value) {
|
||||
double result; check(napi_get_value_double(env, value, &result)); return result;
|
||||
}
|
||||
std::vector<napi_value> array(napi_env env, napi_value value) {
|
||||
bool valid; check(napi_is_array(env, value, &valid));
|
||||
require(valid, "Native arguments must be an array", L"", ERROR_INVALID_PARAMETER);
|
||||
uint32_t length; check(napi_get_array_length(env, value, &length));
|
||||
std::vector<napi_value> result(length);
|
||||
for (uint32_t index = 0; index < length; ++index)
|
||||
check(napi_get_element(env, value, index, &result[index]));
|
||||
return result;
|
||||
}
|
||||
static void finalize(napi_env, void* data, void*) { delete static_cast<Resource*>(data); }
|
||||
napi_value wrap(napi_env env, std::unique_ptr<Resource> value) {
|
||||
auto result = object(env);
|
||||
check(napi_type_tag_object(env, result, &RESOURCE_TAG));
|
||||
check(napi_wrap(env, result, value.get(), finalize, nullptr, nullptr));
|
||||
value.release();
|
||||
return result;
|
||||
}
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind) {
|
||||
bool tagged = false;
|
||||
check(napi_check_object_type_tag(env, value, &RESOURCE_TAG, &tagged));
|
||||
require(tagged, "Native handle does not belong to REA", L"", ERROR_INVALID_HANDLE);
|
||||
void* pointer = nullptr; check(napi_unwrap(env, value, &pointer));
|
||||
auto& result = *static_cast<Resource*>(pointer);
|
||||
require(result.kind == kind && !result.closed, "Native handle is closed or has the wrong kind", L"", ERROR_INVALID_HANDLE);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value failureValue(napi_env env, const Failure& failure) {
|
||||
wchar_t* message = nullptr;
|
||||
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, failure.win32, 0, reinterpret_cast<wchar_t*>(&message), 0, nullptr);
|
||||
std::wstring detail = message == nullptr ? L"" : message;
|
||||
if (message != nullptr) LocalFree(message);
|
||||
const std::wstring constraint(failure.constraint.begin(), failure.constraint.end());
|
||||
auto text = string(env, constraint + (failure.path.empty() ? L"" : L" [" + failure.path + L"]") +
|
||||
L" (Win32 " + std::to_wstring(failure.win32) + L"): " + detail);
|
||||
napi_value error; napi_create_error(env, nullptr, text, &error);
|
||||
const char* code = failure.win32 == ERROR_FILE_NOT_FOUND || failure.win32 == ERROR_PATH_NOT_FOUND ? "ENOENT"
|
||||
: failure.win32 == ERROR_ACCESS_DENIED ? "EACCES" : "ERR_REA_WINDOWS_NATIVE";
|
||||
set(env, error, "code", string(env, std::string(code)));
|
||||
set(env, error, "constraint", string(env, failure.constraint));
|
||||
set(env, error, "requestedPath", string(env, failure.path));
|
||||
set(env, error, "win32Code", number(env, failure.win32));
|
||||
set(env, error, "win32Message", string(env, detail));
|
||||
return error;
|
||||
}
|
||||
|
||||
static napi_value call(napi_env env, napi_callback_info info) {
|
||||
try {
|
||||
std::array<napi_value, 2> args;
|
||||
size_t count = args.size();
|
||||
check(napi_get_cb_info(env, info, &count, args.data(), nullptr, nullptr));
|
||||
require(count == 2, "Native call requires operation and arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
const auto operation = wide(env, args[0]);
|
||||
const auto values = array(env, args[1]);
|
||||
if (operation == L"inspect") {
|
||||
require(values.empty(), "Inspect takes no arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
return inspect(env);
|
||||
}
|
||||
if (operation.rfind(L"process_", 0) == 0) return processCall(env, operation, values);
|
||||
return filesystemCall(env, operation, values);
|
||||
} catch (const Failure& failure) {
|
||||
auto error = failureValue(env, failure);
|
||||
napi_throw(env, error);
|
||||
return nullptr;
|
||||
} catch (const std::exception& failure) {
|
||||
napi_throw_error(env, "ERR_REA_WINDOWS_NATIVE", failure.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static napi_value initialize(napi_env env, napi_value exports) {
|
||||
napi_value function;
|
||||
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, nullptr, &function) != napi_ok) return nullptr;
|
||||
if (napi_set_named_property(env, exports, "call", function) != napi_ok) return nullptr;
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, rea::initialize)
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <aclapi.h>
|
||||
#include <bcrypt.h>
|
||||
#include <node_api.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rea {
|
||||
|
||||
struct Failure : std::runtime_error {
|
||||
DWORD win32;
|
||||
std::wstring path;
|
||||
std::string constraint;
|
||||
Failure(std::string constraint, std::wstring path = L"", DWORD code = GetLastError())
|
||||
: std::runtime_error(constraint), win32(code), path(std::move(path)),
|
||||
constraint(std::move(constraint)) {}
|
||||
};
|
||||
|
||||
inline void require(bool condition, const char* constraint, const std::wstring& path = L"") {
|
||||
if (!condition) {
|
||||
const auto code = GetLastError();
|
||||
throw Failure(constraint, path, code);
|
||||
}
|
||||
}
|
||||
inline void require(bool condition, const char* constraint, const std::wstring& path, DWORD code) {
|
||||
if (!condition) throw Failure(constraint, path, code);
|
||||
}
|
||||
|
||||
class Handle {
|
||||
HANDLE value_ = INVALID_HANDLE_VALUE;
|
||||
public:
|
||||
Handle() = default;
|
||||
explicit Handle(HANDLE value) : value_(value) {}
|
||||
~Handle() { reset(); }
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
Handle(Handle&& other) noexcept : value_(other.release()) {}
|
||||
Handle& operator=(Handle&& other) noexcept { reset(other.release()); return *this; }
|
||||
HANDLE get() const { return value_; }
|
||||
bool valid() const { return value_ != INVALID_HANDLE_VALUE && value_ != nullptr; }
|
||||
HANDLE release() { return std::exchange(value_, INVALID_HANDLE_VALUE); }
|
||||
void reset(HANDLE next = INVALID_HANDLE_VALUE) {
|
||||
if (valid()) CloseHandle(value_);
|
||||
value_ = next;
|
||||
}
|
||||
};
|
||||
|
||||
enum class Kind { File, Runtime, Process };
|
||||
struct Resource {
|
||||
const Kind kind;
|
||||
bool closed = false;
|
||||
explicit Resource(Kind value) : kind(value) {}
|
||||
virtual ~Resource() = default;
|
||||
};
|
||||
struct File : Resource {
|
||||
std::wstring requestedPath;
|
||||
std::wstring path;
|
||||
std::vector<Handle> handles;
|
||||
explicit File(std::wstring path) : Resource(Kind::File), requestedPath(path), path(std::move(path)) {}
|
||||
HANDLE get() const { return handles.back().get(); }
|
||||
};
|
||||
struct Runtime : Resource {
|
||||
std::wstring path;
|
||||
std::vector<Handle> parents;
|
||||
Handle directory;
|
||||
std::vector<Handle> immutableFiles;
|
||||
// Main-thread admission is single-flight; only the accepted copy owns the
|
||||
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
|
||||
// another copy before it can reset that flag or replace pending ownership.
|
||||
bool snapshotPending = false;
|
||||
std::atomic_bool snapshotCancelled{false};
|
||||
Runtime() : Resource(Kind::Runtime) {}
|
||||
};
|
||||
struct Process : Resource {
|
||||
Handle job, process, stdoutRead, stderrRead;
|
||||
DWORD pid = 0;
|
||||
Process() : Resource(Kind::Process) {}
|
||||
};
|
||||
|
||||
void check(napi_status status);
|
||||
napi_value failureValue(napi_env env, const Failure& failure);
|
||||
napi_value object(napi_env env);
|
||||
napi_value string(napi_env env, const std::string& value);
|
||||
napi_value string(napi_env env, const std::wstring& value);
|
||||
napi_value number(napi_env env, double value);
|
||||
napi_value boolean(napi_env env, bool value);
|
||||
napi_value null(napi_env env);
|
||||
void set(napi_env env, napi_value object, const char* key, napi_value value);
|
||||
std::wstring wide(napi_env env, napi_value value);
|
||||
double numeric(napi_env env, napi_value value);
|
||||
std::vector<napi_value> array(napi_env env, napi_value value);
|
||||
napi_value wrap(napi_env env, std::unique_ptr<Resource> resource);
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind);
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& path, DWORD access = GENERIC_READ,
|
||||
bool directory = false);
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parent, const std::wstring& prefix);
|
||||
void closeRuntime(Runtime& runtime);
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment);
|
||||
napi_value processCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
napi_value inspect(napi_env env);
|
||||
|
||||
} // namespace rea
|
||||
@@ -0,0 +1,531 @@
|
||||
#include "authority.hpp"
|
||||
#include <sddl.h>
|
||||
#include <winternl.h>
|
||||
|
||||
namespace rea {
|
||||
|
||||
static std::wstring hex(const unsigned char* bytes, size_t count) {
|
||||
constexpr wchar_t digits[] = L"0123456789abcdef";
|
||||
std::wstring result;
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring localPath(std::wstring path) {
|
||||
if (path.rfind(L"\\\\?\\", 0) == 0) path.erase(0, 4);
|
||||
require(path.size() >= 3 && path[1] == L':' && path[2] == L'\\' &&
|
||||
((path[0] >= L'A' && path[0] <= L'Z') || (path[0] >= L'a' && path[0] <= L'z')),
|
||||
"Only absolute local NTFS drive paths are supported", path, ERROR_NOT_SUPPORTED);
|
||||
require(path.find(L'/', 0) == std::wstring::npos && path.find(L':', 2) == std::wstring::npos,
|
||||
"Alternate streams and non-local path syntax are unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
require(GetDriveTypeW(path.substr(0, 3).c_str()) == DRIVE_FIXED,
|
||||
"Only fixed local NTFS volumes are supported", path, ERROR_NOT_SUPPORTED);
|
||||
if (path.size() > 3 && path.back() == L'\\') path.pop_back();
|
||||
require(path.size() == 3 || path.back() != L'\\', "Repeated path separators are unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
size_t begin = 3;
|
||||
while (begin < path.size()) {
|
||||
const auto end = path.find(L'\\', begin);
|
||||
const auto component = path.substr(begin, end == std::wstring::npos ? end : end - begin);
|
||||
require(!component.empty() && component != L"." && component != L".." &&
|
||||
component.back() != L'.' && component.back() != L' ',
|
||||
"Ambiguous Windows path component is unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
if (end == std::wstring::npos) break;
|
||||
begin = end + 1;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static Handle openComponent(const std::wstring& path, DWORD access, bool directory, DWORD sharing = FILE_SHARE_READ) {
|
||||
// Attribute-only opens do not participate in Windows share-access checks.
|
||||
// Directory guards need FILE_LIST_DIRECTORY to prevent replacement and
|
||||
// in-place reparse conversion while descendant paths are being consumed.
|
||||
Handle handle(CreateFileW((L"\\\\?\\" + path).c_str(), access | FILE_READ_ATTRIBUTES | READ_CONTROL |
|
||||
(directory ? FILE_LIST_DIRECTORY : 0),
|
||||
sharing, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
|
||||
require(handle.valid(), "Open Windows path component failed", path);
|
||||
FILE_ATTRIBUTE_TAG_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle.get(), FileAttributeTagInfo, &info, sizeof(info)),
|
||||
"Read Windows path attributes failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
||||
throw Failure("Reparse path component is unsupported (tag " + std::to_string(info.ReparseTag) + ")",
|
||||
path, ERROR_NOT_SUPPORTED);
|
||||
require(((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory,
|
||||
directory ? "Expected a directory path component" : "Target is not a regular file", path,
|
||||
directory ? ERROR_PATH_NOT_FOUND : ERROR_DIRECTORY);
|
||||
require(GetFileType(handle.get()) == FILE_TYPE_DISK, "Windows object is not a disk file", path, ERROR_NOT_SUPPORTED);
|
||||
return handle;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory) {
|
||||
auto result = std::make_unique<File>(localPath(requested));
|
||||
result->requestedPath = requested;
|
||||
const auto& path = result->path;
|
||||
require(directory || path.size() > 3, "Target is not a regular file", requested, ERROR_DIRECTORY);
|
||||
result->handles.push_back(openComponent(path.substr(0, 3), 0, true));
|
||||
wchar_t filesystem[64]{};
|
||||
DWORD flags = 0;
|
||||
require(GetVolumeInformationByHandleW(result->handles.front().get(), nullptr, 0, nullptr, nullptr,
|
||||
&flags, filesystem, 64), "Read volume semantics failed", path);
|
||||
require(std::wstring(filesystem) == L"NTFS" && (flags & FILE_PERSISTENT_ACLS) != 0,
|
||||
"Backing volume must be NTFS with persistent ACLs", path, ERROR_NOT_SUPPORTED);
|
||||
size_t offset = 3;
|
||||
while (offset < path.size()) {
|
||||
auto end = path.find(L'\\', offset);
|
||||
const bool final = end == std::wstring::npos;
|
||||
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
|
||||
// Once the next component is pinned against deletion, its parent cannot
|
||||
// become empty and NTFS cannot convert that parent into a reparse point.
|
||||
// Relax only ancestor write sharing so unrelated child renames can proceed.
|
||||
// Independent evidence: tests/fixtures/windows/processBoundary.cc attempts
|
||||
// FSCTL_SET_REPARSE_POINT on the nonempty pinned ancestor and observes denial.
|
||||
// SUBST aliases and mounted-folder/reparse namespace variants are outside
|
||||
// the admitted P0 evidence; requested and final handle paths remain distinct.
|
||||
const auto parentPath = offset == 3 ? path.substr(0, 3) : path.substr(0, offset - 1);
|
||||
result->handles.back() = openComponent(parentPath, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
result->handles.push_back(std::move(child));
|
||||
if (final) break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path) {
|
||||
FILE_ID_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle, FileIdInfo, &info, sizeof(info)),
|
||||
"Stable 128-bit Windows file identity is unavailable", path);
|
||||
std::array<wchar_t, 32768> finalPath;
|
||||
const auto length = GetFinalPathNameByHandleW(handle, finalPath.data(), finalPath.size(), FILE_NAME_NORMALIZED);
|
||||
require(length > 0 && length < finalPath.size(), "Read final handle path failed", path);
|
||||
LARGE_INTEGER size{};
|
||||
FILE_STANDARD_INFO standard{};
|
||||
require(GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard)),
|
||||
"Read handle metadata failed", path);
|
||||
size = standard.EndOfFile;
|
||||
require(size.QuadPart >= 0 && size.QuadPart <= 9007199254740991LL, "File size exceeds exact numeric representation", path,
|
||||
ERROR_NOT_SUPPORTED);
|
||||
auto result = object(env);
|
||||
set(env, result, "requestedPath", string(env, path));
|
||||
set(env, result, "finalPath", string(env, std::wstring(finalPath.data(), length)));
|
||||
set(env, result, "filesystem", string(env, std::string("NTFS")));
|
||||
std::array<unsigned char, 8> serial;
|
||||
for (size_t index = 0; index < serial.size(); ++index)
|
||||
serial[index] = static_cast<unsigned char>(info.VolumeSerialNumber >> ((7 - index) * 8));
|
||||
set(env, result, "volumeSerial", string(env, hex(serial.data(), serial.size())));
|
||||
set(env, result, "fileId", string(env, hex(info.FileId.Identifier, 16)));
|
||||
set(env, result, "size", number(env, static_cast<double>(size.QuadPart)));
|
||||
set(env, result, "directory", boolean(env, standard.Directory != FALSE));
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Security {
|
||||
std::vector<unsigned char> user;
|
||||
std::array<unsigned char, SECURITY_MAX_SID_SIZE> system{};
|
||||
std::vector<unsigned char> acl;
|
||||
SECURITY_DESCRIPTOR descriptor{};
|
||||
PSID userSid() const { return reinterpret_cast<const TOKEN_USER*>(user.data())->User.Sid; }
|
||||
Security() {
|
||||
Handle token;
|
||||
HANDLE raw;
|
||||
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw), "Read current user token failed");
|
||||
token.reset(raw);
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &size);
|
||||
user.resize(size);
|
||||
require(GetTokenInformation(token.get(), TokenUser, user.data(), size, &size), "Read current user SID failed");
|
||||
DWORD systemSize = system.size();
|
||||
require(CreateWellKnownSid(WinLocalSystemSid, nullptr, system.data(), &systemSize), "Create SYSTEM SID failed");
|
||||
acl.resize(sizeof(ACL) + 2 * (sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD)) + GetLengthSid(userSid()) + systemSize);
|
||||
auto access = reinterpret_cast<PACL>(acl.data());
|
||||
require(InitializeAcl(access, acl.size(), ACL_REVISION), "Initialize private DACL failed");
|
||||
for (PSID sid : {userSid(), static_cast<PSID>(system.data())})
|
||||
require(AddAccessAllowedAceEx(access, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
|
||||
FILE_ALL_ACCESS, sid), "Populate private DACL failed");
|
||||
require(InitializeSecurityDescriptor(&descriptor, SECURITY_DESCRIPTOR_REVISION) &&
|
||||
SetSecurityDescriptorOwner(&descriptor, userSid(), FALSE) &&
|
||||
SetSecurityDescriptorDacl(&descriptor, TRUE, access, FALSE) &&
|
||||
SetSecurityDescriptorControl(&descriptor, SE_DACL_PROTECTED, SE_DACL_PROTECTED),
|
||||
"Create protected security descriptor failed");
|
||||
}
|
||||
};
|
||||
|
||||
static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD access, bool directory,
|
||||
Security& security, const std::wstring& path) {
|
||||
require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos && name.size() < 32768,
|
||||
"Private object requires one filename component", path, ERROR_INVALID_PARAMETER);
|
||||
UNICODE_STRING coordinate{};
|
||||
coordinate.Buffer = const_cast<wchar_t*>(name.data());
|
||||
coordinate.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t));
|
||||
coordinate.MaximumLength = coordinate.Length;
|
||||
OBJECT_ATTRIBUTES attributes;
|
||||
InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, &security.descriptor);
|
||||
IO_STATUS_BLOCK observation{};
|
||||
HANDLE raw = INVALID_HANDLE_VALUE;
|
||||
const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation, nullptr,
|
||||
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_CREATE,
|
||||
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
|
||||
(directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE), nullptr, 0);
|
||||
if (status < 0) throw Failure("Create private object relative to its admitted parent failed", path,
|
||||
RtlNtStatusToDosError(status));
|
||||
Handle result(raw);
|
||||
require(result.valid() && observation.Information == FILE_CREATED,
|
||||
"Private creation did not return a new object handle", path, ERROR_ACCESS_DENIED);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
|
||||
Security expected;
|
||||
PSID owner = nullptr;
|
||||
PACL acl = nullptr;
|
||||
PSECURITY_DESCRIPTOR raw = nullptr;
|
||||
const DWORD code = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
||||
&owner, nullptr, &acl, nullptr, &raw);
|
||||
if (code != ERROR_SUCCESS) throw Failure("Read private DACL by handle failed", path, code);
|
||||
const auto descriptor = std::unique_ptr<void, decltype(&LocalFree)>(raw, LocalFree);
|
||||
require(owner != nullptr && EqualSid(owner, expected.userSid()), "Private object owner is not the current user", path,
|
||||
ERROR_ACCESS_DENIED);
|
||||
SECURITY_DESCRIPTOR_CONTROL control;
|
||||
DWORD revision;
|
||||
require(GetSecurityDescriptorControl(raw, &control, &revision), "Read DACL control flags failed", path);
|
||||
require(acl != nullptr && (control & SE_DACL_PRESENT) != 0 && (!root || (control & SE_DACL_PROTECTED) != 0),
|
||||
"Private object requires a present protected DACL", path, ERROR_ACCESS_DENIED);
|
||||
require(acl->AceCount == 2, "Private DACL contains unexpected trustees", path, ERROR_ACCESS_DENIED);
|
||||
bool user = false, system = false;
|
||||
for (DWORD index = 0; index < acl->AceCount; ++index) {
|
||||
void* rawAce;
|
||||
require(GetAce(acl, index, &rawAce), "Read private DACL entry failed", path);
|
||||
const auto ace = static_cast<ACCESS_ALLOWED_ACE*>(rawAce);
|
||||
require(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && ace->Mask == FILE_ALL_ACCESS &&
|
||||
(ace->Header.AceFlags & INHERIT_ONLY_ACE) == 0 &&
|
||||
(!root || (ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
|
||||
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)),
|
||||
"Private DACL contains unsupported rights or ACE type", path, ERROR_ACCESS_DENIED);
|
||||
PSID sid = const_cast<DWORD*>(&ace->SidStart);
|
||||
if (EqualSid(sid, expected.userSid())) user = true;
|
||||
else if (EqualSid(sid, expected.system.data())) system = true;
|
||||
else throw Failure("Private DACL permits an unrelated principal", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
|
||||
auto parent = openFile(parentPath, 0, true);
|
||||
require(!prefix.empty() && prefix.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos,
|
||||
"Runtime prefix must be one filename component", parentPath, ERROR_INVALID_PARAMETER);
|
||||
std::array<unsigned char, 16> random;
|
||||
require(BCryptGenRandom(nullptr, random.data(), random.size(), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0,
|
||||
"Generate runtime identity failed", parentPath, ERROR_GEN_FAILURE);
|
||||
auto result = std::make_unique<Runtime>();
|
||||
result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size());
|
||||
Security security;
|
||||
result->directory = createRelative(parent->get(), prefix + hex(random.data(), random.size()),
|
||||
DELETE | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
|
||||
true, security, result->path);
|
||||
try {
|
||||
verifyPrivate(result->directory.get(), result->path, true);
|
||||
// The owned child pins the parent nonempty before write sharing is relaxed.
|
||||
parent->handles.back() = openComponent(parent->path, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
result->parents = std::move(parent->handles);
|
||||
} catch (...) {
|
||||
FILE_DISPOSITION_INFO deletion{TRUE};
|
||||
SetFileInformationByHandle(result->directory.get(), FileDispositionInfo, &deletion, sizeof(deletion));
|
||||
throw;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring relativePath(Runtime& root, const std::wstring& relative) {
|
||||
require(!relative.empty() && relative[0] != L'\\' && relative.find(L':') == std::wstring::npos,
|
||||
"Runtime coordinate must be a relative path", relative, ERROR_INVALID_PARAMETER);
|
||||
return localPath(root.path + L"\\" + relative);
|
||||
}
|
||||
|
||||
static std::unique_ptr<File> runtimeDirectory(Runtime& root, const std::wstring& path) {
|
||||
auto result = std::make_unique<File>(path);
|
||||
HANDLE duplicate;
|
||||
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS), "Duplicate private directory handle failed", path);
|
||||
result->handles.emplace_back(duplicate);
|
||||
size_t offset = root.path.size() + 1;
|
||||
while (offset < path.size()) {
|
||||
const auto end = path.find(L'\\', offset);
|
||||
result->handles.push_back(openComponent(path.substr(0, end), 0, true));
|
||||
if (end == std::wstring::npos) break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void directory(Runtime& root, const std::wstring& relative) {
|
||||
const auto path = relativePath(root, relative);
|
||||
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
|
||||
auto parent = runtimeDirectory(root, parentPath);
|
||||
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
|
||||
Security security;
|
||||
try {
|
||||
auto created = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
|
||||
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
|
||||
true, security, path);
|
||||
verifyPrivate(created.get(), path, false);
|
||||
} catch (const Failure& failure) {
|
||||
if (failure.win32 != ERROR_ALREADY_EXISTS && failure.win32 != ERROR_FILE_EXISTS) throw;
|
||||
auto existing = runtimeDirectory(root, path);
|
||||
verifyPrivate(existing->get(), path, false);
|
||||
}
|
||||
}
|
||||
|
||||
static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
|
||||
const auto path = relativePath(root, relative);
|
||||
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
|
||||
auto parent = runtimeDirectory(root, parentPath);
|
||||
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
|
||||
Security security;
|
||||
auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
|
||||
GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
|
||||
verifyPrivate(file.get(), path, false);
|
||||
return file;
|
||||
}
|
||||
|
||||
static void writeBytes(HANDLE file, const void* bytes, DWORD count, const std::wstring& path) {
|
||||
DWORD written;
|
||||
require(WriteFile(file, bytes, count, &written, nullptr) && written == count, "Write private file failed", path);
|
||||
}
|
||||
|
||||
static void dispose(HANDLE handle, const std::wstring& path) {
|
||||
FILE_BASIC_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)), "Read cleanup attributes failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
|
||||
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
|
||||
require(SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)), "Clear owned readonly attribute failed", path);
|
||||
}
|
||||
FILE_DISPOSITION_INFO deletion{TRUE};
|
||||
require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path);
|
||||
}
|
||||
|
||||
void closeRuntime(Runtime& root) {
|
||||
if (root.closed) return;
|
||||
// Windows keeps the bearer descriptor and snapshot leases until this owner
|
||||
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
|
||||
// closes process jobs but cannot run this walk, so private files can remain.
|
||||
root.immutableFiles.clear();
|
||||
struct ReleaseRuntime {
|
||||
Runtime& root;
|
||||
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
|
||||
} release{root};
|
||||
// Each traversed directory stays locked against replacement. Reparse entries
|
||||
// are unlinked as objects; their targets are never enumerated or deleted.
|
||||
struct Frame {
|
||||
Handle directory;
|
||||
HANDLE search = INVALID_HANDLE_VALUE;
|
||||
std::wstring path;
|
||||
WIN32_FIND_DATAW next{};
|
||||
bool pending = false;
|
||||
Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {
|
||||
search = FindFirstFileExW((L"\\\\?\\" + this->path + L"\\*").c_str(), FindExInfoBasic, &next,
|
||||
FindExSearchNameMatch, nullptr, 0);
|
||||
pending = search != INVALID_HANDLE_VALUE;
|
||||
require(pending || GetLastError() == ERROR_FILE_NOT_FOUND, "Enumerate runtime cleanup failed", this->path);
|
||||
}
|
||||
~Frame() { if (search != INVALID_HANDLE_VALUE) FindClose(search); }
|
||||
};
|
||||
std::vector<std::unique_ptr<Frame>> frames;
|
||||
HANDLE duplicate;
|
||||
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS), "Duplicate runtime cleanup handle failed", root.path);
|
||||
frames.push_back(std::make_unique<Frame>(Handle(duplicate), root.path));
|
||||
while (!frames.empty()) {
|
||||
auto& frame = *frames.back();
|
||||
if (!frame.pending) {
|
||||
if (frame.search != INVALID_HANDLE_VALUE) { FindClose(frame.search); frame.search = INVALID_HANDLE_VALUE; }
|
||||
dispose(frame.directory.get(), frame.path);
|
||||
frames.pop_back();
|
||||
continue;
|
||||
}
|
||||
const auto name = std::wstring(frame.next.cFileName);
|
||||
frame.pending = FindNextFileW(frame.search, &frame.next) != FALSE;
|
||||
require(frame.pending || GetLastError() == ERROR_NO_MORE_FILES, "Continue runtime cleanup enumeration failed", frame.path);
|
||||
if (name == L"." || name == L"..") continue;
|
||||
const auto path = frame.path + L"\\" + name;
|
||||
Handle child(CreateFileW((L"\\\\?\\" + path).c_str(), DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
|
||||
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
|
||||
require(child.valid(), "Open owned cleanup object failed", path);
|
||||
FILE_ATTRIBUTE_TAG_INFO info{};
|
||||
require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)), "Read cleanup object tag failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
|
||||
frames.push_back(std::make_unique<Frame>(std::move(child), path));
|
||||
else dispose(child.get(), path);
|
||||
}
|
||||
}
|
||||
|
||||
struct SnapshotWork {
|
||||
napi_env env;
|
||||
napi_async_work work = nullptr;
|
||||
napi_deferred deferred = nullptr;
|
||||
napi_ref rootReference = nullptr;
|
||||
Runtime* root;
|
||||
std::unique_ptr<File> source;
|
||||
Handle destination;
|
||||
std::wstring path;
|
||||
std::wstring digest;
|
||||
std::unique_ptr<Failure> failure;
|
||||
SnapshotWork(napi_env env, Runtime& root) : env(env), root(&root) {}
|
||||
};
|
||||
|
||||
static void copySnapshot(napi_env, void* pointer) {
|
||||
auto& work = *static_cast<SnapshotWork*>(pointer);
|
||||
try {
|
||||
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
||||
require(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) == 0,
|
||||
"Initialize snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
struct Algorithm { BCRYPT_ALG_HANDLE handle; ~Algorithm() { BCryptCloseAlgorithmProvider(handle, 0); } } owner{algorithm};
|
||||
BCRYPT_HASH_HANDLE hash = nullptr;
|
||||
require(BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) == 0,
|
||||
"Create snapshot SHA-256 state failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
struct Hash { BCRYPT_HASH_HANDLE handle; ~Hash() { BCryptDestroyHash(handle); } } hashOwner{hash};
|
||||
std::array<unsigned char, 65536> buffer;
|
||||
while (true) {
|
||||
require(!work.root->snapshotCancelled.load(), "Admitted snapshot was cancelled", work.path, ERROR_OPERATION_ABORTED);
|
||||
DWORD count;
|
||||
require(ReadFile(work.source->get(), buffer.data(), buffer.size(), &count, nullptr),
|
||||
"Read admitted source handle failed", work.source->path);
|
||||
if (count == 0) break;
|
||||
writeBytes(work.destination.get(), buffer.data(), count, work.path);
|
||||
require(BCryptHashData(hash, buffer.data(), count, 0) == 0,
|
||||
"Hash admitted snapshot bytes failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
}
|
||||
std::array<unsigned char, 32> digest;
|
||||
require(BCryptFinishHash(hash, digest.data(), digest.size(), 0) == 0,
|
||||
"Finish snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
require(FlushFileBuffers(work.destination.get()), "Flush private snapshot failed", work.path);
|
||||
work.digest = hex(digest.data(), digest.size());
|
||||
} catch (const Failure& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure);
|
||||
} catch (const std::exception& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure.what(), work.path, ERROR_GEN_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
|
||||
std::unique_ptr<SnapshotWork> work(static_cast<SnapshotWork*>(pointer));
|
||||
work->root->snapshotPending = false;
|
||||
try {
|
||||
if (work->failure) throw *work->failure;
|
||||
require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED);
|
||||
auto result = object(env);
|
||||
set(env, result, "sha256", string(env, work->digest));
|
||||
set(env, result, "source", identity(env, work->source->get(), work->source->requestedPath));
|
||||
set(env, result, "snapshot", identity(env, work->destination.get(), work->path));
|
||||
work->root->immutableFiles.push_back(std::move(work->destination));
|
||||
check(napi_resolve_deferred(env, work->deferred, result));
|
||||
} catch (const Failure& failure) {
|
||||
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
|
||||
} catch (const std::exception& failure) {
|
||||
napi_reject_deferred(env, work->deferred,
|
||||
failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE)));
|
||||
}
|
||||
napi_delete_reference(env, work->rootReference);
|
||||
napi_delete_async_work(env, work->work);
|
||||
}
|
||||
|
||||
static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
|
||||
const std::wstring& sourcePath, const std::wstring& relative) {
|
||||
auto work = std::make_unique<SnapshotWork>(env, root);
|
||||
work->source = openFile(sourcePath);
|
||||
work->path = relativePath(root, relative);
|
||||
work->destination = createPrivateFile(root, relative);
|
||||
napi_value promise;
|
||||
check(napi_create_promise(env, &work->deferred, &promise));
|
||||
check(napi_create_reference(env, rootValue, 1, &work->rootReference));
|
||||
const auto label = string(env, std::string("REA admitted Windows snapshot"));
|
||||
root.snapshotCancelled.store(false);
|
||||
try {
|
||||
check(napi_create_async_work(env, nullptr, label, copySnapshot, completeSnapshot, work.get(), &work->work));
|
||||
check(napi_queue_async_work(env, work->work));
|
||||
} catch (...) {
|
||||
if (work->work) napi_delete_async_work(env, work->work);
|
||||
napi_delete_reference(env, work->rootReference);
|
||||
throw;
|
||||
}
|
||||
root.snapshotPending = true;
|
||||
work.release();
|
||||
return promise;
|
||||
}
|
||||
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
|
||||
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
|
||||
require(args.size() == static_cast<size_t>(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
if (operation == L"open") {
|
||||
auto file = openFile(wide(env, args[0]));
|
||||
auto result = identity(env, file->get(), file->requestedPath);
|
||||
set(env, result, "handle", wrap(env, std::move(file)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"close") {
|
||||
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
|
||||
file.handles.clear(); file.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"read") {
|
||||
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
|
||||
const auto position = numeric(env, args[1]);
|
||||
const auto length = numeric(env, args[2]);
|
||||
require(position >= 0 && position <= 9007199254740991.0 && position == static_cast<int64_t>(position) &&
|
||||
length >= 0 && length <= 65536 && length == static_cast<DWORD>(length),
|
||||
"Read coordinates exceed the native chunk boundary", file.path, ERROR_INVALID_PARAMETER);
|
||||
LARGE_INTEGER offset; offset.QuadPart = static_cast<int64_t>(position);
|
||||
require(SetFilePointerEx(file.get(), offset, nullptr, FILE_BEGIN), "Seek admitted file handle failed", file.path);
|
||||
std::array<unsigned char, 65536> buffer;
|
||||
DWORD count;
|
||||
require(ReadFile(file.get(), buffer.data(), static_cast<DWORD>(length), &count, nullptr), "Read admitted file handle failed", file.path);
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, buffer.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"runtime_create") {
|
||||
auto root = createRuntime(wide(env, args[0]), wide(env, args[1]));
|
||||
auto result = identity(env, root->directory.get(), root->path);
|
||||
set(env, result, "path", string(env, root->path));
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "handle", wrap(env, std::move(root)));
|
||||
return result;
|
||||
}
|
||||
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
|
||||
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
|
||||
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
|
||||
// Reject overlapping operations before snapshot() resets cancellation. The
|
||||
// one accepted worker and WindowsPrivateRuntime retain the same root lease.
|
||||
if (operation == L"runtime_close") { closeRuntime(root); return null(env); }
|
||||
if (operation == L"runtime_mkdir") { directory(root, wide(env, args[1])); return null(env); }
|
||||
if (operation == L"runtime_open") {
|
||||
const auto path = relativePath(root, wide(env, args[1]));
|
||||
auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
|
||||
file->path = path;
|
||||
file->requestedPath = path;
|
||||
file->handles.push_back(openComponent(path, GENERIC_READ, false));
|
||||
verifyPrivate(file->get(), path, false);
|
||||
auto result = identity(env, file->get(), path);
|
||||
set(env, result, "handle", wrap(env, std::move(file)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"runtime_write") {
|
||||
bool isBuffer; check(napi_is_buffer(env, args[2], &isBuffer));
|
||||
require(isBuffer, "Private write requires a byte buffer", root.path, ERROR_INVALID_PARAMETER);
|
||||
void* bytes; size_t length; check(napi_get_buffer_info(env, args[2], &bytes, &length));
|
||||
require(length <= MAXDWORD, "Private write exceeds Win32 byte representation", root.path, ERROR_INVALID_PARAMETER);
|
||||
auto file = createPrivateFile(root, wide(env, args[1]));
|
||||
writeBytes(file.get(), bytes, static_cast<DWORD>(length), root.path);
|
||||
require(FlushFileBuffers(file.get()), "Flush private runtime file failed", root.path);
|
||||
root.immutableFiles.push_back(std::move(file));
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"runtime_snapshot") {
|
||||
return snapshot(env, args[0], root, wide(env, args[1]), wide(env, args[2]));
|
||||
}
|
||||
throw Failure("Unknown native filesystem operation", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
@@ -0,0 +1,271 @@
|
||||
#include "authority.hpp"
|
||||
|
||||
namespace rea {
|
||||
|
||||
class Attributes {
|
||||
std::vector<unsigned char> storage_;
|
||||
public:
|
||||
explicit Attributes(DWORD count) {
|
||||
SIZE_T size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
|
||||
storage_.resize(size);
|
||||
require(InitializeProcThreadAttributeList(get(), count, 0, &size), "Initialize process attributes failed");
|
||||
}
|
||||
~Attributes() { DeleteProcThreadAttributeList(get()); }
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST get() { return reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage_.data()); }
|
||||
void add(DWORD_PTR key, void* value, SIZE_T size) {
|
||||
require(UpdateProcThreadAttribute(get(), 0, key, value, size, nullptr, nullptr),
|
||||
"Windows atomic process attribute is unavailable");
|
||||
}
|
||||
};
|
||||
|
||||
static Handle createJob() {
|
||||
Handle job(CreateJobObjectW(nullptr, nullptr));
|
||||
require(job.valid(), "Create owned Job Object failed");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION policy{};
|
||||
policy.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
require(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &policy, sizeof(policy)),
|
||||
"Apply kill-on-owner-close Job Object policy failed");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION actual{};
|
||||
require(QueryInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &actual, sizeof(actual), nullptr),
|
||||
"Read Job Object policy failed");
|
||||
require(actual.BasicLimitInformation.LimitFlags == JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
"Job Object policy permits unexpected breakaway", L"", ERROR_ACCESS_DENIED);
|
||||
return job;
|
||||
}
|
||||
|
||||
static void pipe(Handle& read, Handle& write) {
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
HANDLE rawRead, rawWrite;
|
||||
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
|
||||
read.reset(rawRead); write.reset(rawWrite);
|
||||
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
|
||||
}
|
||||
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
|
||||
require(command.size() >= 3 && command[1] == L':' && command[2] == L'\\',
|
||||
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
|
||||
auto result = std::make_unique<Process>();
|
||||
result->job = createJob();
|
||||
Handle stdoutWrite, stderrWrite;
|
||||
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
|
||||
require(input.valid(), "Open owned process null input failed");
|
||||
Attributes attributes(2);
|
||||
HANDLE job = result->job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
|
||||
std::array<HANDLE, 3> inherited{input.get(), stdoutWrite.get(), stderrWrite.get()};
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), sizeof(inherited));
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput = input.get();
|
||||
startup.StartupInfo.hStdOutput = stdoutWrite.get();
|
||||
startup.StartupInfo.hStdError = stderrWrite.get();
|
||||
startup.lpAttributeList = attributes.get();
|
||||
// Windows environment names use ordinal case-insensitive comparison, not
|
||||
// locale collation or JavaScript's expanding Unicode uppercase conversion.
|
||||
auto orderedEnvironment = environment;
|
||||
auto keyLength = [&](const std::wstring& entry) {
|
||||
const auto end = entry.find(L'=', entry.front() == L'=' ? 1 : 0);
|
||||
require(end != std::wstring::npos && end > 0, "Invalid child environment name", command,
|
||||
ERROR_INVALID_PARAMETER);
|
||||
return static_cast<int>(end);
|
||||
};
|
||||
for (const auto& entry : orderedEnvironment) {
|
||||
require(!entry.empty(), "Empty child environment entry", command, ERROR_INVALID_PARAMETER);
|
||||
keyLength(entry);
|
||||
}
|
||||
auto compareNames = [&](const std::wstring& left, const std::wstring& right) {
|
||||
const auto result = CompareStringOrdinal(left.data(), keyLength(left), right.data(), keyLength(right), TRUE);
|
||||
require(result != 0, "Compare Windows environment names failed", command);
|
||||
return result;
|
||||
};
|
||||
std::stable_sort(orderedEnvironment.begin(), orderedEnvironment.end(),
|
||||
[&](const std::wstring& left, const std::wstring& right) {
|
||||
return compareNames(left, right) == CSTR_LESS_THAN;
|
||||
});
|
||||
std::vector<std::wstring> uniqueEnvironment;
|
||||
for (auto& entry : orderedEnvironment) {
|
||||
if (!uniqueEnvironment.empty() && compareNames(uniqueEnvironment.back(), entry) == CSTR_EQUAL)
|
||||
uniqueEnvironment.back() = std::move(entry);
|
||||
else uniqueEnvironment.push_back(std::move(entry));
|
||||
}
|
||||
std::vector<wchar_t> variables;
|
||||
for (const auto& entry : uniqueEnvironment) {
|
||||
variables.insert(variables.end(), entry.begin(), entry.end()); variables.push_back(L'\0');
|
||||
}
|
||||
variables.push_back(L'\0');
|
||||
if (uniqueEnvironment.empty()) variables.push_back(L'\0');
|
||||
std::vector<wchar_t> line(commandLine.begin(), commandLine.end()); line.push_back(L'\0');
|
||||
PROCESS_INFORMATION process{};
|
||||
require(CreateProcessW(command.c_str(), line.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
|
||||
variables.data(), cwd.empty() ? nullptr : cwd.c_str(), &startup.StartupInfo, &process),
|
||||
"Create process atomically inside owned Job Object failed", command);
|
||||
result->process.reset(process.hProcess);
|
||||
Handle thread(process.hThread);
|
||||
result->pid = process.dwProcessId;
|
||||
// An elevated token may default file ownership to Administrators. Normalize
|
||||
// only the suspended child's default owner so its runtime files belong to the
|
||||
// current user; the caller's token and privileges are not modified.
|
||||
HANDLE rawToken;
|
||||
require(OpenProcessToken(result->process.get(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &rawToken),
|
||||
"Open suspended process default-owner token failed", command);
|
||||
Handle token(rawToken);
|
||||
DWORD tokenSize = 0;
|
||||
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &tokenSize);
|
||||
std::vector<unsigned char> tokenUser(tokenSize);
|
||||
require(GetTokenInformation(token.get(), TokenUser, tokenUser.data(), tokenSize, &tokenSize),
|
||||
"Read suspended process user identity failed", command);
|
||||
TOKEN_OWNER owner{reinterpret_cast<TOKEN_USER*>(tokenUser.data())->User.Sid};
|
||||
require(SetTokenInformation(token.get(), TokenOwner, &owner, sizeof(owner)),
|
||||
"Set suspended process file owner failed", command);
|
||||
DWORD_PTR affinity = 0, systemAffinity = 0;
|
||||
require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity),
|
||||
"Read caller process affinity failed", command);
|
||||
if (affinity != 0)
|
||||
require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command);
|
||||
const DWORD priority = GetPriorityClass(GetCurrentProcess());
|
||||
require(priority != 0 && SetPriorityClass(result->process.get(), priority),
|
||||
"Preserve caller process priority failed", command);
|
||||
BOOL assigned = FALSE;
|
||||
if (!IsProcessInJob(result->process.get(), result->job.get(), &assigned) || !assigned) {
|
||||
TerminateJobObject(result->job.get(), 1);
|
||||
throw Failure("Suspended process has no owned Job Object membership", command, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
require(ResumeThread(thread.get()) != MAXDWORD, "Resume job-owned process failed", command);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value readOutput(napi_env env, Handle& read, bool& ended) {
|
||||
std::array<unsigned char, 65536> bytes;
|
||||
DWORD available = 0, count = 0;
|
||||
if (!read.valid()) ended = true;
|
||||
else if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
|
||||
require(GetLastError() == ERROR_BROKEN_PIPE, "Observe owned process output failed");
|
||||
read.reset(); ended = true;
|
||||
} else if (available > 0) {
|
||||
require(ReadFile(read.get(), bytes.data(), std::min<DWORD>(available, bytes.size()), &count, nullptr),
|
||||
"Read owned process output failed");
|
||||
}
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, bytes.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
|
||||
static DWORD activeProcesses(Process& process) {
|
||||
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION state{};
|
||||
require(QueryInformationJobObject(process.job.get(), JobObjectBasicAccountingInformation, &state, sizeof(state), nullptr),
|
||||
"Observe owned Job Object settlement failed");
|
||||
return state.ActiveProcesses;
|
||||
}
|
||||
|
||||
static napi_value callerToken(napi_env env) {
|
||||
HANDLE raw;
|
||||
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &raw), "Read caller token failed");
|
||||
Handle token(raw);
|
||||
DWORD size;
|
||||
TOKEN_ELEVATION elevation{};
|
||||
require(GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size), "Read caller elevation failed");
|
||||
std::vector<unsigned char> user(4096), owner(4096), integrity(4096);
|
||||
require(GetTokenInformation(token.get(), TokenUser, user.data(), user.size(), &size) &&
|
||||
GetTokenInformation(token.get(), TokenOwner, owner.data(), owner.size(), &size) &&
|
||||
GetTokenInformation(token.get(), TokenIntegrityLevel, integrity.data(), integrity.size(), &size),
|
||||
"Read caller token observations failed");
|
||||
HANDLE rawImpersonation;
|
||||
require(DuplicateToken(token.get(), SecurityIdentification, &rawImpersonation), "Observe caller group membership failed");
|
||||
Handle impersonation(rawImpersonation);
|
||||
std::array<unsigned char, SECURITY_MAX_SID_SIZE> administrators;
|
||||
DWORD administratorSize = administrators.size();
|
||||
require(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, administrators.data(), &administratorSize),
|
||||
"Create administrator group identity failed");
|
||||
BOOL administrator = FALSE, assigned = FALSE;
|
||||
require(CheckTokenMembership(impersonation.get(), administrators.data(), &administrator), "Read caller administrator membership failed");
|
||||
require(IsProcessInJob(GetCurrentProcess(), nullptr, &assigned), "Read caller job membership failed");
|
||||
const auto sid = reinterpret_cast<TOKEN_MANDATORY_LABEL*>(integrity.data())->Label.Sid;
|
||||
auto result = object(env);
|
||||
set(env, result, "elevated", boolean(env, elevation.TokenIsElevated != 0));
|
||||
set(env, result, "administrator", boolean(env, administrator != FALSE));
|
||||
set(env, result, "integrityLevel", number(env, *GetSidSubAuthority(sid, *GetSidSubAuthorityCount(sid) - 1)));
|
||||
set(env, result, "defaultOwnerIsUser", boolean(env, EqualSid(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid,
|
||||
reinterpret_cast<TOKEN_OWNER*>(owner.data())->Owner)));
|
||||
set(env, result, "inJob", boolean(env, assigned != FALSE));
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
if (operation == L"process_caller_token") {
|
||||
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
return callerToken(env);
|
||||
}
|
||||
if (operation == L"process_spawn") {
|
||||
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
std::vector<std::wstring> environment;
|
||||
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
|
||||
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
|
||||
auto result = object(env);
|
||||
set(env, result, "pid", number(env, process->pid));
|
||||
set(env, result, "jobAssigned", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
set(env, result, "handle", wrap(env, std::move(process)));
|
||||
return result;
|
||||
}
|
||||
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
|
||||
if (operation == L"process_poll") {
|
||||
auto result = object(env);
|
||||
bool stdoutEnded = false, stderrEnded = false;
|
||||
set(env, result, "stdout", readOutput(env, process.stdoutRead, stdoutEnded));
|
||||
set(env, result, "stderr", readOutput(env, process.stderrRead, stderrEnded));
|
||||
set(env, result, "stdoutEnded", boolean(env, stdoutEnded));
|
||||
set(env, result, "stderrEnded", boolean(env, stderrEnded));
|
||||
const auto exited = WaitForSingleObject(process.process.get(), 0);
|
||||
require(exited == WAIT_OBJECT_0 || exited == WAIT_TIMEOUT, "Observe owned process exit failed");
|
||||
DWORD code = 0;
|
||||
if (exited == WAIT_OBJECT_0) require(GetExitCodeProcess(process.process.get(), &code), "Read owned process exit code failed");
|
||||
set(env, result, "exitCode", exited == WAIT_OBJECT_0 ? number(env, code) : null(env));
|
||||
set(env, result, "activeProcesses", number(env, activeProcesses(process)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"process_alive") return number(env, activeProcesses(process));
|
||||
if (operation == L"process_terminate") {
|
||||
const auto active = activeProcesses(process);
|
||||
if (active > 0) require(TerminateJobObject(process.job.get(), 1), "Terminate owned Job Object failed");
|
||||
return boolean(env, active > 0);
|
||||
}
|
||||
if (operation == L"process_close") {
|
||||
// Closing the job is the crash-safe authority; no PID enumeration is used.
|
||||
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
|
||||
process.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
throw Failure("Unknown native process operation", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
napi_value inspect(napi_env env) {
|
||||
wchar_t temp[32768];
|
||||
const auto length = GetTempPathW(32768, temp);
|
||||
require(length > 0 && length < 32768, "Read temporary directory coordinate failed");
|
||||
auto root = createRuntime(std::wstring(temp, length), L"rea-native-probe-");
|
||||
auto filesystem = identity(env, root->directory.get(), root->path);
|
||||
closeRuntime(*root);
|
||||
auto job = createJob();
|
||||
Attributes attributes(1);
|
||||
HANDLE raw = job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &raw, sizeof(raw));
|
||||
auto result = object(env);
|
||||
set(env, result, "abiVersion", number(env, 1));
|
||||
set(env, result, "nodeApiVersion", number(env, 8));
|
||||
set(env, result, "architecture", string(env, std::string("x64")));
|
||||
set(env, result, "filesystem", filesystem);
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "atomicJobAssignment", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
Generated
+1199
-1204
File diff suppressed because it is too large.
Load diff
+38
-24
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"rea-agents": "scripts/rea.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.19.0 || >=24.11.0"
|
||||
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
|
||||
},
|
||||
"packageManager": "npm@11.16.0",
|
||||
"files": [
|
||||
@@ -44,6 +44,8 @@
|
||||
"bridge/hopper_bridge.py",
|
||||
"bridge/native/ReaNativeUI.swift",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"scripts/rea.mjs",
|
||||
"scripts/electron-active-hook.cjs",
|
||||
"scripts/electron-active-hook-boundaries.cjs",
|
||||
@@ -52,7 +54,10 @@
|
||||
"scripts/prepare.mjs",
|
||||
"skills",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
"LICENSE",
|
||||
"native/windows/build/manifest.json",
|
||||
"native/windows/build/rea-windows-x64.node",
|
||||
"scripts/verify-windows-native-artifact.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||
@@ -73,7 +78,7 @@
|
||||
"precheck:test": "npm run deps:check",
|
||||
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
|
||||
"precheck:pr": "npm run deps:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
|
||||
"precheck:ci": "npm run deps:check",
|
||||
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||
"build:cached": "turbo run build",
|
||||
@@ -82,7 +87,6 @@
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"knip": "knip",
|
||||
"lint:dead": "knip",
|
||||
"jscpd": "jscpd src/ tests/",
|
||||
"scan:todos": "node scripts/scan-todos.mjs",
|
||||
"start": "node scripts/rea.mjs mcp",
|
||||
@@ -119,8 +123,10 @@
|
||||
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
|
||||
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
|
||||
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
|
||||
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
|
||||
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
|
||||
@@ -129,8 +135,13 @@
|
||||
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
|
||||
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
|
||||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
|
||||
"verify:javascript:digests": "npm run build:cached && node scripts/verify-large-javascript-digests.mjs",
|
||||
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
|
||||
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
|
||||
"verify:firmware": "npm run build:cached && node scripts/verify-real-firmware.mjs",
|
||||
"fixtures:firmware": "node scripts/generate-firmware-fixtures.mjs",
|
||||
"verify:android": "npm run build:cached && node scripts/verify-real-android.mjs",
|
||||
"fixtures:android": "node scripts/fetch-android-fixtures.mjs",
|
||||
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
|
||||
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
|
||||
"verify:setup:linux": "npm run build:cached && node scripts/verify-linux-setup-e2e.mjs",
|
||||
@@ -145,8 +156,6 @@
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
|
||||
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
|
||||
"docs:api": "typedoc",
|
||||
"docs:api:cached": "turbo run docs:api",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
|
||||
@@ -156,7 +165,15 @@
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
|
||||
"test:focused": "node scripts/run-development-tests.mjs focused"
|
||||
"test:focused": "node scripts/run-development-tests.mjs focused",
|
||||
"build:windows-native": "node scripts/build-windows-native.mjs",
|
||||
"verify:windows-native:artifact": "node scripts/verify-windows-native-artifact.mjs",
|
||||
"prepublishOnly": "npm run verify:windows-native:artifact",
|
||||
"verify:windows-native": "node scripts/verify-windows-native.mjs",
|
||||
"verify:ghidra:windows:package": "node scripts/verify-packaged-ghidra-windows.mjs",
|
||||
"build:ghidra:nativeaot": "node scripts/build-ghidra-nativeaot.mjs",
|
||||
"build:fixtures:nativeaot": "node scripts/generate-nativeaot-fixtures.mjs",
|
||||
"verify:ghidra:nativeaot": "npm run build:cached && node scripts/verify-real-ghidra-nativeaot.mjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
@@ -172,48 +189,45 @@
|
||||
"@electron/asar": "4.2.0",
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@modelcontextprotocol/client": "2.3.1",
|
||||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"canonicalize": "3.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
"graphology": "0.26.0",
|
||||
"graphology-shortest-path": "2.1.0",
|
||||
"ignore": "7.0.6",
|
||||
"incur": "0.4.13",
|
||||
"incur": "0.6.1",
|
||||
"isomorphic-git": "1.38.7",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"pino": "9.14.0",
|
||||
"pino": "10.4.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"plist": "5.0.0",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "^1.7.0",
|
||||
"smol-toml": "1.7.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fast-check/vitest": "0.4.1",
|
||||
"@fast-check/vitest": "0.5.0",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/node": "26.6.4",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"@vitest/coverage-v8": "5.0.3",
|
||||
"ajv": "8.20.0",
|
||||
"husky": "9.1.7",
|
||||
"jscpd": "5.0.12",
|
||||
"knip": "6.26.0",
|
||||
"knip": "6.39.0",
|
||||
"lint-staged": "16.4.0",
|
||||
"oxfmt": "0.72.0",
|
||||
"oxlint": "1.73.0",
|
||||
"turbo": "2.10.5",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.1.10",
|
||||
"oxlint": "1.87.0",
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"vitest": "5.0.3",
|
||||
"yaml": "2.9.0"
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
{ "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",
|
||||
"path": "src/generatedPackageMetadata.ts"
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readdir,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const source = resolve(
|
||||
process.env.REA_NATIVEAOT_SOURCE ??
|
||||
join(root, "third_party/ghidra-nativeaot"),
|
||||
);
|
||||
const install = process.env.GHIDRA_INSTALL_DIR;
|
||||
if (!install)
|
||||
throw new Error(
|
||||
"NativeAOT build lane requires existing GHIDRA_INSTALL_DIR (Ghidra 12.1.4).",
|
||||
);
|
||||
const properties = await readFile(
|
||||
join(install, "Ghidra/application.properties"),
|
||||
"utf8",
|
||||
);
|
||||
if (!/^application.version=12\.1\.4\s*$/m.test(properties))
|
||||
throw new Error("NativeAOT build lane requires Ghidra 12.1.4.");
|
||||
const output = resolve(
|
||||
process.env.REA_NATIVEAOT_BUILD_ROOT ??
|
||||
join(root, "_reference/nativeaot-integration/extension"),
|
||||
);
|
||||
const java = process.env.JAVA_HOME ? join(process.env.JAVA_HOME, "bin") : null;
|
||||
const run = (command, args) => {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
timeout: 120_000,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
if (result.error || result.status !== 0)
|
||||
throw new Error(
|
||||
`NativeAOT build prerequisite/action ${command} failed: ${result.error?.message ?? ""}\n${result.stdout ?? ""}${result.stderr ?? ""}`,
|
||||
);
|
||||
return result.stdout.trim();
|
||||
};
|
||||
const javac = java ? join(java, "javac") : "javac";
|
||||
if (!/^javac 21(?:\.|$)/u.test(run(javac, ["-version"])))
|
||||
throw new Error(
|
||||
"NativeAOT build lane requires an existing JDK 21 javac; REA does not install or upgrade Java.",
|
||||
);
|
||||
const revision = run("git", ["-C", source, "rev-parse", "HEAD"]);
|
||||
if (
|
||||
revision !== "effeb734fc570c32650f88b159608979dc7b423e" ||
|
||||
run("git", ["-C", source, "status", "--porcelain"])
|
||||
)
|
||||
throw new Error(
|
||||
"NativeAOT build requires the clean pinned upstream source; initialize third_party/ghidra-nativeaot.",
|
||||
);
|
||||
const files = async (path, extension) => {
|
||||
const result = [];
|
||||
for (const entry of await readdir(path, { withFileTypes: true })) {
|
||||
const child = join(path, entry.name);
|
||||
if (entry.isDirectory()) result.push(...(await files(child, extension)));
|
||||
else if (entry.isFile() && child.endsWith(extension)) result.push(child);
|
||||
}
|
||||
return result.sort();
|
||||
};
|
||||
await mkdir(output, { recursive: true });
|
||||
const classes = await mkdtemp(join(output, "classes-"));
|
||||
try {
|
||||
const sources = [
|
||||
...(await files(join(source, "src/main/java"), ".java")),
|
||||
...(await files(join(root, "bridge/ghidra/extensions/nativeaot"), ".java")),
|
||||
];
|
||||
const jars = await files(join(install, "Ghidra"), ".jar");
|
||||
const args = [
|
||||
"-proc:none",
|
||||
"-cp",
|
||||
jars.join(process.platform === "win32" ? ";" : ":"),
|
||||
"-d",
|
||||
classes,
|
||||
...sources,
|
||||
];
|
||||
await writeFile(
|
||||
join(output, "javac.args"),
|
||||
args
|
||||
.map((s) => `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`)
|
||||
.join("\n"),
|
||||
);
|
||||
run(javac, [
|
||||
"-J-Xmx512m",
|
||||
"-J-XX:ActiveProcessorCount=1",
|
||||
`@${join(output, "javac.args")}`,
|
||||
]);
|
||||
await writeFile(
|
||||
join(classes, "LICENSE-ghidra-nativeaot.md"),
|
||||
await readFile(join(source, "LICENSE.md")),
|
||||
);
|
||||
const jar = join(output, "rea-ghidra-nativeaot.jar");
|
||||
run(java ? join(java, "jar") : "jar", [
|
||||
"--create",
|
||||
"--file",
|
||||
jar,
|
||||
"--date=2025-01-01T00:00:00Z",
|
||||
"-C",
|
||||
classes,
|
||||
".",
|
||||
]);
|
||||
const sha256 = createHash("sha256")
|
||||
.update(await readFile(jar))
|
||||
.digest("hex");
|
||||
await writeFile(
|
||||
join(output, "build.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
jar,
|
||||
sha256,
|
||||
upstream_revision: revision,
|
||||
ghidra_version: "12.1.4",
|
||||
integration_api: 1,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
jar,
|
||||
sha256,
|
||||
upstream_revision: revision,
|
||||
ghidra_version: "12.1.4",
|
||||
integration_api: 1,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await rm(classes, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const nodeVersion = "22.19.0";
|
||||
const compiler = "x86_64-w64-mingw32-g++";
|
||||
const cache = join(homedir(), ".cache", "rea-windows-native");
|
||||
const output = join(root, "native", "windows", "build");
|
||||
const run = (command, args) => {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 4 * 1_024 * 1_024,
|
||||
timeout: 120_000,
|
||||
});
|
||||
if (result.error !== undefined)
|
||||
throw new Error(
|
||||
`Windows native build lane requires ${command}: ${result.error.message}`,
|
||||
);
|
||||
if (result.status !== 0)
|
||||
throw new Error(
|
||||
`Windows native build failed: ${result.stdout}${result.stderr}`,
|
||||
);
|
||||
return result.stdout;
|
||||
};
|
||||
if (process.platform !== "linux")
|
||||
throw new Error(
|
||||
"The Windows x64 artifact build lane currently requires Linux and MinGW-w64.",
|
||||
);
|
||||
run(compiler, ["--version"]);
|
||||
await mkdir(cache, { recursive: true });
|
||||
await mkdir(output, { recursive: true });
|
||||
const exists = async (path) =>
|
||||
access(path).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const fetchBytes = async (name) => {
|
||||
const response = await fetch(
|
||||
`https://nodejs.org/dist/v${nodeVersion}/${name}`,
|
||||
{
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
},
|
||||
);
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Node build prerequisite download failed: ${name}: HTTP ${response.status}`,
|
||||
);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
};
|
||||
const checksums = (await fetchBytes("SHASUMS256.txt")).toString("utf8");
|
||||
const verifiedDownload = async (name, path) => {
|
||||
if (!(await exists(path))) await writeFile(path, await fetchBytes(name));
|
||||
const digest = createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const expected = checksums
|
||||
.split("\n")
|
||||
.find((line) => line.slice(66) === name)
|
||||
?.slice(0, 64);
|
||||
if (expected === undefined || digest !== expected)
|
||||
throw new Error(`Node build prerequisite SHA-256 mismatch: ${name}`);
|
||||
};
|
||||
const cachedHeaders = join(
|
||||
homedir(),
|
||||
".cache",
|
||||
"node-gyp",
|
||||
nodeVersion,
|
||||
"include",
|
||||
"node",
|
||||
);
|
||||
let headers = cachedHeaders;
|
||||
if (!(await exists(join(headers, "node_api.h")))) {
|
||||
const archive = join(cache, `node-v${nodeVersion}-headers.tar.gz`);
|
||||
await verifiedDownload(`node-v${nodeVersion}-headers.tar.gz`, archive);
|
||||
run("tar", ["xzf", archive, "-C", cache]);
|
||||
headers = join(cache, `node-v${nodeVersion}`, "include", "node");
|
||||
}
|
||||
const artifact = "rea-windows-x64.node";
|
||||
// MSVC short-import libraries do not produce a correct Node import table with
|
||||
// GNU ld. Generate a GNU import library for the exact stable Node-API symbols.
|
||||
const sources = ["addon.cc", "filesystem.cc", "process.cc"];
|
||||
const sourceText = (
|
||||
await Promise.all(
|
||||
sources.map((path) =>
|
||||
readFile(join(root, "native", "windows", "src", path), "utf8"),
|
||||
),
|
||||
)
|
||||
).join("\n");
|
||||
const symbols = [
|
||||
...new Set(
|
||||
[...sourceText.matchAll(/\b(napi_[a-z0-9_]+)\s*\(/gu)].map(
|
||||
(match) => match[1],
|
||||
),
|
||||
),
|
||||
].sort();
|
||||
const definition = join(cache, "node-api.def");
|
||||
const library = join(cache, "libnode-api.a");
|
||||
await writeFile(
|
||||
definition,
|
||||
`LIBRARY node.exe\nEXPORTS\n${symbols.join("\n")}\n`,
|
||||
);
|
||||
run("x86_64-w64-mingw32-dlltool", ["-d", definition, "-l", library]);
|
||||
run(compiler, [
|
||||
"-std=c++17",
|
||||
"-O2",
|
||||
"-DNAPI_VERSION=8",
|
||||
"-D_WIN32_WINNT=0x0A00",
|
||||
"-shared",
|
||||
"-static",
|
||||
"-static-libgcc",
|
||||
"-static-libstdc++",
|
||||
"-Wl,--no-insert-timestamp",
|
||||
`-I${headers}`,
|
||||
"native/windows/src/addon.cc",
|
||||
"native/windows/src/filesystem.cc",
|
||||
"native/windows/src/process.cc",
|
||||
library,
|
||||
"-ladvapi32",
|
||||
"-lbcrypt",
|
||||
"-lntdll",
|
||||
"-o",
|
||||
join(output, artifact),
|
||||
]);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(join(root, "package.json"), "utf8"),
|
||||
);
|
||||
const bytes = await readFile(join(output, artifact));
|
||||
const metadata = {
|
||||
packageVersion: packageJson.version,
|
||||
platform: "win32",
|
||||
architecture: "x64",
|
||||
abiVersion: 1,
|
||||
nodeApiVersion: 8,
|
||||
artifact,
|
||||
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
await writeFile(
|
||||
join(output, "manifest.json"),
|
||||
`${JSON.stringify(metadata, null, 2)}\n`,
|
||||
);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...metadata, artifactBytes: bytes.length })}\n`,
|
||||
);
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { link, mkdir, open, rm } from "node:fs/promises";
|
||||
import { resolve, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { JADX_RELEASE } from "../src/android/JadxRelease.ts";
|
||||
import fixture from "./fixtures/android-apidemos.json" with { type: "json" };
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length > 1)
|
||||
throw new Error("Usage: node scripts/fetch-android-fixtures.mjs [DIRECTORY]");
|
||||
const directory = resolve(
|
||||
args[0] ??
|
||||
fileURLToPath(new URL("../_reference/apk-integration/", import.meta.url)),
|
||||
);
|
||||
await mkdir(directory, { recursive: true });
|
||||
const hashFile = async (path) => {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
for (const item of [
|
||||
{
|
||||
url: JADX_RELEASE.url,
|
||||
sha256: JADX_RELEASE.sha256,
|
||||
filename: "jadx-headless-mcp-0.7.1-all.jar",
|
||||
},
|
||||
fixture,
|
||||
]) {
|
||||
const path = join(directory, item.filename);
|
||||
try {
|
||||
const digest = await hashFile(path);
|
||||
if (digest !== item.sha256)
|
||||
throw new Error(
|
||||
`Existing ${path} has a different SHA-256; it was not overwritten`,
|
||||
);
|
||||
console.log(`Verified existing ${path}: ${digest}`);
|
||||
continue;
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof Error && "code" in cause && cause.code === "ENOENT"))
|
||||
throw cause;
|
||||
}
|
||||
const temporary = `${path}.${randomUUID()}.partial`;
|
||||
try {
|
||||
const response = await fetch(item.url, {
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
});
|
||||
if (!response.ok || response.body === null)
|
||||
throw new Error(`Download failed: ${item.url}: HTTP ${response.status}`);
|
||||
const handle = await open(temporary, "wx", 0o600);
|
||||
const hash = createHash("sha256");
|
||||
try {
|
||||
for await (const chunk of response.body) {
|
||||
hash.update(chunk);
|
||||
await handle.writeFile(chunk);
|
||||
}
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
const digest = hash.digest("hex");
|
||||
if (digest !== item.sha256)
|
||||
throw new Error(
|
||||
`SHA-256 mismatch for ${item.filename}: expected ${item.sha256}, received ${digest}`,
|
||||
);
|
||||
await link(temporary, path);
|
||||
console.log(`Downloaded and verified ${path}: ${digest}`);
|
||||
} finally {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Appium ApiDemos",
|
||||
"version": "6.0.18",
|
||||
"source": "https://github.com/appium/android-apidemos",
|
||||
"source_revision": "3e716ee04ef3390284bfab761e28da8c15c08143",
|
||||
"license": "Apache-2.0",
|
||||
"license_url": "https://github.com/appium/android-apidemos/blob/3e716ee04ef3390284bfab761e28da8c15c08143/LICENSE",
|
||||
"url": "https://github.com/appium/android-apidemos/releases/download/v6.0.18/ApiDemos-debug.apk",
|
||||
"filename": "ApiDemos-debug.apk",
|
||||
"sha256": "a9eecf37b26cd084855c530db81c2bb1b91f4c1b095a04f47aa7c20e2791f686",
|
||||
"package": "io.appium.android.apis",
|
||||
"class_name": "io.appium.android.apis.ApiDemos",
|
||||
"method_name": "onCreate"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const root = resolve(
|
||||
process.env.REA_FIRMWARE_FIXTURE_ROOT ??
|
||||
"_reference/firmware-integration/generated",
|
||||
);
|
||||
for (const [command, args, purpose] of [
|
||||
[
|
||||
process.env.REA_FIRMWARE_PYTHON ?? "python3",
|
||||
["--version"],
|
||||
"Python 3 standard-library fixture generator",
|
||||
],
|
||||
[
|
||||
process.env.REA_FIRMWARE_CC ?? "cc",
|
||||
["--version"],
|
||||
"host ELF fixture compiler",
|
||||
],
|
||||
]) {
|
||||
try {
|
||||
await exec(command, args);
|
||||
} catch (cause) {
|
||||
throw new Error(`fixtures:firmware requires ${purpose}: ${command}`, {
|
||||
cause,
|
||||
});
|
||||
}
|
||||
}
|
||||
const ext4 =
|
||||
process.env.REA_FIRMWARE_VERIFY_EXT4 === "1"
|
||||
? [process.env.REA_FIRMWARE_MKE2FS_COMMAND ?? "mke2fs"]
|
||||
: [];
|
||||
for (const command of ext4) await exec(command, ["-V"]);
|
||||
const generated = await exec(process.env.REA_FIRMWARE_PYTHON ?? "python3", [
|
||||
fileURLToPath(new URL("./lib/generate-firmware-fixture.py", import.meta.url)),
|
||||
root,
|
||||
process.env.REA_FIRMWARE_CC ?? "cc",
|
||||
...ext4,
|
||||
]);
|
||||
console.log(generated.stdout.trim());
|
||||
@@ -109,6 +109,8 @@ const canonicalizeJson = (value) => {
|
||||
|
||||
const payloadJson = JSON.stringify(
|
||||
canonicalizeJson({ catalog, auxiliaryProviders }),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
const source = await formatGeneratedFile(
|
||||
"src/generatedMcpToolCatalog.ts",
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
if (process.arch !== "x64" || !["linux", "win32"].includes(process.platform))
|
||||
throw new Error(
|
||||
"NativeAOT fixture lane requires Linux or Windows x64; it does not cross-compile NativeAOT.",
|
||||
);
|
||||
const runtime = process.platform === "win32" ? "win-x64" : "linux-x64";
|
||||
const directory = resolve(
|
||||
process.env.REA_NATIVEAOT_FIXTURE_ROOT ??
|
||||
join(root, "_reference/nativeaot-integration/generated"),
|
||||
);
|
||||
const output = join(directory, runtime);
|
||||
await mkdir(output, { recursive: true });
|
||||
const env = {
|
||||
...process.env,
|
||||
DOTNET_CLI_HOME: join(output, "cli-home"),
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1",
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1",
|
||||
DOTNET_PROCESSOR_COUNT: "1",
|
||||
COMPlus_GCHeapHardLimit: "30000000",
|
||||
NUGET_PACKAGES: process.env.NUGET_PACKAGES ?? join(directory, "nuget"),
|
||||
};
|
||||
await writeFile(
|
||||
join(output, "global.json"),
|
||||
JSON.stringify({ sdk: { version: "8.0.416", rollForward: "disable" } }) +
|
||||
"\n",
|
||||
);
|
||||
const run = (command, args) => {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: output,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
timeout: 300_000,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
if (result.error || result.status !== 0)
|
||||
throw new Error(
|
||||
`NativeAOT fixture lane requires/action failed ${command}: ${result.error?.message ?? ""}\n${result.stdout ?? ""}${result.stderr ?? ""}`,
|
||||
);
|
||||
return result.stdout;
|
||||
};
|
||||
const dotnet = process.env.REA_NATIVEAOT_DOTNET ?? "dotnet";
|
||||
if (run(dotnet, ["--version"]).trim() !== "8.0.416")
|
||||
throw new Error(
|
||||
"NativeAOT fixtures require existing .NET SDK 8.0.416; REA does not install SDKs.",
|
||||
);
|
||||
if (process.platform === "linux") {
|
||||
run(process.env.REA_NATIVEAOT_STRIP ?? "strip", ["--version"]);
|
||||
run(process.env.REA_NATIVEAOT_NM ?? "nm", ["--version"]);
|
||||
run(process.env.REA_NATIVEAOT_CC ?? "cc", ["--version"]);
|
||||
run("clang", ["--version"]);
|
||||
}
|
||||
const map = join(output, "NativeAotFixture.map");
|
||||
run(dotnet, [
|
||||
"publish",
|
||||
join(root, "tests/fixtures/nativeaot/NativeAotFixture.csproj"),
|
||||
"-r",
|
||||
runtime,
|
||||
"-c",
|
||||
"Release",
|
||||
"-o",
|
||||
join(output, "symbols"),
|
||||
"--maxcpucount:1",
|
||||
`-p:BaseIntermediateOutputPath=${join(output, "obj")}/`,
|
||||
`-p:BaseOutputPath=${join(output, "bin")}/`,
|
||||
`-p:NativeAotMapPath=${map}`,
|
||||
]);
|
||||
const name =
|
||||
process.platform === "win32" ? "NativeAotFixture.exe" : "NativeAotFixture";
|
||||
const binary = join(output, "symbols", name);
|
||||
let symbols = "";
|
||||
if (process.platform === "linux") {
|
||||
// Preserve the symbol table as an independent oracle while avoiding a full
|
||||
// runtime DWARF import in this metadata-specific lane.
|
||||
run(process.env.REA_NATIVEAOT_STRIP ?? "strip", ["--strip-debug", binary]);
|
||||
symbols = run(process.env.REA_NATIVEAOT_NM ?? "nm", ["-n", binary]);
|
||||
await mkdir(join(output, "stripped"), { recursive: true });
|
||||
const stripped = join(output, "stripped", name);
|
||||
await copyFile(binary, stripped);
|
||||
run(process.env.REA_NATIVEAOT_STRIP ?? "strip", ["--strip-all", stripped]);
|
||||
} else {
|
||||
symbols = await readFile(map, "utf8");
|
||||
}
|
||||
const bytes = await readFile(binary);
|
||||
const candidates = [];
|
||||
for (let offset = 0; offset <= bytes.length - 16; offset++) {
|
||||
if (
|
||||
bytes.readUInt32LE(offset) !== 0x00525452 ||
|
||||
bytes[offset + 14] !== 24 ||
|
||||
bytes[offset + 15] !== 1
|
||||
)
|
||||
continue;
|
||||
const count = bytes.readUInt16LE(offset + 12);
|
||||
if (count === 0 || count > 80 || offset + 16 + count * 24 > bytes.length)
|
||||
continue;
|
||||
candidates.push({
|
||||
offset,
|
||||
major: bytes.readUInt16LE(offset + 4),
|
||||
minor: bytes.readUInt16LE(offset + 6),
|
||||
sections: Array.from({ length: count }, (_, index) => {
|
||||
const row = offset + 16 + index * 24;
|
||||
return {
|
||||
type: bytes.readUInt32LE(row),
|
||||
flags: bytes.readUInt32LE(row + 4),
|
||||
start: `0x${bytes.readBigUInt64LE(row + 8).toString(16)}`,
|
||||
end: `0x${bytes.readBigUInt64LE(row + 16).toString(16)}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (
|
||||
candidates.length !== 1 ||
|
||||
candidates[0].major !== 9 ||
|
||||
candidates[0].minor !== 1
|
||||
)
|
||||
throw new Error(
|
||||
"NativeAOT fixture did not produce exactly one independent RTR 9.1 directory.",
|
||||
);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(join(output, "symbols.txt"), symbols);
|
||||
await writeFile(
|
||||
join(output, "oracle.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
sdk: "8.0.416",
|
||||
runtime_version: "8.0.22",
|
||||
runtime,
|
||||
binary,
|
||||
sha256,
|
||||
headers: candidates,
|
||||
expected_strings: [
|
||||
"REA_NATIVEAOT_FROZEN",
|
||||
"REA_NATIVEAOT_BASE",
|
||||
"REA_NATIVEAOT_DERIVED",
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
// A source-built negative input, not a downloaded third-party target.
|
||||
if (process.platform === "linux") {
|
||||
const source = join(output, "ordinary.c");
|
||||
await writeFile(
|
||||
source,
|
||||
"int ordinary_probe(int x) { return x * 3 + 1; }\nint main(void) { return ordinary_probe(1); }\n",
|
||||
);
|
||||
run(process.env.REA_NATIVEAOT_CC ?? "cc", [
|
||||
"-g",
|
||||
"-O0",
|
||||
source,
|
||||
"-o",
|
||||
join(output, "ordinary"),
|
||||
]);
|
||||
// Small source-built producer regressions run in seconds: no executable
|
||||
// patching, large runtime import, or downloaded opaque negative inputs.
|
||||
const row = "struct Row { unsigned type, flags; const char *start, *end; };";
|
||||
const header =
|
||||
"struct Header { unsigned signature; unsigned short major, minor; unsigned flags; unsigned short count; unsigned char size, type; struct Row row; };";
|
||||
for (const kind of ["unsupported", "malformed", "ambiguous"]) {
|
||||
const major = kind === "unsupported" ? 99 : 9;
|
||||
const declaration = `const struct Header __attribute__((aligned(8))) probe = {0x00525452, ${major}, 1, 0, 1, 24, 1, {207, 1, data + 8, data}};`;
|
||||
const extra =
|
||||
kind === "ambiguous" ? declaration.replace("probe", "second_probe") : "";
|
||||
const negative = join(output, `${kind}.c`);
|
||||
await writeFile(
|
||||
negative,
|
||||
`${row}\n${header}\nconst char data[16] = {0};\n${declaration}\n${extra}\nint main(void) { return 0; }\n`,
|
||||
);
|
||||
run(process.env.REA_NATIVEAOT_CC ?? "cc", [
|
||||
"-O0",
|
||||
"-fno-pie",
|
||||
"-no-pie",
|
||||
negative,
|
||||
"-o",
|
||||
join(output, kind),
|
||||
]);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`PASS source-built ${runtime} NativeAOT fixture, independent RTR 9.1 oracle and SHA-256 ${sha256}; no target execution`,
|
||||
);
|
||||
@@ -0,0 +1,120 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import {
|
||||
cleanupOwnedProcessGroup,
|
||||
readProcessRunId,
|
||||
} from "../../dist/process/ProcessOwnership.js";
|
||||
|
||||
/** Exercise a real CLI signal while the owned Java engine is still running. */
|
||||
export const verifyAndroidCliCancellation = async ({
|
||||
entrypoint,
|
||||
environment,
|
||||
path,
|
||||
repository,
|
||||
execute,
|
||||
}) => {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[entrypoint, "inspect-android-package", path, "--format", "json"],
|
||||
{ cwd: repository, env: environment, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const exited = new Promise((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
let group;
|
||||
let ownership;
|
||||
try {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
const table = (
|
||||
await execute("ps", ["-eo", "pid=,ppid=,pgid=,args="], {
|
||||
timeout: 5_000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
})
|
||||
).stdout;
|
||||
for (const row of table.split("\n")) {
|
||||
const entry = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/u.exec(row);
|
||||
if (
|
||||
entry !== null &&
|
||||
Number(entry[2]) === child.pid &&
|
||||
entry[4].includes("engine.jar") &&
|
||||
entry[4].includes("-Xmx512m")
|
||||
) {
|
||||
group = Number(entry[3]);
|
||||
const runId = await readProcessRunId(Number(entry[1]));
|
||||
assert.ok(
|
||||
runId !== undefined,
|
||||
"Owned Java process did not carry an identity token",
|
||||
);
|
||||
ownership = {
|
||||
runId,
|
||||
leaderPid: Number(entry[1]),
|
||||
processGroupId: group,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (group !== undefined) break;
|
||||
if (child.exitCode !== null || child.signalCode !== null)
|
||||
throw new Error(
|
||||
`CLI exited before acquiring JADX: ${stdout}\n${stderr}`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
assert.ok(
|
||||
group !== undefined,
|
||||
"CLI did not acquire the expected owned Java group",
|
||||
);
|
||||
child.kill("SIGTERM");
|
||||
let timer;
|
||||
const result = await Promise.race([
|
||||
exited,
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error("CLI cancellation did not finish cleanup")),
|
||||
10_000,
|
||||
);
|
||||
}),
|
||||
]).finally(() => clearTimeout(timer));
|
||||
assert.equal(result.code, 143, `${stdout}\n${stderr}`);
|
||||
assert.equal(result.signal, null);
|
||||
assert.equal(JSON.parse(stdout).code, "cancelled");
|
||||
assert.throws(() => process.kill(-group, 0), { code: "ESRCH" });
|
||||
console.log(
|
||||
"PASS real CLI SIGTERM cancellation and owned Java group cleanup",
|
||||
);
|
||||
} finally {
|
||||
await cleanupCancellation(child, exited, ownership);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupCancellation = async (child, exited, ownership) => {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill("SIGTERM");
|
||||
let timer;
|
||||
await Promise.race([
|
||||
exited,
|
||||
new Promise((resolve) => {
|
||||
timer = setTimeout(resolve, 2_000);
|
||||
}),
|
||||
]).finally(() => clearTimeout(timer));
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill("SIGKILL");
|
||||
await exited;
|
||||
}
|
||||
}
|
||||
if (ownership !== undefined)
|
||||
assert.equal(
|
||||
(await cleanupOwnedProcessGroup(ownership)).cleaned,
|
||||
true,
|
||||
"Verifier could not confirm its owned Java group cleanup",
|
||||
);
|
||||
};
|
||||
@@ -6,12 +6,12 @@ export const toolFamilyCatalog = (sources) => {
|
||||
{
|
||||
id: "direct",
|
||||
surface: "official-proxy",
|
||||
contracts: sources.toolContracts.OFFICIAL_TOOL_CONTRACTS,
|
||||
contracts: sources.officialContracts.OFFICIAL_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "enhanced",
|
||||
surface: "enhanced",
|
||||
contracts: sources.toolContracts.ENHANCED_TOOL_CONTRACTS,
|
||||
contracts: sources.enhancedContracts.ENHANCED_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "native",
|
||||
@@ -31,6 +31,16 @@ export const toolFamilyCatalog = (sources) => {
|
||||
...sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "firmware",
|
||||
surface: "firmware-provider",
|
||||
contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "android",
|
||||
surface: "android-provider",
|
||||
contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "browser",
|
||||
surface: "browser-provider",
|
||||
@@ -59,7 +69,7 @@ export const toolFamilyCatalog = (sources) => {
|
||||
{
|
||||
id: "session",
|
||||
surface: "session",
|
||||
contracts: sources.toolContracts.SESSION_TOOL_CONTRACTS,
|
||||
contracts: sources.sessionContracts.SESSION_TOOL_CONTRACTS,
|
||||
},
|
||||
].map(({ id, surface, contracts }) => ({
|
||||
id,
|
||||
@@ -96,6 +106,10 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
|
||||
contracts: sources.ghidraProvider.GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.idaProvider.IDA_PROVIDER_IDENTITY,
|
||||
contracts: sources.idaProvider.IDA_PROVIDER_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
|
||||
@@ -113,6 +127,22 @@ export const providerCatalog = (sources) => {
|
||||
contracts:
|
||||
sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.firmwareProvider.BINWALK_PROVIDER_IDENTITY,
|
||||
contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "inspect_firmware_regions",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.firmwareProvider.UNBLOB_PROVIDER_IDENTITY,
|
||||
contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "extract_firmware",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.androidProvider.JADX_PROVIDER_IDENTITY,
|
||||
contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
|
||||
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
|
||||
|
||||
@@ -37,9 +37,16 @@ const SOURCE_PATHS = {
|
||||
catalogIdentity: "dist/catalogIdentity.js",
|
||||
cli: "dist/cli.js",
|
||||
toolContracts: "dist/contracts/toolContracts.js",
|
||||
officialContracts: "dist/contracts/officialToolContracts.js",
|
||||
enhancedContracts: "dist/contracts/enhancedToolContracts.js",
|
||||
sessionContracts: "dist/contracts/sessionToolContracts.js",
|
||||
nativeContracts: "dist/contracts/nativeToolContracts.js",
|
||||
artifactContracts: "dist/contracts/artifactToolContracts.js",
|
||||
managedContracts: "dist/contracts/managedToolContracts.js",
|
||||
firmwareContracts: "dist/contracts/firmwareToolContracts.js",
|
||||
firmwareProvider: "dist/firmware/FirmwareRelease.js",
|
||||
androidContracts: "dist/contracts/androidToolContracts.js",
|
||||
androidProvider: "dist/android/JadxRelease.js",
|
||||
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
|
||||
browserContracts: "dist/contracts/browserToolContracts.js",
|
||||
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
|
||||
@@ -50,6 +57,7 @@ const SOURCE_PATHS = {
|
||||
supportedClients: "dist/application/SupportedClients.js",
|
||||
hopperProvider: "dist/hopper/HopperProvider.js",
|
||||
ghidraProvider: "dist/ghidra/GhidraProvider.js",
|
||||
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
||||
nativeProvider: "dist/native/NativeMacOSProvider.js",
|
||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||
browserProvider: "dist/browser/CdpBrowserProvider.js",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { ProductCatalog } from "./product-catalog.mjs";
|
||||
|
||||
export function skillReferenceIssues(
|
||||
skillRoot: string,
|
||||
): Promise<readonly string[]>;
|
||||
|
||||
export function documentationFactIssues(
|
||||
root: string,
|
||||
catalog: ProductCatalog,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
const README_PATHS = [
|
||||
"README.md",
|
||||
@@ -28,9 +28,49 @@ const requireText = (issues, path, content, expected) => {
|
||||
if (!content.includes(expected)) issues.push(`${path}: missing ${expected}`);
|
||||
};
|
||||
|
||||
/** Check that local Markdown references travel with an installed skill bundle. */
|
||||
export const skillReferenceIssues = async (skillRoot) => {
|
||||
const issues = [];
|
||||
const paths = (await readdir(skillRoot, { recursive: true }))
|
||||
.filter((path) => path.endsWith(".md"))
|
||||
.sort();
|
||||
for (const path of paths) {
|
||||
const content = await readFile(join(skillRoot, path), "utf8");
|
||||
for (const match of content.matchAll(/\[[^\]\n]*\]\(([^)\s]+)\)/gu)) {
|
||||
const destination = match[1];
|
||||
if (/^(?:https?:\/\/|mailto:|#)/u.test(destination)) continue;
|
||||
const target = resolve(
|
||||
dirname(join(skillRoot, path)),
|
||||
destination.split("#")[0],
|
||||
);
|
||||
const withinBundle = relative(resolve(skillRoot), target);
|
||||
if (
|
||||
withinBundle === ".." ||
|
||||
withinBundle.startsWith(`..${sep}`) ||
|
||||
isAbsolute(withinBundle)
|
||||
) {
|
||||
issues.push(
|
||||
`${path}: reference escapes installed skill bundle: ${destination}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (!(await stat(target)).isFile())
|
||||
issues.push(`${path}: reference is not a file: ${destination}`);
|
||||
} catch (cause) {
|
||||
if (cause?.code !== "ENOENT") throw cause;
|
||||
issues.push(`${path}: missing skill reference: ${destination}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
};
|
||||
|
||||
/** Return every caller-visible documentation mismatch against canonical facts. */
|
||||
export const documentationFactIssues = async (root, catalog) => {
|
||||
const issues = [];
|
||||
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 content = await readFile(join(root, path), "utf8");
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Generate firmware bytes using standard format producers, with an independent oracle."""
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
source = root / "probe.c"
|
||||
source.write_text("int firmware_probe(int x){return x*7+3;} int main(void){return firmware_probe(4);}\n")
|
||||
subprocess.run([sys.argv[2], "-O0", "-g", "-fno-inline", str(source), "-o", str(root / "probe")], check=True)
|
||||
payloads = {"etc/rea.conf": b"rea_fixture=true\n", "usr/bin/probe": (root / "probe").read_bytes()}
|
||||
buffer = io.BytesIO()
|
||||
with tarfile.open(fileobj=buffer, mode="w", format=tarfile.USTAR_FORMAT) as archive:
|
||||
for name, data in payloads.items():
|
||||
info = tarfile.TarInfo(name)
|
||||
info.size = len(data)
|
||||
info.mode = 0o644
|
||||
info.mtime = 0
|
||||
archive.addfile(info, io.BytesIO(data))
|
||||
compressed = gzip.compress(buffer.getvalue(), mtime=0)
|
||||
image = b"REA firmware fixture\n".ljust(64, b"\xff") + compressed + b"\x00" * 128
|
||||
(root / "firmware.bin").write_bytes(image)
|
||||
(root / "oracle.json").write_text(json.dumps({
|
||||
"generator": "Python tarfile USTAR + gzip; host cc; no target execution",
|
||||
"offset": 64, "length": len(compressed), "size": len(image),
|
||||
"sha256": hashlib.sha256(image).hexdigest(),
|
||||
"files": {name: {"sha256": hashlib.sha256(data).hexdigest(), "size": len(data)} for name, data in payloads.items()},
|
||||
}, indent=2) + "\n")
|
||||
print(root)
|
||||
|
||||
if len(sys.argv) > 3:
|
||||
tree = root / "ext4-root"
|
||||
tree.mkdir(exist_ok=True)
|
||||
for name, data in payloads.items():
|
||||
target = tree / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
subprocess.run([sys.argv[3], "-q", "-t", "ext4", "-F", "-d", str(tree), str(root / "ext4.bin"), "8192"], check=True)
|
||||
@@ -39,7 +39,7 @@ const observeVerifierLineage = async (run, observedAt) => {
|
||||
run,
|
||||
observedAt,
|
||||
null,
|
||||
"Windows verifier lineage ownership is unavailable without Job Objects",
|
||||
"The verifier has no retained native Job Object lease for lineage observation; provider jobs are verified by their native owners",
|
||||
);
|
||||
try {
|
||||
const { members, observerPid } = await processSnapshot();
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ const runtimeFiles = isMcpMode
|
||||
|
||||
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
process.stderr.write(
|
||||
`REA's compiled runtime is missing. Run \`npm ci\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
|
||||
`REA's compiled runtime is missing. Run \`npm ci && npm run build:cached\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else if (isMcpMode) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { constants } from "node:buffer";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { canonicalDigest } from "../dist/domain/comparisonSemantics.js";
|
||||
import { createEvidence, parseEvidence } from "../dist/domain/evidence.js";
|
||||
|
||||
// This opt-in check crosses the actual engine string limit; keep it outside
|
||||
// routine source tests. Build first, then run this script with the local Node.
|
||||
const leaf = "x".repeat(1024 * 1024);
|
||||
const encodedLeaf = JSON.stringify(leaf);
|
||||
const count = Math.ceil(constants.MAX_STRING_LENGTH / encodedLeaf.length) + 1;
|
||||
const value = { payload: Array.from({ length: count }, () => leaf) };
|
||||
const expected = createHash("sha256");
|
||||
expected.update('{"payload":[');
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
if (index > 0) expected.update(",");
|
||||
expected.update(encodedLeaf);
|
||||
}
|
||||
expected.update("]}");
|
||||
assert.equal(canonicalDigest(value), expected.digest("hex"));
|
||||
|
||||
const evidence = createEvidence(
|
||||
undefined,
|
||||
{ id: "large-json-fixture", name: "Large JSON fixture", version: "1" },
|
||||
{ operation: "inspect_fixture", parameters: {}, result: value },
|
||||
);
|
||||
assert.equal(parseEvidence(evidence).evidence_id, evidence.evidence_id);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verified: true,
|
||||
serialized_value_bytes: count * encodedLeaf.length + count - 1 + 14,
|
||||
engine_string_limit: constants.MAX_STRING_LENGTH,
|
||||
evidence_id: evidence.evidence_id,
|
||||
})}\n`,
|
||||
);
|
||||
@@ -12,10 +12,15 @@ const buildManagedFixtures = async (workspace) => {
|
||||
writeFile(
|
||||
managedPath,
|
||||
buildManagedPeFixture({
|
||||
methods: [
|
||||
{ name: "Main", flags: 0x0016 },
|
||||
{ name: "NativeMessageBox", flags: 0x2016, rva: 0 },
|
||||
],
|
||||
pinvoke: {
|
||||
moduleName: "user32.dll",
|
||||
importName: "MessageBoxW",
|
||||
mappingFlags: 0x0345,
|
||||
memberRow: 2,
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -59,7 +64,7 @@ const verifyManagedMembers = async ({ cli, managedPath, environment }) => {
|
||||
);
|
||||
if (
|
||||
managedMembers.operation !== "inspect_managed_members" ||
|
||||
managedMembers.normalized_result?.methods?.length !== 1
|
||||
managedMembers.normalized_result?.methods?.length !== 2
|
||||
)
|
||||
throw new Error("packaged managed member CLI failed");
|
||||
return managedMembers;
|
||||
|
||||
@@ -2,11 +2,12 @@ import {
|
||||
lstat,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { spawn } from "@lydell/node-pty";
|
||||
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
runWithStatus,
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
|
||||
|
||||
const OPENCODE_ORIGINAL = `{
|
||||
// OpenCode preferences must survive setup.
|
||||
@@ -473,19 +475,23 @@ const assertSkill = async ({ skillPath, siblingSkillPath, root }) => {
|
||||
);
|
||||
if (skill !== canonicalSkill)
|
||||
throw new Error("packaged skill did not match its canonical source");
|
||||
const installedReference = await readFile(
|
||||
join(skillPath, "..", "references/javascript-applications.md"),
|
||||
"utf8",
|
||||
);
|
||||
const canonicalReference = await readFile(
|
||||
join(
|
||||
root,
|
||||
"skills/reverse-engineer-anything/references/javascript-applications.md",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
if (installedReference !== canonicalReference)
|
||||
throw new Error("packaged skill references did not match canonical source");
|
||||
const canonicalRoot = join(root, "skills/reverse-engineer-anything");
|
||||
for (const reference of await readdir(join(canonicalRoot, "references"))) {
|
||||
const installed = await readFile(
|
||||
join(dirname(skillPath), "references", reference),
|
||||
"utf8",
|
||||
);
|
||||
const canonical = await readFile(
|
||||
join(canonicalRoot, "references", reference),
|
||||
"utf8",
|
||||
);
|
||||
if (installed !== canonical)
|
||||
throw new Error(
|
||||
`packaged skill reference did not match canonical source: ${reference}`,
|
||||
);
|
||||
}
|
||||
const issues = await skillReferenceIssues(dirname(skillPath));
|
||||
if (issues.length > 0) throw new Error(issues.join("\n"));
|
||||
if ((await readFile(siblingSkillPath, "utf8")) !== "unrelated skill\n")
|
||||
throw new Error("packaged skill installation modified a sibling skill");
|
||||
};
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
if (process.platform !== "win32" || process.arch !== "x64")
|
||||
throw new Error(
|
||||
"Packaged Windows Ghidra acceptance requires a Windows x64 host.",
|
||||
);
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-packaged-ghidra-"));
|
||||
const exec = promisify(execFile);
|
||||
let packageRoot = resolve(process.argv[2] ?? ".");
|
||||
// The default lane verifies the npm artifact in an isolated prefix. Explicit
|
||||
// package roots support an already-installed artifact without a second install.
|
||||
if (process.argv[2] === undefined) {
|
||||
try {
|
||||
const npmEntry =
|
||||
process.env.npm_execpath ??
|
||||
join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js");
|
||||
const packed = JSON.parse(
|
||||
(
|
||||
await exec(
|
||||
process.execPath,
|
||||
[
|
||||
npmEntry,
|
||||
"pack",
|
||||
"--json",
|
||||
"--ignore-scripts",
|
||||
"--pack-destination",
|
||||
workspace,
|
||||
],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
)
|
||||
).stdout,
|
||||
);
|
||||
assert.equal(
|
||||
packed.length,
|
||||
1,
|
||||
"The package lane must produce one artifact.",
|
||||
);
|
||||
const prefix = join(workspace, "installed");
|
||||
await exec(
|
||||
process.execPath,
|
||||
[
|
||||
npmEntry,
|
||||
"install",
|
||||
"--prefix",
|
||||
prefix,
|
||||
"--ignore-scripts",
|
||||
"--omit=dev",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
join(workspace, packed[0].filename),
|
||||
],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
packageRoot = join(prefix, "node_modules", "rea-agents");
|
||||
} catch (cause) {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
const target = resolve(
|
||||
process.argv[3] ?? "build/fixtures/rea-ghidra-windows.exe",
|
||||
);
|
||||
let TOOL_CONTRACTS, native, token, sha256;
|
||||
try {
|
||||
({ TOOL_CONTRACTS } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/contracts/toolContracts.js"))
|
||||
));
|
||||
const { requireWindowsNativeAuthority } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/windows/WindowsNativeLoader.js"))
|
||||
);
|
||||
native = requireWindowsNativeAuthority();
|
||||
token = native.call("process_caller_token", []);
|
||||
assert.equal(
|
||||
token.administrator,
|
||||
false,
|
||||
"The real-provider lane must run as an ordinary user.",
|
||||
);
|
||||
assert.equal(
|
||||
token.elevated,
|
||||
false,
|
||||
"The real-provider lane must use a non-elevated token.",
|
||||
);
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
sha256 = digest(await readFile(target));
|
||||
} catch (cause) {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
throw cause;
|
||||
}
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const runtimeParent = join(workspace, "runtime with spaces");
|
||||
await mkdir(runtimeParent);
|
||||
const environment = {
|
||||
...process.env,
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_LOG_LEVEL: "error",
|
||||
GHIDRA_HEADLESS_MAXMEM: "512m",
|
||||
NODE_OPTIONS: "--max-old-space-size=512",
|
||||
TEMP: runtimeParent,
|
||||
TMP: runtimeParent,
|
||||
HOME: workspace,
|
||||
USERPROFILE: workspace,
|
||||
APPDATA: join(workspace, "appdata"),
|
||||
LOCALAPPDATA: join(workspace, "localappdata"),
|
||||
};
|
||||
const entry = join(packageRoot, "scripts/rea.mjs");
|
||||
const report = {
|
||||
ok: false,
|
||||
platform: "win32",
|
||||
architecture: "x64",
|
||||
nonAdmin: true,
|
||||
targetSha256: sha256,
|
||||
cli: {},
|
||||
mcpOperations: [],
|
||||
nativeControls: "available",
|
||||
runtimeCleanup: false,
|
||||
};
|
||||
const assertCleanup = async () => {
|
||||
const entries = await readdir(runtimeParent);
|
||||
report.runtimeResidualCount = entries.length;
|
||||
report.runtimeResidualKinds = entries.map((name) =>
|
||||
name.startsWith("rea-")
|
||||
? "rea-runtime"
|
||||
: name.startsWith("hsperfdata_")
|
||||
? "jvm-performance-data"
|
||||
: "other",
|
||||
);
|
||||
assert.equal(entries.length, 0, "Provider runtime cleanup left files.");
|
||||
};
|
||||
try {
|
||||
const providers = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, "providers", "--json"], {
|
||||
env: environment,
|
||||
timeout: 30_000,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
assert.ok(JSON.stringify(providers).includes("ghidra"));
|
||||
report.cli.providers = true;
|
||||
const inspected = await exec(
|
||||
process.execPath,
|
||||
[entry, "inspect", target, "--provider", "ghidra", "--format", "json"],
|
||||
{ env: environment, timeout: 360_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
const inspection = JSON.parse(inspected.stdout);
|
||||
assert.ok(inspection.error === undefined);
|
||||
report.cli.inspect = true;
|
||||
await assertCleanup();
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entry, "mcp"],
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "rea-packaged-windows-ghidra",
|
||||
version: "1.0.0",
|
||||
});
|
||||
const call = async (name, arguments_) => {
|
||||
report.lastOperation = name;
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
const contract = TOOL_CONTRACTS.find((value) => value.name === name);
|
||||
assert.ok(contract !== undefined, `Missing canonical contract: ${name}`);
|
||||
const argumentsParsed = contract.inputSchema.parse(arguments_);
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: argumentsParsed },
|
||||
{ timeout: 360_000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, `Packaged operation failed: ${name}`);
|
||||
const structured =
|
||||
result.structuredContent ??
|
||||
JSON.parse(result.content.find((item) => item.type === "text").text);
|
||||
const parsed = contract.outputSchema.parse(structured);
|
||||
report.mcpOperations.push(name);
|
||||
return parsed.result;
|
||||
};
|
||||
try {
|
||||
await client.connect(transport);
|
||||
await call("open_binary", { path: target, provider_id: "ghidra" });
|
||||
assert.equal((await call("list_documents", {})).length, 1);
|
||||
await call("list_names", {});
|
||||
const procedures = await call("list_procedures", {});
|
||||
assert.ok(procedures.length > 0);
|
||||
assert.ok((await call("list_segments", {})).length > 0);
|
||||
await call("list_strings", {});
|
||||
const procedure =
|
||||
procedures.find((item) => item.procedure?.external === false) ??
|
||||
procedures[0];
|
||||
assert.ok(procedure !== undefined);
|
||||
const loadImage = await call("inspect_native_load_image", {});
|
||||
assert.equal(
|
||||
loadImage.status,
|
||||
"unsupported",
|
||||
"PE load-image attestation must remain explicitly unsupported.",
|
||||
);
|
||||
assert.ok(loadImage.observations !== undefined);
|
||||
const memory = await call("read_bytes", {
|
||||
address: procedure.address,
|
||||
length: 16,
|
||||
});
|
||||
const mapping = await call("address_to_file_offset", {
|
||||
address: procedure.address,
|
||||
});
|
||||
assert.equal(memory.complete, true);
|
||||
assert.equal(memory.returned_bytes, 16);
|
||||
assert.equal(
|
||||
memory.bytes_hex,
|
||||
(await readFile(target))
|
||||
.subarray(mapping.file_offset, mapping.file_offset + 16)
|
||||
.toString("hex"),
|
||||
);
|
||||
await call("address_name", { address: procedure.address });
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: procedure.value }),
|
||||
procedure.address,
|
||||
);
|
||||
await call("resolve_containing_procedure", { address: procedure.address });
|
||||
await call("search_procedures", { pattern: procedure.value });
|
||||
await call("search_strings", { pattern: "rea" });
|
||||
for (const name of [
|
||||
"procedure_assembly",
|
||||
"procedure_callees",
|
||||
"procedure_callers",
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"read_function_instructions",
|
||||
"procedure_references",
|
||||
"analyze_function",
|
||||
])
|
||||
await call(name, { procedure: procedure.value });
|
||||
await call("xrefs", { address: procedure.address });
|
||||
await call("inspect_native_data_type", { type: "/undefined8" });
|
||||
await call("inspect_native_instruction", { address: procedure.address });
|
||||
await call("resolve_native_call_targets", { address: procedure.address });
|
||||
const expected = (
|
||||
await import(
|
||||
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
|
||||
)
|
||||
).GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(name) =>
|
||||
name !== "ping" &&
|
||||
name !== "shutdown" &&
|
||||
name !== "annotate_native_function",
|
||||
);
|
||||
assert.ok(
|
||||
expected.every((name) => report.mcpOperations.includes(name)),
|
||||
"Packaged verification omitted an admitted operation.",
|
||||
);
|
||||
await call("binary_session", {});
|
||||
await call("close_binary", {});
|
||||
await assertCleanup();
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
assert.equal(
|
||||
digest(await readFile(target)),
|
||||
sha256,
|
||||
"Analysis modified the selected target.",
|
||||
);
|
||||
assert.deepEqual(
|
||||
native.call("process_caller_token", []),
|
||||
token,
|
||||
"Analysis changed caller token authority.",
|
||||
);
|
||||
report.runtimeCleanup = true;
|
||||
report.ok = true;
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
} catch (cause) {
|
||||
report.failure = {
|
||||
name: cause.name,
|
||||
message: cause.message
|
||||
.replaceAll(packageRoot, "<package>")
|
||||
.replaceAll(target, "<target>")
|
||||
.replaceAll(workspace, "<workspace>"),
|
||||
};
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
throw cause;
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
@@ -27,7 +27,11 @@ export async function verifyPackedBridge({
|
||||
"extends HeadlessScript",
|
||||
'request.method.equals("ping")',
|
||||
'request.method.equals("shutdown")',
|
||||
'result.addProperty("read_only", true)',
|
||||
'boolean readOnly = !descriptor.transport.equals("unix-socket")',
|
||||
'result.addProperty("read_only", readOnly)',
|
||||
'case "annotate_native_function"',
|
||||
'currentProgram.startTransaction("REA function annotations")',
|
||||
"currentProgram.endTransaction(transaction, commit)",
|
||||
"analysisTimeoutOccurred()",
|
||||
])
|
||||
if (!ghidraSource.includes(commitment))
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { access, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "../dist/android/AndroidTargetSnapshot.js";
|
||||
import { JADX_RELEASE } from "../dist/android/JadxRelease.js";
|
||||
import fixture from "./fixtures/android-apidemos.json" with { type: "json" };
|
||||
import { verifyAndroidCliCancellation } from "./lib/android-cli-cancellation.mjs";
|
||||
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const apk = process.env.REA_ANDROID_TEST_APK;
|
||||
const jar = process.env.REA_JADX_MCP_JAR;
|
||||
if (apk === undefined || jar === undefined)
|
||||
throw new Error(
|
||||
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run node scripts/fetch-android-fixtures.mjs and select the downloaded files; see docs/android-analysis.md.",
|
||||
);
|
||||
await access(apk);
|
||||
await access(jar);
|
||||
assert.equal(
|
||||
await hashAndroidFile(apk),
|
||||
fixture.sha256,
|
||||
"Real Android lane requires the fixed ApiDemos release fixture",
|
||||
);
|
||||
assert.equal(
|
||||
await hashAndroidFile(jar),
|
||||
JADX_RELEASE.sha256,
|
||||
"Real Android lane requires the audited headless JADX release JAR",
|
||||
);
|
||||
const java =
|
||||
process.env.JAVA_HOME === undefined
|
||||
? "java"
|
||||
: resolve(process.env.JAVA_HOME, "bin/java");
|
||||
const execute = promisify(execFile);
|
||||
await execute(java, ["-version"], { timeout: 10_000 });
|
||||
const entrypoint =
|
||||
process.env.REA_ANDROID_ENTRYPOINT ?? resolve(repository, "scripts/rea.mjs");
|
||||
const environment = { ...process.env, REA_JADX_MCP_JAR: resolve(jar) };
|
||||
const path = resolve(apk);
|
||||
const tasks = [
|
||||
{
|
||||
name: "inspect_android_package",
|
||||
args: ["inspect-android-package", path],
|
||||
input: { path },
|
||||
},
|
||||
{
|
||||
name: "search_android_classes",
|
||||
args: ["search-android-classes", path, "ApiDemos"],
|
||||
input: { path, query: "ApiDemos" },
|
||||
},
|
||||
{
|
||||
name: "inspect_android_class",
|
||||
args: ["inspect-android-class", path, fixture.class_name],
|
||||
input: { path, class_name: fixture.class_name },
|
||||
},
|
||||
{
|
||||
name: "inspect_android_method",
|
||||
args: [
|
||||
"inspect-android-method",
|
||||
path,
|
||||
fixture.class_name,
|
||||
fixture.method_name,
|
||||
],
|
||||
input: {
|
||||
path,
|
||||
class_name: fixture.class_name,
|
||||
method_name: fixture.method_name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trace_android_references",
|
||||
args: [
|
||||
"trace-android-references",
|
||||
path,
|
||||
fixture.class_name,
|
||||
"--method-name",
|
||||
"activityIntent",
|
||||
],
|
||||
input: {
|
||||
path,
|
||||
class_name: fixture.class_name,
|
||||
method_name: "activityIntent",
|
||||
},
|
||||
},
|
||||
];
|
||||
const cliResults = new Map();
|
||||
for (const task of tasks) {
|
||||
const response = await execute(
|
||||
process.execPath,
|
||||
[entrypoint, ...task.args, "--format", "json"],
|
||||
{
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
timeout: 150_000,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(response.stdout));
|
||||
assert.equal(evidence.subject?.digest.sha256, fixture.sha256);
|
||||
assert.equal(evidence.operation, task.name);
|
||||
cliResults.set(task.name, evidence);
|
||||
console.log(`PASS CLI ${task.name}`);
|
||||
}
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const client = new Client({ name: "rea-real-android-verifier", version: "1" });
|
||||
try {
|
||||
await client.connect(transport, { timeout: 30_000 });
|
||||
for (const task of tasks) {
|
||||
const response = await client.callTool(
|
||||
{ name: task.name, arguments: task.input },
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
const structured = response.structuredContent;
|
||||
assert.ok(structured !== undefined && structured.evidence !== undefined);
|
||||
const evidence = parseEvidence(structured.evidence);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result,
|
||||
cliResults.get(task.name).normalized_result,
|
||||
);
|
||||
// Raw observations retain different workspace paths and load durations.
|
||||
// Validate each evidence ID independently; compare portable results/intent.
|
||||
assert.equal(structured.evidence_id, evidence.evidence_id);
|
||||
assert.deepEqual(evidence.parameters, cliResults.get(task.name).parameters);
|
||||
assert.deepEqual(evidence.provider, cliResults.get(task.name).provider);
|
||||
assert.deepEqual(
|
||||
evidence.subject.digest,
|
||||
cliResults.get(task.name).subject.digest,
|
||||
);
|
||||
assert.deepEqual(
|
||||
evidence.limitations,
|
||||
cliResults.get(task.name).limitations,
|
||||
);
|
||||
assert.equal(evidence.confidence, cliResults.get(task.name).confidence);
|
||||
assert.deepEqual(structured.result, evidence.normalized_result);
|
||||
console.log(`PASS MCP/CLI parity ${task.name}`);
|
||||
}
|
||||
} catch (cause) {
|
||||
process.stderr.write(stderr);
|
||||
throw cause;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
const packageResult = cliResults.get(
|
||||
"inspect_android_package",
|
||||
).normalized_result;
|
||||
assert.equal(packageResult.package_name, fixture.package);
|
||||
assert.equal(packageResult.version_name, fixture.version);
|
||||
assert.equal(packageResult.min_sdk, "26");
|
||||
assert.equal(packageResult.target_sdk, "33");
|
||||
assert.equal(packageResult.manifest.status, "complete");
|
||||
assert.ok(packageResult.manifest.text.includes(fixture.package));
|
||||
assert.ok(packageResult.permissions.includes("android.permission.INTERNET"));
|
||||
assert.equal(packageResult.signature_verification, "not_performed");
|
||||
assert.ok(
|
||||
cliResults
|
||||
.get("search_android_classes")
|
||||
.normalized_result.matches.includes(fixture.class_name),
|
||||
);
|
||||
const classResult = cliResults.get("inspect_android_class").normalized_result;
|
||||
assert.ok(
|
||||
classResult.methods.some((method) => method.name === fixture.method_name),
|
||||
);
|
||||
const method = cliResults.get("inspect_android_method").normalized_result;
|
||||
assert.equal(method.representation, "java");
|
||||
assert.equal(method.body_status, "available");
|
||||
assert.equal(method.source.status, "complete");
|
||||
assert.ok(method.source.text.includes("getIntent()"));
|
||||
assert.ok(method.source.text.includes("setListAdapter"));
|
||||
assert.equal(method.method.dex_descriptor, null);
|
||||
const references = cliResults.get("trace_android_references").normalized_result;
|
||||
assert.ok(references.total > 0);
|
||||
assert.ok(
|
||||
references.references.every(
|
||||
(reference) =>
|
||||
reference.dex_offset === null && reference.source_line === null,
|
||||
),
|
||||
);
|
||||
await verifyAndroidCliCancellation({
|
||||
entrypoint,
|
||||
environment,
|
||||
path,
|
||||
repository,
|
||||
execute,
|
||||
});
|
||||
assert.equal(
|
||||
await hashAndroidFile(apk),
|
||||
fixture.sha256,
|
||||
"Original APK must remain unchanged",
|
||||
);
|
||||
const report = {
|
||||
lane: "real-android",
|
||||
platform: process.platform,
|
||||
fixture: { version: fixture.version, sha256: fixture.sha256 },
|
||||
engine: packageResult.engine,
|
||||
operations: tasks.map((task) => task.name),
|
||||
cli_mcp_parity: true,
|
||||
original_apk_unchanged: true,
|
||||
apk_executed: false,
|
||||
cli_sigterm_cleanup: true,
|
||||
};
|
||||
if (process.env.REA_ANDROID_REPORT !== undefined)
|
||||
await writeFile(
|
||||
process.env.REA_ANDROID_REPORT,
|
||||
JSON.stringify(report, null, 2) + "\n",
|
||||
);
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
@@ -36,7 +36,7 @@ import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
|
||||
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
|
||||
|
||||
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 30_000;
|
||||
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000;
|
||||
const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret";
|
||||
const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret";
|
||||
process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
symlink,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { firmwareResultSchemas } from "../dist/domain/firmwareAnalysis.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { hashFirmwareFile } from "../dist/firmware/FirmwareFiles.js";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const entrypoint = resolve(
|
||||
process.env.REA_FIRMWARE_TEST_ENTRYPOINT ??
|
||||
join(repository, "scripts/rea.mjs"),
|
||||
);
|
||||
if (process.platform !== "linux")
|
||||
throw new Error(
|
||||
"verify:firmware requires Linux; other hosts are not verified",
|
||||
);
|
||||
for (const variable of ["REA_BINWALK_COMMAND", "REA_UNBLOB_COMMAND"]) {
|
||||
const path = process.env[variable];
|
||||
if (path === undefined)
|
||||
throw new Error(
|
||||
`verify:firmware requires ${variable}; see docs/firmware-analysis.md`,
|
||||
);
|
||||
await access(path);
|
||||
}
|
||||
const fixtureRoot = resolve(
|
||||
process.env.REA_FIRMWARE_FIXTURE_ROOT ??
|
||||
join(repository, "_reference/firmware-integration/generated"),
|
||||
);
|
||||
await access(join(fixtureRoot, "oracle.json")).catch((cause) => {
|
||||
throw new Error(
|
||||
"verify:firmware requires generated fixtures; run npm run fixtures:firmware",
|
||||
{ cause },
|
||||
);
|
||||
});
|
||||
const oracle = JSON.parse(
|
||||
await readFile(join(fixtureRoot, "oracle.json"), "utf8"),
|
||||
);
|
||||
const path = join(fixtureRoot, "firmware.bin");
|
||||
assert.equal(await hashFirmwareFile(path), oracle.sha256);
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-real-firmware-"));
|
||||
const environment = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([, value]) => value !== undefined),
|
||||
);
|
||||
const client = new Client({ name: "rea-real-firmware-verifier", version: "1" });
|
||||
const cli = async (args, env = environment) => {
|
||||
const response = await exec(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--format", "json"],
|
||||
{
|
||||
env,
|
||||
cwd: repository,
|
||||
timeout: 150_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
return parseEvidence(JSON.parse(response.stdout));
|
||||
};
|
||||
const call = async (name, args) => {
|
||||
const response = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
assert.ok(response.structuredContent !== undefined);
|
||||
return response.structuredContent;
|
||||
};
|
||||
const verifyRegions = (evidence) => {
|
||||
assert.equal(evidence.subject.digest.sha256, oracle.sha256);
|
||||
const result = firmwareResultSchemas.inspect_firmware_regions.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
const gzip = result.regions.find(
|
||||
(region) => region.signature === "gzip" && region.offset === oracle.offset,
|
||||
);
|
||||
assert.ok(
|
||||
gzip !== undefined,
|
||||
"Binwalk must find the independently generated gzip offset",
|
||||
);
|
||||
assert.equal(gzip.reported_size, oracle.length);
|
||||
assert.equal(gzip.size_basis, "provider_reported_validation_unknown");
|
||||
return result.regions.map(({ provider_id, ...region }) => region);
|
||||
};
|
||||
const verifyFiles = async (evidence) => {
|
||||
const result = firmwareResultSchemas.extract_firmware.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
assert.ok(result.chunks.some((chunk) => chunk.handler === "gzip"));
|
||||
for (const [suffix, expected] of Object.entries(oracle.files)) {
|
||||
const file = result.files.find((candidate) =>
|
||||
candidate.relative_path.endsWith(`/${suffix}`),
|
||||
);
|
||||
assert.ok(file !== undefined, `Missing extracted ${suffix}`);
|
||||
assert.equal(file.sha256, expected.sha256);
|
||||
assert.equal(file.size, expected.size);
|
||||
assert.equal(await hashFirmwareFile(file.path), expected.sha256);
|
||||
assert.equal(file.original_file_range, null);
|
||||
assert.equal(file.runtime_address, null);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
try {
|
||||
const inspected = await cli(["inspect-firmware-regions", path]);
|
||||
const cliRegions = verifyRegions(inspected);
|
||||
const extracted = await cli([
|
||||
"extract-firmware",
|
||||
path,
|
||||
join(root, "cli-output"),
|
||||
]);
|
||||
const cliFiles = await verifyFiles(extracted);
|
||||
assert.equal(cliFiles.coverage, "partial");
|
||||
assert.ok(
|
||||
cliFiles.chunks.some(
|
||||
(chunk) =>
|
||||
chunk.handler === null &&
|
||||
chunk.range.offset === 0 &&
|
||||
chunk.range.length === 64,
|
||||
),
|
||||
);
|
||||
await client.connect(
|
||||
new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
}),
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const inspectedMcp = await call("inspect_firmware_regions", { path });
|
||||
const mcpEvidence = parseEvidence(inspectedMcp.evidence);
|
||||
assert.deepEqual(verifyRegions(mcpEvidence), cliRegions);
|
||||
const extractedMcp = await call("extract_firmware", {
|
||||
path,
|
||||
output_directory: join(root, "mcp-output"),
|
||||
});
|
||||
const mcpFiles = await verifyFiles(parseEvidence(extractedMcp.evidence));
|
||||
const contentIdentity = (result) =>
|
||||
result.files.map(({ relative_path, sha256, size }) => ({
|
||||
relative_path,
|
||||
sha256,
|
||||
size,
|
||||
}));
|
||||
assert.deepEqual(contentIdentity(mcpFiles), contentIdentity(cliFiles));
|
||||
const selected = await call("extract_firmware", {
|
||||
path,
|
||||
output_directory: join(root, "selected-output"),
|
||||
range: { offset: oracle.offset, length: oracle.length },
|
||||
});
|
||||
const selectedFiles = await verifyFiles(parseEvidence(selected.evidence));
|
||||
assert.equal(selectedFiles.selection.offset, oracle.offset);
|
||||
assert.equal(selectedFiles.selection.length, oracle.length);
|
||||
const depth = await call("extract_firmware", {
|
||||
path,
|
||||
output_directory: join(root, "depth-output"),
|
||||
max_depth: 1,
|
||||
});
|
||||
const depthResult = firmwareResultSchemas.extract_firmware.parse(
|
||||
depth.result,
|
||||
);
|
||||
assert.equal(depthResult.coverage, "partial");
|
||||
assert.ok(depthResult.depth_limited_paths.length > 0);
|
||||
console.log(
|
||||
"PASS real Binwalk/Unblob: CLI, MCP, independent offsets/digests, range extraction, unknown chunks and depth limits",
|
||||
);
|
||||
if (process.env.REA_FIRMWARE_VERIFY_EXT4 === "1") {
|
||||
const ext4 = join(fixtureRoot, "ext4.bin");
|
||||
await access(ext4);
|
||||
const response = await call("extract_firmware", {
|
||||
path: ext4,
|
||||
output_directory: join(root, "ext4-output"),
|
||||
});
|
||||
const result = firmwareResultSchemas.extract_firmware.parse(
|
||||
response.result,
|
||||
);
|
||||
assert.ok(result.chunks.some((chunk) => chunk.handler === "extfs"));
|
||||
assert.equal(result.coverage, "complete");
|
||||
for (const [suffix, expected] of Object.entries(oracle.files))
|
||||
assert.equal(
|
||||
result.files.find((file) => file.relative_path.endsWith(`/${suffix}`))
|
||||
?.sha256,
|
||||
expected.sha256,
|
||||
);
|
||||
console.log("PASS real ext4 extraction with debugfs; no mounts");
|
||||
// Preserve the process-ownership utility while withholding all extractors.
|
||||
const withoutExtractors = join(root, "without-extractors");
|
||||
await access("/bin/ps");
|
||||
await mkdir(withoutExtractors);
|
||||
await symlink("/bin/ps", join(withoutExtractors, "ps"));
|
||||
const missing = await cli(
|
||||
["extract-firmware", ext4, join(root, "missing-extractor-output")],
|
||||
{ ...environment, PATH: withoutExtractors },
|
||||
);
|
||||
const partial = firmwareResultSchemas.extract_firmware.parse(
|
||||
missing.normalized_result,
|
||||
);
|
||||
assert.equal(partial.coverage, "partial");
|
||||
assert.ok(JSON.stringify(partial.diagnostics).includes("debugfs"));
|
||||
console.log(
|
||||
"PASS real missing-extractor diagnostics retained despite Unblob exit 1",
|
||||
);
|
||||
}
|
||||
if (process.env.REA_FIRMWARE_VERIFY_GHIDRA === "1") {
|
||||
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
||||
throw new Error(
|
||||
"Firmware Ghidra handoff requires GHIDRA_INSTALL_DIR and Java; see docs/ghidra.md",
|
||||
);
|
||||
const child = mcpFiles.files.find((file) =>
|
||||
file.relative_path.endsWith("/usr/bin/probe"),
|
||||
);
|
||||
assert.ok(child !== undefined);
|
||||
await call("open_binary", { path: child.path, provider_id: "ghidra" });
|
||||
const dossier = await call("analyze_function", {
|
||||
procedure: "firmware_probe",
|
||||
});
|
||||
const evidence = parseEvidence(dossier.evidence);
|
||||
assert.equal(
|
||||
evidence.subject.digest.sha256,
|
||||
oracle.files["usr/bin/probe"].sha256,
|
||||
);
|
||||
const pseudocode = await call("procedure_pseudo_code", {
|
||||
procedure: "firmware_probe",
|
||||
});
|
||||
assert.equal(parseEvidence(pseudocode.evidence).provider.id, "ghidra");
|
||||
assert.ok(
|
||||
JSON.stringify(evidence.normalized_result).includes("firmware_probe"),
|
||||
);
|
||||
await call("close_binary", {});
|
||||
console.log(
|
||||
"PASS real Ghidra handoff of the selected extracted host ELF, bound to its digest",
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
await hashFirmwareFile(path),
|
||||
oracle.sha256,
|
||||
"Original firmware bytes must remain unchanged",
|
||||
);
|
||||
} finally {
|
||||
await client.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { buildDosComFixture } from "../tests/conformance/ghidra/dos-com-fixture.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
// This lane intentionally has no game paths, binary seeds or compiler dependency.
|
||||
if (process.argv.length !== 2)
|
||||
throw new Error("Usage: node scripts/verify-real-ghidra-com.mjs");
|
||||
if (!["linux", "darwin"].includes(process.platform))
|
||||
throw new Error(
|
||||
"verify:ghidra:com supports Linux/macOS hosts only; Windows P0 is PE x86-64 only.",
|
||||
);
|
||||
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
||||
throw new Error(
|
||||
"verify:ghidra:com prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).",
|
||||
);
|
||||
const installation = inspectGhidraInstallation({
|
||||
installDir: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { javaHome: process.env.JAVA_HOME }),
|
||||
});
|
||||
if (installation.status !== "available")
|
||||
throw new Error(
|
||||
`verify:ghidra:com prerequisite unavailable: ${JSON.stringify(installation)}`,
|
||||
);
|
||||
const captureDirectory = process.env.REA_COM_PROOF_CAPTURE_DIR;
|
||||
if (captureDirectory !== undefined) {
|
||||
assert.ok(
|
||||
isAbsolute(captureDirectory),
|
||||
"REA_COM_PROOF_CAPTURE_DIR must be absolute",
|
||||
);
|
||||
await mkdir(captureDirectory, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
const packageRoot = process.env.REA_COM_PROOF_PACKAGE_ROOT;
|
||||
if (packageRoot !== undefined && !isAbsolute(packageRoot))
|
||||
throw new Error("REA_COM_PROOF_PACKAGE_ROOT must be absolute");
|
||||
const entrypoint =
|
||||
packageRoot === undefined
|
||||
? fileURLToPath(new URL("./rea.mjs", import.meta.url))
|
||||
: join(packageRoot, "scripts", "rea.mjs");
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-com-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const fixture = buildDosComFixture();
|
||||
const targetPath = join(workspace, "fixture.com");
|
||||
await writeFile(targetPath, fixture.bytes, { mode: 0o600 });
|
||||
const sha256 = digest(fixture.bytes);
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
};
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-real-com-proof", version: "1" });
|
||||
let opened = false;
|
||||
let stderr = "";
|
||||
let report;
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
|
||||
const untyped = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: targetPath, provider_id: "ghidra" },
|
||||
});
|
||||
assert.equal(
|
||||
untyped.isError,
|
||||
true,
|
||||
"Headerless bytes were guessed as COM without explicit interpretation",
|
||||
);
|
||||
const target = await call("open_binary", {
|
||||
path: targetPath,
|
||||
provider_id: "ghidra",
|
||||
format: "dos-com",
|
||||
});
|
||||
opened = true;
|
||||
assert.equal(target.format, "dos-com");
|
||||
assert.equal(target.architecture, "x86");
|
||||
assert.equal(target.sha256, sha256);
|
||||
const image = await call("inspect_native_load_image");
|
||||
assert.equal(
|
||||
image.status,
|
||||
"verified",
|
||||
JSON.stringify(image.checks?.filter((c) => !c.matched)),
|
||||
);
|
||||
assert.equal(image.format, "dos-com");
|
||||
assert.equal(image.header_bytes, 0);
|
||||
assert.equal(image.module_bytes, fixture.bytes.length);
|
||||
assert.equal(image.entry.linear_address, fixture.entry);
|
||||
assert.equal(image.entry.offset, 256);
|
||||
assert.equal(image.observations.entry_context[0].registers.length, 4);
|
||||
const memory = await call("read_bytes", {
|
||||
address: fixture.entry,
|
||||
length: fixture.bytes.length,
|
||||
});
|
||||
assert.equal(memory.bytes_hex, fixture.bytes.toString("hex"));
|
||||
assert.equal(memory.complete, true);
|
||||
const mapping = await call("address_to_file_offset", {
|
||||
address: fixture.near,
|
||||
});
|
||||
assert.equal(mapping.file_offset, 32);
|
||||
const psp = await call("read_bytes", { address: "0x10000", length: 16 });
|
||||
assert.equal(psp.returned_bytes, 0);
|
||||
assert.equal(psp.complete, false);
|
||||
const tail = await call("read_bytes", { address: "0x1013e", length: 8 });
|
||||
assert.equal(tail.returned_bytes, 2);
|
||||
assert.equal(tail.complete, false);
|
||||
const procedures = await call("list_procedures");
|
||||
assert.ok(procedures.some((p) => p.address === fixture.entry));
|
||||
assert.ok(procedures.some((p) => p.address === fixture.near));
|
||||
const dossier = functionDossierSchema.parse(
|
||||
await call("analyze_function", { procedure: fixture.entry }),
|
||||
);
|
||||
assert.ok(dossier.pseudocode.trim().length > 0);
|
||||
assert.equal(dossier.procedure.body.available, true);
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: fixture.entry,
|
||||
});
|
||||
assert.equal(
|
||||
instruction.bytes.replaceAll(" ", "").toLowerCase(),
|
||||
fixture.entry_hex,
|
||||
);
|
||||
const cliImage = await cli("inspect-native-load-image");
|
||||
const stable = (x) => ({
|
||||
...x,
|
||||
observations: {
|
||||
...x.observations,
|
||||
source_files: x.observations.source_files.map(
|
||||
({ name, ...file }) => file,
|
||||
),
|
||||
},
|
||||
});
|
||||
assert.deepEqual(stable(cliImage.normalized_result), stable(image));
|
||||
assert.deepEqual(
|
||||
(
|
||||
await cli("read-bytes", fixture.entry, [
|
||||
"--length",
|
||||
String(fixture.bytes.length),
|
||||
])
|
||||
).normalized_result,
|
||||
memory,
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await cli("address-to-file-offset", fixture.near)).normalized_result,
|
||||
mapping,
|
||||
);
|
||||
const changes = {
|
||||
procedure: fixture.entry,
|
||||
name: "rea_entry",
|
||||
comment: "Regular\nUnicode 註記",
|
||||
inline_comment: "Inline finding",
|
||||
};
|
||||
// Prime provider inventories and the session snapshot before edits.
|
||||
await call("list_names");
|
||||
const updated = await call("annotate_native_function", changes);
|
||||
assert.equal(updated.annotations.name, changes.name);
|
||||
assert.equal(updated.annotations.comment, changes.comment);
|
||||
assert.equal(updated.annotations.inline_comment, changes.inline_comment);
|
||||
assert.equal(updated.dossier.procedure.name, changes.name);
|
||||
assert.ok(updated.dossier.pseudocode.includes(changes.name));
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: changes.name }),
|
||||
fixture.entry,
|
||||
);
|
||||
assert.equal(
|
||||
await call("address_name", { address: fixture.entry }),
|
||||
changes.name,
|
||||
);
|
||||
assert.ok(
|
||||
(await call("list_procedures")).some((p) => p.value === changes.name),
|
||||
);
|
||||
assert.ok((await call("list_names")).some((p) => p.value === changes.name));
|
||||
assert.equal(
|
||||
(await call("analyze_function", { procedure: fixture.entry })).procedure
|
||||
.name,
|
||||
changes.name,
|
||||
);
|
||||
const rejected = await client.callTool({
|
||||
name: "annotate_native_function",
|
||||
arguments: {
|
||||
procedure: fixture.entry,
|
||||
comment: "MUST ROLL BACK",
|
||||
name: "invalid name\n",
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
rejected.isError,
|
||||
true,
|
||||
"Ghidra accepted an invalid function name",
|
||||
);
|
||||
const retained = await call("annotate_native_function", {
|
||||
procedure: fixture.entry,
|
||||
name: changes.name,
|
||||
});
|
||||
assert.equal(
|
||||
retained.annotations.comment,
|
||||
changes.comment,
|
||||
"Failed edit leaked its earlier comment write",
|
||||
);
|
||||
assert.equal(retained.annotations.inline_comment, changes.inline_comment);
|
||||
assert.equal(retained.dossier.procedure.name, changes.name);
|
||||
const cleared = await call("annotate_native_function", {
|
||||
procedure: fixture.entry,
|
||||
comment: "",
|
||||
inline_comment: "",
|
||||
});
|
||||
assert.equal(cleared.annotations.comment, null);
|
||||
assert.equal(cleared.annotations.inline_comment, null);
|
||||
assert.ok(!cleared.dossier.comments.some((c) => c.address === fixture.entry));
|
||||
assert.equal(
|
||||
(
|
||||
await call("read_bytes", {
|
||||
address: fixture.entry,
|
||||
length: fixture.bytes.length,
|
||||
})
|
||||
).bytes_hex,
|
||||
fixture.bytes.toString("hex"),
|
||||
);
|
||||
assert.equal((await call("inspect_native_load_image")).status, "verified");
|
||||
const cliAnnotated = await cli("annotate-native-function", fixture.entry, [
|
||||
"--name",
|
||||
changes.name,
|
||||
"--comment",
|
||||
changes.comment,
|
||||
"--inline-comment",
|
||||
changes.inline_comment,
|
||||
]);
|
||||
assert.deepEqual(cliAnnotated.normalized_result, updated);
|
||||
if (captureDirectory !== undefined)
|
||||
await writeFile(
|
||||
join(captureDirectory, "image.json"),
|
||||
JSON.stringify(image, null, 2),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
await call("close_binary");
|
||||
opened = false;
|
||||
assert.equal(digest(await readFile(targetPath)), sha256);
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
lane: "ghidra-com",
|
||||
packaged_artifact: packageRoot !== undefined,
|
||||
host: process.platform,
|
||||
provider: { id: "ghidra", version: installation.providerVersion },
|
||||
target: { format: "dos-com", sha256, bytes: fixture.bytes.length },
|
||||
load_image: {
|
||||
status: image.status,
|
||||
checks: image.checks.length,
|
||||
entry: fixture.entry,
|
||||
entry_context: image.observations.entry_context,
|
||||
},
|
||||
transport: { cli: true, stdio_mcp: true },
|
||||
memory_evidence: {
|
||||
complete_file_read: true,
|
||||
source_offset: 32,
|
||||
psp_unmapped: true,
|
||||
partial_read_bytes: 2,
|
||||
},
|
||||
functions: { entry: true, near: true, pseudocode_nonempty: true },
|
||||
annotations: {
|
||||
atomic_rollback: true,
|
||||
fresh_inventories: true,
|
||||
fresh_decompiler: true,
|
||||
clear_comments: true,
|
||||
cli_mcp_parity: true,
|
||||
},
|
||||
original_unchanged: true,
|
||||
limitations: image.limitations,
|
||||
};
|
||||
} catch (error) {
|
||||
if (stderr.trim()) process.stderr.write(stderr);
|
||||
throw error;
|
||||
} finally {
|
||||
if (opened)
|
||||
await call("close_binary").catch((error) =>
|
||||
process.stderr.write(String(error)),
|
||||
);
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
assert.ok(report);
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
assert.equal(verifierRun.process_lineage.status, "verified");
|
||||
assert.deepEqual(verifierRun.process_lineage.descendants, []);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
verifier_run: verifierRun,
|
||||
...report,
|
||||
cleanup: "complete",
|
||||
}) + "\n",
|
||||
);
|
||||
function digest(bytes) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
async function call(name, args = {}) {
|
||||
const value = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(value.isError, true, JSON.stringify(value));
|
||||
if (name === "open_binary") return value.structuredContent.result;
|
||||
if (name === "close_binary") return value.structuredContent;
|
||||
const evidence = parseEvidence(value.structuredContent.evidence);
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.deepEqual(evidence.normalized_result, value.structuredContent.result);
|
||||
return value.structuredContent.result;
|
||||
}
|
||||
async function cli(command, address, options = []) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
command,
|
||||
targetPath,
|
||||
...(address === undefined ? [] : [address]),
|
||||
...options,
|
||||
"--target-format",
|
||||
"dos-com",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.subject.format, "dos-com");
|
||||
assert.equal(evidence.analysis_profile.parameters.loader, "BinaryLoader");
|
||||
assert.equal(
|
||||
evidence.analysis_profile.parameters.language_id,
|
||||
"x86:LE:16:Real Mode",
|
||||
);
|
||||
assert.equal(evidence.analysis_profile.parameters.entry_offset, "0x100");
|
||||
assert.equal(digest(await readFile(targetPath)), sha256);
|
||||
return evidence;
|
||||
}
|
||||
@@ -106,6 +106,78 @@ try {
|
||||
assert.equal(target.kind, "executable");
|
||||
assert.equal(target.architecture, "x86");
|
||||
assert.equal(target.sha256, sha256);
|
||||
const image = await call("inspect_native_load_image");
|
||||
assert.equal(
|
||||
image.status,
|
||||
"verified",
|
||||
JSON.stringify(image.checks?.filter((check) => !check.matched)),
|
||||
);
|
||||
assert.equal(image.target_sha256, sha256);
|
||||
assert.equal(image.header_bytes, fixture.header_bytes);
|
||||
assert.equal(image.module_bytes, fixture.module_bytes);
|
||||
assert.equal(image.entry.linear_address, expected.entry);
|
||||
assert.ok(image.checks.every((check) => check.matched));
|
||||
const cliImage = await cliEvidence("inspect-native-load-image");
|
||||
// Snapshot filenames are session-specific. Every measured byte/range/check is stable.
|
||||
const stableImage = (value) => ({
|
||||
...value,
|
||||
observations: {
|
||||
...value.observations,
|
||||
source_files: value.observations.source_files.map(
|
||||
({ name: _name, ...file }) => file,
|
||||
),
|
||||
},
|
||||
});
|
||||
assert.deepEqual(stableImage(cliImage.normalized_result), stableImage(image));
|
||||
const mapped = await call("address_to_file_offset", {
|
||||
address: expected.entry,
|
||||
});
|
||||
assert.equal(mapped.file_offset, fixture.header_bytes);
|
||||
const headerMapping = await call("address_to_file_offset", {
|
||||
address: "HEADER:0x0",
|
||||
});
|
||||
assert.equal(headerMapping.file_offset, 0);
|
||||
const fileRelocation = await call("address_to_file_offset", {
|
||||
address: address(fixture.relocation_offset),
|
||||
});
|
||||
assert.equal(
|
||||
fileRelocation.file_offset,
|
||||
fixture.header_bytes + fixture.relocation_offset,
|
||||
);
|
||||
const memoryRelocation = await call("read_bytes", {
|
||||
address: address(fixture.relocation_offset),
|
||||
length: 2,
|
||||
});
|
||||
assert.equal(memoryRelocation.bytes_hex, "0410");
|
||||
assert.equal(memoryRelocation.complete, true);
|
||||
assert.deepEqual(
|
||||
(
|
||||
await cliEvidence("read-bytes", address(fixture.relocation_offset), [
|
||||
"--length",
|
||||
"2",
|
||||
])
|
||||
).normalized_result,
|
||||
memoryRelocation,
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await cliEvidence("address-to-file-offset", expected.entry))
|
||||
.normalized_result,
|
||||
mapped,
|
||||
);
|
||||
const tail = await call("read_bytes", {
|
||||
address: address(fixture.module_bytes - 2),
|
||||
length: 8,
|
||||
});
|
||||
assert.equal(tail.returned_bytes, 2);
|
||||
assert.equal(tail.complete, false);
|
||||
const unmapped = await call("read_bytes", { address: "0x80000", length: 8 });
|
||||
assert.equal(unmapped.returned_bytes, 0);
|
||||
assert.equal(unmapped.complete, false);
|
||||
const noSource = await client.callTool({
|
||||
name: "address_to_file_offset",
|
||||
arguments: { address: "0x80000" },
|
||||
});
|
||||
assert.equal(noSource.isError, true);
|
||||
const procedures = await call("list_procedures");
|
||||
assert.ok(
|
||||
Array.isArray(procedures),
|
||||
@@ -330,6 +402,22 @@ try {
|
||||
load_segment: profile.parameters.load_segment,
|
||||
},
|
||||
transport: { cli: true, stdio_mcp: true },
|
||||
load_image: {
|
||||
status: image.status,
|
||||
checks: image.checks.length,
|
||||
mappings: image.observations.mappings.length,
|
||||
relocations: image.observations.relocations.length,
|
||||
cli_mcp_parity: true,
|
||||
},
|
||||
memory_evidence: {
|
||||
relocated_word_hex: memoryRelocation.bytes_hex,
|
||||
entry_file_offset: mapped.file_offset,
|
||||
header_file_offset: headerMapping.file_offset,
|
||||
partial_read_bytes: tail.returned_bytes,
|
||||
unmapped_read_bytes: unmapped.returned_bytes,
|
||||
unmapped_file_offset_rejected: true,
|
||||
cli_mcp_parity: true,
|
||||
},
|
||||
functions: facts,
|
||||
instruction_oracles: {
|
||||
entry_bytes: hexBytes(move.bytes),
|
||||
@@ -400,13 +488,21 @@ function hexBytes(bytes) {
|
||||
return bytes.replace(/\s/gu, "").toLowerCase();
|
||||
}
|
||||
function assertProfile(profile) {
|
||||
assert.equal(
|
||||
profile.parameters.load_image_evidence,
|
||||
"independent-mz-mapping-relocations-v1",
|
||||
);
|
||||
assert.equal(
|
||||
profile.parameters.function_body_evidence,
|
||||
"complete-inclusive-ranges-v1",
|
||||
);
|
||||
assert.equal(profile.provider.id, "ghidra");
|
||||
assert.equal(profile.parameters.target_format, "dos-mz");
|
||||
assert.equal(profile.parameters.import_mode, "ephemeral-read-only");
|
||||
assert.equal(profile.parameters.import_mode, "ephemeral-source-immutable");
|
||||
assert.equal(
|
||||
profile.parameters.annotation_policy,
|
||||
"atomic-function-entry-metadata-v1",
|
||||
);
|
||||
assert.equal(profile.parameters.loader, "MzLoader");
|
||||
assert.equal(profile.parameters.language_id, "x86:LE:16:Real Mode");
|
||||
assert.equal(profile.parameters.compiler_spec_id, "default");
|
||||
@@ -471,14 +567,15 @@ async function evidenceCall(name, arguments_) {
|
||||
assertProfile(evidence.analysis_profile.parameters.upstream_analysis_profile);
|
||||
return evidence;
|
||||
}
|
||||
async function cliEvidence(command, procedure) {
|
||||
async function cliEvidence(command, procedure, options = []) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
command,
|
||||
targetPath,
|
||||
procedure,
|
||||
...(procedure === undefined ? [] : [procedure]),
|
||||
...options,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, isAbsolute, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { nativeDataTypeSchema } from "../dist/domain/nativeDataType.js";
|
||||
import { nativeLoadImageSchema } from "../dist/domain/nativeLoadImage.js";
|
||||
|
||||
const mode = process.argv[2] ?? "symbols";
|
||||
if (
|
||||
![
|
||||
"symbols",
|
||||
"stripped",
|
||||
"ordinary",
|
||||
"unsupported",
|
||||
"malformed",
|
||||
"ambiguous",
|
||||
"loader-failure",
|
||||
"default-native",
|
||||
].includes(mode) ||
|
||||
process.argv.length > 3
|
||||
)
|
||||
throw new Error(
|
||||
"Usage: verify-real-ghidra-nativeaot.mjs [symbols|stripped|ordinary|unsupported|malformed|ambiguous|loader-failure|default-native]",
|
||||
);
|
||||
if (process.platform !== "linux" || process.arch !== "x64")
|
||||
throw new Error(
|
||||
"verify:ghidra:nativeaot requires a Linux x64 host; Windows host mutation is not supported.",
|
||||
);
|
||||
for (const key of ["loader-failure", "default-native"].includes(mode)
|
||||
? ["GHIDRA_INSTALL_DIR"]
|
||||
: ["GHIDRA_INSTALL_DIR", "REA_GHIDRA_NATIVEAOT_JAR"])
|
||||
if (!process.env[key] || !isAbsolute(process.env[key]))
|
||||
throw new Error(
|
||||
`verify:ghidra:nativeaot prerequisite missing: absolute ${key}`,
|
||||
);
|
||||
const fixtureRoot = resolve(
|
||||
process.env.REA_NATIVEAOT_PROOF_FIXTURE_ROOT ??
|
||||
"_reference/nativeaot-integration/generated/linux-x64",
|
||||
);
|
||||
const oracle = JSON.parse(
|
||||
await readFile(join(fixtureRoot, "oracle.json"), "utf8"),
|
||||
);
|
||||
const symbolTarget = join(
|
||||
fixtureRoot,
|
||||
"symbols",
|
||||
oracle.runtime === "win-x64" ? "NativeAotFixture.exe" : "NativeAotFixture",
|
||||
);
|
||||
const target =
|
||||
mode === "stripped"
|
||||
? join(fixtureRoot, "stripped", basename(symbolTarget))
|
||||
: [
|
||||
"ordinary",
|
||||
"unsupported",
|
||||
"malformed",
|
||||
"ambiguous",
|
||||
"loader-failure",
|
||||
"default-native",
|
||||
].includes(mode)
|
||||
? join(
|
||||
fixtureRoot,
|
||||
["loader-failure", "default-native"].includes(mode)
|
||||
? "ordinary"
|
||||
: mode,
|
||||
)
|
||||
: symbolTarget;
|
||||
const bytes = await readFile(target);
|
||||
const sha256 = hash(bytes);
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-nativeaot-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const packageRoot = process.env.REA_NATIVEAOT_PROOF_PACKAGE_ROOT;
|
||||
if (packageRoot !== undefined && !isAbsolute(packageRoot))
|
||||
throw new Error("REA_NATIVEAOT_PROOF_PACKAGE_ROOT must be absolute");
|
||||
const entrypoint =
|
||||
packageRoot === undefined
|
||||
? fileURLToPath(new URL("./rea.mjs", import.meta.url))
|
||||
: join(packageRoot, "scripts/rea.mjs");
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
|
||||
REA_GHIDRA_NATIVEAOT_JAR: process.env.REA_GHIDRA_NATIVEAOT_JAR,
|
||||
GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM ?? "768M",
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
};
|
||||
if (mode === "default-native") delete env.REA_GHIDRA_NATIVEAOT_JAR;
|
||||
// Source-built linkage failure exercises the actual JVM loader boundary, not
|
||||
// a simulated producer report. Its class/JAR exist only in the owned workspace.
|
||||
if (mode === "loader-failure")
|
||||
env.REA_GHIDRA_NATIVEAOT_JAR = join(workspace, "bad-extension.jar");
|
||||
async function prepareLoaderFailure() {
|
||||
const source = join(workspace, "NativeAotExtension.java");
|
||||
const classes = join(workspace, "classes");
|
||||
await mkdir(classes);
|
||||
await writeFile(
|
||||
source,
|
||||
`package rea.extensions.nativeaot;
|
||||
public final class NativeAotExtension {
|
||||
static { fail(); }
|
||||
private static void fail() { throw new IllegalStateException("REA_INITIALIZER_FIXTURE"); }
|
||||
public NativeAotExtension() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
const command = (name) =>
|
||||
process.env.JAVA_HOME === undefined
|
||||
? name
|
||||
: join(process.env.JAVA_HOME, "bin", name);
|
||||
await promisify(execFile)(
|
||||
command("javac"),
|
||||
["-J-Xmx512m", "-J-XX:ActiveProcessorCount=1", "-d", classes, source],
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await promisify(execFile)(
|
||||
command("jar"),
|
||||
["--create", "--file", env.REA_GHIDRA_NATIVEAOT_JAR, "-C", classes, "."],
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
}
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-nativeaot-proof", version: "1" });
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
let opened = false;
|
||||
let report;
|
||||
try {
|
||||
if (mode === "loader-failure") await prepareLoaderFailure();
|
||||
await client.connect(transport);
|
||||
const response = await client.callTool(
|
||||
{ name: "open_binary", arguments: { path: target, provider_id: "ghidra" } },
|
||||
{ timeout: 360000 },
|
||||
);
|
||||
if (
|
||||
["unsupported", "malformed", "ambiguous", "loader-failure"].includes(mode)
|
||||
) {
|
||||
// MCP open binds a lazy session; the first inspection starts import.
|
||||
let failure = response;
|
||||
if (failure.isError !== true) {
|
||||
opened = true;
|
||||
failure = await client.callTool(
|
||||
{ name: "inspect_native_load_image", arguments: {} },
|
||||
{ timeout: 360000 },
|
||||
);
|
||||
}
|
||||
assert.equal(failure.isError, true, JSON.stringify(failure));
|
||||
const diagnostic = JSON.stringify(failure);
|
||||
assert.match(
|
||||
diagnostic,
|
||||
mode === "unsupported"
|
||||
? /Unsupported NativeAOT RTR format/
|
||||
: mode === "malformed"
|
||||
? /Reversed NativeAOT section/
|
||||
: mode === "loader-failure"
|
||||
? /ExceptionInInitializerError.*REA_INITIALIZER_FIXTURE/
|
||||
: /Ambiguous NativeAOT directory candidates/,
|
||||
);
|
||||
report = { mode, target, sha256, typed_failure: failure };
|
||||
} else if (mode === "default-native") {
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
opened = true;
|
||||
const image = nativeLoadImageSchema.parse(
|
||||
await call("inspect_native_load_image"),
|
||||
);
|
||||
assert.equal(image.observations.metadata_recovery, undefined);
|
||||
report = { mode, target, sha256, image };
|
||||
} else {
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
opened = true;
|
||||
const image = nativeLoadImageSchema.parse(
|
||||
await call("inspect_native_load_image"),
|
||||
);
|
||||
const summary = image.observations.metadata_recovery?.[0];
|
||||
assert.ok(
|
||||
summary,
|
||||
"Loaded-image inspection omitted inline runtime-format discovery",
|
||||
);
|
||||
assert.equal(summary.format, "dotnet-nativeaot");
|
||||
assert.equal(
|
||||
summary.analysis_artifact_sha256,
|
||||
hash(await readFile(env.REA_GHIDRA_NATIVEAOT_JAR)),
|
||||
);
|
||||
if (mode === "ordinary") {
|
||||
assert.equal(summary.status, "not_applicable");
|
||||
assert.equal(summary.method_tables, 0);
|
||||
assert.deepEqual(summary.types, []);
|
||||
report = { mode, target, sha256, summary };
|
||||
} else {
|
||||
assert.ok(["complete", "partial"].includes(summary.status));
|
||||
assert.equal(summary.format_major, 9);
|
||||
assert.equal(summary.format_minor, 1);
|
||||
assert.equal(summary.types.length, summary.method_tables);
|
||||
assert.ok(summary.method_tables > 0);
|
||||
assert.equal(
|
||||
summary.discovery,
|
||||
mode === "stripped" || oracle.runtime === "win-x64"
|
||||
? "signature-heuristic"
|
||||
: "symbol",
|
||||
);
|
||||
assert.equal(summary.derived_memory.file_offset, null);
|
||||
assert.ok(summary.coverage.frozen_objects_annotated > 0);
|
||||
// Independent compiler/linker symbols identify fixture classes. Recovered
|
||||
// Class_address names are never treated as the original source identities.
|
||||
const symbols = await readFile(join(fixtureRoot, "symbols.txt"), "utf8");
|
||||
const bias =
|
||||
oracle.runtime === "linux-x64"
|
||||
? BigInt(image.observations.image_base)
|
||||
: 0n;
|
||||
const addressOf = (name) => {
|
||||
const line = symbols
|
||||
.split(/\r?\n/u)
|
||||
.find((line) => line.split(/\s+/u).includes(name));
|
||||
assert.ok(line, `Independent symbol missing: ${name}`);
|
||||
const match =
|
||||
oracle.runtime === "linux-x64"
|
||||
? line.match(/^([a-f0-9]+)\s/iu)
|
||||
: line.match(/\s([a-f0-9]{16})\s/iu);
|
||||
assert.ok(match, `Unrecognized independent symbol line: ${line}`);
|
||||
return `0x${(BigInt(`0x${match[1]}`) + bias).toString(16)}`;
|
||||
};
|
||||
const fixtureSymbols =
|
||||
oracle.symbols ??
|
||||
(oracle.runtime === "win-x64"
|
||||
? {
|
||||
base: "??_7NativeAotFixture_BaseProbe@@6B@",
|
||||
derived: "??_7NativeAotFixture_DerivedProbe@@6B@",
|
||||
interface: "??_7NativeAotFixture_IProbe@@6B@",
|
||||
compute: "NativeAotFixture_DerivedProbe__Compute",
|
||||
dispatch: "NativeAotFixture_Program__Dispatch",
|
||||
}
|
||||
: {
|
||||
base: "_ZTV26NativeAotFixture_BaseProbe",
|
||||
derived: "_ZTV29NativeAotFixture_DerivedProbe",
|
||||
interface: "_ZTV23NativeAotFixture_IProbe",
|
||||
compute: "NativeAotFixture_DerivedProbe__Compute",
|
||||
dispatch: "NativeAotFixture_Program__Dispatch",
|
||||
});
|
||||
const lookup = (address) => {
|
||||
const identity = summary.types.find((type) => type.address === address);
|
||||
assert.ok(identity?.type, `Recovered identity missing at ${address}`);
|
||||
return identity.type;
|
||||
};
|
||||
const derivedAddress = addressOf(fixtureSymbols.derived);
|
||||
const baseAddress = addressOf(fixtureSymbols.base);
|
||||
const interfaceAddress = addressOf(fixtureSymbols.interface);
|
||||
const derived = nativeDataTypeSchema.parse(
|
||||
await call("inspect_native_data_type", {
|
||||
type: lookup(derivedAddress),
|
||||
}),
|
||||
);
|
||||
const metadata = derived.metadata_recovery;
|
||||
assert.ok(metadata);
|
||||
assert.equal(metadata.original_name, null);
|
||||
assert.equal(metadata.name_origin, "generated");
|
||||
assert.equal(metadata.method_table_address, derivedAddress);
|
||||
assert.equal(metadata.related_type.address, baseAddress);
|
||||
assert.ok(
|
||||
metadata.interfaces.some((type) => type.address === interfaceAddress),
|
||||
);
|
||||
assert.ok(
|
||||
metadata.virtual_slots.some(
|
||||
(slot) => slot.target_address === addressOf(fixtureSymbols.compute),
|
||||
),
|
||||
);
|
||||
const compute = await call("analyze_function", {
|
||||
procedure: addressOf(fixtureSymbols.compute),
|
||||
});
|
||||
assert.ok(compute.pseudocode.trim());
|
||||
assert.doesNotMatch(
|
||||
compute.pseudocode,
|
||||
/\b__thiscall\b/u,
|
||||
"Upstream x64 calling-convention rewrite leaked into recovered pseudocode",
|
||||
);
|
||||
assert.match(compute.pseudocode, /(?:0xb|\b11\b|<<)/u);
|
||||
const dispatch = await call("analyze_function", {
|
||||
procedure: addressOf(fixtureSymbols.dispatch),
|
||||
});
|
||||
assert.ok(dispatch.pseudocode.trim());
|
||||
for (const literal of oracle.expected_strings)
|
||||
assert.ok(
|
||||
(await call("search_strings", { pattern: literal })).length > 0,
|
||||
`Frozen string missing: ${literal}`,
|
||||
);
|
||||
const observed = await call("read_bytes", {
|
||||
address: metadata.virtual_slots[0].slot_address,
|
||||
length: 8,
|
||||
});
|
||||
assert.equal(observed.complete, true);
|
||||
assert.equal(
|
||||
`0x${Buffer.from(observed.bytes_hex, "hex").readBigUInt64LE().toString(16)}`,
|
||||
metadata.virtual_slots[0].target_address,
|
||||
);
|
||||
report = { mode, target, sha256, summary, derived, compute, dispatch };
|
||||
await call("close_binary");
|
||||
opened = false;
|
||||
// One representative CLI boundary, selected explicitly to avoid repeated
|
||||
// full NativeAOT auto-analysis during each focused iteration.
|
||||
if (process.env.REA_NATIVEAOT_PROOF_CLI === "1") {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"inspect-native-data-type",
|
||||
target,
|
||||
"--type",
|
||||
lookup(derivedAddress),
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 360000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result.metadata_recovery,
|
||||
metadata,
|
||||
);
|
||||
report.cli_metadata = evidence.normalized_result.metadata_recovery;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
hash(await readFile(target)),
|
||||
sha256,
|
||||
"Input executable changed",
|
||||
);
|
||||
if (opened) {
|
||||
await call("close_binary");
|
||||
opened = false;
|
||||
}
|
||||
await client.close();
|
||||
assert.deepEqual(
|
||||
await readdir(runtime),
|
||||
[],
|
||||
"Owned runtime/project/extension files leaked after close",
|
||||
);
|
||||
const capture = process.env.REA_NATIVEAOT_PROOF_CAPTURE_DIR;
|
||||
if (capture !== undefined) {
|
||||
assert.ok(isAbsolute(capture));
|
||||
await mkdir(capture, { recursive: true });
|
||||
await writeFile(
|
||||
join(capture, `${oracle.runtime}-${mode}.json`),
|
||||
JSON.stringify(report, null, 2) + "\n",
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`PASS real Ghidra NativeAOT ${oracle.runtime}/${mode}: MCP metadata, source identity and owned cleanup${report?.compute ? ", independent type/slot oracle and pseudocode" : ""}${report?.cli_metadata ? ", equivalent CLI" : ""}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(stderr);
|
||||
throw error;
|
||||
} finally {
|
||||
if (opened)
|
||||
await client
|
||||
.callTool({ name: "close_binary", arguments: {} })
|
||||
.catch(() => {});
|
||||
await client.close().catch(() => {});
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
function hash(bytes) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
async function call(name, args = {}) {
|
||||
const response = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 360000 },
|
||||
);
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
if (name === "close_binary") return response.structuredContent;
|
||||
const evidence = parseEvidence(response.structuredContent.evidence);
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result,
|
||||
response.structuredContent.result,
|
||||
);
|
||||
return response.structuredContent.result;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { access } from "node:fs/promises";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { isAbsolute, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -22,16 +22,26 @@ import {
|
||||
} from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES as SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(name) => name !== "annotate_native_function",
|
||||
);
|
||||
|
||||
if (process.platform !== "win32" || process.arch !== "x64")
|
||||
throw new Error(
|
||||
"The real Windows Ghidra verifier requires a Windows x64 host",
|
||||
);
|
||||
if (
|
||||
windowsP0Capabilities().some(
|
||||
({ available, effects }) => available && effects.mutatesArtifact,
|
||||
)
|
||||
)
|
||||
throw new Error("Windows Ghidra must not advertise database mutation");
|
||||
const installDir = process.env.GHIDRA_INSTALL_DIR;
|
||||
if (installDir === undefined || !isAbsolute(installDir))
|
||||
throw new Error(
|
||||
@@ -74,6 +84,7 @@ if (!profile.ok || profile.value.profile === null)
|
||||
throw new Error("Windows Ghidra profile could not be committed");
|
||||
|
||||
const client = new GhidraClient({
|
||||
platform: installation.platform,
|
||||
launcher: new GhidraHeadlessLauncher({
|
||||
analyzeHeadlessPath: installation.analyzeHeadlessPath,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
@@ -113,19 +124,41 @@ try {
|
||||
if (
|
||||
!Array.isArray(documents) ||
|
||||
documents.length !== 1 ||
|
||||
!pageHasItems(procedures) ||
|
||||
!Array.isArray(procedures) ||
|
||||
procedures.length === 0 ||
|
||||
!Array.isArray(segments) ||
|
||||
segments.length === 0 ||
|
||||
!isPage(names) ||
|
||||
!isPage(strings)
|
||||
!Array.isArray(names) ||
|
||||
!Array.isArray(strings)
|
||||
)
|
||||
throw new Error("Windows Ghidra inventory proof was incomplete");
|
||||
const procedure = procedures.items.find(
|
||||
const procedure = procedures.find(
|
||||
(item) => item.procedure?.external === false,
|
||||
);
|
||||
if (procedure === undefined)
|
||||
throw new Error("Windows Ghidra fixture exposed no local procedure");
|
||||
|
||||
await inventory("inspect_native_load_image", {});
|
||||
const memory = await inventory("read_bytes", {
|
||||
address: procedure.address,
|
||||
length: 16,
|
||||
});
|
||||
const mapping = await inventory("address_to_file_offset", {
|
||||
address: procedure.address,
|
||||
});
|
||||
const sourceBytes = await readFile(targetPath);
|
||||
if (
|
||||
memory.complete !== true ||
|
||||
memory.returned_bytes !== 16 ||
|
||||
memory.bytes_hex !==
|
||||
sourceBytes
|
||||
.subarray(mapping.file_offset, mapping.file_offset + 16)
|
||||
.toString("hex")
|
||||
)
|
||||
throw new Error(
|
||||
"Windows loaded bytes disagree with the observed source-file mapping",
|
||||
);
|
||||
|
||||
await inventory("address_name", { address: procedure.address });
|
||||
const resolvedAddress = await inventory("procedure_address", {
|
||||
procedure: procedure.value,
|
||||
@@ -149,13 +182,22 @@ try {
|
||||
])
|
||||
await functionOperation(operation, { procedure: procedure.value });
|
||||
await functionOperation("xrefs", { address: procedure.address });
|
||||
await functionOperation("inspect_native_data_type", { type: "/undefined8" });
|
||||
await functionOperation("inspect_native_instruction", {
|
||||
address: procedure.address,
|
||||
});
|
||||
await functionOperation("resolve_native_call_targets", {
|
||||
address: procedure.address,
|
||||
});
|
||||
await functionOperation("analyze_function", {
|
||||
procedure: procedure.value,
|
||||
});
|
||||
|
||||
const expected = [
|
||||
...GHIDRA_INVENTORY_OPERATIONS,
|
||||
...GHIDRA_FUNCTION_OPERATIONS,
|
||||
...GHIDRA_FUNCTION_OPERATIONS.filter(
|
||||
(name) => name !== "annotate_native_function",
|
||||
),
|
||||
];
|
||||
if (
|
||||
observed.size !== expected.length ||
|
||||
@@ -178,11 +220,16 @@ try {
|
||||
transport: "authenticated-loopback-tcp",
|
||||
operations: [...observed].sort((left, right) => left.localeCompare(right)),
|
||||
cleanup: "complete",
|
||||
native_controls: {
|
||||
job_object: true,
|
||||
protected_dacl: true,
|
||||
handle_admission: true,
|
||||
},
|
||||
limitations: [
|
||||
"approved-non-sensitive-fixtures-only",
|
||||
"no-job-object-ownership",
|
||||
"no-private-dacl-proof",
|
||||
"no-reparse-point-authority",
|
||||
"windows-x64-local-ntfs-only",
|
||||
"native-x86-64-pe-applications-only",
|
||||
"no-gui-or-mutation-authority",
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
@@ -226,7 +273,9 @@ function assertSession(session, targetSha256, profileDigest) {
|
||||
session.read_only !== true ||
|
||||
session.analysis_complete !== true ||
|
||||
session.analysis_timed_out !== false ||
|
||||
session.capabilities.join(",") !== SESSION_CAPABILITIES.join(",")
|
||||
session.capabilities.length !== SESSION_CAPABILITIES.length ||
|
||||
new Set(session.capabilities).size !== SESSION_CAPABILITIES.length ||
|
||||
SESSION_CAPABILITIES.some((name) => !session.capabilities.includes(name))
|
||||
)
|
||||
throw new Error(
|
||||
`Windows Ghidra session commitment drifted: ${JSON.stringify(session)}`,
|
||||
@@ -240,7 +289,9 @@ function assertRuntimeCoordinates(runtime, targetSha256) {
|
||||
typeof runtime.runtime_root !== "string" ||
|
||||
typeof runtime.endpoint_path !== "string" ||
|
||||
typeof runtime.project_root !== "string" ||
|
||||
typeof runtime.process_id !== "number"
|
||||
typeof runtime.process_id !== "number" ||
|
||||
typeof runtime.target_admission !== "object" ||
|
||||
runtime.target_admission === null
|
||||
)
|
||||
throw new Error(
|
||||
`Windows Ghidra runtime coordinates are incomplete: ${JSON.stringify(runtime)}`,
|
||||
@@ -262,14 +313,6 @@ async function assertCleanup(runtime) {
|
||||
);
|
||||
}
|
||||
|
||||
const isPage = (value) =>
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
Array.isArray(value.items);
|
||||
|
||||
const pageHasItems = (value) => isPage(value) && value.items.length > 0;
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
await access(path);
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { access, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
target: { type: "string" },
|
||||
procedure: { type: "string", default: "rea_fixture_add" },
|
||||
"package-root": { type: "string" },
|
||||
report: { type: "string" },
|
||||
},
|
||||
});
|
||||
if (values.target === undefined || process.env.REA_IDA_MCP_CONFIG === undefined)
|
||||
throw new Error(
|
||||
"IDA verification requires --target and REA_IDA_MCP_CONFIG; see docs/ida-provider.md. No toolchain is installed by this lane.",
|
||||
);
|
||||
const packageRoot = resolve(
|
||||
values["package-root"] ?? fileURLToPath(new URL("..", import.meta.url)),
|
||||
);
|
||||
const target = resolve(values.target);
|
||||
const entry = join(packageRoot, "scripts", "rea.mjs");
|
||||
const importBuilt = (path) =>
|
||||
import(pathToFileURL(join(packageRoot, "dist", path)).href);
|
||||
const [
|
||||
{ toolContract },
|
||||
{ evidenceSchema },
|
||||
{ functionDossierSchema },
|
||||
{ readIdaConfiguration },
|
||||
{ createIdaMcpConnection },
|
||||
] = await Promise.all([
|
||||
importBuilt("contracts/toolContracts.js"),
|
||||
importBuilt("domain/evidence.js"),
|
||||
importBuilt("domain/hopperValues.js"),
|
||||
importBuilt("ida/IdaConfiguration.js"),
|
||||
importBuilt("ida/IdaMcpConnection.js"),
|
||||
]);
|
||||
const registration = readIdaConfiguration(process.env.REA_IDA_MCP_CONFIG);
|
||||
if (!registration.ok) throw registration.error;
|
||||
const config = registration.value;
|
||||
const environment = Object.fromEntries(
|
||||
Object.entries({
|
||||
...process.env,
|
||||
REA_ANALYSIS_PROVIDER: "ida",
|
||||
REA_LOG_LEVEL: "error",
|
||||
}).filter(([, value]) => value !== undefined),
|
||||
);
|
||||
const report = {
|
||||
passed: false,
|
||||
mode: config.mode,
|
||||
checks: [],
|
||||
observations: [],
|
||||
};
|
||||
let phase = "target admission";
|
||||
const workspaces = new Set();
|
||||
const databases = new Set();
|
||||
const digest = async () => {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(target)) hash.update(chunk);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
const observe = (input) => {
|
||||
const evidence = evidenceSchema.parse(input);
|
||||
assert.equal(evidence.provider.id, "ida");
|
||||
assert.equal(evidence.provider.version, "1");
|
||||
assert.equal(evidence.analysis_profile.parameters.cache_policy, "live");
|
||||
assert.equal(evidence.analysis_profile.parameters.engine_version, null);
|
||||
assert.ok(evidence.limitations.length > 0);
|
||||
assert.ok(
|
||||
evidence.locations.some((location) => location.kind === "artifact-path"),
|
||||
);
|
||||
if (config.mode === "headless") {
|
||||
assert.notEqual(evidence.raw_result.metadata_before.input_path, target);
|
||||
workspaces.add(dirname(evidence.raw_result.metadata_before.input_path));
|
||||
for (const observation of evidence.raw_result.observations) {
|
||||
assert.equal(typeof observation.arguments.database, "string");
|
||||
databases.add(observation.arguments.database);
|
||||
}
|
||||
}
|
||||
report.observations.push(evidence);
|
||||
return evidence.normalized_result;
|
||||
};
|
||||
const cli = async () => {
|
||||
phase = "production CLI function analysis";
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entry,
|
||||
"function",
|
||||
target,
|
||||
values.procedure,
|
||||
"--provider",
|
||||
"ida",
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
env: environment,
|
||||
timeout: config.timeoutMs * 3,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
).catch((cause) => {
|
||||
report.failed_cli = { stdout: cause.stdout, stderr: cause.stderr };
|
||||
throw cause;
|
||||
});
|
||||
const dossier = functionDossierSchema.parse(observe(JSON.parse(stdout)));
|
||||
assert.ok(dossier.pseudocode.length > 0);
|
||||
assert.ok(dossier.assembly.length > 0);
|
||||
assert.equal(dossier.procedure.body.available, false);
|
||||
report.checks.push("production CLI and complete function Evidence");
|
||||
return dossier;
|
||||
};
|
||||
const mcp = async (cliDossier) => {
|
||||
const client = new Client({ name: "rea-real-ida-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entry, "mcp"],
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
transport.stderr?.on("data", () => undefined);
|
||||
const call = async (name, args) => {
|
||||
phase = `production MCP ${name}`;
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: config.timeoutMs * 2 },
|
||||
);
|
||||
if (result.isError === true) report.failed_tool_result = result;
|
||||
assert.notEqual(result.isError, true);
|
||||
return toolContract(name).outputSchema.parse(result.structuredContent);
|
||||
};
|
||||
let opened = false;
|
||||
try {
|
||||
await client.connect(transport, { timeout: config.timeoutMs });
|
||||
await call("open_binary", { path: target, provider_id: "ida" });
|
||||
opened = true;
|
||||
const analyzed = await call("analyze_function", {
|
||||
procedure: values.procedure,
|
||||
});
|
||||
const dossier = functionDossierSchema.parse(observe(analyzed.evidence));
|
||||
assert.deepEqual(dossier.procedure, cliDossier.procedure);
|
||||
assert.equal(dossier.pseudocode, cliDossier.pseudocode);
|
||||
assert.deepEqual(dossier.assembly, cliDossier.assembly);
|
||||
assert.equal(analyzed.evidence_id, analyzed.evidence.evidence_id);
|
||||
for (const [name, args] of [
|
||||
["list_procedures", {}],
|
||||
["list_strings", {}],
|
||||
["procedure_address", { procedure: dossier.procedure.name }],
|
||||
["procedure_assembly", { procedure: dossier.procedure.address }],
|
||||
["procedure_callees", { procedure: dossier.procedure.address }],
|
||||
...(config.mode === "attached"
|
||||
? [["procedure_callers", { procedure: dossier.procedure.address }]]
|
||||
: []),
|
||||
["search_procedures", { pattern: dossier.procedure.name }],
|
||||
["procedure_pseudo_code", { procedure: dossier.procedure.address }],
|
||||
["read_function_instructions", { procedure: dossier.procedure.address }],
|
||||
["search_strings", { pattern: "a.b", mode: "literal" }],
|
||||
["xrefs", { address: dossier.procedure.address }],
|
||||
]) {
|
||||
const result = await call(name, args);
|
||||
observe(result.evidence);
|
||||
}
|
||||
const invalid = await client.callTool({
|
||||
name: "xrefs",
|
||||
arguments: { address: "not-an-address" },
|
||||
});
|
||||
assert.equal(invalid.isError, true);
|
||||
assert.equal(invalid.structuredContent.error.code, "invalid_request");
|
||||
await call("close_binary", {});
|
||||
opened = false;
|
||||
report.checks.push(
|
||||
"one MCP connection, target selection, analysis/search/xrefs, invalid input, and close",
|
||||
);
|
||||
report.checks.push(
|
||||
"CLI/MCP function identity, pseudocode, assembly, and raw Evidence parity",
|
||||
);
|
||||
} finally {
|
||||
const completedPhase = phase;
|
||||
if (opened) await call("close_binary", {});
|
||||
await client.close();
|
||||
phase = completedPhase;
|
||||
}
|
||||
};
|
||||
const cleanup = async () => {
|
||||
phase = "upstream lifecycle verification";
|
||||
const upstream = createIdaMcpConnection(config);
|
||||
try {
|
||||
await upstream.connect();
|
||||
if (config.mode === "attached") {
|
||||
const metadata = await upstream.call("get_metadata", {});
|
||||
assert.equal(metadata.sha256.toLowerCase(), await digest());
|
||||
report.checks.push(
|
||||
"existing GUI target remains open with the original input identity",
|
||||
);
|
||||
} else {
|
||||
const inventory = await upstream.call("idb_list", {});
|
||||
assert.ok(
|
||||
inventory.sessions.every(
|
||||
(session) => !databases.has(session.session_id),
|
||||
),
|
||||
);
|
||||
for (const workspace of workspaces)
|
||||
await assert.rejects(access(workspace), { code: "ENOENT" });
|
||||
report.checks.push(
|
||||
"owned database IDs absent from upstream discovery and private workspaces removed",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await upstream.close();
|
||||
}
|
||||
};
|
||||
try {
|
||||
const before = await digest();
|
||||
await mcp(await cli());
|
||||
await cleanup();
|
||||
assert.equal(await digest(), before);
|
||||
report.checks.push("original binary unchanged and never executed");
|
||||
report.passed = true;
|
||||
} catch (cause) {
|
||||
report.failure = {
|
||||
phase,
|
||||
message: cause instanceof Error ? cause.message : String(cause),
|
||||
};
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (values.report !== undefined)
|
||||
await writeFile(
|
||||
resolve(values.report),
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
passed: report.passed,
|
||||
mode: report.mode,
|
||||
checks: report.checks,
|
||||
...(report.passed
|
||||
? {}
|
||||
: {
|
||||
failed_phase: phase,
|
||||
detail: "Use --report for private local diagnostics.",
|
||||
}),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(new URL("package.json", root), "utf8"),
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
await readFile(new URL("native/windows/build/manifest.json", root), "utf8"),
|
||||
);
|
||||
const bytes = await readFile(
|
||||
new URL("native/windows/build/rea-windows-x64.node", root),
|
||||
);
|
||||
if (
|
||||
manifest.packageVersion !== packageJson.version ||
|
||||
manifest.platform !== "win32" ||
|
||||
manifest.architecture !== "x64" ||
|
||||
manifest.abiVersion !== 1 ||
|
||||
manifest.nodeApiVersion !== 8 ||
|
||||
manifest.artifact !== "rea-windows-x64.node" ||
|
||||
createHash("sha256").update(bytes).digest("hex") !== manifest.artifactSha256
|
||||
)
|
||||
throw new Error(
|
||||
"Windows native artifact metadata or SHA-256 does not match this package; run the Windows native build lane.",
|
||||
);
|
||||
const offset = bytes.length >= 64 ? bytes.readUInt32LE(0x3c) : bytes.length;
|
||||
if (
|
||||
bytes.toString("ascii", 0, 2) !== "MZ" ||
|
||||
offset > bytes.length - 26 ||
|
||||
bytes.toString("ascii", offset, offset + 4) !== "PE\0\0" ||
|
||||
bytes.readUInt16LE(offset + 4) !== 0x8664 ||
|
||||
bytes.readUInt16LE(offset + 24) !== 0x20b
|
||||
)
|
||||
throw new Error("Windows native artifact must be a Windows x64 PE32+ image.");
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, ...manifest, artifactBytes: bytes.length })}\n`,
|
||||
);
|
||||
@@ -0,0 +1,427 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rename,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
open,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
if (process.platform !== "win32")
|
||||
throw new Error(
|
||||
"Windows native conformance requires a real Windows x64 host.",
|
||||
);
|
||||
const packageRoot = resolve(process.argv[2] ?? ".");
|
||||
const { requireWindowsNativeAuthority } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/windows/WindowsNativeLoader.js"))
|
||||
);
|
||||
const native = requireWindowsNativeAuthority();
|
||||
const { WindowsPrivateRuntime } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/windows/WindowsPrivateRuntime.js"))
|
||||
);
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-native-conformance-"));
|
||||
const environment = Object.entries(process.env)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.map(([key, value]) => `${key}=${value}`);
|
||||
const quote = (value) =>
|
||||
`"${value.replace(/(\\*)"/gu, '$1$1\\"').replace(/(\\+)$/u, "$1$1")}"`;
|
||||
const delay = () => new Promise((resolve) => setTimeout(resolve, 25));
|
||||
const exists = (path) =>
|
||||
access(path).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const live = (pid) => {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const launch = (arguments_) =>
|
||||
native.call("process_spawn", [
|
||||
process.execPath,
|
||||
[process.execPath, ...arguments_].map(quote).join(" "),
|
||||
workspace,
|
||||
environment,
|
||||
]);
|
||||
const wait = async (child) => {
|
||||
let output = "";
|
||||
for (let attempt = 0; attempt < 400; attempt++) {
|
||||
const state = native.call("process_poll", [child.handle]);
|
||||
output += state.stdout.toString();
|
||||
if (
|
||||
state.exitCode !== null &&
|
||||
state.activeProcesses === 0 &&
|
||||
state.stdoutEnded &&
|
||||
state.stderrEnded
|
||||
)
|
||||
return { ...state, output };
|
||||
await delay();
|
||||
}
|
||||
throw new Error("Owned job did not settle within the fixture deadline.");
|
||||
};
|
||||
const report = { ok: false, artifact: "verified", controls: {}, processes: {} };
|
||||
try {
|
||||
const sourceDirectory = join(workspace, "source");
|
||||
await mkdir(sourceDirectory);
|
||||
const source = join(sourceDirectory, "target.bin");
|
||||
await writeFile(source, Buffer.alloc(1024 * 1024, 0x37));
|
||||
const snapshotOwner = WindowsPrivateRuntime.create(
|
||||
workspace,
|
||||
"single-flight-",
|
||||
);
|
||||
try {
|
||||
const rejectedController = new AbortController();
|
||||
const accepted = snapshotOwner.snapshot(source, "accepted.bin");
|
||||
const rejected = snapshotOwner.snapshot(
|
||||
source,
|
||||
"rejected.bin",
|
||||
rejectedController.signal,
|
||||
);
|
||||
rejectedController.abort();
|
||||
await assert.rejects(rejected, /Runtime snapshot is still pending/u);
|
||||
const completed = await accepted;
|
||||
assert.equal(
|
||||
completed.sha256,
|
||||
createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex"),
|
||||
);
|
||||
assert.equal(
|
||||
await exists(join(snapshotOwner.observation.path, "rejected.bin")),
|
||||
false,
|
||||
);
|
||||
report.controls.singleFlightCancellationOwnership = true;
|
||||
} finally {
|
||||
await snapshotOwner.close();
|
||||
}
|
||||
const unrelatedFile = join(workspace, "unrelated-file");
|
||||
await writeFile(unrelatedFile, "preserve unrelated writes");
|
||||
const admitted = native.call("open", [source]);
|
||||
try {
|
||||
await assert.rejects(
|
||||
rename(sourceDirectory, `${sourceDirectory}-replacement`),
|
||||
);
|
||||
await assert.rejects(open(source, "r+"));
|
||||
await rename(unrelatedFile, `${unrelatedFile}-renamed`);
|
||||
if (process.argv[3] !== undefined) {
|
||||
const observation = JSON.parse(
|
||||
execFileSync(
|
||||
resolve(process.argv[3]),
|
||||
["reparse-directory", sourceDirectory, workspace],
|
||||
{ encoding: "utf8" },
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
observation.reparseConversionDenied,
|
||||
true,
|
||||
"An admitted ancestor was converted into a junction.",
|
||||
);
|
||||
report.controls.inPlaceAncestorReparseDenied = true;
|
||||
}
|
||||
assert.throws(() => native.call("read", [{}, 0, 1]));
|
||||
assert.throws(() => native.call("read", [admitted.handle, NaN, 1]));
|
||||
assert.equal(native.call("read", [admitted.handle, 0, 1])[0], 0x37);
|
||||
} finally {
|
||||
native.call("close", [admitted.handle]);
|
||||
}
|
||||
assert.throws(() => native.call("read", [admitted.handle, 0, 1]));
|
||||
const junction = join(workspace, "junction");
|
||||
await symlink(sourceDirectory, junction, "junction");
|
||||
assert.throws(
|
||||
() => native.call("open", [join(junction, "target.bin")]),
|
||||
/Reparse/u,
|
||||
);
|
||||
assert.throws(() => native.call("open", [source + ":stream"]));
|
||||
const runtime = native.call("runtime_create", [
|
||||
workspace,
|
||||
"private root with spaces-",
|
||||
]);
|
||||
const sentinel = join(sourceDirectory, "sentinel");
|
||||
await writeFile(sentinel, "preserve");
|
||||
try {
|
||||
const acl = JSON.parse(
|
||||
execFileSync(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"$a=Get-Acl -LiteralPath $env:REA_TEST_PRIVATE_RUNTIME_PATH; $u=[Security.Principal.WindowsIdentity]::GetCurrent().User; @{protected=$a.AreAccessRulesProtected; ownerMatchesUser=($a.GetOwner([Security.Principal.SecurityIdentifier]).Value -eq $u.Value); trusteesOnlyUserAndSystem=(@($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | Where-Object {$_.IdentityReference.Value -ne $u.Value -and $_.IdentityReference.Value -ne 'S-1-5-18'}).Count -eq 0); count=@($a.Access).Count} | ConvertTo-Json -Compress",
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
// A pwsh caller can supply module paths incompatible with the
|
||||
// Windows PowerShell observer. Let that observer build its own path.
|
||||
env: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(process.env).filter(
|
||||
([name]) => name.toLowerCase() !== "psmodulepath",
|
||||
),
|
||||
),
|
||||
REA_TEST_PRIVATE_RUNTIME_PATH: runtime.path,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
assert.deepEqual(acl, {
|
||||
protected: true,
|
||||
ownerMatchesUser: true,
|
||||
trusteesOnlyUserAndSystem: true,
|
||||
count: 2,
|
||||
});
|
||||
native.call("runtime_mkdir", [runtime.handle, "inputs"]);
|
||||
native.call("runtime_write", [
|
||||
runtime.handle,
|
||||
"descriptor.json",
|
||||
Buffer.from("fixture"),
|
||||
]);
|
||||
assert.throws(() =>
|
||||
native.call("runtime_write", [
|
||||
runtime.handle,
|
||||
"descriptor.json",
|
||||
Buffer.from("replace"),
|
||||
]),
|
||||
);
|
||||
assert.throws(() =>
|
||||
native.call("runtime_mkdir", [runtime.handle, "..\\escape"]),
|
||||
);
|
||||
const pending = native.call("runtime_snapshot", [
|
||||
runtime.handle,
|
||||
source,
|
||||
"inputs\\target.bin",
|
||||
]);
|
||||
assert.ok(pending instanceof Promise);
|
||||
assert.throws(
|
||||
() => native.call("runtime_close", [runtime.handle]),
|
||||
/pending/u,
|
||||
);
|
||||
const snapshot = await pending;
|
||||
assert.equal(
|
||||
snapshot.sha256,
|
||||
createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex"),
|
||||
);
|
||||
assert.equal(snapshot.source.requestedPath, source);
|
||||
await assert.rejects(
|
||||
writeFile(join(runtime.path, "inputs", "target.bin"), "replace"),
|
||||
);
|
||||
await symlink(
|
||||
sourceDirectory,
|
||||
join(runtime.path, "outside-junction"),
|
||||
"junction",
|
||||
);
|
||||
const cancelled = native.call("runtime_snapshot", [
|
||||
runtime.handle,
|
||||
source,
|
||||
"cancelled.bin",
|
||||
]);
|
||||
native.call("runtime_snapshot_cancel", [runtime.handle]);
|
||||
await assert.rejects(cancelled, /cancelled/u);
|
||||
report.controls = {
|
||||
...report.controls,
|
||||
protectedDaclReadback: true,
|
||||
sourceReplacementDenied: true,
|
||||
sourceWriteDenied: true,
|
||||
unrelatedFilesystemRenamePreserved: true,
|
||||
junctionAdmissionDenied: true,
|
||||
alternateStreamsDenied: true,
|
||||
forgedAndClosedHandlesDenied: true,
|
||||
immutableSnapshotDigest: true,
|
||||
asynchronousSnapshot: true,
|
||||
snapshotCancellation: true,
|
||||
};
|
||||
} finally {
|
||||
native.call("runtime_close", [runtime.handle]);
|
||||
}
|
||||
assert.equal(await exists(runtime.path), false);
|
||||
assert.equal(await readFile(sentinel, "utf8"), "preserve");
|
||||
report.controls.cleanupDoesNotTraverseJunctions = true;
|
||||
const fixture = join(workspace, "tree.mjs");
|
||||
await writeFile(
|
||||
fixture,
|
||||
`import {spawn} from 'node:child_process'; import {writeFileSync} from 'node:fs';
|
||||
const depth=Number(process.argv[2]); writeFileSync(process.argv[3]+depth,String(process.pid));
|
||||
if(depth>0)spawn(process.execPath,[import.meta.filename,String(depth-1),process.argv[3]],{detached:true,stdio:'ignore'});
|
||||
setInterval(()=>{},1000);\n`,
|
||||
);
|
||||
const pidPrefix = join(workspace, "pid-");
|
||||
const child = launch(["--max-old-space-size=128", fixture, "2", pidPrefix]);
|
||||
const unrelated = spawn(
|
||||
process.execPath,
|
||||
["--max-old-space-size=128", "-e", "setInterval(()=>{},1000)"],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
try {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < 200 && !(await exists(pidPrefix + "0"));
|
||||
attempt++
|
||||
)
|
||||
await delay();
|
||||
const pids = await Promise.all(
|
||||
[0, 1, 2].map(async (depth) =>
|
||||
Number(await readFile(pidPrefix + depth, "utf8")),
|
||||
),
|
||||
);
|
||||
assert.ok(native.call("process_poll", [child.handle]).activeProcesses >= 3);
|
||||
native.call("process_terminate", [child.handle]);
|
||||
assert.equal((await wait(child)).activeProcesses, 0);
|
||||
assert.equal(pids.some(live), false);
|
||||
assert.ok(unrelated.pid !== undefined && live(unrelated.pid));
|
||||
report.processes.descendantsAndDetachedDescendantsTerminated = true;
|
||||
report.processes.unrelatedProcessPreserved = true;
|
||||
} finally {
|
||||
native.call("process_terminate", [child.handle]);
|
||||
await wait(child);
|
||||
native.call("process_close", [child.handle]);
|
||||
if (unrelated.exitCode === null) {
|
||||
const closed = new Promise((resolve) => unrelated.once("close", resolve));
|
||||
unrelated.kill();
|
||||
await closed;
|
||||
}
|
||||
}
|
||||
const ownerClose = launch([
|
||||
"--max-old-space-size=128",
|
||||
fixture,
|
||||
"0",
|
||||
join(workspace, "owner-close-"),
|
||||
]);
|
||||
native.call("process_close", [ownerClose.handle]);
|
||||
for (let attempt = 0; attempt < 100 && live(ownerClose.pid); attempt++)
|
||||
await delay();
|
||||
assert.equal(live(ownerClose.pid), false);
|
||||
report.processes.killOnOwnerClose = true;
|
||||
const exited = launch([
|
||||
"--max-old-space-size=128",
|
||||
"-e",
|
||||
"console.log('observed');process.exit(7)",
|
||||
]);
|
||||
const settled = await wait(exited);
|
||||
assert.equal(settled.exitCode, 7);
|
||||
assert.match(settled.output, /observed/u);
|
||||
native.call("process_close", [exited.handle]);
|
||||
report.processes.normalExitAndOutput = true;
|
||||
const boundaryFixture = process.argv[3];
|
||||
if (boundaryFixture !== undefined) {
|
||||
const before = JSON.parse(
|
||||
execFileSync(boundaryFixture, [], { encoding: "utf8" }),
|
||||
);
|
||||
const boundary = native.call("process_spawn", [
|
||||
boundaryFixture,
|
||||
`${quote(boundaryFixture)} breakaway`,
|
||||
workspace,
|
||||
environment,
|
||||
]);
|
||||
const denied = await wait(boundary);
|
||||
native.call("process_close", [boundary.handle]);
|
||||
assert.equal(JSON.parse(denied.output).breakawayDenied, true);
|
||||
const tokenChild = native.call("process_spawn", [
|
||||
boundaryFixture,
|
||||
quote(boundaryFixture),
|
||||
workspace,
|
||||
environment,
|
||||
]);
|
||||
const observed = await wait(tokenChild);
|
||||
native.call("process_close", [tokenChild.handle]);
|
||||
assert.equal(JSON.parse(observed.output).defaultOwnerIsUser, true);
|
||||
const after = JSON.parse(
|
||||
execFileSync(boundaryFixture, [], { encoding: "utf8" }),
|
||||
);
|
||||
assert.deepEqual(after, before);
|
||||
report.processes.breakawayDenied = true;
|
||||
report.processes.childOwnerNormalizedWithoutChangingCaller = true;
|
||||
report.callerToken = before;
|
||||
const environmentChild = native.call("process_spawn", [
|
||||
boundaryFixture,
|
||||
`${quote(boundaryFixture)} environment`,
|
||||
workspace,
|
||||
[
|
||||
"z_REA=z",
|
||||
"_REA=underscore",
|
||||
"A_REA=a",
|
||||
"REA_CASE=first",
|
||||
"rea_case=last",
|
||||
"REA_ß=sharp",
|
||||
"REA_SS=double",
|
||||
],
|
||||
]);
|
||||
const childEnvironment = await wait(environmentChild);
|
||||
native.call("process_close", [environmentChild.handle]);
|
||||
assert.deepEqual(JSON.parse(childEnvironment.output), {
|
||||
ordinallySorted: true,
|
||||
lastDuplicateSelected: true,
|
||||
unicodeNamesPreserved: true,
|
||||
});
|
||||
report.processes.windowsEnvironmentSemantics = true;
|
||||
}
|
||||
const ownerFixture = join(workspace, "owner.mjs");
|
||||
const ownerPidFile = join(workspace, "crash-child.pid");
|
||||
const moduleUrl = pathToFileURL(
|
||||
join(packageRoot, "dist/windows/WindowsNativeLoader.js"),
|
||||
).href;
|
||||
await writeFile(
|
||||
ownerFixture,
|
||||
`import {writeFileSync} from 'node:fs';
|
||||
import {requireWindowsNativeAuthority} from ${JSON.stringify(moduleUrl)};
|
||||
const native=requireWindowsNativeAuthority();
|
||||
const env=Object.entries(process.env).map(([k,v])=>k+'='+v);
|
||||
const child=native.call('process_spawn',[process.execPath,${JSON.stringify([process.execPath, "--max-old-space-size=128", fixture, "0", join(workspace, "crash-descendant-")].map(quote).join(" "))},${JSON.stringify(workspace)},env]);
|
||||
writeFileSync(${JSON.stringify(ownerPidFile)},String(child.pid));setInterval(()=>{},1000);\n`,
|
||||
);
|
||||
const owner = spawn(
|
||||
process.execPath,
|
||||
["--max-old-space-size=128", ownerFixture],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
try {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < 200 && !(await exists(ownerPidFile));
|
||||
attempt++
|
||||
)
|
||||
await delay();
|
||||
const pid = Number(await readFile(ownerPidFile, "utf8"));
|
||||
const closed = new Promise((resolve) => owner.once("close", resolve));
|
||||
owner.kill("SIGKILL");
|
||||
await closed;
|
||||
for (let attempt = 0; attempt < 100 && live(pid); attempt++) await delay();
|
||||
assert.equal(live(pid), false);
|
||||
report.processes.ownerForcedExitKillsOwnedChild = true;
|
||||
} finally {
|
||||
if (owner.exitCode === null && owner.signalCode === null) {
|
||||
const closed = new Promise((resolve) => owner.once("close", resolve));
|
||||
owner.kill("SIGKILL");
|
||||
await closed;
|
||||
}
|
||||
}
|
||||
report.ok = true;
|
||||
if (process.argv[4] !== undefined)
|
||||
await writeFile(process.argv[4], `${JSON.stringify(report)}\n`);
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
} catch (cause) {
|
||||
console.error(
|
||||
"Native conformance failed",
|
||||
cause.code ?? cause.name,
|
||||
cause.constraint ?? "assertion",
|
||||
);
|
||||
throw cause;
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true }).catch((cause) =>
|
||||
console.error("Fixture cleanup failed", cause.code),
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,8 @@ try {
|
||||
const packagedPaths = new Set(packageResult.files.map(({ path }) => path));
|
||||
for (const required of [
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"native/windows/build/manifest.json",
|
||||
"native/windows/build/rea-windows-x64.node",
|
||||
"dist/main.js",
|
||||
"dist/cli.js",
|
||||
"scripts/rea.mjs",
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@
|
||||
"url": "https://github.com/morluto/rea",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "npm",
|
||||
"registryBaseUrl": "https://registry.npmjs.org",
|
||||
"identifier": "rea-agents",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"runtimeHint": "npx",
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "24"
|
||||
tool_count: 116
|
||||
catalog_digest: "5bb3f994a3c4d0c87a34ff5aab4e28141b5fa6f3a98b5ae7e95686d85d9d983e"
|
||||
version: "26"
|
||||
tool_count: 125
|
||||
catalog_digest: "ed9c36900e9223561d38ba30a44921f4ff101d5eca989920f9edc9dc91df470b"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -15,32 +15,110 @@ behavior not established by available source. For ordinary analysis of a
|
||||
complete source repository, use normal repository tools and do not run REA
|
||||
readiness or provider commands.
|
||||
|
||||
## Connect only when needed
|
||||
|
||||
Installing this skill supplies instructions; it does not register the REA MCP
|
||||
server, install analysis engines, or add tools to an already-running agent.
|
||||
|
||||
If REA tools are available and their registration is not known to be stale,
|
||||
proceed directly to the target. Do not run diagnostics or setup before every
|
||||
investigation. Use the connected server's actual tool list and input schemas;
|
||||
a skill installed from repository main may describe capabilities absent from
|
||||
an older npm release. Keep complete inline Evidence when that server does not
|
||||
advertise retained references.
|
||||
|
||||
When tools are absent or registration is stale:
|
||||
|
||||
1. Diagnose without changing files. For Codex, run
|
||||
`npx -y rea-agents@latest doctor --client codex --json`. Substitute the current
|
||||
supported client: `claude_code`, `claude_desktop`, `codex`, `cursor`,
|
||||
`gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`, `copilot_cli`,
|
||||
`commandcode`, or `vscode`. If the client is unknown, use `doctor --json` and
|
||||
inspect its registration results before choosing a setup scope.
|
||||
2. Distinguish the reason. Missing, malformed, or stale registration needs a
|
||||
scoped configuration repair. An aligned registration with no tools in the
|
||||
active session needs a restart/reconnection; doctor cannot prove that the
|
||||
current agent has connected. A missing provider affects only tasks requiring
|
||||
that provider: static JavaScript inspection needs neither Hopper nor Ghidra,
|
||||
and Android inspection has separate bring-your-own JADX/Java prerequisites.
|
||||
3. For a configuration repair, prepare the read-only plan:
|
||||
`npx -y rea-agents@latest setup --client codex --dry-run --json`.
|
||||
Use the current client's ID, show its exact proposed paths, backups, and
|
||||
changes, and obtain approval before setup writes configuration or installs
|
||||
Hopper. Setup normally installs the matching bundled skill too; include that
|
||||
replacement in the reviewed plan. After approval, apply the same scope with
|
||||
`npx -y rea-agents@latest setup --client codex --yes`. Add `--install-hopper`
|
||||
only if that separate installation was needed and explicitly approved.
|
||||
4. Restart/reconnect the affected agent. Verify that REA tools actually appear
|
||||
in the session, then resume the original investigation. If they remain
|
||||
absent, inspect the client's MCP launch error rather than repeating setup.
|
||||
|
||||
REA setup never installs or upgrades Node.js, npm, Homebrew, Java, Ghidra, IDA,
|
||||
JADX, Binwalk, or Unblob. Use existing prerequisites; do not install unrelated
|
||||
software to repair
|
||||
MCP registration. For an unsupported client, use manual stdio registration
|
||||
with a version-pinned `rea-agents` package or continue through the CLI.
|
||||
|
||||
A concrete CLI fallback for an operator-supplied JavaScript tree or ASAR is:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
No MCP registration or native engine is required. Read the returned Evidence,
|
||||
graph, limitations, and unknowns with the same care as an MCP result; the CLI
|
||||
returns the Evidence record directly. This fallback does not establish that
|
||||
MCP is configured. Native CLI tasks still require their selected engine.
|
||||
|
||||
## Route the target first
|
||||
|
||||
Choose the first tool from the target the user supplied. Do not call
|
||||
`open_binary` unless the target is native or an analysis database.
|
||||
Choose the first tool from the target the user supplied. Use `open_binary` for
|
||||
active-target native or archive workflows; target-free tools take their own
|
||||
explicit path or endpoint and do not need it.
|
||||
|
||||
- ASAR or extracted JavaScript/Electron tree:
|
||||
`analyze_javascript_application`.
|
||||
- Archive, application package, ZIP/APK/IPA/MSIX/AppX, or DMG:
|
||||
- Archive/package member inventory (ZIP/APK/IPA/MSIX/AppX or DMG):
|
||||
`open_binary` with the supplied local path; use `inspect_artifact` when its
|
||||
graph and findings help answer the question.
|
||||
- Android APK code, classes, methods, or incoming references:
|
||||
`inspect_android_package`, then focused Android tools when advertised.
|
||||
Archive member inventory still uses the archive route above.
|
||||
- Managed PE/CLI assembly: `inspect_managed_artifact`.
|
||||
- Firmware image: `inspect_firmware_regions` when advertised. Use
|
||||
`extract_firmware` when extraction is requested, with a caller-selected new
|
||||
absolute output directory. These tools use caller-supplied Binwalk/Unblob on
|
||||
Linux; see the [firmware guide](https://github.com/morluto/rea/blob/main/docs/firmware-analysis.md).
|
||||
- .NET NativeAOT PE/ELF: native Ghidra analysis. Native code can yield pseudocode;
|
||||
consult the NativeAOT workflow in `references/native-and-artifacts.md` for
|
||||
optional metadata recovery and its supported host/layout boundary.
|
||||
- User-owned browser page already open: `list_browser_targets`.
|
||||
- User-owned Electron runtime already open: `list_electron_targets`.
|
||||
- Native executable, library, or analysis database: `open_binary`, then
|
||||
use focused analysis tools directly; call `binary_overview` when metadata or
|
||||
inventory context is useful.
|
||||
inventory context is useful and available from the selected provider.
|
||||
|
||||
For an existing IDA MCP configuration, use `open_binary` with the original
|
||||
input binary and `provider_id: "ida"`; do not pass an `.idb` or `.i64` database.
|
||||
The legacy attached profile binds the already-open GUI input by SHA-256 and
|
||||
leaves its database open. The modern headless profile creates and closes an
|
||||
owned private database without saving. Installation and registration reuse
|
||||
[mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp); see the
|
||||
[IDA provider guide](https://github.com/morluto/rea/blob/main/docs/ida-provider.md).
|
||||
Proceed directly to `analyze_function`, pseudocode, or function/string searches
|
||||
after opening; IDA does not supply `binary_overview`. Consult session availability
|
||||
when composing broader workflows. Modern direct callers and unsupported dossier
|
||||
facets remain unknown; live IDA results are not replayed from snapshots.
|
||||
|
||||
If the app is missing, ask which app to inspect. Resolve a human-readable app
|
||||
name to one clear installed artifact when possible; ask only when matches are
|
||||
ambiguous. Never choose an example app on the user's behalf.
|
||||
|
||||
In a target-free session, use `open_binary` to bind any archive/package or
|
||||
native target whose analysis tool operates on the active target. Do not call a
|
||||
tool hidden from `tools/list`; inspect `binary_session` with
|
||||
`detail: "capabilities"` for the exact remediation when a desired capability
|
||||
is unavailable.
|
||||
native target whose analysis tool operates on the active target. Do not call an
|
||||
unadvertised tool; inspect `binary_session` with `{}` and its
|
||||
`result.tool_availability` for availability reasons and remediation. The tool
|
||||
list stays complete; availability depends on the operation, target, and host.
|
||||
|
||||
## Work summary-first
|
||||
|
||||
@@ -98,23 +176,13 @@ as complete while required questions remain open.
|
||||
[references/native-and-artifacts.md](references/native-and-artifacts.md)
|
||||
- ASARs, extracted JavaScript, feature tracing, and version comparison:
|
||||
[references/javascript-applications.md](references/javascript-applications.md)
|
||||
- Android APK declarations, classes, methods, and static references:
|
||||
[references/android-applications.md](references/android-applications.md)
|
||||
- Passive browser/Electron observation and static/runtime reconciliation:
|
||||
[references/runtime-observation.md](references/runtime-observation.md)
|
||||
- Evidence paging, comparisons, residual unknowns, and verification:
|
||||
[references/evidence-workflows.md](references/evidence-workflows.md)
|
||||
|
||||
## Readiness and setup
|
||||
|
||||
The readiness rule is conditional: when REA tools are available and their
|
||||
registration is not known to be stale, proceed directly; do not run `doctor`
|
||||
before every task. Run `npx -y rea-agents@latest doctor` when the MCP server or
|
||||
required provider is unavailable, registration is reported stale, or the user
|
||||
asks for an environment diagnosis. Propose
|
||||
`npx -y rea-agents@latest setup` only when doctor identifies an alignment or
|
||||
provider problem. Show the exact plan and obtain approval before setup writes
|
||||
configuration or installs Hopper. Restart the agent after MCP registration
|
||||
changes; direct CLI commands remain available immediately.
|
||||
|
||||
## Finish the task
|
||||
|
||||
Explain findings in plain language and tie them to returned evidence. When the
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Android application artifacts
|
||||
|
||||
Use the connected server's advertised Android tools. Repository main includes
|
||||
this family; npm 4.0.1 does not. A skill installation alone cannot add it to an
|
||||
older server.
|
||||
|
||||
Start with `inspect_android_package` on the caller's APK path for package
|
||||
identity, manifest declarations, coverage, and inline Evidence. Use
|
||||
`search_android_classes` to find candidates, `inspect_android_class` for one
|
||||
class's members, `inspect_android_method` for decompilation, and
|
||||
`trace_android_references` for incoming static references. Each call takes its
|
||||
own APK path and explicit selectors; do not open a native session first.
|
||||
Choose an `overload_index` from the class inventory when methods are ambiguous
|
||||
instead of guessing.
|
||||
|
||||
The provider requires a separately supplied headless JADX MCP JAR 0.7.1 and
|
||||
Java 17 or newer. `REA_JADX_MCP_JAR` selects the absolute JAR path; `JAVA_HOME`
|
||||
can select Java. REA setup does not install or configure that JAR, and doctor
|
||||
provider scopes cover Hopper/Ghidra rather than JADX. Use existing tools and
|
||||
report missing prerequisites for the Android task specifically.
|
||||
|
||||
Real verification covers Linux. macOS has the POSIX launch boundary but no
|
||||
claimed real-provider verification; Windows is unsupported. Calls use one
|
||||
worker, a bounded JVM heap, and provider deadlines. The APK is never launched,
|
||||
and no emulator is required.
|
||||
|
||||
Manifest entries and decompiled text are static observations, not execution or
|
||||
policy enforcement. Incoming references are recovered static relationships;
|
||||
reflection, dynamic loading, and unresolved calls remain unknown. Preserve
|
||||
artifact digests, locations, diagnostics, and incomplete coverage. For archive
|
||||
members rather than code analysis, use `open_binary` and `inspect_artifact`.
|
||||
@@ -15,7 +15,9 @@ registration, defaults, or policy enforcement from static analysis.
|
||||
|
||||
Use `trace_application_feature` on existing application Evidence for one
|
||||
literal node ID, route, string, API, IPC channel, module, or native export.
|
||||
Choose a direction and include the complete application Evidence inline. Include
|
||||
Choose a direction and reuse the returned application Evidence ID when the
|
||||
connected server advertises retained references, or supply complete Evidence
|
||||
inline. Include
|
||||
Hopper or Ghidra Evidence only when its artifact digest matches exactly.
|
||||
|
||||
For version comparison, analyze each version once, then call
|
||||
@@ -27,10 +29,25 @@ opposite-side coverage; otherwise report unknown.
|
||||
When the question asks how one exact exported callable's returned object shape
|
||||
changed, analyze each version once and then call
|
||||
`compare_javascript_export_shapes` with explicit module paths and export names.
|
||||
Include the complete Evidence records from both analysis calls. Accept variant
|
||||
Use the returned IDs on the same connection, or complete inline Evidence
|
||||
records, from both analysis calls. Accept variant
|
||||
pairing only through the tool's unique exact literal discriminant. Cite the
|
||||
comparison Evidence and report JSON Pointer changes; dynamic values, ambiguous
|
||||
variants, and incomplete parent-property coverage stay unknown. This is static
|
||||
inference, not runtime behavior. When runtime semantics are needed, run
|
||||
behavioral probes against the relevant application versions and capture them
|
||||
through the available browser, Electron, or process workflows.
|
||||
|
||||
## Reusing application Evidence
|
||||
|
||||
When advertised by the connected server, application trace and compare tools
|
||||
accept complete inline Evidence or
|
||||
`{"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`
|
||||
for their application input (`application`, or `left`/`right`). This notation is
|
||||
a template: replace it with the actual returned ID. npm 4.0.1 accepts only full
|
||||
inline Evidence. Use the exact ID
|
||||
returned by the producer on the same MCP connection. Resolution does not run
|
||||
analysis or select a provider; findings remain inline. Native observation
|
||||
arrays still take full Evidence. `close_binary` clears retained references;
|
||||
export a bundle before closing, or supply portable inline Evidence in another
|
||||
connection. A missing reference includes its ID and recovery guidance.
|
||||
@@ -9,10 +9,25 @@ cross-references. Addresses and recovered pseudocode are analysis observations,
|
||||
not original source. Provider unavailability and unsupported metadata remain
|
||||
unknown rather than false.
|
||||
|
||||
Use `binary_session` with its default summary to check the open target, selected
|
||||
provider, alignment, and recommended remediation. Request the capabilities view
|
||||
with a family filter and page bounds only when choosing a tool. Request the full
|
||||
view only for an explicit session-diagnostic need.
|
||||
Use `binary_session` with no arguments to check the open target, selected
|
||||
provider, and alignment. Its default `result.tool_availability` includes the
|
||||
complete tool inventory with availability reasons and remediation. When choosing
|
||||
a tool, use its entry in that result; `tools/list` retains the complete catalog
|
||||
when the target or provider state changes.
|
||||
|
||||
## DOS MZ with Ghidra
|
||||
|
||||
For an admitted DOS MZ target, use `inspect_native_load_image` to check measured
|
||||
loaded bytes, file mappings, relocations and the entry against the immutable
|
||||
snapshot. Keep `mismatch` and `unsupported` explicit. Verified import covers the
|
||||
reported static image, not runtime DOS or PC-98 hardware behavior.
|
||||
|
||||
Use `read_bytes` for initialized analysis memory and `address_to_file_offset` to
|
||||
anchor one observed address to original file bytes. Loader fixups can change a
|
||||
word; a source offset does not imply byte equality. Complete function body ranges
|
||||
use inclusive ends and can contain gaps. Do not treat the enclosing span as code.
|
||||
For packed targets, retain the original and separately derived artifact identities;
|
||||
decompiling an unpacking stub does not recover the unpacked program.
|
||||
|
||||
## Managed PE/CLI
|
||||
|
||||
@@ -22,6 +37,30 @@ assembly. Keep managed/native boundaries and unavailable reconstruction facts
|
||||
explicit. A bring-your-own reconstruction oracle is separate from the canonical
|
||||
parser and must not become an implicit setup dependency.
|
||||
|
||||
## .NET NativeAOT
|
||||
|
||||
A NativeAOT PE/ELF contains native code. Absence of PE/CLI metadata does not mean
|
||||
analysis must stop at assembly. Use native function analysis for recovered
|
||||
pseudocode; it is not reconstructed original C#.
|
||||
|
||||
With an explicitly configured NativeAOT Ghidra adapter, call
|
||||
`inspect_native_load_image` after opening the target. Its optional
|
||||
`observations.metadata_recovery` returns the format, positive header evidence,
|
||||
coverage, derived-memory identity and type addresses/category paths inline.
|
||||
Then inspect a reported type with `inspect_native_data_type`, follow its
|
||||
`metadata_recovery.related_type`, interfaces and virtual slot addresses, and use
|
||||
`analyze_function` plus string/xref searches for implementation evidence. Keep
|
||||
`not_applicable`, partial recovery and unsupported layouts explicit. Generated
|
||||
`Class_address` names and inferred System.Object/String identities are not
|
||||
original names; custom field layouts remain unknown.
|
||||
|
||||
Recovery modifies an ephemeral analysis database and may replace loaded metadata
|
||||
bytes with derived content. Original executable files remain unchanged. Distinguish
|
||||
original file offsets from derived analysis memory. Windows x64 PE **targets**
|
||||
can be analyzed on a supported Linux host; Windows **host** P0 does not admit
|
||||
this database recovery. See the [NativeAOT guide](https://github.com/morluto/rea/blob/main/docs/ghidra-nativeaot.md)
|
||||
for the tested layout and optional bring-your-own adapter build.
|
||||
|
||||
## Packages and extraction
|
||||
|
||||
Use `inspect_artifact` for application bundles, archives, ZIP/APK/IPA/MSIX/AppX,
|
||||
@@ -40,3 +79,12 @@ hosts, with an owned temporary mount directory and cleanup. It needs no approval
|
||||
flag. Unsupported hosts or mount failures remain explicit limitations; do not
|
||||
claim child inventory when only root identity is available. Extraction is a
|
||||
separate filesystem-writing operation.
|
||||
|
||||
For headerless DOS COM, explicitly open with `format: "dos-com"` (CLI
|
||||
`--target-format dos-com`). Inspect the load image before trusting function
|
||||
analysis: the entry is `0x10100`, file offset zero, with imposed real-mode segment
|
||||
context. PSP/stack/device state remains unmodeled. See [DOS guide](https://github.com/morluto/rea/blob/main/docs/ghidra-dos.md).
|
||||
|
||||
### Function annotations in Ghidra
|
||||
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations.
|
||||
@@ -0,0 +1,46 @@
|
||||
import { constants, createReadStream } from "node:fs";
|
||||
import { chmod, copyFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { AndroidOperation } from "../domain/androidAnalysis.js";
|
||||
|
||||
/** Stream a local file digest without retaining the APK or JAR in memory. */
|
||||
export const hashAndroidFile = async (path: string): Promise<string> => {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
/** Bind decompiler input to the admitted artifact, even if the original changes. */
|
||||
export const snapshotAndroidTarget = async (
|
||||
path: string,
|
||||
sha256: string,
|
||||
root: string,
|
||||
operation: AndroidOperation,
|
||||
): Promise<string> => {
|
||||
const snapshot = join(root, "target.apk");
|
||||
await copyFile(path, snapshot, constants.COPYFILE_EXCL);
|
||||
await chmod(snapshot, 0o400);
|
||||
if ((await hashAndroidFile(snapshot)) !== sha256)
|
||||
throw new AnalysisInputError(operation, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"APK bytes changed after admission; retry against a stable file.",
|
||||
},
|
||||
]);
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
/** Fingerprint the actual immutable engine bytes executed for this observation. */
|
||||
export const snapshotAndroidEngine = async (
|
||||
source: string,
|
||||
root: string,
|
||||
): Promise<{ path: string; sha256: string }> => {
|
||||
const path = join(root, "engine.jar");
|
||||
await copyFile(source, path, constants.COPYFILE_EXCL);
|
||||
await chmod(path, 0o400);
|
||||
return { path, sha256: await hashAndroidFile(path) };
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
import type { z } from "zod";
|
||||
import type { AndroidRequest } from "../domain/androidAnalysis.js";
|
||||
import { androidResultSchemas } from "../domain/androidAnalysis.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
jadxAppSchema,
|
||||
jadxClassPageSchema,
|
||||
jadxClassSchema,
|
||||
jadxLoadSchema,
|
||||
jadxMethodSchema,
|
||||
jadxXrefsSchema,
|
||||
normalizeJadxText,
|
||||
} from "./JadxProtocol.js";
|
||||
|
||||
/** The adapter consumes tool payloads, never provider RPC details outside this layer. */
|
||||
export interface JadxToolPort {
|
||||
json(
|
||||
name: string,
|
||||
input: Readonly<Record<string, JsonValue>>,
|
||||
): Promise<JsonValue>;
|
||||
text(
|
||||
name: string,
|
||||
input: Readonly<Record<string, JsonValue>>,
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
type Engine = z.infer<
|
||||
typeof androidResultSchemas.inspect_android_package
|
||||
>["engine"];
|
||||
type ClassSummary = z.infer<typeof jadxClassSchema>;
|
||||
|
||||
const methodsOf = (summary: ClassSummary) => {
|
||||
const seen = new Map<string, number>();
|
||||
return summary.methods.map((method) => {
|
||||
const index = seen.get(method.name) ?? 0;
|
||||
seen.set(method.name, index + 1);
|
||||
return {
|
||||
name: method.name,
|
||||
reported_signature: method.signature,
|
||||
overload_index: index,
|
||||
is_constructor: method.is_constructor,
|
||||
dex_descriptor: null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const inputFailure = (
|
||||
request: AndroidRequest,
|
||||
field: string,
|
||||
message: string,
|
||||
): never => {
|
||||
throw new AnalysisInputError(request.operation, undefined, [
|
||||
{ path: [field], reason: "invalid_value", message },
|
||||
]);
|
||||
};
|
||||
|
||||
const classSummary = async (
|
||||
tools: JadxToolPort,
|
||||
request: Exclude<
|
||||
AndroidRequest,
|
||||
{ operation: "inspect_android_package" | "search_android_classes" }
|
||||
>,
|
||||
): Promise<ClassSummary> => {
|
||||
const raw = await tools.json("get_class_summary", {
|
||||
class_name: request.input.class_name,
|
||||
});
|
||||
if (
|
||||
raw !== null &&
|
||||
typeof raw === "object" &&
|
||||
!Array.isArray(raw) &&
|
||||
typeof raw.error === "string"
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
`JADX class summary failed: ${raw.error}`,
|
||||
);
|
||||
const summary = jadxClassSchema.safeParse(raw);
|
||||
if (!summary.success)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX class summary failed or was malformed; negative counts are not an empty class",
|
||||
{ cause: summary.error },
|
||||
);
|
||||
const value = summary.data;
|
||||
if (value.full_name !== request.input.class_name)
|
||||
inputFailure(
|
||||
request,
|
||||
"class_name",
|
||||
`Exact class ${request.input.class_name} resolved to ${value.full_name}; use the fully qualified class name from search_android_classes.`,
|
||||
);
|
||||
if (
|
||||
value.method_count !== value.methods.length ||
|
||||
value.field_count !== value.fields.length ||
|
||||
value.inner_class_count !== value.inner_classes.length
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX class member counts do not match the returned inventory",
|
||||
);
|
||||
return value;
|
||||
};
|
||||
|
||||
const inspectPackage = async (
|
||||
tools: JadxToolPort,
|
||||
loaded: z.infer<typeof jadxLoadSchema>,
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
const app = jadxAppSchema.parse(await tools.json("get_app_info", {}));
|
||||
const manifest = normalizeJadxText(
|
||||
await tools.text("get_android_manifest", {}),
|
||||
);
|
||||
return androidResultSchemas.inspect_android_package.parse({
|
||||
engine,
|
||||
package_name: app.package || null,
|
||||
version_name: app.version_name || null,
|
||||
version_code: app.version_code || null,
|
||||
min_sdk: app.min_sdk || null,
|
||||
target_sdk: app.target_sdk || null,
|
||||
permissions: app.permissions,
|
||||
manifest,
|
||||
class_count: loaded.class_count,
|
||||
resource_count: loaded.resource_count,
|
||||
signature_verification: "not_performed",
|
||||
});
|
||||
};
|
||||
|
||||
const searchClasses = async (
|
||||
tools: JadxToolPort,
|
||||
request: Extract<AndroidRequest, { operation: "search_android_classes" }>,
|
||||
loaded: z.infer<typeof jadxLoadSchema>,
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
const classes: string[] = [];
|
||||
let total: number | undefined;
|
||||
do {
|
||||
const page = jadxClassPageSchema.parse(
|
||||
await tools.json("list_classes", {
|
||||
offset: classes.length,
|
||||
limit: 200,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
(total !== undefined && total !== page.total) ||
|
||||
page.offset !== classes.length ||
|
||||
page.limit !== 200 ||
|
||||
page.items.length > 200
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX class pagination changed identity or counts",
|
||||
);
|
||||
total = page.total;
|
||||
classes.push(...page.items);
|
||||
if (
|
||||
classes.length > total ||
|
||||
(page.items.length === 0 && classes.length < total)
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX class pagination is incomplete",
|
||||
);
|
||||
} while (classes.length < total);
|
||||
if (new Set(classes).size !== classes.length || total !== loaded.class_count)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX class inventory is duplicated or differs from the loaded class count",
|
||||
);
|
||||
return androidResultSchemas.search_android_classes.parse({
|
||||
engine,
|
||||
query: request.input.query,
|
||||
total_classes: total,
|
||||
matches: classes.filter((name) => name.includes(request.input.query)),
|
||||
coverage: "complete",
|
||||
});
|
||||
};
|
||||
|
||||
const matchingMethods = (
|
||||
summary: ClassSummary,
|
||||
request: Extract<
|
||||
AndroidRequest,
|
||||
{ operation: "inspect_android_method" | "trace_android_references" }
|
||||
>,
|
||||
) => {
|
||||
const candidates = methodsOf(summary).filter(
|
||||
(method) => method.name === request.input.method_name,
|
||||
);
|
||||
if (request.input.method_name !== undefined && candidates.length === 0)
|
||||
inputFailure(
|
||||
request,
|
||||
"method_name",
|
||||
`Method ${summary.full_name}.${request.input.method_name} was not found; inspect_android_class lists available methods.`,
|
||||
);
|
||||
return candidates;
|
||||
};
|
||||
|
||||
const inspectMethod = async (
|
||||
tools: JadxToolPort,
|
||||
request: Extract<AndroidRequest, { operation: "inspect_android_method" }>,
|
||||
summary: ClassSummary,
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
const candidates = matchingMethods(summary, request);
|
||||
if (candidates.length > 1 && request.input.overload_index === undefined)
|
||||
inputFailure(
|
||||
request,
|
||||
"overload_index",
|
||||
`Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}`).join("; ")}`,
|
||||
);
|
||||
const index = request.input.overload_index ?? 0;
|
||||
const selected = candidates[index];
|
||||
if (selected === undefined)
|
||||
inputFailure(
|
||||
request,
|
||||
"overload_index",
|
||||
`Index ${index} is outside the ${candidates.length} matching overloads.`,
|
||||
);
|
||||
const body = jadxMethodSchema.parse(
|
||||
await tools.json("get_method_body", {
|
||||
class_name: summary.full_name,
|
||||
method_name: request.input.method_name,
|
||||
overload_index: index,
|
||||
smali_fallback: true,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
body.class_name !== summary.full_name ||
|
||||
body.method_name !== request.input.method_name ||
|
||||
body.overload_index !== index ||
|
||||
body.overload_count !== candidates.length
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX returned a different method or overload inventory",
|
||||
);
|
||||
if (body.body.startsWith("// ERROR:"))
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
`JADX did not produce method source: ${body.body}`,
|
||||
);
|
||||
if (body.mode === "smali" && candidates.length > 1)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`JADX 0.7.1 smali fallback joins all same-name overloads for ${summary.full_name}.${request.input.method_name}; it cannot establish source for only overload ${index}.`,
|
||||
);
|
||||
return androidResultSchemas.inspect_android_method.parse({
|
||||
engine,
|
||||
class_name: summary.full_name,
|
||||
method: selected,
|
||||
overload_count: candidates.length,
|
||||
representation: body.mode,
|
||||
body_status:
|
||||
body.body.length === 0 ||
|
||||
body.body ===
|
||||
"// method exists but has no decompiled body (native/abstract)"
|
||||
? "not_available"
|
||||
: "available",
|
||||
fell_back: body.fell_back,
|
||||
markers: body.markers,
|
||||
source: normalizeJadxText(body.body),
|
||||
});
|
||||
};
|
||||
|
||||
/** Normalize only the selected analyst outcome and reject ambiguous engine selectors. */
|
||||
export const analyzeJadxRequest = async (
|
||||
tools: JadxToolPort,
|
||||
request: AndroidRequest,
|
||||
loaded: z.infer<typeof jadxLoadSchema>,
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
if (request.operation === "inspect_android_package")
|
||||
return inspectPackage(tools, loaded, engine);
|
||||
if (request.operation === "search_android_classes")
|
||||
return searchClasses(tools, request, loaded, engine);
|
||||
const summary = await classSummary(tools, request);
|
||||
if (request.operation === "inspect_android_class")
|
||||
return androidResultSchemas.inspect_android_class.parse({
|
||||
engine,
|
||||
class_name: summary.full_name,
|
||||
methods: methodsOf(summary),
|
||||
fields: summary.fields.map((field) => ({
|
||||
name: field.name,
|
||||
reported_type: field.type,
|
||||
})),
|
||||
inner_classes: summary.inner_classes,
|
||||
});
|
||||
if (request.operation === "inspect_android_method")
|
||||
return inspectMethod(tools, request, summary, engine);
|
||||
return traceReferences(tools, request, summary, engine);
|
||||
};
|
||||
|
||||
const traceReferences = async (
|
||||
tools: JadxToolPort,
|
||||
request: Extract<AndroidRequest, { operation: "trace_android_references" }>,
|
||||
summary: ClassSummary,
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
const methodName = request.input.method_name;
|
||||
const candidates = matchingMethods(summary, request);
|
||||
if (candidates.length > 1)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`JADX 0.7.1 references select the first same-name method; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
|
||||
);
|
||||
const expectedTarget =
|
||||
methodName === undefined
|
||||
? summary.full_name
|
||||
: `${summary.full_name}.${methodName}`;
|
||||
const xrefs = jadxXrefsSchema.parse(
|
||||
await tools.json(
|
||||
methodName === undefined ? "get_xrefs_to_class" : "get_xrefs_to_method",
|
||||
{
|
||||
class_name: summary.full_name,
|
||||
...(methodName === undefined ? {} : { method_name: methodName }),
|
||||
limit: 2_147_483_647,
|
||||
resolve_line: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
if (
|
||||
xrefs.target !== expectedTarget ||
|
||||
xrefs.total !== xrefs.count ||
|
||||
xrefs.count !== xrefs.items.length
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX references are incomplete or refer to another target",
|
||||
);
|
||||
return androidResultSchemas.trace_android_references.parse({
|
||||
engine,
|
||||
class_name: summary.full_name,
|
||||
method_name: methodName ?? null,
|
||||
total: xrefs.total,
|
||||
references: xrefs.items.map((reference) => ({
|
||||
kind: reference.kind,
|
||||
name: reference.name,
|
||||
reported_full_name: reference.full_name,
|
||||
containing_class: reference.containing_class,
|
||||
top_class: reference.top_class,
|
||||
source_line: null,
|
||||
dex_offset: null,
|
||||
})),
|
||||
coverage: "complete",
|
||||
});
|
||||
};
|
||||
Loaded 100 of 733 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user