feat(elf): inspect PT_NOTE and SHT_NOTE tables (#4837)

Inspect selected ELF32/ELF64 PT_NOTE and SHT_NOTE declarations without executing
the artifact, retaining independent table identities, source ranges, descriptor
digests, GNU build-id/ABI fields and explicit coverage limitations.

Integrate mvanhorn's implementation and hardening with current main while
preserving byte-entropy registration and the Windows curated regression suite.
The maintainer follow-up uses ordinary merges/pushes, not history rewriting.

Executed validation: 96 focused/shared/docs tests, complete static/docs checks,
eleven real CLI/MCP pairs including GCC ELF32 and Clang big-endian PowerPC64,
and two actual Codex task traces checked against their Evidence. Full costs,
grader correction and verification limits are recorded in PR #4837.

Refs #6262.
This commit is contained in:
Matt Van Horn
2026-10-22 00:02:23 -07:00
committed by GitHub
parent c8ed328538
commit 2edf3b23c1
54 changed files with 2278 additions and 29 deletions
@@ -219,6 +219,12 @@ explicit path or endpoint and do not need it.
delivery requires `--evidence_output`.
See the
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
- ELF note tables: `inspect_elf_notes` with the explicit local ELF32/ELF64
file. This built-in target-free inspection requires no disassembler. Preserve
PT_NOTE and SHT_NOTE ranges, owners, types, descriptor SHA-256, and GNU
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).
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
@@ -96,6 +96,17 @@ dependencies. An omitted package kind stays null. A rustc commit hash is
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
## ELF note tables
Use `inspect_elf_notes` for an explicit local ELF32/ELF64 image when the
question concerns PT_NOTE/SHT_NOTE owners, GNU build-id, or GNU ABI-tag
fields. No `open_binary` or target execution is required. The CLI equivalent
is `inspect-elf-notes PATH`. Preserve artifact SHA-256, table file ranges,
descriptor hashes, and original owner spellings. A missing table is observed
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).
## SQLite snapshots
Use `inspect_sqlite_database` with an explicit local database snapshot for its
+1
View File
@@ -191,6 +191,7 @@ Additional tools and host support depend on the target:
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
| 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) |
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
+1
View File
@@ -32,6 +32,7 @@ export default defineConfig({
{ text: "IL2CPP metadata", link: "/il2cpp-metadata" },
{ text: "Model artifacts", link: "/model-artifacts" },
{ text: "Rust dependencies", link: "/rust-binaries" },
{ text: "ELF notes", link: "/elf-notes" },
{
text: "JavaScript and Electron",
link: "/javascript-artifact-reconstruction",
+10
View File
@@ -155,6 +155,16 @@ dependency edges, and the section's file range. A missing `.dep-v0` section is
null metadata, not proof that the binary has no dependencies. See
[Rust binaries](rust-binaries.md).
Inspect ELF PT_NOTE and SHT_NOTE tables without executing the image:
```bash
rea inspect-elf-notes /absolute/path/to/program --json
```
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).
Configure [Hopper or Ghidra](installation.md#hopper), the
[IDA adapter](ida-provider.md), or [Kuna](kuna-provider.md) before analyzing a
native target. Substitute
+53
View File
@@ -0,0 +1,53 @@
# ELF note tables
`inspect_elf_notes` / `inspect-elf-notes` answers whether one selected local
ELF32/ELF64 image declares `PT_NOTE` program headers or `SHT_NOTE` sections,
which note owners and types those tables store, and which GNU build-id or
ABI-tag fields those notes carry. It reads the file without executing it and
does not require a disassembler, debug package, or package catalog.
```bash
rea inspect-elf-notes /absolute/path/to/program --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_elf_notes",
"arguments": { "path": "/artifacts/program" }
}
```
## Observations
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`tables` lists each declared note program header or section, including the
section name when a string table is present. Each `entries[]` item reports owner,
type, descriptor SHA-256, and a hex preview for descriptors of 64 bytes or less.
For GNU notes, `gnu.build_id_hex`, `gnu.abi_tag`, or `gnu.gold_version` are
filled when the descriptor matches that type. Other owners keep `gnu` null.
`PT_NOTE` and `SHT_NOTE` are examined independently. The same file range may
appear twice when both tables declare it.
A missing table list is observed absence of those declarations. It is not proof
the image never carried notes in another container or overlay.
## Limits and failures
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
32 note entries are examined by default across every table. Retained note
names, section names, and GNU descriptor text share that file's byte budget,
and repeated table ranges count again. Each descriptor range is hashed once,
and each GNU version's trailing NUL run is scanned once; neither walk may
exceed that byte ceiling. Malformed ELF headers, truncated header
tables, declared note tables that extend past the file, and truncated
note entries fail without partial success. ELF extended
numbering (`PN_XNUM`, including a section count stored when `e_shnum` is 0) is
unsupported.
This workflow does not execute the image or verify GNU build-id uniqueness
against package catalogs.
+1 -1
View File
@@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Task | Guides |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [ELF notes](elf-notes.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
+1
View File
@@ -145,6 +145,7 @@ npx rea-agents@latest setup
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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 الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
| تثبيتات سطح المكتب | عائلات وقت التشغيل، والملفات الداعمة، وعلامات المكونات، والخطوات التالية | فحص محدود للملفات والترويسات;[تطبيقات سطح المكتب](../desktop-applications.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
| Desktop-Installationen | Laufzeitkandidaten, Quelldateien, Komponenten und nächste Schritte | Begrenzte Verzeichnis- und Headerprüfung;[Desktop-Anwendungen](../desktop-applications.md) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ npx rea-agents@latest setup
| Δομή ELF χωρίς εκτέλεση | Ενότητες, τμήματα, αρχικά σύμβολα/εγγραφές μετεγκατάστασης και ενδείξεις πιθανών μέτρων προστασίας από στατική ανάλυση | pwntools που παρέχει ο χρήστης σε Linux x64· [διαγνωστικά δυαδικών αρχείων](../binary-diagnostics.md) |
| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
| 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) |
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
+1
View File
@@ -145,6 +145,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
| Instalaciones de escritorio | Posibles entornos de ejecución, archivos de evidencia, componentes y siguientes pasos | Inspección acotada de directorios y cabeceras;[Aplicaciones de escritorio](../desktop-applications.md) |
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](../javascript-application-workflows.md) |
+1
View File
@@ -137,6 +137,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np
| ساختار ELF آفلاین | بخش‌ها، سگمنت‌ها، نمادها و relocationهای اصلی و گزینه‌های احتمالی کاهش آسیب‌پذیری ایستا | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](../recorded-crashes.md) |
| نصب‌های دسکتاپ | نامزدهای زمان اجرا، فایل‌های شاهد، نشانه‌های مؤلفه‌ها و مراحل بعدی | بررسی محدود پوشه‌ها و سرآیندها;[برنامه‌های دسکتاپ](../desktop-applications.md) |
| JavaScript / Electron | ماژول‌ها، importها، source mapها، مسیرها، IPC و ارتباط افزونه‌های بومی | Node.js و npm؛ [تحلیل برنامه](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
| Installations de bureau | Environnements candidats, fichiers justificatifs, composants et étapes suivantes | Inspection bornée des répertoires et en-têtes;[Applications de bureau](../desktop-applications.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](../javascript-application-workflows.md) |
+1
View File
@@ -152,6 +152,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
| Instalasi desktop | Kandidat runtime, berkas pendukung, penanda komponen, dan langkah berikutnya | Pemeriksaan direktori dan header terbatas;[Aplikasi desktop](../desktop-applications.md) |
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
| Installazioni desktop | Runtime candidati, file di supporto, componenti e passi successivi | Ispezione limitata di directory e intestazioni;[Applicazioni desktop](../desktop-applications.md) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| デスクトップのインストール先 | ランタイム候補、根拠ファイル、コンポーネントの目印、次の調査手順 | 上限付きのディレクトリ・ヘッダー検査;[デスクトップアプリ](../desktop-applications.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](../javascript-application-workflows.md) |
+1
View File
@@ -153,6 +153,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
| 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) |
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
+1
View File
@@ -151,6 +151,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.19), 24.x (>=24.11), അല്ലെ
| Offline ELF layout | Sections, segments, യഥാർത്ഥ symbols/relocations, static mitigation candidates | Linux x64-ൽ caller നൽകുന്ന pwntools; [binary diagnostics](../binary-diagnostics.md) |
| Go binaries | ഉൾച്ചേർത്ത compiler version, modules, replacements, build settings | Built-in static ELF/PE/thin Mach-O പരിശോധന; Go installation ആവശ്യമില്ല |
| Rust binaries | ഉൾച്ചേർത്ത cargo-auditable crate പേരുകൾ, versions, sources, dependency edges | Built-in static ELF/PE/thin Mach-O പരിശോധന; [Rust binaries](../rust-binaries.md) |
| 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) |
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, അനുമാനിച്ച arguments, mutability | Linux x64-ൽ local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers, തിരഞ്ഞെടുത്ത സാധാരണ table rows | Local database/WAL snapshot; native SQLite പരിമിതികളോടെ Node.js 24.x >=24.15 അല്ലെങ്കിൽ 26+ |
+1
View File
@@ -145,6 +145,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
| Instalacje desktopowe | Kandydaci środowisk uruchomieniowych, pliki źródłowe dowodów, komponenty i dalsze kroki | Ograniczona inspekcja katalogów i nagłówków;[Aplikacje desktopowe](../desktop-applications.md) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
| Instalações desktop | Ambientes candidatos, arquivos de evidência, componentes e próximos passos | Inspeção limitada de diretórios e cabeçalhos;[Aplicações desktop](../desktop-applications.md) |
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| Установленные настольные приложения | Возможные среды выполнения, файлы с доказательствами, компоненты и дальнейшие шаги | Ограниченная проверка каталогов и заголовков;[Настольные приложения](../desktop-applications.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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 ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
| ไดเรกทอรีติดตั้งแอปเดสก์ท็อป | รันไทม์ที่เป็นไปได้ ไฟล์หลักฐาน ตัวบ่งชี้ส่วนประกอบ และขั้นตอนถัดไป | ตรวจสอบไดเรกทอรีและส่วนหัวโดยจำกัดขอบเขต;[แอปเดสก์ท็อป](../desktop-applications.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-application-workflows.md) |
+1
View File
@@ -147,6 +147,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) |
| 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) |
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
+1
View File
@@ -145,6 +145,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
| Встановлені настільні програми | Можливі середовища виконання, файли доказів, компоненти та наступні кроки | Обмежена перевірка каталогів і заголовків;[Настільні програми](../desktop-applications.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-application-workflows.md) |
+1
View File
@@ -145,6 +145,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
| Thư mục cài đặt ứng dụng máy tính | Các runtime có khả năng được dùng, tệp bằng chứng, dấu hiệu thành phần và bước tiếp theo | Kiểm tra thư mục và phần đầu tệp có giới hạn;[Ứng dụng máy tính](../desktop-applications.md) |
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](../javascript-application-workflows.md) |
+1
View File
@@ -149,6 +149,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 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) |
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
+27 -26
View File
@@ -141,32 +141,33 @@ npx rea-agents@latest setup
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
| 目标 | REA 返回的内容 | 要求与指南 |
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
| 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) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
| 目标 | REA 返回的内容 | 要求与指南 |
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
| 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) |
| 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) |
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
+7
View File
@@ -250,6 +250,13 @@ export const providerCatalog = (sources) => {
({ name }) => name === "inspect_pe_resources",
),
},
{
identity: sources.artifactProviders.ELF_NOTES_PROVIDER,
contracts:
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
({ name }) => name === "inspect_elf_notes",
),
},
{
identity: sources.artifactProviders.BYTE_ENTROPY_PROVIDER,
contracts:
+2
View File
@@ -500,6 +500,8 @@ const workflowAvailabilityFor = ({
};
if (name === "inspect_pe_resources")
return { reason: "available", remediation: null };
if (name === "inspect_elf_notes")
return { reason: "available", remediation: null };
if (name === "inspect_evm_interface") {
if (policy.evmInterfaceEnabled !== true)
return {
@@ -48,6 +48,13 @@ export const PE_RESOURCES_PROVIDER = Object.freeze({
version: "1",
} as const);
/** Provider identity for portable, execution-free ELF note-table decoding. */
export const ELF_NOTES_PROVIDER = Object.freeze({
id: "rea-elf-notes",
name: "REA ELF note parser",
version: "1",
} as const);
/** Provider identity for execution-free Shannon byte-entropy profiling. */
export const BYTE_ENTROPY_PROVIDER = Object.freeze({
id: "byte-entropy",
@@ -0,0 +1,20 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
InspectElfNotesInput,
ElfNotes,
} from "../../domain/native/elfNotes.js";
import type { Result } from "../../domain/result.js";
/** Stable read and ELF note-table decoding, independent of a disassembler session. */
export interface ElfNotesPort {
readonly identity: ProviderIdentity;
close?(): Promise<void>;
inspect(
input: InspectElfNotesInput,
options?: ExecutionOptions,
): Promise<Result<ElfNotes, AnalysisError>>;
}
@@ -0,0 +1,79 @@
import { expect, it } from "vitest";
import { ElfNotesService } from "./ElfNotesService.js";
import { ELF_NOTES_PROVIDER } from "../InvestigationProviders.js";
import type { ElfNotes } from "../../domain/native/elfNotes.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
const identity = ELF_NOTES_PROVIDER;
const report: ElfNotes = {
artifact: {
path: "/artifacts/app",
sha256: "a".repeat(64),
bytes: 512,
},
format: "elf64",
byte_order: "little",
machine: 62,
type: 3,
type_name: "shared",
tables: [],
entries: [],
coverage: { status: "complete", examined_tables: 0, examined_entries: 0 },
limitations: ["No PT_NOTE program header or SHT_NOTE section was declared."],
};
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
const service = new ElfNotesService({
identity,
inspect: () => {
calls += 1;
return Promise.resolve(ok(report));
},
});
const relative = await service.inspect({ path: "app" });
const extra = await service.inspect({
path: report.artifact.path,
extra: true,
});
expect(relative.ok).toBe(false);
expect(extra.ok).toBe(false);
if (!relative.ok) expect(relative.error).toBeInstanceOf(AnalysisInputError);
if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError);
expect(calls).toBe(0);
});
it("records the selected ELF and an absent note table", async () => {
const result = await new ElfNotesService({
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: "elf", architecture: "x86_64" },
normalized_result: report,
});
});
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const service = new ElfNotesService({
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,127 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { ElfNotesPort } from "./ElfNotesPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
inspectElfNotesInputSchema,
elfNotesSchema,
type ElfNotes,
} from "../../domain/native/elfNotes.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
import { elfArchitecture } from "../../domain/elfFixedHeader.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_elf_notes";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class ElfNotesService {
constructor(readonly provider: ElfNotesPort) {}
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 = inspectElfNotesInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const result = await this.provider.inspect(input.data, options);
if (!result.ok) return result;
const report = elfNotesSchema.safeParse(result.value);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
!validReport(
report.data,
input.data.max_entries,
input.data.max_file_bytes,
)
)
return err(
new AnalysisOutputError(
OPERATION,
"ELF note provider returned malformed output or changed artifact identity.",
),
);
const value = report.data;
const architecture = BINARY_ARCHITECTURES.find(
(supported) => supported === elfArchitecture(value.machine),
);
const locations = [
{ kind: "artifact-path" as const, path: value.artifact.path },
...value.tables.map((table) => ({
kind: "file-offset-range" as const,
start: table.location.offset,
end: table.location.offset + table.location.bytes,
})),
];
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
format: "elf",
sha256: value.artifact.sha256,
...(architecture === undefined ? {} : { architecture }),
},
this.provider.identity,
{
operation: OPERATION,
parameters: input.data,
result: value,
confidence: "observed",
limitations: value.limitations,
locations,
},
),
options?.signal,
);
}
}
const validReport = (
report: ElfNotes,
maxEntries: 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_entries > maxEntries ||
report.entries.length > maxEntries
)
return false;
return (
report.tables.every((table) => inside(table.location)) &&
report.entries.every(
(entry) => inside(entry.location) && inside(entry.descriptor.location),
)
);
};
+35
View File
@@ -4,6 +4,7 @@ import {
createByteEntropyService,
createRecordedCrashService,
createBinaryLayoutService,
createElfNotesService,
createPeResourcesService,
} from "../composition/binaryDiagnostics.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
@@ -14,6 +15,7 @@ import { withCommandCancellation } from "./commandCancellation.js";
import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js";
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
@@ -24,6 +26,39 @@ export const registerBinaryDiagnosticsCommands = (
logger: Logger,
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
cli.command(CLI_COMMANDS.inspectElfNotes, {
description:
"Inspect ELF PT_NOTE and SHT_NOTE identities without executing the image",
args: z.object({ path: z.string().describe("Local ELF32/ELF64 image") }),
options: z.object({
maxFileBytes: inspectElfNotesInputSchema.shape.max_file_bytes,
maxEntries: inspectElfNotesInputSchema.shape.max_entries,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const elf = createElfNotesService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectElfNotes,
async () =>
finishAnalysisCommand(
() =>
elf.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_entries: options.maxEntries,
},
{ signal },
),
elf,
"rea-elf-notes",
"inspect_elf_notes",
signal,
),
);
}),
});
const entropy = createByteEntropyService();
cli.command(CLI_COMMANDS.inspectByteEntropy, {
description:
+1
View File
@@ -66,6 +66,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectGpuCodeObjects: "inspect-gpu-code-objects",
inspectRustBinary: "inspect-rust-binary",
inspectPeResources: "inspect-pe-resources",
inspectElfNotes: "inspect-elf-notes",
inspectAnalysisView: "inspect-analysis-view",
listFridaDevices: "list-frida-devices",
listFridaProcesses: "list-frida-processes",
+6
View File
@@ -5,12 +5,18 @@ import { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayo
import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvider.js";
import { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js";
import { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import { ElfNotesProvider } from "../native/elf/ElfNotesProvider.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
/** Portable PE inspection with no engine, installation or target acquisition. */
export const createPeResourcesService = (): PeResourcesService =>
new PeResourcesService(new PeResourcesProvider());
/** Portable ELF note-table inspection with no engine or package catalog. */
export const createElfNotesService = (): ElfNotesService =>
new ElfNotesService(new ElfNotesProvider());
/** Compose the offline adapter without import-time I/O or dependency acquisition. */
export const createBinaryLayoutService = (
environment: Readonly<NodeJS.ProcessEnv> = process.env,
@@ -21,6 +21,10 @@ import {
inspectPeResourcesInputSchema,
peResourcesSchema,
} from "../../domain/native/peResources.js";
import {
inspectElfNotesInputSchema,
elfNotesSchema,
} from "../../domain/native/elfNotes.js";
/** Caller-selected delivery of completed binary layout Evidence. */
export const inspectBinaryLayoutRequestSchema =
@@ -36,6 +40,21 @@ export const inspectByteEntropyRequestSchema = publicInputStrictObject(
/** Offline binary diagnostics are independent of macOS process/UI providers. */
export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
{
name: "inspect_elf_notes",
...toolContractMetadata("inspect_elf_notes"),
kind: "native-provider",
description:
"Inspect one explicit local ELF32/ELF64 image's PT_NOTE program headers and SHT_NOTE sections without executing it or opening a disassembler. Returns note-table file ranges, owners, types, descriptor SHA-256 and hex previews, plus GNU build-id, ABI-tag and gold-version fields when those descriptors match. A missing table is observed absence of those declarations, not proof the image never carried notes in another container. Reads a stable regular file; malformed ELF headers, truncated notes and exhausted budgets return no partial success. Portable on supported Node hosts; no debug package or catalog is required.",
inputSchema: inspectElfNotesInputSchema,
outputSchema: evidenceResultOf(elfNotesSchema),
examples: [
{
title: "Inspect ELF GNU build-id and ABI-tag notes",
input: { path: "/artifacts/program" },
},
],
},
{
name: "inspect_byte_entropy",
...toolContractMetadata("inspect_byte_entropy"),
+1
View File
@@ -27,6 +27,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
resolve_native_call_targets: "states that it does not execute code",
inspect_byte_entropy: "states that it profiles the file without execution",
inspect_pe_resources: "states that it does not execute the image",
inspect_elf_notes: "states that it does not execute the image",
inspect_desktop_application:
"states that directory triage performs no target execution",
inspect_go_binary: "reads embedded metadata without executing the target",
+1
View File
@@ -253,6 +253,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
}),
inspect_byte_entropy: evidence,
inspect_pe_resources: evidence,
inspect_elf_notes: evidence,
inspect_analysis_view: evidence,
inspect_firmware_regions: effects({
mutatesSession: true,
+1
View File
@@ -26,6 +26,7 @@ export class ArtifactOperationError extends AnalysisError {
| "inspect_artifact"
| "inspect_desktop_application"
| "inspect_pe_resources"
| "inspect_elf_notes"
| "extract_artifact"
| "decode_interface_builder"
| "inspect_asset_catalog"
+1 -1
View File
@@ -70,7 +70,7 @@ export const readElfFixedHeader = (
});
};
const elfArchitecture = (
export const elfArchitecture = (
machine: number,
): Exclude<BinaryArchitecture, RawOnlyArchitecture> | undefined => {
switch (machine) {
+47
View File
@@ -0,0 +1,47 @@
import { expect, it } from "vitest";
import {
elfObjectTypeName,
gnuNoteTypeName,
inspectElfNotesInputSchema,
elfNotesSchema,
} from "./elfNotes.js";
it("maps ELF object and GNU note types without inventing unknown names", () => {
expect(elfObjectTypeName(1)).toBe("relocatable");
expect(elfObjectTypeName(2)).toBe("executable");
expect(elfObjectTypeName(3)).toBe("shared");
expect(elfObjectTypeName(4)).toBe("core");
expect(elfObjectTypeName(99)).toBe("unknown");
expect(gnuNoteTypeName(1)).toBe("gnu_abi_tag");
expect(gnuNoteTypeName(3)).toBe("gnu_build_id");
expect(gnuNoteTypeName(99)).toBe("unknown");
});
it("rejects extra caller arguments and admits an empty note inventory", () => {
expect(
inspectElfNotesInputSchema.safeParse({
path: "/artifacts/app",
extra: true,
}).success,
).toBe(false);
const report = elfNotesSchema.parse({
artifact: {
path: "/artifacts/app",
sha256: "a".repeat(64),
bytes: 512,
},
format: "elf64",
byte_order: "little",
machine: 62,
type: 3,
type_name: "shared",
tables: [],
entries: [],
coverage: { status: "complete", examined_tables: 0, examined_entries: 0 },
limitations: [
"No PT_NOTE program header or SHT_NOTE section was declared.",
],
});
expect(report.tables).toEqual([]);
expect(report.entries).toEqual([]);
});
+161
View File
@@ -0,0 +1,161 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { localPathStringSchema } from "../localPath.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_ELF_NOTE_FILE_BYTES = 536870912;
/** Hard ceiling on decoded note entries across every declared table. */
export const MAX_INSPECT_ELF_NOTE_ENTRIES = 256;
export const ELF_NOTE_TYPE_NAMES = [
"gnu_abi_tag",
"gnu_hwcap",
"gnu_build_id",
"gnu_gold_version",
"gnu_property",
"unknown",
] as const;
export type ElfNoteTypeName = (typeof ELF_NOTE_TYPE_NAMES)[number];
export const ELF_OBJECT_TYPE_NAMES = [
"relocatable",
"executable",
"shared",
"core",
"unknown",
] as const;
export type ElfObjectTypeName = (typeof ELF_OBJECT_TYPE_NAMES)[number];
/** Map e_type without inventing names for unknown object kinds. */
export const elfObjectTypeName = (type: number): ElfObjectTypeName => {
switch (type) {
case 1:
return "relocatable";
case 2:
return "executable";
case 3:
return "shared";
case 4:
return "core";
default:
return "unknown";
}
};
/** Map GNU n_type when the owner is GNU; other owners stay unknown. */
export const gnuNoteTypeName = (type: number): ElfNoteTypeName => {
switch (type) {
case 1:
return "gnu_abi_tag";
case 2:
return "gnu_hwcap";
case 3:
return "gnu_build_id";
case 4:
return "gnu_gold_version";
case 5:
return "gnu_property";
default:
return "unknown";
}
};
/** Complete bounded inspection of ELF PT_NOTE and SHT_NOTE tables. */
export const inspectElfNotesInputSchema = z.strictObject({
path: localPathStringSchema.describe(
"Absolute filesystem path to an ELF image",
),
max_file_bytes: z
.number()
.int()
.min(64)
.max(MAX_INSPECT_ELF_NOTE_FILE_BYTES)
.default(67108864)
.describe("Maximum stable artifact bytes to read (default 64 MiB)"),
max_entries: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_ELF_NOTE_ENTRIES)
.default(32)
.describe(
"Maximum note entries to examine across every table; exceeding the budget fails without partial success",
),
});
const gnuAbiTagSchema = z.strictObject({
os: uint,
os_name: z.string().nullable(),
major: uint,
minor: uint,
subminor: uint,
});
const noteEntrySchema = z.strictObject({
index: uint,
table_kind: z.enum(["program", "section"]),
table_index: uint,
location: range,
namesz: uint,
descsz: uint,
type: uint,
type_name: z.enum(ELF_NOTE_TYPE_NAMES),
owner: z.string(),
owner_bytes_hex: z.string().regex(/^[0-9a-f]*$/),
descriptor: z.strictObject({
location: range,
bytes: uint,
sha256: digestSchema,
hex: z
.string()
.regex(/^[0-9a-f]*$/)
.nullable(),
}),
gnu: z
.strictObject({
build_id_hex: z
.string()
.regex(/^[0-9a-f]*$/)
.nullable(),
abi_tag: gnuAbiTagSchema.nullable(),
gold_version: z.string().nullable(),
})
.nullable(),
});
/** Observed ELF note tables; absence is an empty list, not proof notes never existed. */
export const elfNotesSchema = z.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256: digestSchema,
bytes: uint,
}),
format: z.enum(["elf32", "elf64"]),
byte_order: z.enum(["little", "big"]),
machine: uint,
type: uint,
type_name: z.enum(ELF_OBJECT_TYPE_NAMES),
tables: z.array(
z.strictObject({
kind: z.enum(["program", "section"]),
index: uint,
name: z.string().nullable(),
location: range,
alignment: uint,
}),
),
entries: z.array(noteEntrySchema),
coverage: z.strictObject({
status: z.literal("complete"),
examined_tables: uint,
examined_entries: uint,
}),
limitations: z.array(z.string()),
});
export type ElfNotes = z.infer<typeof elfNotesSchema>;
export type InspectElfNotesInput = z.infer<typeof inspectElfNotesInputSchema>;
+406
View File
@@ -0,0 +1,406 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
import { inspectElfNotesInputSchema } from "../../domain/native/elfNotes.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
elfNote,
elfNoteFixture,
elfNoteTablePastEnd,
gnuAbiTagNote,
gnuBuildIdNote,
gnuPropertyNote,
} from "./ElfNotes.fixture.js";
import { parseElfNotes } from "./ElfNoteParser.js";
const defaults = inspectElfNotesInputSchema.parse({
path: "/artifacts/app",
});
const parse = (bytes: Buffer, input: typeof defaults = defaults) =>
parseElfNotes(
bytes,
{
path: input.path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
input,
);
const elf64 = (
fields: {
readonly phoff?: number;
readonly shoff?: number;
readonly phnum?: number;
readonly shnum?: number;
readonly phentsize?: number;
readonly shentsize?: number;
},
tail?: Buffer,
): Buffer => {
const header = Buffer.alloc(64);
header.write("\x7fELF", 0, "latin1");
header[4] = 2;
header[5] = 1;
header[6] = 1;
header.writeUInt16LE(2, 16);
header.writeUInt16LE(62, 18);
header.writeUInt32LE(1, 20);
header.writeBigUInt64LE(BigInt(fields.phoff ?? 0), 32);
header.writeBigUInt64LE(BigInt(fields.shoff ?? 0), 40);
header.writeUInt16LE(64, 52);
header.writeUInt16LE(fields.phentsize ?? 56, 54);
header.writeUInt16LE(fields.phnum ?? 0, 56);
header.writeUInt16LE(fields.shentsize ?? 64, 58);
header.writeUInt16LE(fields.shnum ?? 0, 60);
return tail === undefined ? header : Buffer.concat([header, tail]);
};
it("reads GNU build-id and ABI-tag notes from ELF32 and ELF64", () => {
for (const bits of [32, 64] as const) {
const bytes = elfNoteFixture({ bits, ownership: "program" });
const report = parse(bytes);
expect(report.format).toBe(bits === 64 ? "elf64" : "elf32");
expect(report.byte_order).toBe("little");
expect(report.tables).toHaveLength(1);
expect(report.tables[0]?.kind).toBe("program");
const buildId = report.entries.find(
(entry) => entry.type_name === "gnu_build_id",
);
const abi = report.entries.find(
(entry) => entry.type_name === "gnu_abi_tag",
);
expect(buildId?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
expect(abi?.gnu?.abi_tag).toMatchObject({
os: 0,
os_name: "linux",
major: 3,
minor: 2,
subminor: 0,
});
}
});
it("reads the same notes from an SHT_NOTE section including its name", () => {
const report = parse(elfNoteFixture({ ownership: "section" }));
expect(report.tables).toEqual([
expect.objectContaining({
kind: "section",
name: ".note.gnu.build-id",
}),
]);
expect(
report.entries.some((entry) => entry.type_name === "gnu_build_id"),
).toBe(true);
});
it("reports independent PT_NOTE and SHT_NOTE declarations of the same bytes", () => {
const report = parse(elfNoteFixture({ ownership: "both" }));
expect(report.tables.map((table) => table.kind).sort()).toEqual([
"program",
"section",
]);
expect(
report.entries.filter((entry) => entry.type_name === "gnu_build_id"),
).toHaveLength(2);
});
it("reports missing note tables without claiming notes never existed", () => {
const report = parse(elfNoteFixture({ ownership: "none" }));
expect(report.tables).toEqual([]);
expect(report.entries).toEqual([]);
expect(report.limitations[0]).toMatch(/No PT_NOTE/u);
});
it("fails a truncated note payload without partial success", () => {
const notes = gnuBuildIdNote();
notes.writeUInt32LE(64, 0);
const bytes = elfNoteFixture({ ownership: "program", notes });
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
});
it("keeps non-GNU owners as unknown types with descriptor hashes", () => {
const payload = Buffer.from("hello");
const notes = elfNote("CORE", 1, payload);
const report = parse(elfNoteFixture({ ownership: "program", notes }));
expect(report.entries[0]).toMatchObject({
owner: "CORE",
type_name: "unknown",
gnu: null,
});
expect(report.entries[0]?.descriptor.sha256).toBe(
createHash("sha256").update(payload).digest("hex"),
);
});
it("reads a big-endian ELF32 GNU build-id", () => {
const notes = gnuBuildIdNote(undefined, false);
const report = parse(
elfNoteFixture({
bits: 32,
little: false,
machine: 20,
ownership: "program",
notes,
}),
);
expect(report.byte_order).toBe("big");
expect(report.machine).toBe(20);
expect(report.entries[0]?.gnu?.build_id_hex).toBe(
"0123456789abcdef0123456789abcdef01234567",
);
});
it("reads an 8-aligned GNU property note from a PT_NOTE program header", () => {
const notes = gnuPropertyNote();
expect(notes.length).toBe(48);
const report = parse(
elfNoteFixture({ ownership: "program", notes, alignment: 8 }),
);
expect(report.tables).toEqual([
expect.objectContaining({ kind: "program", alignment: 8 }),
]);
expect(report.entries).toHaveLength(1);
expect(report.entries[0]).toMatchObject({
owner: "GNU",
type: 5,
type_name: "gnu_property",
namesz: 4,
descsz: 32,
});
expect(report.entries[0]?.descriptor.location.offset).toBe(
(report.entries[0]?.location.offset ?? 0) + 16,
);
});
it("reads an 8-aligned GNU property note from an SHT_NOTE section", () => {
const report = parse(
elfNoteFixture({
ownership: "section",
notes: gnuPropertyNote(),
alignment: 8,
sectionName: ".note.gnu.property",
}),
);
expect(report.tables).toEqual([
expect.objectContaining({
kind: "section",
name: ".note.gnu.property",
alignment: 8,
}),
]);
expect(report.entries[0]?.type_name).toBe("gnu_property");
expect(report.entries[0]?.descriptor.bytes).toBe(32);
});
it.each([0, 1] as const)(
"treats declared note alignment %i as 4-byte padding",
(alignment) => {
const report = parse(elfNoteFixture({ ownership: "program", alignment }));
expect(report.tables[0]?.alignment).toBe(4);
expect(
report.entries.some((entry) => entry.type_name === "gnu_build_id"),
).toBe(true);
expect(
report.entries.some((entry) => entry.type_name === "gnu_abi_tag"),
).toBe(true);
},
);
const hostElf = [
"/bin/ls",
"/usr/bin/ls",
"/usr/bin/env",
].reduce<Buffer | null>((found, path) => {
if (found) return found;
try {
const bytes = readFileSync(path);
if (
bytes.length > 64 &&
bytes[0] === 0x7f &&
bytes[1] === 0x45 &&
bytes[2] === 0x4c &&
bytes[3] === 0x46
)
return bytes;
} catch {
return found;
}
return found;
}, null);
it.skipIf(hostElf === null)(
"parses a host ELF without requiring it to contain notes",
() => {
const report = parse(hostElf!);
expect(report.coverage.status).toBe("complete");
const buildId = report.entries.find(
(entry) => entry.type_name === "gnu_build_id",
);
const abi = report.entries.find(
(entry) => entry.type_name === "gnu_abi_tag",
);
if (buildId !== undefined)
expect(buildId.gnu?.build_id_hex).toMatch(/^[0-9a-f]+$/u);
if (abi !== undefined)
expect(abi.gnu?.abi_tag).toMatchObject({
os: expect.any(Number),
major: expect.any(Number),
minor: expect.any(Number),
subminor: expect.any(Number),
});
},
);
const unterminatedNoteSections = (
sections: number,
nameBytes: number,
): Buffer => {
const shnum = sections + 2;
const tableOffset = 64 + shnum * 64;
const shdr = (type: number, offset = 0, size = 0): Buffer => {
const header = Buffer.alloc(64);
header.writeUInt32LE(type, 4);
header.writeBigUInt64LE(BigInt(offset), 24);
header.writeBigUInt64LE(BigInt(size), 32);
return header;
};
const image = Buffer.concat([
elf64({ shoff: 64, shnum }),
shdr(0),
shdr(3, tableOffset, nameBytes),
...Array.from({ length: sections }, () => shdr(7, tableOffset)),
Buffer.alloc(nameBytes, 0x41),
]);
image.writeUInt16LE(1, 62);
return image;
};
it("bounds repeated scans of an unterminated section name", () => {
expect(parse(unterminatedNoteSections(1, 64)).tables[0]?.name).toBeNull();
expect(() => parse(unterminatedNoteSections(3, 400))).toThrow(/byte budget/u);
});
it("rejects note text that repeats beyond the shared byte budget", () => {
const notes = elfNote("A".repeat(40), 1, Buffer.alloc(0));
const once = elfNoteFixture({ ownership: "program", notes });
const twice = elfNoteFixture({ ownership: "both", notes });
const input = inspectElfNotesInputSchema.parse({
path: defaults.path,
max_file_bytes: 200,
});
expect(parse(once, input).entries).toHaveLength(1);
expect(() => parse(twice, input)).toThrow(ArtifactReaderFailure);
expect(() => parse(twice, input)).toThrow(/byte budget/u);
});
it("rejects program and section header tables that do not fit", () => {
expect(() =>
parse(elf64({ phoff: 64, phnum: 1 }, Buffer.from([1, 0, 0, 0]))),
).toThrow(/leaves the artifact/u);
expect(() =>
parse(
elf64({ shoff: 64, shnum: 1, shentsize: 64 }, Buffer.from([0, 0, 0, 0])),
),
).toThrow(/leaves the artifact/u);
});
it("rejects an extended section count stored in section zero", () => {
expect(() => parse(elf64({ shoff: 64, shnum: 0 }))).toThrow(
/extended numbering/u,
);
});
const repeatedNotes = (count: number, notes: Buffer): Buffer => {
const headers = Buffer.alloc(56 * count);
const offset = 64 + headers.length;
for (let index = 0; index < count; index += 1) {
const at = index * 56;
headers.writeUInt32LE(4, at);
headers.writeBigUInt64LE(BigInt(offset), at + 8);
headers.writeBigUInt64LE(BigInt(notes.length), at + 32);
headers.writeBigUInt64LE(4n, at + 48);
}
return Buffer.concat([elf64({ phoff: 64, phnum: count }), headers, notes]);
};
it("reuses one descriptor hash and GNU NUL scan across note headers", () => {
const payload = Buffer.alloc(400);
const hashed = parse(repeatedNotes(6, elfNote("X", 1, payload)));
expect(hashed.entries).toHaveLength(6);
expect(
new Set(hashed.entries.map((entry) => entry.descriptor.sha256)).size,
).toBe(1);
const gold = parse(repeatedNotes(6, elfNote("GNU", 4, payload)));
expect(gold.entries).toHaveLength(6);
expect(gold.entries.every((entry) => entry.gnu?.gold_version === "")).toBe(
true,
);
const distinct = Buffer.concat([
elfNote("X", 1, payload),
elfNote("X", 1, Buffer.alloc(400, 1)),
]);
expect(() =>
parse(
elfNoteFixture({ ownership: "program", notes: distinct }),
inspectElfNotesInputSchema.parse({
path: defaults.path,
max_file_bytes: payload.length + 160,
}),
),
).toThrow(/byte budget/u);
});
it("decodes a GNU gold version by scanning trailing NULs", () => {
const trailing = elfNote("GNU", 4, Buffer.from("gold\0\0x\0", "latin1"));
expect(
parse(elfNoteFixture({ ownership: "program", notes: trailing })).entries[0]
?.gnu?.gold_version,
).toBe("gold\0\0x");
const descriptor = Buffer.concat([Buffer.alloc(131_072), Buffer.from("x")]);
const started = Date.now();
const report = parse(
elfNoteFixture({
ownership: "program",
notes: elfNote("GNU", 4, descriptor),
}),
);
expect(Date.now() - started).toBeLessThan(1_000);
expect(report.entries[0]?.gnu?.gold_version).toBe(`${"\0".repeat(131_072)}x`);
});
it("rejects a declared note table that extends past the file", () => {
const truncated = elfNoteTablePastEnd();
expect(truncated.length).toBe(137);
expect(() => parse(truncated)).toThrow(ArtifactReaderFailure);
expect(() => parse(truncated)).toThrow(/leaves the artifact/u);
const present = Buffer.concat([truncated, Buffer.alloc(3)]);
expect(parse(present).entries).toHaveLength(1);
expect(parse(present).tables[0]?.location).toEqual({
offset: 120,
bytes: 20,
});
});
it("fails when the entry budget is exhausted", () => {
const notes = Buffer.concat([
gnuBuildIdNote(),
gnuAbiTagNote(),
elfNote("GNU", 4, Buffer.from("gold")),
]);
const bytes = elfNoteFixture({ ownership: "program", notes });
expect(() =>
parseElfNotes(
bytes,
{
path: defaults.path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
inspectElfNotesInputSchema.parse({ path: defaults.path, max_entries: 1 }),
),
).toThrow(ArtifactReaderFailure);
});
+455
View File
@@ -0,0 +1,455 @@
import { createHash } from "node:crypto";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
import type {
ElfNotes,
InspectElfNotesInput,
} from "../../domain/native/elfNotes.js";
import {
elfObjectTypeName,
gnuNoteTypeName,
} from "../../domain/native/elfNotes.js";
const PT_NOTE = 4;
const SHT_NOTE = 7;
const SHT_STRTAB = 3;
const HEX_DESCRIPTOR_LIMIT = 64;
interface TextBudget {
readonly limit: number;
used: number;
hashed: number;
scanned: number;
readonly hashes: Map<string, string>;
readonly scans: Map<string, number>;
}
const fail = (message: string): never => {
throw new ArtifactReaderFailure("format", message);
};
const chargeText = (
budget: TextBudget,
bytes: number,
field: "used" | "hashed" | "scanned" = "used",
): void => {
if (
!Number.isSafeInteger(bytes) ||
bytes < 0 ||
bytes > budget.limit - budget[field]
)
throw new ArtifactReaderFailure(
"limit",
`ELF note text exceeds the ${String(budget.limit)}-byte budget.`,
{ boundary: "memory" },
);
budget[field] += bytes;
};
const sha256 = (bytes: Buffer): string =>
createHash("sha256").update(bytes).digest("hex");
const alignTo = (value: number, alignment: number): number => {
if (alignment <= 1) return value;
return Math.ceil(value / alignment) * alignment;
};
const GNU_OS_NAMES: Readonly<Record<number, string>> = {
0: "linux",
1: "hurd",
2: "solaris",
3: "freebsd",
4: "netbsd",
5: "syllable",
6: "nacl",
};
const LIMITATIONS = [
"This workflow does not execute the image, resolve runtime notes, or verify GNU build-id uniqueness against package catalogs.",
"PT_NOTE and SHT_NOTE are examined independently; the same file range may appear twice when both tables declare it.",
] as const;
const ABSENT_LIMITATIONS = [
"No PT_NOTE program header or SHT_NOTE section was declared. That is not proof the image never carried notes in another container or overlay.",
"This workflow does not execute the image, resolve runtime notes, or verify GNU build-id uniqueness against package catalogs.",
] as const;
class ElfReader {
constructor(
readonly bytes: Buffer,
readonly little: boolean,
readonly bits: 32 | 64,
) {}
u16(offset: number): number {
this.require(offset, 2);
return this.little
? this.bytes.readUInt16LE(offset)
: this.bytes.readUInt16BE(offset);
}
u32(offset: number): number {
this.require(offset, 4);
return this.little
? this.bytes.readUInt32LE(offset)
: this.bytes.readUInt32BE(offset);
}
word(offset: number): number {
const width = this.bits === 64 ? 8 : 4;
this.require(offset, width);
if (this.bits === 32) return this.u32(offset);
const value = this.little
? this.bytes.readBigUInt64LE(offset)
: this.bytes.readBigUInt64BE(offset);
if (value > BigInt(Number.MAX_SAFE_INTEGER))
fail("ELF offset or size exceeds a safe integer.");
return Number(value);
}
require(offset: number, length: number): void {
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(length) ||
offset < 0 ||
length < 0 ||
offset > this.bytes.length - length
)
fail(
`ELF range ${String(offset)} + ${String(length)} leaves the artifact.`,
);
}
slice(offset: number, length: number): Buffer {
this.require(offset, length);
return this.bytes.subarray(offset, offset + length);
}
cString(table: Buffer, offset: number, text: TextBudget): string | null {
if (offset < 0 || offset >= table.length) return null;
const affordable = text.limit - text.used;
const available = table.length - offset;
const scanned = Math.min(available, affordable + 1);
const end = table.subarray(offset, offset + scanned).indexOf(0);
if (end < 0) {
chargeText(text, scanned);
return null;
}
chargeText(text, end);
return table.subarray(offset, offset + end).toString("latin1");
}
}
/** Note name/descriptor padding follows the containing table's p_align or
* sh_addralign: 8 stays 8; 0, 1, 4, and other values pad as 4, matching
* readelf/binutils. */
const noteAlignment = (declared: number): number => (declared === 8 ? 8 : 4);
interface ParsedNote {
readonly location: { offset: number; bytes: number };
readonly namesz: number;
readonly descsz: number;
readonly type: number;
readonly owner: string;
readonly owner_bytes_hex: string;
readonly descriptor: Buffer;
readonly descriptor_offset: number;
}
const parseNoteStream = (
reader: ElfReader,
offset: number,
size: number,
alignment: number,
maxEntries: number,
examined: { count: number },
text: TextBudget,
): ParsedNote[] => {
// Alignment padding is not read when a note's aligned size equals the
// declared table. Require the whole table first so a short file is invalid
// input instead of a provider result the service rejects.
reader.require(offset, size);
const notes: ParsedNote[] = [];
let at = 0;
while (at + 12 <= size) {
examined.count += 1;
if (examined.count > maxEntries)
throw new ArtifactReaderFailure(
"limit",
`ELF note tables exceed the ${String(maxEntries)}-entry budget.`,
{ boundary: "capacity" },
);
const header = offset + at;
const namesz = reader.u32(header);
const descsz = reader.u32(header + 4);
const type = reader.u32(header + 8);
// Align offsets from the start of this note. Independently rounding
// namesz/descsz disagrees with binutils when alignment is 8 because the
// 12-byte note header is not 8-aligned (GNU property namesz=4 then starts
// its descriptor at 16, not 20).
const descRel = alignTo(12 + namesz, alignment);
const nextRel = alignTo(descRel + descsz, alignment);
const next = at + nextRel;
if (next > size) fail("ELF note entry exceeds its table.");
if (next <= at) fail("ELF note alignment did not advance.");
const nameBytes = reader.slice(header + 12, namesz);
const descriptor = reader.slice(header + descRel, descsz);
const ownerEnd = nameBytes.indexOf(0);
const ownerBytes = ownerEnd < 0 ? namesz : ownerEnd;
chargeText(text, ownerBytes + namesz * 2);
notes.push({
location: { offset: header, bytes: next - at },
namesz,
descsz,
type,
owner:
ownerEnd < 0
? nameBytes.toString("latin1")
: nameBytes.subarray(0, ownerEnd).toString("latin1"),
owner_bytes_hex: nameBytes.toString("hex"),
descriptor,
descriptor_offset: header + descRel,
});
at = next;
}
if (
at < size &&
reader.slice(offset + at, size - at).some((byte) => byte !== 0)
)
fail("ELF note table has trailing non-zero bytes.");
return notes;
};
const decodeGnu = (
owner: string,
type: number,
descriptor: Buffer,
little: boolean,
text: TextBudget,
): ElfNotes["entries"][number]["gnu"] => {
if (owner !== "GNU") return null;
if (type === 3 && descriptor.length > 0) {
chargeText(text, descriptor.length * 2);
return {
build_id_hex: descriptor.toString("hex"),
abi_tag: null,
gold_version: null,
};
}
if (type === 1 && descriptor.length === 16) {
const read = (offset: number) =>
little
? descriptor.readUInt32LE(offset)
: descriptor.readUInt32BE(offset);
const os = read(0);
return {
build_id_hex: null,
abi_tag: {
os,
os_name: GNU_OS_NAMES[os] ?? null,
major: read(4),
minor: read(8),
subminor: read(12),
},
gold_version: null,
};
}
if (type === 4) {
const range = `${String(descriptor.byteOffset)}:${String(descriptor.length)}`;
let end = text.scans.get(range);
if (end === undefined) {
chargeText(text, descriptor.length, "scanned");
end = descriptor.length;
while (end > 0 && descriptor[end - 1] === 0) end -= 1;
text.scans.set(range, end);
}
chargeText(text, end);
return {
build_id_hex: null,
abi_tag: null,
gold_version: descriptor.toString("latin1", 0, end),
};
}
return {
build_id_hex: null,
abi_tag: null,
gold_version: null,
};
};
/** Decode PT_NOTE and SHT_NOTE tables from one in-memory ELF image. */
export const parseElfNotes = (
bytes: Buffer,
artifact: ElfNotes["artifact"],
input: InspectElfNotesInput,
): ElfNotes => {
const fixed = readElfFixedHeader(bytes);
if (!fixed.ok) return fail(fixed.error);
const header = fixed.value;
const reader = new ElfReader(
bytes,
header.byteOrder === "little",
header.elfClass,
);
const wide = header.elfClass === 64;
const type = reader.u16(16);
const phoff = reader.word(wide ? 32 : 28);
const shoff = reader.word(wide ? 40 : 32);
const phentsize = reader.u16(wide ? 54 : 42);
const phnum = reader.u16(wide ? 56 : 44);
const shentsize = reader.u16(wide ? 58 : 46);
const shnum = reader.u16(wide ? 60 : 48);
const shstrndx = reader.u16(wide ? 62 : 50);
if (
phnum === 0xffff ||
shnum === 0xffff ||
shstrndx === 0xffff ||
(shoff !== 0 && shnum === 0)
)
return fail("ELF extended numbering is unsupported.");
if (phnum !== 0) {
if (phentsize < (wide ? 56 : 32))
return fail("ELF program header width is invalid.");
reader.require(phoff, phnum * phentsize);
}
if (shnum !== 0) {
if (shentsize < (wide ? 64 : 40))
return fail("ELF section header width is invalid.");
reader.require(shoff, shnum * shentsize);
}
const text: TextBudget = {
limit: Math.min(bytes.length, input.max_file_bytes),
used: 0,
hashed: 0,
scanned: 0,
hashes: new Map(),
scans: new Map(),
};
let shstrtab: Buffer | null = null;
if (shnum !== 0 && shstrndx !== 0) {
if (shstrndx >= shnum)
return fail("ELF section name index is out of range.");
const namesHeader = shoff + shstrndx * shentsize;
if (reader.u32(namesHeader + 4) !== SHT_STRTAB)
return fail("ELF section name table is not a string table.");
const strOff = reader.word(namesHeader + (wide ? 24 : 16));
const strSize = reader.word(namesHeader + (wide ? 32 : 20));
shstrtab = reader.slice(strOff, strSize);
}
const tables: ElfNotes["tables"] = [];
const parsed: ReturnType<typeof toEntry>[] = [];
const examined = { count: 0 };
if (phnum !== 0) {
if (phoff < header.headerSize)
return fail("ELF program header table overlaps the native header.");
for (let index = 0; index < phnum; index += 1) {
const at = phoff + index * phentsize;
if (reader.u32(at) !== PT_NOTE) continue;
const fileOffset = reader.word(at + (wide ? 8 : 4));
const fileSize = reader.word(at + (wide ? 32 : 16));
const alignment = noteAlignment(reader.word(at + (wide ? 48 : 28)));
tables.push({
kind: "program",
index,
name: null,
location: { offset: fileOffset, bytes: fileSize },
alignment,
});
for (const note of parseNoteStream(
reader,
fileOffset,
fileSize,
alignment,
input.max_entries,
examined,
text,
))
parsed.push(toEntry(note, "program", index, reader.little, text));
}
}
if (shnum !== 0) {
if (shoff < header.headerSize)
return fail("ELF section header table overlaps the native header.");
for (let index = 0; index < shnum; index += 1) {
const at = shoff + index * shentsize;
if (reader.u32(at + 4) !== SHT_NOTE) continue;
const nameOffset = reader.u32(at);
const fileOffset = reader.word(at + (wide ? 24 : 16));
const fileSize = reader.word(at + (wide ? 32 : 20));
const alignment = noteAlignment(reader.word(at + (wide ? 48 : 32)));
tables.push({
kind: "section",
index,
name:
shstrtab === null ? null : reader.cString(shstrtab, nameOffset, text),
location: { offset: fileOffset, bytes: fileSize },
alignment,
});
for (const note of parseNoteStream(
reader,
fileOffset,
fileSize,
alignment,
input.max_entries,
examined,
text,
))
parsed.push(toEntry(note, "section", index, reader.little, text));
}
}
const absent = tables.length === 0;
return {
artifact,
format: wide ? "elf64" : "elf32",
byte_order: reader.little ? "little" : "big",
machine: header.machine,
type,
type_name: elfObjectTypeName(type),
tables,
entries: parsed.map((entry, index) => ({ ...entry, index })),
coverage: {
status: "complete",
examined_tables: tables.length,
examined_entries: examined.count,
},
limitations: [...(absent ? ABSENT_LIMITATIONS : LIMITATIONS)],
};
};
const toEntry = (
note: ParsedNote,
tableKind: "program" | "section",
tableIndex: number,
little: boolean,
text: TextBudget,
): Omit<ElfNotes["entries"][number], "index"> => {
const gnuOwner = note.owner === "GNU";
const range = `${String(note.descriptor_offset)}:${String(note.descsz)}`;
let digest = text.hashes.get(range);
if (digest === undefined) {
chargeText(text, note.descriptor.length, "hashed");
digest = sha256(note.descriptor);
text.hashes.set(range, digest);
}
return {
table_kind: tableKind,
table_index: tableIndex,
location: note.location,
namesz: note.namesz,
descsz: note.descsz,
type: note.type,
type_name: gnuOwner ? gnuNoteTypeName(note.type) : "unknown",
owner: note.owner,
owner_bytes_hex: note.owner_bytes_hex,
descriptor: {
location: { offset: note.descriptor_offset, bytes: note.descsz },
bytes: note.descsz,
sha256: digest,
hex:
note.descsz <= HEX_DESCRIPTOR_LIMIT
? note.descriptor.toString("hex")
: null,
},
gnu: decodeGnu(note.owner, note.type, note.descriptor, little, text),
};
};
+239
View File
@@ -0,0 +1,239 @@
const concat = (...parts: Buffer[]): Buffer => Buffer.concat(parts);
const alignTo = (value: number, alignment: number): number =>
Math.ceil(value / alignment) * alignment;
/** Effective namesz/descsz padding: 8 stays 8; 0, 1, 4, and other values use 4. */
const notePadding = (declared: number): number => (declared === 8 ? 8 : 4);
const writeWord = (
bytes: Buffer,
offset: number,
value: number,
bits: 32 | 64,
little: boolean,
): void => {
if (bits === 64) {
if (little) bytes.writeBigUInt64LE(BigInt(value), offset);
else bytes.writeBigUInt64BE(BigInt(value), offset);
return;
}
if (little) bytes.writeUInt32LE(value, offset);
else bytes.writeUInt32BE(value, offset);
};
const write16 = (
bytes: Buffer,
offset: number,
value: number,
little: boolean,
): void => {
if (little) bytes.writeUInt16LE(value, offset);
else bytes.writeUInt16BE(value, offset);
};
const write32 = (
bytes: Buffer,
offset: number,
value: number,
little: boolean,
): void => {
if (little) bytes.writeUInt32LE(value, offset);
else bytes.writeUInt32BE(value, offset);
};
/** Encode one ELF note, padding name/descriptor to the table alignment. */
export const elfNote = (
owner: string,
type: number,
descriptor: Buffer,
little = true,
alignment = 4,
): Buffer => {
const name = Buffer.from(`${owner}\0`, "latin1");
const pad = notePadding(alignment);
const descOff = alignTo(12 + name.length, pad);
const bytes = Buffer.alloc(alignTo(descOff + descriptor.length, pad));
write32(bytes, 0, name.length, little);
write32(bytes, 4, descriptor.length, little);
write32(bytes, 8, type, little);
name.copy(bytes, 12);
descriptor.copy(bytes, descOff);
return bytes;
};
/** GNU NT_GNU_BUILD_ID note with a 20-byte descriptor. */
export const gnuBuildIdNote = (
hex = "0123456789abcdef0123456789abcdef01234567",
little = true,
): Buffer => elfNote("GNU", 3, Buffer.from(hex, "hex"), little);
/** GNU NT_GNU_ABI_TAG note: Linux 3.2.0. */
export const gnuAbiTagNote = (little = true): Buffer => {
const descriptor = Buffer.alloc(16);
write32(descriptor, 0, 0, little);
write32(descriptor, 4, 3, little);
write32(descriptor, 8, 2, little);
write32(descriptor, 12, 0, little);
return elfNote("GNU", 1, descriptor, little);
};
/** GNU NT_GNU_PROPERTY_TYPE_0 note with 8-byte table padding. */
export const gnuPropertyNote = (little = true): Buffer => {
const descriptor = Buffer.alloc(32);
write32(descriptor, 0, 0xc0000002, little);
write32(descriptor, 4, 4, little);
write32(descriptor, 8, 3, little);
write32(descriptor, 16, 0xc0008002, little);
write32(descriptor, 20, 4, little);
write32(descriptor, 24, 1, little);
return elfNote("GNU", 5, descriptor, little, 8);
};
export interface ElfNoteImageOptions {
readonly bits?: 32 | 64;
readonly little?: boolean;
readonly notes?: Buffer;
readonly ownership?: "program" | "section" | "both" | "none";
readonly machine?: number;
readonly type?: number;
readonly alignment?: number;
readonly sectionName?: string;
}
/** Minimal ELF32/ELF64 image declaring PT_NOTE and/or SHT_NOTE. */
export const elfNoteFixture = (options: ElfNoteImageOptions = {}): Buffer => {
const bits = options.bits ?? 64;
const little = options.little ?? true;
const ownership = options.ownership ?? "both";
const notes =
options.notes ??
concat(gnuBuildIdNote(undefined, little), gnuAbiTagNote(little));
const declaredAlign = options.alignment ?? 4;
const pad = notePadding(declaredAlign);
const header = bits === 64 ? 64 : 52;
const phent = bits === 64 ? 56 : 32;
const shent = bits === 64 ? 64 : 40;
const program = ownership === "program" || ownership === "both";
const section = ownership === "section" || ownership === "both";
const phnum = program ? 1 : 0;
const sectionName = options.sectionName ?? ".note.gnu.build-id";
const shstrtab = Buffer.from(`\0${sectionName}\0.shstrtab\0`, "latin1");
const shnum = section ? 3 : 0;
const phoff = header;
const shoff = phoff + phnum * phent;
const noteOff = alignTo(shoff + shnum * shent, pad);
const strOff = noteOff + notes.length;
const bytes = Buffer.alloc(strOff + (section ? shstrtab.length : 0));
bytes.writeUInt32LE(0x464c457f, 0);
bytes[4] = bits === 64 ? 2 : 1;
bytes[5] = little ? 1 : 2;
bytes[6] = 1;
write16(bytes, 0x10, options.type ?? 3, little);
write16(bytes, 0x12, options.machine ?? 62, little);
write32(bytes, 0x14, 1, little);
write16(bytes, bits === 64 ? 0x34 : 0x28, header, little);
writeWord(
bytes,
bits === 64 ? 0x20 : 0x1c,
phnum === 0 ? 0 : phoff,
bits,
little,
);
write16(bytes, bits === 64 ? 0x36 : 0x2a, phent, little);
write16(bytes, bits === 64 ? 0x38 : 0x2c, phnum, little);
writeWord(
bytes,
bits === 64 ? 0x28 : 0x20,
shnum === 0 ? 0 : shoff,
bits,
little,
);
write16(bytes, bits === 64 ? 0x3a : 0x2e, shent, little);
write16(bytes, bits === 64 ? 0x3c : 0x30, shnum, little);
write16(bytes, bits === 64 ? 0x3e : 0x32, section ? 2 : 0, little);
if (program) {
const ph = phoff;
write32(bytes, ph, 4, little);
if (bits === 64) {
write32(bytes, ph + 4, 4, little);
writeWord(bytes, ph + 8, noteOff, bits, little);
writeWord(bytes, ph + 32, notes.length, bits, little);
writeWord(bytes, ph + 40, notes.length, bits, little);
writeWord(bytes, ph + 48, declaredAlign, bits, little);
} else {
writeWord(bytes, ph + 4, noteOff, bits, little);
writeWord(bytes, ph + 16, notes.length, bits, little);
writeWord(bytes, ph + 20, notes.length, bits, little);
write32(bytes, ph + 24, 4, little);
writeWord(bytes, ph + 28, declaredAlign, bits, little);
}
}
if (section) {
const noteName = 1;
const strName = `${sectionName}\0`.length + 1;
const noteSh = shoff + shent;
const strSh = shoff + 2 * shent;
write32(bytes, noteSh, noteName, little);
write32(bytes, noteSh + 4, 7, little);
writeWord(bytes, noteSh + (bits === 64 ? 24 : 16), noteOff, bits, little);
writeWord(
bytes,
noteSh + (bits === 64 ? 32 : 20),
notes.length,
bits,
little,
);
writeWord(
bytes,
noteSh + (bits === 64 ? 48 : 32),
declaredAlign,
bits,
little,
);
write32(bytes, strSh, strName, little);
write32(bytes, strSh + 4, 3, little);
writeWord(bytes, strSh + (bits === 64 ? 24 : 16), strOff, bits, little);
writeWord(
bytes,
strSh + (bits === 64 ? 32 : 20),
shstrtab.length,
bits,
little,
);
shstrtab.copy(bytes, strOff);
}
notes.copy(bytes, noteOff);
return bytes;
};
/**
* ELF64 whose only PT_NOTE declares 20 bytes. The file holds the 12-byte
* header, 4-byte name, and 1-byte descriptor; the 3 alignment bytes are absent.
*/
export const elfNoteTablePastEnd = (): Buffer => {
const note = Buffer.alloc(17);
write32(note, 0, 4, true);
write32(note, 4, 1, true);
write32(note, 8, 1, true);
note.write("GNU\0", 12, "latin1");
note[16] = 0x2a;
const header = Buffer.alloc(64);
header.writeUInt32LE(0x464c457f, 0);
header[4] = 2;
header[5] = 1;
header[6] = 1;
write16(header, 0x10, 3, true);
write16(header, 0x12, 62, true);
write32(header, 0x14, 1, true);
write16(header, 0x34, 64, true);
writeWord(header, 0x20, 64, 64, true);
write16(header, 0x36, 56, true);
write16(header, 0x38, 1, true);
const program = Buffer.alloc(56);
write32(program, 0, 4, true);
writeWord(program, 8, 120, 64, true);
writeWord(program, 32, 20, 64, true);
writeWord(program, 48, 4, 64, true);
return concat(header, program, note);
};
+91
View File
@@ -0,0 +1,91 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { ElfNotesService } from "../../application/binaryDiagnostics/ElfNotesService.js";
import {
AnalysisInputError,
AnalysisResourceConstraintError,
} from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import {
elfNote,
elfNoteFixture,
elfNoteTablePastEnd,
} from "./ElfNotes.fixture.js";
import { ElfNotesProvider } from "./ElfNotesProvider.js";
const inspect = async (
path: string,
input: { readonly max_file_bytes?: number } = {},
) => {
const service = new ElfNotesService(new ElfNotesProvider());
try {
return await service.inspect({ path, ...input });
} finally {
await service.close();
}
};
describe("ELF note provider failures", () => {
it("reports a note table past the end of the file as invalid input", async () => {
const root = await createTestTempDirectory("rea-elf-note-past-end-");
const path = join(root, "program");
await writeFile(path, elfNoteTablePastEnd());
const result = await inspect(path);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected invalid input");
expect(result.error).toBeInstanceOf(AnalysisInputError);
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
{
path: ["path"],
reason: "invalid_format",
message: expect.stringContaining("leaves the artifact"),
},
],
});
});
it("names the note text budget instead of host memory", async () => {
const root = await createTestTempDirectory("rea-elf-note-text-budget-");
const path = join(root, "program");
await writeFile(
path,
elfNoteFixture({
ownership: "program",
notes: elfNote("GNU", 3, Buffer.alloc(4096, 1)),
}),
);
const result = await inspect(path);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected a text budget refusal");
const error = result.error as AnalysisResourceConstraintError;
expect(error.resource).toBe("memory");
expect(error.message).toMatch(/byte budget/u);
const projected = projectAnalysisError(error);
expect(projected.retryable).toBe(false);
expect(projected.remediation.action).toContain(
"ELF note text, hash, and scan budget",
);
expect(projected.remediation.action).toMatch(/admitted image length/u);
expect(projected.remediation.action).toMatch(/do not raise it/u);
expect(projected.remediation.action).not.toMatch(/available host memory/u);
});
it("keeps a caller file-size budget distinct from host memory", async () => {
const root = await createTestTempDirectory("rea-elf-note-file-budget-");
const path = join(root, "program");
await writeFile(path, elfNoteFixture({ ownership: "program" }));
const result = await inspect(path, { max_file_bytes: 64 });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected a file-size refusal");
const error = result.error as AnalysisResourceConstraintError;
expect(error.resource).toBe("file-size");
const projected = projectAnalysisError(error);
expect(projected.retryable).toBe(false);
expect(projected.remediation.action).toContain("max_file_bytes");
expect(projected.remediation.action).not.toMatch(/available host memory/u);
});
});
+207
View File
@@ -0,0 +1,207 @@
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 { ElfNotesPort } from "../../application/binaryDiagnostics/ElfNotesPort.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 { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
import {
MAX_INSPECT_ELF_NOTE_ENTRIES,
MAX_INSPECT_ELF_NOTE_FILE_BYTES,
type InspectElfNotesInput,
} from "../../domain/native/elfNotes.js";
import { err, ok } from "../../domain/result.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { ELF_NOTES_PROVIDER } from "../../application/InvestigationProviders.js";
import { parseElfNotes } from "./ElfNoteParser.js";
const OPERATION = "inspect_elf_notes";
/** Portable parser over one bounded, identity-checked regular file. */
export class ElfNotesProvider implements ElfNotesPort {
readonly identity = ELF_NOTES_PROVIDER;
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(
ELF_NOTES_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectElfNotesInput, 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(
parseElfNotes(
snapshot.bytes,
{
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
input,
),
);
});
} catch (cause) {
return err(elfNotesFailure(cause, input, options?.signal));
}
}
}
const NOTE_TEXT_BUDGET =
"The ELF note text, hash, and scan budget tied to the admitted image length";
const inputBudgetRemediation = (
boundary: "file-size" | "capacity",
input: InspectElfNotesInput,
): string =>
boundary === "file-size"
? input.max_file_bytes >= MAX_INSPECT_ELF_NOTE_FILE_BYTES
? `The image exceeds the ${String(MAX_INSPECT_ELF_NOTE_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.`
: input.max_entries >= MAX_INSPECT_ELF_NOTE_ENTRIES
? `The image exceeds the ${String(MAX_INSPECT_ELF_NOTE_ENTRIES)}-entry note-table ceiling this operation accepts; select a smaller artifact.`
: `Raise max_entries above ${String(input.max_entries)} or select a smaller artifact; the limit is the caller-selected note entry budget.`;
const elfNotesFailurePrimary = (
cause: unknown,
input: InspectElfNotesInput,
) => {
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") {
// Caller-selected read and entry budgets name the bound to raise.
// The note text, hash, and scan ceiling equals the admitted image
// length, so host memory cannot raise it.
const noteTextBudget = cause.boundary === "memory";
return new AnalysisResourceConstraintError(
OPERATION,
cause.boundary ?? "memory",
cause.message,
{
max_file_bytes: input.max_file_bytes,
max_entries: input.max_entries,
},
{
cause,
...(cause.boundary === undefined
? {}
: noteTextBudget
? fixedAllowanceOptions(NOTE_TEXT_BUDGET, "parser")
: {
retryable: false as const,
remediationAction: inputBudgetRemediation(
cause.boundary,
input,
),
}),
},
);
}
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 ELF image 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 : "ELF note inspection failed.",
{ cause },
);
};
const elfNotesFailure = (
cause: unknown,
input: InspectElfNotesInput,
signal: AbortSignal | undefined,
) => {
const primary =
cause instanceof ArtifactFileCleanupFailure &&
cause.fileCleanupFailure.outcome.kind === "failed"
? elfNotesFailurePrimary(cause.fileCleanupFailure.outcome.cause, input)
: cause instanceof ArtifactFileCleanupFailure && signal?.aborted
? new AnalysisCancelledError(OPERATION, { cause: signal.reason })
: elfNotesFailurePrimary(cause, input);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
ELF_NOTES_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ operation: OPERATION, cause },
);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
+18 -1
View File
@@ -12,8 +12,13 @@ import { wasmArtifactModule } from "../composition/wasm.js";
import { registerWasmTools } from "./registerWasmTools.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { captureProviderEnvironment } from "./providerEnvironment.js";
import { createPeResourcesService } from "../composition/binaryDiagnostics.js";
import {
createElfNotesService,
createPeResourcesService,
} from "../composition/binaryDiagnostics.js";
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
import { registerElfNotesTool } from "./registerElfNotesTool.js";
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import {
@@ -350,6 +355,7 @@ export const createServer = (
const il2cppMetadata =
options.il2cppMetadata ?? createIl2cppMetadataService();
const peResources = createPeResourcesService();
const elfNotes = createElfNotesService();
const electronFuses = options.electronFuses ?? createElectronFuseService();
const webNetworkCapture =
options.webNetworkCapture ?? createWebNetworkCaptureService(environment);
@@ -451,6 +457,7 @@ export const createServer = (
modelArtifact.close(),
gpuCodeObjects.close(),
peResources.close(),
elfNotes.close(),
electronFuses.close(),
webNetworkCapture.close(),
javascriptRecovery.close?.() ?? Promise.resolve(),
@@ -514,6 +521,7 @@ export const createServer = (
rustBinary,
il2cppMetadata,
peResources,
elfNotes,
electronFuses,
webNetworkCapture,
};
@@ -745,6 +753,7 @@ const registerConfiguredAnalysisTools = (
rustBinary,
il2cppMetadata,
peResources,
elfNotes,
webNetworkCapture,
artifactResources,
}: ServerToolContext,
@@ -794,6 +803,13 @@ const registerConfiguredAnalysisTools = (
recordEvidence,
withAdmittedAnalysis,
);
registerElfNotesTool(
server,
elfNotes,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerGoTools(
server,
goBinary,
@@ -927,6 +943,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly rustBinary: RustBinaryService;
readonly il2cppMetadata: Il2cppMetadataService;
readonly peResources: PeResourcesService;
readonly elfNotes: ElfNotesService;
readonly electronFuses: ElectronFuseService;
readonly webNetworkCapture: WebNetworkCaptureService;
readonly artifactResources: ArtifactResourceScope;
+31
View File
@@ -0,0 +1,31 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Register the explicit-file ELF note-table workflow and retain its complete Evidence. */
export const registerElfNotesTool = (
server: EvidenceMcpServer,
service: ElfNotesService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_elf_notes");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
};
+60
View File
@@ -0,0 +1,60 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { elfNoteFixture } from "../../src/native/elf/ElfNotes.fixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest("reports a GNU build-id through the public CLI", async ({ cli }) => {
const root = await createTestTempDirectory("rea-elf-notes-cli-");
const path = join(root, "program");
await writeFile(path, elfNoteFixture({ ownership: "program" }));
const result = await cli.run({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
result.json,
);
expect(
evidence.normalized_result.entries.find(
(entry) => entry.type_name === "gnu_build_id",
)?.gnu?.build_id_hex,
).toBe("0123456789abcdef0123456789abcdef01234567");
});
cliTest(
"resolves a relative ELF path against the working directory",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-elf-notes-cli-rel-");
await writeFile(
join(root, "unsigned"),
elfNoteFixture({ ownership: "none" }),
);
const result = await cli.run({
arguments: ["inspect-elf-notes", "unsigned", "--json"],
cwd: root,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.tables).toEqual([]);
expect(evidence.subject?.local_path).toBe(join(root, "unsigned"));
},
);
cliTest("reports a truncated ELF as invalid input", async ({ cli }) => {
const root = await createTestTempDirectory("rea-elf-notes-cli-bad-");
const path = join(root, "broken");
await writeFile(path, Buffer.from("\x7fELF"));
const result = await cli.run({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_elf_notes" },
});
});
@@ -218,6 +218,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
{ capture_path: nul, output_directory: "/tmp/exported-scripts" },
],
["inspect_pe_resources", { path: nul }],
["inspect_elf_notes", { path: nul }],
["inspect_binary_layout", { path: nul }],
["inspect_recorded_crash", { path: nul }],
["inspect_wasm_artifact", { path: nul }],
@@ -0,0 +1,56 @@
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createElfNotesService } from "../../../src/composition/binaryDiagnostics.js";
import { elfNoteFixture } from "../../../src/native/elf/ElfNotes.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("ELF note stable filesystem boundary", () => {
it("reports non-file and malformed ELF selections as caller input errors", async () => {
const root = await createTestTempDirectory("rea-elf-notes-files-");
const path = join(root, "program");
const alias = join(root, "alias");
const malformed = join(root, "malformed");
await writeFile(path, elfNoteFixture());
await writeFile(malformed, Buffer.from("not an elf"));
await symlink(path, alias);
const service = createElfNotesService();
for (const [candidate, reason] of [
[root, "invalid_format"],
[alias, "invalid_format"],
[malformed, "invalid_format"],
[join(root, "missing"), "invalid_value"],
[join(path, "child"), "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_elf_notes",
issues: [
{
path: ["path"],
reason,
message: expect.any(String),
},
],
});
}
});
it("returns typed resource and cancellation failures", async () => {
const root = await createTestTempDirectory("rea-elf-notes-budget-");
const path = join(root, "program");
await writeFile(path, elfNoteFixture());
const service = createElfNotesService();
const tooSmall = await service.inspect({ path, max_file_bytes: 64 });
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");
});
});
+70
View File
@@ -0,0 +1,70 @@
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 { elfNoteFixture } from "../../../src/native/elf/ElfNotes.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("ELF note inspection must not start a deep provider");
});
const client = await mcp.connect(createServer({ kind: "session", session }));
return { client, session };
};
mcpTest(
"advertises inspect_elf_notes and reads a GNU build-id",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_elf_notes",
);
expect(advertised?.annotations).toMatchObject({
readOnlyHint: false,
idempotentHint: true,
});
const root = await createTestTempDirectory("rea-elf-notes-mcp-");
const path = join(root, "program");
await writeFile(path, elfNoteFixture({ bits: 32, ownership: "section" }));
const response = await client.callTool({
name: "inspect_elf_notes",
arguments: { path },
});
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
const evidence = toolContract("inspect_elf_notes").outputSchema.parse(
response.structuredContent,
);
expect(evidence.normalized_result.format).toBe("elf32");
expect(
evidence.normalized_result.entries.find(
(entry) => entry.type_name === "gnu_build_id",
)?.gnu?.build_id_hex,
).toBe("0123456789abcdef0123456789abcdef01234567");
} finally {
await session.close();
}
},
);
mcpTest("requires an absolute path for inspect_elf_notes", async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const response = await client.callTool({
name: "inspect_elf_notes",
arguments: { path: "program" },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_elf_notes" },
});
} finally {
await session.close();
}
});