Merge remote-tracking branch 'origin/main' into fix/main-red-ida-javascript-tests
This commit is contained in:
commit
c023db356f
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
export interface BrowserVerifierSite {
|
||||
readonly origin: string;
|
||||
triggerSessionNavigation(): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export function startBrowserVerifierSite(): Promise<BrowserVerifierSite>;
|
||||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export declare const assertOrdinaryWindowsVerifierCaller: (
|
||||
token: unknown,
|
||||
) => void;
|
||||
export declare const requireOrdinaryWindowsVerifierCaller: () => Promise<void>;
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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" }),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>�</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 & 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>),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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++; }"));
|
||||
});
|
||||
|
|
@ -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(":");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -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}`,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue