Compare commits

...
Author SHA1 Message Date
Cursor Agentandmorluto 8eb1be90c4 fix(erlang): pass CLI cancellation into analysis completion
finishAnalysisCommand now requires the caller signal so a completed BEAM
inspection that is already cancelled is reported as cancellation.

Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 06:01:45 +00:00
Cursor Agentandmorluto 44bac35c50 fix(erlang): use the shared public input constructor
inspect_erlang_beam advertised a bare strict object. Public tool inputs
now go through publicInputStrictObject so later shared input policy has
one owner.

Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 06:00:52 +00:00
Cursor Agentandmorluto 23e9e706a7 style: align restacked Erlang rows in translated READMEs
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 06:00:52 +00:00
Cursor Agentandmorluto 60bb5d92e2 test(erlang): reject an empty BEAM path at the CLI
NUL paths cannot be passed through process spawn, so the shared NUL rule
stays covered by the public schema and service tests.

Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 05:59:25 +00:00
Cursor Agentandmorluto 35c6e25fa6 style: format restacked BEAM guide tables
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 05:59:25 +00:00
Cursor Agentandmorluto 9c2b85ff0c fix(erlang): apply the shared path policy to public BEAM input
The advertised inspect_erlang_beam schema accepted any nonempty path and
rejected relative paths only after parsing. It now uses the shared absolute
local-path schema, and the CLI path uses the shared NUL-free local path
before resolution.

Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 05:59:14 +00:00
Matt Van Horn add24bc3ad fix(erlang): bound BEAM tables and read OTP 28+ atom tables
Address the review findings on the BEAM parser:

- Cap export, import, and lambda rows at 65536 combined, checked
  before any row is allocated.
- Reject any form remainder shorter than a chunk header, including
  zero-filled ones.
- Reject AtU8 atom names that are not valid UTF-8 instead of
  replacing bytes.
- Read the OTP 28+ compact AtU8 layout (negated count, compact
  u-tag name lengths), which was rejected as over the atom budget.
- Add a compiler-produced OTP 29 BEAM fixture whose expected atoms,
  table rows, and Code header come from beam_lib on the same file.
2026-10-22 05:59:14 +00:00
Matt Van Hornandmorluto 1e238f271c docs(readme): mirror the Erlang BEAM row in translated READMEs
The README now links the Erlang BEAM inspection docs, and
documentationFacts.test.ts requires every translated README to carry the
same link. Adds the matching row to all 20 files under docs/readme/.

Also formats README_fr.md and README_ml.md with oxfmt, and lists
README_ml.md in website/locales/languages.json so readmeConsistency.test.ts
finds the Malayalam translation the README already links.

Includes: The PR's new README link must be mirrored in every translated README; documentationFacts.test.ts fails without these rows.

Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 05:59:14 +00:00
Matt Van Hornandmorluto 117d3f9036 feat(erlang): inspect BEAM IFF chunks and tables
Add inspect_erlang_beam for FOR1/BEAM containers, including atom,
export, import, and lambda tables plus the Code chunk header. The
workflow reads a stable file without executing bytecode or requiring
an Erlang installation.

Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-22 05:58:44 +00:00
57 changed files with 1724 additions and 2 deletions

No files matched your search

@@ -219,6 +219,12 @@ explicit path or endpoint and do not need it.
delivery requires `--evidence_output`.
See the
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
- Erlang BEAM IFF container: `inspect_erlang_beam` with the explicit local
`.beam` file. This built-in target-free inspection requires no Erlang
installation. Preserve chunk names, atom order, export/import/lambda tables,
and the Code header. Missing optional tables stay null. Bytecode is not
executed. See the
[Erlang BEAM guide](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md).
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
@@ -96,6 +96,16 @@ dependencies. An omitted package kind stays null. A rustc commit hash is
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
## Erlang BEAM files
Use `inspect_erlang_beam` for an explicit local `FOR1`/`BEAM` file when the
question concerns IFF chunks, atom names, exports, imports, lambdas, or the
Code header. No `open_binary`, Erlang installation, or bytecode execution is
required. The CLI equivalent is `inspect-erlang-beam PATH`. Preserve artifact
SHA-256, chunk file ranges, 1-based atom indices, and original atom spellings.
Missing optional tables stay null. See
[Erlang BEAM files](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md).
## SQLite snapshots
Use `inspect_sqlite_database` with an explicit local database snapshot for its
+1
View File
@@ -191,6 +191,7 @@ Additional tools and host support depend on the target:
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas and the Code header | Built-in static BEAM inspection; [Erlang BEAM](docs/erlang-beam.md) |
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
+1
View File
@@ -32,6 +32,7 @@ export default defineConfig({
{ text: "IL2CPP metadata", link: "/il2cpp-metadata" },
{ text: "Model artifacts", link: "/model-artifacts" },
{ text: "Rust dependencies", link: "/rust-binaries" },
{ text: "Erlang BEAM", link: "/erlang-beam" },
{
text: "JavaScript and Electron",
link: "/javascript-artifact-reconstruction",
+10
View File
@@ -155,6 +155,16 @@ dependency edges, and the section's file range. A missing `.dep-v0` section is
null metadata, not proof that the binary has no dependencies. See
[Rust binaries](rust-binaries.md).
Inspect an Erlang BEAM file without executing bytecode:
```bash
rea inspect-erlang-beam /absolute/path/to/app.beam --json
```
The workflow reports IFF chunk names, atom tables, exports, imports, lambdas,
and the Code chunk header. Missing optional tables stay null. See
[Erlang BEAM files](erlang-beam.md).
Configure [Hopper or Ghidra](installation.md#hopper), the
[IDA adapter](ida-provider.md), or [Kuna](kuna-provider.md) before analyzing a
native target. Substitute
+46
View File
@@ -0,0 +1,46 @@
# Erlang BEAM files
`inspect_erlang_beam` / `inspect-erlang-beam` answers whether one selected
local file is a `FOR1`/`BEAM` IFF container, which chunks that form stores, and
which atom, export, import, lambda, and Code-header fields those chunks carry.
It reads the file without executing bytecode and does not require an Erlang
installation or BEAM emulator.
```bash
rea inspect-erlang-beam /absolute/path/to/app.beam --json
```
The CLI resolves relative paths against the caller's working directory. MCP
requires an absolute filesystem path and preserves that exact selection:
```json
{
"name": "inspect_erlang_beam",
"arguments": { "path": "/artifacts/app.beam" }
}
```
## Observations
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`chunks` lists every IFF chunk name, file range, and payload SHA-256.
When `AtU8` or `Atom` is present, `atoms` is the 1-based name table and
`module` is atom 1. Both the pre-OTP 28 table and the OTP 28+ compact table
(negated count, compact-encoded name lengths) are read. `exports`, `imports`, and `lambdas` resolve atom indices to
those names when the table is present; unresolved indices keep `function` or
`module` null.
`code` reports the Code chunk instruction-set, opcode maximum, label count,
function count, and the instruction stream's file range. Missing optional
tables are null, not empty-everywhere.
## Limits and failures
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
64 chunks and 4096 atoms are examined by default, and at most 65536 export,
import, and lambda rows together. Malformed `FOR1`/`BEAM` headers, truncated
chunks, and `AtU8` names that are not valid UTF-8 fail without partial success.
This workflow does not execute bytecode, inflate `LitT` literals, or decode
`Attr`/`CInf` external-term payloads.
+1 -1
View File
@@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Task | Guides |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
+1
View File
@@ -145,6 +145,7 @@ npx rea-agents@latest setup
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
| تثبيتات سطح المكتب | عائلات وقت التشغيل، والملفات الداعمة، وعلامات المكونات، والخطوات التالية | فحص محدود للملفات والترويسات;[تطبيقات سطح المكتب](../desktop-applications.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
| Desktop-Installationen | Laufzeitkandidaten, Quelldateien, Komponenten und nächste Schritte | Begrenzte Verzeichnis- und Headerprüfung;[Desktop-Anwendungen](../desktop-applications.md) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ npx rea-agents@latest setup
| Δομή ELF χωρίς εκτέλεση | Ενότητες, τμήματα, αρχικά σύμβολα/εγγραφές μετεγκατάστασης και ενδείξεις πιθανών μέτρων προστασίας από στατική ανάλυση | pwntools που παρέχει ο χρήστης σε Linux x64· [διαγνωστικά δυαδικών αρχείων](../binary-diagnostics.md) |
| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
+1
View File
@@ -145,6 +145,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
| Instalaciones de escritorio | Posibles entornos de ejecución, archivos de evidencia, componentes y siguientes pasos | Inspección acotada de directorios y cabeceras;[Aplicaciones de escritorio](../desktop-applications.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](../javascript-application-workflows.md) |
+1
View File
@@ -137,6 +137,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np
| ساختار ELF آفلاین | بخش‌ها، سگمنت‌ها، نمادها و relocationهای اصلی و گزینه‌های احتمالی کاهش آسیب‌پذیری ایستا | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](../recorded-crashes.md) |
| نصب‌های دسکتاپ | نامزدهای زمان اجرا، فایل‌های شاهد، نشانه‌های مؤلفه‌ها و مراحل بعدی | بررسی محدود پوشه‌ها و سرآیندها;[برنامه‌های دسکتاپ](../desktop-applications.md) |
| JavaScript / Electron | ماژول‌ها، importها، source mapها، مسیرها، IPC و ارتباط افزونه‌های بومی | Node.js و npm؛ [تحلیل برنامه](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
| Installations de bureau | Environnements candidats, fichiers justificatifs, composants et étapes suivantes | Inspection bornée des répertoires et en-têtes;[Applications de bureau](../desktop-applications.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](../javascript-application-workflows.md) |
+1
View File
@@ -152,6 +152,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
| Instalasi desktop | Kandidat runtime, berkas pendukung, penanda komponen, dan langkah berikutnya | Pemeriksaan direktori dan header terbatas;[Aplikasi desktop](../desktop-applications.md) |
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
| Installazioni desktop | Runtime candidati, file di supporto, componenti e passi successivi | Ispezione limitata di directory e intestazioni;[Applicazioni desktop](../desktop-applications.md) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
| デスクトップのインストール先 | ランタイム候補、根拠ファイル、コンポーネントの目印、次の調査手順 | 上限付きのディレクトリ・ヘッダー検査;[デスクトップアプリ](../desktop-applications.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](../javascript-application-workflows.md) |
+1
View File
@@ -153,6 +153,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
+1
View File
@@ -151,6 +151,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.19), 24.x (>=24.11), അല്ലെ
| Offline ELF layout | Sections, segments, യഥാർത്ഥ symbols/relocations, static mitigation candidates | Linux x64-ൽ caller നൽകുന്ന pwntools; [binary diagnostics](../binary-diagnostics.md) |
| Go binaries | ഉൾച്ചേർത്ത compiler version, modules, replacements, build settings | Built-in static ELF/PE/thin Mach-O പരിശോധന; Go installation ആവശ്യമില്ല |
| Rust binaries | ഉൾച്ചേർത്ത cargo-auditable crate പേരുകൾ, versions, sources, dependency edges | Built-in static ELF/PE/thin Mach-O പരിശോധന; [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, അനുമാനിച്ച arguments, mutability | Linux x64-ൽ local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers, തിരഞ്ഞെടുത്ത സാധാരണ table rows | Local database/WAL snapshot; native SQLite പരിമിതികളോടെ Node.js 24.x >=24.15 അല്ലെങ്കിൽ 26+ |
+1
View File
@@ -145,6 +145,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
| Instalacje desktopowe | Kandydaci środowisk uruchomieniowych, pliki źródłowe dowodów, komponenty i dalsze kroki | Ograniczona inspekcja katalogów i nagłówków;[Aplikacje desktopowe](../desktop-applications.md) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
| Instalações desktop | Ambientes candidatos, arquivos de evidência, componentes e próximos passos | Inspeção limitada de diretórios e cabeçalhos;[Aplicações desktop](../desktop-applications.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](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| Установленные настольные приложения | Возможные среды выполнения, файлы с доказательствами, компоненты и дальнейшие шаги | Ограниченная проверка каталогов и заголовков;[Настольные приложения](../desktop-applications.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
| ไดเรกทอรีติดตั้งแอปเดสก์ท็อป | รันไทม์ที่เป็นไปได้ ไฟล์หลักฐาน ตัวบ่งชี้ส่วนประกอบ และขั้นตอนถัดไป | ตรวจสอบไดเรกทอรีและส่วนหัวโดยจำกัดขอบเขต;[แอปเดสก์ท็อป](../desktop-applications.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-application-workflows.md) |
+1
View File
@@ -147,6 +147,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
+1
View File
@@ -145,6 +145,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
| Встановлені настільні програми | Можливі середовища виконання, файли доказів, компоненти та наступні кроки | Обмежена перевірка каталогів і заголовків;[Настільні програми](../desktop-applications.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
| Thư mục cài đặt ứng dụng máy tính | Các runtime có khả năng được dùng, tệp bằng chứng, dấu hiệu thành phần và bước tiếp theo | Kiểm tra thư mục và phần đầu tệp có giới hạn;[Ứng dụng máy tính](../desktop-applications.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](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
+5
View File
@@ -38,6 +38,7 @@ export const toolFamilyCatalog = (sources) => {
...sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS,
...sources.modelContracts.MODEL_TOOL_CONTRACTS,
...sources.rustContracts.RUST_TOOL_CONTRACTS,
...sources.erlangContracts.ERLANG_BEAM_TOOL_CONTRACTS,
...sources.wasmContracts.WASM_TOOL_CONTRACTS,
...sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
...sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
@@ -286,6 +287,10 @@ export const providerCatalog = (sources) => {
identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
},
{
identity: sources.erlangProvider.ERLANG_BEAM_PROVIDER_IDENTITY,
contracts: sources.erlangContracts.ERLANG_BEAM_TOOL_CONTRACTS,
},
{
identity: sources.electronFuseProvider.ELECTRON_FUSE_PROVIDER_IDENTITY,
contracts: fuseContracts,
+2
View File
@@ -77,6 +77,8 @@ const SOURCE_PATHS = {
il2cppProvider: "dist/il2cpp/Il2cppMetadataProvider.js",
rustContracts: "dist/contracts/rust/rustToolContracts.js",
rustProvider: "dist/rust/RustBinaryProvider.js",
erlangContracts: "dist/contracts/erlang/beamToolContracts.js",
erlangProvider: "dist/erlang/BeamProvider.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
desktopContracts: "dist/contracts/desktopApplicationToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
+1 -1
View File
@@ -17,7 +17,7 @@ export const SOURCE_TEST_PROJECTS = [
group: "source",
include: [
"src/*.test.ts",
"src/{android,apktool,jeb,adb,artifacts,browser,dotnet,electron,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,native,process,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
"src/{android,apktool,jeb,adb,artifacts,browser,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,native,process,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
],
},
{
+1
View File
@@ -417,6 +417,7 @@ const workflowAvailabilityFor = ({
name === "inspect_model_artifact" ||
name === "inspect_gpu_code_objects" ||
name === "inspect_rust_binary" ||
name === "inspect_erlang_beam" ||
name === "inspect_electron_fuses"
)
return { reason: "available", remediation: null };
+20
View File
@@ -0,0 +1,20 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
InspectErlangBeamInput,
ErlangBeam,
} from "../../domain/erlang/beam.js";
import type { Result } from "../../domain/result.js";
/** Stable read and BEAM IFF decoding, independent of a disassembler session. */
export interface BeamPort {
readonly identity: ProviderIdentity;
close?(): Promise<void>;
inspect(
input: InspectErlangBeamInput,
options?: ExecutionOptions,
): Promise<Result<ErlangBeam, AnalysisError>>;
}
@@ -0,0 +1,92 @@
import { expect, it } from "vitest";
import { BeamService } from "./BeamService.js";
import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../../erlang/BeamProvider.js";
import type { ErlangBeam } from "../../domain/erlang/beam.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
const identity = ERLANG_BEAM_PROVIDER_IDENTITY;
const report: ErlangBeam = {
artifact: {
path: "/artifacts/app.beam",
sha256: "a".repeat(64),
bytes: 64,
},
form: { location: { offset: 0, bytes: 12 }, size: 4 },
chunks: [],
atoms: null,
atom_encoding: null,
exports: null,
imports: null,
lambdas: null,
code: null,
module: null,
coverage: { status: "complete", examined_chunks: 0, atoms: 0 },
limitations: ["No atom table was present."],
};
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
const service = new BeamService({
identity,
inspect: () => {
calls += 1;
return Promise.resolve(ok(report));
},
});
const relative = await service.inspect({ path: "app.beam" });
const extra = await service.inspect({
path: report.artifact.path,
extra: true,
});
const nul = await service.inspect({ path: "/artifacts/app\0.beam" });
expect(relative.ok).toBe(false);
expect(extra.ok).toBe(false);
expect(nul.ok).toBe(false);
if (!relative.ok)
expect(relative.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [{ path: ["path"] }],
});
if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError);
if (!nul.ok)
expect(nul.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [{ path: ["path"] }],
});
expect(calls).toBe(0);
});
it("records the selected BEAM form", async () => {
const result = await new BeamService({
identity,
inspect: () => Promise.resolve(ok(report)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(true);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
confidence: "observed",
subject: { format: "file" },
normalized_result: report,
});
});
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const service = new BeamService({
identity,
inspect: () => {
controller.abort();
return Promise.resolve(ok(report));
},
});
const result = await service.inspect(
{ path: report.artifact.path },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
+111
View File
@@ -0,0 +1,111 @@
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { BeamPort } from "./BeamPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
inspectErlangBeamInputSchema,
erlangBeamSchema,
type ErlangBeam,
} from "../../domain/erlang/beam.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_erlang_beam";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class BeamService {
constructor(readonly provider: BeamPort) {}
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const input = inspectErlangBeamInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
const result = await this.provider.inspect(input.data, options);
if (!result.ok) return result;
const report = erlangBeamSchema.safeParse(result.value);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
!validReport(
report.data,
input.data.max_chunks,
input.data.max_atoms,
input.data.max_file_bytes,
)
)
return err(
new AnalysisOutputError(
OPERATION,
"BEAM provider returned malformed output or changed artifact identity.",
),
);
const value = report.data;
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
format: "file",
sha256: value.artifact.sha256,
},
this.provider.identity,
{
operation: OPERATION,
parameters: input.data,
result: value,
confidence: "observed",
limitations: value.limitations,
locations: [
{ kind: "artifact-path" as const, path: value.artifact.path },
{
kind: "file-offset-range" as const,
start: value.form.location.offset,
end: value.form.location.offset + value.form.location.bytes,
},
],
},
),
options?.signal,
);
}
}
const validReport = (
report: ErlangBeam,
maxChunks: number,
maxAtoms: number,
maxBytes: number,
): boolean => {
const inside = ({ offset, bytes }: { offset: number; bytes: number }) =>
offset <= report.artifact.bytes - bytes;
if (
report.artifact.bytes > maxBytes ||
report.coverage.examined_chunks > maxChunks ||
report.chunks.length > maxChunks ||
report.coverage.atoms > maxAtoms ||
(report.atoms !== null && report.atoms.length > maxAtoms)
)
return false;
if (!inside(report.form.location)) return false;
if (report.code !== null && !inside(report.code.instructions)) return false;
return report.chunks.every(
(chunk) => inside(chunk.location) && inside(chunk.payload.location),
);
};
+2
View File
@@ -11,6 +11,7 @@ import { registerGoCommands } from "./cli/goCommands.js";
import { registerModelCommands } from "./cli/modelCommands.js";
import { registerIl2cppCommands } from "./cli/il2cppCommands.js";
import { registerRustCommands } from "./cli/rustCommands.js";
import { registerErlangCommands } from "./cli/erlangCommands.js";
import { registerSqliteCommands } from "./cli/sqliteCommands.js";
import { registerAnalysisViewCommands } from "./cli/analysisViewCommands.js";
import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js";
@@ -138,6 +139,7 @@ export const createCli = (
registerModelCommands(cli, logger);
registerGpuCommands(cli, logger);
registerRustCommands(cli, logger);
registerErlangCommands(cli, logger);
registerSqliteCommands(cli, logger, environment);
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
registerManagedCommands(cli, logger, analysis.runProviderAnalysis);
+55
View File
@@ -0,0 +1,55 @@
import { z } from "incur";
import { createBeamService } from "../composition/erlang.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { inspectErlangBeamInputSchema } from "../domain/erlang/beam.js";
import { localPathStringSchema } from "../domain/localPath.js";
/** Resolve caller paths and inspect BEAM IFF tables through the shared workflow. */
export const registerErlangCommands = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectErlangBeam, {
description:
"Inspect Erlang BEAM atoms, exports, imports and the Code header without executing bytecode",
args: z.object({
path: localPathStringSchema.describe("Local BEAM file"),
}),
options: z.object({
maxFileBytes: inspectErlangBeamInputSchema.shape.max_file_bytes,
maxChunks: inspectErlangBeamInputSchema.shape.max_chunks,
maxAtoms: inspectErlangBeamInputSchema.shape.max_atoms,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const service = createBeamService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectErlangBeam,
async () =>
finishAnalysisCommand(
() =>
service.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_chunks: options.maxChunks,
max_atoms: options.maxAtoms,
},
{ signal },
),
service,
"rea-erlang-beam",
"inspect_erlang_beam",
signal,
),
);
}),
});
};
+1
View File
@@ -64,6 +64,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectModelArtifact: "inspect-model-artifact",
inspectGpuCodeObjects: "inspect-gpu-code-objects",
inspectRustBinary: "inspect-rust-binary",
inspectErlangBeam: "inspect-erlang-beam",
inspectPeResources: "inspect-pe-resources",
inspectAnalysisView: "inspect-analysis-view",
listFridaDevices: "list-frida-devices",
+6
View File
@@ -0,0 +1,6 @@
import { BeamService } from "../application/erlang/BeamService.js";
import { BeamProvider } from "../erlang/BeamProvider.js";
/** Compose bundled BEAM inspection without an Erlang installation or any child process. */
export const createBeamService = (): BeamService =>
new BeamService(new BeamProvider());
+26
View File
@@ -0,0 +1,26 @@
import {
erlangBeamSchema,
inspectErlangBeamInputSchema,
} from "../../domain/erlang/beam.js";
import type { ToolContract } from "../toolContractTypes.js";
import { toolContractMetadata } from "../toolEffects.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
/** Static BEAM IFF inspection is independent of active native-analysis providers. */
export const ERLANG_BEAM_TOOL_CONTRACTS = [
{
name: "inspect_erlang_beam",
...toolContractMetadata("inspect_erlang_beam"),
kind: "artifact-provider",
description:
"Inspect one explicit local Erlang BEAM IFF container without executing bytecode or installing Erlang. Returns FOR1 form bounds, chunk names, payload SHA-256, Atom/AtU8 names, export/import/lambda tables with resolved atom names when present, and the Code chunk instruction-set header. Missing optional tables stay null rather than empty-everywhere. Reads a stable regular file; malformed FOR1/BEAM headers, truncated chunks and exhausted budgets return no partial success. Portable on supported Node hosts; no BEAM emulator is required.",
inputSchema: inspectErlangBeamInputSchema,
outputSchema: evidenceResultOf(erlangBeamSchema),
examples: [
{
title: "Inspect BEAM atoms, exports and the Code header",
input: { path: "/artifacts/app.beam" },
},
],
},
] as const satisfies readonly ToolContract[];
+2
View File
@@ -8,6 +8,7 @@ import { GO_TOOL_CONTRACTS } from "./go/goToolContracts.js";
import { MODEL_TOOL_CONTRACTS } from "./model/modelToolContracts.js";
import { IL2CPP_TOOL_CONTRACTS } from "./il2cpp/il2cppToolContracts.js";
import { RUST_TOOL_CONTRACTS } from "./rust/rustToolContracts.js";
import { ERLANG_BEAM_TOOL_CONTRACTS } from "./erlang/beamToolContracts.js";
import { SQLITE_TOOL_CONTRACTS } from "./sqlite/sqliteToolContracts.js";
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
@@ -54,6 +55,7 @@ export const TOOL_CONTRACTS = [
...MODEL_TOOL_CONTRACTS,
...GPU_TOOL_CONTRACTS,
...RUST_TOOL_CONTRACTS,
...ERLANG_BEAM_TOOL_CONTRACTS,
...WASM_TOOL_CONTRACTS,
...SQLITE_TOOL_CONTRACTS,
...CAPTURE_TOOL_CONTRACTS,
+1
View File
@@ -31,6 +31,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
inspect_go_binary: "reads embedded metadata without executing the target",
inspect_rust_binary:
"reads an embedded dependency list without executing the target",
inspect_erlang_beam: "states that it does not execute bytecode",
inspect_sqlite_database:
"uses a trusted inspector worker without executing application code or caller SQL",
inspect_binary_layout:
+1
View File
@@ -131,6 +131,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
inspect_model_artifact: evidence,
inspect_gpu_code_objects: evidence,
inspect_rust_binary: evidence,
inspect_erlang_beam: evidence,
inspect_sqlite_database: effects({
mutatesSession: true,
writesFilesystem: true,
+1
View File
@@ -26,6 +26,7 @@ export class ArtifactOperationError extends AnalysisError {
| "inspect_artifact"
| "inspect_desktop_application"
| "inspect_pe_resources"
| "inspect_erlang_beam"
| "extract_artifact"
| "decode_interface_builder"
| "inspect_asset_catalog"
+76
View File
@@ -0,0 +1,76 @@
import { expect, it } from "vitest";
import { inspectErlangBeamInputSchema, erlangBeamSchema } from "./beam.js";
import { absoluteLocalPathStringSchema } from "../localPath.js";
const jsonSchema = (schema: {
readonly "~standard": {
readonly jsonSchema: {
readonly input: (options: {
readonly target: "draft-2020-12";
}) => unknown;
};
};
}): Record<string, unknown> =>
schema["~standard"].jsonSchema.input({
target: "draft-2020-12",
}) as Record<string, unknown>;
it("applies the shared absolute local-path policy to the public input", () => {
expect(
inspectErlangBeamInputSchema.safeParse({ path: "app.beam" }).success,
).toBe(false);
expect(inspectErlangBeamInputSchema.safeParse({ path: "" }).success).toBe(
false,
);
expect(
inspectErlangBeamInputSchema.safeParse({ path: "/artifacts/app\0.beam" })
.success,
).toBe(false);
expect(
inspectErlangBeamInputSchema.safeParse({ path: "/artifacts/app.beam" })
.success,
).toBe(true);
const shared = jsonSchema(absoluteLocalPathStringSchema);
const advertised = jsonSchema(inspectErlangBeamInputSchema);
const path = (
advertised.properties as Record<string, Record<string, unknown>>
).path;
const {
$schema: _schema,
description: _sharedDescription,
...sharedPolicy
} = shared;
const { description: _pathDescription, ...pathPolicy } = path ?? {};
expect(pathPolicy).toEqual(sharedPolicy);
expect(path?.description).toBe("Absolute filesystem path to a BEAM file");
});
it("rejects extra caller arguments and admits a BEAM form with no tables", () => {
expect(
inspectErlangBeamInputSchema.safeParse({
path: "/artifacts/app.beam",
extra: true,
}).success,
).toBe(false);
const report = erlangBeamSchema.parse({
artifact: {
path: "/artifacts/app.beam",
sha256: "a".repeat(64),
bytes: 16,
},
form: { location: { offset: 0, bytes: 12 }, size: 4 },
chunks: [],
atoms: null,
atom_encoding: null,
exports: null,
imports: null,
lambdas: null,
code: null,
module: null,
coverage: { status: "complete", examined_chunks: 0, atoms: 0 },
limitations: ["No atom table was present."],
});
expect(report.atoms).toBeNull();
expect(report.module).toBeNull();
});
+131
View File
@@ -0,0 +1,131 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { absoluteLocalPathStringSchema } from "../localPath.js";
import { publicInputStrictObject } from "../publicInput.js";
const uint = z.number().int().min(0).max(0xffffffff);
const range = z.strictObject({ offset: uint, bytes: uint });
/** Hard ceiling on the caller-selected input read budget. */
export const MAX_INSPECT_ERLANG_BEAM_FILE_BYTES = 536870912;
/** Hard ceiling on IFF chunks in one BEAM form. */
export const MAX_INSPECT_ERLANG_BEAM_CHUNKS = 256;
/** Hard ceiling on decoded atom-table entries. */
export const MAX_INSPECT_ERLANG_BEAM_ATOMS = 65536;
/** Hard ceiling on export, import, and lambda rows combined. */
export const MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS = 65536;
/** Complete bounded inspection of one Erlang BEAM IFF container. */
export const inspectErlangBeamInputSchema = publicInputStrictObject({
path: absoluteLocalPathStringSchema.describe(
"Absolute filesystem path to a BEAM file",
),
max_file_bytes: z
.number()
.int()
.min(16)
.max(MAX_INSPECT_ERLANG_BEAM_FILE_BYTES)
.default(67108864)
.describe("Maximum stable artifact bytes to read (default 64 MiB)"),
max_chunks: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_ERLANG_BEAM_CHUNKS)
.default(64)
.describe(
"Maximum IFF chunks to examine; exceeding the budget fails without partial success",
),
max_atoms: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_ERLANG_BEAM_ATOMS)
.default(4096)
.describe(
"Maximum atom-table entries to decode; exceeding the budget fails without partial success",
),
});
const atomSchema = z.strictObject({
index: uint,
name: z.string(),
});
const exportSchema = z.strictObject({
function_index: uint,
function: z.string().nullable(),
arity: uint,
label: uint,
});
const importSchema = z.strictObject({
module_index: uint,
module: z.string().nullable(),
function_index: uint,
function: z.string().nullable(),
arity: uint,
});
const lambdaSchema = z.strictObject({
function_index: uint,
function: z.string().nullable(),
arity: uint,
label: uint,
index: uint,
free_vars: uint,
old_unique: uint,
});
const chunkSchema = z.strictObject({
index: uint,
name: z.string().length(4),
location: range,
payload: z.strictObject({
location: range,
bytes: uint,
sha256: digestSchema,
}),
});
/** Observed BEAM IFF form; missing optional tables stay null rather than empty-everywhere. */
export const erlangBeamSchema = z.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256: digestSchema,
bytes: uint,
}),
form: z.strictObject({
location: range,
size: uint,
}),
chunks: z.array(chunkSchema),
atoms: z.array(atomSchema).nullable(),
atom_encoding: z.enum(["latin1", "utf8"]).nullable(),
exports: z.array(exportSchema).nullable(),
imports: z.array(importSchema).nullable(),
lambdas: z.array(lambdaSchema).nullable(),
code: z
.strictObject({
instruction_set: uint,
opcode_max: uint,
labels: uint,
functions: uint,
extra_header_bytes: uint,
instructions: range,
})
.nullable(),
module: z.string().nullable(),
coverage: z.strictObject({
status: z.literal("complete"),
examined_chunks: uint,
atoms: uint,
}),
limitations: z.array(z.string()),
});
export type ErlangBeam = z.infer<typeof erlangBeamSchema>;
export type InspectErlangBeamInput = z.infer<
typeof inspectErlangBeamInputSchema
>;
+16
View File
@@ -0,0 +1,16 @@
import { expect, it } from "vitest";
import { z } from "zod";
import { publicInputStrictObject } from "./publicInput.js";
it("rejects unknown keys on a public tool input", () => {
const schema = publicInputStrictObject({
path: z.string(),
block_size: z.number().default(4096),
});
expect(schema.parse({ path: "/file" })).toEqual({
path: "/file",
block_size: 4096,
});
expect(schema.safeParse({ path: "/file", extra: true }).success).toBe(false);
});
+9
View File
@@ -0,0 +1,9 @@
import { z } from "zod";
/**
* Closed object schema for a public CLI or MCP tool input.
*
* Unknown keys fail here so public input policy has one owner. Result and
* internal objects stay on their own constructors.
*/
export const publicInputStrictObject = z.strictObject;
+115
View File
@@ -0,0 +1,115 @@
const concat = (...parts: Buffer[]): Buffer => Buffer.concat(parts);
const u32 = (value: number): Buffer => {
const bytes = Buffer.alloc(4);
bytes.writeUInt32BE(value);
return bytes;
};
const pad4 = (bytes: Buffer): Buffer => {
const pad = (4 - (bytes.length % 4)) % 4;
return pad === 0 ? bytes : concat(bytes, Buffer.alloc(pad));
};
const chunk = (name: string, payload: Buffer): Buffer =>
concat(Buffer.from(name, "latin1"), u32(payload.length), pad4(payload));
const atoms = (names: readonly string[], utf8 = true): Buffer => {
const body = concat(
u32(names.length),
...names.map((name) => {
const encoded = Buffer.from(name, utf8 ? "utf8" : "latin1");
return concat(Buffer.from([encoded.length]), encoded);
}),
);
return chunk(utf8 ? "AtU8" : "Atom", body);
};
const table = (name: string, rows: readonly (readonly number[])[]): Buffer => {
const payload = concat(
u32(rows.length),
...rows.flatMap((row) => row.map(u32)),
);
return chunk(name, payload);
};
/** Hand-authored BEAM IFF with AtU8, Code, ExpT, ImpT and FunT chunks. */
export const erlangBeamFixture = (module = "beam_fixture"): Buffer => {
const atomNames = [
module,
"module_info",
"start",
"erlang",
"get_module_info",
];
const codeHeader = concat(
u32(16),
u32(0),
u32(169),
u32(4),
u32(2),
Buffer.from("code"),
);
const chunks = concat(
atoms(atomNames),
chunk("Code", codeHeader),
table("ExpT", [
[3, 0, 2],
[2, 0, 1],
[2, 1, 3],
]),
table("ImpT", [[4, 5, 1]]),
table("FunT", [[3, 1, 4, 0, 0, 42]]),
);
const form = concat(Buffer.from("BEAM", "latin1"), chunks);
return concat(Buffer.from("FOR1", "latin1"), u32(form.length), form);
};
/** BEAM form that declares table counts and does not store row bodies. */
export const erlangBeamDeclaredRowsFixture = (
counts: readonly (readonly [string, number])[],
): Buffer => {
const chunks = concat(
...counts.map(([name, count]) => chunk(name, u32(count))),
);
const form = concat(Buffer.from("BEAM", "latin1"), chunks);
return concat(Buffer.from("FOR1", "latin1"), u32(form.length), form);
};
/** BEAM form whose atom table exceeds a caller-selected atom budget. */
export const erlangBeamManyAtomsFixture = (count: number): Buffer => {
const names = Array.from(
{ length: count },
(_, index) => `a${String(index)}`,
);
const chunks = atoms(names);
const form = concat(Buffer.from("BEAM", "latin1"), chunks);
return concat(Buffer.from("FOR1", "latin1"), u32(form.length), form);
};
/**
* Compiler-produced BEAM: `erlc +deterministic` from Erlang/OTP 29 (compiler
* 10.0.2), which writes the OTP 28+ compact AtU8 layout. Source:
*
* -module(rea_beam_fixture).
* -export([hello/1]).
* hello(Name) -> lists:reverse(Name).
*/
export const erlangBeamOtp29Fixture = (): Buffer =>
Buffer.from(
[
"Rk9SMQAAAixCRUFNQXRVOAAAAE3////5CBByZWFfYmVhbV9maXh0dXJlUGhlbGxv",
"UGxpc3RzcHJldmVyc2WwbW9kdWxlX2luZm9gZXJsYW5n8GdldF9tb2R1bGVfaW5m",
"bwAAAENvZGUAAABFAAAAEAAAAAAAAAC1AAAABwAAAAMBEJkQAhIiEAEgThAAATCZ",
"AAISUgABQEASA04QEAFQmQACElIQAWBAAxNAEgNOICADAAAAU3RyVAAAAABJbXBU",
"AAAAKAAAAAMAAAADAAAABAAAAAEAAAAGAAAABwAAAAEAAAAGAAAABwAAAAJFeHBU",
"AAAAKAAAAAMAAAAFAAAAAQAAAAYAAAAFAAAAAAAAAAQAAAACAAAAAQAAAAJNZXRh",
"AAAALYNsAAAAAWgCdxBlbmFibGVkX2ZlYXR1cmVzbAAAAAF3Cm1heWJlX2V4cHJq",
"agAAAExvY1QAAAAEAAAAAEF0dHIAAAAng2wAAAABaAJ3A3ZzbmwAAAABbhAAVsr2",
"2jNBkgUGfacAt0GWcGpqAENJbmYAAAAbg2wAAAABaAJ3B3ZlcnNpb25rAAYxMC4w",
"LjJqAERiZ2kAAABCg2gDdw1kZWJ1Z19pbmZvX3YxdxFlcmxfYWJzdHJhY3RfY29k",
"ZWgCdwRub25lbAAAAAF3DWRldGVybWluaXN0aWNqAABMaW5lAAAAFQAAAAAAAAAA",
"AAAAAwAAAAEAAAAAMQAAAFR5cGUAAAAKAAAABAAAAAEf/wAA",
].join(""),
"base64",
);
+200
View File
@@ -0,0 +1,200 @@
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import {
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS,
inspectErlangBeamInputSchema,
} from "../domain/erlang/beam.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
erlangBeamDeclaredRowsFixture,
erlangBeamFixture,
erlangBeamManyAtomsFixture,
erlangBeamOtp29Fixture,
} from "./Beam.fixture.js";
import { parseErlangBeam } from "./BeamParser.js";
const defaults = inspectErlangBeamInputSchema.parse({
path: "/artifacts/app.beam",
});
const parse = (bytes: Buffer, input = defaults) =>
parseErlangBeam(
bytes,
{
path: input.path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
input,
);
it("reads atoms, exports, imports, lambdas and the Code header", () => {
const report = parse(erlangBeamFixture("demo"));
expect(report.atom_encoding).toBe("utf8");
expect(report.module).toBe("demo");
expect(report.atoms?.map((atom) => atom.name)).toEqual([
"demo",
"module_info",
"start",
"erlang",
"get_module_info",
]);
expect(report.exports).toEqual([
{ function_index: 3, function: "start", arity: 0, label: 2 },
{ function_index: 2, function: "module_info", arity: 0, label: 1 },
{ function_index: 2, function: "module_info", arity: 1, label: 3 },
]);
expect(report.imports).toEqual([
{
module_index: 4,
module: "erlang",
function_index: 5,
function: "get_module_info",
arity: 1,
},
]);
expect(report.lambdas?.[0]).toMatchObject({
function: "start",
arity: 1,
label: 4,
old_unique: 42,
});
expect(report.code).toMatchObject({
instruction_set: 0,
opcode_max: 169,
labels: 4,
functions: 2,
extra_header_bytes: 0,
instructions: { bytes: 4 },
});
expect(report.chunks.map((chunk) => chunk.name)).toEqual([
"AtU8",
"Code",
"ExpT",
"ImpT",
"FunT",
]);
});
// Expected values come from beam_lib:chunks/2 on the same file under OTP 29.
it("reads a compiler-produced OTP 29 BEAM with the compact atom table", () => {
const report = parse(erlangBeamOtp29Fixture());
expect(report.module).toBe("rea_beam_fixture");
expect(report.atoms?.map((atom) => atom.name)).toEqual([
"rea_beam_fixture",
"hello",
"lists",
"reverse",
"module_info",
"erlang",
"get_module_info",
]);
expect(report.exports).toEqual([
{ function_index: 5, function: "module_info", arity: 1, label: 6 },
{ function_index: 5, function: "module_info", arity: 0, label: 4 },
{ function_index: 2, function: "hello", arity: 1, label: 2 },
]);
expect(
report.imports?.map((row) => [row.module, row.function, row.arity]),
).toEqual([
["lists", "reverse", 1],
["erlang", "get_module_info", 1],
["erlang", "get_module_info", 2],
]);
expect(report.code).toMatchObject({
instruction_set: 0,
opcode_max: 181,
labels: 7,
functions: 3,
extra_header_bytes: 0,
instructions: { bytes: 49 },
});
});
it("fails a truncated FOR1 container without partial success", () => {
expect(() => parse(Buffer.from("FOR1"))).toThrow(ArtifactReaderFailure);
});
it("fails a FOR1 form that is not BEAM", () => {
const bytes = Buffer.alloc(16);
bytes.write("FOR1");
bytes.writeUInt32BE(8, 4);
bytes.write("WAVE", 8);
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
});
const singleChunkBeam = (name: string, payload: Buffer): Buffer => {
const header = Buffer.alloc(8);
header.write(name, 0, "latin1");
header.writeUInt32BE(payload.length, 4);
const pad = (4 - (payload.length % 4)) % 4;
const form = Buffer.concat([
Buffer.from("BEAM"),
header,
payload,
Buffer.alloc(pad),
]);
const size = Buffer.alloc(4);
size.writeUInt32BE(form.length);
return Buffer.concat([Buffer.from("FOR1"), size, form]);
};
it("rejects a short remainder inside the BEAM form", () => {
const bytes = Buffer.alloc(16);
bytes.write("FOR1");
bytes.writeUInt32BE(8, 4);
bytes.write("BEAM", 8);
expect(() => parse(bytes)).toThrow(/BEAM chunk header is truncated/);
});
it("rejects an AtU8 atom name that is not UTF-8", () => {
expect(() =>
parse(singleChunkBeam("AtU8", Buffer.from([0, 0, 0, 1, 1, 0xff]))),
).toThrow(/BEAM atom name is not valid UTF-8/);
});
it("keeps a latin1 atom byte unchanged", () => {
const report = parse(
singleChunkBeam("Atom", Buffer.from([0, 0, 0, 1, 1, 0xff])),
);
expect(report.atom_encoding).toBe("latin1");
expect(report.module).toBe("ÿ");
});
it("rejects combined table rows before allocating them", () => {
const below = Math.floor(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS / 2);
const overflow = MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS - below * 2 + 1;
expect(() =>
parse(
erlangBeamDeclaredRowsFixture([
["ExpT", below],
["ImpT", below],
["FunT", overflow],
]),
),
).toThrow(
expect.objectContaining({
reason: "limit",
boundary: "capacity",
message: expect.stringContaining("row budget"),
}),
);
const count = Buffer.alloc(4);
count.writeUInt32BE(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS + 1);
expect(() => parse(singleChunkBeam("ExpT", count))).toThrow(/row budget/);
count.writeUInt32BE(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS);
expect(() => parse(singleChunkBeam("ExpT", count))).toThrow(/truncated/);
});
it("fails when the atom budget is exhausted", () => {
const bytes = erlangBeamManyAtomsFixture(3);
expect(() =>
parse(
bytes,
inspectErlangBeamInputSchema.parse({
path: defaults.path,
max_atoms: 1,
}),
),
).toThrow(ArtifactReaderFailure);
});
+279
View File
@@ -0,0 +1,279 @@
import { isUtf8 } from "node:buffer";
import { createHash } from "node:crypto";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS,
type ErlangBeam,
type InspectErlangBeamInput,
} from "../domain/erlang/beam.js";
const fail = (message: string): never => {
throw new ArtifactReaderFailure("format", message);
};
const sha256 = (bytes: Buffer): string =>
createHash("sha256").update(bytes).digest("hex");
const LIMITATIONS = [
"This workflow does not execute BEAM bytecode, inflate LitT literals, or decode Attr/CInf external-term payloads.",
"Atom names are structural table entries; they do not authenticate the originating compiler or module checksum.",
] as const;
const requireRange = (bytes: Buffer, offset: number, length: number): void => {
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(length) ||
offset < 0 ||
length < 0 ||
offset > bytes.length - length
)
fail(
`BEAM range ${String(offset)} + ${String(length)} leaves the artifact.`,
);
};
const u32 = (bytes: Buffer, offset: number): number => {
requireRange(bytes, offset, 4);
return bytes.readUInt32BE(offset);
};
const chunkName = (bytes: Buffer, offset: number): string => {
requireRange(bytes, offset, 4);
return bytes.subarray(offset, offset + 4).toString("latin1");
};
const atomAt = (
atoms: readonly { index: number; name: string }[] | null,
index: number,
): string | null => {
if (atoms === null || index < 1) return null;
return atoms[index - 1]?.name ?? null;
};
/** Reads an atom length: one byte, or OTP 28+ compact `u` tag when compact. */
const atomLength = (
payload: Buffer,
at: number,
compact: boolean,
): [length: number, next: number] => {
if (at >= payload.length) fail("BEAM atom table is truncated.");
const first = payload[at]!;
if (!compact) return [first, at + 1];
if ((first & 0x07) !== 0) fail("BEAM atom length is not a u tag.");
if ((first & 0x08) === 0) return [first >> 4, at + 1];
if ((first & 0x10) !== 0) fail("BEAM atom length encoding is unsupported.");
if (at + 1 >= payload.length) fail("BEAM atom table is truncated.");
return [((first & 0xe0) << 3) | payload[at + 1]!, at + 2];
};
const parseAtoms = (
payload: Buffer,
maxAtoms: number,
encoding: "latin1" | "utf8",
): ErlangBeam["atoms"] => {
if (payload.length < 4) fail("BEAM atom table is truncated.");
// OTP 28+ writes a negated count and compact-encoded name lengths.
const declared = payload.readInt32BE(0);
const compact = declared < 0;
const count = Math.abs(declared);
if (count > maxAtoms)
throw new ArtifactReaderFailure(
"limit",
`BEAM atom table exceeds the ${String(maxAtoms)}-atom budget.`,
{ boundary: "capacity" },
);
const atoms: NonNullable<ErlangBeam["atoms"]> = [];
let at = 4;
for (let index = 1; index <= count; index += 1) {
const [length, next] = atomLength(payload, at, compact);
at = next;
requireRange(payload, at, length);
const nameBytes = payload.subarray(at, at + length);
if (encoding === "utf8" && !isUtf8(nameBytes))
fail("BEAM atom name is not valid UTF-8.");
atoms.push({
index,
name: nameBytes.toString(encoding),
});
at += length;
}
if (at > payload.length) fail("BEAM atom table is truncated.");
return atoms;
};
const parseRows = (
payload: Buffer,
width: number,
label: string,
): number[][] => {
if (payload.length < 4) fail(`BEAM ${label} table is truncated.`);
const count = u32(payload, 0);
const need = 4 + count * width * 4;
if (payload.length < need) fail(`BEAM ${label} table is truncated.`);
const rows: number[][] = [];
for (let index = 0; index < count; index += 1) {
const row: number[] = [];
const start = 4 + index * width * 4;
for (let field = 0; field < width; field += 1)
row.push(u32(payload, start + field * 4));
rows.push(row);
}
return rows;
};
const parseCode = (
payload: Buffer,
chunkOffset: number,
): NonNullable<ErlangBeam["code"]> => {
if (payload.length < 20) fail("BEAM Code chunk is truncated.");
const infoSize = u32(payload, 0);
if (infoSize < 16 || infoSize > payload.length - 4)
fail("BEAM Code header size is invalid.");
const instructionsOffset = 4 + infoSize;
return {
instruction_set: u32(payload, 4),
opcode_max: u32(payload, 8),
labels: u32(payload, 12),
functions: u32(payload, 16),
extra_header_bytes: infoSize - 16,
instructions: {
offset: chunkOffset + 8 + instructionsOffset,
bytes: payload.length - instructionsOffset,
},
};
};
/** Decode one FOR1/BEAM IFF container into bounded chunk and table facts. */
export const parseErlangBeam = (
bytes: Buffer,
artifact: ErlangBeam["artifact"],
input: InspectErlangBeamInput,
): ErlangBeam => {
requireRange(bytes, 0, 12);
if (bytes.subarray(0, 4).toString("latin1") !== "FOR1")
fail("Expected a FOR1 IFF container.");
const formSize = u32(bytes, 4);
if (formSize < 4) fail("BEAM form size is shorter than the BEAM tag.");
requireRange(bytes, 8, formSize);
if (bytes.subarray(8, 12).toString("latin1") !== "BEAM")
fail("Expected a BEAM form type.");
const formEnd = 8 + formSize;
if (
formEnd < bytes.length &&
bytes.subarray(formEnd).some((byte) => byte !== 0)
)
fail("BEAM form does not consume the selected file.");
const chunks: ErlangBeam["chunks"] = [];
const payloads = new Map<string, { payload: Buffer; offset: number }>();
let at = 12;
while (at < formEnd) {
if (formEnd - at < 8) fail("BEAM chunk header is truncated.");
if (chunks.length >= input.max_chunks)
throw new ArtifactReaderFailure(
"limit",
`BEAM form exceeds the ${String(input.max_chunks)}-chunk budget.`,
{ boundary: "capacity" },
);
const name = chunkName(bytes, at);
const size = u32(bytes, at + 4);
const payloadOffset = at + 8;
requireRange(bytes, payloadOffset, size);
const padded = payloadOffset + size + ((4 - (size % 4)) % 4);
if (padded > formEnd) fail("BEAM chunk padding exceeds the form.");
const padding = bytes.subarray(payloadOffset + size, padded);
if (padding.some((byte) => byte !== 0))
fail("BEAM chunk padding is non-zero.");
const payload = bytes.subarray(payloadOffset, payloadOffset + size);
chunks.push({
index: chunks.length,
name,
location: { offset: at, bytes: padded - at },
payload: {
location: { offset: payloadOffset, bytes: size },
bytes: size,
sha256: sha256(payload),
},
});
if (!payloads.has(name)) payloads.set(name, { payload, offset: at });
at = padded;
}
const atomChunk = payloads.get("AtU8") ?? payloads.get("Atom");
const atomEncoding = payloads.has("AtU8")
? "utf8"
: payloads.has("Atom")
? "latin1"
: null;
const atoms =
atomChunk === undefined || atomEncoding === null
? null
: parseAtoms(atomChunk.payload, input.max_atoms, atomEncoding);
const declaredRows = (name: string): number => {
const payload = payloads.get(name)?.payload;
return payload !== undefined && payload.length >= 4 ? u32(payload, 0) : 0;
};
if (
declaredRows("ExpT") + declaredRows("ImpT") + declaredRows("FunT") >
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS
)
throw new ArtifactReaderFailure(
"limit",
`BEAM tables exceed the ${String(
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS,
)}-row budget.`,
{ boundary: "capacity" },
);
const exports =
payloads.get("ExpT") === undefined
? null
: parseRows(payloads.get("ExpT")!.payload, 3, "export").map((row) => ({
function_index: row[0]!,
function: atomAt(atoms, row[0]!),
arity: row[1]!,
label: row[2]!,
}));
const imports =
payloads.get("ImpT") === undefined
? null
: parseRows(payloads.get("ImpT")!.payload, 3, "import").map((row) => ({
module_index: row[0]!,
module: atomAt(atoms, row[0]!),
function_index: row[1]!,
function: atomAt(atoms, row[1]!),
arity: row[2]!,
}));
const lambdas =
payloads.get("FunT") === undefined
? null
: parseRows(payloads.get("FunT")!.payload, 6, "lambda").map((row) => ({
function_index: row[0]!,
function: atomAt(atoms, row[0]!),
arity: row[1]!,
label: row[2]!,
index: row[3]!,
free_vars: row[4]!,
old_unique: row[5]!,
}));
const codeChunk = payloads.get("Code");
const code =
codeChunk === undefined
? null
: parseCode(codeChunk.payload, codeChunk.offset);
return {
artifact,
form: { location: { offset: 0, bytes: formEnd }, size: formSize },
chunks,
atoms,
atom_encoding: atomEncoding,
exports,
imports,
lambdas,
code,
module: atoms?.[0]?.name ?? null,
coverage: {
status: "complete",
examined_chunks: chunks.length,
atoms: atoms?.length ?? 0,
},
limitations: [...LIMITATIONS],
};
};
+211
View File
@@ -0,0 +1,211 @@
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
ArtifactFileCleanupFailure,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import type { BeamPort } from "../application/erlang/BeamPort.js";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import {
MAX_INSPECT_ERLANG_BEAM_ATOMS,
MAX_INSPECT_ERLANG_BEAM_CHUNKS,
MAX_INSPECT_ERLANG_BEAM_FILE_BYTES,
MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS,
type InspectErlangBeamInput,
} from "../domain/erlang/beam.js";
import { err, ok } from "../domain/result.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { parseErlangBeam } from "./BeamParser.js";
/** Bundled format reader identity; module names are separate observed facts. */
export const ERLANG_BEAM_PROVIDER_IDENTITY = {
id: "rea-erlang-beam",
name: "REA Erlang BEAM reader",
version: "1",
} as const;
const OPERATION = "inspect_erlang_beam";
/** Portable parser over one bounded, identity-checked regular file. */
export class BeamProvider implements BeamPort {
readonly identity = ERLANG_BEAM_PROVIDER_IDENTITY;
readonly #resources = new ArtifactResourceScope();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
ERLANG_BEAM_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectErlangBeamInput, options?: ExecutionOptions) {
try {
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
input.max_file_bytes,
options?.signal,
undefined,
this.#resources,
);
return ok(
parseErlangBeam(
snapshot.bytes,
{
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
input,
),
);
});
} catch (cause) {
return err(beamFailure(cause, input, options?.signal));
}
}
}
const inputBudgetRemediation = (
boundary: "file-size" | "capacity",
input: InspectErlangBeamInput,
message: string,
): string => {
if (boundary === "file-size")
return input.max_file_bytes >= MAX_INSPECT_ERLANG_BEAM_FILE_BYTES
? `The file exceeds the ${String(MAX_INSPECT_ERLANG_BEAM_FILE_BYTES)}-byte input ceiling this operation accepts; select a smaller artifact.`
: `Raise max_file_bytes above ${String(input.max_file_bytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`;
if (message.includes("atom"))
return input.max_atoms >= MAX_INSPECT_ERLANG_BEAM_ATOMS
? `The file exceeds the ${String(MAX_INSPECT_ERLANG_BEAM_ATOMS)}-atom ceiling this operation accepts; select a smaller artifact.`
: `Raise max_atoms above ${String(input.max_atoms)} or select a smaller artifact; the limit is the caller-selected atom budget.`;
if (message.includes("row budget"))
return `The file exceeds the ${String(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS)}-row ceiling this operation accepts; select a smaller artifact.`;
return input.max_chunks >= MAX_INSPECT_ERLANG_BEAM_CHUNKS
? `The file exceeds the ${String(MAX_INSPECT_ERLANG_BEAM_CHUNKS)}-chunk ceiling this operation accepts; select a smaller artifact.`
: `Raise max_chunks above ${String(input.max_chunks)} or select a smaller artifact; the limit is the caller-selected chunk budget.`;
};
const beamFailurePrimary = (cause: unknown, input: InspectErlangBeamInput) => {
if (cause instanceof ArtifactReaderFailure && cause.reason === "integrity")
return new AnalysisArtifactChangedError(
OPERATION,
input.path,
cause.message,
{
cause,
},
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof ArtifactReaderFailure) {
if (cause.reason === "limit")
return new AnalysisResourceConstraintError(
OPERATION,
cause.boundary ?? "memory",
cause.message,
{
max_file_bytes: input.max_file_bytes,
max_chunks: input.max_chunks,
max_atoms: input.max_atoms,
},
{
cause,
...(cause.boundary === undefined || cause.boundary === "memory"
? {}
: {
retryable: false,
remediationAction: inputBudgetRemediation(
cause.boundary,
input,
cause.message,
),
}),
},
);
if (cause.reason === "path" || cause.reason === "format")
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: cause.message,
},
]);
}
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
cause,
});
if (
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
String(cause.code),
)
)
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected BEAM file could not be read (${String(cause.code)}): ${input.path}.`,
},
]);
}
return new ArtifactOperationError(
OPERATION,
cause instanceof ArtifactReaderFailure ? cause.reason : "io",
undefined,
cause instanceof Error ? cause.message : "BEAM inspection failed.",
{ cause },
);
};
const beamFailure = (
cause: unknown,
input: InspectErlangBeamInput,
signal: AbortSignal | undefined,
) => {
const primary =
cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "failed"
? beamFailurePrimary(cause.fileCleanupFailure.outcome.cause, input)
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
: beamFailurePrimary(cause, input);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
ERLANG_BEAM_PROVIDER_IDENTITY.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ operation: OPERATION, cause },
);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
+16
View File
@@ -41,6 +41,9 @@ import { registerIl2cppTools } from "./registerIl2cppTools.js";
import { createRustBinaryService } from "../composition/rust.js";
import type { RustBinaryService } from "../application/rust/RustBinaryService.js";
import { registerRustTools } from "./registerRustTools.js";
import { createBeamService } from "../composition/erlang.js";
import type { BeamService } from "../application/erlang/BeamService.js";
import { registerErlangTools } from "./registerErlangTools.js";
import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js";
import { registerAnalysisViewTool } from "./registerAnalysisViewTool.js";
import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js";
@@ -179,6 +182,7 @@ export interface CreateServerOptions {
readonly goBinary?: GoBinaryService;
readonly il2cppMetadata?: Il2cppMetadataService;
readonly rustBinary?: RustBinaryService;
readonly erlangBeam?: BeamService;
readonly electronFuses?: ElectronFuseService;
readonly recordedCrash?: RecordedCrashService;
readonly firmwareAnalysis?: FirmwareAnalysisPort;
@@ -346,6 +350,7 @@ export const createServer = (
const gpuCodeObjects = createGpuCodeObjectsService();
const il2cppMetadata =
options.il2cppMetadata ?? createIl2cppMetadataService();
const erlangBeam = options.erlangBeam ?? createBeamService();
const peResources = createPeResourcesService();
const electronFuses = options.electronFuses ?? createElectronFuseService();
const webNetworkCapture =
@@ -447,6 +452,7 @@ export const createServer = (
il2cppMetadata.close(),
modelArtifact.close(),
gpuCodeObjects.close(),
erlangBeam.close(),
peResources.close(),
electronFuses.close(),
webNetworkCapture.close(),
@@ -510,6 +516,7 @@ export const createServer = (
goBinary,
rustBinary,
il2cppMetadata,
erlangBeam,
peResources,
electronFuses,
webNetworkCapture,
@@ -741,6 +748,7 @@ const registerConfiguredAnalysisTools = (
goBinary,
rustBinary,
il2cppMetadata,
erlangBeam,
peResources,
webNetworkCapture,
artifactResources,
@@ -814,6 +822,13 @@ const registerConfiguredAnalysisTools = (
withAdmittedAnalysis,
evidenceById,
);
registerErlangTools(
server,
erlangBeam,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWasmTools(
server,
wasmArtifact,
@@ -922,6 +937,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly goBinary: GoBinaryService;
readonly rustBinary: RustBinaryService;
readonly il2cppMetadata: Il2cppMetadataService;
readonly erlangBeam: BeamService;
readonly peResources: PeResourcesService;
readonly electronFuses: ElectronFuseService;
readonly webNetworkCapture: WebNetworkCaptureService;
+31
View File
@@ -0,0 +1,31 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { BeamService } from "../application/erlang/BeamService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind the exact BEAM contract to its shared workflow and owning session Evidence ledger. */
export const registerErlangTools = (
server: EvidenceMcpServer,
service: BeamService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_erlang_beam");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
};
+67
View File
@@ -0,0 +1,67 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { erlangBeamFixture } from "../../src/erlang/Beam.fixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest("reports BEAM exports through the public CLI", async ({ cli }) => {
const root = await createTestTempDirectory("rea-beam-cli-");
const path = join(root, "app.beam");
await writeFile(path, erlangBeamFixture("cli_mod"));
const result = await cli.run({
arguments: ["inspect-erlang-beam", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_erlang_beam").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.module).toBe("cli_mod");
expect(
evidence.normalized_result.exports?.some(
(entry) => entry.function === "start" && entry.arity === 0,
),
).toBe(true);
});
cliTest(
"resolves a relative BEAM path against the working directory",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-beam-cli-rel-");
await writeFile(join(root, "app.beam"), erlangBeamFixture());
const result = await cli.run({
arguments: ["inspect-erlang-beam", "app.beam", "--json"],
cwd: root,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_erlang_beam").outputSchema.parse(
result.json,
);
expect(evidence.subject?.local_path).toBe(join(root, "app.beam"));
},
);
cliTest("rejects an empty BEAM path before reading", async ({ cli }) => {
const root = await createTestTempDirectory("rea-beam-cli-path-");
const result = await cli.run({
arguments: ["inspect-erlang-beam", "", "--json"],
cwd: root,
});
expect(result.exitCode, result.stderr).not.toBe(0);
expect(result.stdout).not.toContain("sha256");
});
cliTest("reports a truncated BEAM as invalid input", async ({ cli }) => {
const root = await createTestTempDirectory("rea-beam-cli-bad-");
const path = join(root, "broken.beam");
await writeFile(path, Buffer.from("FOR1"));
const result = await cli.run({
arguments: ["inspect-erlang-beam", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_erlang_beam" },
});
});
@@ -218,6 +218,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
{ capture_path: nul, output_directory: "/tmp/exported-scripts" },
],
["inspect_pe_resources", { path: nul }],
["inspect_erlang_beam", { path: nul }],
["inspect_binary_layout", { path: nul }],
["inspect_recorded_crash", { path: nul }],
["inspect_wasm_artifact", { path: nul }],
@@ -0,0 +1,81 @@
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createBeamService } from "../../../src/composition/erlang.js";
import { MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS } from "../../../src/domain/erlang/beam.js";
import {
erlangBeamDeclaredRowsFixture,
erlangBeamFixture,
} from "../../../src/erlang/Beam.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("Erlang BEAM stable filesystem boundary", () => {
it("reports non-file and malformed BEAM selections as caller input errors", async () => {
const root = await createTestTempDirectory("rea-beam-files-");
const path = join(root, "app.beam");
const alias = join(root, "alias.beam");
const malformed = join(root, "malformed.beam");
await writeFile(path, erlangBeamFixture());
await writeFile(malformed, Buffer.from("not a beam"));
await symlink(path, alias);
const service = createBeamService();
for (const [candidate, reason] of [
[root, "invalid_format"],
[alias, "invalid_format"],
[malformed, "invalid_format"],
[join(root, "missing.beam"), "invalid_value"],
[join(path, "child.beam"), "invalid_value"],
] as const) {
const result = await service.inspect({ path: candidate });
if (result.ok) throw new Error("Expected file acquisition failure");
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
operation: "inspect_erlang_beam",
issues: [
{
path: ["path"],
reason,
message: expect.any(String),
},
],
});
}
});
it("returns typed resource and cancellation failures", async () => {
const root = await createTestTempDirectory("rea-beam-budget-");
const path = join(root, "app.beam");
await writeFile(path, erlangBeamFixture());
const service = createBeamService();
const tooSmall = await service.inspect({ path, max_file_bytes: 16 });
if (tooSmall.ok) throw new Error("Expected resource budget failure");
expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError");
const cancelled = await service.inspect(
{ path },
{ signal: AbortSignal.abort() },
);
if (cancelled.ok) throw new Error("Expected cancellation");
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
});
it("stops combined table rows at the shared budget", async () => {
const root = await createTestTempDirectory("rea-beam-rows-");
const path = join(root, "rows.beam");
const below = Math.floor(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS / 2);
await writeFile(
path,
erlangBeamDeclaredRowsFixture([
["ExpT", below],
["ImpT", below],
["FunT", MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS - below * 2 + 1],
]),
);
const result = await createBeamService().inspect({ path });
if (result.ok) throw new Error("Expected row budget failure");
expect(result.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
resource: "capacity",
remediationAction: `The file exceeds the ${String(MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS)}-row ceiling this operation accepts; select a smaller artifact.`,
});
});
});
+69
View File
@@ -0,0 +1,69 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { erlangBeamFixture } from "../../../src/erlang/Beam.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js";
const connect = async (mcp: TestMcp) => {
const session = createTestBinarySession(() => {
throw new Error("BEAM inspection must not start a deep provider");
});
const client = await mcp.connect(createServer({ kind: "session", session }));
return { client, session };
};
mcpTest(
"advertises inspect_erlang_beam and reads the module atom",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_erlang_beam",
);
expect(advertised?.annotations).toMatchObject({
readOnlyHint: false,
idempotentHint: true,
});
const root = await createTestTempDirectory("rea-beam-mcp-");
const path = join(root, "app.beam");
await writeFile(path, erlangBeamFixture("mcp_mod"));
const response = await client.callTool({
name: "inspect_erlang_beam",
arguments: { path },
});
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
const evidence = toolContract("inspect_erlang_beam").outputSchema.parse(
response.structuredContent,
);
expect(evidence.normalized_result.module).toBe("mcp_mod");
expect(evidence.normalized_result.atom_encoding).toBe("utf8");
} finally {
await session.close();
}
},
);
mcpTest(
"requires an absolute path for inspect_erlang_beam",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const response = await client.callTool({
name: "inspect_erlang_beam",
arguments: { path: "app.beam" },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_erlang_beam" },
});
} finally {
await session.close();
}
},
);