Compare commits

...
Author SHA1 Message Date
N0zoM1z0 19a770a705 fix(javascript): integrate media preload evidence correction 2026-10-10 21:57:18 +08:00
morluto 2d426c924c fix(native): format runtime convention guard 2026-10-10 21:50:55 +08:00
a929c9f714 fix(mobile): bound framework conventions to path segments (#1506)
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 21:42:03 +08:00
YsopwnableandGitHub 8c47a9ce4b fix(javascript): yield during receiver and reference classification (#1557) 2026-10-10 21:40:16 +08:00
de619bd334 fix(ida): canonicalize the headless workspace root before opening (#1481)
On macOS the default workspace root is os.tmpdir() under /var/folders,
and /var is a symlink to /private/var. idalib-mcp reports the canonical
input path from idb_open/idb_list, while REA compared it against the
uncanonicalized private copy with normalize(), so every headless IDA
analysis failed its identity check (surfacing as unreadable_output) and
the spawned idalib worker was never released.

Resolve the workspace root with realpath() when allocating the private
workspace so the input path REA sends already matches what upstream
reports.

Co-authored-by: Xingchuan Ma <xingchuan.ma@msn.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:39:33 +08:00
24d9337649 fix(native): preserve compact reference provenance (#1438)
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
2026-10-10 21:39:28 +08:00
2ec93ba166 docs: celebrate 60,000 GitHub stars 🎉 (#1559)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:31:58 -07:00
afdaf98117 fix(hopper): treat a directory launcher path as missing (#1426)
`HOPPER_LAUNCHER_PATH` availability was decided by an `X_OK` access check
alone, which a searchable directory also passes. Pointing it at the
`Hopper Disassembler.app` bundle instead of its launcher made `rea providers`
report Hopper as available and `rea doctor` report it healthy, while analysis
then failed with an unrelated `version_unresolved` rejection.

Require a regular file in both the provider availability probe and the
doctor executable probe, matching the Ghidra launcher check. A directory now
reports `executable_missing`, and doctor reports `config_drift` with its
existing HOPPER_LAUNCHER_PATH remediation.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:31:26 +08:00
944981d8d5 docs: correct stale logging, Ghidra host, and example path statements (#1420)
- CONTRIBUTING: CLI logs go to stderr, not stdout. `createLogger` writes every
  mode to fd 2 so protocol and `--json` stdout stay parseable.
- Get started: Ghidra supports Linux arm64 (`GhidraInstallation` maps
  `linux.arm64`), as installation.md already states.
- Browser guide: drop the caption claiming the hosted request path carries a
  `/rea/` prefix. The guide now targets rea.tools, where the example is served
  at `/examples/notes-web/`, as the evidence table above it shows.
- JavaScript workflows: six CLI commands accept inline JSON or a JSON file,
  not five; `inspect-analysis-view` was added after the sentence was written.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 21:29:31 +08:00
b32a11e1a7 docs: correct host requirements in the README analysis tables (#1422)
- EVM bytecode and recorded crashes: state the Linux x64 host requirement.
  `EvmoleInterfaceProvider` and `PwntoolsDecoder` refuse every other host
  with `capability_unavailable`, but only the neighbouring offline ELF layout
  row said so.
- Translations: Android analysis has supported Windows x64 since 6.2.0
  (#1276), which updated only the English table; 16 translations still said
  Linux/macOS.

Translated cells reuse each README's own wording: the recorded-crash cell
copies that file's ELF-row pwntools phrase, and the EVM and Android cells
follow the construction the same table already uses for host platforms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:29:03 +08:00
0e19db9fce fix(javascript): unify package resolution and resolve exact exports subpaths (#1424)
* fix(javascript): resolve exact package export subpaths

Bare module specifiers with subpaths now resolve against the selected
package's exports map instead of staying external. An exact subpath key
selects its target through the existing condition, array, and null
semantics, and the selected target resolves as one exact file with no
extension or index fallback. A declared exports map encapsulates every
other subpath lookup, and packages without an exports map keep legacy
file and directory-index resolution.

Package-private import aliases remain external pending a separate
increment (#1365).

Verification: npm run check:fast; focused and full
src/application/javascript source tests on Windows;
verify:test-discovery; docs:check. Four pre-existing failures in
JavaScriptFileUrlPaths and JavaScriptModuleSpecifierPaths reproduce on
unmodified main on Windows because their fixtures write file names
containing '?'.

* fix(javascript): unify package path resolution

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 21:26:02 +08:00
YsopwnableandGitHub 66a9ad58fc test(clients): verify native GitHub Copilot CLI integration (#1555) 2026-10-10 21:23:11 +08:00
4b985e5021 fix(artifacts): take nested container families from bytes (#1553)
Inventory gave nested members a ZIP or ASAR family from their path
suffix. Byte classification replaced it only for ZIP magic, so text named
`.apk` or `.asar` stayed a container, and encrypted entries and symlinks
kept the suffix guess. A filesystem `.asar` without an ASAR header failed
the whole inventory as a malformed archive.

Nested ZIP families now follow ZIP magic and ASAR follows its header
pickle, shared with root classification. An unread member named like a
container has an unknown format and a limitation naming the suffix. ASAR
expansion requires the header.

The Android projection adds a limitation when no root AndroidManifest.xml
was observed, and its rejection names the observed root format.

Fixes #1491

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:17:40 -07:00
Rudy CelekliandYsopwnable c8fd2f152b fix(apple): scope keyed archive traversal to the selection
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
2026-10-10 21:17:35 +08:00
morluto 02ef2b1c41 docs(zh): rename title to 逆向一切 2026-10-10 21:10:30 +08:00
morluto 961471205c fix(docs): format Italian README tables 2026-10-10 21:08:41 +08:00
a6d6e595f3 docs: add Italian (it) README translation (#1455)
* docs: add Italian (it) README translation

Adds README_it.md (full Italian translation of the English README) and updates the language nav bar in all existing READMEs to include the new [Italiano] link, appended at the end of the bar (after Bahasa Indonesia) to match the repository's convention for adding new languages.

Rebuilt on top of current main to resolve the merge conflict caused by the recently added Bahasa Indonesia (README_id.md) translation.

Closes #1453

* docs: fix Italian README showcase anchors

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 21:03:39 +08:00
Claude 5fd9f4ad60 fix(javascript): stop reading media preload attributes as Electron preloads
HTML audio and video elements have their own `preload` attribute ("none",
"metadata", "auto"). A React renderer compiles `<audio preload="metadata">`
to `jsx("audio", { preload: "metadata" })`, and the `property:preload`
heuristic reported it as an Electron preload script named `metadata`.

Skip `preload` properties in the props object of a call whose first
argument is the static tag `audio` or `video`. The callee is not checked,
because minified element factories have arbitrary names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPY2GmufMNGQKoEgRUx1pd
2026-10-10 01:33:30 +00:00
68 changed files with 2998 additions and 547 deletions
+4 -3
View File
@@ -124,9 +124,10 @@ for exact-path, awaited cleanup, including failures and timeouts.
rejects remaining REA-owned paths. Never glob-clean shared `/tmp/rea-*` content.
`REA_LOG_LEVEL` selects `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
`silent`. MCP defaults to `info` on stderr; one-shot CLI logging is opt-in on
stdout. Request arguments, bridge tokens, and environment data are redacted
from diagnostic logs.
`silent`. Logs go to stderr: MCP defaults to `info`, and one-shot CLI logging
is opt-in, so stdout carries only protocol messages and command results.
Request arguments, bridge tokens, and environment data are redacted from
diagnostic logs.
## Real Hopper changes
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Reverse Engineer Anything
@@ -151,8 +151,8 @@ Additional tools and host support depend on the target:
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier; [offline EVM guide](docs/evm-bytecode.md) |
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](https://rea.tools/guides/javascript/) |
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) |
@@ -307,7 +307,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md).
## Star history
🎉 **50,000 GitHub stars — thank you!**
🎉 **60,000 GitHub stars — thank you!**
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: هندسة عكسية لأي شيء
@@ -135,13 +135,13 @@ npx rea-agents@latest setup
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/guides/native/) |
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](docs/binary-diagnostics.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](docs/evm-bytecode.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](docs/recorded-crashes.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/guides/javascript/) |
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/guides/browser/) |
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](docs/web-network-captures.md) |
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](docs/managed-code-analysis.md) |
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS؛ [دليل Android](docs/android-analysis.md) |
| حزم Android APK | تصريحات ملف manifest، والأصناف، والدوال الناتجة عن فك الترجمة، والمراجع | JADX دون واجهة رسومية وJDK كامل على Linux/macOS/Windows x64؛ [دليل Android](docs/android-analysis.md) |
| البرامج الثابتة | المناطق، ونتائج الاستخراج، وما يُمرّر إلى تحليل الشيفرة الأصلية | Binwalk / Unblob على Linux؛ [دليل البرامج الثابتة](docs/firmware-analysis.md) |
| الحزم والموارد | قوائم الملفات، والبصمات، وملفات plist، وبنية حزم Apple، والموارد المستخرجة | [دليل تحليل الملفات وJavaScript](docs/javascript-artifact-reconstruction.md)، [تطبيقات Apple](docs/apple-application-analysis.md) |
| سلوك العمليات | مخرجات الطرفية، والتفاعلات، وملاحظات الخروج ونظام الملفات، ومقارنات التشغيل | Linux/macOS مع PTY أصلي؛ [تسجيل سلوك العمليات](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Alles reverse-engineeren
@@ -135,13 +135,13 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/guides/native/) |
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](docs/binary-diagnostics.md) |
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung; [Offline-EVM-Leitfaden](docs/evm-bytecode.md) |
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools; optional GDB/pwndbg; [aufgezeichnete Abstürze](docs/recorded-crashes.md) |
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](docs/evm-bytecode.md) |
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](docs/recorded-crashes.md) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/guides/javascript/) |
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/guides/browser/) |
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](docs/web-network-captures.md) |
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](docs/managed-code-analysis.md) |
| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS; [Android-Leitfaden](docs/android-analysis.md) |
| Android-APKs | Manifest-Deklarationen, Klassen, dekompilierte Methoden und Verweise | JADX ohne grafische Oberfläche und ein vollständiges JDK unter Linux/macOS/Windows x64; [Android-Leitfaden](docs/android-analysis.md) |
| Firmware | Bereiche, Extraktionsergebnisse und Übergaben an die native Analyse | Binwalk / Unblob unter Linux; [Firmware-Leitfaden](docs/firmware-analysis.md) |
| Pakete und Ressourcen | Dateiinventare, Hashwerte, plists, Aufbau von Apple-Bundles und extrahierte Ressourcen | [Artefakt- und JavaScript-Leitfaden](docs/javascript-artifact-reconstruction.md), [Apple-Anwendungen](docs/apple-application-analysis.md) |
| Prozessverhalten | Terminalausgabe, Interaktionen, Beobachtungen zu Prozessende und Dateisystem sowie Laufvergleiche | Linux/macOS mit nativer PTY; [Prozessaufzeichnung](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Ingeniería inversa de cualquier cosa
@@ -135,13 +135,13 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/guides/native/) |
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](docs/binary-diagnostics.md) |
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal; [guía de EVM sin ejecutar](docs/evm-bytecode.md) |
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama; GDB/pwndbg opcionales; [fallos registrados](docs/recorded-crashes.md) |
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](docs/evm-bytecode.md) |
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](docs/recorded-crashes.md) |
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/guides/javascript/) |
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/guides/browser/) |
| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](docs/web-network-captures.md) |
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](docs/managed-code-analysis.md) |
| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS; [guía de Android](docs/android-analysis.md) |
| APK de Android | Declaraciones del manifiesto, clases, métodos descompilados y referencias | JADX sin interfaz gráfica y un JDK completo en Linux/macOS/Windows x64; [guía de Android](docs/android-analysis.md) |
| Firmware | Regiones, resultados de extracción y derivaciones al análisis nativo | Binwalk / Unblob en Linux; [guía de firmware](docs/firmware-analysis.md) |
| Paquetes y recursos | Inventarios de archivos, resúmenes criptográficos, plists, estructura de paquetes de Apple y recursos extraídos | [Guía de artefactos y JavaScript](docs/javascript-artifact-reconstruction.md), [aplicaciones de Apple](docs/apple-application-analysis.md) |
| Comportamiento de procesos | Salida de la terminal, interacciones, observaciones de salida y del sistema de archivos, y comparaciones entre ejecuciones | Linux/macOS con una PTY nativa; [captura de procesos](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center" dir="rtl">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی** · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی** · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: مهندسی معکوس هر چیزی
@@ -127,13 +127,13 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np
| --------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| باینری‌های بومی | شبه‌کد، اسمبلی، رشته‌ها، نمادها، فراخوانی‌ها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/guides/native/) |
| ساختار ELF آفلاین | بخش‌ها، سگمنت‌ها، نمادها و relocationهای اصلی و گزینه‌های احتمالی کاهش آسیب‌پذیری ایستا | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](docs/binary-diagnostics.md) |
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی؛ [راهنمای EVM آفلاین](docs/evm-bytecode.md) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](docs/recorded-crashes.md) |
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](docs/evm-bytecode.md) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](docs/recorded-crashes.md) |
| JavaScript / Electron | ماژول‌ها، importها، source mapها، مسیرها، IPC و ارتباط افزونه‌های بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/guides/javascript/) |
| وب‌سایت‌ها | ساختار صفحه، اسکریپت‌ها، مشاهدات شبکه و اسکرین‌شات‌های درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/guides/browser/) |
| داده‌های ضبط‌شدهٔ شبکه | درخواست‌ها، پاسخ‌ها، payloadهای آشکارشده و مکان آن‌ها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبط‌های بومی mitmproxy؛ [راهنمای ضبط شبکه](docs/web-network-captures.md) |
| اسمبلی‌های .NET | فراداده، دستورالعمل‌های CIL، وابستگی‌های بومی اعلام‌شده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریت‌شده](docs/managed-code-analysis.md) |
| فایل‌های APK اندروید | تعاریف Manifest، کلاس‌ها، متدهای دیکامپایل‌شده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS؛ [راهنمای اندروید](docs/android-analysis.md) |
| فایل‌های APK اندروید | تعاریف Manifest، کلاس‌ها، متدهای دیکامپایل‌شده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](docs/android-analysis.md) |
| Firmware | ناحیه‌ها، نتایج استخراج و تحویل به ابزارهای تحلیل بومی | Binwalk / Unblob در لینوکس؛ [راهنمای Firmware](docs/firmware-analysis.md) |
| بسته‌ها و منابع | فهرست فایل‌ها، digestها، فایل‌های plist، ساختار bundle اپل و منابع استخراج‌شده | [راهنمای آرتیفکت و JavaScript](docs/javascript-artifact-reconstruction.md)، [برنامه‌های اپل](docs/apple-application-analysis.md) |
| رفتار فرایند | خروجی ترمینال، تعاملات، وضعیت خروج، مشاهدات سیستم فایل و مقایسهٔ اجراها | Linux/macOS با PTY بومی؛ [ضبط فرایند](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA : rétro-ingénierie de tout
@@ -136,13 +136,13 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/guides/native/) |
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](docs/binary-diagnostics.md) |
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal ; [guide EVM hors ligne](docs/evm-bytecode.md) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](docs/evm-bytecode.md) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/guides/javascript/) |
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/guides/browser/) |
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](docs/web-network-captures.md) |
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](docs/managed-code-analysis.md) |
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS ; [guide Android](docs/android-analysis.md) |
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS/Windows x64 ; [guide Android](docs/android-analysis.md) |
| Firmware | Régions, résultats d'extraction et passage de relais vers l'analyse native | Binwalk / Unblob sous Linux ; [guide firmware](docs/firmware-analysis.md) |
| Paquets et ressources | Inventaires de fichiers, empreintes, plists, anatomie des bundles Apple et ressources extraites | [Guide artefacts et JavaScript](docs/javascript-artifact-reconstruction.md), [applications Apple](docs/apple-application-analysis.md) |
| Comportement des processus | Sortie du terminal, interactions, code de sortie, observations du système de fichiers et comparaisons | Linux/macOS avec un PTY natif ; [capture de processus](docs/process-capture.md) |
+1 -1
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · **Bahasa Indonesia**
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · **Bahasa Indonesia** · [Italiano](README_it.md)
# REA: Rekayasa Balik Apa Pun
+286
View File
@@ -0,0 +1,286 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · **Italiano**
# REA: Reverse Engineer Anything
### Un unico MCP per l'ingegneria inversa su binari, applicazioni e comportamento a runtime.
**Vedi una funzionalità che ti piace. Capisci come funziona, fino al livello binario.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#cosa-puoi-analizzare)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;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>
**[Sito web](https://rea.tools/) · [Guide](https://rea.tools/guides/) · [Showcase](https://rea.tools/showcase/)**
[Quick start](#quick-start) · [Come funziona REA](#come-funziona-rea) · [Cosa puoi analizzare](#cosa-puoi-analizzare) · [Showcase](#showcase) · [FAQ](#faq) · [Documentazione](#documentazione)
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA che avvia il suo bridge di analisi dentro Hopper mentre ispeziona un binario nativo" width="1200" />
<br /><br />
<table aria-label="Community REA">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>Unisciti alla community di ingegneria inversa</strong>
</a><br />
<sub>Discord · Domande e risposte · Show and Tell</sub>
</td>
</tr>
</table>
<br />
</div>
---
Hai visto una funzionalità in un'app che vorresti nel tuo prodotto? Chiedi al tuo agente di analizzarla con REA. Può ispezionare l'app senza il suo codice sorgente, spiegare come funziona la funzionalità, mostrare le prove e realizzare una versione per il tuo progetto.
REA collega il tuo agente a strumenti per ispezionare binari nativi, app JavaScript ed Electron, assembly .NET e siti web. Puoi usare gli stessi strumenti anche dal terminale. L'analisi viene eseguita localmente e i risultati includono le prove e i limiti dietro ogni conclusione.
La configurazione registra REA con il tuo agente e installa le istruzioni di workflow corrispondenti. L'analisi nativa può usare un'installazione esistente di Hopper, Ghidra o IDA; la configurazione può installare opzionalmente Hopper previa approvazione. L'analisi statica JavaScript non richiede alcun motore di analisi nativo.
> **[Visita il sito web di REA](https://rea.tools/)** per istruzioni di configurazione, guide illustrate e veri casi studio.
## Quick start
### Configura il tuo agente
Con Node.js e npm installati, esegui:
```bash
npx rea-agents setup
```
Scegli i tuoi agenti, rivedi le modifiche proposte e approvale. La configurazione aggiunge il server MCP di REA e le istruzioni di workflow corrispondenti, con backup della configurazione esistente. Riavvia quindi il tuo agente.
La configurazione supporta Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [altri agenti](docs/installation.md#supported-agents). Vedi [installazione e configurazione](docs/installation.md) per la configurazione dei provider e la registrazione manuale MCP.
### Chiedi al tuo agente
```text
Spiega come funziona la ricerca nell'app Note, mostrami le prove e realizza
una funzionalità simile per il mio progetto.
```
Sostituisci Note con la tua app di destinazione e la funzionalità che vuoi capire.
### Usa il terminale
Ispeziona una directory di app JavaScript/Electron estratta o un ASAR:
```bash
npx -y rea-agents@latest analyze-javascript-application /percorso/assoluto/verso/app --json
```
Il risultato include moduli, import, confini Electron e le relative prove. Sostituisci il percorso con il tuo bersaglio, ad esempio `"D:/apps/example"` su Windows.
Per installare il comando `rea` per un uso regolare:
```bash
npm install --global rea-agents
rea --help
```
Per l'analisi nativa, configura prima un provider. Vedi la [guida CLI ed Evidence](docs/cli.md) per i comandi nativi, la selezione del provider, gli snapshot e lo scripting.
### Aggiorna REA
REA cambia rapidamente e le nuove versioni includono correzioni di bug frequenti. Tieni aggiornata la tua installazione.
Per una CLI installata via npm:
```bash
rea update
```
Per aggiornare le registrazioni del tuo agente e lo skill, esegui il comando di configurazione mostrato dall'aggiornamento.
Se usi `npx`, aggiorna la configurazione del tuo agente con:
```bash
npx rea-agents@latest setup
```
Rivedi le modifiche di configurazione e riavvia il tuo agente. Per comandi CLI singoli, usa `npx rea-agents@latest` seguito dal comando.
## Come funziona REA
Il tuo agente chiama REA tramite MCP per ispezionare il bersaglio e tracciare il codice rilevante. REA restituisce i risultati con le relative prove. L'agente li usa per fare domande di approfondimento, spiegare il comportamento o scrivere e testare un'implementazione. Anche i comandi CLI usano gli stessi workflow.
![Flusso di indagine REA: il tuo agente pone domande su un bersaglio locale, REA lo ispeziona e ne traccia il codice usando strumenti di analisi, e l'agente usa il codice, i riferimenti e gli elementi sconosciuti restituiti per spiegare, implementare e testare.](website/public/assets/figures/rea-investigation-flow.svg)
[Apri la figura a dimensione intera](website/public/assets/figures/rea-investigation-flow.svg).
<a id="current-status"></a>
## Cosa puoi analizzare
REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strumenti aggiuntivi e supporto host dipendono dal bersaglio:
| Obiettivo | Cosa restituisce REA | Requisiti e guida |
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/guides/native/) |
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](docs/binary-diagnostics.md) |
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](docs/evm-bytecode.md) |
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](docs/recorded-crashes.md) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/guides/javascript/) |
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/guides/browser/) |
| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](docs/web-network-captures.md) |
| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](docs/managed-code-analysis.md) |
| APK Android | Dichiarazioni manifest, classi, metodi decompilati e riferimenti | JADX headless e un JDK completo su Linux/macOS/Windows x64; [guida Android](docs/android-analysis.md) |
| Firmware | Regioni, risultati di estrazione e passaggi all'analisi nativa | Binwalk / Unblob su Linux; [guida firmware](docs/firmware-analysis.md) |
| Pacchetti e risorse | Inventari file, digest, plist, anatomia dei bundle Apple e risorse estratte | [Guida artifact e JavaScript](docs/javascript-artifact-reconstruction.md), [Applicazioni Apple](docs/apple-application-analysis.md) |
| Comportamento dei processi | Output del terminale, interazioni, osservazioni su uscita e filesystem ed confronti di esecuzione | Linux/macOS con un PTY nativo; [cattura dei processi](docs/process-capture.md) |
L'ispezione statica di JavaScript e .NET legge i file forniti senza eseguire l'applicazione. La cattura a runtime esegue o interagisce con il bersaglio selezionato usando i tuoi permessi utente; ogni guida runtime descrive i suoi effetti.
<a id="choosing-a-deep-analysis-provider"></a>
I formati nativi e il supporto host variano in base al provider. Vedi la [configurazione di Hopper e Ghidra](docs/installation.md#hopper), la [guida IDA](docs/ida-provider.md) e il [supporto sperimentale Windows Ghidra](docs/windows-ghidra-p0.md). Ghidra supporta anche l'[analisi DOS a 16 bit](docs/ghidra-dos.md). Per binari di grandi dimensioni, aumenta il timeout di avvio con `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Per la selezione del provider, vedi la [guida CLI](docs/cli.md#choose-a-provider). Controlla la [disponibilità delle release](docs/installation.md#released-package-and-main) per le funzionalità aggiunte dall'ultima release npm.
## Showcase
[![Illustrazioni degli showcase del sound-pan di DX-Ball, del clipboard-bridge di Notion e dell'anello di proiettili di TH04](docs/assets/rea-showcases.png)](https://rea.tools/showcase/)
### DX-Ball: ricostruisci un calcolo sound-pan
Segui una chiamata sonora fino al suo helper posizione-to-pan, ispeziona le istruzioni e trasforma il pseudocodice incompleto in C. La ricostruzione supera 3.205 casi x86 originali e riproduce tutti i 63 byte di funzione compilati.
[Leggi il case study](https://rea.tools/showcase/dx-ball/) · [Repository di ricostruzione](https://github.com/N0zoM1z0/dx-ball)
### Notion: traccia il clipboard bridge di Electron
Trova l'API clipboard del renderer, seguila attraverso preload e IPC fino al processo principale e ispeziona il formato rich clipboard.
[Leggi il case study](https://rea.tools/showcase/notion/)
### TH04: recupera un calcolo dell'anello di proiettili DOS
Ispeziona le istruzioni a 16 bit del gioco PC-98 originale, recupera i calcoli dell'angolo fisso e mirato e confronta il C++ ricostruito con l'output del compilatore storico.
[Leggi il case study](https://rea.tools/showcase/th04/) · [Repository di ricostruzione](https://github.com/N0zoM1z0/th04)
Se hai usato REA su qualcosa di interessante, ci piacerebbe vederlo. Condividi il tuo caso in una [issue](https://github.com/morluto/rea/issues) o in una [pull request](https://github.com/morluto/rea/pulls), includendo il bersaglio, la tua domanda, come REA ti ha aiutato e cosa hai scoperto.
## FAQ
<details>
<summary><strong>Quali agenti possono usare REA?</strong></summary>
Qualsiasi agente che supporti server MCP locali. La configurazione configura gli [agenti supportati](docs/installation.md#supported-agents); altri client possono usare la [registrazione MCP manuale](docs/installation.md#mcp-registry).
</details>
<details>
<summary><strong>Ho bisogno di Hopper, Ghidra o IDA?</strong></summary>
L'analisi nativa profonda ne usa uno. L'ispezione statica di JavaScript e .NET funziona senza un motore di analisi nativo. La configurazione può installare Hopper previa approvazione; Ghidra e IDA usano le tue installazioni esistenti. Vedi [configurazione provider](docs/installation.md#hopper).
</details>
<details>
<summary><strong>Devo avviare Hopper per primo?</strong></summary>
REA avvia Hopper quando un'operazione ne ha bisogno. Su macOS, una finestra di dialogo al primo avvio potrebbe chiederti di scegliere la modalità demo o di attivare la licenza. Vedi [avvio di Hopper e risoluzione dei problemi](docs/installation.md#launcher-paths-and-troubleshooting).
</details>
<details>
<summary><strong>Cosa fa l'installazione dello skill da skills.sh?</strong></summary>
Lo skill fornisce istruzioni di indagine per il tuo agente. Usa `npx rea-agents setup` per registrare il server MCP di REA e installare le istruzioni corrispondenti, quindi riavvia il tuo agente. Vedi [installazione solo skill](docs/installation.md#skill-only-installation).
</details>
<details>
<summary><strong>Quale codice restituisce REA?</strong></summary>
L'analisi nativa restituisce pseudocodice e assembly. L'analisi JavaScript/Electron recupera moduli e le loro relazioni. Il tuo agente usa questi risultati per scrivere e testare un'implementazione; gli [showcase](#showcase) offrono esempi concreti.
</details>
<details>
<summary><strong>REA carica la mia app da qualche parte?</strong></summary>
REA analizza i bersagli localmente. Il tuo agente riceve i risultati degli strumenti e il suo provider di modello ha la propria politica sui dati.
</details>
<details>
<summary><strong>Cosa devo fare se trovo un bug?</strong></summary>
Aggiorna prima; una release recente potrebbe averlo già risolto.
Per una CLI installata via npm:
```bash
rea update
```
Per la configurazione dell'agente tramite `npx`:
```bash
npx rea-agents@latest setup
```
Se stai usando un agente, completa l'[aggiornamento della configurazione](#aggiorna-rea) e riavvialo. Riprova lo stesso task. Se il problema persiste, [apri un issue](https://github.com/morluto/rea/issues) con la tua versione di REA, il tipo di bersaglio, i passaggi per riprodurlo e l'output di errore.
</details>
## Documentazione
Inizia dalle [guide pratiche](https://rea.tools/guides/) del sito web. Per opzioni esatte, prerequisiti e contratti dei risultati:
- [Installazione e configurazione](docs/installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione.
- [Pronto all'uso e risoluzione dei problemi](docs/installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi.
- [CLI ed Evidence](docs/cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita.
- [Contratti MCP](docs/mcp-contracts.md) e [prompt agente](docs/mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate.
- [Catalogo strumenti](docs/mcp-contracts.md#generated-catalog): inventario generato di strumenti, provider e comandi CLI.
- [Roadmap](docs/roadmap.md): lavoro pianificato e tracker delle capacità.
Segnala vulnerabilità tramite [SECURITY.md](SECURITY.md).
## Cronologia delle stelle
🎉 **50.000 stelle su GitHub — grazie!**
Grazie a tutti coloro che usano REA, segnalano bug, propongono funzionalità, testano le build e contribuiscono con correzioni.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="Cronologia stelle GitHub di REA" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## Disclaimer
REA fornisce strumenti per la ricerca, l'analisi e la ricostruzione di ingegneria inversa lecita. Sei responsabile di ottenere le eventuali autorizzazioni richieste e di rispettare le leggi applicabili. Il progetto non approva l'uso illegale o non autorizzato.
REA è un progetto software open-source. Non abbiamo emesso né approvato alcuna criptovaluta o token. I token che usano il nome REA non sono affiliati al progetto.
## Contributing
Apprezzeremmo il tuo aiuto con REA! [Apri un issue](https://github.com/morluto/rea/issues) per segnalare un bug o suggerire una funzionalità, oppure [invia una pull request](https://github.com/morluto/rea/pulls) per migliorare il codice o la documentazione.
Vedi [CONTRIBUTING.md](CONTRIBUTING.md) per la configurazione di sviluppo e i controlli, [testing](docs/testing.md) per le lane di verifica e la [mappa dell'architettura](docs/architecture.mermaid) per la struttura del progetto.
## License
[MIT](LICENSE)
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA:あらゆるものをリバースエンジニアリング
@@ -135,13 +135,13 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| -------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/guides/native/) |
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](docs/binary-diagnostics.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | ローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | 利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](docs/evm-bytecode.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](docs/recorded-crashes.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/guides/javascript/) |
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/guides/browser/) |
| 保存済みのネットワークキャプチャ | リクエスト、レスポンス、取得可能なペイロード、ソース内の位置 | HAR;mitmproxy ネイティブ形式のキャプチャには Linux 上の mitmdump;[キャプチャガイド](docs/web-network-captures.md) |
| .NET アセンブリ | メタデータ、CIL 命令、宣言されたネイティブ依存関係、ビルドの比較 | 静的な検査;[マネージドコードのガイド](docs/managed-code-analysis.md) |
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
| Android APK | マニフェストの宣言、クラス、逆コンパイルされたメソッド、参照 | Linux/macOS/Windows x64 上のヘッドレス JADX と完全な JDK;[Android ガイド](docs/android-analysis.md) |
| ファームウェア | 領域、抽出結果、ネイティブ分析への引き継ぎ | Linux 上の Binwalk / Unblob;[ファームウェアガイド](docs/firmware-analysis.md) |
| パッケージとリソース | ファイル一覧、ダイジェスト、plist、Apple バンドルの構造、抽出されたリソース | [アーティファクトと JavaScript のガイド](docs/javascript-artifact-reconstruction.md)、[Apple アプリケーション](docs/apple-application-analysis.md) |
| プロセスの動作 | ターミナル出力、対話、終了とファイルシステムの観測結果、実行の比較 | ネイティブ PTY を備えた Linux/macOS;[プロセスキャプチャ](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: 무엇이든 리버스 엔지니어링
@@ -135,13 +135,13 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/guides/native/) |
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](docs/binary-diagnostics.md) |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](docs/evm-bytecode.md) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](docs/recorded-crashes.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/guides/javascript/) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/guides/browser/) |
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](docs/web-network-captures.md) |
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 정적 검사; [관리 코드 가이드](docs/managed-code-analysis.md) |
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](docs/android-analysis.md) |
| 펌웨어 | 영역, 추출 결과, 네이티브 분석으로 전달할 정보 | Linux의 Binwalk / Unblob; [펌웨어 가이드](docs/firmware-analysis.md) |
| 패키지 및 리소스 | 파일 목록, 다이제스트, plist, Apple 번들 구조, 추출한 리소스 | [아티팩트 및 JavaScript 가이드](docs/javascript-artifact-reconstruction.md), [Apple 애플리케이션](docs/apple-application-analysis.md) |
| 프로세스 동작 | 터미널 출력, 상호작용, 종료 및 파일 시스템 관찰 결과, 실행 비교 | 네이티브 PTY를 지원하는 Linux/macOS; [프로세스 캡처](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Reverse Engineer Anything
@@ -135,13 +135,13 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/guides/native/) |
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](docs/binary-diagnostics.md) |
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym; [przewodnik EVM offline](docs/evm-bytecode.md) |
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](docs/recorded-crashes.md) |
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](docs/evm-bytecode.md) |
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](docs/recorded-crashes.md) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/guides/javascript/) |
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/guides/browser/) |
| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](docs/web-network-captures.md) |
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](docs/managed-code-analysis.md) |
| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemie Linux/macOS; [przewodnik Androida](docs/android-analysis.md) |
| Pakiety APK dla Androida | Deklaracje manifestu, klasy, zdekompilowane metody i odwołania | JADX bez interfejsu graficznego i pełny JDK w systemach Linux/macOS/Windows x64; [przewodnik Androida](docs/android-analysis.md) |
| Oprogramowanie układowe | Regiony, wyniki wyodrębniania i przekazanie do analizy kodu natywnego | Binwalk / Unblob w systemie Linux; [przewodnik oprogramowania układowego](docs/firmware-analysis.md) |
| Pakiety i zasoby | Spisy plików, skróty, pliki plist, struktura pakietów Apple i wyodrębnione zasoby | [Przewodnik artefaktów i JavaScript](docs/javascript-artifact-reconstruction.md), [aplikacje Apple](docs/apple-application-analysis.md) |
| Zachowanie procesów | Wyjście terminala, interakcje, obserwacje zakończenia i systemu plików oraz porównania uruchomień | Linux/macOS z natywnym PTY; [przechwytywanie procesów](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Faça engenharia reversa de qualquer coisa
@@ -135,13 +135,13 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/guides/native/) |
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](docs/binary-diagnostics.md) |
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal; [guia de EVM sem execução](docs/evm-bytecode.md) |
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador; GDB/pwndbg opcionais; [falhas registradas](docs/recorded-crashes.md) |
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](docs/evm-bytecode.md) |
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](docs/recorded-crashes.md) |
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/guides/javascript/) |
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/guides/browser/) |
| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](docs/web-network-captures.md) |
| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](docs/managed-code-analysis.md) |
| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS; [guia de Android](docs/android-analysis.md) |
| APKs Android | Declarações do manifesto, classes, métodos descompilados e referências | JADX sem interface gráfica e um JDK completo no Linux/macOS/Windows x64; [guia de Android](docs/android-analysis.md) |
| Firmware | Regiões, resultados de extração e encaminhamentos à análise nativa | Binwalk / Unblob no Linux; [guia de firmware](docs/firmware-analysis.md) |
| Pacotes e recursos | Inventários de arquivos, digests, plists, estrutura de bundles Apple e recursos extraídos | [Guia de artefatos e JavaScript](docs/javascript-artifact-reconstruction.md), [aplicativos Apple](docs/apple-application-analysis.md) |
| Comportamento de processos | Saída do terminal, interações, observações de encerramento e do sistema de arquivos, e comparações entre execuções | Linux/macOS com PTY nativo; [captura de processos](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Реверс-инжиниринг чего угодно
@@ -135,13 +135,13 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/guides/native/) |
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](docs/binary-diagnostics.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление; [руководство по EVM без запуска](docs/evm-bytecode.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной; необязательные GDB/pwndbg; [записанные сбои](docs/recorded-crashes.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](docs/evm-bytecode.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](docs/recorded-crashes.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/guides/javascript/) |
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/guides/browser/) |
| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](docs/web-network-captures.md) |
| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](docs/managed-code-analysis.md) |
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS; [руководство по Android](docs/android-analysis.md) |
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](docs/android-analysis.md) |
| Прошивки | Области, результаты извлечения и передача в нативный анализ | Binwalk / Unblob в Linux; [руководство по прошивкам](docs/firmware-analysis.md) |
| Пакеты и ресурсы | Списки файлов, хеши, plist, структура пакетов Apple и извлечённые ресурсы | [Руководство по артефактам и JavaScript](docs/javascript-artifact-reconstruction.md), [приложения Apple](docs/apple-application-analysis.md) |
| Поведение процессов | Вывод терминала, взаимодействия, наблюдения за завершением и файловой системой, сравнения запусков | Linux/macOS с нативным PTY; [захват процессов](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Reverse Engineer Anything
@@ -135,13 +135,13 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/guides/native/) |
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](docs/binary-diagnostics.md) |
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหก; [คู่มือ EVM แบบออฟไลน์](docs/evm-bytecode.md) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียม; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](docs/recorded-crashes.md) |
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](docs/evm-bytecode.md) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](docs/recorded-crashes.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/guides/javascript/) |
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/guides/browser/) |
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](docs/web-network-captures.md) |
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](docs/managed-code-analysis.md) |
| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS; [คู่มือ Android](docs/android-analysis.md) |
| APK ของ Android | การประกาศใน manifest คลาส เมธอดที่ดีคอมไพล์ และการอ้างอิง | JADX แบบไม่มี GUI และ JDK แบบเต็มบน Linux/macOS/Windows x64; [คู่มือ Android](docs/android-analysis.md) |
| เฟิร์มแวร์ | พื้นที่ข้อมูล ผลการแตกไฟล์ และการส่งต่อไปวิเคราะห์เนทีฟ | Binwalk / Unblob บน Linux; [คู่มือเฟิร์มแวร์](docs/firmware-analysis.md) |
| แพ็กเกจและทรัพยากร | รายการไฟล์ ค่าแฮช plist โครงสร้างบันเดิลของ Apple และทรัพยากรที่แตกออกมา | [คู่มืออาร์ติแฟกต์และ JavaScript](docs/javascript-artifact-reconstruction.md), [แอปพลิเคชัน Apple](docs/apple-application-analysis.md) |
| พฤติกรรมของโปรเซส | เอาต์พุตเทอร์มินัล การโต้ตอบ ข้อมูลการสิ้นสุดและระบบไฟล์ที่สังเกตได้ และการเปรียบเทียบการรัน | Linux/macOS ที่มี PTY เนทีฟ; [การบันทึกโปรเซส](docs/process-capture.md) |
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Her şeyi tersine mühendislikle inceleyin
@@ -135,13 +135,13 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/guides/native/) |
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](docs/binary-diagnostics.md) |
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](docs/evm-bytecode.md) |
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](docs/recorded-crashes.md) |
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](docs/evm-bytecode.md) |
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](docs/recorded-crashes.md) |
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/guides/javascript/) |
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/guides/browser/) |
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR; mitmproxy'nin kendi biçimindeki yakalamalar için Linux üzerinde mitmdump; [ağ yakalama rehberi](docs/web-network-captures.md) |
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](docs/managed-code-analysis.md) |
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](docs/android-analysis.md) |
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](docs/android-analysis.md) |
| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](docs/firmware-analysis.md) |
| Paketler ve kaynaklar | Dosya envanterleri, özet değerleri, plist'ler, Apple paket yapısı ve çıkarılmış kaynaklar | [Artefakt ve JavaScript rehberi](docs/javascript-artifact-reconstruction.md), [Apple uygulamaları](docs/apple-application-analysis.md) |
| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](docs/process-capture.md) |
+15 -15
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Реверс-інжиніринг будь-чого
@@ -131,20 +131,20 @@ npx rea-agents@latest setup
REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а також npm. Додаткові інструменти й підтримувані системи залежать від цілі:
| Ціль | Що повертає REA | Вимоги й посібник |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/guides/native/) |
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](docs/binary-diagnostics.md) |
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення; [посібник EVM без запуску](docs/evm-bytecode.md) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент; необов'язкові GDB/pwndbg; [записані аварійні завершення](docs/recorded-crashes.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/guides/javascript/) |
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/guides/browser/) |
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](docs/web-network-captures.md) |
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](docs/managed-code-analysis.md) |
| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS; [посібник Android](docs/android-analysis.md) |
| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) |
| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) |
| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) |
| Ціль | Що повертає REA | Вимоги й посібник |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/guides/native/) |
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](docs/binary-diagnostics.md) |
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](docs/evm-bytecode.md) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](docs/recorded-crashes.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/guides/javascript/) |
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/guides/browser/) |
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](docs/web-network-captures.md) |
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](docs/managed-code-analysis.md) |
| Android APK | Оголошення маніфесту, класи, декомпільовані методи й посилання | JADX без графічного інтерфейсу й повний JDK у Linux/macOS/Windows x64; [посібник Android](docs/android-analysis.md) |
| Прошивки | Області, результати видобування й передавання до нативного аналізу | Binwalk / Unblob у Linux; [посібник прошивок](docs/firmware-analysis.md) |
| Пакети й ресурси | Переліки файлів, хеші, plist, структура пакетів Apple та видобуті ресурси | [Посібник артефактів і JavaScript](docs/javascript-artifact-reconstruction.md), [застосунки Apple](docs/apple-application-analysis.md) |
| Поведінка процесів | Вивід термінала, взаємодії, спостереження за завершенням і файловою системою та порівняння запусків | Linux/macOS із нативним PTY; [захоплення процесів](docs/process-capture.md) |
Статична перевірка JavaScript і .NET читає надані файли, не запускаючи застосунок. Захоплення під час виконання запускає вибрану ціль або взаємодіє з нею з правами вашого користувача; відповідні посібники описують його вплив.
+4 -4
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA: Kỹ thuật dịch ngược cho mọi thứ
@@ -135,13 +135,13 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/guides/native/) |
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](docs/binary-diagnostics.md) |
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân; [hướng dẫn EVM ngoại tuyến](docs/evm-bytecode.md) |
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](docs/recorded-crashes.md) |
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](docs/evm-bytecode.md) |
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](docs/recorded-crashes.md) |
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/guides/javascript/) |
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/guides/browser/) |
| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](docs/web-network-captures.md) |
| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](docs/managed-code-analysis.md) |
| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS; [hướng dẫn Android](docs/android-analysis.md) |
| APK Android | Khai báo manifest, lớp, phương thức được dịch ngược và tham chiếu | JADX không giao diện đồ họa và JDK đầy đủ trên Linux/macOS/Windows x64; [hướng dẫn Android](docs/android-analysis.md) |
| Phần sụn | Các vùng, kết quả trích xuất và dữ liệu chuyển sang phân tích mã máy | Binwalk / Unblob trên Linux; [hướng dẫn phần sụn](docs/firmware-analysis.md) |
| Gói và tài nguyên | Danh sách tệp, giá trị băm, plist, cấu trúc bundle Apple và tài nguyên được trích xuất | [Hướng dẫn hiện vật phần mềm và JavaScript](docs/javascript-artifact-reconstruction.md), [ứng dụng Apple](docs/apple-application-analysis.md) |
| Hành vi tiến trình | Đầu ra terminal, tương tác, quan sát khi thoát và hệ thống tệp, cùng so sánh các lần chạy | Linux/macOS có PTY gốc; [ghi lại tiến trình](docs/process-capture.md) |
+5 -5
View File
@@ -1,8 +1,8 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA:逆向分析一切
# REA:逆向一切
### 通過一個 MCP 服務,逆向分析二進位檔案、應用程式和執行階段行為。
@@ -135,13 +135,13 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| --------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](docs/binary-diagnostics.md) |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | 本地原始位元組/十六進位輸入載體;[離線 EVM 指南](docs/evm-bytecode.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | 呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](docs/recorded-crashes.md) |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](docs/evm-bytecode.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](docs/recorded-crashes.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/guides/javascript/) |
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/guides/browser/) |
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](docs/web-network-captures.md) |
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](docs/managed-code-analysis.md) |
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS 上的無界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](docs/firmware-analysis.md) |
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 應用](docs/apple-application-analysis.md) |
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](docs/process-capture.md) |
+5 -5
View File
@@ -1,8 +1,8 @@
<div align="center">
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md)
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
# REA:逆向分析一切
# REA:逆向一切
### 通过一个 MCP 服务,逆向分析二进制文件、应用程序和运行时行为。
@@ -135,13 +135,13 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| --------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/guides/native/) |
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](docs/binary-diagnostics.md) |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | 本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | 调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](docs/evm-bytecode.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](docs/recorded-crashes.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/guides/javascript/) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/guides/browser/) |
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](docs/web-network-captures.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](docs/managed-code-analysis.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](docs/android-analysis.md) |
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](docs/firmware-analysis.md) |
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](docs/javascript-artifact-reconstruction.md)、[Apple 应用](docs/apple-application-analysis.md) |
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](docs/process-capture.md) |
+6 -7
View File
@@ -841,14 +841,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
private JsonObject instructionReference(Reference reference) {
JsonObject item = new JsonObject();
RefType type = reference.getReferenceType();
JsonObject kind = referenceKind(reference);
item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
item.addProperty("target_address", canonicalAddress(reference.getToAddress()));
item.addProperty("type", type.getName());
item.addProperty("call", type.isCall());
item.addProperty("jump", type.isJump());
item.addProperty("indirect", type.isIndirect());
item.addProperty("computed", type.isComputed());
item.addProperty("operand_index", reference.getOperandIndex());
for (String field : List.of("type", "call", "jump", "indirect", "computed",
"operand_index", "data", "read", "write", "primary", "provenance"))
item.add(field, kind.get(field));
item.addProperty("source", normalizedSource(reference.getSource()));
return item;
}
+10 -1
View File
@@ -129,6 +129,10 @@ signature validation, full resource-table semantics, native-library analysis or
Android runtime capture. An `.aab` suffix is a ZIP archive, not an APK, so
Android tools and the application graph do not treat it as a standalone APK.
Existing artifact inventory/extraction tools can supply archive evidence.
Inventory records an APK root only for ZIP bytes whose name ends in `.apk`.
The projection adds a limitation when no root `AndroidManifest.xml` was
observed, because such a ZIP may not be an APK. Nested members take a ZIP or
ASAR family from their bytes; an unread member keeps an unknown format.
Inventory keeps ZIP names that differ only in case and records that a
case-insensitive destination cannot store both spellings. Extraction fails
only when that destination directory cannot keep both names. Project that
@@ -149,7 +153,12 @@ rea project-android-application-graph '{"inventory_evidence":[<inventory_artifac
The projection reports exact component paths and hashes, runtime-family hints,
and path-based bridge hypotheses. It does not decode DEX or claim observed
runtime calls. Bridge hypotheses form a DEX/JVM-class by native-library
runtime calls. Framework hints match complete native library names, such as
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
The bridge basis is inferred from the native path and repeated for each managed
component, so it does not identify a specific JNI binding. Bridge hypotheses form a DEX/JVM-class by native-library
Cartesian product. The projection computes the product and exact serialized
candidate-array size before creating candidate objects, then retains a
deterministic prefix within a 2 MiB UTF-8 JSON budget. If candidates are omitted,
+10
View File
@@ -7,6 +7,16 @@ hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
runtime calls.
Framework hints match complete path segments such as `React.framework`,
`ReactNative.framework`, `hermes.framework`, `Flutter.framework`, and
`UnityFramework.framework`. Substrings in `Preact.framework`,
`Community.framework`, or `MyApp.framework` do not identify those families.
A generic `App.framework` also does not identify Flutter by itself. Runtime
families remain inventory-derived hints, not observed loading. Generic framework
and dylib paths do not establish Swift or Objective-C; this projection does not
decode language metadata. A bridge basis is inferred from the native path and
repeated for each paired script, rather than proving a particular binding.
```sh
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
```
+1 -1
View File
@@ -233,7 +233,7 @@ or process workflows.
## CLI and verification
All five CLI commands accept inline JSON or a path to a JSON file. The CLI
All six 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.
+8
View File
@@ -195,6 +195,14 @@ outside-memory and undecodable addresses have separate outcomes. Effective
memory base/index/displacement roles and per-instruction context mode remain
unavailable; `mode` is the program language variant.
Compact instruction references and call-target references preserve the source
address, operand index, DATA/READ/WRITE classification, primary status, provider
`provenance`, and reference `source` (for Ghidra: `analysis`, `default`, `imported`
or `user_defined`). Primary status reports the provider's chosen reference; it
does not establish literal addressing. Analyzer-inferred indexed targets remain
static references, not observed runtime reads or writes. Older evidence may omit
the additive fields, and null or omitted facts mean unavailable rather than false.
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
operation. `flow.classification_evidence` preserves the Listing flow type and
the observed return evidence; mnemonic spelling alone is not the classifier.
+21
View File
@@ -146,6 +146,27 @@ Anthropic Messages/SSE fixture preserves native resource-hint envelopes in its
request artifacts. Usage is synthetic; live Anthropic API, native Windows and
bare-mode personal-skill activation remain unverified by this lane.
`verify:copilot-client` requires installed GitHub Copilot CLI (verified with
`@github/copilot@1.0.95`); select it with `REA_VERIFY_COPILOT_COMMAND`.
The optional POSIX lane uses native `COPILOT_HOME` discovery, an independent
Git workspace, guarded setup plans, preserved registrations, backups and
idempotence. Native `skill add` registers the isolated installed skill; call
mode loads its full body, validates all forwarded input schemas and checks
named-schema JavaScript Evidence for a Unicode path. When Copilot spills a
large MCP result, the model requests native `view` with `forceReadLargeFiles`
and validates the complete returned Evidence rather than its preview.
Use `-- chat` for ordinary
chat or `REA_VERIFY_RUNTIME_ROOT` for a production-only installed package.
The native offline BYOK adapter uses a loopback OpenAI completions/SSE fixture
and `gpt-5.4` model metadata; model inference and token usage are synthetic.
Set `REA_VERIFY_COPILOT_MODEL` to examine another model configuration. In the
verified client, `gpt-4.1` blocks the complete catalog before HTTP with
`compaction_static_context_blocked`, even when requesting a larger BYOK prompt
capacity. The effective capacity is unknown; this lane does not establish a
fix for that client/model limit. It preserves the complete catalog and schemas.
Live model APIs, native Windows and default OS-home skill discovery remain
unverified.
`verify:deepseek-client` requires an installed DeepSeek Harness (`dsh`;
verified with `@deepseek-ai/dsh@0.2.0-rc.2`) and Git. Set
`REA_VERIFY_DEEPSEEK_COMMAND` to select its executable. This optional POSIX lane
+1
View File
@@ -182,6 +182,7 @@
"verify:qwen-client": "npm run build:cached && node scripts/verify-qwen-client.mjs",
"verify:gemini-client": "npm run build:cached && node scripts/verify-gemini-client.mjs",
"verify:opencode-client": "npm run build:cached && node scripts/verify-opencode-client.mjs",
"verify:copilot-client": "npm run build:cached && node scripts/verify-copilot-client.mjs",
"verify:claude-client": "npm run build:cached && node scripts/verify-claude-client.mjs",
"verify:pi-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs pi",
"verify:hermes-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs hermes",
+43 -5
View File
@@ -13,7 +13,7 @@ for (const architecture of ["x86_64", "x86"]) {
"fixtures",
`rea-ghidra-windows${x86 ? "-x86" : ""}.${dll ? "dll" : "exe"}`,
);
const bytes = Buffer.alloc(1024);
const bytes = Buffer.alloc(dll ? 1024 : 1536);
const peOffset = 0x80;
const optionalHeader = peOffset + 24;
const optionalHeaderSize = x86 ? 0xe0 : 0xf0;
@@ -24,7 +24,7 @@ for (const architecture of ["x86_64", "x86"]) {
bytes.writeUInt32LE(peOffset, 0x3c);
bytes.write("PE\0\0", peOffset, "binary");
bytes.writeUInt16LE(x86 ? 0x14c : 0x8664, peOffset + 4);
bytes.writeUInt16LE(1, peOffset + 6);
bytes.writeUInt16LE(dll ? 1 : 2, peOffset + 6);
bytes.writeUInt16LE(optionalHeaderSize, peOffset + 20);
bytes.writeUInt16LE(
(x86 ? 0x102 : 0x22) | (dll ? 0x2000 : 0),
@@ -33,6 +33,7 @@ for (const architecture of ["x86_64", "x86"]) {
bytes.writeUInt16LE(x86 ? 0x10b : 0x20b, optionalHeader);
bytes.writeUInt32LE(0x200, optionalHeader + 4);
if (!dll) bytes.writeUInt32LE(0x200, optionalHeader + 8);
bytes.writeUInt32LE(dll ? 0 : 0x1000, optionalHeader + 16);
bytes.writeUInt32LE(0x1000, optionalHeader + 20);
if (x86) bytes.writeUInt32LE(0x400000, optionalHeader + 28);
@@ -41,7 +42,7 @@ for (const architecture of ["x86_64", "x86"]) {
bytes.writeUInt32LE(0x200, optionalHeader + 36);
bytes.writeUInt16LE(6, optionalHeader + 40);
bytes.writeUInt16LE(6, optionalHeader + 48);
bytes.writeUInt32LE(0x2000, optionalHeader + 56);
bytes.writeUInt32LE(dll ? 0x2000 : 0x3000, optionalHeader + 56);
bytes.writeUInt32LE(0x200, optionalHeader + 60);
bytes.writeUInt16LE(3, optionalHeader + 68);
bytes.writeUInt16LE(x86 ? 0x8140 : 0x8160, optionalHeader + 70);
@@ -60,7 +61,7 @@ for (const architecture of ["x86_64", "x86"]) {
}
bytes.write(".text", sectionTable, "ascii");
bytes.writeUInt32LE(dll ? 0x180 : 0x16, sectionTable + 8);
bytes.writeUInt32LE(dll ? 0x180 : 0x50, sectionTable + 8);
bytes.writeUInt32LE(0x1000, sectionTable + 12);
bytes.writeUInt32LE(0x200, sectionTable + 16);
bytes.writeUInt32LE(0x200, sectionTable + 20);
@@ -70,11 +71,48 @@ for (const architecture of ["x86_64", "x86"]) {
bytes.set(
dll
? [0xe8, 0x0b, 0, 0, 0, 0x83, 0xc0, 1, 0xc3]
: [0xe8, 0x0b, 0, 0, 0, 0x31, 0xc0, 0xc3],
: [0xe8, 0x0b, 0, 0, 0, 0xe8, 0x16, 0, 0, 0, 0x31, 0xc0, 0xc3],
0x200,
);
bytes.set([0xb8, 0x2a, 0, 0, 0, 0xc3], 0x210);
if (!dll) {
const dataSection = sectionTable + 40;
bytes.write(".data", dataSection, "ascii");
bytes.writeUInt32LE(0x20, dataSection + 8);
bytes.writeUInt32LE(0x2000, dataSection + 12);
bytes.writeUInt32LE(0x200, dataSection + 16);
bytes.writeUInt32LE(0x400, dataSection + 20);
bytes.writeUInt32LE(0xc000_0040, dataSection + 36);
bytes.writeUInt32LE(0x1122_3344, 0x400);
const referenceInstructions = x86
? [
"ba00204000", // MOV EDX, address: DATA only.
"a100204000", // MOV EAX, [address]: READ.
"a304204000", // MOV [address], EAX: WRITE.
"ba10204000", // Indexed base, unknown ECX.
"85db", // TEST EBX, EBX: two candidate bases.
"7405", // JZ over the alternate base.
"ba18204000", // MOV EDX, alternate base.
"c6040a01", // MOV byte [EDX + ECX], 1.
"ffd7", // CALL EDI: unknown target.
"c3",
]
: [
"488d15d90f0000", // LEA RDX, [RIP + displacement]: DATA.
"8b05d30f0000", // MOV EAX, [RIP + displacement]: READ.
"8905d10f0000", // MOV [RIP + displacement], EAX: WRITE.
"488d15d60f0000", // Indexed base, unknown RCX.
"85db", // TEST EBX, EBX: two candidate bases.
"7407", // JZ over the alternate base.
"488d15d30f0000", // LEA RDX, alternate base.
"c6040a01", // MOV byte [RDX + RCX], 1.
"41ffd0", // CALL R8: unknown target.
"c3",
];
bytes.set(Buffer.from(referenceInstructions.join(""), "hex"), 0x220);
}
if (dll) {
// Export both functions; an entry-point-free library is discovered by exports.
bytes.writeUInt32LE(0x1040, directories);
@@ -0,0 +1,170 @@
import assert from "node:assert/strict";
/** Verify actual reference facts on the source-owned Windows PE fixture. */
export async function verifyGhidraCompactReferences(call, x86, cli) {
const base = x86 ? 0x400000n : 0x1_4000_0000n;
const address = (offset) => `0x${(base + BigInt(offset)).toString(16)}`;
const cases = [
["data", 0x1020, 0x2000],
["read", x86 ? 0x1025 : 0x1027, 0x2000],
["write", x86 ? 0x102a : 0x102d, 0x2004],
];
const full = await call("procedure_references", {
procedure: address(0x1020),
});
const inspected = {};
for (const [kind, offset, target] of cases) {
const instruction = await call("inspect_native_instruction", {
address: address(offset),
});
inspected[kind] = instruction;
const reference = instruction.references.find(
(item) => item.target_address === address(target),
);
assert.ok(reference, `Missing ${kind} fixture reference`);
assertReference(reference, instruction.address, full.references);
assert.equal(reference.data, true);
assert.equal(reference.read, kind === "read");
assert.equal(reference.write, kind === "write");
}
const indexed = await call("inspect_native_instruction", {
address: address(x86 ? 0x103d : 0x1045),
});
for (const reference of indexed.references)
assertReference(reference, indexed.address, full.references);
assert.ok(
indexed.references.some(
(reference) =>
reference.primary === false && reference.source === "analysis",
),
"Indexed fixture lacks a non-primary analyzer reference",
);
const { direct, targets } = await verifyDirectCall(
call,
address(0x1000),
address(0x1010),
);
const indirectAddress = address(x86 ? 0x1041 : 0x1049);
const indirect = await call("inspect_native_instruction", {
address: indirectAddress,
});
const unresolved = await call("resolve_native_call_targets", {
address: indirectAddress,
});
assert.equal(indirect.flow.kind, "call");
assert.deepEqual(indirect.references, []);
assert.equal(unresolved.status, "unresolved");
assert.deepEqual(unresolved.targets, []);
const returned = await verifyEmptyReferences(
call,
address(x86 ? 0x1043 : 0x104c),
);
if (cli !== undefined) {
for (const instruction of [inspected.write, indexed, direct, returned])
assert.deepEqual(
await cli("inspect-native-instruction", instruction.address),
instruction,
);
assert.deepEqual(
await cli("resolve-native-call-targets", direct.address),
targets,
);
assert.deepEqual(
await cli("resolve-native-call-targets", indirectAddress),
unresolved,
);
}
return {
data_read_write: true,
source_primary_provenance: true,
full_reference_parity: true,
direct_call_reference_parity: true,
indexed_reference_count: indexed.references.length,
indexed_non_primary_count: indexed.references.filter(
(reference) => reference.primary === false,
).length,
indexed_non_primary_analyzer_reference: true,
indexed_reference_kinds: indexed.references.map(
({ type, read, write, primary, source }) => ({
type,
read,
write,
primary,
source,
}),
),
targetless_indirect: true,
empty_and_unavailable: true,
cli_mcp_parity: cli !== undefined,
};
}
async function verifyDirectCall(call, entry, callee) {
const direct = await call("inspect_native_instruction", { address: entry });
const targets = await call("resolve_native_call_targets", { address: entry });
assert.equal(targets.status, "direct");
assert.equal(targets.targets[0].address, callee);
assert.deepEqual(
targets.targets[0].references,
direct.references.filter((item) => item.call),
);
const full = await call("procedure_references", { procedure: entry });
for (const reference of direct.references)
assertReference(reference, entry, full.references);
return { direct, targets };
}
async function verifyEmptyReferences(call, address) {
const returned = await call("inspect_native_instruction", { address });
assert.equal(returned.flow.kind, "return");
assert.deepEqual(returned.references, []);
assert.equal(
(await call("resolve_native_call_targets", { address })).status,
"not-call",
);
const outside = await call("inspect_native_instruction", { address: "0x0" });
assert.equal(outside.status, "outside-memory");
assert.deepEqual(outside.references, []);
assert.equal(
(await call("resolve_native_call_targets", { address: "0x0" })).status,
"unavailable",
);
return returned;
}
function assertReference(reference, source, full) {
assert.equal(reference.source_address, source);
assert.equal(reference.provenance, "ghidra-reference-manager");
assert.equal(typeof reference.primary, "boolean");
assert.equal(typeof reference.source, "string");
assert.ok(reference.source.length > 0);
const edge = full.find(
(item) =>
item.source_address === source &&
item.target_address === reference.target_address &&
item.kind.operand_index === reference.operand_index,
);
assert.ok(edge, "Compact reference lacks its matching full reference");
for (const field of [
"type",
"call",
"jump",
"indirect",
"computed",
"operand_index",
"data",
"read",
"write",
"primary",
"provenance",
])
assert.equal(
reference[field],
edge.kind[field],
`Reference ${field} differs from its full representation`,
);
}
+440
View File
@@ -0,0 +1,440 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv2020 from "ajv/dist/2020.js";
import {
TOOL_CONTRACTS,
toolContract,
} from "../dist/contracts/toolContracts.js";
import { exec } from "./lib/verify-package-core.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
assert(
process.platform !== "win32",
"Native Copilot CLI verifier requires POSIX; Windows is unverified",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const command = process.env.REA_VERIFY_COPILOT_COMMAND ?? "copilot";
const mode = process.argv[2] ?? "call";
const model = process.env.REA_VERIFY_COPILOT_MODEL ?? "gpt-5.4";
assert(
["chat", "call"].includes(mode),
"Usage: verify-copilot-client.mjs [chat|call]",
);
const verifier = createVerifierRun();
const lab = await mkdtemp(join(tmpdir(), "rea-copilot-client-"));
const account = join(lab, "account");
const profile = join(lab, "selected profile");
const workspace = join(lab, "workspace");
const configPath = join(profile, "mcp-config.json");
const skillDirectory = join(
account,
".agents",
"skills",
"reverse-engineer-anything",
);
await Promise.all([mkdir(account), mkdir(profile), mkdir(workspace)]);
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
// Do not inherit the caller's home or credentials into the isolated native profile.
const environment = {
PATH: process.env.PATH,
NODE_OPTIONS: process.env.NODE_OPTIONS,
USERPROFILE: account,
COPILOT_HOME: profile,
COPILOT_OFFLINE: "true",
COPILOT_PROVIDER_TYPE: "openai",
COPILOT_PROVIDER_WIRE_API: "completions",
COPILOT_PROVIDER_API_KEY: "local-fixture-key",
COPILOT_PROVIDER_API_KEY_COMMAND: "",
COPILOT_PROVIDER_BEARER_TOKEN: "",
COPILOT_PROVIDER_HEADERS: "",
COPILOT_PROVIDER_MODEL_ID: model,
COPILOT_PROVIDER_WIRE_MODEL: model,
COPILOT_PROVIDER_MAX_PROMPT_TOKENS: "1000000",
COPILOT_PROVIDER_MAX_OUTPUT_TOKENS: "4096",
REA_PROCESS_RUN_ID: verifier.run_id,
};
let clientVersion;
try {
clientVersion = (
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
).stdout.trim();
} catch (cause) {
throw new Error(
`Copilot CLI preflight failed for ${command}; install Copilot CLI or set REA_VERIFY_COPILOT_COMMAND. ${String(cause)}`,
);
}
const target = join(lab, "fixture α测试");
await mkdir(target);
await writeFile(
join(target, "app.js"),
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
);
let skillRequested = false;
let skillLoaded = false;
let expectedSkillBody;
let evidenceSeen = false;
let finalSeen = false;
let catalogSize = 0;
let schemasChecked = 0;
let spillReadRequested = false;
let artifactDigest;
const schemaValidator = new Ajv2020({ strict: false, validateFormats: false });
const containsText = (value, text) => {
if (typeof value === "string") return value.includes(text);
if (value && typeof value === "object")
return Object.values(value).some((nested) => containsText(nested, text));
return false;
};
const extractEvidence = (value) => {
if (typeof value === "string") {
try {
return extractEvidence(JSON.parse(value));
} catch {
return undefined;
}
}
if (value && typeof value === "object") {
if (
typeof value.evidence_id === "string" &&
value.normalized_result?.graph?.nodes?.length > 0
)
return value;
for (const nested of Object.values(value)) {
const evidence = extractEvidence(nested);
if (evidence) return evidence;
}
}
return undefined;
};
const fixture = await createOpenAiModelFixture({
directory: lab,
prefix: "copilot",
onRequest: async ({ body, tools, requestIndex }) => {
// Auxiliary requests without tools cannot execute the analyst's workflow.
if (tools.length === 0)
return { role: "assistant", content: "REA fixture title" };
const analysis = tools.find((t) =>
t.name.endsWith("analyze_javascript_application"),
);
assert(analysis, "Copilot CLI must expose the REA analysis tool");
const prefix = analysis.name.slice(
0,
-"analyze_javascript_application".length,
);
const expected = TOOL_CONTRACTS.map((t) => prefix + t.name).sort();
const reaTools = tools.filter((t) => expected.includes(t.name));
assert.deepEqual(
reaTools.map((t) => t.name).sort(),
expected,
"Copilot CLI must forward the complete REA catalog",
);
catalogSize = expected.length;
if (schemasChecked === 0) {
for (const tool of reaTools) {
assert(
tool.parameters,
`${tool.name}: missing advertised input schema`,
);
assert(
schemaValidator.validateSchema(tool.parameters),
`${tool.name}: invalid input schema: ${JSON.stringify(schemaValidator.errors)}`,
);
schemasChecked++;
}
}
const replies = body.messages.filter((m) => m.role === "tool");
// Native skill activation injects its context as a user message after the acknowledgement.
skillLoaded ||=
skillRequested &&
expectedSkillBody !== undefined &&
containsText(body.messages, expectedSkillBody) &&
containsText(body.messages, skillDirectory);
const observed = extractEvidence(replies);
if (observed) {
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(observed);
assert.equal(evidence.subject.local_path, target);
const shape = evidence.normalized_result.graph.nodes
.flatMap((n) => n.observations)
.map((o) => o.properties)
.find(
(p) =>
p.semantic_role === "export-return-shapes" &&
p.exported_name === "result",
);
assert(shape?.return_shape_coverage?.projection_complete);
const fields = shape.static_return_shapes.flatMap((s) => s.fields);
assert(
fields.some(
(f) =>
f.path === "/clientCompatibility" &&
f.state === "literal" &&
f.value === "REA_CLIENT_FIXTURE",
),
);
assert(
fields.some(
(f) => f.path === "/count" && f.state === "literal" && f.value === 7,
),
);
const bytes = JSON.stringify(evidence);
artifactDigest = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(lab, "copilot-evidence.json"), bytes);
evidenceSeen = true;
}
if (mode === "chat" || evidenceSeen) {
finalSeen = true;
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
}
assert(requestIndex <= 8, "Local model workflow exceeded its turn budget");
const call = (name, args) => ({
role: "assistant",
content: null,
tool_calls: [
{
id: `call_rea_fixture_${requestIndex}`,
type: "function",
function: { name, arguments: JSON.stringify(args) },
},
],
});
if (!skillRequested) {
const skill = tools.find((t) => t.name === "skill");
assert(skill, "Copilot CLI must expose its native skill tool");
skillRequested = true;
return call(skill.name, { skill: "reverse-engineer-anything" });
}
assert(
skillLoaded,
"Copilot CLI must load the installed personal REA skill",
);
const spilled = replies.find(
(message) =>
typeof message.content === "string" &&
message.content.startsWith("Output too large to read at once "),
);
if (spilled) {
assert(
!spillReadRequested,
"Copilot did not return complete Evidence after its native spill read",
);
const path = /\. Saved to: ([^\n]+)\n/u.exec(spilled.content)?.[1];
assert(
path &&
isAbsolute(path) &&
/^\d+-copilot-tool-output-[a-f\d]+\.txt$/u.test(basename(path)),
"Unexpected native spill path",
);
const view = tools.find((tool) => tool.name === "view");
assert(view, "Copilot must expose its native file reader");
spillReadRequested = true;
return call(view.name, { path, forceReadLargeFiles: true });
}
return call(analysis.name, { input_path: target, format: "directory" });
},
});
const original = JSON.stringify(
{
mcpServers: {
other: {
type: "local",
command: "unrelated-server",
args: [],
enabled: false,
},
},
},
null,
2,
);
await writeFile(configPath, original);
let failure;
try {
for (const args of [
["setup", "--client", "copilot_cli", "--dry-run"],
["setup", "--client", "copilot_cli", "--yes"],
["doctor", "--client", "copilot_cli", "--skill"],
]) {
const result = await exec(
process.execPath,
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
{ env: environment, cwd: workspace, timeout: 60_000 },
);
await writeFile(
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
result.stdout,
);
const parsed = JSON.parse(result.stdout);
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
else {
assert.equal(
parsed.status,
args.includes("--dry-run") ? "planned" : "ready",
);
if (args.includes("--dry-run")) {
assert(parsed.plannedActions.some((a) => a.target === configPath));
assert(parsed.plannedActions.some((a) => a.target === skillDirectory));
assert(
parsed.plannedActions.every(
(a) =>
a.target === configPath ||
a.target === skillDirectory ||
a.target.startsWith(skillDirectory + "/"),
),
);
}
}
}
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const configured = await readFile(configPath, "utf8");
assert(configured.includes("unrelated-server"));
const repeated = await exec(
process.execPath,
[
join(runtimeRoot, "scripts/rea.mjs"),
"setup",
"--client",
"copilot_cli",
"--yes",
"--json",
],
{ env: environment, cwd: workspace, timeout: 60_000 },
);
assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []);
assert.equal(await readFile(configPath, "utf8"), configured);
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const installedSkill = await readFile(
join(skillDirectory, "SKILL.md"),
"utf8",
);
assert(installedSkill.includes("name: reverse-engineer-anything"));
const skillLines = installedSkill.split("\n");
const headerEnd = skillLines.indexOf("---", 1);
assert(
skillLines[0] === "---" && headerEnd > 0,
"Installed skill must have its authored frontmatter",
);
expectedSkillBody = skillLines
.slice(headerEnd + 1)
.join("\n")
.trim();
assert(expectedSkillBody.length > 0);
for (const [name, args] of [
["mcp-list", ["mcp", "list"]],
["skill-add", ["skill", "add", skillDirectory]],
["skill-list", ["skill", "list"]],
]) {
const result = await exec(command, args, {
env: environment,
cwd: workspace,
timeout: 30_000,
});
await writeFile(join(lab, `${name}.stdout`), result.stdout);
await writeFile(join(lab, `${name}.stderr`), result.stderr);
if (name === "mcp-list") assert(result.stdout.includes("rea"));
if (name === "skill-list")
assert(result.stdout.includes("reverse-engineer-anything"));
}
const execution = exec(
command,
[
"--disable-builtin-mcps",
"--disable-mcp-server",
"other",
"--no-ask-user",
"--output-format",
"json",
"--model",
model,
"--allow-tool=skill",
"--allow-tool=read",
"--allow-tool=rea(analyze_javascript_application)",
"--add-dir",
target,
"--prompt",
mode === "chat"
? "Say hello briefly."
: `Use the REA skill and tools to analyze the JavaScript directory ${target}. Report the result.`,
],
{
env: { ...environment, COPILOT_PROVIDER_BASE_URL: fixture.baseUrl },
cwd: workspace,
timeout: 120_000,
maxBuffer: 10 * 1024 * 1024,
},
);
execution.child.stdin.end();
const response = await execution;
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
assert.equal(fixture.failure, undefined);
const events = response.stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line));
assert(
!events.some((event) => event.type === "session.error"),
"Copilot emitted a session error",
);
assert(finalSeen);
assert(
events.some(
(event) =>
event.type === "assistant.message" &&
event.data.content === "REA_CLIENT_COMPATIBILITY_OK",
),
"Copilot must finish with the expected native assistant message",
);
if (mode === "call") assert(evidenceSeen && skillLoaded);
} catch (cause) {
failure = cause;
if (typeof cause?.stdout === "string")
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
if (typeof cause?.stderr === "string")
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
} finally {
await fixture.close();
const lineage = await completeVerifierRun(verifier);
const receipt = {
client: "Copilot CLI",
clientVersion,
mode,
model,
passed: failure === undefined,
artifacts: lab,
runtimeRoot,
host: {
platform: process.platform,
architecture: process.arch,
node: process.versions.node,
},
modelFixture:
"loopback OpenAI completions/SSE, synthetic usage, requested one-million-token BYOK prompt capacity; effective capacity unknown; no live provider claim",
skillDiscovery:
"native COPILOT_HOME MCP configuration and caller-added isolated skill directory; default OS-home discovery unverified",
capacityBoundary:
"gpt-4.1 model configuration blocks the full catalog before HTTP even with a ten-million-token requested prompt override; gpt-5.4 is the verified local fixture configuration",
catalogSize,
schemasChecked,
spillReadRequested,
evidenceSeen,
skillLoaded,
artifactDigest,
finalSeen,
requests: fixture.requests,
probes: fixture.probes,
fixtureFailure: fixture.failure,
verifier: lineage,
};
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
console.log(JSON.stringify(receipt));
}
if (failure !== undefined) throw failure;
@@ -2,6 +2,7 @@
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
@@ -358,6 +359,36 @@ try {
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 });
// Explicit caller-selected targets need not contain our fixture instructions.
const controlledTargets = [
"rea-ghidra-windows.exe",
"rea-ghidra-windows-x86.exe",
].map((name) => resolve("build", "fixtures", name));
if (controlledTargets.includes(target))
report.compactReferences = await verifyGhidraCompactReferences(
call,
target === controlledTargets[1],
async (command, address) => {
const result = await exec(
process.execPath,
[entry, command, target, address, "--provider", "ghidra", "--json"],
{
env: environment,
cwd: callerDirectory,
timeout: 360_000,
maxBuffer: 8 * 1024 * 1024,
},
);
const evidence = JSON.parse(result.stdout);
assert.ok(evidence.error === undefined, `CLI ${command} failed`);
const contract = TOOL_CONTRACTS.find(
({ name }) => name === command.replaceAll("-", "_"),
);
assert.ok(contract, `Missing CLI ${command} contract`);
contract.outputSchema.parse(evidence);
return evidence.normalized_result;
},
);
const expected = (
await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
+10
View File
@@ -24,6 +24,7 @@ import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
const verifierRun = createVerifierRun();
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
@@ -247,6 +248,14 @@ try {
`Windows Ghidra operation proof was incomplete: ${JSON.stringify([...observed])}`,
);
// The reference controls occupy the EXE fixture's additional function.
// DLL fixtures retain their independent exported-caller/return controls.
const compactReferences = dllFixture
? null
: await verifyGhidraCompactReferences(
functionOperation,
architecture === "x86",
);
report = {
ok: true,
provider: { id: "ghidra", version: SUPPORTED_GHIDRA_VERSION },
@@ -261,6 +270,7 @@ try {
transport: "authenticated-loopback-tcp",
operations: [...observed].sort((left, right) => left.localeCompare(right)),
cleanup: "complete",
compact_references: compactReferences,
native_controls: {
job_object: true,
protected_dacl: true,
+13 -1
View File
@@ -1,5 +1,5 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { expect, it } from "vitest";
@@ -46,6 +46,18 @@ it("threads the selected host and environment into system diagnostics", async ()
).toBe(true);
});
it("does not report a directory as an available executable", async () => {
const host = systemDoctorHost({
platform: "darwin",
architecture: "arm64",
environment: {},
execFileOutput: () => Promise.reject(new Error("unexpected command")),
});
await expect(host.executable(dirname(process.execPath))).resolves.toBe(false);
await expect(host.executable(process.execPath)).resolves.toBe(true);
});
it.each([
[{ default: { appdir: "/Applications" } }, "/Applications"],
[
+2 -1
View File
@@ -1,5 +1,5 @@
import { constants } from "node:fs";
import { access, readdir } from "node:fs/promises";
import { access, readdir, stat } from "node:fs/promises";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { join } from "node:path";
@@ -472,6 +472,7 @@ const executableAvailable = async (
): Promise<boolean> => {
try {
await access(path, constants.X_OK);
if (!(await stat(path)).isFile()) return false;
if (platform !== "linux") return true;
const linked = await run("ldd", [path], options);
return linuxSharedLibrariesAvailable(`${linked.stdout}\n${linked.stderr}`);
@@ -5,7 +5,7 @@ import { resolveJavaScriptSourceMapReference } from "../../domain/javascript/jav
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js";
import {
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptOpenReceivers,
classifyParsedJavaScriptOpenReceiversSteps,
} from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
@@ -188,8 +188,8 @@ function* analyzeArtifactFileSteps<
state.parseFailures += 1;
return;
}
const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(parsed);
yield;
const openReceiverFacts =
yield* classifyParsedJavaScriptOpenReceiversSteps(parsed);
const analysis = yield* analyzeParsedJavaScriptStaticSourceSteps(
file.text.value,
parsed,
@@ -387,7 +387,392 @@ describe("contextual JavaScript package path resolution", () => {
).toMatchObject({
resolution_status: "external",
resolved_path: null,
limitations: [expect.stringContaining("subpaths remain unresolved")],
limitations: [
expect.stringContaining("declares no exports target for subpath"),
],
});
});
});
describe("package exports subpath resolution", () => {
it("resolves exact subpath keys through import and require conditions", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
JSON.stringify({
exports: {
".": "./index.cjs",
"./parser": {
import: "./parser.mjs",
require: "./parser.cjs",
default: "./fallback.js",
},
},
}),
),
file("app/node_modules/fixture/index.cjs", "root"),
file("app/node_modules/fixture/parser.mjs", "root"),
file("app/node_modules/fixture/parser.cjs", "root"),
file("app/node_modules/fixture/fallback.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/node_modules/fixture/parser.mjs",
});
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/node_modules/fixture/parser.cjs",
});
});
it("resolves array subpath targets at their first string entry", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
JSON.stringify({
exports: {
"./parser": [{ import: "./parser.mjs" }, "./parser.cjs"],
},
}),
),
file("app/node_modules/fixture/parser.cjs", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/node_modules/fixture/parser.cjs",
});
});
it("resolves subpaths of scoped packages", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/@scope/fixture/package.json",
"root",
JSON.stringify({
exports: { "./parser": "./parser.cjs" },
}),
),
file("app/node_modules/@scope/fixture/parser.cjs", "root"),
]);
expect(
resolve({
declaredPath: "@scope/fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/node_modules/@scope/fixture/parser.cjs",
});
});
});
describe("package exports subpath encapsulation", () => {
it("keeps subpaths encapsulated when only a root target is exported", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
JSON.stringify({ exports: "./index.cjs" }),
),
file("app/node_modules/fixture/index.cjs", "root"),
file("app/node_modules/fixture/parser.cjs", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "external",
resolved_path: null,
limitations: [expect.stringContaining("a root target only")],
});
});
it("blocks subpaths with an explicit null target", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
JSON.stringify({
exports: { ".": "./index.cjs", "./parser": null },
}),
),
file("app/node_modules/fixture/index.cjs", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "external",
resolved_path: null,
limitations: [expect.stringContaining("explicit null exports target")],
});
});
it("reports a missing selected subpath target without index fallback", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
JSON.stringify({
exports: { "./parser": "./parser.cjs" },
}),
),
file("app/node_modules/fixture/parser/index.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "not-found",
resolved_path: null,
limitations: [expect.stringContaining("exports targets are exact files")],
});
});
});
describe("package subpath directory ownership", () => {
it.each(["root", "other"])(
"selects metadata-less package directories only in the %s container",
(container) => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file("app/src/node_modules/fixture/parser.js", container),
file("app/node_modules/fixture/package.json", "root", "{}"),
file("app/node_modules/fixture/parser.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
declared_path: "fixture/parser",
resolution_context: "module-specifier",
resolution_status: "resolved",
resolved_path:
container === "root"
? "app/src/node_modules/fixture/parser.js"
: "app/node_modules/fixture/parser.js",
});
},
);
it("does not mistake a package-name file or sibling prefix for a nearer directory", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file("app/src/node_modules/fixture.js", "root"),
file("app/src/node_modules/fixture-extra/parser.js", "root"),
file("app/node_modules/fixture/parser.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolved_path: "app/node_modules/fixture/parser.js",
resolution_status: "resolved",
});
});
it.each([
["malformed", "{", "not valid package JSON"],
["unavailable", null, "text is unavailable"],
["cyclic", '{"main":"."}', "cycle"],
])("preserves %s nested directory failures", (_name, text, reason) => {
const files = fileMap([
file("app/consumer.js", "root"),
file("app/node_modules/fixture/package.json", "root", "{}"),
file("app/node_modules/fixture/parser/package.json", "root", text),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
declared_path: "fixture/parser",
resolution_context: "module-specifier",
resolved_path: null,
resolution_status: "unavailable",
limitations: [expect.stringContaining(reason)],
});
});
});
describe("package subpath legacy lookup and metadata failures", () => {
it("resolves legacy subpath files and indexes without an exports field", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
'{"main":"./index.cjs"}',
),
file("app/node_modules/fixture/direct.cjs", "root"),
file("app/node_modules/fixture/folder/index.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture/direct",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/node_modules/fixture/direct.cjs",
});
expect(
resolve({
declaredPath: "fixture/folder",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/node_modules/fixture/folder/index.js",
});
});
it("rejects subpath metadata that mixes subpath and condition keys", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file(
"app/node_modules/fixture/package.json",
"root",
JSON.stringify({
exports: { "./parser": "./parser.cjs", node: "./index.cjs" },
}),
),
file("app/node_modules/fixture/parser.cjs", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "rejected",
resolved_path: null,
limitations: [expect.stringContaining("mixes subpath keys")],
});
});
it("reports malformed subpath package metadata as unavailable", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file("app/node_modules/fixture/package.json", "root", "{"),
file("app/node_modules/fixture/parser.cjs", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "unavailable",
resolved_path: null,
limitations: [expect.stringContaining("not valid package JSON")],
});
});
it("reports unavailable metadata text for subpath resolution", () => {
const files = fileMap([
file("app/src/consumer.js", "root"),
file("app/node_modules/fixture/package.json", "root", null),
file("app/node_modules/fixture/parser.cjs", "root"),
]);
expect(
resolve({
declaredPath: "fixture/parser",
sourcePath: "app/src/consumer.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "unavailable",
resolved_path: null,
limitations: [expect.stringContaining("text is unavailable")],
});
});
});
@@ -80,61 +80,54 @@ type CandidateResolution =
readonly limitations: readonly string[];
};
/** Resolve one declaration under its exact syntax/metadata context. */
/** Resolve one declaration while preserving its original input identity. */
export const resolveArtifactPathByContext = (
input: ResolveArtifactPathInput,
): ArtifactPathResolution => resolvePackagePath(input, new Set());
const resolvePackagePath = (
input: ResolveArtifactPathInput,
packageChain: ReadonlySet<string>,
mode: "full" | "files-and-index" = "full",
exportsTargetUrl = false,
): ArtifactPathResolution => {
const rejected = rejectDeclaration(input);
if (rejected !== null) return rejected;
const candidate = contextualCandidate(input, exportsTargetUrl);
if (typeof candidate !== "string") return candidate;
const confined = confineCandidate(input, candidate);
if (typeof confined !== "string") return confined;
if (input.context === "html-reference")
return outcome(
input,
resolveFileCandidates(input, [confined]) ?? {
resolvedPath: null,
status: "not-found",
limitations: [
`The exact HTML resource ${confined} was not found among the selected application files.`,
],
},
);
// An exports target names one exact file; Node tries no extension or index.
const resolved = exportsTargetUrl
? (resolveFileCandidates(input, [confined]) ??
exactExportsTargetNotFound(confined))
: mode === "files-and-index"
? (resolveFileCandidates(input, [
...fileCandidates(confined),
...indexCandidates(confined),
]) ?? notFoundCandidate())
: resolveCandidate(input, confined, packageChain);
return outcome(input, resolved);
const resolution = resolvePath(input);
return resolution.status === "resolved"
? {
declared_path: input.declaredPath,
resolution_context: input.context,
resolved_path: resolution.resolvedPath,
resolution_status: resolution.status,
limitations: resolution.limitations,
}
: {
declared_path: input.declaredPath,
resolution_context: input.context,
resolved_path: null,
resolution_status: resolution.status,
limitations: resolution.limitations,
};
};
const rejectDeclaration = (
input: ResolveArtifactPathInput,
): ArtifactPathResolution | null => {
const declared = input.declaredPath;
const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
const rejected = rejectDeclaration(input.declaredPath);
if (rejected !== null) return rejected;
const candidate = contextualCandidate(input);
if (typeof candidate !== "string") return candidate;
const confined = confineCandidate(candidate);
if (typeof confined !== "string") return confined;
if (input.context === "html-reference")
return (
resolveFileCandidates(input, [confined]) ??
unresolvedCandidate("not-found", [
`The exact HTML resource ${confined} was not found among the selected application files.`,
])
);
return resolveCandidate(input, confined, new Set());
};
const rejectDeclaration = (declared: string): CandidateResolution | null => {
if (declared.length === 0)
return unresolvedOutcome(input, "rejected", [
"The declared path is empty.",
]);
return unresolvedCandidate("rejected", ["The declared path is empty."]);
if (declared.includes("\0") || declared.includes("\\"))
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"NUL and backslash path syntax are not admitted for canonical artifact paths.",
]);
if (!admitsCanonicalPathSyntax(declared))
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"Encoded dot or separator bytes are rejected before artifact path resolution.",
]);
return null;
@@ -142,13 +135,11 @@ const rejectDeclaration = (
const contextualCandidate = (
input: ResolveArtifactPathInput,
exportsTargetUrl: boolean,
): string | ArtifactPathResolution => {
): string | CandidateResolution => {
const { context } = input;
let declared =
context === "html-reference" ||
context === "url-reference" ||
exportsTargetUrl ||
(context === "module-specifier" && input.moduleKind !== "require")
? stripQueryAndFragment(input.declaredPath)
: input.declaredPath;
@@ -157,66 +148,132 @@ const contextualCandidate = (
const fileUrl = fileUrlPath(declared);
if (fileUrl !== undefined) return fileUrl;
if (hasScheme(declared))
return unresolvedOutcome(input, "external", [
return unresolvedCandidate("external", [
"URL and Node builtin schemes are outside static artifact module resolution.",
]);
if (!declared.startsWith(".") && !declared.startsWith("/"))
return bareModuleCandidate(input, declared);
if (input.moduleKind !== "require") {
const decoded = decodeModuleUrlPath(declared);
if (typeof decoded !== "string") return decoded;
declared = decoded;
}
} else if (looksExternal(declared))
return unresolvedOutcome(input, "external", [
return unresolvedCandidate("external", [
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
]);
// Exports targets use URL paths for both loaders; legacy main fields remain literal.
if (
exportsTargetUrl ||
(context === "module-specifier" && input.moduleKind !== "require")
) {
try {
declared = decodeURIComponent(declared);
} catch (cause: unknown) {
void cause;
return unresolvedOutcome(input, "rejected", [
"The module URL path contains malformed percent encoding.",
]);
}
if (declared.includes("\0"))
return unresolvedOutcome(input, "rejected", [
"The decoded module URL path contains NUL.",
]);
}
const relative = declared.startsWith("/") ? declared.slice(1) : declared;
return declared.startsWith("/")
? relative
: posix.join(posix.dirname(input.sourcePath), relative);
return relativeCandidate(input.sourcePath, declared);
};
const decodeModuleUrlPath = (
declared: string,
): string | CandidateResolution => {
let decoded: string;
try {
decoded = decodeURIComponent(declared);
} catch (cause: unknown) {
void cause;
return unresolvedCandidate("rejected", [
"The module URL path contains malformed percent encoding.",
]);
}
return decoded.includes("\0")
? unresolvedCandidate("rejected", [
"The decoded module URL path contains NUL.",
])
: decoded;
};
const relativeCandidate = (sourcePath: string, declared: string): string =>
declared.startsWith("/")
? declared.slice(1)
: posix.join(posix.dirname(sourcePath), declared);
const bareModuleCandidate = (
input: ResolveArtifactPathInput,
declared: string,
): string | ArtifactPathResolution => {
): CandidateResolution => {
const packageName = barePackageName(declared);
if (packageName === null || isBuiltin(declared) || declared.startsWith("#"))
return unresolvedOutcome(input, "external", [
return unresolvedCandidate("external", [
"The bare specifier is a Node builtin, package import map, or invalid package name.",
]);
if (declared !== packageName)
return unresolvedOutcome(input, "external", [
"Bare package subpaths remain unresolved unless an exact package-exports subpath model is available.",
]);
const subpath =
declared === packageName ? "." : `.${declared.slice(packageName.length)}`;
const source = input.files.get(input.sourcePath);
let directory = posix.dirname(input.sourcePath);
while (true) {
const candidate = posix.join(directory, "node_modules", declared);
if (hasContainerCandidate(input.files, candidate, source?.container_sha256))
return candidate;
const candidate = posix.join(directory, "node_modules", packageName);
if (subpath === ".") {
const direct = resolveFileCandidates(input, fileCandidates(candidate));
if (direct !== null) return direct;
}
if (hasContainerDirectory(input.files, candidate, source?.container_sha256))
return resolvePackageSpecifier(input, candidate, subpath);
if (directory === "." || directory === "") break;
directory = posix.dirname(directory);
}
return unresolvedOutcome(input, "external", [
return unresolvedCandidate("external", [
"No matching bare package was inventoried in an enclosing node_modules directory.",
]);
};
/** Package roots and exact subpaths share metadata and exports selection. */
const resolvePackageSpecifier = (
input: ResolveArtifactPathInput,
directory: string,
subpath: string,
): CandidateResolution => {
const metadata = readPackageMetadata(input, directory);
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
const rawExports =
metadata.kind === "available"
? Reflect.get(metadata.value, "exports")
: undefined;
if (rawExports === undefined || rawExports === null) {
if (subpath === ".")
return resolveDirectory(input, directory, metadata, new Set());
const candidate = confineCandidate(posix.join(directory, subpath.slice(2)));
return typeof candidate === "string"
? resolveCandidate(input, candidate, new Set())
: candidate;
}
const exported = selectPackageExport(rawExports, subpath, input.moduleKind);
const packagePath = posix.join(directory, "package.json");
if (exported.status === "rejected")
return unresolvedCandidate("rejected", [
`Package metadata ${packagePath}: ${exported.limitation}`,
]);
if (exported.status === "unmatched")
return unresolvedCandidate("external", [
`Directory package metadata ${packagePath} declares no exports target ${subpath === "." ? "for the active conditions" : `for subpath ${subpath}`}; exports encapsulation permits no file or index lookup. It declares ${exported.declared.join(", ") || "a root target only"}.`,
]);
if (exported.status === "blocked")
return unresolvedCandidate("external", [
`Directory package metadata ${packagePath} blocks subpath ${subpath} with an explicit null exports target.`,
]);
return resolveExportTarget(input, packagePath, exported.value);
};
/** An exports target is a URL naming one exact file for both Node loaders. */
const resolveExportTarget = (
input: ResolveArtifactPathInput,
packagePath: string,
target: string,
): CandidateResolution => {
const rejected = rejectDeclaration(target);
if (rejected !== null) return rejected;
const decoded = decodeModuleUrlPath(stripQueryAndFragment(target));
if (typeof decoded !== "string") return decoded;
const candidate = confineCandidate(relativeCandidate(packagePath, decoded));
if (typeof candidate !== "string") return candidate;
return (
resolveFileCandidates(input, [candidate]) ??
exactExportsTargetNotFound(candidate)
);
};
const barePackageName = (specifier: string): string | null => {
const segments = specifier.split("/");
if (specifier.startsWith("@"))
@@ -226,30 +283,43 @@ const barePackageName = (specifier: string): string | null => {
return segments[0] === "" ? null : (segments[0] ?? null);
};
const hasContainerCandidate = (
const hasContainerDirectory = (
files: ReadonlyMap<string, JavaScriptArtifactFile>,
candidate: string,
containerSha256: string | undefined,
): boolean =>
[
...fileCandidates(candidate),
...indexCandidates(candidate),
posix.join(candidate, "package.json"),
].some((path) => {
const file = files.get(path);
return (
file !== undefined &&
): boolean => {
if (
[...indexCandidates(candidate), posix.join(candidate, "package.json")].some(
(path) => {
const file = files.get(path);
return (
file !== undefined &&
(containerSha256 === undefined ||
file.container_sha256 === containerSha256)
);
},
)
)
return true;
// Inventory has files rather than directory entries. Any same-container
// descendant establishes a package directory even without package.json.
const prefix = `${candidate}/`;
for (const [path, file] of files)
if (
path.startsWith(prefix) &&
(containerSha256 === undefined ||
file.container_sha256 === containerSha256)
);
});
)
return true;
return false;
};
const htmlCandidate = (
input: ResolveArtifactPathInput,
): string | ArtifactPathResolution => {
): string | CandidateResolution => {
const declared = stripQueryAndFragment(htmlUrlText(input.declaredPath));
if (looksExternal(declared))
return unresolvedOutcome(input, "external", [
return unresolvedCandidate("external", [
"External HTML references are not mapped to local artifact assets.",
]);
const rawBase = input.htmlBaseHref;
@@ -258,18 +328,18 @@ const htmlCandidate = (
? rawBase
: stripQueryAndFragment(htmlUrlText(rawBase));
if (base !== undefined && base !== null && looksExternal(base))
return unresolvedOutcome(input, "external", [
return unresolvedCandidate("external", [
"The document base href is external, so its script reference is not a local artifact path.",
]);
// Removing URL tabs and newlines can join an encoded dot or separator that
// the raw declaration split, so admit the parsed URL text again.
if (!admitsCanonicalPathSyntax(declared))
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"Encoded dot or separator bytes are rejected before artifact path resolution.",
]);
const declaredPath = percentDecodeUrlPath(declared);
if (declaredPath === null)
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"The HTML reference path percent-decodes to NUL or to bytes that are not UTF-8.",
]);
if (declaredPath.startsWith("/")) return declaredPath.slice(1);
@@ -281,31 +351,29 @@ const htmlCandidate = (
// admission rules a declared path gets so it cannot smuggle traversal or
// separator syntax past canonicalization.
if (!admitsCanonicalPathSyntax(base))
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"The document base href uses NUL, backslash, or encoded dot and separator bytes that are not admitted for canonical artifact paths.",
]);
const decodedBase = percentDecodeUrlPath(base);
if (decodedBase === null)
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"The document base href path percent-decodes to NUL or to bytes that are not UTF-8.",
]);
const basePath = decodedBase.startsWith("/")
? decodedBase.slice(1)
: posix.join(posix.dirname(input.sourcePath), decodedBase);
if (declaredPath === "")
return emptyHtmlReference(input, decodedBase, basePath);
if (declaredPath === "") return emptyHtmlReference(decodedBase, basePath);
return posix.join(htmlBaseDirectory(decodedBase, basePath), declaredPath);
};
const emptyHtmlReference = (
input: ResolveArtifactPathInput,
base: string,
basePath: string,
): string | ArtifactPathResolution => {
): string | CandidateResolution => {
if (!htmlBaseIsDirectory(base)) return basePath;
const confined = confineCandidate(input, basePath);
const confined = confineCandidate(basePath);
if (typeof confined !== "string") return confined;
return unresolvedOutcome(input, "not-found", [
return unresolvedCandidate("not-found", [
`The HTML reference resolves to directory ${confined || "."}, not an exact selected application file.`,
]);
};
@@ -326,123 +394,132 @@ const htmlBaseIsDirectory = (base: string): boolean =>
const htmlBaseDirectory = (base: string, basePath: string): string =>
htmlBaseIsDirectory(base) ? basePath : posix.dirname(basePath);
const confineCandidate = (
input: ResolveArtifactPathInput,
candidate: string,
): string | ArtifactPathResolution => {
const confineCandidate = (candidate: string): string | CandidateResolution => {
const normalized = normalizeJoinedLogicalPath(candidate);
if (logicalPathEscapesRoot(normalized))
return unresolvedOutcome(input, "rejected", [
return unresolvedCandidate("rejected", [
"The resolved candidate escapes the canonical artifact root.",
]);
return normalized === "." ? "" : normalized;
};
type PackageMetadata =
| {
readonly kind: "available";
readonly path: string;
readonly value: object;
}
| { readonly kind: "absent" }
| { readonly kind: "unavailable"; readonly limitation: string };
const readPackageMetadata = (
input: ResolveArtifactPathInput,
directory: string,
): PackageMetadata => {
const path = posix.join(directory, "package.json");
const file = input.files.get(path);
const source = input.files.get(input.sourcePath);
if (
file === undefined ||
(source !== undefined && file.container_sha256 !== source.container_sha256)
)
return { kind: "absent" };
if (!file.text.included)
return {
kind: "unavailable",
limitation: `Directory package metadata ${path} was inventoried but its text is unavailable: ${file.text.reason}.`,
};
let value: unknown;
try {
value = JSON.parse(file.text.value);
} catch (cause: unknown) {
void cause;
return {
kind: "unavailable",
limitation: `Directory package metadata ${path} is not valid package JSON.`,
};
}
return typeof value === "object" && value !== null
? { kind: "available", path, value }
: {
kind: "unavailable",
limitation: `Directory package metadata ${path} is not valid package JSON.`,
};
};
/** File and directory lookup never interprets a nested directory's exports. */
const resolveCandidate = (
input: ResolveArtifactPathInput,
candidate: string,
packageChain: ReadonlySet<string>,
): CandidateResolution => {
const direct = resolveFileCandidates(input, fileCandidates(candidate));
if (direct !== null) return direct;
const source = input.files.get(input.sourcePath);
const packagePath = posix.join(candidate, "package.json");
const packageFile = input.files.get(packagePath);
if (
packageFile === undefined ||
(source !== undefined &&
packageFile.container_sha256 !== source.container_sha256)
)
return (
resolveFileCandidates(input, indexCandidates(candidate)) ??
notFoundCandidate()
);
if (!packageFile.text.included)
return {
resolvedPath: null,
status: "unavailable",
limitations: [
`Directory package metadata ${packagePath} was inventoried but its text is unavailable: ${packageFile.text.reason}.`,
],
};
const main = packageEntry(
packageFile.text.value,
input.moduleKind,
input.context === "module-specifier" &&
!input.declaredPath.startsWith(".") &&
!input.declaredPath.startsWith("/") &&
!hasScheme(input.declaredPath),
): CandidateResolution =>
resolveFileCandidates(input, fileCandidates(candidate)) ??
resolveDirectory(
input,
candidate,
readPackageMetadata(input, candidate),
packageChain,
);
if (main.status === "invalid")
return {
resolvedPath: null,
status: "unavailable",
limitations: [
`Directory package metadata ${packagePath} is not valid package JSON.`,
],
};
if (main.status === "rejected")
return {
resolvedPath: null,
status: "rejected",
limitations: [`Package metadata ${packagePath}: ${main.limitation}`],
};
if (main.status === "unmatched")
return {
resolvedPath: null,
status: "external",
limitations: [
`Directory package metadata ${packagePath} declares no exports target for the active conditions; it declares ${main.declared.join(", ")}.`,
],
};
if (packageChain.has(packagePath)) {
return {
resolvedPath: null,
status: "unavailable",
limitations: [
`Directory package entrypoint cycle includes ${packagePath}.`,
],
};
}
if (main.status === "missing")
const resolveDirectory = (
input: ResolveArtifactPathInput,
directory: string,
metadata: PackageMetadata,
packageChain: ReadonlySet<string>,
): CandidateResolution => {
if (metadata.kind === "absent")
return (
resolveFileCandidates(input, indexCandidates(candidate)) ??
resolveFileCandidates(input, indexCandidates(directory)) ??
notFoundCandidate()
);
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
if (packageChain.has(metadata.path))
return unresolvedCandidate("unavailable", [
`Directory package entrypoint cycle includes ${metadata.path}.`,
]);
const preferred = [
Reflect.get(metadata.value, "main"),
...(input.moduleKind === undefined
? [Reflect.get(metadata.value, "module")]
: []),
];
const entry = preferred.find((value) => value !== undefined);
if (entry === undefined)
return (
resolveFileCandidates(input, indexCandidates(directory)) ??
notFoundCandidate()
);
if (typeof entry !== "string" || entry.length === 0)
return unresolvedCandidate("unavailable", [
`Directory package metadata ${metadata.path} is not valid package JSON.`,
]);
const entryInput: ResolveArtifactPathInput = {
declaredPath: main.value,
sourcePath: packagePath,
declaredPath: entry,
sourcePath: metadata.path,
context: "package-entrypoint",
files: input.files,
...(input.moduleKind === undefined ? {} : { moduleKind: input.moduleKind }),
};
const chain = new Set([...packageChain, packagePath]);
if (main.source === "legacy") {
const entry = resolvePackagePath(entryInput, chain, "files-and-index");
if (entry.resolution_status !== "not-found") return candidateOutcome(entry);
const index = resolveFileCandidates(input, indexCandidates(candidate));
if (index !== null) return index;
}
// Preserve recursive artifact package lookup only after legacy file/index fallbacks.
return candidateOutcome(
resolvePackagePath(entryInput, chain, "full", main.source === "exports"),
const rejected = rejectDeclaration(entryInput.declaredPath);
if (rejected !== null) return rejected;
const candidate = contextualCandidate(entryInput);
if (typeof candidate !== "string") return candidate;
const confined = confineCandidate(candidate);
if (typeof confined !== "string") return confined;
const selected =
resolveFileCandidates(entryInput, [
...fileCandidates(confined),
...indexCandidates(confined),
]) ?? resolveFileCandidates(input, indexCandidates(directory));
if (selected !== null) return selected;
return resolveCandidate(
entryInput,
confined,
new Set([...packageChain, metadata.path]),
);
};
const candidateOutcome = (
nested: ArtifactPathResolution,
): CandidateResolution =>
nested.resolution_status === "resolved"
? {
resolvedPath: nested.resolved_path,
status: nested.resolution_status,
limitations: nested.limitations,
}
: {
resolvedPath: null,
status: nested.resolution_status,
limitations: nested.limitations,
};
const resolveFileCandidates = (
input: ResolveArtifactPathInput,
candidates: readonly string[],
@@ -468,84 +545,62 @@ const fileCandidates = (candidate: string): readonly string[] => [
const indexCandidates = (candidate: string): readonly string[] =>
EXTENSIONS.map((extension) => posix.join(candidate, `index${extension}`));
const packageEntry = (
text: string,
moduleKind: ResolveArtifactPathInput["moduleKind"],
useExports: boolean,
):
| {
readonly status: "value";
readonly value: string;
readonly source: "legacy" | "exports";
}
| { readonly status: "missing" }
| { readonly status: "invalid" }
type PackageExportOutcome =
| { readonly status: "value"; readonly value: string }
| { readonly status: "rejected"; readonly limitation: string }
| { readonly status: "unmatched"; readonly declared: readonly string[] } => {
try {
const value: unknown = JSON.parse(text);
if (typeof value !== "object" || value === null)
return { status: "invalid" };
const rawExports = Reflect.get(value, "exports");
if (useExports && rawExports !== undefined && rawExports !== null) {
const exported = packageExport(rawExports, moduleKind);
return exported.status === "value"
? { ...exported, source: "exports" }
: exported;
}
// Node imports and requires ignore the bundler-only module field. Keep
// that fallback for callers without an explicit module-loading kind.
const preferred = [
Reflect.get(value, "main"),
...(moduleKind === undefined ? [Reflect.get(value, "module")] : []),
];
const entry = preferred.find((candidate) => candidate !== undefined);
if (entry === undefined) return { status: "missing" };
const legacy = packagePathValue(entry);
return legacy.status === "value" ? { ...legacy, source: "legacy" } : legacy;
} catch (cause: unknown) {
// Reflect-based manifest reads fail closed as invalid.
void cause;
return { status: "invalid" };
}
};
| { readonly status: "blocked" }
| { readonly status: "unmatched"; readonly declared: readonly string[] };
const packageExport = (
/** Only exact non-directory keys participate; wildcard expansion is unsupported. */
const selectPackageExport = (
value: unknown,
subpath: string,
moduleKind: ResolveArtifactPathInput["moduleKind"],
): PackageExportOutcome => {
let target = value;
let declared: string[] = [];
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
const keys = Object.keys(value);
if (
keys.some((key) => key.startsWith(".")) &&
keys.some((key) => !key.startsWith("."))
)
const hasSubpaths = keys.some((key) => key.startsWith("."));
if (hasSubpaths && keys.some((key) => !key.startsWith(".")))
return {
status: "rejected",
limitation:
"The package exports configuration mixes subpath keys and condition keys.",
};
}
const root =
typeof value === "object" && value !== null && Object.hasOwn(value, ".")
? Reflect.get(value, ".")
: value;
const flattened = exportTargets(root, packageExportConditions(moduleKind));
if (flattened.kind === "invalid" || flattened.kind === "invalid-config")
return { status: "rejected", limitation: flattened.limitation };
const first =
flattened.kind === "unmatched" ? undefined : flattened.values[0];
return first === undefined
? {
status: "unmatched",
declared:
typeof root === "object" && root !== null ? Object.keys(root) : [],
}
: { status: "value", value: first };
declared = keys;
if (hasSubpaths) {
if (
subpath.endsWith("/") ||
subpath.includes("*") ||
!Object.hasOwn(value, subpath)
)
return { status: "unmatched", declared };
target = Reflect.get(value, subpath);
} else if (subpath !== ".") return { status: "unmatched", declared };
} else if (subpath !== ".") return { status: "unmatched", declared };
const selected = selectExportTarget(
target,
packageExportConditions(moduleKind),
);
if (selected.kind === "invalid" || selected.kind === "invalid-config")
return { status: "rejected", limitation: selected.limitation };
if (selected.kind === "unmatched")
return {
status: "unmatched",
declared:
typeof target === "object" && target !== null
? Object.keys(target)
: declared,
};
return selected.kind === "blocked"
? { status: "blocked" }
: { status: "value", value: selected.value };
};
type ExportTargets =
| { readonly kind: "targets"; readonly values: readonly string[] }
type ExportTargetOutcome =
| { readonly kind: "value"; readonly value: string }
| { readonly kind: "blocked" }
| { readonly kind: "unmatched" }
| { readonly kind: "invalid"; readonly limitation: string }
| { readonly kind: "invalid-config"; readonly limitation: string };
@@ -553,7 +608,7 @@ type ExportTargets =
const invalidExportTarget = (
value: unknown,
reason: string,
): ExportTargets => ({
): ExportTargetOutcome => ({
kind: "invalid",
limitation: `The package exports target ${JSON.stringify(value) ?? String(value)} ${reason}.`,
});
@@ -581,26 +636,26 @@ const forbiddenExportSegment = (value: string): string | undefined =>
* An array skips unmatched and invalid entries, stops at its first string, and
* preserves the final invalid-versus-null refusal if no string is selected.
*/
const exportTargets = (
const selectExportTarget = (
value: unknown,
conditions: ReadonlySet<string>,
): ExportTargets => {
): ExportTargetOutcome => {
if (typeof value === "string") {
if (!value.startsWith("./"))
return invalidExportTarget(value, 'must start with "./"');
const forbidden = forbiddenExportSegment(value);
return forbidden === undefined
? { kind: "targets", values: [value] }
? { kind: "value", value }
: invalidExportTarget(
value,
`contains forbidden path segment ${JSON.stringify(forbidden)} before URL normalization`,
);
}
if (value === null) return { kind: "targets", values: [] };
if (value === null) return { kind: "blocked" };
if (Array.isArray(value)) {
let invalid: string | null = null;
for (const entry of value) {
const nested = exportTargets(entry, conditions);
const nested = selectExportTarget(entry, conditions);
// Node's array fallback catches invalid targets, not invalid configuration.
if (nested.kind === "invalid-config") return nested;
if (nested.kind === "invalid") {
@@ -609,10 +664,10 @@ const exportTargets = (
}
if (nested.kind === "unmatched") continue;
invalid = null;
if (nested.values.length > 0) return nested;
if (nested.kind === "value") return nested;
}
return invalid === null
? { kind: "targets", values: [] }
? { kind: "blocked" }
: { kind: "invalid", limitation: invalid };
}
if (typeof value !== "object")
@@ -630,7 +685,7 @@ const exportTargets = (
}
for (const [condition, target] of Object.entries(value)) {
if (!conditions.has(condition)) continue;
const nested = exportTargets(target, conditions);
const nested = selectExportTarget(target, conditions);
if (nested.kind !== "unmatched") return nested;
}
return { kind: "unmatched" };
@@ -654,20 +709,6 @@ const packageExportConditions = (
"default",
]);
type PackageExportOutcome =
| { readonly status: "value"; readonly value: string }
| { readonly status: "rejected"; readonly limitation: string }
| { readonly status: "unmatched"; readonly declared: readonly string[] };
const packagePathValue = (
value: unknown,
):
| { readonly status: "value"; readonly value: string }
| { readonly status: "invalid" } =>
typeof value === "string" && value.length > 0
? { status: "value", value }
: { status: "invalid" };
const notFoundCandidate = (): CandidateResolution => ({
resolvedPath: null,
status: "not-found",
@@ -697,29 +738,7 @@ const fileUrlPath = (value: string): string | undefined => {
}
};
const outcome = (
input: ResolveArtifactPathInput,
resolution: CandidateResolution,
): ArtifactPathResolution =>
resolution.status === "resolved"
? {
declared_path: input.declaredPath,
resolution_context: input.context,
resolved_path: resolution.resolvedPath,
resolution_status: resolution.status,
limitations: resolution.limitations,
}
: {
declared_path: input.declaredPath,
resolution_context: input.context,
resolved_path: null,
resolution_status: resolution.status,
limitations: resolution.limitations,
};
const unresolvedOutcome = (
input: ResolveArtifactPathInput,
const unresolvedCandidate = (
status: UnresolvedArtifactPathStatus,
limitations: readonly string[],
): ArtifactPathResolution =>
outcome(input, { resolvedPath: null, status, limitations });
): CandidateResolution => ({ resolvedPath: null, status, limitations });
+7 -2
View File
@@ -22,7 +22,11 @@ export class DirectoryArtifactReader implements ArtifactReader {
this.#rootPromise = realpath(root);
}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
/** Enumerate entries, optionally restricting descent while retaining directories. */
async *entries(
signal?: AbortSignal,
descendInto?: (path: string) => boolean,
): AsyncIterable<ArtifactEntry> {
const root = await this.#rootPromise.catch((cause: unknown) => {
throw directoryIoFailure("resolve artifact root", this.#rootPath, cause);
});
@@ -66,7 +70,8 @@ export class DirectoryArtifactReader implements ArtifactReader {
? "file"
: undefined;
if (kind === undefined) continue;
if (kind === "directory") directories.push(absolute);
if (kind === "directory" && (descendInto?.(path) ?? true))
directories.push(absolute);
yield {
path,
kind,
+13 -8
View File
@@ -148,8 +148,20 @@ export const inspectBundleKeyedArchive = async (input: {
// Match inventory spelling while retaining the raw path for I/O and evidence.
const wanted = selected.path.normalize("NFC");
const matches: ArtifactEntry[] = [];
for await (const entry of reader.entries(input.signal)) {
for await (const entry of reader.entries(input.signal, (directory) =>
wanted.startsWith(`${directory.normalize("NFC")}/`),
)) {
if (entry.path.normalize("NFC") !== wanted) continue;
if (entry.kind !== "file")
throw archivePathError(
"invalid_value",
`Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`,
);
if ((entry.declaredSize ?? 0) > MAX_BYTES)
throw new ArtifactReaderFailure(
"limit",
"Keyed archive exceeds 64 MiB",
);
matches.push(entry);
}
if (matches.length > 1)
@@ -163,13 +175,6 @@ export const inspectBundleKeyedArchive = async (input: {
"invalid_value",
`No regular file exists at ${selected.path} in the active app bundle.`,
);
if (entry.kind !== "file")
throw archivePathError(
"invalid_value",
`Archive path selects a ${entry.kind}, not a regular file: ${selected.path}`,
);
if ((entry.declaredSize ?? 0) > MAX_BYTES)
throw new ArtifactReaderFailure("limit", "Keyed archive exceeds 64 MiB");
const stream = await reader.open(entry, input.signal);
const chunks: Buffer[] = [];
let size = 0;
@@ -15,6 +15,7 @@ import type {
import {
hasZipSignature,
zipPackageFormatForPath,
type ZipPackageFormat,
} from "../../domain/zipPackageFormat.js";
import { mzWindowsHeaderOffset, parseDosMzHeader } from "../../domain/dosMz.js";
@@ -47,37 +48,66 @@ export const indexOccurrencesByPath = (
return byPath;
};
/**
* Create an occurrence before its bytes are read. A container suffix is not a
* container family until the bytes are classified, so a member named like a
* container starts with an unknown format.
*/
export const createOccurrence = (
entry: ArtifactEntry,
path: string,
parent: string | null,
): MutableOccurrence => ({
occurrence_id: `occ_${digestCanonicalValue({ path, kind: entry.kind }, "Artifact")}`,
artifact_id: null,
parent_occurrence_id: parent,
logical_path: path,
entry_kind: entry.kind,
artifact_kind:
): MutableOccurrence => {
const role =
entry.kind === "directory"
? path.toLowerCase().endsWith(".framework")
? "framework"
: "container"
: classifyArtifactPath(path).kind,
artifact_format:
entry.kind === "directory"
? "directory"
: classifyArtifactPath(path).format,
declared_size: entry.declaredSize,
compressed_size: entry.compressedSize,
executable: entry.executable,
encrypted: entry.encrypted,
hash_status: entry.encrypted ? "unavailable" : "not-hashed",
source_location:
entry.byteOffset === null || entry.declaredSize === null
? null
: { offset: entry.byteOffset, length: entry.declaredSize },
limitations: [...entry.limitations],
});
? {
kind: path.toLowerCase().endsWith(".framework")
? ("framework" as const)
: ("container" as const),
format: "directory" as const,
}
: containerSuffixFamily(path) === undefined
? classifyArtifactPath(path)
: ({ kind: "unknown", format: "unknown" } as const);
return {
occurrence_id: `occ_${digestCanonicalValue({ path, kind: entry.kind }, "Artifact")}`,
artifact_id: null,
parent_occurrence_id: parent,
logical_path: path,
entry_kind: entry.kind,
artifact_kind: role.kind,
artifact_format: role.format,
declared_size: entry.declaredSize,
compressed_size: entry.compressedSize,
executable: entry.executable,
encrypted: entry.encrypted,
hash_status: entry.encrypted ? "unavailable" : "not-hashed",
source_location:
entry.byteOffset === null || entry.declaredSize === null
? null
: { offset: entry.byteOffset, length: entry.declaredSize },
limitations: [...entry.limitations],
};
};
/** Record that an unread member's container suffix was not checked against bytes. */
export const noteUnreadContainerSuffix = (
occurrence: MutableOccurrence,
): void => {
if (occurrence.entry_kind === "directory") return;
const family = containerSuffixFamily(occurrence.logical_path);
if (family === undefined) return;
occurrence.limitations.push(
`The path suffix suggests ${family.toUpperCase()}, but these bytes were not read, so the container family is unknown.`,
);
};
const containerSuffixFamily = (
path: string,
): "asar" | ZipPackageFormat | undefined => {
const lower = path.toLowerCase();
return lower.endsWith(".asar") ? "asar" : zipPackageFormatForPath(lower);
};
/** Compare exact child names independently of host locale and traversal order. */
export const materializeDirectoryNodes = (
@@ -284,6 +314,10 @@ export const nearestParent = (
return undefined;
};
/**
* Assign a path role hint. Container families are not path roles: ZIP and
* ASAR families are chosen only after their bytes have been classified.
*/
export const classifyArtifactPath = (
path: string,
): {
@@ -295,10 +329,6 @@ export const classifyArtifactPath = (
return { kind: "source-map", format: "source-map" };
if (/\.(?:m?js|cjs)$/u.test(lower))
return { kind: "javascript", format: "javascript-bundle" };
if (lower.endsWith(".asar")) return { kind: "container", format: "asar" };
const archiveFormat = zipPackageFormatForPath(lower);
if (archiveFormat !== undefined)
return { kind: "container", format: archiveFormat };
if (/\.framework(?:\/|$)/u.test(lower))
return { kind: "framework", format: "file" };
if (/\.(?:node|dylib|so)$/u.test(lower))
@@ -321,7 +351,29 @@ export const classifyArtifactContent = (
readonly kind: ArtifactOccurrence["artifact_kind"];
readonly format: ArtifactOccurrence["artifact_format"];
} => {
return artifactRoleForFormat(path, classifyArtifactBytes(prefix, fileSize));
const format = classifyArtifactBytes(prefix, fileSize);
if (
format === "file" &&
path.toLowerCase().endsWith(".asar") &&
hasAsarHeader(prefix)
)
return { kind: "container", format: "asar" };
return artifactRoleForFormat(path, format);
};
/**
* Electron ASAR starts with a size pickle whose payload is one uint32, then
* a header pickle whose payload begins with a JSON object.
*/
export const hasAsarHeader = (prefix: Buffer): boolean => {
if (prefix.length < 17 || prefix.readUInt32LE(0) !== 4) return false;
const headerSize = prefix.readUInt32LE(4);
const payloadSize = prefix.readUInt32LE(8);
if (headerSize < 16 || payloadSize !== headerSize - 4) return false;
const stringLength = prefix.readUInt32LE(12);
return (
stringLength >= 2 && stringLength <= payloadSize - 4 && prefix[16] === 0x7b
);
};
const artifactRoleForFormat = (
+4 -27
View File
@@ -6,10 +6,7 @@ import {
import type { FileHandle } from "node:fs/promises";
import type { Stats } from "node:fs";
import {
classifyArtifactBytes,
classifyArtifactContent,
} from "./ArtifactGraphConstruction.js";
import { classifyArtifactContent } from "./ArtifactGraphConstruction.js";
import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../ArtifactHash.js";
import type { ArtifactOccurrence } from "../../domain/artifactGraph.js";
import {
@@ -78,35 +75,15 @@ const classifyRootFormat = async (
const prefix = magic.subarray(0, observed.bytesRead);
const lower = path.toLowerCase();
if (hasZipSignature(prefix)) return zipPackageFormatForPath(lower) ?? "zip";
if (lower.endsWith(".asar") && hasAsarHeader(prefix)) return "asar";
if (
lower.endsWith(".pkg") &&
prefix.subarray(0, 4).toString("ascii") === "xar!"
)
return "pkg";
if (lower.endsWith(".dmg") && (await hasKolyTrailer(handle))) return "dmg";
const size = (await handle.stat()).size;
const format = classifyArtifactContent(path, prefix, size).format;
// Path suffixes stay role hints for nested members. A root whose container
// magic did not match is classified from the bytes that were read.
if (format === "asar" || format === "pkg" || format === "dmg")
return classifyArtifactBytes(prefix, size);
return format;
};
/**
* Electron ASAR starts with a size pickle whose payload is one uint32, then
* a header pickle whose payload begins with a JSON object.
*/
const hasAsarHeader = (prefix: Buffer): boolean => {
if (prefix.length < 16 || prefix.readUInt32LE(0) !== 4) return false;
const headerSize = prefix.readUInt32LE(4);
const payloadSize = prefix.readUInt32LE(8);
if (headerSize < 16 || payloadSize !== headerSize - 4) return false;
const stringLength = prefix.readUInt32LE(12);
return (
stringLength >= 2 && stringLength <= payloadSize - 4 && prefix[16] === 0x7b
);
// ASAR is chosen from its header pickle; any other suffix is a role hint.
return classifyArtifactContent(path, prefix, (await handle.stat()).size)
.format;
};
const hasKolyTrailer = async (handle: FileHandle): Promise<boolean> => {
+9 -2
View File
@@ -17,6 +17,7 @@ import {
classifyArtifactBytes,
createArtifactNode,
createOccurrence,
noteUnreadContainerSuffix,
nearestParent,
type MutableOccurrence,
} from "./ArtifactGraphConstruction.js";
@@ -161,7 +162,8 @@ const visitArtifactEntries = async (
occurrence.hash_status = "unavailable";
occurrence.limitations.push(UNAVAILABLE_UNPACKED_LIMITATION);
}
if (digested !== undefined) {
if (digested === undefined) noteUnreadContainerSuffix(occurrence);
else {
context.nodes.set(digested.node.artifact_id, digested.node);
occurrence.artifact_id = digested.node.artifact_id;
occurrence.artifact_kind = digested.classification.kind;
@@ -176,7 +178,12 @@ const visitArtifactEntries = async (
}
context.occurrences.push(occurrence);
context.occurrenceByPath.set(logicalPath, occurrence);
if (expandableAsar && digested !== undefined && !digested.mismatched) {
if (
expandableAsar &&
digested !== undefined &&
digested.classification.format === "asar" &&
!digested.mismatched
) {
const nested = new AsarArtifactReader(entry.adapterKey);
context.ownedReaders.push(nested);
await nested.prepareContainer(digested.node.sha256, context.signal);
+9 -2
View File
@@ -91,7 +91,9 @@ export const projectAndroidApplication = (
parsed.inventory_evidence,
);
if (inventory.manifest.root_format !== "apk")
throw new TypeError("Android application projection requires APK Evidence");
throw new TypeError(
`Android application projection requires APK inventory Evidence; the inventory root format is ${inventory.manifest.root_format}. APK is chosen for a ZIP archive whose name ends in .apk.`,
);
const nodes = new Map(
inventory.nodes.map((node) => [node.artifact_id, node]),
);
@@ -128,6 +130,11 @@ export const projectAndroidApplication = (
"Bridge candidates are path-based hypotheses, not decoded JNI declarations or observed runtime calls.",
"A bridge basis is inferred from the native path and is repeated for every managed component.",
"ZIP inventory cannot see the APK Signing Block, so an empty signing array does not mean the APK is unsigned.",
...(all.some(({ path }) => path === "AndroidManifest.xml")
? []
: [
"No root AndroidManifest.xml occurrence was observed. The APK family comes from the .apk suffix of a ZIP archive, so these bytes may not be an APK.",
]),
...(bridgeProjection.coverage.status === "partial"
? [
`Bridge candidate pairs exceeded the projection safety budget; ${bridgeProjection.coverage.omitted_candidates} hypotheses are omitted. Component arrays still include every component from the supplied inventory pages.`,
@@ -215,7 +222,7 @@ const bridgeBasis = (
if (convention === "react-native") return "react-native-convention";
if (convention === "flutter") return "flutter-convention";
if (convention === "unity") return "unity-convention";
if (/lib\/[^/]+\/lib[^/]+\.so$/u.test(path.toLowerCase()))
if (/(?:^|\/)lib\/[^/]+\/lib[^/]+\.so$/iu.test(path))
return "jni-library-convention";
return "managed-and-native-content";
};
+1 -1
View File
@@ -624,7 +624,7 @@ describe("Apple path conventions", () => {
).toBe(true);
expect(result.limitations).toEqual(
expect.arrayContaining([
"Runtime families are inferred from inventory formats and paths.",
"Runtime families are inferred from inventory formats and bounded framework path conventions, not observed runtime loading.",
"A bridge basis is inferred from the native path and is repeated for every managed component.",
]),
);
+2 -1
View File
@@ -431,7 +431,8 @@ const projectionLimitations = (facts: {
: []),
"Bundle roles follow path conventions. Read each info_plist_path with inspect_plist for CFBundleExecutable, identifiers, and declared services.",
"Bundle identifiers and signing claims require dedicated plist and CMS parsing; this projection reports only exact paths and hashes.",
"Runtime families are inferred from inventory formats and paths.",
"Runtime families are inferred from inventory formats and bounded framework path conventions, not observed runtime loading.",
"Generic framework and dylib paths do not establish Swift or Objective-C; this inventory projection does not decode language metadata.",
"Bridge candidates are path-based hypotheses, not observed runtime calls.",
"A bridge basis is inferred from the native path and is repeated for every managed component.",
];
@@ -24,6 +24,20 @@ describe("Electron option evidence", () => {
});
});
it("does not read an audio or video preload attribute as a preload script", () => {
const analysis = analyzeJavaScriptStaticSource(`
jsx("audio", { controls: true, preload: "metadata", src });
(0, React.createElement)(\`video\`, { preload: "auto" });
jsx("webview", { preload: "./guest.js" });
const options = { preload: "./preload.js" };
`);
expect(analysis.role_paths.map(({ path }) => path)).toEqual([
"./guest.js",
"./preload.js",
]);
});
it("keeps values unknown when a later computed key can override them", () => {
const analysis = analyzeJavaScriptStaticSource(`
new BrowserWindow({ webPreferences: { preload: "./maybe.js" }, [key]: null });
@@ -11,6 +11,7 @@ import {
collectSemanticModuleLink,
collectSemanticCallable,
collectSemanticReferences,
collectSemanticReferencesSteps,
immutableSemanticBindings,
immutableSemanticScopes,
} from "./javascriptSemanticProjection.js";
@@ -31,7 +32,10 @@ import {
semanticScopeId,
semanticVariableScope,
} from "./javascriptSemanticState.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import {
completeSemanticSteps,
traverseJavaScriptAstSteps,
} from "./javascriptSemanticTraversal.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { collectSemanticMemberMutationsSteps } from "./javascriptSemanticMemberMutations.js";
import {
@@ -99,20 +103,15 @@ export const analyzeParsedJavaScriptReferences = (
name?: string,
): readonly JavaScriptSemanticReference[] => {
const state = createState(file.program);
collectDefinitions(file.program, state);
completeSemanticSteps(collectDefinitionsSteps(file.program, state));
return collectSemanticReferences(file.program, state, name);
};
/** Recover semantics from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptSemantics = (
file: ParsedJavaScriptSource,
): JavaScriptSemanticIr => {
const steps = analyzeParsedJavaScriptSemanticsSteps(file);
for (;;) {
const step = steps.next();
if (step.done === true) return step.value;
}
};
): JavaScriptSemanticIr =>
completeSemanticSteps(analyzeParsedJavaScriptSemanticsSteps(file));
/**
* Recover semantics from a parsed artifact with a yield between analysis
@@ -124,15 +123,13 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
file: ParsedJavaScriptSource,
): Generator<void, JavaScriptSemanticIr> {
const state = createState(file.program);
collectDefinitions(file.program, state);
yield;
yield* collectDefinitionsSteps(file.program, state);
yield* collectSemanticMemberMutationsSteps(file.program, state);
traverseJavaScriptAst(file.program, {
yield* traverseJavaScriptAstSteps(file.program, {
enter: (node) => collectSemanticModuleLink(node, state),
});
yield;
const references = collectSemanticReferences(file.program, state);
yield;
const references = yield* collectSemanticReferencesSteps(file.program, state);
const parserPartial = file.errors.length > 0;
const bindings = immutableSemanticBindings(state);
yield;
@@ -193,11 +190,17 @@ export type JavaScriptOpenReceiverFact =
*/
export const classifyParsedJavaScriptOpenReceivers = (
file: ParsedJavaScriptSource,
): ReadonlyMap<number, JavaScriptOpenReceiverFact> => {
): ReadonlyMap<number, JavaScriptOpenReceiverFact> =>
completeSemanticSteps(classifyParsedJavaScriptOpenReceiversSteps(file));
/** Classify `.open` receivers while yielding during lexical and AST scans. */
export function* classifyParsedJavaScriptOpenReceiversSteps(
file: ParsedJavaScriptSource,
): Generator<void, ReadonlyMap<number, JavaScriptOpenReceiverFact>> {
const state = createState(file.program);
collectDefinitions(file.program, state);
yield* collectDefinitionsSteps(file.program, state);
const facts = new Map<number, JavaScriptOpenReceiverFact>();
traverseJavaScriptAst(file.program, {
yield* traverseJavaScriptAstSteps(file.program, {
enter: (node) => {
if (
(!t.isCallExpression(node) && !t.isOptionalCallExpression(node)) ||
@@ -273,7 +276,7 @@ export const classifyParsedJavaScriptOpenReceivers = (
},
});
return facts;
};
}
const openGlobalFact = (
name: string,
@@ -316,12 +319,18 @@ const ELECTRON_MODULE = /^electron(?:\/(?:common|main|renderer|utility))?$/u;
*/
export const classifyParsedJavaScriptElectronBindings = (
file: ParsedJavaScriptSource,
): ReadonlyMap<number, string> => {
): ReadonlyMap<number, string> =>
completeSemanticSteps(classifyParsedJavaScriptElectronBindingsSteps(file));
/** Resolve Electron aliases while yielding during lexical and AST scans. */
export function* classifyParsedJavaScriptElectronBindingsSteps(
file: ParsedJavaScriptSource,
): Generator<void, ReadonlyMap<number, string>> {
const facts = new Map<number, string>();
const state = createState(file.program);
collectDefinitions(file.program, state);
yield* collectDefinitionsSteps(file.program, state);
const exportsByBinding = new Map<string, string | null>();
traverseJavaScriptAst(file.program, {
yield* traverseJavaScriptAstSteps(file.program, {
enter: (node) => {
if (
!t.isCallExpression(node) &&
@@ -343,7 +352,7 @@ export const classifyParsedJavaScriptElectronBindings = (
},
});
return facts;
};
}
const calleeRoot = (node: t.Node): t.Identifier | undefined => {
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
@@ -416,10 +425,10 @@ const createState = (program: t.Program): JavaScriptSemanticAnalysisState => {
};
};
const collectDefinitions = (
function* collectDefinitionsSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): void => {
): Generator<void, void> {
const stack: JavaScriptSemanticScopeState[] = [
currentSemanticScope(state.scopes),
];
@@ -433,7 +442,7 @@ const collectDefinitions = (
readonly scope: JavaScriptSemanticScopeState;
readonly parameters: t.Function["params"];
}[] = [];
traverseJavaScriptAst(program, {
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent, readAncestors) => {
let parentScope = currentSemanticScope(stack);
if (
@@ -516,14 +525,19 @@ const collectDefinitions = (
});
// All declarations must exist before resolving writes, including hoisted
// functions and local declarations that shadow an outer binding.
for (const { node, scope } of assignments) bindAssignment(node, scope, state);
for (const { body, scope, parameters } of functionBodies)
for (const [index, { node, scope }] of assignments.entries()) {
if (index % 64 === 0) yield;
bindAssignment(node, scope, state);
}
for (const [index, { body, scope, parameters }] of functionBodies.entries()) {
if (index % 64 === 0) yield;
for (const parameter of parameters)
for (const identifier of assignedPatternIdentifiers(
t.isTSParameterProperty(parameter) ? parameter.parameter : parameter,
))
copyParameterToBody({ identifier, body, scope, state });
};
}
}
const copyParameterToBody = (input: {
readonly identifier: t.Identifier;
@@ -3,6 +3,10 @@ import { expect, it } from "vitest";
import {
analyzeParsedJavaScriptSemantics,
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptElectronBindings,
classifyParsedJavaScriptElectronBindingsSteps,
classifyParsedJavaScriptOpenReceivers,
classifyParsedJavaScriptOpenReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import {
@@ -30,8 +34,7 @@ it("yields between semantic phases and returns the synchronous result (#1462)",
yields += 1;
step = steps.next();
}
// Every top-level phase and derived collector is a separate step.
expect(yields).toBeGreaterThanOrEqual(13);
expect(yields).toBeGreaterThan(0);
const reparsed = parseJavaScriptSource(source, "main.js");
if (reparsed === null) throw new Error("Expected parsed source");
expect(step.value).toEqual(analyzeParsedJavaScriptSemantics(reparsed));
@@ -59,3 +62,43 @@ it("traverses a large tree in steps with the synchronous visit order (#1462)", (
expect(yields).toBeGreaterThan(0);
expect(stepped).toEqual(synchronous);
});
it("preserves receiver and Electron alias scope across traversal pauses (#1462)", () => {
const padding = Array.from(
{ length: 400 },
(_, index) => `const v${index} = [${index}];`,
).join("\n");
const text = `
import { BrowserWindow as NativeWindow } from "electron";
const ambient = window;
export function local(ambient, NativeWindow) {
${padding}
ambient.open("/local");
new NativeWindow();
}
ambient.open("/global");
new NativeWindow();
`;
const parsed = parseJavaScriptSource(text, "receivers.js");
if (parsed === null) throw new Error("Expected parsed source");
const openSteps = classifyParsedJavaScriptOpenReceiversSteps(parsed);
let openStep = openSteps.next();
expect(openStep.done).toBe(false);
while (openStep.done !== true) openStep = openSteps.next();
expect([...openStep.value]).toEqual([
[text.indexOf('ambient.open("/local")'), "local"],
[text.indexOf('ambient.open("/global")'), "window"],
]);
expect(openStep.value).toEqual(classifyParsedJavaScriptOpenReceivers(parsed));
const electronSteps = classifyParsedJavaScriptElectronBindingsSteps(parsed);
let electronStep = electronSteps.next();
expect(electronStep.done).toBe(false);
while (electronStep.done !== true) electronStep = electronSteps.next();
expect([...electronStep.value]).toEqual([
[text.lastIndexOf("NativeWindow()"), "BrowserWindow"],
]);
expect(electronStep.value).toEqual(
classifyParsedJavaScriptElectronBindings(parsed),
);
});
@@ -15,7 +15,10 @@ import {
} from "./javascriptSemanticState.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import {
completeSemanticSteps,
traverseJavaScriptAstSteps,
} from "./javascriptSemanticTraversal.js";
import {
evaluateSemanticBinding,
evaluateSemanticProvenance,
@@ -104,11 +107,19 @@ export const collectSemanticReferences = (
program: t.Program,
state: JavaScriptSemanticAnalysisState,
name?: string,
): JavaScriptSemanticReference[] => {
): JavaScriptSemanticReference[] =>
completeSemanticSteps(collectSemanticReferencesSteps(program, state, name));
/** Collect lexical references in steps while preserving resolution and visit order. */
export function* collectSemanticReferencesSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
name?: string,
): Generator<void, JavaScriptSemanticReference[]> {
const output: JavaScriptSemanticReference[] = [];
const seen = new Set<string>();
const patterns: t.Node[] = [];
traverseJavaScriptAst(program, {
yield* traverseJavaScriptAstSteps(program, {
enter: (node, parent, readAncestors) => {
if (isPatternNode(node)) patterns.push(node);
if (
@@ -154,7 +165,7 @@ export const collectSemanticReferences = (
},
});
return output;
};
}
/** Freeze collected scopes into deterministic IR order. */
export const immutableSemanticScopes = (
@@ -32,7 +32,7 @@ import {
} from "./javascriptStaticAnalysisState.js";
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
import {
classifyParsedJavaScriptElectronBindings,
classifyParsedJavaScriptElectronBindingsSteps,
classifyParsedJavaScriptOpenReceivers,
type JavaScriptOpenReceiverFact,
} from "./javascriptSemanticAnalysis.js";
@@ -72,11 +72,13 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
openReceiverFacts?: ReadonlyMap<number, JavaScriptOpenReceiverFact>,
): Generator<void, JavaScriptStaticAnalysis> {
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
const electronBindings =
yield* classifyParsedJavaScriptElectronBindingsSteps(file);
yield* traverseStaticSourceSteps(file, {
source,
accumulator,
...(openReceiverFacts === undefined ? {} : { openReceiverFacts }),
electronBindings: classifyParsedJavaScriptElectronBindings(file),
electronBindings,
});
addSourceMapDirectives(source, file.comments ?? [], accumulator);
return finalizeStaticAnalysis(source, file, accumulator);
@@ -65,6 +65,8 @@ export const inspectCall = (
addReference(context, { node, kind: "service-worker", specifier: first });
inspectEndpointCall(node, name, first, context);
inspectStorageCall(node, name, first, context);
if (first === "audio" || first === "video")
noteMediaElementPreloads(node, accumulator);
if (name.endsWith("loadFile") && first !== undefined)
addLocatedFinding(context, {
collection: accumulator.roles,
@@ -95,6 +97,23 @@ export const inspectCall = (
});
};
// Element factories such as jsx("audio", props) and createElement("video",
// props) carry the HTML media `preload` hint ("none", "metadata", "auto"),
// which never names an Electron preload script.
const noteMediaElementPreloads = (
node: JavaScriptCallLike,
accumulator: AnalysisAccumulator,
): void => {
const props = node.arguments[1];
if (!t.isObjectExpression(props)) return;
for (const property of props.properties)
if (
t.isObjectProperty(property) &&
semanticStaticPropertyName(property.key, property.computed) === "preload"
)
accumulator.mediaElementPreloads.add(property);
};
const requireCallSpecifier = (
node: JavaScriptCallLike,
name: string,
@@ -201,6 +220,7 @@ export const inspectRoleProperty = (
context: FindingContext,
): void => {
if (semanticStaticPropertyName(node.key, node.computed) !== "preload") return;
if (context.accumulator.mediaElementPreloads.has(node)) return;
const path = staticPath(node.value);
if (path === undefined) return;
addLocatedFinding(context, {
@@ -47,6 +47,8 @@ export interface JavaScriptAnalysisAccumulator {
readonly utilityProcesses: LocatedJavaScriptFinding<ElectronUtilityProcessFinding>[];
readonly nativeAddonBindings: LocatedJavaScriptFinding<ElectronNativeAddonBindingFinding>[];
readonly moduleRangeIndex: JavaScriptModuleRangeIndex;
/** `preload` attributes of audio and video elements, which are not scripts. */
readonly mediaElementPreloads: WeakSet<t.Node>;
readonly seen: Set<string>;
visitedNodes: number;
unknownFindings: number;
@@ -111,6 +113,7 @@ export const createJavaScriptAnalysisAccumulator = (
utilityProcesses: [],
nativeAddonBindings: [],
moduleRangeIndex: new JavaScriptModuleRangeIndex(sourceLength),
mediaElementPreloads: new WeakSet(),
seen: new Set(),
visitedNodes: 0,
unknownFindings: 0,
@@ -0,0 +1,60 @@
import type { JsonValue } from "../jsonValue.js";
/** Wire-boundary controls; these do not establish real Ghidra behavior. */
export const compactReference = () => ({
source_address: "0x401000",
target_address: "0x402000",
type: "COMPUTED_CALL",
call: true,
jump: false,
indirect: false,
computed: true,
operand_index: 1,
data: false,
read: false,
write: false,
primary: false,
provenance: "ghidra-reference-manager",
source: "analysis",
});
export const compactInstruction = (references: JsonValue[] = []) => ({
address: "0x401000",
status: "decoded",
procedure: "0x401000",
architecture: "x86:LE:32:default",
mode: "default",
bytes: "ffd0",
length: 2,
mnemonic: "CALL",
raw_disassembly: "CALL EAX",
operands: [],
flow: {
kind: "call",
conditional: false,
computed: true,
direct_destinations: [],
},
references,
limitations: ["Static references do not establish runtime targets."],
});
export const compactCallTargets = (references: JsonValue[] = []) => ({
call_site: "0x401000",
procedure: "0x401000",
status: references.length === 0 ? "unresolved" : "resolved-indirect",
mechanism: "computed",
targets:
references.length === 0
? []
: [
{
address: "0x402000",
procedure: null,
status: "resolved-indirect",
basis: "provider-reference",
references,
},
],
limitations: ["Static references do not establish runtime targets."],
});
+27
View File
@@ -13,6 +13,33 @@ const reference = z.strictObject({
indirect: z.boolean(),
computed: z.boolean(),
operand_index: z.number().int(),
source_address: z.string().min(1).nullable().optional(),
data: z.boolean().nullable().optional(),
read: z.boolean().nullable().optional(),
write: z.boolean().nullable().optional(),
primary: z
.boolean()
.nullable()
.optional()
.describe(
"Provider primary-reference status, not proof of literal addressing or a runtime access. Null or omission means unavailable.",
),
provenance: z
.string()
.min(1)
.nullable()
.optional()
.describe(
"Provider subsystem reporting this reference; null or omission means unavailable.",
),
source: z
.string()
.min(1)
.nullable()
.optional()
.describe(
"Provider reference origin, such as analysis, default, imported or user_defined; null or omission means unavailable.",
),
});
/** Provider-decoded facts for one instruction; operand tokens preserve their native ordering. */
export const nativeInstructionSchema = z.strictObject({
+6 -4
View File
@@ -7,6 +7,10 @@ const foldSegment = (segment: string): string =>
const REACT_NATIVE_SEGMENTS = new Set([
"react.framework",
"reactnative.framework",
"libreactnative.so",
"libreactnativejni.so",
"libhermes_executor.so",
"libhermes.so",
"hermes.framework",
]);
@@ -15,16 +19,14 @@ const UNITY_SEGMENTS = new Set(["libunity.so", "unityframework.framework"]);
/**
* A runtime family named by one filename or directory segment.
* `reactnative` may occur inside a segment (`libreactnativejni.so`).
* The other names match the whole segment.
* Only recognized complete library and framework names establish a convention.
*/
export const nativeRuntimeConvention = (
path: string,
): NativeRuntimeConvention | null => {
for (const segment of path.split("/")) {
const folded = foldSegment(segment);
if (folded.includes("reactnative") || REACT_NATIVE_SEGMENTS.has(folded))
return "react-native";
if (REACT_NATIVE_SEGMENTS.has(folded)) return "react-native";
if (FLUTTER_SEGMENTS.has(folded)) return "flutter";
if (UNITY_SEGMENTS.has(folded)) return "unity";
}
+74
View File
@@ -3,6 +3,11 @@ import { describe, expect, it } from "vitest";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import {
compactReference,
compactInstruction,
compactCallTargets,
} from "../domain/native/nativeInstruction.fixture.js";
import {
parseGhidraFunctionResult,
type GhidraFunctionOperation,
@@ -118,6 +123,75 @@ describe("Ghidra function-analysis result values", () => {
});
});
describe("Ghidra compact reference provenance", () => {
const operations = [
["inspect_native_instruction", compactInstruction],
["resolve_native_call_targets", compactCallTargets],
] as const;
it.each(operations)(
"preserves complete, legacy and unknown facts in %s",
(operation, output) => {
const complete = compactReference();
const legacy = Object.fromEntries(
Object.entries(complete).filter(
([key]) =>
![
"source_address",
"data",
"read",
"write",
"primary",
"provenance",
"source",
].includes(key),
),
);
const unknown = {
...legacy,
source_address: null,
data: null,
read: null,
write: null,
primary: null,
provenance: null,
source: null,
};
for (const reference of [complete, legacy, unknown]) {
const expected = output([reference]);
const parsed = parseGhidraFunctionResult(operation, expected);
if (!parsed.ok) throw parsed.error;
expect(parsed.value).toEqual(expected);
}
},
);
it.each(operations)(
"rejects malformed present provenance in %s",
(operation, output) => {
for (const [field, value] of [
["source_address", 1],
["data", "true"],
["read", 1],
["write", 0],
["primary", "false"],
["provenance", ""],
["source", false],
] as const) {
expect(
parseGhidraFunctionResult(
operation,
output([{ ...compactReference(), [field]: value }]),
),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
}
},
);
});
describe("Ghidra jump-table mapping contract", () => {
it("preserves evidenced defaults separately from cases and unresolved targets", () => {
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
+4 -1
View File
@@ -1,6 +1,6 @@
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { fileURLToPath } from "node:url";
import { accessSync, constants } from "node:fs";
import { accessSync, constants, statSync } from "node:fs";
import { createAnalysisExecution } from "../application/AnalysisProvider.js";
import type {
@@ -75,6 +75,9 @@ export class HopperProvider implements AnalysisProviderCandidate {
};
try {
accessSync(this.config.hopperLauncherPath, constants.X_OK);
// A searchable directory, such as an .app bundle, also passes X_OK.
if (!statSync(this.config.hopperLauncherPath).isFile())
throw new TypeError("Hopper launcher is not a regular file");
return {
status: "available",
code: null,
@@ -1,3 +1,5 @@
import { dirname } from "node:path";
import { describe, expect, it } from "vitest";
import { parseConfig } from "../config/parseConfig.js";
@@ -30,6 +32,19 @@ describe("Hopper provider discovery", () => {
});
});
it("reports a directory launcher path as missing", () => {
const directory = dirname(process.execPath);
expect(provider(directory).inspectAvailability()).toEqual({
status: "unavailable",
code: "executable_missing",
reason: `Hopper launcher is missing or not executable: ${directory}`,
diagnostics: {
launcher_path: directory,
platform: process.platform,
},
});
});
it("separates target kind, format, and architecture support", () => {
const hopper = provider(process.execPath);
expect(hopper.inspectTargetSupport(databaseTarget())).toMatchObject({
+4 -2
View File
@@ -1,6 +1,6 @@
import { createHash, randomUUID } from "node:crypto";
import { createReadStream, constants } from "node:fs";
import { copyFile } from "node:fs/promises";
import { copyFile, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join, normalize } from "node:path";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
@@ -39,8 +39,10 @@ export class IdaWorkspace {
target: BinaryTarget,
root = tmpdir(),
): Promise<IdaWorkspace> {
// Upstream reports canonical paths (macOS /var -> /private/var), so the
// private input path must already be canonical for identity checks.
const runtime = await PrivateRuntimeRoot.create({
parent: normalize(root),
parent: await realpath(normalize(root)),
prefix: "rea-ida-",
});
const inputPath = join(runtime.path, basename(target.path));
+23 -1
View File
@@ -1,9 +1,11 @@
import { chmod, readdir, rm } from "node:fs/promises";
import { chmod, readdir, realpath, rm, symlink } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
createIdaTarget,
RecordingIdaMcp,
} from "../../tests/fixtures/idaMcp.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { IdaSessionClient } from "./IdaSessionClient.js";
@@ -33,6 +35,26 @@ const clientFor = (root: string, target: BinaryTarget) => {
};
describe("IDA workspace ownership before input preparation", () => {
it.skipIf(process.platform === "win32")(
"opens the private input through a canonical path when the workspace root is a symlink",
async () => {
const { root, target } = await createIdaTarget();
roots.push(root);
const linked = join(
await createTestTempDirectory("rea-ida-link-"),
"root",
);
await symlink(root, linked);
const { client, producer } = clientFor(linked, target);
expect((await client.execute("health", {})).ok).toBe(true);
const open = producer.calls.find(({ name }) => name === "idb_open");
expect(
String(open?.args.input_path).startsWith(`${await realpath(root)}/`),
).toBe(true);
expect((await client.close()).ok).toBe(true);
},
);
it("retains cleanup ownership when copying the admitted input fails", async () => {
const { root, target } = await createIdaTarget();
roots.push(root);
@@ -0,0 +1,137 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
TextReader,
Uint8ArrayReader,
Uint8ArrayWriter,
ZipWriter,
} from "@zip.js/zip.js";
import { expect } from "vitest";
import { createDirectAnalysis } from "../../../src/composition/directAnalysis.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const android = {
extension: "apk",
operation: "project_android_application_graph",
command: "project-android-application-graph",
scripts: ["classes.dex", "extra.dex"],
nativeBytes: Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]),
libraries: {
"lib/arm64-v8a/libcommunity.so": "jni-library-convention",
"lib/arm64-v8a/libpreact.so": "jni-library-convention",
"lib/arm64-v8a/libreactivity.so": "jni-library-convention",
"lib/arm64-v8a/libhermesfake.so": "jni-library-convention",
"lib/arm64-v8a/libflutter.so.backup.so": "jni-library-convention",
},
positives: {
"lib/arm64-v8a/libreactnativejni.so": "react-native-convention",
"lib/arm64-v8a/libhermes.so": "react-native-convention",
"lib/arm64-v8a/libflutter.so": "flutter-convention",
"lib/arm64-v8a/libunity.so": "unity-convention",
},
};
const apple = {
extension: "ipa",
operation: "project_apple_application_graph",
command: "project-apple-application-graph",
scripts: ["Payload/Fixture.app/main.js", "Payload/Fixture.app/extra.js"],
nativeBytes: Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0, 0, 1]),
libraries: {
"Payload/Fixture.app/Frameworks/App.framework/App":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/MyApp.framework/MyApp":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Community.framework/Community":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Preact.framework/Preact":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Reactivity.framework/Reactivity":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/HermesExtra.framework/HermesExtra":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/Flutter.framework.backup/Flutter":
"javascript-and-native-content",
"Payload/Fixture.app/Frameworks/plain.dylib":
"javascript-and-native-content",
},
positives: {
"Payload/Fixture.app/Frameworks/React.framework/React":
"react-native-convention",
"Payload/Fixture.app/Frameworks/hermes.framework/hermes":
"react-native-convention",
"Payload/Fixture.app/Frameworks/Flutter.framework/Flutter":
"flutter-convention",
"Payload/Fixture.app/Frameworks/UnityFramework.framework/UnityFramework":
"unity-convention",
},
};
cliTest(
"bounds mobile conventions and preserves inference provenance through compiled CLI and real MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-mobile-conventions-");
const { call } = await connectLocalToolsMcp();
const { runProviderAnalysis } = createDirectAnalysis({});
for (const fixture of [android, apple]) {
for (const positive of [false, true]) {
const libraries = positive ? fixture.positives : fixture.libraries;
const path = join(
root,
`${positive ? "positive" : "negative"}.${fixture.extension}`,
);
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const script of fixture.scripts)
await writer.add(script, new TextReader("export const value = 1;"));
for (const name of Object.keys(libraries))
await writer.add(name, new Uint8ArrayReader(fixture.nativeBytes));
await writeFile(path, await writer.close());
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
const input = { inventory_evidence: [inventory] };
const result = await cli.run({
arguments: [fixture.command, JSON.stringify(input), "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = parseEvidence(result.json);
const response = await call(fixture.operation, input);
expect(response.isError).not.toBe(true);
const mcp = parseEvidence(response.structuredContent);
expect(mcp).toEqual(evidence);
expect(evidence.normalized_result).toMatchObject({
runtime_families: positive
? fixture.extension === "apk"
? ["dalvik-art", "flutter", "native", "react-native", "unity"]
: ["flutter", "javascript", "native", "react-native", "unity"]
: fixture.extension === "apk"
? ["dalvik-art", "native"]
: ["javascript", "native"],
coverage: {
status: "complete-within-inventory",
inventory_complete: true,
},
limitations: expect.arrayContaining([
"A bridge basis is inferred from the native path and is repeated for every managed component.",
]),
bridge_candidates: expect.arrayContaining(
Object.entries(libraries).flatMap(([native_path, basis]) =>
fixture.scripts.map((script) =>
expect.objectContaining({
native_path,
basis,
...(fixture.extension === "apk"
? { managed_path: script }
: { source_path: script }),
}),
),
),
),
});
}
}
},
30_000,
);
@@ -0,0 +1,160 @@
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, relative } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"resolves package subpaths with source provenance through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-package-subpaths-");
const source = [
'const nearest = require("fixture/parser");',
'const nested = require("nested/parser");',
'const parser = require("exported/parser");',
'const directory = require("exported/parser/");',
'const hidden = require("exported/hidden");',
'const missing = require("exported/missing");',
'new BrowserWindow({webPreferences:{preload:"exported/parser"}});',
].join("\n");
const files = {
"src/main.cjs": source,
"src/node_modules/fixture/parser.js": "module.exports = 1;",
"node_modules/fixture/package.json": "{}",
"node_modules/fixture/parser.js": "module.exports = 2;",
"node_modules/nested/package.json": "{}",
"node_modules/nested/parser/package.json": JSON.stringify({
main: "./entry.js",
exports: "./decoy.js",
}),
"node_modules/nested/parser/entry.js": "module.exports = 3;",
"node_modules/nested/parser/index.js": "module.exports = 4;",
"node_modules/nested/parser/decoy.js": "module.exports = 5;",
"node_modules/exported/package.json": JSON.stringify({
exports: {
"./parser": "./parser.cjs",
"./parser/": "./parser.cjs",
"./missing": "./missing.cjs",
},
}),
"node_modules/exported/parser.cjs": "module.exports = 6;",
"node_modules/exported/hidden.js": "module.exports = 7;",
"node_modules/exported/missing/index.js": "module.exports = 8;",
};
await Promise.all(
Object.entries(files).map(async ([path, content]) => {
const absolute = join(root, path);
await mkdir(dirname(absolute), { recursive: true });
await writeFile(absolute, content);
}),
);
const nativeRequire = createRequire(join(root, "src/main.cjs"));
const expectedTargets = {
"fixture/parser": "src/node_modules/fixture/parser.js",
"nested/parser": "node_modules/nested/parser/entry.js",
"exported/parser": "node_modules/exported/parser.cjs",
};
for (const [specifier, target] of Object.entries(expectedTargets))
expect(
relative(root, nativeRequire.resolve(specifier)).replaceAll("\\", "/"),
).toBe(target);
for (const specifier of ["exported/parser/", "exported/hidden"])
expect(() => nativeRequire.resolve(specifier)).toThrow(
expect.objectContaining({ code: "ERR_PACKAGE_PATH_NOT_EXPORTED" }),
);
expect(() => nativeRequire.resolve("exported/missing")).toThrow(
expect.objectContaining({ code: "MODULE_NOT_FOUND" }),
);
let document: unknown;
if (surface === "CLI") {
const response = await cli.run({
arguments: ["analyze-javascript-application", root, "--format", "json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(response.exitCode, response.stderr).toBe(0);
document = response.json;
} else {
const { call } = await connectLocalToolsMcp();
const response = await call("analyze_javascript_application", {
input_path: root,
format: "directory",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
document = response.structuredContent;
}
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(document).normalized_result;
const imports = graph.edges.filter(
({ properties }) => properties.module_link_kind === "require",
);
const sourceDigest = createHash("sha256").update(source).digest("hex");
for (const [specifier, target] of Object.entries(expectedTargets))
expect(imports).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
specifier,
resolved_path: target,
resolution_status: "resolved",
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({
available: true,
sha256: sourceDigest,
}),
location: expect.objectContaining({
available: true,
value: expect.objectContaining({
kind: "source-range",
source: "src/main.cjs",
}),
}),
}),
}),
);
for (const [specifier, status] of [
["exported/parser/", "external"],
["exported/hidden", "external"],
["exported/missing", "not-found"],
])
expect(imports).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
specifier,
resolved_path: null,
resolution_status: status,
}),
}),
);
const preloads = graph.nodes.filter(
({ kind }) => kind === "electron-preload",
);
expect(preloads).toHaveLength(1);
expect(preloads[0]?.identity).toMatchObject({
key: "electron-preload:module-specifier:node_modules/exported/parser.cjs",
});
for (const observation of preloads[0]?.observations ?? [])
expect(observation).toMatchObject({
properties: {
declared_path: "exported/parser",
resolution_context: "module-specifier",
resolved_path: "node_modules/exported/parser.cjs",
},
evidence: {
location: {
available: true,
value: { source: "src/main.cjs", start: { line: 7 } },
},
},
});
},
);
@@ -154,9 +154,23 @@ describe("unexpanded ASAR inventory controls", () => {
"resources/encrypted.asar": "resources",
"resources/ordinary.asar": "resources",
});
expect(artifactOccurrenceAt(observed, "resources/app.asar")).toMatchObject({
artifact_kind: "container",
artifact_format: "asar",
});
expect(
artifactOccurrenceAt(observed, "resources/encrypted.asar").hash_status,
).toBe("unavailable");
artifactOccurrenceAt(observed, "resources/ordinary.asar"),
).toMatchObject({ artifact_kind: "resource", artifact_format: "file" });
expect(
artifactOccurrenceAt(observed, "resources/encrypted.asar"),
).toMatchObject({
artifact_kind: "unknown",
artifact_format: "unknown",
hash_status: "unavailable",
limitations: [
"The path suffix suggests ASAR, but these bytes were not read, so the container family is unknown.",
],
});
});
it("keeps ASAR-named directories as directory parents", async () => {
@@ -195,9 +209,30 @@ describe("unexpanded ASAR inventory controls", () => {
},
);
it("keeps malformed filesystem archives rejected", async () => {
const root = await createTestTempDirectory("rea-malformed-asar-");
it("records filesystem files without an ASAR header as ordinary bytes", async () => {
const root = await createTestTempDirectory("rea-ordinary-asar-");
await writeFile(join(root, "ordinary.asar"), "ordinary bytes");
const observed = await inventoryArtifact(root);
expect(artifactParentPaths(observed)).toEqual({
".": null,
"ordinary.asar": ".",
});
expect(artifactOccurrenceAt(observed, "ordinary.asar")).toMatchObject({
artifact_kind: "resource",
artifact_format: "file",
hash_status: "verified",
});
});
it("keeps filesystem archives with a malformed ASAR header rejected", async () => {
const root = await createTestTempDirectory("rea-malformed-asar-");
const json = Buffer.from("{not json");
const header = Buffer.alloc(16);
header.writeUInt32LE(4, 0);
header.writeUInt32LE(json.length + 12, 4);
header.writeUInt32LE(json.length + 8, 8);
header.writeUInt32LE(json.length, 12);
await writeFile(join(root, "broken.asar"), Buffer.concat([header, json]));
await expect(inventoryArtifact(root)).rejects.toMatchObject({
reason: "format",
});
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { chmod, mkdir, readdir, truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { build } from "plist";
@@ -67,6 +67,82 @@ it.each([
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"rejects a selected unreadable directory before descending into it",
async () => {
const root = await bundle();
const selected = join(root, "Contents", "Resources");
await chmod(selected, 0);
try {
await expect(readdir(selected)).rejects.toMatchObject({ code: "EACCES" });
await expect(
inspect(root, { path: "Contents/Resources" }),
).rejects.toMatchObject({
issues: [
{
path: ["path"],
reason: "invalid_value",
message: expect.stringContaining("selects a directory"),
},
],
});
} finally {
await chmod(selected, 0o700);
}
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"inspects an active standalone archive without opening unrelated directories",
async () => {
const root = await createTestTempDirectory("rea-keyed-standalone-scope-");
const path = join(root, "Model.plist");
const bytes = Buffer.from(archive);
const sha256 = createHash("sha256").update(bytes).digest("hex");
await writeFile(path, bytes);
const blocked = join(root, "unrelated");
await mkdir(blocked);
await chmod(blocked, 0);
try {
await expect(readdir(blocked)).rejects.toMatchObject({ code: "EACCES" });
const result = await new ArtifactProvider(process.env, "darwin")
.createClient({ path, sha256, kind: "artifact", format: "plist" })
.execute("inspect_keyed_archive", {});
if (!result.ok) throw result.error;
expect(result.value.result).toMatchObject({
archive_path: "Model.plist",
archive_sha256: sha256,
});
} finally {
await chmod(blocked, 0o700);
}
},
);
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"reports the selected oversized archive limit despite an unreadable sibling",
async () => {
const root = await createTestTempDirectory("rea-keyed-size-scope-");
const path = join(root, "Large.plist");
await writeFile(path, archive);
await truncate(path, 64 * 1024 * 1024 + 1);
const blocked = join(root, "unrelated");
await mkdir(blocked);
await chmod(blocked, 0);
try {
await expect(readdir(blocked)).rejects.toMatchObject({ code: "EACCES" });
await expect(
inspect(root, { path: "Large.plist" }),
).rejects.toMatchObject({
reason: "limit",
message: "Keyed archive exceeds 64 MiB",
});
} finally {
await chmod(blocked, 0o700);
}
},
);
it("reports an absent named root with the archive's available roots", async () => {
const root = await bundle();
await expect(
@@ -184,6 +260,38 @@ it("selects a decomposed bundle path by its normalized spelling and reports the
);
});
it("selects normalized intermediate directories while preserving their raw spelling", async () => {
const root = await createTestTempDirectory("rea-keyed-unicode-ancestors-");
const directory = "Cafe\u0301/Neste\u0301";
const rawPath = `${directory}/Model.plist`;
await mkdir(join(root, directory), { recursive: true });
await writeFile(join(root, rawPath), archive);
const result = await inspect(root, { path: rawPath.normalize("NFC") });
expect(result.archive_path).toBe(rawPath);
});
it.skipIf(process.platform === "darwin" || process.platform === "win32")(
"rejects equivalent archive paths beneath distinct Unicode-equivalent directories",
async () => {
const root = await createTestTempDirectory("rea-keyed-unicode-branches-");
for (const directory of ["Caf\u00e9", "Cafe\u0301"]) {
await mkdir(join(root, directory));
await writeFile(join(root, directory, "Model.plist"), archive);
}
await expect(
inspect(root, { path: "Caf\u00e9/Model.plist" }),
).rejects.toMatchObject({
issues: [
{
path: ["path"],
reason: "invalid_value",
message: expect.stringContaining("Unicode-equivalent"),
},
],
});
},
);
it("keeps an archive readable when a non-finite real is an explicit unknown", async () => {
const root = await bundle();
const bytes = Buffer.from(
+78 -1
View File
@@ -1,10 +1,18 @@
import { lstat, writeFile } from "node:fs/promises";
import { lstat, mkdir, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
TextReader,
Uint8ArrayReader,
Uint8ArrayWriter,
ZipWriter,
} from "@zip.js/zip.js";
import { describe, expect, it } from "vitest";
import { inventoryArtifact } from "../../../src/artifacts/inventory/ArtifactInventory.js";
import { classifyAndHashRoot } from "../../../src/artifacts/inventory/classify.js";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { artifactOccurrenceAt } from "../../fixtures/artifactEntryOrder.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("ZIP signature detection", () => {
@@ -120,3 +128,72 @@ describe("ZIP signature detection", () => {
},
);
});
describe("nested container family", () => {
const plainZip = async (): Promise<Uint8Array> => {
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("readme.txt", new TextReader("hello\n"));
return writer.close();
};
it("takes ZIP families from member bytes, not member suffixes", async () => {
const root = await createTestTempDirectory("rea-nested-zip-family-");
const path = join(root, "outer.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("inner-text.apk", new TextReader("just some text\n"));
await writer.add("inner-real.apk", new Uint8ArrayReader(await plainZip()));
await writer.add("renamed.bin", new Uint8ArrayReader(await plainZip()));
await writer.add("locked.ipa", new TextReader("encrypted bytes"), {
password: "fixture-only",
});
await writeFile(path, await writer.close());
const observed = await inventoryArtifact(path);
expect(artifactOccurrenceAt(observed, "inner-text.apk")).toMatchObject({
artifact_kind: "resource",
artifact_format: "file",
limitations: [],
});
expect(artifactOccurrenceAt(observed, "inner-real.apk")).toMatchObject({
artifact_kind: "container",
artifact_format: "apk",
});
expect(artifactOccurrenceAt(observed, "renamed.bin")).toMatchObject({
artifact_kind: "container",
artifact_format: "zip",
});
expect(artifactOccurrenceAt(observed, "locked.ipa")).toMatchObject({
artifact_kind: "unknown",
artifact_format: "unknown",
hash_status: "unavailable",
limitations: [
"The path suffix suggests IPA, but these bytes were not read, so the container family is unknown.",
],
});
});
it.skipIf(process.platform === "win32")(
"does not name a container family for a symlink it did not follow",
async () => {
const root = await createTestTempDirectory("rea-nested-symlink-family-");
const directory = join(root, "application");
await mkdir(directory);
await writeFile(join(directory, "real.apk"), await plainZip());
await symlink("real.apk", join(directory, "link.apk"));
const observed = await inventoryArtifact(directory);
expect(artifactOccurrenceAt(observed, "link.apk")).toMatchObject({
entry_kind: "symlink",
artifact_kind: "unknown",
artifact_format: "unknown",
hash_status: "not-hashed",
limitations: [
"Symlink target was not followed or disclosed.",
"The path suffix suggests APK, but these bytes were not read, so the container family is unknown.",
],
});
expect(artifactOccurrenceAt(observed, "real.apk")).toMatchObject({
artifact_kind: "container",
artifact_format: "apk",
});
},
);
});
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { ok } from "../../../src/domain/result.js";
import {
compactReference,
compactInstruction,
compactCallTargets,
} from "../../../src/domain/native/nativeInstruction.fixture.js";
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
describe("compact Ghidra reference MCP evidence", () => {
it("retains access kind, non-primary analyzer references and call-target provenance", async () => {
const base = {
...compactReference(),
call: false,
computed: false,
data: true,
};
const references = [
{ ...base, type: "DATA", primary: true, operand_index: 1 },
{ ...base, type: "READ", primary: true, read: true, operand_index: 1 },
{ ...base, type: "WRITE", primary: false, write: true, operand_index: 0 },
];
const outputs = {
inspect_native_instruction: compactInstruction(references),
resolve_native_call_targets: compactCallTargets([compactReference()]),
};
const harness = await connectGhidraMcp(
"ghidra-compact-provenance",
(operation) => {
if (
operation !== "inspect_native_instruction" &&
operation !== "resolve_native_call_targets"
)
throw new TypeError(`Unexpected operation: ${operation}`);
return Promise.resolve(ok(outputs[operation]));
},
);
try {
for (const name of [
"inspect_native_instruction",
"resolve_native_call_targets",
] as const) {
const result = await harness.mcp.callTool({
name,
arguments: { address: "0x401000" },
});
expect(result.isError).not.toBe(true);
const evidence = sessionEvidence(
harness.session,
result.structuredContent,
);
expect(evidence.normalized_result).toEqual(outputs[name]);
}
} finally {
await harness.close();
}
}, 30_000);
it("preserves empty references and unresolved targets without manufacturing provenance", async () => {
const outputs = {
inspect_native_instruction: compactInstruction(),
resolve_native_call_targets: compactCallTargets(),
};
const harness = await connectGhidraMcp(
"ghidra-compact-unknown",
(operation) => {
if (
operation !== "inspect_native_instruction" &&
operation !== "resolve_native_call_targets"
)
throw new TypeError(`Unexpected operation: ${operation}`);
return Promise.resolve(ok(outputs[operation]));
},
);
try {
for (const name of [
"inspect_native_instruction",
"resolve_native_call_targets",
] as const) {
const result = await harness.mcp.callTool({
name,
arguments: { address: "0x401000" },
});
expect(result.isError).not.toBe(true);
expect(
sessionEvidence(harness.session, result.structuredContent)
.normalized_result,
).toEqual(outputs[name]);
}
} finally {
await harness.close();
}
}, 30_000);
});
+38 -1
View File
@@ -119,7 +119,14 @@ describe("Android application projection", () => {
projectAndroidApplicationEvidence({ inventory_evidence: [inventory] }),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
error: {
_tag: "AnalysisInputError",
issues: [
expect.objectContaining({
message: expect.stringContaining("root format is zip"),
}),
],
},
});
});
@@ -168,6 +175,36 @@ describe("Android application projection", () => {
});
});
describe("APK family without a root manifest", () => {
const missingManifest =
"No root AndroidManifest.xml occurrence was observed. The APK family comes from the .apk suffix of a ZIP archive, so these bytes may not be an APK.";
const projectApk = async (entries: Record<string, string>) => {
const root = await createTestTempDirectory("rea-android-manifest-");
const path = join(root, "Plain.apk");
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const [entry, text] of Object.entries(entries))
await writer.add(entry, new TextReader(text));
await writeFile(path, await writer.close());
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
return androidApplicationProjectionResultSchema.parse(
requireSuccessfulProjection(
projectAndroidApplicationEvidence({ inventory_evidence: [inventory] }),
).normalized_result,
);
};
it("says when the APK family rests only on the ZIP filename suffix", async () => {
expect((await projectApk({ "readme.txt": "hello" })).limitations).toContain(
missingManifest,
);
expect(
(await projectApk({ "AndroidManifest.xml": "manifest" })).limitations,
).not.toContain(missingManifest);
});
});
describe("case-distinct APK resource names", () => {
it("inventories and projects an APK whose resource names differ only in case", async () => {
const root = await createTestTempDirectory("rea-android-case-");
-4
View File
@@ -359,10 +359,6 @@
</tbody>
</table>
</div>
<p class="caption">
The hosted example's request path includes the site's
<code>/rea/</code> prefix.
</p>
<div class="code-block">
<div class="code-header">
<span>Read the loaded scripts</span