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:
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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 한도 적용) |
|
||||
|
||||
@@ -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+ |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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+ |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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),
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
@@ -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[];
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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",
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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],
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
};
|
||||
@@ -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.`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user