Merge remote-tracking branch 'origin/main' into fix/main-red-ida-javascript-tests

This commit is contained in:
Kaoru0822 2026-10-14 02:01:45 +00:00
commit c023db356f
135 changed files with 4057 additions and 731 deletions

View File

@ -164,7 +164,7 @@ Additional tools and host support depend on the target:
| Target | What REA returns | Requirements and guide |
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation 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) |
@ -172,8 +172,8 @@ Additional tools and host support depend on the target:
| 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 |
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](https://rea.tools/guides/javascript/) |
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |

View File

@ -377,6 +377,11 @@ Such a stall leaves the initial page unloaded with empty or placeholder CDP URLs
it does not establish permission to capture that page. See
[the keyring diagnosis](https://github.com/morluto/rea/issues/1384#issuecomment-6093781686).
On Windows, browser verifier lanes that launch an owned fixture require a
standard, non-elevated caller token. They inspect the verifier caller before
allocating fixture resources and refuse elevated or unobservable tokens; they do
not change caller authority or claim to inspect the browser process token.
`verify:browser` also captures a source-owned noise canvas as a real PNG above
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
decoding. Its SDK client explicitly permits the larger inline JSON response;

View File

@ -325,7 +325,8 @@ selected engine. See [provider selection](cli.md#choose-a-provider).
On Linux, REA's shared process supervisor requires a procps-compatible `ps`
on REA's `PATH` for ownership checks before launching a child. Ghidra, JADX,
firmware tools and static JavaScript/Electron analysis use these checks.
Install `procps` or put another procps-compatible `ps` on `PATH` before
Install your distribution's `procps` or `procps-ng` package, or put another
procps-compatible `ps` on `PATH` before
running those workflows. A missing or incompatible command causes a refusal
before the supervised child starts.

View File

@ -137,13 +137,13 @@ npx rea-agents@latest setup
| الهدف | ما يعيده REA | المتطلبات والدليل |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](../native-investigation.md) |
| بنية 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) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-application-workflows.md) |
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](../browser-observation.md) |
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](../web-network-captures.md) |
| التقاط الحزم | نقاط النهاية وDNS وTLS SNI وتوزيع البروتوكولات وطلبات HTTP | tshark وcapinfos يوفّرهما المُستدعي؛ [دليل pcap](../pcap-analysis.md) |
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](../managed-code-analysis.md) |

View File

@ -137,13 +137,13 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| Ziel | Was REA zurückgibt | Voraussetzungen und Anleitung |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](../native-investigation.md) |
| 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) |
| 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) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-application-workflows.md) |
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](../browser-observation.md) |
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](../web-network-captures.md) |
| Paketerfassungen | Endpunkte, DNS, TLS SNI, Protokollverteilung und HTTP-Anfragen | Vom Aufrufer bereitgestellte tshark- und capinfos-Binaries; [pcap-Leitfaden](../pcap-analysis.md) |
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](../managed-code-analysis.md) |

View File

@ -141,7 +141,7 @@ npx rea-agents@latest setup
| Στόχος | Τι επιστρέφει το REA | Απαιτήσεις και οδηγός |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Δυαδικά αρχεία με κώδικα μηχανής | Ψευδοκώδικα, κώδικα assembly, συμβολοσειρές, σύμβολα, κλήσεις και αναφορές | Hopper, Ghidra ή IDA· [ανάλυση κώδικα μηχανής](https://rea.tools/guides/native/) |
| Δυαδικά αρχεία με κώδικα μηχανής | Ψευδοκώδικα, κώδικα assembly, συμβολοσειρές, σύμβολα, κλήσεις και αναφορές | Hopper, Ghidra ή IDA· [ανάλυση κώδικα μηχανής](../native-investigation.md) |
| Δομή 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) |
@ -149,8 +149,8 @@ npx rea-agents@latest setup
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
| Καταγεγραμμένες καταρρεύσεις στο Linux | Ακατέργαστες εγγραφές notes, καταχωρητές/σήματα κάθε καταγεγραμμένου νήματος και προαιρετικές πιθανές αντιστοιχίσεις μνήμης | pwntools που παρέχει ο χρήστης σε Linux x64· προαιρετικά GDB/pwndbg· [καταγεγραμμένες καταρρεύσεις](../recorded-crashes.md) |
| JavaScript / Electron | Modules, εισαγωγές, source maps, διαδρομές, IPC και σχέσεις με πρόσθετα κώδικα μηχανής | Node.js και npm· [ανάλυση εφαρμογών](https://rea.tools/guides/javascript/) |
| Ιστότοποι | Δομή σελίδων, scripts, παρατηρήσεις δικτύου και ζητούμενα στιγμιότυπα οθόνης | Πρόγραμμα περιήγησης της οικογένειας Chrome· [ανάλυση μέσω προγράμματος περιήγησης](https://rea.tools/guides/browser/) |
| JavaScript / Electron | Modules, εισαγωγές, source maps, διαδρομές, IPC και σχέσεις με πρόσθετα κώδικα μηχανής | Node.js και npm· [ανάλυση εφαρμογών](../javascript-application-workflows.md) |
| Ιστότοποι | Δομή σελίδων, scripts, παρατηρήσεις δικτύου και ζητούμενα στιγμιότυπα οθόνης | Πρόγραμμα περιήγησης της οικογένειας Chrome· [ανάλυση μέσω προγράμματος περιήγησης](../browser-observation.md) |
| Αποθηκευμένες καταγραφές δικτύου | Αιτήματα, αποκρίσεις, διαθέσιμα δεδομένα φορτίου και θέσεις στην πηγή | HAR ή SAZ του Fiddler Everywhere· mitmdump σε Linux για την εγγενή μορφή mitmproxy· [οδηγός καταγραφών](../web-network-captures.md) |
| Assemblies .NET | Μεταδεδομένα, εντολές CIL, δηλωμένες εξαρτήσεις από κώδικα μηχανής και συγκρίσεις builds | Καμία πρόσθετη απαίτηση (μόνο στατική ανάλυση)· [οδηγός διαχειριζόμενου κώδικα](../managed-code-analysis.md) |
| APK Android | Δηλώσεις manifest, κλάσεις, απομεταγλωττισμένες μέθοδοι και αναφορές | JADX χωρίς γραφικό περιβάλλον και πλήρες JDK σε Linux/macOS/Windows x64· [οδηγός Android](../android-analysis.md) |

View File

@ -137,13 +137,13 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| Objetivo | Qué devuelve REA | Requisitos y guía |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](../native-investigation.md) |
| 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) |
| 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) |
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
| 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) |
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](../browser-observation.md) |
| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](../web-network-captures.md) |
| Capturas de paquetes | Endpoints, DNS, TLS SNI, distribución de protocolos y solicitudes HTTP | tshark y capinfos suministrados por el autor de la llamada; [guía pcap](../pcap-analysis.md) |
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](../managed-code-analysis.md) |

View File

@ -129,13 +129,13 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np
| هدف | خروجی REA | پیش‌نیازها و راهنما |
| ---------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| باینری‌های بومی | شبه‌کد، اسمبلی، رشته‌ها، نمادها، فراخوانی‌ها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
| باینری‌های بومی | شبه‌کد، اسمبلی، رشته‌ها، نمادها، فراخوانی‌ها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](../native-investigation.md) |
| ساختار 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) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](../recorded-crashes.md) |
| JavaScript / Electron | ماژول‌ها، importها، source mapها، مسیرها، IPC و ارتباط افزونه‌های بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
| وب‌سایت‌ها | ساختار صفحه، اسکریپت‌ها، مشاهدات شبکه و اسکرین‌شات‌های درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
| JavaScript / Electron | ماژول‌ها، importها، source mapها، مسیرها، IPC و ارتباط افزونه‌های بومی | Node.js و npm؛ [تحلیل برنامه](../javascript-application-workflows.md) |
| وب‌سایت‌ها | ساختار صفحه، اسکریپت‌ها، مشاهدات شبکه و اسکرین‌شات‌های درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](../browser-observation.md) |
| داده‌های ضبط‌شدهٔ شبکه | درخواست‌ها، پاسخ‌ها، payloadهای آشکارشده و مکان آن‌ها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبط‌های بومی mitmproxy؛ [راهنمای ضبط شبکه](../web-network-captures.md) |
| اسمبلی‌های .NET | فراداده، دستورالعمل‌های CIL، وابستگی‌های بومی اعلام‌شده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریت‌شده](../managed-code-analysis.md) |
| فایل‌های APK اندروید | تعاریف Manifest، کلاس‌ها، متدهای دیکامپایل‌شده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](../android-analysis.md) |

View File

@ -140,14 +140,14 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| Cible | Ce que REA renvoie | Prérequis et guide |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/fr/guides/native/) |
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](../native-investigation.md) |
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) |
| 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) |
| 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) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
| 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) |
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](../browser-observation.md) |
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](../web-network-captures.md) |
| Captures de paquets | Points de terminaison, DNS, TLS SNI, distribution des protocoles et requêtes HTTP | tshark et capinfos fournis par l'appelant ; [guide pcap](../pcap-analysis.md) |
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](../managed-code-analysis.md) |

View File

@ -143,14 +143,14 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| Target | Hasil yang diberikan REA | Persyaratan dan panduan |
| -------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](https://rea.tools/id/guides/native/) |
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](../native-investigation.md) |
| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) |
| 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) |
| 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) |
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](../javascript-application-workflows.md) |
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](../browser-observation.md) |
| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](../web-network-captures.md) |
| Capturan paket | Endpoint, DNS, TLS SNI, distribusi protokol, dan permintaan HTTP | tshark dan capinfos buatan pemanggil; [panduan pcap](../pcap-analysis.md) |
| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](../managed-code-analysis.md) |

View File

@ -140,14 +140,14 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| Obiettivo | Cosa restituisce REA | Requisiti e guida |
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/it/guides/native/) |
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](../native-investigation.md) |
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) |
| 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) |
| 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) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
| 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) |
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](../browser-observation.md) |
| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](../web-network-captures.md) |
| Captured di pacchetti | Endpoint, DNS, TLS SNI, distribuzione dei protocolli e richieste HTTP | tshark e capinfos forniti dal chiamante; [guida pcap](../pcap-analysis.md) |
| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](../managed-code-analysis.md) |

View File

@ -137,13 +137,13 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| 対象 | REA が返すもの | 要件とガイド |
| -------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](../native-investigation.md) |
| オフライン 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) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、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) |

View File

@ -139,7 +139,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
| --------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](../native-investigation.md) |
| 오프라인 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) |
@ -147,8 +147,8 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](../javascript-application-workflows.md) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](../browser-observation.md) |
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR 또는 Fiddler Everywhere SAZ; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 별도 도구 불필요(정적 검사만); [관리 코드 가이드](../managed-code-analysis.md) |
| Android APK | 매니페스트 선언, 클래스, 디컴파일된 메서드, 참조 | Linux/macOS/Windows x64의 헤드리스 JADX와 전체 JDK; [Android 가이드](../android-analysis.md) |

View File

@ -137,13 +137,13 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| Cel | Co zwraca REA | Wymagania i przewodnik |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](../native-investigation.md) |
| 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) |
| 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) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
| 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) |
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](../browser-observation.md) |
| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](../web-network-captures.md) |
| Przechwytywanie pakietów | Punkty końcowe, DNS, TLS SNI, dystrybucja protokołów i żądania HTTP | Dostarczone przez wywołującego tshark i capinfos; [przewodnik pcap](../pcap-analysis.md) |
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](../managed-code-analysis.md) |

View File

@ -137,13 +137,13 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| Alvo | O que o REA retorna | Requisitos e guia |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](../native-investigation.md) |
| 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) |
| 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) |
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
| 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) |
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](../browser-observation.md) |
| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](../web-network-captures.md) |
| Capturas de pacotes | Endpoints, DNS, TLS SNI, distribuição de protocolos e requisições HTTP | tshark e capinfos fornecidos pelo chamador; [guia pcap](../pcap-analysis.md) |
| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](../managed-code-analysis.md) |

View File

@ -137,13 +137,13 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Цель | Что возвращает REA | Требования и руководство |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](../native-investigation.md) |
| Структура 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) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](../browser-observation.md) |
| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](../web-network-captures.md) |
| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](../managed-code-analysis.md) |
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](../android-analysis.md) |

View File

@ -137,13 +137,13 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| เป้าหมาย | สิ่งที่ REA ส่งกลับ | ข้อกำหนดและคู่มือ |
| ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](../native-investigation.md) |
| โครงสร้าง 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) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-application-workflows.md) |
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](../browser-observation.md) |
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](../web-network-captures.md) |
| การจับแพ็กเก็ต | จุดเชื่อมต่อ, DNS, TLS SNI, การกระจายโปรโตคอล และคำขอ HTTP | tshark และ capinfos ที่ผู้เรียกจัดเตรียม; [คู่มือ pcap](../pcap-analysis.md) |
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](../managed-code-analysis.md) |

View File

@ -139,7 +139,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| Hedef | REA'nın döndürdükleri | Gereksinimler ve rehber |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](../native-investigation.md) |
| Ç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) |
@ -147,8 +147,8 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| 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+ |
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](../recorded-crashes.md) |
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/tr/guides/javascript/) |
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/tr/guides/browser/) |
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](../javascript-application-workflows.md) |
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](../browser-observation.md) |
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR veya Fiddler Everywhere SAZ; Linux'ta native mitmproxy için mitmdump; [ağ yakalama rehberi](../web-network-captures.md) |
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](../managed-code-analysis.md) |
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](../android-analysis.md) |

View File

@ -137,13 +137,13 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Ціль | Що повертає REA | Вимоги й посібник |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](../native-investigation.md) |
| Структура 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) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-application-workflows.md) |
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](../browser-observation.md) |
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](../web-network-captures.md) |
| Захоплення пакетів | Кінцеві точки, DNS, TLS SNI, розподіл протоколів та HTTP-запити | Надані викликаючою стороною tshark та capinfos; [підручник pcap](../pcap-analysis.md) |
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](../managed-code-analysis.md) |

View File

@ -137,13 +137,13 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Mục tiêu | REA trả về gì | Yêu cầu và hướng dẫn |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](../native-investigation.md) |
| 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) |
| 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) |
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
| 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) |
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](../browser-observation.md) |
| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](../web-network-captures.md) |
| Bắt gói tin | Điểm cuối, DNS, TLS SNI, phân bố giao thức và yêu cầu HTTP | tshark và capinfos do nơi gọi cung cấp; [hướng dẫn pcap](../pcap-analysis.md) |
| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](../managed-code-analysis.md) |

View File

@ -139,15 +139,15 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| 目標 | REA 返回的內容 | 要求與指南 |
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-TW/guides/native/) |
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | 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) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
| 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) |

View File

@ -139,15 +139,15 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| 目标 | REA 返回的内容 | 要求与指南 |
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-CN/guides/native/) |
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | 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) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
| 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) |

View File

@ -2,7 +2,7 @@ REA npm dependency inventory
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
License identifiers are upstream lockfile declarations, not a legal assessment.
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
Locked package entries: 719; runtime: 175.
Locked package entries: 721; runtime: 177.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@ -3000,6 +3000,11 @@ safer-buffer@2.1.2 | development | MIT
Package: https://www.npmjs.com/package/safer-buffer/v/2.1.2
Archive: https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz
Integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
saxes@6.0.0 | runtime | ISC
Lockfile path: node_modules/saxes
Package: https://www.npmjs.com/package/saxes/v/6.0.0
Archive: https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz
Integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
search-insights@2.17.3 | development | MIT
Lockfile path: node_modules/search-insights
Package: https://www.npmjs.com/package/search-insights/v/2.17.3
@ -3580,6 +3585,11 @@ xmlbuilder@15.1.1 | runtime | MIT
Package: https://www.npmjs.com/package/xmlbuilder/v/15.1.1
Archive: https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz
Integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
xmlchars@2.2.0 | runtime | MIT
Lockfile path: node_modules/xmlchars
Package: https://www.npmjs.com/package/xmlchars/v/2.2.0
Archive: https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz
Integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
yaml@2.9.0 | runtime | ISC
Lockfile path: node_modules/yaml
Package: https://www.npmjs.com/package/yaml/v/2.9.0

View File

@ -1,7 +1,7 @@
REA third-party npm runtime copyright, license and NOTICE texts
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
Runtime dependency fingerprint (SHA-256): b0d0151958456c6e72c5720af80d783893be88a7b4c9a409db8505296fe10bd9
Runtime dependency fingerprint (SHA-256): 49b181923ab240c68c64d35ecdd8f93cb5cd516c765f2282d0e0eca0bcadd13e
===== @babel/helper-string-parser@8.0.6 =====
Declared license: MIT
@ -8441,3 +8441,108 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
===== saxes@6.0.0 =====
Declared license: ISC
Archive: https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz
Integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
Scope: npm runtime dependency, including optional platform packages.
The locked npm archive omits LICENSE; preserved from the matching v6.0.0 upstream tag.
Source: https://raw.githubusercontent.com/lddubeau/saxes/v6.0.0/LICENSE
SHA-256: 0fac2374380621b22e6b50451057721a9c52935b02d16d106a9f04897f061d0e
--- upstream LICENSE ---
The ISC License
Copyright (c) Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
====
The following license is the one that governed sax, from which saxes
was forked. Isaac Schlueter is not *directly* involved with saxes so
don't go bugging him for saxes issues.
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
====
`String.fromCodePoint` by Mathias Bynens is no longer used, but it can
still be found in old commits. It was once used according to terms of
MIT License, as follows:
Copyright Mathias Bynens <https://mathiasbynens.be/>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
===== xmlchars@2.2.0 =====
Declared license: MIT
Archive: https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz
Integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
Scope: npm runtime dependency, including optional platform packages.
--- package/LICENSE ---
Copyright Louis-Dominique Dubeau and contributors to xmlchars
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
package-lock.json generated
View File

@ -38,6 +38,7 @@
"pino": "10.4.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
"saxes": "6.0.0",
"semver": "7.8.5",
"smol-toml": "1.9.0",
"stream-chain": "4.2.6",
@ -8672,6 +8673,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/search-insights": {
"version": "2.17.3",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
@ -10585,6 +10598,12 @@
"node": ">=8.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",

View File

@ -341,6 +341,7 @@
"pino": "10.4.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
"saxes": "6.0.0",
"semver": "7.8.5",
"smol-toml": "1.9.0",
"stream-chain": "4.2.6",

View File

@ -0,0 +1,7 @@
export interface BrowserVerifierSite {
readonly origin: string;
triggerSessionNavigation(): void;
close(): Promise<void>;
}
export function startBrowserVerifierSite(): Promise<BrowserVerifierSite>;

View File

@ -184,11 +184,13 @@ export async function startBrowserVerifierSite() {
async close() {
for (const client of webSockets.clients) client.terminate();
await new Promise((resolve) => webSockets.close(resolve));
await new Promise((resolve, reject) =>
const serverClosed = new Promise((resolve, reject) =>
server.close((error) =>
error === undefined ? resolve() : reject(error),
),
);
server.closeAllConnections();
await serverClosed;
},
};
}

View File

@ -14,36 +14,42 @@ import { webModuleTraceResultSchema } from "../../dist/domain/webModuleTrace.js"
import { NativeModuleResolver } from "../../dist/browser/modules/NativeModuleResolver.js";
import { startBrowserModuleSite } from "../fixtures/browser-module-site.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
import { closeRuntimeFixtureResources } from "./browser-runtime-fixture-lifecycle.mjs";
/** Check native URL resolution against an independently loaded module graph through CLI/MCP. */
export async function verifyBrowserModules(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
const site = await startBrowserModuleSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
REA_BROWSER_EXECUTABLE: executable,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "module-real-e2e", version: "1" });
let site;
let root;
let transport;
let client;
let primaryError;
try {
site = await startBrowserModuleSite();
root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
REA_BROWSER_EXECUTABLE: executable,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
client = new Client({ name: "module-real-e2e", version: "1" });
const oracle = await loadingOracle(executable, site);
const scenario = {
browser: { mode: "launch", executable_path: executable },
@ -298,11 +304,17 @@ export async function verifyBrowserModules(
map_identity_verified: true,
implicit_refetches: 0,
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
const cleanup = [];
if (client !== undefined) cleanup.push(() => client.close());
if (transport !== undefined) cleanup.push(() => transport.close());
if (site !== undefined) cleanup.push(() => site.close());
if (root !== undefined)
cleanup.push(() => rm(root, { recursive: true, force: true }));
await closeRuntimeFixtureResources(cleanup, primaryError);
}
}

View File

@ -14,6 +14,7 @@ import {
} from "../fixtures/browser-network-site.mjs";
import { assertBrowserNetworkEvidence } from "./browser-network-assertions.mjs";
import { scenarioProfiles } from "./browser-scenario-verifier.mjs";
import { closeRuntimeFixtureResources } from "./browser-runtime-fixture-lifecycle.mjs";
/** Exercise selected network content through real CLI and stdio MCP, one browser at a time. */
export async function verifyBrowserNetworkEvidence(
@ -21,9 +22,14 @@ export async function verifyBrowserNetworkEvidence(
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
attachment,
) {
const site = await startBrowserNetworkSite();
const profilesBefore = await scenarioProfiles();
let site;
let profilesBefore;
let transport;
let client;
let primaryError;
try {
site = await startBrowserNetworkSite();
profilesBefore = await scenarioProfiles();
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: executable },
start_url: { url: site.origin },
@ -97,58 +103,50 @@ export async function verifyBrowserNetworkEvidence(
() => assertBrowserNetworkEvidence(swapped, site.origin),
/associated with a different same-URL request/,
);
const transport = new StdioClientTransport({
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "network-real-e2e", version: "1" });
client = new Client({ name: "network-real-e2e", version: "1" });
let mcpProof;
try {
await client.connect(transport);
const result = await client.callTool(
{ name: "capture_browser_scenario", arguments: scenario },
await client.connect(transport);
const result = await client.callTool(
{ name: "capture_browser_scenario", arguments: scenario },
{ timeout: 60_000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const evidence = parseEvidence(result.structuredContent);
mcpProof = assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
site.origin,
);
if (attachment !== undefined) {
const connectedScenario = browserScenarioSchema.parse({
...scenario,
browser: { mode: "connect", ...attachment },
});
const connected = await client.callTool(
{ name: "capture_browser_scenario", arguments: connectedScenario },
{ timeout: 60_000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const evidence = parseEvidence(result.structuredContent);
mcpProof = assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
site.origin,
);
if (attachment !== undefined) {
const connectedScenario = browserScenarioSchema.parse({
...scenario,
browser: { mode: "connect", ...attachment },
});
const connected = await client.callTool(
{ name: "capture_browser_scenario", arguments: connectedScenario },
{ timeout: 60_000 },
);
assert.notEqual(connected.isError, true, JSON.stringify(connected));
const connectedEvidence = parseEvidence(connected.structuredContent);
assert.deepEqual(
assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(
connectedEvidence.normalized_result,
),
site.origin,
"disconnected-external",
assert.notEqual(connected.isError, true, JSON.stringify(connected));
const connectedEvidence = parseEvidence(connected.structuredContent);
assert.deepEqual(
assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(
connectedEvidence.normalized_result,
),
cliProof,
);
assert.ok(
(await fetch(`${attachment.cdp_endpoint}/json/version`)).ok,
"Network capture closed the external browser",
);
}
} finally {
try {
await client.close();
} finally {
await transport.close();
}
site.origin,
"disconnected-external",
),
cliProof,
);
assert.ok(
(await fetch(`${attachment.cdp_endpoint}/json/version`)).ok,
"Network capture closed the external browser",
);
}
assert.deepEqual(mcpProof, cliProof);
for (const path of [
@ -180,7 +178,14 @@ export async function verifyBrowserNetworkEvidence(
profile_cleanup: true,
connected_network_verified: attachment !== undefined,
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await site.close();
const cleanup = [];
if (client !== undefined) cleanup.push(() => client.close());
if (transport !== undefined) cleanup.push(() => transport.close());
if (site !== undefined) cleanup.push(() => site.close());
await closeRuntimeFixtureResources(cleanup, primaryError);
}
}

View File

@ -14,6 +14,7 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
import { webExecutionSchema } from "../../dist/domain/webExecution.js";
import { webEventListenersSchema } from "../../dist/domain/webEventListeners.js";
import { startBrowserRuntimeSite } from "../fixtures/browser-runtime-site.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./windows-ordinary-user-verifier.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
import {
startRuntimeFixtureBrowser,
@ -37,6 +38,7 @@ export async function verifyBrowserRuntime(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
await requireOrdinaryWindowsVerifierCaller();
await access(executable);
const site = await startBrowserRuntimeSite();
let fixture;

View File

@ -12,9 +12,11 @@ import {
BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
waitForBrowserDevtoolsPort,
} from "../../dist/browser/BrowserProcessStartup.js";
import { requireOrdinaryWindowsVerifierCaller } from "./windows-ordinary-user-verifier.mjs";
/** Launch the synthetic fixture browser and await CDP in its own verified process group. */
export async function startRuntimeFixtureBrowser(executable, origin) {
await requireOrdinaryWindowsVerifierCaller();
const profile = await mkdtemp(join(tmpdir(), "rea-real-web-runtime-"));
let owned;
try {

View File

@ -15,6 +15,7 @@ import {
assertExportedScriptAnalysis,
} from "./browser-script-export-assertions.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
import { closeRuntimeFixtureResources } from "./browser-runtime-fixture-lifecycle.mjs";
/** Verify capture → export → existing analysis through real CLI and stdio MCP. */
export async function verifyBrowserScriptExport(
@ -22,24 +23,29 @@ export async function verifyBrowserScriptExport(
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
attachment,
) {
const site = await startBrowserScriptSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-script-export-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = exportCli(entrypoint, env);
const scenario = scriptCaptureScenario(executable, site.origin);
const save = (name, capture) => saveCapture(root, name, capture);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "web-script-real-e2e", version: "1" });
let site;
let root;
let transport;
let client;
let primaryError;
try {
site = await startBrowserScriptSite();
root = await mkdtemp(join(tmpdir(), "rea-browser-script-export-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = exportCli(entrypoint, env);
const scenario = scriptCaptureScenario(executable, site.origin);
const save = (name, capture) => saveCapture(root, name, capture);
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
client = new Client({ name: "web-script-real-e2e", version: "1" });
const cliCapture = await cli(
"capture-browser-scenario",
JSON.stringify(scenario),
@ -103,11 +109,17 @@ export async function verifyBrowserScriptExport(
query_variants_isolated: true,
implicit_refetches: 0,
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
const cleanup = [];
if (client !== undefined) cleanup.push(() => client.close());
if (transport !== undefined) cleanup.push(() => transport.close());
if (site !== undefined) cleanup.push(() => site.close());
if (root !== undefined)
cleanup.push(() => rm(root, { recursive: true, force: true }));
await closeRuntimeFixtureResources(cleanup, primaryError);
}
}

View File

@ -12,35 +12,41 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js";
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
import { closeRuntimeFixtureResources } from "./browser-runtime-fixture-lifecycle.mjs";
/** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */
export async function verifyBrowserSourceMap(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
const site = await startBrowserSourceMapSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "source-map-real-e2e", version: "1" });
let site;
let root;
let transport;
let client;
let primaryError;
try {
site = await startBrowserSourceMapSite();
root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
client = new Client({ name: "source-map-real-e2e", version: "1" });
const capture = await cli(
"capture-browser-scenario",
JSON.stringify({
@ -126,11 +132,17 @@ export async function verifyBrowserSourceMap(
selected_inert_userinfo_context_preserved: true,
public_point_cases: 4,
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
const cleanup = [];
if (client !== undefined) cleanup.push(() => client.close());
if (transport !== undefined) cleanup.push(() => transport.close());
if (site !== undefined) cleanup.push(() => site.close());
if (root !== undefined)
cleanup.push(() => rm(root, { recursive: true, force: true }));
await closeRuntimeFixtureResources(cleanup, primaryError);
}
}
function assertPoint(evidence, { site, input, capture, identity }) {

View File

@ -10,6 +10,7 @@ import { verifyNoTokenOwnedProcesses } from "../../dist/process/ProcessOwnership
import {
observeProcessStartIdentity,
signalProcessWithStartIdentity,
systemProcessOwnershipHost,
} from "../../dist/process/ProcessOwnershipObservation.js";
import { startUnrelatedHopperSentinel } from "./unrelated-hopper-sentinel.mjs";
import {
@ -31,6 +32,11 @@ export async function verifyLinuxHopperOwnerExit({
const outcomes = [];
try {
for (const mode of ["graceful", "SIGKILL"]) {
const baseline = await systemProcessOwnershipHost.captureBaseline?.();
assert.ok(
baseline,
"Could not capture pre-launch process ownership baseline",
);
const transport = new StdioClientTransport({
command: serverCommand,
args: serverArgs,
@ -47,6 +53,18 @@ export async function verifyLinuxHopperOwnerExit({
assert.ok(ownerPid !== null);
const owner = await observeProcessStartIdentity(ownerPid);
assert.equal(owner?.state, "readable");
const ownerProcess = (
await systemProcessOwnershipHost.listProcesses()
).find(({ pid }) => pid === ownerPid);
assert.ok(
ownerProcess,
"Could not observe the launched MCP process group",
);
assert.equal(typeof ownerProcess.processGroupId, "number");
const relation = {
leaderPid: ownerPid,
processGroupId: ownerProcess.processGroupId,
};
requireMcpLifecycleResult(
await client.callTool(
{
@ -75,7 +93,17 @@ export async function verifyLinuxHopperOwnerExit({
const runId = status.analysis_run?.run_id;
assert.equal(typeof runId, "string");
assert.equal(status.analysis_provider_binding?.provider.id, "hopper");
assert.equal((await verifyNoTokenOwnedProcesses(runId)).cleaned, false);
assert.equal(
(
await verifyNoTokenOwnedProcesses(
runId,
systemProcessOwnershipHost,
baseline,
relation,
)
).cleaned,
false,
);
if (mode === "graceful") await client.close();
else {
assert.equal(
@ -90,7 +118,12 @@ export async function verifyLinuxHopperOwnerExit({
const deadline = Date.now() + 10_000;
let cleanup;
do {
cleanup = await verifyNoTokenOwnedProcesses(runId);
cleanup = await verifyNoTokenOwnedProcesses(
runId,
systemProcessOwnershipHost,
baseline,
relation,
);
if (cleanup.cleaned) break;
await delay(100);
} while (Date.now() < deadline);

View File

@ -39,3 +39,8 @@ export function createVerifierRun(): VerifierRunStart;
export function completeVerifierRun(
run: VerifierRunStart,
): Promise<VerifierRun>;
/** Require positive process-lineage evidence before claiming cleanup complete. */
export function assertVerifierCleanupComplete(
processLineage: VerifierProcessLineage,
): void;

View File

@ -33,6 +33,16 @@ export const completeVerifierRun = async (run) => {
};
};
/** Require positive process-lineage evidence before claiming cleanup complete. */
export const assertVerifierCleanupComplete = (processLineage) => {
if (processLineage.status !== "verified")
throw new Error(
`Verifier cleanup could not be verified: ${processLineage.reason}`,
);
if (processLineage.descendants.length !== 0)
throw new Error("Owned descendants survived verifier cleanup");
};
const observeVerifierLineage = async (run, observedAt) => {
if (process.platform === "win32")
return unavailableLineage(

View File

@ -0,0 +1,4 @@
export declare const assertOrdinaryWindowsVerifierCaller: (
token: unknown,
) => void;
export declare const requireOrdinaryWindowsVerifierCaller: () => Promise<void>;

View File

@ -0,0 +1,40 @@
/** Require fixture verifiers on Windows to use the caller's ordinary-user token. */
export const assertOrdinaryWindowsVerifierCaller = (token) => {
if (
typeof token !== "object" ||
token === null ||
!("administrator" in token) ||
!("elevated" in token) ||
token.administrator !== false ||
token.elevated !== false
) {
const integrity =
typeof token === "object" &&
token !== null &&
"integrityLevel" in token &&
Number.isSafeInteger(token.integrityLevel)
? ` (integrity level ${String(token.integrityLevel)})`
: "";
throw new Error(
"Windows browser verifier requires an ordinary, non-elevated user token" +
`${integrity}. Rerun it from a standard-user Windows session; the verifier will not change caller authority.`,
);
}
};
/** Observe the native caller token before a Windows browser fixture allocates resources. */
export const requireOrdinaryWindowsVerifierCaller = async () => {
if (process.platform !== "win32") return;
let token;
try {
const { requireWindowsNativeAuthority } =
await import("../../dist/windows/WindowsNativeLoader.js");
token = requireWindowsNativeAuthority().call("process_caller_token", []);
} catch (cause) {
throw new Error(
"Windows browser verifier could not observe its caller token; use the verified Windows x64 native artifact and rerun from a standard-user session.",
{ cause },
);
}
assertOrdinaryWindowsVerifierCaller(token);
};

View File

@ -6,15 +6,21 @@ import { join } from "node:path";
import { chromium } from "playwright-core";
import { startBrowserVerifierSite } from "./fixtures/browser-verifier-site.mjs";
import { verifyBrowserDomDestinations } from "./lib/browser-dom-destinations-e2e.mjs";
import { closeRuntimeFixtureResources } from "./lib/browser-runtime-fixture-lifecycle.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./lib/windows-ordinary-user-verifier.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (!executable)
throw new Error("verify:browser:dom requires REA_BROWSER_EXECUTABLE");
const profile = await mkdtemp(join(tmpdir(), "rea-browser-dom-"));
const site = await startBrowserVerifierSite();
await requireOrdinaryWindowsVerifierCaller();
let profile;
let site;
let context;
let report;
let primaryError;
try {
profile = await mkdtemp(join(tmpdir(), "rea-browser-dom-"));
site = await startBrowserVerifierSite();
context = await chromium.launchPersistentContext(profile, {
executablePath: executable,
headless: true,
@ -50,9 +56,15 @@ try {
native_oracle: true,
verified: true,
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await context?.close();
await site.close();
await rm(profile, { recursive: true, force: true });
const cleanup = [];
if (context !== undefined) cleanup.push(() => context.close());
if (site !== undefined) cleanup.push(() => site.close());
if (profile !== undefined)
cleanup.push(() => rm(profile, { recursive: true, force: true }));
await closeRuntimeFixtureResources(cleanup, primaryError);
}
process.stdout.write(`${JSON.stringify(report)}\n`);

View File

@ -2,12 +2,14 @@
import { access } from "node:fs/promises";
import { verifyBrowserModules } from "./lib/browser-module-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./lib/windows-ordinary-user-verifier.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (!executable)
throw new Error(
"verify:browser:modules requires REA_BROWSER_EXECUTABLE for caller-supplied Chromium",
);
await requireOrdinaryWindowsVerifierCaller();
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserModules(executable, process.argv[2]);

View File

@ -2,12 +2,14 @@
import { access } from "node:fs/promises";
import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./lib/windows-ordinary-user-verifier.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (executable === undefined || executable === "")
throw new Error(
"verify:browser:network requires REA_BROWSER_EXECUTABLE pointing to an installed Chrome-family browser",
);
await requireOrdinaryWindowsVerifierCaller();
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserNetworkEvidence(executable, process.argv[2]);

View File

@ -2,12 +2,14 @@
import { access } from "node:fs/promises";
import { verifyBrowserScriptExport } from "./lib/browser-script-export-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./lib/windows-ordinary-user-verifier.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (executable === undefined || executable === "")
throw new Error(
"verify:browser:scripts requires REA_BROWSER_EXECUTABLE pointing to an installed Chrome-family browser",
);
await requireOrdinaryWindowsVerifierCaller();
await access(executable);
const run = createVerifierRun();
const proof = await verifyBrowserScriptExport(executable, process.argv[2]);

View File

@ -4,6 +4,9 @@ import { isAbsolute } from "node:path";
import { verifyBrowserSourceMap } from "./lib/browser-source-map-e2e.mjs";
import { verifyBrowserNestedSourceMaps } from "./lib/browser-nested-source-map-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./lib/windows-ordinary-user-verifier.mjs";
await requireOrdinaryWindowsVerifierCaller();
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (!executable || !isAbsolute(executable))
throw new Error(

View File

@ -4,3 +4,6 @@ export function assertClientConfig(
configPath: string,
original: Uint8Array,
): Promise<void>;
/** Return true only when stty output positively reports canonical input and echo. */
export function terminalModesRestored(termios: unknown): boolean;

View File

@ -22,6 +22,12 @@ import {
import { parse as parseJsonc } from "jsonc-parser";
import { skillReferenceIssues } from "./lib/docs-facts.mjs";
export const terminalModesRestored = (termios) => {
if (typeof termios !== "string") return false;
const modes = new Set(termios.trim().split(/\s+/u));
return modes.has("icanon") && modes.has("echo");
};
const OPENCODE_ORIGINAL = `{
// OpenCode preferences must survive setup.
"model": "provider/model",
@ -131,8 +137,7 @@ const verifyInteractiveSetup = async ({
childResult?.status !== 0 ||
!output.includes("\u001b[?25h") ||
(process.platform !== "win32" &&
(!childResult.termios?.includes("icanon") ||
!childResult.termios?.includes("echo"))) ||
!terminalModesRestored(childResult.termios)) ||
!output.includes("No changes were made.")
) {
finish(

View File

@ -39,6 +39,8 @@ import {
verifyScenarioFailureEvidence,
} from "./lib/browser-scenario-verifier.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { closeRuntimeFixtureResources } from "./lib/browser-runtime-fixture-lifecycle.mjs";
import { requireOrdinaryWindowsVerifierCaller } from "./lib/windows-ordinary-user-verifier.mjs";
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
@ -58,13 +60,17 @@ process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE;
process.env.REA_BROWSER_VERIFIER_URL = SCENARIO_URL_SECRET_VALUE;
const verifierRun = createVerifierRun();
await requireOrdinaryWindowsVerifierCaller();
const executable = await browserExecutable();
const profile = await mkdtemp(join(tmpdir(), "rea-real-browser-"));
const site = await startBrowserVerifierSite();
let profile;
let site;
let browser;
let pageProxy;
let report;
let verificationError;
try {
profile = await mkdtemp(join(tmpdir(), "rea-real-browser-"));
site = await startBrowserVerifierSite();
browser = spawn(
executable,
[
@ -409,19 +415,39 @@ try {
scenarioServiceWorkers,
verified: true,
};
} catch (cause) {
verificationError = cause;
throw cause;
} finally {
process.stderr.write("Browser verifier cleanup: closing page proxy\n");
if (pageProxy !== undefined) await pageProxy.close();
process.stderr.write("Browser verifier cleanup: stopping owned Chrome\n");
if (browser !== undefined) await stopProcess(browser);
process.stderr.write("Browser verifier cleanup: closing fixture site\n");
await site.close();
await rm(profile, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 100,
});
const cleanup = [];
if (pageProxy !== undefined) {
cleanup.push(async () => {
process.stderr.write("Browser verifier cleanup: closing page proxy\n");
await pageProxy.close();
});
}
if (browser !== undefined) {
cleanup.push(async () => {
process.stderr.write("Browser verifier cleanup: stopping owned Chrome\n");
await stopProcess(browser);
});
}
if (site !== undefined) {
cleanup.push(async () => {
process.stderr.write("Browser verifier cleanup: closing fixture site\n");
await site.close();
});
}
if (profile !== undefined)
cleanup.push(() =>
rm(profile, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 100,
}),
);
await closeRuntimeFixtureResources(cleanup, verificationError);
}
process.stdout.write(
`${JSON.stringify({

View File

@ -24,7 +24,11 @@ import {
} from "../dist/domain/hopperValues.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { buildDosMzFixture } from "../tests/conformance/ghidra/dos-mz-fixture.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import {
assertVerifierCleanupComplete,
createVerifierRun,
completeVerifierRun,
} from "./lib/verifier-run.mjs";
import { verifyGhidraAddressBoundaries } from "./lib/real-ghidra-address-boundaries.mjs";
// This lane intentionally has no game paths, binary seeds or compiler dependency.
@ -479,12 +483,7 @@ try {
}
assert.ok(report, "No DOS proof report was produced");
const verifierRun = await completeVerifierRun(run);
if (verifierRun.process_lineage.status === "verified")
assert.deepEqual(
verifierRun.process_lineage.descendants,
[],
"Owned descendants survived verifier cleanup",
);
assertVerifierCleanupComplete(verifierRun.process_lineage);
process.stdout.write(
`${JSON.stringify({ verifier_run: verifierRun, ...report, cleanup: "complete" })}\n`,
);

View File

@ -2,8 +2,16 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile, readdir } from "node:fs/promises";
import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { isAbsolute, join } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
@ -51,11 +59,32 @@ const tool = (name, args, cwd) =>
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const run = createVerifierRun();
const root = await PrivateRuntimeRoot.create({ prefix: "rea-wabt-verifier-" });
// Keep provider scratch outside the leased private input root. On Windows the
// root's retained DELETE handle blocks child temp creation beneath that root.
let childTempRoot;
try {
childTempRoot = await mkdtemp(join(tmpdir(), "rea-wabt-child-temp-"));
} catch (cause) {
try {
await root.close();
} catch (cleanupCause) {
throw new AggregateError(
[cause, cleanupCause],
"Could not allocate verifier temp space or release its private root.",
);
}
throw cause;
}
const client = new Client({ name: "wabt-artifact-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: { ...environment, TMPDIR: root.path, TMP: root.path, TEMP: root.path },
env: {
...environment,
TMPDIR: childTempRoot,
TMP: childTempRoot,
TEMP: childTempRoot,
},
stderr: "pipe",
});
let cases = 0;
@ -95,9 +124,9 @@ try {
{
env: {
...environment,
TMPDIR: root.path,
TMP: root.path,
TEMP: root.path,
TMPDIR: childTempRoot,
TMP: childTempRoot,
TEMP: childTempRoot,
},
timeout: 120_000,
maxBuffer: 64 * 1024 * 1024,
@ -388,13 +417,26 @@ try {
}
try {
assert.ok(
!(await readdir(root.path)).some((name) => name.startsWith("rea-wabt-")),
!(await readdir(childTempRoot)).some((name) =>
name.startsWith("rea-wabt-"),
),
"Provider workspace leaked",
);
} catch (cause) {
failures.push(cause);
}
// Each directory is an independent owner. Attempt every cleanup even if a
// residue assertion or another owner's close fails, retaining every error.
try {
await root.close();
} catch (cause) {
failures.push(cause);
}
try {
await rm(childTempRoot, { recursive: true, force: true });
} catch (cause) {
failures.push(cause);
}
if (failures.length > 0)
throw new AggregateError(failures, "WABT real-tool verification failed.");
console.log(

View File

@ -24,7 +24,6 @@ import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { err, ok } from "../../domain/result.js";
import { EvidenceLedger } from "../investigation/EvidenceLedger.js";
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js";
it.each(["/fixtures/unknown.exe", ""])(
"preserves recorded empty native identity fields: path=%j",
@ -433,26 +432,39 @@ it("refuses an object-dense borrowed capture before making a full schema clone",
expect(ledger.forAnalysis(parent.evidence_id)).toBe(borrowed);
});
it("borrows inline Evidence until admission without invoking nested accessors", () => {
let accessed = false;
const evidence = {
get normalized_result() {
accessed = true;
throw new Error("Evidence must be admitted before parsing");
},
};
const input = {
source: { kind: "inline", evidence },
view: { kind: "api-surface" },
};
const parsed = inspectAnalysisViewInputSchema.parse(input);
if (parsed.source.kind !== "inline") throw new Error("Expected inline input");
expect(parsed.source.evidence).toBe(evidence);
expect(inspectAnalysisView(input)).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(accessed).toBe(false);
it("rejects accessor-backed inline Evidence before all view projections", () => {
const views = [
{ kind: "summary" },
{ kind: "capture" },
{ kind: "api-surface" },
] as const;
for (const view of views) {
let accessed = false;
const evidence = {
get normalized_result() {
accessed = true;
throw new Error("Evidence accessor must not be invoked");
},
};
const result = inspectAnalysisView({
source: { kind: "inline", evidence },
view,
});
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(accessed).toBe(false);
}
const parent = analysisViewCaptureEvidence();
for (const view of views)
expect(
inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view,
}).ok,
).toBe(true);
});
it("keeps inline-envelope admission live alongside capture validation and grouping", () => {

View File

@ -5,7 +5,10 @@ import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import { resolveEvidenceInput } from "../EvidenceInputResolver.js";
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import {
@ -159,6 +162,18 @@ const resolveAnalysisViewSource = (
): Result<Evidence, AnalysisError> => {
if (source.kind === "retained-evidence")
return resolveEvidenceInput(source, lookup);
const invalidDataPath = inlineEvidenceInvalidDataPath(source.evidence);
if (invalidDataPath !== undefined)
return err(
new AnalysisInputError(INSPECT_ANALYSIS_VIEW_OPERATION, undefined, [
{
path: ["source", "evidence", ...invalidDataPath],
reason: "invalid_type",
message:
"Inline Evidence must contain plain JSON data properties without accessors or cycles.",
},
]),
);
try {
return ok(parseEvidence(source.evidence));
} catch (cause: unknown) {
@ -185,6 +200,53 @@ const resolveAnalysisViewSource = (
}
};
/** Reject non-JSON object behavior before Zod reads caller-owned Evidence. */
const inlineEvidenceInvalidDataPath = (
root: unknown,
): readonly (string | number)[] | undefined => {
const active = new Set<object>();
const pending: {
readonly value: unknown;
readonly path: readonly (string | number)[];
readonly exit?: object;
}[] = [{ value: root, path: [] }];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) continue;
if (current.exit !== undefined) {
active.delete(current.exit);
continue;
}
const { value, path } = current;
if (typeof value !== "object" || value === null) continue;
const isArray = Array.isArray(value);
const prototype = Object.getPrototypeOf(value);
if (
(isArray && prototype !== Array.prototype) ||
(!isArray && prototype !== Object.prototype && prototype !== null) ||
active.has(value)
)
return path;
active.add(value);
pending.push({ value, path, exit: value });
const keys = Object.getOwnPropertyNames(value);
for (let index = keys.length - 1; index >= 0; index -= 1) {
const key = keys[index];
if (key === undefined) continue;
const descriptor = Object.getOwnPropertyDescriptor(value, key);
const childPath = [
...path,
isArray && /^(0|[1-9]\d*)$/u.test(key) ? Number(key) : key,
];
if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
return childPath;
pending.push({ value: descriptor.value, path: childPath });
}
}
return undefined;
};
const createAnalysisViewEvidence = (
input: InspectAnalysisViewInput,
parent: Evidence,

View File

@ -116,9 +116,7 @@ describe("BinarySession open liveness", () => {
expect(deep.created).toHaveLength(2);
});
it("falls back to the health probe when the child reports no operation health", async () => {
// Without an operation-health hook there is no child-liveness signal,
// so the probe keeps the previous `health` admission behavior.
it("probes instantiated children through health when snapshots are absent", async () => {
const { session, deep } = openSession({ operationHealth: false });
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
@ -126,9 +124,33 @@ describe("BinarySession open liveness", () => {
const second = await session.open(process.execPath);
expect(second.ok).toBe(true);
// The composite's `health` answers ok, so the live client is reused.
// The composite probes this already-instantiated child rather than
// answering health without contacting it.
expect(deep.created).toHaveLength(1);
expect(deep.created[0]?.closeCalled).toBe(false);
expect(deep.created[0]?.healthCalls).toBe(1);
});
it("retires a dead no-snapshot child after its health probe fails", async () => {
const { session, deep } = openSession({ operationHealth: false });
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
const dead = deep.created[0];
dead?.kill();
expect((await session.open(process.execPath)).ok).toBe(true);
expect(dead?.healthCalls).toBe(1);
expect(dead?.closeCalled).toBe(true);
expect(deep.created).toHaveLength(1);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(2);
});
it("does not instantiate a child to answer health before first use", async () => {
const { session, deep } = openSession({ operationHealth: false });
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.open(process.execPath)).ok).toBe(true);
expect(deep.created).toEqual([]);
});
it("reports cancellation without touching the session when aborted mid-probe", async () => {
@ -167,6 +189,7 @@ describe("BinarySession open liveness", () => {
interface KillableChild extends AnalysisClient {
kill(state?: "exited" | "unreachable" | "unknown" | "not_started"): void;
closeCalled: boolean;
healthCalls: number;
}
const deepIdentity: ProviderIdentity = {
@ -269,10 +292,12 @@ const killableDeepProvider = (options: {
createClient: (): KillableChild => {
let alive = true;
let closeCalled = false;
let healthCalls = 0;
let deadState: "exited" | "unreachable" | "unknown" | "not_started" =
"exited";
const client: KillableChild = {
execute: (operation) => {
if (operation === "health") healthCalls += 1;
return alive
? Promise.resolve(
ok(
@ -313,6 +338,9 @@ const killableDeepProvider = (options: {
get closeCalled() {
return closeCalled;
},
get healthCalls() {
return healthCalls;
},
kill: (
state:
| "exited"

View File

@ -0,0 +1,129 @@
import { expect, it, vi } from "vitest";
import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisOperation,
type AnalysisProvider,
type CapabilityDescriptor,
type ProviderIdentity,
} from "../AnalysisProvider.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err, ok } from "../../domain/result.js";
import { CompositeProvider } from "./CompositeProvider.js";
const identity = (id: string): ProviderIdentity => ({
id,
name: id,
version: "1",
});
const operationCapability = (
provider: ProviderIdentity,
operation: Exclude<AnalysisOperation, "health">,
): CapabilityDescriptor => ({
provider,
operation,
available: true,
reason: null,
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
});
const failingProvider = (
id: string,
operation: Exclude<AnalysisOperation, "health">,
resource: string,
) => {
const providerIdentity = identity(id);
const cleanupError = new ProviderCleanupError(id, [resource], {
reason: `${id} cleanup is unresolved`,
child_marker: `${id}-detail`,
});
const close = vi.fn(async () => err(cleanupError));
const provider: AnalysisProvider = {
identity: () => providerIdentity,
capabilities: () => [operationCapability(providerIdentity, operation)],
createClient: (): AnalysisClient => ({
execute: (name) =>
Promise.resolve(
ok(createAnalysisExecution(`${id}:${name}`, providerIdentity)),
),
close,
}),
};
return { provider, close, cleanupError };
};
const executableTarget = {
path: "/tmp/fixture",
sha256: "a".repeat(64),
kind: "executable",
format: "elf",
architecture: "x86_64",
availableArchitectures: ["x86_64"],
} as const;
it("retains every failed child resource across composite cleanup retries", async () => {
const left = failingProvider("provider-left", "address_name", "left-db");
const right = failingProvider("provider-right", "procedure_info", "right-db");
const client = new CompositeProvider([
left.provider,
right.provider,
]).createClient(executableTarget);
expect((await client.execute("address_name", {})).ok).toBe(true);
expect((await client.execute("procedure_info", {})).ok).toBe(true);
for (let attempt = 0; attempt < 2; attempt += 1) {
const closed = await client.close();
expect(closed.ok).toBe(false);
if (closed.ok) throw new Error("Expected composite cleanup failure");
expect(closed.error).toMatchObject({
cleanupIncomplete: true,
cleanupResources: ["left-db", "right-db"],
diagnostics: {
failures: [
{ provider_id: "provider-left", resources: ["left-db"] },
{ provider_id: "provider-right", resources: ["right-db"] },
],
},
});
expect(closed.error).toBeInstanceOf(ProviderCleanupError);
expect(closed.error.diagnostics).toMatchObject({
failures: [
{
diagnostics: { reason: "provider-left cleanup is unresolved" },
},
{
diagnostics: { reason: "provider-right cleanup is unresolved" },
},
],
});
}
expect(left.close).toHaveBeenCalledTimes(2);
expect(right.close).toHaveBeenCalledTimes(2);
});
it("preserves one child's typed cleanup error unchanged", async () => {
const child = failingProvider("provider-only", "address_name", "one-db");
const client = new CompositeProvider([child.provider]).createClient(
executableTarget,
);
expect((await client.execute("address_name", {})).ok).toBe(true);
const closed = await client.close();
expect(closed.ok).toBe(false);
if (closed.ok) throw new Error("Expected child cleanup failure");
expect(closed.error).toBe(child.cleanupError);
});

View File

@ -1,4 +1,6 @@
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { err, ok } from "../../domain/result.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
@ -7,6 +9,7 @@ import type {
AnalysisClientContext,
AnalysisProvider,
CapabilityDescriptor,
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
@ -24,6 +27,88 @@ const compositeProviderIdentity = (
version: null,
});
const closeCompositeClients =
(
identity: ProviderIdentity,
clients: ReadonlyMap<AnalysisProvider, AnalysisClient>,
): AnalysisClient["close"] =>
async (options) => {
const outcomes = await Promise.all(
[...clients.entries()].map(([provider, client]) =>
closeAnalysisClient(client, provider.identity().id, options).then(
(result) => ({ providerId: provider.identity().id, result }),
),
),
);
const failures = outcomes.flatMap(({ providerId, result }) =>
result.ok ? [] : [{ providerId, error: result.error }],
);
if (failures.length === 0) return ok(null);
if (failures.length === 1) {
const failure = failures[0];
if (failure !== undefined) return err(failure.error);
}
const resources = [
...new Set(failures.flatMap(({ error }) => error.cleanupResources)),
];
return err(
new ProviderCleanupError(
identity.id,
resources,
{
reason:
"Multiple composed provider clients could not be confirmed closed.",
failures: failures.map(({ providerId, error }) => ({
provider_id: providerId,
message: error.message,
resources: [...error.cleanupResources],
...(error.diagnostics === undefined
? {}
: { diagnostics: error.diagnostics }),
})),
},
{
cause: new AggregateError(
failures.map(({ error }) => error),
"Composed provider cleanup failed.",
),
},
),
);
};
const executeCompositeHealth = async (
identity: ProviderIdentity,
clients: ReadonlyMap<AnalysisProvider, AnalysisClient>,
options: ExecutionOptions | undefined,
): ReturnType<AnalysisClient["execute"]> => {
for (const [provider, client] of clients) {
const health = client.operationHealthSnapshot?.();
if (
health !== undefined &&
health.state !== "idle" &&
health.state !== "busy"
)
return err(
new ProviderAdapterError(provider.identity().id, "health", {
diagnostics: {
reason: `instantiated provider client is ${health.state}`,
},
}),
);
const checked = await client.execute("health", {}, options);
if (!checked.ok) return checked;
}
return ok({
result: null,
rawResult: null,
provider: identity,
limitations: [],
locations: [],
subject: null,
});
};
/** Deterministically route disjoint operations without eagerly starting children. */
export class CompositeProvider implements AnalysisProvider {
readonly #identity: ProviderIdentity;
@ -85,14 +170,7 @@ export class CompositeProvider implements AnalysisProvider {
clients.set(provider, created);
return created;
};
const close: AnalysisClient["close"] = async (options) => {
const outcomes = await Promise.all(
[...clients.entries()].map(([provider, client]) =>
closeAnalysisClient(client, provider.identity().id, options),
),
);
return outcomes.find((outcome) => !outcome.ok) ?? ok(null);
};
const close = closeCompositeClients(this.#identity, clients);
return {
executeIfReady: (operation, parameters, options) => {
const provider = this.#providerByOperation.get(operation);
@ -103,18 +181,9 @@ export class CompositeProvider implements AnalysisProvider {
Promise.resolve(undefined)
);
},
execute: (operation, parameters, options) => {
execute: async (operation, parameters, options) => {
if (operation === "health")
return Promise.resolve(
ok({
result: null,
rawResult: null,
provider: this.#identity,
limitations: [],
locations: [],
subject: null,
}),
);
return executeCompositeHealth(this.#identity, clients, options);
const provider = this.#providerByOperation.get(operation);
return provider === undefined
? Promise.resolve(
@ -141,16 +210,16 @@ export class CompositeProvider implements AnalysisProvider {
compareUnicodeCodePoints(left.provider.id, right.provider.id),
),
operationHealthSnapshot: () => {
for (const client of clients.values()) {
const snapshot = client.operationHealthSnapshot?.();
if (snapshot !== undefined && snapshot.state !== "idle")
return snapshot;
}
for (const client of clients.values()) {
const snapshot = client.operationHealthSnapshot?.();
if (snapshot !== undefined) return snapshot;
}
return undefined;
const snapshots = [...clients.values()].map((client) =>
client.operationHealthSnapshot?.(),
);
if (snapshots.some((snapshot) => snapshot === undefined))
return undefined;
return (
snapshots.find(
(snapshot) => snapshot !== undefined && snapshot.state !== "idle",
) ?? snapshots[0]
);
},
close,
};

View File

@ -9,6 +9,7 @@ import type {
Session,
Signal,
} from "frida";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { FridaInstrumentationManager } from "../../frida/FridaInstrumentationManager.js";
const signal = <Handler extends (...args: never[]) => void>() => {
@ -40,13 +41,14 @@ class FakeScript implements Script {
readonly beforeLoad: () => Promise<void> = async () => {},
readonly description = "script loaded",
readonly stack = "",
readonly messageValue?: FridaMessage,
) {}
async load(): Promise<void> {
await this.beforeLoad();
if (this.loadFailure !== undefined) throw this.loadFailure;
this.message.emit(
{
this.messageValue ?? {
type: "send",
payload: this.payload,
description: this.description,
@ -73,6 +75,7 @@ class FakeSession implements Session {
nextPayload: unknown = { event: "loaded" };
nextDescription = "script loaded";
nextStack = "";
nextMessage: FridaMessage | undefined;
nextLoadFailure: Error | undefined;
nextUnloadFailureCount = 0;
detachFailureCount = 0;
@ -94,12 +97,14 @@ class FakeSession implements Session {
},
this.nextDescription,
this.nextStack,
this.nextMessage,
);
script.loadFailure = this.nextLoadFailure;
script.unloadFailureCount = this.nextUnloadFailureCount;
this.nextPayload = { event: "loaded" };
this.nextDescription = "script loaded";
this.nextStack = "";
this.nextMessage = undefined;
this.nextLoadFailure = undefined;
this.nextUnloadFailureCount = 0;
this.scripts.push(script);
@ -744,7 +749,93 @@ it("attributes messages to their producing script and returns each load's messag
await manager.closeAll();
});
it("keeps an active script message bucket after its last message is evicted", async () => {
it("retains Frida error source coordinates in script and session evidence", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
expect(started.ok).toBe(true);
if (!started.ok) return;
bindings.device.session.nextMessage = {
type: "error",
description: "instrumentation failed",
stack: "Error: instrumentation failed",
fileName: "agent.js",
lineNumber: 7,
columnNumber: 3,
};
const loaded = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "throw new Error('instrumentation failed');",
});
expect(loaded.ok).toBe(true);
if (loaded.ok) {
expect(loaded.value.messages[0]).toMatchObject({
type: "error",
file_name: "agent.js",
line_number: 7,
column_number: 3,
});
}
expect(manager.status(started.value.sessionId)?.messages[0]).toMatchObject({
file_name: "agent.js",
line_number: 7,
column_number: 3,
});
bindings.device.session.nextMessage = {
type: "error",
description: "location unavailable",
stack: "Error: location unavailable",
};
const withoutLocation = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "throw new Error('location unavailable');",
});
expect(withoutLocation.ok).toBe(true);
if (withoutLocation.ok) {
expect(withoutLocation.value.messages[0]).not.toHaveProperty("file_name");
expect(withoutLocation.value.messages[0]).not.toHaveProperty("line_number");
expect(withoutLocation.value.messages[0]).not.toHaveProperty(
"column_number",
);
}
await manager.closeAll();
});
it("does not carry earlier script truncation into a complete later load", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
expect(started.ok).toBe(true);
if (!started.ok) return;
const first = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('A');",
});
expect(first.ok).toBe(true);
if (!first.ok) return;
const firstScript = bindings.device.session.scripts[0];
expect(firstScript).toBeDefined();
if (firstScript === undefined) return;
for (let index = 0; index < 800; index += 1)
firstScript.message.emit(
{ type: "send", payload: "x".repeat(32_700) },
null,
);
bindings.device.session.nextPayload = { tag: "B" };
const second = await manager.loadScript(started.value.sessionId, {
sourceKind: "inline",
source: "send('B');",
});
expect(second.ok).toBe(true);
if (second.ok) {
expect(second.value.messages).toHaveLength(1);
expect(second.value.messagesTruncated).toBe(false);
}
expect(manager.status(started.value.sessionId)?.messagesTruncated).toBe(true);
await manager.closeAll();
});
it("reports truncation when that script loses its own message", async () => {
const { bindings, manager, started } = await startLocalAttachedSession();
const session = bindings.device.session;
expect(started.ok).toBe(true);
@ -800,6 +891,7 @@ it("keeps an active script message bucket after its last message is evicted", as
);
expect(second.value.messagesTruncated).toBe(true);
}
expect(manager.status(started.value.sessionId)?.messagesTruncated).toBe(true);
expect(manager.status(started.value.sessionId)?.messages).toContainEqual(
expect.objectContaining({
script_id: second.ok ? second.value.scriptId : "",
@ -926,6 +1018,73 @@ it("redacts remote credentials and maps a missing optional binding", async () =>
expect(unavailable.error.message).toContain("Cannot find package 'frida'");
});
it("clears current binding failure after recovery", async () => {
const bindings = new FakeDeviceManager();
let attempts = 0;
const manager = new FridaInstrumentationManager({
deviceManagerLoader: async () => {
attempts += 1;
if (attempts === 1) throw new Error("transient binding load failure");
return bindings;
},
});
const failed = await manager.listDevices();
expect(failed.ok).toBe(false);
if (!failed.ok)
expect(failed.error.message).toContain("transient binding load failure");
expect(manager.availabilityFailure()?.reason).toBe(
"transient binding load failure",
);
const recovered = await manager.listDevices();
expect(recovered.ok).toBe(true);
expect(manager.availabilityFailure()).toBeUndefined();
});
it("keeps remote option conflicts as actionable invalid input", async () => {
const manager = new FridaInstrumentationManager({
deviceManager: new FakeDeviceManager(),
});
const retained = await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042", token: "first-token" },
pid: 512,
});
expect(retained.ok).toBe(true);
if (!retained.ok) return;
const conflicting = [
await manager.listDevices({
address: "127.0.0.1:27042",
token: "other-token",
}),
await manager.listProcesses({
remote: { address: "127.0.0.1:27042", token: "other-token" },
}),
await manager.startSession({
mode: "attach",
remote: { address: "127.0.0.1:27042", token: "other-token" },
pid: 512,
}),
];
for (const result of conflicting) {
expect(result.ok).toBe(false);
if (result.ok) continue;
if (!(result.error instanceof AnalysisInputError)) throw result.error;
expect(result.error.operation).not.toBe("frida_remote_connection");
expect(result.error.issues).toContainEqual(
expect.objectContaining({
path: ["remote"],
reason: "invalid_value",
message: expect.stringContaining("different connection options"),
}),
);
expect(result.error.message).not.toContain("other-token");
}
await manager.closeAll();
});
it("redacts remote credentials echoed by a target script", async () => {
const token = "synthetic-token-from-auth";
const certificate = "synthetic-certificate-from-auth";

View File

@ -77,6 +77,7 @@ export interface AstEdgeInput {
/** Inferred static relationship edge values. */
export interface InferenceEdgeInput extends AstEdgeInput {
readonly limitations?: readonly string[];
readonly confidence?: "high" | "medium" | "low";
readonly relation:
| "imports"
| "loads"
@ -194,6 +195,9 @@ export const addStaticInferenceEdge = (
range: input.range,
operation: "map-static-relationship",
coverage: input.coverage,
...(input.confidence === undefined
? {}
: { confidence: input.confidence }),
...(input.limitations === undefined
? {}
: { limitations: input.limitations }),

View File

@ -201,10 +201,17 @@ const addEndpoint = (
input.value.module_runtime,
) ?? input.asset;
const key = compactStringIdentityKey(input.value.value);
const label = `${input.value.kind} endpoint`;
const candidate = input.value.interpretation === "candidate";
const candidateLimitation =
"The unbound method-like argument is only a candidate; its receiver does not establish a network call.";
const label = `${input.value.kind} ${candidate ? "endpoint candidate" : "endpoint"}`;
const node = context.accumulator.addNode({
kind: "endpoint",
identity: artifactLocalIdentity(input.file.sha256, input.value.kind, key),
identity: artifactLocalIdentity(
input.file.sha256,
input.value.kind,
JSON.stringify([input.value.interpretation ?? "literal", key]),
),
observations: [
{
label:
@ -213,14 +220,25 @@ const addEndpoint = (
endpoint_kind: input.value.kind,
value: input.value.value,
mechanism: input.value.mechanism,
...(candidate ? { interpretation: input.value.interpretation } : {}),
},
evidence: astObservationEvidence({
sha256: input.file.sha256,
path: input.file.path,
range: input.value.location,
operation: "discover-static-endpoint",
coverage: input.coverage,
}),
evidence: candidate
? staticInferenceEvidence({
sha256: input.file.sha256,
path: input.file.path,
range: input.value.location,
operation: "classify-static-endpoint-candidate",
coverage: input.coverage,
confidence: "low",
limitations: [candidateLimitation],
})
: astObservationEvidence({
sha256: input.file.sha256,
path: input.file.path,
range: input.value.location,
operation: "discover-static-endpoint",
coverage: input.coverage,
}),
},
],
});
@ -231,7 +249,16 @@ const addEndpoint = (
range: input.value.location,
coverage: input.coverage,
relation: input.value.kind === "route" ? "exposes" : "calls",
properties: { mechanism: input.value.mechanism },
properties: {
mechanism: input.value.mechanism,
...(candidate ? { interpretation: input.value.interpretation } : {}),
},
...(candidate
? {
confidence: "low" as const,
limitations: [candidateLimitation],
}
: {}),
});
};

View File

@ -65,16 +65,40 @@ it("projects closure and direct interprocedural flow without execution", () => {
);
});
it("projects a destructuring assignment member target as a property write", () => {
const graph = graphFor(`
const source = { count: 1 };
[source.count] = [2];
`);
it.each([
["direct assignment", "source.count = 2;"],
["destructuring assignment", "[source.count] = [2];"],
["for-of head", "for (source.count of [4]) {}"],
["for-in head", "for (source.count in { key: true }) {}"],
["delete", "delete source.count;"],
])("projects a %s member target as a property write", (_name, statement) => {
const graph = graphFor(`const source = { count: 1 }; ${statement}`);
const operations = graph.nodes.filter(
({ properties }) => properties.operation_kind !== undefined,
);
expect(operations.map(({ label }) => label)).toEqual(["write:/count"]);
const write = operations[0];
if (write === undefined) throw new Error("Expected write operation node");
expect(graph.relations).toContainEqual(
expect.objectContaining({
source_node_id: write.node_id,
relation: "writes-property",
}),
);
expect(graph.relations).not.toContainEqual(
expect.objectContaining({
source_node_id: write.node_id,
relation: "reads-property",
}),
);
});
it("continues to project an ordinary member access as a property read", () => {
const graph = graphFor(`
const source = { count: 1 };
const value = source.count;
`);
expect(graph.relations).toContainEqual(
expect.objectContaining({ relation: "reads-property" }),
);
});

View File

@ -2,13 +2,270 @@ import { describe, expect, it } from "vitest";
import {
captureAccessibility,
captureDom,
captureFrames,
captureResources,
} from "./CdpCaptureDocuments.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
const origin = "https://app.example.test";
describe("CDP document capture", () => {
describe("CDP DOM capture", () => {
it("preserves node names and parents when aligned arrays are valid", () => {
const generator = captureDom(
{
strings: [origin + "/app", "#document", "DIV", "SPAN", ""],
documents: [
{
documentURL: 0,
baseURL: 0,
nodes: {
nodeType: [9, 1, 1],
nodeName: [1, 2, 3],
nodeValue: [4, 4, 4],
parentIndex: [-1, 0, 1],
attributes: [[], [], []],
},
},
],
},
new Set([origin]),
inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
}),
);
let captured = generator.next([]);
while (!captured.done) captured = generator.next([]);
expect(captured.value.nodes.map((node) => node.node_name)).toEqual([
"#document",
"DIV",
"SPAN",
]);
expect(captured.value.nodes.map((node) => node.parent_index)).toEqual([
-1, 0, 1,
]);
expect(captured.value.nodes.map((node) => node.node_value_length)).toEqual([
0, 0, 0,
]);
});
it.each(["nodeName", "parentIndex", "nodeValue", "attributes"] as const)(
"preserves DOM fact positions and reports malformed %s entries",
(field) => {
const strings = [
origin + "/app",
"#document",
"DIV",
"SPAN",
"href",
"title",
"/page",
];
const nodeArrays: Record<string, unknown> = {
nodeType: [9, 1, 1],
nodeName: [1, 2, 3],
nodeValue: [0, 0, 0],
parentIndex: [-1, 0, 1],
attributes: [[], [], []],
};
if (field === "nodeName") nodeArrays.nodeName = [1, "malformed", 3];
if (field === "parentIndex")
nodeArrays.parentIndex = [-1, "malformed", 1];
if (field === "nodeValue") nodeArrays.nodeValue = [0, 0, "malformed"];
if (field === "attributes")
nodeArrays.attributes = [[], [4, "malformed", 5, 6], []];
const completeness = new CdpCaptureCompleteness();
const generator = captureDom(
{
strings,
documents: [
{
documentURL: 0,
baseURL: 0,
nodes: nodeArrays,
},
],
},
new Set([origin]),
inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
}),
completeness,
);
let captured = generator.next([]);
while (!captured.done) captured = generator.next([]);
expect(captured.value.total).toBe(3);
expect(captured.value.nodes.map((node) => node.node_name)).toEqual(
field === "nodeValue" ? ["#document", "DIV"] : ["#document", "SPAN"],
);
expect(captured.value.nodes.map((node) => node.index)).toEqual(
field === "nodeValue" ? [0, 1] : [0, 2],
);
expect(captured.value.nodes.map((node) => node.parent_index)).toEqual(
field === "nodeValue" ? [-1, 0] : [-1, 1],
);
if (field === "attributes") {
expect(
captured.value.nodes.map((node) => node.attribute_names),
).toEqual([[], []]);
expect(captured.value.urls).toEqual([]);
}
expect(completeness.snapshot()).toMatchObject({
unavailable_sections: expect.arrayContaining(["dom"]),
excluded: expect.arrayContaining([
{ section: "dom", reason: "invalid_protocol_value", count: 1 },
]),
});
},
);
});
describe("CDP DOM string and namespace validity", () => {
it.each([
{ field: "nodeName", index: -1 },
{ field: "nodeName", index: 20 },
{ field: "nodeName", index: 2, malformedString: true },
{ field: "nodeValue", index: -1 },
{ field: "nodeValue", index: 20 },
{ field: "nodeValue", index: 5, malformedString: true },
{ field: "attributeName", index: -1 },
{ field: "attributeName", index: 20 },
{ field: "attributeValue", index: -1 },
{ field: "attributeValue", index: 20 },
] as const)(
"omits a node when its $field string index is invalid ($index)",
({
field,
index,
malformedString,
}: {
readonly field:
| "nodeName"
| "nodeValue"
| "attributeName"
| "attributeValue";
readonly index: number;
readonly malformedString?: boolean;
}) => {
const strings: unknown[] = [
origin + "/app",
"#document",
"DIV",
"SPAN",
"href",
"/good",
];
const nodeArrays: Record<string, unknown> = {
nodeType: [9, 1, 1],
nodeName: [1, 2, 3],
nodeValue: [0, 0, 0],
parentIndex: [-1, 0, 0],
attributes: [[], [], []],
};
if (field === "nodeName") nodeArrays.nodeName = [1, index, 3];
if (field === "nodeValue") nodeArrays.nodeValue = [0, index, 0];
if (field === "attributeName")
nodeArrays.attributes = [[], [index, 5], []];
if (field === "attributeValue")
nodeArrays.attributes = [[], [4, index], []];
if (malformedString === true) strings[index === 2 ? 2 : 5] = 42;
const completeness = new CdpCaptureCompleteness();
const generator = captureDom(
{
strings,
documents: [
{
documentURL: 0,
baseURL: 0,
nodes: nodeArrays,
},
],
},
new Set([origin]),
inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
}),
completeness,
);
let captured = generator.next([]);
while (!captured.done) captured = generator.next([]);
expect(captured.value.total).toBe(3);
expect(captured.value.nodes.map((node) => node.index)).toEqual([0, 2]);
expect(captured.value.nodes.map((node) => node.node_name)).toEqual([
"#document",
"SPAN",
]);
expect(completeness.snapshot().excluded).toContainEqual({
section: "dom",
reason: "invalid_protocol_value",
count: 1,
});
},
);
it("does not infer SVG ancestry through an element with malformed namespace data", () => {
const strings: unknown[] = [
origin + "/app",
"#document",
"svg",
"a",
"xmlns:xlink",
"http://www.w3.org/1999/xlink",
"xlink:href",
"/svg-link",
];
const completeness = new CdpCaptureCompleteness();
const generator = captureDom(
{
strings,
documents: [
{
documentURL: 0,
baseURL: 0,
nodes: {
nodeType: [9, 1, 1],
nodeName: [1, 2, 3],
nodeValue: [0, 0, 0],
parentIndex: [-1, 0, 1],
attributes: [[], [4, 99], [6, 7]],
},
},
],
},
new Set([origin]),
inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: [origin],
target_id: "page-1",
}),
completeness,
);
let captured = generator.next([]);
while (!captured.done) captured = generator.next([]);
expect(captured.value.nodes.map((node) => node.index)).toEqual([0, 2]);
expect(captured.value.nodes[1]?.attribute_names).toEqual(["xlink:href"]);
expect(captured.value.urls).toEqual([]);
expect(
completeness
.snapshot()
.excluded.some(
({ section, reason }) =>
section === "metadata" && reason === "invalid_protocol_value",
),
).toBe(true);
});
});
describe("CDP frame capture", () => {
it("walks deeply nested frame trees and returns every frame and resource", () => {
let frameTree: Record<string, unknown> = {
frame: { id: "frame-0", url: `${origin}/0` },
@ -30,7 +287,9 @@ describe("CDP document capture", () => {
expect(frames.items).toHaveLength(2_000);
expect(resources.items).toHaveLength(2_000);
});
});
describe("CDP accessibility capture", () => {
it("retains full approved accessibility text", () => {
const capture = captureAccessibility(
[

View File

@ -174,7 +174,7 @@ export function* captureDom(
> {
const root = requiredRecord(result);
const strings = Array.isArray(root.strings)
? root.strings.map((value) => cdpStringValue(value) ?? "")
? root.strings.map(cdpStringValue)
: [];
const nodes: WebPageInspection["dom"]["nodes"] = [];
const urls: WebPageInspection["metadata"]["dom_urls"] = [];
@ -184,44 +184,100 @@ export function* captureDom(
let unresolvedSvgNodes = 0;
for (const document of recordsValue(root.documents)) {
const documentUrl = indexedString(strings, document.documentURL);
if (documentUrl === undefined) {
completeness?.exclude("dom", "invalid_protocol_value");
continue;
}
if (allowedSanitizedUrl(documentUrl, allowedOrigins) === undefined) {
completeness?.exclude("dom", exclusionReasonForUrl(documentUrl));
continue;
}
const baseUrl = indexedString(strings, document.baseURL) || documentUrl;
const baseUrlValue =
document.baseURL === undefined
? documentUrl
: indexedString(strings, document.baseURL);
if (baseUrlValue === undefined) {
completeness?.exclude("dom", "invalid_protocol_value");
continue;
}
const baseUrl = baseUrlValue || documentUrl;
const documentNodes = recordValue(document.nodes);
if (documentNodes === undefined) continue;
const nodeTypes = numberArray(documentNodes.nodeType);
const nodeNames = numberArray(documentNodes.nodeName);
const nodeValues = numberArray(documentNodes.nodeValue);
const parents = numberArray(documentNodes.parentIndex);
const attributes = Array.isArray(documentNodes.attributes)
const rawNodeTypes = numberArray(documentNodes.nodeType);
const rawNodeNames = numberArray(documentNodes.nodeName);
const rawNodeValues = numberArray(documentNodes.nodeValue);
const rawParents = numberArray(documentNodes.parentIndex);
const rawAttributes = Array.isArray(documentNodes.attributes)
? documentNodes.attributes
: [];
: undefined;
if (
rawNodeTypes === undefined ||
rawNodeNames === undefined ||
rawNodeValues === undefined ||
rawParents === undefined ||
rawAttributes === undefined ||
rawNodeNames.length !== rawNodeTypes.length ||
rawNodeValues.length !== rawNodeTypes.length ||
rawParents.length !== rawNodeTypes.length
) {
completeness?.exclude("dom", "invalid_protocol_value");
continue;
}
if (rawAttributes.length > rawNodeTypes.length)
completeness?.exclude(
"dom",
"invalid_protocol_value",
rawAttributes.length - rawNodeTypes.length,
);
const backendNodeIds = Array.isArray(documentNodes.backendNodeId)
? documentNodes.backendNodeId
: [];
const baseIndex = nodes.length;
const baseIndex = total;
total += rawNodeTypes.length;
const ancestry = createDocumentSvgAncestry({
nodeTypes,
nodeNames,
parents,
nodeTypes: rawNodeTypes,
nodeNames: rawNodeNames,
parents: rawParents,
strings,
attributes,
attributes: rawAttributes,
});
total += nodeTypes.length;
const pendingMetadata: PendingDomMetadata[] = [];
for (let index = 0; index < nodeTypes.length; index += 1) {
const attributeIndexes = numberArray(attributes[index]);
const parent = Math.trunc(parents[index] ?? -1);
const nodeIndex = nodes.length;
const nodeName = indexedString(strings, nodeNames[index]);
const attributeNames = uniqueAttributeNames(attributeIndexes, strings);
for (let index = 0; index < rawNodeTypes.length; index += 1) {
const nodeType = rawNodeTypes[index];
const nodeNameIndex = rawNodeNames[index];
const nodeValueIndex = rawNodeValues[index];
const parent = rawParents[index];
const attributeIndexes = numberArray(rawAttributes[index]);
const nodeName = indexedString(strings, nodeNameIndex);
const nodeValue = indexedString(strings, nodeValueIndex);
if (
nodeType === undefined ||
nodeName === undefined ||
nodeValue === undefined ||
parent === undefined ||
parent < -1 ||
parent === index ||
parent >= rawNodeTypes.length ||
attributeIndexes === undefined ||
attributeIndexes.length % 2 !== 0 ||
attributeIndexes.some(
(value) =>
value === undefined || indexedString(strings, value) === undefined,
)
) {
completeness?.exclude("dom", "invalid_protocol_value");
continue;
}
const attributes = attributeIndexes as readonly number[];
const nodeIndex = baseIndex + index;
const attributeNames = uniqueAttributeNames(attributes, strings);
const svgLink = domSvgLink(ancestry, index);
if (svgLink.xmlDocument === undefined)
completeness?.exclude("metadata", "invalid_protocol_value");
let svgQuery: DomSvgNodeQuery | null | undefined;
if (
!svgLink.xmlDocument &&
nodeTypes[index] === 1 &&
svgLink.xmlDocument === false &&
nodeType === 1 &&
attributeNames.includes("xlink:href") &&
!attributeNames.includes("href")
) {
@ -236,16 +292,16 @@ export function* captureDom(
nodes.push({
index: nodeIndex,
parent_index: parent < 0 ? -1 : baseIndex + parent,
node_type: Math.max(0, Math.trunc(nodeTypes[index] ?? 0)),
node_type: nodeType,
node_name: nodeName,
node_value_length: indexedString(strings, nodeValues[index]).length,
node_value_length: nodeValue.length,
attribute_names: attributeNames,
});
pendingMetadata.push({
svgQuery,
options: {
strings,
attributes: attributeIndexes,
attributes,
// The BASE element itself resolves against the document fallback URL.
baseUrl: nodeName.toLowerCase() === "base" ? documentUrl : baseUrl,
documentUrl,
@ -257,7 +313,7 @@ export function* captureDom(
});
if (
pendingMetadata.length === DOM_METADATA_BATCH_SIZE ||
index === nodeTypes.length - 1
index === rawNodeTypes.length - 1
) {
const metadata = yield* captureDomMetadata(
pendingMetadata,
@ -270,6 +326,14 @@ export function* captureDom(
pendingMetadata.length = 0;
}
}
if (pendingMetadata.length > 0) {
const metadata = yield* captureDomMetadata(pendingMetadata, completeness);
urls.push(...metadata.urls);
agentHints.push(...metadata.agentHints);
excludedUrls += metadata.excludedUrls;
unresolvedSvgNodes += metadata.unresolvedSvgNodes;
pendingMetadata.length = 0;
}
}
return {
total,
@ -450,19 +514,25 @@ const accessibilityStates = (
const axText = (value: unknown): string | null =>
cdpStringValue(recordValue(value)?.value) ?? null;
const indexedString = (strings: readonly string[], index: unknown): string => {
const indexedString = (
strings: readonly (string | undefined)[],
index: unknown,
): string | undefined => {
const integer = numberValue(index);
return integer === undefined ? "" : (strings[Math.trunc(integer)] ?? "");
if (integer === undefined || !Number.isSafeInteger(integer) || integer < 0)
return undefined;
return strings[integer];
};
const uniqueAttributeNames = (
attributeIndexes: readonly number[],
strings: readonly string[],
strings: readonly (string | undefined)[],
): string[] => {
const names: string[] = [];
const seen = new Set<string>();
for (let index = 0; index < attributeIndexes.length; index += 2) {
const name = indexedString(strings, attributeIndexes[index]);
if (name === undefined) continue;
if (seen.has(name)) continue;
seen.add(name);
names.push(name);
@ -470,16 +540,20 @@ const uniqueAttributeNames = (
return names;
};
const numberArray = (value: unknown): readonly number[] =>
Array.isArray(value)
? value.flatMap((item) => {
const number = numberValue(item);
return number === undefined ? [] : [number];
})
: [];
const numberArray = (
value: unknown,
): readonly (number | undefined)[] | undefined => {
if (!Array.isArray(value)) return undefined;
return value.map((item) => {
const number = numberValue(item);
return number !== undefined && Number.isSafeInteger(number)
? number
: undefined;
});
};
interface DomMetadataOptions {
readonly strings: readonly string[];
readonly strings: readonly (string | undefined)[];
readonly attributes: readonly number[];
readonly baseUrl: string;
readonly documentUrl: string;
@ -487,7 +561,7 @@ interface DomMetadataOptions {
readonly nodeName: string;
readonly allowedOrigins: ReadonlySet<string>;
readonly svgElement: boolean;
readonly xmlDocument: boolean;
readonly xmlDocument: boolean | undefined;
readonly xlinkHref: string | undefined;
}
@ -525,6 +599,7 @@ const domMetadata = (
// Chrome already folds HTML names; SVG/XML names remain case-sensitive.
const name = indexedString(strings, attributes[index]);
const value = indexedString(strings, attributes[index + 1]);
if (name === undefined || value === undefined) continue;
// HTML keeps the first declaration. A later repeat must not move dom_urls.
if (!pairs.has(name)) pairs.set(name, value);
}

View File

@ -4,10 +4,10 @@ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
interface DomSnapshotNodes {
readonly nodeTypes: readonly number[];
readonly nodeNames: readonly number[];
readonly parents: readonly number[];
readonly strings: readonly string[];
readonly nodeTypes: readonly (number | undefined)[];
readonly nodeNames: readonly (number | undefined)[];
readonly parents: readonly (number | undefined)[];
readonly strings: readonly (string | undefined)[];
readonly attributes: readonly unknown[];
}
@ -17,7 +17,7 @@ interface XmlSvgLink {
}
interface DocumentSvgAncestry {
readonly xml: boolean;
readonly xml: boolean | undefined;
readonly xmlLinks: Map<number, XmlSvgLink>;
}
@ -48,50 +48,59 @@ export const capturedSvgNodeKind = (
interface DomSvgLink {
readonly svgElement: boolean;
readonly xmlDocument: boolean;
readonly xmlDocument: boolean | undefined;
readonly xlinkHref: string | undefined;
}
const UNRESOLVED: XmlSvgLink = {
svgElement: false,
xlinkHref: undefined,
};
const stringAt = (strings: readonly string[], index: unknown): string => {
const stringAt = (
strings: readonly (string | undefined)[],
index: unknown,
): string | undefined => {
const integer = numberValue(index);
return integer === undefined ? "" : (strings[Math.trunc(integer)] ?? "");
if (integer === undefined || !Number.isSafeInteger(integer) || integer < 0)
return undefined;
return strings[integer];
};
const numberArray = (value: unknown): readonly number[] =>
const numberArray = (value: unknown): readonly (number | undefined)[] =>
Array.isArray(value)
? value.flatMap((item) => {
? value.map((item) => {
const number = numberValue(item);
return number === undefined ? [] : [number];
return number !== undefined && Number.isSafeInteger(number)
? number
: undefined;
})
: [];
// DOMSnapshot has no namespace URI. HTML documents use uppercase `HTML` and
// the parser's local names. XML documents preserve prefixed names and xmlns
// declarations, including those on standalone SVG roots.
const xmlDocument = (nodes: DomSnapshotNodes): boolean => {
const documentIndex = nodes.nodeTypes.findIndex(
(nodeType) => Math.trunc(nodeType) === 9,
);
const xmlDocument = (nodes: DomSnapshotNodes): boolean | undefined => {
const documentIndex = nodes.nodeTypes.findIndex((nodeType) => nodeType === 9);
if (documentIndex < 0) return false;
if (
!validSvgSourceNode(nodes, documentIndex) ||
nodes.parents[documentIndex] !== -1
)
return undefined;
for (let index = 0; index < nodes.nodeTypes.length; index += 1) {
if (Math.trunc(nodes.nodeTypes[index] ?? 0) !== 1) continue;
if (Math.trunc(nodes.parents[index] ?? -1) !== documentIndex) continue;
if (nodes.nodeTypes[index] !== 1) continue;
if (nodes.parents[index] !== documentIndex) continue;
const name = stringAt(nodes.strings, nodes.nodeNames[index]);
if (name === undefined || !validSvgSourceNode(nodes, index))
return undefined;
if (name === "HTML") return false;
if (name === "html" || name.includes(":")) return true;
// Explicit namespaces also identify standalone SVG and other XML roots.
// Namespace-free synthetic snapshots retain the legacy HTML behavior.
const attributes = numberArray(nodes.attributes[index]);
return attributes.some(
(value, offset) =>
offset % 2 === 0 &&
xmlnsPrefix(stringAt(nodes.strings, value)) !== undefined,
);
for (let offset = 0; offset + 1 < attributes.length; offset += 2) {
const value = attributes[offset];
const attributeName = stringAt(nodes.strings, value);
if (attributeName === undefined) return undefined;
if (xmlnsPrefix(attributeName) !== undefined) return true;
}
return false;
}
return false;
};
@ -102,7 +111,7 @@ export const createDocumentSvgAncestry = (
const xml = xmlDocument(nodes);
return {
xml,
xmlLinks: xml ? resolveXmlElements(nodes) : new Map(),
xmlLinks: xml === true ? resolveXmlElements(nodes) : new Map(),
};
};
@ -110,14 +119,28 @@ export const domSvgLink = (
ancestry: DocumentSvgAncestry,
index: number,
): DomSvgLink => {
if (!ancestry.xml) {
if (ancestry.xml === false) {
return {
svgElement: false,
xmlDocument: false,
xlinkHref: undefined,
};
}
const resolved = ancestry.xmlLinks.get(index) ?? UNRESOLVED;
if (ancestry.xml === undefined) {
return {
svgElement: false,
xmlDocument: undefined,
xlinkHref: undefined,
};
}
const resolved = ancestry.xmlLinks.get(index);
if (resolved === undefined) {
return {
svgElement: false,
xmlDocument: undefined,
xlinkHref: undefined,
};
}
return {
svgElement: resolved.svgElement,
xmlDocument: true,
@ -143,7 +166,9 @@ const resolveXmlElements = (
const children = new Map<number, number[]>();
const pending: XmlWalkStep[] = [];
for (let node = 0; node < nodes.nodeNames.length; node += 1) {
const parent = Math.trunc(nodes.parents[node] ?? -1);
const parent = nodes.parents[node];
if (parent === undefined || parent < -1 || parent >= nodes.nodeNames.length)
continue;
if (parent < 0) pending.push({ node });
else if (parent < nodes.nodeNames.length && parent !== node) {
const siblings = children.get(parent);
@ -164,10 +189,11 @@ const resolveXmlElements = (
continue;
}
const { node } = step;
if (!validSvgSourceNode(nodes, node)) continue;
const attributes = numberArray(nodes.attributes[node]);
const restore = applyDeclarations(bindings, attributes, nodes.strings);
const svgElement =
Math.trunc(nodes.nodeTypes[node] ?? 0) === 1 &&
nodes.nodeTypes[node] === 1 &&
elementNamespace(
stringAt(nodes.strings, nodes.nodeNames[node]),
bindings,
@ -188,24 +214,30 @@ const resolveXmlElements = (
const applyDeclarations = (
bindings: Map<string, string>,
attributes: readonly number[],
strings: readonly string[],
attributes: readonly (number | undefined)[],
strings: readonly (string | undefined)[],
): readonly NamespaceRestore[] => {
const restore: NamespaceRestore[] = [];
const seen = new Set<string>();
for (let index = 0; index + 1 < attributes.length; index += 2) {
const prefix = xmlnsPrefix(stringAt(strings, attributes[index]));
const nameIndex = attributes[index];
const valueIndex = attributes[index + 1];
if (nameIndex === undefined || valueIndex === undefined) continue;
const name = stringAt(strings, nameIndex);
const value = stringAt(strings, valueIndex);
if (name === undefined || value === undefined) continue;
const prefix = xmlnsPrefix(name);
if (prefix === undefined || seen.has(prefix)) continue;
seen.add(prefix);
restore.push({ prefix, previous: bindings.get(prefix) });
const value = stringAt(strings, attributes[index + 1]);
if (value.length === 0) bindings.delete(prefix);
else bindings.set(prefix, value);
}
return restore;
};
const xmlnsPrefix = (name: string): string | undefined => {
const xmlnsPrefix = (name: string | undefined): string | undefined => {
if (name === undefined) return undefined;
if (name === "xmlns") return "";
if (!name.startsWith("xmlns:")) return undefined;
const prefix = name.slice("xmlns:".length);
@ -215,9 +247,10 @@ const xmlnsPrefix = (name: string): string | undefined => {
};
const elementNamespace = (
nodeName: string,
nodeName: string | undefined,
bindings: ReadonlyMap<string, string>,
): string => {
if (nodeName === undefined) return "";
const colon = nodeName.indexOf(":");
if (colon < 0) return bindings.get("") ?? "";
const prefix = nodeName.slice(0, colon);
@ -235,13 +268,17 @@ const elementNamespace = (
// First prefixed href bound to the XLink namespace. Duplicate names keep the
// earlier value. Plain href is applied by the caller and wins even when empty.
const xlinkHrefValue = (
attributes: readonly number[],
strings: readonly string[],
attributes: readonly (number | undefined)[],
strings: readonly (string | undefined)[],
bindings: ReadonlyMap<string, string>,
): string | undefined => {
const seen = new Set<string>();
for (let index = 0; index + 1 < attributes.length; index += 2) {
const name = stringAt(strings, attributes[index]);
const nameIndex = attributes[index];
const valueIndex = attributes[index + 1];
if (nameIndex === undefined || valueIndex === undefined) continue;
const name = stringAt(strings, nameIndex);
if (name === undefined) continue;
if (seen.has(name)) continue;
seen.add(name);
const colon = name.indexOf(":");
@ -249,7 +286,30 @@ const xlinkHrefValue = (
const prefix = name.slice(0, colon);
const local = name.slice(colon + 1);
if (local !== "href" || bindings.get(prefix) !== XLINK_NAMESPACE) continue;
return stringAt(strings, attributes[index + 1]);
return stringAt(strings, valueIndex);
}
return undefined;
};
const validSvgSourceNode = (
nodes: DomSnapshotNodes,
index: number,
): boolean => {
if (
nodes.nodeTypes[index] === undefined ||
stringAt(nodes.strings, nodes.nodeNames[index]) === undefined
)
return false;
const rawAttributes = nodes.attributes[index];
if (!Array.isArray(rawAttributes) || rawAttributes.length % 2 !== 0)
return false;
const attributes = numberArray(rawAttributes);
return (
attributes.length === rawAttributes.length &&
attributes.every(
(attribute) =>
attribute !== undefined &&
stringAt(nodes.strings, attribute) !== undefined,
)
);
};

View File

@ -82,3 +82,61 @@ it.each([false, true])(
expect(value.fingerprints_complete).toBe(!mixed);
},
);
it("keeps an omitted IndexedDB remote value unknown", async () => {
const replies: Record<string, unknown> = {
"Storage.getUsageAndQuota": { usage: 0, quota: 1000 },
"DOMStorage.getDOMStorageItems": { entries: [] },
"IndexedDB.requestDatabaseNames": { databaseNames: ["db"] },
"CacheStorage.requestCacheNames": { caches: [] },
"Network.getCookies": { cookies: [] },
"IndexedDB.requestDatabase": {
databaseWithObjectStores: {
name: "db",
objectStores: [
{
name: "records",
keyPath: { type: "string", string: "id" },
autoIncrement: false,
indexes: [],
},
],
},
},
"IndexedDB.requestData": {
objectStoreDataEntries: [
{
key: { type: "number", value: 1 },
primaryKey: { type: "number", value: 1 },
},
],
hasMore: false,
},
};
const connection = {
async send(method: string): Promise<unknown> {
if (!(method in replies)) throw new Error(method);
return replies[method];
},
} as unknown as CdpConnection;
const { value } = await captureStorage(
{
connection,
sessionId: undefined,
input: inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
allowed_origins: ["https://example.test"],
target_id: "page-1",
include_storage_keys: true,
include_storage_fingerprints: true,
}),
},
"https://example.test/app",
[],
);
const record = value.content_fingerprints.find(
(item) => item.scope === "indexed_db_record",
);
expect(record).toMatchObject({ value_sha256: null, complete: false });
expect(value.fingerprints_complete).toBe(false);
});

View File

@ -301,7 +301,10 @@ const addIndexedDbRecords = async (
for (const entry of entries) {
const key = stableValue(entry.key);
const primaryKey = stableValue(entry.primaryKey);
const value = stableValue(entry.value);
// A missing remote object is an omission, not the JavaScript value
// "undefined". Keep the unavailable digest null and coverage partial.
const value =
entry.value === undefined ? undefined : stableValue(entry.value);
addFingerprint(run.state, {
scope: "indexed_db_record",
identity: {

View File

@ -243,9 +243,9 @@ export class CdpConnection {
const hasResult = "result" in message;
const hasError = "error" in message;
if (
("sessionId" in message &&
(typeof message.sessionId !== "string" ||
message.sessionId !== pending.sessionId)) ||
(pending.sessionId === undefined
? "sessionId" in message
: message.sessionId !== pending.sessionId) ||
hasResult === hasError
) {
this.#failPending("protocol_error");

View File

@ -0,0 +1,64 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { captureFrames } from "./CdpElectronInspection.js";
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map((path) => rm(path, { recursive: true, force: true })),
);
});
describe("Electron frame capture", () => {
it.each([7, undefined])(
"classifies a malformed child frame ID as unavailable: %s",
async (childId) => {
const root = await mkdtemp(join(tmpdir(), "rea-electron-frame-"));
temporary.push(root);
const main = join(root, "index.html");
const child = join(root, "child.html");
await Promise.all([writeFile(main, "main"), writeFile(child, "child")]);
const mainUrl = pathToFileURL(main).href;
const childUrl = pathToFileURL(child).href;
const completeness = new CdpCaptureCompleteness();
const frames = await captureFrames(
{
frameTree: {
frame: { id: "frame-main", url: mainUrl },
childFrames: [
{
frame: {
...(childId === undefined ? {} : { id: childId }),
url: childUrl,
},
},
],
},
},
completeness,
);
expect(frames.map((frame) => frame.frame_id)).toEqual(["frame-main"]);
expect(completeness.snapshot()).toMatchObject({
unavailable_sections: ["frames"],
excluded: [
{ section: "frames", reason: "invalid_protocol_value", count: 1 },
],
});
expect(completeness.snapshot().policy_filtered_sections).not.toContain(
"frames",
);
expect(completeness.snapshot().conditions).toContain("attach_limited");
},
);
});

View File

@ -232,7 +232,7 @@ const navigatedFrameId = (event: CdpEvent): string | undefined => {
return undefined;
};
const captureFrames = async (
export const captureFrames = async (
result: unknown,
completeness: CdpCaptureCompleteness,
): Promise<ElectronPageInspection["frames"]> => {
@ -245,8 +245,12 @@ const captureFrames = async (
)) {
const frame = recordValue(tree.frame);
const frameId = cdpStringValue(frame?.id);
const path = await authorizedElectronFile(cdpStringValue(frame?.url) ?? "");
if (frameId === undefined || path === undefined) {
if (frame === undefined || frameId === undefined) {
completeness.exclude("frames", "invalid_protocol_value");
continue;
}
const path = await authorizedElectronFile(cdpStringValue(frame.url) ?? "");
if (path === undefined) {
completeness.exclude("frames", "out_of_target_scope");
continue;
}

View File

@ -94,7 +94,7 @@ export const captureCdpScreenshot = async (
const bytes =
encoded === undefined ? undefined : decodeCanonicalBase64(encoded);
if (bytes === undefined)
throw new BrowserObservationError("inspect_web_page", "payload_limit");
throw new BrowserObservationError("inspect_web_page", "protocol_error");
const dimensions = readPngHeaderDimensions(bytes);
if (dimensions === undefined)
throw new BrowserObservationError("inspect_web_page", "protocol_error");

View File

@ -40,12 +40,15 @@ it("projects hook retention and dropped event families through the result schema
estimated_retained_bytes: 1_024,
event_serialized_byte_upper_bound: 512,
retained: 1,
dropped: 1,
dropped: 2,
dropped_ipc: 0,
dropped_runtime: 1,
dropped_event_families: [{ family: "renderer-ipc", count: 1 }],
dropped_runtime: 2,
dropped_event_families: [
{ family: "renderer-ipc", count: 1 },
{ family: "preload-configuration", count: 1 },
],
dropped_event_roles: [{ role: "renderer", count: 1 }],
observed: 2,
observed: 3,
observed_ipc: 1,
observed_runtime: 1,
hook_error: false,
@ -58,18 +61,29 @@ it("projects hook retention and dropped event families through the result schema
estimated_retained_bytes: 1_024,
event_serialized_byte_upper_bound: 512,
retained: 1,
dropped: 1,
dropped_event_families: [{ family: "renderer-ipc", count: 1 }],
dropped: 2,
dropped_event_families: [
{ family: "renderer-ipc", count: 1 },
{ family: "preload-configuration", count: 1 },
],
dropped_event_roles: [{ role: "renderer", count: 1 }],
});
expect(result.ipc).toMatchObject({ observed: 1, dropped: 0 });
expect(result.timeline).toMatchObject({ observed: 2, dropped: 1 });
expect(result.timeline).toMatchObject({ observed: 3, dropped: 2 });
expect(result.coverage.observed_event_families).toContain("renderer-ipc");
expect(result.coverage.unavailable_event_families).not.toContain(
"renderer-ipc",
);
expect(result.coverage.observed_roles).toContain("renderer");
expect(result.coverage.unavailable_event_families).toContain("preload");
expect(result.coverage.observed_event_families).toContain(
"preload-configuration",
);
expect(result.coverage.unavailable_event_families).not.toContain(
"preload-configuration",
);
expect(result.limitations).not.toContain(
"Preload configuration was not exposed by the observed Electron window metadata. Preload paths and execution remain unavailable.",
);
});
it("keeps renderer coverage unavailable when only the main process was observed", () => {

View File

@ -113,6 +113,18 @@ const createCoverage = (hookSnapshot: ElectronHookSnapshot) => {
} as const;
};
const hasObservedPreloadConfiguration = (
hookSnapshot: ElectronHookSnapshot,
): boolean =>
hookSnapshot.events.some(
({ kind, process_type }) =>
kind === "preload" &&
(process_type === "main" || process_type === "browser"),
) ||
hookSnapshot.dropped_event_families.some(
({ family, count }) => family === "preload-configuration" && count > 0,
);
/** Project one provider hook snapshot into the canonical public result. */
export const projectElectronActiveCapture = (
paths: ElectronCapturePaths,
@ -166,7 +178,7 @@ export const projectElectronActiveCapture = (
"IPC direction and sender/receiver identifiers are observed only where Electron exposes them at the hooked boundary.",
"The runtime timeline records lifecycle, navigation, shell, permission, popup, download, protocol, preload, native-addon, process, and IPC events; activity before hook installation is unavailable.",
"The preload and renderer process contexts are not instrumented by the main-process -r hook; preload configuration and contextBridge API-shape events are main-boundary observations, not proof of renderer-side execution.",
...(!hookSnapshot.events.some(({ kind }) => kind === "preload")
...(!hasObservedPreloadConfiguration(hookSnapshot)
? [
"Preload configuration was not exposed by the observed Electron window metadata. Preload paths and execution remain unavailable.",
]

View File

@ -73,6 +73,23 @@ const ownership = {
expectedParentPid: 1,
} satisfies OwnedProcessGroup;
const emptyHookSnapshot = () => ({
events: [],
retention_budget_bytes: 0,
estimated_retained_bytes: 0,
event_serialized_byte_upper_bound: 0,
retained: 0,
dropped: 0,
dropped_ipc: 0,
dropped_runtime: 0,
dropped_event_families: [],
dropped_event_roles: [],
observed: 0,
observed_ipc: 0,
observed_runtime: 0,
hook_error: false,
});
it("does not signal a Windows Electron PID when lineage is unavailable", async () => {
const terminateTree = vi.fn(async () => ({
cleaned: true as const,
@ -138,27 +155,11 @@ it("retains the partial capture without taskkill when Windows lineage is unavail
const terminateTree = vi
.spyOn(processOwnership, "cleanupWindowsProcessTree")
.mockResolvedValue({ cleaned: true, signaled: true });
const hookSnapshot = {
events: [],
retention_budget_bytes: 0,
estimated_retained_bytes: 0,
event_serialized_byte_upper_bound: 0,
retained: 0,
dropped: 0,
dropped_ipc: 0,
dropped_runtime: 0,
dropped_event_families: [],
dropped_event_roles: [],
observed: 0,
observed_ipc: 0,
observed_runtime: 0,
hook_error: false,
};
vi.spyOn(electronActions, "readApplicationState").mockResolvedValue({
windows: [],
metrics: [],
electronVersion: "44.0.0",
hookSnapshot,
hookSnapshot: emptyHookSnapshot(),
});
const application = {
process: () => ({ pid: 424242 }),
@ -206,3 +207,49 @@ it("retains the partial capture without taskkill when Windows lineage is unavail
Object.defineProperty(process, "platform", originalPlatform);
}
});
it("retains the initial snapshot when the final state read fails", async () => {
const initialState = {
windows: [],
metrics: [],
electronVersion: "44.0.0",
hookSnapshot: emptyHookSnapshot(),
};
vi.spyOn(electronActions, "readApplicationState")
.mockResolvedValueOnce(initialState)
.mockRejectedValueOnce(new Error("final renderer state unavailable"));
const application = {
process: () => ({ pid: 424242 }),
close: vi.fn(async () => undefined),
windows: () => [],
};
const provider = new PlaywrightElectronActiveProvider(
{},
async () =>
application as unknown as Awaited<
ReturnType<typeof import("playwright-core")._electron.launch>
>,
);
const result = await provider.capture({
...electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: process.execPath,
actions: [],
}),
application_root: dirname(process.execPath),
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected final state capture to fail");
const partialObservation = result.error.partialObservation;
expect(partialObservation).toMatchObject({
kind: "electron-active-observation",
capture: {
application: { electron_version: "44.0.0" },
limitations: expect.arrayContaining([
expect.stringContaining("initial pre-action snapshot"),
]),
},
});
});

View File

@ -145,11 +145,7 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
)
: ok(capture);
} catch (cause: unknown) {
if (
options.signal?.aborted &&
paths !== undefined &&
initialState !== undefined
) {
if (paths !== undefined && initialState !== undefined) {
const retainedCapture = projectElectronActiveCapture(
paths,
actions,
@ -159,25 +155,33 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
...retainedCapture,
limitations: [
...retainedCapture.limitations,
`Final Electron state was unavailable after cancellation: ${cause instanceof Error ? cause.message : String(cause)}. Windows, metrics, IPC, and timeline are the snapshot collected before actions and may omit their effects.`,
`Capture stopped after the initial pre-action snapshot: ${cause instanceof Error ? cause.message : String(cause)}. Final Windows, metrics, IPC, and timeline state is unknown; the retained snapshot may omit action effects.`,
],
};
}
outcome = err(
options.signal?.aborted === true
? new BrowserObservationError(OPERATION, "cancelled", {
cause,
...(capture === undefined
? {}
: {
partialObservation: {
kind: "electron-active-observation" as const,
capture,
},
}),
})
: providerError(cause),
);
if (options.signal?.aborted === true)
outcome = err(
new BrowserObservationError(OPERATION, "cancelled", {
cause,
...(capture === undefined
? {}
: {
partialObservation: {
kind: "electron-active-observation" as const,
capture,
},
}),
}),
);
else {
const error = providerError(cause);
if (capture !== undefined)
error.retainPartialObservation({
kind: "electron-active-observation",
capture,
});
outcome = err(error);
}
}
if (application !== undefined) {
if (ownership !== undefined) {

View File

@ -11,6 +11,10 @@ import { isUrlLikeModuleSpecifier } from "../domain/webBundleAnalyzerAst.js";
import { analyzeParsedJavaScriptReferences } from "../domain/javascript/javascriptSemanticAnalysis.js";
import { traverseJavaScriptAst } from "../domain/javascript/javascriptSemanticTraversal.js";
import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js";
import {
isTypeOnlyExportDeclaration,
isTypeOnlyImportDeclaration,
} from "../domain/javascript/javascriptModuleDeclaration.js";
import { stringValue } from "../domain/javascript/javascriptStaticAnalysisHelpers.js";
import type {
AnalyzeWebBundleInput,
@ -1061,7 +1065,11 @@ const originalDependency = (
t.isExportAllDeclaration(node) ||
t.isExportNamedDeclaration(node)
)
return node.source === null || node.source === undefined
return node.source === null ||
node.source === undefined ||
(t.isImportDeclaration(node)
? isTypeOnlyImportDeclaration(node)
: isTypeOnlyExportDeclaration(node))
? null
: { kind: "static_import", specifier: node.source.value };
if (t.isImportExpression(node)) {

View File

@ -1,7 +1,12 @@
/** Expected malformed, unsupported or oversized producer data at a historical decoder boundary. */
export class CaptureFormatError extends Error {
constructor(
readonly reason: "format" | "unsupported" | "input-limit" | "limit",
readonly reason:
| "format"
| "unsupported"
| "input-limit"
| "expanded-limit"
| "limit",
message: string,
readonly pointer = "",
) {

View File

@ -51,6 +51,7 @@ const replySchema = z.discriminatedUnion("ok", [
"format",
"unsupported",
"input-limit",
"expanded-limit",
"resource-limit",
"decoder",
"limit",
@ -215,6 +216,18 @@ export class HistoricalCaptureDecoder {
},
},
]);
if (reply.reason === "expanded-limit")
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["capture_path", reply.pointer],
reason: "out_of_range",
message: reply.message,
expected: {
maximum_expanded_capture_bytes:
WEB_NETWORK_CAPTURE_LIMITS.inputBytes,
},
},
]);
if (reply.reason === "resource-limit")
throw captureMemoryFailure(input, reply.message);
if (reply.reason === "decoder")

View File

@ -52,7 +52,7 @@ export const decodeSazCapture = async (
}
if (declaredBytes > WEB_NETWORK_CAPTURE_LIMITS.inputBytes)
throw new CaptureFormatError(
"limit",
"expanded-limit",
"SAZ expanded members exceed the 32 MiB complete-evidence budget.",
);
inventory.push({
@ -161,7 +161,7 @@ export const decodeSazCapture = async (
if (cause instanceof CaptureFormatError) throw cause;
if (cause instanceof ArtifactReaderFailure && cause.reason === "limit")
throw new CaptureFormatError(
"limit",
"expanded-limit",
"SAZ archive exceeds the ZIP reader's resource budget.",
);
throw new CaptureFormatError(

View File

@ -1,4 +1,5 @@
import { expect, it } from "vitest";
import { CaptureFormatError } from "./CaptureFormatError.js";
import { decodeSazMetadata } from "./SazMetadata.js";
it("retains U+FFFD in well-formed metadata as an ordinary XML character", () => {
@ -14,6 +15,9 @@ it("retains U+FFFD in well-formed metadata as an ordinary XML character", () =>
});
it.each([
"<Session>a & b</Session>",
"<Session>a\u0000b</Session>",
"<Session>&#xD800;</Session>",
"<Session a=1><3E></Session>",
'<Session a="x"b="<22>"/>',
"<Session><Broken><3E></Session>",
@ -22,3 +26,24 @@ it.each([
"SAZ metadata is not well-formed XML.",
);
});
it("preserves valid comments, CDATA, declarations, and predefined entities", () => {
const xml =
'<?xml version="1.0"?><Session><!-- note & literal --><Value>A &amp; B</Value><C><![CDATA[C & D]]></C></Session>';
const decoded = JSON.stringify(decodeSazMetadata(Buffer.from(xml)));
expect(decoded).toContain("note & literal");
expect(decoded).toContain("A & B");
expect(decoded).toContain("C & D");
});
it("rejects DTDs without resolving entities", () => {
expect(() =>
decodeSazMetadata(
Buffer.from('<!DOCTYPE Session [<!ENTITY value "external">]><Session/>'),
),
).toThrow(
expect.objectContaining({
reason: "unsupported",
} satisfies Partial<CaptureFormatError>),
);
});

View File

@ -1,4 +1,5 @@
import { DOMParser, ParseError, type Node } from "@xmldom/xmldom";
import { SaxesParser } from "saxes";
import type { JsonValue } from "../../domain/jsonValue.js";
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
import { CaptureFormatError } from "./CaptureFormatError.js";
@ -20,6 +21,7 @@ export const decodeSazMetadata = (bytes: Buffer): JsonValue => {
"SAZ metadata must use UTF-8 or BOM-marked UTF-16.",
);
}
validateSazXml(xml);
let parseFailed = false;
let document: ReturnType<DOMParser["parseFromString"]>;
try {
@ -44,13 +46,6 @@ export const decodeSazMetadata = (bytes: Buffer): JsonValue => {
"SAZ metadata is not well-formed XML.",
);
}
// The pinned parser does not resolve external or DTD-defined entities.
// Inspect its parsed node; text inside comments/CDATA is ordinary evidence.
if (document.doctype !== null)
throw new CaptureFormatError(
"unsupported",
"SAZ metadata DTDs are unsupported.",
);
if (parseFailed)
throw new CaptureFormatError(
"format",
@ -89,3 +84,33 @@ export const decodeSazMetadata = (bytes: Buffer): JsonValue => {
};
return visit(document, 0);
};
/** Validate well-formedness before the DOM parser projects retained evidence. */
const validateSazXml = (xml: string): void => {
const parser = new SaxesParser({
defaultXMLVersion: "1.0",
forceXMLVersion: true,
});
let hasDoctype = false;
parser.on("doctype", () => {
hasDoctype = true;
});
try {
parser.write(xml).close();
} catch {
if (hasDoctype)
throw new CaptureFormatError(
"unsupported",
"SAZ metadata DTDs are unsupported.",
);
throw new CaptureFormatError(
"format",
"SAZ metadata is not well-formed XML.",
);
}
if (hasDoctype)
throw new CaptureFormatError(
"unsupported",
"SAZ metadata DTDs are unsupported.",
);
};

View File

@ -368,7 +368,7 @@ export const enhancedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
name: z.string(),
start: z.string(),
end: z.string(),
length: z.number().min(0),
length: z.number().min(0).nullable(),
image_base: z.string().exactOptional(),
address_space: z.string().exactOptional(),
}),

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
functionBodySchema,
addressDistance,
procedureIdentitySchema,
parseDocuments,
parseListCount,
@ -12,6 +13,15 @@ import {
} from "./hopperValues.js";
describe("Hopper boundary values", () => {
it("distinguishes measured zero, malformed, incomparable, reversed, and oversized extents", () => {
expect(addressDistance("not-an-address", "0x2000")).toBeNull();
expect(addressDistance("ram:0x1000", "rom:0x2000")).toBeNull();
expect(addressDistance("0x2000", "0x1000")).toBeNull();
expect(addressDistance("0x1000", "0x1000")).toBe(0);
expect(addressDistance("0x1000", "0x2000")).toBe(4096);
expect(addressDistance("0x0", "0x20000000000000")).toBeNull();
});
it("parses Hopper's address-keyed name and string maps", () => {
expect(parseNames({ "0x1000": "_main" })).toEqual({
ok: true,

View File

@ -25,17 +25,14 @@ export interface SegmentSummary {
}
/** Return a bounded byte distance between canonical addresses in one space. */
export const addressDistance = (start: string, end: string): number => {
export const addressDistance = (start: string, end: string): number | null => {
const left = addressCoordinate(start);
const right = addressCoordinate(end);
if (left === null || right === null || left.space !== right.space) return 0;
if (left === null || right === null || left.space !== right.space)
return null;
const distance = right.offset - left.offset;
if (distance <= 0n) return 0;
return Number(
distance > BigInt(Number.MAX_SAFE_INTEGER)
? BigInt(Number.MAX_SAFE_INTEGER)
: distance,
);
if (distance < 0n || distance > BigInt(Number.MAX_SAFE_INTEGER)) return null;
return Number(distance);
};
const procedureMapSchema = z.record(z.string(), z.string());

View File

@ -7,7 +7,8 @@ import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
const bootstrap = (external: string, consumer: string, entry = "n(100);") =>
`(()=>{var m={45840:${external},100:(e,t,n)=>{"use strict";${consumer}}},c={};` +
`function n(t){var r=c[t];if(void 0!==r)return r.exports;var o=c[t]={exports:{}};` +
`return m[t](o,o.exports,n),o.exports}${entry}})();`;
`return m[t](o,o.exports,n),o.exports}` +
`n.n=e=>e&&e.__esModule?()=>e.default:()=>e;${entry}})();`;
const EXTERNAL = 'e=>{"use strict";e.exports=require("electron")}';
const CONSUMER =
@ -18,6 +19,35 @@ const electronOf = (source: string) =>
analyzeJavaScriptStaticSource(source).electron;
describe("Electron identity through Webpack externals", () => {
it("follows the default-export getter produced by webpack n.n", () => {
const source = bootstrap(
EXTERNAL,
'const s=n.n(n(45840));new (s().BrowserWindow)({webPreferences:{contextIsolation:false}});s().ipcMain.handle("ready",()=>{});',
);
const electron = electronOf(source);
expect(electron.browser_windows).toHaveLength(1);
expect(electron.ipc).toMatchObject([{ channel: "ready", side: "main" }]);
});
it.each([
[
"a fake getter",
bootstrap(
EXTERNAL,
"const s=n.n=e=>()=>0;new (s(n(45840))().BrowserWindow)({});",
),
],
[
"a getter replaced while evaluating its argument",
bootstrap(
EXTERNAL,
"new ((n.n=e=>()=>0,n.n(n(45840)))().BrowserWindow)({});",
),
],
])("does not infer Electron through %s", (_name, source) => {
expect(electronOf(source).browser_windows).toEqual([]);
});
it("follows the loader passed to a module factory", () => {
const electron = electronOf(bootstrap(EXTERNAL, CONSUMER));
expect(electron.browser_windows).toMatchObject([

View File

@ -25,6 +25,36 @@ describe("Electron option evidence", () => {
});
});
it.each([
["for initializer", 'for (var electron = require("electron"); false;) {}'],
["do body", 'do { var electron = require("electron"); } while (false);'],
])(
"recovers Electron bindings initialized unconditionally in a %s",
(_name, declaration) => {
const analysis = analyzeJavaScriptStaticSource(
`${declaration} new electron.BrowserWindow({webPreferences:{preload:"./preload.js"}});`,
);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.electron.browser_windows).toMatchObject([
{ preload_path: "./preload.js" },
]);
},
);
it.each([
'for (; false;) { var electron = require("electron"); }',
'while (false) { var electron = require("electron"); }',
'if (flag) { for (var electron = require("electron"); false;) {} }',
])(
"keeps conditionally skipped loop declarations unresolved: %s",
(declaration) => {
const analysis = analyzeJavaScriptStaticSource(
`${declaration} new electron.BrowserWindow({});`,
);
expect(analysis.electron.browser_windows).toEqual([]);
},
);
it("does not read an audio or video preload attribute as a preload script", () => {
const analysis = analyzeJavaScriptStaticSource(`
jsx("audio", { controls: true, preload: "metadata", src });

View File

@ -180,7 +180,12 @@ const comparisonEvidence = (
executable_sha256: null,
},
coverage: complete
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
? {
status: "complete",
truncated: false,
omitted_count: 0,
limits: sourceCoverageLimits(input),
}
: {
status: "partial",
truncated:
@ -210,7 +215,7 @@ const changeGraphCoverage = (
status: "complete",
truncated: false,
omitted_count: 0,
limits: [],
limits: sourceCoverageLimits(input),
};
return {
status: "partial",

View File

@ -143,7 +143,7 @@ const nativeEvidence = (sha256: string | undefined, ordinal: number) =>
},
);
describe("application version comparison result", () => {
describe("application version comparison Evidence links", () => {
it("links every exact digest observation only to its paired or unmatched item", () => {
const left = buildGraph(LEFT_SHA, 1);
const right = buildGraph(RIGHT_SHA, 2);
@ -227,7 +227,9 @@ describe("application version comparison result", () => {
].sort(),
);
});
});
describe("application version comparison graph ownership", () => {
it("returns a sealed change graph that passes complete validation", () => {
const result = compare();
expect(
@ -243,6 +245,58 @@ describe("application version comparison result", () => {
);
});
it("retains deduplicated complete input limits on the change graph and inferred edge", () => {
const leftLimits = [
{ name: "modules", value: 100, unit: "items" as const },
];
const rightLimits = [
{ name: "source-bytes", value: 4096, unit: "bytes" as const },
{ name: "modules", value: 100, unit: "items" as const },
];
const withLimits = (
graph: JavaScriptApplicationGraph,
limits: JavaScriptApplicationGraph["coverage"]["limits"],
): JavaScriptApplicationGraph => {
const { graph_id: _graphId, ...input } = graph;
return createJavaScriptApplicationGraph({
...input,
coverage: { ...graph.coverage, limits },
});
};
const result = compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: LEFT_SHA,
graph: withLimits(buildGraph(LEFT_SHA, 1), leftLimits),
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: RIGHT_SHA,
graph: withLimits(buildGraph(RIGHT_SHA, 2), rightLimits),
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
const expectedLimits = [leftLimits[0]!, rightLimits[0]!];
const changed = result.graph.edges.find(
({ relation }) => relation === "changed_from",
);
expect(changed).toBeDefined();
expect(result.graph.coverage).toMatchObject({
status: "complete",
truncated: false,
omitted_count: 0,
limits: expectedLimits,
});
expect(changed?.evidence.coverage).toMatchObject({
status: "complete",
truncated: false,
omitted_count: 0,
limits: expectedLimits,
});
});
it("does not reuse a change graph without an owned proof", () => {
const result = compare();
const copy = structuredClone(result.graph);

View File

@ -18,11 +18,14 @@ import {
moduleFactory,
modulePropertyName,
} from "./javascriptStaticAnalysisHelpers.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
/** Loader bindings and literal externals proven for one standalone bootstrap. */
interface BootstrapExternals {
/** The runtime loader and every factory parameter it is passed as. */
readonly loaderBindingIds: ReadonlySet<string>;
/** Loader bindings with the canonical n.n getter installed before dispatch. */
readonly interopGetterBindingIds: ReadonlySet<string>;
/** Table keys whose factory only re-exports one unshadowed literal require; null when a key repeats. */
readonly externals: ReadonlyMap<string, string | null>;
}
@ -47,6 +50,50 @@ export const semanticBundledRequireOrigin = (
): JavaScriptRequireOrigin | undefined => {
const chain = semanticMemberChain(node);
const current = chain?.root;
if (
chain?.members.length === 0 &&
t.isCallExpression(current) &&
t.isMemberExpression(current.callee) &&
semanticStaticPropertyKey(
current.callee.property,
current.callee.computed,
) === "n" &&
t.isIdentifier(current.callee.object) &&
current.arguments.length === 1
) {
// Webpack's `n.n(module)` returns its default-export getter. For a
// CommonJS external, invoking that getter yields the exact exports object;
// bind its identity to the proven registry result while retaining the
// nested loader call as the source of provenance.
const nested = semanticBundledRequireOrigin(current.arguments[0], state);
const loaderBinding = resolveSemanticBindingState(
state,
current.callee.object,
current.callee.object.name,
);
const proof =
nested === undefined || loaderBinding === undefined
? undefined
: bootstrapProofForLoaderCall(
nested.call,
loaderBinding.bindingId,
state,
);
const nestedCallee = nested?.call.callee;
const loaderBindingId = loaderBinding?.bindingId;
if (
nested !== undefined &&
nested.origin.importedPath.length === 0 &&
t.isIdentifier(nestedCallee) &&
loaderBindingId !== undefined &&
loaderBindingId ===
resolveSemanticBindingState(state, nestedCallee, nestedCallee.name)
?.bindingId &&
proof?.loaderBindingIds.has(loaderBindingId) === true &&
proof.interopGetterBindingIds.has(loaderBindingId)
)
return nested;
}
if (
chain === undefined ||
!t.isCallExpression(current) ||
@ -89,6 +136,191 @@ export const semanticBundledRequireOrigin = (
return undefined;
};
/** Prove the source-local n.n getter and reject visible replacements. */
const canonicalWebpackInteropBindings = (
loaderBindingIds: ReadonlySet<string>,
bootstrapBody: t.BlockStatement,
state: JavaScriptSemanticAnalysisState,
): ReadonlySet<string> => {
const writes = new Map<string, t.AssignmentExpression[]>();
let reflectiveWrite = false;
traverseJavaScriptAst(state.program, {
enter: (node) => {
if (t.isAssignmentExpression(node) && t.isMemberExpression(node.left)) {
const target = node.left;
const bindingId = t.isIdentifier(target.object)
? resolveSemanticBindingState(
state,
target.object,
target.object.name,
)?.bindingId
: undefined;
if (
semanticStaticPropertyKey(target.property, target.computed) === "n" &&
bindingId !== undefined &&
loaderBindingIds.has(bindingId)
)
writes.set(bindingId, [...(writes.get(bindingId) ?? []), node]);
}
if (
!t.isCallExpression(node) ||
!t.isMemberExpression(node.callee) ||
!t.isIdentifier(node.callee.object, { name: "Object" }) ||
![
"defineProperty",
"defineProperties",
"assign",
"setPrototypeOf",
].includes(
semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
) ?? "",
) ||
!t.isIdentifier(node.arguments[0])
)
return;
reflectiveWrite ||= loaderBindingIds.has(
resolveSemanticBindingState(
state,
node.arguments[0],
node.arguments[0].name,
)?.bindingId ?? "",
);
},
});
const proven = new Set<string>();
if (reflectiveWrite) return proven;
const allWrites = [...writes.values()].flat();
const [write] = allWrites;
if (
allWrites.length === 1 &&
write !== undefined &&
isInstalledBeforeEntryDispatch(
write,
bootstrapBody,
loaderBindingIds,
state,
) &&
isCanonicalGetter(write, state)
)
for (const bindingId of loaderBindingIds) proven.add(bindingId);
return proven;
};
const isCanonicalGetter = (
write: t.AssignmentExpression,
state: JavaScriptSemanticAnalysisState,
): boolean => {
if (
!t.isArrowFunctionExpression(write.right) ||
write.right.params.length !== 1 ||
!t.isIdentifier(write.right.params[0])
)
return false;
const parameter = write.right.params[0];
const parameterBinding = resolveSemanticBindingState(
state,
parameter,
parameter.name,
);
const body = write.right.body;
return (
parameterBinding !== undefined &&
t.isConditionalExpression(body) &&
t.isLogicalExpression(body.test, { operator: "&&" }) &&
sameBinding(body.test.left, parameterBinding.bindingId, state) &&
t.isMemberExpression(body.test.right) &&
semanticStaticPropertyKey(
body.test.right.property,
body.test.right.computed,
) === "__esModule" &&
sameBinding(body.test.right.object, parameterBinding.bindingId, state) &&
getterReturnsDefault(body.consequent, parameterBinding.bindingId, state) &&
getterReturnsModule(body.alternate, parameterBinding.bindingId, state)
);
};
const isInstalledBeforeEntryDispatch = (
write: t.AssignmentExpression,
body: t.BlockStatement,
loaderBindingIds: ReadonlySet<string>,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const statement = state.parentsByNode.get(write);
if (!t.isExpressionStatement(statement)) return false;
const writeIndex = body.body.indexOf(statement);
if (writeIndex < 0) return false;
let entryIndex = -1;
for (let index = 0; index < body.body.length; index += 1) {
const topLevel = body.body[index];
if (topLevel === undefined) continue;
traverseJavaScriptAst(topLevel, {
enter: (node, _parent, ancestors) => {
if (
entryIndex < 0 &&
t.isCallExpression(node) &&
t.isIdentifier(node.callee) &&
ancestors().every((ancestor) => !t.isFunction(ancestor)) &&
loaderBindingIds.has(
resolveSemanticBindingState(state, node.callee, node.callee.name)
?.bindingId ?? "",
)
)
entryIndex = index;
},
});
if (entryIndex >= 0) break;
}
return entryIndex > writeIndex;
};
const bootstrapProofForLoaderCall = (
call: t.CallExpression,
loaderBindingId: string,
state: JavaScriptSemanticAnalysisState,
): BootstrapExternals | undefined => {
for (
let ancestor = state.parentsByNode.get(call);
ancestor !== undefined;
ancestor = state.parentsByNode.get(ancestor)
) {
if (!t.isCallExpression(ancestor)) continue;
const proof = bootstrapExternals(ancestor, state);
if (proof?.loaderBindingIds.has(loaderBindingId)) return proof;
}
return undefined;
};
const sameBinding = (
node: t.Node,
bindingId: string,
state: JavaScriptSemanticAnalysisState,
): boolean =>
t.isIdentifier(node) &&
resolveSemanticBindingState(state, node, node.name)?.bindingId === bindingId;
const getterReturnsDefault = (
node: t.Node,
bindingId: string,
state: JavaScriptSemanticAnalysisState,
): boolean =>
t.isArrowFunctionExpression(node) &&
node.params.length === 0 &&
t.isMemberExpression(node.body) &&
semanticStaticPropertyKey(node.body.property, node.body.computed) ===
"default" &&
sameBinding(node.body.object, bindingId, state);
const getterReturnsModule = (
node: t.Node,
bindingId: string,
state: JavaScriptSemanticAnalysisState,
): boolean =>
t.isArrowFunctionExpression(node) &&
node.params.length === 0 &&
sameBinding(node.body, bindingId, state);
const bootstrapExternals = (
call: t.CallExpression,
state: JavaScriptSemanticAnalysisState,
@ -153,7 +385,15 @@ const proveBootstrapExternals = (
if (externals.has(key)) externals.set(key, null);
else externals.set(key, specifier ?? null);
}
return { loaderBindingIds, externals };
return {
loaderBindingIds,
interopGetterBindingIds: canonicalWebpackInteropBindings(
loaderBindingIds,
bootstrap.body,
state,
),
externals,
};
};
/**

View File

@ -8,6 +8,28 @@ import {
} from "./javascriptSemanticAnalysis.fixture.js";
describe("JavaScript semantic analysis: calls 1", () => {
it("does not assign computed method-name reads to method closures", () => {
const ir = analyzeJavaScriptSemantics(`
function outer() {
const name = "m";
const object = { [name]() { return 1; }, ["literal"]() { return 2; } };
class Example {
[name]() { return 3; }
["literal"]() { return 4; }
}
}
`);
expect(ir.closureCaptures).toEqual([]);
const bodyRead = analyzeJavaScriptSemantics(`
function outer() {
const value = 1;
const object = { method() { return value; } };
}
`);
expect(bodyRead.closureCaptures).toHaveLength(1);
});
it("links exact local calls to parameters, returns, and closure captures", () => {
const ir = analyzeJavaScriptSemantics(`
const prefix = "rea";

View File

@ -164,6 +164,32 @@ describe("JavaScript semantic values after explicit property mutations", () => {
});
});
it("invalidates a shared value reached through an object-literal prototype getter", () => {
const value = resultValue(`
const shared = { x: 1 };
const proto = { get v() { return shared; } };
const box = { __proto__: proto };
box.v.x = 2;
return shared.x;
`);
expect(
value?.status === "unknown" ||
(value?.status === "literal" && value.value === 2),
).toBe(true);
});
it("does not follow an object-literal prototype getter past an own property", () => {
expect(
resultValue(`
const shared = { x: 1 };
const proto = { get v() { return shared; } };
const box = { __proto__: proto, v: { x: 3 } };
box.v.x = 2;
return shared.x;
`),
).toEqual({ status: "literal", value: 1 });
});
describe("JavaScript semantic mutations through getter results (#1494)", () => {
it.each([
[

View File

@ -860,13 +860,18 @@ const hasConditionalInitializer = (ancestors: readonly t.Node[]): boolean => {
const node = ancestors[index];
if (t.isFunction(node) || t.isProgram(node) || t.isStaticBlock(node))
return false;
if (
t.isIfStatement(node) ||
t.isLoop(node) ||
t.isSwitchCase(node) ||
t.isTryStatement(node)
)
if (t.isIfStatement(node) || t.isSwitchCase(node) || t.isTryStatement(node))
return true;
if (t.isLoop(node)) {
const bodyOrTest = ancestors[index + 1];
if (
(t.isForStatement(node) && bodyOrTest === node.init) ||
(t.isDoWhileStatement(node) &&
(bodyOrTest === node.body || bodyOrTest === node.test))
)
continue;
return true;
}
}
return false;
};

View File

@ -58,6 +58,7 @@ interface CallCollectionContext {
readonly JavaScriptSemanticBindingState[]
>;
readonly ancestors: t.Node[];
readonly callableStack: JavaScriptSemanticCallable[];
}
/** Recover direct-call and lexical-capture candidates from inert syntax. */
@ -77,6 +78,7 @@ export const collectJavaScriptSemanticCalls = (
const callableById = new Map(
callables.map((callable) => [callable.callableId, callable]),
);
const callableStack: JavaScriptSemanticCallable[] = [];
const context: CallCollectionContext = {
state,
callableById,
@ -84,8 +86,8 @@ export const collectJavaScriptSemanticCalls = (
bindingResolutionCache: new Map(),
parameterBindingCache: new Map(),
ancestors: [],
callableStack,
};
const callableStack: JavaScriptSemanticCallable[] = [];
traverseJavaScriptAst(program, {
enter: (node, parent) => {
const callableId = semanticCallableIdForNode(node);
@ -316,19 +318,49 @@ const collectCapture = (
)
return;
if (semanticReferenceRole(node, parent) === null) return;
const captureOwner = computedMethodKeyOwner(node, owner, context);
if (captureOwner === undefined || captureOwner.bodyScopeId === null) return;
const binding = resolveSemanticBindingState(state, node, node.name);
if (
binding === undefined ||
bindingIsWithinCallable(binding, owner.bodyScopeId, state)
bindingIsWithinCallable(binding, captureOwner.bodyScopeId, state)
)
return;
output.closureCaptures.push({
callableId: owner.callableId,
callableId: captureOwner.callableId,
bindingId: binding.bindingId,
referenceLocation: range(node),
});
};
const computedMethodKeyOwner = (
node: t.Node,
owner: JavaScriptSemanticCallable,
context: CallCollectionContext,
): JavaScriptSemanticCallable | undefined => {
const ownerNode = context.ancestors.findLast(
(candidate) => semanticCallableIdForNode(candidate) === owner.callableId,
);
if (
ownerNode === undefined ||
(!t.isObjectMethod(ownerNode) && !t.isClassMethod(ownerNode)) ||
!ownerNode.computed
)
return owner;
let child = node;
let parent = context.state.parentsByNode.get(child);
while (parent !== undefined && parent !== ownerNode) {
child = parent;
parent = context.state.parentsByNode.get(parent);
}
if (parent !== ownerNode || ownerNode.key !== child) return owner;
return enclosingFunction(
context.callableStack.filter(
({ callableId }) => callableId !== owner.callableId,
),
);
};
const collectDynamicProperty = (
node: t.Node,
owner: JavaScriptSemanticCallable | undefined,

View File

@ -0,0 +1,31 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
const normalizedSyntaxFingerprint = (source: string): string => {
const [fingerprint] = analyzeJavaScriptSemantics(source).functionFingerprints;
expect(fingerprint?.status).toBe("complete");
return fingerprint?.components.normalizedAstSha256 ?? "";
};
it("distinguishes yielding an iterator result from delegating to the iterator", () => {
expect(
normalizedSyntaxFingerprint("function* sample(gen) { yield* gen(); }"),
).not.toBe(
normalizedSyntaxFingerprint("function* sample(gen) { yield gen(); }"),
);
});
it("distinguishes returned prefix and postfix updates", () => {
expect(
normalizedSyntaxFingerprint("function sample(i) { return ++i; }"),
).not.toBe(normalizedSyntaxFingerprint("function sample(i) { return i++; }"));
});
it("keeps formatting and local renames normalized", () => {
expect(
normalizedSyntaxFingerprint(
"function renamed(value) {\n return value++ ;\n }",
),
).toBe(normalizedSyntaxFingerprint("function sample(i) { return i++; }"));
});

View File

@ -198,6 +198,8 @@ const normalizedNodeToken = (node: t.Node, parent: t.Node | null): string => {
primitiveAttribute(node, "optional"),
primitiveAttribute(node, "async"),
primitiveAttribute(node, "generator"),
primitiveAttribute(node, "delegate"),
primitiveAttribute(node, "prefix"),
].filter((value): value is string => value !== null);
return [node.type, ...attributes].join(":");
};

View File

@ -306,9 +306,11 @@ function* checkCoverageSteps(
relationsByFamily.set(family, (relationsByFamily.get(family) ?? 0) + 1);
}
const unknownsById = new Map<string, GraphRecord["unknowns"][number]>();
const unknownFamilies = new Set<string>();
for (const [index, unknown] of graph.unknowns.entries()) {
if (index % 256 === 0) yield;
unknownsById.set(unknown.unknown_id, unknown);
unknownFamilies.add(unknown.family);
}
for (const [index, family] of graph.coverage.families.entries()) {
if (family.retained_relations !== relationsByFamily.get(family.family))
@ -329,7 +331,9 @@ function* checkCoverageSteps(
});
if (
family.status === "complete" &&
(family.omitted_relations !== 0 || family.unknown_ids.length > 0)
(family.omitted_relations !== 0 ||
family.unknown_ids.length > 0 ||
unknownFamilies.has(family.family))
)
context.addIssue({
code: "custom",

View File

@ -2,6 +2,7 @@ import {
semanticFixtureGraph,
semanticFixtureInput,
semanticGraphTestEvidence,
unknownSemanticGraphTestEvidence,
} from "../../../tests/support/javascriptSemanticGraphFixture.js";
import { expect, it } from "vitest";
import { z } from "zod";
@ -10,6 +11,7 @@ import { digestCanonicalValue } from "../canonicalDigest.js";
import type { JsonValue } from "../jsonValue.js";
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
JAVASCRIPT_SEMANTIC_NODE_KINDS,
JAVASCRIPT_SEMANTIC_RELATIONS,
@ -244,6 +246,86 @@ it("matches scalar literal seeds and skips non-scalar literal metadata", () => {
expect(query("unmatched")).toEqual([]);
});
it("scopes callers unknowns to call relations while preserving graph uncertainty", () => {
const contexts = new JavaScriptSemanticEvidenceContextRegistry();
const caller = node("function", "caller", {}, contexts);
const seed = node("function", "seed", {}, contexts);
const call = createJavaScriptSemanticGraphRelation(
{
source_node_id: caller.node_id,
target_node_id: seed.node_id,
relation: "calls",
resolution: "resolved",
properties: {},
evidence: semanticGraphTestEvidence("inferred"),
},
contexts,
);
const readUnknown = createJavaScriptSemanticGraphUnknown(
{
node_id: caller.node_id,
family: "data-flow",
relation_kinds: ["reads"],
reason: "dynamic-property",
detail: "Unresolved property read at the caller.",
candidate_node_ids: [],
evidence: unknownSemanticGraphTestEvidence(),
},
contexts,
);
const graph = createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: SHA,
application_graph_id: `jag_${"b".repeat(64)}`,
root_node_ids: [seed.node_id],
evidence_contexts: contexts.contexts,
nodes: [caller, seed],
relations: [call],
fingerprints: [],
unknowns: [readUnknown],
coverage: {
status: "partial",
truncated: false,
omitted_nodes: null,
omitted_relations: null,
limits: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status:
family === "data-flow" ? ("unknown" as const) : ("complete" as const),
retained_relations:
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[call.relation] === family ? 1 : 0,
omitted_relations: family === "data-flow" ? null : 0,
unknown_ids: family === "data-flow" ? [readUnknown.unknown_id] : [],
})),
},
limitations: ["One property read remains unresolved."],
});
const query = (
direction: "callers" | "backward-provenance",
allowed_relations?: ["calls"],
) =>
queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: seed.node_id },
direction,
...(allowed_relations === undefined ? {} : { allowed_relations }),
});
const callers = query("callers");
const filtered = query("callers", ["calls"]);
const backward = query("backward-provenance");
expect(callers.summary.traversed_nodes).toBe(2);
expect(callers.unknowns).toEqual([]);
expect(callers.summary.relevant_unknowns).toBe(0);
expect(callers.limitations).not.toContain(
"Relevant dynamic, unsupported, incomplete, or ambiguous semantics remain unknown.",
);
expect(filtered.unknowns).toEqual(callers.unknowns);
expect(backward.unknowns.map(({ unknown_id }) => unknown_id)).toEqual([
readUnknown.unknown_id,
]);
expect(callers.coverage.status).toBe("partial");
});
it("counts candidate ownership cycles once and traverses them finitely", () => {
const graph = graphWithCandidateRelations();
const binding = graph.nodes.find(({ label }) => label === "binding");
@ -336,6 +418,69 @@ it("rejects stale identities, dangling endpoints, and incomplete coverage claims
);
});
it("rejects complete family coverage with an unlisted retained frontier", () => {
const input = semanticFixtureInput(true);
input.coverage = {
...input.coverage,
status: "complete",
truncated: false,
omitted_nodes: 0,
omitted_relations: 0,
families: input.coverage.families.map((family) =>
family.family === "call-flow"
? {
...family,
status: "complete",
omitted_relations: 0,
unknown_ids: [],
}
: family,
),
};
input.limitations = [];
expect(() => createJavaScriptSemanticGraph(input)).toThrow(
/Complete family coverage cannot omit or retain unknown facts/u,
);
const valid = createJavaScriptSemanticGraph(semanticFixtureInput(true));
const coverage = {
...valid.coverage,
status: "complete" as const,
truncated: false,
omitted_nodes: 0,
omitted_relations: 0,
families: valid.coverage.families.map((family) =>
family.family === "call-flow"
? {
...family,
status: "complete" as const,
omitted_relations: 0,
unknown_ids: [],
}
: family,
),
};
const { graph_id: _previousId, ...semantic } = valid;
const changed = { ...semantic, coverage, limitations: [] };
const invalid = {
...changed,
graph_id: `jsrg_${digestCanonicalValue(
changed,
"JavaScript semantic graph",
)}`,
};
const parsed = javaScriptSemanticGraphSchema.safeParse(invalid);
expect(parsed.success).toBe(false);
if (parsed.success) throw new Error("Expected inconsistent graph rejection");
expect(parsed.error.issues.map(({ message }) => message)).toContain(
"Complete family coverage cannot omit or retain unknown facts",
);
expect(
javaScriptSemanticGraphSchema.safeParse(JSON.parse(JSON.stringify(invalid)))
.success,
).toBe(false);
});
it("validates evidence context ownership and references", () => {
const graph = semanticFixtureGraph();
const alteredContext = {

View File

@ -785,6 +785,33 @@ export const semanticObjectPropertyReferences = (
continue;
}
const name = semanticStaticPropertyKey(property.key, property.computed);
if (
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__"
) {
const selected = key ?? null;
if (
(selected === null || typeof selected === "object") &&
t.isExpression(property.value)
) {
references.push({
node: property.value,
path: [selected, ...remaining],
});
} else if (
typeof selected === "string" &&
!objectExpressionDefinesKey(node, selected) &&
t.isExpression(property.value)
) {
references.push({
node: property.value,
path: [selected, ...remaining],
});
}
continue;
}
const isGetter = t.isObjectMethod(property) && property.kind === "get";
const isSetter = t.isObjectMethod(property) && property.kind === "set";
if (

View File

@ -250,11 +250,24 @@ const relevantUnknownFrontiers = (
return graph.unknowns.filter(
(unknown) =>
(unknown.node_id === null || nodeIds.has(unknown.node_id)) &&
(allowed === null ||
unknown.relation_kinds.some((relation) => allowed.has(relation))),
unknown.relation_kinds.some(
(relation) =>
directionAllowsRelation(input.direction, relation) &&
(allowed === null || allowed.has(relation)),
),
);
};
const directionAllowsRelation = (
direction: JavaScriptSemanticQueryInput["direction"],
relation: JavaScriptSemanticGraphRelation["relation"],
): boolean =>
direction === "callers"
? relation === "calls"
: direction === "ownership"
? ownershipRelation(relation)
: true;
const relevantCandidateRelationCount = (
graph: JavaScriptSemanticGraph,
nodeIds: ReadonlySet<string>,

View File

@ -0,0 +1,32 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
it("reads exact route-property templates and leaves interpolated values unknown", () => {
const analysis = analyzeJavaScriptStaticSource(`
const routes = [
{ path: \`settings\` },
{ route: \`users/\${userId}\` },
{ path: "/login" },
];
`);
expect(
analysis.endpoints.map(({ value, mechanism, interpretation }) => ({
value,
mechanism,
interpretation,
})),
).toEqual([
{
value: "settings",
mechanism: "property:path",
interpretation: undefined,
},
{
value: "/login",
mechanism: "property:path",
interpretation: undefined,
},
]);
});

View File

@ -420,11 +420,12 @@ export const inspectRouteProperty = (
context: FindingContext,
): void => {
const key = semanticStaticPropertyName(node.key, node.computed);
if ((key === "path" || key === "route") && t.isStringLiteral(node.value))
const value = stringValue(node.value);
if ((key === "path" || key === "route") && value !== undefined)
addEndpoint(context, {
node,
kind: "route",
value: node.value.value,
value,
mechanism: `property:${key}`,
});
};

View File

@ -389,6 +389,62 @@ it("composes fallthrough guards across successive conditional returns", () => {
]);
});
it.each([
["while", 'inspect((() => { while (flag) { return "loop"; } })());'],
["for", 'inspect((() => { for (;;) { return "forever"; } })());'],
["try", 'inspect((() => { try { return "try"; } finally {} })());'],
[
"switch",
'inspect((() => { switch (x) { default: return "switch"; } })());',
],
])(
"keeps unsupported %s return control flow explicitly unknown",
(_name, source) => {
const value = inspectOne(source);
expect(
symbolicVariants(value).some((variant) => variant.kind === "unknown"),
).toBe(true);
expect(value).not.toEqual({ kind: "literal", value: undefined });
},
);
it("preserves later returns with unresolved conditions after unsupported return flow", () => {
const variants = symbolicVariants(
inspectOne(`inspect((() => {
while (flag) { return "inside"; }
return "after";
})());`),
);
expect(variants.some((variant) => variant.kind === "unknown")).toBe(true);
expect(variants).toContainEqual(
expect.objectContaining({
kind: "literal",
value: "after",
conditionUnresolved: true,
}),
);
});
it("does not treat loops without owned returns as unknown return flow", () => {
expect(
inspectOne(`inspect((() => {
while (flag) { work(); }
return "after";
})());`),
).toEqual({ kind: "literal", value: "after" });
});
it("does not treat nested-function returns as wrapper return flow", () => {
expect(
inspectOne(`inspect((() => {
try { function nested() { return "inner"; } }
catch {}
return "outer";
})());`),
).toEqual({ kind: "literal", value: "outer" });
});
it("marks return guards unresolved when a nested join cannot be represented", () => {
const value = inspectOne(`
function choose() {

View File

@ -58,6 +58,17 @@ const isObjectPrototypeSetter = (
!property.shorthand &&
name === "__proto__";
const containsOwnedReturn = (root: t.Node): boolean => {
let found = false;
traverseJavaScriptAst(root, {
skip: (node) => found || t.isFunction(node) || t.isClass(node),
enter: (node) => {
if (t.isReturnStatement(node)) found = true;
},
});
return found;
};
/** Demand-driven substitutions for local bindings, closures and literal module tables. */
export class JavaScriptSymbolicBindings {
readonly #writes = new Map<string, PropertyWrite[]>();
@ -832,6 +843,24 @@ export class JavaScriptSymbolicBindings {
unresolvedFallthrough = true;
}
}
if (
t.isWhileStatement(statement) ||
t.isDoWhileStatement(statement) ||
t.isForStatement(statement) ||
t.isForInStatement(statement) ||
t.isForOfStatement(statement) ||
t.isSwitchStatement(statement) ||
t.isTryStatement(statement)
) {
if (containsOwnedReturn(statement)) {
possible.push(
symbolicUnknown(
`unsupported return control flow: ${statement.type}`,
),
);
unresolvedFallthrough = true;
}
}
}
return { values: possible, fallsThrough: true };
}

View File

@ -902,6 +902,38 @@ const inspection = (source: string) =>
limitations: [],
});
it("recognizes expression-free template literals as route properties", () => {
const result = analyzeCapturedWebBundle(
inspection(`
const routes = [
{ path: \`score-board\`, component: ScoreBoard },
{ route: \`administration\`, component: Admin },
{ path: \`/user/\${name}\`, component: Dynamic },
];
`),
);
expect(result.observations.routes.map(({ value }) => value)).toEqual([
"score-board",
"administration",
]);
});
it("uses the shared parser for decorated captured scripts", () => {
const result = analyzeCapturedWebBundle(
inspection(`@sealed class Widget {} fetch('/decorated');`),
);
expect(result.completeness).toMatchObject({
status: "complete",
parsed_scripts: 1,
parse_failures: 0,
});
expect(result.observations.endpoints).toEqual([
expect.objectContaining({ value: "/decorated" }),
]);
});
it("keeps bare module locations unresolved without inventing URL-relative paths", () => {
const result = analyzeCapturedWebBundle(
inspection(`

Some files were not shown because too many files have changed in this diff Show More