feat(erlang): inspect BEAM IFF chunks and tables (#4833)

* feat(erlang): inspect BEAM IFF chunks and tables

Replay the Erlang BEAM inspection workflow, including table bounds,
OTP 28+ atom layouts, the shared absolute-path policy, and empty-path
CLI coverage, without carrying GitHub workflow edits.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(erlang): use publicInputStrictObject for BEAM tool input

inspect_erlang_beam advertised a public input built with bare
z.strictObject, so later shared input-policy changes would miss it.
Result and internal objects stay on their own constructors.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-22 17:46:15 +08:00
1 parent 44827a5020
commit bfba99827e
55 files changed
+1699 -2

No files matched your search

@@ -233,6 +233,12 @@ explicit path or endpoint and do not need it.
build-id or ABI-tag fields. A missing table is observed absence of those
declarations, not proof the image never carried notes. See the
[ELF notes guide](https://github.com/morluto/rea/blob/main/docs/elf-notes.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
@@ -121,6 +121,16 @@ absence of those declarations, not proof the image never carried notes in
another container. See
[ELF note tables](https://github.com/morluto/rea/blob/main/docs/elf-notes.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
@@ -193,6 +193,7 @@ Additional tools and host support depend on the target:
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](docs/pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](docs/elf-notes.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
@@ -34,6 +34,7 @@ export default defineConfig({
{ text: "Rust dependencies", link: "/rust-binaries" },
{ text: "PE certificates", link: "/pe-certificates" },
{ text: "ELF notes", link: "/elf-notes" },
{ text: "Erlang BEAM", link: "/erlang-beam" },
{
text: "JavaScript and Electron",
link: "/javascript-artifact-reconstruction",
+10
View File
@@ -177,6 +177,16 @@ The workflow reports note owners, types, descriptor hashes, and GNU build-id
or ABI-tag fields when those descriptors match. A missing note table is
observed absence of those declarations. See [ELF note tables](elf-notes.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), [PE certificates](pe-certificates.md), [ELF notes](elf-notes.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), [PE certificates](pe-certificates.md), [ELF notes](elf-notes.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
@@ -147,6 +147,7 @@ npx rea-agents@latest setup
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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 benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -151,6 +151,7 @@ npx rea-agents@latest setup
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -147,6 +147,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -139,6 +139,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -151,6 +151,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -154,6 +154,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -151,6 +151,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -147,6 +147,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -155,6 +155,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -153,6 +153,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.19), 24.x (>=24.11), അല്ലെ
| Rust binaries | ഉൾച്ചേർത്ത cargo-auditable crate പേരുകൾ, versions, sources, dependency edges | Built-in static ELF/PE/thin Mach-O പരിശോധന; [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -147,6 +147,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -147,6 +147,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -147,6 +147,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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) หรือ 26
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -149,6 +149,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| 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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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
@@ -147,6 +147,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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 yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.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) |
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.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,
@@ -316,6 +317,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,config,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,config,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
@@ -418,6 +418,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";
@@ -139,6 +140,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
@@ -65,6 +65,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",
inspectPeCertificates: "inspect-pe-certificates",
inspectNativeExports: "inspect-native-exports",
+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
@@ -35,6 +35,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
@@ -28,6 +28,7 @@ export class ArtifactOperationError extends AnalysisError {
| "inspect_pe_resources"
| "inspect_pe_certificates"
| "inspect_elf_notes"
| "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
>;
+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
@@ -52,6 +52,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";
@@ -193,6 +196,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;
@@ -356,6 +360,7 @@ export const createServer = (
const desktopApplication = createDesktopApplicationService();
const goBinary = options.goBinary ?? createGoBinaryService();
const rustBinary = options.rustBinary ?? createRustBinaryService();
const erlangBeam = options.erlangBeam ?? createBeamService();
const modelArtifact = createModelArtifactService();
const gpuCodeObjects = createGpuCodeObjectsService();
const il2cppMetadata =
@@ -461,6 +466,7 @@ export const createServer = (
desktopApplication.close(),
goBinary.close(),
rustBinary.close(),
erlangBeam.close(),
il2cppMetadata.close(),
modelArtifact.close(),
gpuCodeObjects.close(),
@@ -529,6 +535,7 @@ export const createServer = (
artifactResources,
goBinary,
rustBinary,
erlangBeam,
il2cppMetadata,
peResources,
nativeExports,
@@ -763,6 +770,7 @@ const registerConfiguredAnalysisTools = (
withAdmittedAnalysis,
goBinary,
rustBinary,
erlangBeam,
il2cppMetadata,
peResources,
nativeExports,
@@ -862,6 +870,13 @@ const registerConfiguredAnalysisTools = (
withAdmittedAnalysis,
evidenceById,
);
registerErlangTools(
server,
erlangBeam,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWasmTools(
server,
wasmArtifact,
@@ -969,6 +984,7 @@ const createSessionRecorders = (
interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly goBinary: GoBinaryService;
readonly rustBinary: RustBinaryService;
readonly erlangBeam: BeamService;
readonly il2cppMetadata: Il2cppMetadataService;
readonly peResources: PeResourcesService;
readonly nativeExports: NativeExportsService;
+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" },
});
});
@@ -221,6 +221,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
["inspect_pe_certificates", { path: nul }],
["inspect_native_exports", { path: nul }],
["inspect_elf_notes", { 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();
}
},
);